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/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index f0783fc0..0d95e1c1 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -5,7 +5,6 @@ import mock.exceptions.EventConstructionException; import mock.model.*; import mock.model.commandFactory.CompositeCommand; import network.Messages.LatestMessages; -import network.Messages.RaceSnapshot; import shared.dataInput.*; import shared.enums.XMLFileType; import shared.exceptions.InvalidBoatDataException; @@ -72,8 +71,6 @@ public class Event { */ public Event(boolean singlePlayer) throws EventConstructionException { - singlePlayer = false;//TEMP - String raceXMLFile = "mock/mockXML/raceTest.xml"; String boatsXMLFile = "mock/mockXML/boatTest.xml"; String regattaXMLFile = "mock/mockXML/regattaTest.xml"; diff --git a/racevisionGame/src/main/java/mock/model/MockBoat.java b/racevisionGame/src/main/java/mock/model/MockBoat.java index a2f74dd7..f15ec2f2 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,11 +213,96 @@ public class MockBoat extends Boat { return distanceTravelledMeters; } - public boolean isAutoVMG() { - return autoVMG; + /** + * 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 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..3879e3fc 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()); @@ -333,7 +335,7 @@ public class MockRace extends Race { boat.moveForwards(distanceTravelledMeters); boat.setTimeSinceTackChange(boat.getTimeSinceTackChange() + updatePeriodMilliseconds); - if (boat.isAutoVMG()) { + if (boat.getAutoVMG()) { newOptimalVMG(boat); } @@ -343,7 +345,7 @@ public class MockRace extends Race { } 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/RaceLogic.java b/racevisionGame/src/main/java/mock/model/RaceLogic.java index 7b314d0f..418ade17 100644 --- a/racevisionGame/src/main/java/mock/model/RaceLogic.java +++ b/racevisionGame/src/main/java/mock/model/RaceLogic.java @@ -40,20 +40,23 @@ public class RaceLogic implements RunnableWithFramePeriod { @Override public void run() { race.initialiseBoats(); - this.countdownTimer.start(); + + countdown(); + + raceLoop(); } /** * Countdown timer until race starts. */ - protected AnimationTimer countdownTimer = new AnimationTimer() { + private void countdown() { + long previousFrameTime = System.currentTimeMillis(); - long currentTime = System.currentTimeMillis(); + while (race.getRaceStatusEnum() != RaceStatusEnum.STARTED) { - @Override - public void handle(long arg0) { + long currentTime = System.currentTimeMillis(); //Update race time. race.updateRaceTime(currentTime); @@ -72,47 +75,28 @@ public class RaceLogic implements RunnableWithFramePeriod { race.changeWindDirection(); - - if (race.getRaceStatusEnum() == RaceStatusEnum.STARTED) { race.setBoatsStatusToRacing(); - raceTimer.start(); - this.stop(); } - //Update the animations timer's time. - currentTime = System.currentTimeMillis(); + waitForFramePeriod(previousFrameTime, currentTime, 50); + previousFrameTime = currentTime; + } - }; + } /** * Timer that runs for the duration of the race, until all boats finish. */ - private AnimationTimer raceTimer = new AnimationTimer() { + private void raceLoop() { - /** - * Start time of loop, in milliseconds. - */ - long timeRaceStarted = System.currentTimeMillis(); + long previousFrameTime = System.currentTimeMillis(); - /** - * Current time during a loop iteration. - */ - long currentTime = System.currentTimeMillis(); - - /** - * The time of the previous frame, in milliseconds. - */ - long lastFrameTime = timeRaceStarted; - - long framePeriod = currentTime - lastFrameTime; - - @Override - public void handle(long arg0) { + while (race.getRaceStatusEnum() != RaceStatusEnum.FINISHED) { //Get the current time. - currentTime = System.currentTimeMillis(); + long currentTime = System.currentTimeMillis(); //Execute commands from clients. commands.execute(); @@ -124,7 +108,7 @@ public class RaceLogic implements RunnableWithFramePeriod { if (race.getNumberOfActiveBoats() != 0) { //Get the time period of this frame. - framePeriod = currentTime - lastFrameTime; + long framePeriod = currentTime - previousFrameTime; //For each boat, we update its position, and generate a BoatLocationMessage. for (MockBoat boat : race.getBoats()) { @@ -141,7 +125,6 @@ public class RaceLogic implements RunnableWithFramePeriod { //Otherwise, the race is over! raceFinished.start(); race.setRaceStatusEnum(RaceStatusEnum.FINISHED); - this.stop(); } if (race.getNumberOfActiveBoats() != 0) { @@ -152,10 +135,13 @@ public class RaceLogic implements RunnableWithFramePeriod { server.parseSnapshot(); //Update the last frame time. - this.lastFrameTime = currentTime; + previousFrameTime = currentTime; } + + waitForFramePeriod(previousFrameTime, currentTime, 50); + previousFrameTime = currentTime; } - }; + } /** * Broadcast that the race has finished. diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java index 8dcf1c48..d0b0584b 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java @@ -2,34 +2,52 @@ 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() { 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); - }*/ + 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)); + } } + + /** + * 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..39469cf8 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java @@ -4,27 +4,28 @@ 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); + } else { + boat.setAutoVMG(true); + } } } diff --git a/racevisionGame/src/main/java/network/Messages/AssignPlayerBoat.java b/racevisionGame/src/main/java/network/Messages/AssignPlayerBoat.java index c4197fe9..ab33ac1b 100644 --- a/racevisionGame/src/main/java/network/Messages/AssignPlayerBoat.java +++ b/racevisionGame/src/main/java/network/Messages/AssignPlayerBoat.java @@ -1,6 +1,5 @@ package network.Messages; -import network.Messages.Enums.JoinAcceptanceEnum; import network.Messages.Enums.MessageType; diff --git a/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java b/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java index 7e61b3de..02e1afc6 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.getValueOf(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/Boat.java b/racevisionGame/src/main/java/shared/model/Boat.java index d6e28783..385c4f3b 100644 --- a/racevisionGame/src/main/java/shared/model/Boat.java +++ b/racevisionGame/src/main/java/shared/model/Boat.java @@ -82,6 +82,7 @@ public class Boat { /** * The time at which the boat is estimated to reach the next mark, in milliseconds since unix epoch. */ + @Nullable private ZonedDateTime estimatedTimeAtNextMark; /** @@ -106,6 +107,8 @@ public class Boat { this.bearing = Bearing.fromDegrees(0d); + setCurrentPosition(new GPSCoordinate(0, 0)); + this.status = BoatStatusEnum.UNDEFINED; } @@ -365,6 +368,7 @@ public class Boat { * Returns the time at which the boat should reach the next mark. * @return Time at which the boat should reach next mark. */ + @Nullable public ZonedDateTime getEstimatedTimeAtNextMark() { return estimatedTimeAtNextMark; } 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..b8902d84 100644 --- a/racevisionGame/src/main/java/shared/model/Race.java +++ b/racevisionGame/src/main/java/shared/model/Race.java @@ -343,6 +343,7 @@ public abstract class Race { } + /** * Increments the FPS counter, and adds timePeriod milliseconds to our FPS reset timer. * @param timePeriod Time, in milliseconds, to add to {@link #lastFpsResetTime}. diff --git a/racevisionGame/src/main/java/shared/model/RaceState.java b/racevisionGame/src/main/java/shared/model/RaceState.java index d77a4129..19a1a52c 100644 --- a/racevisionGame/src/main/java/shared/model/RaceState.java +++ b/racevisionGame/src/main/java/shared/model/RaceState.java @@ -2,6 +2,8 @@ package shared.model; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import network.Messages.Enums.RaceStatusEnum; import network.Messages.Enums.RaceTypeEnum; import shared.dataInput.BoatDataSource; @@ -37,6 +39,12 @@ public abstract class RaceState { */ private RegattaDataSource regattaDataSource; + /** + * Legs in the race. + * We have this in a separate list so that it can be observed. + */ + private ObservableList legs; + /** @@ -65,6 +73,9 @@ public abstract class RaceState { */ public RaceState() { + //Legs. + this.legs = FXCollections.observableArrayList(); + //Race clock. this.raceClock = new RaceClock(ZonedDateTime.now()); @@ -90,8 +101,9 @@ public abstract class RaceState { * @param legs The new list of legs to use. */ protected void useLegsList(List legs) { + this.legs.setAll(legs); //We add a "dummy" leg at the end of the race. - if (legs.size() > 0) { + if (getLegs().size() > 0) { getLegs().add(new Leg("Finish", getLegs().size())); } } @@ -126,6 +138,7 @@ public abstract class RaceState { public void setRaceDataSource(RaceDataSource raceDataSource) { this.raceDataSource = raceDataSource; this.getRaceClock().setStartingTime(raceDataSource.getStartDateTime()); + useLegsList(raceDataSource.getLegs()); } /** @@ -309,8 +322,8 @@ public abstract class RaceState { * Returns the legs of the race. * @return Legs of the race. */ - public List getLegs() { - return raceDataSource.getLegs(); + public ObservableList getLegs() { + return legs; } diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java index 11fd8c3b..825cd274 100644 --- a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java +++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java @@ -103,7 +103,7 @@ public class RaceStatusCommand implements Command { } catch (BoatNotFoundException e) { - Logger.getGlobal().log(Level.WARNING, "RaceStatusCommand.updateBoatStatus: " + this + " could not execute. Boat with sourceID: " + boatStatus.getSourceID() + " not found.", e); + //Logger.getGlobal().log(Level.WARNING, "RaceStatusCommand.updateBoatStatus: " + this + " could not execute. Boat with sourceID: " + boatStatus.getSourceID() + " not found.", e); return; } } diff --git a/racevisionGame/src/main/java/visualiser/Controllers/HostController.java b/racevisionGame/src/main/java/visualiser/Controllers/HostController.java index 3360e514..5966c643 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/HostController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/HostController.java @@ -47,7 +47,7 @@ public class HostController extends Controller { */ public void hostGamePressed() throws IOException{ try { - Event game = new Event(true); + Event game = new Event(false); game.start(); connectSocket("localhost", 4942); } catch (EventConstructionException e) { diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java index be5b5cd1..3e4c1398 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java @@ -208,9 +208,15 @@ public class RaceController extends Controller { public void initialiseInfoTable(VisualiserRaceEvent race) { //Copy list of boats. - SortedList sortedBoats = new SortedList<>(race.getVisualiserRaceState().getBoats()); + ObservableList boats = FXCollections.observableArrayList(race.getVisualiserRaceState().getBoats()); + SortedList sortedBoats = new SortedList<>(boats); sortedBoats.comparatorProperty().bind(boatInfoTable.comparatorProperty()); + //Update copy when original changes. + race.getVisualiserRaceState().getBoats().addListener((ListChangeListener.Change c) -> Platform.runLater(() -> { + boats.setAll(race.getVisualiserRaceState().getBoats()); + })); + //Set up table. boatInfoTable.setItems(sortedBoats); 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 b09c4b51..0d9f0562 100644 --- a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java @@ -7,9 +7,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.Arrays; @@ -46,6 +45,7 @@ public class ResizableRaceCanvas extends ResizableCanvas { private boolean annoPath = true; private boolean annoEstTime = true; private boolean annoTimeSinceLastMark = true; + private boolean annoGuideLine = false; @@ -113,6 +113,13 @@ public class ResizableRaceCanvas extends ResizableCanvas { annoSpeed = !annoSpeed; } + /** + * Toggle the guideline annotation + */ + public void toggleGuideLine() { + annoGuideLine = !annoGuideLine; + } + @@ -313,39 +320,35 @@ public class ResizableRaceCanvas extends ResizableCanvas { */ private void drawBoat(VisualiserBoat boat) { - //The position may be null if we haven't received any BoatLocation messages yet. - if (boat.getCurrentPosition() != null) { - - if (boat.isClientBoat()) { - drawClientBoat(boat); - } + if (boat.isClientBoat()) { + drawClientBoat(boat); + } - //Convert position to graph coordinate. - GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition()); + //Convert position to graph coordinate. + GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition()); - //The x coordinates of each vertex of the boat. - double[] x = { - pos.getX() - 6, - pos.getX(), - pos.getX() + 6 }; + //The x coordinates of each vertex of the boat. + double[] x = { + pos.getX() - 6, + pos.getX(), + pos.getX() + 6 }; - //The y coordinates of each vertex of the boat. - double[] y = { - pos.getY() + 12, - pos.getY() - 12, - pos.getY() + 12 }; + //The y coordinates of each vertex of the boat. + double[] y = { + pos.getY() + 12, + pos.getY() - 12, + pos.getY() + 12 }; - //The above shape is essentially a triangle 12px wide, and 24px long. + //The above shape is essentially a triangle 12px wide, and 24px long. - //Draw the boat. - gc.setFill(boat.getColor()); + //Draw the boat. + gc.setFill(boat.getColor()); - gc.save(); - rotate(boat.getBearing().degrees(), pos.getX(), pos.getY()); - gc.fillPolygon(x, y, 3); - gc.restore(); + gc.save(); + rotate(boat.getBearing().degrees(), pos.getX(), pos.getY()); + gc.fillPolygon(x, y, 3); + gc.restore(); - } } @@ -500,6 +503,11 @@ public class ResizableRaceCanvas extends ResizableCanvas { //Race boundary. drawBoundary(); + //Guiding Line + if (annoGuideLine){ + drawRaceLine(); + } + //Boats. drawBoats(); @@ -508,6 +516,140 @@ 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.getVisualiserRaceState().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 + * @param legStartPoint The position the current leg. + * @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); + } @@ -519,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/java/visualiser/model/Sparkline.java b/racevisionGame/src/main/java/visualiser/model/Sparkline.java index d8f21f77..3f7e07b8 100644 --- a/racevisionGame/src/main/java/visualiser/model/Sparkline.java +++ b/racevisionGame/src/main/java/visualiser/model/Sparkline.java @@ -1,14 +1,18 @@ package visualiser.model; import javafx.application.Platform; +import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.scene.paint.Color; +import shared.model.Leg; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** @@ -32,10 +36,10 @@ public class Sparkline { private ObservableList boats; /** - * The number of legs in the race. - * Used to correctly scale the linechart. + * Race legs to observe. + * We need to observe legs as they may be added after the sparkline is created if race.xml is received after this is created. */ - private Integer legNum; + private ObservableList legs; /** @@ -53,21 +57,31 @@ public class Sparkline { */ private NumberAxis yAxis; + /** + * A map between a boat and its data series in the sparkline. + * This is used so that we can remove a series when (or if) a boat is removed from the race. + */ + private Map> boatSeriesMap; + + + /** * Constructor to set up initial sparkline (LineChart) object * @param race The race to listen to. * @param sparklineChart JavaFX LineChart for the sparkline. */ - public Sparkline(VisualiserRaceState race, LineChart sparklineChart) { + public Sparkline(VisualiserRaceState race, LineChart sparklineChart) { this.race = race; this.boats = new SortedList<>(race.getBoats()); - this.legNum = race.getLegCount(); + this.legs = race.getLegs(); this.sparklineChart = sparklineChart; this.yAxis = (NumberAxis) sparklineChart.getYAxis(); this.xAxis = (NumberAxis) sparklineChart.getXAxis(); + this.boatSeriesMap = new HashMap<>(); + createSparkline(); } @@ -79,50 +93,45 @@ public class Sparkline { * Position numbers are displayed. */ private void createSparkline() { - // NOTE: Y axis is in negatives to display correct positions - - //For each boat... - for (VisualiserBoat boat : this.boats) { - //Create data series for each boat. - XYChart.Series series = new XYChart.Series<>(); + //We need to dynamically update the sparkline when boats are added/removed. + boats.addListener((ListChangeListener.Change c) -> { + Platform.runLater(() -> { - //All boats start in "last" place. - series.getData().add(new XYChart.Data<>(0, boats.size())); + while (c.next()) { - //Listen for changes in the boat's leg - we only update the graph when it changes leg. - boat.legProperty().addListener( - (observable, oldValue, newValue) -> { + if (c.wasAdded()) { + for (VisualiserBoat boat : c.getAddedSubList()) { + addBoatSeries(boat); + } - //Get the data to plot. - List boatOrder = race.getLegCompletionOrder().get(oldValue); - //Find boat position in list. - int boatPosition = boatOrder.indexOf(boat) + 1; + } else if (c.wasRemoved()) { + for (VisualiserBoat boat : c.getRemoved()) { + removeBoatSeries(boat); + } + } - //Get leg number. - int legNumber = oldValue.getLegNumber() + 1; + } + //Update height of y axis. + setYAxisLowerBound(); + }); - //Create new data point for boat's position at the new leg. - XYChart.Data dataPoint = new XYChart.Data<>(legNumber, boatPosition); + }); - //Add to series. - Platform.runLater(() -> series.getData().add(dataPoint)); - - }); - - - //Add to chart. - sparklineChart.getData().add(series); - - //Color using boat's color. We need to do this after adding the series to a chart, otherwise we get null pointer exceptions. - series.getNode().setStyle("-fx-stroke: " + colourToHex(boat.getColor()) + ";"); + legs.addListener((ListChangeListener.Change c) -> { + Platform.runLater(() -> xAxis.setUpperBound(race.getLegCount())); + }); + //Initialise chart for existing boats. + for (VisualiserBoat boat : boats) { + addBoatSeries(boat); } + sparklineChart.setCreateSymbols(false); //Set x axis details @@ -131,20 +140,109 @@ public class Sparkline { xAxis.setTickLabelsVisible(false); xAxis.setMinorTickVisible(false); xAxis.setLowerBound(0); - xAxis.setUpperBound(legNum + 2); + xAxis.setUpperBound(race.getLegCount()); xAxis.setTickUnit(1); + + //The y-axis uses negative values, with the minus sign hidden (e.g., boat in 1st has position -1, which becomes 1, boat in 6th has position -6, which becomes 6). + //This is necessary to actually get the y-axis labelled correctly. Negative tick count doesn't work. //Set y axis details - yAxis.setLowerBound(boats.size()); - yAxis.setUpperBound(1); yAxis.setAutoRanging(false); + + yAxis.setTickUnit(1); + yAxis.setMinorTickCount(0); + + yAxis.setUpperBound(0); + setYAxisLowerBound(); + yAxis.setLabel("Position in Race"); - yAxis.setTickUnit(-1);//Negative tick reverses the y axis. yAxis.setTickMarkVisible(true); yAxis.setTickLabelsVisible(true); - yAxis.setTickMarkVisible(true); - yAxis.setMinorTickVisible(true); + yAxis.setMinorTickVisible(false); + + + + //Hide minus number from displaying on axis. + yAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(yAxis) { + @Override + public String toString(Number value) { + if ((value.intValue() == 0) || (value.intValue() < -boats.size())) { + return ""; + + } else { + return String.format("%d", -value.intValue()); + } + } + }); + + } + + + /** + * Sets the lower bound of the y-axis. + */ + private void setYAxisLowerBound() { + yAxis.setLowerBound( -(boats.size() + 1)); + } + + + + /** + * Removes the data series for a given boat from the sparkline. + * @param boat Boat to remove series for. + */ + private void removeBoatSeries(VisualiserBoat boat) { + sparklineChart.getData().remove(boatSeriesMap.get(boat)); + boatSeriesMap.remove(boat); + } + + + /** + * Creates a data series for a boat, and adds it to the sparkline. + * @param boat Boat to add series for. + */ + private void addBoatSeries(VisualiserBoat boat) { + + //Create data series for boat. + XYChart.Series series = new XYChart.Series<>(); + + + //All boats start in "last" place. + series.getData().add(new XYChart.Data<>(0, -(boats.size()))); + + //Listen for changes in the boat's leg - we only update the graph when it changes leg. + boat.legProperty().addListener( + (observable, oldValue, newValue) -> { + + //Get the data to plot. + List boatOrder = race.getLegCompletionOrder().get(oldValue); + //Find boat position in list. + int boatPosition = -(boatOrder.indexOf(boat) + 1); + + //Get leg number. + int legNumber = oldValue.getLegNumber() + 1; + + + //Create new data point for boat's position at the new leg. + XYChart.Data dataPoint = new XYChart.Data<>(legNumber, boatPosition); + + //Add to series. + Platform.runLater(() -> series.getData().add(dataPoint)); + + + }); + + + //Add to chart. + sparklineChart.getData().add(series); + + //Color using boat's color. We need to do this after adding the series to a chart, otherwise we get null pointer exceptions. + series.getNode().setStyle("-fx-stroke: " + colourToHex(boat.getColor()) + ";"); + + + boatSeriesMap.put(boat, series); + } diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java index 0d32c513..0d65d7ad 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java @@ -172,7 +172,7 @@ public class VisualiserBoat extends Boat { */ public String getTimeToNextMarkFormatted(ZonedDateTime currentTime) { - if (getTimeAtLastMark() != null) { + if ((getTimeAtLastMark() != null) && (currentTime != null)) { //Calculate time delta. Duration timeUntil = Duration.between(currentTime, getEstimatedTimeAtNextMark()); @@ -211,7 +211,7 @@ public class VisualiserBoat extends Boat { */ public String getTimeSinceLastMarkFormatted(ZonedDateTime currentTime) { - if (getTimeAtLastMark() != null) { + if ((getTimeAtLastMark() != null) && (currentTime != null)) { //Calculate time delta. Duration timeSince = Duration.between(getTimeAtLastMark(), currentTime); diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java index faefa467..b1767cd5 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java @@ -1,6 +1,7 @@ package visualiser.model; +import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.paint.Color; @@ -97,7 +98,7 @@ public class VisualiserRaceState extends RaceState { this.generateVisualiserBoats(this.boats, getBoatDataSource().getBoats(), raceDataSource.getParticipants(), unassignedColors); } - useLegsList(raceDataSource.getLegs()); + initialiseLegCompletionOrder(); } /** @@ -125,14 +126,9 @@ public class VisualiserRaceState extends RaceState { /** - * See {@link RaceState#useLegsList(List)}. - * Also initialises the {@link #legCompletionOrder} map. - * @param legs The new list of legs to use. + * Initialises the {@link #legCompletionOrder} map. */ - @Override - public void useLegsList(List legs) { - super.useLegsList(legs); - + public void initialiseLegCompletionOrder() { //Initialise the leg completion order map. for (Leg leg : getLegs()) { this.legCompletionOrder.put(leg, new ArrayList<>(this.boats.size())); @@ -206,7 +202,7 @@ public class VisualiserRaceState extends RaceState { if (getPlayerBoatID() != 0) { - for (VisualiserBoat boat : getBoats()) { + for (VisualiserBoat boat : new ArrayList<>(getBoats())) { if (boat.getSourceID() == getPlayerBoatID()) { boat.setClientBoat(true); diff --git a/racevisionGame/src/main/resources/mock/mockXML/raceSinglePlayer.xml b/racevisionGame/src/main/resources/mock/mockXML/raceSinglePlayer.xml index a5d6761f..553c2571 100644 --- a/racevisionGame/src/main/resources/mock/mockXML/raceSinglePlayer.xml +++ b/racevisionGame/src/main/resources/mock/mockXML/raceSinglePlayer.xml @@ -8,12 +8,12 @@ - - - - - - + + + + + + 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 @@ - -