You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
931 lines
30 KiB
931 lines
30 KiB
package seng302.Model;
|
|
|
|
import javafx.animation.AnimationTimer;
|
|
import javafx.collections.FXCollections;
|
|
import javafx.collections.ObservableList;
|
|
import seng302.Constants;
|
|
import seng302.DataInput.RaceDataSource;
|
|
import seng302.MockOutput;
|
|
import seng302.Networking.Messages.BoatLocation;
|
|
import seng302.Networking.Messages.BoatStatus;
|
|
import seng302.Networking.Messages.Enums.BoatStatusEnum;
|
|
import seng302.Networking.Messages.Enums.RaceStatusEnum;
|
|
import seng302.Networking.Messages.Enums.RaceTypeEnum;
|
|
import seng302.Networking.Messages.RaceStatus;
|
|
import seng302.Networking.Utils.AC35UnitConverter;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Random;
|
|
|
|
import static java.lang.Math.cos;
|
|
|
|
|
|
/**
|
|
* Represents a yacht race.
|
|
* Has a course, boats, boundaries, etc...
|
|
* Is responsible for simulating the race, and sending messages to a MockOutput instance.
|
|
*/
|
|
public class Race implements Runnable {
|
|
|
|
/**
|
|
* An observable list of boats in the race.
|
|
*/
|
|
private ObservableList<Boat> boats;
|
|
|
|
/**
|
|
* An observable list of compound marks in the race.
|
|
*/
|
|
private ObservableList<CompoundMark> compoundMarks;
|
|
|
|
/**
|
|
* A list of legs in the race.
|
|
*/
|
|
private List<Leg> legs;
|
|
|
|
/**
|
|
* A list of coordinates describing the boundary of the course.
|
|
*/
|
|
private List<GPSCoordinate> boundary;
|
|
|
|
/**
|
|
* A copy of the boundary list, except "shrunk" inwards by 50m.
|
|
*/
|
|
private List<GPSCoordinate> shrinkBoundary;
|
|
|
|
/**
|
|
* The elapsed time, in milliseconds, of the race.
|
|
*/
|
|
private long totalTimeElapsed;
|
|
|
|
/**
|
|
* The starting timestamp, in milliseconds, of the race.
|
|
*/
|
|
private long startTime;
|
|
|
|
/**
|
|
* The scale factor of the race.
|
|
* Frame periods are multiplied by this to get the amount of time a single frame represents.
|
|
* E.g., frame period = 20ms, scale = 5, frame represents 20 * 5 = 100ms, and so boats are simulated for 100ms, even though only 20ms actually occurred.
|
|
*/
|
|
private int scaleFactor = 15;
|
|
|
|
/**
|
|
* The race ID of the course.
|
|
*/
|
|
private int raceId;
|
|
|
|
/**
|
|
* The current status of the race.
|
|
*/
|
|
private RaceStatusEnum raceStatusEnum;
|
|
|
|
/**
|
|
* The type of race this is.
|
|
*/
|
|
private RaceTypeEnum raceType;
|
|
|
|
/**
|
|
* The percent chance that a boat fails the race, and enters a DNF state, at each checkpoint.
|
|
* 0 = 0%, 100 = 100%.
|
|
*/
|
|
private int dnfChance = 0;
|
|
|
|
|
|
/**
|
|
* The mockOutput to send messages to.
|
|
*/
|
|
private MockOutput mockOutput;
|
|
|
|
|
|
/**
|
|
* Wind direction bearing.
|
|
*/
|
|
private Bearing windDirection;
|
|
|
|
/**
|
|
* Wind speed (knots).
|
|
* Convert this to millimeters per second before passing to RaceStatus.
|
|
*/
|
|
private double windSpeed;
|
|
|
|
private double windDirDegrees;
|
|
private double windDir;
|
|
private int changeWind = 4;
|
|
private static final int windUpperBound = 235;
|
|
private static final int windLowerBound = 215;
|
|
|
|
|
|
|
|
/**
|
|
* Constructs a race object with a given RaceDataSource and sends events to the given mockOutput.
|
|
* @param raceData Data source for race related data (boats, legs, etc...).
|
|
* @param mockOutput The mockOutput to send events to.
|
|
*/
|
|
public Race(RaceDataSource raceData, MockOutput mockOutput) {
|
|
|
|
this.mockOutput = mockOutput;
|
|
|
|
this.boats = FXCollections.observableArrayList(raceData.getBoats());
|
|
this.compoundMarks = FXCollections.observableArrayList(raceData.getCompoundMarks());
|
|
this.boundary = raceData.getBoundary();
|
|
this.shrinkBoundary = GPSCoordinate.getShrinkBoundary(this.boundary);
|
|
|
|
|
|
this.legs = raceData.getLegs();
|
|
this.legs.add(new Leg("Finish", this.legs.size()));
|
|
|
|
this.raceId = raceData.getRaceId();
|
|
|
|
//The start time is current time + 4 minutes, scaled. prestart is 3 minutes, and we add another.
|
|
this.startTime = System.currentTimeMillis() + ((Constants.RacePreStartTime + (1 * 60 * 1000)) / this.scaleFactor);
|
|
|
|
this.setRaceStatusEnum(RaceStatusEnum.NOT_ACTIVE);
|
|
this.raceType = RaceTypeEnum.FLEET_RACE;
|
|
|
|
this.windSpeed = 12;
|
|
this.windDirection = Bearing.fromDegrees(180);
|
|
|
|
|
|
}
|
|
|
|
/**
|
|
* Runnable for the thread.
|
|
*/
|
|
public void run() {
|
|
initialiseBoats();
|
|
initialiseWindDir();
|
|
countdownTimer.start();
|
|
}
|
|
|
|
/**
|
|
* Parse the compound marker boats through mock output.
|
|
*/
|
|
private void parseMarks() {
|
|
for (CompoundMark compoundMark : this.compoundMarks) {
|
|
|
|
//Get the individual marks from the compound mark.
|
|
Mark mark1 = compoundMark.getMark1();
|
|
Mark mark2 = compoundMark.getMark2();
|
|
|
|
//If they aren't null, parse them (some compound marks only have one mark).
|
|
if (mark1 != null) {
|
|
this.parseIndividualMark(mark1);
|
|
}
|
|
|
|
if (mark2 != null) {
|
|
this.parseIndividualMark(mark2);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses an individual marker boat, and sends it to mockOutput.
|
|
* @param mark The marker boat to parse.
|
|
*/
|
|
private void parseIndividualMark(Mark mark) {
|
|
|
|
this.mockOutput.parseBoatLocation(mark.getSourceID(), mark.getPosition().getLatitude(), mark.getPosition().getLongitude(),0,0, totalTimeElapsed+startTime);
|
|
|
|
}
|
|
|
|
/**
|
|
* Parse the boats in the race, and send it to mockOutput.
|
|
*/
|
|
private void parseBoatLocations() {
|
|
|
|
//Parse each boat.
|
|
for (Boat boat : this.boats) {
|
|
|
|
this.parseIndividualBoatLocation(boat);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Parses an individual boat, and sends it to mockOutput.
|
|
* @param boat The boat to parse.
|
|
*/
|
|
private void parseIndividualBoatLocation(Boat boat) {
|
|
|
|
this.mockOutput.parseBoatLocation(
|
|
boat.getSourceID(),
|
|
boat.getCurrentPosition().getLatitude(),
|
|
boat.getCurrentPosition().getLongitude(),
|
|
boat.getBearing().degrees(),
|
|
boat.getCurrentSpeed(),
|
|
startTime + totalTimeElapsed
|
|
);
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates the race status enumeration based on the current time, in milliseconds.
|
|
* @param currentTime The current time, in milliseconds.
|
|
*/
|
|
private void updateRaceStatusEnum(long currentTime) {
|
|
|
|
//The amount of milliseconds until the race starts.
|
|
long timeToStart = this.startTime - currentTime;
|
|
|
|
//Scale the time to start based on the scale factor.
|
|
long timeToStartScaled = timeToStart / this.scaleFactor;
|
|
|
|
|
|
if (timeToStartScaled > Constants.RacePreStartTime) {
|
|
//Time > 3 minutes is the prestart period.
|
|
this.setRaceStatusEnum(RaceStatusEnum.PRESTART);
|
|
|
|
} else if ((timeToStartScaled <= Constants.RacePreStartTime) && (timeToStartScaled >= Constants.RacePreparatoryTime)) {
|
|
//Time between [1, 3] minutes is the warning period.
|
|
this.setRaceStatusEnum(RaceStatusEnum.WARNING);
|
|
|
|
} else if ((timeToStartScaled <= Constants.RacePreparatoryTime) && (timeToStartScaled > 0)) {
|
|
//Time between (0, 1] minutes is the preparatory period.
|
|
this.setRaceStatusEnum(RaceStatusEnum.PREPARATORY);
|
|
|
|
} else {
|
|
//Otherwise, the race has started!
|
|
this.setRaceStatusEnum(RaceStatusEnum.STARTED);
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
/**
|
|
* Parses the race status, and sends it to mockOutput.
|
|
*/
|
|
private void parseRaceStatus() {
|
|
|
|
//A race status message contains a list of boat statuses.
|
|
List<BoatStatus> boatStatuses = new ArrayList<>();
|
|
|
|
//Add each boat status to the status list.
|
|
for (Boat boat : boats) {
|
|
|
|
BoatStatus boatStatus = new BoatStatus(boat.getSourceID(), boat.getStatus(), boat.getCurrentLeg().getLegNumber(), boat.getEstimatedTime());
|
|
|
|
boatStatuses.add(boatStatus);
|
|
}
|
|
|
|
//TODO REFACTOR for consistency, could send parameters to mockOutput instead of the whole racestatus. This will also fix the sequence number issue.
|
|
|
|
//Convert wind direction and speed to ints. //TODO this conversion should be done inside the racestatus class.
|
|
int windDirectionInt = AC35UnitConverter.encodeHeading(this.windDirection.degrees());
|
|
int windSpeedInt = (int) (windSpeed * Constants.KnotsToMMPerSecond);
|
|
|
|
//Create race status object, and send it.
|
|
RaceStatus raceStatus = new RaceStatus(System.currentTimeMillis(), this.raceId, this.getRaceStatusEnum().getValue(), this.startTime, windDirectionInt, windSpeedInt, this.getRaceType().getValue(), boatStatuses);
|
|
|
|
mockOutput.parseRaceStatus(raceStatus);
|
|
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the status of all boats in the race to RACING.
|
|
*/
|
|
private void setBoatsStatusToRacing() {
|
|
|
|
for (Boat boat : this.boats) {
|
|
boat.setStatus(BoatStatusEnum.RACING);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Countdown timer until race starts.
|
|
*/
|
|
protected AnimationTimer countdownTimer = new AnimationTimer() {
|
|
|
|
|
|
long currentTime = System.currentTimeMillis();
|
|
|
|
@Override
|
|
public void handle(long arg0) {
|
|
|
|
//Update the race status based on the current time.
|
|
updateRaceStatusEnum(this.currentTime);
|
|
|
|
//Parse the boat locations.
|
|
parseBoatLocations();
|
|
|
|
//Parse the marks.
|
|
parseMarks();
|
|
|
|
// Change wind direction
|
|
changeWindDir();
|
|
|
|
//Parse the race status.
|
|
parseRaceStatus();
|
|
|
|
|
|
if (getRaceStatusEnum() == RaceStatusEnum.STARTED) {
|
|
System.setProperty("javafx.animation.fullspeed", "true");
|
|
setBoatsStatusToRacing();
|
|
raceTimer.start();
|
|
this.stop();
|
|
}
|
|
|
|
//Update the animations timer's time.
|
|
currentTime = System.currentTimeMillis();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Timer that runs for the duration of the race, until all boats finish.
|
|
*/
|
|
private AnimationTimer raceTimer = new AnimationTimer() {
|
|
|
|
/**
|
|
* Start time of loop, in milliseconds.
|
|
*/
|
|
long timeRaceStarted = System.currentTimeMillis();
|
|
|
|
/**
|
|
* The time of the previous frame, in milliseconds.
|
|
*/
|
|
long lastFrameTime = timeRaceStarted;
|
|
|
|
@Override
|
|
public void handle(long arg0) {
|
|
|
|
//Get the current time.
|
|
long currentTime = System.currentTimeMillis();
|
|
|
|
//Update the total elapsed time.
|
|
totalTimeElapsed = currentTime - this.timeRaceStarted;
|
|
|
|
//As long as there is at least one boat racing, we still simulate the race.
|
|
if (getNumberOfActiveBoats() != 0) {
|
|
|
|
//Get the time period of this frame.
|
|
long framePeriod = currentTime - lastFrameTime;
|
|
//We actually simulate 20ms istead of the amount of time that has occurred, as that ensure that we don't end up with large frame periods on slow computers, causing position issues.
|
|
framePeriod = 20;
|
|
|
|
|
|
//For each boat, we update its position, and generate a BoatLocationMessage.
|
|
for (Boat boat : boats) {
|
|
|
|
//If it is still racing, update its position.
|
|
if (boat.getStatus() == BoatStatusEnum.RACING) {
|
|
|
|
updatePosition(boat, framePeriod, totalTimeElapsed);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
//Otherwise, the race is over!
|
|
raceFinished.start();
|
|
setRaceStatusEnum(RaceStatusEnum.FINISHED);
|
|
this.stop();
|
|
}
|
|
|
|
if (getNumberOfActiveBoats() != 0) {
|
|
// Change wind direction
|
|
changeWindDir();
|
|
|
|
//Parse the boat locations.
|
|
parseBoatLocations();
|
|
|
|
//Parse the marks.
|
|
parseMarks();
|
|
|
|
//Parse the race status.
|
|
parseRaceStatus();
|
|
|
|
|
|
//Update the last frame time.
|
|
this.lastFrameTime = currentTime;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Broadcast that the race has finished.
|
|
*/
|
|
protected AnimationTimer raceFinished = new AnimationTimer(){
|
|
int iters = 0;
|
|
@Override
|
|
public void handle(long now) {
|
|
RaceStatus raceStatus = new RaceStatus(System.currentTimeMillis(), raceId, 4, startTime, 0, 2300, 2, new ArrayList<>());
|
|
mockOutput.parseRaceStatus(raceStatus);
|
|
if (iters > 500){
|
|
mockOutput.stop();
|
|
stop();
|
|
}
|
|
iters++;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Initialise the boats in the race.
|
|
* This sets their starting positions and current legs.
|
|
*/
|
|
public void initialiseBoats() {
|
|
|
|
//Gets the starting positions of the boats.
|
|
List<GPSCoordinate> startingPositions = getSpreadStartingPositions();
|
|
|
|
//Get iterators for our boat and position lists.
|
|
Iterator<Boat> boatIt = this.boats.iterator();
|
|
Iterator<GPSCoordinate> startPositionIt = startingPositions.iterator();
|
|
|
|
//Iterate over the pair of lists.
|
|
while (boatIt.hasNext() && startPositionIt.hasNext()) {
|
|
|
|
//Get the next boat and position.
|
|
Boat boat = boatIt.next();
|
|
GPSCoordinate startPosition = startPositionIt.next();
|
|
|
|
|
|
//The boat starts on the first leg of the race.
|
|
boat.setCurrentLeg(this.legs.get(0));
|
|
|
|
//Boats start with 0 knots speed.
|
|
boat.setCurrentSpeed(0d);
|
|
|
|
//Place the boat at its starting position.
|
|
boat.setCurrentPosition(startPosition);
|
|
|
|
//Boats start facing their next marker.
|
|
boat.setBearing(boat.calculateBearingToNextMarker());
|
|
|
|
//Sets the boats status to prestart - it changes to racing when the race starts.
|
|
boat.setStatus(BoatStatusEnum.PRESTART);
|
|
|
|
//We set a large time since tack change so that it calculates a new VMG when the simulation starts.
|
|
boat.setTimeSinceTackChange(999999);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates a list of starting positions for the different boats, so they do not appear cramped at the start line.
|
|
*
|
|
* @return A list of starting positions.
|
|
*/
|
|
public List<GPSCoordinate> getSpreadStartingPositions() {
|
|
|
|
//The first compound marker of the race - the starting gate.
|
|
CompoundMark compoundMark = this.legs.get(0).getStartCompoundMark();
|
|
|
|
//The position of the two markers from the compound marker.
|
|
GPSCoordinate mark1Position = compoundMark.getMark1Position();
|
|
GPSCoordinate mark2Position = compoundMark.getMark2Position();
|
|
|
|
|
|
//Calculates the azimuth between the two points.
|
|
Azimuth azimuth = GPSCoordinate.calculateAzimuth(mark1Position, mark2Position);
|
|
|
|
//Calculates the distance between the two points.
|
|
double distanceMeters = GPSCoordinate.calculateDistanceMeters(mark1Position, mark2Position);
|
|
|
|
//The number of boats in the race.
|
|
int numberOfBoats = this.boats.size();
|
|
|
|
//Calculates the distance between each boat. We divide by numberOfBoats + 1 to ensure that no boat is placed on one of the starting gate's marks.
|
|
double distanceBetweenBoatsMeters = distanceMeters / (numberOfBoats + 1);
|
|
|
|
|
|
//List to store coordinates in.
|
|
List<GPSCoordinate> positions = new ArrayList<>();
|
|
|
|
//We start spacing boats out from mark 1.
|
|
GPSCoordinate position = mark1Position;
|
|
|
|
//For each boat, displace position, and store it.
|
|
for (int i = 0; i < numberOfBoats; i++) {
|
|
|
|
position = GPSCoordinate.calculateNewPosition(position, distanceBetweenBoatsMeters, azimuth);
|
|
|
|
positions.add(position);
|
|
|
|
}
|
|
|
|
return positions;
|
|
}
|
|
|
|
|
|
/**
|
|
* Calculates a boat's VMG.
|
|
* @param boat The boat to calculate VMG for.
|
|
* @param bearingBounds An array containing the lower and upper acceptable bearing bounds to keep the boat in the course.
|
|
* @return VMG for the specified boat.
|
|
*/
|
|
private VMG calculateVMG(Boat boat, Bearing[] bearingBounds) {
|
|
|
|
//Get the lower and upper acceptable bounds.
|
|
Bearing lowerAcceptableBound = bearingBounds[0];
|
|
Bearing upperAcceptableBound = bearingBounds[1];
|
|
|
|
|
|
//Find the VMG inside these bounds.
|
|
VMG bestVMG = boat.getPolars().calculateVMG(this.windDirection, this.windSpeed, boat.calculateBearingToNextMarker(), lowerAcceptableBound, upperAcceptableBound);
|
|
|
|
|
|
|
|
return bestVMG;
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Determines whether or not a given VMG improves the velocity of a boat, if it were currently using currentVMG.
|
|
* @param currentVMG The current VMG of the boat.
|
|
* @param potentialVMG The new VMG to test.
|
|
* @param bearingToDestination The bearing between the boat and its destination.
|
|
* @return True if the new VMG is improves velocity, false otherwise.
|
|
*/
|
|
private boolean improvesVelocity(VMG currentVMG, VMG potentialVMG, Bearing bearingToDestination) {
|
|
|
|
//Calculates the angle between the boat and its destination.
|
|
Angle angleBetweenDestAndHeading = Angle.fromDegrees(currentVMG.getBearing().degrees() - bearingToDestination.degrees());
|
|
|
|
//Calculates the angle between the new VMG and the boat's destination.
|
|
Angle angleBetweenDestAndNewVMG = Angle.fromDegrees(potentialVMG.getBearing().degrees() - bearingToDestination.degrees());
|
|
|
|
|
|
//Calculate the boat's current velocity.
|
|
double currentVelocity = Math.cos(angleBetweenDestAndHeading.radians()) * currentVMG.getSpeed();
|
|
|
|
//Calculate the potential velocity with the new VMG.
|
|
double vmgVelocity = Math.cos(angleBetweenDestAndNewVMG.radians()) * potentialVMG.getSpeed();
|
|
|
|
//Return whether or not the new VMG gives better velocity.
|
|
return vmgVelocity > currentVelocity;
|
|
|
|
}
|
|
|
|
/**
|
|
* Determines whether or not a given VMG improves the velocity of a boat.
|
|
* @param boat The boat to test.
|
|
* @param vmg The new VMG to test.
|
|
* @return True if the new VMG is improves velocity, false otherwise.
|
|
*/
|
|
private boolean improvesVelocity(Boat boat, VMG vmg) {
|
|
|
|
//Get the boats "current" VMG.
|
|
VMG boatVMG = new VMG(boat.getCurrentSpeed(), boat.getBearing());
|
|
|
|
//Check if the new VMG is better than the boat's current VMG.
|
|
return this.improvesVelocity(boatVMG, vmg, boat.calculateBearingToNextMarker());
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Calculates the distance a boat has travelled and updates its current position according to this value.
|
|
*
|
|
* @param boat The boat to be updated.
|
|
* @param updatePeriodMilliseconds The time, in milliseconds, since the last update.
|
|
* @param totalElapsedMilliseconds The total number of milliseconds that have elapsed since the start of the race.
|
|
*/
|
|
protected void updatePosition(Boat boat, long updatePeriodMilliseconds, long totalElapsedMilliseconds) {
|
|
|
|
//Checks if the current boat has finished the race or not.
|
|
boolean finish = this.isLastLeg(boat.getCurrentLeg());
|
|
|
|
if (!finish) {
|
|
|
|
|
|
//Calculates the distance travelled, in meters, in the current timeslice.
|
|
double distanceTravelledMeters = boat.calculateMetersTravelled(updatePeriodMilliseconds);
|
|
|
|
//Scale it.
|
|
distanceTravelledMeters = distanceTravelledMeters * this.scaleFactor;
|
|
|
|
|
|
//Move the boat forwards that many meters, and advances its time counters by enough milliseconds.
|
|
boat.moveForwards(distanceTravelledMeters, updatePeriodMilliseconds * this.scaleFactor);
|
|
|
|
|
|
//Only get a new VMG if the boat will go outside the course, or X seconds have elapsed.
|
|
boolean willStayInsideCourse = this.checkBearingInsideCourse(boat.getBearing(), boat.getCurrentPosition());
|
|
long tackPeriod = 15000;
|
|
if (!willStayInsideCourse || (boat.getTimeSinceTackChange() > tackPeriod)) {
|
|
|
|
//Calculate the boat's bearing bounds, to ensure that it doesn't go out of the course.
|
|
Bearing[] bearingBounds = this.calculateBearingBounds(boat);
|
|
|
|
|
|
//Calculate the new VMG.
|
|
VMG newVMG = this.calculateVMG(boat, bearingBounds);
|
|
|
|
|
|
//If the new vmg improves velocity, use it.
|
|
if (improvesVelocity(boat, newVMG)) {
|
|
boat.setVMG(newVMG);
|
|
|
|
} else {
|
|
//We also need to use the new VMG if our current bearing will take us out of the course.
|
|
if (!willStayInsideCourse) {
|
|
boat.setVMG(newVMG);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.updateEstimatedTime(boat);
|
|
|
|
|
|
//Check the boats position (update leg and stuff).
|
|
this.checkPosition(boat, totalTimeElapsed);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Calculates the upper and lower bounds that the boat may have in order to not go outside of the course.
|
|
* @param boat The boat to check.
|
|
* @return An array of bearings. The first is the lower bound, the second is the upper bound.
|
|
*/
|
|
private Bearing[] calculateBearingBounds(Boat boat) {
|
|
|
|
Bearing[] bearings = new Bearing[2];
|
|
|
|
Bearing lowerBearing = Bearing.fromDegrees(0.001);
|
|
Bearing upperBearing = Bearing.fromDegrees(359.999);
|
|
|
|
|
|
|
|
double lastAngle = -1;
|
|
boolean lastAngleWasGood = false;
|
|
|
|
//Check all bearings between [0, 360).
|
|
for (double angle = 0; angle < 360; angle += 1) {
|
|
|
|
//Create bearing from angle.
|
|
Bearing bearing = Bearing.fromDegrees(angle);
|
|
|
|
//Check that if it is acceptable.
|
|
boolean bearingIsGood = this.checkBearingInsideCourse(bearing, boat.getCurrentPosition());
|
|
|
|
|
|
if (lastAngle != -1) {
|
|
|
|
if (lastAngleWasGood && !bearingIsGood) {
|
|
//We have flipped over from good bearings to bad bearings. So the last good bearing is the upper bearing.
|
|
upperBearing = Bearing.fromDegrees(lastAngle);
|
|
}
|
|
|
|
if (!lastAngleWasGood && bearingIsGood) {
|
|
//We have flipped over from bad bearings to good bearings. So the current bearing is the lower bearing.
|
|
lowerBearing = Bearing.fromDegrees(angle);
|
|
}
|
|
|
|
}
|
|
|
|
lastAngle = angle;
|
|
lastAngleWasGood = bearingIsGood;
|
|
|
|
}
|
|
|
|
|
|
|
|
//TODO BUG if it can't find either upper or lower, it returns (0, 359.999). Should return (boatbearing, boatbearing+0.0001)
|
|
bearings[0] = lowerBearing;
|
|
bearings[1] = upperBearing;
|
|
|
|
return bearings;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Checks if a given bearing, starting at a given position, would put a boat out of the course boundaries.
|
|
* @param bearing The bearing to check.
|
|
* @param position The position to start from.
|
|
* @return True if the bearing would keep the boat in the course, false if it would take it out of the course.
|
|
*/
|
|
private boolean checkBearingInsideCourse(Bearing bearing, GPSCoordinate position) {
|
|
|
|
//Get azimuth from bearing.
|
|
Azimuth azimuth = Azimuth.fromBearing(bearing);
|
|
|
|
|
|
//Tests to see if a point in front of the boat is out of bounds.
|
|
double epsilonMeters = 50d;
|
|
GPSCoordinate testCoord = GPSCoordinate.calculateNewPosition(position, epsilonMeters, azimuth);
|
|
|
|
//If it isn't inside the boundary, calculate new bearing.
|
|
if (GPSCoordinate.isInsideBoundary(testCoord, this.shrinkBoundary)) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Checks if a boat has finished any legs, or has pulled out of race (DNF).
|
|
* @param boat The boat to check.
|
|
* @param timeElapsed The total time, in milliseconds, that has elapsed since the race started.
|
|
*/
|
|
protected void checkPosition(Boat boat, long timeElapsed) {
|
|
|
|
//The distance, in nautical miles, within which the boat needs to get in order to consider that it has reached the marker.
|
|
double epsilonNauticalMiles = 100.0 / Constants.NMToMetersConversion; //100 meters. TODO should be more like 5-10.
|
|
|
|
if (boat.calculateDistanceToNextMarker() < epsilonNauticalMiles) {
|
|
//Boat has reached its target marker, and has moved on to a new leg.
|
|
|
|
|
|
|
|
//Calculate how much the boat overshot the marker by.
|
|
double overshootMeters = boat.calculateDistanceToNextMarker();
|
|
|
|
|
|
//Move boat on to next leg.
|
|
Leg nextLeg = this.legs.get(boat.getCurrentLeg().getLegNumber() + 1);
|
|
boat.setCurrentLeg(nextLeg);
|
|
|
|
//Add overshoot distance into the distance travelled for the next leg.
|
|
boat.setDistanceTravelledInLeg(overshootMeters);
|
|
|
|
//Setting a high value for this allows the boat to immediately do a large turn, as it needs to in order to get to the next mark.
|
|
boat.setTimeSinceTackChange(999999);
|
|
|
|
|
|
//Check if the boat has finished or stopped racing.
|
|
|
|
if (this.isLastLeg(boat.getCurrentLeg())) {
|
|
//Boat has finished.
|
|
boat.setTimeFinished(timeElapsed);
|
|
boat.setCurrentSpeed(0);
|
|
boat.setStatus(BoatStatusEnum.FINISHED);
|
|
} else if (doNotFinish()) {
|
|
//Boat has pulled out of race.
|
|
boat.setTimeFinished(timeElapsed);
|
|
boat.setCurrentLeg(new Leg("DNF", -1));
|
|
boat.setCurrentSpeed(0);
|
|
boat.setStatus(BoatStatusEnum.DNF);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Determines whether or not a specific leg is the last leg in the race.
|
|
* @param leg The leg to check.
|
|
* @return Returns true if it is the last, false otherwse.
|
|
*/
|
|
private boolean isLastLeg(Leg leg) {
|
|
|
|
//Get the last leg.
|
|
Leg lastLeg = this.legs.get(this.legs.size() - 1);
|
|
|
|
//Check its ID.
|
|
int lastLegID = lastLeg.getLegNumber();
|
|
|
|
//Get the specified leg's ID.
|
|
int legID = leg.getLegNumber();
|
|
|
|
|
|
//Check if they are the same.
|
|
return legID == lastLegID;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the chance each boat has of failing at a gate or marker
|
|
*
|
|
* @param chance percentage chance a boat has of failing per checkpoint.
|
|
*/
|
|
protected void setDnfChance(int chance) {
|
|
if (chance >= 0 && chance <= 100) {
|
|
dnfChance = chance;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decides if a boat should received a DNF status.
|
|
* @return True means it should DNF, false means it shouldn't.
|
|
*/
|
|
protected boolean doNotFinish() {
|
|
Random rand = new Random();
|
|
return rand.nextInt(100) < dnfChance;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the current race status.
|
|
* @return The current race status.
|
|
*/
|
|
public RaceStatusEnum getRaceStatusEnum() {
|
|
return raceStatusEnum;
|
|
}
|
|
|
|
/**
|
|
* Sets the current race status.
|
|
* @param raceStatusEnum The new status of the race.
|
|
*/
|
|
private void setRaceStatusEnum(RaceStatusEnum raceStatusEnum) {
|
|
this.raceStatusEnum = raceStatusEnum;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the type of race this is.
|
|
* @return The type of race this is.
|
|
*/
|
|
public RaceTypeEnum getRaceType() {
|
|
return raceType;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the number of boats that are still active in the race.
|
|
* They become inactive by either finishing or withdrawing.
|
|
* @return The number of boats still active in the race.
|
|
*/
|
|
protected int getNumberOfActiveBoats() {
|
|
|
|
int numberofActiveBoats = 0;
|
|
|
|
for (Boat boat : this.boats) {
|
|
|
|
//If the boat is currently racing, count it.
|
|
if (boat.getStatus() == BoatStatusEnum.RACING) {
|
|
numberofActiveBoats++;
|
|
}
|
|
|
|
}
|
|
|
|
return numberofActiveBoats;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns an observable list of boats in the race.
|
|
* @return List of boats in the race.
|
|
*/
|
|
public ObservableList<Boat> getBoats() {
|
|
return boats;
|
|
}
|
|
|
|
protected void initialiseWindDir(){
|
|
windDirDegrees = 225;
|
|
windDir = AC35UnitConverter.convertHeading(windDirDegrees);
|
|
/*windDir = new Random().nextInt(65535+1);
|
|
windDir = BoatLocation.convertHeadingIntToDouble(255);*/
|
|
this.windDirection = new Bearing((int)windDir);
|
|
}
|
|
|
|
protected void changeWindDir(){
|
|
int r = new Random().nextInt(changeWind)+1;
|
|
if(r==1){
|
|
windDirDegrees = (0.5 + windDirDegrees) % 360;
|
|
} else if (r==2){
|
|
windDirDegrees = ((windDirDegrees - 0.5) + 360) % 360;///keep the degrees positive when below 0
|
|
}
|
|
if (windDirDegrees > windUpperBound){
|
|
windDirDegrees = windUpperBound;
|
|
}
|
|
if (windDirDegrees < windLowerBound){
|
|
windDirDegrees = windLowerBound;
|
|
}
|
|
|
|
windDir = AC35UnitConverter.convertHeading(windDirDegrees);
|
|
this.windDirection = new Bearing(windDirDegrees);
|
|
}
|
|
|
|
protected void setChangeWind(int changeVal){
|
|
if (changeVal>=0){
|
|
changeWind = changeVal;
|
|
}
|
|
}
|
|
|
|
protected int getWind(){
|
|
return (int)windDir;
|
|
}
|
|
|
|
/**
|
|
* Updates the boat's estimated time to next mark if positive
|
|
* @param boat to estimate time given its velocity
|
|
*/
|
|
private void updateEstimatedTime(Boat boat) {
|
|
double velocityToMark = boat.getCurrentSpeed() * cos(boat.getBearing().radians() - boat.calculateBearingToNextMarker().radians()) / Constants.KnotsToMMPerSecond;
|
|
if (velocityToMark > 0) {
|
|
long timeFromNow = (long)(1000*boat.calculateDistanceToNextMarker()/velocityToMark);
|
|
boat.setEstimatedTime(startTime + totalTimeElapsed + timeFromNow);
|
|
}
|
|
}
|
|
}
|