package mock.model; import javafx.animation.AnimationTimer; import mock.xml.*; import network.Messages.BoatLocation; import network.Messages.BoatStatus; import network.Messages.Enums.BoatStatusEnum; import network.Messages.Enums.RaceStatusEnum; import shared.dataInput.BoatDataSource; import shared.dataInput.RaceDataSource; import shared.dataInput.RegattaDataSource; import shared.exceptions.BoatNotFoundException; import shared.enums.RoundingType; import shared.model.*; import shared.model.Bearing; import shared.model.Race; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; 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; /** * Object used to generate changes in wind speed/direction. */ private WindGenerator windGenerator; /** * 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 polars The polars table to be used for boat simulation. * @param timeScale The timeScale for the race. See {@link Constants#RaceTimeScale}. * @param windGenerator The wind generator used for the race. */ public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, Polars polars, int timeScale, WindGenerator windGenerator) { super(boatDataSource, raceDataSource, regattaDataSource); this.scaleFactor = timeScale; this.boats = this.generateMockBoats(boatDataSource.getBoats(), raceDataSource.getParticipants(), polars); this.shrinkBoundary = GPSCoordinate.getShrinkBoundary(this.boundary); this.windGenerator = windGenerator; //Wind. this.setWind(windGenerator.generateBaselineWind()); this.colliderRegistry.addAllColliders(boats); } /** * 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; } /** * Updates the race time to a specified value, in milliseconds since the unix epoch. * @param currentTime Milliseconds since unix epoch. */ public void updateRaceTime(long currentTime) { this.raceClock.setUTCTime(currentTime); } /** * Updates the race status enumeration based on the current time. */ public void updateRaceStatusEnum() { //The millisecond duration of the race. Negative means it hasn't started, so we flip sign. 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); } } /** * Sets the status of all boats in the race to RACING. */ public void setBoatsStatusToRacing() { for (MockBoat boat : this.boats) { boat.setStatus(BoatStatusEnum.RACING); } } /** * Sets the estimated time at next mark for each boat to a specified time. This is used during the countdown timer to provide this value to boat before the race starts. * @param time The time to provide to each boat. */ public void setBoatsTimeNextMark(ZonedDateTime time) { for (MockBoat boat : this.boats) { boat.setEstimatedTimeAtNextMark(time); } } /** * Initialise the boats in the race. * This sets their starting positions and current legs. */ @Override public 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.setPosition(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(Long.MAX_VALUE); } } /** * 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. */ private 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; } /** * 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. */ public 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. */ public 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 && totalElapsedMilliseconds >= updatePeriodMilliseconds && boat.isSailsOut()) { checkPosition(boat, totalElapsedMilliseconds); setBoatSpeed(boat); //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); boat.setTimeSinceTackChange(boat.getTimeSinceTackChange() + updatePeriodMilliseconds); if (boat.getAutoVMG()) { newOptimalVMG(boat); } } else { boat.setCurrentSpeed(0); } this.updateEstimatedTime(boat); } private void newOptimalVMG(MockBoat boat) { long tackPeriod = 1000; if (boat.getTimeSinceTackChange() > tackPeriod) { //Calculate the new VMG. VMG newVMG = boat.getPolars().calculateVMG( this.getWindDirection(), this.getWindSpeed(), boat.calculateBearingToNextMarker(), Bearing.fromDegrees(0d), Bearing.fromDegrees(359.99999d)); //If the new vmg improves velocity, use it. if (improvesVelocity(boat, newVMG)) { boat.setVMG(newVMG); } } } private void setBoatSpeed(MockBoat boat) { VMG vmg = boat.getPolars().calculateVMG( this.getWindDirection(), this.getWindSpeed(), boat.getBearing(), Bearing.fromDegrees(boat.getBearing().degrees() - 1), Bearing.fromDegrees(boat.getBearing().degrees() + 1)); if (vmg.getSpeed() > 0) { boat.setCurrentSpeed(vmg.getSpeed()); } } /** * 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.getPosition()); 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 to be run on boats rounding marks on the port side * @param boat the boat that is rounding a mark * @param roundingChecks the checks to run * @param legBearing the direction of the leg */ private void boatRoundingCheckPort(MockBoat boat, List roundingChecks, Bearing legBearing) { //boats must pass all checks in order to round a mark //boolean for if boat has to/needs to pass through a gate boolean gateCheck = boat.getCurrentLeg().getEndCompoundMark().getMark2() == null || boat.isBetweenGate(boat.getCurrentLeg().getEndCompoundMark()); Mark roundingMark = boat.getCurrentLeg().getEndCompoundMark().getMarkForRounding(legBearing); switch (boat.getRoundingStatus()) { case 0://hasn't started rounding if (boat.isPortSide(roundingMark) && GPSCoordinate.passesLine(roundingMark.getPosition(), roundingChecks.get(0), boat.getPosition(), legBearing) && gateCheck && boat.isBetweenGate(roundingMark, Mark.tempMark(roundingChecks.get(0)))) { boat.increaseRoundingStatus(); if (boat.getCurrentLeg().getLegNumber() + 2 >= legs.size()){ //boat has finished race boat.increaseRoundingStatus(); } } break; case 1://has been parallel to the mark; if (boat.isPortSide(roundingMark) && GPSCoordinate.passesLine(roundingMark.getPosition(), roundingChecks.get(1), boat.getPosition(), Bearing.fromDegrees(legBearing.degrees() - 90)) &&//negative 90 from bearing because of port rounding boat.isBetweenGate(roundingMark, Mark.tempMark(roundingChecks.get(1)))) { boat.increaseRoundingStatus(); } break; case 2://has traveled 180 degrees around the mark //Move boat on to next leg. boat.resetRoundingStatus(); Leg nextLeg = this.legs.get(boat.getCurrentLeg().getLegNumber() + 1); boat.setCurrentLeg(nextLeg); break; } } /** * Checks to be run on boats rounding marks on the starboard side * @param boat the boat that is rounding a mark * @param roundingChecks the checks to run * @param legBearing the direction of the leg */ private void boatRoundingCheckStarboard(MockBoat boat, List roundingChecks, Bearing legBearing){ //boats must pass all checks in order to round a mark //boolean for if boat has to/needs to pass through a gate boolean gateCheck = boat.getCurrentLeg().getEndCompoundMark().getMark2() == null || boat.isBetweenGate(boat.getCurrentLeg().getEndCompoundMark()); Mark roundingMark = boat.getCurrentLeg().getEndCompoundMark().getMarkForRounding(legBearing); switch (boat.getRoundingStatus()) { case 0://hasn't started rounding if (boat.isStarboardSide(roundingMark) && GPSCoordinate.passesLine(roundingMark.getPosition(), roundingChecks.get(0), boat.getPosition(), legBearing) && gateCheck && boat.isBetweenGate(roundingMark, Mark.tempMark(roundingChecks.get(0)))) { boat.increaseRoundingStatus(); if (boat.getCurrentLeg().getLegNumber() + 2 >= legs.size()){ //boat has finished race boat.increaseRoundingStatus(); } } break; case 1://has been parallel to the mark if (boat.isStarboardSide(roundingMark) && GPSCoordinate.passesLine(roundingMark.getPosition(), roundingChecks.get(1), boat.getPosition(), Bearing.fromDegrees(legBearing.degrees() + 90)) && //positive 90 from bearing because of starboard rounding boat.isBetweenGate(roundingMark, Mark.tempMark(roundingChecks.get(1)))) { boat.increaseRoundingStatus(); } break; case 2://has traveled 180 degrees around the mark //Move boat on to next leg. boat.resetRoundingStatus(); Leg nextLeg = this.legs.get(boat.getCurrentLeg().getLegNumber() + 1); boat.setCurrentLeg(nextLeg); break; } } /** * 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 = boat.getCurrentLeg().getEndCompoundMark().getRoundingDistance(); //250 meters. if (boat.calculateDistanceToNextMarker() < epsilonNauticalMiles) { //Boat is within an acceptable distance from the mark. GPSCoordinate startDirectionLinePoint = boat.getCurrentLeg().getStartCompoundMark().getMark1Position(); GPSCoordinate endDirectionLinePoint = boat.getCurrentLeg().getEndCompoundMark().getMark1Position(); Bearing bearingOfDirectionLine = GPSCoordinate.calculateBearing(startDirectionLinePoint, endDirectionLinePoint); //use the direction line to create three invisible points that act as crossover lines a boat must cross //to round a mark double bearingToAdd; if (boat.getCurrentLeg().getEndCompoundMark().getRoundingType() == RoundingType.Port || boat.getCurrentLeg().getEndCompoundMark().getRoundingType() == RoundingType.SP){ bearingToAdd = 90; }else{ bearingToAdd = -90; } GPSCoordinate roundCheck1 = GPSCoordinate.calculateNewPosition(endDirectionLinePoint, epsilonNauticalMiles, Azimuth.fromDegrees(bearingOfDirectionLine.degrees() + bearingToAdd)); GPSCoordinate roundCheck2; try{ Leg nextLeg = legs.get(legs.indexOf(boat.getCurrentLeg()) + 1); GPSCoordinate startNextDirectionLinePoint = nextLeg.getStartCompoundMark().getMark1Position(); GPSCoordinate endNextDirectionLinePoint = nextLeg.getEndCompoundMark().getMark1Position(); Bearing bearingOfNextDirectionLine = GPSCoordinate.calculateBearing(startNextDirectionLinePoint, endNextDirectionLinePoint); roundCheck2 = GPSCoordinate.calculateNewPosition(endDirectionLinePoint, epsilonNauticalMiles, Azimuth.fromDegrees(bearingOfNextDirectionLine.degrees() + bearingToAdd)); }catch(NullPointerException e){ //this is caused by the last leg not being having a leg after it roundCheck2 = roundCheck1; } List roundingChecks = new ArrayList(Arrays.asList(roundCheck1, roundCheck2)); switch (boat.getCurrentLeg().getEndCompoundMark().getRoundingType()) { case SP://Not yet implemented so these gates will be rounded port side case Port: boatRoundingCheckPort(boat, roundingChecks, bearingOfDirectionLine); break; case PS://not yet implemented so these gates will be rounded starboard side case Starboard: boatRoundingCheckStarboard(boat, roundingChecks, bearingOfDirectionLine); break; } //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); } } } /** * 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; } /** * Returns a boat by sourceID. * @param sourceID The source ID the boat. * @return The boat. * @throws BoatNotFoundException Thrown if there is not boat with the specified sourceID. */ public MockBoat getBoat(int sourceID) throws BoatNotFoundException { for (MockBoat boat : boats) { if (boat.getSourceID() == sourceID) { return boat; } } throw new BoatNotFoundException("Boat with sourceID: " + sourceID + " was not found."); } /** * Changes the wind direction randomly, while keeping it within [windLowerBound, windUpperBound]. */ public void changeWindDirection() { Wind nextWind = windGenerator.generateNextWind(raceWind.getValue()); setWind(nextWind); } /** * 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) { //Calculate milliseconds until boat reaches mark. long timeFromNow = (long) (1000 * boat.calculateDistanceToNextMarker() / velocityToMark); //Calculate time at which it will reach mark. ZonedDateTime timeAtMark = this.raceClock.getCurrentTime().plus(timeFromNow, ChronoUnit.MILLIS); boat.setEstimatedTimeAtNextMark(timeAtMark); } } public List getCompoundMarks() { return compoundMarks; } }