From bbbb1f2eb03e1fd576bb1b027638854e500d6c9d Mon Sep 17 00:00:00 2001 From: fjc40 Date: Tue, 4 Jul 2017 15:53:17 +1200 Subject: [PATCH 01/25] Renamed sharedModel module to racevisionGame - this is intended to be our single module program. Moved Boat/MockBoat/VisualiserBoat into it. Moved Polars, polarParser, VMG, and polar exception. Moved the networking stuff into it. Moved angle, azimuth, bearing into it. Moved gpscoordinate into it. Moved mark/compoundMark into it. Moved leg into it. Moved trackpoint into it. --- {sharedModel => racevisionGame}/pom.xml | 4 +- .../main/java/mock/dataInput/PolarParser.java | 106 ++++ .../exceptions/InvalidPolarFileException.java | 24 + .../src/main/java/mock/model/MockBoat.java | 194 ++++++ .../src/main/java/mock/model/Polars.java | 431 ++++++++++++++ .../src/main/java/mock/model/VMG.java | 48 ++ .../java/network/BinaryMessageDecoder.java | 274 +++++++++ .../java/network/BinaryMessageEncoder.java | 103 ++++ .../Exceptions/InvalidMessageException.java | 25 + .../MessageDecoders/AverageWindDecoder.java | 55 ++ .../MessageDecoders/BoatLocationDecoder.java | 139 +++++ .../MessageDecoders/CourseWindDecoder.java | 61 ++ .../MessageDecoders/MarkRoundingDecoder.java | 51 ++ .../RaceStartStatusDecoder.java | 65 ++ .../MessageDecoders/RaceStatusDecoder.java | 117 ++++ .../MessageDecoders/XMLMessageDecoder.java | 81 +++ .../RaceVisionByteEncoder.java | 296 ++++++++++ .../MessageEncoders/XMLMessageEncoder.java | 58 ++ .../main/java/network/Messages/AC35Data.java | 31 + .../java/network/Messages/AverageWind.java | 35 ++ .../java/network/Messages/BoatLocation.java | 557 ++++++++++++++++++ .../java/network/Messages/BoatStatus.java | 68 +++ .../java/network/Messages/CourseWind.java | 57 ++ .../java/network/Messages/CourseWinds.java | 23 + .../Messages/Enums/BoatStatusEnum.java | 75 +++ .../network/Messages/Enums/MessageType.java | 79 +++ .../Messages/Enums/RaceStatusEnum.java | 108 ++++ .../network/Messages/Enums/RaceTypeEnum.java | 87 +++ .../main/java/network/Messages/Heartbeat.java | 29 + .../java/network/Messages/MarkRounding.java | 69 +++ .../java/network/Messages/RaceMessage.java | 26 + .../network/Messages/RaceStartStatus.java | 25 + .../java/network/Messages/RaceStatus.java | 129 ++++ .../java/network/Messages/XMLMessage.java | 57 ++ .../network/PacketDump/AC35DumpReader.java | 71 +++ .../java/network/PacketDump/AC35Packet.java | 17 + .../java/network/Utils/AC35UnitConverter.java | 43 ++ .../java/network/Utils/ByteConverter.java | 266 +++++++++ .../src/main/java/shared/model/Angle.java | 130 ++++ .../src/main/java/shared/model/Azimuth.java | 68 +++ .../src/main/java/shared/model/Bearing.java | 66 +++ .../src/main/java/shared/model/Boat.java | 296 ++++++++++ .../main/java/shared/model/CompoundMark.java | 110 ++++ .../src/main/java/shared/model/Constants.java | 49 ++ .../main/java/shared/model/GPSCoordinate.java | 516 ++++++++++++++++ .../src/main/java/shared/model/Leg.java | 128 ++++ .../src/main/java/shared/model/Mark.java | 64 ++ .../java/visualiser/model/TrackPoint.java | 61 ++ .../java/visualiser/model/VisualiserBoat.java | 124 ++++ sharedModel/src/main/java/seng302/App.java | 13 - 50 files changed, 5594 insertions(+), 15 deletions(-) rename {sharedModel => racevisionGame}/pom.xml (98%) create mode 100644 racevisionGame/src/main/java/mock/dataInput/PolarParser.java create mode 100644 racevisionGame/src/main/java/mock/exceptions/InvalidPolarFileException.java create mode 100644 racevisionGame/src/main/java/mock/model/MockBoat.java create mode 100644 racevisionGame/src/main/java/mock/model/Polars.java create mode 100644 racevisionGame/src/main/java/mock/model/VMG.java create mode 100644 racevisionGame/src/main/java/network/BinaryMessageDecoder.java create mode 100644 racevisionGame/src/main/java/network/BinaryMessageEncoder.java create mode 100644 racevisionGame/src/main/java/network/Exceptions/InvalidMessageException.java create mode 100644 racevisionGame/src/main/java/network/MessageDecoders/AverageWindDecoder.java create mode 100644 racevisionGame/src/main/java/network/MessageDecoders/BoatLocationDecoder.java create mode 100644 racevisionGame/src/main/java/network/MessageDecoders/CourseWindDecoder.java create mode 100644 racevisionGame/src/main/java/network/MessageDecoders/MarkRoundingDecoder.java create mode 100644 racevisionGame/src/main/java/network/MessageDecoders/RaceStartStatusDecoder.java create mode 100644 racevisionGame/src/main/java/network/MessageDecoders/RaceStatusDecoder.java create mode 100644 racevisionGame/src/main/java/network/MessageDecoders/XMLMessageDecoder.java create mode 100644 racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java create mode 100644 racevisionGame/src/main/java/network/MessageEncoders/XMLMessageEncoder.java create mode 100644 racevisionGame/src/main/java/network/Messages/AC35Data.java create mode 100644 racevisionGame/src/main/java/network/Messages/AverageWind.java create mode 100644 racevisionGame/src/main/java/network/Messages/BoatLocation.java create mode 100644 racevisionGame/src/main/java/network/Messages/BoatStatus.java create mode 100644 racevisionGame/src/main/java/network/Messages/CourseWind.java create mode 100644 racevisionGame/src/main/java/network/Messages/CourseWinds.java create mode 100644 racevisionGame/src/main/java/network/Messages/Enums/BoatStatusEnum.java create mode 100644 racevisionGame/src/main/java/network/Messages/Enums/MessageType.java create mode 100644 racevisionGame/src/main/java/network/Messages/Enums/RaceStatusEnum.java create mode 100644 racevisionGame/src/main/java/network/Messages/Enums/RaceTypeEnum.java create mode 100644 racevisionGame/src/main/java/network/Messages/Heartbeat.java create mode 100644 racevisionGame/src/main/java/network/Messages/MarkRounding.java create mode 100644 racevisionGame/src/main/java/network/Messages/RaceMessage.java create mode 100644 racevisionGame/src/main/java/network/Messages/RaceStartStatus.java create mode 100644 racevisionGame/src/main/java/network/Messages/RaceStatus.java create mode 100644 racevisionGame/src/main/java/network/Messages/XMLMessage.java create mode 100644 racevisionGame/src/main/java/network/PacketDump/AC35DumpReader.java create mode 100644 racevisionGame/src/main/java/network/PacketDump/AC35Packet.java create mode 100644 racevisionGame/src/main/java/network/Utils/AC35UnitConverter.java create mode 100644 racevisionGame/src/main/java/network/Utils/ByteConverter.java create mode 100644 racevisionGame/src/main/java/shared/model/Angle.java create mode 100644 racevisionGame/src/main/java/shared/model/Azimuth.java create mode 100644 racevisionGame/src/main/java/shared/model/Bearing.java create mode 100644 racevisionGame/src/main/java/shared/model/Boat.java create mode 100644 racevisionGame/src/main/java/shared/model/CompoundMark.java create mode 100644 racevisionGame/src/main/java/shared/model/Constants.java create mode 100644 racevisionGame/src/main/java/shared/model/GPSCoordinate.java create mode 100644 racevisionGame/src/main/java/shared/model/Leg.java create mode 100644 racevisionGame/src/main/java/shared/model/Mark.java create mode 100644 racevisionGame/src/main/java/visualiser/model/TrackPoint.java create mode 100644 racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java delete mode 100644 sharedModel/src/main/java/seng302/App.java diff --git a/sharedModel/pom.xml b/racevisionGame/pom.xml similarity index 98% rename from sharedModel/pom.xml rename to racevisionGame/pom.xml index 4b814efa..f64fb5a6 100644 --- a/sharedModel/pom.xml +++ b/racevisionGame/pom.xml @@ -8,8 +8,8 @@ jar - sharedModel - sharedModel + racevisionGame + racevisionGame 1.0-SNAPSHOT diff --git a/racevisionGame/src/main/java/mock/dataInput/PolarParser.java b/racevisionGame/src/main/java/mock/dataInput/PolarParser.java new file mode 100644 index 00000000..a45d77ed --- /dev/null +++ b/racevisionGame/src/main/java/mock/dataInput/PolarParser.java @@ -0,0 +1,106 @@ +package mock.dataInput; + + + +import mock.exceptions.InvalidPolarFileException; +import mock.model.Polars; + +import java.io.*; +import java.util.ArrayList; + + +/** + * Responsible for parsing a polar data file, and creating a Polar data object. + */ +public class PolarParser { + + + /** + * Given a filename, this function parses it and generates a Polar object, which can be queried for polar information. + * @param filename The filename to load and read data from (loaded as a resource). + * @return A Polar table containing data from the given file. + */ + public static Polars parse(String filename) throws InvalidPolarFileException { + //Temporary table to return later. + Polars polarTable = new Polars(); + + + //Open the file for reading. + InputStream fileStream = PolarParser.class.getClassLoader().getResourceAsStream(filename); + if (fileStream == null) { + throw new InvalidPolarFileException("Could not open polar data file: " + filename); + } + //Wrap it with buffered input stream to set encoding and buffer. + InputStreamReader in = null; + try { + in = new InputStreamReader(fileStream, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new InvalidPolarFileException("Unsupported encoding: UTF-8", e); + } + BufferedReader inputStream = new BufferedReader(in); + + + //We expect the polar data file to have the column headings: + // Tws, Twa0, Bsp0, Twa1, Bsp1, UpTwa, UpBsp, Twa2, Bsp2, Twa3, Bsp3, Twa4, Bsp4, Twa5, Bsp5, Twa6, Bsp6, DnTwa, DnBsp, Twa7, Bsp7 + //and to have 7 rows of data. + //Angles are expected to be in degrees, and velocities in knots. + + + //We read data rows, and split them into arrays of elements. + ArrayList dataRows = new ArrayList<>(7); + try { + //Heading row. + //We skip the heading row by reading it. + String headingRow = inputStream.readLine(); + + //Data rows. + while (inputStream.ready()) { + //Read line. + String dataRow = inputStream.readLine(); + + //Split line. + String[] dataElements = dataRow.split(","); + + //Add to collection. + dataRows.add(dataElements); + + } + + } catch (IOException e) { + throw new InvalidPolarFileException("Could not read from polar data file: " + filename, e); + } + + //Finished reading in data, now we need to construct polar rows and table from it. + //For each row... + int rowNumber = 0; + for (String[] row : dataRows) { + + //For each pair of columns (the pair is angle, speed). + //We start at column 1 since column 0 is the wind speed column. + for (int i = 1; i < row.length; i += 2) { + + //Add angle+speed=velocity estimate to polar table. + try { + + //Add the polar value to the polar table + double windSpeedKnots = Double.parseDouble(row[0]); + double angleDegrees = Double.parseDouble(row[i]); + Bearing angle = Bearing.fromDegrees(angleDegrees); + double boatSpeedKnots = Double.parseDouble(row[i + 1]); + polarTable.addEstimate(windSpeedKnots, angle, boatSpeedKnots); + + } catch (NumberFormatException e) { + throw new InvalidPolarFileException("Could not convert (Row,Col): (" + rowNumber + "," + i +") = " + row[i] + " to a double.", e); + + } + } + + //Increment row number. + rowNumber++; + + } + + return polarTable; + } + +} diff --git a/racevisionGame/src/main/java/mock/exceptions/InvalidPolarFileException.java b/racevisionGame/src/main/java/mock/exceptions/InvalidPolarFileException.java new file mode 100644 index 00000000..58c65656 --- /dev/null +++ b/racevisionGame/src/main/java/mock/exceptions/InvalidPolarFileException.java @@ -0,0 +1,24 @@ +package mock.exceptions; + +/** + * An exception thrown when we cannot parse a polar data file. + */ +public class InvalidPolarFileException extends RuntimeException { + + /** + * Constructs the exception with a given message. + * @param message Message to store. + */ + public InvalidPolarFileException(String message) { + super(message); + } + + /** + * Constructs the exception with a given message and cause. + * @param message Message to store. + * @param cause Cause to store. + */ + public InvalidPolarFileException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/racevisionGame/src/main/java/mock/model/MockBoat.java b/racevisionGame/src/main/java/mock/model/MockBoat.java new file mode 100644 index 00000000..6afdae35 --- /dev/null +++ b/racevisionGame/src/main/java/mock/model/MockBoat.java @@ -0,0 +1,194 @@ +package mock.model; + + +import shared.model.*; + + +/** + * Represents a Boat on the mock side of a race. + * This adds mock specific functionality to a boat. + */ +public class MockBoat extends Boat { + + + /** + * This stores a boat's polars table. + * Can be used to calculate VMG. + */ + private Polars polars; + + /** + * 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; + + + + /** + * Constructs a boat object with a given sourceID, name, country/team abbreviation, and polars table. + * + * @param sourceID The id of the boat + * @param name The name of the Boat. + * @param country The abbreviation or country code for the boat. + * @param polars The polars table to use for this boat. + */ + public MockBoat(int sourceID, String name, String country, Polars polars) { + super(sourceID, name, country); + + this.polars = polars; + } + + + + + /** + * Calculate the bearing of the boat to its next marker. + * @return The bearing to the next marker. + */ + public Bearing calculateBearingToNextMarker() { + + //Get the start and end points. + GPSCoordinate currentPosition = this.getCurrentPosition(); + GPSCoordinate nextMarkerPosition = this.getCurrentLeg().getEndCompoundMark().getAverageGPSCoordinate(); + + //Calculate bearing. + Bearing bearing = GPSCoordinate.calculateBearing(currentPosition, nextMarkerPosition); + + return bearing; + } + + + + /** + * Calculates the distance between the boat and its target marker in nautical miles. + * @return The distance (in nautical miles) between the boat and its target marker. + */ + public double calculateDistanceToNextMarker() { + + //Get start and end markers. + GPSCoordinate startPosition = this.getCurrentPosition(); + + //When boats finish, their "current leg" doesn't have an end marker. + if (this.getCurrentLeg().getEndCompoundMark() == null) { + return 0d; + } + + GPSCoordinate endMarker = this.getCurrentLeg().getEndCompoundMark().getAverageGPSCoordinate(); + + + //Calculate distance. + double distanceNauticalMiles = GPSCoordinate.calculateDistanceNauticalMiles(startPosition, endMarker); + + return distanceNauticalMiles; + } + + + + + /** + * Returns the polars table for this boat. + * @return The polars table for this boat. + */ + public Polars getPolars() { + return polars; + } + + /** + * Sets the polars table for this boat. + * @param polars The new polars table for this boat. + */ + public void setPolars(Polars polars) { + this.polars = polars; + } + + + /** + * Returns the time since the boat changed its tack, in milliseconds. + * @return Time since the boat changed its tack, in milliseconds. + */ + public long getTimeSinceTackChange() { + return timeSinceTackChange; + } + + /** + * Sets the time since the boat changed it's tack, in milliseconds. + * @param timeSinceTackChange Time since the boat changed its tack, in milliseconds. + */ + public void setTimeSinceTackChange(long timeSinceTackChange) { + this.timeSinceTackChange = timeSinceTackChange; + } + + + /** + * Moves the boat meters forward in the direction that it is facing + * @param meters The number of meters to move forward. + * @param milliseconds The number of milliseconds to advance the boat's timers by. + */ + public void moveForwards(double meters, long milliseconds) { + + + //Update the boat's time since last tack. + this.setTimeSinceTackChange(this.getTimeSinceTackChange() + milliseconds); + + //Update the time into the current leg. + this.setTimeElapsedInCurrentLeg(this.getTimeElapsedInCurrentLeg() + milliseconds); + + //Update the distance into the current leg. + this.setDistanceTravelledInLeg(this.getDistanceTravelledInLeg() + meters); + + //Updates the current position of the boat. + GPSCoordinate newPosition = GPSCoordinate.calculateNewPosition(this.getCurrentPosition(), meters, Azimuth.fromBearing(this.getBearing())); + this.setCurrentPosition(newPosition); + + } + + + /** + * Sets the boats speed and bearing to those in the given VMG. + * @param newVMG The new VMG to use for the boat - contains speed and bearing. + */ + public void setVMG(VMG newVMG) { + this.setBearing(newVMG.getBearing()); + this.setCurrentSpeed(newVMG.getSpeed()); + this.setTimeSinceTackChange(0); + } + + + /** + * Calculates the number of nautical miles the boat will travel in a given time slice. + * E.g., in 53 milliseconds a boat may travel 0.0002 nautical miles. + * @param timeSlice The timeslice to use. + * @return The distance travelled, in nautical miles, over the given timeslice. + */ + public double calculateNauticalMilesTravelled(long timeSlice) { + + //The proportion of one hour the current timeslice is. + //This will be a low fractional number, so we need to go from long -> double. + double hourProportion = ((double) timeSlice) / Constants.OneHourMilliseconds; + + //Calculates the distance travelled, in nautical miles, in the current timeslice. + //distanceTravelledNM = speed (nm p hr) * time taken to update loop + double distanceTravelledNM = this.getCurrentSpeed() * hourProportion; + + return distanceTravelledNM; + } + + /** + * Calculates the number of meters the boat will travel in a given time slice. + * E.g., in 53 milliseconds a boat may travel 0.02 meters. + * @param timeSlice The timeslice to use. + * @return The distance travelled, in meters, over the given timeslice. + */ + public double calculateMetersTravelled(long timeSlice) { + + //Calculate the distance travelled, in nautical miles. + double distanceTravelledNM = this.calculateNauticalMilesTravelled(timeSlice); + + //Convert to meters. + double distanceTravelledMeters = distanceTravelledNM * Constants.NMToMetersConversion; + + return distanceTravelledMeters; + } + +} diff --git a/racevisionGame/src/main/java/mock/model/Polars.java b/racevisionGame/src/main/java/mock/model/Polars.java new file mode 100644 index 00000000..08df0325 --- /dev/null +++ b/racevisionGame/src/main/java/mock/model/Polars.java @@ -0,0 +1,431 @@ +package mock.model; + +import javafx.util.Pair; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Encapsulates an entire polar table. Has a function to calculate VMG. + */ +public class Polars { + + /** + * Internal store of data. Maps {@literal Pair} to boatSpeed. + */ + private Map, Double> polarValues = new HashMap<>(); + + + /** + * Stores a list of angles from the polar table - this is used during the calculateVMG function. + * Maps between windSpeed and a list of angles for that wind speed. + */ + private HashMap> polarAngles = new HashMap<>(); + + + + + /** + * Ctor. + */ + public Polars() { + } + + + /** + * Adds an estimated velocity to the polar table object, for a given (windSpeed, windAngle) pair. That is, stores a mapping from (windSpeed, windAngle) to (boatVelocity). + * Note: an estimate means given a specific wind speed of trueWindSpeed, if the boat travels relativeWindAngle degrees towards the wind, it will move at boatSpeed knots. E.g., trueWindSpeed = 20kn, relativeWindAngle = 45 degrees, boatSpeed = 25kn. If the boat travels towards the wind, plus or minus 45 degrees either side, it will move at 25kn. + * @param trueWindSpeed The true wind speed of the estimate. + * @param relativeWindAngle The relative wind angle between the wind direction + 180 degrees and the boat's direction of the estimate. + * @param boatSpeed The boat speed of the estimate. + */ + public void addEstimate(double trueWindSpeed, Bearing relativeWindAngle, double boatSpeed) { + + //We also add the same values with a complementary angle (e.g., angle = 50, complement = 360 - 50 = 310). This is because the data file contains angles [0, 180), but we need [0, 360). + + //Create the array to store angles for this wind speed if it doesn't exist. + if (!this.polarAngles.containsKey(trueWindSpeed)) { + this.polarAngles.put(trueWindSpeed, new ArrayList<>()); + } + + //Add estimate to map. + Pair newKeyPositive = new Pair<>(trueWindSpeed, relativeWindAngle); + this.polarValues.put(newKeyPositive, boatSpeed); + + //Get the "negative" bearing - that is, the equivalent bearing between [180, 360). + Bearing negativeBearing = Bearing.fromDegrees(360d - relativeWindAngle.degrees()); + + + //Ensure that the positive and negative angles aren't the same (e.g., pos = 0, neg = 360 - 0 = 0. + if (!negativeBearing.equals(relativeWindAngle)) { + Pair newKeyNegative = new Pair<>(trueWindSpeed, negativeBearing); + this.polarValues.put(newKeyNegative, boatSpeed); + } + + + //Add angle to angle list. Don't add if it already contains them. + if (!this.polarAngles.get(trueWindSpeed).contains(relativeWindAngle)) { + this.polarAngles.get(trueWindSpeed).add(relativeWindAngle); + } + + if (!this.polarAngles.get(trueWindSpeed).contains(negativeBearing)) { + this.polarAngles.get(trueWindSpeed).add(negativeBearing); + } + + + } + + + + /** + * Calculates the VMG for a given wind angle, wind speed, and angle to destination. Will only return VMGs that have a true bearing (angle) within a given bound - this is to ensure that you can calculate VMGs without going out of bounds. + *
+ * If you don't care about bearing bounds, simply pass in lower = 0, upper = 359.9. + *
+ * Passing in lower = 0, upper = 0, or lower = 0, upper = 360 will both be treated the same as lower = 0, upper = 359.99999. + *

+ * The resulting angle of the VMG will be within the interval [bearingLowerBound, bearingUpperBound]. + *

+ * If the lower bound is greater than the upper bound (e.g., lower = 70, upper = 55), then it checks that {@literal VMGAngle >= lower OR VMGAngle <= upper} (e.g., {@literal [70, 55] means angle >= 70, OR angle =< 55}). + *

+ * Returns a VMG with 0 speed and 0 bearing if there are no VMGs with {@literal velocity > 0} in the acceptable bearing bounds. + * @param trueWindAngle The current true wind angle. + * @param trueWindSpeed The current true wind speed. Knots. + * @param destinationAngle The angle between the boat and the destination point. + * @param bearingLowerBound The lowest bearing (angle) that the boat may travel on. + * @param bearingUpperBound The highest bearing (angle) that the boat may travel on. + * @return The VMG. + */ + public VMG calculateVMG(Bearing trueWindAngle, double trueWindSpeed, Bearing destinationAngle, Bearing bearingLowerBound, Bearing bearingUpperBound) { + + //Sorts polar angles. + for (List angles : this.polarAngles.values()) { + angles.sort(null); + } + + + //If the user enters [0, 360] for their bounds, there won't be any accepted angles, as Bearing(360) turn into Bearing(0) (it has the interval [0, 360)). + //So if both bearing bounds are zero, we assume that the user wanted [0, 360) for the interval. + //So, we give them Bearing(359.99999) as the upper bound. + if ((bearingLowerBound.degrees() == 0d) && (bearingUpperBound.degrees() == 0d)) { + bearingUpperBound = Bearing.fromDegrees(359.99999d); + } + + + + + //If the lower bound is greater than the upper bound, we have a "flipped" interval. That is for, e.g., [70, 55] the lower bound is greater than the upper bound, and so it checks that (VMGAngle >= 70 OR VMGAngle =< 55), instead of (VMGAngle >= 70 AND VMGAngle =< 55). + boolean flippedInterval = Polars.isFlippedInterval(bearingLowerBound, bearingUpperBound); + + + + //We need to find the upper and lower wind speeds from the Polars table, for a given current wind speed (e.g., current wind speed is 11kn, therefore lower = 8kn, upper = 12kn). + double polarWindSpeedLowerBound = 0d; + double polarWindSpeedUpperBound = 9999999d;//Start this off with a value larger than any in the Polars table so that it actually works. + //This indicates whether or not we've managed to find a wind speed larger than the current wind speed (the upper bound) in the Polars table (in cases where the current wind speed is larger than any in the file we will never find an upper bound). + boolean foundUpperBoundWindSpeed = false; + boolean foundLowerBoundWindSpeed = false; + for (Pair key : this.polarValues.keySet()) { + + //The key is Pair, so pair.key is windSpeed. + double currentPolarSpeed = key.getKey(); + + //Lower bound. + if ((currentPolarSpeed >= polarWindSpeedLowerBound) && (currentPolarSpeed <= trueWindSpeed)) { + polarWindSpeedLowerBound = currentPolarSpeed; + foundLowerBoundWindSpeed = true; + } + + //Upper bound. + if ((currentPolarSpeed < polarWindSpeedUpperBound) && (currentPolarSpeed > trueWindSpeed)) { + polarWindSpeedUpperBound = currentPolarSpeed; + foundUpperBoundWindSpeed = true; + } + + } + + + + //Find the angle with the best VMG. + //We need to find the VMGs for both lower and upper bound wind speeds, and interpolate between them. + List vmgs = new ArrayList<>(); + + //Put wind speed bounds we found above into a list. + List windSpeedBounds = new ArrayList<>(2); + + if (foundLowerBoundWindSpeed) { + windSpeedBounds.add(polarWindSpeedLowerBound); + } + if (foundUpperBoundWindSpeed) { + windSpeedBounds.add(polarWindSpeedUpperBound); + } + + + //Calculate VMG for any wind speed bounds we found. + for (double polarWindSpeed : windSpeedBounds) { + + //The list of polar angles for this wind speed. + List polarAngles = this.polarAngles.get(polarWindSpeed); + + + double bestVMGVelocity = 0; + double bestVMGSpeed = 0; + Bearing bestVMGAngle = Bearing.fromDegrees(0d); + + //Calculate the VMG for all possible angles at this wind speed. + for (double angleDegree = 0; angleDegree < 360; angleDegree += 1) { + Bearing angle = Bearing.fromDegrees(angleDegree); + + //This is the true bearing of the boat, if it went at the angle against the wind. + //For angle < 90 OR angle > 270, it means that the boat is going into the wind (tacking). + //For angle > 90 AND angle < 270, it means that the boat is actually going with the wind (gybing). + double trueBoatBearingDegrees = trueWindAngle.degrees() + angle.degrees() + 180d; + Bearing trueBoatBearing = Bearing.fromDegrees(trueBoatBearingDegrees); + + + //Check that the boat's bearing would actually be acceptable. + //We continue (skip to next iteration) if it is outside of the interval. + if (!Polars.isBearingInsideInterval(trueBoatBearing, bearingLowerBound, bearingUpperBound)) { + continue; + } + + + //Basic linear interpolation. Find the nearest two angles from the table, and interpolate between them. + + //Check which pair of adjacent angles the angle is between. + boolean foundAdjacentAngles = false; + Bearing lowerBound = Bearing.fromDegrees(0d); + Bearing upperBound = Bearing.fromDegrees(0d); + for (int i = 0; i < polarAngles.size() - 1; i++) { + Bearing currentAngle = polarAngles.get(i); + Bearing nextAngle = polarAngles.get(i + 1); + //Check that angle is in interval [lower, upper). + if ((angle.degrees() >= currentAngle.degrees()) && (angle.degrees() < nextAngle.degrees())) { + foundAdjacentAngles = true; + lowerBound = currentAngle; + upperBound = nextAngle; + break; + } + } + + if (!foundAdjacentAngles) { + //If we never found the interval, then it must be the "last" interval, between the i'th and 0'th values - angles are periodic, so they wrap around. + lowerBound = polarAngles.get(polarAngles.size() - 1); + upperBound = polarAngles.get(0); + } + + + //Calculate how far between those points the angle is. + + + //This is how far between the lower and upper bounds the angle is, as a proportion (e.g., 0.5 = half-way, 0.9 = close to upper). + double interpolationScalar = calculatePeriodicLinearInterpolateScalar(lowerBound.degrees(), upperBound.degrees(), 360, angle.degrees()); + + //Get the estimated boat speeds for the lower and upper angles. + Pair lowerKey = new Pair<>(polarWindSpeed, lowerBound); + Pair upperKey = new Pair<>(polarWindSpeed, upperBound); + double lowerSpeed = this.polarValues.get(lowerKey); + double upperSpeed = this.polarValues.get(upperKey); + + //Calculate the speed at the interpolated angle. + double interpolatedSpeed = calculateLinearInterpolation(lowerSpeed, upperSpeed, interpolationScalar); + + + //This is the delta angle between the boat's true bearing and the destination. + double angleBetweenDestAndTackDegrees = trueBoatBearing.degrees() - destinationAngle.degrees(); + Bearing angleBetweenDestAndTack = Bearing.fromDegrees(angleBetweenDestAndTackDegrees); + + //This is the estimated velocity towards the target (e.g., angling away from the target reduces velocity). + double interpolatedVelocity = Math.cos(angleBetweenDestAndTack.radians()) * interpolatedSpeed; + + + //Check that the velocity is better, if so, update our best VMG so far, for this wind speed. + if (interpolatedVelocity > bestVMGVelocity) { + bestVMGVelocity = interpolatedVelocity; + bestVMGSpeed = interpolatedSpeed; + bestVMGAngle = trueBoatBearing; + } + + } + //Angle iteration loop is finished. + + //Create the VMG, and add to list. + VMG vmg = new VMG(bestVMGSpeed, bestVMGAngle); + vmgs.add(vmg); + + } + + + //If we never found an upper bound for the wind speed, we will only have one VMG (for the lower bound), so we can't interpolate/extrapolate anything. + if (!foundUpperBoundWindSpeed) { + return vmgs.get(0); + } else { + //We may have more than one VMG. If we found an upper and lower bound we will have two, if we only found an upper bound (e.g., wind speed = 2kn, upper = 4kn, lower = n/a) we will only have one VMG, but must interpolate between that and a new VMG with 0kn speed. + + //We do a simple linear interpolation. + + VMG vmg1 = vmgs.get(0); + VMG vmg2; + if (vmgs.size() > 1) { + //If we have a second VMG use it. + vmg2 = vmgs.get(1); + } else { + //Otherwise create a VMG with zero speed, but the same angle. This is what our VMG would be with 0 knot wind speed (boats don't move at 0 knots). + //We also need to swap them around, as vmg1 needs to be the vmg for the lower bound wind speed, and vmg2 is the upper bound wind speed. + vmg2 = vmg1; + vmg1 = new VMG(0, vmg1.getBearing()); + } + + + //Get the interpolation scalar for the current wind speed. + double interpolationScalar = calculateLinearInterpolateScalar(polarWindSpeedLowerBound, polarWindSpeedUpperBound, trueWindSpeed); + + //We then calculate the interpolated VMG speed and angle using the interpolation scalar. + double interpolatedSpeed = calculateLinearInterpolation(vmg1.getSpeed(), vmg2.getSpeed(), interpolationScalar); + double interpolatedAngleDegrees = calculateLinearInterpolation(vmg1.getBearing().degrees(), vmg2.getBearing().degrees(), interpolationScalar); + + Bearing interpolatedAngle = Bearing.fromDegrees(interpolatedAngleDegrees); + + + //Return the interpolated VMG. + return new VMG(interpolatedSpeed, interpolatedAngle); + + } + + + } + + + /** + * Determines whether an interval is "flipped". This means that the lower bound is greater than the upper bound (e.g., [290, 43] degrees). + * @param lowerBound The lower bound. + * @param upperBound The upper bound. + * @return True if the interval is flipped, false otherwise. + */ + public static boolean isFlippedInterval(Bearing lowerBound, Bearing upperBound) { + + //If the lower bound is greater than the upper bound, we have a "flipped" interval. + boolean flippedInterval = false; + if (lowerBound.degrees() > upperBound.degrees()) { + flippedInterval = true; + } + + return flippedInterval; + } + + + /** + * Determines if a bearing is inside an interval. + * @param bearing The bearing to check. + * @param lowerBound The lower bound of the interval. + * @param upperBound The upper bound of the interval. + * @return True if the bearing is inside the interval, false otherwise. + */ + public static boolean isBearingInsideInterval(Bearing bearing, Bearing lowerBound, Bearing upperBound) { + + //Check if it's a flipped interval. + boolean flippedInterval = Polars.isFlippedInterval(lowerBound, upperBound); + + if (flippedInterval) { + //Bearing must be inside [lower, upper], where lower > upper. So, bearing must be >= lower, or bearing < upper. We use inverted logic since we are skipping if it is true. + if ((bearing.degrees() >= lowerBound.degrees()) || (bearing.degrees() <= upperBound.degrees())) { + return true; + } else { + return false; + } + + } else { + //Bearing must be inside [lower, upper]. + if ((bearing.degrees() >= lowerBound.degrees()) && (bearing.degrees() <= upperBound.degrees())) { + return true; + } else { + return false; + } + + } + + + } + + + /** + * Calculate the linear interpolation scalar for a value between two bounds. E.g., lower = 7, upper = 10, value = 8, therefore the scalar (or the proportion between the bounds) is 0.333. + * Also assumes that the bounds are periodic - e.g., for angles a lower bound of 350deg and upper bound of 5deg is in interval of 15 degrees. + * @param lowerBound The lower bound to interpolate between. + * @param upperBound The upper bound to interpolate between. + * @param value The value that sits between the lower and upper bounds. + * @param period The period of the bounds (e.g., for angles, they have a period of 360 degrees). + * @return The interpolation scalar for the value between two bounds. + */ + public static double calculatePeriodicLinearInterpolateScalar(double lowerBound, double upperBound, double period, double value) { + + //This is the "distance" between the value and its lower bound. + //I.e., L----V-----------U + // <----> is lowerDelta. + double lowerDelta = value - lowerBound; + + //This is the "distance" between the upper and lower bound. + //I.e., L----V-----------U + // <----------------> is intervalDelta. + //This can potentially be negative if we have, e.g., lower = 340deg, upper = 0deg, delta = -340deg. + double intervalDelta = upperBound - lowerBound; + //If it _is_ negative, modulo it to make it positive. + //E.g., -340deg = +20deg. + while (intervalDelta < 0) { + intervalDelta += period; + } + + //This is how far between the lower and upper bounds the value is, as a proportion (e.g., 0.5 = half-way, 0.9 = close to upper). + double interpolationScalar = lowerDelta / intervalDelta; + + return interpolationScalar; + } + + /** + * Calculate the linear interpolation scalar for a value between two bounds. E.g., lower = 7, upper = 10, value = 8, therefore the scalar (or the proportion between the bounds) is 0.333. + * Assumes that the upper bound is larger than the lower bound. + * @param lowerBound The lower bound to interpolate between. + * @param upperBound The upper bound to interpolate between. + * @param value The value that sits between the lower and upper bounds. + * @return The interpolation scalar for the value between two bounds. + */ + public static double calculateLinearInterpolateScalar(double lowerBound, double upperBound, double value) { + + //This is the "distance" between the value and its lower bound. + //I.e., L----V-----------U + // <----> is lowerDelta. + double lowerDelta = value - lowerBound; + + //This is the "distance" between the upper and lower bound. + //I.e., L----V-----------U + // <----------------> is intervalDelta. + double intervalDelta = upperBound - lowerBound; + + //This is how far between the lower and upper bounds the value is, as a proportion (e.g., 0.5 = half-way, 0.9 = close to upper). + double interpolationScalar = lowerDelta / intervalDelta; + + return interpolationScalar; + } + + + /** + * Does a linear interpolation between two bounds, using an interpolation scalar - i.e., value = lower + (scalar * delta). + * @param lowerBound Lower bound to interpolate from. + * @param upperBound Upper bound to interpolate to. + * @param interpolationScalar Interpolation scalar - the proportion the target value sits between the two bounds. + * @return The interpolated value. + */ + public static double calculateLinearInterpolation(double lowerBound, double upperBound, double interpolationScalar) { + + //Get the delta between upper and lower bounds. + double boundDelta = upperBound - lowerBound; + + //Calculate the speed at the interpolated angle. + double interpolatedValue = lowerBound + (boundDelta * interpolationScalar); + + return interpolatedValue; + } + +} diff --git a/racevisionGame/src/main/java/mock/model/VMG.java b/racevisionGame/src/main/java/mock/model/VMG.java new file mode 100644 index 00000000..905fadce --- /dev/null +++ b/racevisionGame/src/main/java/mock/model/VMG.java @@ -0,0 +1,48 @@ +package mock.model; + +import shared.model.Bearing; + +/** + * This class encapsulates VMG - that is, velocity made good. It has a speed component and a bearing component. + */ +public class VMG { + + /** + * Speed component of the VMG, in knots. + */ + private double speed; + + /** + * Bearing component of the VMG. + */ + private Bearing bearing; + + + /** + * Ctor. Creates a VMG object with a given speed and bearing (that is, a velocity). + * @param speed Speed component of the VMG. + * @param bearing Bearing component of the VMG. + */ + public VMG(double speed, Bearing bearing) { + this.speed = speed; + this.bearing = bearing; + } + + + /** + * Returns the speed component of this VMG object, measured in knots. + * @return Speed component of this VMG object. + */ + public double getSpeed() { + return speed; + } + + /** + * Returns the bearing component of this VMG object. + * @return Bearing component of this VMG object. + */ + public Bearing getBearing() { + return bearing; + } + +} diff --git a/racevisionGame/src/main/java/network/BinaryMessageDecoder.java b/racevisionGame/src/main/java/network/BinaryMessageDecoder.java new file mode 100644 index 00000000..183491df --- /dev/null +++ b/racevisionGame/src/main/java/network/BinaryMessageDecoder.java @@ -0,0 +1,274 @@ +package network; + +import seng302.Networking.Exceptions.InvalidMessageException; +import seng302.Networking.Messages.Enums.MessageType; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.zip.CRC32; + +/** + * This class can be used to decode/convert a byte array into a messageBody object, descended from AC35Data. + */ +public class BinaryMessageDecoder { + + ///Length of the header. + private static final int headerLength = 15; + ///Length of the CRC. + private static final int CRCLength = 4;//TODO these should probably be static defined somewhere else to be shared. + + ///The value the first sync byte should have. + private static final byte syncByte1 = (byte) 0x47; + //The value the second sync byte should have. + private static final byte syncByte2 = (byte) 0x83; + + ///The full message. + private byte[] fullMessage; + ///The messageHeader. + private byte[] messageHeader; + ///The messageBody. + private byte[] messageBody; + + ///The sync bytes from the header.. + private byte headerSync1; + private byte headerSync2; + + ///The message type from the header. + private byte headerMessageType; + + ///The timestamp from the header. + private long headerTimeStamp; + + ///The source ID from the header. + private int headerSourceID; + + ///The message body length from the header. + private int messageBodyLength; + + ///CRC value read from message header. + private long messageCRCValue; + ///Calculated CRC value from message. + private long calculatedCRCValue; + + + /** + * Ctor. + * @param fullMessage Entire encoded binary message. + */ + public BinaryMessageDecoder(byte[] fullMessage) { + this.fullMessage = fullMessage; + + //Get the messageHeader. + this.messageHeader = Arrays.copyOfRange(this.fullMessage, 0, 15); + + //Get the sync bytes. + this.headerSync1 = this.messageHeader[0]; + this.headerSync2 = this.messageHeader[1]; + + //Get the message type. + this.headerMessageType = this.messageHeader[2]; + + //Get the header timestamp. + this.headerTimeStamp = ByteConverter.bytesToLong(Arrays.copyOfRange(this.messageHeader, 3, 9)); + + //Get the source ID for the message. + this.headerSourceID = ByteConverter.bytesToInt(Arrays.copyOfRange(this.messageHeader, 9, 13)); + + //Get the length of the message body. + this.messageBodyLength = ByteConverter.bytesToInt(Arrays.copyOfRange(this.messageHeader, 13, 15)); + + + //Get the messageBody. + this.messageBody = Arrays.copyOfRange(this.fullMessage, this.headerLength, this.headerLength + this.messageBodyLength); + + //Get the CRC value. + this.messageCRCValue = ByteConverter.bytesToLong(Arrays.copyOfRange(this.fullMessage, this.fullMessage.length - CRCLength, this.fullMessage.length)); + + //Combine the header and body into a single array. + ByteBuffer headerBodyByteBuffer = ByteBuffer.allocate(messageHeader.length + messageBody.length); + headerBodyByteBuffer.put(messageHeader); + headerBodyByteBuffer.put(messageBody); + + //Calculate the CRC value from the header+body array. + CRC32 crc = new CRC32(); + crc.reset(); + crc.update(headerBodyByteBuffer.array()); + this.calculatedCRCValue = crc.getValue(); + } + + + /** + * Decodes the byte array (binary message) this object was initialized with, and returns the corresponding message object. + * @return Message object corresponding to the binary message. + * @throws InvalidMessageException If the message cannot be decoded. + */ + public AC35Data decode() throws InvalidMessageException { + + //Run through the checks to ensure that the message is valid. + if (messageBody.length != messageBodyLength) {//keep like this - hba65 + //Check the message body length. + throw new InvalidMessageException("MessageBody length in header does not equal the messageBody length. MessageBody length in header is: " + messageBodyLength + ", should be: " + messageBody.length); + + }else if (headerSync1 != syncByte1) { + //Check the first sync byte. + throw new InvalidMessageException("Sync byte 1 is wrong. Sync byte is: " + headerSync1 + ", should be: " + syncByte1); + + }else if (headerSync2 != syncByte2) { + //Check the second sync byte. + throw new InvalidMessageException("Sync byte 2 is wrong. Sync byte is: " + headerSync2 + ", should be: " + syncByte2); + + }else if (calculatedCRCValue != messageCRCValue) { + //Check the CRC value. + throw new InvalidMessageException("CRC value is wrong. The calculated value is: " + calculatedCRCValue + ", should be: " + messageCRCValue); + + } + + //Now we create the message object based on what is actually in the message body. + MessageType mType = MessageType.fromByte(headerMessageType); + + switch(mType) { + case HEARTBEAT: + //System.out.println("Decoding HeartBeat Message!"); + //TODO maybe use HeartbeatDecoder.decode(message). + //TODO also, decoders for each message type should encapsulate the constructing of the object. E.g., return HeartbeatDecoder.decode(message);. + return new Heartbeat(ByteConverter.bytesToLong(messageBody)); + + case RACESTATUS: + //System.out.println("Race Status Message"); + RaceStatusDecoder rsdecoder = new RaceStatusDecoder(messageBody); + return new RaceStatus(rsdecoder.getTime(), rsdecoder.getRace(), rsdecoder.getRaceState(), rsdecoder.getStartTime(), rsdecoder.getRaceWindDir(), rsdecoder.getRaceWindSpeed(), rsdecoder.getRaceType(), rsdecoder.getBoats()); + + case DISPLAYTEXTMESSAGE: + //System.out.println("Display Text Message"); + //No decoder for this. + //throw new InvalidMessageException("Cannot decode DISPLAYTEXTMESSAGE - no decoder."); + + case XMLMESSAGE: + //System.out.println("XML Message!"); + XMLMessageDecoder xmdecoder = new XMLMessageDecoder(messageBody); + xmdecoder.decode(); + return new XMLMessage(xmdecoder.getAckNumber(), xmdecoder.getTimeStamp(), xmdecoder.getXmlMsgSubType(), xmdecoder.getSequenceNumber(), xmdecoder.getXmlMsgLength(), xmdecoder.getXmlMessageInputStream()); + + case RACESTARTSTATUS: + //System.out.println("Race Start Status Message"); + RaceStartStatusDecoder rssDecoder = new RaceStartStatusDecoder(messageBody); + return new RaceStartStatus(rssDecoder.getTime(), rssDecoder.getAck(), rssDecoder.getStartTime(), rssDecoder.getRaceID(), rssDecoder. getNotification()); + + case YACHTEVENTCODE: + //System.out.println("Yacht Action Code!"); + //No decoder for this. + //throw new InvalidMessageException("Cannot decode YACHTEVENTCODE - no decoder."); + + case YACHTACTIONCODE: + //System.out.println("Yacht Action Code!"); + //No decoder for this. + //throw new InvalidMessageException("Cannot decode YACHTACTIONCODE - no decoder."); + + case CHATTERTEXT: + //System.out.println("Chatter Text Message!"); + //No decoder for this. + //throw new InvalidMessageException("Cannot decode CHATTERTEXT - no decoder."); + + case BOATLOCATION: + //System.out.println("Boat Location Message!"); + BoatLocationDecoder blDecoder = new BoatLocationDecoder(messageBody); + return blDecoder.getMessage(); + + case MARKROUNDING: + //System.out.println("Mark Rounding Message!"); + MarkRoundingDecoder mrDecoder = new MarkRoundingDecoder(messageBody); + return mrDecoder.getMarkRounding(); + + case COURSEWIND: + //System.out.println("Course Wind Message!"); + CourseWindDecoder cwDecoder = new CourseWindDecoder(messageBody); + return new CourseWinds(cwDecoder.getMessageVersionNumber(), cwDecoder.getByteWindID(), cwDecoder.getLoopMessages()); + + case AVGWIND: + //System.out.println("Average Wind Message!"); + AverageWindDecoder awDecoder = new AverageWindDecoder(messageBody); + return awDecoder.getAverageWind(); + + default: + //System.out.println("Broken Message!"); + //throw new InvalidMessageException("Broken message! Did not recognise message type: " + headerMessageType + "."); + return null; + + } + } + + + /** + * Returns the first sync byte value. + * @return The first sync byte value. + */ + public byte getHeaderSync1() { + return headerSync1; + } + + /** + * Returns the second sync byte value. + * @return The second sync byte value. + */ + public byte getHeaderSync2() { + return headerSync2; + } + + /** + * Returns the message type. + * @return The message type. + */ + public byte getHeaderMessageType() { + return headerMessageType; + } + + /** + * Returns the header timestamp. + * @return The header timestamp. + */ + public long getHeaderTimeStamp() { + return headerTimeStamp; + } + + /** + * Returns the header source ID. + * @return The header source ID. + */ + public int getHeaderSourceID() { + return headerSourceID; + } + + /** + * Returns the message body length, according to the header. + * @return The message body length. + */ + public int getMessageBodyLength() { + return messageBodyLength; + } + + /** + * Returns the message CRC value, according to the header. + * @return The message CRC value. + */ + public long getMessageCRCValue() { + return messageCRCValue; + } + + /** + * Returns the calculated CRC value from the message header + body contents. + * @return The calculated CRC value. + */ + public long getCalculatedCRCValue() { + return calculatedCRCValue; + } + + + /** + * Returns the message body. + * @return The message body. + */ + public byte[] getMessageBody() { + return messageBody; + } +} + diff --git a/racevisionGame/src/main/java/network/BinaryMessageEncoder.java b/racevisionGame/src/main/java/network/BinaryMessageEncoder.java new file mode 100644 index 00000000..0c008444 --- /dev/null +++ b/racevisionGame/src/main/java/network/BinaryMessageEncoder.java @@ -0,0 +1,103 @@ +package network; + + +import seng302.Networking.Messages.Enums.MessageType; + +import java.nio.ByteBuffer; +import java.util.zip.CRC32; + +import static seng302.Networking.Utils.ByteConverter.*; + +/** + * This class can be used to encode/convert a byte array message body, plus header data into a byte array containing the entire message, ready to send. + */ +public class BinaryMessageEncoder { + + ///Length of the header. + private static final int headerLength = 15; + ///Length of the CRC. + private static final int CRCLength = 4;//TODO these should probably be static defined somewhere else to be shared. + + ///The full message. + private byte[] fullMessage; + ///The message header. + private byte[] messageHeader; + ///The message body. + private byte[] messageBody; + + ///First sync byte value. + private byte headerSync1 = (byte)0x47; + ///Second sync byte value. + private byte headerSync2 = (byte)0x83; + + ///The message type to place in header. + private byte headerMessageType; + ///The timestamp to place in header. + private long headerTimeStamp; + ///The source ID to place in header. + private int headerSourceID; + ///The message length to place in header. + private short bodyMessageLength; + + ///The calculated CRC value. + private long calculatedCRCValue; + + /** + * Ctor. Constructs a encoder and encodes the full message. Retrieve it with encoder.getFullMessage(). + * @param headerMessageType The message type to send. + * @param headerTimeStamp Timestamp of the message. + * @param headerSourceID Source ID of the message. + * @param bodyMessageLength The length of the body of the message. + * @param messageBody The body of the message (that is, the payload). + */ + public BinaryMessageEncoder(MessageType headerMessageType, long headerTimeStamp, int headerSourceID, short bodyMessageLength, byte[] messageBody) { + //Set the header parameters. + this.headerMessageType = headerMessageType.getValue(); + this.headerTimeStamp = headerTimeStamp; + this.headerSourceID = headerSourceID; + this.bodyMessageLength = bodyMessageLength; + + //Place the header parameters into a buffer. + ByteBuffer tempHeaderByteBuffer = ByteBuffer.allocate(this.headerLength); + tempHeaderByteBuffer.put(this.headerSync1); + tempHeaderByteBuffer.put(this.headerSync2); + tempHeaderByteBuffer.put(this.headerMessageType); + tempHeaderByteBuffer.put(longToBytes(this.headerTimeStamp, 6)); + tempHeaderByteBuffer.put(intToBytes(this.headerSourceID)); + tempHeaderByteBuffer.put(shortToBytes(this.bodyMessageLength)); + + this.messageHeader = tempHeaderByteBuffer.array(); + + //Set the message body. + this.messageBody = messageBody; + + + //Place header and body into a buffer. + ByteBuffer tempHeaderBodyByteBuffer = ByteBuffer.allocate(this.messageHeader.length + this.bodyMessageLength); + tempHeaderBodyByteBuffer.put(this.messageHeader); + tempHeaderBodyByteBuffer.put(this.messageBody); + + //Calculate the CRC from header + body. + CRC32 crc = new CRC32(); + crc.reset(); + crc.update(tempHeaderBodyByteBuffer.array()); + this.calculatedCRCValue = crc.getValue(); + + //Place header, body, and CRC value in buffer. + ByteBuffer tempFullMessageByteBuffer = ByteBuffer.allocate(this.messageHeader.length + this.messageBody.length + this.CRCLength); + tempFullMessageByteBuffer.put(this.messageHeader); + tempFullMessageByteBuffer.put(this.messageBody); + tempFullMessageByteBuffer.put(intToBytes((int) this.calculatedCRCValue)); + + //Set the full message. + this.fullMessage = tempFullMessageByteBuffer.array(); + } + + /** + * Returns the full encoded message. This includes the header, body, and CRC. + * @return Full encoded message. + */ + public byte[] getFullMessage() { + return fullMessage; + } +} diff --git a/racevisionGame/src/main/java/network/Exceptions/InvalidMessageException.java b/racevisionGame/src/main/java/network/Exceptions/InvalidMessageException.java new file mode 100644 index 00000000..6af32842 --- /dev/null +++ b/racevisionGame/src/main/java/network/Exceptions/InvalidMessageException.java @@ -0,0 +1,25 @@ +package network.Exceptions; + +/** + * Exception which is thrown when a message is read, but it is invalid in some way (CRC is wrong, sync bytes, etc...). + */ +public class InvalidMessageException extends Exception +{ + + /** + * Ctor. + * @param message String message. + */ + public InvalidMessageException(String message) { + super(message); + } + + /** + * Ctor. + * @param message String message. + * @param cause Cause of the exception. + */ + public InvalidMessageException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/racevisionGame/src/main/java/network/MessageDecoders/AverageWindDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/AverageWindDecoder.java new file mode 100644 index 00000000..22548063 --- /dev/null +++ b/racevisionGame/src/main/java/network/MessageDecoders/AverageWindDecoder.java @@ -0,0 +1,55 @@ +package network.MessageDecoders; + +import seng302.Networking.Messages.AverageWind; +import seng302.Networking.Utils.ByteConverter; + +import java.util.Arrays; + +/** + * Created by hba56 on 23/04/17. + */ +public class AverageWindDecoder { + byte messageVersionNumber; + byte[] byteTime; + byte[] byteRawPeriod; + byte[] byteRawSpeed; + byte[] bytePeriod2; + byte[] byteSpeed2; + byte[] bytePeriod3; + byte[] byteSpeed3; + byte[] bytePeriod4; + byte[] byteSpeed4; + + AverageWind averageWind; + + public AverageWindDecoder(byte[] encodedAverageWind) { + messageVersionNumber = encodedAverageWind[0]; + byteTime = Arrays.copyOfRange(encodedAverageWind, 1, 7); + byteRawPeriod = Arrays.copyOfRange(encodedAverageWind, 7, 9); + byteRawSpeed = Arrays.copyOfRange(encodedAverageWind, 9, 11); + bytePeriod2 = Arrays.copyOfRange(encodedAverageWind, 11, 13); + byteSpeed2 = Arrays.copyOfRange(encodedAverageWind, 13, 15); + bytePeriod3 = Arrays.copyOfRange(encodedAverageWind, 15, 17); + byteSpeed3 = Arrays.copyOfRange(encodedAverageWind, 17, 19); + bytePeriod4 = Arrays.copyOfRange(encodedAverageWind, 19, 21); + byteSpeed4 = Arrays.copyOfRange(encodedAverageWind, 21, 23); + + int msgNum = ByteConverter.bytesToInt(messageVersionNumber); + long lngTime = ByteConverter.bytesToLong(byteTime); + int intRawPeriod = ByteConverter.bytesToInt(byteRawPeriod); + int intRawSpeed = ByteConverter.bytesToInt(byteRawSpeed); + int intPeriod2 = ByteConverter.bytesToInt(bytePeriod2); + int intSpeed2 = ByteConverter.bytesToInt(byteSpeed2); + int intPeriod3 = ByteConverter.bytesToInt(bytePeriod3); + int intSpeed3 = ByteConverter.bytesToInt(byteSpeed3); + int intPeriod4 = ByteConverter.bytesToInt(bytePeriod4); + int intSpeed4 = ByteConverter.bytesToInt(byteSpeed4); + + this.averageWind = new AverageWind(msgNum, lngTime, intRawPeriod, intRawSpeed, intPeriod2, intSpeed2, intPeriod3, intSpeed3, intPeriod4, intSpeed4); + + } + + public AverageWind getAverageWind() { + return averageWind; + } +} diff --git a/racevisionGame/src/main/java/network/MessageDecoders/BoatLocationDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/BoatLocationDecoder.java new file mode 100644 index 00000000..b7504baa --- /dev/null +++ b/racevisionGame/src/main/java/network/MessageDecoders/BoatLocationDecoder.java @@ -0,0 +1,139 @@ +package network.MessageDecoders; + +import seng302.Networking.Messages.BoatLocation; + +import java.util.Arrays; + +import static seng302.Networking.Utils.ByteConverter.*; + +/** + * Created by hba56 on 21/04/17. + */ +public class BoatLocationDecoder { + private byte messageVersionNumber; + private byte[] time; + private byte[] sourceID; + private byte[] seqNum; + private byte deviceType; + private byte[] latitude; + private byte[] longitude; + private byte[] altitude; + private byte[] heading; + private byte[] pitch; + private byte[] roll; + private byte[] boatSpeed; + private byte[] cog; + private byte[] sog; + private byte[] apparentWindSpeed; + private byte[] apparentWindAngle; + private byte[] trueWindSpeed; + private byte[] trueWindDirection; + private byte[] trueWindAngle; + private byte[] currentDrift; + private byte[] currentSet; + private byte[] rudderAngle; + + private BoatLocation message; + + public BoatLocationDecoder(byte[] encodedBoatLocation) { + byte numMessageVersionNumber = 0; + long numTime = 0; + int numSourceID = 0; + int numSeqNum = 0; + byte numDeviceType = 0; + int numLatitude = 0; + int numLongitude = 0; + int numAltitude = 0; + int numHeading = 0; + short numPitch = 0; + short numRoll = 0; + int numBoatSpeed = 0; + int numCog = 0; + int numSog = 0; + int numApparentWindSpeed = 0; + short numApparentWindAngle = 0; + int numTrueWindSpeed = 0; + short numTrueWindDirection = 0; + short numTrueWindAngle = 0; + int numCurrentDrift = 0; + int numCurrentSet = 0; + short numRudderAngle = 0; + + try { + messageVersionNumber = encodedBoatLocation[0]; + numMessageVersionNumber = messageVersionNumber; + time = Arrays.copyOfRange(encodedBoatLocation, 1, 7); + numTime = bytesToLong(time); + sourceID = Arrays.copyOfRange(encodedBoatLocation, 7, 11); + numSourceID = bytesToInt(sourceID); + seqNum = Arrays.copyOfRange(encodedBoatLocation, 11, 15); + numSeqNum = bytesToInt(seqNum); + deviceType = encodedBoatLocation[15]; + numDeviceType = deviceType; + latitude = Arrays.copyOfRange(encodedBoatLocation, 16, 20); + numLatitude = bytesToInt(latitude); + longitude = Arrays.copyOfRange(encodedBoatLocation, 20, 24); + numLongitude = bytesToInt(longitude); + altitude = Arrays.copyOfRange(encodedBoatLocation, 24, 28); + numAltitude = bytesToInt(altitude); + heading = Arrays.copyOfRange(encodedBoatLocation, 28, 30); + numHeading = bytesToInt(heading); + pitch = Arrays.copyOfRange(encodedBoatLocation, 30, 32); + numPitch = bytesToShort(pitch); + roll = Arrays.copyOfRange(encodedBoatLocation, 32, 34); + numRoll = bytesToShort(roll); + boatSpeed = Arrays.copyOfRange(encodedBoatLocation, 34, 36); + numBoatSpeed = bytesToInt(boatSpeed); + cog = Arrays.copyOfRange(encodedBoatLocation, 36, 38); + numCog = bytesToInt(cog); + sog = Arrays.copyOfRange(encodedBoatLocation, 38, 40); + numSog = bytesToInt(sog); + apparentWindSpeed = Arrays.copyOfRange(encodedBoatLocation, 40, 42); + numApparentWindSpeed = bytesToInt(apparentWindSpeed); + apparentWindAngle = Arrays.copyOfRange(encodedBoatLocation, 42, 44); + numApparentWindAngle = bytesToShort(apparentWindAngle); + trueWindSpeed = Arrays.copyOfRange(encodedBoatLocation, 44, 46); + numTrueWindSpeed = bytesToInt(trueWindSpeed); + trueWindDirection = Arrays.copyOfRange(encodedBoatLocation, 46, 48); + numTrueWindDirection = bytesToShort(trueWindDirection); + trueWindAngle = Arrays.copyOfRange(encodedBoatLocation, 48, 50); + numTrueWindAngle = bytesToShort(trueWindAngle); + currentDrift = Arrays.copyOfRange(encodedBoatLocation, 50, 52); + numCurrentDrift = bytesToInt(currentDrift); + currentSet = Arrays.copyOfRange(encodedBoatLocation, 52, 54); + numCurrentSet = bytesToShort(currentSet); + rudderAngle = Arrays.copyOfRange(encodedBoatLocation, 54, 56); + numRudderAngle = bytesToShort(rudderAngle); + } catch(ArrayIndexOutOfBoundsException e){ + + } + + message = new BoatLocation(numMessageVersionNumber, numTime, + numSourceID, numSeqNum, numDeviceType, numLatitude, + numLongitude, numAltitude, numHeading, numPitch, + numRoll, numBoatSpeed, numCog, numSog, numApparentWindSpeed, + numApparentWindAngle, numTrueWindSpeed, numTrueWindDirection, + numTrueWindAngle, numCurrentDrift, numCurrentSet, numRudderAngle + );/* + message = new BoatLocation(messageVersionNumber, bytesToLong(time), + bytesToInt(sourceID), bytesToInt(seqNum), + deviceType, bytesToInt(latitude), + bytesToInt(longitude), bytesToInt(altitude), + bytesToInt(heading), bytesToShort(pitch), + bytesToShort(roll), bytesToInt(boatSpeed), + bytesToInt(cog), bytesToInt(sog), + bytesToInt(apparentWindSpeed), bytesToShort(apparentWindAngle), + bytesToInt(trueWindSpeed), bytesToShort(trueWindDirection), + bytesToShort(trueWindAngle), bytesToInt(currentDrift), + bytesToInt(currentSet), bytesToShort(rudderAngle) + );*/ + +// System.out.println(bytesToInt(sourceID)); +// System.out.println(bytesToInt(boatSpeed)); + } + + + public BoatLocation getMessage() { + return message; + } +} diff --git a/racevisionGame/src/main/java/network/MessageDecoders/CourseWindDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/CourseWindDecoder.java new file mode 100644 index 00000000..eebb8dc6 --- /dev/null +++ b/racevisionGame/src/main/java/network/MessageDecoders/CourseWindDecoder.java @@ -0,0 +1,61 @@ +package network.MessageDecoders; + +import seng302.Networking.Messages.CourseWind; + +import java.util.ArrayList; +import java.util.Arrays; + +import static seng302.Networking.Utils.ByteConverter.*; + +/** + * Created by hba56 on 23/04/17. + */ +public class CourseWindDecoder { + byte messageVersionNumber; + byte byteWindID; + byte loopCount; + ArrayList loopMessages = new ArrayList(); + + public CourseWindDecoder(byte[] encodedCourseWind) { + final int lengthInBytesOfMessages = 20; + + messageVersionNumber = encodedCourseWind[0]; + byteWindID = encodedCourseWind[1]; + loopCount = encodedCourseWind[2]; + byte[] loopMessagesBytes = Arrays.copyOfRange(encodedCourseWind, 3, lengthInBytesOfMessages*loopCount+3); + int messageLoopIndex = 0; + + for (int i=0; i < loopCount; i++) { + byte[] messageBytes = Arrays.copyOfRange(loopMessagesBytes, messageLoopIndex, messageLoopIndex+20); + ArrayList test = new ArrayList(); + byte[] windId = Arrays.copyOfRange(messageBytes, 0, 1); + byte[] time = Arrays.copyOfRange(messageBytes, 1, 7); + byte[] raceID = Arrays.copyOfRange(messageBytes, 7, 11); + byte[] windDirection = Arrays.copyOfRange(messageBytes, 11, 13); + byte[] windSpeed = Arrays.copyOfRange(messageBytes, 13, 15); + byte[] bestUpwindAngle = Arrays.copyOfRange(messageBytes, 15, 17); + byte[] bestDownwindAngle = Arrays.copyOfRange(messageBytes, 17, 19); + byte[] flags = Arrays.copyOfRange(messageBytes, 19, 20); + + CourseWind message = new CourseWind(windId[0], bytesToLong(time), + bytesToInt(raceID), bytesToInt(windDirection), + bytesToInt(windSpeed), bytesToInt(bestUpwindAngle), + bytesToInt(bestDownwindAngle), flags[0]); + + loopMessages.add(message); + messageLoopIndex += 20; + } + } + + public ArrayList getLoopMessages() { + return loopMessages; + } + + public byte getMessageVersionNumber() { + return messageVersionNumber; + } + + public byte getByteWindID() { + return byteWindID; + } +} diff --git a/racevisionGame/src/main/java/network/MessageDecoders/MarkRoundingDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/MarkRoundingDecoder.java new file mode 100644 index 00000000..2dccdf77 --- /dev/null +++ b/racevisionGame/src/main/java/network/MessageDecoders/MarkRoundingDecoder.java @@ -0,0 +1,51 @@ +package network.MessageDecoders; + +import seng302.Networking.Messages.MarkRounding; +import seng302.Networking.Utils.ByteConverter; + +import java.util.Arrays; + +/** + * Created by hba56 on 23/04/17. + */ +public class MarkRoundingDecoder { + byte messageVersionNumber; + byte[] byteTime; + byte[] byteAck; + byte[] byteRaceID; + byte[] byteSourceID; + byte byteBoatStatus; + byte byteRoundingSide; + byte byteMarkType; + byte byteMarkID; + + MarkRounding markRounding; + + public MarkRoundingDecoder(byte[] encodedMarkRounding) { + messageVersionNumber = encodedMarkRounding[0]; + byteTime = Arrays.copyOfRange(encodedMarkRounding, 1, 7); + byteAck = Arrays.copyOfRange(encodedMarkRounding, 7, 9); + byteRaceID = Arrays.copyOfRange(encodedMarkRounding, 9, 13); + byteSourceID = Arrays.copyOfRange(encodedMarkRounding, 13, 17); + byteBoatStatus = encodedMarkRounding[17]; + byteRoundingSide = encodedMarkRounding[18]; + byteMarkType = encodedMarkRounding[19]; + byteMarkID = encodedMarkRounding[20]; + + int intMsgVer = ByteConverter.bytesToInt(messageVersionNumber); + long lngTime = ByteConverter.bytesToLong(byteTime); + int intAck = ByteConverter.bytesToInt(byteAck); + int intRaceID = ByteConverter.bytesToInt(byteRaceID); + int intSourceID = ByteConverter.bytesToInt(byteSourceID); + int intBoatState = ByteConverter.bytesToInt(byteBoatStatus); + int intRoundingSide = ByteConverter.bytesToInt(byteRoundingSide); + int intMarkType = ByteConverter.bytesToInt(byteMarkType); + int intMarkID = ByteConverter.bytesToInt(byteMarkID); + + markRounding = new MarkRounding(intMsgVer, lngTime, intAck, intRaceID, intSourceID, intBoatState, intRoundingSide, intMarkType, intMarkID); + } + + public MarkRounding getMarkRounding() { + return markRounding; + } +} diff --git a/racevisionGame/src/main/java/network/MessageDecoders/RaceStartStatusDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/RaceStartStatusDecoder.java new file mode 100644 index 00000000..fc922503 --- /dev/null +++ b/racevisionGame/src/main/java/network/MessageDecoders/RaceStartStatusDecoder.java @@ -0,0 +1,65 @@ +package network.MessageDecoders; + + +import java.util.Arrays; + +import static seng302.Networking.Utils.ByteConverter.*; + +/** + * Created by hba56 on 21/04/17. + */ +public class RaceStartStatusDecoder { + private byte messageVersion; + private byte[] timestamp; + private byte[] ackNumber; + private byte[] raceStartTime; + private byte[] raceIdentifier; + private byte notificationType; + + private long time; + private short ack; + private long startTime; + private int raceID; + private char notification; + + + public RaceStartStatusDecoder(byte[] encodedRaceStartStatus) { + messageVersion = encodedRaceStartStatus[0]; + timestamp = Arrays.copyOfRange(encodedRaceStartStatus, 1, 7); + ackNumber = Arrays.copyOfRange(encodedRaceStartStatus, 7, 9); + raceStartTime = Arrays.copyOfRange(encodedRaceStartStatus, 9, 15); + raceIdentifier = Arrays.copyOfRange(encodedRaceStartStatus, 15, 19); + notificationType = encodedRaceStartStatus[19]; + + time = bytesToLong(timestamp); + ack = bytesToShort(ackNumber); + startTime = bytesToLong(raceStartTime); + raceID = bytesToInt(raceIdentifier); + notification = bytesToChar(notificationType); + } + + + public byte getMessageVersion() { + return messageVersion; + } + + public long getTime() { + return time; + } + + public short getAck() { + return ack; + } + + public long getStartTime() { + return startTime; + } + + public int getRaceID() { + return raceID; + } + + public char getNotification() { + return notification; + } +} diff --git a/racevisionGame/src/main/java/network/MessageDecoders/RaceStatusDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/RaceStatusDecoder.java new file mode 100644 index 00000000..d0b05f95 --- /dev/null +++ b/racevisionGame/src/main/java/network/MessageDecoders/RaceStatusDecoder.java @@ -0,0 +1,117 @@ +package network.MessageDecoders; + +import seng302.Networking.Messages.BoatStatus; + +import java.util.ArrayList; +import java.util.Arrays; + +import static seng302.Networking.Utils.ByteConverter.*; + +/** + * Created by hba56 on 21/04/17. + */ +public class RaceStatusDecoder { + private byte versionNum; + private byte[] timeBytes; + private byte[] raceID; + private byte raceStatus; + private byte[] expectedStart; + private byte[] raceWind; + private byte[] windSpeed; + private byte numBoats; + private byte bytesRaceType; + private byte[] boatsBytes; + + private long time; + private int race; + private int raceState; + private long startTime; + private int raceWindDir; + private short raceWindSpeed; + private int numberOfBoats; + private int raceType; + private ArrayList boats = new ArrayList<>(); + + + public RaceStatusDecoder(byte[] encodedRaceStatus){ + versionNum = encodedRaceStatus[0]; + timeBytes = Arrays.copyOfRange(encodedRaceStatus, 1, 7); + raceID = Arrays.copyOfRange(encodedRaceStatus, 7, 11); + raceStatus = encodedRaceStatus[11]; + expectedStart = Arrays.copyOfRange(encodedRaceStatus, 12, 18); + raceWind = Arrays.copyOfRange(encodedRaceStatus, 18, 20); + windSpeed = Arrays.copyOfRange(encodedRaceStatus, 20, 22); + numBoats = encodedRaceStatus[22]; + bytesRaceType = encodedRaceStatus[23]; + boatsBytes = Arrays.copyOfRange(encodedRaceStatus, 24, 25+20*this.numBoats); + + time = bytesToLong(timeBytes); + race = bytesToInt(raceID); + raceState = bytesToInt(raceStatus); + startTime = bytesToLong(expectedStart); + raceWindDir = bytesToInt(raceWind); + raceWindSpeed = bytesToShort(windSpeed); + numberOfBoats = bytesToInt(numBoats); + + int boatLoopIndex = 0; + + for (int i=0; i < numberOfBoats; i++) { + byte[] boatBytes = Arrays.copyOfRange(boatsBytes, boatLoopIndex, boatLoopIndex+20); + + byte[] sourceID = Arrays.copyOfRange(boatBytes, 0, 3); + byte boatStatus = boatBytes[4]; + byte legNumber = boatBytes[5]; + byte numPenaltiesAwarded = boatBytes[6]; + byte numPenaltiesServed = boatBytes[7]; + byte[] estTimeAtNextMark = Arrays.copyOfRange(boatBytes, 8, 14); + byte[] estTimeAtFinish = Arrays.copyOfRange(boatBytes, 14, 20); + + BoatStatus boat = new BoatStatus(bytesToInt(sourceID),boatStatus, + legNumber, numPenaltiesAwarded, numPenaltiesServed, + bytesToLong(estTimeAtNextMark), bytesToLong(estTimeAtFinish)); + + boats.add(boat); + boatLoopIndex += 20; + } + } + + public byte getVersionNum() { + return versionNum; + } + + public long getTime() { + return time; + } + + public int getRace() { + return race; + } + + public int getRaceState() { + return raceState; + } + + public long getStartTime() { + return startTime; + } + + public int getRaceWindDir() { + return raceWindDir; + } + + public short getRaceWindSpeed() { + return raceWindSpeed; + } + + public int getNumberOfBoats() { + return numberOfBoats; + } + + public int getRaceType() { + return raceType; + } + + public ArrayList getBoats() { + return boats; + } +} diff --git a/racevisionGame/src/main/java/network/MessageDecoders/XMLMessageDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/XMLMessageDecoder.java new file mode 100644 index 00000000..2894599a --- /dev/null +++ b/racevisionGame/src/main/java/network/MessageDecoders/XMLMessageDecoder.java @@ -0,0 +1,81 @@ +package network.MessageDecoders; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static seng302.Networking.Utils.ByteConverter.bytesToLong; +import static seng302.Networking.Utils.ByteConverter.bytesToShort; + +/** + * Created by hba56 on 20/04/17. + */ +public class XMLMessageDecoder { + private byte messageVersionNumber; + private short ackNumber; + private long timeStamp; + private byte xmlMsgSubType; + private short sequenceNumber; + private short xmlMsgLength; + private String xmlMessage; + + private byte[] bytes; + + public XMLMessageDecoder(byte[] bytes) { + this.bytes = bytes; + } + + public void decode(){ + byte[] ackNumberBytes = Arrays.copyOfRange(bytes, 1, 3); + byte[] timeStampBytes = Arrays.copyOfRange(bytes, 3, 9); + byte[] sequenceNumberBytes = Arrays.copyOfRange(bytes, 10, 12); + byte[] xmlMsgLengthBytes = Arrays.copyOfRange(bytes, 12, 14); + byte[] xmlMessagebytes = Arrays.copyOfRange(bytes, 14, bytes.length); + + this.xmlMsgSubType = bytes[9]; + this.messageVersionNumber = bytes[0]; + this.ackNumber = bytesToShort(ackNumberBytes); + + this.timeStamp = bytesToLong(timeStampBytes); + + this.sequenceNumber = bytesToShort(sequenceNumberBytes); + this.xmlMsgLength = bytesToShort(xmlMsgLengthBytes); + this.xmlMessage = new String(xmlMessagebytes); + } + + public byte getMessageVersionNumber() { + return messageVersionNumber; + } + + public short getAckNumber() { + return ackNumber; + } + + public long getTimeStamp() { + return timeStamp; + } + + public byte getXmlMsgSubType() { + return xmlMsgSubType; + } + + public short getSequenceNumber() { + return sequenceNumber; + } + + public short getXmlMsgLength() { + return xmlMsgLength; + } + + /** + * this will be used latter for the vis + * @return xml string as inputsource + */ + public InputStream getXmlMessageInputStream() { + InputStream is = new ByteArrayInputStream(xmlMessage.trim().getBytes(StandardCharsets.UTF_8)); +// InputSource is = new InputSource(new StringReader(xmlMessage.trim())); + return is; + } + +} diff --git a/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java new file mode 100644 index 00000000..1a1f2b49 --- /dev/null +++ b/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java @@ -0,0 +1,296 @@ +package network.MessageEncoders; + + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import static seng302.Networking.Utils.ByteConverter.*; + +/** + * Created by fwy13 on 19/04/17. + */ +public class RaceVisionByteEncoder { + + /** + * Serializes a heartbeat message. + * @param seq Heartbeat value. + * @return Serialized message. + */ + public static byte[] heartBeat(long seq){ + ByteBuffer heartBeat = ByteBuffer.allocate(4); + heartBeat.put(longToBytes(seq, 4)); + byte [] result = heartBeat.array(); + return result; + } + + /** + * Serializes a RaceStatus message. + * @param raceStatus Message to serialize. + * @return Serialized (byte array) message, ready to be written to a socket. + */ + public static byte[] raceStatus(RaceStatus raceStatus){ + + List boatStatuses = raceStatus.getBoatStatuses(); + + ByteBuffer raceStatusMessage = ByteBuffer.allocate(24 + 20* boatStatuses.size()); + //Version Number 1 bytes + byte versionNum = 0b10; //this changes with the pdf. (2) + byte[] timeBytes = longToBytes(raceStatus.getCurrentTime(), 6);//time (6 bytes) + byte[] raceID = ByteBuffer.allocate(4).put(intToBytes(raceStatus.getRaceID())).array();//race identifier incase multiple races are going at once. + byte[] raceStatusByte = intToBytes(raceStatus.getRaceStatus(), 1);//race status 0 - 10 + byte[] expectedStart = longToBytes(raceStatus.getExpectedStartTime(), 6);//number of milliseconds from Jan 1, 1970 for when the data is valid + byte[] raceWind = ByteBuffer.allocate(2).put(intToBytes(raceStatus.getWindDirection(), 2)).array();//North = 0x0000 East = 0x4000 South = 0x8000. + byte[] windSpeed = ByteBuffer.allocate(2).put(intToBytes(raceStatus.getWindSpeed(), 2)).array();//mm/sec + byte[] numBoats = intToBytes(boatStatuses.size(), 1); + byte[] bytesRaceType = intToBytes(raceStatus.getRaceType(), 1);//1 match race, 2 fleet race + + raceStatusMessage.put(versionNum); + raceStatusMessage.put(timeBytes); + raceStatusMessage.put(raceID); + raceStatusMessage.put(raceStatusByte); + raceStatusMessage.put(expectedStart); + raceStatusMessage.put(raceWind); + raceStatusMessage.put(windSpeed); + raceStatusMessage.put(numBoats); + raceStatusMessage.put(bytesRaceType); + + for (int i = 0; i < boatStatuses.size(); i++){ + byte[] sourceID = intToBytes(boatStatuses.get(i).getSourceID()); + byte[] boatStatus = intToBytes(boatStatuses.get(i).getBoatStatus(), 1); + byte[] legNum = intToBytes(boatStatuses.get(i).getLegNumber(), 1); + byte[] numPenalties = intToBytes(boatStatuses.get(i).getNumPenaltiesAwarded(), 1); + byte[] numPenaltiesServed = intToBytes(boatStatuses.get(i).getNumPenaltiesServed(), 1); + byte[] estNextMarkTime = longToBytes(boatStatuses.get(i).getEstTimeAtNextMark(), 6); + byte[] estFinishTime = longToBytes( boatStatuses.get(i).getEstTimeAtFinish(), 6); + + raceStatusMessage.put(sourceID); + raceStatusMessage.put(boatStatus); + raceStatusMessage.put(legNum); + raceStatusMessage.put(numPenalties); + raceStatusMessage.put(numPenaltiesServed); + raceStatusMessage.put(estNextMarkTime); + raceStatusMessage.put(estFinishTime); + } + + return raceStatusMessage.array(); + } + + public byte[] displayTextMessage(RaceMessage[] message){ + //ByteBuffer result = ByteBuffer.allocate(4 + numLines * 32); + int messageVersionNumber = 0b1;//version number + short ackNum = 0;//no clue what this does just a placeholder for 2 bytes. + byte[] messLines = intToBytes(message.length, 1); + +// result.putInt(messageVersionNumber); +// result.putShort(ackNum); +// result.put(messLines); + + ArrayList messages = new ArrayList(); + int size = 4; + + for (int i = 0; i < message.length; i ++){ + int messageLen = message[i].getMessageText().getBytes().length; + byte[] messageAsBytes = message[i].getMessageText().getBytes(); + if (messageLen < 30){ + messageLen = 30; + } + ByteBuffer mess = ByteBuffer.allocate(2 + messageLen); + mess.put(intToBytes(message[i].getLineNumber(), 1)); + mess.put(intToBytes(messageLen, 1)); + for (int j = 0; j < messageLen; j ++){ + mess.put(messageAsBytes[j]); + } + messages.add(mess.array()); + size += 2 + messageLen; + } + + ByteBuffer result = ByteBuffer.allocate(size); + result.put(intToBytes(messageVersionNumber, 1)); + result.putShort(ackNum); + result.put(messLines); + + for(byte[] mess: messages){ + result.put(mess); + } + + return result.array(); + } + + public byte[] raceStartStatus(long time, short ack, long startTime, int raceID, char notification){ + int messageVersion = 0b1; + byte[] timestamp = longToBytes(time, 6); + byte[] ackNumber = intToBytes(ack, 2); + byte[] raceStartTime = longToBytes(startTime, 6); + int raceIdentifier = raceID; + byte[] notificationType = intToBytes(notification, 1); + + ByteBuffer result = ByteBuffer.allocate(20); + result.put(intToBytes(messageVersion, 1)); + result.put(timestamp); + result.put(ackNumber); + result.put(raceStartTime); + result.put(intToBytes(raceIdentifier)); + result.put(notificationType); + + return result.array(); + } + + public byte[] yachtEventCode(long time, short acknowledgeNumber, int raceID, int destSourceID, int incidentID, + int eventID){ + int messageVersion = 0b10; + byte[] encodeTime = longToBytes(time, 6); + short ackNum = acknowledgeNumber; + int raceUID = raceID;//TODO chekc if this is an into for a 4 char string. + int destSource = destSourceID; + int incident = incidentID; + byte[] event = intToBytes(eventID, 1); + + ByteBuffer result = ByteBuffer.allocate(22); + result.put(intToBytes(messageVersion, 1)); + result.put(encodeTime); + result.putShort(ackNum); + result.put(intToBytes(raceUID)); + result.put(intToBytes(destSource)); + result.put(intToBytes(incident)); + result.put(event); + return result.array(); + } + + public byte[] chatterText(int messageType, String message){ + int messageVersion = 0b1; + byte[] type = intToBytes(messageType, 1); + byte[] text = message.getBytes(); + byte[] length = intToBytes(text.length, 1); + + ByteBuffer result = ByteBuffer.allocate(3 + text.length); + result.put(intToBytes(messageVersion, 1)); + result.put(type); + result.put(length); + result.put(text); + + return result.array(); + } + + public static byte[] boatLocation(BoatLocation boatLocation){ + int messageVersionNumber = 0b1; + byte[] time = longToBytes(boatLocation.getTime(), 6); + byte[] sourceID = intToBytes(boatLocation.getSourceID(), 4); + byte[] seqNum = longToBytes(boatLocation.getSequenceNumber(), 4); + byte[] deviceType = intToBytes(boatLocation.getDeviceType(), 1); + byte[] latitude = intToBytes(boatLocation.getLatitude(), 4); + byte[] longitude = intToBytes(boatLocation.getLongitude(), 4); + byte[] altitude = intToBytes(boatLocation.getAltitude(), 4); + byte[] heading = intToBytes(boatLocation.getHeading(), 2); + byte[] pitch = intToBytes(boatLocation.getPitch(), 2); + byte[] roll = intToBytes(boatLocation.getRoll(), 2); + byte[] boatSpeed = intToBytes(boatLocation.getBoatSpeed(), 2); + byte[] cog = intToBytes(boatLocation.getBoatCOG(), 2); + byte[] sog = intToBytes(boatLocation.getBoatSOG(), 2); + byte[] apparentWindSpeed = intToBytes(boatLocation.getApparentWindSpeed(), 2); + byte[] apparentWindAngle = intToBytes(boatLocation.getApparentWindAngle(), 2); + byte[] trueWindSpeed = intToBytes(boatLocation.getTrueWindSpeed(), 2); + byte[] trueWindDirection = intToBytes(boatLocation.getTrueWindDirection(), 2); + byte[] trueWindAngle = intToBytes(boatLocation.getTrueWindAngle(), 2); + byte[] currentDrift = intToBytes(boatLocation.getCurrentDrift(), 2); + byte[] currentSet = intToBytes(boatLocation.getCurrentSet(), 2); + byte[] rudderAngle = intToBytes(boatLocation.getRudderAngle(), 2); + + ByteBuffer result = ByteBuffer.allocate(56); + result.put(intToBytes(messageVersionNumber, 1)); + result.put(time); + result.put(sourceID); + result.put(seqNum); + result.put(deviceType); + result.put(latitude); + result.put(longitude); + result.put(altitude); + result.put(heading); + result.put(pitch); + result.put(roll); + result.put(boatSpeed); + result.put(cog); + result.put(sog); + result.put(apparentWindSpeed); + result.put(apparentWindAngle); + result.put(trueWindSpeed); + result.put(trueWindDirection); + result.put(trueWindAngle); + result.put(currentDrift); + result.put(currentSet); + result.put(rudderAngle); + return result.array(); + } + + public byte[] markRounding(int time, int ackNumber, int raceID, int sourceID, int boatStatus, int roundingSide, int markType, int markID){ + int messageVersionNumber = 0b1; + byte[] byteTime = longToBytes(time, 6); + byte[] byteAck = intToBytes(ackNumber, 2); + byte[] byteRaceID = intToBytes(raceID, 4); + byte[] byteSourceID = intToBytes(sourceID, 4); + byte[] byteBoatStatus = intToBytes(boatStatus, 1); + byte[] byteRoundingSide = intToBytes(roundingSide, 1); + byte[] byteMarkType = intToBytes(markType, 1); + byte[] byteMarkID = intToBytes(markID, 1); + + ByteBuffer result = ByteBuffer.allocate(21); + result.put(intToBytes(messageVersionNumber, 1)); + result.put(byteTime); + result.put(byteAck); + result.put(byteRaceID); + result.put(byteSourceID); + result.put(byteBoatStatus); + result.put(byteRoundingSide); + result.put(byteMarkType); + result.put(byteMarkID); + return result.array(); + } + + public byte[] courseWind(byte windID, ArrayList courseWinds){ + int messageVersionNumber = 0b1; + byte byteWindID = windID; + byte[] loopcount = intToBytes(courseWinds.size(), 1); + ByteBuffer result = ByteBuffer.allocate(3 + 20 * courseWinds.size()); + result.put(intToBytes(messageVersionNumber, 1)); + result.put(byteWindID); + result.put(loopcount); + for (CourseWind wind: courseWinds){ + result.put(intToBytes(wind.getID(), 1)); + result.put(longToBytes(wind.getTime(), 6)); + result.put(intToBytes(wind.getRaceID(), 4)); + result.put(intToBytes(wind.getWindDirection(), 2)); + result.put(intToBytes(wind.getWindSpeed(), 2)); + result.put(intToBytes(wind.getBestUpwindAngle(), 2)); + result.put(intToBytes(wind.getBestDownwindAngle(), 2)); + result.put(intToBytes(wind.getFlags(), 1)); + } + return result.array(); + } + + public byte[] averageWind(int time, int rawPeriod, int rawSampleSpeed, int period2, int speed2, int period3, int speed3, int period4, int speed4){ + int messageVersionNumber = 0b1; + byte[] byteTime = longToBytes(time,6); + byte[] byteRawPeriod = intToBytes(rawPeriod, 2); + byte[] byteRawSpeed = intToBytes(rawSampleSpeed, 2); + byte[] bytePeriod2 = intToBytes(period2, 2); + byte[] byteSpeed2 = intToBytes(speed2, 2); + byte[] bytePeriod3 = intToBytes(period3, 2); + byte[] byteSpeed3 = intToBytes(speed3, 2); + byte[] bytePeriod4 = intToBytes(period4, 2); + byte[] byteSpeed4 = intToBytes(speed4, 2); + + ByteBuffer result = ByteBuffer.allocate(23); + result.put(intToBytes(messageVersionNumber, 1)); + result.put(byteTime); + result.put(byteRawPeriod); + result.put(byteRawSpeed); + result.put(bytePeriod2); + result.put(byteSpeed2); + result.put(bytePeriod3); + result.put(byteSpeed3); + result.put(bytePeriod4); + result.put(byteSpeed4); + return result.array(); + } + +} diff --git a/racevisionGame/src/main/java/network/MessageEncoders/XMLMessageEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/XMLMessageEncoder.java new file mode 100644 index 00000000..43c88a61 --- /dev/null +++ b/racevisionGame/src/main/java/network/MessageEncoders/XMLMessageEncoder.java @@ -0,0 +1,58 @@ +package network.MessageEncoders; + +import java.nio.ByteBuffer; + +import static seng302.Networking.Utils.ByteConverter.*; + +/** + * Encodes a XML file into a message of AC35 format + */ +public class XMLMessageEncoder { + private byte[] messageVersionNumber; + private short ackNumber; + private long timeStamp; + private byte[] xmlMsgSubType; + private short sequenceNumber; + private short xmlMsgLength; + private String xmlMessage; + + public XMLMessageEncoder(short ackNumber, long timeStamp, int xmlMsgSubType, short sequenceNumber, short xmlMsgLength, String xmlMessage) { + this.messageVersionNumber = intToBytes(1, 1); + this.ackNumber = ackNumber; + this.timeStamp = timeStamp; + this.xmlMsgSubType = intToBytes(xmlMsgSubType, 1); + this.sequenceNumber = sequenceNumber; + this.xmlMsgLength = xmlMsgLength; + this.xmlMessage = xmlMessage; + } + + public byte[] encode() { + byte[] messageBytes = xmlMessage.getBytes(); + if (messageBytes.length > this.xmlMsgLength) { + //System.err.println("Xml message is to big"); + return null; + } + ByteBuffer tempOutputByteBuffer = ByteBuffer.allocate(14 + messageBytes.length); + + //ackNumber converted to bytes + byte[] ackNumberBytes = shortToBytes(ackNumber, 2); + + //sequenceNumber converted to bytes + byte[] sequenceNumberBytes = shortToBytes(sequenceNumber, 2); + + //xmlMsgLength converted to bytes + byte[] xmlMsgLengthBytes = shortToBytes(xmlMsgLength, 2); + + + tempOutputByteBuffer.put(messageVersionNumber); + tempOutputByteBuffer.put(ackNumberBytes); + tempOutputByteBuffer.put(longToBytes(timeStamp, 6)); + tempOutputByteBuffer.put(xmlMsgSubType); + tempOutputByteBuffer.put(sequenceNumberBytes); + tempOutputByteBuffer.put(xmlMsgLengthBytes); + tempOutputByteBuffer.put(messageBytes); + + return tempOutputByteBuffer.array(); + } + +} diff --git a/racevisionGame/src/main/java/network/Messages/AC35Data.java b/racevisionGame/src/main/java/network/Messages/AC35Data.java new file mode 100644 index 00000000..0b08b32a --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/AC35Data.java @@ -0,0 +1,31 @@ +package network.Messages; + + +import seng302.Networking.Messages.Enums.MessageType; + +/** + * The base class for all message types. + */ +public abstract class AC35Data { + + ///Message type from the header. + private MessageType type; + + + /** + * Ctor. + * @param type The concrete type of this message. + */ + public AC35Data (MessageType type){ + this.type = type; + } + + + /** + * The concrete type of message this is. + * @return The type of message this is. + */ + public MessageType getType() { + return type; + } +} diff --git a/racevisionGame/src/main/java/network/Messages/AverageWind.java b/racevisionGame/src/main/java/network/Messages/AverageWind.java new file mode 100644 index 00000000..7cc420fb --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/AverageWind.java @@ -0,0 +1,35 @@ +package network.Messages; + +import seng302.Networking.Messages.Enums.MessageType; + +/** + * Created by fwy13 on 25/04/17. + */ +public class AverageWind extends AC35Data { + + private int msgNum; + private long lngTime; + private int rawPeriod; + private int rawSpeed; + private int period2; + private int speed2; + private int period3; + private int speed3; + private int period4; + private int speed4; + + public AverageWind(int msgNum, long lngTime, int rawPeriod, int rawSpeed, int period2, int speed2, int period3, int speed3, int period4, int speed4){ + super(MessageType.AVGWIND); + this.msgNum = msgNum; + this.lngTime = lngTime; + this.rawPeriod = rawPeriod; + this.rawSpeed = rawSpeed; + this.period2 = period2; + this.speed2 = speed2; + this.period3 = period3; + this.speed3 = speed3; + this.period4 = period4; + this.speed4 = speed4; + } + +} diff --git a/racevisionGame/src/main/java/network/Messages/BoatLocation.java b/racevisionGame/src/main/java/network/Messages/BoatLocation.java new file mode 100644 index 00000000..eaa23df5 --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/BoatLocation.java @@ -0,0 +1,557 @@ +package network.Messages; + +import seng302.Networking.Messages.Enums.MessageType; +import seng302.Networking.Utils.AC35UnitConverter; + +import static seng302.Networking.Utils.AC35UnitConverter.convertGPS; +import static seng302.Networking.Utils.AC35UnitConverter.convertGPSToInt; + +/** + * Represents the information in a boat location message (AC streaming spec: 4.9). + */ +public class BoatLocation extends AC35Data { + + //Knots x this = meters per second. + public static final double KnotsToMetersPerSecondConversionFactor = + 0.514444; + public static final byte Unknown = 0; + public static final byte RacingYacht = 1; + public static final byte CommitteeBoat = 2; + public static final byte Mark = 3; + public static final byte Pin = 4; + public static final byte ChaseBoat = 5; + public static final byte MedicalBoat = 6; + public static final byte MarshallBoat = 7; + public static final byte UmpireBoat = 8; + public static final byte UmpireSoftwareApplication = 9; + public static final byte PrincipalRaceOfficerApplication = 10; + public static final byte WeatherStation = 11; + public static final byte Helicopter = 12; + public static final byte DataProcessingApplication = 13; + ///Version number of the message - is always 1. + private byte messageVersionNumber = 1; + ///Time of the event - milliseconds since jan 1 1970. Proper type is 6 byte int. + private long time; + ///Source ID of the boat. + private int sourceID; + ///Sequence number of the message. + private long sequenceNumber; + ///Device type of the message (physical source of the message). + private byte deviceType; + ///Latitude of the boat. + private int latitude; + + ///Longitude of the boat. + private int longitude; + + ///Altitude of the boat. + private int altitude; + + ///Heading of the boat. Clockwise, 0 = north. Proper type is unsigned 2 byte int. + private int heading; + + ///Pitch of the boat. + private short pitch; + + ///Roll of the boat. + private short roll; + + ///Speed of the boat. Proper type is unsigned 2 byte int. millimeters per second. + private int boatSpeed; + + ///Course over ground (COG) of the boat. Proper type is unsigned 2 byte int. + private int boatCOG; + + ///Speed over ground (SOG) of the boat. Proper type is unsigned 2 byte int. millimeters per second. + private int boatSOG; + + ///Apparent wind speed at time of event. Proper type is unsigned 2 byte int. millimeters per second. + private int apparentWindSpeed; + + ///Apparent wind angle at time of the event. Wind over starboard = positive. + private short apparentWindAngle; + + ///True wind speed. Proper type is unsigned 2 byte int. millimeters per second. + private int trueWindSpeed; + + ///True wind direction. Proper type is unsigned 2 byte int. 0x0000 = North, etc.. + private int trueWindDirection; + + ///True wind angle. Clockwise compass direction, 0 = north. + private short trueWindAngle; + + ///Current drift. Proper type is unsigned 2 byte int. millimeters per second. + private int currentDrift; + + ///Current set. Proper type is unsigned 2 byte int. Clockwise compass direction, 0 = north. + private int currentSet; + + ///Rudder angle. Positive is rudder set to turn yacht to port. + private short rudderAngle; + + + /** + * Ctor. Default. + */ + public BoatLocation() { + super(MessageType.BOATLOCATION); + } + + /** + * Ctor, with all parameters. + * + * @param messageVersionNumber message number + * @param time time of message + * @param sourceID id of boat + * @param sequenceNumber number of boat message + * @param deviceType type of boat + * @param latitude lat of boat + * @param longitude lon of boat + * @param altitude altitude of boat + * @param heading heading of boat + * @param pitch pitch of boat + * @param roll roll of boat + * @param boatSpeed boats speed + * @param boatCOG boat cog + * @param boatSOG boat sog + * @param apparentWindSpeed wind speed + * @param apparentWindAngle wind angle + * @param trueWindSpeed true wind speed + * @param trueWindDirection true wind direction + * @param trueWindAngle true wind angle + * @param currentDrift current drift + * @param currentSet current set + * @param rudderAngle rudder angle + */ + public BoatLocation(byte messageVersionNumber, long time, int sourceID, long sequenceNumber, byte deviceType, int latitude, int longitude, int altitude, int heading, short pitch, short roll, int boatSpeed, int boatCOG, int boatSOG, int apparentWindSpeed, short apparentWindAngle, int trueWindSpeed, int trueWindDirection, short trueWindAngle, int currentDrift, int currentSet, short rudderAngle) { + super(MessageType.BOATLOCATION); + + this.messageVersionNumber = messageVersionNumber; + this.time = time; + this.sourceID = sourceID; + this.sequenceNumber = sequenceNumber; + this.deviceType = deviceType; + this.latitude = latitude; + this.longitude = longitude; + this.altitude = altitude; + this.heading = heading; + this.pitch = pitch; + this.roll = roll; + this.boatSpeed = boatSpeed; + this.boatCOG = boatCOG; + this.boatSOG = boatSOG; + this.apparentWindSpeed = apparentWindSpeed; + this.apparentWindAngle = apparentWindAngle; + this.trueWindSpeed = trueWindSpeed; + this.trueWindDirection = trueWindDirection; + this.trueWindAngle = trueWindAngle; + this.currentDrift = currentDrift; + this.currentSet = currentSet; + this.rudderAngle = rudderAngle; + } + + public BoatLocation(int sourceID, double lat, double lon, long sequenceNumber, double heading, double boatSpeed, long time) { + super(MessageType.BOATLOCATION); + + this.messageVersionNumber = (byte) 1; + this.time = time; + this.sourceID = sourceID; + this.sequenceNumber = sequenceNumber; + this.deviceType = 1; + this.latitude = convertGPSToInt(lat); + this.longitude = convertGPSToInt(lon); + this.altitude = 0; + this.heading = convertHeadingDoubleToInt(heading); + this.pitch = 0; + this.roll = 0; + this.boatSpeed = convertBoatSpeedDoubleToInt(boatSpeed); + this.boatCOG = 0; + this.boatSOG = convertBoatSpeedDoubleToInt(boatSpeed); + this.apparentWindSpeed = 0; + this.apparentWindAngle = 0; + this.trueWindSpeed = 0; + this.trueWindDirection = 0; + this.trueWindAngle = 0; + this.currentDrift = 0; + this.currentSet = 0; + this.rudderAngle = 0; + } + + + //Getters and setters for message properties. + + /** + * Converts a double representing a latitude or longitude coordinate to an int, as required by the streaming spec format. + * + * @param coordinate Latitude or longitude to convert. Double. + * @return int representation of coordinate. + */ + public static int convertCoordinateDoubleToInt(double coordinate) { + int coordinateInt = (int) ((coordinate / 180.0) * 2147483648.0); + + return coordinateInt; + } + + /** + * Converts an int representing a latitude or longitude coordinate to a double, as required by the streaming spec format. + * + * @param coordinate Latitude or longitude to convert. int. + * @return double representation of coordinate. + */ + public static double convertCoordinateIntToDouble(int coordinate) { + double coordinateDouble = (double) ((coordinate * 180.0) / 2147483648.0); + + return coordinateDouble; + } + + /** + * Converts an int representing a heading to a double, as required by the streaming spec format. + * + * @param heading Heading to convert. int. + * @return double representation of heading. + */ + public static double convertHeadingIntToDouble(int heading) { + + double headingDouble = (double) ((heading * 360.0) / 65536.0); + + return headingDouble; + } + + /** + * Converts a double representing a heading to an int, as required by the streaming spec format. + * + * @param heading Heading to convert. double. + * @return int representation of heading. + */ + public static int convertHeadingDoubleToInt(double heading) { + + int headingInt = (int) ((heading * 65536.0) / 360.0); + + return headingInt; + } + + /** + * Converts a short representing the wind's true angle to a double, as required by the streaming spec format. + * + * @param angle Angle to convert. short. + * @return double representation of heading. + */ + public static double convertTrueWindAngleShortToDouble(short angle) { + + double angleDouble = (double) ((angle * 180.0) / 32768.0); + + return angleDouble; + } + + /** + * Converts a double representing the wind's true angle to a short, as required by the streaming spec format. + * + * @param angle Angle to convert. double. + * @return short representation of heading. + */ + public static short convertTrueWindAngleDoubleToShort(double angle) { + + short angleShort = (short) ((angle / 180.0) * 32768.0); + + return angleShort; + } + + /** + * Converts a double representing the speed of a boat in knots to an int in millimeters per second, as required by the streaming spec format. + * + * @param speed Speed in knots, stored as a double. + * @return Speed in millimeters per second, stored as an int (using only the two least significant bytes). + */ + public static int convertBoatSpeedDoubleToInt(double speed) { + //Calculate meters per second. + double metersPerSecond = speed * KnotsToMetersPerSecondConversionFactor; + + //Calculate millimeters per second. + double millimetersPerSecond = metersPerSecond * 1000.0; + + //Convert to an int. + int millimetersPerSecondInt = (int) Math.round(millimetersPerSecond); + + return millimetersPerSecondInt; + } + + /** + * Converts an int representing the speed of a boat in millimeters per second to a double in knots, as required by the streaming spec format. + * + * @param speed Speed in millimeters per second, stored as an int. + * @return Speed in knots, stored as a double. + */ + public static double convertBoatSpeedIntToDouble(int speed) { + //Calculate meters per second. + double metersPerSecond = speed / 1000.0; + + //Calculate knots. + double knots = metersPerSecond / KnotsToMetersPerSecondConversionFactor; + + return knots; + } + + public byte getMessageVersionNumber() { + return messageVersionNumber; + } + + public void setMessageVersionNumber(byte messageVersionNumber) { + this.messageVersionNumber = messageVersionNumber; + } + + public long getTime() { + return time; + } + + public void setTime(long time) { + this.time = time; + } + + public int getSourceID() { + return sourceID; + } + + public void setSourceID(int sourceID) { + this.sourceID = sourceID; + } + + public long getSequenceNumber() { + return sequenceNumber; + } + + public void setSequenceNumber(long sequenceNumber) { + this.sequenceNumber = sequenceNumber; + } + + public byte getDeviceType() { + return deviceType; + } + + public void setDeviceType(byte deviceType) { + this.deviceType = deviceType; + } + + public int getLatitude() { + return latitude; + } + + public void setLatitude(int latitude) { + this.latitude = latitude; + } + + public int getLongitude() { + return longitude; + } + + public double getLatitudeDouble(){ + return convertGPS(this.latitude); + } + + public double getLongitudeDouble(){ + return convertGPS(this.longitude); + } + + public void setLongitude(int longitude) { + this.longitude = longitude; + } + + public int getAltitude() { + return altitude; + } + + public void setAltitude(int altitude) { + this.altitude = altitude; + } + + public int getHeading() { + return heading; + } + + public void setHeading(int heading) { + this.heading = heading; + } + + public short getPitch() { + return pitch; + } + + public void setPitch(short pitch) { + this.pitch = pitch; + } + + public short getRoll() { + return roll; + } + + public void setRoll(short roll) { + this.roll = roll; + } + + public int getBoatSpeed() { + return boatSpeed; + } + + public void setBoatSpeed(int boatSpeed) { + this.boatSpeed = boatSpeed; + } + + public int getBoatCOG() { + return boatCOG; + } + + public void setBoatCOG(int boatCOG) { + this.boatCOG = boatCOG; + } + + public int getBoatSOG() { + return boatSOG; + } + + public void setBoatSOG(int boatSOG) { + this.boatSOG = boatSOG; + } + + public int getApparentWindSpeed() { + return apparentWindSpeed; + } + + public void setApparentWindSpeed(int apparentWindSpeed) { + this.apparentWindSpeed = apparentWindSpeed; + } + + public short getApparentWindAngle() { + return apparentWindAngle; + } + + public void setApparentWindAngle(short apparentWindAngle) { + this.apparentWindAngle = apparentWindAngle; + } + + public int getTrueWindSpeed() { + return trueWindSpeed; + } + + public void setTrueWindSpeed(int trueWindSpeed) { + this.trueWindSpeed = trueWindSpeed; + } + + public int getTrueWindDirection() + { + return trueWindDirection; + } + + public void setTrueWindDirection(int trueWindDirection) + { + this.trueWindDirection = trueWindDirection; + } + + public short getTrueWindAngle() { + return trueWindAngle; + } + + public void setTrueWindAngle(short trueWindAngle) { + this.trueWindAngle = trueWindAngle; + } + + public int getCurrentDrift() { + return currentDrift; + } + + public void setCurrentDrift(int currentDrift) { + this.currentDrift = currentDrift; + } + + public int getCurrentSet() { + return currentSet; + } + + public void setCurrentSet(int currentSet) { + this.currentSet = currentSet; + } + + public short getRudderAngle() { + return rudderAngle; + } + + public void setRudderAngle(short rudderAngle) { + this.rudderAngle = rudderAngle; + } + + public double getHeadingDegrees(){ + return AC35UnitConverter.convertHeading(getHeading()); + } + + public double getTrueWindAngleDegrees(){ + return AC35UnitConverter.convertTrueWindAngle(getTrueWindAngle()); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + + builder.append("Message version number: "); + builder.append(this.getMessageVersionNumber()); + + builder.append("\nTime: "); + builder.append(this.getTime()); + + builder.append("\nSource ID: "); + builder.append(this.getSourceID()); + + builder.append("\nSequence number: "); + builder.append(this.getSequenceNumber()); + + builder.append("\nDevice type: "); + builder.append(this.getDeviceType()); + + builder.append("\nLatitude: "); + builder.append(this.getLatitude()); + + builder.append("\nLongitude: "); + builder.append(this.getLongitude()); + + builder.append("\nAltitude: "); + builder.append(this.getAltitude()); + + builder.append("\nHeading: "); + builder.append(this.getHeading()); + + builder.append("\nPitch: "); + builder.append(this.getPitch()); + + builder.append("\nRoll: "); + builder.append(this.getRoll()); + + builder.append("\nBoat speed (mm/sec): "); + builder.append(this.getBoatSpeed()); + + builder.append("\nBoat COG: "); + builder.append(this.getBoatCOG()); + + builder.append("\nBoat SOG: "); + builder.append(this.getBoatSOG()); + + builder.append("\nApparent wind speed: "); + builder.append(this.getApparentWindSpeed()); + + builder.append("\nApparent wind angle: "); + builder.append(this.getApparentWindAngle()); + + builder.append("\nTrue wind speed: "); + builder.append(this.getTrueWindSpeed()); + + builder.append("\nTrue wind angle: "); + builder.append(this.getTrueWindAngle()); + + builder.append("\nCurrent drift: "); + builder.append(this.getCurrentDrift()); + + builder.append("\nCurrent set: "); + builder.append(this.getCurrentSet()); + + builder.append("\nRudder angle: "); + builder.append(this.getRudderAngle()); + + return builder.toString(); + } +} diff --git a/racevisionGame/src/main/java/network/Messages/BoatStatus.java b/racevisionGame/src/main/java/network/Messages/BoatStatus.java new file mode 100644 index 00000000..8310f49e --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/BoatStatus.java @@ -0,0 +1,68 @@ +package network.Messages; + +import seng302.Networking.Messages.Enums.BoatStatusEnum; +import seng302.Networking.Utils.ByteConverter; + +/** + * Created by hba56 on 23/04/17. + */ +public class BoatStatus { + + private int sourceID; + private byte boatStatus; + private byte legNumber; + private byte numPenaltiesAwarded; + private byte numPenaltiesServed; + private long estTimeAtNextMark; + private long estTimeAtFinish; + + public BoatStatus(int sourceID, byte boatStatus, byte legNumber, byte numPenaltiesAwarded, byte numPenaltiesServed, long estTimeAtNextMark, long estTimeAtFinish) { + this.sourceID = sourceID; + this.boatStatus = boatStatus; + this.legNumber = legNumber; + this.numPenaltiesAwarded = numPenaltiesAwarded; + this.numPenaltiesServed = numPenaltiesServed; + this.estTimeAtNextMark = estTimeAtNextMark; + this.estTimeAtFinish = estTimeAtFinish; + } + + + public BoatStatus(int sourceID, BoatStatusEnum boatStatusEnum, int legNum, long estTimeAtNextMark) { + this.sourceID = sourceID; + this.boatStatus = boatStatusEnum.getValue(); + this.legNumber = ByteConverter.intToBytes(legNum)[0]; + this.numPenaltiesAwarded = 0; + this.numPenaltiesServed = 0; + this.estTimeAtFinish = 0; + this.estTimeAtNextMark = estTimeAtNextMark; + } + + + public int getSourceID() { + return sourceID; + } + + public byte getBoatStatus() { + return boatStatus; + } + + public byte getLegNumber() { + return legNumber; + } + + public byte getNumPenaltiesAwarded() { + return numPenaltiesAwarded; + } + + public byte getNumPenaltiesServed() { + return numPenaltiesServed; + } + + public long getEstTimeAtNextMark() { + return estTimeAtNextMark; + } + + public long getEstTimeAtFinish() { + return estTimeAtFinish; + } +} diff --git a/racevisionGame/src/main/java/network/Messages/CourseWind.java b/racevisionGame/src/main/java/network/Messages/CourseWind.java new file mode 100644 index 00000000..d159d5c1 --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/CourseWind.java @@ -0,0 +1,57 @@ +package network.Messages; + +import seng302.Networking.Messages.Enums.MessageType; + +/** + * Created by fwy13 on 21/04/17. + */ +public class CourseWind extends AC35Data { + + private int ID, raceID, windDirection, windSpeed, bestUpwindAngle, bestDownwindAngle, flags; + private long time; + + public CourseWind(int ID, long time, int raceID, int windDirection, int windSpeed, int bestUpwindAngle, int bestDownwindAngle, + int flags){ + super(MessageType.COURSEWIND); + this.ID = ID; + this.time = time; + this.raceID = raceID; + this.windDirection = windDirection; + this.windSpeed = windSpeed; + this.bestUpwindAngle = bestUpwindAngle; + this.bestDownwindAngle = bestDownwindAngle; + this.flags = flags; + } + + public int getID() { + return ID; + } + + public int getRaceID() { + return raceID; + } + + public int getWindDirection() { + return windDirection; + } + + public int getWindSpeed() { + return windSpeed; + } + + public int getBestUpwindAngle() { + return bestUpwindAngle; + } + + public int getBestDownwindAngle() { + return bestDownwindAngle; + } + + public int getFlags() { + return flags; + } + + public long getTime() { + return time; + } +} diff --git a/racevisionGame/src/main/java/network/Messages/CourseWinds.java b/racevisionGame/src/main/java/network/Messages/CourseWinds.java new file mode 100644 index 00000000..2ed88970 --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/CourseWinds.java @@ -0,0 +1,23 @@ +package network.Messages; + +import seng302.Networking.Messages.Enums.MessageType; + +import java.util.ArrayList; + +/** + * Created by fwy13 on 25/04/17. + */ +public class CourseWinds extends AC35Data { + + private int msgVerNum; + private int selectedWindID; + private ArrayList courseWinds; + + public CourseWinds(int msgVerNum, int selectedWindID, ArrayList courseWinds){ + super(MessageType.COURSEWIND); + this.msgVerNum = msgVerNum; + this.selectedWindID = selectedWindID; + this.courseWinds = courseWinds; + } + +} diff --git a/racevisionGame/src/main/java/network/Messages/Enums/BoatStatusEnum.java b/racevisionGame/src/main/java/network/Messages/Enums/BoatStatusEnum.java new file mode 100644 index 00000000..f9268101 --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/Enums/BoatStatusEnum.java @@ -0,0 +1,75 @@ +package network.Messages.Enums; + +import java.util.HashMap; +import java.util.Map; + +/** + * Enumeration that encapsulates the various statuses a boat can have. + */ +public enum BoatStatusEnum { + UNDEFINED(0), + PRESTART(1), + RACING(2), + FINISHED(3), + DNS(4), + DNF(5), + DSQ(6), + OCS(7), + NOT_A_STATUS(-1); + + ///Primitive value of the enum. + private byte value; + + + /** + * Ctor. Creates a BoatStatusEnum from a given primitive integer value, cast to a byte. + * @param value Integer, which is cast to byte, to construct from. + */ + private BoatStatusEnum(int value) { + this.value = (byte)value; + } + + /** + * Returns the primitive value of the enum. + * @return Primitive value of the enum. + */ + public byte getValue() { + return value; + } + + + ///Stores a mapping between Byte values and BoatStatusEnum values. + private static final Map byteToStatusMap = new HashMap<>(); + + + /* + Static initialization block. Initializes the byteToStatusMap. + */ + static { + for (BoatStatusEnum type : BoatStatusEnum.values()) { + byteToStatusMap.put(type.value, type); + } + } + + + /** + * Returns the enumeration value which corresponds to a given byte value. + * @param boatStatusByte Byte value to convert to a BoatStatusEnum value. + * @return The BoatStatusEnum value which corresponds to the given byte value. + */ + public static BoatStatusEnum fromByte(byte boatStatusByte) { + //Gets the corresponding MessageType from the map. + BoatStatusEnum type = byteToStatusMap.get(boatStatusByte); + + if (type == null) { + //If the byte value wasn't found, return the NOT_A_STATUS BoatStatusEnum. + return BoatStatusEnum.NOT_A_STATUS; + } + else { + //Otherwise, return the BoatStatusEnum. + return type; + } + + } + +} diff --git a/racevisionGame/src/main/java/network/Messages/Enums/MessageType.java b/racevisionGame/src/main/java/network/Messages/Enums/MessageType.java new file mode 100644 index 00000000..6b8fd775 --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/Enums/MessageType.java @@ -0,0 +1,79 @@ +package network.Messages.Enums; + +import java.util.HashMap; +import java.util.Map; + +/** + * Enumeration that encapsulates the various types of messages that can be sent. + */ +public enum MessageType { + HEARTBEAT(1), + RACESTATUS(12), + DISPLAYTEXTMESSAGE(20), + XMLMESSAGE(26), + RACESTARTSTATUS(27), + YACHTEVENTCODE(29), + YACHTACTIONCODE(31), + CHATTERTEXT(36), + BOATLOCATION(37), + MARKROUNDING(38), + COURSEWIND(44), + AVGWIND(47), + NOTAMESSAGE(0); + + ///Primitive value of the enum. + private byte value; + + /** + * Ctor. Creates a MessageType enum from a given primitive integer value, cast to a byte. + * @param value Integer, which is cast to byte, to construct from. + */ + private MessageType(int value) { + this.value = (byte)value; + } + + /** + * Returns the primitive value of the enum. + * @return Primitive value of the enum. + */ + public byte getValue() { + return value; + } + + + ///Stores a mapping between Byte values and MessageType values. + private static final Map byteToTypeMap = new HashMap<>(); + + + /* + Static initialization block. Initializes the byteToTypeMap. + */ + static { + for (MessageType type : MessageType.values()) { + byteToTypeMap.put(type.value, type); + } + } + + + /** + * Returns the enumeration value which corresponds to a given byte value. + * @param messageTypeByte Byte value to convert to a MessageType value. + * @return The MessageType value which corresponds to the given byte value. + */ + public static MessageType fromByte(byte messageTypeByte) { + //Gets the corresponding MessageType from the map. + MessageType type = byteToTypeMap.get(messageTypeByte); + + if (type == null) { + //If the byte value wasn't found, return the NOTAMESSAGE MessageType. + return MessageType.NOTAMESSAGE; + } + else { + //Otherwise, return the MessageType. + return type; + } + + } + + +} diff --git a/racevisionGame/src/main/java/network/Messages/Enums/RaceStatusEnum.java b/racevisionGame/src/main/java/network/Messages/Enums/RaceStatusEnum.java new file mode 100644 index 00000000..973d4347 --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/Enums/RaceStatusEnum.java @@ -0,0 +1,108 @@ +package network.Messages.Enums; + + +import java.util.HashMap; +import java.util.Map; + +/** + * Enumeration that encapsulates the various statuses a race can have. See AC35 streaming spec, 4.2. + */ +public enum RaceStatusEnum { + + NOT_ACTIVE(0), + + /** + * Between 3:00 and 1:00 minutes before start. + */ + WARNING(1), + + /** + * Less than 1:00 minutes before start. + */ + PREPARATORY(2), + STARTED(3), + + /** + * Obsolete. + */ + FINISHED(4), + + /** + * Obsolete. + */ + RETIRED(5), + ABANDONED(6), + POSTPONED(7), + TERMINATED(8), + RACE_START_TIME_NOT_SET(9), + + /** + * More than 3:00 minutes until start. + */ + PRESTART(10), + + /** + * Used to indicate that a given byte value is invalid. + */ + NOT_A_STATUS(-1); + + + /** + * Primitive value of the enum. + */ + private byte value; + + + /** + * Ctor. Creates a RaceStatusEnum from a given primitive integer value, cast to a byte. + * @param value Integer, which is cast to byte, to construct from. + */ + private RaceStatusEnum(int value) { + this.value = (byte) value; + } + + /** + * Returns the primitive value of the enum. + * @return Primitive value of the enum. + */ + public byte getValue() { + return value; + } + + + /** + * Stores a mapping between Byte values and RaceStatusEnum values. + */ + private static final Map byteToStatusMap = new HashMap<>(); + + + /* + Static initialization block. Initializes the byteToStatusMap. + */ + static { + for (RaceStatusEnum type : RaceStatusEnum.values()) { + RaceStatusEnum.byteToStatusMap.put(type.value, type); + } + } + + + /** + * Returns the enumeration value which corresponds to a given byte value. + * @param raceStatusByte Byte value to convert to a RaceStatusEnum value. + * @return The RaceStatusEnum value which corresponds to the given byte value. + */ + public static RaceStatusEnum fromByte(byte raceStatusByte) { + //Gets the corresponding MessageType from the map. + RaceStatusEnum type = RaceStatusEnum.byteToStatusMap.get(raceStatusByte); + + if (type == null) { + //If the byte value wasn't found, return the NOT_A_STATUS RaceStatusEnum. + return RaceStatusEnum.NOT_A_STATUS; + } else { + //Otherwise, return the RaceStatusEnum. + return type; + } + + } + +} diff --git a/racevisionGame/src/main/java/network/Messages/Enums/RaceTypeEnum.java b/racevisionGame/src/main/java/network/Messages/Enums/RaceTypeEnum.java new file mode 100644 index 00000000..15c47e01 --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/Enums/RaceTypeEnum.java @@ -0,0 +1,87 @@ +package network.Messages.Enums; + +import java.util.HashMap; +import java.util.Map; + +/** + * Enumeration that encapsulates the various types of races. See AC35 streaming spec, 4.2. + */ +public enum RaceTypeEnum { + + + /** + * A race between two boats. + */ + MATCH_RACE(1), + + /** + * A race between a fleet of boats. + */ + FLEET_RACE(2), + + /** + * Used to indicate that a given byte value is invalid. + */ + NOT_A_STATUS(-1); + + + /** + * Primitive value of the enum. + */ + private byte value; + + + /** + * Ctor. Creates a RaceTypeEnum from a given primitive integer value, cast to a byte. + * @param value Integer, which is cast to byte, to construct from. + */ + private RaceTypeEnum(int value) { + this.value = (byte) value; + } + + /** + * Returns the primitive value of the enum. + * @return Primitive value of the enum. + */ + public byte getValue() { + return value; + } + + + /** + * Stores a mapping between Byte values and RaceStatusEnum values. + */ + private static final Map byteToStatusMap = new HashMap<>(); + + + /* + Static initialization block. Initializes the byteToStatusMap. + */ + static { + for (RaceTypeEnum type : RaceTypeEnum.values()) { + RaceTypeEnum.byteToStatusMap.put(type.value, type); + } + } + + + /** + * Returns the enumeration value which corresponds to a given byte value. + * @param raceTypeEnum Byte value to convert to a RaceTypeEnum value. + * @return The RaceTypeEnum value which corresponds to the given byte value. + */ + public static RaceTypeEnum fromByte(byte raceTypeEnum) { + //Gets the corresponding MessageType from the map. + 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; + } else { + //Otherwise, return the RaceTypeEnum. + return type; + } + + } + + +} diff --git a/racevisionGame/src/main/java/network/Messages/Heartbeat.java b/racevisionGame/src/main/java/network/Messages/Heartbeat.java new file mode 100644 index 00000000..3b50bf36 --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/Heartbeat.java @@ -0,0 +1,29 @@ +package network.Messages; + +import seng302.Networking.Messages.Enums.MessageType; + +/** + * Represents a Heartbeat message. + */ +public class Heartbeat extends AC35Data { + + ///Sequence number of the heartbeat. + private long sequenceNumber; + + /** + * Ctor. + * @param sequenceNumber Sequence number of the heartbeat. + */ + public Heartbeat(long sequenceNumber) { + super(MessageType.HEARTBEAT); + this.sequenceNumber = sequenceNumber; + } + + /** + * Returns the sequence number of this heartbeat message. + * @return Sequence number of this heartbeat message. + */ + public long getSequenceNumber() { + return sequenceNumber; + } +} diff --git a/racevisionGame/src/main/java/network/Messages/MarkRounding.java b/racevisionGame/src/main/java/network/Messages/MarkRounding.java new file mode 100644 index 00000000..d0148906 --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/MarkRounding.java @@ -0,0 +1,69 @@ +package network.Messages; + +import seng302.Networking.Messages.Enums.MessageType; + +/** + * Created by fwy13 on 25/04/17. + */ +public class MarkRounding extends AC35Data { + + private int msgVerNum; + private long time; + private int ackNum; + private int raceID; + private int sourceID; + private int boatStatus; + private int roundingSide; + private int markType; + private int markID; + + public static int BoatStatusUnknown = 0; + public static int BoatStatusRacing = 1; + public static int BoatStatusDSQ = 2; + public static int BoatStatusWithdrawn = 3; + + public static int RoundingSideUnknown = 0; + public static int RoundingSidePort = 1; + public static int RoundingSideStarboard = 2; + + public static int MarkTypeUnknown = 0; + public static int MarkTypeRoundingMark = 1; + public static int MarkTypeGate = 2; + + public static int MarkIDEntryLimitLine = 100; + public static int MarkIDEntryLine = 101; + public static int MarkIDRaceStartStartline = 102; + public static int MarkIDRaceFinishline = 103; + public static int MarkIDSpeedTestStart = 104; + public static int MarkIDSpeedTestFinish = 105; + public static int MarkIDClearStart = 106; + + public MarkRounding(int msgVerNum, long time, int ackNum, int raceID, int sourceID, int boatStatus, int roundingSide, int markType, int markID){ + super(MessageType.MARKROUNDING); + this.msgVerNum = msgVerNum; + this.time = time; + this.ackNum = ackNum; + this.raceID = raceID; + this.sourceID = sourceID; + this.boatStatus = boatStatus; + this.roundingSide = roundingSide; + this.markType = markType; + this.markID = markID; + } + + /** + * Returns the boat (source) ID for this message. + * @return Boat ID for this message. + */ + public int getSourceID() { + return sourceID; + } + + /** + * Returns the timestamp for this message. + * @return Timestamp for this message. + */ + public long getTime() { + return time; + } +} diff --git a/racevisionGame/src/main/java/network/Messages/RaceMessage.java b/racevisionGame/src/main/java/network/Messages/RaceMessage.java new file mode 100644 index 00000000..856f2961 --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/RaceMessage.java @@ -0,0 +1,26 @@ +package network.Messages; + +import seng302.Networking.Messages.Enums.MessageType; + +/** + * Created by fwy13 on 19/04/17. + */ +public class RaceMessage extends AC35Data { + + private int lineNumber; + private String messageText; + + public RaceMessage(int lineNumber, String messageText){ + super(MessageType.DISPLAYTEXTMESSAGE); + this.lineNumber = lineNumber; + this.messageText = messageText; + } + + public int getLineNumber() { + return lineNumber; + } + + public String getMessageText() { + return messageText; + } +} diff --git a/racevisionGame/src/main/java/network/Messages/RaceStartStatus.java b/racevisionGame/src/main/java/network/Messages/RaceStartStatus.java new file mode 100644 index 00000000..ae6b9fb5 --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/RaceStartStatus.java @@ -0,0 +1,25 @@ +package network.Messages; + +import seng302.Networking.Messages.Enums.MessageType; + +/** + * Created by fwy13 on 25/04/17. + */ +public class RaceStartStatus extends AC35Data { + + private long timestamp; + private int ackNum; + private long raceStartTime; + private int raceID; + private int notificationType; + + public RaceStartStatus(long timestamp, int ackNum, long raceStartTime, int raceID, int notificationType){ + super(MessageType.RACESTARTSTATUS); + this.timestamp = timestamp; + this.ackNum = ackNum; + this.raceStartTime = raceStartTime; + this.raceID = raceID; + this.notificationType = notificationType; + } + +} diff --git a/racevisionGame/src/main/java/network/Messages/RaceStatus.java b/racevisionGame/src/main/java/network/Messages/RaceStatus.java new file mode 100644 index 00000000..03b1e5de --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/RaceStatus.java @@ -0,0 +1,129 @@ +package network.Messages; + +import seng302.Networking.Messages.Enums.MessageType; +import seng302.Networking.Utils.AC35UnitConverter; + +import java.util.List; + +/** + * Created by fwy13 on 25/04/17. + */ +public class RaceStatus extends AC35Data { + + private long currentTime; + private int raceID; + private int raceStatus; + private long expectedStartTime; + private int windDirection; + private int windSpeed; + private int raceType; + private List boatStatuses; + + public RaceStatus(long currentTime, int raceID, int raceStatus, long expectedStartTime, int windDirection, int windSpeed, int raceType, List boatStatuses){ + super(MessageType.RACESTATUS); + this.currentTime = currentTime; + this.raceID = raceID; + this.raceStatus = raceStatus; + this.expectedStartTime = expectedStartTime; + this.windDirection = windDirection; + this.windSpeed = windSpeed; + this.raceType = raceType; + this.boatStatuses = boatStatuses;//note this is not a copy so any alterations to the parent will affect this. + } + + + + ///Getters. + + public long getCurrentTime() + { + return currentTime; + } + + public int getRaceID() + { + return raceID; + } + + /** + * + * @return race status number + */ + public int getRaceStatus() + { + return raceStatus; + } + + public long getExpectedStartTime() + { + return expectedStartTime; + } + + public int getWindDirection() + { + return windDirection; + } + + public int getWindSpeed() + { + return windSpeed; + } + + public int getRaceType() + { + return raceType; + } + + public List getBoatStatuses() + { + return boatStatuses; + } + + public boolean isNotActive() { + return raceStatus == 0; + } + + public boolean isWarning() { + return raceStatus == 1; + } + + public boolean isPreparatory() { + return raceStatus == 2; + } + + public boolean isStarted() { + return raceStatus == 3; + } + + public boolean isFinished() { + return raceStatus == 4; + } + + public boolean isRetired() { + return raceStatus == 5; + } + + public boolean isAbandoned() { + return raceStatus == 6; + } + + public boolean isPostponed() { + return raceStatus == 7; + } + + public boolean isTerminated() { + return raceStatus == 8; + } + + public boolean isStartTimeSet() { + return raceStatus != 9; + } + + public boolean isPrestart() { + return raceStatus == 10; + } + + public double getScaledWindDirection() { + return (double) AC35UnitConverter.convertHeading(windDirection); + } +} diff --git a/racevisionGame/src/main/java/network/Messages/XMLMessage.java b/racevisionGame/src/main/java/network/Messages/XMLMessage.java new file mode 100644 index 00000000..4f9542b6 --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/XMLMessage.java @@ -0,0 +1,57 @@ +package network.Messages; + +import seng302.Networking.Messages.Enums.MessageType; + +import java.io.InputStream; + +/** + * Created by fwy13 on 25/04/17. + */ +public class XMLMessage extends AC35Data { + + private int ackNumber; + private long timeStamp; + private int xmlMsgSubType; + private int sequenceNumber; + private int xmlMsgLength; + private InputStream xmlMessage; + + public static int XMLTypeRegatta = 5; + public static int XMLTypeRace = 6; + public static int XMLTypeBoat = 7; + + /** + * Constructor for an XML Message + * @param ackNumber Number for acknowledgement inherited for the AC35Data Packet + * @param timeStamp Time received + * @param xmlMsgSubType Type of XML message + * @param sequenceNumber Order that it has arrived in + * @param xmlMsgLength Length of the xml message + * @param xmlMessage XML message + */ + public XMLMessage(int ackNumber, long timeStamp, int xmlMsgSubType, int sequenceNumber, int xmlMsgLength, InputStream xmlMessage){ + super(MessageType.XMLMESSAGE); + this.ackNumber = ackNumber; + this.timeStamp = timeStamp; + this.xmlMsgSubType = xmlMsgSubType; + this.sequenceNumber = sequenceNumber; + this.xmlMsgLength = xmlMsgLength; + this.xmlMessage = xmlMessage; + } + + /** + * Get the XML Message + * @return the XML message as an input stream + */ + public InputStream getXmlMessage() { + return xmlMessage; + } + + /** + * Get the type of message + * @return Gets the type of message the XML message is + */ + public int getXmlMsgSubType() { + return xmlMsgSubType; + } +} diff --git a/racevisionGame/src/main/java/network/PacketDump/AC35DumpReader.java b/racevisionGame/src/main/java/network/PacketDump/AC35DumpReader.java new file mode 100644 index 00000000..fbc2b857 --- /dev/null +++ b/racevisionGame/src/main/java/network/PacketDump/AC35DumpReader.java @@ -0,0 +1,71 @@ +package network.PacketDump; + +import seng302.Networking.BinaryMessageDecoder; +import seng302.Networking.Exceptions.InvalidMessageException; +import seng302.Networking.Messages.AC35Data; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Created by fwy13 on 25/04/17. + */ +public class AC35DumpReader { + + private byte[] dump; + private ArrayList packets; + + public AC35DumpReader(String url) throws IOException, URISyntaxException { + + URL uri = getClass().getClassLoader().getResource(url); + Path path = Paths.get(uri.toURI()); + dump = Files.readAllBytes(path); + + packets = new ArrayList<>(); + + readAllPackets(); + } + + private void readAllPackets(){ + int pointer = 0; + while(pointer < dump.length){ + byte[] messLen = new byte[2]; + messLen[1] = dump[pointer + 13]; + messLen[0] = dump[pointer + 14]; + int messageLength = ByteBuffer.wrap(messLen).getShort(); + //System.out.println(messageLength); + + packets.add(new AC35Packet(Arrays.copyOfRange(dump, pointer, pointer + messageLength + 19))); + + pointer += 19 + messageLength; + } + for (AC35Packet pack: packets){ + BinaryMessageDecoder decoder = new BinaryMessageDecoder(pack.getData()); + + try { + AC35Data data = decoder.decode(); + } + catch (InvalidMessageException e) { + System.out.println(e.getMessage()); + } + } + + } + + public static void main(String[] args){ + try { + AC35DumpReader ac35DumpReader = new AC35DumpReader("dataDumps/ac35.bin"); + } catch (IOException e) { + e.printStackTrace(); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + } +} diff --git a/racevisionGame/src/main/java/network/PacketDump/AC35Packet.java b/racevisionGame/src/main/java/network/PacketDump/AC35Packet.java new file mode 100644 index 00000000..4b978d48 --- /dev/null +++ b/racevisionGame/src/main/java/network/PacketDump/AC35Packet.java @@ -0,0 +1,17 @@ +package network.PacketDump; + +/** + * Created by fwy13 on 25/04/17. + */ +public class AC35Packet { + + byte[] data; + + public AC35Packet(byte[] data){ + this.data = data; + } + + public byte[] getData() { + return data; + } +} diff --git a/racevisionGame/src/main/java/network/Utils/AC35UnitConverter.java b/racevisionGame/src/main/java/network/Utils/AC35UnitConverter.java new file mode 100644 index 00000000..73f6d0e9 --- /dev/null +++ b/racevisionGame/src/main/java/network/Utils/AC35UnitConverter.java @@ -0,0 +1,43 @@ +package network.Utils; + +/** + * Created by fwy13 on 28/04/17. + */ +public class AC35UnitConverter { + + public static double convertGPS(int value){ + //converts latitude or longitue to angle + return (double) value * 180.0 / 2147483648.0;//2^31 = 2147483648 + } + + public static int convertGPSToInt(double value){ + //converts latitude or longitue to angle + return (int) (value * 2147483648.0/180.0);//2^31 = 2147483648 + } + + public static double convertHeading(long value){ + return (double) value * 360.0/65536.0;//2^15 + } + + public static double convertHeading(int value){ + return (double) value * 360.0/65536.0;//2^15 + } + + + public static double convertHeading(double value){ + return value * 360.0/65536.0;//2^15 + } + + public static int encodeHeading(int value){ + return (int) (value / 360.0 * 65536.0);//2^15 + } + + public static int encodeHeading(double value){ + return (int) (value / 360.0 * 65536.0);//2^15 + } + + public static double convertTrueWindAngle(long value){ + return (double) value * 180.0/32768.0;//-2^15 to 2^15 + } + +} diff --git a/racevisionGame/src/main/java/network/Utils/ByteConverter.java b/racevisionGame/src/main/java/network/Utils/ByteConverter.java new file mode 100644 index 00000000..8c1172da --- /dev/null +++ b/racevisionGame/src/main/java/network/Utils/ByteConverter.java @@ -0,0 +1,266 @@ +package network.Utils; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +/** + * Created by fwy13 on 25/04/17. + */ +public class ByteConverter { + + public static int IntegerSize = 4; + public static int LongSize = 8; + public static int CharSize = 2; + public static int ShortSize = 2; + + + //default for AC35 is Little Endian therefore all overloads will be done with Little_Endian unless told else wise + + ////////////////////////////////////////////////// + //Bytes[] to number conversions + ////////////////////////////////////////////////// + + ////////////////////////////////////////////////// + //Integer + ////////////////////////////////////////////////// + + /** + * @param bite bite to convert + * @return int + */ + public static int bytesToInt(byte bite){ + byte[] bytes = {bite}; + return bytesToInt(bytes, ByteOrder.LITTLE_ENDIAN); + } + + /** + * @param bytes bytes to convert + * @return int + */ + public static int bytesToInt(byte[] bytes){ + return bytesToInt(bytes, ByteOrder.LITTLE_ENDIAN); + } + + /** + * @param bytes bytes to convert + * @param byteOrder order of the bytes + * @return int + */ + public static int bytesToInt(byte[] bytes, ByteOrder byteOrder){ + byte[] bites = convertBytesToNum(bytes,byteOrder, IntegerSize); + return ByteBuffer.wrap(bites).order(byteOrder).getInt(); + } + + ////////////////////////////////////////////////// + //Long + ////////////////////////////////////////////////// + + public static long bytesToLong(byte bite){ + byte[] bytes = {bite}; + return bytesToLong(bytes, ByteOrder.LITTLE_ENDIAN); + } + + public static long bytesToLong(byte[] bytes){ + return bytesToLong(bytes, ByteOrder.LITTLE_ENDIAN); + } + + public static long bytesToLong(byte[] bytes, ByteOrder byteOrder){ + byte[] bites = convertBytesToNum(bytes,byteOrder, LongSize); + return ByteBuffer.wrap(bites).order(byteOrder).getLong(); + } + + ////////////////////////////////////////////////// + //Short + ////////////////////////////////////////////////// + + public static short bytesToShort(byte bite){ + byte[] bytes = {bite}; + return bytesToShort(bytes, ByteOrder.LITTLE_ENDIAN); + } + + public static short bytesToShort(byte[] bytes){ + return bytesToShort(bytes, ByteOrder.LITTLE_ENDIAN); + } + + public static short bytesToShort(byte[] bytes, ByteOrder byteOrder){ + byte[] bites = convertBytesToNum(bytes,byteOrder, ShortSize); + return ByteBuffer.wrap(bites).order(byteOrder).getShort(); + } + + ////////////////////////////////////////////////// + //Char + ////////////////////////////////////////////////// + + public static char bytesToChar(byte bite){ + byte[] bytes = {bite}; + return bytesToChar(bytes, ByteOrder.LITTLE_ENDIAN); + } + + public static char bytesToChar(byte[] bytes){ + return bytesToChar(bytes, ByteOrder.LITTLE_ENDIAN); + } + + public static char bytesToChar(byte[] bytes, ByteOrder byteOrder){ + byte[] bites = convertBytesToNum(bytes,byteOrder, CharSize); + return ByteBuffer.wrap(bites).order(byteOrder).getChar(); + } + + ////////////////////////////////////////////////// + //Conversion Function + ////////////////////////////////////////////////// + + private static byte[] convertBytesToNum(byte[] bytes, ByteOrder byteOrder, int maxSize){ + byte[] bites = new byte[maxSize]; + if (byteOrder == ByteOrder.LITTLE_ENDIAN) { + for (int i = 0; i < bytes.length; i++) { + if (i > maxSize){//break if over hte limit + break; + } + bites[i] = bytes[i]; + } + for (int i = bytes.length; i < maxSize; i++) { + bites[i] = 0b0; + } + }else{//if big endian + for (int i = 0; i < maxSize - bytes.length; i++) { + bites[i] = 0b0; + } + for (int i = maxSize - bytes.length; i < maxSize; i++) { + if (i > maxSize){//break if over the limit + break; + } + bites[i] = bytes[i - maxSize + bytes.length]; + } + } + return bites; + } + + ////////////////////////////////////////////////////////// + //Number to Byte[] conversions + ////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////// + //Integer + ////////////////////////////////////////////////// + + public static byte[] intToBytes(int i){ + return intToBytes(i, 4, ByteOrder.LITTLE_ENDIAN); + } + + public static byte[] intToBytes(int i ,int size){ + return intToBytes(i, size, ByteOrder.LITTLE_ENDIAN); + } + + /** + * Converts an Integer to a Byte Array + * @param i the integer to be converted + * @param size Size that the byte array should be + * @param byteOrder the order that the bytes should be ie Big Endian + * @return bytes array + */ + public static byte[] intToBytes(int i ,int size, ByteOrder byteOrder){ + ByteBuffer buffer = ByteBuffer.allocate(IntegerSize); + buffer.order(byteOrder); + buffer.putInt(i); + byte[] copy = buffer.array(); + return convertNumtoBytes(copy, size, byteOrder, IntegerSize); + } + + ////////////////////////////////////////////////// + //Long + ////////////////////////////////////////////////// + + public static byte[] longToBytes(long i){ + return longToBytes(i, LongSize, ByteOrder.LITTLE_ENDIAN); + } + + public static byte[] longToBytes(long i ,int size){ + return longToBytes(i, size, ByteOrder.LITTLE_ENDIAN); + } + + /** + * Converts an Long to a Byte Array + * @param i the Long to be converted + * @param size Size that the byte array should be + * @param byteOrder the order that the bytes should be ie Big Endian + * @return byte array + */ + public static byte[] longToBytes(long i ,int size, ByteOrder byteOrder){ + ByteBuffer buffer = ByteBuffer.allocate(LongSize); + buffer.order(byteOrder); + buffer.putLong(i); + byte[] copy = buffer.array(); + return convertNumtoBytes(copy, size, byteOrder, LongSize); + } + + ////////////////////////////////////////////////// + //Short + ////////////////////////////////////////////////// + + public static byte[] shortToBytes(short i){ + return shortToBytes(i, ShortSize, ByteOrder.LITTLE_ENDIAN); + } + + public static byte[] shortToBytes(short i ,int size){ + return shortToBytes(i, size, ByteOrder.LITTLE_ENDIAN); + } + + /** + * Converts an Short to a Byte Array + * @param i the Short to be converted + * @param size Size that the byte array should be + * @param byteOrder the order that the bytes should be ie Big Endian + * @return byte array + */ + public static byte[] shortToBytes(short i ,int size, ByteOrder byteOrder){ + ByteBuffer buffer = ByteBuffer.allocate(ShortSize); + buffer.order(byteOrder); + buffer.putShort(i); + byte[] copy = buffer.array(); + return convertNumtoBytes(copy, size, byteOrder, ShortSize); + } + + ////////////////////////////////////////////////// + //Char + ////////////////////////////////////////////////// + + public static byte[] charToBytes(char i){ + return charToBytes(i, CharSize, ByteOrder.LITTLE_ENDIAN); + } + + public static byte[] charToBytes(char i ,int size){ + return charToBytes(i, size, ByteOrder.LITTLE_ENDIAN); + } + + /** + * Converts an Char to a Byte Array + * @param i the Char to be converted + * @param size Size that the byte array should be + * @param byteOrder the order that the bytes should be ie Big Endian + * @return byte array + */ + public static byte[] charToBytes(char i ,int size, ByteOrder byteOrder){ + ByteBuffer buffer = ByteBuffer.allocate(CharSize); + buffer.order(byteOrder); + buffer.putChar(i); + byte[] copy = buffer.array(); + return convertNumtoBytes(copy, size, byteOrder, CharSize); + } + + ////////////////////////////////////////////////// + //Conversion Function + ////////////////////////////////////////////////// + + private static byte[] convertNumtoBytes(byte[] copy ,int size, ByteOrder byteOrder, int fullsize){ + byte[] bytes = new byte[size]; + if (byteOrder == ByteOrder.LITTLE_ENDIAN){ + bytes = Arrays.copyOfRange(copy, 0, size); + }else{// if it is Big Endian + bytes = Arrays.copyOfRange(copy, fullsize - size, fullsize); + } + return bytes; + } + + +} diff --git a/racevisionGame/src/main/java/shared/model/Angle.java b/racevisionGame/src/main/java/shared/model/Angle.java new file mode 100644 index 00000000..769257c4 --- /dev/null +++ b/racevisionGame/src/main/java/shared/model/Angle.java @@ -0,0 +1,130 @@ +package shared.model; + +/** + * This represents an angle. + * Has functions to return angle as either degrees or radians. + */ +public class Angle implements Comparable { + + /** + * The angle stored in this object. + * Degrees. + */ + private double degrees; + + + /** + * Ctor. + * Don't use this. + * This is protected because you need to use the static helper functions {@link #fromDegrees(double)} and {@link #fromRadians(double)} to construct an Angle object. + * + * @param degrees The value, in degrees, to initialize this Angle object with. + */ + protected Angle(double degrees) { + this.degrees = degrees; + } + + + /** + * Constructs an Angle object from an angle value in degrees. + * @param degrees Angle value in degrees. + * @return Angle object. + */ + public static Angle fromDegrees(double degrees) { + Angle angle = new Angle(degrees); + return angle; + } + + /** + * Constructs an Angle object from an angle value in radians. + * @param radians Angle value in radians. + * @return Angle object. + */ + public static Angle fromRadians(double radians) { + return Angle.fromDegrees(Math.toDegrees(radians)); + } + + + + /** + * Returns the value of this Angle object, in degrees. + * @return The value of this Angle object, in degrees. + */ + public double degrees() { + return this.degrees; + } + + + /** + * Returns the value of this Angle object, in radians. + * @return The value of this Angle object, in radians. + */ + public double radians() { + return Math.toRadians(this.degrees); + } + + + + /** + * Returns true if two Angle objects have equal values. + * @param obj Other angle object to compare. + * @return True if they are equal, false otherwise. + */ + @Override + public boolean equals(Object obj) { + //Cast other side. + Angle other = (Angle) obj; + + //Compare values. + if (this.degrees() == other.degrees()) { + return true; + } else { + return false; + } + } + + + /** + * Returns an int describing the ordering between this angle object, and another. + * @param o Other angle to compare to. + * @return {@literal int < 0} if this angle is less than the other angle, {@literal int > 0} if this angle is greater than the other angle, and {@literal int = 0} if this angle is equal to the other angle, + */ + @Override + public int compareTo(Angle o) { + + if (this.degrees() < o.degrees()) { + return -1; + } else if (this.degrees() > o.degrees()) { + return 1; + } else { + return 0; + } + + } + + + /** + * Converts an angle to an angle in a given periodic interval (e.g., degrees have a periodic interval of 360, radians have a periodic interval of 2Pi) of [lowerBound, upperBound). + * @param angle The angle to convert. + * @param lowerBound The lower bound of the interval. + * @param upperBound The upper bound of the interval. + * @param period The period of the interval. + * @return The angle in the desired periodic interval. + */ + public static double toPeriodicInterval(double angle, double lowerBound, double upperBound, double period) { + + + while (angle >= upperBound) { + //Too large. + angle -= period; + } + + while (angle < lowerBound) { + //Too small. + angle += period; + } + + return angle; + + } +} diff --git a/racevisionGame/src/main/java/shared/model/Azimuth.java b/racevisionGame/src/main/java/shared/model/Azimuth.java new file mode 100644 index 00000000..540d46e1 --- /dev/null +++ b/racevisionGame/src/main/java/shared/model/Azimuth.java @@ -0,0 +1,68 @@ +package shared.model; + + + +/** + * Represents an azimuth. + * If treated as an absolute azimuth this is the angle between north and a target point. + * If treated as a relative azimuth, this is the angle between from one target point to the other. + * It has the interval [-180, 180) degrees, and clockwise is positive. + */ +public class Azimuth extends Angle{ + + + /** + * Ctor. + * This is protected because you need to use the static helper functions {@link #fromDegrees(double)} and {@link #fromRadians(double)} to construct an Azimuth object. + * + * @param degrees The value, in degrees, to initialize this Azimuth object with. + */ + protected Azimuth(double degrees) { + super(degrees); + } + + + /** + * Converts an angle in degrees into an angle in degrees in the correct interval for an azimuth - [-180, 180). + * E.g., converts -183 to 177, or converts 250 to -110, or converts 180 to -180. + * @param degrees Degree value to convert. + * @return Degree value in interval [-180, 180). + */ + public static double toAzimuthInterval(double degrees) { + + return Angle.toPeriodicInterval(degrees, -180d, 180d, 360d); + } + + /** + * Constructs an Azimuth object from an angle value in degrees. + * @param degrees Azimuth value in degrees. + * @return Azimuth object. + */ + public static Azimuth fromDegrees(double degrees) { + //Ensure the angle is in the correct interval. + double degreesInInterval = Azimuth.toAzimuthInterval(degrees); + + Azimuth azimuth = new Azimuth(degreesInInterval); + return azimuth; + } + + /** + * Constructs an Azimuth object from an angle value in radians. + * @param radians Azimuth value in radians. + * @return Azimuth object. + */ + public static Azimuth fromRadians(double radians) { + return Azimuth.fromDegrees(Math.toDegrees(radians)); + } + + + /** + * Constructs an Azimuth object from a Bearing object. + * @param bearing Bearing object to read value from. + * @return Azimuth object. + */ + public static Azimuth fromBearing(Bearing bearing) { + return Azimuth.fromDegrees(bearing.degrees()); + } + +} diff --git a/racevisionGame/src/main/java/shared/model/Bearing.java b/racevisionGame/src/main/java/shared/model/Bearing.java new file mode 100644 index 00000000..83f0eab1 --- /dev/null +++ b/racevisionGame/src/main/java/shared/model/Bearing.java @@ -0,0 +1,66 @@ +package shared.model; + +/** + * Represents a bearing. Also known as a heading. + * If treated as an absolute bearing this is the angle between north and a target point. + * If treated as a relative bearing, this is the angle between from one target point to the other. + * Has the interval [0, 360) degrees, and clockwise is positive. + */ +public class Bearing extends Angle { + + + /** + * Ctor. + * This is protected because you need to use the static helper functions {@link #fromDegrees(double)} and {@link #fromRadians(double)} to construct a Bearing object. + * + * @param degrees The value, in degrees, to initialize this Bearing object with. + */ + protected Bearing(double degrees) { + super(degrees); + } + + + /** + * Converts an angle in degrees into an angle in degrees in the correct interval for a bearing - [0, 360). + * E.g., converts -183 to 177, or converts 425 to 65. + * @param degrees Degree value to convert. + * @return Degree value in interval [0, 360). + */ + public static double toBearingInterval(double degrees) { + + return Angle.toPeriodicInterval(degrees, -0d, 360d, 360d); + } + + /** + * Constructs a Bearing object from an angle value in degrees. + * @param degrees Bearing value in degrees. + * @return Bearing object. + */ + public static Bearing fromDegrees(double degrees) { + //Ensure the angle is in the correct interval. + double degreesInInterval = Bearing.toBearingInterval(degrees); + + Bearing bearing = new Bearing(degreesInInterval); + return bearing; + } + + /** + * Constructs a Bearing object from an angle value in radians. + * @param radians Bearing value in radians. + * @return Bearing object. + */ + public static Bearing fromRadians(double radians) { + return Bearing.fromDegrees(Math.toDegrees(radians)); + } + + + /** + * Constructs a Bearing object from an Azimuth object. + * @param azimuth Azimuth object to read value from. + * @return Bearing object. + */ + public static Bearing fromAzimuth(Azimuth azimuth) { + return Bearing.fromDegrees(azimuth.degrees()); + } + +} diff --git a/racevisionGame/src/main/java/shared/model/Boat.java b/racevisionGame/src/main/java/shared/model/Boat.java new file mode 100644 index 00000000..1d5d822b --- /dev/null +++ b/racevisionGame/src/main/java/shared/model/Boat.java @@ -0,0 +1,296 @@ +package shared.model; + + +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 { + /** + * The name of the boat/team. + */ + private String name; + + /** + * The current speed of the boat, in knots. + * TODO knots + */ + private double currentSpeed; + + /** + * The current bearing/heading of the boat. + */ + private Bearing bearing; + + /** + * The current position of the boat. + */ + private GPSCoordinate currentPosition; + + /** + * The country or team abbreviation of the boat. + */ + private String country; + + /** + * The source ID of the boat. + * This uniquely identifies an entity during a race. + */ + private int sourceID; + + /** + * The leg of the race that the boat is currently on. + */ + private Leg currentLeg; + + /** + * The distance, in meters, that the boat has travelled in the current leg. + * TODO meters + */ + private double distanceTravelledInLeg; + + /** + * The time, in milliseconds, that has elapsed during the current leg. + * TODO milliseconds + */ + private long timeElapsedInCurrentLeg; + + /** + * The timestamp, in milliseconds, of when the boat finished the race. + * Is -1 if it hasn't finished. + * TODO milliseconds + */ + private long timeFinished = -1; + + /** + * The current status of the boat. + */ + private BoatStatusEnum status; + + + + private long estimatedTime = 0; + + + /** + * Constructs a boat object with a given sourceID, name, country/team abbreviation, and polars table. + * + * @param sourceID The id of the boat + * @param name The name of the Boat. + * @param country The abbreviation or country code for the boat. + */ + public Boat(int sourceID, String name, String country) { + this.country = country; + this.name = name; + this.sourceID = sourceID; + + this.bearing = Bearing.fromDegrees(0d); + + this.status = BoatStatusEnum.UNDEFINED; + } + + + + + /** + * Returns the name of the boat/team. + * @return Name of the boat/team. + */ + public String getName() { + return name; + } + + /** + * Sets the name of the boat/team. + * @param name Name of the boat/team. + */ + public void setName(String name) { + this.name = name; + } + + + /** + * Returns the current speed of the boat, in knots. + * @return The current speed of the boat, in knots. + */ + public double getCurrentSpeed() { + return currentSpeed; + } + + /** + * Sets the speed of the boat, in knots. + * @param currentSpeed The new speed of the boat, in knots. + */ + public void setCurrentSpeed(double currentSpeed) { + this.currentSpeed = currentSpeed; + } + + + /** + * Gets the country/team abbreviation of the boat. + * @return The country/team abbreviation of the boat. + */ + public String getCountry() { + return country; + } + + /** + * Sets the country/team abbreviation of the boat. + * @param country The new country/team abbreviation for the boat. + */ + public void setCountry(String country) { + this.country = country; + } + + + /** + * Returns the source ID of the boat. + * @return The source ID of the boat. + */ + public int getSourceID() { + return sourceID; + } + + /** + * Sets the source ID of the boat. + * @param sourceID The new source ID for the boat. + */ + public void setSourceID(int sourceID) { + this.sourceID = sourceID; + } + + /** + * Returns the current leg of the race the boat is in. + * @return The current leg of the race the boat is in. + */ + public Leg getCurrentLeg() { + return currentLeg; + } + + /** + * Sets the current leg of the race the boat is in. + * Clears time elapsed in current leg and distance travelled in current leg. + * @param currentLeg The new leg of the race the boat is in. + */ + public void setCurrentLeg(Leg currentLeg) { + this.currentLeg = currentLeg; + this.setTimeElapsedInCurrentLeg(0); + this.setDistanceTravelledInLeg(0); + } + + + /** + * Returns the distance, in meters, the boat has travelled in the current leg. + * @return The distance, in meters, the boat has travelled in the current leg. + */ + public double getDistanceTravelledInLeg() { + return distanceTravelledInLeg; + } + + /** + * Sets the distance, in meters, the boat has travelled in the current leg. + * @param distanceTravelledInLeg The distance, in meters, the boat has travelled in the current leg. + */ + public void setDistanceTravelledInLeg(double distanceTravelledInLeg) { + this.distanceTravelledInLeg = distanceTravelledInLeg; + } + + + /** + * Returns the current position of the boat. + * @return The current position of the boat. + */ + public GPSCoordinate getCurrentPosition() { + return currentPosition; + } + + /** + * Sets the current position of the boat. + * @param currentPosition The new position for the boat. + */ + public void setCurrentPosition(GPSCoordinate currentPosition) { + this.currentPosition = currentPosition; + } + + + /** + * Gets the timestamp, in milliseconds, at which the boat finished the race. + * @return The timestamp, in milliseconds, at which the boat finished the race. + */ + public long getTimeFinished() { + return timeFinished; + } + + /** + * Sets the timestamp, in milliseconds, at which the boat finished the race. + * @param timeFinished The timestamp, in milliseconds, at which the boat finished the race. + */ + public void setTimeFinished(long timeFinished) { + this.timeFinished = timeFinished; + } + + + + + /** + * Returns the current bearing of the boat. + * @return The current bearing of the boat. + */ + public Bearing getBearing() { + return bearing; + } + + /** + * Sets the current bearing of the boat. + * @param bearing The new bearing of the boat. + */ + public void setBearing(Bearing bearing) { + this.bearing = bearing; + } + + + /** + * Returns the time, in milliseconds, that has elapsed since the boat started the current leg. + * @return The time, in milliseconds, that has elapsed since the boat started the current leg. + */ + public long getTimeElapsedInCurrentLeg() { + return timeElapsedInCurrentLeg; + } + + /** + * Sets the time, in milliseconds, that has elapsed since the boat started the current leg. + * @param timeElapsedInCurrentLeg The new time, in milliseconds, that has elapsed since the boat started the current leg. + */ + public void setTimeElapsedInCurrentLeg(long timeElapsedInCurrentLeg) { + this.timeElapsedInCurrentLeg = timeElapsedInCurrentLeg; + } + + + /** + * Returns the status of the boat. + * @return The sttus of the boat. + */ + public BoatStatusEnum getStatus() { + return status; + } + + /** + * Sets the status of the boat. + * @param status The new status of the boat. + */ + public void setStatus(BoatStatusEnum status) { + this.status = status; + } + + + + + public long getEstimatedTime() { + return estimatedTime; + } + + public void setEstimatedTime(long estimatedTime) { + this.estimatedTime = estimatedTime; + } +} diff --git a/racevisionGame/src/main/java/shared/model/CompoundMark.java b/racevisionGame/src/main/java/shared/model/CompoundMark.java new file mode 100644 index 00000000..17808ee3 --- /dev/null +++ b/racevisionGame/src/main/java/shared/model/CompoundMark.java @@ -0,0 +1,110 @@ +package shared.model; + + +/** + * Represents a compound mark - that is, either one or two individual marks which form a single compound mark. + */ +public class CompoundMark { + + /** + * The first mark in the compound mark. + */ + private Mark mark1; + + /** + * The second mark in the compound mark. + */ + private Mark mark2; + + /** + * The average coordinate of the compound mark. + */ + private GPSCoordinate averageGPSCoordinate; + + + /** + * Constructs a compound mark from a single mark. + * @param mark1 The individual mark that comprises this compound mark. + */ + public CompoundMark(Mark mark1) { + this.mark1 = mark1; + this.averageGPSCoordinate = calculateAverage(); + + } + + + /** + * Constructs a compound mark from a pair of marks. + * @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) { + this.mark1 = mark1; + this.mark2 = mark2; + this.averageGPSCoordinate = calculateAverage(); + + } + + + /** + * Returns the first mark of the compound mark. + * @return The first mark of the compound mark. + */ + public Mark getMark1() { + return mark1; + } + + /** + * Returns the second mark of the compound mark. + * @return The second mark of the compound mark. + */ + public Mark getMark2() { + return mark2; + } + + + /** + * Returns the position of the first mark in the compound mark. + * @return The position of the first mark in the compound mark. + */ + public GPSCoordinate getMark1Position() { + return mark1.getPosition(); + } + + /** + * Returns the position of the second mark in the compound mark. + * @return The position of the second mark in the compound mark. + */ + public GPSCoordinate getMark2Position() { + return mark2.getPosition(); + } + + + /** + * Returns the average coordinate of the compound mark. + * @return The average coordinate of the compound mark. + */ + public GPSCoordinate getAverageGPSCoordinate() { + return averageGPSCoordinate; + } + + + /** + * Calculates the average coordinate of the compound mark. + * @return The average coordinate of the compound mark. + */ + private GPSCoordinate calculateAverage() { + + //If the compound mark only contains one mark, the average is simply the first mark's position. + if (this.mark2 == null) { + return this.getMark1Position(); + } + + + //Otherwise, calculate the average of both marks. + GPSCoordinate averageCoordinate = GPSCoordinate.calculateAverageCoordinate(this.getMark1Position(), this.getMark2Position()); + + return averageCoordinate; + + } +} diff --git a/racevisionGame/src/main/java/shared/model/Constants.java b/racevisionGame/src/main/java/shared/model/Constants.java new file mode 100644 index 00000000..c71dcb64 --- /dev/null +++ b/racevisionGame/src/main/java/shared/model/Constants.java @@ -0,0 +1,49 @@ +package shared.model; + +/** + * Constants that are used throughout the program + * Created by Erika on 19-Mar-17. + */ +public class Constants { + + /** + * Multiply by this factor to convert nautical miles to meters. + *
+ * Divide by this factor to convert meters to nautical miles. + *
+ * 1 nautical mile = 1852 meters. + */ + public static final int NMToMetersConversion = 1852; + + + + /** + * Multiply by this factor to convert Knots to millimeters per second. + *
+ * Divide by this factor to convert millimeters per second to Knots. + *
+ * 1 knot = 514.444 millimeters per second. + */ + public static final double KnotsToMMPerSecond = 514.444; + + + /** + * The race pre-start time, in milliseconds. 3 minutes. + */ + public static final long RacePreStartTime = 3 * 60 * 1000; + + + /** + * The race preparatory time, in milliseconds. 1 minutes. + */ + public static final long RacePreparatoryTime = 1 * 60 * 1000; + + + /** + * The number of milliseconds in one hour. + */ + public static long OneHourMilliseconds = 1 * 60 * 60 * 1000; + + + +} diff --git a/racevisionGame/src/main/java/shared/model/GPSCoordinate.java b/racevisionGame/src/main/java/shared/model/GPSCoordinate.java new file mode 100644 index 00000000..5018e351 --- /dev/null +++ b/racevisionGame/src/main/java/shared/model/GPSCoordinate.java @@ -0,0 +1,516 @@ +package shared.model; + +import javafx.util.Pair; +import org.geotools.referencing.GeodeticCalculator; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; + +/** + * GPS Coordinate for the world map, containing a longitude and latitude. + * Created by esa46 on 15/03/17. + */ +public class GPSCoordinate { + + /** + * The latitude of the coordinate. + */ + private double latitude; + + /** + * The longitude of the coordinate. + */ + private double longitude; + + + + /** + * Constructs a GPSCoordinate from a latitude and longitude value. + * @param latitude Latitude the coordinate is located at. + * @param longitude Longitude that the coordinate is located at. + */ + public GPSCoordinate(double latitude, double longitude) { + this.latitude = latitude; + this.longitude = longitude; + } + + + + /** + * Gets the Latitude that the Coordinate is at. + * + * @return Returns the latitude of the Coordinate. + */ + public double getLatitude() { + return latitude; + } + + /** + * Gets the Longitude that the Coordinate is at. + * + * @return Returns the longitude of the Coordinate. + */ + public double getLongitude() { + return longitude; + } + + /** + * To String method of the Coordinate in the form Latitude: $f, Longitude: $f. + * + * @return A String representation of the GPSCoordinate Class. + */ + public String toString() { + return String.format("Latitude: %f, Longitude: %f", latitude, longitude); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + GPSCoordinate that = (GPSCoordinate) o; + + if (Math.abs(this.latitude - that.latitude) > 1e-7) return false; + return (Math.abs(this.longitude - that.longitude) < 1e-7); + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(latitude); + result = (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(longitude); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + + + /** + * Calculates min and max values and passed it to calculate if coordinate is in the boundary + * @param coordinate coordinate of interest + * @param boundary List of points which make a boundary + * @return true if coordinate is in the boundary + */ + public static boolean isInsideBoundary(GPSCoordinate coordinate, List boundary) { + int length = boundary.size(); + + boolean inside = false; + + // Check if inside using ray casting algorithm + for (int i = 0, j = length - 1; i < length; j = i++) { + if (intersects(boundary.get(i), boundary.get(j), coordinate)) { + inside = !inside; + } + + + } + return inside; + + } + + /** + * Calculates if the coordinate is in the boundary + * @param coordinate coordinate of interest + * @param boundary List of points which make a boundary + * @param minValues max GPS + * @param maxValues min GPS + * @return true if coordinate is in the boundary + */ + public static boolean isInsideBoundary(GPSCoordinate coordinate, List boundary, GPSCoordinate minValues, GPSCoordinate maxValues) { + double minLat = minValues.getLatitude(); + double minLon = minValues.getLongitude(); + double maxLat = maxValues.getLatitude(); + double maxLon = maxValues.getLongitude(); + double coordinateLat = coordinate.getLatitude(); + double coordinateLon = coordinate.getLongitude(); + // End computation early + if (coordinateLat <= minLat || coordinateLat >= maxLat || coordinateLon <= minLon || coordinateLon >= maxLon) { + return false; + } else { + return isInsideBoundary(coordinate, boundary); + } + } + + + /** + * Helper function to find if a point is in a boundary + * @param boundaryA The first coordinate of the boundary. + * @param boundaryB The second coordinate of the boundary. + * @param coordinate The coordinate to test. + * @return true if a line from the point intersects the two boundary points + */ + private static boolean intersects(GPSCoordinate boundaryA, GPSCoordinate boundaryB, GPSCoordinate coordinate) { + double boundaryALat = boundaryA.getLatitude(); + double boundaryALon = boundaryA.getLongitude(); + double boundaryBLat = boundaryB.getLatitude(); + double boundaryBLon = boundaryB.getLongitude(); + double coordinateLat = coordinate.getLatitude(); + double coordinateLon = coordinate.getLongitude(); + + if (boundaryALat > boundaryBLat) { + return intersects(boundaryB, boundaryA, coordinate); + } + if (coordinateLat == boundaryALat || coordinateLat == boundaryBLat) { + // Move coordinate off intersection line + coordinateLat += 1e-9; + } + if (coordinateLat > boundaryBLat || coordinateLat < boundaryALat || coordinateLon > Double.max(boundaryALon, boundaryBLon)) { + return false; + } + if (coordinateLon < Double.min(boundaryALon, boundaryBLon)) { + return true; + } + + double aRatio = (coordinateLat - boundaryALat) / (coordinateLon - boundaryALon); + double bRatio = (coordinateLat - boundaryBLat) / (coordinateLon - boundaryBLon); + return aRatio >= bRatio; + + } + + + + /** + * Calculates the azimuth between two points. + * @param start The starting point. + * @param end The ending point. + * @return The azimuth from the start point to the end point. + */ + public static Azimuth calculateAzimuth(GPSCoordinate start, GPSCoordinate end) { + + GeodeticCalculator calc = new GeodeticCalculator(); + + calc.setStartingGeographicPoint(start.getLongitude(), start.getLatitude()); + calc.setDestinationGeographicPoint(end.getLongitude(), end.getLatitude()); + + return Azimuth.fromDegrees(calc.getAzimuth()); + } + + + /** + * Calculates the bearing between two points. + * @param start The starting point. + * @param end The ending point. + * @return The Bearing from the start point to the end point. + */ + public static Bearing calculateBearing(GPSCoordinate start, GPSCoordinate end) { + + //Calculates the azimuth between the two points. + Azimuth azimuth = GPSCoordinate.calculateAzimuth(start, end); + + //And converts it into a bearing. + return Bearing.fromAzimuth(azimuth); + } + + + /** + * Calculates the distance, in meters, between two points. + * Note: all other distance calculations (e.g., nautical miles between two points) should use this, and convert from meters to their desired unit. + * @param start The starting point. + * @param end The ending point. + * @return The distance, in meters, between the two given points. + */ + public static double calculateDistanceMeters(GPSCoordinate start, GPSCoordinate end) { + + GeodeticCalculator calc = new GeodeticCalculator(); + + calc.setStartingGeographicPoint(start.getLongitude(), start.getLatitude()); + calc.setDestinationGeographicPoint(end.getLongitude(), end.getLatitude()); + + double distanceMeters = calc.getOrthodromicDistance(); + + return distanceMeters; + } + + + /** + * Calculates the distance, in nautical miles, between two points. + * @param start The starting point. + * @param end The ending point. + * @return The distance, in nautical miles, between the two given points. + */ + public static double calculateDistanceNauticalMiles(GPSCoordinate start, GPSCoordinate end) { + //Find distance in meters. + double distanceMeters = GPSCoordinate.calculateDistanceMeters(start, end); + + //Convert to nautical miles. + double distanceNauticalMiles = distanceMeters / Constants.NMToMetersConversion; + + return distanceNauticalMiles; + } + + + /** + * Calculates the GPS position an entity will be at, given a starting position, distance (in meters), and an azimuth. + * + * @param oldCoordinates GPS coordinates of the entity's starting position. + * @param distanceMeters The distance in meters. + * @param azimuth The entity's current azimuth. + * @return The entity's new coordinate. + */ + public static GPSCoordinate calculateNewPosition(GPSCoordinate oldCoordinates, double distanceMeters, Azimuth azimuth) { + + + GeodeticCalculator calc = new GeodeticCalculator(); + + //Set starting position. + calc.setStartingGeographicPoint(oldCoordinates.getLongitude(), oldCoordinates.getLatitude()); + + //Set direction. + calc.setDirection(azimuth.degrees(), distanceMeters); + + //Get the destination. + Point2D destinationPoint = calc.getDestinationGeographicPoint(); + + return new GPSCoordinate(destinationPoint.getY(), destinationPoint.getX()); + } + + + /** + * Calculates the average coordinate of two coordinates. + * @param point1 The first coordinate to average. + * @param point2 The second coordinate to average. + * @return The average of the two coordinates. + */ + public static GPSCoordinate calculateAverageCoordinate(GPSCoordinate point1, GPSCoordinate point2) { + + //Calculate distance between them. + double distanceMeters = GPSCoordinate.calculateDistanceMeters(point1, point2); + //We want the average, so get half the distance between points. + distanceMeters = distanceMeters / 2d; + + //Calculate azimuth between them. + Azimuth azimuth = GPSCoordinate.calculateAzimuth(point1, point2); + + //Calculate the middle coordinate. + GPSCoordinate middleCoordinate = GPSCoordinate.calculateNewPosition(point1, distanceMeters, azimuth); + + return middleCoordinate; + + } + + + /** + * Takes a list of GPS coordinates describing a course boundary, and "shrinks" it inwards by 50m. + * @param boundary The boundary of course. + * @return A copy of the course boundary list, shrunk inwards by 50m. + */ + public static List getShrinkBoundary(List boundary) { + + + double shrinkDistance = 50d; + List shrunkBoundary = new ArrayList<>(boundary.size()); + //This is a list of edges that have been shrunk/shifted inwards. + List> shrunkEdges = new ArrayList<>(); + + + //We need to invert some of our operations depending if the boundary is clockwise or anti-clockwise. + boolean isClockwise = GPSCoordinate.isClockwisePolygon(boundary); + double clockwiseScaleFactor = 0; + + if (isClockwise) { + clockwiseScaleFactor = 1; + } else { + clockwiseScaleFactor = -1; + } + + + /* + Starting at a vertex, face anti-clockwise along an adjacent edge. + Replace the edge with a new, parallel edge placed at distance d to the "left" of the old one. + Repeat for all edges. + Find the intersections of the new edges to get the new vertices. + Detect if you've become a crossed polynomial and decide what to do about it. Probably add a new vertex at the crossing-point and get rid of some old ones. I'm not sure whether there's a better way to detect this than just to compare every pair of non-adjacent edges to see if their intersection lies between both pairs of vertices. + */ + + //For the first (size-1) adjacent pairs. + for (int i = 0; i < (boundary.size() - 1); i++) { + + //Get the points. + GPSCoordinate firstPoint = boundary.get(i); + GPSCoordinate secondPoint = boundary.get(i + 1); + + //Get the bearing between two adjacent points. + Bearing bearing = GPSCoordinate.calculateBearing(firstPoint, secondPoint); + + //Calculate angle perpendicular to bearing. + Bearing perpendicularBearing = Bearing.fromDegrees(bearing.degrees() + (90d * clockwiseScaleFactor)); + + //Translate both first and second point by 50m, using this bearing. These form our inwards shifted edge. + GPSCoordinate firstPointTranslated = GPSCoordinate.calculateNewPosition(firstPoint, shrinkDistance, Azimuth.fromBearing(perpendicularBearing)); + GPSCoordinate secondPointTranslated = GPSCoordinate.calculateNewPosition(secondPoint, shrinkDistance, Azimuth.fromBearing(perpendicularBearing)); + + //Add edge to list. + shrunkEdges.add(new Pair<>(firstPointTranslated, secondPointTranslated)); + + } + + //For the final adjacent pair, between the last and first point. + //Get the points. + GPSCoordinate firstPoint = boundary.get(boundary.size() - 1); + GPSCoordinate secondPoint = boundary.get(0); + + //Get the bearing between two adjacent points. + Bearing bearing = GPSCoordinate.calculateBearing(firstPoint, secondPoint); + + //Calculate angle perpendicular to bearing. + Bearing perpendicularBearing = Bearing.fromDegrees(bearing.degrees() + (90d * clockwiseScaleFactor)); + + //Translate both first and second point by 50m, using this bearing. These form our inwards shifted edge. + GPSCoordinate firstPointTranslated = GPSCoordinate.calculateNewPosition(firstPoint, shrinkDistance, Azimuth.fromBearing(perpendicularBearing)); + GPSCoordinate secondPointTranslated = GPSCoordinate.calculateNewPosition(secondPoint, shrinkDistance, Azimuth.fromBearing(perpendicularBearing)); + + //Add edge to list. + shrunkEdges.add(new Pair<>(firstPointTranslated, secondPointTranslated)); + + + //We now have a list of edges that have been shifted inwards. + //We need to find the intersections between adjacent vertices in our edge list. E.g., intersection between edge1-right, and edge2-left. + + //For the first (size-1) adjacent pairs. + for (int i = 0; i < (shrunkEdges.size() - 1); i++) { + + //Get the pair of adjacent edges. + Pair edge1 = shrunkEdges.get(i); + Pair edge2 = shrunkEdges.get(i + 1); + + //Get the x and y coordinates of first edge. + double x1 = edge1.getKey().getLongitude(); + double x2 = edge1.getValue().getLongitude(); + double y1 = edge1.getKey().getLatitude(); + double y2 = edge1.getValue().getLatitude(); + + //Get the x and y coordinates of second edge. + double x3 = edge2.getKey().getLongitude(); + double x4 = edge2.getValue().getLongitude(); + double y3 = edge2.getKey().getLatitude(); + double y4 = edge2.getValue().getLatitude(); + + //Find the equations for both edges. + // y = a*x + b + //First equation. + double a1 = (y2 - y1) / (x2 - x1); + double b1 = y1 - a1 * x1; + + //Second equation. + double a2 = (y4 - y3) / (x4 - x3); + double b2 = y3 - a2 * x3; + + + //Find intersecting x coordinate. + // a1 * x + b1 = a2 * x + b2 + double x0 = -(b1 - b2) / (a1 - a2); + //Find intersecting y coordinate. + double y0 = x0 * a1 + b1; + + //Add this to shrunk boundary list. + GPSCoordinate coordinate = new GPSCoordinate(y0, x0); + shrunkBoundary.add(coordinate); + + } + + + //For the final adjacent pair, between the last and first point. + //Get the pair of adjacent edges. + Pair edge1 = shrunkEdges.get(shrunkEdges.size() - 1); + Pair edge2 = shrunkEdges.get(0); + + //Get the x and y coordinates of first edge. + double x1 = edge1.getKey().getLongitude(); + double x2 = edge1.getValue().getLongitude(); + double y1 = edge1.getKey().getLatitude(); + double y2 = edge1.getValue().getLatitude(); + + //Get the x and y coordinates of second edge. + double x3 = edge2.getKey().getLongitude(); + double x4 = edge2.getValue().getLongitude(); + double y3 = edge2.getKey().getLatitude(); + double y4 = edge2.getValue().getLatitude(); + + //Find the equations for both edges. + // y = a*x + b + //First equation. + double a1 = (y2 - y1) / (x2 - x1); + double b1 = y1 - a1 * x1; + + //Second equation. + double a2 = (y4 - y3) / (x4 - x3); + double b2 = y3 - a2 * x3; + + + //Find intersecting x coordinate. + // a1 * x + b1 = a2 * x + b2 + double x0 = -(b1 - b2) / (a1 - a2); + //Find intersecting y coordinate. + double y0 = x0 * a1 + b1; + + //Add this to shrunk boundary list. + GPSCoordinate coordinate = new GPSCoordinate(y0, x0); + shrunkBoundary.add(coordinate); + + + + return shrunkBoundary; + + } + + + /** + * Determines if a list of coordinates describes a boundary polygon in clockwise or anti-clockwise order. + * @param boundary The list of coordinates. + * @return True if clockwise, false if anti-clockwise. + */ + public static boolean isClockwisePolygon(List boundary) { + + /* From https://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order + sum all pairs (x2 − x1)(y2 + y1) + point[0] = (5,0) edge[0]: (6-5)(4+0) = 4 + point[1] = (6,4) edge[1]: (4-6)(5+4) = -18 + point[2] = (4,5) edge[2]: (1-4)(5+5) = -30 + point[3] = (1,5) edge[3]: (1-1)(0+5) = 0 + point[4] = (1,0) edge[4]: (5-1)(0+0) = 0 + --- + -44 counter-clockwise + */ + + double sum = 0; + + //For the first (size-1) adjacent pairs. + for (int i = 0; i < (boundary.size() - 1); i++) { + + //Get the points. + GPSCoordinate firstPoint = boundary.get(i); + GPSCoordinate secondPoint = boundary.get(i + 1); + + double xDelta = secondPoint.getLongitude() - firstPoint.getLongitude(); + double ySum = secondPoint.getLatitude() + firstPoint.getLatitude(); + + double product = xDelta * ySum; + + sum += product; + + } + + //For the final adjacent pair, between the last and first point. + //Get the points. + GPSCoordinate firstPoint = boundary.get(boundary.size() - 1); + GPSCoordinate secondPoint = boundary.get(0); + + double xDelta = secondPoint.getLongitude() - firstPoint.getLongitude(); + double ySum = secondPoint.getLatitude() + firstPoint.getLatitude(); + + double product = xDelta * ySum; + + sum += product; + + + //sum > 0 is clockwise, sum < 0 is anticlockwise. + return sum > 0; + + + } + +} + diff --git a/racevisionGame/src/main/java/shared/model/Leg.java b/racevisionGame/src/main/java/shared/model/Leg.java new file mode 100644 index 00000000..d9233f62 --- /dev/null +++ b/racevisionGame/src/main/java/shared/model/Leg.java @@ -0,0 +1,128 @@ +package shared.model; + + +/** + * Leg of the race, this is what each part of the race is divided into, from mark to mark. + */ +public class Leg { + + /** + * The name of the leg. + */ + private String name; + + /** + * The distance of the leg, in nautical miles. + */ + private double distanceNauticalMiles; + + /** + * The starting marker of the leg. + */ + private CompoundMark startCompoundMark; + + /** + * The ending marking of the leg. + */ + private CompoundMark endCompoundMark; + + /** + * The leg number within a race. + */ + private int legNumber; + + + + /** + * Constructs a leg from a name, start marker, end marker, and leg number. + * + * @param name Name of the Leg. + * @param start Starting marker of the leg. + * @param end Ending marker of the leg. + * @param number Leg's position within the race. + */ + public Leg(String name, CompoundMark start, CompoundMark end, int number) { + this.name = name; + this.startCompoundMark = start; + this.endCompoundMark = end; + this.legNumber = number; + this.calculateLegDistance(); + } + + + /** + * Constructs a leg from a name and leg number. + * This is currently used for constructing "dummy" DNF and Finish legs. + * + * @param name Name of the leg. + * @param number Leg's position within the race. + */ + public Leg(String name, int number) { + this.name = name; + this.legNumber = number; + } + + + /** + * Returns the name of the Leg. + * @return The name of the Leg. + */ + public String getName() { + return name; + } + + + /** + * Get the distance in nautical miles. + * @return The total distance of the leg. + */ + public double getDistanceNauticalMiles() { + return distanceNauticalMiles; + } + + + /** + * Returns the leg number of the leg within a race. + * @return The leg number of the leg within a race + */ + public int getLegNumber() { + return legNumber; + } + + + /** + * Returns the starting marker of the leg. + * @return The starting marker of the leg. + */ + public CompoundMark getStartCompoundMark() { + return startCompoundMark; + } + + + /** + * Returns the ending marker of the leg. + * @return The ending marker of the leg. + */ + public CompoundMark getEndCompoundMark() { + return endCompoundMark; + } + + + + /** + * Calculates the distance of the leg, in nautical miles. + */ + public void calculateLegDistance() { + + //Gets the start and end coordinates. + GPSCoordinate startMarker = this.startCompoundMark.getAverageGPSCoordinate(); + GPSCoordinate endMarker = this.endCompoundMark.getAverageGPSCoordinate(); + + //Calculates the distance between markers. + double distanceNauticalMiles = GPSCoordinate.calculateDistanceNauticalMiles(startMarker, endMarker); + + this.distanceNauticalMiles = distanceNauticalMiles; + } + + +} diff --git a/racevisionGame/src/main/java/shared/model/Mark.java b/racevisionGame/src/main/java/shared/model/Mark.java new file mode 100644 index 00000000..da9308de --- /dev/null +++ b/racevisionGame/src/main/java/shared/model/Mark.java @@ -0,0 +1,64 @@ +package shared.model; + +/** + * Represents an individual mark. + * Has a source ID, name, and position. + */ +public class Mark { + + /** + * The source ID of the mark. + */ + private int sourceID; + + /** + * The name of the mark. + */ + private String name; + + /** + * The position of the mark. + */ + private GPSCoordinate position; + + + + /** + * Constructs a mark with a given source ID, name, and position. + * @param sourceID The source ID of the mark. + * @param name The name of the mark. + * @param position The position of the mark. + */ + public Mark(int sourceID, String name, GPSCoordinate position) { + this.sourceID = sourceID; + this.name = name; + this.position = position; + } + + + /** + * Returns the name of the mark. + * @return The name of the mark. + */ + public String getName() { + return name; + } + + /** + * Returns the source ID of the mark. + * @return The source ID of the mark. + */ + public int getSourceID() { + return sourceID; + } + + /** + * Returns the position of the mark. + * @return The position of the mark. + */ + public GPSCoordinate getPosition() { + return position; + } + + +} diff --git a/racevisionGame/src/main/java/visualiser/model/TrackPoint.java b/racevisionGame/src/main/java/visualiser/model/TrackPoint.java new file mode 100644 index 00000000..f994f32c --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/TrackPoint.java @@ -0,0 +1,61 @@ +package visualiser.model; + + +import shared.model.GPSCoordinate; + +/** + * A TrackPoint is a point plotted to display the track a + * {@link VisualiserBoat Boat} has travelled in a race.
+ * TrackPoints are displayed on a + * {@link seng302.Model.ResizableRaceCanvas ResizableRaceCanvas}, via the + * {@link seng302.Controllers.RaceController RaceController}.
+ * Track points can be made visible or hidden via the RaceController's + * {@link seng302.Model.Annotations Annotations}. + */ +public class TrackPoint { + private final GPSCoordinate coordinate; + private final long timeAdded; + private final long expiry; + private final double minAlpha; + + /** + * Creates a new track point with fixed GPS coordinates and time, to reach minimum opacity on expiry. + * + * @param coordinate position of point on physical race map + * @param timeAdded system clock at time of addition + * @param expiry time to minimum opacity after added + */ + public TrackPoint(GPSCoordinate coordinate, long timeAdded, long expiry) { + this.coordinate = coordinate; + this.timeAdded = timeAdded; + this.expiry = expiry; + this.minAlpha = 0.1; + } + + /** + * Gets the position of the point on physical race map. + * + * @return GPS coordinate of point + */ + public GPSCoordinate getCoordinate() { + return coordinate; + } + + /** + * Gets opacity of point scaled by age in proportion to expiry, between 1 and minimum opacity inclusive. + * + * @return greater of minimum opacity and scaled opacity + */ + public double getAlpha() { + return Double.max(minAlpha, 1.0 - (double) (System.currentTimeMillis() - timeAdded) / expiry); + } + + /** + * Gets time point was added to track. + * + * @return system clock at time of addition + */ + public long getTimeAdded() { + return timeAdded; + } +} diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java new file mode 100644 index 00000000..01e86b42 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java @@ -0,0 +1,124 @@ +package visualiser.model; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import network.Messages.Enums.BoatStatusEnum; +import org.geotools.referencing.GeodeticCalculator; +import shared.model.Boat; +import shared.model.GPSCoordinate; + +import java.awt.geom.Point2D; +import java.time.ZonedDateTime; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Represents a Boat on the visualiser side of a race. + * 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}. + */ +public class VisualiserBoat extends Boat { + + + private final Queue track = new ConcurrentLinkedQueue<>(); + + private long nextValidTime = 0; + + private ZonedDateTime timeSinceLastMark; + + /** + * 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. + */ + public VisualiserBoat(int sourceID, String name, String abbrev) { + super(sourceID, name, abbrev); + + } + + + /** + * Returns the position of the end of the boat's wake, which is 180 degrees + * from the boat's heading, and whose length is proportional to the boat's + * speed. + * + * @return GPSCoordinate of wake endpoint. + */ + public GPSCoordinate getWake() { + double reverseHeading = getBearing().degrees() - 180; + double wakeScale = 5; + double distance = wakeScale * getCurrentSpeed(); + + GeodeticCalculator calc = new GeodeticCalculator(); + calc.setStartingGeographicPoint( + new Point2D.Double(getCurrentPosition().getLongitude(), getCurrentPosition().getLatitude()) + ); + calc.setDirection(reverseHeading, distance); + Point2D endpoint = calc.getDestinationGeographicPoint(); + return new GPSCoordinate(endpoint.getY(), endpoint.getX()); + } + + /** + * Adds a new point to boat's track. + * @param coordinate of point on track + * @see seng302.Model.TrackPoint + */ + public void addTrackPoint(GPSCoordinate coordinate) { + Boolean added = System.currentTimeMillis() >= nextValidTime; + long currentTime = System.currentTimeMillis(); + if (added && (this.getStatus() == BoatStatusEnum.RACING)) { + float trackPointTimeInterval = 5000; + nextValidTime = currentTime + (long) trackPointTimeInterval; + int TRACK_POINT_LIMIT = 10; + track.add(new TrackPoint(coordinate, currentTime, TRACK_POINT_LIMIT * (long) trackPointTimeInterval)); + } + } + + /** + * Returns the boat's sampled track between start of race and current time. + * @return queue of track points + * @see seng302.Model.TrackPoint + */ + public Queue getTrack() { + return track; + } + + + /** + * Print method prints the name of the boat + * + * @return Name of the boat. + */ + public String toString() { + return getName(); + } + + public ZonedDateTime getTimeSinceLastMark() { + return timeSinceLastMark; + } + + public void setTimeSinceLastMark(ZonedDateTime timeSinceLastMark) { + this.timeSinceLastMark = timeSinceLastMark; + } + + + public String getFormattedEstTime() { + if (getEstimatedTime() < 0) { + return " -"; + } + if (getEstimatedTime() <= 60) { + return " " + getEstimatedTime() + "s"; + } else { + long seconds = getEstimatedTime() % 60; + long minutes = (getEstimatedTime() - seconds) / 60; + return String.format(" %dm %ds", minutes, seconds); + } + + + } +} diff --git a/sharedModel/src/main/java/seng302/App.java b/sharedModel/src/main/java/seng302/App.java deleted file mode 100644 index 3f8ee22d..00000000 --- a/sharedModel/src/main/java/seng302/App.java +++ /dev/null @@ -1,13 +0,0 @@ -package seng302; - -/** - * Created by f123 on 27-Apr-17. - */ -public class App { - - public static void main(String[] args) { - - } - - -} From d0d63ca236051a82ef3bfa75b56cad11e7d36924 Mon Sep 17 00:00:00 2001 From: fjc40 Date: Tue, 4 Jul 2017 16:22:25 +1200 Subject: [PATCH 02/25] Copied remaining files into appropriate package. These need to be refactored and put into the shared package. --- .../src/main/java/mock/app/App.java | 73 +++ .../src/main/java/mock/app/Event.java | 80 ++++ .../src/main/java/mock/app/MockOutput.java | 254 ++++++++++ .../java/mock/dataInput/BoatDataSource.java | 14 + .../java/mock/dataInput/BoatXMLReader.java | 149 ++++++ .../java/mock/dataInput/RaceDataSource.java | 32 ++ .../java/mock/dataInput/RaceXMLReader.java | 287 ++++++++++++ .../main/java/mock/dataInput/XMLReader.java | 115 +++++ .../exceptions/InvalidBoatDataException.java | 14 + .../exceptions/InvalidRaceDataException.java | 13 + .../StreamedCourseXMLException.java | 7 + .../Controllers/ConnectionController.java | 97 ++++ .../visualiser/Controllers/Controller.java | 32 ++ .../Controllers/FinishController.java | 64 +++ .../Controllers/MainController.java | 60 +++ .../Controllers/RaceController.java | 199 ++++++++ .../Controllers/StartController.java | 212 +++++++++ .../src/main/java/visualiser/app/App.java | 38 ++ .../java/visualiser/app/VisualiserInput.java | 432 ++++++++++++++++++ .../visualiser/dataInput/BoatXMLReader.java | 156 +++++++ .../visualiser/dataInput/RaceDataSource.java | 26 ++ .../dataInput/RegattaXMLReader.java | 165 +++++++ .../dataInput/StreamedCourseXMLReader.java | 290 ++++++++++++ .../java/visualiser/dataInput/XMLReader.java | 52 +++ .../StreamedCourseXMLException.java | 7 + .../java/visualiser/model/Annotations.java | 284 ++++++++++++ .../visualiser/model/GraphCoordinate.java | 42 ++ .../main/java/visualiser/model/RaceClock.java | 133 ++++++ .../java/visualiser/model/RaceConnection.java | 57 +++ .../main/java/visualiser/model/RaceMap.java | 76 +++ .../visualiser/model/ResizableCanvas.java | 55 +++ .../visualiser/model/ResizableRaceCanvas.java | 392 ++++++++++++++++ .../visualiser/model/ResizableRaceMap.java | 93 ++++ .../main/java/visualiser/model/Sparkline.java | 180 ++++++++ .../java/visualiser/model/StreamedCourse.java | 106 +++++ .../java/visualiser/model/StreamedRace.java | 285 ++++++++++++ 36 files changed, 4571 insertions(+) create mode 100644 racevisionGame/src/main/java/mock/app/App.java create mode 100644 racevisionGame/src/main/java/mock/app/Event.java create mode 100644 racevisionGame/src/main/java/mock/app/MockOutput.java create mode 100644 racevisionGame/src/main/java/mock/dataInput/BoatDataSource.java create mode 100644 racevisionGame/src/main/java/mock/dataInput/BoatXMLReader.java create mode 100644 racevisionGame/src/main/java/mock/dataInput/RaceDataSource.java create mode 100644 racevisionGame/src/main/java/mock/dataInput/RaceXMLReader.java create mode 100644 racevisionGame/src/main/java/mock/dataInput/XMLReader.java create mode 100644 racevisionGame/src/main/java/mock/exceptions/InvalidBoatDataException.java create mode 100644 racevisionGame/src/main/java/mock/exceptions/InvalidRaceDataException.java create mode 100644 racevisionGame/src/main/java/mock/exceptions/StreamedCourseXMLException.java create mode 100644 racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java create mode 100644 racevisionGame/src/main/java/visualiser/Controllers/Controller.java create mode 100644 racevisionGame/src/main/java/visualiser/Controllers/FinishController.java create mode 100644 racevisionGame/src/main/java/visualiser/Controllers/MainController.java create mode 100644 racevisionGame/src/main/java/visualiser/Controllers/RaceController.java create mode 100644 racevisionGame/src/main/java/visualiser/Controllers/StartController.java create mode 100644 racevisionGame/src/main/java/visualiser/app/App.java create mode 100644 racevisionGame/src/main/java/visualiser/app/VisualiserInput.java create mode 100644 racevisionGame/src/main/java/visualiser/dataInput/BoatXMLReader.java create mode 100644 racevisionGame/src/main/java/visualiser/dataInput/RaceDataSource.java create mode 100644 racevisionGame/src/main/java/visualiser/dataInput/RegattaXMLReader.java create mode 100644 racevisionGame/src/main/java/visualiser/dataInput/StreamedCourseXMLReader.java create mode 100644 racevisionGame/src/main/java/visualiser/dataInput/XMLReader.java create mode 100644 racevisionGame/src/main/java/visualiser/exceptions/StreamedCourseXMLException.java create mode 100644 racevisionGame/src/main/java/visualiser/model/Annotations.java create mode 100644 racevisionGame/src/main/java/visualiser/model/GraphCoordinate.java create mode 100644 racevisionGame/src/main/java/visualiser/model/RaceClock.java create mode 100644 racevisionGame/src/main/java/visualiser/model/RaceConnection.java create mode 100644 racevisionGame/src/main/java/visualiser/model/RaceMap.java create mode 100644 racevisionGame/src/main/java/visualiser/model/ResizableCanvas.java create mode 100644 racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java create mode 100644 racevisionGame/src/main/java/visualiser/model/ResizableRaceMap.java create mode 100644 racevisionGame/src/main/java/visualiser/model/Sparkline.java create mode 100644 racevisionGame/src/main/java/visualiser/model/StreamedCourse.java create mode 100644 racevisionGame/src/main/java/visualiser/model/StreamedRace.java diff --git a/racevisionGame/src/main/java/mock/app/App.java b/racevisionGame/src/main/java/mock/app/App.java new file mode 100644 index 00000000..97ecf8d5 --- /dev/null +++ b/racevisionGame/src/main/java/mock/app/App.java @@ -0,0 +1,73 @@ +package mock.app; + + +import javafx.application.Application; +import javafx.stage.Stage; +import mock.dataInput.PolarParser; +import mock.model.Polars; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public class App extends Application { + + /** + * Entry point for running the programme + * + * @param args for starting the programme + */ + public static void main(String[] args) { + launch(args); + } + + @Override + public void start(Stage primaryStage) { + try { + Polars boatPolars = PolarParser.parse("polars/acc_polars.csv"); + + String regattaXML = readFile("mockXML/regattaTest.xml", StandardCharsets.UTF_8); + String raceXML = readFile("mockXML/raceTest.xml", StandardCharsets.UTF_8); + String boatXML = readFile("mockXML/boatTest.xml", StandardCharsets.UTF_8); + + Event raceEvent = new Event(raceXML, regattaXML, boatXML, boatPolars); + raceEvent.start(); + + } catch (Exception e) { + //Catch all exceptions, print, and exit. + e.printStackTrace(); + System.exit(1); + } + } + + /** + * Reads the Initial Race XML files that are necessary to run the mock. + * @param path path of the XML + * @param encoding encoding of the xml + * @return + * @throws IOException No file etc + * @throws ParserConfigurationException Issue with the XML formatting + * @throws SAXException Issue with XML formatting + * @throws TransformerException Issue with the XML format + */ + private String readFile(String path, Charset encoding) throws IOException, ParserConfigurationException, SAXException, TransformerException { + + InputSource fXmlFile = new InputSource(getClass().getClassLoader().getResourceAsStream(path)); + + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + Document doc = dBuilder.parse(fXmlFile); + doc.getDocumentElement().normalize(); + + return XMLReader.getContents(doc); + + } + +} diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java new file mode 100644 index 00000000..213966a6 --- /dev/null +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -0,0 +1,80 @@ +package mock.app; + +import mock.model.Polars; +import network.Messages.Enums.MessageType; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.text.ParseException; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + + +/** + * A Race Event, this holds all of the race's information as well as handling the connection to its clients. + */ +public class Event { + + String raceXML; + String regattaXML; + String boatXML; + Polars boatPolars; + MockOutput mockOutput; + + public Event(String raceXML, String regattaXML, String boatXML, Polars boatPolars) { + + this.raceXML = getRaceXMLAtCurrentTime(raceXML); + this.boatXML = boatXML; + this.regattaXML = regattaXML; + this.boatPolars = boatPolars; + try { + mockOutput = new MockOutput(); + new Thread(mockOutput).start(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Sends the initial race data and then begins race simulation + */ + public void start() { + try { + sendXMLs(); + Race newRace = new Race(new RaceXMLReader(this.raceXML, new BoatXMLReader(boatXML, this.boatPolars)), mockOutput); + new Thread((newRace)).start(); + + } catch (ParserConfigurationException | IOException | SAXException | ParseException | StreamedCourseXMLException e) { + e.printStackTrace(); + } + } + + /** + * Sends out each xml string, via the mock output + */ + private void sendXMLs() { + + mockOutput.setRegattaXml(regattaXML); + mockOutput.parseXMLString(regattaXML, MessageType.XMLMESSAGE.getValue()); + + mockOutput.setRaceXml(raceXML); + mockOutput.parseXMLString(raceXML, MessageType.XMLMESSAGE.getValue()); + + mockOutput.setBoatsXml(boatXML); + mockOutput.parseXMLString(boatXML, MessageType.XMLMESSAGE.getValue()); + } + + /** + * Sets the xml description of the race to show the race was created now, and starts in 3 minutes + * @param raceXML + * @return String containing edited xml + */ + private String getRaceXMLAtCurrentTime(String raceXML) { + 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))); + } + +} diff --git a/racevisionGame/src/main/java/mock/app/MockOutput.java b/racevisionGame/src/main/java/mock/app/MockOutput.java new file mode 100644 index 00000000..e0a03f80 --- /dev/null +++ b/racevisionGame/src/main/java/mock/app/MockOutput.java @@ -0,0 +1,254 @@ +package mock.app; + + + +import network.BinaryMessageEncoder; +import network.MessageEncoders.RaceVisionByteEncoder; +import network.MessageEncoders.XMLMessageEncoder; +import network.Messages.BoatLocation; +import network.Messages.Enums.MessageType; +import network.Messages.RaceStatus; +import network.Messages.XMLMessage; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.concurrent.ArrayBlockingQueue; + +/** + * TCP server to send race information to connected clients. + */ +public class MockOutput implements Runnable +{ + ///Timestamp of the last sent heartbeat message. + private long lastHeartbeatTime; + + ///Period for the heartbeat - that is, how often we send it. + private double heartbeatPeriod = 5.0; + + ///Port to expose server on. + private int serverPort = 4942; + ///Socket used to listen for clients on. + private ServerSocket serverSocket; + ///Socket used to communicate with a client. + private Socket mockSocket; + ///Output stream which wraps around mockSocket outstream. + private DataOutputStream outToVisualiser; + + ///A queue that contains items that are waiting to be sent. + private ArrayBlockingQueue messagesToSendQueue = new ArrayBlockingQueue<>(99999999); + + ///Sequence numbers used in messages. + private short messageNumber = 1; + private short xmlSequenceNumber = 1; + private int heartbeatSequenceNum = 1; + private int boatLocationSequenceNumber = 1; + private int raceStatusSequenceNumber = 1; + + ///Strings containing XML data as strings. + private String raceXml; + private String regattaXml; + private String boatsXml; + + private boolean stop = false; //whether or not hte thread keeps running + + /** + * Ctor. + * @throws IOException if server socket cannot be opened. + */ + public MockOutput() throws IOException { + lastHeartbeatTime = System.currentTimeMillis(); + serverSocket = new ServerSocket(serverPort); + + } + /** + * Calculates the time since last heartbeat message, in seconds. + * @return Time since last heartbeat message, in seconds. + */ + private double timeSinceHeartbeat() { + long now = System.currentTimeMillis(); + return (now - lastHeartbeatTime) / 1000.0; + } + + //returns the heartbeat message + + /** + * Increment the heartbeat value + * @return message for heartbeat data + */ + private byte[] heartbeat(){ + byte[] heartbeatMessage = RaceVisionByteEncoder.heartBeat(heartbeatSequenceNum); + heartbeatSequenceNum++; + BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(MessageType.HEARTBEAT, System.currentTimeMillis(), messageNumber, (short)heartbeatMessage.length, heartbeatMessage); + messageNumber++; + return binaryMessageEncoder.getFullMessage(); + } + + /** + * Used to give the mockOutput an xml string to be made into a message and sent + * @param xmlString the xml string to send + * @param messageType the kind of xml string, values given in AC35 spec (5 regatta, 6 race, 7 boat) + */ + public synchronized void parseXMLString(String xmlString, int messageType){ + XMLMessageEncoder encoder = new XMLMessageEncoder(messageNumber, System.currentTimeMillis(), messageType, xmlSequenceNumber,(short) xmlString.length(), xmlString); + //iterates the sequence numbers + xmlSequenceNumber++; + byte[] encodedXML = encoder.encode(); + + BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(MessageType.XMLMESSAGE, System.currentTimeMillis(), messageNumber, (short)encodedXML.length, encodedXML); + //iterates the message number + messageNumber++; + + addMessageToBufferToSend(binaryMessageEncoder.getFullMessage()); + } + + /** + * Used to give the mocOutput information about boat location to be made into a message and sent + * @param sourceID id of the boat + * @param lat latitude of boat + * @param lon longitude of boat + * @param heading heading of boat + * @param speed speed of boat + * @param time historical time of race + */ + public synchronized void parseBoatLocation(int sourceID, double lat, double lon, double heading, double speed, long time){ + + BoatLocation boatLocation = new BoatLocation(sourceID, lat, lon, boatLocationSequenceNumber, heading, speed, time); + //iterates the sequence number + boatLocationSequenceNumber++; + + //encodeds the messages + byte[] encodedBoatLoc = RaceVisionByteEncoder.boatLocation(boatLocation); + + //encodeds the full message with header + BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(MessageType.BOATLOCATION, System.currentTimeMillis(), messageNumber, (short)encodedBoatLoc.length, + encodedBoatLoc); + + //iterates the message number + messageNumber++; + + addMessageToBufferToSend(binaryMessageEncoder.getFullMessage()); + } + + /** + * Parse the race status data and add it to the buffer to be sent + * @param raceStatus race status to parses + */ + public synchronized void parseRaceStatus(RaceStatus raceStatus){ + + //iterates the sequence number + raceStatusSequenceNumber++; + + //encodeds the messages + byte[] encodedRaceStatus = RaceVisionByteEncoder.raceStatus(raceStatus); + + //encodeds the full message with header + BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(MessageType.RACESTATUS, System.currentTimeMillis(), messageNumber, (short)encodedRaceStatus.length, + encodedRaceStatus); + + //iterates the message number + messageNumber++; + + addMessageToBufferToSend(binaryMessageEncoder.getFullMessage()); + + } + + /** + * Add a message to the buffer to be sent + * @param messagesToSendBuffer message to add to the buffer + */ + private synchronized void addMessageToBufferToSend(byte[] messagesToSendBuffer) { + this.messagesToSendQueue.add(messagesToSendBuffer); + } + + /** + * Sending loop of the Server + */ + public void run() { + + try { + while (!stop){ + System.out.println("Waiting for a connection...");//TEMP DEBUG REMOVE + mockSocket = serverSocket.accept(); + + outToVisualiser = new DataOutputStream(mockSocket.getOutputStream()); + + if (boatsXml == null || regattaXml == null || raceXml == null){ + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + continue; + } + + parseXMLString(raceXml, XMLMessage.XMLTypeRace); + parseXMLString(regattaXml, XMLMessage.XMLTypeRegatta); + parseXMLString(boatsXml, XMLMessage.XMLTypeBoat); + + + while(true) { + try { + //Sends a heartbeat every so often. + if (timeSinceHeartbeat() >= heartbeatPeriod) { + outToVisualiser.write(heartbeat()); + lastHeartbeatTime = System.currentTimeMillis(); + } + + //Checks the buffer to see if there is anything to send. + while (messagesToSendQueue.size() > 0) { + //Grabs message from head of queue. + byte[] binaryMessage = messagesToSendQueue.remove(); + + //sends the message to the visualiser + outToVisualiser.write(binaryMessage); + + } + }catch(SocketException e){ + break; + } + + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void stop(){ + stop = true; + } + + /** + * Sets the Race XML to send + * @param raceXml XML to send to the CLient + */ + public void setRaceXml(String raceXml) { + this.raceXml = raceXml; + } + + /** + * Sets the Regatta XMl to send + * @param regattaXml XML to send to CLient + */ + public void setRegattaXml(String regattaXml) { + this.regattaXml = regattaXml; + } + + /** + * Sets the Boats XML to send + * @param boatsXml XMl to send to the CLient + */ + public void setBoatsXml(String boatsXml) { + this.boatsXml = boatsXml; + } + + public static void main(String argv[]) throws Exception + { + MockOutput client = new MockOutput(); + client.run(); + } + +} diff --git a/racevisionGame/src/main/java/mock/dataInput/BoatDataSource.java b/racevisionGame/src/main/java/mock/dataInput/BoatDataSource.java new file mode 100644 index 00000000..92ac15b1 --- /dev/null +++ b/racevisionGame/src/main/java/mock/dataInput/BoatDataSource.java @@ -0,0 +1,14 @@ +package mock.dataInput; + +import seng302.Model.Boat; +import seng302.Model.Mark; + +import java.util.Map; + +/** + * Boats Data + */ +public interface BoatDataSource { + Map getBoats(); + Map getMarkerBoats(); +} diff --git a/racevisionGame/src/main/java/mock/dataInput/BoatXMLReader.java b/racevisionGame/src/main/java/mock/dataInput/BoatXMLReader.java new file mode 100644 index 00000000..949c6b23 --- /dev/null +++ b/racevisionGame/src/main/java/mock/dataInput/BoatXMLReader.java @@ -0,0 +1,149 @@ +package mock.dataInput; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; +import seng302.Model.Boat; +import seng302.Model.GPSCoordinate; +import seng302.Model.Mark; +import seng302.Model.Polars; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Xml Reader class for Boat XML used for the race + */ +public class BoatXMLReader extends XMLReader implements BoatDataSource { + + private final Map boatMap = new HashMap<>(); + private final Map markerMap = new HashMap<>(); + + /** + * Polars table to assign to each boat. + */ + Polars boatPolars; + + + /** + * Constructor for Boat XML + * + * @param filePath Name/path of file to read. Read as a resource. + * @param boatPolars polars used by the boats + * @throws IOException error + * @throws SAXException error + * @throws ParserConfigurationException error + */ + public BoatXMLReader(String filePath, Polars boatPolars) throws IOException, SAXException, ParserConfigurationException { + super(filePath); + this.boatPolars = boatPolars; + read(); + } + + /** + * Read the XML + */ + public void read() { + readSettings(); + readShapes(); + readBoats(); + } + + /** + * Read the Boats + */ + private void readBoats() { + Element nBoats = (Element) doc.getElementsByTagName("Boats").item(0); + for (int i = 0; i < nBoats.getChildNodes().getLength(); i++) { + Node boat = nBoats.getChildNodes().item(i); + if (boat.getNodeName().equals("Boat")) { + readBoatNode(boat); + } + } + } + + /** + * Ignored data + */ + private void readShapes() { + + } + + /** + * Ignored data + */ + private void readSettings() { + + } + + /** + * Checks if the node (XMl data node) is a Yacht or not + * @param boatNode Node from the XML + * @return Whether the node is a yacht node or not + */ + private boolean isYachtNode(Node boatNode) { + return boatNode.getAttributes().getNamedItem("Type").getTextContent().toLowerCase().equals("yacht"); + } + + /** + * Reads the information about one boat + * Ignored values: ShapeID, StoweName, HullNum, Skipper, Type + */ + private void readBoatNode(Node boatNode) { + int sourceID = Integer.parseInt(boatNode.getAttributes().getNamedItem("SourceID").getTextContent()); + String name = boatNode.getAttributes().getNamedItem("BoatName").getTextContent(); + + if (isYachtNode(boatNode)) readYacht(boatNode, sourceID, name); + else readMark(boatNode, sourceID, name); + } + + /** + * Read a Yacht Node + * @param boatNode Node to be read + * @param sourceID Source ID of the Yacht + * @param name Name of the Boat + */ + private void readYacht(Node boatNode, int sourceID, String name) { + String shortName = boatNode.getAttributes().getNamedItem("ShortName").getTextContent(); + if (exists(boatNode, "Country")) { + String country = boatNode.getAttributes().getNamedItem("Country").getTextContent(); + boatMap.put(sourceID, new Boat(sourceID, name, country, this.boatPolars)); + } else { + boatMap.put(sourceID, new Boat(sourceID, name, shortName, this.boatPolars)); + } + } + + /** + * Read Marker Boats + * @param boatNode Node to be read + * @param sourceID Source ID of the boat + * @param name Name of the Marker Boat + */ + private void readMark(Node boatNode, int sourceID, String name) { + Node nCoord = ((Element)boatNode).getElementsByTagName("GPSposition").item(0); + double x = Double.parseDouble(nCoord.getAttributes().getNamedItem("X").getTextContent()); + double y = Double.parseDouble(nCoord.getAttributes().getNamedItem("Y").getTextContent()); + Mark mark = new Mark(sourceID, name, new GPSCoordinate(y,x)); + markerMap.put(sourceID, mark); + } + + /** + * Get the boats that are going to participate in this race + * @return Dictionary of boats that are to participate in this race indexed by SourceID + */ + @Override + public Map getBoats() { + return boatMap; + } + + /** + * Get the marker BOats that are participating in this race + * @return Dictionary of the Markers BOats that are in this race indexed by their Source ID. + */ + @Override + public Map getMarkerBoats() { + return markerMap; + } +} diff --git a/racevisionGame/src/main/java/mock/dataInput/RaceDataSource.java b/racevisionGame/src/main/java/mock/dataInput/RaceDataSource.java new file mode 100644 index 00000000..54c97a41 --- /dev/null +++ b/racevisionGame/src/main/java/mock/dataInput/RaceDataSource.java @@ -0,0 +1,32 @@ +package mock.dataInput; + +import seng302.Model.Boat; +import seng302.Model.CompoundMark; +import seng302.Model.GPSCoordinate; +import seng302.Model.Leg; + +import java.time.ZonedDateTime; +import java.util.List; + +/** + * Data Class for a Race + */ +public interface RaceDataSource { + List getBoats(); + + List getLegs(); + + List getBoundary(); + + List getCompoundMarks(); + + int getRaceId(); + + String getRaceType(); + + ZonedDateTime getZonedDateTime(); + + GPSCoordinate getMapTopLeft(); + + GPSCoordinate getMapBottomRight(); +} diff --git a/racevisionGame/src/main/java/mock/dataInput/RaceXMLReader.java b/racevisionGame/src/main/java/mock/dataInput/RaceXMLReader.java new file mode 100644 index 00000000..4eb35f9f --- /dev/null +++ b/racevisionGame/src/main/java/mock/dataInput/RaceXMLReader.java @@ -0,0 +1,287 @@ +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 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 boundary = new ArrayList<>(); + private final Map compoundMarkMap = new HashMap<>(); + private final Map participants = new HashMap<>(); + private final List legs = new ArrayList<>(); + private final List compoundMarks = new ArrayList<>(); + private ZonedDateTime creationTimeDate; + private ZonedDateTime raceStartTime; + private int raceID; + private String raceType; + private boolean postpone; + + private Map boats; + private Map 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 getBoundary() { + return boundary; + } + + public GPSCoordinate getMapTopLeft() { + return mapTopLeft; + } + + public GPSCoordinate getMapBottomRight() { + return mapBottomRight; + } + + public List getLegs() { + return legs; + } + + public List 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 getBoats() { + return new ArrayList<>(participants.values()); + } +} diff --git a/racevisionGame/src/main/java/mock/dataInput/XMLReader.java b/racevisionGame/src/main/java/mock/dataInput/XMLReader.java new file mode 100644 index 00000000..3ac56f54 --- /dev/null +++ b/racevisionGame/src/main/java/mock/dataInput/XMLReader.java @@ -0,0 +1,115 @@ +package mock.dataInput; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; + +/** + * Base Reader for XML Files + */ +public abstract class XMLReader { + + protected Document doc; + + /** + * Read in XML file + * @param filePath filepath for XML file + * @throws ParserConfigurationException If a document builder cannot be created. + * @throws IOException If any IO errors occur while parsing the XML file. + * @throws SAXException If any parse error occurs while parsing. + */ + public XMLReader(String filePath) throws ParserConfigurationException, IOException, SAXException { + + InputSource fXmlFile; + if (filePath.contains("<")) { + fXmlFile = new InputSource(); + fXmlFile.setCharacterStream(new StringReader(filePath)); + + } else { + fXmlFile = new InputSource(getClass().getClassLoader().getResourceAsStream(filePath)); + } + + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + doc = dBuilder.parse(fXmlFile); + doc.getDocumentElement().normalize(); + } + + /** + * Alternate constructor + * @param xmlFile File to be read + * @param isWholeFile boolean value whether entire file is being passed + */ + public XMLReader(String xmlFile, Boolean isWholeFile) { + + } + + /** + * Return Document data of the read-in XML + * @return XML document + */ + public Document getDocument() { + return doc; + } + + /** + * Get content of a tag in an element + * @param n Element to read tags from + * @param tagName Name of the tag + * @return Content of the tag + */ + public String getTextValueOfNode(Element n, String tagName) { + return n.getElementsByTagName(tagName).item(0).getTextContent(); + } + + /** + * Get attributes for an element + * @param n Element to read attributes from + * @param attr Attributes of element + * @return Attributes of element + */ + public String getAttribute(Element n, String attr) { + return n.getAttribute(attr); + } + + protected boolean exists(Node node, String attribute) { + return node.getAttributes().getNamedItem(attribute) != null; + } + + /** + * Get the contents of the XML FILe. + * @param document holds all xml information + * @return String representation of document + * @throws TransformerException when document is malformed, and cannot be turned into a string + */ + public static String getContents(Document document) throws TransformerException { + DOMSource source = new DOMSource(document); + + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + + StringWriter stringWriter = new StringWriter(); + StreamResult result = new StreamResult(stringWriter); + transformer.transform(source, result); + + return stringWriter.toString(); + } + +} diff --git a/racevisionGame/src/main/java/mock/exceptions/InvalidBoatDataException.java b/racevisionGame/src/main/java/mock/exceptions/InvalidBoatDataException.java new file mode 100644 index 00000000..1b36c34e --- /dev/null +++ b/racevisionGame/src/main/java/mock/exceptions/InvalidBoatDataException.java @@ -0,0 +1,14 @@ +package mock.exceptions; + +/** + * An exception thrown when we cannot generate Boats.xml and send an XML message. + */ +public class InvalidBoatDataException extends RuntimeException { + + public InvalidBoatDataException() { + } + + public InvalidBoatDataException(String message) { + super(message); + } +} diff --git a/racevisionGame/src/main/java/mock/exceptions/InvalidRaceDataException.java b/racevisionGame/src/main/java/mock/exceptions/InvalidRaceDataException.java new file mode 100644 index 00000000..8b85cd1d --- /dev/null +++ b/racevisionGame/src/main/java/mock/exceptions/InvalidRaceDataException.java @@ -0,0 +1,13 @@ +package mock.exceptions; + +/** + * Exception thrown when we cannot generate Race.xml data, and send an XML message. + */ +public class InvalidRaceDataException extends RuntimeException { + public InvalidRaceDataException() { + } + + public InvalidRaceDataException(String message) { + super(message); + } +} diff --git a/racevisionGame/src/main/java/mock/exceptions/StreamedCourseXMLException.java b/racevisionGame/src/main/java/mock/exceptions/StreamedCourseXMLException.java new file mode 100644 index 00000000..e173184e --- /dev/null +++ b/racevisionGame/src/main/java/mock/exceptions/StreamedCourseXMLException.java @@ -0,0 +1,7 @@ +package mock.exceptions; + +/** + * Created by cbt24 on 25/04/17. + */ +public class StreamedCourseXMLException extends Throwable { +} diff --git a/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java b/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java new file mode 100644 index 00000000..a84f4fc7 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java @@ -0,0 +1,97 @@ +package visualiser.Controllers; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.layout.AnchorPane; +import seng302.RaceConnection; + +import java.io.IOException; +import java.net.Socket; +import java.net.URL; +import java.util.ResourceBundle; + +/** + * Controls the connection that the VIsualiser can connect to. + */ +public class ConnectionController extends Controller { + @FXML + private AnchorPane connectionWrapper; + @FXML + private TableView connectionTable; + @FXML + private TableColumn hostnameColumn; + @FXML + private TableColumn statusColumn; + @FXML + private Button connectButton; + + @FXML + private TextField urlField; + @FXML + private TextField portField; + + private ObservableList connections; + + @Override + public void initialize(URL location, ResourceBundle resources) { + // TODO - replace with config file + connections = FXCollections.observableArrayList(); + connections.add(new RaceConnection("livedata.americascup.com", 4941)); + connections.add(new RaceConnection("localhost", 4942)); + + connectionTable.setItems(connections); + hostnameColumn.setCellValueFactory(cellData -> cellData.getValue().hostnameProperty()); + statusColumn.setCellValueFactory(cellData -> cellData.getValue().statusProperty()); + + connectionTable.getSelectionModel().selectedItemProperty().addListener((obs, prev, curr) -> { + if (curr != null && ((RaceConnection)curr).check()) connectButton.setDisable(false); + else connectButton.setDisable(true); + }); + connectButton.setDisable(true); + } + + /** + * Sets current status of all connections. + */ + public void checkConnections() { + for(RaceConnection connection: connections) { + connection.check(); + } + } + + public AnchorPane startWrapper(){ + return connectionWrapper; + } + + /** + * Connects to host currently selected in table. Button enabled only if host is ready. + */ + public void connectSocket() { + try{ + RaceConnection connection = (RaceConnection)connectionTable.getSelectionModel().getSelectedItem(); + Socket socket = new Socket(connection.getHostname(), connection.getPort()); + connectionWrapper.setVisible(false); + parent.enterLobby(socket); + } catch (IOException e) { /* Never reached */ } + } + + /** + * adds a new connection + */ + public void addConnection(){ + String hostName = urlField.getText(); + String portString = portField.getText(); + try{ + int port = Integer.parseInt(portString); + connections.add(new RaceConnection(hostName, port)); + }catch(NumberFormatException e){ + System.err.println("Port number entered is not a number"); + } + + } +} diff --git a/racevisionGame/src/main/java/visualiser/Controllers/Controller.java b/racevisionGame/src/main/java/visualiser/Controllers/Controller.java new file mode 100644 index 00000000..220b7816 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Controllers/Controller.java @@ -0,0 +1,32 @@ +package visualiser.Controllers; + +import javafx.fxml.Initializable; + +import java.net.URL; +import java.util.ResourceBundle; + +/** + * Controller parent for app controllers. + * Created by fwy13 on 15/03/2017. + */ +public abstract class Controller implements Initializable { + protected MainController parent; + + /** + * Sets the parent of the application + * + * @param parent controller + */ + public void setParent(MainController parent) { + this.parent = parent; + } + + /** + * Initialisation class that is run on start up. + * + * @param location resources location + * @param resources resources bundle + */ + @Override + public abstract void initialize(URL location, ResourceBundle resources); +} diff --git a/racevisionGame/src/main/java/visualiser/Controllers/FinishController.java b/racevisionGame/src/main/java/visualiser/Controllers/FinishController.java new file mode 100644 index 00000000..c526d900 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Controllers/FinishController.java @@ -0,0 +1,64 @@ +package visualiser.Controllers; + + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.layout.AnchorPane; +import seng302.Model.Boat; + +import java.net.URL; +import java.util.ResourceBundle; + + +/** + * Finish Screen for when the race finishs. + */ +public class FinishController extends Controller { + + @FXML + AnchorPane finishWrapper; + + @FXML + TableView boatInfoTable; + + @FXML + TableColumn boatRankColumn; + + @FXML + TableColumn boatNameColumn; + + @FXML + Label raceWinnerLabel; + + /** + * Sets up the finish table + * @param boats Boats to display + */ + private void setFinishTable(ObservableList boats){ + boatInfoTable.setItems(boats); + boatNameColumn.setCellValueFactory(cellData -> cellData.getValue().getName()); + boatRankColumn.setCellValueFactory(cellData -> cellData.getValue().positionProperty()); + + raceWinnerLabel.setText("Winner: "+ boatNameColumn.getCellObservableValue(0).getValue()); + raceWinnerLabel.setWrapText(true); + + } + + + @Override + public void initialize(URL location, ResourceBundle resources){ + } + + /** + * Display the table + * @param boats boats to display on the table. + */ + public void enterFinish(ObservableList boats){ + finishWrapper.setVisible(true); + setFinishTable(boats); + } + +} diff --git a/racevisionGame/src/main/java/visualiser/Controllers/MainController.java b/racevisionGame/src/main/java/visualiser/Controllers/MainController.java new file mode 100644 index 00000000..5c3d15fb --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Controllers/MainController.java @@ -0,0 +1,60 @@ +package visualiser.Controllers; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.layout.AnchorPane; +import seng302.Model.Boat; +import seng302.Model.RaceClock; +import seng302.VisualiserInput; + +import java.net.Socket; +import java.net.URL; +import java.util.ResourceBundle; + +/** + * Controller that everything is overlayed onto. This makes it so that changing scenes does not resize your stage. + */ +public class MainController extends Controller { + @FXML private StartController startController; + @FXML private RaceController raceController; + @FXML private ConnectionController connectionController; + @FXML private FinishController finishController; + + public void beginRace(VisualiserInput visualiserInput, RaceClock raceClock) { + raceController.startRace(visualiserInput, raceClock); + } + + public void enterLobby(Socket socket) { + startController.enterLobby(socket); + } + + public void enterFinish(ObservableList boats) { finishController.enterFinish(boats); } + + /** + * Main Controller for the applications will house the menu and the displayed pane. + * + * @param location of resources + * @param resources bundle + */ + @Override + public void initialize(URL location, ResourceBundle resources) { + startController.setParent(this); + raceController.setParent(this); + connectionController.setParent(this); + finishController.setParent(this); + AnchorPane.setTopAnchor(startController.startWrapper(), 0.0); + AnchorPane.setBottomAnchor(startController.startWrapper(), 0.0); + AnchorPane.setLeftAnchor(startController.startWrapper(), 0.0); + AnchorPane.setRightAnchor(startController.startWrapper(), 0.0); + + AnchorPane.setTopAnchor(connectionController.startWrapper(), 0.0); + AnchorPane.setBottomAnchor(connectionController.startWrapper(), 0.0); + AnchorPane.setLeftAnchor(connectionController.startWrapper(), 0.0); + AnchorPane.setRightAnchor(connectionController.startWrapper(), 0.0); + + AnchorPane.setTopAnchor(finishController.finishWrapper, 0.0); + AnchorPane.setBottomAnchor(finishController.finishWrapper, 0.0); + AnchorPane.setLeftAnchor(finishController.finishWrapper, 0.0); + AnchorPane.setRightAnchor(finishController.finishWrapper, 0.0); + } +} diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java new file mode 100644 index 00000000..b2546ab4 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java @@ -0,0 +1,199 @@ +package visualiser.Controllers; + + +import javafx.application.Platform; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.chart.LineChart; +import javafx.scene.control.*; +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 java.net.URL; +import java.util.ResourceBundle; + +/** + * Created by fwy13 on 15/03/2017. + */ +public class RaceController extends Controller { + private ResizableRaceCanvas raceMap; + private ResizableRaceMap raceBoundaries; + + private RaceClock raceClock; + private Sparkline sparkline; + + private int legNum; + + @FXML GridPane canvasBase; + @FXML Pane arrow; + @FXML SplitPane race; + @FXML StackPane arrowPane; + @FXML Label timer; + @FXML Label FPS; + @FXML Label timeZone; + @FXML CheckBox showFPS; + @FXML TableView boatInfoTable; + @FXML TableColumn boatPlacingColumn; + @FXML TableColumn boatTeamColumn; + @FXML TableColumn boatMarkColumn; + @FXML TableColumn boatSpeedColumn; + @FXML LineChart sparklineChart; + @FXML AnchorPane annotationPane; + + /** + * Updates the ResizableRaceCanvas (raceMap) with most recent data + * + * @param boats boats that are to be displayed in the race + * @param boatMarkers Markers for boats + * @see ResizableRaceCanvas + */ + public void updateMap(ObservableList boats, ObservableList boatMarkers) { + raceMap.setBoats(boats); + raceMap.setBoatMarkers(boatMarkers); + raceMap.update(); + raceBoundaries.draw(); + //stop if the visualiser is no longer running + } + + /** + * Updates the array listened by the TableView (boatInfoTable) that displays the boat information. + * + * @param race Race to listen to. + */ + public void setInfoTable(StreamedRace race) { + boatInfoTable.setItems(race.getStartingBoats()); + boatTeamColumn.setCellValueFactory(cellData -> cellData.getValue().getName()); + boatSpeedColumn.setCellValueFactory(cellData -> cellData.getValue().getVelocityProp()); + boatMarkColumn.setCellValueFactory(cellData -> cellData.getValue().getCurrentLegName()); + boatPlacingColumn.setCellValueFactory(cellData -> cellData.getValue().positionProperty()); + } + + + @Override + public void initialize(URL location, ResourceBundle resources) { + //listener for fps + showFPS.selectedProperty().addListener((ov, old_val, new_val) -> { + if (showFPS.isSelected()) { + FPS.setVisible(true); + } else { + FPS.setVisible(false); + } + }); + } + + /** + * Creates and sets initial display for Sparkline for race positions. + * @param boats boats to display on the sparkline + */ + public void createSparkLine(ObservableList boats){ + sparkline = new Sparkline(boats, legNum, sparklineChart); + } + + /** + * Updates the sparkline to display current boat positions. + * @param boatsInRace used for current boat positions. + */ + public void updateSparkline(ObservableList boatsInRace){ + sparkline.updateSparkline(boatsInRace); + } + + /** + * Initializes and runs the race, based on the user's chosen scale factor + * Currently uses an example racecourse + * + * @param visualiserInput input from network + * @param raceClock The RaceClock to use for the race's countdown/elapsed duration + timezone. + */ + public void startRace(VisualiserInput visualiserInput, RaceClock raceClock) { + + legNum = visualiserInput.getCourse().getLegs().size()-1; + + makeArrow(); + + raceMap = new ResizableRaceCanvas(visualiserInput.getCourse()); + raceMap.setMouseTransparent(true); + raceMap.widthProperty().bind(canvasBase.widthProperty()); + raceMap.heightProperty().bind(canvasBase.heightProperty()); + raceMap.draw(); + raceMap.setVisible(true); + raceMap.setArrow(arrow.getChildren().get(0)); + + canvasBase.getChildren().add(0, raceMap); + + raceBoundaries = new ResizableRaceMap(visualiserInput.getCourse()); + raceBoundaries.setMouseTransparent(true); + raceBoundaries.widthProperty().bind(canvasBase.widthProperty()); + raceBoundaries.heightProperty().bind(canvasBase.heightProperty()); + raceBoundaries.draw(); + raceBoundaries.setVisible(true); + + canvasBase.getChildren().add(0, raceBoundaries); + + race.setVisible(true); + + timeZone.setText(raceClock.getTimeZone()); + + //RaceClock.duration isn't necessarily being changed in the javaFX thread, so we need to runlater the update. + raceClock.durationProperty().addListener((observable, oldValue, newValue) -> { + Platform.runLater(() -> { + timer.setText(newValue); + }); + }); + + this.raceClock = raceClock; + raceMap.setRaceClock(raceClock); + + StreamedRace newRace = new StreamedRace(visualiserInput, this); + + initializeFPS(); + + // set up annotation displays + new Annotations(annotationPane, raceMap); + + new Thread((newRace)).start(); + } + + /** + * Finish Race View + * @param boats boats there are in the race. + */ + public void finishRace(ObservableList boats){ + race.setVisible(false); + parent.enterFinish(boats); + } + + /** + * Set the value for the fps label + * + * @param fps fps that the label will be updated to + */ + public void setFrames(String fps) { + FPS.setText((fps)); + } + + /** + * Set up FPS display at bottom of screen + */ + private void initializeFPS() { + showFPS.setVisible(true); + showFPS.selectedProperty().addListener((ov, old_val, new_val) -> { + if (showFPS.isSelected()) { + FPS.setVisible(true); + } else { + FPS.setVisible(false); + } + }); + } + + private void makeArrow() { + arrowPane.getChildren().add(arrow); + } + + public RaceClock getRaceClock() { + return raceClock; + } +} diff --git a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java new file mode 100644 index 00000000..1b7078d7 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java @@ -0,0 +1,212 @@ +package visualiser.Controllers; + +import javafx.animation.AnimationTimer; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.GridPane; +import seng302.Mock.StreamedCourse; +import seng302.Model.Boat; +import seng302.Model.RaceClock; +import seng302.VisualiserInput; + +import java.io.IOException; +import java.net.Socket; +import java.net.URL; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Observable; +import java.util.Observer; +import java.util.ResourceBundle; + +/** + * Controller to for waiting for the race to start + */ +public class StartController extends Controller implements Observer { + + @FXML private GridPane start; + @FXML private AnchorPane startWrapper; + @FXML private Label raceTitleLabel; + @FXML private Label raceStartLabel; + + @FXML private TableView boatNameTable; + @FXML private TableColumn boatNameColumn; + @FXML private TableColumn boatCodeColumn; + @FXML private Label timeZoneTime; + @FXML private Label timer; + @FXML private Label raceStatusLabel; + + //@FXML Button fifteenMinButton; + + private RaceClock raceClock; + + private StreamedCourse raceData; + private int raceStat; + + private VisualiserInput visualiserInput; + + ///Tracks whether the race has been started (that is, has startRaceNoScaling() be called). + private boolean hasRaceStarted = false; + + //Tracks whether or not a clock has been created and setup, which occurs after receiving enough information. + private boolean hasCreatedClock = false; + + /** + * Begins the race with a scale factor of 1 + */ + private void startRaceNoScaling() { + //while(visualiserInput.getRaceStatus() == null);//TODO probably remove this. + + countdownTimer(); + } + + @Override + public void initialize(URL location, ResourceBundle resources){ + raceData = new StreamedCourse(); + raceData.addObserver(this); + } + + public AnchorPane startWrapper(){ + return startWrapper; + } + + /** + * Initiliases the tables that are to be shown on the pane + */ + private void initialiseTables() { + List boats = raceData.getBoats(); + ObservableList observableBoats = FXCollections.observableArrayList(boats); + + boatNameTable.setItems(observableBoats); + boatNameColumn.setCellValueFactory(cellData -> cellData.getValue().getName()); + boatCodeColumn.setCellValueFactory(new PropertyValueFactory<>("abbrev")); + } + + /** + * Countdown timer until race starts. + */ + private void countdownTimer() { + new AnimationTimer() { + @Override + public void handle(long arg0) { + raceStat = visualiserInput.getRaceStatus().getRaceStatus(); + raceStatusLabel.setText("Race Status: " + visualiserInput.getRaceStatus().getRaceStatus()); + if (raceStat==2 || raceStat == 3) { + stop(); + + startWrapper.setVisible(false); + start.setVisible(false); + + parent.beginRace(visualiserInput, raceClock); + } + } + }.start(); + } + + /** + * Sets the clock that displays the time of at the current race venue. + */ + private void setRaceClock() { + raceClock = new RaceClock(raceData.getZonedDateTime()); + + raceClock.timeStringProperty().addListener((observable, oldValue, newValue) -> { + Platform.runLater(() -> { + timeZoneTime.setText(newValue); + }); + }); +//TEMP REMOVE + //timeZoneTime.textProperty().bind(raceClock.timeStringProperty()); + raceClock.run(); + } + + /** + * Sets the time that the race is going to start. + */ + private void setStartingTime() { + String dateFormat = "'Starting time:' HH:mm dd/MM/YYYY"; + Platform.runLater(()-> { + long utcTime = visualiserInput.getRaceStatus().getExpectedStartTime(); + raceClock.setStartingTime(raceClock.getLocalTime(utcTime)); + raceStartLabel.setText(DateTimeFormatter.ofPattern(dateFormat).format(raceClock.getStartingTime())); + + raceClock.durationProperty().addListener((observable, oldValue, newValue) -> { + Platform.runLater(() -> { + timer.setText(newValue); + }); + }); + }); + } + + /** + * set the current time, may be used to update the time on the clock. + */ + private void setCurrentTime() { + Platform.runLater(()-> + raceClock.setUTCTime(visualiserInput.getRaceStatus().getCurrentTime()) + ); + } + + @Override + public void update(Observable o, Object arg) { + if(o instanceof StreamedCourse) { + StreamedCourse streamedCourse = (StreamedCourse) o; + if(streamedCourse.hasReadBoats()) { + initialiseTables(); + } + if(streamedCourse.hasReadRegatta()) { + Platform.runLater(() -> raceTitleLabel.setText(streamedCourse.getRegattaName())); + } + if (streamedCourse.hasReadCourse()) { + Platform.runLater(() -> { + if (!this.hasCreatedClock) { + this.hasCreatedClock = true; + setRaceClock(); + if (visualiserInput.getRaceStatus() == null) { + return;//TEMP BUG FIX if the race isn't sending race status messages (our mock currently doesn't), then it would block the javafx thread with the previous while loop. + }// TODO - replace with observer on VisualiserInput + setStartingTime(); + setCurrentTime(); + } + }); + } + + //TODO this is a somewhat temporary fix for when not all of the race data (boats, course, regatta) is received in time. + //Previously, startRaceNoScaling was called in the enterLobby function after the visualiserInput was started, but when connecting to the official data source it sometimes didn't send all of the race data, causing startRaceNoScaling to start, even though we didn't have enough information to start it. + if (streamedCourse.hasReadBoats() && streamedCourse.hasReadCourse() && streamedCourse.hasReadRegatta()) { + Platform.runLater(() -> { + if (!this.hasRaceStarted) { + if(visualiserInput.getRaceStatus() == null) { + } + else { + this.hasRaceStarted = true; + startRaceNoScaling(); + } + } + }); + } + + } + } + + /** + * Show starting information for a race given a socket. + * @param socket network source of information + */ + public void enterLobby(Socket socket) { + startWrapper.setVisible(true); + try { + visualiserInput = new VisualiserInput(socket, raceData); + new Thread(visualiserInput).start(); + + } catch (IOException e) { + e.printStackTrace(); + } + } + +} diff --git a/racevisionGame/src/main/java/visualiser/app/App.java b/racevisionGame/src/main/java/visualiser/app/App.java new file mode 100644 index 00000000..cfbf3a9b --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/app/App.java @@ -0,0 +1,38 @@ +package visualiser.app; + +import javafx.application.Application; +import javafx.application.Platform; +import javafx.event.EventHandler; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; +import javafx.stage.WindowEvent; + +public class App extends Application { + + /** + * Entry point for running the programme + * + * @param args for starting the programme + */ + public static void main(String[] args) { + launch(args); + } + + public void start(Stage stage) throws Exception { + stage.setOnCloseRequest(new EventHandler() { + @Override + public void handle(WindowEvent event) { + Platform.exit(); + System.exit(0); + } + }); + FXMLLoader loader = new FXMLLoader(getClass().getResource("/scenes/main.fxml")); + Parent root = loader.load(); + Scene scene = new Scene(root, 1200, 800); + stage.setScene(scene); + stage.setTitle("RaceVision - Team 7"); + stage.show(); + } +} diff --git a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java b/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java new file mode 100644 index 00000000..39a4cc5c --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java @@ -0,0 +1,432 @@ +package visualiser.app; +import javafx.application.Platform; +import org.xml.sax.SAXException; +import seng302.Networking.BinaryMessageDecoder; +import seng302.Networking.Exceptions.InvalidMessageException; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.DataInputStream; +import java.io.IOException; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static seng302.Networking.Utils.ByteConverter.bytesToShort; + +/** + * TCP client which receives packets/messages from a race data source + * (e.g., mock source, official source), and exposes them to any observers. + * @see seng302.Mock.StreamedCourse + */ +public class VisualiserInput implements Runnable { + + ///Timestamp of the last heartbeat. + private long lastHeartbeatTime = -1; + ///Sequence number of the last heartbeat. + private long lastHeartbeatSequenceNum = -1; + + ///The socket that we have connected to. + private Socket connectionSocket; + + ///Object to store parsed course data. //TODO comment? + private StreamedCourse course; + + ///The last RaceStatus message received. + private RaceStatus raceStatus; + + ///A map of the last BoatStatus message received, for each boat. + private final Map boatStatusMap = new HashMap<>(); + + ///A map of the last BoatLocation message received, for each boat. + private final Map boatLocationMap = new HashMap<>(); + + ///The last AverageWind message received. + private AverageWind averageWind; + + ///The last CourseWinds message received. + private CourseWinds courseWinds; + + ///A map of the last MarkRounding message received, for each boat. + private final Map markRoundingMap = new HashMap<>(); + + ///InputStream (from the socket). + private DataInputStream inStream; + + /** + * Ctor. + * @param socket Socket from which we will receive race data. + * @param course TODO comment? + * @throws IOException If there is something wrong with the socket's input stream. + */ + public VisualiserInput(Socket socket, StreamedCourse course) throws IOException { + this.connectionSocket = socket; + //We wrap a DataInputStream around the socket's InputStream because it has the stream.readFully(buffer) function, which is a blocking read until the buffer has been filled. + this.inStream = new DataInputStream(connectionSocket.getInputStream()); + this.course = course; + + this.lastHeartbeatTime = System.currentTimeMillis(); + } + + /** + * Provides StreamedCourse container for fixed course data. + * @return Course for current VisualiserInput instance. + * @see seng302.Mock.StreamedCourse + */ + public StreamedCourse getCourse() { + return course; + } + + /** + * Returns the last boat location message associated with the given boat source ID. + * @param sourceID Unique global identifier for the boat. + * @return The most recent location message. + */ + public BoatLocation getBoatLocationMessage(int sourceID) { + return boatLocationMap.get(sourceID); + } + + public BoatStatus getBoatStatusMessage(int sourceID) { + return boatStatusMap.get(sourceID); + } + + /** + * Calculates the time since last heartbeat, in milliseconds. + * @return Time since last heartbeat, in milliseconds.. + */ + private double timeSinceHeartbeat() { + long now = System.currentTimeMillis(); + return (now - lastHeartbeatTime); + } + + /** + * Returns the boat locations map. Maps from Integer (Boat ID) to BoatLocation. + * @return Map of boat locations. + */ + public Map getBoatLocationMap() { + return boatLocationMap; + } + + /** + * Gets the status of the race. + * @return The status of the race. + */ + public RaceStatus getRaceStatus() { + return raceStatus; + } + + /** + * Returns the boat statuses map. Maps from Integer (Boat ID) to BoatStatus. + * @return Map of boat statuses. + */ + public Map getBoatStatusMap() { + return boatStatusMap; + } + + /** + * Returns the average wind of the race. + * @return Average wind in the race. + */ + public AverageWind getAverageWind() { + return averageWind; + } + + /** + * Returns winds in the course. + * @return Winds that are in the course. + */ + public CourseWinds getCourseWinds() { + return courseWinds; + } + + + /** + * Returns the mark roundings map. Maps from Integer (Boat ID) to MarkRounding. + * @return Map of mark roundings. + */ + public Map getMarkRoundingMap() { + return markRoundingMap; + } + + /** + * Sets the wind direction for the current course. + * @param direction The new wind direction for the course. + */ + private void setCourseWindDirection(double direction) { + this.course.setWindDirection(direction); + } + + /** + * Reads and returns the next message as an array of bytes from the socket. Use getNextMessage() to get the actual message object instead. + * @return Encoded binary message bytes. + * @throws IOException Thrown when an error occurs while reading from the socket. + */ + private byte[] getNextMessageBytes() throws IOException { + inStream.mark(0); + short CRCLength = 4; + short headerLength = 15; + + //Read the header of the next message. + byte[] headerBytes = new byte[headerLength]; + inStream.readFully(headerBytes); + + //Read the message body length. + byte[] messageBodyLengthBytes = Arrays.copyOfRange(headerBytes, headerLength - 2, headerLength); + short messageBodyLength = bytesToShort(messageBodyLengthBytes); + + //Read the message body. + byte[] messageBodyBytes = new byte[messageBodyLength]; + inStream.readFully(messageBodyBytes); + + //Read the message CRC. + byte[] messageCRCBytes = new byte[CRCLength]; + inStream.readFully(messageCRCBytes); + + //Put the head + body + crc into one large array. + ByteBuffer messageBytes = ByteBuffer.allocate(headerBytes.length + messageBodyBytes.length + messageCRCBytes.length); + messageBytes.put(headerBytes); + messageBytes.put(messageBodyBytes); + messageBytes.put(messageCRCBytes); + + return messageBytes.array(); + } + + /** + * Reads and returns the next message object from the socket. + * @return The message object. Use instanceof for concrete type. + * @throws IOException Thrown when an error occurs while reading from the socket. + * @throws InvalidMessageException Thrown when the message is invalid in some way. + */ + private AC35Data getNextMessage() throws IOException, InvalidMessageException + { + //Get the next message from the socket as a block of bytes. + byte[] messageBytes = this.getNextMessageBytes(); + + //Decode the binary message into an appropriate message object. + BinaryMessageDecoder decoder = new BinaryMessageDecoder(messageBytes); + + return decoder.decode(); + + } + + /** + * Main loop which reads messages from the socket, and exposes them. + */ + public void run(){ + boolean receiverLoop = true; + //receiver loop that gets the input + while (receiverLoop) { + + //If no heartbeat has been received in more the heartbeat period + //then the connection will need to be restarted. + //System.out.println("time since last heartbeat: " + timeSinceHeartbeat());//TEMP REMOVE + long heartBeatPeriod = 10 * 1000; + if (timeSinceHeartbeat() > heartBeatPeriod) { + System.out.println("Connection has stopped, trying to reconnect."); + + //Attempt to reconnect the socket. + try {//This attempt doesn't really work. Under what circumstances would + this.connectionSocket = new Socket(this.connectionSocket.getInetAddress(), this.connectionSocket.getPort()); + //this.connectionSocket.connect(this.connectionSocket.getRemoteSocketAddress()); + //Reset the heartbeat timer. + this.lastHeartbeatTime = System.currentTimeMillis(); + } + catch (IOException e) { + System.err.println("Unable to reconnect."); + + //Wait 500ms. Ugly hack, should refactor. + long waitPeriod = 500; + long waitTimeStart = System.currentTimeMillis() + waitPeriod; + + while (System.currentTimeMillis() < waitTimeStart){ + //Nothing. Busyloop. + } + + //Swallow the exception. + continue; + } + + } + + //Reads the next message. + AC35Data message; + try { + message = this.getNextMessage(); + } + catch (InvalidMessageException | IOException e) { + //Prints exception to stderr, and iterate loop (that is, read the next message). + System.err.println("Unable to read message: " + e.getMessage()); + try { + inStream.reset(); + } catch (IOException e1) { + e1.printStackTrace(); + } + //Continue to the next loop iteration/message. + continue; + }/* + + //Add it to message queue. + this.messagesReceivedQueue.add(message);*/ + + + //Checks which message is being received and does what is needed for that message. + //Heartbeat. + if (message instanceof Heartbeat) { + Heartbeat heartbeat = (Heartbeat) message; + + //Check that the heartbeat number is greater than the previous value, and then set the last heartbeat time. + if (heartbeat.getSequenceNumber() > this.lastHeartbeatSequenceNum) { + lastHeartbeatTime = System.currentTimeMillis(); + lastHeartbeatSequenceNum = heartbeat.getSequenceNumber(); + //System.out.println("HeartBeat Message! " + lastHeartbeatSequenceNum); + } + } + //RaceStatus. + else if (message instanceof RaceStatus) { + RaceStatus raceStatus = (RaceStatus) message; + + //System.out.println("Race Status Message"); + this.raceStatus = raceStatus; + for (BoatStatus boatStatus: this.raceStatus.getBoatStatuses()) { + this.boatStatusMap.put(boatStatus.getSourceID(), boatStatus); + } + setCourseWindDirection(raceStatus.getScaledWindDirection()); + } + //DisplayTextMessage. + /*else if (message instanceof DisplayTextMessage) { + //System.out.println("Display Text Message"); + //No decoder for this. + }*/ + //XMLMessage. + else if (message instanceof XMLMessage) { + XMLMessage xmlMessage = (XMLMessage) message; + + //System.out.println("XML Message!"); + + Platform.runLater(()-> { + if (xmlMessage.getXmlMsgSubType() == XMLMessage.XMLTypeRegatta) { + //System.out.println("Setting Regatta"); + try { + course.setRegattaXMLReader(new RegattaXMLReader(xmlMessage.getXmlMessage())); + + } + //TODO REFACTOR should put all of these exceptions behind a RegattaXMLReaderException. + catch (IOException | SAXException | ParserConfigurationException e) { + System.err.println("Error creating RegattaXMLReader: " + e.getMessage()); + //Continue to the next loop iteration/message. + } + + } else if (xmlMessage.getXmlMsgSubType() == XMLMessage.XMLTypeRace) { + //System.out.println("Setting Course"); + try { + course.setStreamedCourseXMLReader(new StreamedCourseXMLReader(xmlMessage.getXmlMessage())); + } + //TODO REFACTOR should put all of these exceptions behind a StreamedCourseXMLReaderException. + catch (IOException | SAXException | ParserConfigurationException | StreamedCourseXMLException e) { + System.err.println("Error creating StreamedCourseXMLReader: " + e.getMessage()); + //Continue to the next loop iteration/message. + } + + } else if (xmlMessage.getXmlMsgSubType() == XMLMessage.XMLTypeBoat) { + //System.out.println("Setting Boats"); + try { + course.setBoatXMLReader(new BoatXMLReader(xmlMessage.getXmlMessage())); + } + //TODO REFACTOR should put all of these exceptions behind a BoatXMLReaderException. + catch (IOException | SAXException | ParserConfigurationException e) { + System.err.println("Error creating BoatXMLReader: " + e.getMessage()); + //Continue to the next loop iteration/message. + } + + } + }); + } + //RaceStartStatus. + else if (message instanceof RaceStartStatus) { + + //System.out.println("Race Start Status Message"); + } + //YachtEventCode. + /*else if (message instanceof YachtEventCode) { + YachtEventCode yachtEventCode = (YachtEventCode) message; + + //System.out.println("Yacht Event Code!"); + //No decoder for this. + + }*/ + //YachtActionCode. + /*else if (message instanceof YachtActionCode) { + YachtActionCode yachtActionCode = (YachtActionCode) message; + + //System.out.println("Yacht Action Code!"); + //No decoder for this. + + }*/ + //ChatterText. + /*else if (message instanceof ChatterText) { + ChatterText chatterText = (ChatterText) message; + + //System.out.println("Chatter Text Message!"); + //No decoder for this. + + }*/ + //BoatLocation. + else if (message instanceof BoatLocation) { + BoatLocation boatLocation = (BoatLocation) message; + + //System.out.println("Boat Location!"); + if (this.boatLocationMap.containsKey(boatLocation.getSourceID())) { + //If our boatlocation map already contains a boat location message for this boat, check that the new message is actually for a later timestamp (i.e., newer). + if (boatLocation.getTime() > this.boatLocationMap.get(boatLocation.getSourceID()).getTime()){ + //If it is, replace the old message. + this.boatLocationMap.put(boatLocation.getSourceID(), boatLocation); + } + }else{ + //If the map _doesn't_ already contain a message for this boat, insert the message. + this.boatLocationMap.put(boatLocation.getSourceID(), boatLocation); + } + } + //MarkRounding. + else if (message instanceof MarkRounding) { + MarkRounding markRounding = (MarkRounding) message; + + //System.out.println("Mark Rounding Message!"); + + if (this.markRoundingMap.containsKey(markRounding.getSourceID())) { + //If our markRoundingMap already contains a mark rounding message for this boat, check that the new message is actually for a later timestamp (i.e., newer). + if (markRounding.getTime() > this.markRoundingMap.get(markRounding.getSourceID()).getTime()){ + //If it is, replace the old message. + this.markRoundingMap.put(markRounding.getSourceID(), markRounding); + } + }else{ + //If the map _doesn't_ already contain a message for this boat, insert the message. + this.markRoundingMap.put(markRounding.getSourceID(), markRounding); + } + + } + //CourseWinds. + else if (message instanceof CourseWinds) { + + //System.out.println("Course Wind Message!"); + this.courseWinds = (CourseWinds) message; + + } + //AverageWind. + else if (message instanceof AverageWind) { + + //System.out.println("Average Wind Message!"); + this.averageWind = (AverageWind) message; + + } + //Unrecognised message. + else { + System.out.println("Broken Message!"); + } + + } + } + + + +} diff --git a/racevisionGame/src/main/java/visualiser/dataInput/BoatXMLReader.java b/racevisionGame/src/main/java/visualiser/dataInput/BoatXMLReader.java new file mode 100644 index 00000000..c0e562f4 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/dataInput/BoatXMLReader.java @@ -0,0 +1,156 @@ +package visualiser.dataInput; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; +import seng302.Model.Boat; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * XML Reader that reads in a file and initializes + * {@link seng302.Mock.StreamedBoat StreamedBoat}s that will be participating + * in a race. + */ +public class BoatXMLReader extends XMLReader { + private final Map streamedBoatMap = new HashMap<>(); + private Map participants = new HashMap<>(); + + /** + * Constructor for Boat XML Reader + * @param filePath path of the file + * @throws IOException error + * @throws SAXException error + * @throws ParserConfigurationException error + */ + public BoatXMLReader(String filePath) throws IOException, SAXException, ParserConfigurationException { + this(filePath, true); + } + + /** + * Constructor for Boat XML Reader + * @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 + */ + public BoatXMLReader(String filePath, boolean read) throws IOException, SAXException, ParserConfigurationException { + super(filePath); + if (read) { + read(); + } + } + + /** + * Constructor for Boat XML Reader + * @param xmlString sting to read + * @throws IOException error + * @throws SAXException error + * @throws ParserConfigurationException error + */ + public BoatXMLReader(InputStream xmlString) throws IOException, SAXException, ParserConfigurationException { + super(xmlString); + read(); + } + + public void read() { + readSettings(); + readShapes(); + readBoats(); + } + + /** + * Reads boats settings. + * INFORMATION FROM HERE IS IGNORED FOR NOW + */ + private void readSettings() { + + } + + /** + * Reads different kinds of boat. + * INFORMATION FROM HERE IS IGNORED FOR NOW + */ + private void readShapes() { + + } + + /** + * Reads the boats in the race + */ + private void readBoats() { + Element nBoats = (Element) doc.getElementsByTagName("Boats").item(0); + for (int i = 0; i < nBoats.getChildNodes().getLength(); i++) { + Node boat = nBoats.getChildNodes().item(i); + if (boat.getNodeName().equals("Boat") && boat.getAttributes().getNamedItem("Type").getTextContent().equals("Yacht")) { + readSingleBoat(boat); + } + } + } + + /** + * Reads the information about one boat + * Ignored values: ShapeID, StoweName, HullNum, Skipper, Type + * @param boat The node to read boat data from. + */ + private void readSingleBoat(Node boat) { + StreamedBoat streamedBoat; + String country = null; + int sourceID = Integer.parseInt(boat.getAttributes().getNamedItem("SourceID").getTextContent()); + String boatName = boat.getAttributes().getNamedItem("BoatName").getTextContent(); + String shortName = boat.getAttributes().getNamedItem("ShortName").getTextContent(); + if (exists(boat, "Country")) country = boat.getAttributes().getNamedItem("Country").getTextContent(); + + // Ignore all non participating boats + if (participants.containsKey(sourceID)) { + + if (!streamedBoatMap.containsKey(sourceID)) { + if (country != null) { + streamedBoat = new StreamedBoat(sourceID, boatName, country); + } else { + streamedBoat = new StreamedBoat(sourceID, boatName, shortName); + } + streamedBoatMap.put(sourceID, streamedBoat); + // Override boat with new boat + participants.put(sourceID, streamedBoat); + } + + for (int i = 0; i < boat.getChildNodes().getLength(); i++) { + Node GPSposition = boat.getChildNodes().item(i); + if (GPSposition.getNodeName().equals("GPSposition")) + readBoatPositionInformation(sourceID, GPSposition); + } + } + } + + + /** + * Reads the positional information about a boat + * Ignored values: FlagPosition, MastTop, Z value of GPSPosition + * @param sourceID The source ID of the boat. + * @param GPSposition The relative GPS position of the boat. + */ + private void readBoatPositionInformation(int sourceID, Node GPSposition) { + // TODO Get relative point before implementing. (GPSposition is based + // off a relative point). + } + + /** + * Sets the participants + * @param participants boats participating the race mapped by their source ID's + */ + public void setParticipants(Map participants) { + this.participants = participants; + } + + public List getBoats() { + return new ArrayList<>(streamedBoatMap.values()); + } +} diff --git a/racevisionGame/src/main/java/visualiser/dataInput/RaceDataSource.java b/racevisionGame/src/main/java/visualiser/dataInput/RaceDataSource.java new file mode 100644 index 00000000..fb6ebcba --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/dataInput/RaceDataSource.java @@ -0,0 +1,26 @@ +package visualiser.dataInput; + +import seng302.Model.Boat; +import seng302.Model.Leg; +import seng302.Model.Marker; + +import java.time.ZonedDateTime; +import java.util.List; + +/** + * An object that holds relevant data for a race.
+ * Information includes: {@link seng302.Model.Boat Boat}s, + * {@link seng302.Model.Leg Leg}s, {@link seng302.Model.Marker Marker}s and + * the {@link seng302.GPSCoordinate GPSCoordinate}s to create a + * {@link seng302.Model.ResizableRaceMap ResizableRaceMap}. + */ +public interface RaceDataSource { + List getBoats(); + List getLegs(); + List getMarkers(); + List getBoundary(); + + ZonedDateTime getZonedDateTime(); + GPSCoordinate getMapTopLeft(); + GPSCoordinate getMapBottomRight(); +} diff --git a/racevisionGame/src/main/java/visualiser/dataInput/RegattaXMLReader.java b/racevisionGame/src/main/java/visualiser/dataInput/RegattaXMLReader.java new file mode 100644 index 00000000..a1c3670f --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/dataInput/RegattaXMLReader.java @@ -0,0 +1,165 @@ +package visualiser.dataInput; + +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; +import seng302.GPSCoordinate; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.InputStream; + +/** + * Created by jjg64 on 19/04/17. + */ +public class RegattaXMLReader extends XMLReader { + private int regattaID; + private String regattaName; + private int raceID = 0; + private String courseName; + private double centralLatitude; + private double centralLongitude; + private double centralAltitude; + private float utcOffset; + private float magneticVariation; + + /** + * Constructor for Regatta XML + * + * @param filePath path of the file + * @throws IOException error + * @throws SAXException error + * @throws ParserConfigurationException error + */ + public RegattaXMLReader(String filePath) throws IOException, SAXException, ParserConfigurationException { + this(filePath, true); + } + + /** + * Constructor for Regatta 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 + */ + private RegattaXMLReader(String filePath, boolean read) throws IOException, SAXException, ParserConfigurationException { + super(filePath); + if (read) { + read(); + } + } + + /** + * Alternate Constructor that takes in an inputstream instead + * @param xmlString Input stream of the XML + * @throws IOException Error with input + * @throws SAXException Error with XML Format + * @throws ParserConfigurationException Error with XMl contents + */ + public RegattaXMLReader(InputStream xmlString) throws IOException, SAXException, ParserConfigurationException { + super(xmlString); + read(); + } + + /** + * Read the XML + */ + private void read() { + NodeList attributeConfig = doc.getElementsByTagName("RegattaConfig"); + Element attributes = (Element) attributeConfig.item(0); + makeRegatta(attributes); + } + + /** + * Extracts the information from the attributes + * @param attributes attributes to extract information form. + */ + private void makeRegatta(Element attributes) { + this.regattaID = Integer.parseInt(getTextValueOfNode(attributes, "RegattaID")); + this.regattaName = getTextValueOfNode(attributes, "RegattaName"); + this.courseName = getTextValueOfNode(attributes, "CourseName"); + this.centralLatitude = Double.parseDouble(getTextValueOfNode(attributes, "CentralLatitude")); + this.centralLongitude = Double.parseDouble(getTextValueOfNode(attributes, "CentralLongitude")); + this.centralAltitude = Double.parseDouble(getTextValueOfNode(attributes, "CentralAltitude")); + this.utcOffset = Float.parseFloat(getTextValueOfNode(attributes, "UtcOffset")); + this.magneticVariation = Float.parseFloat(getTextValueOfNode(attributes, "MagneticVariation")); + } + + public int getRegattaID() { + return regattaID; + } + + public void setRegattaID(int ID) { + this.regattaID = ID; + } + + public String getRegattaName() { + return regattaName; + } + + public void setRegattaName(String regattaName) { + this.regattaName = regattaName; + } + + public int getRaceID() { + return raceID; + } + + public void setRaceID(int raceID) { + this.raceID = raceID; + } + + public String getCourseName() { + return courseName; + } + + public void setCourseName(String courseName) { + this.courseName = courseName; + } + + public double getCentralLatitude() { + return centralLatitude; + } + + public void setCentralLatitude(double centralLatitude) { + this.centralLatitude = centralLatitude; + } + + public double getCentralLongitude() { + return centralLongitude; + } + + public void setCentralLongitude(double centralLongitude) { + this.centralLongitude = centralLongitude; + } + + public double getCentralAltitude() { + return centralAltitude; + } + + public void setCentralAltitude(double centralAltitude) { + this.centralAltitude = centralAltitude; + } + + public float getUtcOffset() { + return utcOffset; + } + + public void setUtcOffset(float utcOffset) { + this.utcOffset = utcOffset; + } + + public float getMagneticVariation() { + return magneticVariation; + } + + public void setMagneticVariation(float magneticVariation) { + this.magneticVariation = magneticVariation; + } + + public GPSCoordinate getGPSCoordinate() { + return new GPSCoordinate(centralLatitude, centralLongitude); + } +} diff --git a/racevisionGame/src/main/java/visualiser/dataInput/StreamedCourseXMLReader.java b/racevisionGame/src/main/java/visualiser/dataInput/StreamedCourseXMLReader.java new file mode 100644 index 00000000..91a01aaa --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/dataInput/StreamedCourseXMLReader.java @@ -0,0 +1,290 @@ +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 boundary = new ArrayList<>(); + private final Map compoundMarks = new HashMap<>(); + private final Map participants = new HashMap<>(); + private final List legs = new ArrayList<>(); + private final List 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 getBoundary() { + return boundary; + } + + public GPSCoordinate getMapTopLeft() { + return mapTopLeft; + } + + public GPSCoordinate getMapBottomRight() { + return mapBottomRight; + } + + public List getLegs() { + return legs; + } + + public List 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 getParticipants() { + return participants; + } +} diff --git a/racevisionGame/src/main/java/visualiser/dataInput/XMLReader.java b/racevisionGame/src/main/java/visualiser/dataInput/XMLReader.java new file mode 100644 index 00000000..146433cd --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/dataInput/XMLReader.java @@ -0,0 +1,52 @@ +package visualiser.dataInput; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.InputStream; + +/** + * The abstract class for reading in XML race data. + */ +public abstract class XMLReader { + + protected Document doc; + + protected XMLReader(String filePath) throws ParserConfigurationException, IOException, SAXException { + InputStream fXmlFile = getClass().getClassLoader().getResourceAsStream(filePath); + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + doc = dBuilder.parse(fXmlFile); + doc.getDocumentElement().normalize(); + } + + protected XMLReader(InputStream xmlInput) throws ParserConfigurationException, IOException, SAXException { + + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + doc = dBuilder.parse(xmlInput); + } + + public Document getDocument() { + return doc; + } + + protected String getTextValueOfNode(Element n, String tagName) { + return n.getElementsByTagName(tagName).item(0).getTextContent(); + } + + protected boolean exists(Node node, String attribute) { + return node.getAttributes().getNamedItem(attribute) != null; + } + + public String getAttribute(Element n, String attr) { + return n.getAttribute(attr); + } + +} diff --git a/racevisionGame/src/main/java/visualiser/exceptions/StreamedCourseXMLException.java b/racevisionGame/src/main/java/visualiser/exceptions/StreamedCourseXMLException.java new file mode 100644 index 00000000..a9b1055d --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/exceptions/StreamedCourseXMLException.java @@ -0,0 +1,7 @@ +package visualiser.exceptions; + +/** + * Created by cbt24 on 25/04/17. + */ +public class StreamedCourseXMLException extends Throwable { +} diff --git a/racevisionGame/src/main/java/visualiser/model/Annotations.java b/racevisionGame/src/main/java/visualiser/model/Annotations.java new file mode 100644 index 00000000..3f03e630 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/Annotations.java @@ -0,0 +1,284 @@ +package visualiser.model; + +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.RadioButton; +import javafx.scene.control.Toggle; +import javafx.scene.layout.AnchorPane; + +import java.util.HashMap; +import java.util.Map; + +/** + * Class that processes user selected annotation visibility options to + * display the requested information on the + * {@link seng302.Model.ResizableRaceMap ResizbleRaceMap}. These are displayed + * via the {@link seng302.Controllers.RaceController RaceController}.
+ * Annotation options for a {@link seng302.Model.Boat Boat} include: its name, + * abbreviation, speed, the time since it passed the last + * {@link seng302.Model.Marker Marker}, estimated time to the next marker, + * and a path it has travelled made up of + * {@link seng302.Model.TrackPoint TrackPoint}s. + */ +public class Annotations { + private ResizableRaceCanvas raceMap; + + // checkable objects in the anchor pane + private Map checkBoxes = new HashMap<>(); + private Map annoToggles = new HashMap<>(); + + // maps of selected and saved annotations + private Map importantAnno = new HashMap<>(); + private Map annoShownBeforeHide = new HashMap<>(); + + // string values match the fx:id value of check boxes + private static String nameCheckAnno = "showName"; + private static String abbrevCheckAnno = "showAbbrev"; + private static String speedCheckAnno = "showSpeed"; + private static String pathCheckAnno = "showBoatPath"; + private static String timeCheckAnno = "showTime"; + private static String estTimeCheckAnno = "showEstTime"; + + // string values match the fx:id value of radio buttons + private static String noBtn = "noBtn"; + private static String hideBtn = "hideAnnoRBtn"; + private static String showBtn = "showAnnoRBtn"; + private static String partialBtn = "partialAnnoRBtn"; + private static String importantBtn = "importantAnnoRBtn"; + + private Boolean selectShow = false; + private String buttonChecked; + private String prevBtnChecked; + @FXML Button saveAnnoBtn; + + /** + * Constructor to set up and display initial annotations + * @param annotationPane javaFX pane containing annotation options + * @param raceMap the canvas to update annotation displays + */ + public Annotations(AnchorPane annotationPane, ResizableRaceCanvas raceMap){ + this.raceMap = raceMap; + + for (Node child : annotationPane.getChildren()) { + // collect all check boxes into a map + if (child.getClass()==CheckBox.class){ + checkBoxes.put(child.getId(), (CheckBox)child); + } + // collect annotation toggle radio buttons into a map + else if (child.getClass()== RadioButton.class){ + //annotationGroup.getToggles().add((RadioButton)child); + annoToggles.put(child.getId(), (RadioButton)child); + } + else if (child.getClass() == Button.class){ + saveAnnoBtn = (Button)child; + } + } + initializeAnnotations(); + } + + + /** + * Set up initial boat annotations and shows all data. + * Defines partial annotations. + * Creates listeners for when the user selects a different annotation + * visibility. + */ + public void initializeAnnotations() { + for (Map.Entry checkBox : checkBoxes.entrySet()) + { annoShownBeforeHide.put(checkBox.getKey(), true); } + + addCheckBoxListeners(); + addSaveAnnoListener(); + addAnnoToggleListeners(); + + annoToggles.get(showBtn).setSelected(true); + } + + /** + * Creates listeners for each checkbox so the annotation display is + * updated when a user selects a different level of annotation visibility. + */ + private void addCheckBoxListeners(){ + //listener for show name in annotation + checkBoxes.get(nameCheckAnno).selectedProperty() + .addListener((ov, old_val, new_val) -> { + if (old_val != new_val) { + raceMap.toggleAnnoName(); + storeCurrentAnnotationState(nameCheckAnno, new_val); + raceMap.update(); + } + }); + + //listener for show abbreviation for annotation + checkBoxes.get(abbrevCheckAnno).selectedProperty() + .addListener((ov, old_val, new_val) -> { + if (old_val != new_val) { + raceMap.toggleAnnoAbbrev(); + storeCurrentAnnotationState(abbrevCheckAnno, new_val); + raceMap.update(); + } + }); + + //listener for show boat path for annotation + checkBoxes.get(pathCheckAnno).selectedProperty() + .addListener((ov, old_val, new_val) -> { + if (old_val != new_val) { + raceMap.toggleBoatPath(); + storeCurrentAnnotationState(pathCheckAnno, new_val); + raceMap.update(); + } + }); + + //listener to show speed for annotation + checkBoxes.get(speedCheckAnno).selectedProperty() + .addListener((ov, old_val, new_val) -> { + if (old_val != new_val) { + raceMap.toggleAnnoSpeed(); + storeCurrentAnnotationState(speedCheckAnno, new_val); + raceMap.update(); + } + }); + + //listener to show time for annotation + checkBoxes.get(timeCheckAnno).selectedProperty() + .addListener((ov, old_val, new_val) -> { + if (old_val != new_val) { + raceMap.toggleAnnoTime(); + storeCurrentAnnotationState(timeCheckAnno, new_val); + raceMap.update(); + } + }); + + //listener to show estimated time for annotation + checkBoxes.get(estTimeCheckAnno).selectedProperty() + .addListener((ov, old_val, new_val) -> { + if (old_val != new_val) { + raceMap.toggleAnnoEstTime(); + storeCurrentAnnotationState(estTimeCheckAnno, new_val); + raceMap.update(); + } + }); + } + + /** + * Creates a listener so the system knows when to save a users currently + * selected annotation options as important for future use. + */ + private void addSaveAnnoListener(){ + //listener to save currently selected annotations as important + saveAnnoBtn.setOnAction(event -> { + importantAnno.clear(); + for (Map.Entry checkBox : checkBoxes.entrySet()){ + importantAnno.put(checkBox.getKey(), + checkBox.getValue().isSelected()); + } + }); + } + + /** + * Creates listeners for each visibility option so that the annotation + * display is updated when a user selects a different level of annotation + * visibility. + */ + private void addAnnoToggleListeners(){ + //listener for hiding all annotations + RadioButton hideAnnoRBtn = (RadioButton)annoToggles.get(hideBtn); + hideAnnoRBtn.setOnAction((e)->{ + buttonChecked = hideBtn; + selectShow = false; + for (Map.Entry checkBox : checkBoxes.entrySet()){ + checkBox.getValue().setSelected(false); + } + raceMap.update(); + buttonChecked = noBtn; + prevBtnChecked = hideBtn; + selectShow = true; + }); + + //listener for showing previously visible annotations + RadioButton showAnnoRBTN = (RadioButton)annoToggles.get(showBtn); + showAnnoRBTN.setOnAction((e)->{ + if (selectShow) { + buttonChecked = showBtn; + for (Map.Entry checkBox : checkBoxes.entrySet()){ + checkBox.getValue().setSelected( + annoShownBeforeHide.get(checkBox.getKey())); + } + raceMap.update(); + buttonChecked = noBtn; + prevBtnChecked = showBtn; + } + selectShow = true; + }); + + //listener for showing a predetermined subset of annotations + RadioButton partialAnnoRBTN = (RadioButton)annoToggles.get(partialBtn); + partialAnnoRBTN.setOnAction((e)->{ + selectShow = false; + buttonChecked = partialBtn; + for (Map.Entry checkBox : checkBoxes.entrySet()){ + // the checkbox defaults for partial annotations + if (checkBox.getKey().equals(abbrevCheckAnno) + || checkBox.getKey().equals(speedCheckAnno)){ + checkBox.getValue().setSelected(true); + } + else { checkBox.getValue().setSelected(false); } + } + raceMap.update(); + buttonChecked = noBtn; + prevBtnChecked = partialBtn; + selectShow = true; + }); + + //listener for showing all saved important annotations + RadioButton importantAnnoRBTN = (RadioButton)annoToggles.get(importantBtn); + importantAnnoRBTN.setOnAction((e) ->{ + selectShow = false; + buttonChecked = importantBtn; + if (importantAnno.size()>0){ + for (Map.Entry checkBox : checkBoxes.entrySet()){ + checkBox.getValue().setSelected + (importantAnno.get(checkBox.getKey())); + } + } + buttonChecked = noBtn; + prevBtnChecked = importantBtn; + selectShow = true; + }); + } + + /** + * Updates the current state of an annotation so that when a user + * deselects the hidden visibility option, the previous annotations will + * be displayed again. + * @param dictionaryAnnotationKey annotation checkbox + * @param selected boolean value representing the state of the checkbox + */ + private void storeCurrentAnnotationState(String dictionaryAnnotationKey, boolean selected){ + if (buttonChecked != hideBtn) { + //if we are checking the box straight out of hide instead of using the radio buttons + annoShownBeforeHide.put(dictionaryAnnotationKey, selected); + if (prevBtnChecked == hideBtn && buttonChecked == noBtn){ + storeCurrentAnnotationDictionary(); + } + if (buttonChecked == noBtn) { + selectShow = false; + annoToggles.get(showBtn).setSelected(true); + } + } + } + + /** + * Stores all current annotation states so that when a user + * deselects the hidden visibility option, the previous annotations will + * be displayed again. + */ + private void storeCurrentAnnotationDictionary(){ + for (Map.Entry checkBox : checkBoxes.entrySet()){ + annoShownBeforeHide.put(checkBox.getKey(), + checkBox.getValue().isSelected()); + } + } +} diff --git a/racevisionGame/src/main/java/visualiser/model/GraphCoordinate.java b/racevisionGame/src/main/java/visualiser/model/GraphCoordinate.java new file mode 100644 index 00000000..9dddf48e --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/GraphCoordinate.java @@ -0,0 +1,42 @@ +package visualiser.model; + +/** + * It is a coordinate representing a location on the + * {@link seng302.Model.ResizableRaceMap ResizableRaceMap}. + * It has been converted from a {@link seng302.GPSCoordinate GPSCoordinate} + * to display objects in their relative positions. + */ +public class GraphCoordinate { + private final int x; + private final int y; + + /** + * Constructor method. + * + * @param x X coordinate. + * @param y Y coordinate. + */ + public GraphCoordinate(int x, int y) { + this.x = x; + this.y = y; + } + + /** + * Returns the X coordinate. + * + * @return x axis Coordinate. + */ + public int getX() { + return x; + } + + /** + * Returns the Y coordinate. + * + * @return y axis Coordinate. + */ + public int getY() { + return y; + } + +} diff --git a/racevisionGame/src/main/java/visualiser/model/RaceClock.java b/racevisionGame/src/main/java/visualiser/model/RaceClock.java new file mode 100644 index 00000000..a74f00ff --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/RaceClock.java @@ -0,0 +1,133 @@ +package visualiser.model; + +import com.github.bfsmith.geotimezone.TimeZoneLookup; +import com.github.bfsmith.geotimezone.TimeZoneResult; +import javafx.animation.AnimationTimer; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import seng302.GPSCoordinate; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Date; + +/** + * This class is used to implement a clock which keeps track of and + * displays times relevant to a race. This is displayed on the + * {@link seng302.Model.ResizableRaceCanvas ResizableRaceCanvas} via the + * {@link seng302.Controllers.RaceController RaceController} and the + * {@link seng302.Controllers.StartController StartController}. + */ +public class RaceClock implements Runnable { + private long lastTime; + private final ZoneId zoneId; + private ZonedDateTime time; + private ZonedDateTime startingTime; + private final StringProperty timeString; + private StringProperty duration; + + public RaceClock(ZonedDateTime zonedDateTime) { + this.zoneId = zonedDateTime.getZone(); + this.timeString = new SimpleStringProperty(); + this.duration = new SimpleStringProperty(); + this.time = zonedDateTime; + setTime(time); + } + + public static ZonedDateTime getCurrentZonedDateTime(GPSCoordinate gpsCoordinate) { + TimeZoneLookup timeZoneLookup = new TimeZoneLookup(); + TimeZoneResult timeZoneResult = timeZoneLookup.getTimeZone(gpsCoordinate.getLatitude(), gpsCoordinate.getLongitude()); + ZoneId zone = ZoneId.of(timeZoneResult.getResult()); + return LocalDateTime.now(zone).atZone(zone); + } + + public void run() { + new AnimationTimer() { + @Override + public void handle(long now) { + updateTime(); + } + }.start(); + } + + /** + * Sets time to arbitrary zoned time. + * + * @param time arbitrary time with timezone. + */ + private void setTime(ZonedDateTime time) { + this.time = time; + this.timeString.set(DateTimeFormatter.ofPattern("HH:mm:ss dd/MM/YYYY Z").format(time)); + this.lastTime = System.currentTimeMillis(); + + + if(startingTime != null) { + long seconds = Duration.between(startingTime.toLocalDateTime(), time.toLocalDateTime()).getSeconds(); + if(seconds < 0) + duration.set(String.format("Starting in: %02d:%02d:%02d", -seconds/3600, -(seconds%3600)/60, -seconds%60)); + else + duration.set(String.format("Time: %02d:%02d:%02d", seconds/3600, (seconds%3600)/60, seconds%60)); + } + + } + + /** + * Sets time to given UTC time in seconds from Unix epoch, preserving timezone. + * @param time UTC time + */ + public void setUTCTime(long time) { + Date utcTime = new Date(time); + setTime(utcTime.toInstant().atZone(this.zoneId)); + } + + public ZonedDateTime getStartingTime() { + return startingTime; + } + + public void setStartingTime(ZonedDateTime startingTime) { + this.startingTime = startingTime; + } + + /** + * Get ZonedDateTime corresponding to local time zone and given UTC time. + * @param time time in mills + * @return local date time + */ + public ZonedDateTime getLocalTime(long time) { + Date utcTime = new Date(time); + return utcTime.toInstant().atZone(this.zoneId); + } + + /** + * Updates time by duration elapsed since last update. + */ + private void updateTime() { + this.time = this.time.plus(Duration.of(System.currentTimeMillis() - this.lastTime, ChronoUnit.MILLIS)); + this.lastTime = System.currentTimeMillis(); + setTime(time); + } + + public String getDuration() { + return duration.get(); + } + + public StringProperty durationProperty() { + return duration; + } + + public String getTimeZone() { + return zoneId.toString(); + } + + public ZonedDateTime getTime() { + return time; + } + + public StringProperty timeStringProperty() { + return timeString; + } +} diff --git a/racevisionGame/src/main/java/visualiser/model/RaceConnection.java b/racevisionGame/src/main/java/visualiser/model/RaceConnection.java new file mode 100644 index 00000000..de02fafa --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/RaceConnection.java @@ -0,0 +1,57 @@ +package visualiser.model; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; + +/** + * Created by cbt24 on 3/05/17. + */ +public class RaceConnection { + private final StringProperty hostname; + private final int port; + private final StringProperty status; + + public RaceConnection(String hostname, int port) { + this.hostname = new SimpleStringProperty(hostname); + this.port = port; + this.status = new SimpleStringProperty(""); + check(); + } + + /** + * Tries to create a socket to hostname and port, indicates status after test. + * @return true if socket can connect + */ + @SuppressWarnings("unused") + public boolean check() { + InetSocketAddress i = new InetSocketAddress(hostname.get(), port); + try (Socket s = new Socket()){ + s.connect(i, 5000); + status.set("Ready"); + return true; + } catch (IOException e) {} + + status.set("Offline"); + return false; + } + + public String getHostname() { + return hostname.get(); + } + + public StringProperty hostnameProperty() { + return hostname; + } + + public int getPort() { + return port; + } + + public StringProperty statusProperty() { + return status; + } +} diff --git a/racevisionGame/src/main/java/visualiser/model/RaceMap.java b/racevisionGame/src/main/java/visualiser/model/RaceMap.java new file mode 100644 index 00000000..3f58b95a --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/RaceMap.java @@ -0,0 +1,76 @@ +package visualiser.model; + +/** + * The base size of the map to be used for the + * {@link seng302.Model.ResizableRaceMap ResizableRaceMap} and + * {@link seng302.Model.ResizableRaceCanvas ResizableRaceCanvas}. It is used + * to convert {@link seng302.GPSCoordinate GPSCoordinate}s to relative + * {@link seng302.GraphCoordinate GraphCoordinate}s. + */ +public class RaceMap { + private final double x1; + private final double x2; + private final double y1; + private final double y2; + private int width, height; + + /** + * Constructor Method. + * + * @param x1 Longitude of the top left point. + * @param y1 Latitude of the top left point. + * @param x2 Longitude of the top right point. + * @param y2 Latitude of the top right point. + * @param width width that the Canvas the race is to be drawn on is. + * @param height height that the Canvas the race is to be drawn on is. + */ + public RaceMap(double y1, double x1, double y2, double x2, int height, int width) { + this.x1 = x1; + this.x2 = x2; + this.y1 = y1; + this.y2 = y2; + this.width = width; + this.height = height; + } + + /** + * Converts GPS coordinates to coordinates for container + * + * @param lat GPS latitude + * @param lon GPS longitude + * @return GraphCoordinate (pair of doubles) + * @see GraphCoordinate + */ + private GraphCoordinate convertGPS(double lat, double lon) { + int difference = Math.abs(width - height); + int size = width; + if (width > height) { + size = height; + return new GraphCoordinate((int) ((size * (lon - x1) / (x2 - x1)) + difference / 2), (int) (size - (size * (lat - y1) / (y2 - y1)))); + } else { + return new GraphCoordinate((int) (size * (lon - x1) / (x2 - x1)), (int) ((size - (size * (lat - y1) / (y2 - y1))) + difference / 2)); + } + + //return new GraphCoordinate((int) (width * (lon - x1) / (x2 - x1)), (int) (height - (height * (lat - y1) / (y2 - y1)))); + } + + /** + * Converts the GPS Coordinate to GraphCoordinates + * + * @param coordinate GPSCoordinate representation of Latitude and Longitude. + * @return GraphCoordinate that the GPS is coordinates are to be displayed on the map. + * @see GraphCoordinate + * @see GPSCoordinate + */ + public GraphCoordinate convertGPS(GPSCoordinate coordinate) { + return convertGPS(coordinate.getLatitude(), coordinate.getLongitude()); + } + + public void setWidth(int width) { + this.width = width; + } + + public void setHeight(int height) { + this.height = height; + } +} diff --git a/racevisionGame/src/main/java/visualiser/model/ResizableCanvas.java b/racevisionGame/src/main/java/visualiser/model/ResizableCanvas.java new file mode 100644 index 00000000..9e691037 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/ResizableCanvas.java @@ -0,0 +1,55 @@ +package visualiser.model; + +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; + +/** + * The abstract class for the resizable race canvases. + */ +public abstract class ResizableCanvas extends Canvas { + protected final GraphicsContext gc; + + public ResizableCanvas(){ + this.gc = this.getGraphicsContext2D(); + // Redraw canvas when size changes. + widthProperty().addListener(evt -> draw()); + heightProperty().addListener(evt -> draw()); + + } + + abstract void draw(); + + + /** + * Set the Canvas to resizable. + * + * @return That the Canvas is resizable. + */ + @Override + public boolean isResizable() { + return true; + } + + /** + * Returns the preferred width of the Canvas + * + * @param width of canvas + * @return Returns the width of the Canvas + */ + @Override + public double prefWidth(double width) { + return getWidth(); + } + + /** + * Returns the preferred height of the Canvas + * + * @param height of canvas + * @return Returns the height of the Canvas + */ + @Override + public double prefHeight(double height) { + return getHeight(); + } + +} diff --git a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java new file mode 100644 index 00000000..f1b1b42b --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java @@ -0,0 +1,392 @@ +package visualiser.model; + + +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; +import javafx.scene.transform.Rotate; +import seng302.Mock.StreamedCourse; +import seng302.RaceDataSource; +import seng302.RaceMap; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.*; + +/** + * This JavaFX Canvas is used to update and display details for a + * {@link seng302.RaceMap RaceMap} via the + * {@link seng302.Controllers.RaceController RaceController}.
+ * It fills it's parent and cannot be downsized.
+ * Details displayed include: + * {@link seng302.Model.Boat Boats} (and their + * {@link seng302.Model.TrackPoint TrackPoint}s), + * {@link seng302.Model.Marker Markers}, a + * {@link seng302.Model.RaceClock RaceClock}, a wind direction arrow and + * various user selected {@link seng302.Model.Annotations Annotations}. + */ +public class ResizableRaceCanvas extends ResizableCanvas { + private RaceMap map; + private List boats; + private List boatMarkers; + private boolean annoName = true; + private boolean annoAbbrev = true; + private boolean annoSpeed = true; + private boolean annoPath = true; + private boolean annoEstTime = true; + private boolean annoTimeSinceLastMark = true; + private List colours; + private final List markers; + private final RaceDataSource raceData; + private Map boatColours = new HashMap<>(); + private Node arrow; + + private RaceClock raceClock; + + public ResizableRaceCanvas(RaceDataSource raceData) { + super(); + + double lat1 = raceData.getMapTopLeft().getLatitude(); + double long1 = raceData.getMapTopLeft().getLongitude(); + double lat2 = raceData.getMapBottomRight().getLatitude(); + double long2 = raceData.getMapBottomRight().getLongitude(); + + setMap(new RaceMap(lat1, long1, lat2, long2, (int) getWidth(), (int) getHeight())); + + this.markers = raceData.getMarkers(); + makeColours(); + this.raceData = raceData; + } + + /** + * Sets the boats that are to be displayed in this race. + * + * @param boats in race + */ + public void setBoats(List boats) { + this.boats = boats; + mapBoatColours(); + } + + + /** + * Sets the boat markers that are to be displayed in this race. + * + * @param boatMarkers in race + */ + public void setBoatMarkers(ObservableList boatMarkers) { + this.boatMarkers = boatMarkers; + } + + /** + * Sets the RaceMap that the RaceCanvas is to be displaying for. + * + * @param map race map + */ + private void setMap(RaceMap map) { + this.map = map; + } + + private void displayBoat(Boat boat, double angle, Color colour) { + if (boat.getCurrentPosition() != null) { + GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition()); + + double[] x = {pos.getX() - 6, pos.getX(), pos.getX() + 6}; + double[] y = {pos.getY() + 12, pos.getY() - 12, pos.getY() + 12}; + gc.setFill(colour); + + gc.save(); + rotate(angle, pos.getX(), pos.getY()); + gc.fillPolygon(x, y, 3); + gc.restore(); + } + } + + /** + * Displays a line on the map with rectangles on the starting and ending point of the line. + * + * @param graphCoordinateA Starting Point of the line in GraphCoordinate. + * @param graphCoordinateB End Point of the line in GraphCoordinate. + * @param paint Colour the line is to coloured. + * @see GraphCoordinate + * @see Color + * @see Paint + */ + private void displayLine(GraphCoordinate graphCoordinateA, GraphCoordinate graphCoordinateB, Paint paint) { + gc.setStroke(paint); + gc.setFill(paint); + gc.fillOval(graphCoordinateA.getX() - 3, graphCoordinateA.getY() - 3, 6, 6); + gc.fillOval(graphCoordinateB.getX() - 3, graphCoordinateB.getY() - 3, 6, 6); + gc.strokeLine(graphCoordinateA.getX(), graphCoordinateA.getY(), graphCoordinateB.getX(), graphCoordinateB.getY()); + } + + /** + * Display a point on the Canvas + * + * @param graphCoordinate Coordinate that the point is to be displayed at. + * @param paint Colour that the boat is to be coloured. + * @see GraphCoordinate + * @see Paint + * @see Color + */ + private void displayPoint(GraphCoordinate graphCoordinate, Paint paint) { + gc.setFill(paint); + gc.fillOval(graphCoordinate.getX(), graphCoordinate.getY(), 10, 10); + } + + + /** + * Displays an arrow representing wind direction on the Canvas + * + * @param angle Angle that the arrow is to be facing in degrees 0 degrees = North (Up). + * @see GraphCoordinate + */ + private void displayWindArrow(double angle) { + angle = angle % 360; + + // show direction wind is coming from + if (angle<180){angle = angle + 180;} + else {angle = angle - 180;} + + if (arrow != null && arrow.getRotate() != angle) { + arrow.setRotate(angle); + } + } + + /** + * Rotates things on the canvas Note: this must be called in between gc.save() and gc.restore() else they will rotate everything + * + * @param angle Bearing angle to rotate at in degrees + * @param px Pivot point x of rotation. + * @param py Pivot point y of rotation. + */ + private void rotate(double angle, double px, double py) { + Rotate r = new Rotate(angle, px, py); + gc.setTransform(r.getMxx(), r.getMyx(), r.getMxy(), r.getMyy(), r.getTx(), r.getTy()); + } + + /** + * Display given name and speed of boat at a graph coordinate + * + * @param name name of the boat + * @param abbrev abbreviation of the boat name + * @param speed speed of the boat + * @param coordinate coordinate the text appears + * @param timeSinceLastMark time since the last mark was passed + */ + private void displayText(String name, String abbrev, double speed, GraphCoordinate coordinate, String estTime, ZonedDateTime timeSinceLastMark) { + String text = ""; + //Check name toggle value + if (annoName){ + text += String.format("%s ", name); + } + //Check abbreviation toggle value + if (annoAbbrev){ + text += String.format("%s ", abbrev); + } + //Check speed toggle value + if (annoSpeed){ + text += String.format("%.2fkn ", speed); + } + if (annoEstTime) { + text += estTime; + } + //Check time since last mark toggle value + if(annoTimeSinceLastMark){ + Duration timeSince = Duration.between(timeSinceLastMark, raceClock.getTime()); + text += String.format(" %ds ", timeSince.getSeconds()); + } + //String text = String.format("%s, %2$.2fkn", name, speed); + long xCoord = coordinate.getX() + 20; + long yCoord = coordinate.getY(); + if (xCoord + (text.length() * 7) >= getWidth()) { + xCoord -= text.length() * 7; + } + if (yCoord - (text.length() * 2) <= 0) { + yCoord += 30; + } + gc.fillText(text, xCoord, yCoord); + } + + /** + * Draws race map with up to date data. + */ + public void update() { + this.draw(); + this.updateBoats(); + } + + /** + * Draw race markers + */ + private void drawMarkers() { + for(Marker marker: markers) { + GraphCoordinate mark1 = this.map.convertGPS(marker.getMark1()); + // removed drawing of lines between the marks as only + // the start and finish line should have a line drawn + if(marker.isCompoundMark()) { + GraphCoordinate mark2 = this.map.convertGPS(marker.getMark2()); + displayPoint(mark1, Color.LIMEGREEN); + displayPoint(mark2, Color.LIMEGREEN); + } else { + displayPoint(mark1, Color.GREEN); + } + } + } + + /** + * Draws the Race Map + */ + public void draw() { + + double width = getWidth(); + double height = getHeight(); + + gc.clearRect(0, 0, width, height); + + if (map == null) { + return;//TODO this should return a exception in the future + } + this.map.setHeight((int) height); + this.map.setWidth((int) width); + + gc.setLineWidth(2); + updateBoats(); + drawMarkers(); + + //display wind direction arrow - specify origin point and angle - angle now set to random angle + if (raceData instanceof StreamedCourse) { + displayWindArrow(((StreamedCourse) raceData).getWindDirection()); + } else { + displayWindArrow(150); + } + } + + + /** + * Toggle name display in annotation + */ + public void toggleAnnoName() { + annoName = !annoName; + } + + /** + * Toggle boat path display in annotation + */ + public void toggleBoatPath() { + annoPath = !annoPath; + } + + public void toggleAnnoEstTime() { + annoEstTime = !annoEstTime; + } + + /** + * Toggle boat time display in annotation + */ + public void toggleAnnoTime() { annoTimeSinceLastMark = !annoTimeSinceLastMark;} + + /** + * Toggle abbreviation display in annotation + */ + public void toggleAnnoAbbrev() { + annoAbbrev = !annoAbbrev; + } + + /** + * Toggle speed display in annotation + */ + public void toggleAnnoSpeed() { + annoSpeed = !annoSpeed; + } + + /** + * Draws boats while race in progress, when leg heading is set. + */ + private void updateBoats() { + if (boats != null) { + if (boatColours.size() < boats.size()) mapBoatColours(); + for (Boat boat : boats) { + boolean finished = boat.getCurrentLeg().getName().equals("Finish") || boat.getCurrentLeg().getName().equals("DNF"); + boolean isStart = boat.isStarted(); + int sourceID = boat.getSourceID(); + if (!finished && isStart) { + displayBoat(boat, boat.getHeading(), boatColours.get(sourceID)); + GraphCoordinate wakeFrom = this.map.convertGPS(boat.getCurrentPosition()); + GraphCoordinate wakeTo = this.map.convertGPS(boat.getWake()); + displayLine(wakeFrom, wakeTo, boatColours.get(sourceID)); + } else if (!isStart) { + displayBoat(boat, boat.getHeading(), boatColours.get(sourceID)); + } else { + displayBoat(boat, 0, boatColours.get(sourceID)); + } + + if (Duration.between(boat.getTimeSinceLastMark(), raceClock.getTime()).getSeconds() < 0) { + boat.setTimeSinceLastMark(raceClock.getTime()); + } + + //If the race hasn't started, we set the time since last mark to the current time, to ensure we don't start counting until the race actually starts. + if (boat.isStarted() == false) { + boat.setTimeSinceLastMark(raceClock.getTime()); + } + + displayText(boat.toString(), boat.getAbbrev(), boat.getVelocity(), this.map.convertGPS(boat.getCurrentPosition()), boat.getFormattedEstTime(), boat.getTimeSinceLastMark()); + //TODO this needs to be fixed. + drawTrack(boat, boatColours.get(sourceID)); + } + } + } + + /** + * Draws all track points for a given boat. Colour is set by boat, opacity by track point. + * @param boat whose track is displayed + * @param colour The color to use for the track. + * @see seng302.Model.TrackPoint + */ + private void drawTrack(Boat boat, Color colour) { + if (annoPath) { + for (TrackPoint point : boat.getTrack()) { + GraphCoordinate scaledCoordinate = this.map.convertGPS(point.getCoordinate()); + gc.setFill(new Color(colour.getRed(), colour.getGreen(), colour.getBlue(), point.getAlpha())); + gc.fillOval(scaledCoordinate.getX(), scaledCoordinate.getY(), 5, 5); + } + } + } + + /** + * makes colours + */ + private void makeColours() { + colours = new ArrayList<>(Arrays.asList( + Color.BLUEVIOLET, + Color.BLACK, + Color.RED, + Color.ORANGE, + Color.DARKOLIVEGREEN, + Color.LIMEGREEN, + Color.PURPLE, + Color.DARKGRAY, + Color.YELLOW + )); + } + + public void setArrow(Node arrow) { + this.arrow = arrow; + } + + public void setRaceClock(RaceClock raceClock) { + this.raceClock = raceClock; + } + + private void mapBoatColours() { + int currentColour = 0; + for (Boat boat : boats) { + if (!boatColours.containsKey(boat.getSourceID())) { + boatColours.put(boat.getSourceID(), colours.get(currentColour)); + } + currentColour = (currentColour + 1) % colours.size(); + } + } + +} diff --git a/racevisionGame/src/main/java/visualiser/model/ResizableRaceMap.java b/racevisionGame/src/main/java/visualiser/model/ResizableRaceMap.java new file mode 100644 index 00000000..d7a0def3 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceMap.java @@ -0,0 +1,93 @@ +package visualiser.model; + +import javafx.scene.paint.Color; +import seng302.GPSCoordinate; +import seng302.RaceDataSource; +import seng302.RaceMap; + +import java.util.List; + +/** + * This JavaFX Canvas is used to generate the size of a + * {@link seng302.RaceMap RaceMap} using co-ordinates from a + * {@link seng302.RaceDataSource RaceDataSource}. This is done via the + * {@link seng302.Controllers.RaceController RaceController}. + */ +public class ResizableRaceMap extends ResizableCanvas { + private RaceMap map; + private final List raceBoundaries; + private double[] xpoints = {}; + private double[] ypoints = {}; + + /** + * Constructor + * @param raceData Race which it is taking its information to be displayed from + */ + public ResizableRaceMap(RaceDataSource raceData){ + super(); + raceBoundaries = raceData.getBoundary(); + + double lat1 = raceData.getMapTopLeft().getLatitude(); + double long1 = raceData.getMapTopLeft().getLongitude(); + double lat2 = raceData.getMapBottomRight().getLatitude(); + double long2 = raceData.getMapBottomRight().getLongitude(); + setMap(new RaceMap(lat1, long1, lat2, long2, (int) getWidth(), (int) getHeight())); + //draw(); + } + + /** + * Sets the map race that it is supposed to be viewing. + * @param map the map to be set + */ + private void setMap(RaceMap map) { + this.map = map; + } + + /** + * Draw boundary of the race. + */ + private void drawBoundaries() { + if (this.raceBoundaries == null) { + return; + } + gc.setFill(Color.AQUA); + setRaceBoundCoordinates(); + + gc.setLineWidth(1); + gc.fillPolygon(xpoints, ypoints, xpoints.length); + } + + /** + * Sets the coordinately of the race boundaries + */ + private void setRaceBoundCoordinates() { + xpoints = new double[this.raceBoundaries.size()]; + ypoints = new double[this.raceBoundaries.size()]; + for (int i = 0; i < raceBoundaries.size(); i++) { + GraphCoordinate coord = map.convertGPS(raceBoundaries.get(i)); + xpoints[i] = coord.getX(); + ypoints[i] = coord.getY(); + } + } + + /** + * Draw update for the canvas + */ + public void draw(){ + + double width = getWidth(); + double height = getHeight(); + + gc.clearRect(0, 0, width, height); + + if (map == null) { + return;//TODO this should return a exception in the future + } + this.map.setHeight((int) height); + this.map.setWidth((int) width); + + gc.setLineWidth(2); + drawBoundaries(); + } + +} diff --git a/racevisionGame/src/main/java/visualiser/model/Sparkline.java b/racevisionGame/src/main/java/visualiser/model/Sparkline.java new file mode 100644 index 00000000..b494eb9a --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/Sparkline.java @@ -0,0 +1,180 @@ +package visualiser.model; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.paint.Color; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Class to process and modify a sparkline display. This display keeps visual + * track of {@link seng302.Model.Boat Boats}s in a race and their current + * placing position as they complete each {@link seng302.Model.Leg Leg} by + * passing a course {@link seng302.Model.Marker Marker}.
+ * This sparkline is displayed using the + * {@link seng302.Controllers.RaceController RaceController}. + */ +public class Sparkline { + private ArrayList colours; + private ArrayList startBoats = new ArrayList<>(); + private Map boatColours = new HashMap<>(); + private Integer legNum; + private Integer sparkLineNumber = 0; + @FXML LineChart sparklineChart; + @FXML NumberAxis xAxis; + @FXML NumberAxis yAxis; + + + /** + * Constructor to set up initial sparkline (LineChart) object + * @param boats boats to display on the sparkline + * @param legNum total number of legs in the race + * @param sparklineChart javaFX LineChart for the sparkline + */ + public Sparkline(ObservableList boats, Integer legNum, + LineChart sparklineChart) { + this.sparklineChart = sparklineChart; + this.legNum = legNum; + this.yAxis = (NumberAxis)sparklineChart.getYAxis(); + this.xAxis = (NumberAxis)sparklineChart.getXAxis(); + startBoats.addAll(boats); + + makeColours(); + mapBoatColours(); + createSparkline(); + } + + + /** + * Creates and sets initial display for Sparkline for race positions. + * A data series for each boat in the race is added. + * Position numbers are displayed. + */ + public void createSparkline(){ + // NOTE: Y axis is in negatives to display correct positions + + // all boats start in 'last' place + for (int i=0; i series = new XYChart.Series(); + series.getData().add(new XYChart.Data(0, -startBoats.size())); + series.getData().add(new XYChart.Data(0, -startBoats.size())); + sparklineChart.getData().add(series); + sparklineChart.getData().get(i).getNode().setStyle("-fx-stroke: " + + ""+boatColours.get(startBoats.get(i).getSourceID())+";"); + } + + sparklineChart.setCreateSymbols(false); + + // set x axis details + xAxis.setAutoRanging(false); + xAxis.setTickMarkVisible(false); + xAxis.setTickLabelsVisible(false); + xAxis.setMinorTickVisible(false); + xAxis.setUpperBound((startBoats.size()+1)*legNum); + xAxis.setTickUnit((startBoats.size()+1)*legNum); + + // set y axis details + yAxis.setLowerBound(-(startBoats.size()+1)); + yAxis.setUpperBound(0); + yAxis.setAutoRanging(false); + yAxis.setLabel("Position in Race"); + yAxis.setTickUnit(1); + yAxis.setTickMarkVisible(false); + yAxis.setMinorTickVisible(false); + + // hide minus number from displaying on axis + yAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(yAxis) { + @Override + public String toString(Number value) { + if ((Double)value == 0.0 + || (Double)value < -startBoats.size()){ + return ""; + } + else { + return String.format("%7.0f", -value.doubleValue()); + } + } + }); + } + + /** + * Updates the sparkline to display current boat positions. + * New points are plotted to represent each boat when required. + * @param boatsInRace current position of the boats in race + */ + public void updateSparkline(ObservableList boatsInRace){ + int placingVal = boatsInRace.size(); + sparkLineNumber++; + + for (int i = boatsInRace.size() - 1; i >= 0; i--){ + for (int j = startBoats.size() - 1; j >= 0; j--){ + if (boatsInRace.get(i)==startBoats.get(j)){ + + // when a boat is on its first leg + if (boatsInRace.get(i).getCurrentLeg().getLegNumber()==0){ + // adjust boats latest point on X axis + sparklineChart.getData().get(j).getData().get(1) + .setXValue(sparkLineNumber); + } + + // when a boat first enters its second leg + else if (boatsInRace.get(i).getCurrentLeg().getLegNumber + ()==1 && sparklineChart.getData().get(j).getData + ().size()==2){ + // adjust boats position from start mark + sparklineChart.getData().get(j).getData().get(1) + .setYValue(-placingVal); + sparklineChart.getData().get(j).getData().get(1) + .setXValue(sparkLineNumber); + sparklineChart.getData().get(j).getData().add(new XYChart.Data<> + (sparkLineNumber, -placingVal)); + } + + // plot new point for boats current position + else { + sparklineChart.getData().get(j).getData().add + (new XYChart.Data<>(sparkLineNumber, -placingVal)); + } + placingVal-=1; + } + } + } + } + + private void makeColours() { + colours = new ArrayList<>(Arrays.asList( + colourToHex(Color.BLUEVIOLET), + colourToHex(Color.BLACK), + colourToHex(Color.RED), + colourToHex(Color.ORANGE), + colourToHex(Color.DARKOLIVEGREEN), + colourToHex(Color.LIMEGREEN), + colourToHex(Color.PURPLE), + colourToHex(Color.DARKGRAY), + colourToHex(Color.YELLOW) + )); + } + + private String colourToHex(Color color) { + return String.format( "#%02X%02X%02X", + (int)( color.getRed() * 255 ), + (int)( color.getGreen() * 255 ), + (int)( color.getBlue() * 255 ) ); + } + + private void mapBoatColours() { + int currentColour = 0; + for (Boat boat : startBoats) { + if (!boatColours.containsKey(boat.getSourceID())) { + boatColours.put(boat.getSourceID(), colours.get(currentColour)); + } + currentColour = (currentColour + 1) % colours.size(); + } + } +} diff --git a/racevisionGame/src/main/java/visualiser/model/StreamedCourse.java b/racevisionGame/src/main/java/visualiser/model/StreamedCourse.java new file mode 100644 index 00000000..6c2f1450 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/StreamedCourse.java @@ -0,0 +1,106 @@ +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 getBoats() { + return boatXMLReader.getBoats(); + } + + public List getLegs() { + return streamedCourseXMLReader.getLegs(); + } + + public List getMarkers() { return streamedCourseXMLReader.getMarkers(); } + + public List getBoundary() { + return streamedCourseXMLReader.getBoundary(); + } + + public ZonedDateTime getZonedDateTime() { + return streamedCourseXMLReader.getRaceStartTime(); + } + + public GPSCoordinate getMapTopLeft() { + return streamedCourseXMLReader.getMapTopLeft(); + } + + public GPSCoordinate getMapBottomRight() { + return streamedCourseXMLReader.getMapBottomRight(); + } +} diff --git a/racevisionGame/src/main/java/visualiser/model/StreamedRace.java b/racevisionGame/src/main/java/visualiser/model/StreamedRace.java new file mode 100644 index 00000000..a0232f3b --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/StreamedRace.java @@ -0,0 +1,285 @@ +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 startingBoats; + private final ObservableList boatMarkers; + private final List 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 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; + + } + +} From 7f027c8cc5d56911e27220d2cd963e309f848f9c Mon Sep 17 00:00:00 2001 From: fjc40 Date: Tue, 4 Jul 2017 17:50:43 +1200 Subject: [PATCH 03/25] Refactored and moved XMLReader, RaceDataSource, BoatXMLReader, BoatDataSource, RegattaXMLReader to shared/dataInput. --- .../java/mock/dataInput/BoatDataSource.java | 14 - .../main/java/mock/dataInput/PolarParser.java | 1 + .../java/mock/dataInput/RaceDataSource.java | 32 - .../java/mock/dataInput/RaceXMLReader.java | 2 + .../src/main/java/mock/model/Polars.java | 1 + .../src/main/java/mock/model/Race.java | 927 ++++++++++++++++++ .../java/shared/dataInput/BoatDataSource.java | 25 + .../dataInput/BoatXMLReader.java | 37 +- .../java/shared/dataInput/RaceDataSource.java | 72 ++ .../dataInput/RegattaXMLReader.java | 80 +- .../{mock => shared}/dataInput/XMLReader.java | 71 +- .../shared/exceptions/XMLReaderException.java | 15 + .../visualiser/dataInput/BoatXMLReader.java | 156 --- .../visualiser/dataInput/RaceDataSource.java | 26 - .../java/visualiser/dataInput/XMLReader.java | 52 - 15 files changed, 1160 insertions(+), 351 deletions(-) delete mode 100644 racevisionGame/src/main/java/mock/dataInput/BoatDataSource.java delete mode 100644 racevisionGame/src/main/java/mock/dataInput/RaceDataSource.java create mode 100644 racevisionGame/src/main/java/mock/model/Race.java create mode 100644 racevisionGame/src/main/java/shared/dataInput/BoatDataSource.java rename racevisionGame/src/main/java/{mock => shared}/dataInput/BoatXMLReader.java (82%) create mode 100644 racevisionGame/src/main/java/shared/dataInput/RaceDataSource.java rename racevisionGame/src/main/java/{visualiser => shared}/dataInput/RegattaXMLReader.java (75%) rename racevisionGame/src/main/java/{mock => shared}/dataInput/XMLReader.java (58%) create mode 100644 racevisionGame/src/main/java/shared/exceptions/XMLReaderException.java delete mode 100644 racevisionGame/src/main/java/visualiser/dataInput/BoatXMLReader.java delete mode 100644 racevisionGame/src/main/java/visualiser/dataInput/RaceDataSource.java delete mode 100644 racevisionGame/src/main/java/visualiser/dataInput/XMLReader.java diff --git a/racevisionGame/src/main/java/mock/dataInput/BoatDataSource.java b/racevisionGame/src/main/java/mock/dataInput/BoatDataSource.java deleted file mode 100644 index 92ac15b1..00000000 --- a/racevisionGame/src/main/java/mock/dataInput/BoatDataSource.java +++ /dev/null @@ -1,14 +0,0 @@ -package mock.dataInput; - -import seng302.Model.Boat; -import seng302.Model.Mark; - -import java.util.Map; - -/** - * Boats Data - */ -public interface BoatDataSource { - Map getBoats(); - Map getMarkerBoats(); -} diff --git a/racevisionGame/src/main/java/mock/dataInput/PolarParser.java b/racevisionGame/src/main/java/mock/dataInput/PolarParser.java index a45d77ed..d33c0ac5 100644 --- a/racevisionGame/src/main/java/mock/dataInput/PolarParser.java +++ b/racevisionGame/src/main/java/mock/dataInput/PolarParser.java @@ -4,6 +4,7 @@ package mock.dataInput; import mock.exceptions.InvalidPolarFileException; import mock.model.Polars; +import shared.model.Bearing; import java.io.*; import java.util.ArrayList; diff --git a/racevisionGame/src/main/java/mock/dataInput/RaceDataSource.java b/racevisionGame/src/main/java/mock/dataInput/RaceDataSource.java deleted file mode 100644 index 54c97a41..00000000 --- a/racevisionGame/src/main/java/mock/dataInput/RaceDataSource.java +++ /dev/null @@ -1,32 +0,0 @@ -package mock.dataInput; - -import seng302.Model.Boat; -import seng302.Model.CompoundMark; -import seng302.Model.GPSCoordinate; -import seng302.Model.Leg; - -import java.time.ZonedDateTime; -import java.util.List; - -/** - * Data Class for a Race - */ -public interface RaceDataSource { - List getBoats(); - - List getLegs(); - - List getBoundary(); - - List getCompoundMarks(); - - int getRaceId(); - - String getRaceType(); - - ZonedDateTime getZonedDateTime(); - - GPSCoordinate getMapTopLeft(); - - GPSCoordinate getMapBottomRight(); -} diff --git a/racevisionGame/src/main/java/mock/dataInput/RaceXMLReader.java b/racevisionGame/src/main/java/mock/dataInput/RaceXMLReader.java index 4eb35f9f..b5e410b6 100644 --- a/racevisionGame/src/main/java/mock/dataInput/RaceXMLReader.java +++ b/racevisionGame/src/main/java/mock/dataInput/RaceXMLReader.java @@ -6,6 +6,8 @@ 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; diff --git a/racevisionGame/src/main/java/mock/model/Polars.java b/racevisionGame/src/main/java/mock/model/Polars.java index 08df0325..32ee8842 100644 --- a/racevisionGame/src/main/java/mock/model/Polars.java +++ b/racevisionGame/src/main/java/mock/model/Polars.java @@ -1,6 +1,7 @@ package mock.model; import javafx.util.Pair; +import shared.model.Bearing; import java.util.ArrayList; import java.util.HashMap; diff --git a/racevisionGame/src/main/java/mock/model/Race.java b/racevisionGame/src/main/java/mock/model/Race.java new file mode 100644 index 00000000..3e90408a --- /dev/null +++ b/racevisionGame/src/main/java/mock/model/Race.java @@ -0,0 +1,927 @@ +package mock.model; + +import javafx.animation.AnimationTimer; +import javafx.collections.FXCollections; +import mock.app.MockOutput; +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 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. + * Has a course, boats, boundaries, etc... + * Is responsible for simulating the race, and sending messages to a MockOutput instance. + */ +public class Race implements Runnable { + + /** + * An observable list of boats in the race. + */ + private ObservableList boats; + + /** + * An observable list of compound marks in the race. + */ + private ObservableList compoundMarks; + + /** + * A list of legs in the race. + */ + private List legs; + + /** + * A list of coordinates describing the boundary of the course. + */ + private List boundary; + + /** + * A copy of the boundary list, except "shrunk" inwards by 50m. + */ + private List 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. + * Frame periods are multiplied by this to get the amount of time a single frame represents. + * E.g., frame period = 20ms, scale = 5, frame represents 20 * 5 = 100ms, and so boats are simulated for 100ms, even though only 20ms actually occurred. + */ + 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. + * 0 = 0%, 100 = 100%. + */ + private int dnfChance = 0; + + + /** + * The mockOutput to send messages to. + */ + private MockOutput mockOutput; + + + /** + * Wind direction bearing. + */ + private Bearing windDirection; + + /** + * Wind speed (knots). + * Convert this to millimeters per second before passing to RaceStatus. + */ + private double windSpeed; + + 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...). + * @param mockOutput The mockOutput to send events to. + */ + public Race(RaceDataSource raceData, MockOutput mockOutput) { + + this.mockOutput = mockOutput; + + this.boats = FXCollections.observableArrayList(raceData.getBoats()); + this.compoundMarks = FXCollections.observableArrayList(raceData.getCompoundMarks()); + this.boundary = raceData.getBoundary(); + this.shrinkBoundary = GPSCoordinate.getShrinkBoundary(this.boundary); + + + this.legs = raceData.getLegs(); + this.legs.add(new Leg("Finish", this.legs.size())); + + this.raceId = raceData.getRaceId(); + + //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); + + + } + + /** + * Runnable for the thread. + */ + public void run() { + initialiseBoats(); + initialiseWindDir(); + countdownTimer.start(); + } + + /** + * Parse the compound marker boats through mock output. + */ + private void parseMarks() { + for (CompoundMark compoundMark : this.compoundMarks) { + + //Get the individual marks from the compound mark. + Mark mark1 = compoundMark.getMark1(); + Mark mark2 = compoundMark.getMark2(); + + //If they aren't null, parse them (some compound marks only have one mark). + if (mark1 != null) { + this.parseIndividualMark(mark1); + } + + if (mark2 != null) { + this.parseIndividualMark(mark2); + } + + } + } + + /** + * Parses an individual marker boat, and sends it to mockOutput. + * @param mark The marker boat to parse. + */ + private void parseIndividualMark(Mark mark) { + + this.mockOutput.parseBoatLocation(mark.getSourceID(), mark.getPosition().getLatitude(), mark.getPosition().getLongitude(),0,0, totalTimeElapsed+startTime); + + } + + /** + * Parse the boats in the race, and send it to mockOutput. + */ + private void parseBoatLocations() { + + //Parse each boat. + for (Boat boat : this.boats) { + + this.parseIndividualBoatLocation(boat); + + } + + } + + /** + * Parses an individual boat, and sends it to mockOutput. + * @param boat The boat to parse. + */ + private void parseIndividualBoatLocation(Boat boat) { + + this.mockOutput.parseBoatLocation( + boat.getSourceID(), + boat.getCurrentPosition().getLatitude(), + boat.getCurrentPosition().getLongitude(), + boat.getBearing().degrees(), + boat.getCurrentSpeed(), + startTime + totalTimeElapsed + ); + + } + + + /** + * Updates the race status enumeration based on the current time, in milliseconds. + * @param currentTime The current time, in milliseconds. + */ + private void updateRaceStatusEnum(long currentTime) { + + //The amount of milliseconds until the race starts. + long timeToStart = this.startTime - currentTime; + + //Scale the time to start based on the scale factor. + long timeToStartScaled = timeToStart / this.scaleFactor; + + + if (timeToStartScaled > Constants.RacePreStartTime) { + //Time > 3 minutes is the prestart period. + this.setRaceStatusEnum(RaceStatusEnum.PRESTART); + + } else if ((timeToStartScaled <= Constants.RacePreStartTime) && (timeToStartScaled >= Constants.RacePreparatoryTime)) { + //Time between [1, 3] minutes is the warning period. + this.setRaceStatusEnum(RaceStatusEnum.WARNING); + + } else if ((timeToStartScaled <= Constants.RacePreparatoryTime) && (timeToStartScaled > 0)) { + //Time between (0, 1] minutes is the preparatory period. + this.setRaceStatusEnum(RaceStatusEnum.PREPARATORY); + + } else { + //Otherwise, the race has started! + this.setRaceStatusEnum(RaceStatusEnum.STARTED); + + } + + + } + + /** + * Parses the race status, and sends it to mockOutput. + */ + private void parseRaceStatus() { + + //A race status message contains a list of boat statuses. + List boatStatuses = new ArrayList<>(); + + //Add each boat status to the status list. + for (Boat boat : boats) { + + BoatStatus boatStatus = new BoatStatus(boat.getSourceID(), boat.getStatus(), boat.getCurrentLeg().getLegNumber(), boat.getEstimatedTime()); + + boatStatuses.add(boatStatus); + } + + //TODO REFACTOR for consistency, could send parameters to mockOutput instead of the whole racestatus. This will also fix the sequence number issue. + + //Convert wind direction and speed to ints. //TODO this conversion should be done inside the racestatus class. + int windDirectionInt = AC35UnitConverter.encodeHeading(this.windDirection.degrees()); + 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); + + mockOutput.parseRaceStatus(raceStatus); + + + } + + + /** + * Sets the status of all boats in the race to RACING. + */ + private void setBoatsStatusToRacing() { + + for (Boat boat : this.boats) { + boat.setStatus(BoatStatusEnum.RACING); + } + } + + + /** + * Countdown timer until race starts. + */ + protected AnimationTimer countdownTimer = new AnimationTimer() { + + + long currentTime = System.currentTimeMillis(); + + @Override + public void handle(long arg0) { + + //Update the race status based on the current time. + updateRaceStatusEnum(this.currentTime); + + //Parse the boat locations. + parseBoatLocations(); + + //Parse the marks. + parseMarks(); + + // Change wind direction + changeWindDir(); + + //Parse the race status. + parseRaceStatus(); + + + if (getRaceStatusEnum() == RaceStatusEnum.STARTED) { + System.setProperty("javafx.animation.fullspeed", "true"); + setBoatsStatusToRacing(); + raceTimer.start(); + this.stop(); + } + + //Update the animations timer's time. + currentTime = System.currentTimeMillis(); + } + }; + + + /** + * Timer that runs for the duration of the race, until all boats finish. + */ + private AnimationTimer raceTimer = new AnimationTimer() { + + /** + * Start time of loop, in milliseconds. + */ + long timeRaceStarted = System.currentTimeMillis(); + + /** + * The time of the previous frame, in milliseconds. + */ + long lastFrameTime = timeRaceStarted; + + @Override + public void handle(long arg0) { + + //Get the current time. + long currentTime = System.currentTimeMillis(); + + //Update the total elapsed time. + totalTimeElapsed = currentTime - this.timeRaceStarted; + + //As long as there is at least one boat racing, we still simulate the race. + if (getNumberOfActiveBoats() != 0) { + + //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. + framePeriod = 20; + + + //For each boat, we update its position, and generate a BoatLocationMessage. + for (Boat boat : boats) { + + //If it is still racing, update its position. + if (boat.getStatus() == BoatStatusEnum.RACING) { + + updatePosition(boat, framePeriod, totalTimeElapsed); + + } + + } + + } else { + //Otherwise, the race is over! + raceFinished.start(); + setRaceStatusEnum(RaceStatusEnum.FINISHED); + this.stop(); + } + + if (getNumberOfActiveBoats() != 0) { + // Change wind direction + changeWindDir(); + + //Parse the boat locations. + parseBoatLocations(); + + //Parse the marks. + parseMarks(); + + //Parse the race status. + parseRaceStatus(); + + + //Update the last frame time. + this.lastFrameTime = currentTime; + } + } + }; + + /** + * Broadcast that the race has finished. + */ + protected AnimationTimer raceFinished = new AnimationTimer(){ + 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); + if (iters > 500){ + mockOutput.stop(); + stop(); + } + iters++; + } + }; + + /** + * Initialise the boats in the race. + * This sets their starting positions and current legs. + */ + public void initialiseBoats() { + + //Gets the starting positions of the boats. + List startingPositions = getSpreadStartingPositions(); + + //Get iterators for our boat and position lists. + Iterator boatIt = this.boats.iterator(); + Iterator startPositionIt = startingPositions.iterator(); + + //Iterate over the pair of lists. + while (boatIt.hasNext() && startPositionIt.hasNext()) { + + //Get the next boat and position. + Boat boat = boatIt.next(); + GPSCoordinate startPosition = startPositionIt.next(); + + + //The boat starts on the first leg of the race. + boat.setCurrentLeg(this.legs.get(0)); + + //Boats start with 0 knots speed. + boat.setCurrentSpeed(0d); + + //Place the boat at its starting position. + boat.setCurrentPosition(startPosition); + + //Boats start facing their next marker. + boat.setBearing(boat.calculateBearingToNextMarker()); + + //Sets the boats status to prestart - it changes to racing when the race starts. + boat.setStatus(BoatStatusEnum.PRESTART); + + //We set a large time since tack change so that it calculates a new VMG when the simulation starts. + boat.setTimeSinceTackChange(999999); + + } + + } + + + /** + * Creates a list of starting positions for the different boats, so they do not appear cramped at the start line. + * + * @return A list of starting positions. + */ + public List getSpreadStartingPositions() { + + //The first compound marker of the race - the starting gate. + CompoundMark compoundMark = this.legs.get(0).getStartCompoundMark(); + + //The position of the two markers from the compound marker. + GPSCoordinate mark1Position = compoundMark.getMark1Position(); + GPSCoordinate mark2Position = compoundMark.getMark2Position(); + + + //Calculates the azimuth between the two points. + Azimuth azimuth = GPSCoordinate.calculateAzimuth(mark1Position, mark2Position); + + //Calculates the distance between the two points. + double distanceMeters = GPSCoordinate.calculateDistanceMeters(mark1Position, mark2Position); + + //The number of boats in the race. + int numberOfBoats = this.boats.size(); + + //Calculates the distance between each boat. We divide by numberOfBoats + 1 to ensure that no boat is placed on one of the starting gate's marks. + double distanceBetweenBoatsMeters = distanceMeters / (numberOfBoats + 1); + + + //List to store coordinates in. + List positions = new ArrayList<>(); + + //We start spacing boats out from mark 1. + GPSCoordinate position = mark1Position; + + //For each boat, displace position, and store it. + for (int i = 0; i < numberOfBoats; i++) { + + position = GPSCoordinate.calculateNewPosition(position, distanceBetweenBoatsMeters, azimuth); + + positions.add(position); + + } + + return positions; + } + + + /** + * Calculates a boat's VMG. + * @param boat The boat to calculate VMG for. + * @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) { + + //Get the lower and upper acceptable bounds. + Bearing lowerAcceptableBound = bearingBounds[0]; + Bearing upperAcceptableBound = bearingBounds[1]; + + + //Find the VMG inside these bounds. + VMG bestVMG = boat.getPolars().calculateVMG(this.windDirection, this.windSpeed, boat.calculateBearingToNextMarker(), lowerAcceptableBound, upperAcceptableBound); + + + + return bestVMG; + + } + + + /** + * Determines whether or not a given VMG improves the velocity of a boat, if it were currently using currentVMG. + * @param currentVMG The current VMG of the boat. + * @param potentialVMG The new VMG to test. + * @param bearingToDestination The bearing between the boat and its destination. + * @return True if the new VMG is improves velocity, false otherwise. + */ + private boolean improvesVelocity(VMG currentVMG, VMG potentialVMG, Bearing bearingToDestination) { + + //Calculates the angle between the boat and its destination. + Angle angleBetweenDestAndHeading = Angle.fromDegrees(currentVMG.getBearing().degrees() - bearingToDestination.degrees()); + + //Calculates the angle between the new VMG and the boat's destination. + Angle angleBetweenDestAndNewVMG = Angle.fromDegrees(potentialVMG.getBearing().degrees() - bearingToDestination.degrees()); + + + //Calculate the boat's current velocity. + double currentVelocity = Math.cos(angleBetweenDestAndHeading.radians()) * currentVMG.getSpeed(); + + //Calculate the potential velocity with the new VMG. + double vmgVelocity = Math.cos(angleBetweenDestAndNewVMG.radians()) * potentialVMG.getSpeed(); + + //Return whether or not the new VMG gives better velocity. + return vmgVelocity > currentVelocity; + + } + + /** + * Determines whether or not a given VMG improves the velocity of a boat. + * @param boat The boat to test. + * @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) { + + //Get the boats "current" VMG. + VMG boatVMG = new VMG(boat.getCurrentSpeed(), boat.getBearing()); + + //Check if the new VMG is better than the boat's current VMG. + return this.improvesVelocity(boatVMG, vmg, boat.calculateBearingToNextMarker()); + + } + + + /** + * Calculates the distance a boat has travelled and updates its current position according to this value. + * + * @param boat The boat to be updated. + * @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) { + + //Checks if the current boat has finished the race or not. + boolean finish = this.isLastLeg(boat.getCurrentLeg()); + + if (!finish) { + + + //Calculates the distance travelled, in meters, in the current timeslice. + double distanceTravelledMeters = boat.calculateMetersTravelled(updatePeriodMilliseconds); + + //Scale it. + distanceTravelledMeters = distanceTravelledMeters * this.scaleFactor; + + + //Move the boat forwards that many meters, and advances its time counters by enough milliseconds. + boat.moveForwards(distanceTravelledMeters, updatePeriodMilliseconds * this.scaleFactor); + + + //Only get a new VMG if the boat will go outside the course, or X seconds have elapsed. + boolean willStayInsideCourse = this.checkBearingInsideCourse(boat.getBearing(), boat.getCurrentPosition()); + long tackPeriod = 15000; + if (!willStayInsideCourse || (boat.getTimeSinceTackChange() > tackPeriod)) { + + //Calculate the boat's bearing bounds, to ensure that it doesn't go out of the course. + Bearing[] bearingBounds = this.calculateBearingBounds(boat); + + + //Calculate the new VMG. + VMG newVMG = this.calculateVMG(boat, bearingBounds); + + + //If the new vmg improves velocity, use it. + if (improvesVelocity(boat, newVMG)) { + boat.setVMG(newVMG); + + } else { + //We also need to use the new VMG if our current bearing will take us out of the course. + if (!willStayInsideCourse) { + boat.setVMG(newVMG); + } + } + } + + this.updateEstimatedTime(boat); + + + //Check the boats position (update leg and stuff). + this.checkPosition(boat, totalTimeElapsed); + + } + + } + + /** + * Calculates the upper and lower bounds that the boat may have in order to not go outside of the course. + * @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) { + + Bearing[] bearings = new Bearing[2]; + + Bearing lowerBearing = Bearing.fromDegrees(0.001); + Bearing upperBearing = Bearing.fromDegrees(359.999); + + + + double lastAngle = -1; + boolean lastAngleWasGood = false; + + //Check all bearings between [0, 360). + for (double angle = 0; angle < 360; angle += 1) { + + //Create bearing from angle. + Bearing bearing = Bearing.fromDegrees(angle); + + //Check that if it is acceptable. + boolean bearingIsGood = this.checkBearingInsideCourse(bearing, boat.getCurrentPosition()); + + + if (lastAngle != -1) { + + if (lastAngleWasGood && !bearingIsGood) { + //We have flipped over from good bearings to bad bearings. So the last good bearing is the upper bearing. + upperBearing = Bearing.fromDegrees(lastAngle); + } + + if (!lastAngleWasGood && bearingIsGood) { + //We have flipped over from bad bearings to good bearings. So the current bearing is the lower bearing. + lowerBearing = Bearing.fromDegrees(angle); + } + + } + + lastAngle = angle; + lastAngleWasGood = bearingIsGood; + + } + + + + //TODO BUG if it can't find either upper or lower, it returns (0, 359.999). Should return (boatbearing, boatbearing+0.0001) + bearings[0] = lowerBearing; + bearings[1] = upperBearing; + + return bearings; + } + + + + /** + * Checks if a given bearing, starting at a given position, would put a boat out of the course boundaries. + * @param bearing The bearing to check. + * @param position The position to start from. + * @return True if the bearing would keep the boat in the course, false if it would take it out of the course. + */ + private boolean checkBearingInsideCourse(Bearing bearing, GPSCoordinate position) { + + //Get azimuth from bearing. + Azimuth azimuth = Azimuth.fromBearing(bearing); + + + //Tests to see if a point in front of the boat is out of bounds. + double epsilonMeters = 50d; + GPSCoordinate testCoord = GPSCoordinate.calculateNewPosition(position, epsilonMeters, azimuth); + + //If it isn't inside the boundary, calculate new bearing. + if (GPSCoordinate.isInsideBoundary(testCoord, this.shrinkBoundary)) { + return true; + } else { + return false; + } + + } + + + /** + * Checks if a boat has finished any legs, or has pulled out of race (DNF). + * @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) { + + //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. + + if (boat.calculateDistanceToNextMarker() < epsilonNauticalMiles) { + //Boat has reached its target marker, and has moved on to a new leg. + + + + //Calculate how much the boat overshot the marker by. + double overshootMeters = boat.calculateDistanceToNextMarker(); + + + //Move boat on to next leg. + Leg nextLeg = this.legs.get(boat.getCurrentLeg().getLegNumber() + 1); + boat.setCurrentLeg(nextLeg); + + //Add overshoot distance into the distance travelled for the next leg. + boat.setDistanceTravelledInLeg(overshootMeters); + + //Setting a high value for this allows the boat to immediately do a large turn, as it needs to in order to get to the next mark. + boat.setTimeSinceTackChange(999999); + + + //Check if the boat has finished or stopped racing. + + if (this.isLastLeg(boat.getCurrentLeg())) { + //Boat has finished. + boat.setTimeFinished(timeElapsed); + boat.setCurrentSpeed(0); + boat.setStatus(BoatStatusEnum.FINISHED); + } else if (doNotFinish()) { + //Boat has pulled out of race. + boat.setTimeFinished(timeElapsed); + boat.setCurrentLeg(new Leg("DNF", -1)); + boat.setCurrentSpeed(0); + boat.setStatus(BoatStatusEnum.DNF); + + } + + } + + } + + + /** + * 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; + } + + + /** + * Sets the chance each boat has of failing at a gate or marker + * + * @param chance percentage chance a boat has of failing per checkpoint. + */ + protected void setDnfChance(int chance) { + if (chance >= 0 && chance <= 100) { + dnfChance = chance; + } + } + + /** + * Decides if a boat should received a DNF status. + * @return True means it should DNF, false means it shouldn't. + */ + protected boolean doNotFinish() { + Random rand = new Random(); + return rand.nextInt(100) < dnfChance; + } + + + /** + * 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. + * They become inactive by either finishing or withdrawing. + * @return The number of boats still active in the race. + */ + protected int getNumberOfActiveBoats() { + + int numberofActiveBoats = 0; + + for (Boat boat : this.boats) { + + //If the boat is currently racing, count it. + if (boat.getStatus() == BoatStatusEnum.RACING) { + numberofActiveBoats++; + } + + } + + return numberofActiveBoats; + } + + + /** + * Returns an observable list of boats in the race. + * @return List of boats in the race. + */ + public ObservableList 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); + } + + protected void changeWindDir(){ + int r = new Random().nextInt(changeWind)+1; + if(r==1){ + windDirDegrees = (0.5 + windDirDegrees) % 360; + } 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; + } + + windDir = AC35UnitConverter.convertHeading(windDirDegrees); + this.windDirection = new Bearing(windDirDegrees); + } + + protected void setChangeWind(int changeVal){ + if (changeVal>=0){ + changeWind = changeVal; + } + } + + 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) { + 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); + } + } +} diff --git a/racevisionGame/src/main/java/shared/dataInput/BoatDataSource.java b/racevisionGame/src/main/java/shared/dataInput/BoatDataSource.java new file mode 100644 index 00000000..40f12c75 --- /dev/null +++ b/racevisionGame/src/main/java/shared/dataInput/BoatDataSource.java @@ -0,0 +1,25 @@ +package shared.dataInput; + + +import shared.model.Boat; +import shared.model.Mark; + +import java.util.Map; + +/** + * Provides information about the boats and marker boats in a race. + */ +public interface BoatDataSource { + + /** + * Returns a map between source ID and boat for all boats in the race. + * @return Map between source ID and boat. + */ + Map getBoats(); + + /** + * Returns a map between source ID and mark for all marks in the race. + * @return Map between source ID and mark. + */ + Map getMarkerBoats(); +} diff --git a/racevisionGame/src/main/java/mock/dataInput/BoatXMLReader.java b/racevisionGame/src/main/java/shared/dataInput/BoatXMLReader.java similarity index 82% rename from racevisionGame/src/main/java/mock/dataInput/BoatXMLReader.java rename to racevisionGame/src/main/java/shared/dataInput/BoatXMLReader.java index 949c6b23..5e155090 100644 --- a/racevisionGame/src/main/java/mock/dataInput/BoatXMLReader.java +++ b/racevisionGame/src/main/java/shared/dataInput/BoatXMLReader.java @@ -1,44 +1,39 @@ -package mock.dataInput; +package shared.dataInput; import org.w3c.dom.Element; import org.w3c.dom.Node; -import org.xml.sax.SAXException; -import seng302.Model.Boat; -import seng302.Model.GPSCoordinate; -import seng302.Model.Mark; -import seng302.Model.Polars; - -import javax.xml.parsers.ParserConfigurationException; -import java.io.IOException; +import shared.exceptions.XMLReaderException; +import shared.model.Boat; +import shared.model.GPSCoordinate; +import shared.model.Mark; + import java.util.HashMap; import java.util.Map; /** - * Xml Reader class for Boat XML used for the race + * Xml Reader class for Boat XML used for the race. */ public class BoatXMLReader extends XMLReader implements BoatDataSource { + /** + * A map of source ID to boat for all boats in the race. + */ private final Map boatMap = new HashMap<>(); - private final Map markerMap = new HashMap<>(); /** - * Polars table to assign to each boat. + * A map of source ID to mark for all marks in the race. */ - Polars boatPolars; + private final Map markerMap = new HashMap<>(); /** * Constructor for Boat XML * * @param filePath Name/path of file to read. Read as a resource. - * @param boatPolars polars used by the boats - * @throws IOException error - * @throws SAXException error - * @throws ParserConfigurationException error + * @throws XMLReaderException Thrown if the file cannot be parsed. */ - public BoatXMLReader(String filePath, Polars boatPolars) throws IOException, SAXException, ParserConfigurationException { + public BoatXMLReader(String filePath) throws XMLReaderException { super(filePath); - this.boatPolars = boatPolars; read(); } @@ -109,9 +104,9 @@ public class BoatXMLReader extends XMLReader implements BoatDataSource { String shortName = boatNode.getAttributes().getNamedItem("ShortName").getTextContent(); if (exists(boatNode, "Country")) { String country = boatNode.getAttributes().getNamedItem("Country").getTextContent(); - boatMap.put(sourceID, new Boat(sourceID, name, country, this.boatPolars)); + boatMap.put(sourceID, new Boat(sourceID, name, country)); } else { - boatMap.put(sourceID, new Boat(sourceID, name, shortName, this.boatPolars)); + boatMap.put(sourceID, new Boat(sourceID, name, shortName)); } } diff --git a/racevisionGame/src/main/java/shared/dataInput/RaceDataSource.java b/racevisionGame/src/main/java/shared/dataInput/RaceDataSource.java new file mode 100644 index 00000000..d9238bcc --- /dev/null +++ b/racevisionGame/src/main/java/shared/dataInput/RaceDataSource.java @@ -0,0 +1,72 @@ +package shared.dataInput; + +import network.Messages.Enums.RaceTypeEnum; +import shared.model.Boat; +import shared.model.CompoundMark; +import shared.model.GPSCoordinate; +import shared.model.Leg; + +import java.time.ZonedDateTime; +import java.util.List; + +/** + * An object that holds relevant data for a race.
+ * Information includes: {@link shared.model.Boat Boat}s, + * {@link shared.model.Leg Leg}s, {@link shared.model.CompoundMark CompoundMark}s and + * the {@link shared.model.GPSCoordinate GPSCoordinate}s. + */ +public interface RaceDataSource { + /** + * Returns the list of boats competing in the race. + * @return Boats competing in the race. + */ + List getBoats(); + + /** + * Returns the list of legs in the race. + * @return The list of legs in the race. + */ + List getLegs(); + + /** + * Returns a list of coordinates representing the boundary of the race. + * @return The boundary of the race. + */ + List getBoundary(); + + /** + * Returns a list of CompoundMarks in the race. + * @return + */ + List getCompoundMarks(); + + /** + * Returns the ID of the race. + * @return The ID of the race. + */ + int getRaceId(); + + /** + * Returns the type of race. + * @return The type of race. + */ + RaceTypeEnum getRaceType(); + + /** + * Returns the start time/date of the race. + * @return The race's start time. + */ + ZonedDateTime getZonedDateTime(); + + /** + * Returns the GPS coordinate of the top left of the race map area. + * @return Top left GPS coordinate. + */ + GPSCoordinate getMapTopLeft(); + + /** + * Returns the GPS coordinate of the bottom right of the race map area. + * @return Bottom right GPS coordinate. + */ + GPSCoordinate getMapBottomRight(); +} diff --git a/racevisionGame/src/main/java/visualiser/dataInput/RegattaXMLReader.java b/racevisionGame/src/main/java/shared/dataInput/RegattaXMLReader.java similarity index 75% rename from racevisionGame/src/main/java/visualiser/dataInput/RegattaXMLReader.java rename to racevisionGame/src/main/java/shared/dataInput/RegattaXMLReader.java index a1c3670f..812ba79e 100644 --- a/racevisionGame/src/main/java/visualiser/dataInput/RegattaXMLReader.java +++ b/racevisionGame/src/main/java/shared/dataInput/RegattaXMLReader.java @@ -1,68 +1,91 @@ -package visualiser.dataInput; +package shared.dataInput; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; -import seng302.GPSCoordinate; +import shared.dataInput.XMLReader; +import shared.exceptions.XMLReaderException; +import shared.model.GPSCoordinate; import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; import java.io.InputStream; /** - * Created by jjg64 on 19/04/17. + * XML reader class for regatta xml file. */ public class RegattaXMLReader extends XMLReader { + /** + * The regatta ID. + */ private int regattaID; + + /** + * The regatta name. + */ private String regattaName; + + /** + * The race ID. + */ private int raceID = 0; + + /** + * The course name. + */ private String courseName; + + /** + * The central latitude of the course. + */ private double centralLatitude; + + /** + * The central longitude of the course. + */ private double centralLongitude; + + /** + * The central altitude of the course. + */ private double centralAltitude; + + /** + * The UTC offset of the course. + */ private float utcOffset; - private float magneticVariation; /** - * Constructor for Regatta XML - * - * @param filePath path of the file - * @throws IOException error - * @throws SAXException error - * @throws ParserConfigurationException error + * The magnetic variation of the course. */ - public RegattaXMLReader(String filePath) throws IOException, SAXException, ParserConfigurationException { - this(filePath, true); - } + private float magneticVariation; + + /** * Constructor for Regatta 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 + * @param filePath path of the file to read. Read as a resource. + * @throws XMLReaderException Thrown if the file cannot be parsed. */ - private RegattaXMLReader(String filePath, boolean read) throws IOException, SAXException, ParserConfigurationException { + public RegattaXMLReader(String filePath) throws XMLReaderException { super(filePath); - if (read) { - read(); - } + read(); } + + /** * Alternate Constructor that takes in an inputstream instead * @param xmlString Input stream of the XML - * @throws IOException Error with input - * @throws SAXException Error with XML Format - * @throws ParserConfigurationException Error with XMl contents + * @throws XMLReaderException Thrown if the input stream cannot be parsed. */ - public RegattaXMLReader(InputStream xmlString) throws IOException, SAXException, ParserConfigurationException { + public RegattaXMLReader(InputStream xmlString) throws XMLReaderException { super(xmlString); read(); } + /** * Read the XML */ @@ -77,14 +100,19 @@ public class RegattaXMLReader extends XMLReader { * @param attributes attributes to extract information form. */ private void makeRegatta(Element attributes) { + this.regattaID = Integer.parseInt(getTextValueOfNode(attributes, "RegattaID")); this.regattaName = getTextValueOfNode(attributes, "RegattaName"); this.courseName = getTextValueOfNode(attributes, "CourseName"); + this.centralLatitude = Double.parseDouble(getTextValueOfNode(attributes, "CentralLatitude")); this.centralLongitude = Double.parseDouble(getTextValueOfNode(attributes, "CentralLongitude")); this.centralAltitude = Double.parseDouble(getTextValueOfNode(attributes, "CentralAltitude")); + this.utcOffset = Float.parseFloat(getTextValueOfNode(attributes, "UtcOffset")); + this.magneticVariation = Float.parseFloat(getTextValueOfNode(attributes, "MagneticVariation")); + } public int getRegattaID() { diff --git a/racevisionGame/src/main/java/mock/dataInput/XMLReader.java b/racevisionGame/src/main/java/shared/dataInput/XMLReader.java similarity index 58% rename from racevisionGame/src/main/java/mock/dataInput/XMLReader.java rename to racevisionGame/src/main/java/shared/dataInput/XMLReader.java index 3ac56f54..189c8626 100644 --- a/racevisionGame/src/main/java/mock/dataInput/XMLReader.java +++ b/racevisionGame/src/main/java/shared/dataInput/XMLReader.java @@ -1,10 +1,10 @@ -package mock.dataInput; +package shared.dataInput; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; -import org.xml.sax.InputSource; import org.xml.sax.SAXException; +import shared.exceptions.XMLReaderException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -16,7 +16,7 @@ import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import java.io.IOException; -import java.io.StringReader; +import java.io.InputStream; import java.io.StringWriter; /** @@ -27,38 +27,61 @@ public abstract class XMLReader { protected Document doc; /** - * Read in XML file - * @param filePath filepath for XML file - * @throws ParserConfigurationException If a document builder cannot be created. - * @throws IOException If any IO errors occur while parsing the XML file. - * @throws SAXException If any parse error occurs while parsing. + * Read an XML file by name as a resource. + * @param filePath filepath for XML file. Loaded as a resource. + * @throws XMLReaderException Thrown if the file cannot be parsed. */ - public XMLReader(String filePath) throws ParserConfigurationException, IOException, SAXException { + public XMLReader(String filePath) throws XMLReaderException { - InputSource fXmlFile; - if (filePath.contains("<")) { - fXmlFile = new InputSource(); - fXmlFile.setCharacterStream(new StringReader(filePath)); + //Read file as resource. + InputStream xmlInputStream = getClass().getClassLoader().getResourceAsStream(filePath); - } else { - fXmlFile = new InputSource(getClass().getClassLoader().getResourceAsStream(filePath)); - } + this.doc = parseInputStream(xmlInputStream); - DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); - doc = dBuilder.parse(fXmlFile); - doc.getDocumentElement().normalize(); } /** - * Alternate constructor - * @param xmlFile File to be read - * @param isWholeFile boolean value whether entire file is being passed + * Reads an XML file from an input stream. + * @param xmlInputStream The input stream to parse. + * @throws XMLReaderException Thrown if the input stream cannot be parsed. */ - public XMLReader(String xmlFile, Boolean isWholeFile) { + public XMLReader(InputStream xmlInputStream) throws XMLReaderException { + this.doc = parseInputStream(xmlInputStream); } + + /** + * Parses an input stream into a document. + * @param inputStream The xml input stream to parse. + * @return The parsed document. + * @throws XMLReaderException Thrown when a document builder cannot be constructed, or the stream cannot be parsed. + */ + private Document parseInputStream(InputStream inputStream) throws XMLReaderException { + + //Create document builder. + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + + DocumentBuilder dBuilder = null; + try { + dBuilder = dbFactory.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new XMLReaderException("Could not create a DocumentBuilder.", e); + } + + //Parse document. + Document document = null; + try { + document = dBuilder.parse(inputStream); + } catch (SAXException | IOException e) { + throw new XMLReaderException("Could not parse the xml input stream.", e); + } + document.getDocumentElement().normalize(); + + return document; + } + + /** * Return Document data of the read-in XML * @return XML document diff --git a/racevisionGame/src/main/java/shared/exceptions/XMLReaderException.java b/racevisionGame/src/main/java/shared/exceptions/XMLReaderException.java new file mode 100644 index 00000000..7948be58 --- /dev/null +++ b/racevisionGame/src/main/java/shared/exceptions/XMLReaderException.java @@ -0,0 +1,15 @@ +package shared.exceptions; + +/** + * An exception thrown when an XMLReader cannot be constructed for some reason. + */ +public class XMLReaderException extends Exception { + + public XMLReaderException(String message) { + super(message); + } + + public XMLReaderException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/racevisionGame/src/main/java/visualiser/dataInput/BoatXMLReader.java b/racevisionGame/src/main/java/visualiser/dataInput/BoatXMLReader.java deleted file mode 100644 index c0e562f4..00000000 --- a/racevisionGame/src/main/java/visualiser/dataInput/BoatXMLReader.java +++ /dev/null @@ -1,156 +0,0 @@ -package visualiser.dataInput; - -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.xml.sax.SAXException; -import seng302.Model.Boat; - -import javax.xml.parsers.ParserConfigurationException; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * XML Reader that reads in a file and initializes - * {@link seng302.Mock.StreamedBoat StreamedBoat}s that will be participating - * in a race. - */ -public class BoatXMLReader extends XMLReader { - private final Map streamedBoatMap = new HashMap<>(); - private Map participants = new HashMap<>(); - - /** - * Constructor for Boat XML Reader - * @param filePath path of the file - * @throws IOException error - * @throws SAXException error - * @throws ParserConfigurationException error - */ - public BoatXMLReader(String filePath) throws IOException, SAXException, ParserConfigurationException { - this(filePath, true); - } - - /** - * Constructor for Boat XML Reader - * @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 - */ - public BoatXMLReader(String filePath, boolean read) throws IOException, SAXException, ParserConfigurationException { - super(filePath); - if (read) { - read(); - } - } - - /** - * Constructor for Boat XML Reader - * @param xmlString sting to read - * @throws IOException error - * @throws SAXException error - * @throws ParserConfigurationException error - */ - public BoatXMLReader(InputStream xmlString) throws IOException, SAXException, ParserConfigurationException { - super(xmlString); - read(); - } - - public void read() { - readSettings(); - readShapes(); - readBoats(); - } - - /** - * Reads boats settings. - * INFORMATION FROM HERE IS IGNORED FOR NOW - */ - private void readSettings() { - - } - - /** - * Reads different kinds of boat. - * INFORMATION FROM HERE IS IGNORED FOR NOW - */ - private void readShapes() { - - } - - /** - * Reads the boats in the race - */ - private void readBoats() { - Element nBoats = (Element) doc.getElementsByTagName("Boats").item(0); - for (int i = 0; i < nBoats.getChildNodes().getLength(); i++) { - Node boat = nBoats.getChildNodes().item(i); - if (boat.getNodeName().equals("Boat") && boat.getAttributes().getNamedItem("Type").getTextContent().equals("Yacht")) { - readSingleBoat(boat); - } - } - } - - /** - * Reads the information about one boat - * Ignored values: ShapeID, StoweName, HullNum, Skipper, Type - * @param boat The node to read boat data from. - */ - private void readSingleBoat(Node boat) { - StreamedBoat streamedBoat; - String country = null; - int sourceID = Integer.parseInt(boat.getAttributes().getNamedItem("SourceID").getTextContent()); - String boatName = boat.getAttributes().getNamedItem("BoatName").getTextContent(); - String shortName = boat.getAttributes().getNamedItem("ShortName").getTextContent(); - if (exists(boat, "Country")) country = boat.getAttributes().getNamedItem("Country").getTextContent(); - - // Ignore all non participating boats - if (participants.containsKey(sourceID)) { - - if (!streamedBoatMap.containsKey(sourceID)) { - if (country != null) { - streamedBoat = new StreamedBoat(sourceID, boatName, country); - } else { - streamedBoat = new StreamedBoat(sourceID, boatName, shortName); - } - streamedBoatMap.put(sourceID, streamedBoat); - // Override boat with new boat - participants.put(sourceID, streamedBoat); - } - - for (int i = 0; i < boat.getChildNodes().getLength(); i++) { - Node GPSposition = boat.getChildNodes().item(i); - if (GPSposition.getNodeName().equals("GPSposition")) - readBoatPositionInformation(sourceID, GPSposition); - } - } - } - - - /** - * Reads the positional information about a boat - * Ignored values: FlagPosition, MastTop, Z value of GPSPosition - * @param sourceID The source ID of the boat. - * @param GPSposition The relative GPS position of the boat. - */ - private void readBoatPositionInformation(int sourceID, Node GPSposition) { - // TODO Get relative point before implementing. (GPSposition is based - // off a relative point). - } - - /** - * Sets the participants - * @param participants boats participating the race mapped by their source ID's - */ - public void setParticipants(Map participants) { - this.participants = participants; - } - - public List getBoats() { - return new ArrayList<>(streamedBoatMap.values()); - } -} diff --git a/racevisionGame/src/main/java/visualiser/dataInput/RaceDataSource.java b/racevisionGame/src/main/java/visualiser/dataInput/RaceDataSource.java deleted file mode 100644 index fb6ebcba..00000000 --- a/racevisionGame/src/main/java/visualiser/dataInput/RaceDataSource.java +++ /dev/null @@ -1,26 +0,0 @@ -package visualiser.dataInput; - -import seng302.Model.Boat; -import seng302.Model.Leg; -import seng302.Model.Marker; - -import java.time.ZonedDateTime; -import java.util.List; - -/** - * An object that holds relevant data for a race.
- * Information includes: {@link seng302.Model.Boat Boat}s, - * {@link seng302.Model.Leg Leg}s, {@link seng302.Model.Marker Marker}s and - * the {@link seng302.GPSCoordinate GPSCoordinate}s to create a - * {@link seng302.Model.ResizableRaceMap ResizableRaceMap}. - */ -public interface RaceDataSource { - List getBoats(); - List getLegs(); - List getMarkers(); - List getBoundary(); - - ZonedDateTime getZonedDateTime(); - GPSCoordinate getMapTopLeft(); - GPSCoordinate getMapBottomRight(); -} diff --git a/racevisionGame/src/main/java/visualiser/dataInput/XMLReader.java b/racevisionGame/src/main/java/visualiser/dataInput/XMLReader.java deleted file mode 100644 index 146433cd..00000000 --- a/racevisionGame/src/main/java/visualiser/dataInput/XMLReader.java +++ /dev/null @@ -1,52 +0,0 @@ -package visualiser.dataInput; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.xml.sax.SAXException; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import java.io.IOException; -import java.io.InputStream; - -/** - * The abstract class for reading in XML race data. - */ -public abstract class XMLReader { - - protected Document doc; - - protected XMLReader(String filePath) throws ParserConfigurationException, IOException, SAXException { - InputStream fXmlFile = getClass().getClassLoader().getResourceAsStream(filePath); - DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); - doc = dBuilder.parse(fXmlFile); - doc.getDocumentElement().normalize(); - } - - protected XMLReader(InputStream xmlInput) throws ParserConfigurationException, IOException, SAXException { - - DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); - doc = dBuilder.parse(xmlInput); - } - - public Document getDocument() { - return doc; - } - - protected String getTextValueOfNode(Element n, String tagName) { - return n.getElementsByTagName(tagName).item(0).getTextContent(); - } - - protected boolean exists(Node node, String attribute) { - return node.getAttributes().getNamedItem(attribute) != null; - } - - public String getAttribute(Element n, String attr) { - return n.getAttribute(attr); - } - -} From 8d36d89570b50e01b95dedbcd439f36c885298b4 Mon Sep 17 00:00:00 2001 From: fjc40 Date: Thu, 6 Jul 2017 01:30:55 +1200 Subject: [PATCH 04/25] 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. --- .../src/main/java/mock/app/Event.java | 16 +- .../java/mock/dataInput/RaceXMLReader.java | 289 ----------- .../StreamedCourseXMLException.java | 7 - .../src/main/java/mock/model/MockBoat.java | 14 +- .../mock/model/{Race.java => MockRace.java} | 329 ++++++------- .../java/network/Messages/BoatStatus.java | 5 +- .../network/Messages/Enums/RaceTypeEnum.java | 29 +- .../java/network/Messages/RaceStatus.java | 24 +- .../java/shared/dataInput/BoatXMLReader.java | 36 +- .../java/shared/dataInput/RaceDataSource.java | 25 +- .../java/shared/dataInput/RaceXMLReader.java | 465 ++++++++++++++++++ .../shared/dataInput/RegattaDataSource.java | 70 +++ .../shared/dataInput/RegattaXMLReader.java | 37 +- .../exceptions/InvalidBoatDataException.java | 11 +- .../exceptions/InvalidRaceDataException.java | 10 +- .../InvalidRegattaDataException.java | 15 + .../src/main/java/shared/model/Angle.java | 19 + .../src/main/java/shared/model/Azimuth.java | 41 ++ .../src/main/java/shared/model/Bearing.java | 43 +- .../src/main/java/shared/model/Boat.java | 25 +- .../main/java/shared/model/CompoundMark.java | 34 +- .../src/main/java/shared/model/Mark.java | 8 +- .../src/main/java/shared/model/Race.java | 184 +++++++ .../Controllers/RaceController.java | 27 +- .../dataInput/StreamedCourseXMLReader.java | 290 ----------- .../StreamedCourseXMLException.java | 7 - .../main/java/visualiser/model/Sparkline.java | 2 +- .../java/visualiser/model/StreamedCourse.java | 106 ---- .../java/visualiser/model/StreamedRace.java | 285 ----------- .../java/visualiser/model/VisualiserBoat.java | 40 +- .../java/visualiser/model/VisualiserRace.java | 398 +++++++++++++++ .../src/main/resources/mockXML/boatTest.xml | 55 +++ .../src/main/resources/mockXML/raceTest.xml | 57 +++ .../main/resources/mockXML/regattaTest.xml | 10 + .../src/main/resources/polars/acc_polars.csv | 8 + 35 files changed, 1803 insertions(+), 1218 deletions(-) delete mode 100644 racevisionGame/src/main/java/mock/dataInput/RaceXMLReader.java delete mode 100644 racevisionGame/src/main/java/mock/exceptions/StreamedCourseXMLException.java rename racevisionGame/src/main/java/mock/model/{Race.java => MockRace.java} (77%) create mode 100644 racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java create mode 100644 racevisionGame/src/main/java/shared/dataInput/RegattaDataSource.java rename racevisionGame/src/main/java/{mock => shared}/exceptions/InvalidBoatDataException.java (55%) rename racevisionGame/src/main/java/{mock => shared}/exceptions/InvalidRaceDataException.java (55%) create mode 100644 racevisionGame/src/main/java/shared/exceptions/InvalidRegattaDataException.java create mode 100644 racevisionGame/src/main/java/shared/model/Race.java delete mode 100644 racevisionGame/src/main/java/visualiser/dataInput/StreamedCourseXMLReader.java delete mode 100644 racevisionGame/src/main/java/visualiser/exceptions/StreamedCourseXMLException.java delete mode 100644 racevisionGame/src/main/java/visualiser/model/StreamedCourse.java delete mode 100644 racevisionGame/src/main/java/visualiser/model/StreamedRace.java create mode 100644 racevisionGame/src/main/java/visualiser/model/VisualiserRace.java create mode 100644 racevisionGame/src/main/resources/mockXML/boatTest.xml create mode 100644 racevisionGame/src/main/resources/mockXML/raceTest.xml create mode 100644 racevisionGame/src/main/resources/mockXML/regattaTest.xml create mode 100644 racevisionGame/src/main/resources/polars/acc_polars.csv diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index 213966a6..a49a0d66 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -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; + } } diff --git a/racevisionGame/src/main/java/mock/dataInput/RaceXMLReader.java b/racevisionGame/src/main/java/mock/dataInput/RaceXMLReader.java deleted file mode 100644 index b5e410b6..00000000 --- a/racevisionGame/src/main/java/mock/dataInput/RaceXMLReader.java +++ /dev/null @@ -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 boundary = new ArrayList<>(); - private final Map compoundMarkMap = new HashMap<>(); - private final Map participants = new HashMap<>(); - private final List legs = new ArrayList<>(); - private final List compoundMarks = new ArrayList<>(); - private ZonedDateTime creationTimeDate; - private ZonedDateTime raceStartTime; - private int raceID; - private String raceType; - private boolean postpone; - - private Map boats; - private Map 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 getBoundary() { - return boundary; - } - - public GPSCoordinate getMapTopLeft() { - return mapTopLeft; - } - - public GPSCoordinate getMapBottomRight() { - return mapBottomRight; - } - - public List getLegs() { - return legs; - } - - public List 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 getBoats() { - return new ArrayList<>(participants.values()); - } -} diff --git a/racevisionGame/src/main/java/mock/exceptions/StreamedCourseXMLException.java b/racevisionGame/src/main/java/mock/exceptions/StreamedCourseXMLException.java deleted file mode 100644 index e173184e..00000000 --- a/racevisionGame/src/main/java/mock/exceptions/StreamedCourseXMLException.java +++ /dev/null @@ -1,7 +0,0 @@ -package mock.exceptions; - -/** - * Created by cbt24 on 25/04/17. - */ -public class StreamedCourseXMLException extends Throwable { -} diff --git a/racevisionGame/src/main/java/mock/model/MockBoat.java b/racevisionGame/src/main/java/mock/model/MockBoat.java index 6afdae35..c8c6825b 100644 --- a/racevisionGame/src/main/java/mock/model/MockBoat.java +++ b/racevisionGame/src/main/java/mock/model/MockBoat.java @@ -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; + } + + /** diff --git a/racevisionGame/src/main/java/mock/model/Race.java b/racevisionGame/src/main/java/mock/model/MockRace.java similarity index 77% rename from racevisionGame/src/main/java/mock/model/Race.java rename to racevisionGame/src/main/java/mock/model/MockRace.java index 3e90408a..d5ae1b4c 100644 --- a/racevisionGame/src/main/java/mock/model/Race.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -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 boats; + private List boats; - /** - * An observable list of compound marks in the race. - */ - private ObservableList compoundMarks; - - /** - * A list of legs in the race. - */ - private List legs; - /** - * A list of coordinates describing the boundary of the course. - */ - private List boundary; /** * A copy of the boundary list, except "shrunk" inwards by 50m. */ private List 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 generateMockBoats(Map boats, List sourceIDs, Polars polars) { + + List 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 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,9 +423,10 @@ 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); - if (iters > 500){ + + 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 startingPositions = getSpreadStartingPositions(); //Get iterators for our boat and position lists. - Iterator boatIt = this.boats.iterator(); + Iterator boatIt = this.boats.iterator(); Iterator 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 getBoats() { + public List 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(){ - int r = new Random().nextInt(changeWind)+1; - if(r==1){ - windDirDegrees = (0.5 + windDirDegrees) % 360; - } 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; + + /** + * 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) { + //Add 0.5 degrees to the wind bearing. + this.windDirection.setDegrees(this.windDirection.degrees() + 0.5); + + } else if (r == 2) { + //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); - } + //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 void setChangeWind(int changeVal){ - if (changeVal>=0){ - changeWind = changeVal; } } - 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); + long timeFromNow = (long) (1000 * boat.calculateDistanceToNextMarker() / velocityToMark); boat.setEstimatedTime(startTime + totalTimeElapsed + timeFromNow); } + } } diff --git a/racevisionGame/src/main/java/network/Messages/BoatStatus.java b/racevisionGame/src/main/java/network/Messages/BoatStatus.java index 8310f49e..54996726 100644 --- a/racevisionGame/src/main/java/network/Messages/BoatStatus.java +++ b/racevisionGame/src/main/java/network/Messages/BoatStatus.java @@ -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. diff --git a/racevisionGame/src/main/java/network/Messages/Enums/RaceTypeEnum.java b/racevisionGame/src/main/java/network/Messages/Enums/RaceTypeEnum.java index 15c47e01..6a03b37e 100644 --- a/racevisionGame/src/main/java/network/Messages/Enums/RaceTypeEnum.java +++ b/racevisionGame/src/main/java/network/Messages/Enums/RaceTypeEnum.java @@ -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; diff --git a/racevisionGame/src/main/java/network/Messages/RaceStatus.java b/racevisionGame/src/main/java/network/Messages/RaceStatus.java index 03b1e5de..1a3ce7eb 100644 --- a/racevisionGame/src/main/java/network/Messages/RaceStatus.java +++ b/racevisionGame/src/main/java/network/Messages/RaceStatus.java @@ -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); } } diff --git a/racevisionGame/src/main/java/shared/dataInput/BoatXMLReader.java b/racevisionGame/src/main/java/shared/dataInput/BoatXMLReader.java index 5e155090..2320b01a 100644 --- a/racevisionGame/src/main/java/shared/dataInput/BoatXMLReader.java +++ b/racevisionGame/src/main/java/shared/dataInput/BoatXMLReader.java @@ -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,16 +29,44 @@ 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); - read(); + + //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 */ diff --git a/racevisionGame/src/main/java/shared/dataInput/RaceDataSource.java b/racevisionGame/src/main/java/shared/dataInput/RaceDataSource.java index d9238bcc..668714e9 100644 --- a/racevisionGame/src/main/java/shared/dataInput/RaceDataSource.java +++ b/racevisionGame/src/main/java/shared/dataInput/RaceDataSource.java @@ -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 getBoats(); + List 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 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. diff --git a/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java b/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java new file mode 100644 index 00000000..a5179a1b --- /dev/null +++ b/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java @@ -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 boundary = new ArrayList<>(); + + /** + * A map between compoundMarkID and a CompoundMark for all CompoundMarks in a race. + */ + private final Map compoundMarkMap = new HashMap<>(); + + /** + * A list of boat sourceIDs participating in the race. + */ + private final List participants = new ArrayList<>(); + + /** + * A list of legs in the race. + */ + private final List 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 boats; + private Map 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 ".." 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 "..." 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 "..." 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 "..." 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 getBoundary() { + return boundary; + } + + public GPSCoordinate getMapTopLeft() { + return mapTopLeft; + } + + public GPSCoordinate getMapBottomRight() { + return mapBottomRight; + } + + public List getLegs() { + return legs; + } + + public List 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 getParticipants() { + return participants; + } +} diff --git a/racevisionGame/src/main/java/shared/dataInput/RegattaDataSource.java b/racevisionGame/src/main/java/shared/dataInput/RegattaDataSource.java new file mode 100644 index 00000000..7fa9aa43 --- /dev/null +++ b/racevisionGame/src/main/java/shared/dataInput/RegattaDataSource.java @@ -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(); + + + +} diff --git a/racevisionGame/src/main/java/shared/dataInput/RegattaXMLReader.java b/racevisionGame/src/main/java/shared/dataInput/RegattaXMLReader.java index 812ba79e..8fd3f2be 100644 --- a/racevisionGame/src/main/java/shared/dataInput/RegattaXMLReader.java +++ b/racevisionGame/src/main/java/shared/dataInput/RegattaXMLReader.java @@ -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); - read(); + + //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); - read(); + + //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); } diff --git a/racevisionGame/src/main/java/mock/exceptions/InvalidBoatDataException.java b/racevisionGame/src/main/java/shared/exceptions/InvalidBoatDataException.java similarity index 55% rename from racevisionGame/src/main/java/mock/exceptions/InvalidBoatDataException.java rename to racevisionGame/src/main/java/shared/exceptions/InvalidBoatDataException.java index 1b36c34e..6f2c7a64 100644 --- a/racevisionGame/src/main/java/mock/exceptions/InvalidBoatDataException.java +++ b/racevisionGame/src/main/java/shared/exceptions/InvalidBoatDataException.java @@ -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); + } } diff --git a/racevisionGame/src/main/java/mock/exceptions/InvalidRaceDataException.java b/racevisionGame/src/main/java/shared/exceptions/InvalidRaceDataException.java similarity index 55% rename from racevisionGame/src/main/java/mock/exceptions/InvalidRaceDataException.java rename to racevisionGame/src/main/java/shared/exceptions/InvalidRaceDataException.java index 8b85cd1d..83f6a295 100644 --- a/racevisionGame/src/main/java/mock/exceptions/InvalidRaceDataException.java +++ b/racevisionGame/src/main/java/shared/exceptions/InvalidRaceDataException.java @@ -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); + } } diff --git a/racevisionGame/src/main/java/shared/exceptions/InvalidRegattaDataException.java b/racevisionGame/src/main/java/shared/exceptions/InvalidRegattaDataException.java new file mode 100644 index 00000000..007e534b --- /dev/null +++ b/racevisionGame/src/main/java/shared/exceptions/InvalidRegattaDataException.java @@ -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); + } +} diff --git a/racevisionGame/src/main/java/shared/model/Angle.java b/racevisionGame/src/main/java/shared/model/Angle.java index 769257c4..d96ff50e 100644 --- a/racevisionGame/src/main/java/shared/model/Angle.java +++ b/racevisionGame/src/main/java/shared/model/Angle.java @@ -127,4 +127,23 @@ public class Angle implements Comparable { 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)); + } + } diff --git a/racevisionGame/src/main/java/shared/model/Azimuth.java b/racevisionGame/src/main/java/shared/model/Azimuth.java index 540d46e1..0f6202f6 100644 --- a/racevisionGame/src/main/java/shared/model/Azimuth.java +++ b/racevisionGame/src/main/java/shared/model/Azimuth.java @@ -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()); + } + + + } diff --git a/racevisionGame/src/main/java/shared/model/Bearing.java b/racevisionGame/src/main/java/shared/model/Bearing.java index 83f0eab1..950c4094 100644 --- a/racevisionGame/src/main/java/shared/model/Bearing.java +++ b/racevisionGame/src/main/java/shared/model/Bearing.java @@ -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()); + } + + + } diff --git a/racevisionGame/src/main/java/shared/model/Boat.java b/racevisionGame/src/main/java/shared/model/Boat.java index 1d5d822b..2f09815a 100644 --- a/racevisionGame/src/main/java/shared/model/Boat.java +++ b/racevisionGame/src/main/java/shared/model/Boat.java @@ -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. diff --git a/racevisionGame/src/main/java/shared/model/CompoundMark.java b/racevisionGame/src/main/java/shared/model/CompoundMark.java index 17808ee3..7070270f 100644 --- a/racevisionGame/src/main/java/shared/model/CompoundMark.java +++ b/racevisionGame/src/main/java/shared/model/CompoundMark.java @@ -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. diff --git a/racevisionGame/src/main/java/shared/model/Mark.java b/racevisionGame/src/main/java/shared/model/Mark.java index da9308de..5781861a 100644 --- a/racevisionGame/src/main/java/shared/model/Mark.java +++ b/racevisionGame/src/main/java/shared/model/Mark.java @@ -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; + } } diff --git a/racevisionGame/src/main/java/shared/model/Race.java b/racevisionGame/src/main/java/shared/model/Race.java new file mode 100644 index 00000000..1b82425e --- /dev/null +++ b/racevisionGame/src/main/java/shared/model/Race.java @@ -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 compoundMarks; + + /** + * A list of legs in the race. + */ + protected List legs; + + /** + * A list of coordinates describing the boundary of the course. + */ + protected List 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; + } + + + +} diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java index b2546ab4..60183c38 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java @@ -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 boats){ + public void createSparkLine(ObservableList 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 boatsInRace){ + public void updateSparkline(ObservableList 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 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(); diff --git a/racevisionGame/src/main/java/visualiser/dataInput/StreamedCourseXMLReader.java b/racevisionGame/src/main/java/visualiser/dataInput/StreamedCourseXMLReader.java deleted file mode 100644 index 91a01aaa..00000000 --- a/racevisionGame/src/main/java/visualiser/dataInput/StreamedCourseXMLReader.java +++ /dev/null @@ -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 boundary = new ArrayList<>(); - private final Map compoundMarks = new HashMap<>(); - private final Map participants = new HashMap<>(); - private final List legs = new ArrayList<>(); - private final List 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 getBoundary() { - return boundary; - } - - public GPSCoordinate getMapTopLeft() { - return mapTopLeft; - } - - public GPSCoordinate getMapBottomRight() { - return mapBottomRight; - } - - public List getLegs() { - return legs; - } - - public List 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 getParticipants() { - return participants; - } -} diff --git a/racevisionGame/src/main/java/visualiser/exceptions/StreamedCourseXMLException.java b/racevisionGame/src/main/java/visualiser/exceptions/StreamedCourseXMLException.java deleted file mode 100644 index a9b1055d..00000000 --- a/racevisionGame/src/main/java/visualiser/exceptions/StreamedCourseXMLException.java +++ /dev/null @@ -1,7 +0,0 @@ -package visualiser.exceptions; - -/** - * Created by cbt24 on 25/04/17. - */ -public class StreamedCourseXMLException extends Throwable { -} diff --git a/racevisionGame/src/main/java/visualiser/model/Sparkline.java b/racevisionGame/src/main/java/visualiser/model/Sparkline.java index b494eb9a..a7a74043 100644 --- a/racevisionGame/src/main/java/visualiser/model/Sparkline.java +++ b/racevisionGame/src/main/java/visualiser/model/Sparkline.java @@ -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 boatsInRace){ + public void updateSparkline(ObservableList boatsInRace){ int placingVal = boatsInRace.size(); sparkLineNumber++; diff --git a/racevisionGame/src/main/java/visualiser/model/StreamedCourse.java b/racevisionGame/src/main/java/visualiser/model/StreamedCourse.java deleted file mode 100644 index 6c2f1450..00000000 --- a/racevisionGame/src/main/java/visualiser/model/StreamedCourse.java +++ /dev/null @@ -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 getBoats() { - return boatXMLReader.getBoats(); - } - - public List getLegs() { - return streamedCourseXMLReader.getLegs(); - } - - public List getMarkers() { return streamedCourseXMLReader.getMarkers(); } - - public List getBoundary() { - return streamedCourseXMLReader.getBoundary(); - } - - public ZonedDateTime getZonedDateTime() { - return streamedCourseXMLReader.getRaceStartTime(); - } - - public GPSCoordinate getMapTopLeft() { - return streamedCourseXMLReader.getMapTopLeft(); - } - - public GPSCoordinate getMapBottomRight() { - return streamedCourseXMLReader.getMapBottomRight(); - } -} diff --git a/racevisionGame/src/main/java/visualiser/model/StreamedRace.java b/racevisionGame/src/main/java/visualiser/model/StreamedRace.java deleted file mode 100644 index a0232f3b..00000000 --- a/racevisionGame/src/main/java/visualiser/model/StreamedRace.java +++ /dev/null @@ -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 startingBoats; - private final ObservableList boatMarkers; - private final List 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 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; - - } - -} diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java index 01e86b42..a2117422 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java @@ -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 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 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 diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java new file mode 100644 index 00000000..89b31180 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java @@ -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 boats; + + /** + * An observable list of marker boats in the race. + */ + private final ObservableList 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 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 generateVisualiserBoats(Map boats, List sourceIDs, List colors) { + + List 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 boats, Map boatLocationMap, Map 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 boatMarkers, Map boatLocationMap, Map 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 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 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 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; + + } + +} diff --git a/racevisionGame/src/main/resources/mockXML/boatTest.xml b/racevisionGame/src/main/resources/mockXML/boatTest.xml new file mode 100644 index 00000000..84f911bc --- /dev/null +++ b/racevisionGame/src/main/resources/mockXML/boatTest.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/racevisionGame/src/main/resources/mockXML/raceTest.xml b/racevisionGame/src/main/resources/mockXML/raceTest.xml new file mode 100644 index 00000000..63fac32d --- /dev/null +++ b/racevisionGame/src/main/resources/mockXML/raceTest.xml @@ -0,0 +1,57 @@ + + + 5326 + FLEET + CREATION_TIME + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/racevisionGame/src/main/resources/mockXML/regattaTest.xml b/racevisionGame/src/main/resources/mockXML/regattaTest.xml new file mode 100644 index 00000000..86526130 --- /dev/null +++ b/racevisionGame/src/main/resources/mockXML/regattaTest.xml @@ -0,0 +1,10 @@ + + 3 + New Zealand Test + North Head + -36.82791529 + 174.81218919 + 0.00 + 12 + 14.1 + \ No newline at end of file diff --git a/racevisionGame/src/main/resources/polars/acc_polars.csv b/racevisionGame/src/main/resources/polars/acc_polars.csv new file mode 100644 index 00000000..ee7ea80e --- /dev/null +++ b/racevisionGame/src/main/resources/polars/acc_polars.csv @@ -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 From 3a0b81834fe5aec16024b8520ba50f0de439dd54 Mon Sep 17 00:00:00 2001 From: fjc40 Date: Thu, 6 Jul 2017 15:25:00 +1200 Subject: [PATCH 05/25] Refactored mock.Event to work with MockRace. Fixed the networking imports. Race now keeps a reference to its Race, Boat, and Regatta data sources. VisualiserRace can be provided with new Race/Boat/Regatta data sources during runtime. Currently it updates boundary and legs with a new RaceDataSource, and marker boats with a new BoatDataSource. --- .../src/main/java/mock/app/App.java | 3 +- .../src/main/java/mock/app/Event.java | 31 ++++++++++++---- .../java/network/BinaryMessageDecoder.java | 18 +++++---- .../java/network/BinaryMessageEncoder.java | 5 ++- .../MessageDecoders/AverageWindDecoder.java | 5 ++- .../MessageDecoders/BoatLocationDecoder.java | 8 +++- .../MessageDecoders/CourseWindDecoder.java | 7 +++- .../MessageDecoders/MarkRoundingDecoder.java | 5 ++- .../RaceStartStatusDecoder.java | 3 +- .../MessageDecoders/RaceStatusDecoder.java | 14 ++++--- .../MessageDecoders/XMLMessageDecoder.java | 5 ++- .../RaceVisionByteEncoder.java | 6 ++- .../MessageEncoders/XMLMessageEncoder.java | 3 +- .../main/java/network/Messages/AC35Data.java | 2 +- .../java/network/Messages/AverageWind.java | 3 +- .../java/network/Messages/BoatLocation.java | 22 ++++------- .../java/network/Messages/CourseWind.java | 3 +- .../java/network/Messages/CourseWinds.java | 9 +++-- .../main/java/network/Messages/Heartbeat.java | 3 +- .../java/network/Messages/MarkRounding.java | 3 +- .../java/network/Messages/RaceMessage.java | 3 +- .../network/Messages/RaceStartStatus.java | 3 +- .../java/network/Messages/RaceStatus.java | 2 +- .../java/network/Messages/XMLMessage.java | 3 +- .../network/PacketDump/AC35DumpReader.java | 7 ++-- .../java/shared/dataInput/RaceXMLReader.java | 8 +--- .../src/main/java/shared/model/Boat.java | 2 +- .../src/main/java/shared/model/Race.java | 36 +++++++++++++++--- .../Controllers/RaceController.java | 3 +- .../java/visualiser/model/VisualiserRace.java | 37 +++++++++++++++++-- 30 files changed, 179 insertions(+), 83 deletions(-) diff --git a/racevisionGame/src/main/java/mock/app/App.java b/racevisionGame/src/main/java/mock/app/App.java index 97ecf8d5..72f2de0e 100644 --- a/racevisionGame/src/main/java/mock/app/App.java +++ b/racevisionGame/src/main/java/mock/app/App.java @@ -8,6 +8,7 @@ import mock.model.Polars; import org.w3c.dom.Document; import org.xml.sax.InputSource; import org.xml.sax.SAXException; +import shared.dataInput.XMLReader; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -51,7 +52,7 @@ public class App extends Application { * Reads the Initial Race XML files that are necessary to run the mock. * @param path path of the XML * @param encoding encoding of the xml - * @return + * @return A string containing the contents of the specified file. * @throws IOException No file etc * @throws ParserConfigurationException Issue with the XML formatting * @throws SAXException Issue with XML formatting diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index a49a0d66..2c67de2f 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -1,8 +1,14 @@ package mock.app; +import mock.model.MockRace; import mock.model.Polars; import network.Messages.Enums.MessageType; import org.xml.sax.SAXException; +import shared.dataInput.*; +import shared.exceptions.InvalidBoatDataException; +import shared.exceptions.InvalidRaceDataException; +import shared.exceptions.InvalidRegattaDataException; +import shared.exceptions.XMLReaderException; import shared.model.Constants; import javax.xml.parsers.ParserConfigurationException; @@ -17,11 +23,11 @@ import java.time.format.DateTimeFormatter; */ public class Event { - String raceXML; - String regattaXML; - String boatXML; - Polars boatPolars; - MockOutput mockOutput; + private String raceXML; + private String regattaXML; + private String boatXML; + private Polars boatPolars; + private MockOutput mockOutput; public Event(String raceXML, String regattaXML, String boatXML, Polars boatPolars) { @@ -29,6 +35,7 @@ public class Event { this.boatXML = boatXML; this.regattaXML = regattaXML; this.boatPolars = boatPolars; + try { mockOutput = new MockOutput(); new Thread(mockOutput).start(); @@ -43,10 +50,18 @@ public class Event { public void start() { try { sendXMLs(); - Race newRace = new Race(new RaceXMLReader(this.raceXML, new BoatXMLReader(boatXML, this.boatPolars)), mockOutput); - new Thread((newRace)).start(); - } catch (ParserConfigurationException | IOException | SAXException | ParseException | StreamedCourseXMLException e) { + //Parse the XML files into data sources. + RaceDataSource raceDataSource = new RaceXMLReader(this.raceXML); + BoatDataSource boatDataSource = new BoatXMLReader(this.boatXML); + RegattaDataSource regattaDataSource = new RegattaXMLReader(this.regattaXML); + + //Create and start race. + MockRace newRace = new MockRace(boatDataSource, raceDataSource, regattaDataSource, this.boatPolars, this.mockOutput); + + new Thread(newRace).start(); + + } catch (XMLReaderException | InvalidBoatDataException | InvalidRaceDataException | InvalidRegattaDataException e) { e.printStackTrace(); } } diff --git a/racevisionGame/src/main/java/network/BinaryMessageDecoder.java b/racevisionGame/src/main/java/network/BinaryMessageDecoder.java index 183491df..1f8ba8e9 100644 --- a/racevisionGame/src/main/java/network/BinaryMessageDecoder.java +++ b/racevisionGame/src/main/java/network/BinaryMessageDecoder.java @@ -1,7 +1,11 @@ package network; -import seng302.Networking.Exceptions.InvalidMessageException; -import seng302.Networking.Messages.Enums.MessageType; + +import network.Exceptions.InvalidMessageException; +import network.MessageDecoders.*; +import network.Messages.*; +import network.Messages.Enums.MessageType; +import static network.Utils.ByteConverter.*; import java.nio.ByteBuffer; import java.util.Arrays; @@ -69,20 +73,20 @@ public class BinaryMessageDecoder { this.headerMessageType = this.messageHeader[2]; //Get the header timestamp. - this.headerTimeStamp = ByteConverter.bytesToLong(Arrays.copyOfRange(this.messageHeader, 3, 9)); + this.headerTimeStamp = bytesToLong(Arrays.copyOfRange(this.messageHeader, 3, 9)); //Get the source ID for the message. - this.headerSourceID = ByteConverter.bytesToInt(Arrays.copyOfRange(this.messageHeader, 9, 13)); + this.headerSourceID = bytesToInt(Arrays.copyOfRange(this.messageHeader, 9, 13)); //Get the length of the message body. - this.messageBodyLength = ByteConverter.bytesToInt(Arrays.copyOfRange(this.messageHeader, 13, 15)); + this.messageBodyLength = bytesToInt(Arrays.copyOfRange(this.messageHeader, 13, 15)); //Get the messageBody. this.messageBody = Arrays.copyOfRange(this.fullMessage, this.headerLength, this.headerLength + this.messageBodyLength); //Get the CRC value. - this.messageCRCValue = ByteConverter.bytesToLong(Arrays.copyOfRange(this.fullMessage, this.fullMessage.length - CRCLength, this.fullMessage.length)); + this.messageCRCValue = bytesToLong(Arrays.copyOfRange(this.fullMessage, this.fullMessage.length - CRCLength, this.fullMessage.length)); //Combine the header and body into a single array. ByteBuffer headerBodyByteBuffer = ByteBuffer.allocate(messageHeader.length + messageBody.length); @@ -131,7 +135,7 @@ public class BinaryMessageDecoder { //System.out.println("Decoding HeartBeat Message!"); //TODO maybe use HeartbeatDecoder.decode(message). //TODO also, decoders for each message type should encapsulate the constructing of the object. E.g., return HeartbeatDecoder.decode(message);. - return new Heartbeat(ByteConverter.bytesToLong(messageBody)); + return new Heartbeat(bytesToLong(messageBody)); case RACESTATUS: //System.out.println("Race Status Message"); diff --git a/racevisionGame/src/main/java/network/BinaryMessageEncoder.java b/racevisionGame/src/main/java/network/BinaryMessageEncoder.java index 0c008444..f7dc12a7 100644 --- a/racevisionGame/src/main/java/network/BinaryMessageEncoder.java +++ b/racevisionGame/src/main/java/network/BinaryMessageEncoder.java @@ -1,12 +1,13 @@ package network; -import seng302.Networking.Messages.Enums.MessageType; + +import network.Messages.Enums.MessageType; +import static network.Utils.ByteConverter.*; import java.nio.ByteBuffer; import java.util.zip.CRC32; -import static seng302.Networking.Utils.ByteConverter.*; /** * This class can be used to encode/convert a byte array message body, plus header data into a byte array containing the entire message, ready to send. diff --git a/racevisionGame/src/main/java/network/MessageDecoders/AverageWindDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/AverageWindDecoder.java index 22548063..13de3c80 100644 --- a/racevisionGame/src/main/java/network/MessageDecoders/AverageWindDecoder.java +++ b/racevisionGame/src/main/java/network/MessageDecoders/AverageWindDecoder.java @@ -1,7 +1,8 @@ package network.MessageDecoders; -import seng302.Networking.Messages.AverageWind; -import seng302.Networking.Utils.ByteConverter; + +import network.Messages.AverageWind; +import network.Utils.ByteConverter; import java.util.Arrays; diff --git a/racevisionGame/src/main/java/network/MessageDecoders/BoatLocationDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/BoatLocationDecoder.java index b7504baa..db90a343 100644 --- a/racevisionGame/src/main/java/network/MessageDecoders/BoatLocationDecoder.java +++ b/racevisionGame/src/main/java/network/MessageDecoders/BoatLocationDecoder.java @@ -1,10 +1,14 @@ package network.MessageDecoders; -import seng302.Networking.Messages.BoatLocation; + +import network.Messages.BoatLocation; import java.util.Arrays; -import static seng302.Networking.Utils.ByteConverter.*; +import static network.Utils.ByteConverter.bytesToInt; +import static network.Utils.ByteConverter.bytesToLong; +import static network.Utils.ByteConverter.bytesToShort; + /** * Created by hba56 on 21/04/17. diff --git a/racevisionGame/src/main/java/network/MessageDecoders/CourseWindDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/CourseWindDecoder.java index eebb8dc6..038a79d2 100644 --- a/racevisionGame/src/main/java/network/MessageDecoders/CourseWindDecoder.java +++ b/racevisionGame/src/main/java/network/MessageDecoders/CourseWindDecoder.java @@ -1,11 +1,14 @@ package network.MessageDecoders; -import seng302.Networking.Messages.CourseWind; + +import network.Messages.CourseWind; import java.util.ArrayList; import java.util.Arrays; -import static seng302.Networking.Utils.ByteConverter.*; +import static network.Utils.ByteConverter.bytesToInt; +import static network.Utils.ByteConverter.bytesToLong; + /** * Created by hba56 on 23/04/17. diff --git a/racevisionGame/src/main/java/network/MessageDecoders/MarkRoundingDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/MarkRoundingDecoder.java index 2dccdf77..000e86ee 100644 --- a/racevisionGame/src/main/java/network/MessageDecoders/MarkRoundingDecoder.java +++ b/racevisionGame/src/main/java/network/MessageDecoders/MarkRoundingDecoder.java @@ -1,7 +1,8 @@ package network.MessageDecoders; -import seng302.Networking.Messages.MarkRounding; -import seng302.Networking.Utils.ByteConverter; + +import network.Messages.MarkRounding; +import network.Utils.ByteConverter; import java.util.Arrays; diff --git a/racevisionGame/src/main/java/network/MessageDecoders/RaceStartStatusDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/RaceStartStatusDecoder.java index fc922503..236c5d27 100644 --- a/racevisionGame/src/main/java/network/MessageDecoders/RaceStartStatusDecoder.java +++ b/racevisionGame/src/main/java/network/MessageDecoders/RaceStartStatusDecoder.java @@ -3,7 +3,8 @@ package network.MessageDecoders; import java.util.Arrays; -import static seng302.Networking.Utils.ByteConverter.*; +import static network.Utils.ByteConverter.*; + /** * Created by hba56 on 21/04/17. diff --git a/racevisionGame/src/main/java/network/MessageDecoders/RaceStatusDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/RaceStatusDecoder.java index d0b05f95..e4d147df 100644 --- a/racevisionGame/src/main/java/network/MessageDecoders/RaceStatusDecoder.java +++ b/racevisionGame/src/main/java/network/MessageDecoders/RaceStatusDecoder.java @@ -1,11 +1,15 @@ package network.MessageDecoders; -import seng302.Networking.Messages.BoatStatus; + +import network.Messages.BoatStatus; import java.util.ArrayList; import java.util.Arrays; -import static seng302.Networking.Utils.ByteConverter.*; +import static network.Utils.ByteConverter.bytesToInt; +import static network.Utils.ByteConverter.bytesToLong; +import static network.Utils.ByteConverter.bytesToShort; + /** * Created by hba56 on 21/04/17. @@ -24,7 +28,7 @@ public class RaceStatusDecoder { private long time; private int race; - private int raceState; + private byte raceState; private long startTime; private int raceWindDir; private short raceWindSpeed; @@ -47,7 +51,7 @@ public class RaceStatusDecoder { time = bytesToLong(timeBytes); race = bytesToInt(raceID); - raceState = bytesToInt(raceStatus); + raceState = raceStatus; startTime = bytesToLong(expectedStart); raceWindDir = bytesToInt(raceWind); raceWindSpeed = bytesToShort(windSpeed); @@ -87,7 +91,7 @@ public class RaceStatusDecoder { return race; } - public int getRaceState() { + public byte getRaceState() { return raceState; } diff --git a/racevisionGame/src/main/java/network/MessageDecoders/XMLMessageDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/XMLMessageDecoder.java index 2894599a..5af12a78 100644 --- a/racevisionGame/src/main/java/network/MessageDecoders/XMLMessageDecoder.java +++ b/racevisionGame/src/main/java/network/MessageDecoders/XMLMessageDecoder.java @@ -5,8 +5,9 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Arrays; -import static seng302.Networking.Utils.ByteConverter.bytesToLong; -import static seng302.Networking.Utils.ByteConverter.bytesToShort; +import static network.Utils.ByteConverter.bytesToLong; +import static network.Utils.ByteConverter.bytesToShort; + /** * Created by hba56 on 20/04/17. diff --git a/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java index 1a1f2b49..9137ee24 100644 --- a/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java +++ b/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java @@ -1,11 +1,15 @@ package network.MessageEncoders; +import network.Messages.*; + +import static network.Utils.ByteConverter.*; + import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; -import static seng302.Networking.Utils.ByteConverter.*; + /** * Created by fwy13 on 19/04/17. diff --git a/racevisionGame/src/main/java/network/MessageEncoders/XMLMessageEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/XMLMessageEncoder.java index 43c88a61..b4f6d060 100644 --- a/racevisionGame/src/main/java/network/MessageEncoders/XMLMessageEncoder.java +++ b/racevisionGame/src/main/java/network/MessageEncoders/XMLMessageEncoder.java @@ -2,7 +2,8 @@ package network.MessageEncoders; import java.nio.ByteBuffer; -import static seng302.Networking.Utils.ByteConverter.*; +import static network.Utils.ByteConverter.*; + /** * Encodes a XML file into a message of AC35 format diff --git a/racevisionGame/src/main/java/network/Messages/AC35Data.java b/racevisionGame/src/main/java/network/Messages/AC35Data.java index 0b08b32a..0b8b1a9b 100644 --- a/racevisionGame/src/main/java/network/Messages/AC35Data.java +++ b/racevisionGame/src/main/java/network/Messages/AC35Data.java @@ -1,7 +1,7 @@ package network.Messages; -import seng302.Networking.Messages.Enums.MessageType; +import network.Messages.Enums.MessageType; /** * The base class for all message types. diff --git a/racevisionGame/src/main/java/network/Messages/AverageWind.java b/racevisionGame/src/main/java/network/Messages/AverageWind.java index 7cc420fb..1ba17bd5 100644 --- a/racevisionGame/src/main/java/network/Messages/AverageWind.java +++ b/racevisionGame/src/main/java/network/Messages/AverageWind.java @@ -1,6 +1,7 @@ package network.Messages; -import seng302.Networking.Messages.Enums.MessageType; + +import network.Messages.Enums.MessageType; /** * Created by fwy13 on 25/04/17. diff --git a/racevisionGame/src/main/java/network/Messages/BoatLocation.java b/racevisionGame/src/main/java/network/Messages/BoatLocation.java index eaa23df5..f78c3575 100644 --- a/racevisionGame/src/main/java/network/Messages/BoatLocation.java +++ b/racevisionGame/src/main/java/network/Messages/BoatLocation.java @@ -1,19 +1,18 @@ package network.Messages; -import seng302.Networking.Messages.Enums.MessageType; -import seng302.Networking.Utils.AC35UnitConverter; -import static seng302.Networking.Utils.AC35UnitConverter.convertGPS; -import static seng302.Networking.Utils.AC35UnitConverter.convertGPSToInt; +import network.Messages.Enums.MessageType; +import network.Utils.AC35UnitConverter; +import shared.model.Constants; + +import static network.Utils.AC35UnitConverter.convertGPS; +import static network.Utils.AC35UnitConverter.convertGPSToInt; /** * Represents the information in a boat location message (AC streaming spec: 4.9). */ public class BoatLocation extends AC35Data { - //Knots x this = meters per second. - public static final double KnotsToMetersPerSecondConversionFactor = - 0.514444; public static final byte Unknown = 0; public static final byte RacingYacht = 1; public static final byte CommitteeBoat = 2; @@ -263,11 +262,8 @@ public class BoatLocation extends AC35Data { * @return Speed in millimeters per second, stored as an int (using only the two least significant bytes). */ public static int convertBoatSpeedDoubleToInt(double speed) { - //Calculate meters per second. - double metersPerSecond = speed * KnotsToMetersPerSecondConversionFactor; - //Calculate millimeters per second. - double millimetersPerSecond = metersPerSecond * 1000.0; + double millimetersPerSecond = speed * Constants.KnotsToMMPerSecond; //Convert to an int. int millimetersPerSecondInt = (int) Math.round(millimetersPerSecond); @@ -282,11 +278,9 @@ public class BoatLocation extends AC35Data { * @return Speed in knots, stored as a double. */ public static double convertBoatSpeedIntToDouble(int speed) { - //Calculate meters per second. - double metersPerSecond = speed / 1000.0; //Calculate knots. - double knots = metersPerSecond / KnotsToMetersPerSecondConversionFactor; + double knots = speed / Constants.KnotsToMMPerSecond; return knots; } diff --git a/racevisionGame/src/main/java/network/Messages/CourseWind.java b/racevisionGame/src/main/java/network/Messages/CourseWind.java index d159d5c1..727d5fcc 100644 --- a/racevisionGame/src/main/java/network/Messages/CourseWind.java +++ b/racevisionGame/src/main/java/network/Messages/CourseWind.java @@ -1,6 +1,7 @@ package network.Messages; -import seng302.Networking.Messages.Enums.MessageType; + +import network.Messages.Enums.MessageType; /** * Created by fwy13 on 21/04/17. diff --git a/racevisionGame/src/main/java/network/Messages/CourseWinds.java b/racevisionGame/src/main/java/network/Messages/CourseWinds.java index 2ed88970..fc575867 100644 --- a/racevisionGame/src/main/java/network/Messages/CourseWinds.java +++ b/racevisionGame/src/main/java/network/Messages/CourseWinds.java @@ -1,8 +1,9 @@ package network.Messages; -import seng302.Networking.Messages.Enums.MessageType; -import java.util.ArrayList; +import network.Messages.Enums.MessageType; + +import java.util.List; /** * Created by fwy13 on 25/04/17. @@ -11,9 +12,9 @@ public class CourseWinds extends AC35Data { private int msgVerNum; private int selectedWindID; - private ArrayList courseWinds; + private List courseWinds; - public CourseWinds(int msgVerNum, int selectedWindID, ArrayList courseWinds){ + public CourseWinds(int msgVerNum, int selectedWindID, List courseWinds){ super(MessageType.COURSEWIND); this.msgVerNum = msgVerNum; this.selectedWindID = selectedWindID; diff --git a/racevisionGame/src/main/java/network/Messages/Heartbeat.java b/racevisionGame/src/main/java/network/Messages/Heartbeat.java index 3b50bf36..8c0014d7 100644 --- a/racevisionGame/src/main/java/network/Messages/Heartbeat.java +++ b/racevisionGame/src/main/java/network/Messages/Heartbeat.java @@ -1,6 +1,7 @@ package network.Messages; -import seng302.Networking.Messages.Enums.MessageType; + +import network.Messages.Enums.MessageType; /** * Represents a Heartbeat message. diff --git a/racevisionGame/src/main/java/network/Messages/MarkRounding.java b/racevisionGame/src/main/java/network/Messages/MarkRounding.java index d0148906..a13f0ba7 100644 --- a/racevisionGame/src/main/java/network/Messages/MarkRounding.java +++ b/racevisionGame/src/main/java/network/Messages/MarkRounding.java @@ -1,6 +1,7 @@ package network.Messages; -import seng302.Networking.Messages.Enums.MessageType; + +import network.Messages.Enums.MessageType; /** * Created by fwy13 on 25/04/17. diff --git a/racevisionGame/src/main/java/network/Messages/RaceMessage.java b/racevisionGame/src/main/java/network/Messages/RaceMessage.java index 856f2961..9c44e38f 100644 --- a/racevisionGame/src/main/java/network/Messages/RaceMessage.java +++ b/racevisionGame/src/main/java/network/Messages/RaceMessage.java @@ -1,6 +1,7 @@ package network.Messages; -import seng302.Networking.Messages.Enums.MessageType; + +import network.Messages.Enums.MessageType; /** * Created by fwy13 on 19/04/17. diff --git a/racevisionGame/src/main/java/network/Messages/RaceStartStatus.java b/racevisionGame/src/main/java/network/Messages/RaceStartStatus.java index ae6b9fb5..8dc442ab 100644 --- a/racevisionGame/src/main/java/network/Messages/RaceStartStatus.java +++ b/racevisionGame/src/main/java/network/Messages/RaceStartStatus.java @@ -1,6 +1,7 @@ package network.Messages; -import seng302.Networking.Messages.Enums.MessageType; + +import network.Messages.Enums.MessageType; /** * Created by fwy13 on 25/04/17. diff --git a/racevisionGame/src/main/java/network/Messages/RaceStatus.java b/racevisionGame/src/main/java/network/Messages/RaceStatus.java index 1a3ce7eb..42bb8208 100644 --- a/racevisionGame/src/main/java/network/Messages/RaceStatus.java +++ b/racevisionGame/src/main/java/network/Messages/RaceStatus.java @@ -21,7 +21,7 @@ public class RaceStatus extends AC35Data { private int raceType; private List boatStatuses; - public RaceStatus(long currentTime, int raceID, int raceStatus, long expectedStartTime, int windDirection, int windSpeed, int raceType, List boatStatuses){ + public RaceStatus(long currentTime, int raceID, byte raceStatus, long expectedStartTime, int windDirection, int windSpeed, int raceType, List boatStatuses){ super(MessageType.RACESTATUS); this.currentTime = currentTime; this.raceID = raceID; diff --git a/racevisionGame/src/main/java/network/Messages/XMLMessage.java b/racevisionGame/src/main/java/network/Messages/XMLMessage.java index 4f9542b6..5f6d7b8d 100644 --- a/racevisionGame/src/main/java/network/Messages/XMLMessage.java +++ b/racevisionGame/src/main/java/network/Messages/XMLMessage.java @@ -1,6 +1,7 @@ package network.Messages; -import seng302.Networking.Messages.Enums.MessageType; + +import network.Messages.Enums.MessageType; import java.io.InputStream; diff --git a/racevisionGame/src/main/java/network/PacketDump/AC35DumpReader.java b/racevisionGame/src/main/java/network/PacketDump/AC35DumpReader.java index fbc2b857..8022c374 100644 --- a/racevisionGame/src/main/java/network/PacketDump/AC35DumpReader.java +++ b/racevisionGame/src/main/java/network/PacketDump/AC35DumpReader.java @@ -1,8 +1,9 @@ package network.PacketDump; -import seng302.Networking.BinaryMessageDecoder; -import seng302.Networking.Exceptions.InvalidMessageException; -import seng302.Networking.Messages.AC35Data; + +import network.BinaryMessageDecoder; +import network.Exceptions.InvalidMessageException; +import network.Messages.AC35Data; import java.io.IOException; import java.net.URISyntaxException; diff --git a/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java b/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java index a5179a1b..6a226261 100644 --- a/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java +++ b/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java @@ -77,25 +77,19 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource { */ private RaceTypeEnum raceType; - //TODO maybe remove these? - private Map boats; - private Map 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 { + public RaceXMLReader(String filePath) throws XMLReaderException, InvalidRaceDataException { super(filePath); - this.boats = boatData.getBoats(); - this.marks = boatData.getMarkerBoats(); //Attempt to read race xml file. try { diff --git a/racevisionGame/src/main/java/shared/model/Boat.java b/racevisionGame/src/main/java/shared/model/Boat.java index 2f09815a..358bd663 100644 --- a/racevisionGame/src/main/java/shared/model/Boat.java +++ b/racevisionGame/src/main/java/shared/model/Boat.java @@ -7,7 +7,7 @@ import network.Messages.Enums.BoatStatusEnum; /** * Boat Model that is used to store information on the boats that are running in the race. */ -public abstract class Boat { +public class Boat { /** * The name of the boat/team. */ diff --git a/racevisionGame/src/main/java/shared/model/Race.java b/racevisionGame/src/main/java/shared/model/Race.java index 1b82425e..8b170134 100644 --- a/racevisionGame/src/main/java/shared/model/Race.java +++ b/racevisionGame/src/main/java/shared/model/Race.java @@ -25,6 +25,21 @@ import static java.lang.Math.cos; public abstract class Race implements Runnable { + /** + * The source of race related data. + */ + protected RaceDataSource raceDataSource; + + /** + * The source of boat related data. + */ + protected BoatDataSource boatDataSource; + + /** + * The source of regatta related data. + */ + protected RegattaDataSource regattaDataSource; + /** * A list of compound marks in the race. @@ -90,15 +105,19 @@ public abstract class Race implements Runnable { */ public Race(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource) { + //Keep a reference to data sources. + this.raceDataSource = raceDataSource; + this.boatDataSource = boatDataSource; + this.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.useLegsList(raceDataSource.getLegs()); + this.raceId = raceDataSource.getRaceId(); @@ -126,8 +145,15 @@ public abstract class Race implements Runnable { protected abstract void initialiseBoats(); - - + /** + * Updates the race to use a new list of legs, and adds a dummy "Finish" leg at the end. + * @param legs The new list of legs to use. + */ + protected void useLegsList(List legs) { + //We add a "dummy" leg at the end of the race. + this.legs = legs; + this.legs.add(new Leg("Finish", this.legs.size())); + } /** * Determines whether or not a specific leg is the last leg in the race. diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java index 60183c38..ac08fa50 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java @@ -14,6 +14,7 @@ import javafx.scene.paint.Color; import visualiser.model.RaceClock; import visualiser.model.Sparkline; import visualiser.model.VisualiserBoat; +import visualiser.model.VisualiserRace; import java.net.URL; import java.util.ArrayList; @@ -69,7 +70,7 @@ public class RaceController extends Controller { * * @param race Race to listen to. */ - public void setInfoTable(StreamedRace race) { + public void setInfoTable(VisualiserRace race) { boatInfoTable.setItems(race.getStartingBoats()); boatTeamColumn.setCellValueFactory(cellData -> cellData.getValue().getName()); boatSpeedColumn.setCellValueFactory(cellData -> cellData.getValue().getVelocityProp()); diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java index 89b31180..fd42dff0 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java @@ -41,14 +41,12 @@ public class VisualiserRace extends Race { /** * An observable list of marker boats in the race. */ - private final ObservableList boatMarkers; + private ObservableList 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 colors, VisualiserInput visualiserInput, RaceController controller) { @@ -68,6 +66,39 @@ public class VisualiserRace extends Race { } + /** + * Sets the race data source for this race to a new RaceDataSource. + * Uses the boundary and legs specified by the new RaceDataSource. + * @param raceDataSource The new RaceDataSource to use. + */ + public void setRaceDataSource(RaceDataSource raceDataSource) { + this.raceDataSource = raceDataSource; + + this.boundary = raceDataSource.getBoundary(); + + this.useLegsList(raceDataSource.getLegs()); + } + + /** + * Sets the boat data source for this race to a new BoatDataSource. + * Uses the marker boats specified by the new BoatDataSource. + * @param boatDataSource The new BoatDataSource to use. + */ + public void setBoatDataSource(BoatDataSource boatDataSource) { + this.boatDataSource = boatDataSource; + + this.boatMarkers = FXCollections.observableArrayList(boatDataSource.getMarkerBoats().values()); + } + + /** + * Sets the regatta data source for this race to a new RegattaDataSource. + * @param regattaDataSource The new RegattaDataSource to use. + */ + public void setRegattaDataSource(RegattaDataSource regattaDataSource) { + this.regattaDataSource = regattaDataSource; + } + + /** * 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. From 8e18ad62ca4e2f04280af9f340cf193d1aeee9ff Mon Sep 17 00:00:00 2001 From: fjc40 Date: Thu, 6 Jul 2017 16:02:11 +1200 Subject: [PATCH 06/25] Added LatestMessages to network.Messages. This is an object that encapsulates the latest up to date set of race messages. Race stores a reference to it. MockRace writes to it, and eventually, VisualiserRace will read from it. Updated MockRace, MockOutput, Event to use it. Angle now implements hashCode(). --- .../src/main/java/mock/app/Event.java | 11 +- .../src/main/java/mock/app/MockOutput.java | 197 +++++++++++++----- .../src/main/java/mock/model/MockRace.java | 52 +++-- .../java/network/Messages/LatestMessages.java | 142 +++++++++++++ .../src/main/java/shared/model/Angle.java | 7 + .../main/java/shared/model/GPSCoordinate.java | 16 +- .../src/main/java/shared/model/Race.java | 18 +- .../Controllers/StartController.java | 19 +- .../java/visualiser/app/VisualiserInput.java | 8 +- 9 files changed, 361 insertions(+), 109 deletions(-) create mode 100644 racevisionGame/src/main/java/network/Messages/LatestMessages.java diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index 2c67de2f..1328f2a2 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -3,6 +3,7 @@ package mock.app; import mock.model.MockRace; import mock.model.Polars; import network.Messages.Enums.MessageType; +import network.Messages.LatestMessages; import org.xml.sax.SAXException; import shared.dataInput.*; import shared.exceptions.InvalidBoatDataException; @@ -28,6 +29,8 @@ public class Event { private String boatXML; private Polars boatPolars; private MockOutput mockOutput; + private LatestMessages latestMessages; + public Event(String raceXML, String regattaXML, String boatXML, Polars boatPolars) { @@ -36,9 +39,13 @@ public class Event { this.regattaXML = regattaXML; this.boatPolars = boatPolars; + this.latestMessages = new LatestMessages(); + + try { - mockOutput = new MockOutput(); + this.mockOutput = new MockOutput(this.latestMessages); new Thread(mockOutput).start(); + } catch (IOException e) { e.printStackTrace(); } @@ -57,7 +64,7 @@ public class Event { RegattaDataSource regattaDataSource = new RegattaXMLReader(this.regattaXML); //Create and start race. - MockRace newRace = new MockRace(boatDataSource, raceDataSource, regattaDataSource, this.boatPolars, this.mockOutput); + MockRace newRace = new MockRace(boatDataSource, raceDataSource, regattaDataSource, this.boatPolars, this.latestMessages); new Thread(newRace).start(); diff --git a/racevisionGame/src/main/java/mock/app/MockOutput.java b/racevisionGame/src/main/java/mock/app/MockOutput.java index e0a03f80..ca584376 100644 --- a/racevisionGame/src/main/java/mock/app/MockOutput.java +++ b/racevisionGame/src/main/java/mock/app/MockOutput.java @@ -7,6 +7,7 @@ import network.MessageEncoders.RaceVisionByteEncoder; import network.MessageEncoders.XMLMessageEncoder; import network.Messages.BoatLocation; import network.Messages.Enums.MessageType; +import network.Messages.LatestMessages; import network.Messages.RaceStatus; import network.Messages.XMLMessage; @@ -37,8 +38,14 @@ public class MockOutput implements Runnable ///Output stream which wraps around mockSocket outstream. private DataOutputStream outToVisualiser; - ///A queue that contains items that are waiting to be sent. - private ArrayBlockingQueue messagesToSendQueue = new ArrayBlockingQueue<>(99999999); + + /** + * An object containing the set of latest messages to send. + * Every server frame, MockOutput reads messages from this, and send them. + */ + private LatestMessages latestMessages; + + ///Sequence numbers used in messages. private short messageNumber = 1; @@ -56,11 +63,15 @@ public class MockOutput implements Runnable /** * Ctor. + * @param latestMessages The collection of messages to send to connected clients. * @throws IOException if server socket cannot be opened. */ - public MockOutput() throws IOException { - lastHeartbeatTime = System.currentTimeMillis(); - serverSocket = new ServerSocket(serverPort); + public MockOutput(LatestMessages latestMessages) throws IOException { + + this.lastHeartbeatTime = System.currentTimeMillis(); + this.serverSocket = new ServerSocket(serverPort); + + this.latestMessages = latestMessages; } /** @@ -91,78 +102,93 @@ public class MockOutput implements Runnable * @param xmlString the xml string to send * @param messageType the kind of xml string, values given in AC35 spec (5 regatta, 6 race, 7 boat) */ - public synchronized void parseXMLString(String xmlString, int messageType){ - XMLMessageEncoder encoder = new XMLMessageEncoder(messageNumber, System.currentTimeMillis(), messageType, xmlSequenceNumber,(short) xmlString.length(), xmlString); + public synchronized byte[] parseXMLString(String xmlString, int messageType) { + + XMLMessageEncoder encoder = new XMLMessageEncoder( + messageNumber, + System.currentTimeMillis(), + messageType, + xmlSequenceNumber, + (short) xmlString.length(), + xmlString); + + //iterates the sequence numbers xmlSequenceNumber++; + byte[] encodedXML = encoder.encode(); - BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(MessageType.XMLMESSAGE, System.currentTimeMillis(), messageNumber, (short)encodedXML.length, encodedXML); + BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder( + MessageType.XMLMESSAGE, + System.currentTimeMillis(), + messageNumber, + (short)encodedXML.length, + encodedXML); + + //iterates the message number messageNumber++; - addMessageToBufferToSend(binaryMessageEncoder.getFullMessage()); + return binaryMessageEncoder.getFullMessage(); + } /** - * Used to give the mocOutput information about boat location to be made into a message and sent - * @param sourceID id of the boat - * @param lat latitude of boat - * @param lon longitude of boat - * @param heading heading of boat - * @param speed speed of boat - * @param time historical time of race + * Encodes/serialises a BoatLocation message, and returns it. + * @param boatLocation The BoatLocation message to serialise. + * @return The BoatLocation message in a serialised form. */ - public synchronized void parseBoatLocation(int sourceID, double lat, double lon, double heading, double speed, long time){ + private synchronized byte[] parseBoatLocation(BoatLocation boatLocation){ - BoatLocation boatLocation = new BoatLocation(sourceID, lat, lon, boatLocationSequenceNumber, heading, speed, time); - //iterates the sequence number - boatLocationSequenceNumber++; - //encodeds the messages + //Encodes the message. byte[] encodedBoatLoc = RaceVisionByteEncoder.boatLocation(boatLocation); - //encodeds the full message with header - BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(MessageType.BOATLOCATION, System.currentTimeMillis(), messageNumber, (short)encodedBoatLoc.length, - encodedBoatLoc); + //Encodes the full message with header. + BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder( + MessageType.BOATLOCATION, + System.currentTimeMillis(), + messageNumber, + (short) encodedBoatLoc.length, + encodedBoatLoc ); //iterates the message number messageNumber++; - addMessageToBufferToSend(binaryMessageEncoder.getFullMessage()); + return binaryMessageEncoder.getFullMessage(); + } /** - * Parse the race status data and add it to the buffer to be sent - * @param raceStatus race status to parses + * Encodes/serialises a RaceStatus message, and returns it. + * @param raceStatus The RaceStatus message to serialise. + * @return The RaceStatus message in a serialised form. */ - public synchronized void parseRaceStatus(RaceStatus raceStatus){ + private synchronized byte[] parseRaceStatus(RaceStatus raceStatus){ //iterates the sequence number raceStatusSequenceNumber++; - //encodeds the messages + //Encodes the messages. byte[] encodedRaceStatus = RaceVisionByteEncoder.raceStatus(raceStatus); - //encodeds the full message with header - BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(MessageType.RACESTATUS, System.currentTimeMillis(), messageNumber, (short)encodedRaceStatus.length, - encodedRaceStatus); + //Encodes the full message with header. + BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder( + MessageType.RACESTATUS, + System.currentTimeMillis(), + messageNumber, + (short) encodedRaceStatus.length, + encodedRaceStatus ); //iterates the message number messageNumber++; - addMessageToBufferToSend(binaryMessageEncoder.getFullMessage()); + return binaryMessageEncoder.getFullMessage(); - } - /** - * Add a message to the buffer to be sent - * @param messagesToSendBuffer message to add to the buffer - */ - private synchronized void addMessageToBufferToSend(byte[] messagesToSendBuffer) { - this.messagesToSendQueue.add(messagesToSendBuffer); } + /** * Sending loop of the Server */ @@ -175,7 +201,7 @@ public class MockOutput implements Runnable outToVisualiser = new DataOutputStream(mockSocket.getOutputStream()); - if (boatsXml == null || regattaXml == null || raceXml == null){ + if (boatsXml == null || regattaXml == null || raceXml == null) { try { Thread.sleep(500); } catch (InterruptedException e) { @@ -184,34 +210,91 @@ public class MockOutput implements Runnable continue; } - parseXMLString(raceXml, XMLMessage.XMLTypeRace); - parseXMLString(regattaXml, XMLMessage.XMLTypeRegatta); - parseXMLString(boatsXml, XMLMessage.XMLTypeBoat); + //Encode xml files. We send them inside the loop, depending on the sentXMLs boolean. + byte[] raceXMLBlob = parseXMLString(raceXml, XMLMessage.XMLTypeRace); + byte[] regattaXMLBlob = parseXMLString(regattaXml, XMLMessage.XMLTypeRegatta); + byte[] boatsXMLBlob = parseXMLString(boatsXml, XMLMessage.XMLTypeBoat); + + + long previousFrameTime = System.currentTimeMillis(); + boolean sentXMLs = false; while(true) { try { - //Sends a heartbeat every so often. - if (timeSinceHeartbeat() >= heartbeatPeriod) { - outToVisualiser.write(heartbeat()); - lastHeartbeatTime = System.currentTimeMillis(); - } - //Checks the buffer to see if there is anything to send. - while (messagesToSendQueue.size() > 0) { - //Grabs message from head of queue. - byte[] binaryMessage = messagesToSendQueue.remove(); + long currentFrameTime = System.currentTimeMillis(); + + //This is the time elapsed, in milliseconds, since the last server "frame". + long framePeriod = currentFrameTime - previousFrameTime; + + //We only attempt to send packets every X milliseconds. + long minimumFramePeriod = 16; + if (framePeriod >= minimumFramePeriod) { + + //Sends a heartbeat every so often. + if (timeSinceHeartbeat() >= heartbeatPeriod) { + outToVisualiser.write(heartbeat()); + lastHeartbeatTime = System.currentTimeMillis(); + } + + //Send XML messages. + if (!sentXMLs) { + outToVisualiser.write(raceXMLBlob); + outToVisualiser.write(regattaXMLBlob); + outToVisualiser.write(boatsXMLBlob); + sentXMLs = true; + } + + //Sens the RaceStatus message. + if (this.latestMessages.getRaceStatus() != null) { + byte[] raceStatusBlob = this.parseRaceStatus(this.latestMessages.getRaceStatus()); + + this.outToVisualiser.write(raceStatusBlob); + } - //sends the message to the visualiser - outToVisualiser.write(binaryMessage); + //Send all of the BoatLocation messages. + for (int sourceID : this.latestMessages.getBoatLocationMap().keySet()) { + + //Get the message. + BoatLocation boatLocation = this.latestMessages.getBoatLocation(sourceID); + if (boatLocation != null) { + + //Encode. + byte[] boatLocationBlob = this.parseBoatLocation(boatLocation); + + //Write it. + this.outToVisualiser.write(boatLocationBlob); + + } + } + + previousFrameTime = currentFrameTime; + + + } else { + //Wait until the frame period will be large enough. + long timeToWait = minimumFramePeriod - framePeriod; + + try { + Thread.sleep(timeToWait); + } catch (InterruptedException e) { + //If we get interrupted, exit the function. + e.printStackTrace(); + //Re-set the interrupt flag. + Thread.currentThread().interrupt(); + return; + } } - }catch(SocketException e){ + + } catch (SocketException e) { break; } } } + } catch (IOException e) { e.printStackTrace(); } @@ -247,7 +330,7 @@ public class MockOutput implements Runnable public static void main(String argv[]) throws Exception { - MockOutput client = new MockOutput(); + MockOutput client = new MockOutput(new LatestMessages()); client.run(); } diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index d5ae1b4c..9413fc62 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -2,8 +2,10 @@ package mock.model; import javafx.animation.AnimationTimer; import mock.app.MockOutput; +import network.Messages.BoatLocation; import network.Messages.BoatStatus; import network.Messages.Enums.BoatStatusEnum; +import network.Messages.LatestMessages; import network.Messages.RaceStatus; import network.Utils.AC35UnitConverter; import shared.dataInput.BoatDataSource; @@ -52,12 +54,6 @@ public class MockRace extends Race { private int dnfChance = 0; - /** - * The mockOutput to send messages to. - */ - private MockOutput mockOutput; - - /** * Used to generate random numbers when changing the wind direction. @@ -88,13 +84,12 @@ public class MockRace extends Race { * @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. + * @param latestMessages The LatestMessages to send events to. */ - public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, Polars polars, MockOutput mockOutput) { + public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, Polars polars, LatestMessages latestMessages) { - super(boatDataSource, raceDataSource, regattaDataSource); + super(boatDataSource, raceDataSource, regattaDataSource, latestMessages); - this.mockOutput = mockOutput; this.boats = this.generateMockBoats(boatDataSource.getBoats(), raceDataSource.getParticipants(), polars); @@ -175,14 +170,20 @@ public class MockRace extends Race { */ private void parseIndividualMark(Mark mark) { - this.mockOutput.parseBoatLocation( + //Create message. + BoatLocation boatLocation = new BoatLocation( mark.getSourceID(), mark.getPosition().getLatitude(), mark.getPosition().getLongitude(), - 0, - 0, - this.totalTimeElapsed + this.startTime - ); + boatLocationSequenceNumber, + 0, 0, + totalTimeElapsed + startTime); + + //Iterates the sequence number. + boatLocationSequenceNumber++; + + this.latestMessages.setBoatLocation(boatLocation); + } @@ -206,14 +207,19 @@ public class MockRace extends Race { */ private void parseIndividualBoatLocation(MockBoat boat) { - this.mockOutput.parseBoatLocation( + BoatLocation boatLocation = new BoatLocation( boat.getSourceID(), boat.getCurrentPosition().getLatitude(), boat.getCurrentPosition().getLongitude(), + boatLocationSequenceNumber, boat.getBearing().degrees(), boat.getCurrentSpeed(), - startTime + totalTimeElapsed - ); + startTime + totalTimeElapsed); + + //Iterates the sequence number. + boatLocationSequenceNumber++; + + this.latestMessages.setBoatLocation(boatLocation); } @@ -272,7 +278,6 @@ public class MockRace extends Race { boatStatuses.add(boatStatus); } - //TODO REFACTOR for consistency, could send parameters to mockOutput instead of the whole racestatus. This will also fix the sequence number issue. //Convert wind direction and speed to ints. //TODO this conversion should be done inside the racestatus class. int windDirectionInt = AC35UnitConverter.encodeHeading(this.windDirection.degrees()); @@ -283,12 +288,14 @@ public class MockRace extends Race { System.currentTimeMillis(), this.raceId, this.getRaceStatusEnum().getValue(), - this.startTime, windDirectionInt, + this.startTime, + windDirectionInt, windSpeedInt, this.getRaceType().getValue(), - boatStatuses ); + boatStatuses); + - mockOutput.parseRaceStatus(raceStatus); + this.latestMessages.setRaceStatus(raceStatus); } @@ -427,7 +434,6 @@ public class MockRace extends Race { parseRaceStatus(); if (iters > 500) { - mockOutput.stop(); stop(); } iters++; diff --git a/racevisionGame/src/main/java/network/Messages/LatestMessages.java b/racevisionGame/src/main/java/network/Messages/LatestMessages.java new file mode 100644 index 00000000..139543a0 --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/LatestMessages.java @@ -0,0 +1,142 @@ +package network.Messages; + +import java.util.HashMap; +import java.util.Map; + +/** + * This class contains a set of the latest messages received (e.g., the latest RaceStatus, the latest BoatLocation for each boat, etc...). + */ +public class LatestMessages { + + /** + * The latest RaceStatus message. + */ + private RaceStatus raceStatus; + + /** + * A map of the last BoatStatus message received, for each boat. + */ + private final Map boatStatusMap = new HashMap<>(); + + /** + * A map of the last BoatLocation message received, for each boat. + */ + private final Map boatLocationMap = new HashMap<>(); + + /** + * The last AverageWind message received. + */ + private AverageWind averageWind; + + /** + * The last CourseWinds message received. + */ + private CourseWinds courseWinds; + + + + + /** + * Ctor. + */ + public LatestMessages() { + } + + + + + /** + * Gets the latest RaceStatus message received. + * @return The latest RaceStatus message received. + */ + public RaceStatus getRaceStatus() { + return raceStatus; + } + + /** + * Sets the latest RaceStatus message received. + * @param raceStatus The new RaceStatus message to store. + */ + public void setRaceStatus(RaceStatus raceStatus) { + this.raceStatus = raceStatus; + } + + + + /** + * Returns the latest BoatStatus message received for a given boat. + * @param sourceID Source ID of the boat. + * @return The latest BoatStatus message for the specified boat. + */ + public BoatStatus getBoatStatus(int sourceID) { + return boatStatusMap.get(sourceID); + } + + /** + * Inserts a BoatStatus message for a given boat. + * @param boatStatus The BoatStatus message to set. + */ + public void setBoatStatus(BoatStatus boatStatus) { + boatStatusMap.put(boatStatus.getSourceID(), boatStatus); + } + + + + /** + * Returns the latest BoatLocation message received for a given boat. + * @param sourceID Source ID of the boat. + * @return The latest BoatLocation message for the specified boat. + */ + public BoatLocation getBoatLocation(int sourceID) { + return boatLocationMap.get(sourceID); + } + + /** + * Inserts a BoatLocation message for a given boat. + * @param boatLocation The BoatLocation message to set. + */ + public void setBoatLocation(BoatLocation boatLocation) { + //TODO should compare the sequence number of the new boatLocation with the existing boatLocation for this boat (if it exists), and use the newer one. + boatLocationMap.put(boatLocation.getSourceID(), boatLocation); + } + + + + /** + * Gets the latest AverageWind message received. + * @return The latest AverageWind message received. + */ + public AverageWind getAverageWind() { + return averageWind; + } + + /** + * Sets the latest AverageWind message received. + * @param averageWind The new AverageWind message to store. + */ + public void setAverageWind(AverageWind averageWind) { + this.averageWind = averageWind; + } + + + /** + * Gets the latest CourseWinds message received. + * @return The latest CourseWinds message received. + */ + public CourseWinds getCourseWinds() { + return courseWinds; + } + + /** + * Sets the latest CourseWinds message received. + * @param courseWinds The new CourseWinds message to store. + */ + public void setCourseWinds(CourseWinds courseWinds) { + this.courseWinds = courseWinds; + } + + + public Map getBoatLocationMap() { + return boatLocationMap; + } +} diff --git a/racevisionGame/src/main/java/shared/model/Angle.java b/racevisionGame/src/main/java/shared/model/Angle.java index d96ff50e..00610b65 100644 --- a/racevisionGame/src/main/java/shared/model/Angle.java +++ b/racevisionGame/src/main/java/shared/model/Angle.java @@ -84,6 +84,13 @@ public class Angle implements Comparable { } + @Override + public int hashCode() { + return Double.hashCode(this.degrees); + } + + + /** * Returns an int describing the ordering between this angle object, and another. * @param o Other angle to compare to. diff --git a/racevisionGame/src/main/java/shared/model/GPSCoordinate.java b/racevisionGame/src/main/java/shared/model/GPSCoordinate.java index 5018e351..63302bf2 100644 --- a/racevisionGame/src/main/java/shared/model/GPSCoordinate.java +++ b/racevisionGame/src/main/java/shared/model/GPSCoordinate.java @@ -335,12 +335,12 @@ public class GPSCoordinate { //Get the bearing between two adjacent points. Bearing bearing = GPSCoordinate.calculateBearing(firstPoint, secondPoint); - //Calculate angle perpendicular to bearing. - Bearing perpendicularBearing = Bearing.fromDegrees(bearing.degrees() + (90d * clockwiseScaleFactor)); + //Calculate angle perpindicular to bearing. + Bearing perpindicularBearing = Bearing.fromDegrees(bearing.degrees() + (90d * clockwiseScaleFactor)); //Translate both first and second point by 50m, using this bearing. These form our inwards shifted edge. - GPSCoordinate firstPointTranslated = GPSCoordinate.calculateNewPosition(firstPoint, shrinkDistance, Azimuth.fromBearing(perpendicularBearing)); - GPSCoordinate secondPointTranslated = GPSCoordinate.calculateNewPosition(secondPoint, shrinkDistance, Azimuth.fromBearing(perpendicularBearing)); + GPSCoordinate firstPointTranslated = GPSCoordinate.calculateNewPosition(firstPoint, shrinkDistance, Azimuth.fromBearing(perpindicularBearing)); + GPSCoordinate secondPointTranslated = GPSCoordinate.calculateNewPosition(secondPoint, shrinkDistance, Azimuth.fromBearing(perpindicularBearing)); //Add edge to list. shrunkEdges.add(new Pair<>(firstPointTranslated, secondPointTranslated)); @@ -355,12 +355,12 @@ public class GPSCoordinate { //Get the bearing between two adjacent points. Bearing bearing = GPSCoordinate.calculateBearing(firstPoint, secondPoint); - //Calculate angle perpendicular to bearing. - Bearing perpendicularBearing = Bearing.fromDegrees(bearing.degrees() + (90d * clockwiseScaleFactor)); + //Calculate angle perpindicular to bearing. + Bearing perpindicularBearing = Bearing.fromDegrees(bearing.degrees() + (90d * clockwiseScaleFactor)); //Translate both first and second point by 50m, using this bearing. These form our inwards shifted edge. - GPSCoordinate firstPointTranslated = GPSCoordinate.calculateNewPosition(firstPoint, shrinkDistance, Azimuth.fromBearing(perpendicularBearing)); - GPSCoordinate secondPointTranslated = GPSCoordinate.calculateNewPosition(secondPoint, shrinkDistance, Azimuth.fromBearing(perpendicularBearing)); + GPSCoordinate firstPointTranslated = GPSCoordinate.calculateNewPosition(firstPoint, shrinkDistance, Azimuth.fromBearing(perpindicularBearing)); + GPSCoordinate secondPointTranslated = GPSCoordinate.calculateNewPosition(secondPoint, shrinkDistance, Azimuth.fromBearing(perpindicularBearing)); //Add edge to list. shrunkEdges.add(new Pair<>(firstPointTranslated, secondPointTranslated)); diff --git a/racevisionGame/src/main/java/shared/model/Race.java b/racevisionGame/src/main/java/shared/model/Race.java index 8b170134..49b13b50 100644 --- a/racevisionGame/src/main/java/shared/model/Race.java +++ b/racevisionGame/src/main/java/shared/model/Race.java @@ -5,6 +5,7 @@ import javafx.collections.FXCollections; import mock.model.VMG; import network.Messages.Enums.RaceStatusEnum; import network.Messages.Enums.RaceTypeEnum; +import network.Messages.LatestMessages; import shared.dataInput.BoatDataSource; import shared.dataInput.RaceDataSource; import shared.dataInput.RegattaDataSource; @@ -40,6 +41,18 @@ public abstract class Race implements Runnable { */ protected RegattaDataSource regattaDataSource; + /** + * The collection of latest race messages. + * Can be either read from or written to. + */ + protected LatestMessages latestMessages; + + /** + * The sequence number of the latest boatLocation message sent or received. + */ + protected int boatLocationSequenceNumber = 1; + + /** * A list of compound marks in the race. @@ -102,14 +115,17 @@ public abstract class Race implements Runnable { * @param boatDataSource Data source for boat related data (yachts and marker boats). * @param raceDataSource Data source for race related data (participating boats, legs, etc...). * @param regattaDataSource Data source for race related data (course name, location, timezone, etc...). + * @param latestMessages The collection of latest messages, which can be written to, or read from. */ - public Race(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource) { + public Race(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages) { //Keep a reference to data sources. this.raceDataSource = raceDataSource; this.boatDataSource = boatDataSource; this.regattaDataSource = regattaDataSource; + this.latestMessages = latestMessages; + this.compoundMarks = raceDataSource.getCompoundMarks(); diff --git a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java index 1b7078d7..7b126d2f 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java @@ -11,10 +11,9 @@ import javafx.scene.control.TableView; import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; -import seng302.Mock.StreamedCourse; -import seng302.Model.Boat; -import seng302.Model.RaceClock; -import seng302.VisualiserInput; +import visualiser.app.VisualiserInput; +import visualiser.model.RaceClock; +import visualiser.model.VisualiserBoat; import java.io.IOException; import java.net.Socket; @@ -35,18 +34,16 @@ public class StartController extends Controller implements Observer { @FXML private Label raceTitleLabel; @FXML private Label raceStartLabel; - @FXML private TableView boatNameTable; - @FXML private TableColumn boatNameColumn; - @FXML private TableColumn boatCodeColumn; + @FXML private TableView boatNameTable; + @FXML private TableColumn boatNameColumn; + @FXML private TableColumn boatCodeColumn; @FXML private Label timeZoneTime; @FXML private Label timer; @FXML private Label raceStatusLabel; - //@FXML Button fifteenMinButton; private RaceClock raceClock; - private StreamedCourse raceData; private int raceStat; private VisualiserInput visualiserInput; @@ -80,8 +77,8 @@ public class StartController extends Controller implements Observer { * Initiliases the tables that are to be shown on the pane */ private void initialiseTables() { - List boats = raceData.getBoats(); - ObservableList observableBoats = FXCollections.observableArrayList(boats); + List boats = raceData.getBoats(); + ObservableList observableBoats = FXCollections.observableArrayList(boats); boatNameTable.setItems(observableBoats); boatNameColumn.setCellValueFactory(cellData -> cellData.getValue().getName()); diff --git a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java b/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java index 39a4cc5c..0d6d97ed 100644 --- a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java +++ b/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java @@ -1,8 +1,7 @@ package visualiser.app; import javafx.application.Platform; +import network.Messages.*; import org.xml.sax.SAXException; -import seng302.Networking.BinaryMessageDecoder; -import seng302.Networking.Exceptions.InvalidMessageException; import javax.xml.parsers.ParserConfigurationException; import java.io.DataInputStream; @@ -13,12 +12,9 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; -import static seng302.Networking.Utils.ByteConverter.bytesToShort; - /** * TCP client which receives packets/messages from a race data source * (e.g., mock source, official source), and exposes them to any observers. - * @see seng302.Mock.StreamedCourse */ public class VisualiserInput implements Runnable { @@ -30,8 +26,6 @@ public class VisualiserInput implements Runnable { ///The socket that we have connected to. private Socket connectionSocket; - ///Object to store parsed course data. //TODO comment? - private StreamedCourse course; ///The last RaceStatus message received. private RaceStatus raceStatus; From f057ad58b7e90d24d4c3b59b38ac409538c8f5cd Mon Sep 17 00:00:00 2001 From: fjc40 Date: Fri, 7 Jul 2017 02:05:21 +1200 Subject: [PATCH 07/25] LatestMessages can hold MarkRounding messages. It also holds XMLMessage for each message type. MockOutput now uses LatestMessages for xml messages. Moved xml message encoding into RaceVisionByteEncoder. Removed XMLMessageEncoder. Moved XML message sub types into an enumeration (XMLMessageType). XMLMessage can now be queried for its attributes (like timestamp, length, etc...). VisualiserInput now uses LatestMessages. VisualiserInput now (as it did previously) uses switch statements for checking packet type, instead of if statements. VisualiserRace now uses LatestMessages instead of VisualiserInput. --- pom.xml | 2 +- racevisionGame/pom.xml | 6 + .../src/main/java/mock/app/Event.java | 5 +- .../src/main/java/mock/app/MockOutput.java | 238 +++++++---- .../java/network/BinaryMessageDecoder.java | 2 +- .../MessageDecoders/XMLMessageDecoder.java | 20 +- .../RaceVisionByteEncoder.java | 50 ++- .../MessageEncoders/XMLMessageEncoder.java | 59 --- .../main/java/network/Messages/AC35Data.java | 4 +- .../Messages/Enums/XMLMessageType.java | 87 ++++ .../main/java/network/Messages/Heartbeat.java | 4 +- .../java/network/Messages/LatestMessages.java | 150 +++++++ .../java/network/Messages/XMLMessage.java | 102 ++++- .../src/main/java/shared/model/Race.java | 7 +- .../Controllers/ConnectionController.java | 6 +- .../Controllers/MainController.java | 5 +- .../Controllers/StartController.java | 8 +- .../java/visualiser/app/VisualiserInput.java | 404 +++++++----------- .../main/java/visualiser/model/RaceClock.java | 8 +- .../java/visualiser/model/VisualiserRace.java | 24 +- 20 files changed, 770 insertions(+), 421 deletions(-) delete mode 100644 racevisionGame/src/main/java/network/MessageEncoders/XMLMessageEncoder.java create mode 100644 racevisionGame/src/main/java/network/Messages/Enums/XMLMessageType.java diff --git a/pom.xml b/pom.xml index 2003bb14..2aa9257c 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ mock visualiser network - sharedModel + racevisionGame https://eng-git.canterbury.ac.nz/SENG302-2016/team-7 diff --git a/racevisionGame/pom.xml b/racevisionGame/pom.xml index f64fb5a6..bf2952b7 100644 --- a/racevisionGame/pom.xml +++ b/racevisionGame/pom.xml @@ -45,6 +45,12 @@ 9.0 + + com.github.bfsmith + geotimezone + 1.0.3 + +
diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index 1328f2a2..6b41db4e 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -3,7 +3,9 @@ package mock.app; import mock.model.MockRace; import mock.model.Polars; import network.Messages.Enums.MessageType; +import network.Messages.Enums.XMLMessageType; import network.Messages.LatestMessages; +import network.Messages.XMLMessage; import org.xml.sax.SAXException; import shared.dataInput.*; import shared.exceptions.InvalidBoatDataException; @@ -79,13 +81,10 @@ public class Event { private void sendXMLs() { mockOutput.setRegattaXml(regattaXML); - mockOutput.parseXMLString(regattaXML, MessageType.XMLMESSAGE.getValue()); mockOutput.setRaceXml(raceXML); - mockOutput.parseXMLString(raceXML, MessageType.XMLMESSAGE.getValue()); mockOutput.setBoatsXml(boatXML); - mockOutput.parseXMLString(boatXML, MessageType.XMLMESSAGE.getValue()); } /** diff --git a/racevisionGame/src/main/java/mock/app/MockOutput.java b/racevisionGame/src/main/java/mock/app/MockOutput.java index ca584376..e217accf 100644 --- a/racevisionGame/src/main/java/mock/app/MockOutput.java +++ b/racevisionGame/src/main/java/mock/app/MockOutput.java @@ -4,38 +4,46 @@ package mock.app; import network.BinaryMessageEncoder; import network.MessageEncoders.RaceVisionByteEncoder; -import network.MessageEncoders.XMLMessageEncoder; -import network.Messages.BoatLocation; +import network.Messages.*; import network.Messages.Enums.MessageType; -import network.Messages.LatestMessages; -import network.Messages.RaceStatus; -import network.Messages.XMLMessage; +import network.Messages.Enums.XMLMessageType; import java.io.DataOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; -import java.util.concurrent.ArrayBlockingQueue; /** * TCP server to send race information to connected clients. */ public class MockOutput implements Runnable { - ///Timestamp of the last sent heartbeat message. + /** + * Timestamp of the last sent heartbeat message. + */ private long lastHeartbeatTime; - ///Period for the heartbeat - that is, how often we send it. + /** + * Period for the heartbeat - that is, how often we send it. + */ private double heartbeatPeriod = 5.0; - ///Port to expose server on. + /** + * Port to expose server on. + */ private int serverPort = 4942; - ///Socket used to listen for clients on. + /** + * Socket used to listen for clients on. + */ private ServerSocket serverSocket; - ///Socket used to communicate with a client. + /** + * Socket used to communicate with a client. + */ private Socket mockSocket; - ///Output stream which wraps around mockSocket outstream. + /** + * Output stream which wraps around mockSocket outstream. + */ private DataOutputStream outToVisualiser; @@ -47,17 +55,32 @@ public class MockOutput implements Runnable - ///Sequence numbers used in messages. - private short messageNumber = 1; - private short xmlSequenceNumber = 1; + /** + * Ack numbers used in messages. + */ + private int ackNumber = 1; + + + /** + * Sequence number for race xml messages. + */ + private short raceXMLSequenceNumber = 1; + + /** + * Sequence number for boat xml messages. + */ + private short boatXMLSequenceNumber = 1; + + /** + * Sequence number for regatta xml messages. + */ + private short regattaXMLSequenceNumber = 1; + + /** + * Sequence number for heartbeat messages. + */ private int heartbeatSequenceNum = 1; - private int boatLocationSequenceNumber = 1; - private int raceStatusSequenceNumber = 1; - ///Strings containing XML data as strings. - private String raceXml; - private String regattaXml; - private String boatsXml; private boolean stop = false; //whether or not hte thread keeps running @@ -74,6 +97,19 @@ public class MockOutput implements Runnable this.latestMessages = latestMessages; } + + + /** + * Increments the ackNumber value, and returns it. + * @return Incremented ackNumber. + */ + private int getNextAckNumber(){ + this.ackNumber++; + + return this.ackNumber; + } + + /** * Calculates the time since last heartbeat message, in seconds. * @return Time since last heartbeat message, in seconds. @@ -83,56 +119,105 @@ public class MockOutput implements Runnable return (now - lastHeartbeatTime) / 1000.0; } - //returns the heartbeat message /** - * Increment the heartbeat value - * @return message for heartbeat data + * Generates the next heartbeat message and returns it. Increments the heartbeat sequence number. + * @return The next heartbeat message. */ - private byte[] heartbeat(){ - byte[] heartbeatMessage = RaceVisionByteEncoder.heartBeat(heartbeatSequenceNum); + private Heartbeat createHeartbeatMessage() { + + //Create the heartbeat message. + Heartbeat heartbeat = new Heartbeat(this.heartbeatSequenceNum); heartbeatSequenceNum++; - BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(MessageType.HEARTBEAT, System.currentTimeMillis(), messageNumber, (short)heartbeatMessage.length, heartbeatMessage); - messageNumber++; + + return heartbeat; + } + + /** + * Serializes a heartbeat message into a packet to be sent, and returns the byte array. + * @param heartbeat The heartbeat message to serialize. + * @return Byte array containing the next heartbeat message. + */ + private byte[] parseHeartbeat(Heartbeat heartbeat) { + + //Serializes the heartbeat message. + byte[] heartbeatMessage = RaceVisionByteEncoder.heartBeat(heartbeat); + + //Places the serialized message in a packet. + BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder( + MessageType.HEARTBEAT, + System.currentTimeMillis(), + getNextAckNumber(), + (short) heartbeatMessage.length, + heartbeatMessage ); + return binaryMessageEncoder.getFullMessage(); + } + /** - * Used to give the mockOutput an xml string to be made into a message and sent - * @param xmlString the xml string to send - * @param messageType the kind of xml string, values given in AC35 spec (5 regatta, 6 race, 7 boat) + * Creates an XMLMessage of a specified subtype using the xml contents string. + * @param xmlString The contents of the xml file. + * @param messageType The subtype of xml message (race, regatta, boat). + * @return The created XMLMessage object. */ - public synchronized byte[] parseXMLString(String xmlString, int messageType) { + private XMLMessage createXMLMessage(String xmlString, XMLMessageType messageType) { + + //Get the correct sequence number to use, and increment it. + short sequenceNumber = 0; + if (messageType == XMLMessageType.RACE) { + sequenceNumber = this.raceXMLSequenceNumber; + this.raceXMLSequenceNumber++; + + } else if (messageType == XMLMessageType.BOAT) { + sequenceNumber = this.boatXMLSequenceNumber; + this.boatXMLSequenceNumber++; + + } else if (messageType == XMLMessageType.REGATTA) { + sequenceNumber = this.regattaXMLSequenceNumber; + this.regattaXMLSequenceNumber++; - XMLMessageEncoder encoder = new XMLMessageEncoder( - messageNumber, + } + + //Create the message. + XMLMessage message = new XMLMessage( + XMLMessage.currentVersionNumber, + getNextAckNumber(), System.currentTimeMillis(), messageType, - xmlSequenceNumber, - (short) xmlString.length(), - xmlString); + sequenceNumber, + xmlString ); - //iterates the sequence numbers - xmlSequenceNumber++; + return message; + } + + /** + * Encodes/serialises a XMLMessage message, and returns it. + * @param xmlMessage The XMLMessage message to serialise. + * @return The XMLMessage message in a serialised form. + */ + private synchronized byte[] parseXMLMessage(XMLMessage xmlMessage) { - byte[] encodedXML = encoder.encode(); + //Serialize the xml message. + byte[] encodedXML = RaceVisionByteEncoder.xmlMessage(xmlMessage); + //Place the message in a packet. BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder( MessageType.XMLMESSAGE, System.currentTimeMillis(), - messageNumber, - (short)encodedXML.length, - encodedXML); + xmlMessage.getAckNumber(), //We use the ack number from the xml message. + (short) encodedXML.length, + encodedXML ); - //iterates the message number - messageNumber++; - return binaryMessageEncoder.getFullMessage(); } + + /** * Encodes/serialises a BoatLocation message, and returns it. * @param boatLocation The BoatLocation message to serialise. @@ -148,12 +233,10 @@ public class MockOutput implements Runnable BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder( MessageType.BOATLOCATION, System.currentTimeMillis(), - messageNumber, + getNextAckNumber(), (short) encodedBoatLoc.length, encodedBoatLoc ); - //iterates the message number - messageNumber++; return binaryMessageEncoder.getFullMessage(); @@ -166,9 +249,6 @@ public class MockOutput implements Runnable */ private synchronized byte[] parseRaceStatus(RaceStatus raceStatus){ - //iterates the sequence number - raceStatusSequenceNumber++; - //Encodes the messages. byte[] encodedRaceStatus = RaceVisionByteEncoder.raceStatus(raceStatus); @@ -176,12 +256,10 @@ public class MockOutput implements Runnable BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder( MessageType.RACESTATUS, System.currentTimeMillis(), - messageNumber, + getNextAckNumber(), (short) encodedRaceStatus.length, encodedRaceStatus ); - //iterates the message number - messageNumber++; return binaryMessageEncoder.getFullMessage(); @@ -196,12 +274,14 @@ public class MockOutput implements Runnable try { while (!stop){ + //Wait for a client to connect. System.out.println("Waiting for a connection...");//TEMP DEBUG REMOVE mockSocket = serverSocket.accept(); outToVisualiser = new DataOutputStream(mockSocket.getOutputStream()); - if (boatsXml == null || regattaXml == null || raceXml == null) { + //Wait until all of the xml files have been set. + if (!this.latestMessages.hasAllXMLMessages()) { try { Thread.sleep(500); } catch (InterruptedException e) { @@ -210,11 +290,6 @@ public class MockOutput implements Runnable continue; } - //Encode xml files. We send them inside the loop, depending on the sentXMLs boolean. - byte[] raceXMLBlob = parseXMLString(raceXml, XMLMessage.XMLTypeRace); - byte[] regattaXMLBlob = parseXMLString(regattaXml, XMLMessage.XMLTypeRegatta); - byte[] boatsXMLBlob = parseXMLString(boatsXml, XMLMessage.XMLTypeBoat); - long previousFrameTime = System.currentTimeMillis(); boolean sentXMLs = false; @@ -234,19 +309,25 @@ public class MockOutput implements Runnable //Sends a heartbeat every so often. if (timeSinceHeartbeat() >= heartbeatPeriod) { - outToVisualiser.write(heartbeat()); + outToVisualiser.write(parseHeartbeat(createHeartbeatMessage())); lastHeartbeatTime = System.currentTimeMillis(); } //Send XML messages. if (!sentXMLs) { + //Serialise them. + byte[] raceXMLBlob = parseXMLMessage(latestMessages.getRaceXMLMessage()); + byte[] regattaXMLBlob = parseXMLMessage(latestMessages.getRegattaXMLMessage()); + byte[] boatsXMLBlob = parseXMLMessage(latestMessages.getBoatXMLMessage()); + + //Send them. outToVisualiser.write(raceXMLBlob); outToVisualiser.write(regattaXMLBlob); outToVisualiser.write(boatsXMLBlob); sentXMLs = true; } - //Sens the RaceStatus message. + //Sends the RaceStatus message. if (this.latestMessages.getRaceStatus() != null) { byte[] raceStatusBlob = this.parseRaceStatus(this.latestMessages.getRaceStatus()); @@ -305,29 +386,42 @@ public class MockOutput implements Runnable } /** - * Sets the Race XML to send - * @param raceXml XML to send to the CLient + * Sets the Race XML to send. + * @param raceXml XML to send to the Client. */ public void setRaceXml(String raceXml) { - this.raceXml = raceXml; + //Create the message. + XMLMessage message = this.createXMLMessage(raceXml, XMLMessageType.RACE); + + //Place it in LatestMessages. + this.latestMessages.setRaceXMLMessage(message); } /** - * Sets the Regatta XMl to send - * @param regattaXml XML to send to CLient + * Sets the Regatta XMl to send. + * @param regattaXml XML to send to Client. */ public void setRegattaXml(String regattaXml) { - this.regattaXml = regattaXml; + //Create the message. + XMLMessage message = this.createXMLMessage(regattaXml, XMLMessageType.REGATTA); + + //Place it in LatestMessages. + this.latestMessages.setRegattaXMLMessage(message); } /** - * Sets the Boats XML to send - * @param boatsXml XMl to send to the CLient + * Sets the Boats XML to send. + * @param boatsXml XMl to send to the Client. */ public void setBoatsXml(String boatsXml) { - this.boatsXml = boatsXml; + //Create the message. + XMLMessage message = this.createXMLMessage(boatsXml, XMLMessageType.BOAT); + + //Place it in LatestMessages. + this.latestMessages.setBoatXMLMessage(message); } + public static void main(String argv[]) throws Exception { MockOutput client = new MockOutput(new LatestMessages()); diff --git a/racevisionGame/src/main/java/network/BinaryMessageDecoder.java b/racevisionGame/src/main/java/network/BinaryMessageDecoder.java index 1f8ba8e9..1c34ca85 100644 --- a/racevisionGame/src/main/java/network/BinaryMessageDecoder.java +++ b/racevisionGame/src/main/java/network/BinaryMessageDecoder.java @@ -151,7 +151,7 @@ public class BinaryMessageDecoder { //System.out.println("XML Message!"); XMLMessageDecoder xmdecoder = new XMLMessageDecoder(messageBody); xmdecoder.decode(); - return new XMLMessage(xmdecoder.getAckNumber(), xmdecoder.getTimeStamp(), xmdecoder.getXmlMsgSubType(), xmdecoder.getSequenceNumber(), xmdecoder.getXmlMsgLength(), xmdecoder.getXmlMessageInputStream()); + return new XMLMessage(XMLMessage.currentVersionNumber, xmdecoder.getAckNumber(), xmdecoder.getTimeStamp(), xmdecoder.getXmlMsgSubType(), xmdecoder.getSequenceNumber(), xmdecoder.getXmlMessageContents()); case RACESTARTSTATUS: //System.out.println("Race Start Status Message"); diff --git a/racevisionGame/src/main/java/network/MessageDecoders/XMLMessageDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/XMLMessageDecoder.java index 5af12a78..18ead92e 100644 --- a/racevisionGame/src/main/java/network/MessageDecoders/XMLMessageDecoder.java +++ b/racevisionGame/src/main/java/network/MessageDecoders/XMLMessageDecoder.java @@ -1,5 +1,7 @@ package network.MessageDecoders; +import network.Messages.Enums.XMLMessageType; + import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -42,7 +44,7 @@ public class XMLMessageDecoder { this.sequenceNumber = bytesToShort(sequenceNumberBytes); this.xmlMsgLength = bytesToShort(xmlMsgLengthBytes); - this.xmlMessage = new String(xmlMessagebytes); + this.xmlMessage = new String(xmlMessagebytes).trim(); } public byte getMessageVersionNumber() { @@ -57,8 +59,8 @@ public class XMLMessageDecoder { return timeStamp; } - public byte getXmlMsgSubType() { - return xmlMsgSubType; + public XMLMessageType getXmlMsgSubType() { + return XMLMessageType.fromByte(xmlMsgSubType); } public short getSequenceNumber() { @@ -69,14 +71,14 @@ public class XMLMessageDecoder { return xmlMsgLength; } + + /** - * this will be used latter for the vis - * @return xml string as inputsource + * Returns the contents of the XML message (e.g., the contents of a race.xml file). + * @return The contents of the XML message. */ - public InputStream getXmlMessageInputStream() { - InputStream is = new ByteArrayInputStream(xmlMessage.trim().getBytes(StandardCharsets.UTF_8)); -// InputSource is = new InputSource(new StringReader(xmlMessage.trim())); - return is; + public String getXmlMessageContents() { + return xmlMessage; } } diff --git a/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java index 9137ee24..2e811d96 100644 --- a/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java +++ b/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java @@ -6,6 +6,7 @@ import network.Messages.*; import static network.Utils.ByteConverter.*; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -18,13 +19,16 @@ public class RaceVisionByteEncoder { /** * Serializes a heartbeat message. - * @param seq Heartbeat value. + * @param heartbeat Heartbeat message. * @return Serialized message. */ - public static byte[] heartBeat(long seq){ + public static byte[] heartBeat(Heartbeat heartbeat) { + ByteBuffer heartBeat = ByteBuffer.allocate(4); - heartBeat.put(longToBytes(seq, 4)); - byte [] result = heartBeat.array(); + heartBeat.put(longToBytes(heartbeat.getSequenceNumber(), 4)); + + byte[] result = heartBeat.array(); + return result; } @@ -176,6 +180,44 @@ public class RaceVisionByteEncoder { return result.array(); } + + /** + * Serializes an xml message into a byte array. + * @param xmlMessage The message to serialize. + * @return byte array contaning serialized message. + */ + public static byte[] xmlMessage(XMLMessage xmlMessage) { + + + byte[] messageBytes = xmlMessage.getXmlMessage().getBytes(StandardCharsets.UTF_8); + + ByteBuffer tempOutputByteBuffer = ByteBuffer.allocate(14 + messageBytes.length); + + //ackNumber converted to bytes + byte[] ackNumberBytes = intToBytes(xmlMessage.getAckNumber(), 2); + + //Timestamp converted to bytes. + byte[] timestampBytes = longToBytes(xmlMessage.getTimeStamp(), 6); + + //sequenceNumber converted to bytes + byte[] sequenceNumberBytes = intToBytes(xmlMessage.getSequenceNumber(), 2); + + //xmlMsgLength converted to bytes + byte[] xmlMsgLengthBytes = intToBytes(xmlMessage.getXmlMsgLength(), 2); + + + tempOutputByteBuffer.put(xmlMessage.getVersionNumber()); + tempOutputByteBuffer.put(ackNumberBytes); + tempOutputByteBuffer.put(timestampBytes); + tempOutputByteBuffer.put(xmlMessage.getXmlMsgSubType().getValue()); + tempOutputByteBuffer.put(sequenceNumberBytes); + tempOutputByteBuffer.put(xmlMsgLengthBytes); + tempOutputByteBuffer.put(messageBytes); + + return tempOutputByteBuffer.array(); + + } + public static byte[] boatLocation(BoatLocation boatLocation){ int messageVersionNumber = 0b1; byte[] time = longToBytes(boatLocation.getTime(), 6); diff --git a/racevisionGame/src/main/java/network/MessageEncoders/XMLMessageEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/XMLMessageEncoder.java deleted file mode 100644 index b4f6d060..00000000 --- a/racevisionGame/src/main/java/network/MessageEncoders/XMLMessageEncoder.java +++ /dev/null @@ -1,59 +0,0 @@ -package network.MessageEncoders; - -import java.nio.ByteBuffer; - -import static network.Utils.ByteConverter.*; - - -/** - * Encodes a XML file into a message of AC35 format - */ -public class XMLMessageEncoder { - private byte[] messageVersionNumber; - private short ackNumber; - private long timeStamp; - private byte[] xmlMsgSubType; - private short sequenceNumber; - private short xmlMsgLength; - private String xmlMessage; - - public XMLMessageEncoder(short ackNumber, long timeStamp, int xmlMsgSubType, short sequenceNumber, short xmlMsgLength, String xmlMessage) { - this.messageVersionNumber = intToBytes(1, 1); - this.ackNumber = ackNumber; - this.timeStamp = timeStamp; - this.xmlMsgSubType = intToBytes(xmlMsgSubType, 1); - this.sequenceNumber = sequenceNumber; - this.xmlMsgLength = xmlMsgLength; - this.xmlMessage = xmlMessage; - } - - public byte[] encode() { - byte[] messageBytes = xmlMessage.getBytes(); - if (messageBytes.length > this.xmlMsgLength) { - //System.err.println("Xml message is to big"); - return null; - } - ByteBuffer tempOutputByteBuffer = ByteBuffer.allocate(14 + messageBytes.length); - - //ackNumber converted to bytes - byte[] ackNumberBytes = shortToBytes(ackNumber, 2); - - //sequenceNumber converted to bytes - byte[] sequenceNumberBytes = shortToBytes(sequenceNumber, 2); - - //xmlMsgLength converted to bytes - byte[] xmlMsgLengthBytes = shortToBytes(xmlMsgLength, 2); - - - tempOutputByteBuffer.put(messageVersionNumber); - tempOutputByteBuffer.put(ackNumberBytes); - tempOutputByteBuffer.put(longToBytes(timeStamp, 6)); - tempOutputByteBuffer.put(xmlMsgSubType); - tempOutputByteBuffer.put(sequenceNumberBytes); - tempOutputByteBuffer.put(xmlMsgLengthBytes); - tempOutputByteBuffer.put(messageBytes); - - return tempOutputByteBuffer.array(); - } - -} diff --git a/racevisionGame/src/main/java/network/Messages/AC35Data.java b/racevisionGame/src/main/java/network/Messages/AC35Data.java index 0b8b1a9b..4b9ed952 100644 --- a/racevisionGame/src/main/java/network/Messages/AC35Data.java +++ b/racevisionGame/src/main/java/network/Messages/AC35Data.java @@ -8,7 +8,9 @@ import network.Messages.Enums.MessageType; */ public abstract class AC35Data { - ///Message type from the header. + /** + * Message type from the header. + */ private MessageType type; diff --git a/racevisionGame/src/main/java/network/Messages/Enums/XMLMessageType.java b/racevisionGame/src/main/java/network/Messages/Enums/XMLMessageType.java new file mode 100644 index 00000000..94c523fc --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/Enums/XMLMessageType.java @@ -0,0 +1,87 @@ +package network.Messages.Enums; + +import java.util.HashMap; +import java.util.Map; + +/** + * Enumeration that encapsulates the various types of XML messages that can be sent. + */ +public enum XMLMessageType { + + /** + * A regatta.xml message. + */ + REGATTA(5), + + /** + * A race.xml message. + */ + RACE(6), + + /** + * A boats.xml message. + */ + BOAT(7), + + /** + * Used for unrecognised byte values. + */ + NOT_A_MESSAGE_TYPE(0); + + + ///Primitive value of the enum. + private byte value; + + /** + * Ctor. Creates a XMLMessageType enum from a given primitive integer value, cast to a byte. + * @param value Integer, which is cast to byte, to construct from. + */ + private XMLMessageType(int value) { + this.value = (byte)value; + } + + /** + * Returns the primitive value of the enum. + * @return Primitive value of the enum. + */ + public byte getValue() { + return value; + } + + + ///Stores a mapping between Byte values and XMLMessageType values. + private static final Map byteToTypeMap = new HashMap<>(); + + + /* + Static initialization block. Initializes the byteToTypeMap. + */ + static { + for (XMLMessageType type : XMLMessageType.values()) { + byteToTypeMap.put(type.value, type); + } + } + + + /** + * Returns the enumeration value which corresponds to a given byte value. + * @param messageTypeByte Byte value to convert to a XMLMessageType value. + * @return The XMLMessageType value which corresponds to the given byte value. + */ + public static XMLMessageType fromByte(byte messageTypeByte) { + //Gets the corresponding XMLMessageType from the map. + XMLMessageType type = byteToTypeMap.get(messageTypeByte); + + if (type == null) { + //If the byte value wasn't found, return the NOTAMESSAGE XMLMessageType. + return XMLMessageType.NOT_A_MESSAGE_TYPE; + } + else { + //Otherwise, return the XMLMessageType. + return type; + } + + } + + +} diff --git a/racevisionGame/src/main/java/network/Messages/Heartbeat.java b/racevisionGame/src/main/java/network/Messages/Heartbeat.java index 8c0014d7..fb1dd23f 100644 --- a/racevisionGame/src/main/java/network/Messages/Heartbeat.java +++ b/racevisionGame/src/main/java/network/Messages/Heartbeat.java @@ -8,7 +8,9 @@ import network.Messages.Enums.MessageType; */ public class Heartbeat extends AC35Data { - ///Sequence number of the heartbeat. + /** + * Sequence number of the heartbeat. + */ private long sequenceNumber; /** diff --git a/racevisionGame/src/main/java/network/Messages/LatestMessages.java b/racevisionGame/src/main/java/network/Messages/LatestMessages.java index 139543a0..a0375ea6 100644 --- a/racevisionGame/src/main/java/network/Messages/LatestMessages.java +++ b/racevisionGame/src/main/java/network/Messages/LatestMessages.java @@ -1,5 +1,8 @@ package network.Messages; +import network.Messages.Enums.XMLMessageType; +import shared.dataInput.RaceDataSource; + import java.util.HashMap; import java.util.Map; @@ -23,6 +26,11 @@ public class LatestMessages { */ private final Map boatLocationMap = new HashMap<>(); + /** + * A map of the last MarkRounding message received, for each boat. + */ + private final Map markRoundingMap = new HashMap<>(); + /** * The last AverageWind message received. */ @@ -34,6 +42,22 @@ public class LatestMessages { private CourseWinds courseWinds; + /** + * The latest race data XML message. + */ + private XMLMessage raceXMLMessage; + + /** + * The latest boat data XML message. + */ + private XMLMessage boatXMLMessage; + + /** + * The latest regatta data XML message. + */ + private XMLMessage regattaXMLMessage; + + /** @@ -100,6 +124,24 @@ public class LatestMessages { boatLocationMap.put(boatLocation.getSourceID(), boatLocation); } + /** + * Returns the latest MarkRounding message received for a given boat. + * @param sourceID Source ID of the boat. + * @return The latest MarkRounding message for the specified boat. + */ + public MarkRounding getMarkRounding(int sourceID) { + return markRoundingMap.get(sourceID); + } + + /** + * Inserts a MarkRounding message for a given boat. + * @param markRounding The MarkRounding message to set. + */ + public void setMarkRounding(MarkRounding markRounding) { + //TODO should compare the sequence number of the new boatLocation with the existing boatLocation for this boat (if it exists), and use the newer one. + markRoundingMap.put(markRounding.getSourceID(), markRounding); + } + /** @@ -136,7 +178,115 @@ public class LatestMessages { } + /** + * Returns the map of boat sourceIDs to BoatLocation messages. + * @return Map between boat sourceID and BoatLocation. + */ public Map getBoatLocationMap() { return boatLocationMap; } + + /** + * Returns the map of boat sourceIDs to BoatStatus messages. + * @return Map between boat sourceID and BoatStatus. + */ + public Map getBoatStatusMap() { + return boatStatusMap; + } + + /** + * Returns the map of boat sourceIDs to MarkRounding messages. + * @return Map between boat sourceID and MarkRounding. + */ + public Map getMarkRoundingMap() { + return markRoundingMap; + } + + + + /** + * Returns the latest race xml message. + * @return The latest race xml message. + */ + public XMLMessage getRaceXMLMessage() { + return raceXMLMessage; + } + + /** + * Sets the latest race xml message to a specified race XML message. + * @param raceXMLMessage The new race XML message to use. + */ + public void setRaceXMLMessage(XMLMessage raceXMLMessage) { + this.raceXMLMessage = raceXMLMessage; + } + + + /** + * Returns the latest boat xml message. + * @return The latest boat xml message. + */ + public XMLMessage getBoatXMLMessage() { + return boatXMLMessage; + } + + /** + * Sets the latest boat xml message to a specified boat XML message. + * @param boatXMLMessage The new boat XML message to use. + */ + public void setBoatXMLMessage(XMLMessage boatXMLMessage) { + this.boatXMLMessage = boatXMLMessage; + } + + + /** + * Returns the latest regatta xml message. + * @return The latest regatta xml message. + */ + public XMLMessage getRegattaXMLMessage() { + return regattaXMLMessage; + } + + /** + * Sets the latest regatta xml message to a specified regatta XML message. + * @param regattaXMLMessage The new regatta XML message to use. + */ + public void setRegattaXMLMessage(XMLMessage regattaXMLMessage) { + this.regattaXMLMessage = regattaXMLMessage; + } + + /** + * Checks the type of xml message, and places it in this LatestMessages object. + * @param xmlMessage The new xml message to use. + */ + public void setXMLMessage(XMLMessage xmlMessage) { + + if (xmlMessage.getXmlMsgSubType() == XMLMessageType.RACE) { + this.setRaceXMLMessage(xmlMessage); + + } else if (xmlMessage.getXmlMsgSubType() == XMLMessageType.REGATTA) { + this.setRegattaXMLMessage(xmlMessage); + + } else if (xmlMessage.getXmlMsgSubType() == XMLMessageType.BOAT) { + this.setBoatXMLMessage(xmlMessage); + + } + + } + + /** + * Returns whether or not there is an xml message for each message type. + * @return True if race, boat, and regatta have an xml message, false otherwise. + */ + public boolean hasAllXMLMessages() { + + if ((this.regattaXMLMessage == null) || (this.boatXMLMessage == null) || (this.raceXMLMessage == null)) { + return false; + + } else { + return true; + + } + + } + } diff --git a/racevisionGame/src/main/java/network/Messages/XMLMessage.java b/racevisionGame/src/main/java/network/Messages/XMLMessage.java index 5f6d7b8d..e3a4aa1b 100644 --- a/racevisionGame/src/main/java/network/Messages/XMLMessage.java +++ b/racevisionGame/src/main/java/network/Messages/XMLMessage.java @@ -2,49 +2,86 @@ package network.Messages; import network.Messages.Enums.MessageType; +import network.Messages.Enums.XMLMessageType; import java.io.InputStream; +import java.nio.charset.StandardCharsets; /** * Created by fwy13 on 25/04/17. */ public class XMLMessage extends AC35Data { + + /** + * The current version number for xml messages is 1. + */ + public static byte currentVersionNumber = 1; + + /** + * The version number of the message. + */ + private byte versionNumber; + + /** + * The ack number of the message. + */ private int ackNumber; + + /** + * The timestamp of the message. + * Milliseconds since unix epoch. + */ private long timeStamp; - private int xmlMsgSubType; + + /** + * The subtype of the xml message (e.g., race xml message). + */ + private XMLMessageType xmlMsgSubType; + + /** + * The sequence number of this specific xml subtype. + * Increments whenever the xml contents for a specific xml subtype changes. + */ private int sequenceNumber; + + /** + * The length of the xml message. + * Number of bytes. + */ private int xmlMsgLength; - private InputStream xmlMessage; - public static int XMLTypeRegatta = 5; - public static int XMLTypeRace = 6; - public static int XMLTypeBoat = 7; + /** + * The contents of the xml message. + */ + private String xmlMessage; + /** * Constructor for an XML Message + * @param versionNumber The version number of the xml message. * @param ackNumber Number for acknowledgement inherited for the AC35Data Packet * @param timeStamp Time received * @param xmlMsgSubType Type of XML message * @param sequenceNumber Order that it has arrived in - * @param xmlMsgLength Length of the xml message * @param xmlMessage XML message */ - public XMLMessage(int ackNumber, long timeStamp, int xmlMsgSubType, int sequenceNumber, int xmlMsgLength, InputStream xmlMessage){ + public XMLMessage(byte versionNumber, int ackNumber, long timeStamp, XMLMessageType xmlMsgSubType, int sequenceNumber, String xmlMessage) { super(MessageType.XMLMESSAGE); + this.versionNumber = versionNumber; this.ackNumber = ackNumber; this.timeStamp = timeStamp; this.xmlMsgSubType = xmlMsgSubType; this.sequenceNumber = sequenceNumber; - this.xmlMsgLength = xmlMsgLength; + this.xmlMsgLength = xmlMessage.getBytes(StandardCharsets.UTF_8).length; this.xmlMessage = xmlMessage; } /** - * Get the XML Message - * @return the XML message as an input stream + * Get the XML Message. + * @return the XML message as string. */ - public InputStream getXmlMessage() { + public String getXmlMessage() { return xmlMessage; } @@ -52,7 +89,48 @@ public class XMLMessage extends AC35Data { * Get the type of message * @return Gets the type of message the XML message is */ - public int getXmlMsgSubType() { + public XMLMessageType getXmlMsgSubType() { return xmlMsgSubType; } + + + /** + * Returns the version number of this xml message. + * @return The version number of this xml message. + */ + public byte getVersionNumber() { + return versionNumber; + } + + /** + * Returns the ack number of this xml message. + * @return The ack number of this xml message. + */ + public int getAckNumber() { + return ackNumber; + } + + /** + * Returns the timestamp of this xml message. + * @return The timestamp of this xml message. + */ + public long getTimeStamp() { + return timeStamp; + } + + /** + * Returns the sequence number of this xml message. This is specific to each message subtype. + * @return The sequence number of this xml message. + */ + public int getSequenceNumber() { + return sequenceNumber; + } + + /** + * Returns the length, in number of bytes, of the xml message. + * @return The length, in bytes, of the xml message. + */ + public int getXmlMsgLength() { + return xmlMsgLength; + } } diff --git a/racevisionGame/src/main/java/shared/model/Race.java b/racevisionGame/src/main/java/shared/model/Race.java index 49b13b50..84e22935 100644 --- a/racevisionGame/src/main/java/shared/model/Race.java +++ b/racevisionGame/src/main/java/shared/model/Race.java @@ -48,10 +48,15 @@ public abstract class Race implements Runnable { protected LatestMessages latestMessages; /** - * The sequence number of the latest boatLocation message sent or received. + * The sequence number of the latest BoatLocation message sent or received. */ protected int boatLocationSequenceNumber = 1; + /** + * The sequence number of the latest RaceStatus message sent or received. + */ + protected int raceStatusSequenceNumber = 1; + /** diff --git a/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java b/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java index a84f4fc7..7dacbd5e 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java @@ -8,7 +8,7 @@ import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TextField; import javafx.scene.layout.AnchorPane; -import seng302.RaceConnection; +import visualiser.model.RaceConnection; import java.io.IOException; import java.net.Socket; @@ -22,7 +22,7 @@ public class ConnectionController extends Controller { @FXML private AnchorPane connectionWrapper; @FXML - private TableView connectionTable; + private TableView connectionTable; @FXML private TableColumn hostnameColumn; @FXML @@ -73,7 +73,7 @@ public class ConnectionController extends Controller { */ public void connectSocket() { try{ - RaceConnection connection = (RaceConnection)connectionTable.getSelectionModel().getSelectedItem(); + RaceConnection connection = connectionTable.getSelectionModel().getSelectedItem(); Socket socket = new Socket(connection.getHostname(), connection.getPort()); connectionWrapper.setVisible(false); parent.enterLobby(socket); diff --git a/racevisionGame/src/main/java/visualiser/Controllers/MainController.java b/racevisionGame/src/main/java/visualiser/Controllers/MainController.java index 5c3d15fb..6fa4c96c 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/MainController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/MainController.java @@ -6,6 +6,7 @@ import javafx.scene.layout.AnchorPane; import seng302.Model.Boat; import seng302.Model.RaceClock; import seng302.VisualiserInput; +import visualiser.model.VisualiserBoat; import java.net.Socket; import java.net.URL; @@ -28,7 +29,9 @@ public class MainController extends Controller { startController.enterLobby(socket); } - public void enterFinish(ObservableList boats) { finishController.enterFinish(boats); } + public void enterFinish(ObservableList boats) { + finishController.enterFinish(boats); + } /** * Main Controller for the applications will house the menu and the displayed pane. diff --git a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java index 7b126d2f..85288e1d 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java @@ -14,6 +14,7 @@ import javafx.scene.layout.GridPane; import visualiser.app.VisualiserInput; import visualiser.model.RaceClock; import visualiser.model.VisualiserBoat; +import visualiser.model.VisualiserRace; import java.io.IOException; import java.net.Socket; @@ -48,6 +49,11 @@ public class StartController extends Controller implements Observer { private VisualiserInput visualiserInput; + /** + * The race object which describes the currently occurring race. + */ + private VisualiserRace visualiserRace; + ///Tracks whether the race has been started (that is, has startRaceNoScaling() be called). private boolean hasRaceStarted = false; @@ -65,7 +71,7 @@ public class StartController extends Controller implements Observer { @Override public void initialize(URL location, ResourceBundle resources){ - raceData = new StreamedCourse(); + this.visualiserRace = new VisualiserRace(); raceData.addObserver(this); } diff --git a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java b/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java index 0d6d97ed..9c629a37 100644 --- a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java +++ b/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java @@ -1,7 +1,16 @@ package visualiser.app; import javafx.application.Platform; +import network.BinaryMessageDecoder; +import network.Exceptions.InvalidMessageException; import network.Messages.*; import org.xml.sax.SAXException; +import shared.dataInput.BoatXMLReader; +import shared.dataInput.RaceXMLReader; +import shared.dataInput.RegattaXMLReader; +import shared.exceptions.InvalidBoatDataException; +import shared.exceptions.InvalidRaceDataException; +import shared.exceptions.InvalidRegattaDataException; +import shared.exceptions.XMLReaderException; import javax.xml.parsers.ParserConfigurationException; import java.io.DataInputStream; @@ -12,78 +21,65 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import static network.Utils.ByteConverter.bytesToShort; + /** * TCP client which receives packets/messages from a race data source * (e.g., mock source, official source), and exposes them to any observers. */ public class VisualiserInput implements Runnable { - ///Timestamp of the last heartbeat. + /** + * Timestamp of the last heartbeat. + */ private long lastHeartbeatTime = -1; - ///Sequence number of the last heartbeat. + /** + * Sequence number of the last heartbeat. + */ private long lastHeartbeatSequenceNum = -1; - ///The socket that we have connected to. - private Socket connectionSocket; + /** + * The socket that we have connected to. + */ + private Socket connectionSocket; - ///The last RaceStatus message received. - private RaceStatus raceStatus; - ///A map of the last BoatStatus message received, for each boat. - private final Map boatStatusMap = new HashMap<>(); + /** + * InputStream (from the socket). + */ + private DataInputStream inStream; - ///A map of the last BoatLocation message received, for each boat. - private final Map boatLocationMap = new HashMap<>(); - ///The last AverageWind message received. - private AverageWind averageWind; + /** + * An object containing the set of latest messages to write to. + * Every server frame, VisualiserInput reads messages from its inputStream, and write them to this. + */ + private LatestMessages latestMessages; - ///The last CourseWinds message received. - private CourseWinds courseWinds; - ///A map of the last MarkRounding message received, for each boat. - private final Map markRoundingMap = new HashMap<>(); - - ///InputStream (from the socket). - private DataInputStream inStream; /** * Ctor. * @param socket Socket from which we will receive race data. - * @param course TODO comment? * @throws IOException If there is something wrong with the socket's input stream. */ - public VisualiserInput(Socket socket, StreamedCourse course) throws IOException { + public VisualiserInput(Socket socket) throws IOException { + this.connectionSocket = socket; + //We wrap a DataInputStream around the socket's InputStream because it has the stream.readFully(buffer) function, which is a blocking read until the buffer has been filled. this.inStream = new DataInputStream(connectionSocket.getInputStream()); - this.course = course; + + this.latestMessages = new LatestMessages(); + this.lastHeartbeatTime = System.currentTimeMillis(); } - /** - * Provides StreamedCourse container for fixed course data. - * @return Course for current VisualiserInput instance. - * @see seng302.Mock.StreamedCourse - */ - public StreamedCourse getCourse() { - return course; - } - /** - * Returns the last boat location message associated with the given boat source ID. - * @param sourceID Unique global identifier for the boat. - * @return The most recent location message. - */ - public BoatLocation getBoatLocationMessage(int sourceID) { - return boatLocationMap.get(sourceID); - } - public BoatStatus getBoatStatusMessage(int sourceID) { - return boatStatusMap.get(sourceID); - } + /** * Calculates the time since last heartbeat, in milliseconds. @@ -94,62 +90,7 @@ public class VisualiserInput implements Runnable { return (now - lastHeartbeatTime); } - /** - * Returns the boat locations map. Maps from Integer (Boat ID) to BoatLocation. - * @return Map of boat locations. - */ - public Map getBoatLocationMap() { - return boatLocationMap; - } - - /** - * Gets the status of the race. - * @return The status of the race. - */ - public RaceStatus getRaceStatus() { - return raceStatus; - } - /** - * Returns the boat statuses map. Maps from Integer (Boat ID) to BoatStatus. - * @return Map of boat statuses. - */ - public Map getBoatStatusMap() { - return boatStatusMap; - } - - /** - * Returns the average wind of the race. - * @return Average wind in the race. - */ - public AverageWind getAverageWind() { - return averageWind; - } - - /** - * Returns winds in the course. - * @return Winds that are in the course. - */ - public CourseWinds getCourseWinds() { - return courseWinds; - } - - - /** - * Returns the mark roundings map. Maps from Integer (Boat ID) to MarkRounding. - * @return Map of mark roundings. - */ - public Map getMarkRoundingMap() { - return markRoundingMap; - } - - /** - * Sets the wind direction for the current course. - * @param direction The new wind direction for the course. - */ - private void setCourseWindDirection(double direction) { - this.course.setWindDirection(direction); - } /** * Reads and returns the next message as an array of bytes from the socket. Use getNextMessage() to get the actual message object instead. @@ -258,166 +199,153 @@ public class VisualiserInput implements Runnable { } //Continue to the next loop iteration/message. continue; - }/* - - //Add it to message queue. - this.messagesReceivedQueue.add(message);*/ + } //Checks which message is being received and does what is needed for that message. - //Heartbeat. - if (message instanceof Heartbeat) { - Heartbeat heartbeat = (Heartbeat) message; - - //Check that the heartbeat number is greater than the previous value, and then set the last heartbeat time. - if (heartbeat.getSequenceNumber() > this.lastHeartbeatSequenceNum) { - lastHeartbeatTime = System.currentTimeMillis(); - lastHeartbeatSequenceNum = heartbeat.getSequenceNumber(); - //System.out.println("HeartBeat Message! " + lastHeartbeatSequenceNum); - } - } - //RaceStatus. - else if (message instanceof RaceStatus) { - RaceStatus raceStatus = (RaceStatus) message; - - //System.out.println("Race Status Message"); - this.raceStatus = raceStatus; - for (BoatStatus boatStatus: this.raceStatus.getBoatStatuses()) { - this.boatStatusMap.put(boatStatus.getSourceID(), boatStatus); - } - setCourseWindDirection(raceStatus.getScaledWindDirection()); - } - //DisplayTextMessage. - /*else if (message instanceof DisplayTextMessage) { - //System.out.println("Display Text Message"); - //No decoder for this. - }*/ - //XMLMessage. - else if (message instanceof XMLMessage) { - XMLMessage xmlMessage = (XMLMessage) message; - - //System.out.println("XML Message!"); - - Platform.runLater(()-> { - if (xmlMessage.getXmlMsgSubType() == XMLMessage.XMLTypeRegatta) { - //System.out.println("Setting Regatta"); - try { - course.setRegattaXMLReader(new RegattaXMLReader(xmlMessage.getXmlMessage())); - - } - //TODO REFACTOR should put all of these exceptions behind a RegattaXMLReaderException. - catch (IOException | SAXException | ParserConfigurationException e) { - System.err.println("Error creating RegattaXMLReader: " + e.getMessage()); - //Continue to the next loop iteration/message. - } - - } else if (xmlMessage.getXmlMsgSubType() == XMLMessage.XMLTypeRace) { - //System.out.println("Setting Course"); - try { - course.setStreamedCourseXMLReader(new StreamedCourseXMLReader(xmlMessage.getXmlMessage())); - } - //TODO REFACTOR should put all of these exceptions behind a StreamedCourseXMLReaderException. - catch (IOException | SAXException | ParserConfigurationException | StreamedCourseXMLException e) { - System.err.println("Error creating StreamedCourseXMLReader: " + e.getMessage()); - //Continue to the next loop iteration/message. - } - - } else if (xmlMessage.getXmlMsgSubType() == XMLMessage.XMLTypeBoat) { - //System.out.println("Setting Boats"); - try { - course.setBoatXMLReader(new BoatXMLReader(xmlMessage.getXmlMessage())); - } - //TODO REFACTOR should put all of these exceptions behind a BoatXMLReaderException. - catch (IOException | SAXException | ParserConfigurationException e) { - System.err.println("Error creating BoatXMLReader: " + e.getMessage()); - //Continue to the next loop iteration/message. - } - - } - }); - } - //RaceStartStatus. - else if (message instanceof RaceStartStatus) { + switch (message.getType()) { - //System.out.println("Race Start Status Message"); - } - //YachtEventCode. - /*else if (message instanceof YachtEventCode) { - YachtEventCode yachtEventCode = (YachtEventCode) message; + //Heartbeat. + case HEARTBEAT: { + Heartbeat heartbeat = (Heartbeat) message; + + //Check that the heartbeat number is greater than the previous value, and then set the last heartbeat time. + if (heartbeat.getSequenceNumber() > this.lastHeartbeatSequenceNum) { + lastHeartbeatTime = System.currentTimeMillis(); + lastHeartbeatSequenceNum = heartbeat.getSequenceNumber(); + //System.out.println("HeartBeat Message! " + lastHeartbeatSequenceNum); + } + } + + //RaceStatus. + case RACESTATUS: { + RaceStatus raceStatus = (RaceStatus) message; + + //System.out.println("Race Status Message"); + this.latestMessages.setRaceStatus(raceStatus); + + for (BoatStatus boatStatus : raceStatus.getBoatStatuses()) { + this.latestMessages.setBoatStatus(boatStatus); + } + + } + + //DisplayTextMessage. + case DISPLAYTEXTMESSAGE: { + //System.out.println("Display Text Message"); + //No decoder for this. + } + + //XMLMessage. + case XMLMESSAGE: { + XMLMessage xmlMessage = (XMLMessage) message; + + //System.out.println("XML Message!"); + + this.latestMessages.setXMLMessage(xmlMessage); + + } + + //RaceStartStatus. + case RACESTARTSTATUS: { + + //System.out.println("Race Start Status Message"); + } + + //YachtEventCode. + case YACHTEVENTCODE: { + //YachtEventCode yachtEventCode = (YachtEventCode) message; //System.out.println("Yacht Event Code!"); //No decoder for this. - }*/ - //YachtActionCode. - /*else if (message instanceof YachtActionCode) { - YachtActionCode yachtActionCode = (YachtActionCode) message; + } - //System.out.println("Yacht Action Code!"); - //No decoder for this. + //YachtActionCode. + case YACHTACTIONCODE: { + //YachtActionCode yachtActionCode = (YachtActionCode) message; - }*/ - //ChatterText. - /*else if (message instanceof ChatterText) { - ChatterText chatterText = (ChatterText) message; + //System.out.println("Yacht Action Code!"); + // No decoder for this. - //System.out.println("Chatter Text Message!"); - //No decoder for this. + } - }*/ - //BoatLocation. - else if (message instanceof BoatLocation) { - BoatLocation boatLocation = (BoatLocation) message; - - //System.out.println("Boat Location!"); - if (this.boatLocationMap.containsKey(boatLocation.getSourceID())) { - //If our boatlocation map already contains a boat location message for this boat, check that the new message is actually for a later timestamp (i.e., newer). - if (boatLocation.getTime() > this.boatLocationMap.get(boatLocation.getSourceID()).getTime()){ - //If it is, replace the old message. - this.boatLocationMap.put(boatLocation.getSourceID(), boatLocation); - } - }else{ - //If the map _doesn't_ already contain a message for this boat, insert the message. - this.boatLocationMap.put(boatLocation.getSourceID(), boatLocation); - } - } - //MarkRounding. - else if (message instanceof MarkRounding) { - MarkRounding markRounding = (MarkRounding) message; - - //System.out.println("Mark Rounding Message!"); - - if (this.markRoundingMap.containsKey(markRounding.getSourceID())) { - //If our markRoundingMap already contains a mark rounding message for this boat, check that the new message is actually for a later timestamp (i.e., newer). - if (markRounding.getTime() > this.markRoundingMap.get(markRounding.getSourceID()).getTime()){ - //If it is, replace the old message. - this.markRoundingMap.put(markRounding.getSourceID(), markRounding); - } - }else{ - //If the map _doesn't_ already contain a message for this boat, insert the message. - this.markRoundingMap.put(markRounding.getSourceID(), markRounding); - } + //ChatterText. + case CHATTERTEXT: { + //ChatterText chatterText = (ChatterText) message; - } - //CourseWinds. - else if (message instanceof CourseWinds) { + //System.out.println("Chatter Text Message!"); + //No decoder for this. - //System.out.println("Course Wind Message!"); - this.courseWinds = (CourseWinds) message; + } - } - //AverageWind. - else if (message instanceof AverageWind) { + //BoatLocation. + case BOATLOCATION: { + BoatLocation boatLocation = (BoatLocation) message; + + //System.out.println("Boat Location!"); + + BoatLocation existingBoatLocation = this.latestMessages.getBoatLocationMap().get(boatLocation.getSourceID()); + if (existingBoatLocation != null) { + //If our boatlocation map already contains a boat location message for this boat, check that the new message is actually for a later timestamp (i.e., newer). + if (boatLocation.getTime() > existingBoatLocation.getTime()) { + //If it is, replace the old message. + this.latestMessages.setBoatLocation(boatLocation); + } + } else { + //If the map _doesn't_ already contain a message for this boat, insert the message. + this.latestMessages.setBoatLocation(boatLocation); + } + } - //System.out.println("Average Wind Message!"); - this.averageWind = (AverageWind) message; + //MarkRounding. + case MARKROUNDING: { + MarkRounding markRounding = (MarkRounding) message; - } - //Unrecognised message. - else { - System.out.println("Broken Message!"); - } + //System.out.println("Mark Rounding Message!"); + + MarkRounding existingMarkRounding = this.latestMessages.getMarkRoundingMap().get(markRounding.getSourceID()); + if (existingMarkRounding != null) { + + //If our markRoundingMap already contains a mark rounding message for this boat, check that the new message is actually for a later timestamp (i.e., newer). + if (markRounding.getTime() > existingMarkRounding.getTime()) { + //If it is, replace the old message. + this.latestMessages.setMarkRounding(markRounding); + } + + } else { + //If the map _doesn't_ already contain a message for this boat, insert the message. + this.latestMessages.setMarkRounding(markRounding); + } + + } + + //CourseWinds. + case COURSEWIND: { + + //System.out.println("Course Wind Message!"); + CourseWinds courseWinds = (CourseWinds) message; + + this.latestMessages.setCourseWinds(courseWinds); + + } + + //AverageWind. + case AVGWIND: { + + //System.out.println("Average Wind Message!"); + AverageWind averageWind = (AverageWind) message; + + this.latestMessages.setAverageWind(averageWind); + + } + + //Unrecognised message. + default: { + System.out.println("Broken Message!"); + } + } } } diff --git a/racevisionGame/src/main/java/visualiser/model/RaceClock.java b/racevisionGame/src/main/java/visualiser/model/RaceClock.java index a74f00ff..b076e4bf 100644 --- a/racevisionGame/src/main/java/visualiser/model/RaceClock.java +++ b/racevisionGame/src/main/java/visualiser/model/RaceClock.java @@ -5,7 +5,7 @@ import com.github.bfsmith.geotimezone.TimeZoneResult; import javafx.animation.AnimationTimer; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; -import seng302.GPSCoordinate; +import shared.model.GPSCoordinate; import java.time.Duration; import java.time.LocalDateTime; @@ -18,9 +18,9 @@ import java.util.Date; /** * This class is used to implement a clock which keeps track of and * displays times relevant to a race. This is displayed on the - * {@link seng302.Model.ResizableRaceCanvas ResizableRaceCanvas} via the - * {@link seng302.Controllers.RaceController RaceController} and the - * {@link seng302.Controllers.StartController StartController}. + * {@link ResizableRaceCanvas} via the + * {@link visualiser.Controllers.RaceController} and the + * {@link visualiser.Controllers.StartController}. */ public class RaceClock implements Runnable { private long lastTime; diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java index fd42dff0..7644a93b 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java @@ -9,6 +9,7 @@ import network.Messages.BoatLocation; import network.Messages.BoatStatus; import network.Messages.Enums.BoatStatusEnum; import network.Messages.Enums.RaceStatusEnum; +import network.Messages.LatestMessages; import network.Messages.RaceStatus; import shared.dataInput.BoatDataSource; import shared.dataInput.RaceDataSource; @@ -30,9 +31,6 @@ import java.util.Map; public class VisualiserRace extends Race { - //TODO replace with LatestMessages - private final VisualiserInput visualiserInput; - /** * An observable list of boats in the race. */ @@ -49,9 +47,17 @@ public class VisualiserRace extends Race { - public VisualiserRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, List colors, VisualiserInput visualiserInput, RaceController controller) { + /** + * Constructs a race object with a given RaceDataSource, BoatDataSource, and RegattaDataSource and receives events from LatestMessages. + * @param boatDataSource Data source for boat related data (yachts and marker boats). + * @param raceDataSource Data source for race related data (participating boats, legs, etc...). + * @param regattaDataSource Data source for race related data (course name, location, timezone, etc...). + * @param colors A collection of colors used to assign a color to each boat. + * @param latestMessages The LatestMessages to send events to. + */ + public VisualiserRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, List colors, LatestMessages latestMessages, RaceController controller) { - super(boatDataSource, raceDataSource, regattaDataSource); + super(boatDataSource, raceDataSource, regattaDataSource, latestMessages); this.boats = FXCollections.observableArrayList(this.generateVisualiserBoats(boatDataSource.getBoats(), raceDataSource.getParticipants(), colors)); @@ -59,10 +65,8 @@ public class VisualiserRace extends Race { this.boatMarkers = FXCollections.observableArrayList(boatDataSource.getMarkerBoats().values()); - this.controller = controller; - this.visualiserInput = visualiserInput; } @@ -327,16 +331,16 @@ public class VisualiserRace extends Race { //Update racing boats. - updateBoats(boats, visualiserInput.getBoatLocationMap(), visualiserInput.getBoatStatusMap()); + updateBoats(boats, latestMessages.getBoatLocationMap(), latestMessages.getBoatStatusMap()); //And their positions (e.g., 5th). updateBoatPositions(boats); //Update marker boats. - updateMarkers(boatMarkers, visualiserInput.getBoatLocationMap(), visualiserInput.getBoatStatusMap()); + updateMarkers(boatMarkers, latestMessages.getBoatLocationMap(), latestMessages.getBoatStatusMap()); //Update race status. - updateRaceStatus(visualiserInput.getRaceStatus()); + updateRaceStatus(latestMessages.getRaceStatus()); //TODO tidy this circular dependency up From abbbf7014604014c19ce2d13a00e381610d12d15 Mon Sep 17 00:00:00 2001 From: fjc40 Date: Sat, 8 Jul 2017 23:21:29 +1200 Subject: [PATCH 08/25] Refactored Race, MockRace, and VisualiserRace to use RaceClock instead of keeping their own timers. Moved FPS tracking to Race class, so both VisualiserRace and MockRace can monitor their FPS. LatestMessages is now observable. It notifies observers when an XMLMessage is received. Boat now has StringProperty for name and country/abbreviation. Moved the MockRace timescale value to Constants.RaceTimeScale. This is passed in to MockRace on construction. Tidied up StartController. Copied the visualiser's resources into the resources folder. Refactored RaceClock. Added comments. Tidied code a bit. Moved to shared.model. Started work on RaceController. --- .../src/main/java/mock/app/App.java | 8 +- .../src/main/java/mock/app/Event.java | 4 +- .../src/main/java/mock/model/MockRace.java | 82 ++-- .../java/network/Messages/BoatLocation.java | 2 + .../Messages/Enums/RaceStatusEnum.java | 4 + .../java/network/Messages/LatestMessages.java | 10 +- .../exceptions/InvalidBoatDataException.java | 2 +- .../exceptions/InvalidRaceDataException.java | 2 +- .../InvalidRegattaDataException.java | 2 +- .../src/main/java/shared/model/Boat.java | 32 +- .../src/main/java/shared/model/Constants.java | 8 + .../src/main/java/shared/model/Race.java | 116 +++++- .../src/main/java/shared/model/RaceClock.java | 356 ++++++++++++++++++ .../Controllers/MainController.java | 16 +- .../Controllers/RaceController.java | 125 +++--- .../Controllers/StartController.java | 311 +++++++++------ .../java/visualiser/app/VisualiserInput.java | 9 +- .../main/java/visualiser/model/RaceClock.java | 133 ------- .../visualiser/model/ResizableRaceCanvas.java | 1 + .../main/java/visualiser/model/Sparkline.java | 2 +- .../java/visualiser/model/VisualiserRace.java | 66 +--- .../resources/visualiser/images/arrow.png | Bin 0 -> 16120 bytes .../mock/mockXML/boatXML/boatTest.xml | 251 ++++++++++++ .../mock/mockXML/raceXML/raceTest.xml | 91 +++++ .../mock/mockXML/regattaXML/regattaTest.xml | 20 + .../resources/visualiser/raceXML/Boats.xml | 119 ++++++ .../resources/visualiser/raceXML/Race.xml | 58 +++ .../resources/visualiser/raceXML/Regatta.xml | 12 + .../visualiser/raceXML/bermuda_AC35.xml | 269 +++++++++++++ .../resources/visualiser/scenes/arrow.fxml | 34 ++ .../resources/visualiser/scenes/connect.fxml | 74 ++++ .../resources/visualiser/scenes/finish.fxml | 43 +++ .../resources/visualiser/scenes/main.fxml | 11 + .../resources/visualiser/scenes/race.fxml | 107 ++++++ .../resources/visualiser/scenes/start.fxml | 46 +++ 35 files changed, 2006 insertions(+), 420 deletions(-) create mode 100644 racevisionGame/src/main/java/shared/model/RaceClock.java delete mode 100644 racevisionGame/src/main/java/visualiser/model/RaceClock.java create mode 100644 racevisionGame/src/main/resources/visualiser/images/arrow.png create mode 100644 racevisionGame/src/main/resources/visualiser/mock/mockXML/boatXML/boatTest.xml create mode 100644 racevisionGame/src/main/resources/visualiser/mock/mockXML/raceXML/raceTest.xml create mode 100644 racevisionGame/src/main/resources/visualiser/mock/mockXML/regattaXML/regattaTest.xml create mode 100644 racevisionGame/src/main/resources/visualiser/raceXML/Boats.xml create mode 100644 racevisionGame/src/main/resources/visualiser/raceXML/Race.xml create mode 100644 racevisionGame/src/main/resources/visualiser/raceXML/Regatta.xml create mode 100644 racevisionGame/src/main/resources/visualiser/raceXML/bermuda_AC35.xml create mode 100644 racevisionGame/src/main/resources/visualiser/scenes/arrow.fxml create mode 100644 racevisionGame/src/main/resources/visualiser/scenes/connect.fxml create mode 100644 racevisionGame/src/main/resources/visualiser/scenes/finish.fxml create mode 100644 racevisionGame/src/main/resources/visualiser/scenes/main.fxml create mode 100644 racevisionGame/src/main/resources/visualiser/scenes/race.fxml create mode 100644 racevisionGame/src/main/resources/visualiser/scenes/start.fxml diff --git a/racevisionGame/src/main/java/mock/app/App.java b/racevisionGame/src/main/java/mock/app/App.java index 72f2de0e..3a98a171 100644 --- a/racevisionGame/src/main/java/mock/app/App.java +++ b/racevisionGame/src/main/java/mock/app/App.java @@ -32,11 +32,11 @@ public class App extends Application { @Override public void start(Stage primaryStage) { try { - Polars boatPolars = PolarParser.parse("polars/acc_polars.csv"); + Polars boatPolars = PolarParser.parse("mock/polars/acc_polars.csv"); - String regattaXML = readFile("mockXML/regattaTest.xml", StandardCharsets.UTF_8); - String raceXML = readFile("mockXML/raceTest.xml", StandardCharsets.UTF_8); - String boatXML = readFile("mockXML/boatTest.xml", StandardCharsets.UTF_8); + String regattaXML = readFile("mock/mockXML/regattaTest.xml", StandardCharsets.UTF_8); + String raceXML = readFile("mock/mockXML/raceTest.xml", StandardCharsets.UTF_8); + String boatXML = readFile("mock/mockXML/boatTest.xml", StandardCharsets.UTF_8); Event raceEvent = new Event(raceXML, regattaXML, boatXML, boatPolars); raceEvent.start(); diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index 6b41db4e..1d5364d5 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -66,7 +66,7 @@ public class Event { RegattaDataSource regattaDataSource = new RegattaXMLReader(this.regattaXML); //Create and start race. - MockRace newRace = new MockRace(boatDataSource, raceDataSource, regattaDataSource, this.boatPolars, this.latestMessages); + MockRace newRace = new MockRace(boatDataSource, raceDataSource, regattaDataSource, this.latestMessages, this.boatPolars, Constants.RaceTimeScale); new Thread(newRace).start(); @@ -97,6 +97,8 @@ public class Event { //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; + //Scale the time using our time scalar. + secondsToAdd = secondsToAdd / Constants.RaceTimeScale; DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"); ZonedDateTime creationTime = ZonedDateTime.now(); diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index 9413fc62..2f9e4cd4 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -41,10 +41,9 @@ public class MockRace extends Race { /** * The scale factor of the race. - * Frame periods are multiplied by this to get the amount of time a single frame represents. - * E.g., frame period = 20ms, scale = 5, frame represents 20 * 5 = 100ms, and so boats are simulated for 100ms, even though only 20ms actually occurred. + * See {@link Constants#RaceTimeScale}. */ - private int scaleFactor = 5; + private int scaleFactor; /** @@ -83,13 +82,15 @@ public class MockRace extends Race { * @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 latestMessages The LatestMessages to send events to. + * @param polars The polars table to be used for boat simulation. + * @param timeScale The timeScale for the race. See {@link Constants#RaceTimeScale}. */ - public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, Polars polars, LatestMessages latestMessages) { + public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, Polars polars, int timeScale) { super(boatDataSource, raceDataSource, regattaDataSource, latestMessages); + this.scaleFactor = timeScale; this.boats = this.generateMockBoats(boatDataSource.getBoats(), raceDataSource.getParticipants(), polars); @@ -138,7 +139,7 @@ public class MockRace extends Race { public void run() { initialiseBoats(); initialiseWindDirection(); - countdownTimer.start(); + this.countdownTimer.start(); } @@ -175,12 +176,12 @@ public class MockRace extends Race { mark.getSourceID(), mark.getPosition().getLatitude(), mark.getPosition().getLongitude(), - boatLocationSequenceNumber, + this.boatLocationSequenceNumber, 0, 0, - totalTimeElapsed + startTime); + this.raceClock.getCurrentTimeMilli()); //Iterates the sequence number. - boatLocationSequenceNumber++; + this.boatLocationSequenceNumber++; this.latestMessages.setBoatLocation(boatLocation); @@ -211,13 +212,13 @@ public class MockRace extends Race { boat.getSourceID(), boat.getCurrentPosition().getLatitude(), boat.getCurrentPosition().getLongitude(), - boatLocationSequenceNumber, + this.boatLocationSequenceNumber, boat.getBearing().degrees(), boat.getCurrentSpeed(), - startTime + totalTimeElapsed); + this.raceClock.getCurrentTimeMilli()); //Iterates the sequence number. - boatLocationSequenceNumber++; + this.boatLocationSequenceNumber++; this.latestMessages.setBoatLocation(boatLocation); @@ -225,27 +226,32 @@ public class MockRace extends Race { /** - * Updates the race status enumeration based on the current time, in milliseconds. - * @param currentTime The current time, in milliseconds. + * Updates the race time to a specified value, in milliseconds since the unix epoch. + * @param currentTime Milliseconds since unix epoch. */ - private void updateRaceStatusEnum(long currentTime) { + private void updateRaceTime(long currentTime) { + this.raceClock.setUTCTime(currentTime); + } - //The amount of milliseconds until the race starts. - long timeToStart = this.startTime - currentTime; - //Scale the time to start based on the scale factor. - long timeToStartScaled = timeToStart / this.scaleFactor; + /** + * Updates the race status enumeration based on the current time. + */ + private void updateRaceStatusEnum() { + + //The amount of milliseconds until the race starts. + long timeToStart = this.raceClock.getDurationMilli(); - if (timeToStartScaled > Constants.RacePreStartTime) { + if (timeToStart > Constants.RacePreStartTime) { //Time > 3 minutes is the prestart period. this.setRaceStatusEnum(RaceStatusEnum.PRESTART); - } else if ((timeToStartScaled <= Constants.RacePreStartTime) && (timeToStartScaled >= Constants.RacePreparatoryTime)) { + } else if ((timeToStart <= Constants.RacePreStartTime) && (timeToStart >= Constants.RacePreparatoryTime)) { //Time between [1, 3] minutes is the warning period. this.setRaceStatusEnum(RaceStatusEnum.WARNING); - } else if ((timeToStartScaled <= Constants.RacePreparatoryTime) && (timeToStartScaled > 0)) { + } else if ((timeToStart <= Constants.RacePreparatoryTime) && (timeToStart > 0)) { //Time between (0, 1] minutes is the preparatory period. this.setRaceStatusEnum(RaceStatusEnum.PREPARATORY); @@ -267,7 +273,7 @@ public class MockRace extends Race { List boatStatuses = new ArrayList<>(); //Add each boat status to the status list. - for (MockBoat boat : boats) { + for (MockBoat boat : this.boats) { BoatStatus boatStatus = new BoatStatus( boat.getSourceID(), @@ -281,14 +287,14 @@ public class MockRace extends Race { //Convert wind direction and speed to ints. //TODO this conversion should be done inside the racestatus class. int windDirectionInt = AC35UnitConverter.encodeHeading(this.windDirection.degrees()); - int windSpeedInt = (int) (windSpeed * Constants.KnotsToMMPerSecond); + int windSpeedInt = (int) (this.windSpeed * Constants.KnotsToMMPerSecond); //Create race status object, and send it. RaceStatus raceStatus = new RaceStatus( System.currentTimeMillis(), this.raceId, this.getRaceStatusEnum().getValue(), - this.startTime, + this.raceClock.getStartingTimeMilli(), windDirectionInt, windSpeedInt, this.getRaceType().getValue(), @@ -323,8 +329,11 @@ public class MockRace extends Race { @Override public void handle(long arg0) { + //Update race time. + updateRaceTime(currentTime); + //Update the race status based on the current time. - updateRaceStatusEnum(this.currentTime); + updateRaceStatusEnum(); //Parse the boat locations. parseBoatLocations(); @@ -361,6 +370,11 @@ public class MockRace extends Race { */ long timeRaceStarted = System.currentTimeMillis(); + /** + * Current time during a loop iteration. + */ + long currentTime = System.currentTimeMillis(); + /** * The time of the previous frame, in milliseconds. */ @@ -370,19 +384,17 @@ public class MockRace extends Race { public void handle(long arg0) { //Get the current time. - long currentTime = System.currentTimeMillis(); + currentTime = System.currentTimeMillis(); + + //Update race time. + updateRaceTime(currentTime); - //Update the total elapsed time. - totalTimeElapsed = currentTime - this.timeRaceStarted; //As long as there is at least one boat racing, we still simulate the race. if (getNumberOfActiveBoats() != 0) { //Get the time period of this frame. long framePeriod = currentTime - lastFrameTime; - //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 (MockBoat boat : boats) { @@ -390,7 +402,7 @@ public class MockRace extends Race { //If it is still racing, update its position. if (boat.getStatus() == BoatStatusEnum.RACING) { - updatePosition(boat, framePeriod, totalTimeElapsed); + updatePosition(boat, framePeriod, raceClock.getDurationMilli()); } @@ -654,7 +666,7 @@ public class MockRace extends Race { //Check the boats position (update leg and stuff). - this.checkPosition(boat, totalTimeElapsed); + this.checkPosition(boat, totalElapsedMilliseconds); } @@ -899,7 +911,7 @@ public class MockRace extends Race { if (velocityToMark > 0) { long timeFromNow = (long) (1000 * boat.calculateDistanceToNextMarker() / velocityToMark); - boat.setEstimatedTime(startTime + totalTimeElapsed + timeFromNow); + boat.setEstimatedTime(this.raceClock.getCurrentTimeMilli() + timeFromNow); } } diff --git a/racevisionGame/src/main/java/network/Messages/BoatLocation.java b/racevisionGame/src/main/java/network/Messages/BoatLocation.java index f78c3575..301584b4 100644 --- a/racevisionGame/src/main/java/network/Messages/BoatLocation.java +++ b/racevisionGame/src/main/java/network/Messages/BoatLocation.java @@ -13,6 +13,7 @@ import static network.Utils.AC35UnitConverter.convertGPSToInt; */ public class BoatLocation extends AC35Data { + //TODO move these to an enum. public static final byte Unknown = 0; public static final byte RacingYacht = 1; public static final byte CommitteeBoat = 2; @@ -27,6 +28,7 @@ public class BoatLocation extends AC35Data { public static final byte WeatherStation = 11; public static final byte Helicopter = 12; public static final byte DataProcessingApplication = 13; + ///Version number of the message - is always 1. private byte messageVersionNumber = 1; ///Time of the event - milliseconds since jan 1 1970. Proper type is 6 byte int. diff --git a/racevisionGame/src/main/java/network/Messages/Enums/RaceStatusEnum.java b/racevisionGame/src/main/java/network/Messages/Enums/RaceStatusEnum.java index 973d4347..2c187d0e 100644 --- a/racevisionGame/src/main/java/network/Messages/Enums/RaceStatusEnum.java +++ b/racevisionGame/src/main/java/network/Messages/Enums/RaceStatusEnum.java @@ -20,6 +20,10 @@ public enum RaceStatusEnum { * Less than 1:00 minutes before start. */ PREPARATORY(2), + + /** + * Race has started. + */ STARTED(3), /** diff --git a/racevisionGame/src/main/java/network/Messages/LatestMessages.java b/racevisionGame/src/main/java/network/Messages/LatestMessages.java index a0375ea6..bf0e14b0 100644 --- a/racevisionGame/src/main/java/network/Messages/LatestMessages.java +++ b/racevisionGame/src/main/java/network/Messages/LatestMessages.java @@ -5,11 +5,13 @@ import shared.dataInput.RaceDataSource; import java.util.HashMap; import java.util.Map; +import java.util.Observable; /** * This class contains a set of the latest messages received (e.g., the latest RaceStatus, the latest BoatLocation for each boat, etc...). + * Currently, LatestMessage only notifies observers of change when a new XMLMessage is received. */ -public class LatestMessages { +public class LatestMessages extends Observable { /** * The latest RaceStatus message. @@ -218,6 +220,8 @@ public class LatestMessages { */ public void setRaceXMLMessage(XMLMessage raceXMLMessage) { this.raceXMLMessage = raceXMLMessage; + + this.notifyObservers(); } @@ -235,6 +239,8 @@ public class LatestMessages { */ public void setBoatXMLMessage(XMLMessage boatXMLMessage) { this.boatXMLMessage = boatXMLMessage; + + this.notifyObservers(); } @@ -252,6 +258,8 @@ public class LatestMessages { */ public void setRegattaXMLMessage(XMLMessage regattaXMLMessage) { this.regattaXMLMessage = regattaXMLMessage; + + this.notifyObservers(); } /** diff --git a/racevisionGame/src/main/java/shared/exceptions/InvalidBoatDataException.java b/racevisionGame/src/main/java/shared/exceptions/InvalidBoatDataException.java index 6f2c7a64..ba12215c 100644 --- a/racevisionGame/src/main/java/shared/exceptions/InvalidBoatDataException.java +++ b/racevisionGame/src/main/java/shared/exceptions/InvalidBoatDataException.java @@ -3,7 +3,7 @@ package shared.exceptions; /** * 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 class InvalidBoatDataException extends Exception { public InvalidBoatDataException(String message) { super(message); diff --git a/racevisionGame/src/main/java/shared/exceptions/InvalidRaceDataException.java b/racevisionGame/src/main/java/shared/exceptions/InvalidRaceDataException.java index 83f6a295..e181f675 100644 --- a/racevisionGame/src/main/java/shared/exceptions/InvalidRaceDataException.java +++ b/racevisionGame/src/main/java/shared/exceptions/InvalidRaceDataException.java @@ -3,7 +3,7 @@ package shared.exceptions; /** * 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 class InvalidRaceDataException extends Exception { public InvalidRaceDataException(String message) { super(message); diff --git a/racevisionGame/src/main/java/shared/exceptions/InvalidRegattaDataException.java b/racevisionGame/src/main/java/shared/exceptions/InvalidRegattaDataException.java index 007e534b..8a5d581e 100644 --- a/racevisionGame/src/main/java/shared/exceptions/InvalidRegattaDataException.java +++ b/racevisionGame/src/main/java/shared/exceptions/InvalidRegattaDataException.java @@ -3,7 +3,7 @@ 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 class InvalidRegattaDataException extends Exception { public InvalidRegattaDataException(String message) { super(message); diff --git a/racevisionGame/src/main/java/shared/model/Boat.java b/racevisionGame/src/main/java/shared/model/Boat.java index 358bd663..e05e7791 100644 --- a/racevisionGame/src/main/java/shared/model/Boat.java +++ b/racevisionGame/src/main/java/shared/model/Boat.java @@ -1,6 +1,7 @@ package shared.model; +import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import network.Messages.Enums.BoatStatusEnum; @@ -11,7 +12,7 @@ public class Boat { /** * The name of the boat/team. */ - private String name; + private StringProperty name = new SimpleStringProperty(); /** * The current speed of the boat, in knots. @@ -32,7 +33,7 @@ public class Boat { /** * The country or team abbreviation of the boat. */ - private String country; + private StringProperty country = new SimpleStringProperty(); /** * The source ID of the boat. @@ -89,9 +90,10 @@ public class Boat { * @param country The abbreviation or country code for the boat. */ public Boat(int sourceID, String name, String country) { - this.country = country; - this.name = name; + this.sourceID = sourceID; + this.setName(name); + this.setCountry(country); this.bearing = Bearing.fromDegrees(0d); @@ -106,7 +108,7 @@ public class Boat { * @return Name of the boat/team. */ public String getName() { - return name; + return name.getValue(); } /** @@ -114,9 +116,16 @@ public class Boat { * @param name Name of the boat/team. */ public void setName(String name) { - this.name = name; + this.name.setValue(name); } + /** + * Returns the name property of the boat. + * @return The name of the boat, in a StringProperty. + */ + public StringProperty nameProperty() { + return name; + } /** * Returns the current speed of the boat, in knots. @@ -140,7 +149,7 @@ public class Boat { * @return The country/team abbreviation of the boat. */ public String getCountry() { - return country; + return country.getValue(); } /** @@ -148,9 +157,16 @@ public class Boat { * @param country The new country/team abbreviation for the boat. */ public void setCountry(String country) { - this.country = country; + this.country.setValue(country); } + /** + * Returns the country/abbreviation property of the boat. + * @return The country/abbreviation of the boat, in a StringProperty. + */ + public StringProperty countryProperty() { + return country; + } /** * Returns the source ID of the boat. diff --git a/racevisionGame/src/main/java/shared/model/Constants.java b/racevisionGame/src/main/java/shared/model/Constants.java index c71dcb64..1db211cf 100644 --- a/racevisionGame/src/main/java/shared/model/Constants.java +++ b/racevisionGame/src/main/java/shared/model/Constants.java @@ -27,6 +27,14 @@ public class Constants { public static final double KnotsToMMPerSecond = 514.444; + + /** + * The scale factor of the race. + * Frame periods are multiplied by this to get the amount of time a single frame represents. + * E.g., frame period = 20ms, scale = 5, frame represents 20 * 5 = 100ms, and so boats are simulated for 100ms, even though only 20ms actually occurred. + */ + public static final int RaceTimeScale = 1; + /** * The race pre-start time, in milliseconds. 3 minutes. */ diff --git a/racevisionGame/src/main/java/shared/model/Race.java b/racevisionGame/src/main/java/shared/model/Race.java index 84e22935..771f3e94 100644 --- a/racevisionGame/src/main/java/shared/model/Race.java +++ b/racevisionGame/src/main/java/shared/model/Race.java @@ -1,8 +1,5 @@ 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 network.Messages.LatestMessages; @@ -10,12 +7,7 @@ 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; /** @@ -75,15 +67,11 @@ public abstract class Race implements Runnable { protected List boundary; - /** - * The elapsed time, in milliseconds, of the race. - */ - protected long totalTimeElapsed; /** - * The starting timestamp, in milliseconds, of the race. + * The clock which tracks the race's start time, current time, and elapsed duration. */ - protected long startTime; + protected RaceClock raceClock; /** @@ -91,6 +79,11 @@ public abstract class Race implements Runnable { */ protected int raceId; + /** + * The name of the regatta. + */ + protected String regattaName; + /** * The current status of the race. */ @@ -114,6 +107,23 @@ public abstract class Race implements Runnable { protected double windSpeed; + /** + * The number of frames per second. + * We essentially track the number of frames generated per second, over a one second period. When {@link #lastFpsResetTime} reaches 1 second, {@link #currentFps} is reset. + */ + private int currentFps = 0; + + /** + * The number of frames per second we generated over the last 1 second period. + */ + private int lastFps = 0; + + /** + * The time, in milliseconds, since we last reset our {@link #currentFps} counter. + */ + private long lastFpsResetTime; + + /** * Constructs a race object with a given BoatDataSource, RaceDataSource, and RegattaDataSource. @@ -132,28 +142,37 @@ public abstract class Race implements Runnable { this.latestMessages = latestMessages; + //Marks. this.compoundMarks = raceDataSource.getCompoundMarks(); + //Boundaries. this.boundary = raceDataSource.getBoundary(); + //Legs. this.useLegsList(raceDataSource.getLegs()); + //Race ID. this.raceId = raceDataSource.getRaceId(); + //Regatta name. + this.regattaName = regattaDataSource.getRegattaName(); - this.startTime = raceDataSource.getStartDateTime().toInstant().toEpochMilli(); - + //Race clock. + this.raceClock = new RaceClock(this.raceDataSource.getStartDateTime()); + //this.raceClock.run();//TODO looks like we may not actually need this. + //Race status. this.setRaceStatusEnum(RaceStatusEnum.NOT_ACTIVE); + //Race type. this.raceType = raceDataSource.getRaceType(); + //Wind speed. this.windSpeed = 0; + //Wind direction. this.windDirection = Bearing.fromDegrees(0); - this.totalTimeElapsed = 0; - } @@ -226,6 +245,67 @@ public abstract class Race implements Runnable { return raceType; } + /** + * Returns the name of the regatta. + * @return The name of the regatta. + */ + public String getRegattaName() { + return regattaName; + } + + /** + * Returns the wind bearing. + * @return The wind bearing. + */ + public Bearing getWindDirection() { + return windDirection; + } + + /** + * Returns the wind speed. + * Measured in knots. + * @return The wind speed. + */ + public double getWindSpeed() { + return windSpeed; + } + + /** + * Returns the RaceClock for this race. + * This is used to track the start time, current time, and elapsed duration of the race. + * @return The RaceClock for the race. + */ + public RaceClock getRaceClock() { + return raceClock; + } + /** + * Returns the number of frames generated per second. + * @return Frames per second. + */ + public int getFps() { + return lastFps; + } + + + /** + * Increments the FPS counter, and adds timePeriod milliseconds to our FPS reset timer. + * @param timePeriod Time, in milliseconds, to add to {@link #lastFpsResetTime}. + */ + protected void incrementFps(long timePeriod) { + //Increment. + this.currentFps++; + + //Add period to timer. + this.lastFpsResetTime += timePeriod; + + //If we have reached 1 second period, snapshot the framerate and reset. + if (this.lastFpsResetTime > 1000) { + this.lastFps = this.currentFps; + + this.currentFps = 0; + this.lastFpsResetTime = 0; + } + } } diff --git a/racevisionGame/src/main/java/shared/model/RaceClock.java b/racevisionGame/src/main/java/shared/model/RaceClock.java new file mode 100644 index 00000000..15f90682 --- /dev/null +++ b/racevisionGame/src/main/java/shared/model/RaceClock.java @@ -0,0 +1,356 @@ +package shared.model; + +import com.github.bfsmith.geotimezone.TimeZoneLookup; +import com.github.bfsmith.geotimezone.TimeZoneResult; +import com.sun.istack.internal.Nullable; +import javafx.animation.AnimationTimer; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import shared.model.GPSCoordinate; +import visualiser.model.ResizableRaceCanvas; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Date; + +/** + * This class is used to implement a clock which keeps track of and + * displays times relevant to a race. This is displayed on the + * {@link ResizableRaceCanvas} via the + * {@link visualiser.Controllers.RaceController} and the + * {@link visualiser.Controllers.StartController}. + */ +public class RaceClock implements Runnable { + + /** + * The time that we last updated the current race time at. + */ + private long lastTime; + + /** + * The time zone of the race. + */ + private final ZoneId zoneId; + + /** + * The start time of the race. + */ + private ZonedDateTime startingTime; + + /** + * The current time of the race. + */ + private final StringProperty startingTimeProperty = new SimpleStringProperty(); + + + /** + * The current time of the race. + */ + @Nullable + private ZonedDateTime currentTime; + + /** + * The current time of the race. + */ + private final StringProperty currentTimeProperty = new SimpleStringProperty(); + + /** + * The time until the race starts, or elapsed time in the race after it has started. + */ + private StringProperty durationProperty = new SimpleStringProperty(); + + + //Format strings. + /** + * Format string used for starting time. + */ + private String startingTimeFormat = "'Starting time:' HH:mm dd/MM/YYYY"; + + /** + * Format string used for current time. + */ + private String currentTimeFormat = "'Starting time:' HH:mm dd/MM/YYYY"; + + /** + * Format string used for duration before it has started. + */ + private String durationBeforeStartFormat = "Starting in: %02d:%02d:%02d"; + /** + * Format string used for duration once the race has started. + */ + private String durationAfterStartFormat = "Time: %02d:%02d:%02d"; + + + + + /** + * Constructs a RaceClock using a specified starting ZonedDateTime. + * @param startingTime The ZonedDateTime that the race starts at. + */ + public RaceClock(ZonedDateTime startingTime) { + this.zoneId = startingTime.getZone(); + + //Set start time. + setStartingTime(startingTime); + + } + + + /** + * Returns the ZonedDateTime corresponding to a specified GPSCoordinate. + * @param gpsCoordinate The GPSCoordinate to lookup. + * @return The ZonedDateTime for the coordinate. + */ + public static ZonedDateTime getCurrentZonedDateTime(GPSCoordinate gpsCoordinate) { + TimeZoneLookup timeZoneLookup = new TimeZoneLookup(); + TimeZoneResult timeZoneResult = timeZoneLookup.getTimeZone(gpsCoordinate.getLatitude(), gpsCoordinate.getLongitude()); + ZoneId zone = ZoneId.of(timeZoneResult.getResult()); + return LocalDateTime.now(zone).atZone(zone); + } + + /** + * Starts the race clock. + */ + public void run() { + new AnimationTimer() { + @Override + public void handle(long now) { + updateTime(); + } + }.start(); + } + + + + /** + * Sets time to given UTC time in seconds from Unix epoch, preserving timezone. + * @param time UTC time. + */ + public void setUTCTime(long time) { + Date utcTime = new Date(time); + setCurrentTime(utcTime.toInstant().atZone(this.zoneId)); + } + + + + /** + * Get ZonedDateTime corresponding to local time zone and given UTC time. + * @param time time in mills + * @return local date time + */ + public ZonedDateTime getLocalTime(long time) { + Date utcTime = new Date(time); + return utcTime.toInstant().atZone(this.zoneId); + } + + /** + * Updates time by duration elapsed since last update. + */ + private void updateTime() { + + //Get duration elapsed since last update. + Duration duration = Duration.of(System.currentTimeMillis() - this.lastTime, ChronoUnit.MILLIS); + + //Add this duration to the current time. + ZonedDateTime newCurrentTime = this.currentTime.plus(duration); + setCurrentTime(newCurrentTime); + + } + + + /** + * Returns the starting time of the race. + * @return The starting time of the race. + */ + public ZonedDateTime getStartingTime() { + return startingTime; + } + + /** + * Returns the race start time, expressed as the number of milliseconds since the unix epoch. + * @return Start time expressed as milliseconds since unix epoch. + */ + public long getStartingTimeMilli() { + return startingTime.toInstant().toEpochMilli(); + } + + /** + * Sets the starting time of the race. + * @param startingTime The starting time of the race. + */ + public void setStartingTime(ZonedDateTime startingTime) { + this.startingTime = startingTime; + + //Convert time into string. + String startingTimeString = DateTimeFormatter.ofPattern(this.startingTimeFormat).format(startingTime); + + //Use it. + setStartingTimeString(startingTimeString); + } + + /** + * Returns the starting time of the race, as a string. + * @return The starting time of the race, as a string. + */ + public String getStartingTimeString() { + return startingTimeProperty.get(); + } + + /** + * Sets the starting time string of the race. + * This should only be called by {@link #setStartingTime(ZonedDateTime)}. + * @param startingTime The new value for the starting time string. + */ + private void setStartingTimeString(String startingTime) { + this.startingTimeProperty.setValue(startingTime); + } + + /** + * Returns the starting time property. + * @return The starting time property. + */ + public StringProperty startingTimeProperty() { + return startingTimeProperty; + } + + + + /** + * Returns the race duration, in milliseconds. + * A negative value means that the race has not started. + * @return Race duration in milliseconds. + */ + public long getDurationMilli() { + return getCurrentTimeMilli() - getStartingTimeMilli(); + } + + + /** + * Returns the race duration, as a string. + * @return Duration as a string. + */ + public String getDurationString() { + return durationProperty.get(); + } + + /** + * Sets the duration time string of the race. + * @param duration The new value for the duration time string. + */ + private void setDurationString(String duration) { + this.durationProperty.setValue(duration); + } + + /** + * Returns the duration property. + * @return The duration property. + */ + public StringProperty durationProperty() { + return durationProperty; + } + + + + + /** + * Returns the current time of the race. + * @return The current time of the race. + */ + public ZonedDateTime getCurrentTime() { + return currentTime; + } + + /** + * Returns the race current time, expressed as the number of milliseconds since the unix epoch. + * @return Current time expressed as milliseconds since unix epoch. + */ + public long getCurrentTimeMilli() { + return currentTime.toInstant().toEpochMilli(); + } + + /** + * Sets the current time of the race. + * @param currentTime The current time of the race. + */ + private void setCurrentTime(ZonedDateTime currentTime) { + this.currentTime = currentTime; + + //Convert time into string. + String currentTimeString = DateTimeFormatter.ofPattern(this.currentTimeFormat).format(currentTime); + + //Use it. + setCurrentTimeString(currentTimeString); + + //Store the last time we updated the current time at. + this.lastTime = System.currentTimeMillis(); + + //Update the duration string. + updateDurationString(); + + } + + /** + * Updates the duration string based on the start time and current time. + * This requires {@link #currentTime} to be non-null. + */ + private void updateDurationString() { + //Calculates the duration in seconds. + long seconds = Duration.between(startingTime.toLocalDateTime(), currentTime.toLocalDateTime()).getSeconds(); + + //Check if the race has already started or not. This determines the format string used. + String formatString; + if (seconds < 0) { + //Race hasn't started. + formatString = this.durationBeforeStartFormat; + //The seconds value is negative, so we make it positive. + seconds = seconds * -1; + } else { + //Race has started. + formatString = this.durationAfterStartFormat; + } + + //Format the seconds value. + //Hours : minutes : seconds. + String formattedDuration = String.format(formatString, seconds / 3600, (seconds % 3600) / 60, seconds % 60); + + //Use it. + setDurationString(formattedDuration); + } + + /** + * Returns the current time of the race, as a string. + * @return The current time of the race, as a string. + */ + public String getCurrentTimeString() { + return currentTimeProperty.get(); + } + + /** + * Sets the current time string of the race. + * @param currentTime The new value for the current time string. + */ + private void setCurrentTimeString(String currentTime) { + this.currentTimeProperty.setValue(currentTime); + } + + /** + * Returns the current time property. + * @return The current time property. + */ + public StringProperty currentTimeProperty() { + return currentTimeProperty; + } + + + /** + * Returns the time zone of the race, as a string. + * @return The race time zone. + */ + public String getTimeZone() { + return zoneId.toString(); + } +} diff --git a/racevisionGame/src/main/java/visualiser/Controllers/MainController.java b/racevisionGame/src/main/java/visualiser/Controllers/MainController.java index 6fa4c96c..e2bf6424 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/MainController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/MainController.java @@ -3,10 +3,9 @@ package visualiser.Controllers; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.layout.AnchorPane; -import seng302.Model.Boat; -import seng302.Model.RaceClock; -import seng302.VisualiserInput; +import visualiser.app.VisualiserInput; import visualiser.model.VisualiserBoat; +import visualiser.model.VisualiserRace; import java.net.Socket; import java.net.URL; @@ -21,8 +20,13 @@ public class MainController extends Controller { @FXML private ConnectionController connectionController; @FXML private FinishController finishController; - public void beginRace(VisualiserInput visualiserInput, RaceClock raceClock) { - raceController.startRace(visualiserInput, raceClock); + /** + * Transitions from the StartController screen (displays pre-race information) to the RaceController (displays the actual race). + * @param visualiserInput The object used to read packets from the race server. + * @param visualiserRace The object modelling the race. + */ + public void beginRace(VisualiserInput visualiserInput, VisualiserRace visualiserRace) { + raceController.startRace(visualiserInput, visualiserRace); } public void enterLobby(Socket socket) { @@ -41,10 +45,12 @@ public class MainController extends Controller { */ @Override public void initialize(URL location, ResourceBundle resources) { + startController.setParent(this); raceController.setParent(this); connectionController.setParent(this); finishController.setParent(this); + AnchorPane.setTopAnchor(startController.startWrapper(), 0.0); AnchorPane.setBottomAnchor(startController.startWrapper(), 0.0); AnchorPane.setLeftAnchor(startController.startWrapper(), 0.0); diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java index ac08fa50..75b42142 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java @@ -10,45 +10,84 @@ import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; -import javafx.scene.paint.Color; -import visualiser.model.RaceClock; +import visualiser.app.VisualiserInput; +import shared.model.RaceClock; import visualiser.model.Sparkline; import visualiser.model.VisualiserBoat; import visualiser.model.VisualiserRace; import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import java.util.ResourceBundle; /** - * Created by fwy13 on 15/03/2017. + * Controller used to display a running race. */ public class RaceController extends Controller { + + /** + * The object used to read packets from the connected server. + */ + private VisualiserInput visualiserInput; + + /** + * The race object which describes the currently occurring race. + */ + private VisualiserRace visualiserRace; + private ResizableRaceCanvas raceMap; private ResizableRaceMap raceBoundaries; - private RaceClock raceClock; + /** + * The sparkline graph. + */ private Sparkline sparkline; - private int legNum; - - @FXML GridPane canvasBase; - @FXML Pane arrow; - @FXML SplitPane race; - @FXML StackPane arrowPane; - @FXML Label timer; - @FXML Label FPS; - @FXML Label timeZone; - @FXML CheckBox showFPS; - @FXML TableView boatInfoTable; - @FXML TableColumn boatPlacingColumn; - @FXML TableColumn boatTeamColumn; - @FXML TableColumn boatMarkColumn; - @FXML TableColumn boatSpeedColumn; - @FXML LineChart sparklineChart; - @FXML AnchorPane annotationPane; + + @FXML private GridPane canvasBase; + @FXML private Pane arrow; + @FXML private SplitPane race; + @FXML private StackPane arrowPane; + @FXML private Label timer; + @FXML private Label FPS; + @FXML private Label timeZone; + @FXML private CheckBox showFPS; + @FXML private TableView boatInfoTable; + @FXML private TableColumn boatPlacingColumn; + @FXML private TableColumn boatTeamColumn; + @FXML private TableColumn boatMarkColumn; + @FXML private TableColumn boatSpeedColumn; + @FXML private LineChart sparklineChart; + @FXML private AnchorPane annotationPane; + + + /** + * Ctor. + */ + public RaceController() { + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + initialiseFpsToggle(); + } + + + /** + * Initialises a listener for the fps toggle. + */ + private void initialiseFpsToggle() { + + showFPS.selectedProperty().addListener((ov, old_val, new_val) -> { + if (showFPS.isSelected()) { + FPS.setVisible(true); + + } else { + FPS.setVisible(false); + + } + }); + + } /** * Updates the ResizableRaceCanvas (raceMap) with most recent data @@ -79,17 +118,7 @@ public class RaceController extends Controller { } - @Override - public void initialize(URL location, ResourceBundle resources) { - //listener for fps - showFPS.selectedProperty().addListener((ov, old_val, new_val) -> { - if (showFPS.isSelected()) { - FPS.setVisible(true); - } else { - FPS.setVisible(false); - } - }); - } + /** * Creates and sets initial display for Sparkline for race positions. @@ -107,14 +136,16 @@ public class RaceController extends Controller { sparkline.updateSparkline(boatsInRace); } + /** - * Initializes and runs the race, based on the user's chosen scale factor - * Currently uses an example racecourse - * - * @param visualiserInput input from network - * @param raceClock The RaceClock to use for the race's countdown/elapsed duration + timezone. + * Displays a specified race. + * @param visualiserInput Object used to read packets from server. + * @param visualiserRace Object modelling the race. */ - public void startRace(VisualiserInput visualiserInput, RaceClock raceClock) { + public void startRace(VisualiserInput visualiserInput, VisualiserRace visualiserRace) { + + this.visualiserInput = visualiserInput; + this.visualiserRace = visualiserRace; legNum = visualiserInput.getCourse().getLegs().size()-1; @@ -154,17 +185,7 @@ public class RaceController extends Controller { raceMap.setRaceClock(raceClock); //TODO move this list of colors somewhere more sensible. - List 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(); diff --git a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java index 85288e1d..fba47a15 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java @@ -2,51 +2,72 @@ package visualiser.Controllers; import javafx.animation.AnimationTimer; import javafx.application.Platform; -import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; -import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; +import javafx.scene.paint.Color; +import network.Messages.Enums.RaceStatusEnum; +import network.Messages.LatestMessages; +import shared.dataInput.*; +import shared.exceptions.InvalidBoatDataException; +import shared.exceptions.InvalidRaceDataException; +import shared.exceptions.InvalidRegattaDataException; +import shared.exceptions.XMLReaderException; import visualiser.app.VisualiserInput; -import visualiser.model.RaceClock; import visualiser.model.VisualiserBoat; import visualiser.model.VisualiserRace; import java.io.IOException; import java.net.Socket; import java.net.URL; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Observable; -import java.util.Observer; -import java.util.ResourceBundle; +import java.util.*; /** - * Controller to for waiting for the race to start + * Controller to for waiting for the race to start. */ public class StartController extends Controller implements Observer { @FXML private GridPane start; @FXML private AnchorPane startWrapper; + + /** + * The name of the race/regatta. + */ @FXML private Label raceTitleLabel; + + /** + * The time the race starts at. + */ @FXML private Label raceStartLabel; + /** + * The current time at the race location. + */ + @FXML private Label timeZoneTime; + + /** + * Time until the race starts. + */ + @FXML private Label timer; + @FXML private TableView boatNameTable; @FXML private TableColumn boatNameColumn; @FXML private TableColumn boatCodeColumn; - @FXML private Label timeZoneTime; - @FXML private Label timer; - @FXML private Label raceStatusLabel; + /** + * The status of the race. + */ + @FXML private Label raceStatusLabel; - private RaceClock raceClock; - private int raceStat; + /** + * The object used to read packets from the connected server. + */ private VisualiserInput visualiserInput; /** @@ -54,147 +75,223 @@ public class StartController extends Controller implements Observer { */ private VisualiserRace visualiserRace; - ///Tracks whether the race has been started (that is, has startRaceNoScaling() be called). - private boolean hasRaceStarted = false; + /** + * An array of colors used to assign colors to each boat - passed in to the VisualiserRace constructor. + */ + List colors = new ArrayList<>(Arrays.asList( + Color.BLUEVIOLET, + Color.BLACK, + Color.RED, + Color.ORANGE, + Color.DARKOLIVEGREEN, + Color.LIMEGREEN, + Color.PURPLE, + Color.DARKGRAY, + Color.YELLOW + )); + - //Tracks whether or not a clock has been created and setup, which occurs after receiving enough information. - private boolean hasCreatedClock = false; /** - * Begins the race with a scale factor of 1 + * Ctor. */ - private void startRaceNoScaling() { - //while(visualiserInput.getRaceStatus() == null);//TODO probably remove this. - - countdownTimer(); + public StartController() { } @Override - public void initialize(URL location, ResourceBundle resources){ - this.visualiserRace = new VisualiserRace(); - raceData.addObserver(this); + public void initialize(URL location, ResourceBundle resources) { + } + + + /** + * Starts the race. + * Called once we have received all XML files from the server. + * @param latestMessages The set of latest race messages to use for race. + * @throws XMLReaderException Thrown if XML file cannot be parsed. + * @throws InvalidRaceDataException Thrown if XML file cannot be parsed. + * @throws InvalidBoatDataException Thrown if XML file cannot be parsed. + * @throws InvalidRegattaDataException Thrown if XML file cannot be parsed. + */ + private void startRace(LatestMessages latestMessages) throws XMLReaderException, InvalidRaceDataException, InvalidBoatDataException, InvalidRegattaDataException { + + //Create data sources from latest messages for the race. + RaceDataSource raceDataSource = new RaceXMLReader(latestMessages.getRaceXMLMessage().getXmlMessage()); + BoatDataSource boatDataSource = new BoatXMLReader(latestMessages.getBoatXMLMessage().getXmlMessage()); + RegattaDataSource regattaDataSource = new RegattaXMLReader(latestMessages.getRegattaXMLMessage().getXmlMessage()); + + //Create race. + this.visualiserRace = new VisualiserRace(boatDataSource, raceDataSource, regattaDataSource, latestMessages, this.colors); + + + //Initialise the boat table. + initialiseBoatTable(this.visualiserRace); + + //Initialise the race name. + initialiseRaceName(this.visualiserRace); + + //Initialises the race clock. + initialiseRaceClock(this.visualiserRace); + + //Starts the race countdown timer. + countdownTimer(); } + + + public AnchorPane startWrapper(){ return startWrapper; } + /** - * Initiliases the tables that are to be shown on the pane + * Initialises the boat table that is to be shown on the pane. + * @param visualiserRace The race to get data from. */ - private void initialiseTables() { - List boats = raceData.getBoats(); - ObservableList observableBoats = FXCollections.observableArrayList(boats); + private void initialiseBoatTable(VisualiserRace visualiserRace) { - boatNameTable.setItems(observableBoats); - boatNameColumn.setCellValueFactory(cellData -> cellData.getValue().getName()); - boatCodeColumn.setCellValueFactory(new PropertyValueFactory<>("abbrev")); + //Get the boats. + ObservableList boats = visualiserRace.getBoats(); + + //Populate table. + boatNameTable.setItems(boats); + boatNameColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty()); + boatCodeColumn.setCellValueFactory(cellData -> cellData.getValue().countryProperty()); } /** - * Countdown timer until race starts. + * Initialises the race name which is shown on the pane. + * @param visualiserRace The race to get data from. */ - private void countdownTimer() { - new AnimationTimer() { - @Override - public void handle(long arg0) { - raceStat = visualiserInput.getRaceStatus().getRaceStatus(); - raceStatusLabel.setText("Race Status: " + visualiserInput.getRaceStatus().getRaceStatus()); - if (raceStat==2 || raceStat == 3) { - stop(); + private void initialiseRaceName(VisualiserRace visualiserRace) { - startWrapper.setVisible(false); - start.setVisible(false); + raceTitleLabel.setText(visualiserRace.getRegattaName()); - parent.beginRace(visualiserInput, raceClock); - } - } - }.start(); } /** - * Sets the clock that displays the time of at the current race venue. + * Initialises the race clock/timer labels for the start time, current time, and remaining time. + * @param visualiserRace The race to get data from. */ - private void setRaceClock() { - raceClock = new RaceClock(raceData.getZonedDateTime()); + private void initialiseRaceClock(VisualiserRace visualiserRace) { - raceClock.timeStringProperty().addListener((observable, oldValue, newValue) -> { + //Start time. + initialiseRaceClockStartTime(visualiserRace); + + //Current time. + initialiseRaceClockCurrentTime(visualiserRace); + + //Remaining time. + initialiseRaceClockDuration(visualiserRace); + + } + + + /** + * Initialises the race current time label. + * @param visualiserRace The race to get data from. + */ + private void initialiseRaceClockStartTime(VisualiserRace visualiserRace) { + + visualiserRace.getRaceClock().startingTimeProperty().addListener((observable, oldValue, newValue) -> { + Platform.runLater(() -> { + raceStartLabel.setText(newValue); + }); + }); + + } + + + /** + * Initialises the race current time label. + * @param visualiserRace The race to get data from. + */ + private void initialiseRaceClockCurrentTime(VisualiserRace visualiserRace) { + + visualiserRace.getRaceClock().currentTimeProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> { timeZoneTime.setText(newValue); }); }); -//TEMP REMOVE - //timeZoneTime.textProperty().bind(raceClock.timeStringProperty()); - raceClock.run(); + } /** - * Sets the time that the race is going to start. + * Initialises the race duration label. + * @param visualiserRace The race to get data from. */ - private void setStartingTime() { - String dateFormat = "'Starting time:' HH:mm dd/MM/YYYY"; - Platform.runLater(()-> { - long utcTime = visualiserInput.getRaceStatus().getExpectedStartTime(); - raceClock.setStartingTime(raceClock.getLocalTime(utcTime)); - raceStartLabel.setText(DateTimeFormatter.ofPattern(dateFormat).format(raceClock.getStartingTime())); + private void initialiseRaceClockDuration(VisualiserRace visualiserRace) { - raceClock.durationProperty().addListener((observable, oldValue, newValue) -> { - Platform.runLater(() -> { - timer.setText(newValue); - }); + visualiserRace.getRaceClock().durationProperty().addListener((observable, oldValue, newValue) -> { + Platform.runLater(() -> { + timer.setText(newValue); }); }); + } /** - * set the current time, may be used to update the time on the clock. + * Countdown timer until race starts. */ - private void setCurrentTime() { - Platform.runLater(()-> - raceClock.setUTCTime(visualiserInput.getRaceStatus().getCurrentTime()) - ); + private void countdownTimer() { + new AnimationTimer() { + @Override + public void handle(long arg0) { + + //Get the current race status. + RaceStatusEnum raceStatus = visualiserRace.getRaceStatusEnum(); + + //Display it. + raceStatusLabel.setText("Race Status: " + raceStatus.name()); + + //If the race has reached the preparatory phase, or has started... + if (raceStatus == RaceStatusEnum.PREPARATORY || raceStatus == RaceStatusEnum.STARTED) { + //Stop this timer. + stop(); + + //Hide this, and display the race controller. + startWrapper.setVisible(false); + start.setVisible(false); + + parent.beginRace(visualiserInput, visualiserRace); + } + } + }.start(); } + + + /** + * Function to handle changes in objects we observe. + * We observe LatestMessages. + * @param o The observed object. + * @param arg The {@link Observable#notifyObservers(Object)} parameter. + */ @Override public void update(Observable o, Object arg) { - if(o instanceof StreamedCourse) { - StreamedCourse streamedCourse = (StreamedCourse) o; - if(streamedCourse.hasReadBoats()) { - initialiseTables(); - } - if(streamedCourse.hasReadRegatta()) { - Platform.runLater(() -> raceTitleLabel.setText(streamedCourse.getRegattaName())); - } - if (streamedCourse.hasReadCourse()) { - Platform.runLater(() -> { - if (!this.hasCreatedClock) { - this.hasCreatedClock = true; - setRaceClock(); - if (visualiserInput.getRaceStatus() == null) { - return;//TEMP BUG FIX if the race isn't sending race status messages (our mock currently doesn't), then it would block the javafx thread with the previous while loop. - }// TODO - replace with observer on VisualiserInput - setStartingTime(); - setCurrentTime(); - } - }); - } - //TODO this is a somewhat temporary fix for when not all of the race data (boats, course, regatta) is received in time. - //Previously, startRaceNoScaling was called in the enterLobby function after the visualiserInput was started, but when connecting to the official data source it sometimes didn't send all of the race data, causing startRaceNoScaling to start, even though we didn't have enough information to start it. - if (streamedCourse.hasReadBoats() && streamedCourse.hasReadCourse() && streamedCourse.hasReadRegatta()) { + //Check that we actually have LatestMessages. + if (o instanceof LatestMessages) { + LatestMessages latestMessages = (LatestMessages) o; + + //If we've received all of the xml files, start the race. Only start it if it hasn't already been created. + if (latestMessages.hasAllXMLMessages() && this.visualiserRace == null) { + + //Need to handle it in the javafx thread. Platform.runLater(() -> { - if (!this.hasRaceStarted) { - if(visualiserInput.getRaceStatus() == null) { - } - else { - this.hasRaceStarted = true; - startRaceNoScaling(); - } + try { + this.startRace(latestMessages); + + } catch (XMLReaderException | InvalidBoatDataException | InvalidRaceDataException | InvalidRegattaDataException e) { + //We currently don't handle this in meaningful way, as it should never occur. + //If we reach this point it means that malformed XML files were sent. + e.printStackTrace(); + } }); } - } + } /** @@ -204,8 +301,12 @@ public class StartController extends Controller implements Observer { public void enterLobby(Socket socket) { startWrapper.setVisible(true); try { - visualiserInput = new VisualiserInput(socket, raceData); - new Thread(visualiserInput).start(); + //Begin reading packets from the socket/server. + this.visualiserInput = new VisualiserInput(socket); + //Store a reference to latestMessages so that we can observe it. + LatestMessages latestMessages = this.visualiserInput.getLatestMessages(); + latestMessages.addObserver(this); + new Thread(this.visualiserInput).start(); } catch (IOException e) { e.printStackTrace(); diff --git a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java b/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java index 9c629a37..d3641cf6 100644 --- a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java +++ b/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java @@ -78,8 +78,13 @@ public class VisualiserInput implements Runnable { } - - + /** + * Returns the LatestMessages object, which can be queried for any received race related messages. + * @return The LatestMessages object. + */ + public LatestMessages getLatestMessages() { + return latestMessages; + } /** * Calculates the time since last heartbeat, in milliseconds. diff --git a/racevisionGame/src/main/java/visualiser/model/RaceClock.java b/racevisionGame/src/main/java/visualiser/model/RaceClock.java deleted file mode 100644 index b076e4bf..00000000 --- a/racevisionGame/src/main/java/visualiser/model/RaceClock.java +++ /dev/null @@ -1,133 +0,0 @@ -package visualiser.model; - -import com.github.bfsmith.geotimezone.TimeZoneLookup; -import com.github.bfsmith.geotimezone.TimeZoneResult; -import javafx.animation.AnimationTimer; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; -import shared.model.GPSCoordinate; - -import java.time.Duration; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.temporal.ChronoUnit; -import java.util.Date; - -/** - * This class is used to implement a clock which keeps track of and - * displays times relevant to a race. This is displayed on the - * {@link ResizableRaceCanvas} via the - * {@link visualiser.Controllers.RaceController} and the - * {@link visualiser.Controllers.StartController}. - */ -public class RaceClock implements Runnable { - private long lastTime; - private final ZoneId zoneId; - private ZonedDateTime time; - private ZonedDateTime startingTime; - private final StringProperty timeString; - private StringProperty duration; - - public RaceClock(ZonedDateTime zonedDateTime) { - this.zoneId = zonedDateTime.getZone(); - this.timeString = new SimpleStringProperty(); - this.duration = new SimpleStringProperty(); - this.time = zonedDateTime; - setTime(time); - } - - public static ZonedDateTime getCurrentZonedDateTime(GPSCoordinate gpsCoordinate) { - TimeZoneLookup timeZoneLookup = new TimeZoneLookup(); - TimeZoneResult timeZoneResult = timeZoneLookup.getTimeZone(gpsCoordinate.getLatitude(), gpsCoordinate.getLongitude()); - ZoneId zone = ZoneId.of(timeZoneResult.getResult()); - return LocalDateTime.now(zone).atZone(zone); - } - - public void run() { - new AnimationTimer() { - @Override - public void handle(long now) { - updateTime(); - } - }.start(); - } - - /** - * Sets time to arbitrary zoned time. - * - * @param time arbitrary time with timezone. - */ - private void setTime(ZonedDateTime time) { - this.time = time; - this.timeString.set(DateTimeFormatter.ofPattern("HH:mm:ss dd/MM/YYYY Z").format(time)); - this.lastTime = System.currentTimeMillis(); - - - if(startingTime != null) { - long seconds = Duration.between(startingTime.toLocalDateTime(), time.toLocalDateTime()).getSeconds(); - if(seconds < 0) - duration.set(String.format("Starting in: %02d:%02d:%02d", -seconds/3600, -(seconds%3600)/60, -seconds%60)); - else - duration.set(String.format("Time: %02d:%02d:%02d", seconds/3600, (seconds%3600)/60, seconds%60)); - } - - } - - /** - * Sets time to given UTC time in seconds from Unix epoch, preserving timezone. - * @param time UTC time - */ - public void setUTCTime(long time) { - Date utcTime = new Date(time); - setTime(utcTime.toInstant().atZone(this.zoneId)); - } - - public ZonedDateTime getStartingTime() { - return startingTime; - } - - public void setStartingTime(ZonedDateTime startingTime) { - this.startingTime = startingTime; - } - - /** - * Get ZonedDateTime corresponding to local time zone and given UTC time. - * @param time time in mills - * @return local date time - */ - public ZonedDateTime getLocalTime(long time) { - Date utcTime = new Date(time); - return utcTime.toInstant().atZone(this.zoneId); - } - - /** - * Updates time by duration elapsed since last update. - */ - private void updateTime() { - this.time = this.time.plus(Duration.of(System.currentTimeMillis() - this.lastTime, ChronoUnit.MILLIS)); - this.lastTime = System.currentTimeMillis(); - setTime(time); - } - - public String getDuration() { - return duration.get(); - } - - public StringProperty durationProperty() { - return duration; - } - - public String getTimeZone() { - return zoneId.toString(); - } - - public ZonedDateTime getTime() { - return time; - } - - public StringProperty timeStringProperty() { - return timeString; - } -} diff --git a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java index f1b1b42b..dc965004 100644 --- a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java @@ -9,6 +9,7 @@ import javafx.scene.transform.Rotate; import seng302.Mock.StreamedCourse; import seng302.RaceDataSource; import seng302.RaceMap; +import shared.model.RaceClock; import java.time.Duration; import java.time.ZonedDateTime; diff --git a/racevisionGame/src/main/java/visualiser/model/Sparkline.java b/racevisionGame/src/main/java/visualiser/model/Sparkline.java index a7a74043..7a7f902a 100644 --- a/racevisionGame/src/main/java/visualiser/model/Sparkline.java +++ b/racevisionGame/src/main/java/visualiser/model/Sparkline.java @@ -22,7 +22,7 @@ import java.util.Map; */ public class Sparkline { private ArrayList colours; - private ArrayList startBoats = new ArrayList<>(); + private ArrayList startBoats = new ArrayList<>(); private Map boatColours = new HashMap<>(); private Integer legNum; private Integer sparkLineNumber = 0; diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java index 7644a93b..7ba2ef19 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java @@ -15,9 +15,7 @@ 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; @@ -41,10 +39,6 @@ public class VisualiserRace extends Race { */ private ObservableList boatMarkers; - //TODO remove these controller references once refactored - private RaceController controller; - protected FinishController finishController; - /** @@ -52,10 +46,10 @@ public class VisualiserRace extends Race { * @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 colors A collection of colors used to assign a color to each boat. * @param latestMessages The LatestMessages to send events to. + * @param colors A collection of colors used to assign a color to each boat. */ - public VisualiserRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, List colors, LatestMessages latestMessages, RaceController controller) { + public VisualiserRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, List colors) { super(boatDataSource, raceDataSource, regattaDataSource, latestMessages); @@ -64,9 +58,6 @@ public class VisualiserRace extends Race { this.boatMarkers = FXCollections.observableArrayList(boatDataSource.getMarkerBoats().values()); - - this.controller = controller; - } @@ -148,7 +139,7 @@ public class VisualiserRace extends Race { for (VisualiserBoat boat : boats) { boat.setCurrentLeg(startingLeg); - boat.setTimeSinceLastMark(controller.getRaceClock().getTime()); + boat.setTimeSinceLastMark(this.raceClock.getCurrentTime()); } @@ -209,7 +200,7 @@ public class VisualiserRace extends Race { if (legNumber >= 1 && legNumber < legs.size()) { if (boat.getCurrentLeg() != legs.get(legNumber)) { boat.setCurrentLeg(legs.get(legNumber)); - boat.setTimeSinceLastMark(controller.getRaceClock().getTime()); + boat.setTimeSinceLastMark(this.raceClock.getCurrentTime()); } } @@ -279,54 +270,41 @@ public class VisualiserRace extends Race { //Wind speed. this.windSpeed = raceStatus.getWindSpeedKnots(); + //Current race time. + this.raceClock.setUTCTime(raceStatus.getCurrentTime()); + } - 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. + * Starts the race. + * This updates the race based on {@link #latestMessages}. */ private void startRaceStream() { - System.setProperty("javafx.animation.fullspeed", "true"); - - - new AnimationTimer() { - final long timeRaceStarted = System.currentTimeMillis(); //start time of loop + //final long timeRaceStarted = System.currentTimeMillis(); //start time of loop int fps = 0; //init fps value - long timeCurrent = System.currentTimeMillis(); //current time + //long timeCurrent = System.currentTimeMillis(); //current time @Override public void handle(long arg0) { - totalTimeElapsed = System.currentTimeMillis() - timeRaceStarted; + //totalTimeElapsed = System.currentTimeMillis() - timeRaceStarted; @@ -343,22 +321,11 @@ public class VisualiserRace extends Race { updateRaceStatus(latestMessages.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(); } @@ -400,12 +367,7 @@ public class VisualiserRace extends Race { } - /** - * Update call for the controller. - */ - private void setControllerListeners() { - if (controller != null) controller.setInfoTable(this); - } + /** * Returns the boats participating in the race. diff --git a/racevisionGame/src/main/resources/visualiser/images/arrow.png b/racevisionGame/src/main/resources/visualiser/images/arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..fab6e21d62803e12fb373e877e8f14edd8e9caae GIT binary patch literal 16120 zcmeHOdt8j^+kc*?Nzas~sYY^`%yf_z6LP4j<*8{>qU^{Ll6E8PN;-_%tvoRdIW&kN zDN)-F)@j+ad)?RRzV7R3 zX4A~Dsn(W*EeV3K4hara5kx`+@#Du&m@r}D#EIVC-abA)a=F~k&u{YN z$qd8z`}+q31Sk}Wz`#JIQW+E!6dW8J5)v|P+O*Kn(CO2shlhvHoH=vWtXUBe5p(9u znLBrGWMt&LdGqGapC1(!6%!K^8ymZ1$&$FZxaG^2$H&JfBqXd_wJIqoDLFYgB_(C` z>eZ>Ksb78dReE}QMn*_kWR;%5(apR^rUAuSh*6a0q_UzfacW+)^UVeW5{{8z43JMAf z3lAJPP*hZO=+L3!;^LB$lG4&rgTZk0=+P4=PMkb>^3+9Q00$*2=O)iuxg>dYOyXi9A!b>8 zvMhdavShq$jBKpec(eIm5rjM{B+x%1bz#dNUzDwQHhaJ#JJH%{!j0^Lb4u!?`)q>k zCo^^Il_5dOEIQk6rvG|I6j))Cv#WSk#OxZsxVDqG^WW`Hntc80;q$*~Z(Q;H>WDh5 z<^AG9GZqKbrHur+D3B$_I5UK!t%TS@3yB~eO}J1z*0%5M|5I*R_{^)YtHWlEY2C1{ zDw{Q3I}5K(q3$c$OZtxQ+SYXI&sTR|>@ItGoGs%EQ_H4wyf{AX%X8Ip-gOK!S+JgV zlJ>B|u9?)I11A4H?4{z;69{SBrhocn$H=#1AC9JY_g~jV7aaKg@2l@;WQv56_8+Dc z=B9pU^F)g@i~hT9hj8V~$!qMWXjkT;Rp#Wk?YwS}VOjg^CGBb5V;kFDFYd6pd>)+D z$6W^(JiYyD?AZwJs*49~kOfbUp@KvWxDmf9YkZzg~|S7UR%F)KI=kOY<<`ApG#KhD=%2a2eS#$-Okc>=Wcm) zzc((-+3y~N^3MIzIeFjlb$4wg_kOdmCq4h3@K%0SOjbl?#8D3`FN}<)S3}aUCjV78 z(LFwzKMB8;hCk_;@}KT&`ik?kuyt9j^vn#S=#49Lw&W9S{lpek&nC8Q5UcK<4qY{s z(hM2AI(S8$tkRkp>$E1z_6mB6SU3O1SuJ7CUJ&~h|FBH|=wBYKFZ-gGd_e)x*raal zB|l;>S#a+@N1j4yHs3wpOD+pz6INJo{yM`K%35?B`FOHom$<%{e9my@?5TC~UhXp}oN zarO!&3$Ca*dk4YZxraG>6>P$Q&0M5rgZ#5{j+}=BjLX9~H^$pg$BBo4{h5Xn52G}*5Z8YQbvZ2zdu+#ve*{y@x!TU(L^K8-2%y@6F<~*|r?0*Ju;!c#N z?ra3dzJX{QCHr8il=0sDA=Z&#f0W3H+fkaNv)oaTzm8}W$v&7WVZ7Iea;AEK{oz4Q zydR~pKl>%e&Jr;h1G#GbhcEnXbz^UU=N1w%X?hNR%ol#T`UMB?Ct_CVx%{q#wE7(f z=Myn2CUFIYdfu(x%)xm?%;HJUdh=@+U+AFT$H9Aun3ysSUJsI#LmaFpVj|19R9_60 zDc^JOE+S_3B(DBY&JHO?4$dWFW=!G^f(cNWT+P8dh?tNv4t4`cavcY6Cu01|xJ);P z%A`LzIERSwS^dFyTDUvuIR|eeVkS)Dth_}F9g;dZcnc9TT3)S3ClsvVQL_E`dW!s{P z;0yC$fKKg!V-qLT!s$kllF~SfRi0=c&SXH2z+h1;Q|1swLE3u7QvJaowid?wgU*>5 zaYOpgh=!7-wvt>J%9 zAwVed$8vx$Y-k{x&|j>wfTFQbR4Yvx0Wg3~-~j}uEtUfu0}#EHz;b{jkM69K&PH_K z@rCMA#o+*>$Y?KV4%0@gS9_@4ncopByB1aR4EBVVST&)c8(H8jbiyH4JE1Q`s*ON z8=}+M_PO$62kic;oy0F!U?_oT5s$V(hh7Fz^l%r5=0h}uZD+t*8kKDU(QN{QJwo&+ zEp&pV{H6LwtFY>jqDgc*^u-uT(-Sy55TN6(d#aOLx2XKkS^blP_iq4KAlvRJFAfVd zigKh(DlJx(qE56ziQig*VJCv@0$SL02egQy4T>-sv;?Km*GW?XKgDgNRCo6V> z)*Q6B7#Na+|ImOI4h@tS$HS&@S0;6^STzYom4bGwz%T?>)qxfc{SI*o0d0|@X$fdW zpoL++pryzP`=`1e#h}e18h-^X67)W@E(o+CM$t;p+JhExnq(_EhilqCIAI?xypQm& zz;3O1^l|L=3TVHgG`Hb|FF-q*tauLENr)5fB`KgqhAxB?27@--C^{}>e*Z>ciMMgu z?-Y34$F^rsni=xq)Xi#-iG1M?Pj!#1P&Mcv{#sE{RJJvxnNugN#K`et`G0?TQ(EZ@ z`*vX?2Nl_?Vf7KD5mvQf*&Lzd+*K_eG@eC@>^Z0qJtXu3kA*O{ z4M}MuR-Ma(3qsj;CwcMjIEUUqV4uKaB8>fmN4G)8Z7egw1tD6|aX4l*5*Rit32-b?i zksTO&)3*ye3ea6JxOPxtmEStG#|4IMPlKBrT2$$nE6{#Zhr^9Z_JAV$2@+{OkCqoZ z*2>oRzSsK#uJCFo5lYHR71`)=W$sLTxK^ZSD2Wee@9h_OAa;N3C(3=WZ&q!#LuB0O<~e-CVskBWOE>@ z=?bqZ16D~vswc`vW7iwihP!r>)g_8-^oNu=l<6CgdV_QtdoNeu;RMndd>szyN(D&W z$;42QqAQfSGOh51O=(oqHeQt&q>UimPL$)|Xl9J{)_h&(4QZteU=C3(1{i3pZ=rQ} z0fqtGMwAZ$7-X#fgVyZ>xDDVIqI@90X~z2VwC-zw#{h07$_D_P4(n*0FThrSn}~8- zv1%w3ZKZXcwbDvkfE$T&WXR9}V|@k`0Yp>(V*^oc1rUm&pa@_VKn+pO2iRjA6ag#( zxQ-|{2M9%@p$K4)MOj3-8Ne`Oy_nVwsg)|T)P^BK$!&xS6>{61ks(}e4JER0c0qx_ zW7_G^r~;x~E>_hq)&D3b6IaQLN8FU9tfEW}$-$qLL^U<=s!E>e4u%@*&+v7jbyDSC zwIS4AG6!Lmx2P~k$`C87T3JdAW%@BW_#_Wn+?ZpbT2Uy#D9UsSAkI!bz-d}hHNe@F z=^#L~`hjkY44@T4`3YshMd+jm%Uu~6s$nz05XzJda3~bHGRF|VYJf`0v=*QQ%K=V> zB5e3$%9IEYmjb;Db1Xu|ZOd<#;F zOe8^y+8QWfY7nf_;;5!BUezoOZXsnl4N_Z>hO+s)1RgSlu|ATo+lk^@zf>>xAQQvn#bgk8!ay-fHG-7>JQRjHa`cXA;$W*w5|??qQp+c zWa31SB4lwcObtS&M-G0;tGWiCR)BO1;e(P}kf$~z*-A=ZCZ#P&K%)#(viaHILfRW> zofo*!{N(mzqBFQiY|xphQD{YFI8yw0Rm;HT1THP%V=Y$Q)2Iz~LP;FX%)7AKfik&b zgDxNqVDr}rJmQtcdLLSsg!rKK$t}o4OOOtND-j6 zg_7s+|LA44 zeIROiBo!MeFMf!wH#(F(iV}m7v-vvZGu_%UEY2k&(PUCBT2y#K^99zlVa*IGHUw*; zuqK2(iefvbRT~EIb>>*)SroP0pNjRz`=Lwq^K?WcLYImQ#B*Co^$n@{@#3iEK2+=k zdGVrkYQrsBSNT-8b~KE1VH^%AEYa`gZ6P8zV+}5cnD0V+)|5mo_n>0kz-bM81KFdf zJ9In4E?T!9%O}AzLh=-zp0%UH{U&907bz?Ya4e7GRgFsyzPu9~*+4|D2dQ(53iI9O zTB$jj-Evzh)*7UPKOA;t(TVSu?dB~gb1*zrAdb+TI$#u9Xx z>OK-mu43ny>Qem3M9g<5%vw}e?IoD+%rVuac$10a0b;6~YA>oArmp*ofy)h zvapw6zB9*Emokb>903qhoz_k=wFkt(=t?GHzB}QC<#rOxcjiWb5;76={TRa=P` z@x@e^;z%Zn0QSdnTM5Pla|h}Kz*LvgpG@3%J@&gSMOS=*lhID1 z?a2FbLc+%5)pehv%hOZxO8N<2=Dh6qeJ9~_sclI~)=ypj1;a3WS#)^aS^99ygB?$< zqO-rxbhEM^`gCU++dP$R4&NjarO$GbU>q}cCjU0bJNM$xMql!jv8`&NnfBlLX=|H< z*k)OE|DmJ8b$>X%xS#1^>_4NtJYx{PoqDJHkC>(&X}qPI`2TF9%q zf9mH;jXg5_w&shAcV_+cuG3GUc4pMCub;Yhsd4h|xT-K-Rn6qQ8T|xr6loip1KH;0 zyWiuj_wx#*nD~4dSEnXA7@$%evk6^&i?=$1NL;*RmYl`91CTFMM^|d!5_ad8gSc;Xq-vS?mYk~ zipli6o+IY#T)LP~2YXl9m|mR3-E8CWUsYz>jt0Mv)o#qT7w?ZwNMPSQX~gKAxF!0R zg=Q8Vm41G&-59~klcOe{Fy=hIa}w9Y+`^xioB4Jc{N%5PGlEwyMol_#BEas-ShBzx!)a-1RJN z=l9)SHd{ZZb%)+PIbMCt%;ujn7jvgx`Gi;XBJ-b>u@ADeZ=YYxv$(Q~7hHKgUHf1K zW%KI&%I>nXd3elG-}NXbqiyPiXJ%={rArnUp1C~e{QRQMz2~vX>~gdIi?_^-Aq_u^ zV@G_hv9t=F-6~NGa*3GB98R!a%PRbMjn6;9vD(07XVwTZr#+IMH@yA<_ZAp35}ebYqW zxYRdV_RY?H&l&n2r1U*0>U*lz_t>!S5o_Nw7g<)DtA``hAkx=~Yh%&)ripjud@U~B&wQog=_S5EI6i`%Y` zHQl+dw++}t6g7XHXr1=M#l74ww|jnxuAz$p+1l4n5(R1Bth`1PDe;SMxz?|4WgJ`U z%zWE<#Al;l_39<*GhLXc)k;(3IKQVS*Z=))QJC}f_H~1t{>CS%+qe2;``wweBzdMQ zGxzeGzec?b%(QYCiH}P^EK3@g=Dfyi^8kD{y1mY4i{G{IxG!hAF>`;)`D^@3-juu| ze|)Um_WcmkwfBpI`Yon3v%7wn60&B;@s3~n_iy0Q&)dt=3Lh#4IsA>!mOttqk$xy= zNyG_)tWb3=JbL@zZCm&~pRlfO>u71+ci~p|yUX)<^pUq`+HNFvTDEn!cIT0oKf>p( z|J^$5EmJc*x_#H0wl#~#pTMWS^Cov1!n;gwGCGbVF)N>kzbnbfY>s~Svb%cbB7BLf m?@NFGkG>0OhOgIkTNst?DFF-E1^6~95uywWJQlEM?SBEoDz&@- literal 0 HcmV?d00001 diff --git a/racevisionGame/src/main/resources/visualiser/mock/mockXML/boatXML/boatTest.xml b/racevisionGame/src/main/resources/visualiser/mock/mockXML/boatXML/boatTest.xml new file mode 100644 index 00000000..51f788b0 --- /dev/null +++ b/racevisionGame/src/main/resources/visualiser/mock/mockXML/boatXML/boatTest.xml @@ -0,0 +1,251 @@ + + + + + 2012-05-17T07:49:40+0200 + + 12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/racevisionGame/src/main/resources/visualiser/mock/mockXML/raceXML/raceTest.xml b/racevisionGame/src/main/resources/visualiser/mock/mockXML/raceXML/raceTest.xml new file mode 100644 index 00000000..05cb2201 --- /dev/null +++ b/racevisionGame/src/main/resources/visualiser/mock/mockXML/raceXML/raceTest.xml @@ -0,0 +1,91 @@ + + + + + 11080703 + + Match + + 2011-08-06T13:25:00-0000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/racevisionGame/src/main/resources/visualiser/mock/mockXML/regattaXML/regattaTest.xml b/racevisionGame/src/main/resources/visualiser/mock/mockXML/regattaXML/regattaTest.xml new file mode 100644 index 00000000..9ec88ccc --- /dev/null +++ b/racevisionGame/src/main/resources/visualiser/mock/mockXML/regattaXML/regattaTest.xml @@ -0,0 +1,20 @@ + + + + 3 + + New Zealand Test + + North Head + + -36.82791529 + + 174.81218919 + + 0.00 + + 12 + + 14.1 + + \ No newline at end of file diff --git a/racevisionGame/src/main/resources/visualiser/raceXML/Boats.xml b/racevisionGame/src/main/resources/visualiser/raceXML/Boats.xml new file mode 100644 index 00000000..d586c830 --- /dev/null +++ b/racevisionGame/src/main/resources/visualiser/raceXML/Boats.xml @@ -0,0 +1,119 @@ + + + + 2017-04-19T15:49:40+1200 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/racevisionGame/src/main/resources/visualiser/raceXML/Race.xml b/racevisionGame/src/main/resources/visualiser/raceXML/Race.xml new file mode 100644 index 00000000..88a1be01 --- /dev/null +++ b/racevisionGame/src/main/resources/visualiser/raceXML/Race.xml @@ -0,0 +1,58 @@ + + + 17041901 + Fleet + 2017-04-19T15:30:00+1200 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/racevisionGame/src/main/resources/visualiser/raceXML/Regatta.xml b/racevisionGame/src/main/resources/visualiser/raceXML/Regatta.xml new file mode 100644 index 00000000..23fde025 --- /dev/null +++ b/racevisionGame/src/main/resources/visualiser/raceXML/Regatta.xml @@ -0,0 +1,12 @@ + + + + 1 + Seng302 Mock Test + Bermuda AC35 + -32.296577 + 64.854304 + 0.00 + -4 + -14.78 + \ No newline at end of file diff --git a/racevisionGame/src/main/resources/visualiser/raceXML/bermuda_AC35.xml b/racevisionGame/src/main/resources/visualiser/raceXML/bermuda_AC35.xml new file mode 100644 index 00000000..943784ea --- /dev/null +++ b/racevisionGame/src/main/resources/visualiser/raceXML/bermuda_AC35.xml @@ -0,0 +1,269 @@ + + 5326 + + + ORACLE TEAM USA + 20 + USA + BLUEVIOLET + + + Land Rover BAR + 30 + GBR + BLACK + + + SoftBank Team Japan + 25 + JPN + RED + + + Groupama Team France + 20 + FRA + ORANGE + + + Artemis Racing + 29 + SWE + DARKOLIVEGREEN + + + Emirates Team New Zealand + 62 + NZL + LIMEGREEN + + + + + Start to Mark 1 + + + + 32.296577 + -64.854304 + + + 32.293771 + -64.855242 + + + + + + + 32.293039 + -64.843983 + + + + + + Mark 1 to Leeward Gate + + + + 32.293039 + -64.843983 + + + + + + + 32.309693 + -64.835249 + + + 32.308046 + -64.831785 + + + + + + Leeward Gate to Windward Gate + + + + 32.309693 + -64.835249 + + + 32.308046 + -64.831785 + + + + + + + 32.284680 + -64.850045 + + + 32.280164 + -64.847591 + + + + + + Windward Gate to Leeward Gate + + + + 32.284680 + -64.850045 + + + 32.280164 + -64.847591 + + + + + + + 32.309693 + -64.835249 + + + 32.308046 + -64.831785 + + + + + + Leeward Gate to Finish + + + + 32.309693 + -64.835249 + + + 32.308046 + -64.831785 + + + + + + + 32.317379 + -64.839291 + + + 32.317257 + -64.836260 + + + + + + + + + 32.313922 + -64.837168 + + + 32.317379 + -64.839291 + + + 32.317911 + -64.836996 + + + 32.317257 + -64.836260 + + + 32.304273 + -64.822834 + + + 32.279097 + -64.841545 + + + 32.279604 + -64.849871 + + + 32.289545 + -64.854162 + + + 32.290198 + -64.858711 + + + 32.297164 + -64.856394 + + + 32.296148 + -64.849184 + + + + Start Line + + 32.296577 + -64.854304 + + + 32.293771 + -64.855242 + + + + Mark + + 32.293039 + -64.843983 + + + + Windward Gate + + 32.284680 + -64.850045 + + + 32.280164 + -64.847591 + + + + Leeward Gate + + 32.309693 + -64.835249 + + + 32.308046 + -64.831785 + + + + Finish Line + + 32.317379 + -64.839291 + + + 32.317257 + -64.836260 + + + + diff --git a/racevisionGame/src/main/resources/visualiser/scenes/arrow.fxml b/racevisionGame/src/main/resources/visualiser/scenes/arrow.fxml new file mode 100644 index 00000000..6e8a88b5 --- /dev/null +++ b/racevisionGame/src/main/resources/visualiser/scenes/arrow.fxml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/racevisionGame/src/main/resources/visualiser/scenes/connect.fxml b/racevisionGame/src/main/resources/visualiser/scenes/connect.fxml new file mode 100644 index 00000000..3b0ed923 --- /dev/null +++ b/racevisionGame/src/main/resources/visualiser/scenes/connect.fxml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - -