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.
main
fjc40 9 years ago
parent 93c1199392
commit bbbb1f2eb0

@ -8,8 +8,8 @@
</parent>
<packaging>jar</packaging>
<name>sharedModel</name>
<artifactId>sharedModel</artifactId>
<name>racevisionGame</name>
<artifactId>racevisionGame</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>

@ -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<String[]> 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;
}
}

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

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

@ -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<windSpeed, windAngle>} to boatSpeed.
*/
private Map<Pair<Double, Bearing>, 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<Double, List<Bearing>> 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<Double, Bearing> 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<Double, Bearing> 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.
* <br>
* If you don't care about bearing bounds, simply pass in lower = 0, upper = 359.9.
* <br>
* Passing in lower = 0, upper = 0, or lower = 0, upper = 360 will both be treated the same as lower = 0, upper = 359.99999.
* <br><br>
* The resulting angle of the VMG will be within the interval [bearingLowerBound, bearingUpperBound].
* <br><br>
* 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}).
* <br><br>
* 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<Bearing> 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<Double, Bearing> key : this.polarValues.keySet()) {
//The key is Pair<windSpeed, windAngle>, 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<VMG> vmgs = new ArrayList<>();
//Put wind speed bounds we found above into a list.
List<Double> 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<Bearing> 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<Double, Bearing> lowerKey = new Pair<>(polarWindSpeed, lowerBound);
Pair<Double, Bearing> 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;
}
}

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

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

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

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

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

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

@ -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<CourseWind> 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<CourseWind> getLoopMessages() {
return loopMessages;
}
public byte getMessageVersionNumber() {
return messageVersionNumber;
}
public byte getByteWindID() {
return byteWindID;
}
}

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

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

@ -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<BoatStatus> 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<BoatStatus> getBoats() {
return boats;
}
}

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

@ -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<BoatStatus> 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<byte[]> messages = new ArrayList<byte[]>();
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<CourseWind> 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();
}
}

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

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

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

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

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

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

@ -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<CourseWind> courseWinds;
public CourseWinds(int msgVerNum, int selectedWindID, ArrayList<CourseWind> courseWinds){
super(MessageType.COURSEWIND);
this.msgVerNum = msgVerNum;
this.selectedWindID = selectedWindID;
this.courseWinds = courseWinds;
}
}

@ -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<Byte, BoatStatusEnum> 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;
}
}
}

@ -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<Byte, MessageType> 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;
}
}
}

@ -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<Byte, RaceStatusEnum> 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;
}
}
}

@ -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<Byte, RaceTypeEnum> 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;
}
}
}

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

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

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

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

@ -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<BoatStatus> boatStatuses;
public RaceStatus(long currentTime, int raceID, int raceStatus, long expectedStartTime, int windDirection, int windSpeed, int raceType, List<BoatStatus> 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<BoatStatus> 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);
}
}

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

@ -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<AC35Packet> 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();
}
}
}

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

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

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

@ -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<Angle> {
/**
* 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;
}
}

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

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

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

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

@ -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.
* <br>
* Divide by this factor to convert meters to nautical miles.
* <br>
* 1 nautical mile = 1852 meters.
*/
public static final int NMToMetersConversion = 1852;
/**
* Multiply by this factor to convert Knots to millimeters per second.
* <br>
* Divide by this factor to convert millimeters per second to Knots.
* <br>
* 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;
}

@ -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<GPSCoordinate> 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<GPSCoordinate> 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<GPSCoordinate> getShrinkBoundary(List<GPSCoordinate> boundary) {
double shrinkDistance = 50d;
List<GPSCoordinate> shrunkBoundary = new ArrayList<>(boundary.size());
//This is a list of edges that have been shrunk/shifted inwards.
List<Pair<GPSCoordinate, GPSCoordinate>> 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<GPSCoordinate, GPSCoordinate> edge1 = shrunkEdges.get(i);
Pair<GPSCoordinate, GPSCoordinate> 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<GPSCoordinate, GPSCoordinate> edge1 = shrunkEdges.get(shrunkEdges.size() - 1);
Pair<GPSCoordinate, GPSCoordinate> 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<GPSCoordinate> 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;
}
}

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

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

@ -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. <br>
* TrackPoints are displayed on a
* {@link seng302.Model.ResizableRaceCanvas ResizableRaceCanvas}, via the
* {@link seng302.Controllers.RaceController RaceController}. <br>
* 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;
}
}

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

@ -1,13 +0,0 @@
package seng302;
/**
* Created by f123 on 27-Apr-17.
*/
public class App {
public static void main(String[] args) {
}
}
Loading…
Cancel
Save