diff --git a/racevisionGame/src/main/java/visualiser/Controllers/ArrowController.java b/racevisionGame/src/main/java/visualiser/Controllers/ArrowController.java new file mode 100644 index 00000000..3e81cd16 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Controllers/ArrowController.java @@ -0,0 +1,152 @@ +package visualiser.Controllers; + + +import javafx.application.Platform; +import javafx.beans.property.Property; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; +import javafx.scene.shape.Circle; +import shared.model.Bearing; +import shared.model.Wind; +import visualiser.model.VisualiserRace; + +/** + * Controller for the arrow.fxml view. + */ +public class ArrowController { + + + @FXML + private Pane compass; + + @FXML + private StackPane arrowStackPane; + + @FXML + private ImageView arrowImage; + + @FXML + private Circle circle; + + @FXML + private Label northLabel; + + @FXML + private Label windLabel; + + @FXML + private Label speedLabel; + + + /** + * This is the property our arrow control binds to. + */ + private Property wind; + + + /** + * Constructor. + */ + public ArrowController() { + } + + + /** + * Sets which wind property the arrow control should bind to. + * @param wind The wind property to bind to. + */ + public void setWindProperty(Property wind) { + this.wind = wind; + + wind.addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + Platform.runLater(() -> updateWind(newValue)); + } + }); + } + + + /** + * Updates the control to use the new wind value. + * This updates the arrow direction (due to bearing), arrow length (due to speed), and label (due to speed). + * @param wind The wind value to use. + */ + private void updateWind(Wind wind) { + updateWindBearing(wind.getWindDirection()); + updateWindSpeed(wind.getWindSpeed()); + } + + + /** + * Updates the control to account for the new wind speed. + * This changes the length (height) of the wind arrow, and updates the speed label. + * @param speedKnots The new wind speed, in knots. + */ + private void updateWindSpeed(double speedKnots) { + updateWindArrowLength(speedKnots); + updateWindSpeedLabel(speedKnots); + } + + /** + * Updates the length of the wind arrow according to the specified wind speed. + * @param speedKnots Wind speed, in knots. + */ + private void updateWindArrowLength(double speedKnots) { + + //At 2 knots, the arrow reaches its minimum height, and at 30 knots it reaches its maximum height. + double minKnots = 2; + double maxKnots = 30; + double deltaKnots = maxKnots - minKnots; + + double minHeight = 25; + double maxHeight = 75; + double deltaHeight = maxHeight - minHeight; + + //Clamp speed. + if (speedKnots > maxKnots) { + speedKnots = maxKnots; + } else if (speedKnots < minKnots) { + speedKnots = minKnots; + } + + //How far between the knots bounds is the current speed? + double currentDeltaKnots = speedKnots - minKnots; + double currentKnotsScalar = currentDeltaKnots / deltaKnots; + + //Thus, how far between the pixel height bounds should the arrow height be? + double newHeight = minHeight + (currentKnotsScalar * deltaHeight); + + arrowImage.setFitHeight(newHeight); + } + + /** + * Updates the wind speed label according to the specified wind speed. + * @param speedKnots Wind speed, in knots. + */ + private void updateWindSpeedLabel(double speedKnots) { + speedLabel.setText(String.format("%.1fkn", speedKnots)); + } + + + /** + * Updates the control to account for a new wind bearing. + * This rotates the arrow according to the bearing. + * @param bearing The bearing to use to rotate arrow. + */ + private void updateWindBearing(Bearing bearing) { + + //We need to display wind-from, so add 180 degrees. + Bearing fromBearing = Bearing.fromDegrees(bearing.degrees() + 180d); + + //Rotate the wind arrow. + arrowStackPane.setRotate(fromBearing.degrees()); + } + + + + +} diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java index 5777c06e..f34c57a8 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java @@ -5,7 +5,6 @@ import javafx.animation.AnimationTimer; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.scene.chart.LineChart; import javafx.scene.control.*; @@ -24,10 +23,8 @@ import visualiser.gameController.Keys.ControlKey; import visualiser.gameController.Keys.KeyFactory; import visualiser.model.*; -import java.awt.*; import java.io.IOException; import java.net.URL; -import java.text.DecimalFormat; import java.util.ResourceBundle; /** @@ -60,6 +57,11 @@ public class RaceController extends Controller { */ private Sparkline sparkline; + /** + * The arrow controller. + */ + @FXML private ArrowController arrowController; + /** * Service for sending keystrokes to server */ @@ -67,8 +69,18 @@ public class RaceController extends Controller { @FXML private GridPane canvasBase; - @FXML private Pane arrow; + + @FXML private SplitPane race; + + /** + * This is the root node of the arrow control. + */ + @FXML private Pane arrow; + + /** + * This is the pane we place the actual arrow control inside of. + */ @FXML private StackPane arrowPane; @FXML private Label timer; @FXML private Label FPS; @@ -118,15 +130,15 @@ public class RaceController extends Controller { //Fps display. initialiseFps(this.visualiserRace); - //Need to add the included arrow pane to the arrowPane container. - initialiseArrow(); - //Information table. initialiseInfoTable(this.visualiserRace); //Sparkline. initialiseSparkline(this.visualiserRace); + //Arrow. + initialiseArrow(this.visualiserRace); + //Race canvas. initialiseRaceCanvas(this.visualiserRace); @@ -294,7 +306,7 @@ public class RaceController extends Controller { private void initialiseRaceCanvas(VisualiserRace race) { //Create canvas. - raceCanvas = new ResizableRaceCanvas(race, arrow.getChildren().get(0)); + raceCanvas = new ResizableRaceCanvas(race); //Set properties. raceCanvas.setMouseTransparent(true); @@ -367,10 +379,11 @@ public class RaceController extends Controller { /** - * Adds the included arrow pane (see arrow.fxml) to the arrowPane (see race.fxml). + * Initialises the arrow controller with data from the race to observe. + * @param race The race to observe. */ - private void initialiseArrow() { - arrowPane.getChildren().add(arrow); + private void initialiseArrow(VisualiserRace race) { + arrowController.setWindProperty(race.windProperty()); } diff --git a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java index 7664f854..e9b4c31c 100644 --- a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java @@ -12,7 +12,6 @@ import shared.model.GPSCoordinate; import shared.model.Mark; import shared.model.RaceClock; -import java.time.Duration; import java.util.List; /** @@ -39,12 +38,6 @@ public class ResizableRaceCanvas extends ResizableCanvas { */ private VisualiserRace visualiserRace; - /** - * The background of the race. - * We render the background whenever the race boundary changes, or the screen size changes. - */ - private Image background; - private boolean annoName = true; private boolean annoAbbrev = true; @@ -54,22 +47,15 @@ public class ResizableRaceCanvas extends ResizableCanvas { private boolean annoTimeSinceLastMark = true; - /** - * The wind arrow node. - */ - private Node arrow; - /** * Constructs a {@link ResizableRaceCanvas} using a given {@link VisualiserRace}. * @param visualiserRace The race that data is read from in order to be drawn. - * @param arrow The wind arrow's node. */ - public ResizableRaceCanvas(VisualiserRace visualiserRace, Node arrow) { + public ResizableRaceCanvas(VisualiserRace visualiserRace) { super(); this.visualiserRace = visualiserRace; - this.arrow = arrow; RaceDataSource raceData = visualiserRace.getRaceDataSource(); @@ -375,32 +361,6 @@ public class ResizableRaceCanvas extends ResizableCanvas { - /** - * Displays an arrow representing wind direction on the Canvas. - * This function accepts a wind-to bearing, but displays a wind-from bearing. - * - * @param angle Angle that the arrow is to be facing in degrees 0 degrees = North (Up). - * @see GraphCoordinate - */ - private void displayWindArrow(double angle) { - - //We need to display wind-from, so add 180 degrees. - angle += 180d; - - //Get it within [0, 360). - while (angle >= 360d) { - angle -= 360d; - } - - //Rotate the wind arrow. - if (arrow != null && arrow.getRotate() != angle) { - arrow.setRotate(angle); - } - } - - - - /** * Draws all of the {@link Mark}s on the canvas. */ @@ -440,13 +400,9 @@ public class ResizableRaceCanvas extends ResizableCanvas { this.map.setWidth((int) getWidth()); this.map.setHeight((int) getHeight()); - //Redraw the boundary. - redrawBoundaryImage(); - //Draw the race. drawRace(); - } @@ -459,15 +415,13 @@ public class ResizableRaceCanvas extends ResizableCanvas { /** - * Draws the race boundary, and saves the image to {@link #background}. - * You should call {@link #clear()} before calling this. + * Draws the race boundary. */ - private void redrawBoundaryImage() { + private void drawBoundary() { //Prepare to draw. gc.setLineWidth(1); gc.setFill(Color.AQUA); - gc.drawImage(new Image(getClass().getClassLoader().getResourceAsStream("images/WaterBackground.png")), 0, 0); //Calculate the screen coordinates of the boundary. @@ -487,9 +441,6 @@ public class ResizableRaceCanvas extends ResizableCanvas { //Draw the boundary. gc.fillPolygon(xpoints, ypoints, xpoints.length); - //Render boundary to image. - this.background = snapshot(null, null); - } /** @@ -511,18 +462,6 @@ public class ResizableRaceCanvas extends ResizableCanvas { //Marks. drawMarks(); - //Wind arrow. This rotates the wind arrow node. - displayWindArrow(this.visualiserRace.getWindDirection().degrees()); - - } - - - /** - * Draws the race boundary image onto the canvas. - * See {@link #background}. - */ - private void drawBoundary() { - gc.drawImage(this.background, 0, 0); } diff --git a/racevisionGame/src/main/resources/css/dayMode.css b/racevisionGame/src/main/resources/css/dayMode.css index b15f242b..d0f62fb7 100644 --- a/racevisionGame/src/main/resources/css/dayMode.css +++ b/racevisionGame/src/main/resources/css/dayMode.css @@ -50,4 +50,6 @@ .scroll-bar > .increment-button:pressed > .increment-arrow, .scroll-bar > .decrement-button:pressed > .decrement-arrow { -fx-background-color: -fx-mark-highlight-color, rgb(255, 255, 255); -} \ No newline at end of file +} + + diff --git a/racevisionGame/src/main/resources/css/nightMode.css b/racevisionGame/src/main/resources/css/nightMode.css index 406cc60b..7fe6a67b 100644 --- a/racevisionGame/src/main/resources/css/nightMode.css +++ b/racevisionGame/src/main/resources/css/nightMode.css @@ -51,4 +51,9 @@ .scroll-bar > .increment-button:pressed > .increment-arrow, .scroll-bar > .decrement-button:pressed > .decrement-arrow { -fx-background-color: -fx-mark-highlight-color, rgb(255, 255, 255); -} \ No newline at end of file +} + + +#arrowImage { + -fx-image: url("/visualiser/images/arrowLight.png"); +} diff --git a/racevisionGame/src/main/resources/visualiser/images/arrowLight.png b/racevisionGame/src/main/resources/visualiser/images/arrowLight.png new file mode 100644 index 00000000..7a80459d Binary files /dev/null and b/racevisionGame/src/main/resources/visualiser/images/arrowLight.png differ diff --git a/racevisionGame/src/main/resources/visualiser/scenes/arrow.fxml b/racevisionGame/src/main/resources/visualiser/scenes/arrow.fxml index 6e8a88b5..4057753d 100644 --- a/racevisionGame/src/main/resources/visualiser/scenes/arrow.fxml +++ b/racevisionGame/src/main/resources/visualiser/scenes/arrow.fxml @@ -1,34 +1,58 @@ - - - - - - - + + + + + + + + + + + - + + + + + + + + + - + - - - - - + + + + + + + + + + + + - - - - + - + diff --git a/racevisionGame/src/main/resources/visualiser/scenes/race.fxml b/racevisionGame/src/main/resources/visualiser/scenes/race.fxml index 76da5379..159d725c 100644 --- a/racevisionGame/src/main/resources/visualiser/scenes/race.fxml +++ b/racevisionGame/src/main/resources/visualiser/scenes/race.fxml @@ -16,7 +16,6 @@ - @@ -76,7 +75,11 @@ - + + + + + diff --git a/racevisionGame/src/test/java/mock/model/WindGeneratorTest.java b/racevisionGame/src/test/java/mock/model/WindGeneratorTest.java new file mode 100644 index 00000000..74ad7421 --- /dev/null +++ b/racevisionGame/src/test/java/mock/model/WindGeneratorTest.java @@ -0,0 +1,111 @@ +package mock.model; + +import org.junit.Before; +import org.junit.Test; +import shared.model.Bearing; +import shared.model.Wind; + +import static org.junit.Assert.*; + + +public class WindGeneratorTest { + + + private WindGenerator windGenerator; + + private Bearing windBaselineBearing; + private Bearing windBearingLowerBound; + private Bearing windBearingUpperBound; + private double windBaselineSpeed; + private double windSpeedLowerBound; + private double windSpeedUpperBound; + + private double speedKnotsEpsilon; + private double bearingDegreeEpsilon; + + + @Before + public void setUp() throws Exception { + + + //Bounds. + this.windBaselineBearing = Bearing.fromDegrees(88.3); + this.windBearingLowerBound = Bearing.fromDegrees(66.5); + this.windBearingUpperBound = Bearing.fromDegrees(248.6); + this.windBaselineSpeed = 13; + this.windSpeedLowerBound = 7; + this.windSpeedUpperBound = 20; + + this.windGenerator = new WindGenerator( + windBaselineBearing, + windBearingLowerBound, + windBearingUpperBound, + windBaselineSpeed, + windSpeedLowerBound, + windSpeedUpperBound ); + + this.bearingDegreeEpsilon = 0.001; + this.speedKnotsEpsilon = 0.001; + } + + + /** + * Tests if the baseline wind generated it accurate. + */ + @Test + public void generateBaselineWindTest() { + + Wind wind = windGenerator.generateBaselineWind(); + + assertEquals(windBaselineSpeed, wind.getWindSpeed(), speedKnotsEpsilon); + assertEquals(windBaselineBearing.degrees(), wind.getWindDirection().degrees(), bearingDegreeEpsilon); + + } + + /** + * Tests if the random wind generated is inside the bounds. + */ + @Test + public void generateRandomWindTest() { + + int randomWindCount = 1000; + + for (int i = 0; i < randomWindCount; i++) { + + Wind wind = windGenerator.generateRandomWind(); + + assertTrue(wind.getWindSpeed() >= windSpeedLowerBound); + assertTrue(wind.getWindSpeed() <= windSpeedUpperBound); + + assertTrue(wind.getWindDirection().degrees() >= windBearingLowerBound.degrees()); + assertTrue(wind.getWindDirection().degrees() <= windBearingUpperBound.degrees()); + + } + + } + + + /** + * Tests if the next wind generated is inside the bounds. + */ + @Test + public void generateNextWindTest() { + + Wind wind = windGenerator.generateBaselineWind(); + + int randomWindCount = 1000; + + for (int i = 0; i < randomWindCount; i++) { + + wind = windGenerator.generateNextWind(wind); + + assertTrue(wind.getWindSpeed() >= windSpeedLowerBound); + assertTrue(wind.getWindSpeed() <= windSpeedUpperBound); + + assertTrue(wind.getWindDirection().degrees() >= windBearingLowerBound.degrees()); + assertTrue(wind.getWindDirection().degrees() <= windBearingUpperBound.degrees()); + + } + + } +}