@ -3,7 +3,6 @@ package seng302.Model;
import javafx.animation.AnimationTimer ;
import javafx.collections.FXCollections ;
import javafx.collections.ObservableList ;
import org.geotools.referencing.GeodeticCalculator ;
import seng302.Constants ;
import seng302.DataInput.RaceDataSource ;
@ -11,39 +10,94 @@ 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 java.awt.geom.Point2D ;
import java.io.* ;
import java.util.ArrayList ;
import java.util.Iterator ;
import java.util.List ;
import java.util.Random ;
import static java.lang.Math.cos ;
import static java.lang.Math.max ;
import static java.lang.Math.min ;
/ * *
* Parent class for races
* Created by fwy13 on 3 / 03 / 17.
* 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 {
protected ObservableList < Boat > startingBoats ;
protected ObservableList < CompoundMark > compoundMarks ;
protected List < Leg > legs ;
protected int boatsFinished = 0 ;
protected long totalTimeElapsed ;
protected int scaleFactor = 15 ;
/ * *
* 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 ;
/ * *
* 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 = 20 ms , scale = 5 , frame represents 20 * 5 = 100 ms , and so boats are simulated for 100 ms , even though only 20 ms actually occurred .
* /
private int scaleFactor = 25 ;
/ * *
* The race ID of the course .
* /
private int raceId ;
private int dnfChance = 0 ; //percentage chance a boat fails at each checkpoint
/ * *
* 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 ;
private List < GPSCoordinate > boundary ;
/ * *
* Wind direction bearing .
* /
private double windDirection ;
private Bearing windDirection ;
/ * *
* Wind speed ( knots ) .
@ -51,18 +105,33 @@ public class Race implements Runnable {
* /
private double windSpeed ;
/ * *
* 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 . startingBoats = FXCollections . observableArrayList ( raceData . getBoats ( ) ) ;
this . legs = raceData . getLegs ( ) ;
this . mockOutput = mockOutput ;
this . boats = FXCollections . observableArrayList ( raceData . getBoats ( ) ) ;
this . compoundMarks = FXCollections . observableArrayList ( raceData . getCompoundMarks ( ) ) ;
this . boundary = raceData . getBoundary ( ) ;
this . legs = raceData . getLegs ( ) ;
this . legs . add ( new Leg ( "Finish" , this . legs . size ( ) ) ) ;
this . raceId = raceData . getRaceId ( ) ;
this . mockOutput = mockOutput ;
this . boundary = raceData . getBoundary ( ) ;
this . startTime = System . currentTimeMillis ( ) + ( Constants . PRE_RACE_WAIT_TIME / this . scaleFactor ) ;
this . windSpeed = 12 ; //TODO could use input parameters for these. And should fluctuate during race.
this . windDirection = 180 ;
//The start time is current time + 4 minutes, scaled. prestart is 3 minutes, and we add another.
this . startTime = System . currentTimeMillis ( ) + ( ( Constants . RacePreStartTime + ( 1 * 1000 ) ) / this . scaleFactor ) ;
this . setRaceStatusEnum ( RaceStatusEnum . NOT_ACTIVE ) ;
this . raceType = RaceTypeEnum . FLEET_RACE ;
this . windSpeed = 12 ;
this . windDirection = Bearing . fromDegrees ( 180 ) ;
}
@ -76,171 +145,348 @@ public class Race implements Runnable {
}
/ * *
* Parse the marker boats through mock output
* Parse the compound marker boats through mock output .
* /
public void parseMarks ( ) {
for ( CompoundMark mark : compoundMarks ) {
mockOutput . parseBoatLocation ( mark . getMark1Source ( ) . getSourceID ( ) , mark . getMark1 ( ) . getLatitude ( ) , mark . getMark1 ( ) . getLongitude ( ) , 0 , 0 ) ;
if ( mark . getMark2Source ( ) ! = null ) {
mockOutput . parseBoatLocation ( mark . getMark2Source ( ) . getSourceID ( ) , mark . getMark2 ( ) . getLatitude ( ) , mark . getMark2 ( ) . getLongitude ( ) , 0 , 0 ) ;
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 ) ;
}
/ * *
* Countdown timer until race starts .
* 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 ( )
) ;
}
/ * *
* 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 ( ) ) ;
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 = BoatLocation . convertHeadingDoubleToInt ( 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 ( ) ;
long timeLeft ;
@Override
public void handle ( long arg0 ) {
timeLeft = startTime - currentTime ;
if ( timeLeft < = 0 ) {
//Update the race status based on the current time.
updateRaceStatusEnum ( this . currentTime ) ;
//Parse the boat locations.
parseBoatLocations ( ) ;
//Parse the marks.
parseMarks ( ) ;
//Parse the race status.
parseRaceStatus ( ) ;
if ( getRaceStatusEnum ( ) = = RaceStatusEnum . STARTED ) {
System . setProperty ( "javafx.animation.fullspeed" , "true" ) ;
setBoatsStatusToRacing ( ) ;
raceTimer . start ( ) ;
stop ( ) ;
this . stop( ) ;
}
ArrayList < BoatStatus > boatStatuses = new ArrayList < > ( ) ;
for ( Boat boat : startingBoats ) {
mockOutput . parseBoatLocation ( boat . getSourceID ( ) , boat . getCurrentPosition ( ) . getLatitude ( ) ,
boat . getCurrentPosition ( ) . getLongitude ( ) , boat . getHeading ( ) , 0 ) ;
boatStatuses . add ( new BoatStatus ( boat . getSourceID ( ) , BoatStatusEnum . PRESTART , 0 ) ) ;
}
parseMarks ( ) ;
int raceStatusNumber = timeLeft < = 60000 / scaleFactor & & timeLeft > 0 ? 2 : 1 ;
RaceStatus raceStatus = new RaceStatus ( System . currentTimeMillis ( ) , raceId , raceStatusNumber , startTime , 0 , 2300 , 1 , boatStatuses ) ;
mockOutput . parseRaceStatus ( raceStatus ) ;
//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.
/ * *
* Start time of loop , in milliseconds .
* /
long timeRaceStarted = System . currentTimeMillis ( ) ;
int boatOffset = 0 ;
/ * *
* The time of the previous frame , in milliseconds .
* /
long lastFrameTime = timeRaceStarted ;
@Override
public void handle ( long arg0 ) {
if ( boatsFinished < startingBoats . size ( ) ) {
//Get the current time.
long currentTime = System . currentTimeMillis ( ) ;
//Update the total elapsed time.
totalTimeElapsed = currentTime - timeRaceStarted ;
ArrayList < BoatStatus > boatStatuses = new ArrayList < > ( ) ;
//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 ( int i = 0 ; i < startingBoats . size ( ) ; i + + ) {
Boat boat = startingBoats . get ( ( i + boatOffset ) % startingBoats . size ( ) ) ;
if ( boat ! = null ) {
//Update position.
if ( boat . getTimeFinished ( ) < 0 ) {
updatePosition ( boat , 15 ) ;
checkPosition ( boat , totalTimeElapsed ) ;
}
if ( boat . getTimeFinished ( ) > 0 ) {
mockOutput . parseBoatLocation ( boat . getSourceID ( ) , boat . getCurrentPosition ( ) . getLatitude ( ) , boat . getCurrentPosition ( ) . getLongitude ( ) , boat . getHeading ( ) , boat . getCurrentSpeed ( ) ) ;
boatStatuses . add ( new BoatStatus ( boat . getSourceID ( ) , BoatStatusEnum . FINISHED , boat . getCurrentLeg ( ) . getLegNumber ( ) ) ) ;
} else {
mockOutput . parseBoatLocation ( boat . getSourceID ( ) , boat . getCurrentPosition ( ) . getLatitude ( ) , boat . getCurrentPosition ( ) . getLongitude ( ) , boat . getHeading ( ) , boat . getCurrentSpeed ( ) ) ;
boatStatuses . add ( new BoatStatus ( boat . getSourceID ( ) ,
boat . getCurrentLeg ( ) . getLegNumber ( ) > = 0 ? BoatStatusEnum . RACING : BoatStatusEnum . DNF , boat . getCurrentLeg ( ) . getLegNumber ( ) ) ) ;
}
RaceStatus raceStatus = new RaceStatus ( System . currentTimeMillis ( ) , raceId , 4 , startTime , BoatLocation . convertHeadingDoubleToInt ( windDirection ) , ( int ) ( windSpeed * Constants . KnotsToMMPerSecond ) , 2 , boatStatuses ) ; //TODO FIX replace magic values.
} else {
stop ( ) ;
for ( Boat boat : boats ) {
//If it is still racing, update its position.
if ( boat . getStatus ( ) = = BoatStatusEnum . RACING ) {
updatePosition ( boat , framePeriod , totalTimeElapsed ) ;
}
}
parseMarks ( ) ;
boatOffset = ( boatOffset + 1 ) % ( startingBoats . size ( ) ) ;
RaceStatus raceStatus = new RaceStatus ( System . currentTimeMillis ( ) , raceId , 3 , startTime , BoatLocation . convertHeadingDoubleToInt ( windDirection ) , ( int ) ( windSpeed * Constants . KnotsToMMPerSecond ) , 2 , boatStatuses ) ; //TODO FIX replace magic values.
mockOutput . parseRaceStatus ( raceStatus ) ;
} else {
//Otherwise, the race is over!
setRaceStatusEnum ( RaceStatusEnum . FINISHED ) ;
this . stop ( ) ;
}
//Parse the boat locations.
parseBoatLocations ( ) ;
//Parse the marks.
parseMarks ( ) ;
//Parse the race status.
parseRaceStatus ( ) ;
//Update the last frame time.
this . lastFrameTime = currentTime ;
}
} ;
/ * *
* Initialise the boats in the race .
* This sets their starting positions and current legs .
* /
public void initialiseBoats ( ) {
Leg officialStart = legs . get ( 0 ) ;
String name = officialStart . getName ( ) ;
Marker endMark = officialStart . getEndCompoundMark ( ) ;
ArrayList < GPSCoordinate > startingPositions = getSpreadStartingPositions ( ) ;
for ( int i = 0 ; i < startingBoats . size ( ) ; i + + ) {
Boat boat = startingBoats . get ( i ) ;
if ( boat ! = null ) {
Leg newLeg = new Leg ( name , new Marker ( startingPositions . get ( i ) ) , endMark , 0 ) ;
boat . setCurrentLeg ( newLeg ) ;
boat . setCurrentSpeed ( Constants . TEST_VELOCITIES [ i ] ) ; //TODO we should get rid of TEST_VELOCITIES since speed is based off of wind speed/angle.
boat . setCurrentPosition ( startingPositions . get ( i ) ) ;
boat . setHeading ( boat . calculateHeading ( ) ) ;
boat . setTimeSinceTackChange ( 999999 ) ; //We set a large time since tack change so that it calculates a new VMG when the simulation starts.
}
//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
* Creates a list of starting positions for the different boats , so they do not appear cramped at the start line .
*
* @return list of starting positions
* @return A list of starting positions .
* /
public ArrayList < GPSCoordinate > getSpreadStartingPositions ( ) {
public List < GPSCoordinate > getSpreadStartingPositions ( ) {
//The first compound marker of the race - the starting gate.
CompoundMark compoundMark = this . legs . get ( 0 ) . getStartCompoundMark ( ) ;
int nBoats = startingBoats . size ( ) ;
Marker compoundMark = legs . get ( 0 ) . getStartCompoundMark ( ) ;
//The position of the two markers from the compound marker.
GPSCoordinate mark1Position = compoundMark . getMark1Position ( ) ;
GPSCoordinate mark2Position = compoundMark . getMark2Position ( ) ;
GeodeticCalculator initialCalc = new GeodeticCalculator ( ) ;
initialCalc . setStartingGeographicPoint ( compoundMark . getMark1 ( ) . getLongitude ( ) , compoundMark . getMark1 ( ) . getLatitude ( ) ) ;
initialCalc . setDestinationGeographicPoint ( compoundMark . getMark2 ( ) . getLongitude ( ) , compoundMark . getMark2 ( ) . getLatitude ( ) ) ;
double azimuth = initialCalc . getAzimuth ( ) ;
double distanceBetweenMarkers = initialCalc . getOrthodromicDistance ( ) ;
double distanceBetweenBoats = distanceBetweenMarkers / ( nBoats + 1 ) ;
//Calculates the azimuth between the two points.
Azimuth azimuth = GPSCoordinate . calculateAzimuth ( mark1Position , mark2Position ) ;
GeodeticCalculator positionCalc = new GeodeticCalculator ( ) ;
positionCalc . setStartingGeographicPoint ( compoundMark . getMark1 ( ) . getLongitude ( ) , compoundMark . getMark1 ( ) . getLatitude ( ) ) ;
ArrayList < GPSCoordinate > positions = new ArrayList < > ( ) ;
//Calculates the distance between the two points.
double distanceMeters = GPSCoordinate . calculateDistanceMeters ( mark1Position , mark2Position ) ;
for ( int i = 0 ; i < nBoats ; i + + ) {
positionCalc . setDirection ( azimuth , distanceBetweenBoats ) ;
Point2D position = positionCalc . getDestinationGeographicPoint ( ) ;
positions . add ( new GPSCoordinate ( position . getY ( ) , position . getX ( ) ) ) ;
//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 ) ;
positionCalc = new GeodeticCalculator ( ) ;
positionCalc . setStartingGeographicPoint ( position ) ;
}
return positions ;
}
/ * *
* Calculates the boats next GPS position based on its distance travelled and heading
*
* @param oldCoordinates GPS coordinates of the boat ' s starting position
* @param distanceTravelled distance in nautical miles
* @param azimuth boat ' s current direction . Value between - 180 and 180
* @return The boat ' s new coordinate
* /
public static GPSCoordinate calculatePosition ( GPSCoordinate oldCoordinates , double distanceTravelled , double azimuth ) {
//Find new coordinate using current heading and distance
GeodeticCalculator geodeticCalculator = new GeodeticCalculator ( ) ;
//Load start point into calculator
Point2D startPoint = new Point2D . Double ( oldCoordinates . getLongitude ( ) , oldCoordinates . getLatitude ( ) ) ;
geodeticCalculator . setStartingGeographicPoint ( startPoint ) ;
//load direction and distance travelled into calculator
geodeticCalculator . setDirection ( azimuth , distanceTravelled * Constants . NMToMetersConversion ) ;
//get new point
Point2D endPoint = geodeticCalculator . getDestinationGeographicPoint ( ) ;
return new GPSCoordinate ( endPoint . getY ( ) , endPoint . getX ( ) ) ;
}
* Calculates a boat ' s VMG .
* @param boat The boat to calculate VMG for .
* @return VMG for the specified boat .
* /
private VMG calculateVMG ( Boat boat ) {
private VMG calculateHeading ( Boat boat ) {
//How fast a boat can turn, in degrees per millisecond.
double turnRate = 0.03 ;
@ -248,110 +494,188 @@ public class Race implements Runnable {
double turnAngle = turnRate * boat . getTimeSinceTackChange ( ) ;
//Find the bounds on what angle the boat is allowed to travel at. The bounds cap out at [0, 360).
double bound1 = Math . max ( boat . getHeading ( ) - turnAngle , 0 ) ;
double bound2 = Math . min ( boat . getHeading ( ) + turnAngle , 360 ) ;
double bound1Degrees = Math . max ( boat . getBearing ( ) . degrees ( ) - turnAngle , 0 ) ;
double bound2Degrees = Math . min ( boat . getBearing ( ) . degrees ( ) + turnAngle , 360 ) ;
Bearing bound1 = Bearing . fromDegrees ( bound1Degrees ) ;
Bearing bound2 = Bearing . fromDegrees ( bound2Degrees ) ;
return boat . getPolars ( ) . calculateVMG ( this . windDirection , this . windSpeed , boat . calculateBearingToDestination ( ) , bound1 , bound2 ) ;
return boat . getPolars ( ) . calculateVMG ( this . windDirection , this . windSpeed , boat . calculateBearingTo NextMarker ( ) , bound1 , bound2 ) ;
}
private boolean improvesVelocity ( Boat boat , VMG newHeading ) {
double angleBetweenDestAndHeading = boat . getHeading ( ) - boat . calculateBearingToDestination ( ) ;
double angleBetweenDestAndNewVMG = newHeading . getBearing ( ) - boat . calculateBearingToDestination ( ) ;
double currentVelocity = cos ( Math . toRadians ( angleBetweenDestAndHeading ) ) * boat . getVelocity ( ) ;
double vmgVelocity = cos ( Math . toRadians ( angleBetweenDestAndNewVMG ) ) * newHeading . getSpeed ( ) ;
/ * *
* 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 ) {
//Calculates the angle between the boat and its destination.
Angle angleBetweenDestAndHeading = Angle . fromDegrees ( boat . getBearing ( ) . degrees ( ) - boat . calculateBearingToNextMarker ( ) . degrees ( ) ) ;
//Calculates the angle between the new VMG and the boat's destination.
Angle angleBetweenDestAndNewVMG = Angle . fromDegrees ( vmg . getBearing ( ) . degrees ( ) - boat . calculateBearingToNextMarker ( ) . degrees ( ) ) ;
//Calculate the boat's current velocity.
double currentVelocity = Math . cos ( angleBetweenDestAndHeading . radians ( ) ) * boat . getCurrentSpeed ( ) ;
//Calculate the potential velocity with the new VMG.
double vmgVelocity = Math . cos ( angleBetweenDestAndNewVMG . radians ( ) ) * vmg . getSpeed ( ) ;
//Return whether or not the new VMG gives better velocity.
return vmgVelocity > currentVelocity ;
}
/ * *
* Calculates the distance a boat has travelled and updates its current position according to this value .
*
* @param boat to be updated
* @param millisecondsElapsed since last update
* @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 , int millisecondsElapsed ) {
protected void updatePosition ( Boat boat , long updatePeriodMilliseconds , long totalElapsedMilliseconds ) {
//distanceTravelled = velocity (nm p hr) * time taken to update loop
double distanceTravelled = ( boat . getCurrentSpeed ( ) * this . scaleFactor * millisecondsElapsed ) / 3600000 ;
double totalDistanceTravelled ;
//Checks if the current boat has finished the race or not.
boolean finish = this . isLastLeg ( boat . getCurrentLeg ( ) ) ;
boolean finish = boat . getCurrentLeg ( ) . getName ( ) . equals ( "Finish" ) ;
if ( ! finish ) {
double totalDistanceTravelledInTack = distanceTravelled ; //TODO FIX// + boat.getDistanceTravelledInTack();
boat . setTimeSinceTackChange ( boat . getTimeSinceTackChange ( ) + this . scaleFactor * millisecondsElapsed ) ;
//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 ) ;
VMG newHeading = calculateHeading ( boat ) ;
//Calculate the new VMG.
VMG newVMG = this . calculateVMG ( boat ) ;
if ( improvesVelocity ( boat , newHeading ) ) {
boat . setHeading ( newHeading . getBearing ( ) ) ;
boat . setCurrentSpeed ( newHeading . getSpeed ( ) ) ;
boat . setTimeSinceTackChange ( 0 ) ;
//If the new vmg improves velocity, use it.
if ( improvesVelocity ( boat , newVMG ) ) {
boat . setVMG ( newVMG ) ;
}
double azimuth = boat . getHeading ( ) ;
if ( azimuth > 180 ) {
azimuth = azimuth - 360 ;
}
//tests to see if a point in front of the boat is out of bounds, if so mirror heading in the wind
GPSCoordinate test = calculatePosition ( boat . getCurrentPosition ( ) , ( 100.0 / Constants . NMToMetersConversion ) , azimuth ) ;
if ( ! GPSCoordinate . isInsideBoundary ( test , boundary ) ) {
double tempHeading = ( boat . getHeading ( ) - this . windDirection + 90 ) % 360 ;
boat . setHeading ( tempHeading ) ;
}
//calc the distance travelled in a straight line to windward
//double angleBetweenDestAndHeading = boat.getHeading() - boat.calculateBearingToDestination();
totalDistanceTravelled = cos ( Math . toRadians ( boat . getHeading ( ) - boat . calculateBearingToDestination ( ) ) ) * totalDistanceTravelledInTack ;
boat . setDistanceTravelledInLeg ( totalDistanceTravelled ) ;
//Ensure that the boat doesn't leave the course bounds.
this . forceBoatBearingInBounds ( boat ) ;
//Check the boats position (update leg and stuff).
checkPosition ( boat , totalTimeElapsed ) ;
}
}
/ * *
* Checks if the boat ' s current bearing would put it out of course bounds , and adjusts it if it does .
* @param boat The boat to check .
* /
private void forceBoatBearingInBounds ( Boat boat ) {
//Get the boat's azimuth.
Azimuth azimuth = Azimuth . fromBearing ( boat . getBearing ( ) ) ;
//Tests to see if a point in front of the boat is out of bounds, if so mirror heading in the wind.
double epsilonMeters = 100 d ;
GPSCoordinate testCoord = GPSCoordinate . calculateNewPosition ( boat . getCurrentPosition ( ) , epsilonMeters , azimuth ) ;
//Calculate boat's new position by adding the distance travelled onto the start point of the leg
azimuth = boat . getHeading ( ) ;
azimuth = azimuth > 180 ? azimuth - 360 : azimuth ;
boat . setCurrentPosition ( calculatePosition ( boat . getCurrentPosition ( ) , totalDistanceTravelledInTack , azimuth ) ) ;
//If it isn't inside the boundary, calculate new bearing.
if ( ! GPSCoordinate . isInsideBoundary ( testCoord , this . boundary ) ) {
Bearing tempBearing = Bearing . fromDegrees ( boat . getBearing ( ) . degrees ( ) - this . windDirection . degrees ( ) + 90 ) ;
boat . set Bearing( tempBearing ) ;
}
}
/ * *
* 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 ) {
//System.out.println(boat.getDistanceTravelledInLeg());
//System.out.println(boat.getCurrentLeg().getDistance());
//System.out.println(" ");
//if (boat.getDistanceTravelledInLeg() > boat.getCurrentLeg().getDistance()) {
//The distance (in nautical miles) within which the boat needs to get in order to consider that it has reached the marker.
double epsilon = 100.0 / Constants . NMToMetersConversion ; //100 meters. TODO should be more like 5-10.
if ( boat . calculateDistanceToNextMarker ( ) < epsilon ) {
//boat has passed onto new leg
if ( boat . getCurrentLeg ( ) . getName ( ) . equals ( "Finish" ) ) {
//boat has finished
boatsFinished + + ;
boat . setTimeFinished ( 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 ( ) ) {
boatsFinished + + ;
//Boat has pulled out of race.
boat . setTimeFinished ( timeElapsed ) ;
boat . setCurrentLeg ( new Leg ( "DNF" , - 1 ) ) ;
boat . setCurrentSpeed ( 0 ) ;
} else {
//Calculate how much the boat overshot the marker by
boat . setDistanceTravelledInLeg ( boat . getDistanceTravelledInLeg ( ) - boat . getCurrentLeg ( ) . getDistance ( ) ) ;
//Move boat on to next leg
Leg nextLeg = legs . get ( boat . getCurrentLeg ( ) . getLegNumber ( ) + 1 ) ;
boat . setCurrentLeg ( nextLeg ) ;
//Add overshoot distance into the distance travelled for the next leg
boat . setDistanceTravelledInLeg ( boat . getDistanceTravelledInLeg ( ) ) ;
//Setting a high value for this allows the boat to immediately do a large turn, as it has needs to in order to get to the next mark.
boat . setTimeSinceTackChange ( 999999 ) ;
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
*
@ -363,9 +687,69 @@ public class Race implements Runnable {
}
}
/ * *
* 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 ;
}
}