package visualiser.Controllers; import com.interactivemesh.jfx.importer.stl.StlMeshImporter; 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.AmbientLight; import javafx.scene.PointLight; import javafx.scene.chart.LineChart; import javafx.scene.control.*; import javafx.scene.effect.Light; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.GridPane; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.shape.MeshView; import javafx.scene.transform.Translate; import javafx.util.Callback; import network.Messages.Enums.RaceStatusEnum; import shared.dataInput.RaceDataSource; import shared.model.Leg; import shared.model.Mark; import visualiser.app.App; import visualiser.gameController.ControllerClient; import visualiser.gameController.Keys.ControlKey; import visualiser.layout.*; import visualiser.model.*; import visualiser.utils.GPSConverter; import java.io.IOException; import java.net.URL; import java.util.Optional; import java.util.ResourceBundle; import java.util.logging.Level; import java.util.logging.Logger; import static visualiser.app.App.keyFactory; /** * Controller used to display a running race. */ public class RaceController extends Controller { /** * The race object which describes the currently occurring race. */ private VisualiserRaceEvent visualiserRace; /** * Service for sending keystrokes to server */ private ControllerClient controllerClient; private boolean isHost; /** * state of the info table */ private boolean infoTableShow; private View3D view3D; private ObservableList viewSubjects; /** * The arrow controller. */ @FXML private ArrowController arrowController; @FXML private GridPane canvasBase; @FXML private SplitPane racePane; /** * This is the pane we place the actual arrow control inside of. */ @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; /** * Ctor. */ public RaceController() { } @Override public void initialize(URL location, ResourceBundle resources) { infoTableShow = true; // Initialise keyboard handler racePane.addEventFilter(KeyEvent.KEY_PRESSED, event -> { String codeString = event.getCode().toString(); if (codeString.equals("TAB")){toggleTable();} 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, "RaceController was interrupted on thread: " + Thread.currentThread() + "while sending: " + controlKey, e); } } 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) { parent.endEvent(); racePane.setVisible(false); App.app.showMainStage(App.getStage()); } } 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) { racePane.setVisible(false); App.app.showMainStage(App.getStage()); } } } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } }); } /** * Initialises the various UI components to listen to the {@link #visualiserRace}. */ private void initialiseRace() { //Fps display. initialiseFps(this.visualiserRace); //Information table. initialiseInfoTable(this.visualiserRace); //Arrow. initialiseArrow(this.visualiserRace); initialiseView3D(this.visualiserRace); //Race timezone label. initialiseRaceTimezoneLabel(this.visualiserRace); //Race clock. initialiseRaceClock(this.visualiserRace); //Start the race animation timer. raceTimer(); } private void initialiseView3D(VisualiserRaceEvent race) { viewSubjects = FXCollections.observableArrayList(); AmbientLight ambientLight = new AmbientLight(new Color(1, 1, 1, 0.75)); ambientLight.setTranslateX(250); ambientLight.setTranslateZ(210); ambientLight.setLightOn(true); PointLight pointLight = new PointLight(); ambientLight.setTranslateX(250); ambientLight.setTranslateZ(210); ambientLight.setLightOn(true); // Import boat mesh URL asset = HostController.class.getClassLoader().getResource("assets/V1.2 Complete Boat.stl"); StlMeshImporter importer = new StlMeshImporter(); importer.read(asset); // Configure camera angles and control URL markerAsset = HostController.class.getClassLoader().getResource("assets/Bouy V1.1.stl"); StlMeshImporter importerMark = new StlMeshImporter(); importerMark.read(markerAsset); URL alternateBoatAsset = HostController.class.getClassLoader().getResource("assets/V1.3 BurgerBoat.stl"); StlMeshImporter importerBurgerBoat = new StlMeshImporter(); importerBurgerBoat.read(alternateBoatAsset); view3D = new View3D(false); view3D.setItems(viewSubjects); view3D.setDistance(1050); view3D.setBirdsEye(); view3D.enableTracking(); view3D.addAmbientLight(ambientLight); view3D.addPointLight(pointLight); canvasBase.add(view3D, 0, 0); // Set up projection from GPS to view RaceDataSource raceData = visualiserRace.getVisualiserRaceState().getRaceDataSource(); final GPSConverter gpsConverter = new GPSConverter(raceData, 450, 450); // Set up sea surface SeaSurface sea = new SeaSurface(750, 200); sea.setX(250); sea.setZ(210); viewSubjects.add(sea); SkyBox skyBox = new SkyBox(750, 200, 250, 0, 210); viewSubjects.addAll(skyBox.getSkyBoxPlanes()); Boundary3D boundary3D = new Boundary3D(visualiserRace.getVisualiserRaceState().getRaceDataSource().getBoundary(), gpsConverter); for (Subject3D subject3D: boundary3D.getBoundaryNodes()){ viewSubjects.add(subject3D); } // Position and add each mark to view for(Mark mark: race.getVisualiserRaceState().getMarks()) { MeshView mesh = new MeshView(importerMark.getImport()); Subject3D markModel = new Subject3D(mesh); markModel.setX(gpsConverter.convertGPS(mark.getPosition()).getX()); markModel.setZ(gpsConverter.convertGPS(mark.getPosition()).getY()); viewSubjects.add(markModel); } // Position and add each boat to view for(VisualiserBoat boat: race.getVisualiserRaceState().getBoats()) { MeshView mesh; if(boat.getSourceID() == race.getVisualiserRaceState().getPlayerBoatID()) { mesh = new MeshView(importer.getImport()); } else { mesh = new MeshView(importerBurgerBoat.getImport()); } Subject3D boatModel = new Subject3D(mesh); viewSubjects.add(boatModel); // Track this boat's movement with the new subject AnimationTimer trackBoat = new AnimationTimer() { @Override public void handle(long now) { boatModel.setHeading(boat.getBearing().degrees()); boatModel.setX(gpsConverter.convertGPS(boat.getPosition()).getX()); boatModel.setZ(gpsConverter.convertGPS(boat.getPosition()).getY()); } }; trackBoat.start(); } // Fix initial bird's-eye position view3D.updatePivot(new Translate(250, 0, 210)); // Bind zooming to scrolling view3D.setOnScroll(e -> { view3D.updateDistance(e.getDeltaY()); }); // Bind zooming to keypress (Z/X default) racePane.addEventFilter(KeyEvent.KEY_PRESSED, e -> { ControlKey key = keyFactory.getKey(e.getCode().toString()); if(key != null) { switch (key.toString()) { case "Zoom In": view3D.updateDistance(-10); break; case "Zoom Out": view3D.updateDistance(10); break; } } }); } /** * 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(VisualiserRaceEvent 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(VisualiserRaceEvent visualiserRace) { 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. * @param race Race to listen to. */ public void initialiseInfoTable(VisualiserRaceEvent race) { //Copy list of boats. ObservableList boats = FXCollections.observableArrayList(race.getVisualiserRaceState().getBoats()); SortedList sortedBoats = new SortedList<>(boats); sortedBoats.comparatorProperty().bind(boatInfoTable.comparatorProperty()); //Update copy when original changes. race.getVisualiserRaceState().getBoats().addListener((ListChangeListener.Change c) -> Platform.runLater(() -> { boats.setAll(race.getVisualiserRaceState().getBoats()); })); //Set up table. boatInfoTable.setItems(sortedBoats); //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().placingProperty() ); } /** * Initialises the race time zone label with the race's time zone. * @param race The race to get time zone from. */ private void initialiseRaceTimezoneLabel(VisualiserRaceEvent race) { timeZone.setText(race.getVisualiserRaceState().getRaceClock().getTimeZone()); } /** * Initialises the race clock to listen to the specified race. * @param race The race to listen to. */ private void initialiseRaceClock(VisualiserRaceEvent race) { //RaceClock.duration isn't necessarily being changed in the javaFX thread, so we need to runlater the update. race.getVisualiserRaceState().getRaceClock().durationProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> { timer.setText(newValue); }); }); } /** * Displays a specified race. * @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.controllerClient = controllerClient; this.isHost = isHost; initialiseRace(); //Display this controller. racePane.setVisible(true); } /** * Transition from the race view to the finish view. * @param boats boats there are in the race. */ public void finishRace(ObservableList boats) { racePane.setVisible(false); parent.enterFinish(boats); } /** * Initialises the arrow controller with data from the race to observe. * @param race The race to observe. */ private void initialiseArrow(VisualiserRaceEvent race) { arrowController.setWindProperty(race.getVisualiserRaceState().windProperty()); } /** * Timer which monitors the race. */ private void raceTimer() { new AnimationTimer() { @Override public void handle(long arg0) { //Get the current race status. RaceStatusEnum raceStatus = visualiserRace.getVisualiserRaceState().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.getVisualiserRaceState().getBoats()); } else { //Sort the tableview. Doesn't automatically work for all columns. boatInfoTable.sort(); } //Return to main screen if we lose connection. if (!visualiserRace.getServerConnection().isAlive()) { racePane.setVisible(false); //parent.enterTitle(); try { App.app.showMainStage(App.getStage()); } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } //TODO currently this doesn't work correctly - the title screen remains visible after clicking join game //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())/racePane.getWidth(); if (infoTableShow){ racePane.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{ racePane.setDividerPositions(1); arrowPane.setScaleX(1); arrowPane.setScaleY(1); arrowPane.setTranslateX(0); arrowPane.setTranslateY(0); } boatInfoTable.refresh(); infoTableShow = !infoTableShow; } }