From 27cf0e153994a084dd547c4176ac0dcd095e332f Mon Sep 17 00:00:00 2001 From: Joseph Gardner Date: Wed, 2 Aug 2017 16:31:55 +1200 Subject: [PATCH 01/56] Started splitting MockRace into RaceLogic and RaceState. #refactor #story[1094] --- .../src/main/java/mock/model/MockRace.java | 10 - .../src/main/java/mock/model/RaceLogic.java | 73 +++ .../src/main/java/mock/model/RaceState.java | 156 ++++- .../src/main/java/mock/model/SplitTODO.java | 555 ++++++++++++++++++ .../src/main/java/shared/model/Race.java | 6 +- .../gameController/ControllerServer.java | 10 +- 6 files changed, 795 insertions(+), 15 deletions(-) create mode 100644 racevisionGame/src/main/java/mock/model/SplitTODO.java diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index fec34ad7..b9fb32be 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -33,14 +33,6 @@ public class MockRace extends Race { */ private List boats; - - - /** - * A copy of the boundary list, except "shrunk" inwards by 50m. - */ - private List shrinkBoundary; - - /** * The scale factor of the race. * See {@link Constants#RaceTimeScale}. @@ -69,8 +61,6 @@ public class MockRace extends Race { this.boats = this.generateMockBoats(boatDataSource.getBoats(), raceDataSource.getParticipants(), polars); - this.shrinkBoundary = GPSCoordinate.getShrinkBoundary(this.boundary); - //Set up wind generator. It may be tidier to create this outside the race (with the values sourced from a data file maybe?) and pass it in. this.windGenerator = new WindGenerator( Bearing.fromDegrees(225), diff --git a/racevisionGame/src/main/java/mock/model/RaceLogic.java b/racevisionGame/src/main/java/mock/model/RaceLogic.java index b8e904a6..0673e226 100644 --- a/racevisionGame/src/main/java/mock/model/RaceLogic.java +++ b/racevisionGame/src/main/java/mock/model/RaceLogic.java @@ -1,4 +1,77 @@ package mock.model; +import network.Messages.Enums.BoatStatusEnum; +import network.Messages.LatestMessages; +import shared.dataInput.BoatDataSource; +import shared.dataInput.RaceDataSource; +import shared.dataInput.RegattaDataSource; +import shared.model.*; + +import java.util.Iterator; +import java.util.List; + public class RaceLogic { + private RaceState raceState; + + /** + * The scale factor of the race. + * See {@link Constants#RaceTimeScale}. + */ + private int scaleFactor; + + /** + * Object used to generate changes in wind speed/direction. + */ + private WindGenerator windGenerator; + + public RaceLogic(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, Polars polars, int timeScale) { + this.raceState = new RaceState(boatDataSource, raceDataSource, regattaDataSource, latestMessages, polars); + this.raceState.run(); + + //Set up wind generator. It may be tidier to create this outside the race (with the values sourced from a data file maybe?) and pass it in. + this.windGenerator = new WindGenerator( + Bearing.fromDegrees(225), + Bearing.fromDegrees(215), + Bearing.fromDegrees(235), + 12d, + 8d, + 16d ); + raceState.setWind(windGenerator.generateBaselineWind()); + } + + private void changeWindDirection() { + Wind nextWind = windGenerator.generateNextWind(raceState.getWind()); + + raceState.setWind(nextWind); + } + + /** + * Returns the number of boats that are still active in the race. + * They become inactive by either finishing or withdrawing. + * @return The number of boats still active in the race. + */ + protected int getNumberOfActiveBoats() { + + int numberOfActiveBoats = 0; + + for (MockBoat boat : raceState.getBoats()) { + + //If the boat is currently racing, count it. + if (boat.getStatus() == BoatStatusEnum.RACING) { + numberOfActiveBoats++; + } + + } + + return numberOfActiveBoats; + } + + /** + * Returns a list of boats in the race. + * @return List of boats in the race. + */ + public List getBoats() { + return raceState.getBoats(); + } + } diff --git a/racevisionGame/src/main/java/mock/model/RaceState.java b/racevisionGame/src/main/java/mock/model/RaceState.java index 4b13cbb4..699417c5 100644 --- a/racevisionGame/src/main/java/mock/model/RaceState.java +++ b/racevisionGame/src/main/java/mock/model/RaceState.java @@ -1,4 +1,158 @@ package mock.model; -public class RaceState { +import network.Messages.Enums.BoatStatusEnum; +import network.Messages.LatestMessages; +import shared.dataInput.BoatDataSource; +import shared.dataInput.RaceDataSource; +import shared.dataInput.RegattaDataSource; +import shared.model.*; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class RaceState extends Race { + + /** + * An observable list of boats in the race. + */ + private List boats; + + private Wind wind; + + public RaceState(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, Polars polars) { + super(boatDataSource, raceDataSource, regattaDataSource, latestMessages); + this.boats = this.generateMockBoats(boatDataSource.getBoats(), raceDataSource.getParticipants(), polars); + } + + /** + * Generates a list of MockBoats given a list of Boats, and a list of participating boats. + * @param boats The map of Boats describing boats that are potentially in the race. Maps boat sourceID to boat. + * @param sourceIDs The list of boat sourceIDs describing which specific boats are actually participating. + * @param polars The polars table to be used for boat simulation. + * @return A list of MockBoats that are participating in the race. + */ + private List generateMockBoats(Map boats, List sourceIDs, Polars polars) { + + List mockBoats = new ArrayList<>(sourceIDs.size()); + + //For each sourceID participating... + for (int sourceID : sourceIDs) { + + //Get the boat associated with the sourceID. + Boat boat = boats.get(sourceID); + + //Construct a MockBoat using the Boat and Polars. + MockBoat mockBoat = new MockBoat(boat, polars); + + mockBoats.add(mockBoat); + + } + + return mockBoats; + + } + + /** + * Initialise the boats in the race. + * This sets their starting positions and current legs. + */ + @Override + protected void initialiseBoats() { + + //Gets the starting positions of the boats. + List startingPositions = getSpreadStartingPositions(); + + //Get iterators for our boat and position lists. + Iterator boatIt = this.boats.iterator(); + Iterator startPositionIt = startingPositions.iterator(); + + //Iterate over the pair of lists. + while (boatIt.hasNext() && startPositionIt.hasNext()) { + + //Get the next boat and position. + MockBoat boat = boatIt.next(); + GPSCoordinate startPosition = startPositionIt.next(); + + + //The boat starts on the first leg of the race. + boat.setCurrentLeg(this.legs.get(0)); + + //Boats start with 0 knots speed. + boat.setCurrentSpeed(0d); + + //Place the boat at its starting position. + boat.setCurrentPosition(startPosition); + + //Boats start facing their next marker. + boat.setBearing(boat.calculateBearingToNextMarker()); + + //Sets the boats status to prestart - it changes to racing when the race starts. + boat.setStatus(BoatStatusEnum.PRESTART); + + //We set a large time since tack change so that it calculates a new VMG when the simulation starts. + boat.setTimeSinceTackChange(999999); + + } + + } + + /** + * Creates a list of starting positions for the different boats, so they do not appear cramped at the start line. + * + * @return A list of starting positions. + */ + public List getSpreadStartingPositions() { + + //The first compound marker of the race - the starting gate. + CompoundMark compoundMark = this.legs.get(0).getStartCompoundMark(); + + //The position of the two markers from the compound marker. + GPSCoordinate mark1Position = compoundMark.getMark1Position(); + GPSCoordinate mark2Position = compoundMark.getMark2Position(); + + + //Calculates the azimuth between the two points. + Azimuth azimuth = GPSCoordinate.calculateAzimuth(mark1Position, mark2Position); + + //Calculates the distance between the two points. + double distanceMeters = GPSCoordinate.calculateDistanceMeters(mark1Position, mark2Position); + + //The number of boats in the race. + int numberOfBoats = this.boats.size(); + + //Calculates the distance between each boat. We divide by numberOfBoats + 1 to ensure that no boat is placed on one of the starting gate's marks. + double distanceBetweenBoatsMeters = distanceMeters / (numberOfBoats + 1); + + + //List to store coordinates in. + List positions = new ArrayList<>(); + + //We start spacing boats out from mark 1. + GPSCoordinate position = mark1Position; + + //For each boat, displace position, and store it. + for (int i = 0; i < numberOfBoats; i++) { + + position = GPSCoordinate.calculateNewPosition(position, distanceBetweenBoatsMeters, azimuth); + + positions.add(position); + + } + + return positions; + } + + public void run() { + initialiseBoats(); + } + + public Wind getWind() { + return wind; + } + + public List getBoats() { + return boats; + } } 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/shared/model/Race.java b/racevisionGame/src/main/java/shared/model/Race.java index aec57882..3235f60f 100644 --- a/racevisionGame/src/main/java/shared/model/Race.java +++ b/racevisionGame/src/main/java/shared/model/Race.java @@ -226,7 +226,7 @@ public abstract class Race implements Runnable { * Sets the current race status. * @param raceStatusEnum The new status of the race. */ - protected void setRaceStatusEnum(RaceStatusEnum raceStatusEnum) { + public void setRaceStatusEnum(RaceStatusEnum raceStatusEnum) { this.raceStatusEnum = raceStatusEnum; } @@ -253,7 +253,7 @@ public abstract class Race implements Runnable { * @param windBearing New wind bearing. * @param windSpeedKnots New wind speed, in knots. */ - protected void setWind(Bearing windBearing, double windSpeedKnots) { + public void setWind(Bearing windBearing, double windSpeedKnots) { Wind wind = new Wind(windBearing, windSpeedKnots); setWind(wind); } @@ -262,7 +262,7 @@ public abstract class Race implements Runnable { * Updates the race to have a specified wind (bearing and speed). * @param wind New wind. */ - protected void setWind(Wind wind) { + public void setWind(Wind wind) { this.raceWind.setValue(wind); } diff --git a/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java b/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java index fb6a257b..dc3f3a03 100644 --- a/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java +++ b/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java @@ -9,6 +9,7 @@ import visualiser.gameController.Keys.KeyFactory; import java.io.DataInputStream; import java.io.IOException; import java.net.Socket; +import java.util.Queue; /** * Service for dispatching key press data to race from client @@ -23,6 +24,9 @@ public class ControllerServer implements Runnable { */ private DataInputStream inputStream; + // Last boat action received + private Queue boatActions; + /** * Initialise server-side controller with live client socket * @param socket to client @@ -49,11 +53,15 @@ public class ControllerServer implements Runnable { BinaryMessageDecoder encodedMessage = new BinaryMessageDecoder(message); BoatActionDecoder boatActionDecoder = new BoatActionDecoder(encodedMessage.getMessageBody()); BoatActionEnum decodedMessage = boatActionDecoder.getBoatAction(); - System.out.println("Received key: " + decodedMessage); + boatActions.add(decodedMessage); } } catch (IOException e) { e.printStackTrace(); } } } + + public BoatActionEnum getNextBoatAction() { + return boatActions.remove(); + } } From c633de21f50f5def04c438a62ebd3c15831cf574 Mon Sep 17 00:00:00 2001 From: hba56 Date: Wed, 2 Aug 2017 16:33:03 +1200 Subject: [PATCH 02/56] added new methods to the mock boats to let them check which side a mark is on to them #story[1101] --- .../src/main/java/mock/model/MockBoat.java | 24 +++ .../test/java/mock/model/MockBoatTest.java | 153 +++++++++++++++++- 2 files changed, 175 insertions(+), 2 deletions(-) diff --git a/racevisionGame/src/main/java/mock/model/MockBoat.java b/racevisionGame/src/main/java/mock/model/MockBoat.java index c8c6825b..05efd0cf 100644 --- a/racevisionGame/src/main/java/mock/model/MockBoat.java +++ b/racevisionGame/src/main/java/mock/model/MockBoat.java @@ -203,4 +203,28 @@ public class MockBoat extends Boat { return distanceTravelledMeters; } + /** + * Check if a mark is on the port side of the boat + */ + public boolean isPortSide(Mark mark){ + //if this boat is lower than the mark check which way it is facing + if(this.getCurrentPosition().getLongitude() < mark.getPosition().getLongitude()){ + return this.getBearing().degrees() <= 180; + }else{ + return this.getBearing().degrees() > 180; + } + } + + /** + * Check if a mark is on the starboard side of the boat + */ + public boolean isStarboardSide(Mark mark){ + //if this boat is lower than the mark check which way it is facing + if(this.getCurrentPosition().getLongitude() < mark.getPosition().getLongitude()){ + return this.getBearing().degrees() >= 180; + }else{ + return this.getBearing().degrees() < 180; + } + } + } diff --git a/racevisionGame/src/test/java/mock/model/MockBoatTest.java b/racevisionGame/src/test/java/mock/model/MockBoatTest.java index b1ee551c..71b8f715 100644 --- a/racevisionGame/src/test/java/mock/model/MockBoatTest.java +++ b/racevisionGame/src/test/java/mock/model/MockBoatTest.java @@ -1,7 +1,156 @@ package mock.model; -import static org.junit.Assert.*; +import mock.dataInput.PolarParser; +import mock.exceptions.InvalidPolarFileException; +import org.junit.Before; +import org.junit.Test; +import shared.model.Bearing; +import shared.model.GPSCoordinate; +import shared.model.Mark; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; public class MockBoatTest { -//TODO + + /** + * boat made for testing + */ + private MockBoat firstTestBoat; + + private Mark markToTest; + + private GPSCoordinate highGPS; + private GPSCoordinate middleGPS; + private GPSCoordinate lowGPS; + + /** + * Creates the Polars object for the tests. + */ + @Before + public void setUp() { + //Read in polars. + try { + //Parse data file. + Polars polars = PolarParser.parse("mock/polars/acc_polars.csv"); + + firstTestBoat = new MockBoat(1, "test", "NZ", polars); + highGPS = new GPSCoordinate(100, 100); + middleGPS = new GPSCoordinate(100, 75); + lowGPS = new GPSCoordinate(100, 50); + markToTest = new Mark(1, "test MARK", middleGPS); + } + catch (InvalidPolarFileException e) { + fail("Couldn't parse polar file."); + } + } + + //////////////////////////////Mark Higher//////////////////////////////// + + /** + * Tests if the boat is lower than the mark that the port side method works if + * boat is facing east + */ + @Test + public void testIsPortSide() { + firstTestBoat.setBearing(Bearing.fromDegrees(90)); + firstTestBoat.setCurrentPosition(lowGPS); + markToTest.setPosition(highGPS); + + assertEquals(firstTestBoat.isPortSide(markToTest), true); + } + + /** + * Tests if the boat is lower than the mark that the port side method works if + * boat is facing west + */ + @Test + public void testIsPortSideWrong() { + firstTestBoat.setBearing(Bearing.fromDegrees(270)); + firstTestBoat.setCurrentPosition(lowGPS); + markToTest.setPosition(highGPS); + + assertEquals(firstTestBoat.isPortSide(markToTest), false); + } + + /** + * Tests if the boat is lower than the mark that the starboard side method works if + * boat is facing east + */ + @Test + public void testIsStarboardSideWrong() { + firstTestBoat.setBearing(Bearing.fromDegrees(90)); + firstTestBoat.setCurrentPosition(lowGPS); + markToTest.setPosition(highGPS); + + assertEquals(firstTestBoat.isStarboardSide(markToTest), false); + } + + /** + * Tests if the boat is lower than the mark that the starboard side method works if + * boat is facing west + */ + @Test + public void testIsStarboardSide() { + firstTestBoat.setBearing(Bearing.fromDegrees(270)); + firstTestBoat.setCurrentPosition(lowGPS); + markToTest.setPosition(highGPS); + + assertEquals(firstTestBoat.isStarboardSide(markToTest), true); + } + + + //////////////////////////////Mark Lower//////////////////////////////// + + /** + * Tests if the boat is higher than the mark that the port side method works if + * boat is facing east + */ + @Test + public void testIsPortSideHigherWrong() { + firstTestBoat.setBearing(Bearing.fromDegrees(90)); + firstTestBoat.setCurrentPosition(highGPS); + markToTest.setPosition(lowGPS); + + assertEquals(firstTestBoat.isPortSide(markToTest), false); + } + + /** + * Tests if the boat is higher than the mark that the port side method works if + * boat is facing west + */ + @Test + public void testIsPortSideHigher() { + firstTestBoat.setBearing(Bearing.fromDegrees(270)); + firstTestBoat.setCurrentPosition(highGPS); + markToTest.setPosition(lowGPS); + + assertEquals(firstTestBoat.isPortSide(markToTest), true); + } + + /** + * Tests if the boat is higher than the mark that the starboard side method works if + * boat is facing east + */ + @Test + public void testIsStarboardSideHigher() { + firstTestBoat.setBearing(Bearing.fromDegrees(90)); + firstTestBoat.setCurrentPosition(highGPS); + markToTest.setPosition(lowGPS); + + assertEquals(firstTestBoat.isStarboardSide(markToTest), true); + } + + /** + * Tests if the boat is higher than the mark that the starboard side method works if + * boat is facing west + */ + @Test + public void testIsStarboardSideHigherWrong() { + firstTestBoat.setBearing(Bearing.fromDegrees(270)); + firstTestBoat.setCurrentPosition(highGPS); + markToTest.setPosition(lowGPS); + + assertEquals(firstTestBoat.isStarboardSide(markToTest), false); + } } From 13922bc2841591178c95602159ca8d9cf88e0b03 Mon Sep 17 00:00:00 2001 From: hba56 Date: Wed, 2 Aug 2017 17:06:52 +1200 Subject: [PATCH 03/56] updated javadoc #story[1101] --- racevisionGame/src/main/java/mock/model/MockBoat.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/racevisionGame/src/main/java/mock/model/MockBoat.java b/racevisionGame/src/main/java/mock/model/MockBoat.java index 05efd0cf..44e8e4e6 100644 --- a/racevisionGame/src/main/java/mock/model/MockBoat.java +++ b/racevisionGame/src/main/java/mock/model/MockBoat.java @@ -205,6 +205,7 @@ public class MockBoat extends Boat { /** * Check if a mark is on the port side of the boat + * @param mark mark to be passed */ public boolean isPortSide(Mark mark){ //if this boat is lower than the mark check which way it is facing @@ -217,6 +218,7 @@ public class MockBoat extends Boat { /** * Check if a mark is on the starboard side of the boat + * @param mark mark to be passed */ public boolean isStarboardSide(Mark mark){ //if this boat is lower than the mark check which way it is facing From b7af4e19cfcfb4ce56776b8c39b802f79df2cf2f Mon Sep 17 00:00:00 2001 From: hba56 Date: Wed, 2 Aug 2017 18:09:12 +1200 Subject: [PATCH 04/56] new method to check if a boat is between gates as well as updated the gps values to fit better with real life values #story[1101] --- .../src/main/java/mock/model/MockBoat.java | 16 +++++++++++++ .../test/java/mock/model/MockBoatTest.java | 24 ++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/racevisionGame/src/main/java/mock/model/MockBoat.java b/racevisionGame/src/main/java/mock/model/MockBoat.java index 44e8e4e6..97fe68ef 100644 --- a/racevisionGame/src/main/java/mock/model/MockBoat.java +++ b/racevisionGame/src/main/java/mock/model/MockBoat.java @@ -206,6 +206,7 @@ public class MockBoat extends Boat { /** * 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){ //if this boat is lower than the mark check which way it is facing @@ -219,6 +220,7 @@ public class MockBoat extends Boat { /** * 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 @@ -229,4 +231,18 @@ public class MockBoat extends Boat { } } + /** + * 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){ + if ((this.isPortSide(gate.getMark1()) && this.isStarboardSide(gate.getMark2())) || + (this.isStarboardSide(gate.getMark2()) && this.isPortSide(gate.getMark1()))){ + return true; + }else{ + return false; + } + } + } diff --git a/racevisionGame/src/test/java/mock/model/MockBoatTest.java b/racevisionGame/src/test/java/mock/model/MockBoatTest.java index 71b8f715..32c28ce1 100644 --- a/racevisionGame/src/test/java/mock/model/MockBoatTest.java +++ b/racevisionGame/src/test/java/mock/model/MockBoatTest.java @@ -5,6 +5,7 @@ import mock.exceptions.InvalidPolarFileException; import org.junit.Before; import org.junit.Test; import shared.model.Bearing; +import shared.model.CompoundMark; import shared.model.GPSCoordinate; import shared.model.Mark; @@ -19,6 +20,7 @@ public class MockBoatTest { private MockBoat firstTestBoat; private Mark markToTest; + private Mark markToTest2; private GPSCoordinate highGPS; private GPSCoordinate middleGPS; @@ -35,10 +37,11 @@ public class MockBoatTest { Polars polars = PolarParser.parse("mock/polars/acc_polars.csv"); firstTestBoat = new MockBoat(1, "test", "NZ", polars); - highGPS = new GPSCoordinate(100, 100); - middleGPS = new GPSCoordinate(100, 75); - lowGPS = new GPSCoordinate(100, 50); + highGPS = new GPSCoordinate(-64.854000, 32.296577); + middleGPS = new GPSCoordinate(-64.854000, 32.292500); + lowGPS = new GPSCoordinate(-64.854000, 32.290000); markToTest = new Mark(1, "test MARK", middleGPS); + markToTest2 = new Mark(2, "test MARK2", middleGPS); } catch (InvalidPolarFileException e) { fail("Couldn't parse polar file."); @@ -153,4 +156,19 @@ public class MockBoatTest { assertEquals(firstTestBoat.isStarboardSide(markToTest), false); } + + /** + * Tests if a boat is between a gate + */ + @Test + public void testIsBetweenGate(){ + markToTest.setPosition(highGPS); + markToTest2.setPosition(lowGPS); + CompoundMark testGate = new CompoundMark(1, "test GATE", markToTest, markToTest2); + + firstTestBoat.setCurrentPosition(middleGPS); + + assertEquals(firstTestBoat.isBetweenGate(testGate), true); + + } } From f212414bd99f5c4273e945fb9b325799e6ef6e40 Mon Sep 17 00:00:00 2001 From: hba56 Date: Thu, 3 Aug 2017 13:27:00 +1200 Subject: [PATCH 05/56] Added in a new basis for boats to round marks, gave mockboats a status to say how far through a rounding they are and made a method in GPScoordinate public so it can be used to calculate intersections. This branch will not run the game any more as boats can't move on to the next leg until they can be controlled by the user. #story[1087] --- .../src/main/java/mock/model/MockBoat.java | 19 ++++ .../src/main/java/mock/model/MockRace.java | 105 +++++++++++++++--- .../main/java/shared/model/GPSCoordinate.java | 2 +- 3 files changed, 110 insertions(+), 16 deletions(-) diff --git a/racevisionGame/src/main/java/mock/model/MockBoat.java b/racevisionGame/src/main/java/mock/model/MockBoat.java index 97fe68ef..2874aa30 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; + /** @@ -245,4 +253,15 @@ public class MockBoat extends Boat { } } + public Integer getRoundingStatus() { + return Integer.valueOf(roundingStatus); + } + + public void increaseRoundingStatus() { + this.roundingStatus++; + } + + public void resetRoundingStatus() { + this.roundingStatus = 0; + } } diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index ad259bdc..3c394767 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -771,39 +771,60 @@ public class MockRace extends Race { } - /** * 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) { + protected void checkPosition(MockBoat boat, long timeElapsed) {//TODO cater for gates and rounding that aren't port side //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. + double epsilonNauticalMiles = 250.0 / Constants.NMToMetersConversion; //250 meters. if (boat.calculateDistanceToNextMarker() < epsilonNauticalMiles) { - //Boat has reached its target marker, and has moved on to a new leg. - + //Boat is within an acceptable distance from the mark. + GPSCoordinate startDirectionLinePoint = boat.getCurrentLeg().getStartCompoundMark().getMark1Position(); + //todo will need to change this for gates, so that the end point is the side of the gate needed to be rounded + GPSCoordinate endDirectionLinePoint = boat.getCurrentLeg().getEndCompoundMark().getMark1Position(); - //Calculate how much the boat overshot the marker by. - double overshootMeters = boat.calculateDistanceToNextMarker(); + 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 + GPSCoordinate roundCheck1 = GPSCoordinate.calculateNewPosition(startDirectionLinePoint, + epsilonNauticalMiles, Azimuth.fromDegrees(bearingOfDirectionLine.degrees() + 90));//adding 90 so the check line is parallel - //Move boat on to next leg. - Leg nextLeg = this.legs.get(boat.getCurrentLeg().getLegNumber() + 1); - boat.setCurrentLeg(nextLeg); + GPSCoordinate roundCheck2 = GPSCoordinate.calculateNewPosition(startDirectionLinePoint, + epsilonNauticalMiles, Azimuth.fromDegrees(bearingOfDirectionLine.degrees())); - //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); + //boats must pass all checks in order to round a mark + switch (boat.getRoundingStatus()){ + case 0://hasn't started rounding + if (boat.isPortSide(boat.getCurrentLeg().getEndCompoundMark().getMark1()) && + GPSCoordinate.intersects(boat.getCurrentLeg().getEndCompoundMark().getMark1().getPosition(), + roundCheck1, boat.getCurrentPosition())){ + boat.increaseRoundingStatus(); + } + break; + case 1://has been parallel to the mark + if (boat.isPortSide(boat.getCurrentLeg().getEndCompoundMark().getMark1()) && + GPSCoordinate.intersects(boat.getCurrentLeg().getEndCompoundMark().getMark1().getPosition(), + roundCheck2, boat.getCurrentPosition())){ + 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; + } //Check if the boat has finished or stopped racing. - if (this.isLastLeg(boat.getCurrentLeg())) { //Boat has finished. boat.setTimeFinished(timeElapsed); @@ -824,6 +845,60 @@ public class MockRace extends Race { } +//old method fo checking if boats passed a mark +// +// /** +// * Checks if a boat has finished any legs, or has pulled out of race (DNF). +// * @param boat The boat to check. +// * @param timeElapsed The total time, in milliseconds, that has elapsed since the race started. +// */ +// protected void checkPosition(MockBoat boat, long timeElapsed) { +// +// //The distance, in nautical miles, within which the boat needs to get in order to consider that it has reached the marker. +// double epsilonNauticalMiles = 100.0 / Constants.NMToMetersConversion; //100 meters. TODO should be more like 5-10. +// +// if (boat.calculateDistanceToNextMarker() < epsilonNauticalMiles) { +// //Boat has reached its target marker, and has moved on to a new leg. +// +// +// +// //Calculate how much the boat overshot the marker by. +// double overshootMeters = boat.calculateDistanceToNextMarker(); +// +// +// //Move boat on to next leg. +// Leg nextLeg = this.legs.get(boat.getCurrentLeg().getLegNumber() + 1); +// boat.setCurrentLeg(nextLeg); +// +// //Add overshoot distance into the distance travelled for the next leg. +// boat.setDistanceTravelledInLeg(overshootMeters); +// +// //Setting a high value for this allows the boat to immediately do a large turn, as it needs to in order to get to the next mark. +// boat.setTimeSinceTackChange(999999); +// +// +// //Check if the boat has finished or stopped racing. +// +// if (this.isLastLeg(boat.getCurrentLeg())) { +// //Boat has finished. +// boat.setTimeFinished(timeElapsed); +// boat.setCurrentSpeed(0); +// boat.setStatus(BoatStatusEnum.FINISHED); +// +// } else if (doNotFinish()) { +// //Boat has pulled out of race. +// boat.setTimeFinished(timeElapsed); +// boat.setCurrentLeg(new Leg("DNF", -1)); +// boat.setCurrentSpeed(0); +// boat.setStatus(BoatStatusEnum.DNF); +// +// } +// +// } +// +// } + + /** diff --git a/racevisionGame/src/main/java/shared/model/GPSCoordinate.java b/racevisionGame/src/main/java/shared/model/GPSCoordinate.java index ee5eaaff..566413c6 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(); From 14ce5fcaffbf773d66719ab3640ca5dfd006dcaf Mon Sep 17 00:00:00 2001 From: zwu18 Date: Fri, 4 Aug 2017 03:56:55 +1200 Subject: [PATCH 06/56] Made changes to TackGybeCommand and VMGCommand classes to fit pattern layout. #Story[1097] --- .../src/main/java/mock/model/MockRace.java | 4 ++-- .../model/commandFactory/CommandFactory.java | 2 +- .../model/commandFactory/TackGybeCommand.java | 11 ++++++++++- .../mock/model/commandFactory/VMGCommand.java | 17 +++++++++-------- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index bfcf5efc..81042828 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -261,7 +261,6 @@ public class MockRace extends Race { //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) { @@ -417,6 +416,7 @@ public class MockRace extends Race { */ public boolean improvesVelocity(VMG currentVMG, VMG potentialVMG, Bearing bearingToDestination) { + //Calculates the angle between the boat and its destination. Angle angleBetweenDestAndHeading = Angle.fromDegrees(currentVMG.getBearing().degrees() - bearingToDestination.degrees()); @@ -478,7 +478,7 @@ public class MockRace extends Race { boat.moveForwards(distanceTravelledMeters); - long tackPeriod = 15000; + long tackPeriod = 1000; if (boat.getTimeSinceTackChange() > tackPeriod) { //Calculate the new VMG. diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/CommandFactory.java b/racevisionGame/src/main/java/mock/model/commandFactory/CommandFactory.java index ace2f3be..8a53547c 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/CommandFactory.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/CommandFactory.java @@ -5,5 +5,5 @@ import mock.model.MockRace; public interface CommandFactory { - void runCommand(MockBoat boat, MockRace race); + void execute(); } diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java index ef776536..3a7dc0eb 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java @@ -11,8 +11,15 @@ import shared.model.Bearing; public class TackGybeCommand implements CommandFactory { //The refactoring of MockRace will require changes to be made + + private MockBoat boat; + + public TackGybeCommand(final MockBoat boat){ + this.boat = boat; + } + @Override - public void runCommand(MockBoat boat, MockRace race) { + public void execute() { /*VMG newVMG = boat.getPolars().calculateVMG( race.getWindDirection(), race.getWindSpeed(), @@ -23,5 +30,7 @@ public class TackGybeCommand implements CommandFactory { if(race.improvesVelocity(boatVMG, newVMG, boat.calculateBearingToNextMarker())){ boat.setVMG(newVMG); }*/ + Bearing newBearing = Bearing.fromDegrees(360d - boat.getBearing().degrees()); + boat.setBearing(newBearing); } } diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java index 5d11a27b..a28cfa57 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java @@ -10,15 +10,16 @@ import shared.model.Bearing; */ public class VMGCommand implements CommandFactory { + private MockBoat boat; + + public VMGCommand(final MockBoat boat){ + this.boat = boat; + } + + //The refactoring of MockRace will require changes to be made @Override - public void runCommand(MockBoat boat, MockRace race) { - /*VMG newVMG = boat.getPolars().calculateVMG( - race.getWindDirection(), - race.getWindSpeed(), - boat.calculateBearingToNextMarker(), - Bearing.fromDegrees(0d), - Bearing.fromDegrees(359.99999d)); - boat.setVMG(newVMG);*/ + public void execute() { + //MOCKBOAT SHOULD HAVE PARAMETER TO TOGGLE AUTO-VMG ON AND OFF } } From be8b0e672dfb2c77be2283e66ef89687d694bb02 Mon Sep 17 00:00:00 2001 From: hba56 Date: Fri, 4 Aug 2017 22:01:21 +1200 Subject: [PATCH 07/56] Updated the xml reader to pull in the rounding type of the compound marks and set each mark with that value #story[1101] --- .../java/shared/dataInput/RaceXMLReader.java | 15 ++++++++ .../main/java/shared/enums/RoundingType.java | 34 +++++++++++++++++++ .../main/java/shared/model/CompoundMark.java | 23 +++++++++++++ .../src/main/java/shared/model/Mark.java | 1 - 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 racevisionGame/src/main/java/shared/enums/RoundingType.java diff --git a/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java b/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java index 7e61b3de..a270056a 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,18 @@ 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.valueOf(cornerRounding)); + //For each following corner, create a leg between cornerN and cornerN+1. for(int i = 1; i < corners.getLength(); i++) { @@ -346,9 +355,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..eaba4e73 --- /dev/null +++ b/racevisionGame/src/main/java/shared/enums/RoundingType.java @@ -0,0 +1,34 @@ +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; +} diff --git a/racevisionGame/src/main/java/shared/model/CompoundMark.java b/racevisionGame/src/main/java/shared/model/CompoundMark.java index b9f45753..38e972dc 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,20 @@ public class CompoundMark { return averageCoordinate; } + + /** + * 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; + } } diff --git a/racevisionGame/src/main/java/shared/model/Mark.java b/racevisionGame/src/main/java/shared/model/Mark.java index 5781861a..19dc8f26 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. From db1efab225d31ca3b6d99d8865fc423d68e603df Mon Sep 17 00:00:00 2001 From: hba56 Date: Fri, 4 Aug 2017 22:17:17 +1200 Subject: [PATCH 08/56] rounding checks are now done by port or starboard side depending on what the compound marks type is #story[1101] --- .../src/main/java/mock/model/MockRace.java | 93 ++++++++++++++----- 1 file changed, 71 insertions(+), 22 deletions(-) diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index 3c394767..448344a6 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -4,18 +4,17 @@ 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 network.Messages.Enums.RaceStatusEnum; import shared.dataInput.RegattaDataSource; import shared.model.*; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; -import java.time.temporal.TemporalUnit; import java.util.*; import static java.lang.Math.cos; @@ -771,6 +770,68 @@ public class MockRace extends Race { } + /** + * 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 + */ + private void boatRoundingCheckPort(MockBoat boat, List roundingChecks){ + //boats must pass all checks in order to round a mark + switch (boat.getRoundingStatus()) { + case 0://hasn't started rounding + if (boat.isPortSide(boat.getCurrentLeg().getEndCompoundMark().getMark1()) && + GPSCoordinate.intersects(boat.getCurrentLeg().getEndCompoundMark().getMark1().getPosition(), + roundingChecks.get(0), boat.getCurrentPosition())) { + boat.increaseRoundingStatus(); + } + break; + case 1://has been parallel to the mark + if (boat.isPortSide(boat.getCurrentLeg().getEndCompoundMark().getMark1()) && + GPSCoordinate.intersects(boat.getCurrentLeg().getEndCompoundMark().getMark1().getPosition(), + roundingChecks.get(1), boat.getCurrentPosition())) { + 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 + */ + private void boatRoundingCheckStarboard(MockBoat boat, List roundingChecks){ + //boats must pass all checks in order to round a mark + switch (boat.getRoundingStatus()) { + case 0://hasn't started rounding + if (boat.isStarboardSide(boat.getCurrentLeg().getEndCompoundMark().getMark1()) && + GPSCoordinate.intersects(boat.getCurrentLeg().getEndCompoundMark().getMark1().getPosition(), + roundingChecks.get(0), boat.getCurrentPosition())) { + boat.increaseRoundingStatus(); + } + break; + case 1://has been parallel to the mark + if (boat.isStarboardSide(boat.getCurrentLeg().getEndCompoundMark().getMark1()) && + GPSCoordinate.intersects(boat.getCurrentLeg().getEndCompoundMark().getMark1().getPosition(), + roundingChecks.get(1), boat.getCurrentPosition())) { + 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. @@ -798,28 +859,16 @@ public class MockRace extends Race { GPSCoordinate roundCheck2 = GPSCoordinate.calculateNewPosition(startDirectionLinePoint, epsilonNauticalMiles, Azimuth.fromDegrees(bearingOfDirectionLine.degrees())); + List roundingChecks = new ArrayList(Arrays.asList(roundCheck1, roundCheck2)); - //boats must pass all checks in order to round a mark - switch (boat.getRoundingStatus()){ - case 0://hasn't started rounding - if (boat.isPortSide(boat.getCurrentLeg().getEndCompoundMark().getMark1()) && - GPSCoordinate.intersects(boat.getCurrentLeg().getEndCompoundMark().getMark1().getPosition(), - roundCheck1, boat.getCurrentPosition())){ - boat.increaseRoundingStatus(); - } - break; - case 1://has been parallel to the mark - if (boat.isPortSide(boat.getCurrentLeg().getEndCompoundMark().getMark1()) && - GPSCoordinate.intersects(boat.getCurrentLeg().getEndCompoundMark().getMark1().getPosition(), - roundCheck2, boat.getCurrentPosition())){ - boat.increaseRoundingStatus(); - } + switch (boat.getCurrentLeg().getEndCompoundMark().getRoundingType()) { + case SP://Not yet implemented so these gates will be rounded port side + case Port: + boatRoundingCheckPort(boat, roundingChecks); 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); + case PS://not yet implemented so these gates will be rounded starboard side + case Starboard: + boatRoundingCheckStarboard(boat, roundingChecks); break; } From 0b74acadff0488e2f61359a55567031851c09f3a Mon Sep 17 00:00:00 2001 From: hba56 Date: Fri, 4 Aug 2017 22:19:07 +1200 Subject: [PATCH 09/56] rounding checks are now done by port or starboard side depending on what the compound marks type is #story[1101] --- racevisionGame/src/main/java/mock/model/MockRace.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index 448344a6..25997ebd 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -837,7 +837,7 @@ public class MockRace extends Race { * @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) {//TODO cater for gates and rounding that aren't port side + protected void checkPosition(MockBoat boat, long timeElapsed) {//TODO cater for gates (SP or PS in the switch) //The distance, in nautical miles, within which the boat needs to get in order to consider that it has reached the marker. double epsilonNauticalMiles = 250.0 / Constants.NMToMetersConversion; //250 meters. @@ -861,7 +861,7 @@ public class MockRace extends Race { List roundingChecks = new ArrayList(Arrays.asList(roundCheck1, roundCheck2)); - switch (boat.getCurrentLeg().getEndCompoundMark().getRoundingType()) { + switch (boat.getCurrentLeg().getEndCompoundMark().getRoundingType()) {//todo may need to implement SP and PS case SP://Not yet implemented so these gates will be rounded port side case Port: boatRoundingCheckPort(boat, roundingChecks); From 074e2e590bcbfa968dfed5a306967fb2b4d5d3ea Mon Sep 17 00:00:00 2001 From: zwu18 Date: Sun, 6 Aug 2017 14:48:46 +1200 Subject: [PATCH 10/56] VMGCommand now toggles autoVMG on and off. Linked up observer and observable so the current boat can now be controlled. #Story[1097] --- .../src/main/java/mock/app/ConnectionAcceptor.java | 13 ++++++++++++- racevisionGame/src/main/java/mock/app/Event.java | 5 +++++ .../src/main/java/mock/model/MockBoat.java | 4 ++++ .../mock/model/commandFactory/TackGybeCommand.java | 2 ++ .../java/mock/model/commandFactory/VMGCommand.java | 7 +++++++ .../visualiser/gameController/ControllerServer.java | 9 ++++++++- 6 files changed, 38 insertions(+), 2 deletions(-) diff --git a/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java b/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java index 97a974a9..37ed83ed 100644 --- a/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java +++ b/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java @@ -1,5 +1,6 @@ package mock.app; +import mock.model.RaceLogic; import network.Messages.Enums.XMLMessageType; import network.Messages.LatestMessages; import network.Messages.XMLMessage; @@ -43,6 +44,10 @@ public class ConnectionAcceptor implements Runnable { private short boatXMLSequenceNumber; //regatta xml sequence number private short regattaXMLSequenceNumber; + //controller server + private ControllerServer controllerServer; + // + private RaceLogic rl = null; /** * Connection Acceptor Constructor @@ -65,6 +70,11 @@ public class ConnectionAcceptor implements Runnable { return serverPort; } + + public void setRace(RaceLogic rl){ + this.rl = rl; + } + /** * Run the Acceptor */ @@ -76,9 +86,10 @@ public class ConnectionAcceptor implements Runnable { Socket mockSocket = serverSocket.accept(); DataOutputStream outToVisualiser = new DataOutputStream(mockSocket.getOutputStream()); MockOutput mockOutput = new MockOutput(latestMessages, outToVisualiser); - ControllerServer controllerServer = new ControllerServer(mockSocket); + this.controllerServer = new ControllerServer(mockSocket, rl); new Thread(mockOutput).start(); new Thread(controllerServer).start(); + System.out.println("I'm in connectionAcceptor"); mockOutputList.add(mockOutput); System.out.println(String.format("%d number of Visualisers Connected.", mockOutputList.size())); } catch (IOException e) { diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index b4b0586c..15113159 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -82,6 +82,7 @@ public class Event { * @throws InvalidRegattaDataException Thrown if the regatta xml file cannot be parsed. */ public void start() throws InvalidRaceDataException, XMLReaderException, InvalidBoatDataException, InvalidRegattaDataException { + new Thread(mockOutput).start(); sendXMLs(); @@ -94,7 +95,11 @@ public class Event { //Create and start race. RaceLogic newRace = new RaceLogic(new MockRace(boatDataSource, raceDataSource, regattaDataSource, this.latestMessages, this.boatPolars, Constants.RaceTimeScale), this.latestMessages); + mockOutput.setRace(newRace); + new Thread(newRace).start(); + + System.out.println("I'm in event"); } /** diff --git a/racevisionGame/src/main/java/mock/model/MockBoat.java b/racevisionGame/src/main/java/mock/model/MockBoat.java index 104fa264..2ed15b6c 100644 --- a/racevisionGame/src/main/java/mock/model/MockBoat.java +++ b/racevisionGame/src/main/java/mock/model/MockBoat.java @@ -203,4 +203,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/commandFactory/TackGybeCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java index 150a1da8..11acfa0a 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java @@ -2,6 +2,7 @@ package mock.model.commandFactory; import mock.model.MockBoat; import mock.model.MockRace; +import shared.model.Bearing; /** * Created by David on 2/08/2017. @@ -28,5 +29,6 @@ public class TackGybeCommand implements Command { if(race.improvesVelocity(boatVMG, newVMG, boat.calculateBearingToNextMarker())){ boat.setVMG(newVMG); }*/ + this.boat.setBearing(Bearing.fromDegrees(360 - boat.getBearing().degrees())); } } diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java index 64cc6a9f..9cd558a5 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java @@ -25,5 +25,12 @@ public class VMGCommand implements Command { 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/visualiser/gameController/ControllerServer.java b/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java index a2f8c80e..af2efec9 100644 --- a/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java +++ b/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java @@ -1,5 +1,6 @@ package visualiser.gameController; +import mock.model.RaceLogic; import mock.model.commandFactory.Command; import mock.model.commandFactory.CommandFactory; import network.BinaryMessageDecoder; @@ -29,13 +30,19 @@ public class ControllerServer extends Observable implements Runnable { * Last received boat action */ private BoatActionEnum action; + /** + * + */ + private RaceLogic rc; /** * Initialise server-side controller with live client socket * @param socket to client */ - public ControllerServer(Socket socket) { + public ControllerServer(Socket socket, RaceLogic rc) { this.socket = socket; + this.rc = rc; + this.addObserver(rc); try { this.inputStream = new DataInputStream(this.socket.getInputStream()); } catch (IOException e) { From 18f14c7542e258b466b8b624c5dda72f3293358e Mon Sep 17 00:00:00 2001 From: zwu18 Date: Sun, 6 Aug 2017 16:12:53 +1200 Subject: [PATCH 11/56] VMGCommand now toggles autoVMG on and off. Linked up observer and observable so the current boat can now be controlled. Fixed autoVMG not working. #Story[1097] --- .../src/main/java/mock/model/MockRace.java | 2 +- .../model/commandFactory/TackGybeCommand.java | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index 0ae5cfcc..7cb91ebc 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -350,7 +350,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. diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java index 11acfa0a..ff04c54e 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java @@ -2,6 +2,7 @@ package mock.model.commandFactory; import mock.model.MockBoat; import mock.model.MockRace; +import mock.model.VMG; import shared.model.Bearing; /** @@ -19,16 +20,14 @@ public class TackGybeCommand implements Command { //The refactoring of MockRace will require changes to be made @Override public void execute() { - /*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); + /*if(boat.getBearing().degrees()>180){ + boat.setBearing(Bearing.fromDegrees(360 - race.getWindDirection().degrees())); + } else { + boat.setBearing(Bearing.fromDegrees(race.getWindDirection().degrees())); }*/ - this.boat.setBearing(Bearing.fromDegrees(360 - boat.getBearing().degrees())); + System.out.println(race.getWindDirection().degrees()); + double angle = Math.max(race.getWindDirection().degrees(), boat.getBearing().degrees()) - Math.min(race.getWindDirection().degrees(), boat.getBearing().degrees()); + boat.setBearing(Bearing.fromDegrees(angle)); } } + From a38898982764d19815b981b0e1879d50f05fb706 Mon Sep 17 00:00:00 2001 From: zwu18 Date: Sun, 6 Aug 2017 16:38:44 +1200 Subject: [PATCH 12/56] Reworked TackGybeCommand. Current boat in race will now tack and gybe when control is pressed. #Story[1097] --- .../model/commandFactory/TackGybeCommand.java | 22 ++++++++++++++++--- .../mock/model/commandFactory/VMGCommand.java | 7 ------ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java index ff04c54e..f964a17e 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java @@ -25,9 +25,25 @@ public class TackGybeCommand implements Command { } else { boat.setBearing(Bearing.fromDegrees(race.getWindDirection().degrees())); }*/ - System.out.println(race.getWindDirection().degrees()); - double angle = Math.max(race.getWindDirection().degrees(), boat.getBearing().degrees()) - Math.min(race.getWindDirection().degrees(), boat.getBearing().degrees()); - boat.setBearing(Bearing.fromDegrees(angle)); + /*double angle = Math.max(race.getWindDirection().degrees(), boat.getBearing().degrees()) - Math.min(race.getWindDirection().degrees(), boat.getBearing().degrees()); + boat.setBearing(Bearing.fromDegrees(angle));*/ + 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)); + } + } + + private double calcDistance(double degreeA, double degreeB){ + double phi = Math.abs(degreeB - degreeA) % 360; + double distance = phi > 180 ? 360 - phi : phi; + return distance; } + } diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java index 9cd558a5..d6e3d988 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java @@ -18,13 +18,6 @@ public class VMGCommand implements Command { //The refactoring of MockRace will require changes to be made @Override public void execute() { - /*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!"); From ddaa2623ca3eb2a11ae4d2709b231cbf2e7cd710 Mon Sep 17 00:00:00 2001 From: zwu18 Date: Mon, 7 Aug 2017 02:56:24 +1200 Subject: [PATCH 13/56] Added tests for TackGybeCommand class. #Story[1097] --- .../model/commandFactory/TackGybeCommand.java | 2 +- .../commandFactory/TackGybeCommandTest.java | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 racevisionGame/src/test/java/mock/model/commandFactory/TackGybeCommandTest.java diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java index f964a17e..447ab2fb 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java @@ -39,7 +39,7 @@ public class TackGybeCommand implements Command { } } - private double calcDistance(double degreeA, double degreeB){ + public double calcDistance(double degreeA, double degreeB){ double phi = Math.abs(degreeB - degreeA) % 360; double distance = phi > 180 ? 360 - phi : phi; return distance; diff --git a/racevisionGame/src/test/java/mock/model/commandFactory/TackGybeCommandTest.java b/racevisionGame/src/test/java/mock/model/commandFactory/TackGybeCommandTest.java new file mode 100644 index 00000000..7ac71956 --- /dev/null +++ b/racevisionGame/src/test/java/mock/model/commandFactory/TackGybeCommandTest.java @@ -0,0 +1,42 @@ +package mock.model.commandFactory; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Created by David on 7/08/2017. + */ +public class TackGybeCommandTest { + + private double degreeA; + private double degreeB; + private double degreeC; + private double degreeD; + TackGybeCommand tgc; + + //Run before tests + @Before + public void setUp(){ + degreeA = 150.0; + degreeB = 300.0; + degreeC = 10.0; + degreeD = 350.0; + tgc = new TackGybeCommand(null, null); + } + + //Test when degree difference is <180 + @Test + public void angleDistanceCalculationLow(){ + double result = tgc.calcDistance(degreeA, degreeB); + assertEquals(150.0, result, 0); + } + + //Test when degree difference is >180 + @Test + public void angleDistanceCalculationHigh(){ + double result = tgc.calcDistance(degreeC, degreeD); + assertEquals(20.0, result, 0); + } +} From 0bf2c6106b21833485d9a2ef4521add1754f18b5 Mon Sep 17 00:00:00 2001 From: Joseph Gardner Date: Mon, 7 Aug 2017 12:03:34 +1200 Subject: [PATCH 14/56] Boat updating speed after key press has been fixed. #fix #story[1094] --- racevisionGame/src/main/java/mock/model/MockRace.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index 7cb91ebc..3ac8418e 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -374,8 +374,8 @@ 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()); } From 950a801d1609b09e7ef2465be97f239da810daa7 Mon Sep 17 00:00:00 2001 From: hba56 Date: Mon, 7 Aug 2017 12:16:37 +1200 Subject: [PATCH 15/56] rounding fix for the xml #story[1087] --- .idea/codeStyleSettings.xml | 6 ++++++ .../src/main/java/mock/model/MockBoat.java | 2 +- .../src/main/java/mock/model/MockRace.java | 3 +++ .../main/java/shared/dataInput/RaceXMLReader.java | 3 ++- .../src/main/java/shared/enums/RoundingType.java | 15 +++++++++++++++ .../src/main/resources/mock/mockXML/raceTest.xml | 12 ++++++------ 6 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 .idea/codeStyleSettings.xml diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 00000000..5352bdf8 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/racevisionGame/src/main/java/mock/model/MockBoat.java b/racevisionGame/src/main/java/mock/model/MockBoat.java index 2874aa30..795e2675 100644 --- a/racevisionGame/src/main/java/mock/model/MockBoat.java +++ b/racevisionGame/src/main/java/mock/model/MockBoat.java @@ -28,7 +28,7 @@ public class MockBoat extends Boat { * 1: passed only first check * 2: passed first and second check */ - private Integer roundingStatus; + private Integer roundingStatus = 0; diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index 25997ebd..a3be5381 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -779,6 +779,7 @@ public class MockRace extends Race { //boats must pass all checks in order to round a mark switch (boat.getRoundingStatus()) { case 0://hasn't started rounding + System.out.println("round 0"); if (boat.isPortSide(boat.getCurrentLeg().getEndCompoundMark().getMark1()) && GPSCoordinate.intersects(boat.getCurrentLeg().getEndCompoundMark().getMark1().getPosition(), roundingChecks.get(0), boat.getCurrentPosition())) { @@ -786,6 +787,7 @@ public class MockRace extends Race { } break; case 1://has been parallel to the mark + System.out.println("round 1"); if (boat.isPortSide(boat.getCurrentLeg().getEndCompoundMark().getMark1()) && GPSCoordinate.intersects(boat.getCurrentLeg().getEndCompoundMark().getMark1().getPosition(), roundingChecks.get(1), boat.getCurrentPosition())) { @@ -793,6 +795,7 @@ public class MockRace extends Race { } break; case 2://has traveled 180 degrees around the mark + System.out.println("round 2"); //Move boat on to next leg. boat.resetRoundingStatus(); Leg nextLeg = this.legs.get(boat.getCurrentLeg().getLegNumber() + 1); diff --git a/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java b/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java index a270056a..135cd988 100644 --- a/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java +++ b/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java @@ -344,7 +344,8 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource { String legName = lastCompoundMark.getName(); //Sets the rounding type of this compound mark - lastCompoundMark.setRoundingType(RoundingType.valueOf(cornerRounding)); + + 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++) { diff --git a/racevisionGame/src/main/java/shared/enums/RoundingType.java b/racevisionGame/src/main/java/shared/enums/RoundingType.java index eaba4e73..8f8e719a 100644 --- a/racevisionGame/src/main/java/shared/enums/RoundingType.java +++ b/racevisionGame/src/main/java/shared/enums/RoundingType.java @@ -31,4 +31,19 @@ public enum RoundingType { * 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/resources/mock/mockXML/raceTest.xml b/racevisionGame/src/main/resources/mock/mockXML/raceTest.xml index 83e36f85..e068e9b8 100644 --- a/racevisionGame/src/main/resources/mock/mockXML/raceTest.xml +++ b/racevisionGame/src/main/resources/mock/mockXML/raceTest.xml @@ -8,12 +8,12 @@ - - - - - - + + + + + + From 4195d41814021cd1ba6f7e27e82d104bcd196f30 Mon Sep 17 00:00:00 2001 From: hba56 Date: Mon, 7 Aug 2017 12:28:44 +1200 Subject: [PATCH 16/56] removed outdated comments #story[1087] --- racevisionGame/src/main/java/mock/model/MockBoat.java | 2 ++ racevisionGame/src/main/java/mock/model/MockRace.java | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/racevisionGame/src/main/java/mock/model/MockBoat.java b/racevisionGame/src/main/java/mock/model/MockBoat.java index 795e2675..214d9c10 100644 --- a/racevisionGame/src/main/java/mock/model/MockBoat.java +++ b/racevisionGame/src/main/java/mock/model/MockBoat.java @@ -71,6 +71,8 @@ public class MockBoat extends Boat { //Get the start and end points. GPSCoordinate currentPosition = this.getCurrentPosition(); GPSCoordinate nextMarkerPosition = this.getCurrentLeg().getEndCompoundMark().getAverageGPSCoordinate(); +// GPSCoordinate nextMarkerPosition = this.getCurrentLeg().getEndCompoundMark().getMark1Position(); +// todo:may need to change this so that boats are send to corners of the gates rather than the middle //Calculate bearing. Bearing bearing = GPSCoordinate.calculateBearing(currentPosition, nextMarkerPosition); diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index a3be5381..c534219b 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -779,7 +779,7 @@ public class MockRace extends Race { //boats must pass all checks in order to round a mark switch (boat.getRoundingStatus()) { case 0://hasn't started rounding - System.out.println("round 0"); +// System.out.println("round 0"); if (boat.isPortSide(boat.getCurrentLeg().getEndCompoundMark().getMark1()) && GPSCoordinate.intersects(boat.getCurrentLeg().getEndCompoundMark().getMark1().getPosition(), roundingChecks.get(0), boat.getCurrentPosition())) { @@ -787,7 +787,7 @@ public class MockRace extends Race { } break; case 1://has been parallel to the mark - System.out.println("round 1"); +// System.out.println("round 1"); if (boat.isPortSide(boat.getCurrentLeg().getEndCompoundMark().getMark1()) && GPSCoordinate.intersects(boat.getCurrentLeg().getEndCompoundMark().getMark1().getPosition(), roundingChecks.get(1), boat.getCurrentPosition())) { @@ -795,7 +795,7 @@ public class MockRace extends Race { } break; case 2://has traveled 180 degrees around the mark - System.out.println("round 2"); +// System.out.println("round 2"); //Move boat on to next leg. boat.resetRoundingStatus(); Leg nextLeg = this.legs.get(boat.getCurrentLeg().getLegNumber() + 1); @@ -840,7 +840,7 @@ public class MockRace extends Race { * @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) {//TODO cater for gates (SP or PS in the switch) + 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 = 250.0 / Constants.NMToMetersConversion; //250 meters. @@ -864,7 +864,7 @@ public class MockRace extends Race { List roundingChecks = new ArrayList(Arrays.asList(roundCheck1, roundCheck2)); - switch (boat.getCurrentLeg().getEndCompoundMark().getRoundingType()) {//todo may need to implement SP and PS + switch (boat.getCurrentLeg().getEndCompoundMark().getRoundingType()) { case SP://Not yet implemented so these gates will be rounded port side case Port: boatRoundingCheckPort(boat, roundingChecks); From 62752c142a3842a3518ee271b0a7702457284ece Mon Sep 17 00:00:00 2001 From: hba56 Date: Mon, 7 Aug 2017 12:47:55 +1200 Subject: [PATCH 17/56] building blocks for drawing the race line around the course #story[1087] --- racevisionGame/src/main/java/shared/model/Race.java | 7 +++++++ .../java/visualiser/model/ResizableRaceCanvas.java | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/racevisionGame/src/main/java/shared/model/Race.java b/racevisionGame/src/main/java/shared/model/Race.java index 415e9f77..8b327fb4 100644 --- a/racevisionGame/src/main/java/shared/model/Race.java +++ b/racevisionGame/src/main/java/shared/model/Race.java @@ -323,6 +323,13 @@ public abstract class Race implements Runnable { 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/model/ResizableRaceCanvas.java b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java index 7664f854..386468c8 100644 --- a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java @@ -1,6 +1,7 @@ package visualiser.model; +import javafx.collections.ObservableList; import javafx.scene.Node; import javafx.scene.image.Image; import javafx.scene.paint.Color; @@ -9,6 +10,7 @@ import javafx.scene.transform.Rotate; import network.Messages.Enums.BoatStatusEnum; import shared.dataInput.RaceDataSource; import shared.model.GPSCoordinate; +import shared.model.Leg; import shared.model.Mark; import shared.model.RaceClock; @@ -516,6 +518,17 @@ 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(); + + for (Leg leg: legs) { + //todo calculate and draw line around this leg + } + + } /** * Draws the race boundary image onto the canvas. From 2920b6cf235039c344c8158269083e51754afdd9 Mon Sep 17 00:00:00 2001 From: cbt24 Date: Mon, 7 Aug 2017 16:47:25 +1200 Subject: [PATCH 18/56] Removed build-breaking code from MockRace #story[1096] --- .../src/main/java/mock/model/MockRace.java | 83 ------------------- 1 file changed, 83 deletions(-) diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index 3a6ad64a..fe1a60c1 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -575,13 +575,6 @@ public class MockRace extends Race { boat.setCurrentSpeed(0); boat.setStatus(BoatStatusEnum.FINISHED); - } else if (doNotFinish()) { - //Boat has pulled out of race. - boat.setTimeFinished(timeElapsed); - boat.setCurrentLeg(new Leg("DNF", -1)); - boat.setCurrentSpeed(0); - boat.setStatus(BoatStatusEnum.DNF); - } } @@ -589,82 +582,6 @@ public class MockRace extends Race { } -//old method fo checking if boats passed a mark -// -// /** -// * Checks if a boat has finished any legs, or has pulled out of race (DNF). -// * @param boat The boat to check. -// * @param timeElapsed The total time, in milliseconds, that has elapsed since the race started. -// */ -// protected void checkPosition(MockBoat boat, long timeElapsed) { -// -// //The distance, in nautical miles, within which the boat needs to get in order to consider that it has reached the marker. -// double epsilonNauticalMiles = 100.0 / Constants.NMToMetersConversion; //100 meters. TODO should be more like 5-10. -// -// if (boat.calculateDistanceToNextMarker() < epsilonNauticalMiles) { -// //Boat has reached its target marker, and has moved on to a new leg. -// -// -// -// //Calculate how much the boat overshot the marker by. -// double overshootMeters = boat.calculateDistanceToNextMarker(); -// -// -// //Move boat on to next leg. -// Leg nextLeg = this.legs.get(boat.getCurrentLeg().getLegNumber() + 1); -// boat.setCurrentLeg(nextLeg); -// -// //Add overshoot distance into the distance travelled for the next leg. -// boat.setDistanceTravelledInLeg(overshootMeters); -// -// //Setting a high value for this allows the boat to immediately do a large turn, as it needs to in order to get to the next mark. -// boat.setTimeSinceTackChange(999999); -// -// -// //Check if the boat has finished or stopped racing. -// -// if (this.isLastLeg(boat.getCurrentLeg())) { -// //Boat has finished. -// boat.setTimeFinished(timeElapsed); -// boat.setCurrentSpeed(0); -// boat.setStatus(BoatStatusEnum.FINISHED); -// -// } else if (doNotFinish()) { -// //Boat has pulled out of race. -// boat.setTimeFinished(timeElapsed); -// boat.setCurrentLeg(new Leg("DNF", -1)); -// boat.setCurrentSpeed(0); -// boat.setStatus(BoatStatusEnum.DNF); -// -// } -// -// } -// -// } - - - - - /** - * Sets the chance each boat has of failing at a gate or marker - * - * @param chance percentage chance a boat has of failing per checkpoint. - */ - protected void setDnfChance(int chance) { - if (chance >= 0 && chance <= 100) { - dnfChance = chance; - } - } - - /** - * Decides if a boat should received a DNF status. - * @return True means it should DNF, false means it shouldn't. - */ - protected boolean doNotFinish() { - Random rand = new Random(); - return rand.nextInt(100) < dnfChance; - } - /** From ef3f468b19e9caaa8de5ce17bc541e86d560ba3e Mon Sep 17 00:00:00 2001 From: hba56 Date: Tue, 8 Aug 2017 00:02:07 +1200 Subject: [PATCH 19/56] arrows displayed for each leg #story[1101] --- .../visualiser/model/ResizableRaceCanvas.java | 72 ++++++++++++++++--- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java index 386468c8..10ff60ec 100644 --- a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java @@ -1,7 +1,6 @@ package visualiser.model; -import javafx.collections.ObservableList; import javafx.scene.Node; import javafx.scene.image.Image; import javafx.scene.paint.Color; @@ -9,12 +8,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.Leg; -import shared.model.Mark; -import shared.model.RaceClock; +import shared.model.*; -import java.time.Duration; import java.util.List; /** @@ -513,6 +508,9 @@ public class ResizableRaceCanvas extends ResizableCanvas { //Marks. drawMarks(); + //Guiding Line + drawRaceLine(); + //Wind arrow. This rotates the wind arrow node. displayWindArrow(this.visualiserRace.getWindDirection().degrees()); @@ -523,11 +521,67 @@ public class ResizableRaceCanvas extends ResizableCanvas { */ public void drawRaceLine(){ List legs = this.visualiserRace.getLegs(); - - for (Leg leg: legs) { - //todo calculate and draw line around this leg + for (int i = 0; i < legs.size() -1; i++) { + drawLineRounding(legs.get(i)); } + } + + private void drawLineRounding(Leg leg){ + //finds the direction of the leg as a bearing + GPSCoordinate startDirectionLinePoint = leg.getStartCompoundMark().getAverageGPSCoordinate(); + GPSCoordinate endDirectionLinePoint = leg.getEndCompoundMark().getAverageGPSCoordinate(); + Bearing bearingOfDirectionLine = GPSCoordinate.calculateBearing(startDirectionLinePoint, endDirectionLinePoint); + + //use the direction line to find a point parallel to it by the mark + GPSCoordinate pointToStartCurve = GPSCoordinate.calculateNewPosition(endDirectionLinePoint, + 150, Azimuth.fromDegrees(bearingOfDirectionLine.degrees()+90)); + + //use the direction line to find a point to curve too + GPSCoordinate pointToEndCurve = GPSCoordinate.calculateNewPosition(endDirectionLinePoint, + 150, Azimuth.fromDegrees(bearingOfDirectionLine.degrees())); + + //use the curve points to find the two control points for the bezier curve + Bearing bearingOfCurveLine = GPSCoordinate.calculateBearing(pointToStartCurve, pointToEndCurve); + GPSCoordinate controlPoint1 = GPSCoordinate.calculateNewPosition(pointToStartCurve, + 150, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+ 50)); + + //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(controlPoint1); + + gc.setStroke(Color.RED); + gc.setLineWidth(1); + + gc.beginPath(); + gc.moveTo(startPath.getX(), startPath.getY()); + gc.lineTo(curvePoint.getX(), curvePoint.getY()); + gc.bezierCurveTo(c1.getX(), c1.getY(), c1.getX(), c1.getY(), curvePointEnd.getX(), curvePointEnd.getY()); + gc.stroke(); + gc.closePath(); + gc.save(); + + drawArrowHead(controlPoint1, pointToEndCurve); + } + + 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); } /** From 22722286ef40602e231b0fda5d4c11500bbce4c0 Mon Sep 17 00:00:00 2001 From: hba56 Date: Tue, 8 Aug 2017 14:47:11 +1200 Subject: [PATCH 20/56] all paths now join up #story[1087] --- .../visualiser/model/ResizableRaceCanvas.java | 109 ++++++++++++------ 1 file changed, 74 insertions(+), 35 deletions(-) diff --git a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java index 10ff60ec..2263de91 100644 --- a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java @@ -521,48 +521,87 @@ public class ResizableRaceCanvas extends ResizableCanvas { */ 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++) { - drawLineRounding(legs.get(i)); + nextStartPoint = drawLineRounding(legs, i, legStartPoint); + legStartPoint = nextStartPoint; } } - private void drawLineRounding(Leg leg){ - //finds the direction of the leg as a bearing - GPSCoordinate startDirectionLinePoint = leg.getStartCompoundMark().getAverageGPSCoordinate(); - GPSCoordinate endDirectionLinePoint = leg.getEndCompoundMark().getAverageGPSCoordinate(); - Bearing bearingOfDirectionLine = GPSCoordinate.calculateBearing(startDirectionLinePoint, endDirectionLinePoint); - - //use the direction line to find a point parallel to it by the mark - GPSCoordinate pointToStartCurve = GPSCoordinate.calculateNewPosition(endDirectionLinePoint, - 150, Azimuth.fromDegrees(bearingOfDirectionLine.degrees()+90)); - - //use the direction line to find a point to curve too - GPSCoordinate pointToEndCurve = GPSCoordinate.calculateNewPosition(endDirectionLinePoint, - 150, Azimuth.fromDegrees(bearingOfDirectionLine.degrees())); - - //use the curve points to find the two control points for the bezier curve - Bearing bearingOfCurveLine = GPSCoordinate.calculateBearing(pointToStartCurve, pointToEndCurve); - GPSCoordinate controlPoint1 = GPSCoordinate.calculateNewPosition(pointToStartCurve, - 150, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+ 50)); - - //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(controlPoint1); + /** + * 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; + endDirectionLinePoint = legs.get(index).getEndCompoundMark().getMark1Position(); + 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); + + //use the direction line to find a point parallel to it by the mark + GPSCoordinate pointToStartCurve = GPSCoordinate.calculateNewPosition(endDirectionLinePoint, + 150, Azimuth.fromDegrees(bearingOfDirectionLine.degrees()+90)); + + //use the direction line to find a point to curve too + GPSCoordinate pointToEndCurve = GPSCoordinate.calculateNewPosition(endDirectionLinePoint, + 150, Azimuth.fromDegrees(bearingOfNextDirectionLine.degrees()+90)); + + //use the curve points to find the two control points for the bezier curve + Bearing bearingOfCurveLine = GPSCoordinate.calculateBearing(pointToStartCurve, pointToEndCurve); + GPSCoordinate controlPoint = GPSCoordinate.calculateNewPosition(pointToStartCurve, + 75, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+ 50)); + + //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); + + gc.setStroke(Color.RED); + gc.setLineWidth(1); + + gc.beginPath(); + gc.moveTo(startPath.getX(), startPath.getY()); + gc.lineTo(curvePoint.getX(), curvePoint.getY()); + gc.bezierCurveTo(c1.getX(), c1.getY(), c1.getX(), c1.getY(), curvePointEnd.getX(), curvePointEnd.getY()); + gc.stroke(); + gc.closePath(); + gc.save(); - gc.setStroke(Color.RED); - gc.setLineWidth(1); + drawArrowHead(controlPoint, pointToEndCurve); - gc.beginPath(); - gc.moveTo(startPath.getX(), startPath.getY()); - gc.lineTo(curvePoint.getX(), curvePoint.getY()); - gc.bezierCurveTo(c1.getX(), c1.getY(), c1.getX(), c1.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()); - drawArrowHead(controlPoint1, pointToEndCurve); + 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){ From bb75806781f46618deaf0c3ce4b4469db6efbc51 Mon Sep 17 00:00:00 2001 From: hba56 Date: Tue, 8 Aug 2017 23:23:46 +1200 Subject: [PATCH 21/56] Cleaned up line to be in a shippable shape #story[1101] --- .../visualiser/model/ResizableRaceCanvas.java | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java index 2263de91..379f99e5 100644 --- a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java @@ -3,8 +3,7 @@ package visualiser.model; import javafx.scene.Node; import javafx.scene.image.Image; -import javafx.scene.paint.Color; -import javafx.scene.paint.Paint; +import javafx.scene.paint.*; import javafx.scene.transform.Rotate; import network.Messages.Enums.BoatStatusEnum; import shared.dataInput.RaceDataSource; @@ -502,15 +501,15 @@ public class ResizableRaceCanvas extends ResizableCanvas { //Race boundary. drawBoundary(); + //Guiding Line + drawRaceLine(); + //Boats. drawBoats(); //Marks. drawMarks(); - //Guiding Line - drawRaceLine(); - //Wind arrow. This rotates the wind arrow node. displayWindArrow(this.visualiserRace.getWindDirection().degrees()); @@ -558,36 +557,49 @@ public class ResizableRaceCanvas extends ResizableCanvas { //use the direction line to find a point parallel to it by the mark GPSCoordinate pointToStartCurve = GPSCoordinate.calculateNewPosition(endDirectionLinePoint, - 150, Azimuth.fromDegrees(bearingOfDirectionLine.degrees()+90)); + 100, Azimuth.fromDegrees(bearingOfDirectionLine.degrees()+90)); //use the direction line to find a point to curve too GPSCoordinate pointToEndCurve = GPSCoordinate.calculateNewPosition(endDirectionLinePoint, - 150, Azimuth.fromDegrees(bearingOfNextDirectionLine.degrees()+90)); + 100, Azimuth.fromDegrees(bearingOfNextDirectionLine.degrees()+90)); //use the curve points to find the two control points for the bezier curve + GPSCoordinate controlPoint; + GPSCoordinate controlPoint2; Bearing bearingOfCurveLine = GPSCoordinate.calculateBearing(pointToStartCurve, pointToEndCurve); - GPSCoordinate controlPoint = GPSCoordinate.calculateNewPosition(pointToStartCurve, - 75, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+ 50)); + if ((bearingOfDirectionLine.degrees() - bearingOfNextDirectionLine.degrees() +360)%360< 145){ + //small turn + controlPoint = GPSCoordinate.calculateNewPosition(pointToStartCurve, + 50, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+45)); + controlPoint2 = controlPoint; + }else{ + //large turn + controlPoint = GPSCoordinate.calculateNewPosition(pointToStartCurve, + 150, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+90)); + controlPoint2 = GPSCoordinate.calculateNewPosition(pointToEndCurve, + 150, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+90)); + } + //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.setStroke(Color.RED); - gc.setLineWidth(1); + gc.setLineWidth(2); + gc.setStroke(Color.MEDIUMAQUAMARINE); gc.beginPath(); gc.moveTo(startPath.getX(), startPath.getY()); gc.lineTo(curvePoint.getX(), curvePoint.getY()); - gc.bezierCurveTo(c1.getX(), c1.getY(), c1.getX(), c1.getY(), curvePointEnd.getX(), curvePointEnd.getY()); + drawArrowHead(startDirectionLinePoint, pointToStartCurve); + gc.bezierCurveTo(c1.getX(), c1.getY(), c2.getX(), c2.getY(), curvePointEnd.getX(), curvePointEnd.getY()); gc.stroke(); gc.closePath(); gc.save(); - drawArrowHead(controlPoint, pointToEndCurve); - return pointToEndCurve; }else{//last leg so no curve GraphCoordinate startPath = this.map.convertGPS(legStartPoint); From 03713d3699fd76cc7b64463093c4c7573d6cde9e Mon Sep 17 00:00:00 2001 From: zwu18 Date: Wed, 9 Aug 2017 16:37:29 +1200 Subject: [PATCH 22/56] Added checkPosition into MockRace which was removed. Boat now updates legs again. #Story[1097] --- .../src/main/java/mock/model/MockRace.java | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index 3ac8418e..87c0fa7c 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -346,7 +346,7 @@ public class MockRace extends Race { this.updateEstimatedTime(boat); } - + checkPosition(boat, totalElapsedMilliseconds); } private void newOptimalVMG(MockBoat boat) { @@ -443,6 +443,44 @@ public class MockRace extends Race { } + /** + * 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 + if (this.isLastLeg(boat.getCurrentLeg())) { + //Boat has finished. + boat.setTimeFinished(timeElapsed); + boat.setCurrentSpeed(0); + boat.setStatus(BoatStatusEnum.FINISHED); + } + } + + } + + public List getCompoundMarks() { return compoundMarks; } From e53d72f24b722c79e333e3917e6473925eca84d8 Mon Sep 17 00:00:00 2001 From: hba56 Date: Wed, 9 Aug 2017 23:25:42 +1200 Subject: [PATCH 23/56] compound marks now can give the mark they have that needs to be rounded this is used to draw the line around the correct mark #story[1101] --- .../main/java/shared/model/CompoundMark.java | 87 +++++++++++++++++++ .../visualiser/model/ResizableRaceCanvas.java | 32 +++++-- 2 files changed, 110 insertions(+), 9 deletions(-) diff --git a/racevisionGame/src/main/java/shared/model/CompoundMark.java b/racevisionGame/src/main/java/shared/model/CompoundMark.java index 38e972dc..05256655 100644 --- a/racevisionGame/src/main/java/shared/model/CompoundMark.java +++ b/racevisionGame/src/main/java/shared/model/CompoundMark.java @@ -164,4 +164,91 @@ public class CompoundMark { 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/visualiser/model/ResizableRaceCanvas.java b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java index 96a8d6f9..333ed54b 100644 --- a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java @@ -1,12 +1,13 @@ package visualiser.model; -import javafx.scene.Node; import javafx.scene.image.Image; -import javafx.scene.paint.*; +import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; import javafx.scene.transform.Rotate; import network.Messages.Enums.BoatStatusEnum; import shared.dataInput.RaceDataSource; +import shared.enums.RoundingType; import shared.model.*; import java.util.List; @@ -510,7 +511,12 @@ public class ResizableRaceCanvas extends ResizableCanvas { //finds the direction of the current leg as a bearing startDirectionLinePoint = legStartPoint; - endDirectionLinePoint = legs.get(index).getEndCompoundMark().getMark1Position(); + 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 @@ -519,13 +525,22 @@ public class ResizableRaceCanvas extends ResizableCanvas { 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()+90)); + 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()+90)); + 100, Azimuth.fromDegrees(bearingOfNextDirectionLine.degrees()+degreesToAdd)); //use the curve points to find the two control points for the bezier curve GPSCoordinate controlPoint; @@ -534,14 +549,14 @@ public class ResizableRaceCanvas extends ResizableCanvas { if ((bearingOfDirectionLine.degrees() - bearingOfNextDirectionLine.degrees() +360)%360< 145){ //small turn controlPoint = GPSCoordinate.calculateNewPosition(pointToStartCurve, - 50, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+45)); + 50, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+(degreesToAdd/2))); controlPoint2 = controlPoint; }else{ //large turn controlPoint = GPSCoordinate.calculateNewPosition(pointToStartCurve, - 150, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+90)); + 150, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+degreesToAdd)); controlPoint2 = GPSCoordinate.calculateNewPosition(pointToEndCurve, - 150, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+90)); + 150, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+degreesToAdd)); } @@ -618,7 +633,6 @@ public class ResizableRaceCanvas extends ResizableCanvas { * @see TrackPoint */ private void drawTrack(VisualiserBoat boat) { - //Check that track points are enabled. if (this.annoPath) { From 7cc39abe5772bf29bc541535d0076eb7c8ce8a6b Mon Sep 17 00:00:00 2001 From: fjc40 Date: Thu, 10 Aug 2017 12:13:40 +1200 Subject: [PATCH 24/56] WIP. Probably need to cherry pick stuff out of here. Added ClientConnection and server-side handshake. Added MessageSerialiser and Deserialiser. #story[1095] --- .../java/mock/app/ConnectionAcceptor.java | 100 ++++-- .../src/main/java/mock/app/Event.java | 90 +++-- .../src/main/java/mock/app/MockOutput.java | 307 ++---------------- .../java/mock/enums/ConnectionStateEnum.java | 84 +++++ .../EventConstructionException.java | 24 ++ .../SourceIDAllocationException.java | 24 ++ .../java/mock/model/ClientConnection.java | 243 ++++++++++++++ .../java/mock/model/HeartBeatService.java | 110 +++++++ .../src/main/java/mock/model/MockRace.java | 20 ++ .../src/main/java/mock/model/RaceLogic.java | 39 ++- .../src/main/java/mock/model/RaceServer.java | 76 +++-- .../java/mock/model/SourceIdAllocator.java | 70 ++++ .../commandFactory/CompositeCommand.java | 25 ++ .../MessageControllers/MessageController.java | 9 + .../RaceVisionByteEncoder.java | 28 +- .../network/MessageRouters/MessageRouter.java | 11 + .../java/network/Messages/BoatAction.java | 21 ++ .../java/network/Messages/LatestMessages.java | 26 +- .../java/network/Messages/RaceSnapshot.java | 41 +++ .../StreamRelated/MessageDeserialiser.java | 156 +++++++++ .../StreamRelated/MessageSerialiser.java | 116 +++++++ .../exceptions/BoatNotFoundException.java | 15 + .../shared/exceptions/HandshakeException.java | 24 ++ .../shared/model/RunnableWithFramePeriod.java | 64 ++++ .../Controllers/ConnectionController.java | 29 +- .../Controllers/HostController.java | 16 +- .../Controllers/RaceController.java | 7 +- .../Controllers/StartController.java | 25 +- .../java/visualiser/app/VisualiserInput.java | 306 ++--------------- .../gameController/ControllerClient.java | 40 +-- .../gameController/ControllerServer.java | 81 +++-- .../gameController/Keys/KeyFactory.java | 4 +- .../visualiser/model/ServerConnection.java | 228 +++++++++++++ .../mock/model/SourceIdAllocatorTest.java | 126 +++++++ .../model/commandFactory/WindCommandTest.java | 31 ++ 35 files changed, 1830 insertions(+), 786 deletions(-) create mode 100644 racevisionGame/src/main/java/mock/enums/ConnectionStateEnum.java create mode 100644 racevisionGame/src/main/java/mock/exceptions/EventConstructionException.java create mode 100644 racevisionGame/src/main/java/mock/exceptions/SourceIDAllocationException.java create mode 100644 racevisionGame/src/main/java/mock/model/ClientConnection.java create mode 100644 racevisionGame/src/main/java/mock/model/HeartBeatService.java create mode 100644 racevisionGame/src/main/java/mock/model/SourceIdAllocator.java create mode 100644 racevisionGame/src/main/java/mock/model/commandFactory/CompositeCommand.java create mode 100644 racevisionGame/src/main/java/network/MessageControllers/MessageController.java create mode 100644 racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java create mode 100644 racevisionGame/src/main/java/network/Messages/RaceSnapshot.java create mode 100644 racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java create mode 100644 racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java create mode 100644 racevisionGame/src/main/java/shared/exceptions/BoatNotFoundException.java create mode 100644 racevisionGame/src/main/java/shared/exceptions/HandshakeException.java create mode 100644 racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java create mode 100644 racevisionGame/src/main/java/visualiser/model/ServerConnection.java create mode 100644 racevisionGame/src/test/java/mock/model/SourceIdAllocatorTest.java create mode 100644 racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java diff --git a/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java b/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java index 85548a45..120da194 100644 --- a/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java +++ b/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java @@ -1,21 +1,22 @@ package mock.app; +import mock.enums.ConnectionStateEnum; +import mock.model.ClientConnection; +import mock.model.SourceIdAllocator; +import mock.model.commandFactory.CompositeCommand; import network.Messages.Enums.XMLMessageType; import network.Messages.LatestMessages; +import network.Messages.RaceSnapshot; import network.Messages.XMLMessage; -import org.mockito.Mock; -import visualiser.gameController.ControllerServer; -import java.io.DataOutputStream; import java.io.IOException; -import java.lang.reflect.Array; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.ArrayBlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Connection acceptor for multiple clients @@ -31,10 +32,31 @@ public class ConnectionAcceptor implements Runnable { * Socket used to listen for clients on. */ private ServerSocket serverSocket; - //mock outputs - private ArrayBlockingQueue mockOutputList = new ArrayBlockingQueue<>(16, true); - //latest messages + + + /** + * List of client connections. + */ + private ArrayBlockingQueue clientConnections = new ArrayBlockingQueue<>(16, true); + + /** + * Snapshot of the race. + */ private LatestMessages latestMessages; + + /** + * Collection of commands from clients for race to execute. + */ + private CompositeCommand compositeCommand; + + /** + * Used to allocate source IDs to clients. + */ + private SourceIdAllocator sourceIdAllocator; + + + + //Acknowledgement number for packets private int ackNumber = 0; //race xml sequence number @@ -47,14 +69,20 @@ public class ConnectionAcceptor implements Runnable { /** * Connection Acceptor Constructor * @param latestMessages Latest messages to be sent + * @param compositeCommand Collection of commands for race to execute. + * @param sourceIdAllocator Object used to allocate source IDs for clients. * @throws IOException if a server socket cannot be instantiated. */ - public ConnectionAcceptor(LatestMessages latestMessages) throws IOException { + public ConnectionAcceptor(LatestMessages latestMessages, CompositeCommand compositeCommand, SourceIdAllocator sourceIdAllocator) throws IOException { this.latestMessages = latestMessages; + this.compositeCommand = compositeCommand; + this.sourceIdAllocator = sourceIdAllocator; + this.serverSocket = new ServerSocket(serverPort); - CheckClientConnection checkClientConnection = new CheckClientConnection(mockOutputList); + CheckClientConnection checkClientConnection = new CheckClientConnection(clientConnections); new Thread(checkClientConnection, "ConnectionAcceptor()->CheckClientConnection thread").start(); + } public String getAddress() throws UnknownHostException { @@ -71,28 +99,26 @@ public class ConnectionAcceptor implements Runnable { @Override public void run() { - while(mockOutputList.remainingCapacity() > 0) { + while(clientConnections.remainingCapacity() > 0) { try { System.out.println("Waiting for a connection...");//TEMP DEBUG REMOVE Socket mockSocket = serverSocket.accept(); - //TODO at this point we need to assign the connection a boat source ID, if they requested to participate. - DataOutputStream outToVisualiser = new DataOutputStream(mockSocket.getOutputStream()); - MockOutput mockOutput = new MockOutput(latestMessages, outToVisualiser); - ControllerServer controllerServer = new ControllerServer(mockSocket); //TODO probably pass assigned boat source ID into ControllerServer. + ClientConnection clientConnection = new ClientConnection(mockSocket, sourceIdAllocator, latestMessages, compositeCommand); + + clientConnections.add(clientConnection); + + new Thread(clientConnection, "ConnectionAcceptor.run()->ClientConnection thread " + clientConnection).start(); - new Thread(mockOutput, "ConnectionAcceptor.run()->MockOutput thread" + mockOutput).start(); - new Thread(controllerServer, "ConnectionAcceptor.run()->ControllerServer thread" + controllerServer).start(); - mockOutputList.add(mockOutput); - System.out.println(String.format("%d number of Visualisers Connected.", mockOutputList.size()));//TEMP + Logger.getGlobal().log(Level.INFO, String.format("%d number of Visualisers Connected.", clientConnections.size())); } catch (IOException e) { - e.printStackTrace();//TODO handle this properly + Logger.getGlobal().log(Level.WARNING, "Got an IOException while a client was attempting to connect.", e); } @@ -104,14 +130,14 @@ public class ConnectionAcceptor implements Runnable { */ class CheckClientConnection implements Runnable{ - private ArrayBlockingQueue mocks; + private ArrayBlockingQueue connections; /** * Constructor - * @param mocks Mocks "connected" + * @param connections Clients "connected" */ - public CheckClientConnection(ArrayBlockingQueue mocks){ - this.mocks = mocks; + public CheckClientConnection(ArrayBlockingQueue connections){ + this.connections = connections; } /** @@ -119,21 +145,27 @@ public class ConnectionAcceptor implements Runnable { */ @Override public void run() { - double timeSinceLastHeartBeat = System.currentTimeMillis(); + while(true) { - //System.out.println(mocks.size());//used to see current amount of visualisers connected. - ArrayBlockingQueue m = new ArrayBlockingQueue<>(16, true, mocks); - for (MockOutput mo : m) { - try { - mo.sendHeartBeat(); - } catch (IOException e) { - mocks.remove(mo); + //System.out.println(connections.size());//used to see current amount of visualisers connected. + ArrayBlockingQueue clientConnections = new ArrayBlockingQueue<>(16, true, connections); + + for (ClientConnection client : clientConnections) { + if (!client.isAlive()) { + connections.remove(client); + + Logger.getGlobal().log(Level.WARNING, "CheckClientConnection is removing the dead connection: " + client); } } + try { Thread.sleep(100); + } catch (InterruptedException e) { - e.printStackTrace(); + Logger.getGlobal().log(Level.WARNING, "CheckClientConnection was interrupted while sleeping.", e); + Thread.currentThread().interrupt(); + return; + } } } diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index b98de4dc..7d7c940b 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -1,10 +1,14 @@ package mock.app; import mock.dataInput.PolarParser; +import mock.exceptions.EventConstructionException; import mock.model.MockRace; import mock.model.Polars; import mock.model.RaceLogic; +import mock.model.SourceIdAllocator; +import mock.model.commandFactory.CompositeCommand; import network.Messages.LatestMessages; +import network.Messages.RaceSnapshot; import shared.dataInput.*; import shared.enums.XMLFileType; import shared.exceptions.InvalidBoatDataException; @@ -19,14 +23,18 @@ import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.logging.Level; +import java.util.logging.Logger; /** * A Race Event, this holds all of the race's information as well as handling the connection to its clients. */ public class Event { - private static Event theEvent = new Event(); + /** + * Contents of the various xml files. + */ private String raceXML; private String regattaXML; private String boatXML; @@ -35,36 +43,75 @@ public class Event { private Polars boatPolars; + /** + * Data sources containing data from the xml files. + */ + RaceDataSource raceDataSource; + BoatDataSource boatDataSource; + RegattaDataSource regattaDataSource; + + private ConnectionAcceptor connectionAcceptor; private LatestMessages latestMessages; + private CompositeCommand compositeCommand; + + /** + * This is used to allocate source IDs. + */ + private SourceIdAllocator sourceIdAllocator; + + + + + /** * Constructs an event, using various XML files. + * @throws EventConstructionException Thrown if we cannot create an Event for any reason. */ - private Event() { + public Event() throws EventConstructionException { + + //Read XML files. try { this.raceXML = getRaceXMLAtCurrentTime(XMLReader.readXMLFileToString("mock/mockXML/raceTest.xml", StandardCharsets.UTF_8)); this.boatXML = XMLReader.readXMLFileToString("mock/mockXML/boatsSinglePlayer.xml", StandardCharsets.UTF_8); this.regattaXML = XMLReader.readXMLFileToString("mock/mockXML/regattaTest.xml", StandardCharsets.UTF_8); - this.xmlFileType = XMLFileType.Contents; - this.boatPolars = PolarParser.parse("mock/polars/acc_polars.csv"); + } catch (TransformerException | XMLReaderException e) { + throw new EventConstructionException("Could not read XML files.", e); + } + + this.xmlFileType = XMLFileType.Contents; + + this.boatPolars = PolarParser.parse("mock/polars/acc_polars.csv"); + + + //Parse the XML files into data sources. + try { + this.raceDataSource = new RaceXMLReader(this.raceXML, this.xmlFileType); + this.boatDataSource = new BoatXMLReader(this.boatXML, this.xmlFileType); + this.regattaDataSource = new RegattaXMLReader(this.regattaXML, this.xmlFileType); + + + } catch (XMLReaderException | InvalidRaceDataException | InvalidRegattaDataException | InvalidBoatDataException e) { + throw new EventConstructionException("Could not parse XML files.", e); - this.latestMessages = new LatestMessages(); - this.connectionAcceptor = new ConnectionAcceptor(latestMessages); } - catch (IOException e) { - e.printStackTrace(); - } catch (XMLReaderException e) { - e.printStackTrace(); - } catch (TransformerException e) { - e.printStackTrace(); + + //Create connection acceptor. + this.sourceIdAllocator = new SourceIdAllocator(raceDataSource.getParticipants()); + this.compositeCommand = new CompositeCommand(); + this.latestMessages = new LatestMessages(); + + try { + this.connectionAcceptor = new ConnectionAcceptor(latestMessages, compositeCommand, sourceIdAllocator); + + } catch (IOException e) { + throw new EventConstructionException("Could not create ConnectionAcceptor.", e); } } - public static Event getEvent() { - return theEvent; - } + public String getAddress() throws UnknownHostException { return connectionAcceptor.getAddress(); @@ -76,23 +123,16 @@ public class Event { /** * Sends the initial race data and then begins race simulation. - * @throws InvalidRaceDataException Thrown if the race xml file cannot be parsed. - * @throws XMLReaderException Thrown if any of the xml files cannot be parsed. - * @throws InvalidBoatDataException Thrown if the boat xml file cannot be parsed. - * @throws InvalidRegattaDataException Thrown if the regatta xml file cannot be parsed. */ - public void start() throws InvalidRaceDataException, XMLReaderException, InvalidBoatDataException, InvalidRegattaDataException { + public void start() { new Thread(connectionAcceptor, "Event.Start()->ConnectionAcceptor thread").start(); sendXMLs(); - //Parse the XML files into data sources. - RaceDataSource raceDataSource = new RaceXMLReader(this.raceXML, this.xmlFileType); - BoatDataSource boatDataSource = new BoatXMLReader(this.boatXML, this.xmlFileType); - RegattaDataSource regattaDataSource = new RegattaXMLReader(this.regattaXML, this.xmlFileType); + //Create and start race. - RaceLogic newRace = new RaceLogic(new MockRace(boatDataSource, raceDataSource, regattaDataSource, this.latestMessages, this.boatPolars, Constants.RaceTimeScale), this.latestMessages); + RaceLogic newRace = new RaceLogic(new MockRace(boatDataSource, raceDataSource, regattaDataSource, this.latestMessages, this.boatPolars, Constants.RaceTimeScale), this.latestMessages, this.compositeCommand); new Thread(newRace, "Event.Start()->RaceLogic thread").start(); } diff --git a/racevisionGame/src/main/java/mock/app/MockOutput.java b/racevisionGame/src/main/java/mock/app/MockOutput.java index 87bc9f95..9536507b 100644 --- a/racevisionGame/src/main/java/mock/app/MockOutput.java +++ b/racevisionGame/src/main/java/mock/app/MockOutput.java @@ -2,37 +2,25 @@ package mock.app; -import network.BinaryMessageEncoder; -import network.Exceptions.InvalidMessageException; -import network.MessageEncoders.RaceVisionByteEncoder; import network.Messages.*; -import network.Messages.Enums.MessageType; +import shared.model.RunnableWithFramePeriod; -import java.io.DataOutputStream; -import java.io.IOException; -import java.net.SocketException; +import java.util.List; +import java.util.concurrent.BlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; /** * TCP server to send race information to connected clients. */ -public class MockOutput implements Runnable -{ - /** - * Timestamp of the last sent heartbeat message. - */ - private long lastHeartbeatTime; +public class MockOutput implements RunnableWithFramePeriod { + - /** - * Period for the heartbeat - that is, how often we send it. - */ - private double heartbeatPeriod = 5.0; /** - * Output stream which wraps around mockSocket outstream. + * A queue to send messages to client. */ - private DataOutputStream outToVisualiser; + private BlockingQueue outgoingMessages; /** @@ -43,187 +31,21 @@ public class MockOutput implements Runnable - /** - * Ack numbers used in messages. - */ - private int ackNumber = 1; - /** - * Sequence number for heartbeat messages. - */ - private int heartbeatSequenceNum = 1; /** * Ctor. - * @param latestMessages Latests Messages that the Mock is to send out - * @param outToVisualiser DataStream to output to Visualisers - * @throws IOException if server socket cannot be opened. + * @param latestMessages Latest Messages that the Mock is to send out + * @param outgoingMessages A queue to place outgoing messages on. */ - public MockOutput(LatestMessages latestMessages, DataOutputStream outToVisualiser) throws IOException { - - this.outToVisualiser = outToVisualiser; - - this.lastHeartbeatTime = System.currentTimeMillis(); - + public MockOutput(LatestMessages latestMessages, BlockingQueue outgoingMessages) { + this.outgoingMessages = outgoingMessages; this.latestMessages = latestMessages; - - } - - - /** - * Increments the ackNumber value, and returns it. - * @return Incremented ackNumber. - */ - private int getNextAckNumber(){ - this.ackNumber++; - - return this.ackNumber; - } - - - /** - * Calculates the time since last heartbeat message, in seconds. - * @return Time since last heartbeat message, in seconds. - */ - private double timeSinceHeartbeat() { - long now = System.currentTimeMillis(); - return (now - lastHeartbeatTime) / 1000.0; - } - - - /** - * Generates the next heartbeat message and returns it. Increments the heartbeat sequence number. - * @return The next heartbeat message. - */ - private HeartBeat createHeartbeatMessage() { - - //Create the heartbeat message. - HeartBeat heartBeat = new HeartBeat(this.heartbeatSequenceNum); - heartbeatSequenceNum++; - - return heartBeat; - } - - /** - * Serializes a heartbeat message into a packet to be sent, and returns the byte array. - * @param heartBeat The heartbeat message to serialize. - * @return Byte array containing the next heartbeat message. - * @throws InvalidMessageException Thrown if the message cannot be encoded. - */ - private byte[] parseHeartbeat(HeartBeat heartBeat) throws InvalidMessageException { - - //Serializes the heartbeat message. - byte[] heartbeatMessage = RaceVisionByteEncoder.encode(heartBeat); - - //Places the serialized message in a packet. - BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder( - MessageType.HEARTBEAT, - System.currentTimeMillis(), - getNextAckNumber(), - (short) heartbeatMessage.length, - heartbeatMessage ); - - return binaryMessageEncoder.getFullMessage(); - - } - - /** - * Encodes/serialises a XMLMessage message, and returns it. - * @param xmlMessage The XMLMessage message to serialise. - * @return The XMLMessage message in a serialised form. - * @throws InvalidMessageException Thrown if the message cannot be encoded. - */ - private synchronized byte[] parseXMLMessage(XMLMessage xmlMessage) throws InvalidMessageException { - - //Serialize the xml message. - byte[] encodedXML = RaceVisionByteEncoder.encode(xmlMessage); - - //Place the message in a packet. - BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder( - MessageType.XMLMESSAGE, - System.currentTimeMillis(), - xmlMessage.getAckNumber(), //We use the ack number from the xml message. - (short) encodedXML.length, - encodedXML ); - - - return binaryMessageEncoder.getFullMessage(); - } - /** - * Encodes/serialises a BoatLocation message, and returns it. - * @param boatLocation The BoatLocation message to serialise. - * @return The BoatLocation message in a serialised form. - * @throws InvalidMessageException If the message cannot be encoded. - */ - private synchronized byte[] parseBoatLocation(BoatLocation boatLocation) throws InvalidMessageException { - - - //Encodes the message. - byte[] encodedBoatLoc = RaceVisionByteEncoder.encode(boatLocation); - - //Encodes the full message with header. - BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder( - MessageType.BOATLOCATION, - System.currentTimeMillis(), - getNextAckNumber(), - (short) encodedBoatLoc.length, - encodedBoatLoc ); - - - return binaryMessageEncoder.getFullMessage(); - - } - - /** - * Encodes/serialises a RaceStatus message, and returns it. - * @param raceStatus The RaceStatus message to serialise. - * @return The RaceStatus message in a serialised form. - * @throws InvalidMessageException Thrown if the message cannot be encoded. - */ - private synchronized byte[] parseRaceStatus(RaceStatus raceStatus) throws InvalidMessageException { - - //Encodes the messages. - byte[] encodedRaceStatus = RaceVisionByteEncoder.encode(raceStatus); - - //Encodes the full message with header. - BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder( - MessageType.RACESTATUS, - System.currentTimeMillis(), - getNextAckNumber(), - (short) encodedRaceStatus.length, - encodedRaceStatus ); - - - return binaryMessageEncoder.getFullMessage(); - - - } - - /** - * Sends a heartbeat - * @throws IOException if the socket is no longer open at both ends the heartbeat returns an error. - */ - public void sendHeartBeat() throws IOException { - //Sends a heartbeat every so often. - if (timeSinceHeartbeat() >= heartbeatPeriod) { - - HeartBeat heartBeat = createHeartbeatMessage(); - - try { - outToVisualiser.write(parseHeartbeat(heartBeat)); - } catch (InvalidMessageException e) { - Logger.getGlobal().log(Level.WARNING, "Could not encode HeartBeat: " + heartBeat, e); - } - - lastHeartbeatTime = System.currentTimeMillis(); - } - } - /** * Sending loop of the Server */ @@ -251,107 +73,40 @@ public class MockOutput implements Runnable long previousFrameTime = System.currentTimeMillis(); boolean sentXMLs = false; - try { - while (!Thread.interrupted()) { - try { - long currentFrameTime = System.currentTimeMillis(); + while (!Thread.interrupted()) { - //This is the time elapsed, in milliseconds, since the last server "frame". - long framePeriod = currentFrameTime - previousFrameTime; - - //We only attempt to send packets every X milliseconds. - long minimumFramePeriod = 16; - if (framePeriod >= minimumFramePeriod) { - - //Send XML messages. - if (!sentXMLs) { - //Serialise them. - - try { - byte[] raceXMLBlob = parseXMLMessage(latestMessages.getRaceXMLMessage()); - byte[] regattaXMLBlob = parseXMLMessage(latestMessages.getRegattaXMLMessage()); - byte[] boatsXMLBlob = parseXMLMessage(latestMessages.getBoatXMLMessage()); - - //Send them. - outToVisualiser.write(raceXMLBlob); - outToVisualiser.write(regattaXMLBlob); - outToVisualiser.write(boatsXMLBlob); - sentXMLs = true; - - } catch (InvalidMessageException e) { - Logger.getGlobal().log(Level.WARNING, "Could not encode XMLMessage: " + latestMessages.getRaceXMLMessage(), e); - continue; //Go to next iteration. - } - - } - - //Sends the RaceStatus message. - if (this.latestMessages.getRaceStatus() != null) { - - try { - byte[] raceStatusBlob = this.parseRaceStatus(this.latestMessages.getRaceStatus()); - - this.outToVisualiser.write(raceStatusBlob); - - } catch (InvalidMessageException e) { - Logger.getGlobal().log(Level.WARNING, "Could not encode RaceStatus: " + latestMessages.getRaceStatus(), e); - } - } - - //Send all of the BoatLocation messages. - for (int sourceID : this.latestMessages.getBoatLocationMap().keySet()) { - - //Get the message. - BoatLocation boatLocation = this.latestMessages.getBoatLocation(sourceID); - if (boatLocation != null) { - - - try { - //Encode. - byte[] boatLocationBlob = this.parseBoatLocation(boatLocation); - - //Write it. - this.outToVisualiser.write(boatLocationBlob); - - } catch (InvalidMessageException e) { - Logger.getGlobal().log(Level.WARNING, "Could not encode BoatLocation: " + boatLocation, e); - } - - - - } - } + try { - previousFrameTime = currentFrameTime; + long currentFrameTime = System.currentTimeMillis(); + waitForFramePeriod(previousFrameTime, currentFrameTime, 16); + previousFrameTime = currentFrameTime; - } else { - //Wait until the frame period will be large enough. - long timeToWait = minimumFramePeriod - framePeriod; + //Send XML messages. + if (!sentXMLs) { - try { - Thread.sleep(timeToWait); - } catch (InterruptedException e) { - //If we get interrupted, exit the function. - Logger.getGlobal().log(Level.WARNING, "MockOutput.run().sleep(framePeriod) was interrupted on thread: " + Thread.currentThread(), e); - //Re-set the interrupt flag. - Thread.currentThread().interrupt(); - return; - } + outgoingMessages.put(latestMessages.getRaceXMLMessage()); + outgoingMessages.put(latestMessages.getRegattaXMLMessage()); + outgoingMessages.put(latestMessages.getBoatXMLMessage()); - } + sentXMLs = true; + } - } catch (SocketException e) { - break; + List snapshot = latestMessages.getSnapshot(); + for (AC35Data message : snapshot) { + outgoingMessages.put(message); } + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.WARNING, "MockOutput.run() interrupted while putting message in queue.", e); + Thread.currentThread().interrupt(); + return; } - } catch (IOException e) { - e.printStackTrace(); } + } diff --git a/racevisionGame/src/main/java/mock/enums/ConnectionStateEnum.java b/racevisionGame/src/main/java/mock/enums/ConnectionStateEnum.java new file mode 100644 index 00000000..4d4961bb --- /dev/null +++ b/racevisionGame/src/main/java/mock/enums/ConnectionStateEnum.java @@ -0,0 +1,84 @@ +package mock.enums; + +import java.util.HashMap; +import java.util.Map; + +/** + * The states in which a connection to a client may have. + */ +public enum ConnectionStateEnum { + + UNKNOWN(0), + + /** + * We're waiting for the client to complete the joining handshake (see {@link network.Messages.RequestToJoin}. + */ + WAITING_FOR_HANDSHAKE(1), + + /** + * The client has completed the handshake, and is connected. + */ + CONNECTED(2), + + /** + * The client has timed out. + */ + TIMED_OUT(3); + + + + + private byte value; + + /** + * Ctor. Creates a ConnectionStateEnum from a given primitive integer value, cast to a byte. + * @param value Integer, which is cast to byte, to construct from. + */ + private ConnectionStateEnum(int value) { + this.value = (byte) value; + } + + /** + * Returns the primitive value of the enum. + * @return Primitive value of the enum. + */ + public byte getValue() { + return value; + } + + + /** + * Stores a mapping between Byte values and ConnectionStateEnum values. + */ + private static final Map byteToStatusMap = new HashMap<>(); + + + /* + Static initialization block. Initializes the byteToStatusMap. + */ + static { + for (ConnectionStateEnum type : ConnectionStateEnum.values()) { + ConnectionStateEnum.byteToStatusMap.put(type.value, type); + } + } + + + /** + * Returns the enumeration value which corresponds to a given byte value. + * @param connectionState Byte value to convert to a ConnectionStateEnum value. + * @return The ConnectionStateEnum value which corresponds to the given byte value. + */ + public static ConnectionStateEnum fromByte(byte connectionState) { + //Gets the corresponding MessageType from the map. + ConnectionStateEnum type = ConnectionStateEnum.byteToStatusMap.get(connectionState); + + if (type == null) { + //If the byte value wasn't found, return the UNKNOWN connectionState. + return ConnectionStateEnum.UNKNOWN; + } else { + //Otherwise, return the connectionState. + return type; + } + + } +} diff --git a/racevisionGame/src/main/java/mock/exceptions/EventConstructionException.java b/racevisionGame/src/main/java/mock/exceptions/EventConstructionException.java new file mode 100644 index 00000000..0f1d9b9f --- /dev/null +++ b/racevisionGame/src/main/java/mock/exceptions/EventConstructionException.java @@ -0,0 +1,24 @@ +package mock.exceptions; + +/** + * An exception thrown when we cannot create an {@link mock.app.Event}. + */ +public class EventConstructionException extends Exception { + + /** + * Constructs the exception with a given message. + * @param message Message to store. + */ + public EventConstructionException(String message) { + super(message); + } + + /** + * Constructs the exception with a given message and cause. + * @param message Message to store. + * @param cause Cause to store. + */ + public EventConstructionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/racevisionGame/src/main/java/mock/exceptions/SourceIDAllocationException.java b/racevisionGame/src/main/java/mock/exceptions/SourceIDAllocationException.java new file mode 100644 index 00000000..6623d9cb --- /dev/null +++ b/racevisionGame/src/main/java/mock/exceptions/SourceIDAllocationException.java @@ -0,0 +1,24 @@ +package mock.exceptions; + +/** + * An exception thrown when we cannot allocate a source ID. + */ +public class SourceIDAllocationException extends Exception { + + /** + * Constructs the exception with a given message. + * @param message Message to store. + */ + public SourceIDAllocationException(String message) { + super(message); + } + + /** + * Constructs the exception with a given message and cause. + * @param message Message to store. + * @param cause Cause to store. + */ + public SourceIDAllocationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/racevisionGame/src/main/java/mock/model/ClientConnection.java b/racevisionGame/src/main/java/mock/model/ClientConnection.java new file mode 100644 index 00000000..71d85348 --- /dev/null +++ b/racevisionGame/src/main/java/mock/model/ClientConnection.java @@ -0,0 +1,243 @@ +package mock.model; + + +import mock.app.MockOutput; +import mock.enums.ConnectionStateEnum; +import shared.exceptions.HandshakeException; +import mock.exceptions.SourceIDAllocationException; +import mock.model.commandFactory.CompositeCommand; +import network.Messages.*; +import network.Messages.Enums.JoinAcceptanceEnum; +import network.Messages.Enums.MessageType; +import network.Messages.Enums.RequestToJoinEnum; +import network.StreamRelated.MessageDeserialiser; +import network.StreamRelated.MessageSerialiser; +import visualiser.gameController.ControllerServer; + +import java.io.IOException; +import java.net.Socket; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class handles the client connection handshake, and creation of MockOutput and ControllerServer. + */ +public class ClientConnection implements Runnable { + + /** + * The socket for the client's connection. + */ + private Socket socket; + + /** + * Periodically sends HeartBeat messages to client. + */ + private HeartBeatService heartBeatService; + + + /** + * Used to allocate source ID to client, if they request to participate. + */ + private SourceIdAllocator sourceIdAllocator; + + /** + * Latest snapshot of the race, to send to client. Currently only used for XML messages. + */ + private LatestMessages latestMessages; + + + /** + * Collection of commands from client for race to execute. + */ + private CompositeCommand compositeCommand; + + /** + * Used to send the race snapshot to client. + */ + private MockOutput mockOutput; + + /** + * Used to receive client input, and turn it into commands. + */ + private ControllerServer controllerServer; + + + /** + * Used to write messages to socket. + */ + private MessageSerialiser messageSerialiser; + + /** + * Stores messages to write to socket. + */ + private BlockingQueue outputQueue; + + /** + * Used to read messages from socket. + */ + private MessageDeserialiser messageDeserialiser; + + /** + * Stores messages read from socket. + */ + private BlockingQueue inputQueue; + + /** + * The state of the connection to the client. + */ + private ConnectionStateEnum connectionState = ConnectionStateEnum.UNKNOWN; + + + + + + + /** + * Creates a client connection, using a given socket. + * @param socket The socket which connects to the client. + * @param sourceIdAllocator Used to allocate a source ID for the client. + * @param latestMessages Latest race snapshot to send to client. + * @param compositeCommand Collection of commands for race to execute. + * @throws IOException Thrown if there is a problem with the client socket. + */ + public ClientConnection(Socket socket, SourceIdAllocator sourceIdAllocator, LatestMessages latestMessages, CompositeCommand compositeCommand) throws IOException { + this.socket = socket; + this.sourceIdAllocator = sourceIdAllocator; + this.latestMessages = latestMessages; + this.compositeCommand = compositeCommand; + + this.outputQueue = new LinkedBlockingQueue<>(); + this.inputQueue = new LinkedBlockingQueue<>(); + + + this.messageSerialiser = new MessageSerialiser(socket.getOutputStream(), outputQueue); + this.messageDeserialiser = new MessageDeserialiser(socket.getInputStream(), inputQueue); + + new Thread(messageSerialiser, "ClientConnection()->MessageSerialiser thread " + messageSerialiser).start(); + new Thread(messageDeserialiser, "ClientConnection()->MessageDeserialiser thread " + messageDeserialiser).start(); + + + this.heartBeatService = new HeartBeatService(outputQueue); + new Thread(heartBeatService, "ClientConnection()->HeartBeatService thread " + heartBeatService).start(); + + } + + + + @Override + public void run() { + try { + handshake(); + + } catch (HandshakeException | SourceIDAllocationException e) { + Logger.getGlobal().log(Level.WARNING, "Client handshake failed.", e); + Thread.currentThread().interrupt(); + return; + } + + } + + + /** + * Initiates the handshake with the client. + * @throws HandshakeException Thrown if something goes wrong with the handshake. + * @throws SourceIDAllocationException Thrown if we cannot allocate a sourceID. + */ + private void handshake() throws SourceIDAllocationException, HandshakeException { + + //This function is a bit messy, and could probably be refactored a bit. + + connectionState = ConnectionStateEnum.WAITING_FOR_HANDSHAKE; + + + + RequestToJoin requestToJoin = waitForRequestToJoin(); + + int allocatedSourceID = 0; + + //If they want to participate, give them a source ID number. + if (requestToJoin.getRequestType() == RequestToJoinEnum.PARTICIPANT) { + + allocatedSourceID = sourceIdAllocator.allocateSourceID(); + + this.controllerServer = new ControllerServer(compositeCommand, inputQueue, allocatedSourceID); + new Thread(controllerServer, "ClientConnection.run()->ControllerServer thread" + controllerServer).start(); + + } + + this.mockOutput = new MockOutput(latestMessages, outputQueue); + new Thread(mockOutput, "ClientConnection.run()->MockOutput thread" + mockOutput).start(); + + sendJoinAcceptanceMessage(allocatedSourceID); + + connectionState = ConnectionStateEnum.CONNECTED; + + } + + + /** + * Waits until the client sends a {@link RequestToJoin} message, and returns it. + * @return The {@link RequestToJoin} message. + * @throws HandshakeException Thrown if we get interrupted while waiting. + */ + private RequestToJoin waitForRequestToJoin() throws HandshakeException { + + try { + + + while (connectionState == ConnectionStateEnum.WAITING_FOR_HANDSHAKE) { + + AC35Data message = inputQueue.take(); + + //We need to wait until they actually send a join request. + if (message.getType() == MessageType.REQUEST_TO_JOIN) { + return (RequestToJoin) message; + } + + } + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new HandshakeException("Handshake failed. Thread: " + Thread.currentThread() + " was interrupted while waiting on the incoming message queue.", e); + + } + + + throw new HandshakeException("Handshake was cancelled. Connection state is now: " + connectionState); + + } + + + /** + * Sends the client a {@link JoinAcceptance} message, containing their assigned sourceID. + * @param sourceID The sourceID to assign to client. + * @throws HandshakeException Thrown if the thread is interrupted while placing message on the outgoing message queue. + */ + private void sendJoinAcceptanceMessage(int sourceID) throws HandshakeException { + + //Send them the source ID. + JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL, sourceID); + + try { + outputQueue.put(joinAcceptance); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new HandshakeException("Handshake failed. Thread: " + Thread.currentThread() + " interrupted while placing JoinAcceptance message on outgoing message queue.", e); + } + + } + + + /** + * Determines whether or not this connection is still alive. + * This is based off whether the {@link MessageSerialiser} is still alive. + * @return True if it is alive, false otherwise. + */ + public boolean isAlive() { + return messageSerialiser.isRunning(); + } + + +} diff --git a/racevisionGame/src/main/java/mock/model/HeartBeatService.java b/racevisionGame/src/main/java/mock/model/HeartBeatService.java new file mode 100644 index 00000000..232eb9ad --- /dev/null +++ b/racevisionGame/src/main/java/mock/model/HeartBeatService.java @@ -0,0 +1,110 @@ +package mock.model; + +import network.Messages.AC35Data; +import network.Messages.HeartBeat; +import shared.model.RunnableWithFramePeriod; + +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * This class is responsible for sending {@link HeartBeat} messages to queue. + */ +public class HeartBeatService implements RunnableWithFramePeriod { + + /** + * Timestamp of the last sent heartbeat message. + */ + private long lastHeartbeatTime; + + /** + * Period for the heartbeat - that is, how often we send it. Milliseconds. + */ + private long heartbeatPeriod = 5000; + + + /** + * The messages we're writing to the stream. + */ + private BlockingQueue messagesToSend; + + + + /** + * Sequence number for heartbeat messages. + */ + private int heartbeatSequenceNum = 1; + + + /** + * Constructs a new HeartBeatService to send heartBeat messages to a given outputStream. + * @param messagesToSend The queue to send heartBeat messages to. + */ + public HeartBeatService(BlockingQueue messagesToSend) { + this.messagesToSend = messagesToSend; + this.lastHeartbeatTime = System.currentTimeMillis(); + } + + + + + /** + * Increments the {@link #heartbeatSequenceNum} value, and returns it. + * @return Incremented heat beat number. + */ + private int getNextHeartBeatNumber(){ + this.heartbeatSequenceNum++; + + return this.heartbeatSequenceNum; + } + + + + /** + * Generates the next heartbeat message and returns it. Increments the heartbeat sequence number. + * @return The next heartbeat message. + */ + private HeartBeat createHeartbeatMessage() { + + HeartBeat heartBeat = new HeartBeat(getNextHeartBeatNumber()); + + return heartBeat; + } + + + /** + * Puts a HeartBeat message on the message queue. + * @throws InterruptedException Thrown if the thread is interrupted. + */ + private void sendHeartBeat() throws InterruptedException { + + HeartBeat heartBeat = createHeartbeatMessage(); + + messagesToSend.put(heartBeat); + } + + + + @Override + public void run() { + + while (!Thread.interrupted()) { + long currentFrameTime = System.currentTimeMillis(); + waitForFramePeriod(lastHeartbeatTime, currentFrameTime, heartbeatPeriod); + lastHeartbeatTime = currentFrameTime; + + try { + sendHeartBeat(); + + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.WARNING, "HeartBeatService: " + this + " sendHeartBeat() was interrupted on thread: " + Thread.currentThread(), e); + Thread.currentThread().interrupt(); + return; + + } + } + + } +} diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index 9755099d..e389f474 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -7,6 +7,7 @@ import shared.dataInput.BoatDataSource; import shared.dataInput.RaceDataSource; import network.Messages.Enums.RaceStatusEnum; import shared.dataInput.RegattaDataSource; +import shared.exceptions.BoatNotFoundException; import shared.model.*; import shared.model.Bearing; @@ -411,6 +412,25 @@ public class MockRace extends Race { return boats; } + /** + * Returns a boat by sourceID. + * @param sourceID The source ID the boat. + * @return The boat. + * @throws BoatNotFoundException Thrown if there is not boat with the specified sourceID. + */ + public MockBoat getBoat(int sourceID) throws BoatNotFoundException { + + for (MockBoat boat : boats) { + + if (boat.getSourceID() == sourceID) { + return boat; + } + + } + + throw new BoatNotFoundException("Boat with sourceID: " + sourceID + " was not found."); + } + /** * Changes the wind direction randomly, while keeping it within [windLowerBound, windUpperBound]. */ diff --git a/racevisionGame/src/main/java/mock/model/RaceLogic.java b/racevisionGame/src/main/java/mock/model/RaceLogic.java index 9e810761..42bea8ef 100644 --- a/racevisionGame/src/main/java/mock/model/RaceLogic.java +++ b/racevisionGame/src/main/java/mock/model/RaceLogic.java @@ -1,12 +1,15 @@ package mock.model; import javafx.animation.AnimationTimer; +import mock.model.commandFactory.CompositeCommand; import network.Messages.Enums.BoatStatusEnum; import network.Messages.Enums.RaceStatusEnum; import network.Messages.LatestMessages; -import shared.model.Race; + + public class RaceLogic implements Runnable { + /** * State of current race modified by this object */ @@ -16,14 +19,18 @@ public class RaceLogic implements Runnable { */ private RaceServer server; + private CompositeCommand commands; + /** * Initialises race loop with state and server message queue * @param race state of race to modify * @param messages to send to server + * @param compositeCommand Commands from clients to execute. */ - public RaceLogic(MockRace race, LatestMessages messages) { + public RaceLogic(MockRace race, LatestMessages messages, CompositeCommand compositeCommand) { this.race = race; this.server = new RaceServer(race, messages); + this.commands = compositeCommand; } /** @@ -56,17 +63,13 @@ public class RaceLogic implements Runnable { //Provide boat's with an estimated time at next mark until the race starts. race.setBoatsTimeNextMark(race.getRaceClock().getCurrentTime()); - //Parse the boat locations. - server.parseBoatLocations(); - - //Parse the marks. - server.parseMarks(); + //Parse the race snapshot. + server.parseSnapshot(); // Change wind direction race.changeWindDirection(); - //Parse the race status. - server.parseRaceStatus(); + if (race.getRaceStatusEnum() == RaceStatusEnum.STARTED) { @@ -109,6 +112,9 @@ public class RaceLogic implements Runnable { //Get the current time. currentTime = System.currentTimeMillis(); + //Execute commands from clients. + commands.execute(race); + //Update race time. race.updateRaceTime(currentTime); @@ -123,7 +129,6 @@ public class RaceLogic implements Runnable { //If it is still racing, update its position. if (boat.getStatus() == BoatStatusEnum.RACING) { - race.updatePosition(boat, framePeriod, race.getRaceClock().getDurationMilli()); } @@ -141,15 +146,8 @@ public class RaceLogic implements Runnable { // Change wind direction race.changeWindDirection(); - //Parse the boat locations. - server.parseBoatLocations(); - - //Parse the marks. - server.parseMarks(); - - //Parse the race status. - server.parseRaceStatus(); - + //Parse the race snapshot. + server.parseSnapshot(); //Update the last frame time. this.lastFrameTime = currentTime; @@ -165,7 +163,7 @@ public class RaceLogic implements Runnable { @Override public void handle(long now) { - server.parseRaceStatus(); + server.parseSnapshot(); if (iters > 500) { stop(); @@ -173,4 +171,5 @@ public class RaceLogic implements Runnable { iters++; } }; + } diff --git a/racevisionGame/src/main/java/mock/model/RaceServer.java b/racevisionGame/src/main/java/mock/model/RaceServer.java index d776b693..969a4c71 100644 --- a/racevisionGame/src/main/java/mock/model/RaceServer.java +++ b/racevisionGame/src/main/java/mock/model/RaceServer.java @@ -1,10 +1,7 @@ package mock.model; -import network.Messages.BoatLocation; -import network.Messages.BoatStatus; +import network.Messages.*; import network.Messages.Enums.BoatLocationDeviceEnum; -import network.Messages.LatestMessages; -import network.Messages.RaceStatus; import network.Utils.AC35UnitConverter; import shared.model.Bearing; import shared.model.CompoundMark; @@ -21,10 +18,6 @@ public class RaceServer { private MockRace race; private LatestMessages latestMessages; - /** - * The sequence number of the latest RaceStatus message sent or received. - */ - private int raceStatusSequenceNumber = 1; /** * The sequence number of the latest BoatLocation message sent or received. @@ -39,10 +32,31 @@ public class RaceServer { /** - * Parses an individual marker boat, and sends it to mockOutput. + * Parses the race to create a snapshot, and places it in latestMessages. + */ + public void parseSnapshot() { + + List snapshotMessages = new ArrayList<>(); + + //Parse the boat locations. + snapshotMessages.addAll(parseBoatLocations()); + + //Parse the marks. + snapshotMessages.addAll(parseMarks()); + + //Parse the race status. + snapshotMessages.add(parseRaceStatus()); + + latestMessages.setSnapshot(snapshotMessages); + } + + + /** + * Parses an individual marker boat, and returns it. * @param mark The marker boat to parse. + * @return The BoatLocation message. */ - private void parseIndividualMark(Mark mark) { + private BoatLocation parseIndividualMark(Mark mark) { //Create message. BoatLocation boatLocation = new BoatLocation( mark.getSourceID(), @@ -57,13 +71,17 @@ public class RaceServer { //Iterates the sequence number. this.boatLocationSequenceNumber++; - this.latestMessages.setBoatLocation(boatLocation); + return boatLocation; } /** - * Parse the compound marker boats through mock output. + * Parse the compound marker boats, and returns a list of BoatLocation messages. + * @return BoatLocation messages for each mark. */ - public void parseMarks() { + private List parseMarks() { + + List markLocations = new ArrayList<>(race.getCompoundMarks().size()); + for (CompoundMark compoundMark : race.getCompoundMarks()) { //Get the individual marks from the compound mark. @@ -72,31 +90,40 @@ public class RaceServer { //If they aren't null, parse them (some compound marks only have one mark). if (mark1 != null) { - this.parseIndividualMark(mark1); + markLocations.add(this.parseIndividualMark(mark1)); } if (mark2 != null) { - this.parseIndividualMark(mark2); + markLocations.add(this.parseIndividualMark(mark2)); } } + + return markLocations; } /** - * Parse the boats in the race, and send it to mockOutput. + * Parse the boats in the race, and returns all of their BoatLocation messages. + * @return List of BoatLocation messages, for each boat. */ - public void parseBoatLocations() { + private List parseBoatLocations() { + + List boatLocations = new ArrayList<>(race.getBoats().size()); + //Parse each boat. for (MockBoat boat : race.getBoats()) { - this.parseIndividualBoatLocation(boat); + boatLocations.add(this.parseIndividualBoatLocation(boat)); } + + return boatLocations; } /** - * Parses an individual boat, and sends it to mockOutput. + * Parses an individual boat, and returns it. * @param boat The boat to parse. + * @return The BoatLocation message. */ - private void parseIndividualBoatLocation(MockBoat boat) { + private BoatLocation parseIndividualBoatLocation(MockBoat boat) { BoatLocation boatLocation = new BoatLocation( boat.getSourceID(), @@ -111,16 +138,17 @@ public class RaceServer { //Iterates the sequence number. this.boatLocationSequenceNumber++; - this.latestMessages.setBoatLocation(boatLocation); + return boatLocation; } /** - * Parses the race status, and sends it to mockOutput. + * Parses the race status, and returns it. + * @return The race status message. */ - public void parseRaceStatus() { + private RaceStatus parseRaceStatus() { //A race status message contains a list of boat statuses. List boatStatuses = new ArrayList<>(); @@ -151,6 +179,6 @@ public class RaceServer { race.getRaceType(), boatStatuses); - this.latestMessages.setRaceStatus(raceStatus); + return raceStatus; } } diff --git a/racevisionGame/src/main/java/mock/model/SourceIdAllocator.java b/racevisionGame/src/main/java/mock/model/SourceIdAllocator.java new file mode 100644 index 00000000..3b62a8a7 --- /dev/null +++ b/racevisionGame/src/main/java/mock/model/SourceIdAllocator.java @@ -0,0 +1,70 @@ +package mock.model; + + +import mock.exceptions.SourceIDAllocationException; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class is responsible for allocating boat source IDs for use in a race, upon request. + */ +public class SourceIdAllocator { + + + /** + * This list contains all unallocated source IDs. + */ + List unallocatedIDs = new ArrayList<>(); + + + /** + * This list contains all allocated source IDs. + */ + List allocatedIDs = new ArrayList<>(); + + + /** + * Creates a source ID allocator, using the given list of unallocated source IDs. + * @param unallocatedIDs List of unallocated source IDs. + */ + public SourceIdAllocator(List unallocatedIDs) { + //We need to copy the list. + this.unallocatedIDs.addAll(unallocatedIDs); + } + + + /** + * Allocates a source ID for a boat. + * @return The allocated source ID. + * @throws SourceIDAllocationException Thrown if we cannot allocate any more source IDs. + */ + public synchronized int allocateSourceID() throws SourceIDAllocationException { + + if (!unallocatedIDs.isEmpty()) { + + int sourceID = unallocatedIDs.remove(0); + + allocatedIDs.add(sourceID); + + return sourceID; + + } else { + throw new SourceIDAllocationException("Could not allocate a source ID."); + + } + } + + + /** + * Returns a source ID to the source ID allocator, so that it can be reused. + * @param sourceID Source ID to return. + */ + public void returnSourceID(Integer sourceID) { + + //We remove an Integer, not an int, so that we remove by value not by index. + allocatedIDs.remove(sourceID); + + unallocatedIDs.add(sourceID); + } +} diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/CompositeCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/CompositeCommand.java new file mode 100644 index 00000000..ff09103d --- /dev/null +++ b/racevisionGame/src/main/java/mock/model/commandFactory/CompositeCommand.java @@ -0,0 +1,25 @@ +package mock.model.commandFactory; + +import mock.model.MockRace; + +import java.util.Stack; + +/** + * Wraps multiple commands into a composite to execute queued commands during a frame. + */ +public class CompositeCommand implements Command { + private Stack commands; + + public CompositeCommand() { + this.commands = new Stack<>(); + } + + public void addCommand(Command command) { + commands.push(command); + } + + @Override + public void execute(MockRace race) { + while(!commands.isEmpty()) commands.pop().execute(race); + } +} diff --git a/racevisionGame/src/main/java/network/MessageControllers/MessageController.java b/racevisionGame/src/main/java/network/MessageControllers/MessageController.java new file mode 100644 index 00000000..7b6cca14 --- /dev/null +++ b/racevisionGame/src/main/java/network/MessageControllers/MessageController.java @@ -0,0 +1,9 @@ +package network.MessageControllers; + + + +public class MessageController { + + + +} diff --git a/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java index 54c10272..303b30db 100644 --- a/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java +++ b/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java @@ -1,6 +1,7 @@ package network.MessageEncoders; +import network.BinaryMessageEncoder; import network.Exceptions.InvalidMessageException; import network.Exceptions.InvalidMessageTypeException; import network.Messages.*; @@ -104,7 +105,7 @@ public class RaceVisionByteEncoder { /** - * Encodes a given message. + * Encodes a given message, to be placed inside a binary message (see {@link BinaryMessageEncoder}). * @param message Message to encode. * @return Encoded message. * @throws InvalidMessageException If the message cannot be encoded. @@ -126,4 +127,29 @@ public class RaceVisionByteEncoder { } + /** + * Encodes a given messages, using a given ackNumber, and returns a binary message ready to be sent over-the-wire. + * @param message The message to send. + * @param ackNumber The ackNumber of the message. + * @return A binary message ready to be transmitted. + * @throws InvalidMessageException Thrown if the message cannot be encoded. + */ + public static byte[] encodeBinaryMessage(AC35Data message, int ackNumber) throws InvalidMessageException { + + //Encodes the message. + byte[] encodedMessage = RaceVisionByteEncoder.encode(message); + + //Encodes the full message with header. + BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder( + message.getType(), + System.currentTimeMillis(), + ackNumber, + (short) encodedMessage.length, + encodedMessage ); + + + return binaryMessageEncoder.getFullMessage(); + } + + } diff --git a/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java b/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java new file mode 100644 index 00000000..4eaa6dce --- /dev/null +++ b/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java @@ -0,0 +1,11 @@ +package network.MessageRouters; + + +/** + * This class routes {@link network.Messages.AC35Data} messages to an appropriate message controller. + */ +public class MessageRouter { + + + +} diff --git a/racevisionGame/src/main/java/network/Messages/BoatAction.java b/racevisionGame/src/main/java/network/Messages/BoatAction.java index fcc96aa8..93c6a310 100644 --- a/racevisionGame/src/main/java/network/Messages/BoatAction.java +++ b/racevisionGame/src/main/java/network/Messages/BoatAction.java @@ -13,6 +13,12 @@ public class BoatAction extends AC35Data { */ private BoatActionEnum boatAction; + + /** + * The source ID of the boat this action relates to. + */ + private int sourceID = 0; + /** * Constructs a BoatActon message with a given action. * @param boatAction Action to use. @@ -30,4 +36,19 @@ public class BoatAction extends AC35Data { return boatAction; } + /** + * Returns the boat source ID for this message. + * @return The source ID for this message. + */ + public int getSourceID() { + return sourceID; + } + + /** + * Sets the boat source ID for this message. + * @param sourceID The source for this message. + */ + public void setSourceID(int sourceID) { + this.sourceID = sourceID; + } } diff --git a/racevisionGame/src/main/java/network/Messages/LatestMessages.java b/racevisionGame/src/main/java/network/Messages/LatestMessages.java index f35fc52e..147f58e7 100644 --- a/racevisionGame/src/main/java/network/Messages/LatestMessages.java +++ b/racevisionGame/src/main/java/network/Messages/LatestMessages.java @@ -3,9 +3,7 @@ package network.Messages; import network.Messages.Enums.XMLMessageType; import shared.dataInput.RaceDataSource; -import java.util.HashMap; -import java.util.Map; -import java.util.Observable; +import java.util.*; /** * This class contains a set of the latest messages received (e.g., the latest RaceStatus, the latest BoatLocation for each boat, etc...). @@ -44,6 +42,12 @@ public class LatestMessages extends Observable { private CourseWinds courseWinds; + /** + * A list of messages containing a snapshot of the race. + */ + private List snapshot = new ArrayList<>(); + + /** * The latest race data XML message. */ @@ -69,6 +73,22 @@ public class LatestMessages extends Observable { } + /** + * Returns a copy of the race snapshot. + * @return Copy of the race snapshot. + */ + public List getSnapshot() { + return new ArrayList<>(snapshot); + } + + + /** + * Sets the snapshot of the race. + * @param snapshot New snapshot of race. + */ + public void setSnapshot(List snapshot) { + this.snapshot = snapshot; + } /** diff --git a/racevisionGame/src/main/java/network/Messages/RaceSnapshot.java b/racevisionGame/src/main/java/network/Messages/RaceSnapshot.java new file mode 100644 index 00000000..212c8dab --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/RaceSnapshot.java @@ -0,0 +1,41 @@ +package network.Messages; + + +import java.util.ArrayList; +import java.util.List; + + +/** + * Represents a snapshot of the race's state. + * Contains a list of {@link AC35Data} messages. + * Send a copy of each message to a connected client. + */ +public class RaceSnapshot { + + /** + * The contents of the snapshot. + */ + private List snapshot; + + + /** + * Constructs a snapshot using a given list of messages. + * @param snapshot Messages to use as snapshot. + */ + public RaceSnapshot(List snapshot) { + this.snapshot = snapshot; + } + + + /** + * Gets the contents of the snapshot. + * This is a shallow copy. + * @return Contents of the snapshot. + */ + public List getSnapshot() { + + List copy = new ArrayList<>(snapshot); + + return copy; + } +} diff --git a/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java b/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java new file mode 100644 index 00000000..39cb0024 --- /dev/null +++ b/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java @@ -0,0 +1,156 @@ +package network.StreamRelated; + + +import network.BinaryMessageDecoder; +import network.Exceptions.InvalidMessageException; +import network.MessageEncoders.RaceVisionByteEncoder; +import network.Messages.AC35Data; +import shared.model.RunnableWithFramePeriod; + +import java.io.*; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static network.Utils.ByteConverter.bytesToShort; + +/** + * This class is responsible for converting data from an input stream into a queue of {@link AC35Data} messages. + */ +public class MessageDeserialiser implements RunnableWithFramePeriod { + + + /** + * The stream we're reading from. + */ + private DataInputStream inputStream; + + /** + * The messages we've read. + */ + private BlockingQueue messagesRead; + + + /** + * Ack numbers used in messages. + */ + private int ackNumber = 1; + + + /** + * Constructs a new MessageSerialiser to write a queue of messages to a given stream. + * @param inputStream The stream to write to. + * @param messagesRead The messages to send. + */ + public MessageDeserialiser(InputStream inputStream, BlockingQueue messagesRead) { + this.inputStream = new DataInputStream(inputStream); + this.messagesRead = messagesRead; + } + + + /** + * Increments the ackNumber value, and returns it. + * @return Incremented ackNumber. + */ + private int getNextAckNumber(){ + this.ackNumber++; + + return this.ackNumber; + } + + + + /** + * Reads and returns the next message as an array of bytes from the input stream. Use getNextMessage() to get the actual message object instead. + * @return Encoded binary message bytes. + * @throws IOException Thrown when an error occurs while reading from the input stream. + */ + private byte[] getNextMessageBytes() throws IOException { + inputStream.mark(0); + short CRCLength = 4; + short headerLength = 15; + + //Read the header of the next message. + byte[] headerBytes = new byte[headerLength]; + inputStream.readFully(headerBytes); + + //Read the message body length. + byte[] messageBodyLengthBytes = Arrays.copyOfRange(headerBytes, headerLength - 2, headerLength); + short messageBodyLength = bytesToShort(messageBodyLengthBytes); + + //Read the message body. + byte[] messageBodyBytes = new byte[messageBodyLength]; + inputStream.readFully(messageBodyBytes); + + //Read the message CRC. + byte[] messageCRCBytes = new byte[CRCLength]; + inputStream.readFully(messageCRCBytes); + + //Put the head + body + crc into one large array. + ByteBuffer messageBytes = ByteBuffer.allocate(headerBytes.length + messageBodyBytes.length + messageCRCBytes.length); + messageBytes.put(headerBytes); + messageBytes.put(messageBodyBytes); + messageBytes.put(messageCRCBytes); + + return messageBytes.array(); + } + + + /** + * Reads and returns the next message object from the input stream. + * @return The message object. + * @throws IOException Thrown when an error occurs while reading from the input stream. + * @throws InvalidMessageException Thrown when the message is invalid in some way. + */ + private AC35Data getNextMessage() throws IOException, InvalidMessageException + { + //Get the next message from the socket as a block of bytes. + byte[] messageBytes = this.getNextMessageBytes(); + + //Decode the binary message into an appropriate message object. + BinaryMessageDecoder decoder = new BinaryMessageDecoder(messageBytes); + + return decoder.decode(); + + } + + + + @Override + public void run() { + + long previousFrameTime = System.currentTimeMillis(); + + while (!Thread.interrupted()) { + + + long currentFrameTime = System.currentTimeMillis(); + waitForFramePeriod(previousFrameTime, currentFrameTime, 16); + previousFrameTime = currentFrameTime; + + + //Reads the next message. + try { + AC35Data message = this.getNextMessage(); + messagesRead.add(message); + } + catch (InvalidMessageException | IOException e) { + + Logger.getGlobal().log(Level.WARNING, "Unable to read message.", e); + + try { + inputStream.reset(); + } catch (IOException e1) { + Logger.getGlobal().log(Level.WARNING, "Unable to reset inputStream.", e); + } + + } + + } + + } +} diff --git a/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java b/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java new file mode 100644 index 00000000..02e6f7a6 --- /dev/null +++ b/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java @@ -0,0 +1,116 @@ +package network.StreamRelated; + + +import network.Exceptions.InvalidMessageException; +import network.MessageEncoders.RaceVisionByteEncoder; +import network.Messages.AC35Data; +import shared.model.RunnableWithFramePeriod; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class is responsible for writing a queue of {@link network.Messages.AC35Data} messages to an output stream. + */ +public class MessageSerialiser implements RunnableWithFramePeriod { + + + /** + * The stream we're writing to. + */ + private DataOutputStream outputStream; + + /** + * The messages we're writing to the stream. + */ + private BlockingQueue messagesToSend; + + + /** + * Ack numbers used in messages. + */ + private int ackNumber = 1; + + /** + * Determines whether or not this runnable is currently running. + */ + private boolean isRunning; + + + + /** + * Constructs a new MessageSerialiser to write a queue of messages to a given stream. + * @param outputStream The stream to write to. + * @param messagesToSend The messages to send. + */ + public MessageSerialiser(OutputStream outputStream, BlockingQueue messagesToSend) { + this.outputStream = new DataOutputStream(outputStream); + this.messagesToSend = messagesToSend; + } + + + /** + * Increments the ackNumber value, and returns it. + * @return Incremented ackNumber. + */ + private int getNextAckNumber(){ + this.ackNumber++; + + return this.ackNumber; + } + + /** + * Determines whether or not this runnable is running. + * @return True means that it is still running, false means that it has stopped. + */ + public boolean isRunning() { + return isRunning; + } + + + @Override + public void run() { + + long previousFrameTime = System.currentTimeMillis(); + + isRunning = true; + + while (isRunning) { + + + long currentFrameTime = System.currentTimeMillis(); + waitForFramePeriod(previousFrameTime, currentFrameTime, 16); + previousFrameTime = currentFrameTime; + + + //Send the messages. + List messages = new ArrayList<>(); + messagesToSend.drainTo(messages); + + for (AC35Data message : messages) { + try { + byte[] messageBytes = RaceVisionByteEncoder.encodeBinaryMessage(message, getNextAckNumber()); + + outputStream.write(messageBytes); + + + } catch (InvalidMessageException e) { + Logger.getGlobal().log(Level.WARNING, "Could not encode message: " + message, e); + + } catch (IOException e) { + Logger.getGlobal().log(Level.WARNING, "Could not write message to outputStream: " + outputStream, e); + isRunning = false; + + } + } + + } + + } +} diff --git a/racevisionGame/src/main/java/shared/exceptions/BoatNotFoundException.java b/racevisionGame/src/main/java/shared/exceptions/BoatNotFoundException.java new file mode 100644 index 00000000..f3fed55c --- /dev/null +++ b/racevisionGame/src/main/java/shared/exceptions/BoatNotFoundException.java @@ -0,0 +1,15 @@ +package shared.exceptions; + +/** + * An exception thrown when a specific boat cannot be found. + */ +public class BoatNotFoundException extends Exception { + + public BoatNotFoundException(String message) { + super(message); + } + + public BoatNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/racevisionGame/src/main/java/shared/exceptions/HandshakeException.java b/racevisionGame/src/main/java/shared/exceptions/HandshakeException.java new file mode 100644 index 00000000..2f62e286 --- /dev/null +++ b/racevisionGame/src/main/java/shared/exceptions/HandshakeException.java @@ -0,0 +1,24 @@ +package shared.exceptions; + +/** + * An exception thrown when we the client-server handshake fails. + */ +public class HandshakeException extends Exception { + + /** + * Constructs the exception with a given message. + * @param message Message to store. + */ + public HandshakeException(String message) { + super(message); + } + + /** + * Constructs the exception with a given message and cause. + * @param message Message to store. + * @param cause Cause to store. + */ + public HandshakeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java b/racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java new file mode 100644 index 00000000..af633af3 --- /dev/null +++ b/racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java @@ -0,0 +1,64 @@ +package shared.model; + + +import network.Exceptions.InvalidMessageException; +import network.MessageEncoders.RaceVisionByteEncoder; +import network.Messages.AC35Data; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This interface is a {@link Runnable} interface, with the ability to sleep until a given time period has elapsed. + */ +public interface RunnableWithFramePeriod extends Runnable { + + + + + + + /** + * Waits for enough time for the period of this frame to be greater than minimumFramePeriod. + * @param previousFrameTime The timestamp of the previous frame. + * @param currentFrameTime The timestamp of the current frame. + * @param minimumFramePeriod The minimum period the frame must be. + */ + default void waitForFramePeriod(long previousFrameTime, long currentFrameTime, long minimumFramePeriod) { + + + //This is the time elapsed, in milliseconds, since the last server "frame". + long framePeriod = currentFrameTime - previousFrameTime; + + //We only attempt to send packets every X milliseconds. + if (framePeriod >= minimumFramePeriod) { + return; + + } else { + //Wait until the frame period will be large enough. + long timeToWait = minimumFramePeriod - framePeriod; + + try { + Thread.sleep(timeToWait); + + } catch (InterruptedException e) { + //If we get interrupted, exit the function. + Logger.getGlobal().log(Level.SEVERE, "RunnableWithFramePeriod.waitForFramePeriod().sleep(framePeriod) was interrupted on thread: " + Thread.currentThread(), e); + //Re-set the interrupt flag. + Thread.currentThread().interrupt(); + return; + + } + + } + + } + +} diff --git a/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java b/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java index ae8c682c..5f1e2d8d 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java @@ -144,32 +144,5 @@ public class ConnectionController extends Controller { } } - /** - * Sets up a new host - */ - public void addLocal() { - try { - //We don't want to host more than one game. - if (!currentlyHostingGame) { - Event game = Event.getEvent(); - urlField.textProperty().set(game.getAddress()); - portField.textProperty().set(Integer.toString(game.getPort())); - - game.start(); - addConnection(); - - currentlyHostingGame = true; - } - } catch (InvalidRaceDataException e) { - e.printStackTrace(); - } catch (XMLReaderException e) { - e.printStackTrace(); - } catch (InvalidBoatDataException e) { - e.printStackTrace(); - } catch (InvalidRegattaDataException e) { - e.printStackTrace(); - } catch (UnknownHostException e) { - e.printStackTrace(); - } - } + } diff --git a/racevisionGame/src/main/java/visualiser/Controllers/HostController.java b/racevisionGame/src/main/java/visualiser/Controllers/HostController.java index e87ea689..7873e8e6 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/HostController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/HostController.java @@ -6,6 +6,7 @@ import javafx.scene.control.*; import javafx.scene.layout.AnchorPane; import javafx.stage.Stage; import mock.app.Event; +import mock.exceptions.EventConstructionException; import shared.exceptions.InvalidBoatDataException; import shared.exceptions.InvalidRaceDataException; import shared.exceptions.InvalidRegattaDataException; @@ -17,6 +18,8 @@ import java.net.Socket; import java.net.URL; import java.net.UnknownHostException; import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Controller for Hosting a game. @@ -44,17 +47,12 @@ public class HostController extends Controller { */ public void hostGamePressed() throws IOException{ try { - Event game = Event.getEvent(); + Event game = new Event(); game.start(); connectSocket("localhost", 4942); - } catch (InvalidRaceDataException e) { - e.printStackTrace(); - } catch (XMLReaderException e) { - e.printStackTrace(); - } catch (InvalidBoatDataException e) { - e.printStackTrace(); - } catch (InvalidRegattaDataException e) { - e.printStackTrace(); + } catch (EventConstructionException e) { + Logger.getGlobal().log(Level.SEVERE, "Could not create Event.", e); + throw new RuntimeException(e); } } diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java index f34c57a8..5a1836f6 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java @@ -26,6 +26,8 @@ import visualiser.model.*; import java.io.IOException; import java.net.URL; import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Controller used to display a running race. @@ -115,8 +117,9 @@ public class RaceController extends Controller { controllerClient.sendKey(controlKey); controlKey.onAction(); // Change key state if applicable event.consume(); - } catch (IOException e) { - e.printStackTrace(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Logger.getGlobal().log(Level.WARNING, "RaceController was interrupted on thread: " + Thread.currentThread() + "while sending: " + controlKey, e); } } }); diff --git a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java index 8db4ec60..2f13aae2 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java @@ -20,6 +20,7 @@ import shared.exceptions.InvalidRegattaDataException; import shared.exceptions.XMLReaderException; import visualiser.app.VisualiserInput; import visualiser.gameController.ControllerClient; +import visualiser.model.ServerConnection; import visualiser.model.VisualiserBoat; import visualiser.model.VisualiserRace; @@ -27,6 +28,8 @@ import java.io.IOException; import java.net.Socket; import java.net.URL; import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Controller to for waiting for the race to start. @@ -66,18 +69,18 @@ public class StartController extends Controller implements Observer { @FXML private Label raceStatusLabel; - /** - * The object used to read packets from the connected server. + * Our connection to the server. */ - private VisualiserInput visualiserInput; + private ServerConnection serverConnection; + /** * The race object which describes the currently occurring race. */ private VisualiserRace visualiserRace; - private ControllerClient controllerClient; + /** * An array of colors used to assign colors to each boat - passed in to the VisualiserRace constructor. @@ -309,17 +312,17 @@ public class StartController extends Controller implements Observer { public void enterLobby(Socket socket) { startWrapper.setVisible(true); try { - //Begin reading packets from the socket/server. - this.visualiserInput = new VisualiserInput(socket); - //Send controller input to server - this.controllerClient = new ControllerClient(socket); + + LatestMessages latestMessages = new LatestMessages(); + this.serverConnection = new ServerConnection(socket, latestMessages); + + //Store a reference to latestMessages so that we can observe it. - LatestMessages latestMessages = this.visualiserInput.getLatestMessages(); latestMessages.addObserver(this); - new Thread(this.visualiserInput).start(); + new Thread(this.serverConnection).start(); } catch (IOException e) { - e.printStackTrace(); + Logger.getGlobal().log(Level.WARNING, "Could not connection to server.", e); } } diff --git a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java b/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java index e77a2fef..8ad14a1b 100644 --- a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java +++ b/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java @@ -2,12 +2,14 @@ package visualiser.app; import network.BinaryMessageDecoder; import network.Exceptions.InvalidMessageException; import network.Messages.*; +import shared.model.RunnableWithFramePeriod; import java.io.DataInputStream; import java.io.IOException; import java.net.Socket; import java.nio.ByteBuffer; import java.util.Arrays; +import java.util.concurrent.BlockingQueue; import static network.Utils.ByteConverter.bytesToShort; @@ -15,7 +17,7 @@ import static network.Utils.ByteConverter.bytesToShort; * TCP client which receives packets/messages from a race data source * (e.g., mock source, official source), and exposes them to any observers. */ -public class VisualiserInput implements Runnable { +public class VisualiserInput implements RunnableWithFramePeriod { /** * Timestamp of the last heartbeat. @@ -27,40 +29,28 @@ public class VisualiserInput implements Runnable { private long lastHeartbeatSequenceNum = -1; - /** - * The socket that we have connected to. - */ - private Socket connectionSocket; - /** - * InputStream (from the socket). + * Incoming messages from server. */ - private DataInputStream inStream; + private BlockingQueue incomingMessages; /** * An object containing the set of latest messages to write to. - * Every server frame, VisualiserInput reads messages from its inputStream, and write them to this. + * Every server frame, VisualiserInput reads messages from its incomingMessages, and write them to this. */ private LatestMessages latestMessages; - /** - * Ctor. - * @param socket Socket from which we will receive race data. - * @throws IOException If there is something wrong with the socket's input stream. + * Constructs a visualiserInput to convert an incoming stream of messages into LatestMessages. + * @param latestMessages Object to place messages in. + * @param incomingMessages The incoming queue of messages. */ - public VisualiserInput(Socket socket) throws IOException { - - this.connectionSocket = socket; - - //We wrap a DataInputStream around the socket's InputStream because it has the stream.readFully(buffer) function, which is a blocking read until the buffer has been filled. - this.inStream = new DataInputStream(connectionSocket.getInputStream()); - - this.latestMessages = new LatestMessages(); - + public VisualiserInput(LatestMessages latestMessages, BlockingQueue incomingMessages) { + this.latestMessages = latestMessages; + this.incomingMessages = incomingMessages; this.lastHeartbeatTime = System.currentTimeMillis(); } @@ -85,279 +75,21 @@ public class VisualiserInput implements Runnable { - /** - * Reads and returns the next message as an array of bytes from the socket. Use getNextMessage() to get the actual message object instead. - * @return Encoded binary message bytes. - * @throws IOException Thrown when an error occurs while reading from the socket. - */ - private byte[] getNextMessageBytes() throws IOException { - inStream.mark(0); - short CRCLength = 4; - short headerLength = 15; - - //Read the header of the next message. - byte[] headerBytes = new byte[headerLength]; - inStream.readFully(headerBytes); - - //Read the message body length. - byte[] messageBodyLengthBytes = Arrays.copyOfRange(headerBytes, headerLength - 2, headerLength); - short messageBodyLength = bytesToShort(messageBodyLengthBytes); - - //Read the message body. - byte[] messageBodyBytes = new byte[messageBodyLength]; - inStream.readFully(messageBodyBytes); - - //Read the message CRC. - byte[] messageCRCBytes = new byte[CRCLength]; - inStream.readFully(messageCRCBytes); - - //Put the head + body + crc into one large array. - ByteBuffer messageBytes = ByteBuffer.allocate(headerBytes.length + messageBodyBytes.length + messageCRCBytes.length); - messageBytes.put(headerBytes); - messageBytes.put(messageBodyBytes); - messageBytes.put(messageCRCBytes); - - return messageBytes.array(); - } - - /** - * Reads and returns the next message object from the socket. - * @return The message object. Use instanceof for concrete type. - * @throws IOException Thrown when an error occurs while reading from the socket. - * @throws InvalidMessageException Thrown when the message is invalid in some way. - */ - private AC35Data getNextMessage() throws IOException, InvalidMessageException - { - //Get the next message from the socket as a block of bytes. - byte[] messageBytes = this.getNextMessageBytes(); - - //Decode the binary message into an appropriate message object. - BinaryMessageDecoder decoder = new BinaryMessageDecoder(messageBytes); - - return decoder.decode(); - - } - - /** - * Main loop which reads messages from the socket, and exposes them. - */ - public void run(){ - boolean receiverLoop = true; - //receiver loop that gets the input - while (receiverLoop) { - - //If no heartbeat has been received in more the heartbeat period - //then the connection will need to be restarted. - //System.out.println("time since last heartbeat: " + timeSinceHeartbeat());//TEMP REMOVE - long heartBeatPeriod = 10 * 1000; - if (timeSinceHeartbeat() > heartBeatPeriod) { - System.out.println("Connection has stopped, trying to reconnect."); - - //Attempt to reconnect the socket. - try {//This attempt doesn't really work. Under what circumstances would - this.connectionSocket = new Socket(this.connectionSocket.getInetAddress(), this.connectionSocket.getPort()); - //this.connectionSocket.connect(this.connectionSocket.getRemoteSocketAddress()); - //Reset the heartbeat timer. - this.lastHeartbeatTime = System.currentTimeMillis(); - } - catch (IOException e) { - System.err.println("Unable to reconnect."); - - //Wait 500ms. Ugly hack, should refactor. - long waitPeriod = 500; - long waitTimeStart = System.currentTimeMillis() + waitPeriod; - - while (System.currentTimeMillis() < waitTimeStart){ - //Nothing. Busyloop. - } - - //Swallow the exception. - continue; - } - - } - - //Reads the next message. - AC35Data message; - try { - message = this.getNextMessage(); - } - catch (InvalidMessageException | IOException e) { - //Prints exception to stderr, and iterate loop (that is, read the next message). - System.err.println("Unable to read message: " + e.getMessage()); - try { - inStream.reset(); - } catch (IOException e1) { - e1.printStackTrace(); - } - //Continue to the next loop iteration/message. - continue; - } - - - //Checks which message is being received and does what is needed for that message. - switch (message.getType()) { - - //Heartbeat. - case HEARTBEAT: { - HeartBeat heartBeat = (HeartBeat) message; - - //Check that the heartbeat number is greater than the previous value, and then set the last heartbeat time. - if (heartBeat.getSequenceNumber() > this.lastHeartbeatSequenceNum) { - lastHeartbeatTime = System.currentTimeMillis(); - lastHeartbeatSequenceNum = heartBeat.getSequenceNumber(); - //System.out.println("HeartBeat Message! " + lastHeartbeatSequenceNum); - } - - break; - } - - //RaceStatus. - case RACESTATUS: { - RaceStatus raceStatus = (RaceStatus) message; - - //System.out.println("Race Status Message"); - this.latestMessages.setRaceStatus(raceStatus); - - for (BoatStatus boatStatus : raceStatus.getBoatStatuses()) { - this.latestMessages.setBoatStatus(boatStatus); - } - - break; - } - - //DisplayTextMessage. - case DISPLAYTEXTMESSAGE: { - //System.out.println("Display Text Message"); - //No decoder for this. - - break; - } - - //XMLMessage. - case XMLMESSAGE: { - XMLMessage xmlMessage = (XMLMessage) message; - - //System.out.println("XML Message!"); - - this.latestMessages.setXMLMessage(xmlMessage); - - break; - } - - //RaceStartStatus. - case RACESTARTSTATUS: { - - //System.out.println("Race Start Status Message"); - - break; - } - - //YachtEventCode. - case YACHTEVENTCODE: { - //YachtEventCode yachtEventCode = (YachtEventCode) message; - - //System.out.println("Yacht Event Code!"); - //No decoder for this. - - break; - } - - //YachtActionCode. - case YACHTACTIONCODE: { - //YachtActionCode yachtActionCode = (YachtActionCode) message; - - //System.out.println("Yacht Action Code!"); - // No decoder for this. - - break; - } - - //ChatterText. - case CHATTERTEXT: { - //ChatterText chatterText = (ChatterText) message; - - //System.out.println("Chatter Text Message!"); - //No decoder for this. - - break; - } - - //BoatLocation. - case BOATLOCATION: { - BoatLocation boatLocation = (BoatLocation) message; - - //System.out.println("Boat Location!"); - - BoatLocation existingBoatLocation = this.latestMessages.getBoatLocationMap().get(boatLocation.getSourceID()); - if (existingBoatLocation != null) { - //If our boatlocation map already contains a boat location message for this boat, check that the new message is actually for a later timestamp (i.e., newer). - if (boatLocation.getTime() > existingBoatLocation.getTime()) { - //If it is, replace the old message. - this.latestMessages.setBoatLocation(boatLocation); - } - } else { - //If the map _doesn't_ already contain a message for this boat, insert the message. - this.latestMessages.setBoatLocation(boatLocation); - } - - break; - } - - //MarkRounding. - case MARKROUNDING: { - MarkRounding markRounding = (MarkRounding) message; - - //System.out.println("Mark Rounding Message!"); - - MarkRounding existingMarkRounding = this.latestMessages.getMarkRoundingMap().get(markRounding.getSourceID()); - if (existingMarkRounding != null) { - - //If our markRoundingMap already contains a mark rounding message for this boat, check that the new message is actually for a later timestamp (i.e., newer). - if (markRounding.getTime() > existingMarkRounding.getTime()) { - //If it is, replace the old message. - this.latestMessages.setMarkRounding(markRounding); - } - - } else { - //If the map _doesn't_ already contain a message for this boat, insert the message. - this.latestMessages.setMarkRounding(markRounding); - } - - break; - } - - //CourseWinds. - case COURSEWIND: { - - //System.out.println("Course Wind Message!"); - CourseWinds courseWinds = (CourseWinds) message; - - this.latestMessages.setCourseWinds(courseWinds); - break; - } + @Override + public void run() { - //AverageWind. - case AVGWIND: { + //Handshake. - //System.out.println("Average Wind Message!"); - AverageWind averageWind = (AverageWind) message; + //Main loop. + // take message + // create command + // place in command queue - this.latestMessages.setAverageWind(averageWind); - break; - } - //Unrecognised message. - default: { - System.out.println("Broken Message!"); - break; - } - } - } } } diff --git a/racevisionGame/src/main/java/visualiser/gameController/ControllerClient.java b/racevisionGame/src/main/java/visualiser/gameController/ControllerClient.java index bb91f2a4..9b38a5ca 100644 --- a/racevisionGame/src/main/java/visualiser/gameController/ControllerClient.java +++ b/racevisionGame/src/main/java/visualiser/gameController/ControllerClient.java @@ -3,6 +3,7 @@ package visualiser.gameController; import network.BinaryMessageEncoder; import network.Exceptions.InvalidMessageException; import network.MessageEncoders.RaceVisionByteEncoder; +import network.Messages.AC35Data; import network.Messages.BoatAction; import network.Messages.Enums.BoatActionEnum; import network.Messages.Enums.MessageType; @@ -13,6 +14,7 @@ import java.io.IOException; import java.net.Socket; import java.net.SocketException; import java.nio.ByteBuffer; +import java.util.concurrent.BlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; @@ -20,28 +22,18 @@ import java.util.logging.Logger; * Basic service for sending key presses to game server */ public class ControllerClient { - /** - * Socket to server - */ - Socket socket; /** - * Output stream wrapper for socket to server + * Queue of messages to be sent to server. */ - DataOutputStream outputStream; + private BlockingQueue outgoingMessages; /** * Initialise controller client with live socket. - * @param socket to server + * @param outgoingMessages Queue to place messages on to send to server. */ - public ControllerClient(Socket socket) { - this.socket = socket; - - try { - this.outputStream = new DataOutputStream(socket.getOutputStream()); - } catch (IOException e) { - e.printStackTrace(); - } + public ControllerClient(BlockingQueue outgoingMessages) { + this.outgoingMessages = outgoingMessages; } /** @@ -49,27 +41,13 @@ public class ControllerClient { * @param key to send * @throws IOException if socket write fails */ - public void sendKey(ControlKey key) throws IOException { + public void sendKey(ControlKey key) throws InterruptedException { BoatActionEnum protocolCode = key.getProtocolCode(); if(protocolCode != BoatActionEnum.NOT_A_STATUS) { BoatAction boatAction = new BoatAction(protocolCode); - //Encode BoatAction. - try { - byte[] encodedBoatAction = RaceVisionByteEncoder.encode(boatAction); - - BinaryMessageEncoder binaryMessage = new BinaryMessageEncoder(MessageType.BOATACTION, System.currentTimeMillis(), 0, - (short) encodedBoatAction.length, encodedBoatAction); - - System.out.println("Sending out key: " + protocolCode); - outputStream.write(binaryMessage.getFullMessage()); - - } catch (InvalidMessageException e) { - Logger.getGlobal().log(Level.WARNING, "Could not encode BoatAction: " + boatAction, e); - - } - + outgoingMessages.put(boatAction); } } diff --git a/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java b/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java index d4c62d11..3757dc01 100644 --- a/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java +++ b/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java @@ -1,16 +1,19 @@ package visualiser.gameController; +import mock.model.commandFactory.Command; +import mock.model.commandFactory.CommandFactory; +import mock.model.commandFactory.CompositeCommand; import network.BinaryMessageDecoder; import network.Exceptions.InvalidMessageException; import network.MessageDecoders.BoatActionDecoder; +import network.Messages.AC35Data; import network.Messages.BoatAction; -import network.Messages.Enums.BoatActionEnum; -import visualiser.gameController.Keys.ControlKey; -import visualiser.gameController.Keys.KeyFactory; +import network.Messages.Enums.MessageType; import java.io.DataInputStream; import java.io.IOException; -import java.net.Socket; +import java.io.InputStream; +import java.util.concurrent.BlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; @@ -18,57 +21,69 @@ import java.util.logging.Logger; * Service for dispatching key press data to race from client */ public class ControllerServer implements Runnable { + + /** - * Socket to client + * Queue of incoming messages from client. */ - private Socket socket; + private BlockingQueue inputQueue; + + /** - * Wrapper for input from client + * Collection of commands from client for race to execute. */ - private DataInputStream inputStream; + private CompositeCommand compositeCommand; /** - * Initialise server-side controller with live client socket - * @param socket to client + * This is the source ID associated with the client. */ - public ControllerServer(Socket socket) { - this.socket = socket; - try { - this.inputStream = new DataInputStream(this.socket.getInputStream()); - } catch (IOException e) { - e.printStackTrace(); - } + private int clientSourceID; + + + + /** + * Initialise server-side controller with live client socket. + * @param compositeCommand Commands for the race to execute. + * @param inputQueue The queue of messages to read from. + * @param clientSourceID The source ID of the client's boat. + */ + public ControllerServer(CompositeCommand compositeCommand, BlockingQueue inputQueue, int clientSourceID) { + this.compositeCommand = compositeCommand; + this.inputQueue = inputQueue; + this.clientSourceID = clientSourceID; } + + /** * Wait for controller key input from client and loop. */ @Override public void run() { - while(true) { - byte[] message = new byte[20]; - try { - if (inputStream.available() > 0) { + while(!Thread.interrupted()) { - inputStream.read(message); + try { - BinaryMessageDecoder encodedMessage = new BinaryMessageDecoder(message); - BoatActionDecoder boatActionDecoder = new BoatActionDecoder(); + AC35Data message = inputQueue.take(); - try { - boatActionDecoder.decode(encodedMessage.getMessageBody()); - BoatAction boatAction = boatActionDecoder.getMessage(); - System.out.println("Received key: " + boatAction.getBoatAction()); + if (message.getType() == MessageType.BOATACTION) { - } catch (InvalidMessageException e) { - Logger.getGlobal().log(Level.WARNING, "Could not decode BoatAction message.", e); - } + BoatAction boatAction = (BoatAction) message; + boatAction.setSourceID(clientSourceID); + Command command = CommandFactory.createCommand(boatAction); + compositeCommand.addCommand(command); } - } catch (IOException e) { - e.printStackTrace(); + + + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.WARNING, "ControllerServer Interrupted while waiting for message on incoming message queue.", e); + Thread.currentThread().interrupt(); + return; } + } + } } diff --git a/racevisionGame/src/main/java/visualiser/gameController/Keys/KeyFactory.java b/racevisionGame/src/main/java/visualiser/gameController/Keys/KeyFactory.java index ef1368f0..be95abd3 100644 --- a/racevisionGame/src/main/java/visualiser/gameController/Keys/KeyFactory.java +++ b/racevisionGame/src/main/java/visualiser/gameController/Keys/KeyFactory.java @@ -27,8 +27,8 @@ public class KeyFactory { keyState.put("SPACE", new VMGKey("VMG")); keyState.put("SHIFT", new SailsToggleKey("Toggle Sails")); keyState.put("ENTER", new TackGybeKey("Tack/Gybe")); - keyState.put("PAGE_UP", new UpWindKey("Upwind")); - keyState.put("PAGE_DOWN", new DownWindKey("Downwind")); + keyState.put("UP", new UpWindKey("Upwind")); + keyState.put("DOWN", new DownWindKey("Downwind")); } /** diff --git a/racevisionGame/src/main/java/visualiser/model/ServerConnection.java b/racevisionGame/src/main/java/visualiser/model/ServerConnection.java new file mode 100644 index 00000000..ececdd4c --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/ServerConnection.java @@ -0,0 +1,228 @@ +package visualiser.model; + + +import mock.app.MockOutput; +import mock.enums.ConnectionStateEnum; +import mock.exceptions.SourceIDAllocationException; +import mock.model.HeartBeatService; +import mock.model.SourceIdAllocator; +import mock.model.commandFactory.CompositeCommand; +import network.Messages.AC35Data; +import network.Messages.Enums.JoinAcceptanceEnum; +import network.Messages.Enums.MessageType; +import network.Messages.Enums.RequestToJoinEnum; +import network.Messages.JoinAcceptance; +import network.Messages.LatestMessages; +import network.Messages.RequestToJoin; +import network.StreamRelated.MessageDeserialiser; +import network.StreamRelated.MessageSerialiser; +import shared.exceptions.HandshakeException; +import visualiser.app.VisualiserInput; +import visualiser.gameController.ControllerClient; +import visualiser.gameController.ControllerServer; + +import java.io.IOException; +import java.net.Socket; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class handles the client-server connection handshake, and creation of VisualiserInput and ControllerClient. + */ +public class ServerConnection implements Runnable { + + /** + * The socket for the connection to server. + */ + private Socket socket; + + + /** + * Latest snapshot of the race, received from the server. + */ + private LatestMessages latestMessages; + + + /** + * Used to convert incoming messages into a race snapshot. + */ + private VisualiserInput visualiserInput; + + /** + * Used to send client input to server. + */ + private ControllerClient controllerClient; + + + /** + * Used to write messages to socket. + */ + private MessageSerialiser messageSerialiser; + + /** + * Stores messages to write to socket. + */ + private BlockingQueue outputQueue; + + /** + * Used to read messages from socket. + */ + private MessageDeserialiser messageDeserialiser; + + /** + * Stores messages read from socket. + */ + private BlockingQueue inputQueue; + + /** + * The state of the connection to the client. + */ + private ConnectionStateEnum connectionState = ConnectionStateEnum.UNKNOWN; + + + + + + + /** + * Creates a server connection, using a given socket. + * @param socket The socket which connects to the client. + * @param latestMessages Latest race snapshot to send to client. + * @throws IOException Thrown if there is a problem with the client socket. + */ + public ServerConnection(Socket socket, LatestMessages latestMessages) throws IOException { + this.socket = socket; + this.latestMessages = latestMessages; + + this.outputQueue = new LinkedBlockingQueue<>(); + this.inputQueue = new LinkedBlockingQueue<>(); + + + this.messageSerialiser = new MessageSerialiser(socket.getOutputStream(), outputQueue); + this.messageDeserialiser = new MessageDeserialiser(socket.getInputStream(), inputQueue); + + new Thread(messageSerialiser, "ServerConnection()->MessageSerialiser thread " + messageSerialiser).start(); + new Thread(messageDeserialiser, "ServerConnection()->MessageDeserialiser thread " + messageDeserialiser).start(); + + } + + + + @Override + public void run() { + try { + handshake(); + + } catch (HandshakeException e) { + Logger.getGlobal().log(Level.WARNING, "Server handshake failed.", e); + Thread.currentThread().interrupt(); + return; + } + + } + + + /** + * Initiates the handshake with the server. + * @throws HandshakeException Thrown if something goes wrong with the handshake. + */ + private void handshake() throws HandshakeException { + + //This function is a bit messy, and could probably be refactored a bit. + + connectionState = ConnectionStateEnum.WAITING_FOR_HANDSHAKE; + + + sendJoinAcceptanceMessage(RequestToJoinEnum.PARTICIPANT); + + + JoinAcceptance joinAcceptance = waitForJoinAcceptance(); + + int allocatedSourceID = 0; + + //If we join successfully... + if (joinAcceptance.getAcceptanceType() == JoinAcceptanceEnum.JOIN_SUCCESSFUL) { + + allocatedSourceID = joinAcceptance.getSourceID(); + //TODO need to do something with the ID - maybe flag the correct visualiser boat as being the client's boat? + + this.controllerClient = new ControllerClient(inputQueue); + //new Thread(controllerClient, "ServerConnection.run()->ControllerClient thread " + controllerClient).start(); + + } + + this.visualiserInput = new VisualiserInput(latestMessages, outputQueue); + new Thread(visualiserInput, "ServerConnection.run()->VisualiserInput thread " + visualiserInput).start(); + + + connectionState = ConnectionStateEnum.CONNECTED; + + } + + + /** + * Waits until the server sends a {@link JoinAcceptance} message, and returns it. + * @return The {@link JoinAcceptance} message. + * @throws HandshakeException Thrown if we get interrupted while waiting. + */ + private JoinAcceptance waitForJoinAcceptance() throws HandshakeException { + + try { + + + while (connectionState == ConnectionStateEnum.WAITING_FOR_HANDSHAKE) { + + AC35Data message = inputQueue.take(); + + //We need to wait until they actually send a join request. + if (message.getType() == MessageType.JOIN_ACCEPTANCE) { + return (JoinAcceptance) message; + } + + } + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new HandshakeException("Handshake failed. Thread: " + Thread.currentThread() + " was interrupted while waiting on the incoming message queue.", e); + + } + + + throw new HandshakeException("Handshake was cancelled. Connection state is now: " + connectionState); + + } + + + /** + * Sends the server a {@link RequestToJoin} message. + * @param requestType The type of request to send + * @throws HandshakeException Thrown if the thread is interrupted while placing message on the outgoing message queue. + */ + private void sendJoinAcceptanceMessage(RequestToJoinEnum requestType) throws HandshakeException { + + //Send them the source ID. + RequestToJoin requestToJoin = new RequestToJoin(requestType); + + try { + outputQueue.put(requestToJoin); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new HandshakeException("Handshake failed. Thread: " + Thread.currentThread() + " interrupted while placing RequestToJoin message on outgoing message queue.", e); + } + + } + + + /** + * Determines whether or not this connection is still alive. + * This is based off whether the {@link MessageSerialiser} is still alive. + * @return True if it is alive, false otherwise. + */ + public boolean isAlive() { + return messageSerialiser.isRunning(); + } + + +} diff --git a/racevisionGame/src/test/java/mock/model/SourceIdAllocatorTest.java b/racevisionGame/src/test/java/mock/model/SourceIdAllocatorTest.java new file mode 100644 index 00000000..7240e01b --- /dev/null +++ b/racevisionGame/src/test/java/mock/model/SourceIdAllocatorTest.java @@ -0,0 +1,126 @@ +package mock.model; + +import mock.exceptions.SourceIDAllocationException; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + + +/** + * Tests if allocating source IDs works. + */ +public class SourceIdAllocatorTest { + + /** + * This is the list of source IDs that we start with. + */ + private List originalSourceIDs; + + /** + * Used to allocate source IDs. + */ + private SourceIdAllocator sourceIdAllocator; + + + @Before + public void setUp() throws Exception { + + originalSourceIDs = new ArrayList<>(); + originalSourceIDs.add(120); + originalSourceIDs.add(121); + originalSourceIDs.add(122); + originalSourceIDs.add(123); + originalSourceIDs.add(124); + originalSourceIDs.add(125); + + + sourceIdAllocator = new SourceIdAllocator(originalSourceIDs); + + } + + + /** + * Tests that allocation fails when we don't have any source IDs to allocate. + */ + @Test + public void emptyAllocationTest() { + + SourceIdAllocator allocator = new SourceIdAllocator(new ArrayList<>()); + + + try { + int sourceID = allocator.allocateSourceID(); + + fail("Exception should have been thrown, but wasn't."); + + } catch (SourceIDAllocationException e) { + + //We expect this exception to be thrown - success. + + } + + } + + + /** + * Tests that we can allocate a source ID. + * @throws Exception Thrown in case of error. + */ + @Test + public void allocationTest() throws Exception { + + + int sourceID = sourceIdAllocator.allocateSourceID(); + + } + + + /** + * Tests that we can allocate source IDs, but it will eventually be unable to allocate source IDs. + */ + @Test + public void allocationEventuallyFailsTest() { + + while (true) { + + try { + int sourceID = sourceIdAllocator.allocateSourceID(); + + } catch (SourceIDAllocationException e) { + //We expect to encounter this exception after enough allocations - success. + break; + + } + + } + + } + + + /** + * Tests if we can allocate a source ID, return it, and reallocate it. + * @throws Exception Thrown in case of error. + */ + @Test + public void reallocationTest() throws Exception { + + List sourceIDList = new ArrayList<>(); + sourceIDList.add(123); + + SourceIdAllocator sourceIdAllocator = new SourceIdAllocator(sourceIDList); + + //Allocate. + int sourceID = sourceIdAllocator.allocateSourceID(); + + //Return. + sourceIdAllocator.returnSourceID(sourceID); + + //Reallocate. + int sourceID2 = sourceIdAllocator.allocateSourceID(); + + } +} diff --git a/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java b/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java new file mode 100644 index 00000000..c3d0df04 --- /dev/null +++ b/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java @@ -0,0 +1,31 @@ +package mock.model.commandFactory; + +import mock.model.MockRace; +import network.Messages.Enums.BoatActionEnum; +import org.junit.Before; +import org.junit.Test; +import shared.model.Boat; +import shared.model.Race; +import visualiser.model.VisualiserRace; + +import static org.testng.Assert.*; + +/** + * Created by connortaylorbrown on 4/08/17. + */ +public class WindCommandTest { + private Race race; + private Boat boat; + private Command upwind; + private Command downwind; + + @Before + public void setUp() { + boat = new Boat(0, "Bob", "NZ"); + } + + @Test + public void upwindCommandDecreasesAngle() { + + } +} \ No newline at end of file From 4da37348046ccb232a47b750e0542d7f61539178 Mon Sep 17 00:00:00 2001 From: hba56 Date: Thu, 10 Aug 2017 12:17:19 +1200 Subject: [PATCH 25/56] added the ability to toggle the race guide line on and off #story[1087] --- .../java/visualiser/model/Annotations.java | 11 ++++++++++ .../visualiser/model/ResizableRaceCanvas.java | 12 +++++++++- .../resources/visualiser/scenes/race.fxml | 22 ++++++++++++------- 3 files changed, 36 insertions(+), 9 deletions(-) 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 333ed54b..a44dd8eb 100644 --- a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java @@ -49,6 +49,7 @@ public class ResizableRaceCanvas extends ResizableCanvas { private boolean annoPath = true; private boolean annoEstTime = true; private boolean annoTimeSinceLastMark = true; + private boolean annoGuideLine = false; @@ -116,6 +117,13 @@ public class ResizableRaceCanvas extends ResizableCanvas { annoSpeed = !annoSpeed; } + /** + * Toggle the guideline annotation + */ + public void toggleGuideLine() { + annoGuideLine = !annoGuideLine; + } + @@ -470,7 +478,9 @@ public class ResizableRaceCanvas extends ResizableCanvas { drawBoundary(); //Guiding Line - drawRaceLine(); + if (annoGuideLine){ + drawRaceLine(); + } //Boats. drawBoats(); diff --git a/racevisionGame/src/main/resources/visualiser/scenes/race.fxml b/racevisionGame/src/main/resources/visualiser/scenes/race.fxml index 159d725c..d7615b76 100644 --- a/racevisionGame/src/main/resources/visualiser/scenes/race.fxml +++ b/racevisionGame/src/main/resources/visualiser/scenes/race.fxml @@ -1,12 +1,17 @@ + + + + - + + @@ -30,16 +35,17 @@ - -