package mock.model; import javafx.animation.AnimationTimer; import mock.app.MockOutput; import network.Messages.BoatLocation; import network.Messages.BoatStatus; import network.Messages.Enums.BoatStatusEnum; import network.Messages.LatestMessages; import network.Messages.RaceStatus; import network.Utils.AC35UnitConverter; import shared.dataInput.BoatDataSource; import shared.dataInput.RaceDataSource; import network.Messages.Enums.RaceStatusEnum; import shared.dataInput.RegattaDataSource; import shared.model.*; import java.util.*; 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 MockRace extends Race { /** * An observable list of boats in the race. */ private List boats; /** * A copy of the boundary list, except "shrunk" inwards by 50m. */ private List shrinkBoundary; /** * The scale factor of the race. * See {@link Constants#RaceTimeScale}. */ private int scaleFactor; /** * 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; /** * Used to generate random numbers when changing the wind direction. */ private int changeWind = 4; /** * The bearing the wind direction starts at. */ private static final Bearing windBaselineBearing = Bearing.fromDegrees(225); /** * The lower bearing angle that the wind may have. */ private static final Bearing windLowerBound = Bearing.fromDegrees(215); /** * The upper bearing angle that the wind may have. */ private static final Bearing windUpperBound = Bearing.fromDegrees(235); /** * Constructs a race object with a given RaceDataSource, BoatDataSource, and RegattaDataSource and sends events to the given mockOutput. * @param boatDataSource Data source for boat related data (yachts and marker boats). * @param raceDataSource Data source for race related data (participating boats, legs, etc...). * @param regattaDataSource Data source for race related data (course name, location, timezone, etc...). * @param latestMessages The LatestMessages to send events to. * @param polars The polars table to be used for boat simulation. * @param timeScale The timeScale for the race. See {@link Constants#RaceTimeScale}. */ public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, Polars polars, int timeScale) { super(boatDataSource, raceDataSource, regattaDataSource, latestMessages); this.scaleFactor = timeScale; this.boats = this.generateMockBoats(boatDataSource.getBoats(), raceDataSource.getParticipants(), polars); this.shrinkBoundary = GPSCoordinate.getShrinkBoundary(this.boundary); this.windSpeed = 12; this.windDirection = Bearing.fromDegrees(180); } /** * Generates a list of MockBoats given a list of Boats, and a list of participating boats. * @param boats The map of Boats describing boats that are potentially in the race. Maps boat sourceID to boat. * @param sourceIDs The list of boat sourceIDs describing which specific boats are actually participating. * @param polars The polars table to be used for boat simulation. * @return A list of MockBoats that are participating in the race. */ private List generateMockBoats(Map boats, List sourceIDs, Polars polars) { List mockBoats = new ArrayList<>(sourceIDs.size()); //For each sourceID participating... for (int sourceID : sourceIDs) { //Get the boat associated with the sourceID. Boat boat = boats.get(sourceID); //Construct a MockBoat using the Boat and Polars. MockBoat mockBoat = new MockBoat(boat, polars); mockBoats.add(mockBoat); } return mockBoats; } /** * Runnable for the thread. */ public void run() { initialiseBoats(); initialiseWindDirection(); this.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) { //Create message. BoatLocation boatLocation = new BoatLocation( mark.getSourceID(), mark.getPosition().getLatitude(), mark.getPosition().getLongitude(), this.boatLocationSequenceNumber, 0, 0, this.raceClock.getCurrentTimeMilli()); //Iterates the sequence number. this.boatLocationSequenceNumber++; this.latestMessages.setBoatLocation(boatLocation); } /** * Parse the boats in the race, and send it to mockOutput. */ private void parseBoatLocations() { //Parse each boat. for (MockBoat boat : this.boats) { this.parseIndividualBoatLocation(boat); } } /** * Parses an individual boat, and sends it to mockOutput. * @param boat The boat to parse. */ private void parseIndividualBoatLocation(MockBoat boat) { BoatLocation boatLocation = new BoatLocation( boat.getSourceID(), boat.getCurrentPosition().getLatitude(), boat.getCurrentPosition().getLongitude(), this.boatLocationSequenceNumber, boat.getBearing().degrees(), boat.getCurrentSpeed(), this.raceClock.getCurrentTimeMilli()); //Iterates the sequence number. this.boatLocationSequenceNumber++; this.latestMessages.setBoatLocation(boatLocation); } /** * Updates the race time to a specified value, in milliseconds since the unix epoch. * @param currentTime Milliseconds since unix epoch. */ private void updateRaceTime(long currentTime) { this.raceClock.setUTCTime(currentTime); } /** * Updates the race status enumeration based on the current time. */ private void updateRaceStatusEnum() { //The amount of milliseconds until the race starts. long timeToStart = this.raceClock.getDurationMilli(); if (timeToStart > Constants.RacePreStartTime) { //Time > 3 minutes is the prestart period. this.setRaceStatusEnum(RaceStatusEnum.PRESTART); } else if ((timeToStart <= Constants.RacePreStartTime) && (timeToStart >= Constants.RacePreparatoryTime)) { //Time between [1, 3] minutes is the warning period. this.setRaceStatusEnum(RaceStatusEnum.WARNING); } else if ((timeToStart <= Constants.RacePreparatoryTime) && (timeToStart > 0)) { //Time between (0, 1] minutes is the preparatory period. this.setRaceStatusEnum(RaceStatusEnum.PREPARATORY); } else { //Otherwise, the race has started! this.setRaceStatusEnum(RaceStatusEnum.STARTED); } } /** * Parses the race status, and sends it to mockOutput. */ private void parseRaceStatus() { //A race status message contains a list of boat statuses. List boatStatuses = new ArrayList<>(); //Add each boat status to the status list. for (MockBoat boat : this.boats) { BoatStatus boatStatus = new BoatStatus( boat.getSourceID(), boat.getStatus(), boat.getCurrentLeg().getLegNumber(), boat.getEstimatedTime() ); boatStatuses.add(boatStatus); } //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) (this.windSpeed * Constants.KnotsToMMPerSecond); //Create race status object, and send it. RaceStatus raceStatus = new RaceStatus( System.currentTimeMillis(), this.raceId, this.getRaceStatusEnum().getValue(), this.raceClock.getStartingTimeMilli(), windDirectionInt, windSpeedInt, this.getRaceType().getValue(), boatStatuses); this.latestMessages.setRaceStatus(raceStatus); } /** * Sets the status of all boats in the race to RACING. */ private void setBoatsStatusToRacing() { for (MockBoat 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 race time. updateRaceTime(currentTime); //Update the race status based on the current time. updateRaceStatusEnum(); //Parse the boat locations. parseBoatLocations(); //Parse the marks. parseMarks(); // Change wind direction changeWindDirection(); //Parse the race status. parseRaceStatus(); if (getRaceStatusEnum() == RaceStatusEnum.STARTED) { 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(); /** * Current time during a loop iteration. */ long currentTime = System.currentTimeMillis(); /** * The time of the previous frame, in milliseconds. */ long lastFrameTime = timeRaceStarted; @Override public void handle(long arg0) { //Get the current time. currentTime = System.currentTimeMillis(); //Update race time. updateRaceTime(currentTime); //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; //For each boat, we update its position, and generate a BoatLocationMessage. for (MockBoat boat : boats) { //If it is still racing, update its position. if (boat.getStatus() == BoatStatusEnum.RACING) { updatePosition(boat, framePeriod, raceClock.getDurationMilli()); } } } else { //Otherwise, the race is over! raceFinished.start(); setRaceStatusEnum(RaceStatusEnum.FINISHED); this.stop(); } if (getNumberOfActiveBoats() != 0) { // Change wind direction changeWindDirection(); //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) { parseRaceStatus(); if (iters > 500) { stop(); } iters++; } }; /** * Initialise the boats in the race. * This sets their starting positions and current legs. */ @Override protected void initialiseBoats() { //Gets the starting positions of the boats. List startingPositions = getSpreadStartingPositions(); //Get iterators for our boat and position lists. Iterator boatIt = this.boats.iterator(); Iterator startPositionIt = startingPositions.iterator(); //Iterate over the pair of lists. while (boatIt.hasNext() && startPositionIt.hasNext()) { //Get the next boat and position. MockBoat boat = boatIt.next(); GPSCoordinate startPosition = startPositionIt.next(); //The boat starts on the first leg of the race. boat.setCurrentLeg(this.legs.get(0)); //Boats start with 0 knots speed. boat.setCurrentSpeed(0d); //Place the boat at its starting position. boat.setCurrentPosition(startPosition); //Boats start facing their next marker. boat.setBearing(boat.calculateBearingToNextMarker()); //Sets the boats status to prestart - it changes to racing when the race starts. boat.setStatus(BoatStatusEnum.PRESTART); //We set a large time since tack change so that it calculates a new VMG when the simulation starts. boat.setTimeSinceTackChange(999999); } } /** * Creates a list of starting positions for the different boats, so they do not appear cramped at the start line. * * @return A list of starting positions. */ public List getSpreadStartingPositions() { //The first compound marker of the race - the starting gate. CompoundMark compoundMark = this.legs.get(0).getStartCompoundMark(); //The position of the two markers from the compound marker. GPSCoordinate mark1Position = compoundMark.getMark1Position(); GPSCoordinate mark2Position = compoundMark.getMark2Position(); //Calculates the azimuth between the two points. Azimuth azimuth = GPSCoordinate.calculateAzimuth(mark1Position, mark2Position); //Calculates the distance between the two points. double distanceMeters = GPSCoordinate.calculateDistanceMeters(mark1Position, mark2Position); //The number of boats in the race. int numberOfBoats = this.boats.size(); //Calculates the distance between each boat. We divide by numberOfBoats + 1 to ensure that no boat is placed on one of the starting gate's marks. double distanceBetweenBoatsMeters = distanceMeters / (numberOfBoats + 1); //List to store coordinates in. List positions = new ArrayList<>(); //We start spacing boats out from mark 1. GPSCoordinate position = mark1Position; //For each boat, displace position, and store it. for (int i = 0; i < numberOfBoats; i++) { position = GPSCoordinate.calculateNewPosition(position, distanceBetweenBoatsMeters, azimuth); positions.add(position); } return positions; } /** * Calculates a boat's VMG. * @param boat The boat to calculate VMG for. * @param bearingBounds An array containing the lower and upper acceptable bearing bounds to keep the boat in the course. * @return VMG for the specified boat. */ private VMG calculateVMG(MockBoat 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(MockBoat 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(MockBoat 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, totalElapsedMilliseconds); } } /** * 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(MockBoat 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(MockBoat boat, long timeElapsed) { //The distance, in nautical miles, within which the boat needs to get in order to consider that it has reached the marker. double epsilonNauticalMiles = 100.0 / Constants.NMToMetersConversion; //100 meters. TODO should be more like 5-10. 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); } } } /** * 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 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 (MockBoat boat : this.boats) { //If the boat is currently racing, count it. if (boat.getStatus() == BoatStatusEnum.RACING) { numberOfActiveBoats++; } } return numberOfActiveBoats; } /** * Returns a list of boats in the race. * @return List of boats in the race. */ public List getBoats() { return boats; } /** * Initialises the wind bearing with the value of the windBaselineBearing. */ protected void initialiseWindDirection() { //Set the starting bearing. this.windDirection = Bearing.fromDegrees(MockRace.windBaselineBearing.degrees()); } /** * Changes the wind direction randomly, while keeping it within [windLowerBound, windUpperBound]. */ protected void changeWindDirection() { //Randomly add or remove 0.5 degrees. int r = new Random().nextInt(changeWind) + 1; if (r == 1) { //Add 0.5 degrees to the wind bearing. this.windDirection.setDegrees(this.windDirection.degrees() + 0.5); } else if (r == 2) { //Minus 0.5 degrees from the wind bearing. this.windDirection.setDegrees(this.windDirection.degrees() - 0.5); } //Ensure that the wind is in the correct bounds. if (this.windDirection.degrees() > MockRace.windUpperBound.degrees()) { this.windDirection.setBearing(MockRace.windUpperBound); } else if (this.windDirection.degrees() < MockRace.windLowerBound.degrees()) { this.windDirection.setBearing(MockRace.windLowerBound); } } /** * Updates the boat's estimated time to next mark if positive * @param boat to estimate time given its velocity */ private void updateEstimatedTime(MockBoat boat) { double velocityToMark = boat.getCurrentSpeed() * cos(boat.getBearing().radians() - boat.calculateBearingToNextMarker().radians()) / Constants.KnotsToMMPerSecond; if (velocityToMark > 0) { long timeFromNow = (long) (1000 * boat.calculateDistanceToNextMarker() / velocityToMark); boat.setEstimatedTime(this.raceClock.getCurrentTimeMilli() + timeFromNow); } } }