diff --git a/.gitignore b/.gitignore index 9e503182..ede3b67f 100644 --- a/.gitignore +++ b/.gitignore @@ -92,6 +92,7 @@ nbactions.xml .idea/misc.xml .idea/compiler.xml .idea/modules.xml +.idea/codeStyleSettings.xml # Sensitive or high-churn files: .idea/dataSources.ids diff --git a/racevisionGame/src/main/java/mock/model/MockBoat.java b/racevisionGame/src/main/java/mock/model/MockBoat.java index a2f74dd7..c62779d6 100644 --- a/racevisionGame/src/main/java/mock/model/MockBoat.java +++ b/racevisionGame/src/main/java/mock/model/MockBoat.java @@ -22,6 +22,14 @@ public class MockBoat extends Boat { */ private long timeSinceTackChange = 0; + /** + * This stores the boats current status of rounding a mark + * 0: not started rounding + * 1: passed only first check + * 2: passed first and second check + */ + private Integer roundingStatus = 0; + /** * Stores whether the boat is on autoVMG or not */ @@ -67,10 +75,19 @@ public class MockBoat extends Boat { //Get the start and end points. GPSCoordinate currentPosition = this.getCurrentPosition(); - GPSCoordinate nextMarkerPosition = this.getCurrentLeg().getEndCompoundMark().getAverageGPSCoordinate(); + GPSCoordinate nextMarkerPosition; + + // if boat is at the finish + if (this.getCurrentLeg().getEndCompoundMark() == null) { + nextMarkerPosition = currentPosition; + } + else { + nextMarkerPosition = this.getCurrentLeg().getEndCompoundMark().getAverageGPSCoordinate(); + } //Calculate bearing. Bearing bearing = GPSCoordinate.calculateBearing(currentPosition, nextMarkerPosition); + return bearing; } @@ -196,6 +213,90 @@ public class MockBoat extends Boat { return distanceTravelledMeters; } + /** + * Check if a mark is on the port side of the boat + * @param mark mark to be passed + * @return true if mark is on port side + */ + public boolean isPortSide(Mark mark){ + Bearing towardsMark = GPSCoordinate.calculateBearing(this.getCurrentPosition(), mark.getPosition()); + if (towardsMark.degrees() > 315 || towardsMark.degrees() <= 45){ + //south quadrant + return this.getBearing().degrees() <= 180; + } else if(towardsMark.degrees() > 45 && towardsMark.degrees() <= 135){ + //west quadrant + return (this.getBearing().degrees() <= 270 && this.getBearing().degrees() >= 90); + }else if(towardsMark.degrees() > 135 && towardsMark.degrees() <= 225){ + //north quadrant + return this.getBearing().degrees() >= 180; + }else if(towardsMark.degrees() > 225 && towardsMark.degrees() <= 315){ + //east quadrant + return (this.getBearing().degrees() <= 90 || this.getBearing().degrees() >= 270); + }else{ + //should not reach here + return false; + } + } + + /** + * Check if a mark is on the starboard side of the boat + * @param mark mark to be passed + * @return true if mark is on starboard side + */ + public boolean isStarboardSide(Mark mark){ + //if this boat is lower than the mark check which way it is facing + Bearing towardsMark = GPSCoordinate.calculateBearing(this.getCurrentPosition(), mark.getPosition()); + if (towardsMark.degrees() > 315 || towardsMark.degrees() <= 45){ + //south quadrant + return !(this.getBearing().degrees() <= 180); + } else if(towardsMark.degrees() > 45 && towardsMark.degrees() <= 135){ + //west quadrant + return !(this.getBearing().degrees() <= 270 && this.getBearing().degrees() >= 90); + }else if(towardsMark.degrees() > 135 && towardsMark.degrees() <= 225){ + //north quadrant + return !(this.getBearing().degrees() >= 180); + }else if(towardsMark.degrees() > 225 && towardsMark.degrees() <= 315){ + //east quadrant + return !(this.getBearing().degrees() <= 90 || this.getBearing().degrees() >= 270); + }else{ + //should not reach here + return false; + } + } + + /** + * Used to check if this boat is between a gate + * @param gate the gate to be checked + * @return true if the boat is between two marks that make up a gate + */ + public boolean isBetweenGate(CompoundMark gate){ + return (this.isPortSide(gate.getMark1()) && this.isStarboardSide(gate.getMark2())) || + (this.isStarboardSide(gate.getMark1()) && this.isPortSide(gate.getMark2())); + } + + /** + * Used to check if this boat is between a two marks + * @param mark1 the first mark + * @param mark2 the second mark + * @return true if the boat is between two marks + */ + public boolean isBetweenGate(Mark mark1, Mark mark2){ + return (this.isPortSide(mark1) && this.isStarboardSide(mark2)) || + (this.isStarboardSide(mark1) && this.isPortSide(mark2)); + } + + public Integer getRoundingStatus() { + return Integer.valueOf(roundingStatus); + } + + public void increaseRoundingStatus() { + this.roundingStatus++; + } + + public void resetRoundingStatus() { + this.roundingStatus = 0; + } + public boolean isAutoVMG() { return autoVMG; } @@ -203,4 +304,8 @@ public class MockBoat extends Boat { public void setAutoVMG(boolean autoVMG) { this.autoVMG = autoVMG; } + + public boolean getAutoVMG(){ + return autoVMG; + } } diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index 3d895478..84e33daf 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -1,14 +1,14 @@ package mock.model; import network.Messages.Enums.BoatStatusEnum; +import network.Messages.Enums.RaceStatusEnum; import network.Messages.LatestMessages; import shared.dataInput.BoatDataSource; import shared.dataInput.RaceDataSource; -import network.Messages.Enums.RaceStatusEnum; import shared.dataInput.RegattaDataSource; import shared.exceptions.BoatNotFoundException; +import shared.enums.RoundingType; import shared.model.*; -import shared.model.Bearing; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; @@ -315,6 +315,8 @@ public class MockRace extends Race { if (!finish && totalElapsedMilliseconds >= updatePeriodMilliseconds) { + checkPosition(boat, totalElapsedMilliseconds); + if (boat.getCurrentSpeed() == 0) { newOptimalVMG(boat); boat.setBearing(boat.calculateBearingToNextMarker()); @@ -339,11 +341,11 @@ public class MockRace extends Race { this.updateEstimatedTime(boat); } - + checkPosition(boat, totalElapsedMilliseconds); } private void newOptimalVMG(MockBoat boat) { - long tackPeriod = 15000; + long tackPeriod = 1000; if (boat.getTimeSinceTackChange() > tackPeriod) { //Calculate the new VMG. @@ -367,13 +369,258 @@ public class MockRace extends Race { this.getWindDirection(), this.getWindSpeed(), boat.getBearing(), - boat.getBearing(), - 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.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 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.getCurrentPosition(), 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.getCurrentPosition(), + 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.getCurrentPosition(), 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.getCurrentPosition(), 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. @@ -456,4 +703,5 @@ public class MockRace extends Race { } + } diff --git a/racevisionGame/src/main/java/mock/model/SplitTODO.java b/racevisionGame/src/main/java/mock/model/SplitTODO.java new file mode 100644 index 00000000..20f14973 --- /dev/null +++ b/racevisionGame/src/main/java/mock/model/SplitTODO.java @@ -0,0 +1,555 @@ +//package mock.model; +// +//import javafx.animation.AnimationTimer; +//import network.Messages.BoatLocation; +//import network.Messages.BoatStatus; +//import network.Messages.Enums.BoatStatusEnum; +//import network.Messages.Enums.RaceStatusEnum; +//import network.Messages.LatestMessages; +//import network.Messages.RaceStatus; +//import network.Utils.AC35UnitConverter; +//import shared.dataInput.BoatDataSource; +//import shared.dataInput.RaceDataSource; +//import shared.dataInput.RegattaDataSource; +//import shared.model.*; +// +//import java.time.ZonedDateTime; +//import java.time.temporal.ChronoUnit; +//import java.util.ArrayList; +//import java.util.Iterator; +//import java.util.List; +//import java.util.Map; +// +//import static java.lang.Math.cos; +// +///** +// * Unused class, copy of MockRace so methods can be deleted once they are moved (more of a checklist) +// */ +//public class SplitTODO { +// +// +// /** +// * 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 { +// +// /** +// * 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; +// } +// +// +// +// /** +// * 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 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); +// +// } +// +// +// } +// +// /** +// * 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.getEstimatedTimeAtNextMark().toInstant().toEpochMilli() ); +// +// 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.getWindDirection().degrees()); +// int windSpeedInt = (int) (this.getWindSpeed() * 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); +// } +// } +// +// +// /** +// * 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. +// */ +// private void setBoatsTimeNextMark(ZonedDateTime time) { +// +// for (MockBoat boat : this.boats) { +// boat.setEstimatedTimeAtNextMark(time); +// } +// } +// +// +// /** +// * 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(); +// +// //Provide boat's with an estimated time at next mark until the race starts. +// setBoatsTimeNextMark(raceClock.getCurrentTime()); +// +// //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++; +// } +// }; +// +// +// /** +// * Calculates a boat's VMG. +// * @param boat The boat to calculate VMG for. +// * @return VMG for the specified boat. +// */ +// private VMG calculateVMG(MockBoat boat) { +// +// +// //Find the VMG inside these bounds. +// VMG bestVMG = boat.getPolars().calculateVMG(this.getWindDirection(), this.getWindSpeed(), boat.calculateBearingToNextMarker(), Bearing.fromDegrees(0d), Bearing.fromDegrees(359.99999d)); +// +// +// 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); +// +// long tackPeriod = 15000; +// if (boat.getTimeSinceTackChange() > tackPeriod) { +// //Calculate the new VMG. +// VMG newVMG = this.calculateVMG(boat); +// +// +// //If the new vmg improves velocity, use it. +// if (improvesVelocity(boat, newVMG)) { +// boat.setVMG(newVMG); +// +// } +// } +// +// this.updateEstimatedTime(boat); +// +// +// //Check the boats position (update leg and stuff). +// this.checkPosition(boat, totalElapsedMilliseconds); +// +// } +// +// } +// +// +// /** +// * 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); +// +// } +// +// } +// +// } +// +// /** +// * 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); +// } +// +// } +// } +//} diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java index 8dcf1c48..50023719 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java @@ -2,34 +2,49 @@ package mock.model.commandFactory; import mock.model.MockBoat; import mock.model.MockRace; +import shared.model.Bearing; /** - * Created by David on 2/08/2017. + * Command class for tacking and gybing */ public class TackGybeCommand implements Command { private MockRace race; private MockBoat boat; + /** + * Constructor for class + * @param race mock race + * @param boat mock boat to update + */ public TackGybeCommand(MockRace race, MockBoat boat) { this.race = race; this.boat = boat; } - //The refactoring of MockRace will require changes to be made @Override public void execute() { + double boatAngle = boat.getBearing().degrees(); + double windAngle =race.getWindDirection().degrees(); + double differenceAngle = calcDistance(boatAngle, windAngle); + double angleA = windAngle + differenceAngle; + double angleB = windAngle - differenceAngle; + if(angleA % 360 == boatAngle){ + boat.setBearing(Bearing.fromDegrees(angleB)); + } else { + boat.setBearing(Bearing.fromDegrees(angleA)); + } + } - boat.setAutoVMG(false); - - /*VMG newVMG = boat.getPolars().calculateVMG( - race.getWindDirection(), - race.getWindSpeed(), - boat.calculateBearingToNextMarker(), - Bearing.fromDegrees(0d), - Bearing.fromDegrees(359.99999d)); - VMG boatVMG = new VMG(boat.getCurrentSpeed(), boat.getBearing()); - if(race.improvesVelocity(boatVMG, newVMG, boat.calculateBearingToNextMarker())){ - boat.setVMG(newVMG); - }*/ + /** + * Method to calculate smallest angle between 2 angles + * @param degreeA first angle degree + * @param degreeB second angle degree + * @return the calculated smallest angle + */ + public double calcDistance(double degreeA, double degreeB){ + double phi = Math.abs(degreeB - degreeA) % 360; + return phi > 180 ? 360 - phi : phi; } + } + diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java index 1a1eeda5..8c7d2043 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java @@ -4,27 +4,30 @@ import mock.model.MockBoat; import mock.model.MockRace; /** - * Created by David on 2/08/2017. + * Command class for autoVMG */ public class VMGCommand implements Command { private MockRace race; private MockBoat boat; + /** + * Constructor for class + * @param race mock race + * @param boat mock boat to update + */ public VMGCommand(MockRace race, MockBoat boat) { this.race = race; this.boat = boat; } - //The refactoring of MockRace will require changes to be made @Override public void execute() { - boat.setAutoVMG(true); - /*VMG newVMG = boat.getPolars().calculateVMG( - race.getWindDirection(), - race.getWindSpeed(), - boat.calculateBearingToNextMarker(), - Bearing.fromDegrees(0d), - Bearing.fromDegrees(359.99999d)); - boat.setVMG(newVMG);*/ + if (boat.getAutoVMG()){ + boat.setAutoVMG(false); + System.out.println("Auto VMG off!"); + } else { + boat.setAutoVMG(true); + System.out.println("Auto VMG on!"); + } } } diff --git a/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java b/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java index 7e61b3de..135cd988 100644 --- a/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java +++ b/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java @@ -5,6 +5,7 @@ import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import shared.enums.RoundingType; import shared.enums.XMLFileType; import shared.exceptions.InvalidRaceDataException; import shared.exceptions.XMLReaderException; @@ -313,6 +314,8 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource { return element.getAttribute("Name"); } + private String getCompoundMarkRounding(Element element){return element.getAttribute("Rounding");} + /** * Populates list of legs given CompoundMarkSequence element and referenced CompoundMark elements. @@ -331,12 +334,19 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource { //Gets the ID number of this corner element. int cornerID = getCompoundMarkID(cornerElement); + //gets the Rounding of this corner element + String cornerRounding = getCompoundMarkRounding(cornerElement); + //Gets the CompoundMark associated with this corner. CompoundMark lastCompoundMark = this.compoundMarkMap.get(cornerID); //The name of the leg is the name of the first compoundMark in the leg. String legName = lastCompoundMark.getName(); + //Sets the rounding type of this compound mark + + lastCompoundMark.setRoundingType(RoundingType.getValueOf(cornerRounding)); + //For each following corner, create a leg between cornerN and cornerN+1. for(int i = 1; i < corners.getLength(); i++) { @@ -346,9 +356,15 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource { //Gets the ID number of this corner element. cornerID = getCompoundMarkID(cornerElement); + //gets the Rounding of this corner element + cornerRounding = getCompoundMarkRounding(cornerElement); + //Gets the CompoundMark associated with this corner. CompoundMark currentCompoundMark = this.compoundMarkMap.get(cornerID); + //Sets the rounding type of this compound mark + currentCompoundMark.setRoundingType(RoundingType.valueOf(cornerRounding)); + //Create a leg from these two adjacent compound marks. Leg leg = new Leg(legName, lastCompoundMark, currentCompoundMark, i - 1); legs.add(leg); diff --git a/racevisionGame/src/main/java/shared/enums/RoundingType.java b/racevisionGame/src/main/java/shared/enums/RoundingType.java new file mode 100644 index 00000000..8f8e719a --- /dev/null +++ b/racevisionGame/src/main/java/shared/enums/RoundingType.java @@ -0,0 +1,49 @@ +package shared.enums; + +/** + * Enum for the types of rounding that can be done + */ +public enum RoundingType { + /** + * This is means it must be rounded port side + */ + Port, + + /** + * This is means it must be rounded starboard side + */ + Starboard, + + /** + * The boat within the compound mark with the SeqID + * of 1 should be rounded to starboard and the boat + * within the compound mark with the SeqID of 2 should + * be rounded to port. + */ + SP, + + /** + * The boat within the compound mark with the SeqID + * of 1 should be rounded to port and the boat + * within the compound mark with the SeqID of 2 should + * be rounded to starboard. + * + * opposite of SP + */ + PS; + + public static RoundingType getValueOf(String value) { + switch (value) { + case "Port": + return RoundingType.Port; + case "Starboard": + return RoundingType.Starboard; + case "SP": + return RoundingType.Port; + case "PS": + return RoundingType.Starboard; + default: + return null; + } + } +} diff --git a/racevisionGame/src/main/java/shared/model/CompoundMark.java b/racevisionGame/src/main/java/shared/model/CompoundMark.java index b9f45753..12d88cfc 100644 --- a/racevisionGame/src/main/java/shared/model/CompoundMark.java +++ b/racevisionGame/src/main/java/shared/model/CompoundMark.java @@ -1,6 +1,8 @@ package shared.model; +import shared.enums.RoundingType; + /** * Represents a compound mark - that is, either one or two individual marks which form a single compound mark. */ @@ -31,6 +33,11 @@ public class CompoundMark { */ private GPSCoordinate averageGPSCoordinate; + /** + * The side that the mark must be rounded on + */ + private RoundingType roundingType; + /** * Constructs a compound mark from a single mark. @@ -141,4 +148,120 @@ public class CompoundMark { return averageCoordinate; } + + /** + * Used to find how far apart the marks that make up this gate are + * If this compound mark is only one point return base length of 250m + * @return the acceptable distance to round a mark + */ + public double getRoundingDistance(){ + if (mark2 != null){ + return GPSCoordinate.calculateDistanceMeters(mark1.getPosition(), mark2.getPosition()); + }else{ + return 400; + } + } + + /** + * Used to get how this mark should be rounded + * @return rounding type for mark + */ + public RoundingType getRoundingType() { + return roundingType; + } + + /** + * Used to set the type of rounding for this mark + * @param roundingType rounding type to set + */ + public void setRoundingType(RoundingType roundingType) { + this.roundingType = roundingType; + } + + /** + * Used to find the mark that is to be rounded at a gate when approaching from the south + * will also give the single mark if there is only one + * @param bearing the bearing a boat will approach form + * @return the mark to round + */ + public Mark getMarkForRounding(Bearing bearing){ + Mark westMostMark; + Mark eastMostMark; + Mark northMostMark; + Mark southMostMark; + + //check to see if there are two marks + if (mark2 == null){ + return mark1; + } + + //finds the mark furthest west and east + if(this.getMark1Position().getLatitude() > this.getMark2Position().getLatitude()){ + westMostMark = this.mark1; + eastMostMark = this.mark2; + }else{ + westMostMark = this.mark2; + eastMostMark = this.mark1; + } + + //finds the mark furthest north and south + if(this.getMark1Position().getLongitude() > this.getMark2Position().getLongitude()){ + northMostMark = this.mark1; + southMostMark = this.mark2; + }else{ + northMostMark = this.mark2; + southMostMark = this.mark1; + } + + if (bearing.degrees() > 315 || bearing.degrees() <= 45){ + //north + switch (this.getRoundingType()){ + case SP: + case Port: + return westMostMark; + case PS: + case Starboard: + return eastMostMark; + default:return null; + } + }else if(bearing.degrees() > 45 && bearing.degrees() <= 135){ + //east + switch (this.getRoundingType()){ + case SP: + case Port: + return northMostMark; + case PS: + case Starboard: + return southMostMark; + default:return null; + } + }else if(bearing.degrees() > 135 && bearing.degrees() <= 225){ + //south + switch (this.getRoundingType()){ + case SP: + case Port: + return eastMostMark; + case PS: + case Starboard: + return westMostMark; + default:return null; + } + }else if(bearing.degrees() > 225 && bearing.degrees() <= 315){ + //west + switch (this.getRoundingType()){ + case SP: + case Port: + return southMostMark; + case PS: + case Starboard: + return northMostMark; + default:return null; + } + }else{ + return null; + } + + } + + } diff --git a/racevisionGame/src/main/java/shared/model/GPSCoordinate.java b/racevisionGame/src/main/java/shared/model/GPSCoordinate.java index ee5eaaff..8bd1511d 100644 --- a/racevisionGame/src/main/java/shared/model/GPSCoordinate.java +++ b/racevisionGame/src/main/java/shared/model/GPSCoordinate.java @@ -142,7 +142,7 @@ public class GPSCoordinate { * @param coordinate The coordinate to test. * @return true if a line from the point intersects the two boundary points */ - private static boolean intersects(GPSCoordinate boundaryA, GPSCoordinate boundaryB, GPSCoordinate coordinate) { + public static boolean intersects(GPSCoordinate boundaryA, GPSCoordinate boundaryB, GPSCoordinate coordinate) { double boundaryALat = boundaryA.getLatitude(); double boundaryALon = boundaryA.getLongitude(); double boundaryBLat = boundaryB.getLatitude(); @@ -170,6 +170,46 @@ public class GPSCoordinate { } + /** + * Checks to see if a point passes or lands on a line + * @param linePointA first point for a line + * @param linePointB second point for a line + * @param point point to check + * @param directionBearing direction of the correct side of line + * @return true if on the correct side + */ + public static boolean passesLine(GPSCoordinate linePointA, GPSCoordinate linePointB, GPSCoordinate point, Bearing directionBearing) { + double d = lineCheck(linePointA, linePointB, point);//this gives a number < 0 for one side and > 0 for an other + + //to find if the side is the correct one + //compare with point that is known on the correct side + GPSCoordinate pointForComparison = GPSCoordinate.calculateNewPosition(linePointA, + 250, Azimuth.fromDegrees(directionBearing.degrees())); + double d2 = lineCheck(linePointA, linePointB, pointForComparison); + + return (d > 0 && d2 > 0) || (d < 0 && d2 < 0) || d == 0; + } + + /** + * returns a double that is positive or negative based on which + * side of the line it is on. returns 0 if it is on the line + * @param linePointA first point to make up the line + * @param linePointB second point to make up the line + * @param point the point to check + * @return greater than 0 for one side, less than 0 for another + */ + private static double lineCheck(GPSCoordinate linePointA, GPSCoordinate linePointB, GPSCoordinate point) { + double linePointALat = linePointA.getLatitude(); + double linePointALon = linePointA.getLongitude(); + double linePointBLat = linePointB.getLatitude(); + double linePointBLon = linePointB.getLongitude(); + double pointLat = point.getLatitude(); + double pointLon = point.getLongitude(); + + double d1 = (pointLat - linePointALat) * (linePointBLon - linePointALon); + double d2 = (pointLon - linePointALon) * (linePointBLat - linePointALat); + return d1 - d2; //this gives a number < 0 for one side and > 0 for an other + } /** diff --git a/racevisionGame/src/main/java/shared/model/Mark.java b/racevisionGame/src/main/java/shared/model/Mark.java index 5781861a..2b01184a 100644 --- a/racevisionGame/src/main/java/shared/model/Mark.java +++ b/racevisionGame/src/main/java/shared/model/Mark.java @@ -22,7 +22,6 @@ public class Mark { private GPSCoordinate position; - /** * Constructs a mark with a given source ID, name, and position. * @param sourceID The source ID of the mark. @@ -35,6 +34,15 @@ public class Mark { this.position = position; } + /** + * Used to create marks that are not visible in the race + * @param position position of the mark + * @return the new mark + */ + public static Mark tempMark(GPSCoordinate position){ + return new Mark(-1, "Hidden Mark", position); + } + /** * Returns the name of the mark. diff --git a/racevisionGame/src/main/java/shared/model/Race.java b/racevisionGame/src/main/java/shared/model/Race.java index 75e2cf2c..1a366bfb 100644 --- a/racevisionGame/src/main/java/shared/model/Race.java +++ b/racevisionGame/src/main/java/shared/model/Race.java @@ -342,6 +342,13 @@ public abstract class Race { return lastFps; } + /** + * Returns the legs of this race + * @return list of legs + */ + public List getLegs() { + return legs; + } /** * Increments the FPS counter, and adds timePeriod milliseconds to our FPS reset timer. diff --git a/racevisionGame/src/main/java/visualiser/gameController/InputChecker.java b/racevisionGame/src/main/java/visualiser/gameController/InputChecker.java index 34a7d544..057b5721 100644 --- a/racevisionGame/src/main/java/visualiser/gameController/InputChecker.java +++ b/racevisionGame/src/main/java/visualiser/gameController/InputChecker.java @@ -7,8 +7,6 @@ import visualiser.gameController.Keys.KeyFactory; import java.util.HashMap; -import static javafx.application.Application.launch; - /** * Class for checking what keys are currently being used */ @@ -28,7 +26,7 @@ public class InputChecker { ControlKey controlKey = keyFactory.getKey(codeString); if (controlKey != null) { controlKey.onAction(); - System.out.println(controlKey.toString() + " is Pressed."); +// System.out.println(controlKey.toString() + " is Pressed."); } currentlyActiveKeys.put(codeString, true); } @@ -39,7 +37,7 @@ public class InputChecker { ControlKey controlKey = keyFactory.getKey(codeString); if (controlKey != null) { controlKey.onRelease(); - System.out.println(controlKey.toString() + " is Released."); +// System.out.println(controlKey.toString() + " is Released."); } currentlyActiveKeys.remove(event.getCode().toString()); }); @@ -51,7 +49,7 @@ public class InputChecker { ControlKey controlKey = keyFactory.getKey(key); if (controlKey != null){ controlKey.onHold(); - System.out.println(controlKey.toString() + " is Held."); +// System.out.println(controlKey.toString() + " is Held."); } } // for (String key : InputKeys.stringKeysMap.keySet()){ diff --git a/racevisionGame/src/main/java/visualiser/model/Annotations.java b/racevisionGame/src/main/java/visualiser/model/Annotations.java index 09e3831a..54976c35 100644 --- a/racevisionGame/src/main/java/visualiser/model/Annotations.java +++ b/racevisionGame/src/main/java/visualiser/model/Annotations.java @@ -40,6 +40,7 @@ public class Annotations { private static String pathCheckAnno = "showBoatPath"; private static String timeCheckAnno = "showTime"; private static String estTimeCheckAnno = "showEstTime"; + private static String guideLineAnno = "showGuideline"; // string values match the fx:id value of radio buttons private static String noBtn = "noBtn"; @@ -160,6 +161,16 @@ public class Annotations { raceMap.draw(); } }); + + //listener to show estimated time for annotation + checkBoxes.get(guideLineAnno).selectedProperty() + .addListener((ov, old_val, new_val) -> { + if (old_val != new_val) { + raceMap.toggleGuideLine(); + storeCurrentAnnotationState(guideLineAnno, new_val); + raceMap.draw(); + } + }); } /** diff --git a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java index 5a0243f6..b141eb63 100644 --- a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java @@ -6,9 +6,8 @@ import javafx.scene.paint.Paint; import javafx.scene.transform.Rotate; import network.Messages.Enums.BoatStatusEnum; import shared.dataInput.RaceDataSource; -import shared.model.GPSCoordinate; -import shared.model.Mark; -import shared.model.RaceClock; +import shared.enums.RoundingType; +import shared.model.*; import java.util.ArrayList; import java.util.List; @@ -44,6 +43,7 @@ public class ResizableRaceCanvas extends ResizableCanvas { private boolean annoPath = true; private boolean annoEstTime = true; private boolean annoTimeSinceLastMark = true; + private boolean annoGuideLine = false; @@ -111,6 +111,13 @@ public class ResizableRaceCanvas extends ResizableCanvas { annoSpeed = !annoSpeed; } + /** + * Toggle the guideline annotation + */ + public void toggleGuideLine() { + annoGuideLine = !annoGuideLine; + } + @@ -497,6 +504,11 @@ public class ResizableRaceCanvas extends ResizableCanvas { //Race boundary. drawBoundary(); + //Guiding Line + if (annoGuideLine){ + drawRaceLine(); + } + //Boats. drawBoats(); @@ -505,6 +517,139 @@ public class ResizableRaceCanvas extends ResizableCanvas { } + /** + * draws a transparent line around the course that shows the paths boats must travel + */ + public void drawRaceLine(){ + List legs = this.visualiserRace.getLegs(); + GPSCoordinate legStartPoint = legs.get(0).getStartCompoundMark().getAverageGPSCoordinate(); + GPSCoordinate nextStartPoint; + for (int i = 0; i < legs.size() -1; i++) { + nextStartPoint = drawLineRounding(legs, i, legStartPoint); + legStartPoint = nextStartPoint; + } + } + + /** + * Draws a line around a course that shows where boats need to go. This method + * draws the line leg by leg + * @param legs the legs of a race + * @param index the index of the current leg to use + * @return the end point of the current leg that has been drawn + */ + private GPSCoordinate drawLineRounding(List legs, int index, GPSCoordinate legStartPoint){ + GPSCoordinate startDirectionLinePoint; + GPSCoordinate endDirectionLinePoint; + Bearing bearingOfDirectionLine; + + GPSCoordinate startNextDirectionLinePoint; + GPSCoordinate endNextDirectionLinePoint; + Bearing bearingOfNextDirectionLine; + + //finds the direction of the current leg as a bearing + startDirectionLinePoint = legStartPoint; + GPSCoordinate tempEndDirectionLinePoint = legs.get(index).getEndCompoundMark().getAverageGPSCoordinate(); + + bearingOfDirectionLine = GPSCoordinate.calculateBearing(startDirectionLinePoint, tempEndDirectionLinePoint); + + //after finding the initial bearing pick the mark used for rounding + endDirectionLinePoint = legs.get(index).getEndCompoundMark().getMarkForRounding(bearingOfDirectionLine).getPosition(); + bearingOfDirectionLine = GPSCoordinate.calculateBearing(startDirectionLinePoint, endDirectionLinePoint); + + //finds the direction of the next leg as a bearing + if (index < legs.size() -2){ // not last leg + startNextDirectionLinePoint = legs.get(index + 1).getStartCompoundMark().getMark1Position(); + endNextDirectionLinePoint = legs.get(index + 1).getEndCompoundMark().getMark1Position(); + bearingOfNextDirectionLine = GPSCoordinate.calculateBearing(startNextDirectionLinePoint, endNextDirectionLinePoint); + + double degreesToAdd; + //find which side is need to be used + if (legs.get(index).getEndCompoundMark().getRoundingType() == RoundingType.Port || + legs.get(index).getEndCompoundMark().getRoundingType() == RoundingType.SP){ + degreesToAdd = 90; + }else{ + degreesToAdd = -90; + } + + //use the direction line to find a point parallel to it by the mark + GPSCoordinate pointToStartCurve = GPSCoordinate.calculateNewPosition(endDirectionLinePoint, + 100, Azimuth.fromDegrees(bearingOfDirectionLine.degrees()+degreesToAdd)); + + //use the direction line to find a point to curve too + GPSCoordinate pointToEndCurve = GPSCoordinate.calculateNewPosition(endDirectionLinePoint, + 100, Azimuth.fromDegrees(bearingOfNextDirectionLine.degrees()+degreesToAdd)); + + //use the curve points to find the two control points for the bezier curve + GPSCoordinate controlPoint; + GPSCoordinate controlPoint2; + Bearing bearingOfCurveLine = GPSCoordinate.calculateBearing(pointToStartCurve, pointToEndCurve); + if ((bearingOfDirectionLine.degrees() - bearingOfNextDirectionLine.degrees() +360)%360< 145){ + //small turn + controlPoint = GPSCoordinate.calculateNewPosition(pointToStartCurve, + 50, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+(degreesToAdd/2))); + controlPoint2 = controlPoint; + }else{ + //large turn + controlPoint = GPSCoordinate.calculateNewPosition(pointToStartCurve, + 150, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+degreesToAdd)); + controlPoint2 = GPSCoordinate.calculateNewPosition(pointToEndCurve, + 150, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+degreesToAdd)); + } + + + //change all gps into graph coordinate + GraphCoordinate startPath = this.map.convertGPS(startDirectionLinePoint); + GraphCoordinate curvePoint = this.map.convertGPS(pointToStartCurve); + GraphCoordinate curvePointEnd = this.map.convertGPS(pointToEndCurve); + GraphCoordinate c1 = this.map.convertGPS(controlPoint); + GraphCoordinate c2 = this.map.convertGPS(controlPoint2); + + gc.setLineWidth(2); + gc.setStroke(Color.MEDIUMAQUAMARINE); + + gc.beginPath(); + gc.moveTo(startPath.getX(), startPath.getY()); + gc.lineTo(curvePoint.getX(), curvePoint.getY()); + drawArrowHead(startDirectionLinePoint, pointToStartCurve); + gc.bezierCurveTo(c1.getX(), c1.getY(), c2.getX(), c2.getY(), curvePointEnd.getX(), curvePointEnd.getY()); + gc.stroke(); + gc.closePath(); + gc.save(); + + return pointToEndCurve; + }else{//last leg so no curve + GraphCoordinate startPath = this.map.convertGPS(legStartPoint); + GraphCoordinate endPath = this.map.convertGPS(legs.get(index).getEndCompoundMark().getAverageGPSCoordinate()); + + gc.beginPath(); + gc.moveTo(startPath.getX(), startPath.getY()); + gc.lineTo(endPath.getX(), endPath.getY()); + gc.stroke(); + gc.closePath(); + gc.save(); + drawArrowHead(legStartPoint, legs.get(index).getEndCompoundMark().getAverageGPSCoordinate()); + return null; + } + } + + private void drawArrowHead(GPSCoordinate start, GPSCoordinate end){ + + GraphCoordinate lineStart = this.map.convertGPS(start); + GraphCoordinate lineEnd = this.map.convertGPS(end); + + double arrowAngle = Math.toRadians(45.0); + double arrowLength = 10.0; + double dx = lineStart.getX() - lineEnd.getX(); + double dy = lineStart.getY() - lineEnd.getY(); + double angle = Math.atan2(dy, dx); + double x1 = Math.cos(angle + arrowAngle) * arrowLength + lineEnd.getX(); + double y1 = Math.sin(angle + arrowAngle) * arrowLength + lineEnd.getY(); + + double x2 = Math.cos(angle - arrowAngle) * arrowLength + lineEnd.getX(); + double y2 = Math.sin(angle - arrowAngle) * arrowLength + lineEnd.getY(); + gc.strokeLine(lineEnd.getX(), lineEnd.getY(), x1, y1); + gc.strokeLine(lineEnd.getX(), lineEnd.getY(), x2, y2); + } @@ -516,7 +661,6 @@ public class ResizableRaceCanvas extends ResizableCanvas { * @see TrackPoint */ private void drawTrack(VisualiserBoat boat) { - //Check that track points are enabled. if (this.annoPath) { diff --git a/racevisionGame/src/main/resources/mock/mockXML/raceTest.xml b/racevisionGame/src/main/resources/mock/mockXML/raceTest.xml index 51199001..edd634dc 100644 --- a/racevisionGame/src/main/resources/mock/mockXML/raceTest.xml +++ b/racevisionGame/src/main/resources/mock/mockXML/raceTest.xml @@ -13,12 +13,12 @@ - - - - - - + + + + + + diff --git a/racevisionGame/src/main/resources/visualiser/scenes/race.fxml b/racevisionGame/src/main/resources/visualiser/scenes/race.fxml index e6544cad..718e65ff 100644 --- a/racevisionGame/src/main/resources/visualiser/scenes/race.fxml +++ b/racevisionGame/src/main/resources/visualiser/scenes/race.fxml @@ -1,5 +1,9 @@ + + + + @@ -22,7 +26,7 @@ - + @@ -46,16 +50,17 @@ - -