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.

827 lines
30 KiB

package visualiser.Controllers;
import com.interactivemesh.jfx.importer.stl.StlMeshImporter;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
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.paint.Material;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.Shape3D;
import javafx.scene.shape.Sphere;
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.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;
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;
private TutorialState currentState;
private ArrayList<TutorialState> tutorialStates;
private boolean isTutorial = false;
private String keyToPress;
/**
* state of the info table
*/
private boolean infoTableShow;
private View3D view3D;
private ObservableList<Subject3D> viewSubjects;
private Subject3D nextMarkArrow;
private ChangeListener<? super GPSCoordinate> pointToMark;
/**
* The arrow controller.
*/
@FXML private ArrowController arrowController;
@FXML private GridPane canvasBase;
@FXML private SplitPane racePane;
@FXML private Label tutorialText;
/**
* 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() {
this.nextMarkArrow = new Annotation3D(new Box(1,3,0));
}
@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
//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, "RaceController was interrupted on thread: " + Thread.currentThread() + "while sending: " + controlKey, e);
} catch (Exception e) {
e.printStackTrace();
}
}
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(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 = 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);
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()) {
MeshView mesh;
if(boat.getSourceID() == race.getVisualiserRaceState().getPlayerBoatID()) {
mesh = new MeshView(importer.getImport());
} else {
mesh = new MeshView(importerBurgerBoat.getImport());
}
Subject3D boatModel = new Subject3D(mesh, boat.getSourceID());
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();
Material markColor = new PhongMaterial(new Color(0.15,0.9,0.2,1));
CompoundMark nextMark = boat.getCurrentLeg().getEndCompoundMark();
view3D.getShape(nextMark.getMark1().getSourceID()).setMaterial(markColor);
if(nextMark.getMark2() != null) {
view3D.getShape(nextMark.getMark2().getSourceID()).setMaterial(markColor);
}
boat.legProperty().addListener((o, prev, curr) -> swapColours(curr));
}
// 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(prev);
}
});
// 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.updateDistance(-10);
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.updateDistance(10);
break;
}
}
});
}
private void addThirdPersonAnnotations(Subject3D subject3D) {
viewSubjects.add(nextMarkArrow);
try {
VisualiserBoat boat = visualiserRace.getVisualiserRaceState().getBoat(subject3D.getSourceID());
this.pointToMark = (o, prev, curr) -> {
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() + 10);
nextMarkArrow.setHeading(headingToMark.degrees());
};
boat.positionProperty().addListener(pointToMark);
} catch (BoatNotFoundException e) {
e.printStackTrace();
}
}
private void removeThirdPersonAnnotations(Subject3D subject3D) {
viewSubjects.remove(nextMarkArrow);
try {
VisualiserBoat boat = visualiserRace.getVisualiserRaceState().getBoat(subject3D.getSourceID());
boat.positionProperty().removeListener(pointToMark);
} catch (BoatNotFoundException e) {
e.printStackTrace();
}
}
/**
* 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();
Shape3D start1 = view3D.getShape(start.getMark1().getSourceID());
Shape3D end1 = view3D.getShape(end.getMark1().getSourceID());
Material nextMark = start1.getMaterial();
Material lastMark = end1.getMaterial();
start1.setMaterial(lastMark);
if(start.getMark2() != null) {
Shape3D start2 = view3D.getShape(start.getMark2().getSourceID());
start2.setMaterial(lastMark);
}
end1.setMaterial(nextMark);
if(end.getMark2() != null) {
Shape3D end2 = view3D.getShape(end.getMark2().getSourceID());
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.
* @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;
//Check if the game is a tutorial
if (parent.getGameType()==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);
}
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;
}
/**
* 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<String, ControlKey> 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. To 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<ButtonType> result = alert.showAndWait();
if (result.get() == ButtonType.OK) {
parent.endEvent();
racePane.setVisible(false);
App.app.showMainStage(App.getStage());
}
break;
default:
//State not found. Exit tutorial to title menu
parent.endEvent();
racePane.setVisible(false);
App.app.showMainStage(App.getStage());
break;
}
}
}