diff --git a/src/main/java/seng302/App.java b/src/main/java/seng302/App.java index 3625db47..afcd3430 100644 --- a/src/main/java/seng302/App.java +++ b/src/main/java/seng302/App.java @@ -62,7 +62,7 @@ public class App extends Application { } //Change the StreamReceiver in this else block to change the default data source. else{ -// sr = new StreamReceiver("localhost", 4949, "RaceStream"); + //sr = new StreamReceiver("localhost", 4949, "RaceStream"); sr = new StreamReceiver("livedata.americascup.com", 4941, "RaceStream"); } diff --git a/src/main/java/seng302/models/BoatGroup.java b/src/main/java/seng302/models/BoatGroup.java index bea2f04b..767ca893 100644 --- a/src/main/java/seng302/models/BoatGroup.java +++ b/src/main/java/seng302/models/BoatGroup.java @@ -1,6 +1,7 @@ package seng302.models; import javafx.geometry.Point2D; +import javafx.scene.CacheHint; import javafx.scene.Group; import javafx.scene.paint.Color; import javafx.scene.shape.Line; @@ -99,8 +100,13 @@ public class BoatGroup extends RaceObject { boatPoly.setOnMouseEntered(event -> boatPoly.setFill(Color.FLORALWHITE)); boatPoly.setOnMouseExited(event -> boatPoly.setFill(color)); boatPoly.setOnMouseClicked(event -> setIsSelected(!isSelected)); + boatPoly.setCache(true); + boatPoly.setCacheHint(CacheHint.SPEED); + teamNameObject = new Text(boat.getShortName()); + teamNameObject.setCache(true); + teamNameObject.setCacheHint(CacheHint.SPEED); velocityObject = new Text(String.valueOf(boat.getVelocity())); DateFormat format = new SimpleDateFormat("mm:ss"); String timeToNextMark = format @@ -113,6 +119,8 @@ public class BoatGroup extends RaceObject { } else { legTimeObject = new Text("Last mark: -"); } + velocityObject.setCache(true); + velocityObject.setCacheHint(CacheHint.SPEED); teamNameObject.setX(TEAMNAME_X_OFFSET); teamNameObject.setY(TEAMNAME_Y_OFFSET); @@ -214,33 +222,30 @@ public class BoatGroup extends RaceObject { * on. */ public void updatePosition(long timeInterval) { - //Calculate the movement of the boat. - if (isMaximized) { - double dx = pixelVelocityX * timeInterval; - double dy = pixelVelocityY * timeInterval; - double rotation = rotationalVelocity * timeInterval; - distanceTravelled += Math.abs(dx) + Math.abs(dy); - moveGroupBy(dx, dy, rotation); - //Draw a new section of the trail every 20 pixels of movement. - if (distanceTravelled > 20) { - distanceTravelled = 0; - if (lastPoint != null) { - Line l = new Line( - lastPoint.getX(), - lastPoint.getY(), - boatPoly.getLayoutX(), - boatPoly.getLayoutY() - ); - l.getStrokeDashArray().setAll(3d, 7d); - l.setStroke(boat.getColour()); - lineGroup.getChildren().add(l); - } - if (destinationSet) { //Only begin drawing after the first destination is set - lastPoint = new Point2D(boatPoly.getLayoutX(), boatPoly.getLayoutY()); - } + double dx = pixelVelocityX * timeInterval; + double dy = pixelVelocityY * timeInterval; + double rotation = rotationalVelocity * timeInterval; + distanceTravelled += Math.abs(dx) + Math.abs(dy); + moveGroupBy(dx, dy, rotation); + //Draw a new section of the trail every 20 pixels of movement. + if (distanceTravelled > 20) { + distanceTravelled = 0; + if (lastPoint != null) { + Line l = new Line( + lastPoint.getX(), + lastPoint.getY(), + boatPoly.getLayoutX(), + boatPoly.getLayoutY() + ); + l.getStrokeDashArray().setAll(3d, 7d); + l.setStroke(boat.getColour()); + lineGroup.getChildren().add(l); + } + if (destinationSet) { //Only begin drawing after the first destination is set + lastPoint = new Point2D(boatPoly.getLayoutX(), boatPoly.getLayoutY()); } - wake.updatePosition(timeInterval); } + wake.updatePosition(timeInterval); } /** @@ -257,32 +262,21 @@ public class BoatGroup extends RaceObject { if (setToInitialLocation) { destinationSet = true; boat.setVelocity(groundSpeed); - if (currentRotation < 0) { - currentRotation = 360 - currentRotation; - } double dx = newXValue - boatPoly.getLayoutX(); double dy = newYValue - boatPoly.getLayoutY(); - //Check movement is reasonable. Assumes a 1000 * 1000 canvas - if (Math.abs(dx) > 50 || Math.abs(dy) > 50) { - dx = 0; - dy = 0; - moveTo(newXValue, newYValue); - } + pixelVelocityX = dx / expectedUpdateInterval; pixelVelocityY = dy / expectedUpdateInterval; rotationalGoal = rotation; calculateRotationalVelocity(); - if (wakeGenerationDelay > 0) { - wake.rotate(rotationalGoal); - rotateTo(rotationalGoal); //Need to test with this removed. + if (Math.abs(rotationalVelocity) > 0.075) { rotationalVelocity = 0; - wakeGenerationDelay--; - } else { - wake.setRotationalVelocity(rotationalVelocity, rotationalGoal, - boat.getVelocity()); + rotateTo(rotationalGoal); + wake.rotate(rotationalGoal); } + wake.setRotationalVelocity(rotationalVelocity, boat.getVelocity()); velocityObject.setText(String.format("%.2f m/s", boat.getVelocity())); DateFormat format = new SimpleDateFormat("mm:ss"); // estimate time to next mark @@ -304,27 +298,6 @@ public class BoatGroup extends RaceObject { } } //If minimized generate lines every 5 calls to set destination. - if (!isMaximized) { - setToInitialLocation = false; - wakeGenerationDelay = 2; - if (setCallCount-- == 0) { - setCallCount = 5; - if (lastPoint != null) { - Line l = new Line( - lastPoint.getX(), - lastPoint.getY(), - newXValue, - newYValue - ); - l.getStrokeDashArray().setAll(3d, 7d); - l.setStroke(boatPoly.getFill()); - lineStorage.add(l); - } - if (destinationSet) { //Only begin drawing after the first destination is set - lastPoint = new Point2D(newXValue, newYValue); - } - } - } } public void setDestination(double newXValue, double newYValue, double groundSpeed, diff --git a/src/main/java/seng302/models/Wake.java b/src/main/java/seng302/models/Wake.java index 55d4381c..630b075a 100644 --- a/src/main/java/seng302/models/Wake.java +++ b/src/main/java/seng302/models/Wake.java @@ -1,30 +1,34 @@ package seng302.models; +import javafx.scene.CacheHint; import javafx.scene.Group; import javafx.scene.paint.Color; import javafx.scene.shape.Arc; import javafx.scene.shape.ArcType; +import javafx.scene.shape.StrokeLineCap; import javafx.scene.transform.Rotate; /** - * By default wake is a group containing 5 arcs. Each arc starts from the same point. Each arc is larger and more - * transparent than the last. On calling updatePositions() arcs rotate at velocities given by setRotationalVelocity(). - * The larger and more transparent an arc is the longer the delay before it rotates at the latest velocity. It is - * assumed that rotationalVelocities() are set regularly as wakes do not stop rotating and an array of velocities needs - * to be populated for the class to work as expected. + * A group containing objects used to represent wakes onscreen. Contains functionality for their animation. */ class Wake extends Group { - private int numWakes = 5; - private double[] velocities = new double[13]; + //The number of wakes + private int numWakes = 8; + //The total possible difference between the first wake and the last. Increasing/Decreasing this will make wakes fan out more/less. + private final double MAX_DIFF = 75; + //Increasing/decreasing this will alter the speed that wakes converge when the heading stop changing. Anything over about 1500 may cause oscillation. + private final int UNIFICATION_SPEED = 750; + + private Arc[] arcs = new Arc[numWakes]; + private double[] rotationalVelocities = new double[numWakes]; private double[] rotations = new double[numWakes]; - private int[] velocityIndices = new int[numWakes]; - private double sum = 0; - private static double max; + private double baseRad; /** * Create a wake at the given location. + * * @param startingX x location where the tip of wake arcs will be. * @param startingY y location where the tip of wake arcs will be. */ @@ -34,74 +38,77 @@ class Wake extends Group { Arc arc; for (int i = 0; i < numWakes; i++) { //Default triangle is -110 deg out of phase with a default wake and has angle of 40 deg. - arc = new Arc(0,0,0,0,-110,40); - //Opacity increases from 0.5 -> 0 evenly over the 5 wake arcs. - arc.setFill(new Color(0.18, 0.7, 1.0, 1.0 + -0.175 * i)); - arc.setType(ArcType.ROUND); + arc = new Arc(0, 0, 0, 0, -110, 40); + arc.setCache(true); + arc.setCacheHint(CacheHint.SPEED); + arc.setType(ArcType.OPEN); + arc.setStroke(new Color(0.18, 0.7, 1.0, 1.0 + (-0.99 / numWakes * i))); + arc.setStrokeWidth(3.0); + arc.setStrokeLineCap(StrokeLineCap.ROUND); + arc.setFill(new Color(0.0, 0.0, 0.0, 0.0)); + baseRad = (20 / numWakes); arcs[i] = arc; } super.getChildren().addAll(arcs); } /** - * Sets the rotationalVelocity of each arc. Each arc is 3 velocities behind the next smallest arc. The smallest uses - * the latest given velocity. + * Sets the rotationalVelocity of each arc. + * * @param rotationalVelocity The rotationalVelocity the wake should move at. - * @param rotationGoal Where the wake will rotate to if the wake is calculated to be on a straight section. This is - * used to prevent desynchronisation with the Boat polygon. - * @param velocity The real world velocity of the boat in m/s. + * @param velocity The real world velocity of the boat in m/s. */ - void setRotationalVelocity (double rotationalVelocity, double rotationGoal, double velocity) { - sum -= Math.abs(velocities[(velocityIndices[0] + 10) % 13]); - sum += Math.abs(rotationalVelocity); - max = Math.max(max, rotationalVelocity); - if (sum < (max / 3)) - rotate (rotationGoal); //In relatively straight segments the wake snaps to match the boats current position. - //This stops the wake from eventually becoming out of sync with the boat. - //This accounts for rogue rotations that are greater than what would be realistic. Value is kinda rough. - //Basically just for our internal mock. - if (Math.abs(rotationalVelocity) > 0.05) { - rotationalVelocity = 0; - rotate(rotationGoal); - } - //Update the index of the array of recent velocities that each wake uses. Each wake is 3 velocities behind the - //next smallest wake. - velocityIndices[0] = (13 + (velocityIndices[0] - 1) % 13) % 13; - velocities[velocityIndices[0]] = rotationalVelocity; - for (int i = 1; i < numWakes; i++) - velocityIndices[i] = (velocityIndices[0] + 3 * i) % 13; + void setRotationalVelocity(double rotationalVelocity, double velocity) { + rotationalVelocities[0] = rotationalVelocity; + for (int i = 1; i < numWakes; i++) { + double wakeSeparationRad = Math.toRadians(rotations[i - 1] - rotations[i]); + double shortestDistance = Math.atan2( + Math.sin(wakeSeparationRad), + Math.cos(wakeSeparationRad) + ); + double distDeg = Math.toDegrees(shortestDistance); - //Scale wakes based on velocity. - double baseRad = 20; - double rad; - for (Arc arc :arcs) { - rad = baseRad + velocity; + if (rotationalVelocities[i - 1] < 0.01 && rotationalVelocities[i - 1] > -0.01) { + rotationalVelocities[i] = distDeg / UNIFICATION_SPEED * Math.log(Math.abs(distDeg) + 1) / Math.log(MAX_DIFF / numWakes); + + } else { + if (distDeg < (MAX_DIFF / numWakes)) + rotationalVelocities[i] = rotationalVelocities[i - 1] * Math.log(Math.abs(distDeg) + 1) / Math.log(MAX_DIFF / numWakes); + else + rotationalVelocities[i] = rotationalVelocities[i - 1]; + } + } + + double rad = baseRad + velocity; + for (Arc arc : arcs) { arc.setRadiusX(rad); arc.setRadiusY(rad); - baseRad += 5 + (velocity / 2); + rad += (20 / numWakes) + (velocity / 2); } } /** * Arcs rotate based on the distance they would have travelled over the supplied time interval. + * * @param timeInterval the time interval, in microseconds, that the wake should move. */ - void updatePosition (long timeInterval) { + void updatePosition(long timeInterval) { for (int i = 0; i < numWakes; i++) { - rotations[i] = rotations[i] + velocities[velocityIndices[i]] * timeInterval; + rotations[i] = rotations[i] + rotationalVelocities[i] * timeInterval; arcs[i].getTransforms().setAll(new Rotate(rotations[i])); } } /** * Rotate all wakes to the given rotation. + * * @param rotation the from north angle in degrees to rotate to. */ - void rotate (double rotation) { + void rotate(double rotation) { for (int i = 0; i < arcs.length; i++) { rotations[i] = rotation; + rotationalVelocities[i] = 0; arcs[i].getTransforms().setAll(new Rotate(rotation)); } } - } diff --git a/src/main/java/seng302/models/parsers/StreamParser.java b/src/main/java/seng302/models/parsers/StreamParser.java index ef35a7ff..837f459f 100644 --- a/src/main/java/seng302/models/parsers/StreamParser.java +++ b/src/main/java/seng302/models/parsers/StreamParser.java @@ -17,6 +17,7 @@ import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.PriorityBlockingQueue; /** @@ -36,8 +37,8 @@ public class StreamParser extends Thread{ private static boolean raceFinished = false; private static boolean streamStatus = false; private static long timeSinceStart = -1; - private static Map boats = new HashMap<>(); - private static Map boatsPos = new TreeMap<>(); + private static Map boats = new ConcurrentHashMap<>(); + private static Map boatsPos = new ConcurrentSkipListMap<>(); private static double windDirection = 0; private static Long currentTimeLong; private static String currentTimeString; diff --git a/src/main/java/seng302/server/messages/BoatLocationMessage.java b/src/main/java/seng302/server/messages/BoatLocationMessage.java index 6cf0739d..5e605170 100644 --- a/src/main/java/seng302/server/messages/BoatLocationMessage.java +++ b/src/main/java/seng302/server/messages/BoatLocationMessage.java @@ -1,10 +1,7 @@ package seng302.server.messages; -import java.io.DataOutputStream; import java.io.IOException; -import java.nio.channels.Channels; import java.nio.channels.SocketChannel; -import java.nio.channels.WritableByteChannel; public class BoatLocationMessage extends Message { private final int MESSAGE_SIZE = 56; @@ -44,7 +41,7 @@ public class BoatLocationMessage extends Message { public BoatLocationMessage(int sourceId, int sequenceNum, double latitude, double longitude, double heading, long boatSpeed){ boatSpeed /= 10; messageVersionNumber = 1; - time = System.currentTimeMillis() / 1000L; + time = System.currentTimeMillis(); this.sourceId = sourceId; this.sequenceNum = sequenceNum; this.deviceType = DeviceType.RACING_YACHT;