package visualiser.Controllers; 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.*; import javafx.scene.control.Label; import javafx.scene.input.KeyEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.util.Callback; import network.Messages.Enums.RaceStatusEnum; import shared.model.Leg; import visualiser.app.VisualiserInput; import visualiser.gameController.Keys.ControlKey; import visualiser.gameController.Keys.KeyFactory; import visualiser.model.*; import java.awt.*; import java.net.URL; import java.text.DecimalFormat; import java.util.ResourceBundle; /** * Controller used to display a running race. */ public class RaceController extends Controller { /** * The object used to read packets from the connected server. */ private VisualiserInput visualiserInput; /** * The race object which describes the currently occurring race. */ private VisualiserRace visualiserRace; /** * An additional observable list of boats. This is used by the table view, to allow it to sort boats without effecting the race's own list of boats. */ private ObservableList tableBoatList; /** * The canvas that draws the race. */ private ResizableRaceCanvas raceCanvas; /** * The sparkline graph. */ private Sparkline sparkline; @FXML private GridPane canvasBase; @FXML private Pane arrow; @FXML private SplitPane race; @FXML private StackPane arrowPane; @FXML private Label timer; @FXML private Label FPS; @FXML private Label timeZone; @FXML private CheckBox showFPS; @FXML private TableView boatInfoTable; @FXML private TableColumn boatPlacingColumn; @FXML private TableColumn boatTeamColumn; @FXML private TableColumn boatMarkColumn; @FXML private TableColumn boatSpeedColumn; @FXML private LineChart sparklineChart; @FXML private AnchorPane annotationPane; /** * Ctor. */ public RaceController() { } @Override public void initialize(URL location, ResourceBundle resources) { // Initialise keyboard handler race.addEventFilter(KeyEvent.KEY_PRESSED, event -> { String codeString = event.getCode().toString(); ControlKey controlKey = KeyFactory.getKey(codeString); if(controlKey != null) { System.out.println(controlKey.toString() + " is Pressed."); } }); } /** * Initialises the various UI components to listen to the {@link #visualiserRace}. */ private void initialiseRace() { //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); //Race canvas. initialiseRaceCanvas(this.visualiserRace); //Race timezone label. initialiseRaceTimezoneLabel(this.visualiserRace); //Race clock. initialiseRaceClock(this.visualiserRace); //Start the race animation timer. raceTimer(); } /** * Initialises the frame rate functionality. This allows for toggling the frame rate, and connect the fps label to the race's fps property. * @param visualiserRace The race to connect the fps label to. */ private void initialiseFps(VisualiserRace visualiserRace) { //On/off toggle. initialiseFpsToggle(); //Label value. initialiseFpsLabel(visualiserRace); } /** * Initialises a listener for the fps toggle. */ private void initialiseFpsToggle() { showFPS.selectedProperty().addListener((ov, old_val, new_val) -> { if (showFPS.isSelected()) { FPS.setVisible(true); } else { FPS.setVisible(false); } }); } /** * Initialises the fps label to update when the race fps changes. * @param visualiserRace The race to monitor the frame rate of. */ private void initialiseFpsLabel(VisualiserRace visualiserRace) { visualiserRace.fpsProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> this.FPS.setText("FPS: " + newValue.toString())); }); } /** * Initialises the information table view to listen to a given race. * @param race Race to listen to. */ public void initialiseInfoTable(VisualiserRace race) { //Copy list of boats. this.tableBoatList = FXCollections.observableArrayList(race.getBoats()); //Set up table. boatInfoTable.setItems(this.tableBoatList); //Set up each column. //Name. boatTeamColumn.setCellValueFactory( cellData -> cellData.getValue().nameProperty() ); //Speed. boatSpeedColumn.setCellValueFactory( cellData -> cellData.getValue().currentSpeedProperty() ); //Kind of ugly, but allows for formatting an observed speed. boatSpeedColumn.setCellFactory( //Callback object. new Callback, TableCell>() { //Callback function. @Override public TableCell call(TableColumn param) { //We return a table cell that populates itself with a Number, and formats it. return new TableCell(){ //Function to update the cell text. @Override protected void updateItem(Number item, boolean empty) { if (item != null) { super.updateItem(item, empty); setText(String.format("%.2fkn", item.doubleValue())); } } }; } } ); //Last mark. boatMarkColumn.setCellValueFactory( cellData -> cellData.getValue().legProperty() ); //Kind of ugly, but allows for turning an observed Leg into a string. boatMarkColumn.setCellFactory( //Callback object. new Callback, TableCell>() { //Callback function. @Override public TableCell call(TableColumn param) { //We return a table cell that populates itself with a Leg's name. return new TableCell(){ //Function to update the cell text. @Override protected void updateItem(Leg item, boolean empty) { if (item != null) { super.updateItem(item, empty); setText(item.getName()); } } }; } } ); //Current place within race. boatPlacingColumn.setCellValueFactory( cellData -> cellData.getValue().positionProperty() ); } /** * Initialises the {@link Sparkline}, and listens to a specified {@link VisualiserRace}. * @param race The race to listen to. */ private void initialiseSparkline(VisualiserRace race) { //The race.getBoats() we are passing in is sorted by position in race inside the race class. this.sparkline = new Sparkline(this.visualiserRace, this.sparklineChart); } /** * Initialises the {@link ResizableRaceCanvas}, provides the race to read data from. * @param race Race to read data from. */ private void initialiseRaceCanvas(VisualiserRace race) { //Create canvas. raceCanvas = new ResizableRaceCanvas(race, arrow.getChildren().get(0)); //Set properties. raceCanvas.setMouseTransparent(true); raceCanvas.widthProperty().bind(canvasBase.widthProperty()); raceCanvas.heightProperty().bind(canvasBase.heightProperty()); //Draw it and show it. raceCanvas.draw(); raceCanvas.setVisible(true); //Add to scene. canvasBase.getChildren().add(0, raceCanvas); } /** * Intialises the race time zone label with the race's time zone. * @param race The race to get time zone from. */ private void initialiseRaceTimezoneLabel(VisualiserRace race) { timeZone.setText(race.getRaceClock().getTimeZone()); } /** * Initialises the race clock to listen to the specified race. * @param race The race to listen to. */ private void initialiseRaceClock(VisualiserRace race) { //RaceClock.duration isn't necessarily being changed in the javaFX thread, so we need to runlater the update. race.getRaceClock().durationProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> { timer.setText(newValue); }); }); } /** * Displays a specified race. * @param visualiserInput Object used to read packets from server. * @param visualiserRace Object modelling the race. */ public void startRace(VisualiserInput visualiserInput, VisualiserRace visualiserRace) { this.visualiserInput = visualiserInput; this.visualiserRace = visualiserRace; initialiseRace(); //Display this controller. race.setVisible(true); // set up annotation displays new Annotations(annotationPane, raceCanvas); } /** * Transition from the race view to the finish view. * @param boats boats there are in the race. */ public void finishRace(ObservableList boats){ race.setVisible(false); parent.enterFinish(boats); } /** * Adds the included arrow pane (see arrow.fxml) to the arrowPane (see race.fxml). */ private void initialiseArrow() { arrowPane.getChildren().add(arrow); } /** * Timer which monitors the race. */ private void raceTimer() { new AnimationTimer() { @Override public void handle(long arg0) { //Get the current race status. RaceStatusEnum raceStatus = visualiserRace.getRaceStatusEnum(); //If the race has finished, go to finish view. if (raceStatus == RaceStatusEnum.FINISHED) { //Stop this timer. stop(); //Hide this, and display the finish controller. finishRace(visualiserRace.getBoats()); } else { //Otherwise, render the canvas. raceCanvas.drawRace(); //Sort the tableview. Doesn't automatically work for all columns. boatInfoTable.sort(); } } }.start(); } }