package visualiser.Controllers; import javafx.animation.AnimationTimer; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; import javafx.fxml.FXML; import javafx.scene.chart.LineChart; import javafx.scene.control.*; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.StackPane; import javafx.util.Callback; import network.Messages.Enums.RaceStatusEnum; import shared.model.Leg; import visualiser.app.App; import visualiser.gameController.ControllerClient; import visualiser.gameController.Keys.ControlKey; import visualiser.gameController.Keys.KeyFactory; import visualiser.model.*; import java.io.IOException; import java.util.List; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; /** * Controller used to display a running race. */ public class RaceViewController extends Controller2 { private VisualiserRaceEvent visualiserRace; private VisualiserRaceState raceState; private ControllerClient controllerClient; private ResizableRaceCanvas raceCanvas; private KeyFactory keyFactory = new KeyFactory(); private boolean infoTableShow = true; // shown or hidden private boolean isHost; // note: it says it's not used but it is! do not remove :) private @FXML ArrowController arrowController; private @FXML GridPane canvasBase; private @FXML SplitPane race; private @FXML StackPane arrowPane; private @FXML Label timer; private @FXML Label FPS; private @FXML Label timeZone; private @FXML CheckBox showFPS; private @FXML TableView boatInfoTable; private @FXML TableColumn boatPlacingColumn; private @FXML TableColumn boatTeamColumn; private @FXML TableColumn boatMarkColumn; private @FXML TableColumn boatSpeedColumn; private @FXML LineChart sparklineChart; private @FXML AnchorPane annotationPane; /** * Displays a specified race. * Intended to be called on loading the scene. * @param visualiserRace Object modelling the race. * @param controllerClient Socket Client that manipulates the controller. * @param isHost is user a host */ public void startRace(VisualiserRaceEvent visualiserRace, ControllerClient controllerClient, Boolean isHost) { this.visualiserRace = visualiserRace; this.raceState = visualiserRace.getVisualiserRaceState(); this.controllerClient = controllerClient; this.isHost = isHost; keyFactory.load(); initKeypressHandler(); initialiseRaceVisuals(); } /** * Sets up the listener and actions that occur when a key is pressed. */ public void initKeypressHandler() { race.addEventFilter(KeyEvent.KEY_PRESSED, event -> { String codeString = event.getCode().toString(); // tab key if (codeString.equals("TAB")){toggleTable();} // any key pressed ControlKey controlKey = keyFactory.getKey(codeString); if(controlKey != null) { try { controlKey.onAction(); // Change key state if applicable controllerClient.sendKey(controlKey); event.consume(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Logger.getGlobal().log(Level.WARNING, "RaceViewController was interrupted on thread: " + Thread.currentThread() + "while sending: " + controlKey, e); } } // escape key if(event.getCode() == KeyCode.ESCAPE) { try { if (isHost) { Alert alert = new Alert(Alert.AlertType.CONFIRMATION); alert.setTitle("Exit Race"); alert.setContentText("Do you wish to quit the race? You are the host"); Optional result = alert.showAndWait(); if (result.get() == ButtonType.OK) { App.game.endEvent(); loadTitleScreen(); } } else { Alert alert = new Alert(Alert.AlertType.CONFIRMATION); alert.setTitle("Exit Race"); alert.setContentText("Do you wish to quit the race?"); Optional result = alert.showAndWait(); if (result.get() == ButtonType.OK) { loadTitleScreen(); } } } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } }); } /** * Initialises the various UI components to listen to the {@link #visualiserRace}. */ private void initialiseRaceVisuals() { // initialise displays initialiseFps(); initialiseInfoTable(); initialiseRaceCanvas(); initialiseView3D(); initialiseRaceClock(); raceTimer(); // start the timer new Annotations(this.annotationPane, this.raceCanvas); new Sparkline(this.raceState, this.sparklineChart); timeZone.setText(this.raceState.getRaceClock().getTimeZone()); arrowController.setWindProperty(this.raceState.windProperty()); } private void initialiseView3D() { List boats = raceState.getBoats(); for(VisualiserBoat boat: boats) { boat.positionProperty().addListener((o, prev, curr) -> { System.out.println(boat.getCountry() + " is at " + curr.toString()); }); } } /** * Initialises the frame rate functionality. This allows for toggling the * frame rate, and connect the fps label to the race's fps property. */ private void initialiseFps() { // fps toggle listener showFPS.selectedProperty().addListener((ov, old_val, new_val) -> { if (showFPS.isSelected()) { FPS.setVisible(true); } else { FPS.setVisible(false); } }); // fps label display this.visualiserRace.getFrameRateProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> this.FPS.setText("FPS: " + newValue.toString())); }); } /** * Initialises the information table view to listen to a given race. */ public void initialiseInfoTable() { // list of boats to display data for ObservableList boats = FXCollections .observableArrayList(this.visualiserRace.getVisualiserRaceState().getBoats()); SortedList sortedBoats = new SortedList<>(boats); sortedBoats.comparatorProperty().bind(boatInfoTable.comparatorProperty()); // update list when boat information changes this.visualiserRace.getVisualiserRaceState().getBoats().addListener( (ListChangeListener.Change c) -> Platform.runLater(() -> { boats.setAll(this.visualiserRace.getVisualiserRaceState().getBoats()); })); // set table data boatInfoTable.setItems(sortedBoats); boatTeamColumn.setCellValueFactory( cellData -> cellData.getValue().nameProperty()); boatSpeedColumn.setCellValueFactory( cellData -> cellData.getValue().currentSpeedProperty()); boatMarkColumn.setCellValueFactory( cellData -> cellData.getValue().legProperty()); boatPlacingColumn.setCellValueFactory( cellData -> cellData.getValue().placingProperty()); //Kind of ugly, but allows for formatting an observed speed. boatSpeedColumn.setCellFactory( new Callback, TableCell>() { @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())); } } }; } }); //Kind of ugly, but allows for turning an observed Leg into a string. boatMarkColumn.setCellFactory( new Callback, TableCell>() { @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()); } } }; } }); } /** * Initialises the {@link ResizableRaceCanvas}, provides the race to * read data from. */ private void initialiseRaceCanvas() { //Create canvas. raceCanvas = new ResizableRaceCanvas(raceState); raceCanvas.setMouseTransparent(true); raceCanvas.widthProperty().bind(canvasBase.widthProperty()); raceCanvas.heightProperty().bind(canvasBase.heightProperty()); // draw and display raceCanvas.draw(); raceCanvas.setVisible(true); canvasBase.getChildren().add(0, raceCanvas); } /** * Initialises the race clock to listen to the specified race. */ private void initialiseRaceClock() { raceState.getRaceClock().durationProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> { timer.setText(newValue); }); }); } /** * Transition from the race view to the finish view. */ public void finishRace() throws IOException { RaceFinishController fc = (RaceFinishController)loadScene("raceFinish.fxml"); fc.loadFinish(raceState.getBoats()); } /** * Timer which monitors the race. */ private void raceTimer() { new AnimationTimer() { @Override public void handle(long arg0) { //If the race has finished, go to finish view. if (raceState.getRaceStatusEnum() == RaceStatusEnum.FINISHED) { stop(); // stop the timer try { finishRace(); } catch (IOException e) { e.printStackTrace(); } } else { // refresh visual race display raceCanvas.drawRace(); boatInfoTable.sort(); } //Return to main screen if we lose connection. if (!visualiserRace.getServerConnection().isAlive()) { try { loadTitleScreen(); } catch (Exception e) { e.printStackTrace(); } //TODO we should display an error to the user //TODO also need to "reset" any state (race, connections, etc...). } } }.start(); } /** * toggles if the info table is shown */ private void toggleTable() { double tablePercent = 1 - (boatPlacingColumn.getPrefWidth() + boatTeamColumn.getPrefWidth() + boatMarkColumn.getPrefWidth() + boatSpeedColumn.getPrefWidth())/race.getWidth(); if (infoTableShow) { race.setDividerPositions(tablePercent); arrowPane.setScaleX(0.5); arrowPane.setScaleY(0.5); arrowPane.setTranslateX(0 + (arrowPane.getScene().getWidth()/4)*tablePercent); arrowPane.setTranslateY(0 - arrowPane.getScene().getHeight()/4); } else { race.setDividerPositions(1); arrowPane.setScaleX(1); arrowPane.setScaleY(1); arrowPane.setTranslateX(0); arrowPane.setTranslateY(0); } boatInfoTable.refresh(); infoTableShow = !infoTableShow; } }