@ -3,7 +3,6 @@ package seng302.Model;
import javafx.animation.AnimationTimer ;
import javafx.animation.AnimationTimer ;
import javafx.collections.FXCollections ;
import javafx.collections.FXCollections ;
import javafx.collections.ObservableList ;
import javafx.collections.ObservableList ;
import org.geotools.referencing.GeodeticCalculator ;
import seng302.Constants ;
import seng302.Constants ;
import seng302.DataInput.RaceDataSource ;
import seng302.DataInput.RaceDataSource ;
@ -11,39 +10,94 @@ import seng302.MockOutput;
import seng302.Networking.Messages.BoatLocation ;
import seng302.Networking.Messages.BoatLocation ;
import seng302.Networking.Messages.BoatStatus ;
import seng302.Networking.Messages.BoatStatus ;
import seng302.Networking.Messages.Enums.BoatStatusEnum ;
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.Messages.RaceStatus ;
import java.awt.geom.Point2D ;
import java.io.* ;
import java.util.ArrayList ;
import java.util.ArrayList ;
import java.util.Iterator ;
import java.util.List ;
import java.util.List ;
import java.util.Random ;
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
* Represents a yacht race .
* Created by fwy13 on 3 / 03 / 17.
* Has a course , boats , boundaries , etc . . .
* Is responsible for simulating the race , and sending messages to a MockOutput instance .
* /
* /
public class Race implements Runnable {
public class Race implements Runnable {
protected ObservableList < Boat > startingBoats ;
/ * *
protected ObservableList < CompoundMark > compoundMarks ;
* An observable list of boats in the race .
protected List < Leg > legs ;
* /
protected int boatsFinished = 0 ;
private ObservableList < Boat > boats ;
protected long totalTimeElapsed ;
protected int scaleFactor = 15 ;
/ * *
* 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 ;
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 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 MockOutput mockOutput ;
private List < GPSCoordinate > boundary ;
/ * *
/ * *
* Wind direction bearing .
* Wind direction bearing .
* /
* /
private double windDirection ;
private Bearing windDirection ;
/ * *
/ * *
* Wind speed ( knots ) .
* Wind speed ( knots ) .
@ -51,18 +105,33 @@ public class Race implements Runnable {
* /
* /
private double windSpeed ;
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 ) {
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 . compoundMarks = FXCollections . observableArrayList ( raceData . getCompoundMarks ( ) ) ;
this . boundary = raceData . getBoundary ( ) ;
this . legs = raceData . getLegs ( ) ;
this . legs . add ( new Leg ( "Finish" , this . legs . size ( ) ) ) ;
this . legs . add ( new Leg ( "Finish" , this . legs . size ( ) ) ) ;
this . raceId = raceData . getRaceId ( ) ;
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.
//The start time is current time + 4 minutes, scaled. prestart is 3 minutes, and we add another.
this . windDirection = 180 ;
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 ( ) {
private void parseMarks ( ) {
for ( CompoundMark mark : compoundMarks ) {
for ( CompoundMark compoundMark : this . compoundMarks ) {
mockOutput . parseBoatLocation ( mark . getMark1Source ( ) . getSourceID ( ) , mark . getMark1 ( ) . getLatitude ( ) , mark . getMark1 ( ) . getLongitude ( ) , 0 , 0 ) ;
if ( mark . getMark2Source ( ) ! = null ) {
//Get the individual marks from the compound mark.
mockOutput . parseBoatLocation ( mark . getMark2Source ( ) . getSourceID ( ) , mark . getMark2 ( ) . getLatitude ( ) , mark . getMark2 ( ) . getLongitude ( ) , 0 , 0 ) ;
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 ( ) {
protected AnimationTimer countdownTimer = new AnimationTimer ( ) {
long currentTime = System . currentTimeMillis ( ) ;
long currentTime = System . currentTimeMillis ( ) ;
long timeLeft ;
@Override
@Override
public void handle ( long arg0 ) {
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" ) ;
System . setProperty ( "javafx.animation.fullspeed" , "true" ) ;
setBoatsStatusToRacing ( ) ;
raceTimer . start ( ) ;
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 ( ) ;
currentTime = System . currentTimeMillis ( ) ;
}
}
} ;
} ;
/ * *
* Timer that runs for the duration of the race , until all boats finish .
* /
private AnimationTimer raceTimer = new AnimationTimer ( ) {
private AnimationTimer raceTimer = new AnimationTimer ( ) {
//Start time of loop.
/ * *
* Start time of loop , in milliseconds .
* /
long timeRaceStarted = System . currentTimeMillis ( ) ;
long timeRaceStarted = System . currentTimeMillis ( ) ;
int boatOffset = 0 ;
/ * *
* The time of the previous frame , in milliseconds .
* /
long lastFrameTime = timeRaceStarted ;
@Override
@Override
public void handle ( long arg0 ) {
public void handle ( long arg0 ) {
if ( boatsFinished < startingBoats . size ( ) ) {
//Get the current time.
long currentTime = System . currentTimeMillis ( ) ;
//Update the total elapsed time.
//Get the current time.
totalTimeElapsed = currentTime - timeRaceStarted ;
long currentTime = System . currentTimeMillis ( ) ;
ArrayList < BoatStatus > boatStatuses = new ArrayList < > ( ) ;
//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 each boat, we update its position, and generate a BoatLocationMessage.
for ( int i = 0 ; i < startingBoats . size ( ) ; i + + ) {
for ( Boat boat : boats ) {
Boat boat = startingBoats . get ( ( i + boatOffset ) % startingBoats . size ( ) ) ;
if ( boat ! = null ) {
//If it is still racing, update its position.
//Update position.
if ( boat . getStatus ( ) = = BoatStatusEnum . RACING ) {
if ( boat . getTimeFinished ( ) < 0 ) {
updatePosition ( boat , 15 ) ;
updatePosition ( boat , framePeriod , totalTimeElapsed ) ;
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 ( ) ;
}
}
}
}
parseMarks ( ) ;
boatOffset = ( boatOffset + 1 ) % ( startingBoats . size ( ) ) ;
} else {
RaceStatus raceStatus = new RaceStatus ( System . currentTimeMillis ( ) , raceId , 3 , startTime , BoatLocation . convertHeadingDoubleToInt ( windDirection ) , ( int ) ( windSpeed * Constants . KnotsToMMPerSecond ) , 2 , boatStatuses ) ; //TODO FIX replace magic values.
//Otherwise, the race is over!
mockOutput . parseRaceStatus ( raceStatus ) ;
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 ( ) {
public void initialiseBoats ( ) {
Leg officialStart = legs . get ( 0 ) ;
String name = officialStart . getName ( ) ;
//Gets the starting positions of the boats.
Marker endMark = officialStart . getEndCompoundMark ( ) ;
List < GPSCoordinate > startingPositions = getSpreadStartingPositions ( ) ;
ArrayList < GPSCoordinate > startingPositions = getSpreadStartingPositions ( ) ;
//Get iterators for our boat and position lists.
for ( int i = 0 ; i < startingBoats . size ( ) ; i + + ) {
Iterator < Boat > boatIt = this . boats . iterator ( ) ;
Boat boat = startingBoats . get ( i ) ;
Iterator < GPSCoordinate > startPositionIt = startingPositions . iterator ( ) ;
if ( boat ! = null ) {
Leg newLeg = new Leg ( name , new Marker ( startingPositions . get ( i ) ) , endMark , 0 ) ;
//Iterate over the pair of lists.
boat . setCurrentLeg ( newLeg ) ;
while ( boatIt . hasNext ( ) & & startPositionIt . hasNext ( ) ) {
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 ) ) ;
//Get the next boat and position.
boat . setHeading ( boat . calculateHeading ( ) ) ;
Boat boat = boatIt . next ( ) ;
boat . setTimeSinceTackChange ( 999999 ) ; //We set a large time since tack change so that it calculates a new VMG when the simulation starts.
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 ( ) ;
//The position of the two markers from the compound marker.
Marker compoundMark = legs . get ( 0 ) . getStartCompoundMark ( ) ;
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 ( ) ;
//Calculates the azimuth between the two points.
double distanceBetweenMarkers = initialCalc . getOrthodromicDistance ( ) ;
Azimuth azimuth = GPSCoordinate . calculateAzimuth ( mark1Position , mark2Position ) ;
double distanceBetweenBoats = distanceBetweenMarkers / ( nBoats + 1 ) ;
GeodeticCalculator positionCalc = new GeodeticCalculator ( ) ;
//Calculates the distance between the two points.
positionCalc . setStartingGeographicPoint ( compoundMark . getMark1 ( ) . getLongitude ( ) , compoundMark . getMark1 ( ) . getLatitude ( ) ) ;
double distanceMeters = GPSCoordinate . calculateDistanceMeters ( mark1Position , mark2Position ) ;
ArrayList < GPSCoordinate > positions = new ArrayList < > ( ) ;
for ( int i = 0 ; i < nBoats ; i + + ) {
//The number of boats in the race.
positionCalc . setDirection ( azimuth , distanceBetweenBoats ) ;
int numberOfBoats = this . boats . size ( ) ;
Point2D position = positionCalc . getDestinationGeographicPoint ( ) ;
positions . add ( new GPSCoordinate ( position . getY ( ) , position . getX ( ) ) ) ;
//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 ;
return positions ;
}
}
/ * *
/ * *
* Calculates the boats next GPS position based on its distance travelled and heading
* Calculates a boat ' s VMG .
*
* @param boat The boat to calculate VMG for .
* @param oldCoordinates GPS coordinates of the boat ' s starting position
* @return VMG for the specified boat .
* @param distanceTravelled distance in nautical miles
* /
* @param azimuth boat ' s current direction . Value between - 180 and 180
private VMG calculateVMG ( Boat boat ) {
* @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 ( ) ) ;
}
private VMG calculateHeading ( Boat boat ) {
//How fast a boat can turn, in degrees per millisecond.
//How fast a boat can turn, in degrees per millisecond.
double turnRate = 0.03 ;
double turnRate = 0.03 ;
@ -248,110 +494,188 @@ public class Race implements Runnable {
double turnAngle = turnRate * boat . getTimeSinceTackChange ( ) ;
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).
//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 bound1Degrees = Math . max ( boat . getBearing ( ) . degrees ( ) - turnAngle , 0 ) ;
double bound2 = Math . min ( boat . getHeading ( ) + turnAngle , 360 ) ;
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 ( ) ;
* Determines whether or not a given VMG improves the velocity of a boat .
double currentVelocity = cos ( Math . toRadians ( angleBetweenDestAndHeading ) ) * boat . getVelocity ( ) ;
* @param boat The boat to test .
double vmgVelocity = cos ( Math . toRadians ( angleBetweenDestAndNewVMG ) ) * newHeading . getSpeed ( ) ;
* @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 ;
return vmgVelocity > currentVelocity ;
}
}
/ * *
/ * *
* Calculates the distance a boat has travelled and updates its current position according to this value .
* Calculates the distance a boat has travelled and updates its current position according to this value .
*
*
* @param boat to be updated
* @param boat The boat to be updated .
* @param millisecondsElapsed since last update
* @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
//Checks if the current boat has finished the race or not.
double distanceTravelled = ( boat . getCurrentSpeed ( ) * this . scaleFactor * millisecondsElapsed ) / 3600000 ;
boolean finish = this . isLastLeg ( boat . getCurrentLeg ( ) ) ;
double totalDistanceTravelled ;
boolean finish = boat . getCurrentLeg ( ) . getName ( ) . equals ( "Finish" ) ;
if ( ! 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.
//Calculate the new VMG.
VMG newVMG = this . calculateVMG ( boat ) ;
if ( improvesVelocity ( boat , newHeading ) ) {
//If the new vmg improves velocity, use it.
boat . setHeading ( newHeading . getBearing ( ) ) ;
if ( improvesVelocity ( boat , newVMG ) ) {
boat . setCurrentSpeed ( newHeading . getSpeed ( ) ) ;
boat . setVMG ( newVMG ) ;
boat . setTimeSinceTackChange ( 0 ) ;
}
}
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
//Ensure that the boat doesn't leave the course bounds.
//double angleBetweenDestAndHeading = boat.getHeading() - boat.calculateBearingToDestination();
this . forceBoatBearingInBounds ( boat ) ;
totalDistanceTravelled = cos ( Math . toRadians ( boat . getHeading ( ) - boat . calculateBearingToDestination ( ) ) ) * totalDistanceTravelledInTack ;
boat . setDistanceTravelledInLeg ( totalDistanceTravelled ) ;
//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
//If it isn't inside the boundary, calculate new bearing.
azimuth = boat . getHeading ( ) ;
if ( ! GPSCoordinate . isInsideBoundary ( testCoord , this . boundary ) ) {
azimuth = azimuth > 180 ? azimuth - 360 : azimuth ;
Bearing tempBearing = Bearing . fromDegrees ( boat . getBearing ( ) . degrees ( ) - this . windDirection . degrees ( ) + 90 ) ;
boat . setCurrentPosition ( calculatePosition ( boat . getCurrentPosition ( ) , totalDistanceTravelledInTack , azimuth ) ) ;
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 ) {
protected void checkPosition ( Boat boat , long timeElapsed ) {
//System.out.println(boat.getDistanceTravelledInLeg());
//System.out.println(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.
//System.out.println(" ");
double epsilonNauticalMiles = 100.0 / Constants . NMToMetersConversion ; //100 meters. TODO should be more like 5-10.
//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.
if ( boat . calculateDistanceToNextMarker ( ) < epsilonNauticalMiles ) {
double epsilon = 100.0 / Constants . NMToMetersConversion ; //100 meters. TODO should be more like 5-10.
//Boat has reached its target marker, and has moved on to a new leg.
if ( boat . calculateDistanceToNextMarker ( ) < epsilon ) {
//boat has passed onto new leg
if ( boat . getCurrentLeg ( ) . getName ( ) . equals ( "Finish" ) ) {
//Calculate how much the boat overshot the marker by.
//boat has finished
double overshootMeters = boat . calculateDistanceToNextMarker ( ) ;
boatsFinished + + ;
boat . setTimeFinished ( timeElapsed ) ;
//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 . setTimeFinished ( timeElapsed ) ;
boat . setCurrentSpeed ( 0 ) ;
boat . setCurrentSpeed ( 0 ) ;
boat . setStatus ( BoatStatusEnum . FINISHED ) ;
} else if ( doNotFinish ( ) ) {
} else if ( doNotFinish ( ) ) {
boatsFinished + + ;
//Boat has pulled out of race.
boat . setTimeFinished ( timeElapsed ) ;
boat . setTimeFinished ( timeElapsed ) ;
boat . setCurrentLeg ( new Leg ( "DNF" , - 1 ) ) ;
boat . setCurrentLeg ( new Leg ( "DNF" , - 1 ) ) ;
boat . setCurrentSpeed ( 0 ) ;
boat . setCurrentSpeed ( 0 ) ;
} else {
boat . setStatus ( BoatStatusEnum . DNF ) ;
//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 ) ;
}
}
}
}
}
/ * *
* 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
* 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 ( ) {
protected boolean doNotFinish ( ) {
Random rand = new Random ( ) ;
Random rand = new Random ( ) ;
return rand . nextInt ( 100 ) < dnfChance ;
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 ;
}
}
}