You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
563 lines
19 KiB
563 lines
19 KiB
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<Subject3D> 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<VisualiserBoat> boatInfoTable;
|
|
@FXML private TableColumn<VisualiserBoat, String> boatPlacingColumn;
|
|
@FXML private TableColumn<VisualiserBoat, String> boatTeamColumn;
|
|
@FXML private TableColumn<VisualiserBoat, Leg> boatMarkColumn;
|
|
@FXML private TableColumn<VisualiserBoat, Number> boatSpeedColumn;
|
|
@FXML private LineChart<Number, Number> 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<ButtonType> 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<ButtonType> 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<VisualiserBoat> boats = FXCollections.observableArrayList(race.getVisualiserRaceState().getBoats());
|
|
SortedList<VisualiserBoat> sortedBoats = new SortedList<>(boats);
|
|
sortedBoats.comparatorProperty().bind(boatInfoTable.comparatorProperty());
|
|
|
|
//Update copy when original changes.
|
|
race.getVisualiserRaceState().getBoats().addListener((ListChangeListener.Change<? extends VisualiserBoat> 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<TableColumn<VisualiserBoat, Number>, TableCell<VisualiserBoat, Number>>() {
|
|
|
|
//Callback function.
|
|
@Override
|
|
public TableCell<VisualiserBoat, Number> call(TableColumn<VisualiserBoat, Number> param) {
|
|
//We return a table cell that populates itself with a Number, and formats it.
|
|
return new TableCell<VisualiserBoat, Number>(){
|
|
|
|
//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<TableColumn<VisualiserBoat, Leg>, TableCell<VisualiserBoat, Leg>>() {
|
|
|
|
//Callback function.
|
|
@Override
|
|
public TableCell<VisualiserBoat, Leg> call(TableColumn<VisualiserBoat, Leg> param) {
|
|
//We return a table cell that populates itself with a Leg's name.
|
|
return new TableCell<VisualiserBoat, Leg>(){
|
|
|
|
//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<VisualiserBoat> 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;
|
|
}
|
|
|
|
}
|