package visualiser.Controllers; import com.interactivemesh.jfx.importer.stl.StlMeshImporter; import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer; import eu.hansolo.medusa.*; import eu.hansolo.medusa.events.UpdateEvent; 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.input.KeyCode; 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.scene.paint.Color; import javafx.scene.paint.Material; import javafx.scene.paint.PhongMaterial; import javafx.scene.shape.Cylinder; import javafx.scene.paint.Stop; import javafx.scene.shape.MeshView; import javafx.scene.shape.Shape3D; import javafx.scene.transform.Translate; import javafx.util.Callback; import network.Messages.Enums.RaceStatusEnum; import shared.dataInput.RaceDataSource; import shared.exceptions.BoatNotFoundException; import shared.model.*; import visualiser.app.App; import visualiser.enums.TutorialState; import visualiser.gameController.ControllerClient; import visualiser.gameController.Keys.ControlKey; import visualiser.gameController.Keys.KeyFactory; import visualiser.layout.*; import visualiser.model.*; import visualiser.utils.GPSConverter; import java.io.IOException; import java.net.URL; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; /** * Controller used to display a running race. */ public class RaceViewController extends Controller { private VisualiserRaceEvent visualiserRace; private VisualiserRaceState raceState; private ControllerClient controllerClient; private KeyFactory keyFactory = new KeyFactory(); private boolean infoTableShow = true; // shown or hidden private boolean isHost; private TutorialState currentState; private ArrayList tutorialStates; private boolean isTutorial = false; private String keyToPress; private View3D view3D; private WindCompass windCompass; private ObservableList viewSubjects; private Gauge gauge; private FGauge fGauge; private long positionDelay = 1000; private long positionTime = 0; private ResizableRaceCanvas raceCanvas; private boolean mapToggle = true; /** * Arrow pointing to next mark in third person */ private Subject3D nextMarkArrow; /** * Animation loop for rotating mark arrow */ private AnimationTimer pointToMark; // note: it says it's not used but it is! do not remove :) private @FXML ArrowController arrowController; private @FXML NextMarkController nextMarkController; private @FXML GridPane canvasBase; private @FXML GridPane canvasBase1; private @FXML SplitPane racePane; private @FXML StackPane arrowPane; private @FXML Pane nextMarkPane; 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 Label tutorialText; private @FXML AnchorPane infoWrapper; private @FXML AnchorPane lineChartWrapper; private @FXML StackPane speedPane; private @FXML AnchorPane raceAnchorPane; /** * 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(); tutorialCheck(); initKeypressHandler(); initialiseRaceVisuals(); initialiseRaceCanvas(); } /** * Checks if the current game is a tutorial race and sets up initial * tutorial displays if it is. */ private void tutorialCheck(){ if (App.gameType == 4) { isTutorial = true; tutorialText.setVisible(true); tutorialStates = new ArrayList<>(Arrays.asList(TutorialState.values())); currentState = tutorialStates.get(0); tutorialStates.remove(0); searchMapForKey("Upwind"); tutorialText.setText( "Welcome to the tutorial! Exit at anytime with ESC. \nWe will first learn how to turn upwind. Press " + keyToPress + " to turn upwind."); } else { isTutorial = false; tutorialText.setVisible(false); } } private AnimationTimer arrowToNextMark; private void initKeypressHandler() { racePane.addEventFilter(KeyEvent.KEY_PRESSED, event -> { String codeString = event.getCode().toString(); // tab key if (codeString.equals("TAB")){toggleTable();} //map key if (codeString.equals("M")){bigMap();} // any key pressed ControlKey controlKey = keyFactory.getKey(codeString); if(controlKey != null) { try { controlKey.onAction(); // Change key state if applicable //Check if current race is a tutorial if (isTutorial){ //Check if current tutorial state has the same boat protocol code as key press if (controlKey.getProtocolCode().equals(currentState.getAction())){ //Update tutorial checkTutorialState(); } } 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); Logger.getGlobal().log(Level.WARNING, "RaceController was interrupted on thread: " + Thread.currentThread() + "while sending: " + controlKey, e); } catch (Exception e) { e.printStackTrace(); } } // 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 (Exception e) { e.printStackTrace(); } } }); } /** * Create speedometer */ private void initialiseSpeedometer() { //Create the Medusa Gauge gauge = GaugeBuilder.create() .prefSize(200,200) .foregroundBaseColor(Color.WHITE) .title("Title") .subTitle("Speed") .unit("Knots") .decimals(2) .lcdVisible(true) .lcdDesign(LcdDesign.STANDARD) .lcdFont(LcdFont.DIGITAL_BOLD) .scaleDirection(Gauge.ScaleDirection.CLOCKWISE) .minValue(0) .maxValue(50) .startAngle(320) .angleRange(280) .tickLabelDecimals(0) .tickLabelLocation(TickLabelLocation.INSIDE) .tickLabelOrientation(TickLabelOrientation.ORTHOGONAL) .onlyFirstAndLastTickLabelVisible(false) .tickLabelSectionsVisible(false) .tickLabelColor(Color.BLACK) .tickMarkSectionsVisible(false) .majorTickMarksVisible(true) .majorTickMarkType(TickMarkType.TRAPEZOID) .mediumTickMarksVisible(false) .mediumTickMarkType(TickMarkType.LINE) .minorTickMarksVisible(true) .minorTickMarkType(TickMarkType.LINE) .ledVisible(false) .ledType(Gauge.LedType.STANDARD) .ledColor(Color.rgb(255, 200, 0)) .ledBlinking(false) .needleShape(Gauge.NeedleShape.ANGLED) .needleSize(Gauge.NeedleSize.STANDARD) .needleColor(Color.CRIMSON) .startFromZero(false) .returnToZero(false) .knobType(Gauge.KnobType.METAL) .knobColor(Color.LIGHTGRAY) .interactive(false) .thresholdVisible(true) .threshold(50) .thresholdColor(Color.RED) .checkThreshold(true) .gradientBarEnabled(true) .gradientBarStops(new Stop(0.0, Color.BLUE), new Stop(0.25, Color.CYAN), new Stop(0.5, Color.LIME), new Stop(0.75, Color.YELLOW), new Stop(1.0, Color.RED)) .markersVisible(true) .build(); //Create a gauge with a frame and background that utilizes a Medusa gauge fGauge = FGaugeBuilder .create() .prefSize(200, 200) .gauge(gauge) .gaugeDesign(GaugeDesign.METAL) .gaugeBackground(GaugeDesign.GaugeBackground.CARBON) .foregroundVisible(true) .build(); speedPane.getChildren().add(fGauge); } /** * Initialises the various UI components to listen to the {@link #visualiserRace}. */ private void initialiseRaceVisuals() { // initialise displays initialiseFps(); initialiseInfoTable(); initialiseView3D(this.visualiserRace); initialiseRaceClock(); initialiseSpeedometer(); raceTimer(); // start the timer nextMarkPane.toFront(); speedometerLoop(); new Sparkline(this.raceState, this.sparklineChart); timeZone.setText(this.raceState.getRaceClock().getTimeZone()); arrowController.setWindProperty(this.raceState.windProperty()); } private void initialiseView3D(VisualiserRaceEvent race) { viewSubjects = FXCollections.observableArrayList(); try { nextMarkController.initialiseArrowView(race.getVisualiserRaceState().getBoat(race.getVisualiserRaceState().getPlayerBoatID())); } catch (BoatNotFoundException e) { e.printStackTrace(); } AmbientLight ambientLight = new AmbientLight(Color.web("#CCCCFF")); ambientLight.setTranslateX(250); ambientLight.setTranslateZ(210); ambientLight.setLightOn(true); PointLight pointLight = new PointLight(Color.web("#AAAAFF")); pointLight.setTranslateX(250); pointLight.setTranslateZ(210); pointLight.setLightOn(true); // Import boat mesh URL asset = RaceViewController.class.getClassLoader().getResource("assets/V1.2 Complete Boat.stl"); StlMeshImporter importer = new StlMeshImporter(); importer.read(asset); // Configure camera angles and control URL markerAsset = RaceViewController.class.getClassLoader().getResource("assets/Bouy V1.1.stl"); StlMeshImporter importerMark = new StlMeshImporter(); importerMark.read(markerAsset); URL alternateBoatAsset = RaceViewController.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); windCompass = new WindCompass(view3D, this.raceState.windProperty()); arrowPane.getChildren().add(windCompass); // Set up projection from GPS to view RaceDataSource raceData = visualiserRace.getVisualiserRaceState().getRaceDataSource(); final GPSConverter gpsConverter = new GPSConverter(raceData, 450, 450); SkyBox skyBox = new SkyBox(750, 200, 250, 0, 210); viewSubjects.addAll(skyBox.getSkyBoxPlanes()); // Set up sea surface SeaSurface sea = new SeaSurface(750, 200); sea.setX(250); sea.setZ(210); viewSubjects.add(sea); // Set up sea surface overlay SeaSurface seaOverlay = new SeaSurface(4000, 200); seaOverlay.setX(250); seaOverlay.setZ(210); viewSubjects.add(seaOverlay); 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, mark.getSourceID()); 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()) { Shape3D mesh = Assets3D.getBoat(); PhongMaterial boatColorMat = new PhongMaterial(boat.getColor()); //mesh.setMaterial(boatColorMat); Subject3D boatModel = new Subject3D(mesh, boat.getSourceID()); // Configure visualiser for client's boat if (boat.isClientBoat()) { // Add player boat highlight Shockwave boatHighlight = new Shockwave(10); boatHighlight.getMesh().setMaterial(new PhongMaterial(new Color(1, 1, 0, 0.1))); viewSubjects.add(boatHighlight); // Track player boat with camera viewSubjects.add(boatModel); Platform.runLater(() -> { view3D.trackSubject(boatModel); view3D.setThirdPerson(); }); // Track player boat with highlight AnimationTimer highlightTrack = new AnimationTimer() { @Override public void handle(long now) { boatHighlight.setX(gpsConverter.convertGPS(boat.getPosition()).getX()); boatHighlight.setZ(gpsConverter.convertGPS(boat.getPosition()).getY()); } }; highlightTrack.start(); // Highlight next mark only for player boat Material markColor = new PhongMaterial(new Color(0.15,0.9,0.2,1)); CompoundMark nextMark = boat.getCurrentLeg().getEndCompoundMark(); view3D.getShape(nextMark.getMark1().getSourceID()).getMesh().setMaterial(markColor); if(nextMark.getMark2() != null) { view3D.getShape(nextMark.getMark2().getSourceID()).getMesh().setMaterial(markColor); } boat.legProperty().addListener((o, prev, curr) -> Platform.runLater(() -> swapColours(curr))); } else { viewSubjects.add(boatModel); } //add sail Sails3D sails3D = new Sails3D(); Subject3D sailsSubject = new Subject3D(sails3D, 0); sails3D.setMouseTransparent(true); sails3D.setMaterial(boatColorMat); sailsSubject.setXRot(0d); sailsSubject.setHeading(visualiserRace.getVisualiserRaceState().getWindDirection().degrees()); viewSubjects.add(sailsSubject); AnimationTimer sailsFollowBoat = new AnimationTimer() { double sailCurrent = visualiserRace.getVisualiserRaceState().getWindDirection().degrees(); boolean canLuff = true; double turnRate = 5; @Override public void handle(long now) { double sailDir; //if sails are out etc if (boat.isSailsOut()) { double windDir = visualiserRace.getVisualiserRaceState().getWindDirection().degrees(); double windOffset = (360 - windDir + boat.getBearing().degrees()) % 360; sailDir = windOffset / 180 * 270 + windDir + 180; boolean leftOfWind = windOffset >= 180; if (leftOfWind){ sailDir = -sailDir; } else { sailDir = windDir - sailDir; } } else { sailDir = visualiserRace.getVisualiserRaceState().getWindDirection().degrees(); } //get new place to move towards double compA = ((sailCurrent - sailDir) % 360 + 360) % 360;//degrees right if (compA > 180) compA = 360 - compA; double compB = ((sailDir - sailCurrent) % 360 + 360) % 360;//degrees left if (compB > compA){ if (compA > turnRate){ sailCurrent = ((sailCurrent - turnRate) % 360 + 360) % 360; canLuff = false; } else { sailCurrent = sailDir; canLuff = true; } } else { if (compB > turnRate){ sailCurrent = ((sailCurrent + turnRate) % 360 + 360) % 360; canLuff = false; } else { sailCurrent = sailDir; canLuff = true; } } sailsSubject.setHeading(sailCurrent); if (canLuff) { if (boat.isSailsOut()) { if (sails3D.isLuffing()) { sails3D.stopLuffing(); } } else { if (!sails3D.isLuffing()) { sails3D.startLuffing(); } } } sailsSubject.setX(gpsConverter.convertGPS(boat.getPosition()).getX()); sailsSubject.setZ(gpsConverter.convertGPS(boat.getPosition()).getY()); } }; sailsFollowBoat.start(); // 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(); Subject3D shockwave = new Shockwave(10); viewSubjects.add(shockwave); boat.hasCollidedProperty().addListener((o, prev, curr) -> Platform.runLater(() -> showCollision(boat, shockwave))); } // Fix initial bird's-eye position view3D.updatePivot(new Translate(250, 0, 210)); view3D.targetProperty().addListener((o, prev, curr)-> { if(curr != null && visualiserRace.getVisualiserRaceState().isVisualiserBoat(curr.getSourceID())) { addThirdPersonAnnotations(curr); } else { removeThirdPersonAnnotations(); } }); // 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": //Check if race is a tutorial if (isTutorial) { //Check if the current tutorial state is zoom-in if (currentState.equals(TutorialState.ZOOMIN)) { try { //Update tutorial checkTutorialState(); } catch (Exception e1) { e1.printStackTrace(); } } } view3D.zoomIn(); break; case "Zoom Out": //Check if race is a tutorial if(isTutorial) { //Check if current tutorial state is zoom-out if (currentState.equals(TutorialState.ZOOMOUT)) { try { //Update tutorial checkTutorialState(); } catch (Exception e1) { e1.printStackTrace(); } } } view3D.zoomOut(); break; } } }); } private void showCollision(VisualiserBoat boat, Subject3D shockwave) { Subject3D boatModel = view3D.getShape(boat.getSourceID()); AnimationTimer shockFront = new AnimationTimer() { double opacity = 1; @Override public void handle(long now) { shockwave.setX(boatModel.getPosition().getX()); shockwave.setY(boatModel.getPosition().getY()); shockwave.setZ(boatModel.getPosition().getZ()); if(opacity <= 0) { shockwave.getMesh().setMaterial(new PhongMaterial(Color.TRANSPARENT)); boat.setHasCollided(false); this.stop(); } else { shockwave.getMesh().setMaterial(new PhongMaterial(new Color(1,0,0,opacity))); opacity -= 0.1; } } }; shockFront.start(); } private void addThirdPersonAnnotations(Subject3D subject3D) { nextMarkController.show3d(); // viewSubjects.add(nextMarkArrow); // final VisualiserBoat boat; // try { // boat = visualiserRace.getVisualiserRaceState().getBoat(subject3D.getSourceID()); // } catch (BoatNotFoundException e) { // e.printStackTrace(); // return; // } // arrowToNextMark = new AnimationTimer() { // @Override // public void handle(long now) { // CompoundMark target = boat.getCurrentLeg().getEndCompoundMark(); // Bearing headingToMark = GPSCoordinate.calculateBearing(boat.getPosition(), target.getAverageGPSCoordinate()); // // nextMarkArrow.setX(view3D.getPivot().getX()); // nextMarkArrow.setY(view3D.getPivot().getY()); // nextMarkArrow.setZ(view3D.getPivot().getZ() + 15); // nextMarkArrow.setHeading(headingToMark.degrees()); // } // }; // arrowToNextMark.start(); } private void removeThirdPersonAnnotations() { nextMarkController.show2d(); } /** * Swap the colour of the next mark to pass with the last mark passed * @param leg boat has started on */ private void swapColours(Leg leg) { CompoundMark start = leg.getStartCompoundMark(); CompoundMark end = leg.getEndCompoundMark(); //The last leg "finish" doesn't have compound marks. if (start == null || end == null ) { return; } Shape3D start1 = view3D.getShape(start.getMark1().getSourceID()).getMesh(); Shape3D end1 = view3D.getShape(end.getMark1().getSourceID()).getMesh(); Material nextMark = start1.getMaterial(); Material lastMark = end1.getMaterial(); start1.setMaterial(lastMark); if(start.getMark2() != null) { Shape3D start2 = view3D.getShape(start.getMark2().getSourceID()).getMesh(); start2.setMaterial(lastMark); } end1.setMaterial(nextMark); if(end.getMark2() != null) { Shape3D end2 = view3D.getShape(end.getMark2().getSourceID()).getMesh(); end2.setMaterial(nextMark); } } /** * 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. */ private 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 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. * @throws IOException Thrown if the finish scene cannot be loaded. */ private 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 { 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(); } /** * Animation timer loop for the speedometer */ private void speedometerLoop(){ new AnimationTimer(){ @Override public void handle(long arg0){ if (raceState.getRaceStatusEnum() == RaceStatusEnum.FINISHED) { stop(); // stop the timer } else { try { //Set the current speed value of the boat gauge.setValue(raceState.getBoat(raceState.getPlayerBoatID()).getCurrentSpeed()); fGauge.getGauge().setValue(raceState.getBoat(raceState.getPlayerBoatID()).getCurrentSpeed()); //Create list with sorted boat placements List boatList = boatInfoTable.getItems(); for (VisualiserBoat boat : boatList){ if(raceState.getPlayerBoatID()==boat.getSourceID()){ //Set boat current placement value as title of speedometer gauge.titleProperty().setValue("Position: " + (boatInfoTable.getItems().indexOf(boat)+1)); fGauge.getGauge().titleProperty().setValue("Position: " + (boatInfoTable.getItems().indexOf(boat)+1)); } } } catch (BoatNotFoundException e) { e.printStackTrace(); } } } }.start(); } /** * toggles if the info table is shown */ private void toggleTable() { infoWrapper.setVisible(infoTableShow); boatInfoTable.refresh(); infoTableShow = !infoTableShow; } /** * Get the next tutorial state */ private void updateTutorialState(){ //Next tutorial state is popped from list currentState = tutorialStates.get(0); tutorialStates.remove(0); } /** * Search key map for key given string of command * @param command the command of the key */ private void searchMapForKey(String command){ //For loop through keyFactory for (Map.Entry entry: keyFactory.getKeyState().entrySet()){ if(entry.getValue().toString().equals(command)){ //Found next key required to press keyToPress = entry.getKey(); } } } /** * Updates tutorial state and gui display for tutorial text * @throws Exception Exception thrown */ private void checkTutorialState() throws Exception { //Switch statement to check what the current tutorial state is switch (currentState){ case UPWIND: //Set next key to press as the downwind key searchMapForKey("Downwind"); //Update tutorial text tutorialText.setText("Nice! To turn downwind press " + keyToPress + "."); updateTutorialState(); break; case DOWNWIND: //Set next key to press as the tack/gybe key searchMapForKey("Tack/Gybe"); //Update tutorial text tutorialText.setText("Nice! To tack or gybe press " + keyToPress + "."); updateTutorialState(); break; case TACKGYBE: //Set next key to press as the VMG key searchMapForKey("VMG"); //Update tutorial text tutorialText.setText("Nice! To use VMG press " + keyToPress + ". This will turn the boat."); updateTutorialState(); break; case VMG: //Set next key to press as the sails-in key searchMapForKey("Toggle Sails"); //Update tutorial text tutorialText.setText("Nice! To sails in press " + keyToPress + ". This will stop the boat."); updateTutorialState(); break; case SAILSIN: //Set next key to press as the sails-out key searchMapForKey("Toggle Sails"); //Update tutorial text tutorialText.setText("Nice! To sails out press " + keyToPress + " again. The will start moving again."); updateTutorialState(); break; case SAILSOUT: //Set next key to press as the zoom-in key searchMapForKey("Zoom In"); //Update tutorial text tutorialText.setText("Nice! To zoom in press " + keyToPress + "."); updateTutorialState(); break; case ZOOMIN: //Set next key to press as the zoom-out key searchMapForKey("Zoom Out"); //Update tutorial text tutorialText.setText("Nice! You will also be able to zoom into boats and marks by clicking them. \nTo zoom out press " + keyToPress + "."); updateTutorialState(); break; case ZOOMOUT: //Finished tutorial. Display pop-up for exiting the tutorial tutorialText.setText("Congratulations! You're done!"); Alert alert = new Alert(Alert.AlertType.INFORMATION); alert.setTitle("Finished Tutorial"); alert.setHeaderText("You have finished the tutorial."); alert.setContentText("Now you know the controls you are ready to race!"); Optional result = alert.showAndWait(); if (result.get() == ButtonType.OK) { App.game.endEvent(); loadTitleScreen(); } break; default: //State not found. Exit tutorial to title menu App.game.endEvent(); loadTitleScreen(); break; } } /** * Initialises the map */ private void initialiseRaceCanvas() { //Create canvas. raceCanvas = new ResizableRaceCanvas(raceState); //Set properties. raceCanvas.setMouseTransparent(true); raceCanvas.widthProperty().bind(canvasBase1.widthProperty()); raceCanvas.heightProperty().bind(canvasBase1.heightProperty()); //Draw it and show it. raceCanvas.draw(); raceCanvas.setVisible(true); //Add to scene. canvasBase1.getChildren().add(0, raceCanvas); } private void bigMap(){ if (mapToggle){ raceCanvas.widthProperty().bind(canvasBase.widthProperty()); raceCanvas.heightProperty().bind(canvasBase.heightProperty()); raceCanvas.setFullScreen(true); raceCanvas.setOpacity(0.6); canvasBase1.getChildren().remove(raceCanvas); canvasBase.getChildren().add(1, raceCanvas); }else{ raceCanvas.widthProperty().bind(canvasBase1.widthProperty()); raceCanvas.heightProperty().bind(canvasBase1.heightProperty()); raceCanvas.setFullScreen(false); canvasBase.getChildren().remove(raceCanvas); canvasBase1.getChildren().add(0, raceCanvas); } mapToggle = !mapToggle; } }