diff --git a/mock/src/main/java/seng302/DataInput/XMLReader.java b/mock/src/main/java/seng302/DataInput/XMLReader.java index c01891e3..a44b4183 100644 --- a/mock/src/main/java/seng302/DataInput/XMLReader.java +++ b/mock/src/main/java/seng302/DataInput/XMLReader.java @@ -50,8 +50,8 @@ public abstract class XMLReader { /** * Alternate constructor - * @param xmlFile - * @param isWholeFile + * @param xmlFile File to be read + * @param isWholeFile boolean value whether entire file is being passed */ public XMLReader(String xmlFile, Boolean isWholeFile) { @@ -91,9 +91,9 @@ public abstract class XMLReader { /** * Get the contents of the XML FILe. - * @param document - * @return - * @throws TransformerException + * @param document holds all xml information + * @return String representation of document + * @throws TransformerException when document is malformed, and cannot be turned into a string */ public static String getContents(Document document) throws TransformerException { DOMSource source = new DOMSource(document); diff --git a/mock/src/main/java/seng302/MockOutput.java b/mock/src/main/java/seng302/MockOutput.java index 4bb23af1..5ec96d2a 100644 --- a/mock/src/main/java/seng302/MockOutput.java +++ b/mock/src/main/java/seng302/MockOutput.java @@ -108,10 +108,11 @@ public class MockOutput implements Runnable * @param lon longitude of boat * @param heading heading of boat * @param speed speed of boat + * @param time historical time of race */ - public synchronized void parseBoatLocation(int sourceID, double lat, double lon, double heading, double speed){ + public synchronized void parseBoatLocation(int sourceID, double lat, double lon, double heading, double speed, long time){ - BoatLocation boatLocation = new BoatLocation(sourceID, lat, lon, boatLocationSequenceNumber, heading, speed); + BoatLocation boatLocation = new BoatLocation(sourceID, lat, lon, boatLocationSequenceNumber, heading, speed, time); //iterates the sequence number boatLocationSequenceNumber++; diff --git a/mock/src/main/java/seng302/Model/Angle.java b/mock/src/main/java/seng302/Model/Angle.java index 0fa6db0c..8fa767dd 100644 --- a/mock/src/main/java/seng302/Model/Angle.java +++ b/mock/src/main/java/seng302/Model/Angle.java @@ -54,9 +54,6 @@ public class Angle implements Comparable { return this.degrees; } - public void setDegrees(double degrees) { - this.degrees = degrees; - } /** * Returns the value of this Angle object, in radians. diff --git a/mock/src/main/java/seng302/Model/Boat.java b/mock/src/main/java/seng302/Model/Boat.java index 7e9fc9b6..1cc95a1e 100644 --- a/mock/src/main/java/seng302/Model/Boat.java +++ b/mock/src/main/java/seng302/Model/Boat.java @@ -81,6 +81,8 @@ public class Boat { */ private long timeSinceTackChange = 0; + private long estimatedTime = 0; + /** * Constructs a boat object with a given sourceID, name, country/team abbreviation, and polars table. @@ -442,4 +444,11 @@ public class Boat { return distanceTravelledMeters; } + public long getEstimatedTime() { + return estimatedTime; + } + + public void setEstimatedTime(long estimatedTime) { + this.estimatedTime = estimatedTime; + } } diff --git a/mock/src/main/java/seng302/Model/GPSCoordinate.java b/mock/src/main/java/seng302/Model/GPSCoordinate.java index 3f9527e2..a185b865 100644 --- a/mock/src/main/java/seng302/Model/GPSCoordinate.java +++ b/mock/src/main/java/seng302/Model/GPSCoordinate.java @@ -1,10 +1,12 @@ package seng302.Model; +import javafx.util.Pair; import org.geotools.referencing.GeodeticCalculator; import org.opengis.geometry.DirectPosition; import seng302.Constants; import java.awt.geom.Point2D; +import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -292,5 +294,226 @@ public class GPSCoordinate { } + + /** + * Takes a list of GPS coordinates describing a course boundary, and "shrinks" it inwards by 50m. + * @param boundary The boundary of course. + * @return A copy of the course boundary list, shrunk inwards by 50m. + */ + public static List getShrinkBoundary(List boundary) { + + + double shrinkDistance = 50d; + List shrunkBoundary = new ArrayList<>(boundary.size()); + //This is a list of edges that have been shrunk/shifted inwards. + List> shrunkEdges = new ArrayList<>(); + + + //We need to invert some of our opertations depending if the boundary is clockwise or anti-clockwise. + boolean isClockwise = GPSCoordinate.isClockwisePolygon(boundary); + double clockwiseScaleFactor = 0; + + if (isClockwise) { + clockwiseScaleFactor = 1; + } else { + clockwiseScaleFactor = -1; + } + + + /** + * Starting at a vertex, face anti-clockwise along an adjacent edge. + Replace the edge with a new, parallel edge placed at distance d to the "left" of the old one. + Repeat for all edges. + Find the intersections of the new edges to get the new vertices. + Detect if you've become a crossed polynomial and decide what to do about it. Probably add a new vertex at the crossing-point and get rid of some old ones. I'm not sure whether there's a better way to detect this than just to compare every pair of non-adjacent edges to see if their intersection lies between both pairs of vertices. + */ + + //For the first (size-1) adjacent pairs. + for (int i = 0; i < (boundary.size() - 1); i++) { + + //Get the points. + GPSCoordinate firstPoint = boundary.get(i); + GPSCoordinate secondPoint = boundary.get(i + 1); + + //Get the bearing between two adjacent points. + Bearing bearing = GPSCoordinate.calculateBearing(firstPoint, secondPoint); + + //Calculate angle perpendicular to bearing. + Bearing perpindicularBearing = Bearing.fromDegrees(bearing.degrees() + (90d * clockwiseScaleFactor)); + + //Translate both first and second point by 50m, using this bearing. These form our inwards shifted edge. + GPSCoordinate firstPointTranslated = GPSCoordinate.calculateNewPosition(firstPoint, shrinkDistance, Azimuth.fromBearing(perpindicularBearing)); + GPSCoordinate secondPointTranslated = GPSCoordinate.calculateNewPosition(secondPoint, shrinkDistance, Azimuth.fromBearing(perpindicularBearing)); + + //Add edge to list. + shrunkEdges.add(new Pair<>(firstPointTranslated, secondPointTranslated)); + + } + + //For the final adjacent pair, between the last and first point. + //Get the points. + GPSCoordinate firstPoint = boundary.get(boundary.size() - 1); + GPSCoordinate secondPoint = boundary.get(0); + + //Get the bearing between two adjacent points. + Bearing bearing = GPSCoordinate.calculateBearing(firstPoint, secondPoint); + + //Calculate angle perpendicular to bearing. + Bearing perpindicularBearing = Bearing.fromDegrees(bearing.degrees() + (90d * clockwiseScaleFactor)); + + //Translate both first and second point by 50m, using this bearing. These form our inwards shifted edge. + GPSCoordinate firstPointTranslated = GPSCoordinate.calculateNewPosition(firstPoint, shrinkDistance, Azimuth.fromBearing(perpindicularBearing)); + GPSCoordinate secondPointTranslated = GPSCoordinate.calculateNewPosition(secondPoint, shrinkDistance, Azimuth.fromBearing(perpindicularBearing)); + + //Add edge to list. + shrunkEdges.add(new Pair<>(firstPointTranslated, secondPointTranslated)); + + + //We now have a list of edges that have been shifted inwards. + //We need to find the intersections between adjacent vertices in our edge list. E.g., intersection between edge1-right, and edge2-left. + + //For the first (size-1) adjacent pairs. + for (int i = 0; i < (shrunkEdges.size() - 1); i++) { + + //Get the pair of adjacent edges. + Pair edge1 = shrunkEdges.get(i); + Pair edge2 = shrunkEdges.get(i + 1); + + //Get the x and y coordinates of first edge. + double x1 = edge1.getKey().getLongitude(); + double x2 = edge1.getValue().getLongitude(); + double y1 = edge1.getKey().getLatitude(); + double y2 = edge1.getValue().getLatitude(); + + //Get the x and y coordinates of second edge. + double x3 = edge2.getKey().getLongitude(); + double x4 = edge2.getValue().getLongitude(); + double y3 = edge2.getKey().getLatitude(); + double y4 = edge2.getValue().getLatitude(); + + //Find the equations for both edges. + // y = a*x + b + //First equation. + double a1 = (y2 - y1) / (x2 - x1); + double b1 = y1 - a1 * x1; + + //Second equation. + double a2 = (y4 - y3) / (x4 - x3); + double b2 = y3 - a2 * x3; + + + //Find intersecting x coordinate. + // a1 * x + b1 = a2 * x + b2 + double x0 = -(b1 - b2) / (a1 - a2); + //Find intersecting y coordinate. + double y0 = x0 * a1 + b1; + + //Add this to shrunk boundary list. + GPSCoordinate coordinate = new GPSCoordinate(y0, x0); + shrunkBoundary.add(coordinate); + + } + + + //For the final adjacent pair, between the last and first point. + //Get the pair of adjacent edges. + Pair edge1 = shrunkEdges.get(shrunkEdges.size() - 1); + Pair edge2 = shrunkEdges.get(0); + + //Get the x and y coordinates of first edge. + double x1 = edge1.getKey().getLongitude(); + double x2 = edge1.getValue().getLongitude(); + double y1 = edge1.getKey().getLatitude(); + double y2 = edge1.getValue().getLatitude(); + + //Get the x and y coordinates of second edge. + double x3 = edge2.getKey().getLongitude(); + double x4 = edge2.getValue().getLongitude(); + double y3 = edge2.getKey().getLatitude(); + double y4 = edge2.getValue().getLatitude(); + + //Find the equations for both edges. + // y = a*x + b + //First equation. + double a1 = (y2 - y1) / (x2 - x1); + double b1 = y1 - a1 * x1; + + //Second equation. + double a2 = (y4 - y3) / (x4 - x3); + double b2 = y3 - a2 * x3; + + + //Find intersecting x coordinate. + // a1 * x + b1 = a2 * x + b2 + double x0 = -(b1 - b2) / (a1 - a2); + //Find intersecting y coordinate. + double y0 = x0 * a1 + b1; + + //Add this to shrunk boundary list. + GPSCoordinate coordinate = new GPSCoordinate(y0, x0); + shrunkBoundary.add(coordinate); + + + + return shrunkBoundary; + + } + + + /** + * Determines if a list of coordinates describes a boundary polygon in clockwise or anti-clockwise order. + * @param boundary The list of coodinates. + * @return True if clockwise, false if anti-clockwise. + */ + public static boolean isClockwisePolygon(List boundary) { + + /** From https://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order + * sum all pairs (x2 − x1)(y2 + y1) + point[0] = (5,0) edge[0]: (6-5)(4+0) = 4 + point[1] = (6,4) edge[1]: (4-6)(5+4) = -18 + point[2] = (4,5) edge[2]: (1-4)(5+5) = -30 + point[3] = (1,5) edge[3]: (1-1)(0+5) = 0 + point[4] = (1,0) edge[4]: (5-1)(0+0) = 0 + --- + -44 counter-clockwise + */ + + double sum = 0; + + //For the first (size-1) adjacent pairs. + for (int i = 0; i < (boundary.size() - 1); i++) { + + //Get the points. + GPSCoordinate firstPoint = boundary.get(i); + GPSCoordinate secondPoint = boundary.get(i + 1); + + double xDelta = secondPoint.getLongitude() - firstPoint.getLongitude(); + double ySum = secondPoint.getLatitude() + firstPoint.getLatitude(); + + double product = xDelta * ySum; + + sum += product; + + } + + //For the final adjacent pair, between the last and first point. + //Get the points. + GPSCoordinate firstPoint = boundary.get(boundary.size() - 1); + GPSCoordinate secondPoint = boundary.get(0); + + double xDelta = secondPoint.getLongitude() - firstPoint.getLongitude(); + double ySum = secondPoint.getLatitude() + firstPoint.getLatitude(); + + double product = xDelta * ySum; + + sum += product; + + + //sum > 0 is clockwise, sum < 0 is anticlockwise. + return sum > 0; + + + } + } diff --git a/mock/src/main/java/seng302/Model/Polars.java b/mock/src/main/java/seng302/Model/Polars.java index 1e3251f3..cf7d448d 100644 --- a/mock/src/main/java/seng302/Model/Polars.java +++ b/mock/src/main/java/seng302/Model/Polars.java @@ -118,10 +118,7 @@ public class Polars { //If the lower bound is greater than the upper bound, we have a "flipped" interval. That is for, e.g., [70, 55] the lower bound is greater than the upper bound, and so it checks that (VMGAngle >= 70 OR VMGAngle =< 55), instead of (VMGAngle >= 70 AND VMGAngle =< 55). - boolean flippedInterval = false; - if (bearingLowerBound.degrees() > bearingUpperBound.degrees()) { - flippedInterval = true; - } + boolean flippedInterval = Polars.isFlippedInterval(bearingLowerBound, bearingUpperBound); @@ -179,7 +176,7 @@ public class Polars { Bearing bestVMGAngle = Bearing.fromDegrees(0d); //Calculate the VMG for all possible angles at this wind speed. - for (double angleDegree = 0; angleDegree < 360; angleDegree += 0.1) { + for (double angleDegree = 0; angleDegree < 360; angleDegree += 1) { Bearing angle = Bearing.fromDegrees(angleDegree); //This is the true bearing of the boat, if it went at the angle against the wind. @@ -191,22 +188,11 @@ public class Polars { //Check that the boat's bearing would actually be acceptable. //We continue (skip to next iteration) if it is outside of the interval. - if (flippedInterval) { - //Bearing must be inside [lower, upper], where lower > upper. So, bearing must be >= lower, or bearing < upper. We use inverted logic since we are skipping if it is true. - if ((trueBoatBearing.degrees() < bearingLowerBound.degrees()) & (trueBoatBearing.degrees() > bearingUpperBound.degrees())) { - continue; - } - - } else { - //Bearing must be inside [lower, upper]. - if ((trueBoatBearing.degrees() < bearingLowerBound.degrees()) || (trueBoatBearing.degrees() > bearingUpperBound.degrees())) { - continue; - } - + if (!Polars.isBearingInsideInterval(trueBoatBearing, bearingLowerBound, bearingUpperBound)) { + continue; } - //Basic linear interpolation. Find the nearest two angles from the table, and interpolate between them. //Check which pair of adjacent angles the angle is between. @@ -313,6 +299,58 @@ public class Polars { } + /** + * Determines whether an interval is "flipped". This means that the lower bound is greater than the upper bound (e.g., [290, 43] degrees). + * @param lowerBound The lower bound. + * @param upperBound The upper bound. + * @return True if the interval is flipped, false otherwise. + */ + public static boolean isFlippedInterval(Bearing lowerBound, Bearing upperBound) { + + //If the lower bound is greater than the upper bound, we have a "flipped" interval. + boolean flippedInterval = false; + if (lowerBound.degrees() > upperBound.degrees()) { + flippedInterval = true; + } + + return flippedInterval; + } + + + /** + * Determines if a bearing is inside an interval. + * @param bearing The bearing to check. + * @param lowerBound The lower bound of the interval. + * @param upperBound The upper bound of the interval. + * @return True if the bearing is inside the interval, false otherwise. + */ + public static boolean isBearingInsideInterval(Bearing bearing, Bearing lowerBound, Bearing upperBound) { + + //Check if it's a flipped interval. + boolean flippedInterval = Polars.isFlippedInterval(lowerBound, upperBound); + + if (flippedInterval) { + //Bearing must be inside [lower, upper], where lower > upper. So, bearing must be >= lower, or bearing < upper. We use inverted logic since we are skipping if it is true. + if ((bearing.degrees() >= lowerBound.degrees()) || (bearing.degrees() <= upperBound.degrees())) { + return true; + } else { + return false; + } + + } else { + //Bearing must be inside [lower, upper]. + if ((bearing.degrees() >= lowerBound.degrees()) && (bearing.degrees() <= upperBound.degrees())) { + return true; + } else { + return false; + } + + } + + + } + + /** * Calculate the linear interpolation scalar for a value between two bounds. E.g., lower = 7, upper = 10, value = 8, therefore the scalar (or the proportion between the bounds) is 0.333. * Also assumes that the bounds are periodic - e.g., for angles a lower bound of 350deg and upper bound of 5deg is in interval of 15 degrees. diff --git a/mock/src/main/java/seng302/Model/Race.java b/mock/src/main/java/seng302/Model/Race.java index 96473d2f..c81974db 100644 --- a/mock/src/main/java/seng302/Model/Race.java +++ b/mock/src/main/java/seng302/Model/Race.java @@ -19,6 +19,8 @@ import java.util.Iterator; import java.util.List; import java.util.Random; +import static java.lang.Math.cos; + /** * Represents a yacht race. @@ -47,6 +49,11 @@ public class Race implements Runnable { */ private List boundary; + /** + * A copy of the boundary list, except "shrunk" inwards by 50m. + */ + private List shrinkBoundary; + /** * The elapsed time, in milliseconds, of the race. */ @@ -62,7 +69,7 @@ public class Race implements Runnable { * Frame periods are multiplied by this to get the amount of time a single frame represents. * E.g., frame period = 20ms, scale = 5, frame represents 20 * 5 = 100ms, and so boats are simulated for 100ms, even though only 20ms actually occurred. */ - private int scaleFactor = 5; + private int scaleFactor = 15; /** * The race ID of the course. @@ -123,13 +130,16 @@ public class Race implements Runnable { this.boats = FXCollections.observableArrayList(raceData.getBoats()); this.compoundMarks = FXCollections.observableArrayList(raceData.getCompoundMarks()); this.boundary = raceData.getBoundary(); + this.shrinkBoundary = GPSCoordinate.getShrinkBoundary(this.boundary); + + this.legs = raceData.getLegs(); this.legs.add(new Leg("Finish", this.legs.size())); this.raceId = raceData.getRaceId(); //The start time is current time + 4 minutes, scaled. prestart is 3 minutes, and we add another. - this.startTime = System.currentTimeMillis() + ((Constants.RacePreStartTime + (1 * 1000)) / this.scaleFactor); + this.startTime = System.currentTimeMillis() + ((Constants.RacePreStartTime + (1 * 60 * 1000)) / this.scaleFactor); this.setRaceStatusEnum(RaceStatusEnum.NOT_ACTIVE); this.raceType = RaceTypeEnum.FLEET_RACE; @@ -177,7 +187,7 @@ public class Race implements Runnable { */ private void parseIndividualMark(Mark mark) { - this.mockOutput.parseBoatLocation(mark.getSourceID(), mark.getPosition().getLatitude(), mark.getPosition().getLongitude(),0,0); + this.mockOutput.parseBoatLocation(mark.getSourceID(), mark.getPosition().getLatitude(), mark.getPosition().getLongitude(),0,0, totalTimeElapsed+startTime); } @@ -206,7 +216,8 @@ public class Race implements Runnable { boat.getCurrentPosition().getLatitude(), boat.getCurrentPosition().getLongitude(), boat.getBearing().degrees(), - boat.getCurrentSpeed() + boat.getCurrentSpeed(), + startTime + totalTimeElapsed ); } @@ -257,7 +268,7 @@ public class Race implements Runnable { //Add each boat status to the status list. for (Boat boat : boats) { - BoatStatus boatStatus = new BoatStatus(boat.getSourceID(), boat.getStatus(), boat.getCurrentLeg().getLegNumber()); + BoatStatus boatStatus = new BoatStatus(boat.getSourceID(), boat.getStatus(), boat.getCurrentLeg().getLegNumber(), boat.getEstimatedTime()); boatStatuses.add(boatStatus); } @@ -380,21 +391,23 @@ public class Race implements Runnable { this.stop(); } - // Change wind direction - changeWindDir(); + if (getNumberOfActiveBoats() != 0) { + // Change wind direction + changeWindDir(); - //Parse the boat locations. - parseBoatLocations(); + //Parse the boat locations. + parseBoatLocations(); - //Parse the marks. - parseMarks(); + //Parse the marks. + parseMarks(); - //Parse the race status. - parseRaceStatus(); + //Parse the race status. + parseRaceStatus(); - //Update the last frame time. - this.lastFrameTime = currentTime; + //Update the last frame time. + this.lastFrameTime = currentTime; + } } }; @@ -405,7 +418,7 @@ public class Race implements Runnable { int iters = 0; @Override public void handle(long now) { - RaceStatus raceStatus = new RaceStatus(System.currentTimeMillis(), raceId, 4, startTime, 0, 2300, 2, null); + RaceStatus raceStatus = new RaceStatus(System.currentTimeMillis(), raceId, 4, startTime, 0, 2300, 2, new ArrayList<>()); mockOutput.parseRaceStatus(raceStatus); if (iters > 500){ mockOutput.stop(); @@ -514,67 +527,64 @@ public class Race implements Runnable { */ private VMG calculateVMG(Boat boat, Bearing[] bearingBounds) { - //How fast a boat can turn, in degrees per millisecond. - double turnRate = 0.03; - - //How much the boat is allowed to turn, considering how long since it last turned. - double turnAngle = turnRate * boat.getTimeSinceTackChange(); - //This ensures that the boat bounds don't flip around. - turnAngle = Math.min(turnAngle, 179.999); - - //Find the bounds on what angle the boat is allowed to travel at. The bounds cap out at [0, 360). - double bound1Degrees = boat.getBearing().degrees() - turnAngle; - double bound2Degrees = boat.getBearing().degrees() + turnAngle; - - Bearing bound1 = Bearing.fromDegrees(bound1Degrees); - Bearing bound2 = Bearing.fromDegrees(bound2Degrees); - - //Get the lower and upper acceptable bounds. Bearing lowerAcceptableBound = bearingBounds[0]; Bearing upperAcceptableBound = bearingBounds[1]; - //Find the bounds on what angle the boat can travel on, taking into account both turning rate and the acceptable bounds. - bound1Degrees = Math.max(bound1.degrees(), lowerAcceptableBound.degrees()); - bound2Degrees = Math.min(bound2.degrees(), upperAcceptableBound.degrees()); + //Find the VMG inside these bounds. + VMG bestVMG = boat.getPolars().calculateVMG(this.windDirection, this.windSpeed, boat.calculateBearingToNextMarker(), lowerAcceptableBound, upperAcceptableBound); - bound1 = Bearing.fromDegrees(bound1Degrees); - bound2 = Bearing.fromDegrees(bound2Degrees); - //Calculate VMG using these bounds. - return boat.getPolars().calculateVMG(this.windDirection, this.windSpeed, boat.calculateBearingToNextMarker(), bound1, bound2); + return bestVMG; } /** - * 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. + * 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(Boat boat, VMG vmg) { + private boolean improvesVelocity(VMG currentVMG, VMG potentialVMG, Bearing bearingToDestination) { //Calculates the angle between the boat and its destination. - Angle angleBetweenDestAndHeading = Angle.fromDegrees(boat.getBearing().degrees() - boat.calculateBearingToNextMarker().degrees()); + 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(vmg.getBearing().degrees() - boat.calculateBearingToNextMarker().degrees()); + Angle angleBetweenDestAndNewVMG = Angle.fromDegrees(potentialVMG.getBearing().degrees() - bearingToDestination.degrees()); //Calculate the boat's current velocity. - double currentVelocity = Math.cos(angleBetweenDestAndHeading.radians()) * boat.getCurrentSpeed(); + double currentVelocity = Math.cos(angleBetweenDestAndHeading.radians()) * currentVMG.getSpeed(); //Calculate the potential velocity with the new VMG. - double vmgVelocity = Math.cos(angleBetweenDestAndNewVMG.radians()) * vmg.getSpeed(); + 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(Boat 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. @@ -602,30 +612,33 @@ public class Race implements Runnable { boat.moveForwards(distanceTravelledMeters, updatePeriodMilliseconds * this.scaleFactor); - //Calculate the boat's bearing bounds, to ensure that it doesn't go out of the course. - Bearing[] bearingBounds = this.calculateBearingBounds(boat); + //Only get a new VMG if the boat will go outside the course, or X seconds have elapsed. + boolean willStayInsideCourse = this.checkBearingInsideCourse(boat.getBearing(), boat.getCurrentPosition()); + long tackPeriod = 15000; + if (!willStayInsideCourse || (boat.getTimeSinceTackChange() > tackPeriod)) { - //Calculate the new VMG. - VMG newVMG = this.calculateVMG(boat, bearingBounds); + //Calculate the boat's bearing bounds, to ensure that it doesn't go out of the course. + Bearing[] bearingBounds = this.calculateBearingBounds(boat); + //Calculate the new VMG. + VMG newVMG = this.calculateVMG(boat, bearingBounds); - Azimuth azimuth = Azimuth.fromBearing(boat.getBearing()); - GPSCoordinate testBounds = GPSCoordinate.calculateNewPosition(boat.getCurrentPosition(), - (100.0 / Constants.NMToMetersConversion), azimuth); - //If the new vmg improves velocity, use it. - if (improvesVelocity(boat, newVMG)) { - boat.setVMG(newVMG); - }else if (!GPSCoordinate.isInsideBoundary(testBounds, boundary)){ - //checks to see if the new vmg sends the boat out of bounds and if so mirrors its direction in the wind - double currDegrees = newVMG.getBearing().degrees(); - double windDirectionDegrees = this.windDirection.degrees(); - double tempHeading = (currDegrees - windDirectionDegrees +90)%360; - newVMG.getBearing().setDegrees(tempHeading); - boat.setVMG(newVMG); + //If the new vmg improves velocity, use it. + if (improvesVelocity(boat, newVMG)) { + boat.setVMG(newVMG); + + } else { + //We also need to use the new VMG if our current bearing will take us out of the course. + if (!willStayInsideCourse) { + boat.setVMG(newVMG); + } + } } + this.updateEstimatedTime(boat); + //Check the boats position (update leg and stuff). this.checkPosition(boat, totalTimeElapsed); @@ -643,12 +656,13 @@ public class Race implements Runnable { Bearing[] bearings = new Bearing[2]; - Bearing lowerBearing = Bearing.fromDegrees(0); + Bearing lowerBearing = Bearing.fromDegrees(0.001); Bearing upperBearing = Bearing.fromDegrees(359.999); - boolean foundAnyBadBearing = false; - boolean foundLowerBearing = false; + + double lastAngle = -1; + boolean lastAngleWasGood = false; //Check all bearings between [0, 360). for (double angle = 0; angle < 360; angle += 1) { @@ -659,29 +673,29 @@ public class Race implements Runnable { //Check that if it is acceptable. boolean bearingIsGood = this.checkBearingInsideCourse(bearing, boat.getCurrentPosition()); - if (bearingIsGood) { - //The lower bearing will be the first acceptable bearing after finding any unacceptable bearing. - if (foundAnyBadBearing && !foundLowerBearing) { - lowerBearing = bearing; - foundLowerBearing = true; - } + if (lastAngle != -1) { - //The upper bearing will be the last acceptable bearing before finding any unacceptable bearing. - if (!foundAnyBadBearing) { - upperBearing = bearing; + if (lastAngleWasGood && !bearingIsGood) { + //We have flipped over from good bearings to bad bearings. So the last good bearing is the upper bearing. + upperBearing = Bearing.fromDegrees(lastAngle); } - - } else { - - foundAnyBadBearing = true; + if (!lastAngleWasGood && bearingIsGood) { + //We have flipped over from bad bearings to good bearings. So the current bearing is the lower bearing. + lowerBearing = Bearing.fromDegrees(angle); + } } + lastAngle = angle; + lastAngleWasGood = bearingIsGood; + } + + //TODO BUG if it can't find either upper or lower, it returns (0, 359.999). Should return (boatbearing, boatbearing+0.0001) bearings[0] = lowerBearing; bearings[1] = upperBearing; @@ -694,6 +708,7 @@ public class Race implements Runnable { * Checks if a given bearing, starting at a given position, would put a boat out of the course boundaries. * @param bearing The bearing to check. * @param position The position to start from. + * @return True if the bearing would keep the boat in the course, false if it would take it out of the course. */ private boolean checkBearingInsideCourse(Bearing bearing, GPSCoordinate position) { @@ -702,11 +717,11 @@ public class Race implements Runnable { //Tests to see if a point in front of the boat is out of bounds. - double epsilonMeters = 100d; + double epsilonMeters = 50d; GPSCoordinate testCoord = GPSCoordinate.calculateNewPosition(position, epsilonMeters, azimuth); //If it isn't inside the boundary, calculate new bearing. - if (GPSCoordinate.isInsideBoundary(testCoord, this.boundary)) { + if (GPSCoordinate.isInsideBoundary(testCoord, this.shrinkBoundary)) { return true; } else { return false; @@ -752,7 +767,6 @@ public class Race implements Runnable { boat.setTimeFinished(timeElapsed); boat.setCurrentSpeed(0); boat.setStatus(BoatStatusEnum.FINISHED); - } else if (doNotFinish()) { //Boat has pulled out of race. boat.setTimeFinished(timeElapsed); @@ -889,8 +903,6 @@ public class Race implements Runnable { } windDir = AC35UnitConverter.convertHeading(windDirDegrees); - - System.out.println(windDirDegrees + " + " + windDir); this.windDirection = new Bearing(windDirDegrees); } @@ -903,4 +915,16 @@ public class Race implements Runnable { protected int getWind(){ return (int)windDir; } -} \ No newline at end of file + + /** + * Updates the boat's estimated time to next mark if positive + * @param boat to estimate time given its velocity + */ + private void updateEstimatedTime(Boat boat) { + double velocityToMark = boat.getCurrentSpeed() * cos(boat.getBearing().radians() - boat.calculateBearingToNextMarker().radians()) / Constants.KnotsToMMPerSecond; + if (velocityToMark > 0) { + long timeFromNow = (long)(1000*boat.calculateDistanceToNextMarker()/velocityToMark); + boat.setEstimatedTime(startTime + totalTimeElapsed + timeFromNow); + } + } +} diff --git a/mock/src/test/java/seng302/Model/RaceTest.java b/mock/src/test/java/seng302/Model/RaceTest.java index 7365e320..f1bf440f 100644 --- a/mock/src/test/java/seng302/Model/RaceTest.java +++ b/mock/src/test/java/seng302/Model/RaceTest.java @@ -57,7 +57,7 @@ public class RaceTest{ Race testRace = new Race(raceDataSource, mockOutput); testRace.initialiseBoats(); testRace.countdownTimer.handle(1); - verify(mockOutput, atLeast(boatDataSource.getBoats().size())).parseBoatLocation(anyInt(), anyDouble(), anyDouble(), anyDouble(), anyDouble()); + verify(mockOutput, atLeast(boatDataSource.getBoats().size())).parseBoatLocation(anyInt(), anyDouble(), anyDouble(), anyDouble(), anyDouble(), anyLong()); } catch (ParserConfigurationException | IOException | SAXException | ParseException | StreamedCourseXMLException e) { e.printStackTrace(); diff --git a/network/src/main/java/seng302/Networking/Messages/BoatLocation.java b/network/src/main/java/seng302/Networking/Messages/BoatLocation.java index 9cb0bc78..9ebde2ef 100644 --- a/network/src/main/java/seng302/Networking/Messages/BoatLocation.java +++ b/network/src/main/java/seng302/Networking/Messages/BoatLocation.java @@ -152,11 +152,11 @@ public class BoatLocation extends AC35Data { this.rudderAngle = rudderAngle; } - public BoatLocation(int sourceID, double lat, double lon, long sequenceNumber, double heading, double boatSpeed) { + public BoatLocation(int sourceID, double lat, double lon, long sequenceNumber, double heading, double boatSpeed, long time) { super(MessageType.BOATLOCATION); this.messageVersionNumber = (byte) 1; - this.time = System.currentTimeMillis(); + this.time = time; this.sourceID = sourceID; this.sequenceNumber = sequenceNumber; this.deviceType = 1; @@ -227,7 +227,7 @@ public class BoatLocation extends AC35Data { */ public static int convertHeadingDoubleToInt(double heading) { - int headingInt = (int) ((heading / 360.0) * 65536.0); + int headingInt = (int) ((heading * 65536.0) / 360.0); return headingInt; } diff --git a/network/src/main/java/seng302/Networking/Messages/BoatStatus.java b/network/src/main/java/seng302/Networking/Messages/BoatStatus.java index dacb5d5e..291bc50b 100644 --- a/network/src/main/java/seng302/Networking/Messages/BoatStatus.java +++ b/network/src/main/java/seng302/Networking/Messages/BoatStatus.java @@ -27,14 +27,14 @@ public class BoatStatus { } - public BoatStatus(int sourceID, BoatStatusEnum boatStatusEnum, int legNum) { + public BoatStatus(int sourceID, BoatStatusEnum boatStatusEnum, int legNum, long estTimeAtNextMark) { this.sourceID = sourceID; this.boatStatus = boatStatusEnum.getValue(); this.legNumber = ByteConverter.intToBytes(legNum)[0]; this.numPenaltiesAwarded = 0; this.numPenaltiesServed = 0; this.estTimeAtFinish = 0; - this.estTimeAtNextMark = 0; + this.estTimeAtNextMark = estTimeAtNextMark; } diff --git a/visualiser/src/main/java/seng302/Controllers/RaceController.java b/visualiser/src/main/java/seng302/Controllers/RaceController.java index 0c126146..d36964a1 100644 --- a/visualiser/src/main/java/seng302/Controllers/RaceController.java +++ b/visualiser/src/main/java/seng302/Controllers/RaceController.java @@ -39,6 +39,7 @@ public class RaceController extends Controller { private static String speedCheckAnno = "speed"; private static String pathCheckAnno = "path"; private static String timeCheckAnno = "time"; + private static String estTimeCheckAnno = "est time"; private static int noBtn = 0; private static int hideBtn = 1; @@ -67,6 +68,7 @@ public class RaceController extends Controller { @FXML CheckBox showAbbrev; @FXML CheckBox showSpeed; @FXML CheckBox showTime; + @FXML CheckBox showEstTime; @FXML Label timer; @FXML Label FPS; @FXML Label timeZone; @@ -356,7 +358,6 @@ public class RaceController extends Controller { if (buttonChecked != hideBtn) { //if we are checking the box straight out of hide instead of using the radio buttons annoShownBeforeHide.put(dictionaryAnnotationKey, selected); - System.out.println(); if (prevBtnChecked == hideBtn && buttonChecked == noBtn){ storeCurrentAnnotationDictionary(); } @@ -373,6 +374,7 @@ public class RaceController extends Controller { annoShownBeforeHide.put(pathCheckAnno, showBoatPath.isSelected()); annoShownBeforeHide.put(speedCheckAnno, showSpeed.isSelected()); annoShownBeforeHide.put(timeCheckAnno, showTime.isSelected()); + annoShownBeforeHide.put(estTimeCheckAnno, showEstTime.isSelected()); } /** @@ -386,7 +388,8 @@ public class RaceController extends Controller { importantAnno.put(abbrevCheckAnno, false); importantAnno.put(pathCheckAnno, false); importantAnno.put(speedCheckAnno, false); - importantAnno.put(timeCheckAnno, true); + importantAnno.put(timeCheckAnno, false); + importantAnno.put(estTimeCheckAnno, false); annoShownBeforeHide = new HashMap<>(); annoShownBeforeHide.put(nameCheckAnno, true); @@ -394,6 +397,7 @@ public class RaceController extends Controller { annoShownBeforeHide.put(pathCheckAnno, true); annoShownBeforeHide.put(speedCheckAnno, true); annoShownBeforeHide.put(timeCheckAnno, true); + annoShownBeforeHide.put(estTimeCheckAnno, true); //listener for show name in annotation showName.selectedProperty().addListener((ov, old_val, new_val) -> { if (old_val != new_val) { @@ -439,6 +443,14 @@ public class RaceController extends Controller { raceMap.update(); } }); + showEstTime.selectedProperty().addListener((ov, old_val, new_val) -> { + if (old_val != new_val) { + raceMap.toggleAnnoEstTime(); + radioBtnChecked = false; + storeCurrentAnnotationState(estTimeCheckAnno, new_val); + raceMap.update(); + } + }); //listener to save currently selected annotation saveAnno.setOnAction(event -> { presetAnno.clear(); @@ -447,9 +459,10 @@ public class RaceController extends Controller { presetAnno.add(showSpeed.isSelected()); presetAnno.add(showBoatPath.isSelected()); presetAnno.add(showTime.isSelected()); + presetAnno.add(showEstTime.isSelected()); }); + //listener for hiding hideAnnoRBTN.setOnAction((e)->{ - System.out.println("called hide"); buttonChecked = hideBtn; selectShow = false; showName.setSelected(false); @@ -457,39 +470,14 @@ public class RaceController extends Controller { showBoatPath.setSelected(false); showSpeed.setSelected(false); showTime.setSelected(false); + showEstTime.setSelected(false); raceMap.update(); buttonChecked = noBtn; prevBtnChecked = hideBtn; selectShow = true; }); - //listener for hiding - /*hideAnnoRBTN.selectedProperty().addListener((ov, old_val, new_val) ->{ - buttonChecked = hideBtn; - showName.setSelected(false); - showAbbrev.setSelected(false); - showBoatPath.setSelected(false); - showSpeed.setSelected(false); - showTime.setSelected(false); - raceMap.update(); - buttonChecked = noBtn; - prevBtnChecked = hideBtn; - });*/ //listener for showing all annotations showAnnoRBTN.setOnAction((e)->{ - if (selectShow) { - System.out.println("called show"); - buttonChecked = showBtn; - showName.setSelected(annoShownBeforeHide.get(nameCheckAnno)); - showAbbrev.setSelected(annoShownBeforeHide.get(abbrevCheckAnno)); - showBoatPath.setSelected(annoShownBeforeHide.get(pathCheckAnno)); - showSpeed.setSelected(annoShownBeforeHide.get(speedCheckAnno)); - showTime.setSelected(annoShownBeforeHide.get(timeCheckAnno)); - raceMap.update(); - buttonChecked = noBtn; - prevBtnChecked = showBtn; - } - selectShow = true; - });/*.addListener((ov, old_val, new_val) ->{ if (selectShow) { buttonChecked = showBtn; showName.setSelected(annoShownBeforeHide.get(nameCheckAnno)); @@ -497,15 +485,15 @@ public class RaceController extends Controller { showBoatPath.setSelected(annoShownBeforeHide.get(pathCheckAnno)); showSpeed.setSelected(annoShownBeforeHide.get(speedCheckAnno)); showTime.setSelected(annoShownBeforeHide.get(timeCheckAnno)); + showEstTime.setSelected(annoShownBeforeHide.get(estTimeCheckAnno)); raceMap.update(); buttonChecked = noBtn; prevBtnChecked = showBtn; } selectShow = true; - });*/ + }); //listener for showing all important partialAnnoRBTN.setOnAction((e)->{ - System.out.println("called Partial"); selectShow = false; buttonChecked = partialBtn; showName.setSelected(false); @@ -513,6 +501,7 @@ public class RaceController extends Controller { showSpeed.setSelected(true); showBoatPath.setSelected(false); showTime.setSelected(false); + showEstTime.setSelected(false); raceMap.update(); buttonChecked = noBtn; @@ -521,7 +510,6 @@ public class RaceController extends Controller { }); //listener for showing all important importantAnnoRBTN.setOnAction((e) ->{ - System.out.println("called Important"); selectShow = false; buttonChecked = importantBtn; if (presetAnno.size() > 0) { @@ -530,6 +518,7 @@ public class RaceController extends Controller { showSpeed.setSelected(presetAnno.get(2)); showBoatPath.setSelected(presetAnno.get(3)); showTime.setSelected(presetAnno.get(4)); + showEstTime.setSelected(presetAnno.get(5)); storeCurrentAnnotationDictionary(); raceMap.update(); } diff --git a/visualiser/src/main/java/seng302/Controllers/StartController.java b/visualiser/src/main/java/seng302/Controllers/StartController.java index 87996f76..db8091ff 100644 --- a/visualiser/src/main/java/seng302/Controllers/StartController.java +++ b/visualiser/src/main/java/seng302/Controllers/StartController.java @@ -98,7 +98,6 @@ public class StartController extends Controller implements Observer { raceStat = visualiserInput.getRaceStatus().getRaceStatus(); raceStatusLabel.setText("Race Status: " + visualiserInput.getRaceStatus().getRaceStatus()); if (raceStat==2 || raceStat == 3) { - System.out.println("countdown finished!");//TEMP DEBUG REMOVE stop(); startWrapper.setVisible(false); diff --git a/visualiser/src/main/java/seng302/Mock/StreamedRace.java b/visualiser/src/main/java/seng302/Mock/StreamedRace.java index 0b867f4d..71b9c68a 100644 --- a/visualiser/src/main/java/seng302/Mock/StreamedRace.java +++ b/visualiser/src/main/java/seng302/Mock/StreamedRace.java @@ -114,16 +114,17 @@ public class StreamedRace implements Runnable { * Updates the boat's gps coordinates * * @param boat to be updated - * @param millisecondsElapsed time since last update */ - private void updatePosition(Boat boat, int millisecondsElapsed) { + private void updatePosition(Boat boat) { int sourceID = boat.getSourceID(); BoatLocation boatLocation = visualiserInput.getBoatLocationMessage(sourceID); + BoatStatus boatStatus = visualiserInput.getBoatStatusMessage(sourceID); if(boatLocation != null) { double lat = boatLocation.getLatitudeDouble(); double lon = boatLocation.getLongitudeDouble(); boat.setCurrentPosition(new GPSCoordinate(lat, lon)); boat.setHeading(boatLocation.getHeadingDegrees()); + boat.setEstTime(convertEstTime(boatStatus.getEstTimeAtNextMark(), boatLocation.getTime())); double MMPS_TO_KN = 0.001944; boat.setVelocity(boatLocation.getBoatSOG() * MMPS_TO_KN); } @@ -198,19 +199,20 @@ public class StreamedRace implements Runnable { public void handle(long arg0) { totalTimeElapsed = System.currentTimeMillis() - timeRaceStarted; - for (Boat boat : startingBoats) { - if (boat != null && !boat.isFinished()) { - updatePosition(boat, Math.round(1000 / lastFPS) > 20 ? 15 : Math.round(1000 / lastFPS)); - checkPosition(boat, totalTimeElapsed); + for (Boat boat : startingBoats) { + if (boat != null && !boat.isFinished()) { + updatePosition(boat); + checkPosition(boat, totalTimeElapsed); + } + } - } - for (Marker mark: boatMarkers){ - if (mark != null){ - updateMarker(mark); + for (Marker mark: boatMarkers){ + if (mark != null){ + updateMarker(mark); + } } - } - //System.out.println(boatsFinished + ":" + startingBoats.size()); - if (visualiserInput.getRaceStatus().isFinished()){ + + if (visualiserInput.getRaceStatus().isFinished()) { controller.finishRace(startingBoats); stop(); } @@ -258,4 +260,18 @@ public class StreamedRace implements Runnable { return startingBoats; } + /** + * Takes an estimated time an event will occur, and converts it to the number of seconds before the event will occur. + * + * @param estTimeMillis + * @return int difference between time the race started and the estimated time + */ + private int convertEstTime(long estTimeMillis, long currentTime) { + + long estElapsedMillis = estTimeMillis - currentTime; + int estElapsedSecs = Math.round(estElapsedMillis/1000); + return estElapsedSecs; + + } + } diff --git a/visualiser/src/main/java/seng302/Model/Boat.java b/visualiser/src/main/java/seng302/Model/Boat.java index a99ed148..c9765107 100644 --- a/visualiser/src/main/java/seng302/Model/Boat.java +++ b/visualiser/src/main/java/seng302/Model/Boat.java @@ -29,6 +29,7 @@ public class Boat { private boolean started = false; private boolean dnf = false; private int sourceID; + private int estTime; private final Queue track = new ConcurrentLinkedQueue<>(); private long nextValidTime = 0; @@ -248,4 +249,21 @@ public class Boat { public void setTimeSinceLastMark(ZonedDateTime timeSinceLastMark) { this.timeSinceLastMark = timeSinceLastMark; } + + public void setEstTime(int estTime) { this.estTime = estTime; } + + public String getFormattedEstTime() { + if (estTime < 0) { + return " -"; + } + if (estTime <= 60) { + return " " + estTime + "s"; + } else { + int seconds = estTime % 60; + int minutes = (estTime - seconds) / 60; + return String.format(" %dm %ds", minutes, seconds); + } + + + } } diff --git a/visualiser/src/main/java/seng302/Model/ResizableRaceCanvas.java b/visualiser/src/main/java/seng302/Model/ResizableRaceCanvas.java index 5c26b293..9dfab1a9 100644 --- a/visualiser/src/main/java/seng302/Model/ResizableRaceCanvas.java +++ b/visualiser/src/main/java/seng302/Model/ResizableRaceCanvas.java @@ -31,6 +31,7 @@ public class ResizableRaceCanvas extends ResizableCanvas { private boolean annoAbbrev = true; private boolean annoSpeed = true; private boolean annoPath = true; + private boolean annoEstTime = true; private boolean annoTimeSinceLastMark = true; private List colours; private final List markers; @@ -177,7 +178,7 @@ public class ResizableRaceCanvas extends ResizableCanvas { * @param coordinate coordinate the text appears * @param timeSinceLastMark time since the last mark was passed */ - private void displayText(String name, String abbrev, double speed, GraphCoordinate coordinate, ZonedDateTime timeSinceLastMark) { + private void displayText(String name, String abbrev, double speed, GraphCoordinate coordinate, String estTime, ZonedDateTime timeSinceLastMark) { String text = ""; //Check name toggle value if (annoName){ @@ -191,10 +192,13 @@ public class ResizableRaceCanvas extends ResizableCanvas { if (annoSpeed){ text += String.format("%.2fkn ", speed); } + if (annoEstTime) { + text += estTime; + } //Check time since last mark toggle value if(annoTimeSinceLastMark){ Duration timeSince = Duration.between(timeSinceLastMark, raceClock.getTime()); - text += String.format("%d", timeSince.getSeconds()); + text += String.format(" %ds ", timeSince.getSeconds()); } //String text = String.format("%s, %2$.2fkn", name, speed); long xCoord = coordinate.getX() + 20; @@ -277,6 +281,10 @@ public class ResizableRaceCanvas extends ResizableCanvas { annoPath = !annoPath; } + public void toggleAnnoEstTime() { + annoEstTime = !annoEstTime; + } + /** * Toggle boat time display in annotation */ @@ -320,7 +328,7 @@ public class ResizableRaceCanvas extends ResizableCanvas { if (Duration.between(boat.getTimeSinceLastMark(), raceClock.getTime()).getSeconds() < 0) { boat.setTimeSinceLastMark(raceClock.getTime()); } - displayText(boat.toString(), boat.getAbbrev(), boat.getVelocity(), this.map.convertGPS(boat.getCurrentPosition()), boat.getTimeSinceLastMark()); + displayText(boat.toString(), boat.getAbbrev(), boat.getVelocity(), this.map.convertGPS(boat.getCurrentPosition()), boat.getFormattedEstTime(), boat.getTimeSinceLastMark()); //TODO this needs to be fixed. drawTrack(boat, boatColours.get(sourceID)); } diff --git a/visualiser/src/main/java/seng302/VisualiserInput.java b/visualiser/src/main/java/seng302/VisualiserInput.java index 4c3df9d9..1c552904 100644 --- a/visualiser/src/main/java/seng302/VisualiserInput.java +++ b/visualiser/src/main/java/seng302/VisualiserInput.java @@ -88,6 +88,10 @@ public class VisualiserInput implements Runnable { return boatLocationMap.get(sourceID); } + public BoatStatus getBoatStatusMessage(int sourceID) { + return boatStatusMap.get(sourceID); + } + /** * Calculates the time since last heartbeat, in milliseconds. * @return Time since last heartbeat, in milliseconds.. diff --git a/visualiser/src/main/resources/scenes/race.fxml b/visualiser/src/main/resources/scenes/race.fxml index 900b731d..5f11b477 100644 --- a/visualiser/src/main/resources/scenes/race.fxml +++ b/visualiser/src/main/resources/scenes/race.fxml @@ -1,12 +1,24 @@ - - - - - - + + + + + + + + + + + + + + + + + + @@ -24,7 +36,7 @@ - + @@ -33,13 +45,14 @@ - -