Merge branch 'master' into story70
# Conflicts: # racevisionGame/src/main/java/visualiser/Controllers/HostController.java # racevisionGame/src/main/java/visualiser/Controllers/TitleController.java # racevisionGame/src/main/java/visualiser/app/App.java
This commit is contained in:
@@ -71,8 +71,10 @@ public class FinishController extends Controller {
|
||||
|
||||
|
||||
//Winner label.
|
||||
raceWinnerLabel.setText("Winner: "+ boatNameColumn.getCellObservableValue(0).getValue());
|
||||
raceWinnerLabel.setWrapText(true);
|
||||
if (boats.size() > 0) {
|
||||
raceWinnerLabel.setText("Winner: " + boatNameColumn.getCellObservableValue(0).getValue());
|
||||
raceWinnerLabel.setWrapText(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
package visualiser.Controllers;
|
||||
|
||||
import com.interactivemesh.jfx.importer.stl.StlMeshImporter;
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.SplitPane;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.shape.MeshView;
|
||||
import mock.app.Event;
|
||||
import org.xml.sax.SAXException;
|
||||
import mock.exceptions.EventConstructionException;
|
||||
import shared.exceptions.InvalidBoatDataException;
|
||||
import shared.exceptions.InvalidRaceDataException;
|
||||
import shared.exceptions.InvalidRegattaDataException;
|
||||
import shared.exceptions.XMLReaderException;
|
||||
import visualiser.layout.Subject3D;
|
||||
import visualiser.layout.View3D;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.net.URL;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
@@ -27,28 +35,62 @@ public class HostController extends Controller {
|
||||
|
||||
|
||||
@FXML
|
||||
TextField gameNameField;
|
||||
|
||||
@FXML
|
||||
TextField hostNameField;
|
||||
private ImageView imageView;
|
||||
|
||||
@FXML
|
||||
AnchorPane hostWrapper;
|
||||
|
||||
@FXML
|
||||
AnchorPane imagePane;
|
||||
|
||||
@FXML
|
||||
SplitPane splitPane;
|
||||
|
||||
@FXML
|
||||
AnchorPane specPane;
|
||||
|
||||
@FXML
|
||||
GridPane playerContainer;
|
||||
|
||||
private Event game;
|
||||
|
||||
private int gameType = 0;
|
||||
|
||||
private View3D view3D;
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
ObservableList<Subject3D> subjects = FXCollections.observableArrayList();
|
||||
|
||||
view3D = new View3D();
|
||||
view3D.setItems(subjects);
|
||||
playerContainer.add(view3D, 0,0);
|
||||
|
||||
URL asset = HostController.class.getClassLoader().getResource("assets/V1.2 Complete Boat.stl");
|
||||
|
||||
StlMeshImporter importer = new StlMeshImporter();
|
||||
importer.read(asset);
|
||||
Subject3D subject = new Subject3D(new MeshView(importer.getImport()));
|
||||
|
||||
subjects.add(subject);
|
||||
|
||||
view3D.setDistance(50);
|
||||
view3D.setYaw(45);
|
||||
view3D.setPitch(20);
|
||||
|
||||
AnimationTimer rotate = new AnimationTimer() {
|
||||
@Override
|
||||
public void handle(long now) {
|
||||
subject.setHeading(subject.getHeading().getAngle() + 0.1);
|
||||
}
|
||||
};
|
||||
rotate.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hosts a game
|
||||
* @throws IOException if socket cannot be connected to
|
||||
*/
|
||||
public void hostGamePressed() throws IOException{
|
||||
public void hostGamePressed() {
|
||||
try {
|
||||
this.game = new Event(gameType);
|
||||
connectSocket("localhost", 4942);
|
||||
@@ -83,12 +125,34 @@ public class HostController extends Controller {
|
||||
* Hosts a game.
|
||||
*/
|
||||
public void hostGame(){
|
||||
splitPane.setResizableWithParent(specPane, false);
|
||||
splitPane.lookupAll(".split-pane-divider").stream().forEach(div -> div.setMouseTransparent(true));
|
||||
imageView.fitWidthProperty().bind(imagePane.widthProperty());
|
||||
imageView.fitHeightProperty().bind(imagePane.heightProperty());
|
||||
hostWrapper.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu button pressed. Prompt alert then return to menu
|
||||
*/
|
||||
public void menuBtnPressed(){
|
||||
hostWrapper.setVisible(false);
|
||||
parent.enterTitle();
|
||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||
alert.setTitle("Quitting race");
|
||||
alert.setContentText("Do you wish to quit the race?");
|
||||
alert.setHeaderText("You are about to quit the race");
|
||||
Optional<ButtonType> result = alert.showAndWait();
|
||||
if(result.get() == ButtonType.OK){
|
||||
hostWrapper.setVisible(false);
|
||||
parent.enterTitle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start button pressed. Currently only prints out start
|
||||
*/
|
||||
public void startBtnPressed(){
|
||||
//System.out.println("Should start the race. This button is only visible for the host");
|
||||
hostGamePressed();
|
||||
}
|
||||
|
||||
public void setGameType(int gameType){
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
package visualiser.Controllers;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import visualiser.gameController.Keys.ControlKey;
|
||||
import visualiser.gameController.Keys.KeyFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static visualiser.app.App.keyFactory;
|
||||
|
||||
/**
|
||||
* Controller for the scene used to display and update current key bindings.
|
||||
*/
|
||||
public class KeyBindingsController {
|
||||
private @FXML Button btnSave;
|
||||
private @FXML Button btnCancel;
|
||||
private @FXML Button btnReset;
|
||||
private @FXML ListView lstControl;
|
||||
private @FXML ListView lstKey;
|
||||
private @FXML ListView lstDescription;
|
||||
private @FXML AnchorPane anchor;
|
||||
private KeyFactory newKeyFactory;
|
||||
private Boolean changed = false; // keyBindings have been modified
|
||||
private Button currentButton = null; // last button clicked
|
||||
|
||||
public void initialize(){
|
||||
// create new key factory to modify, keeping the existing one safe
|
||||
newKeyFactory = copyExistingFactory();
|
||||
initializeTable();
|
||||
populateTable();
|
||||
setKeyListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up table before populating it.
|
||||
* Set up includes headings, CSS styling and modifying default properties.
|
||||
*/
|
||||
public void initializeTable(){
|
||||
// set the headings for each column
|
||||
lstKey.getItems().add("Key");
|
||||
lstControl.getItems().add("Command");
|
||||
lstDescription.getItems().add("Description");
|
||||
lstKey.getSelectionModel().select(0);
|
||||
lstControl.getSelectionModel().select(0);
|
||||
lstDescription.getSelectionModel().select(0);
|
||||
|
||||
// add CSS stylesheet once the scene has been created
|
||||
lstKey.sceneProperty().addListener((obs, oldScene, newScene) -> {
|
||||
if (newScene != null) {
|
||||
newScene.getStylesheets().add("/css/keyBindings.css");
|
||||
}
|
||||
});
|
||||
|
||||
// stop the columns from being selectable, so only the buttons are
|
||||
lstKey.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldvalue, newValue) ->
|
||||
Platform.runLater(() ->
|
||||
lstKey.getSelectionModel().select(0)));
|
||||
lstDescription.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldvalue, newValue) ->
|
||||
Platform.runLater(() ->
|
||||
lstDescription.getSelectionModel().select(0)));
|
||||
lstControl.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldvalue, newValue) ->
|
||||
Platform.runLater(() ->
|
||||
lstControl.getSelectionModel().select(0)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the table with commands and their key binding details.
|
||||
*/
|
||||
public void populateTable(){
|
||||
// add each command to the table
|
||||
for (Map.Entry<String, ControlKey> entry : newKeyFactory.getKeyState().entrySet()) {
|
||||
// create button for command
|
||||
Button button = new Button(entry.getKey());
|
||||
button.setMinWidth(120);
|
||||
button.setId(entry.getValue().toString());
|
||||
button.setOnAction(e -> currentButton = button);
|
||||
// display details for command in table
|
||||
lstControl.getItems().add(entry.getValue());
|
||||
lstKey.getItems().add(button);
|
||||
lstDescription.getItems().add(entry.getValue().getProtocolCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of the {@link KeyFactory} that does not modify the original.
|
||||
* @return new keyfactory to be modified
|
||||
*/
|
||||
public KeyFactory copyExistingFactory(){
|
||||
newKeyFactory = new KeyFactory();
|
||||
Map<String, ControlKey> oldKeyState = keyFactory.getKeyState();
|
||||
Map<String, ControlKey> newKeyState = new HashMap<>();
|
||||
|
||||
// copy over commands and their keys
|
||||
for (Map.Entry<String, ControlKey> entry : oldKeyState.entrySet()){
|
||||
newKeyState.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
newKeyFactory.setKeyState(newKeyState);
|
||||
return newKeyFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a listener for the base anchorpane for key presses.
|
||||
* It updates the current key bindings of the {@link KeyFactory} if
|
||||
* required.
|
||||
*/
|
||||
public void setKeyListener(){
|
||||
anchor.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
// if esc, cancel current button click
|
||||
if (event.getCode() == KeyCode.ESCAPE){
|
||||
btnCancel.requestFocus();
|
||||
currentButton = null;
|
||||
}
|
||||
// if a button was clicked
|
||||
else if (currentButton != null) {
|
||||
// check if a button is already mapped to this key
|
||||
for (int i = 1; i < lstKey.getItems().size(); i++) {
|
||||
Button button = (Button)lstKey.getItems().get(i);
|
||||
// update buttons text and remove key binding from command
|
||||
if (button.getText().equals(event.getCode().toString())) {
|
||||
button.setText("");
|
||||
newKeyFactory.updateKey(button.getId(), button.getId());
|
||||
}
|
||||
}
|
||||
// update text on the button
|
||||
currentButton.setText(event.getCode().toString());
|
||||
// update the control key
|
||||
newKeyFactory.updateKey(event.getCode().toString(),
|
||||
currentButton.getId());
|
||||
// remove current button selection
|
||||
currentButton = null;
|
||||
changed = true;
|
||||
btnCancel.requestFocus();
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel and exits the key bindings menu. Changes are not forced to be
|
||||
* saved or fixed if invalid, and instead are defaulted back to the last
|
||||
* successful saved state.
|
||||
*/
|
||||
public void cancel(){
|
||||
((Stage)btnCancel.getScene().getWindow()).close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all key bindings to the built-in defaults.
|
||||
*/
|
||||
public void reset(){
|
||||
lstKey.getItems().clear();
|
||||
lstControl.getItems().clear();
|
||||
lstDescription.getItems().clear();
|
||||
newKeyFactory = new KeyFactory();
|
||||
initializeTable();
|
||||
populateTable();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace existing {@link KeyFactory} with the modified key bindings.
|
||||
*/
|
||||
public void save(){
|
||||
if (isFactoryValid()) {
|
||||
keyFactory = newKeyFactory;
|
||||
newKeyFactory = new KeyFactory();
|
||||
changed = false;
|
||||
keyFactory.save(); // save persistently
|
||||
loadNotification("Key bindings were successfully saved.", false);
|
||||
} else {
|
||||
loadNotification("One or more key bindings are missing. " +
|
||||
"Failed to save.", true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the {@link KeyFactory} being modified is valid and that no
|
||||
* commands are missing a key binding.
|
||||
* @return True if valid, false if invalid
|
||||
*/
|
||||
public Boolean isFactoryValid(){
|
||||
for (Map.Entry<String, ControlKey> entry : newKeyFactory.getKeyState().entrySet
|
||||
()) {
|
||||
if (entry.getKey().toString()==entry.getValue().toString()){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method used to stop a user from exiting key bindings without saving
|
||||
* their changes to the {@link KeyFactory}.
|
||||
* @param we {@link WindowEvent} close request to be consumed if settings
|
||||
* have not been successfully saved.
|
||||
*/
|
||||
public void onExit(WindowEvent we){
|
||||
// if modified KeyFactory hasn't been saved
|
||||
if (changed){
|
||||
loadNotification("Please cancel or save your changes before exiting" +
|
||||
".", true);
|
||||
we.consume();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a popup window giving confirmation/warning of user activity.
|
||||
* @param message the message to be displayed to the user
|
||||
* @param warning true if the message to be displayed is due to user error
|
||||
*/
|
||||
public void loadNotification(String message, Boolean warning){
|
||||
Parent root = null;
|
||||
FXMLLoader loader = new FXMLLoader(getClass().getResource
|
||||
("/visualiser/scenes/notification.fxml"));
|
||||
try {
|
||||
root = loader.load();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
NotificationController controller = loader.getController();
|
||||
Stage stage = new Stage();
|
||||
stage.setScene(new Scene(root));
|
||||
stage.centerOnScreen();
|
||||
stage.initModality(Modality.APPLICATION_MODAL);
|
||||
stage.show();
|
||||
// displays given message in the window
|
||||
controller.setMessage(message, warning);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import java.util.ResourceBundle;
|
||||
public class LobbyController extends Controller {
|
||||
|
||||
@FXML
|
||||
AnchorPane lobbyWrapper;
|
||||
private AnchorPane lobbyWrapper;
|
||||
@FXML
|
||||
private TableView<RaceConnection> lobbyTable;
|
||||
@FXML
|
||||
|
||||
@@ -3,7 +3,6 @@ package visualiser.Controllers;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import visualiser.app.App;
|
||||
import visualiser.gameController.ControllerClient;
|
||||
import visualiser.model.VisualiserBoat;
|
||||
import visualiser.model.VisualiserRaceEvent;
|
||||
@@ -40,6 +39,7 @@ public class MainController extends Controller {
|
||||
* Transitions from the StartController screen (displays pre-race information) to the RaceController (displays the actual race).
|
||||
* @param visualiserRace The object modelling the race.
|
||||
* @param controllerClient Socket Client that manipulates the controller.
|
||||
* @param isHost if the client is the host of a race or not.
|
||||
*/
|
||||
public void beginRace(VisualiserRaceEvent visualiserRace, ControllerClient controllerClient, Boolean isHost) {
|
||||
raceController.startRace(visualiserRace, controllerClient, isHost);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package visualiser.Controllers;
|
||||
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
/**
|
||||
* Controller for a popup notification regarding user activity.
|
||||
*/
|
||||
public class NotificationController {
|
||||
private @FXML Label lblDescription;
|
||||
private @FXML Text txtMessage;
|
||||
|
||||
/**
|
||||
* Closes the popup window once clicked.
|
||||
*/
|
||||
public void ok(){
|
||||
((Stage)lblDescription.getScene().getWindow()).close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the appropriate popup notification.
|
||||
* @param message message for the user
|
||||
* @param warning if true warning text shown, if false success text shown
|
||||
*/
|
||||
public void setMessage(String message, Boolean warning){
|
||||
lblDescription.setText(message);
|
||||
if (!warning){
|
||||
txtMessage.setText("Success!");
|
||||
txtMessage.setFill(Color.GREEN);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package visualiser.Controllers;
|
||||
|
||||
|
||||
import com.interactivemesh.jfx.importer.stl.StlMeshImporter;
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -10,21 +10,27 @@ import javafx.collections.transformation.SortedList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.chart.LineChart;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.input.ScrollEvent;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.shape.MeshView;
|
||||
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.model.Leg;
|
||||
import shared.model.Mark;
|
||||
import visualiser.app.App;
|
||||
import visualiser.gameController.ControllerClient;
|
||||
import visualiser.gameController.Keys.ControlKey;
|
||||
import visualiser.gameController.Keys.KeyFactory;
|
||||
import visualiser.layout.Subject3D;
|
||||
import visualiser.layout.View3D;
|
||||
import visualiser.model.*;
|
||||
import visualiser.utils.GPSConverter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
@@ -33,59 +39,41 @@ 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;
|
||||
|
||||
/**
|
||||
* The canvas that draws the race.
|
||||
*/
|
||||
private ResizableRaceCanvas raceCanvas;
|
||||
|
||||
/**
|
||||
* The sparkline graph.
|
||||
*/
|
||||
private Sparkline sparkline;
|
||||
|
||||
/**
|
||||
* 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 race;
|
||||
|
||||
/**
|
||||
* This is the root node of the arrow control.
|
||||
*/
|
||||
@FXML private Pane arrow;
|
||||
@FXML private SplitPane racePane;
|
||||
|
||||
/**
|
||||
* This is the pane we place the actual arrow control inside of.
|
||||
@@ -101,9 +89,6 @@ public class RaceController extends Controller {
|
||||
@FXML private TableColumn<VisualiserBoat, Leg> boatMarkColumn;
|
||||
@FXML private TableColumn<VisualiserBoat, Number> boatSpeedColumn;
|
||||
@FXML private LineChart<Number, Number> sparklineChart;
|
||||
@FXML private AnchorPane annotationPane;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Ctor.
|
||||
@@ -113,11 +98,10 @@ public class RaceController extends Controller {
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
KeyFactory keyFactory = KeyFactory.getFactory();
|
||||
infoTableShow = true;
|
||||
|
||||
// Initialise keyboard handler
|
||||
race.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
racePane.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
String codeString = event.getCode().toString();
|
||||
|
||||
if (codeString.equals("TAB")){toggleTable();}
|
||||
@@ -143,7 +127,7 @@ public class RaceController extends Controller {
|
||||
Optional<ButtonType> result = alert.showAndWait();
|
||||
if (result.get() == ButtonType.OK) {
|
||||
parent.endEvent();
|
||||
race.setVisible(false);
|
||||
racePane.setVisible(false);
|
||||
App.app.showMainStage(App.getStage());
|
||||
}
|
||||
} else {
|
||||
@@ -152,7 +136,7 @@ public class RaceController extends Controller {
|
||||
alert.setContentText("Do you wish to quit the race?");
|
||||
Optional<ButtonType> result = alert.showAndWait();
|
||||
if (result.get() == ButtonType.OK) {
|
||||
race.setVisible(false);
|
||||
racePane.setVisible(false);
|
||||
App.app.showMainStage(App.getStage());
|
||||
}
|
||||
}
|
||||
@@ -176,14 +160,10 @@ public class RaceController extends Controller {
|
||||
//Information table.
|
||||
initialiseInfoTable(this.visualiserRace);
|
||||
|
||||
//Sparkline.
|
||||
initialiseSparkline(this.visualiserRace);
|
||||
|
||||
//Arrow.
|
||||
initialiseArrow(this.visualiserRace);
|
||||
|
||||
//Race canvas.
|
||||
initialiseRaceCanvas(this.visualiserRace);
|
||||
initialiseView3D(this.visualiserRace);
|
||||
|
||||
//Race timezone label.
|
||||
initialiseRaceTimezoneLabel(this.visualiserRace);
|
||||
@@ -191,11 +171,79 @@ public class RaceController extends Controller {
|
||||
//Race clock.
|
||||
initialiseRaceClock(this.visualiserRace);
|
||||
|
||||
|
||||
//Start the race animation timer.
|
||||
raceTimer();
|
||||
}
|
||||
|
||||
private void initialiseView3D(VisualiserRaceEvent race) {
|
||||
viewSubjects = FXCollections.observableArrayList();
|
||||
|
||||
// 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
|
||||
view3D = new View3D();
|
||||
view3D.setDistance(1050);
|
||||
view3D.setYaw(0);
|
||||
view3D.setPitch(60);
|
||||
view3D.enableTracking();
|
||||
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);
|
||||
|
||||
view3D.setItems(viewSubjects);
|
||||
// Position and add each mark to view
|
||||
for(Mark mark: race.getVisualiserRaceState().getMarks()) {
|
||||
Subject3D subject = new Subject3D(new Sphere(2));
|
||||
subject.setX(gpsConverter.convertGPS(mark.getPosition()).getX());
|
||||
subject.setZ(gpsConverter.convertGPS(mark.getPosition()).getY());
|
||||
|
||||
viewSubjects.add(subject);
|
||||
}
|
||||
// Position and add each boat to view
|
||||
for(VisualiserBoat boat: race.getVisualiserRaceState().getBoats()) {
|
||||
MeshView mesh = new MeshView(importer.getImport());
|
||||
Subject3D subject = new Subject3D(mesh);
|
||||
viewSubjects.add(subject);
|
||||
|
||||
// Track this boat's movement with the new subject
|
||||
AnimationTimer trackBoat = new AnimationTimer() {
|
||||
@Override
|
||||
public void handle(long now) {
|
||||
subject.setHeading(boat.getBearing().degrees());
|
||||
subject.setX(gpsConverter.convertGPS(boat.getPosition()).getX());
|
||||
subject.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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@@ -338,42 +386,8 @@ public class RaceController extends Controller {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialises the {@link Sparkline}, and listens to a specified {@link VisualiserRaceEvent}.
|
||||
* @param race The race to listen to.
|
||||
*/
|
||||
private void initialiseSparkline(VisualiserRaceEvent race) {
|
||||
//The race.getBoats() we are passing in is sorted by position in race inside the race class.
|
||||
this.sparkline = new Sparkline(this.visualiserRace.getVisualiserRaceState(), this.sparklineChart);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialises the {@link ResizableRaceCanvas}, provides the race to read data from.
|
||||
* @param race Race to read data from.
|
||||
*/
|
||||
private void initialiseRaceCanvas(VisualiserRaceEvent race) {
|
||||
|
||||
//Create canvas.
|
||||
raceCanvas = new ResizableRaceCanvas(race);
|
||||
|
||||
//Set properties.
|
||||
raceCanvas.setMouseTransparent(true);
|
||||
raceCanvas.widthProperty().bind(canvasBase.widthProperty());
|
||||
raceCanvas.heightProperty().bind(canvasBase.heightProperty());
|
||||
|
||||
//Draw it and show it.
|
||||
raceCanvas.draw();
|
||||
raceCanvas.setVisible(true);
|
||||
|
||||
//Add to scene.
|
||||
canvasBase.getChildren().add(0, raceCanvas);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Intialises the race time zone label with the race's time zone.
|
||||
* 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) {
|
||||
@@ -411,11 +425,7 @@ public class RaceController extends Controller {
|
||||
initialiseRace();
|
||||
|
||||
//Display this controller.
|
||||
race.setVisible(true);
|
||||
|
||||
|
||||
// set up annotation displays
|
||||
new Annotations(annotationPane, raceCanvas);
|
||||
racePane.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -423,7 +433,7 @@ public class RaceController extends Controller {
|
||||
* @param boats boats there are in the race.
|
||||
*/
|
||||
public void finishRace(ObservableList<VisualiserBoat> boats) {
|
||||
race.setVisible(false);
|
||||
racePane.setVisible(false);
|
||||
parent.enterFinish(boats);
|
||||
}
|
||||
|
||||
@@ -458,18 +468,13 @@ public class RaceController extends Controller {
|
||||
finishRace(visualiserRace.getVisualiserRaceState().getBoats());
|
||||
|
||||
} else {
|
||||
//Otherwise, render the canvas.
|
||||
raceCanvas.drawRace();
|
||||
|
||||
|
||||
//Sort the tableview. Doesn't automatically work for all columns.
|
||||
boatInfoTable.sort();
|
||||
|
||||
}
|
||||
|
||||
//Return to main screen if we lose connection.
|
||||
if (!visualiserRace.getServerConnection().isAlive()) {
|
||||
race.setVisible(false);
|
||||
racePane.setVisible(false);
|
||||
//parent.enterTitle();
|
||||
try {
|
||||
App.app.showMainStage(App.getStage());
|
||||
@@ -491,10 +496,10 @@ public class RaceController extends Controller {
|
||||
* toggles if the info table is shown
|
||||
*/
|
||||
private void toggleTable() {
|
||||
double tablePercent = 1 - (boatPlacingColumn.getPrefWidth() + boatTeamColumn.getPrefWidth() + boatMarkColumn.getPrefWidth() + boatSpeedColumn.getPrefWidth())/race.getWidth();
|
||||
double tablePercent = 1 - (boatPlacingColumn.getPrefWidth() + boatTeamColumn.getPrefWidth() + boatMarkColumn.getPrefWidth() + boatSpeedColumn.getPrefWidth())/racePane.getWidth();
|
||||
|
||||
if (infoTableShow){
|
||||
race.setDividerPositions(tablePercent);
|
||||
racePane.setDividerPositions(tablePercent);
|
||||
|
||||
arrowPane.setScaleX(0.5);
|
||||
arrowPane.setScaleY(0.5);
|
||||
@@ -502,7 +507,7 @@ public class RaceController extends Controller {
|
||||
arrowPane.setTranslateY(0 - arrowPane.getScene().getHeight()/4);
|
||||
|
||||
}else{
|
||||
race.setDividerPositions(1);
|
||||
racePane.setDividerPositions(1);
|
||||
|
||||
arrowPane.setScaleX(1);
|
||||
arrowPane.setScaleY(1);
|
||||
|
||||
@@ -225,12 +225,10 @@ public class StartController extends Controller {
|
||||
//Get the current race status.
|
||||
RaceStatusEnum raceStatus = visualiserRaceEvent.getVisualiserRaceState().getRaceStatusEnum();
|
||||
|
||||
//Display it.
|
||||
raceStatusLabel.setText("Race Status: " + raceStatus.name());
|
||||
|
||||
|
||||
//If the race has reached the preparatory phase, or has started...
|
||||
if (raceStatus == RaceStatusEnum.PREPARATORY || raceStatus == RaceStatusEnum.STARTED) {
|
||||
if (raceStatus == RaceStatusEnum.WARNING
|
||||
|| raceStatus == RaceStatusEnum.PREPARATORY
|
||||
|| raceStatus == RaceStatusEnum.STARTED) {
|
||||
//Stop this timer.
|
||||
stop();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package visualiser.Controllers;
|
||||
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
@@ -15,6 +16,7 @@ import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import mock.app.Event;
|
||||
import mock.exceptions.EventConstructionException;
|
||||
import javafx.stage.WindowEvent;
|
||||
import visualiser.app.App;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -110,18 +112,26 @@ public class TitleController extends Controller {
|
||||
* Called when control button is pressed. New pop up window displaying controls
|
||||
*/
|
||||
public void controlBtnPressed(){
|
||||
FXMLLoader loader = new FXMLLoader();
|
||||
loader.setLocation(getClass().getResource("/visualiser/scenes/controls.fxml"));
|
||||
Parent layout;
|
||||
try {
|
||||
layout = loader.load();
|
||||
FXMLLoader loader = new FXMLLoader();
|
||||
loader.setLocation(getClass().getResource("/visualiser/scenes/keyBindings.fxml"));
|
||||
Parent layout = loader.load();
|
||||
Scene scene = new Scene(layout);
|
||||
Stage popupStage = new Stage();
|
||||
popupStage.setResizable(false);
|
||||
popupStage.setTitle("Game Controls");
|
||||
popupStage.initModality(Modality.WINDOW_MODAL);
|
||||
popupStage.centerOnScreen();
|
||||
popupStage.setScene(scene);
|
||||
popupStage.showAndWait();
|
||||
popupStage.show();
|
||||
KeyBindingsController controller = loader.getController();
|
||||
popupStage.setOnCloseRequest(new EventHandler<WindowEvent>() {
|
||||
public void handle(WindowEvent we) {
|
||||
if (we.getEventType() == WindowEvent.WINDOW_CLOSE_REQUEST) {
|
||||
controller.onExit(we);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
@@ -29,19 +29,17 @@ import javafx.stage.StageStyle;
|
||||
import javafx.stage.WindowEvent;
|
||||
import javafx.util.Duration;
|
||||
import visualiser.Controllers.MainController;
|
||||
|
||||
import java.io.IOException;
|
||||
import visualiser.gameController.Keys.KeyFactory;
|
||||
|
||||
public class App extends Application {
|
||||
|
||||
|
||||
private static Stage stage;
|
||||
private Pane splashLayout;
|
||||
private ProgressBar loadProgress;
|
||||
private Label progressText;
|
||||
private static final int SPLASH_WIDTH = 676;
|
||||
private static final int SPLASH_HEIGHT = 227;
|
||||
|
||||
public static KeyFactory keyFactory = new KeyFactory();
|
||||
public static App app;
|
||||
|
||||
/**
|
||||
@@ -55,6 +53,9 @@ public class App extends Application {
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
// load the user's personalised key bindings
|
||||
keyFactory.load();
|
||||
|
||||
ImageView splash = new ImageView(new Image(
|
||||
getClass().getClassLoader().getResourceAsStream("images/splashScreen.png")
|
||||
));
|
||||
|
||||
@@ -3,10 +3,11 @@ package visualiser.gameController;
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.scene.Scene;
|
||||
import visualiser.gameController.Keys.ControlKey;
|
||||
import visualiser.gameController.Keys.KeyFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import static visualiser.app.App.keyFactory;
|
||||
|
||||
/**
|
||||
* Class for checking what keys are currently being used
|
||||
*/
|
||||
@@ -18,7 +19,7 @@ public class InputChecker {
|
||||
* @param scene Scene the controller is to run in parallel with.
|
||||
*/
|
||||
public void runWithScene(Scene scene){
|
||||
KeyFactory keyFactory = KeyFactory.getFactory();
|
||||
// KeyFactory keyFactory = KeyFactory.getFactory();
|
||||
|
||||
scene.setOnKeyPressed(event -> {
|
||||
String codeString = event.getCode().toString();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package visualiser.gameController.Keys;
|
||||
|
||||
import javafx.scene.input.KeyCode;
|
||||
import network.Messages.Enums.BoatActionEnum;
|
||||
|
||||
/**
|
||||
@@ -45,7 +44,7 @@ public abstract class ControlKey {
|
||||
/**
|
||||
* What this key should do when the command is issued for it to do its job.
|
||||
*/
|
||||
public abstract void onAction();//may want to make it take in a visualiser and stuff in the future.
|
||||
public abstract void onAction();
|
||||
|
||||
/**
|
||||
* What to do when the key is held
|
||||
|
||||
@@ -9,11 +9,9 @@ public class DownWindKey extends ControlKey {
|
||||
|
||||
/**
|
||||
* Constructor for Control
|
||||
* @param name name of the key
|
||||
*
|
||||
*/
|
||||
public DownWindKey(String name) {
|
||||
super(name, BoatActionEnum.DOWNWIND);
|
||||
public DownWindKey() {
|
||||
super("Downwind", BoatActionEnum.DOWNWIND);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package visualiser.gameController.Keys;
|
||||
|
||||
import java.beans.XMLDecoder;
|
||||
import java.beans.XMLEncoder;
|
||||
import java.io.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -13,30 +16,17 @@ public class KeyFactory {
|
||||
private Map<String, ControlKey> keyState;
|
||||
|
||||
/**
|
||||
* Singleton instance to enforce consistent key state
|
||||
* Constructor for key state, set up initial state of each action.
|
||||
*/
|
||||
private static KeyFactory theFactory = new KeyFactory();
|
||||
|
||||
/**
|
||||
* Singleton constructor for key state, set up initial state of each action.
|
||||
*/
|
||||
private KeyFactory() {
|
||||
public KeyFactory() {
|
||||
this.keyState = new HashMap<>();
|
||||
keyState.put("Z", new ZoomInKey("Zoom In"));
|
||||
keyState.put("X", new ZoomOutKey("Zoom Out"));
|
||||
keyState.put("SPACE", new VMGKey("VMG"));
|
||||
keyState.put("SHIFT", new SailsToggleKey("Toggle Sails"));
|
||||
keyState.put("ENTER", new TackGybeKey("Tack/Gybe"));
|
||||
keyState.put("UP", new UpWindKey("Upwind"));
|
||||
keyState.put("DOWN", new DownWindKey("Downwind"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get singleton instance of KeyFactory to interact with key state
|
||||
* @return automatically constructed KeyFactory
|
||||
*/
|
||||
public static KeyFactory getFactory() {
|
||||
return theFactory;
|
||||
keyState.put("Z", new ZoomInKey());
|
||||
keyState.put("X", new ZoomOutKey());
|
||||
keyState.put("SPACE", new VMGKey());
|
||||
keyState.put("SHIFT", new SailsToggleKey());
|
||||
keyState.put("ENTER", new TackGybeKey());
|
||||
keyState.put("UP", new UpWindKey());
|
||||
keyState.put("DOWN", new DownWindKey());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,4 +38,86 @@ public class KeyFactory {
|
||||
return keyState.get(key);
|
||||
}
|
||||
|
||||
public Map<String, ControlKey> getKeyState() {
|
||||
return keyState;
|
||||
}
|
||||
|
||||
public void setKeyState(Map<String, ControlKey> keyState) {
|
||||
this.keyState = keyState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the key bound to a particular command in the keystate.
|
||||
* @param newKey the new key value for the command
|
||||
* @param command the command to be updated
|
||||
*/
|
||||
public void updateKey(String newKey, String command){
|
||||
ControlKey controlKey = null;
|
||||
String oldKey = null;
|
||||
for (Map.Entry<String, ControlKey> entry : keyState.entrySet()) {
|
||||
// if this is the correct command
|
||||
if (entry.getValue().toString()==command){
|
||||
controlKey = entry.getValue();
|
||||
oldKey = entry.getKey();
|
||||
}
|
||||
}
|
||||
keyState.remove(oldKey, controlKey);
|
||||
keyState.put(newKey, controlKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persistently saves the keybindings the user has set.
|
||||
*/
|
||||
public void save(){
|
||||
try {
|
||||
// open the filestream and write to it
|
||||
FileOutputStream fos = new FileOutputStream(
|
||||
System.getProperty("user.dir")+
|
||||
"/settings/keyBindings.xml");
|
||||
XMLEncoder xmlEncoder = new XMLEncoder(fos);
|
||||
xmlEncoder.writeObject(this.keyState);
|
||||
xmlEncoder.close();
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the persistently saved keybindings the user has set.
|
||||
*/
|
||||
public void load(){
|
||||
try {
|
||||
// access settings folder, create if it doesn't exist
|
||||
File settingsFolder = new File(
|
||||
System.getProperty("user.dir")+"/settings");
|
||||
if (!settingsFolder.exists()){
|
||||
settingsFolder.mkdir();
|
||||
}
|
||||
|
||||
// access keybindings xml file, create if it doesn't exist
|
||||
File savedFile = new File(
|
||||
settingsFolder+"/keyBindings.xml");
|
||||
if (!savedFile.exists()){
|
||||
savedFile.createNewFile();
|
||||
FileOutputStream fos = new FileOutputStream(savedFile);
|
||||
XMLEncoder xmlEncoder = new XMLEncoder(fos);
|
||||
xmlEncoder.writeObject(this.keyState);
|
||||
xmlEncoder.close();
|
||||
}
|
||||
|
||||
// load the saved settings into the game
|
||||
InputStream is = new FileInputStream(savedFile);
|
||||
XMLDecoder xmlDecoder = new XMLDecoder(is);
|
||||
Map<String, ControlKey> savedKeyState
|
||||
= (Map<String, ControlKey>)xmlDecoder.readObject();
|
||||
xmlDecoder.close();
|
||||
this.keyState = savedKeyState;
|
||||
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,11 +10,9 @@ public class SailsToggleKey extends ControlKey {
|
||||
|
||||
/**
|
||||
* Constructor for Control
|
||||
* @param name name of the key
|
||||
*
|
||||
*/
|
||||
public SailsToggleKey(String name) {
|
||||
super(name, BoatActionEnum.NOT_A_STATUS);
|
||||
public SailsToggleKey() {
|
||||
super("Toggle Sails", BoatActionEnum.NOT_A_STATUS);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,11 +9,9 @@ public class TackGybeKey extends ControlKey {
|
||||
|
||||
/**
|
||||
* Constructor for Control
|
||||
* @param name name of the key
|
||||
*
|
||||
*/
|
||||
public TackGybeKey(String name) {
|
||||
super(name, BoatActionEnum.TACK_GYBE);
|
||||
public TackGybeKey() {
|
||||
super("Tack/Gybe", BoatActionEnum.TACK_GYBE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -9,11 +9,9 @@ public class UpWindKey extends ControlKey {
|
||||
|
||||
/**
|
||||
* Constructor for Control
|
||||
* @param name name of the key
|
||||
*
|
||||
*/
|
||||
public UpWindKey(String name) {
|
||||
super(name, BoatActionEnum.UPWIND);
|
||||
public UpWindKey() {
|
||||
super("Upwind", BoatActionEnum.UPWIND);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package visualiser.gameController.Keys;
|
||||
|
||||
import javafx.scene.input.KeyCode;
|
||||
import network.Messages.Enums.BoatActionEnum;
|
||||
|
||||
/**
|
||||
@@ -10,11 +9,9 @@ public class VMGKey extends ControlKey{
|
||||
|
||||
/**
|
||||
* Constructor for Control
|
||||
*
|
||||
* @param name name of the key
|
||||
*/
|
||||
public VMGKey(String name) {
|
||||
super(name, BoatActionEnum.AUTO_PILOT);
|
||||
public VMGKey() {
|
||||
super("VMG", BoatActionEnum.AUTO_PILOT);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,8 +5,8 @@ package visualiser.gameController.Keys;
|
||||
*/
|
||||
public class ZoomInKey extends ControlKey {
|
||||
|
||||
public ZoomInKey(String name) {
|
||||
super(name);
|
||||
public ZoomInKey() {
|
||||
super("Zoom In");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -7,11 +7,9 @@ public class ZoomOutKey extends ControlKey{
|
||||
|
||||
/**
|
||||
* Constructor for Control
|
||||
* @param name name of the key
|
||||
*
|
||||
*/
|
||||
public ZoomOutKey(String name) {
|
||||
super(name);
|
||||
public ZoomOutKey() {
|
||||
super("Zoom Out");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package visualiser.layout;
|
||||
|
||||
import javafx.scene.shape.Shape3D;
|
||||
import javafx.scene.transform.Rotate;
|
||||
import javafx.scene.transform.Translate;
|
||||
|
||||
/**
|
||||
* Wrapper for controlling the position and heading of rendered 3D models.
|
||||
*/
|
||||
public class Subject3D {
|
||||
/**
|
||||
* Rendered mesh
|
||||
*/
|
||||
private Shape3D mesh;
|
||||
|
||||
/**
|
||||
* Position translation updated by state listeners
|
||||
*/
|
||||
private Translate position;
|
||||
|
||||
/**
|
||||
* Heading rotation updated by state listeners
|
||||
*/
|
||||
private Rotate heading;
|
||||
|
||||
/**
|
||||
* Constructor for view subject wrapper
|
||||
* @param mesh to be rendered
|
||||
*/
|
||||
public Subject3D(Shape3D mesh) {
|
||||
this.mesh = mesh;
|
||||
this.position = new Translate();
|
||||
this.heading = new Rotate(0, Rotate.Y_AXIS);
|
||||
|
||||
this.mesh.getTransforms().addAll(position, heading, new Rotate(90, Rotate.X_AXIS), new Rotate(180, Rotate.Y_AXIS));
|
||||
}
|
||||
|
||||
public Shape3D getMesh() {
|
||||
return mesh;
|
||||
}
|
||||
|
||||
public Translate getPosition() {
|
||||
return this.position;
|
||||
}
|
||||
|
||||
public Rotate getHeading() {
|
||||
return heading;
|
||||
}
|
||||
|
||||
public void setX(double x) {
|
||||
position.setX(x);
|
||||
}
|
||||
|
||||
public void setY(double y) {
|
||||
position.setY(y);
|
||||
}
|
||||
|
||||
public void setZ(double z) {
|
||||
position.setZ(z);
|
||||
}
|
||||
|
||||
public void setHeading(double angle) {
|
||||
heading.setAngle(angle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package visualiser.layout;
|
||||
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.PerspectiveCamera;
|
||||
import javafx.scene.SubScene;
|
||||
import javafx.scene.input.PickResult;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Shape3D;
|
||||
import javafx.scene.transform.Rotate;
|
||||
import javafx.scene.transform.Translate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Control for rendering 3D objects visible through a PerspectiveCamera. Implements Adapter Pattern to
|
||||
* interface with camera, and allows clients to add shapes to the scene. All scenes contain sea plane and
|
||||
* sky box, whose textures are set with special methods.
|
||||
*/
|
||||
public class View3D extends Pane {
|
||||
/**
|
||||
* Container for group and camera
|
||||
*/
|
||||
private SubScene scene;
|
||||
/**
|
||||
* Observable list of renderable items
|
||||
*/
|
||||
private ObservableList<Subject3D> items;
|
||||
/**
|
||||
* Map for selecting Subject3D from Shape3D
|
||||
*/
|
||||
private Map<Shape3D, Subject3D> selectionMap;
|
||||
/**
|
||||
* Subject tracked by camera
|
||||
*/
|
||||
private Subject3D target;
|
||||
/**
|
||||
* Rendering container for shapes
|
||||
*/
|
||||
private Group world;
|
||||
/**
|
||||
* Near limit of view frustum
|
||||
*/
|
||||
private double nearClip;
|
||||
/**
|
||||
* Far limit of view frustum
|
||||
*/
|
||||
private double farClip;
|
||||
/**
|
||||
* Camera origin
|
||||
*/
|
||||
private Translate pivot;
|
||||
/**
|
||||
* Distance of camera from pivot point
|
||||
*/
|
||||
private Translate distance;
|
||||
/**
|
||||
* Angle along ground between z-axis and camera
|
||||
*/
|
||||
private Rotate yaw;
|
||||
/**
|
||||
* Angle between ground plane and camera direction
|
||||
*/
|
||||
private Rotate pitch;
|
||||
/**
|
||||
* Single listener for subject heading changes
|
||||
*/
|
||||
private ChangeListener<? super Number> pivotHeading = (o, prev, curr) -> yaw.setAngle((double)curr);
|
||||
/**
|
||||
* Single listener for subject position (x) changes
|
||||
*/
|
||||
private ChangeListener<? super Number> pivotX = (o, prev, curr) -> pivot.setX((double)curr);
|
||||
/**
|
||||
* Single listener for subject position (y) changes
|
||||
*/
|
||||
private ChangeListener<? super Number> pivotY = (o, prev, curr) -> pivot.setY((double)curr);
|
||||
/**
|
||||
* Single listener for subject position (z) changes
|
||||
*/
|
||||
private ChangeListener<? super Number> pivotZ = (o, prev, curr) -> pivot.setZ((double)curr);
|
||||
/**
|
||||
* Distance to switch from third person to bird's eye
|
||||
*/
|
||||
private double THIRD_PERSON_LIMIT = 100;
|
||||
|
||||
/**
|
||||
* Default constructor for View3D. Sets up Scene and PerspectiveCamera.
|
||||
*/
|
||||
public View3D() {
|
||||
this.world = new Group();
|
||||
this.selectionMap = new HashMap<>();
|
||||
this.target = null;
|
||||
this.scene = new SubScene(world, 300, 300);
|
||||
|
||||
scene.widthProperty().bind(this.widthProperty());
|
||||
scene.heightProperty().bind(this.heightProperty());
|
||||
scene.setFill(new Color(0.2, 0.6, 1, 1));
|
||||
|
||||
scene.setCamera(buildCamera());
|
||||
|
||||
this.getChildren().add(scene);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up camera view frustum and binds transformations
|
||||
* @return perspective camera
|
||||
*/
|
||||
private PerspectiveCamera buildCamera() {
|
||||
PerspectiveCamera camera = new PerspectiveCamera(true);
|
||||
|
||||
// Set up view frustum
|
||||
nearClip = 0.1;
|
||||
farClip = 3000.0;
|
||||
camera.setNearClip(nearClip);
|
||||
camera.setFarClip(farClip);
|
||||
|
||||
// Set up transformations
|
||||
pivot = new Translate();
|
||||
distance = new Translate();
|
||||
yaw = new Rotate(0, Rotate.Y_AXIS);
|
||||
pitch = new Rotate(0, Rotate.X_AXIS);
|
||||
camera.getTransforms().addAll(pivot, yaw, pitch, distance);
|
||||
|
||||
return camera;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the list of subjects to be automatically added or removed from the view as the list
|
||||
* changes.
|
||||
* @param items list managed by client
|
||||
*/
|
||||
public void setItems(ObservableList<Subject3D> items) {
|
||||
this.items = items;
|
||||
this.items.addListener((ListChangeListener<? super Subject3D>) c -> {
|
||||
while(c.next()) {
|
||||
if (c.wasRemoved() || c.wasAdded()) {
|
||||
for (Subject3D shape : c.getRemoved()) {
|
||||
world.getChildren().remove(shape.getMesh());
|
||||
selectionMap.remove(shape.getMesh());
|
||||
}
|
||||
for (Subject3D shape : c.getAddedSubList()) {
|
||||
world.getChildren().add(shape.getMesh());
|
||||
selectionMap.put(shape.getMesh(), shape);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept mouse clicks on subjects in view. The applied listener cannot be removed.
|
||||
*/
|
||||
public void enableTracking() {
|
||||
scene.setOnMousePressed(e -> {
|
||||
PickResult result = e.getPickResult();
|
||||
if(result != null && result.getIntersectedNode() != null && result.getIntersectedNode() instanceof Shape3D) {
|
||||
trackSubject(selectionMap.get(result.getIntersectedNode()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop camera from following the last selected subject
|
||||
*/
|
||||
private void untrackSubject() {
|
||||
if(target != null) {
|
||||
target.getPosition().xProperty().removeListener(pivotX);
|
||||
target.getPosition().yProperty().removeListener(pivotY);
|
||||
target.getPosition().zProperty().removeListener(pivotZ);
|
||||
target.getHeading().angleProperty().removeListener(pivotHeading);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set camera to follow the selected subject
|
||||
* @param subject to track
|
||||
*/
|
||||
private void trackSubject(Subject3D subject) {
|
||||
untrackSubject();
|
||||
target = subject;
|
||||
|
||||
updatePivot(target.getPosition());
|
||||
setYaw(target.getHeading().getAngle());
|
||||
|
||||
target.getPosition().xProperty().addListener(pivotX);
|
||||
target.getPosition().yProperty().addListener(pivotY);
|
||||
target.getPosition().zProperty().addListener(pivotZ);
|
||||
target.getHeading().angleProperty().addListener(pivotHeading);
|
||||
|
||||
this.setDistance(THIRD_PERSON_LIMIT);
|
||||
this.setPitch(20);
|
||||
}
|
||||
|
||||
public void setNearClip(double nearClip) {
|
||||
this.nearClip = nearClip;
|
||||
}
|
||||
|
||||
public void setFarClip(double farClip) {
|
||||
this.farClip = farClip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the coordinates of the camera pivot once.
|
||||
* @param pivot source of coordinates
|
||||
*/
|
||||
public void updatePivot(Translate pivot) {
|
||||
this.pivot.setX(pivot.getX());
|
||||
this.pivot.setY(pivot.getY());
|
||||
this.pivot.setZ(pivot.getZ());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set distance of camera from pivot
|
||||
* @param distance in units
|
||||
*/
|
||||
public void setDistance(double distance) {
|
||||
this.distance.setZ(-distance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds delta to current distance and changes camera mode if applicable.
|
||||
* Third person limit specifies the distance at which a third person camera
|
||||
* switches to bird's-eye, remaining focused on the same position.
|
||||
* @param delta amount to change distance by
|
||||
*/
|
||||
public void updateDistance(double delta) {
|
||||
double distance = -this.distance.getZ() + delta;
|
||||
|
||||
if(distance <= 0) {
|
||||
this.setDistance(0);
|
||||
} else if(distance > THIRD_PERSON_LIMIT) {
|
||||
untrackSubject();
|
||||
this.setYaw(0);
|
||||
this.setPitch(60);
|
||||
this.setDistance(distance);
|
||||
} else {
|
||||
this.setDistance(distance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set angle of camera from z-axis along ground
|
||||
* @param yaw in degrees
|
||||
*/
|
||||
public void setYaw(double yaw) {
|
||||
this.yaw.setAngle(yaw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set elevation of camera
|
||||
* @param pitch in degrees
|
||||
*/
|
||||
public void setPitch(double pitch) {
|
||||
this.pitch.setAngle(-pitch);
|
||||
}
|
||||
}
|
||||
@@ -39,9 +39,9 @@ public class ResizableRaceCanvas extends ResizableCanvas {
|
||||
private Image sailsLuff = new Image("/images/sailsLuff.gif", 25, 10, false, false);
|
||||
|
||||
/**
|
||||
* The race we read data from and draw.
|
||||
* The race state we read data from and draw.
|
||||
*/
|
||||
private VisualiserRaceEvent visualiserRace;
|
||||
private VisualiserRaceState raceState;
|
||||
|
||||
|
||||
private boolean annoName = true;
|
||||
@@ -56,14 +56,14 @@ public class ResizableRaceCanvas extends ResizableCanvas {
|
||||
|
||||
/**
|
||||
* Constructs a {@link ResizableRaceCanvas} using a given {@link VisualiserRaceEvent}.
|
||||
* @param visualiserRace The race that data is read from in order to be drawn.
|
||||
* @param raceState The race state to be drawn.
|
||||
*/
|
||||
public ResizableRaceCanvas(VisualiserRaceEvent visualiserRace) {
|
||||
public ResizableRaceCanvas(VisualiserRaceState raceState) {
|
||||
super();
|
||||
|
||||
this.visualiserRace = visualiserRace;
|
||||
this.raceState = raceState;
|
||||
|
||||
RaceDataSource raceData = visualiserRace.getVisualiserRaceState().getRaceDataSource();
|
||||
RaceDataSource raceData = raceState.getRaceDataSource();
|
||||
|
||||
double lat1 = raceData.getMapTopLeft().getLatitude();
|
||||
double long1 = raceData.getMapTopLeft().getLongitude();
|
||||
@@ -276,8 +276,8 @@ public class ResizableRaceCanvas extends ResizableCanvas {
|
||||
boat.getCountry(),
|
||||
boat.getCurrentSpeed(),
|
||||
this.map.convertGPS(boat.getPosition()),
|
||||
boat.getTimeToNextMarkFormatted(this.visualiserRace.getVisualiserRaceState().getRaceClock().getCurrentTime()),
|
||||
boat.getTimeSinceLastMarkFormatted(this.visualiserRace.getVisualiserRaceState().getRaceClock().getCurrentTime()),
|
||||
boat.getTimeToNextMarkFormatted(raceState.getRaceClock().getCurrentTime()),
|
||||
boat.getTimeSinceLastMarkFormatted(raceState.getRaceClock().getCurrentTime()),
|
||||
Color.BLACK,
|
||||
20 );
|
||||
|
||||
@@ -291,7 +291,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
|
||||
*/
|
||||
private void drawBoats() {
|
||||
|
||||
List<VisualiserBoat> boats = new ArrayList<>(visualiserRace.getVisualiserRaceState().getBoats());
|
||||
List<VisualiserBoat> boats = new ArrayList<>(raceState.getBoats());
|
||||
//Sort to ensure we draw boats in consistent order.
|
||||
boats.sort(Comparator.comparingInt(Boat::getSourceID));
|
||||
|
||||
@@ -510,7 +510,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
|
||||
*/
|
||||
private void drawMarks() {
|
||||
|
||||
for (Mark mark : new ArrayList<>(visualiserRace.getVisualiserRaceState().getMarks())) {
|
||||
for (Mark mark : new ArrayList<>(raceState.getMarks())) {
|
||||
drawMark(mark);
|
||||
}
|
||||
}
|
||||
@@ -574,7 +574,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
|
||||
|
||||
|
||||
//Calculate the screen coordinates of the boundary.
|
||||
List<GPSCoordinate> boundary = new ArrayList<>(visualiserRace.getVisualiserRaceState().getBoundary());
|
||||
List<GPSCoordinate> boundary = new ArrayList<>(raceState.getBoundary());
|
||||
double[] xpoints = new double[boundary.size()];
|
||||
double[] ypoints = new double[boundary.size()];
|
||||
|
||||
@@ -601,8 +601,8 @@ public class ResizableRaceCanvas extends ResizableCanvas {
|
||||
public void drawRace() {
|
||||
|
||||
//Update RaceMap with new GPS values of race.
|
||||
this.map.setGPSTopLeft(visualiserRace.getVisualiserRaceState().getRaceDataSource().getMapTopLeft());
|
||||
this.map.setGPSBotRight(visualiserRace.getVisualiserRaceState().getRaceDataSource().getMapBottomRight());
|
||||
this.map.setGPSTopLeft(raceState.getRaceDataSource().getMapTopLeft());
|
||||
this.map.setGPSBotRight(raceState.getRaceDataSource().getMapBottomRight());
|
||||
|
||||
|
||||
clear();
|
||||
@@ -627,7 +627,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
|
||||
* draws a transparent line around the course that shows the paths boats must travel
|
||||
*/
|
||||
public void drawRaceLine(){
|
||||
List<Leg> legs = this.visualiserRace.getVisualiserRaceState().getLegs();
|
||||
List<Leg> legs = raceState.getLegs();
|
||||
GPSCoordinate legStartPoint = legs.get(0).getStartCompoundMark().getAverageGPSCoordinate();
|
||||
GPSCoordinate nextStartPoint;
|
||||
for (int i = 0; i < legs.size() -1; i++) {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package visualiser.model;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.paint.Color;
|
||||
import network.Messages.Enums.BoatStatusEnum;
|
||||
import shared.model.Azimuth;
|
||||
import shared.model.Boat;
|
||||
import shared.model.Constants;
|
||||
import shared.model.GPSCoordinate;
|
||||
import shared.model.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.ZonedDateTime;
|
||||
@@ -61,7 +60,8 @@ public class VisualiserBoat extends Boat {
|
||||
private boolean isClientBoat = false;
|
||||
|
||||
|
||||
|
||||
private ObjectProperty<GPSCoordinate> positionProperty;
|
||||
private ObjectProperty<Bearing> bearingProperty;
|
||||
|
||||
|
||||
/**
|
||||
@@ -239,4 +239,38 @@ public class VisualiserBoat extends Boat {
|
||||
public void setClientBoat(boolean clientBoat) {
|
||||
isClientBoat = clientBoat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GPSCoordinate getPosition() {
|
||||
return positionProperty.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPosition(GPSCoordinate position) {
|
||||
if(this.positionProperty == null) {
|
||||
this.positionProperty = new SimpleObjectProperty<>();
|
||||
}
|
||||
this.positionProperty.set(position);
|
||||
}
|
||||
|
||||
public ObjectProperty<GPSCoordinate> positionProperty() {
|
||||
return positionProperty;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bearing getBearing() {
|
||||
return bearingProperty.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBearing(Bearing bearing) {
|
||||
if(this.bearingProperty == null) {
|
||||
this.bearingProperty = new SimpleObjectProperty<>();
|
||||
}
|
||||
this.bearingProperty.set(bearing);
|
||||
}
|
||||
|
||||
public ObjectProperty<Bearing> bearingProperty() {
|
||||
return bearingProperty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package visualiser.utils;
|
||||
|
||||
import shared.dataInput.RaceDataSource;
|
||||
import shared.model.GPSCoordinate;
|
||||
import visualiser.model.GraphCoordinate;
|
||||
|
||||
/**
|
||||
* Converts GPS coordinates to view volume coordinates. Longitudes are equally spaced at all latitudes,
|
||||
* which leads to inaccurate distance measurements close to the poles. This is acceptable as races are
|
||||
* not likely to be set there.
|
||||
*/
|
||||
public class GPSConverter {
|
||||
private double longRight;
|
||||
private double longLeft;
|
||||
private double latBottom;
|
||||
private double latTop;
|
||||
/**
|
||||
* Conversion factor from longitude to view units
|
||||
*/
|
||||
private double longitudeFactor;
|
||||
/**
|
||||
* Conversion factor from latitude to view units
|
||||
*/
|
||||
private double latitudeFactor;
|
||||
|
||||
/**
|
||||
* Set up projection with default view boundaries from RaceDataSource
|
||||
* @param source for view boundaries
|
||||
* @param longitudeFactor separation of a degree of longitude in view units
|
||||
* @param latitudeFactor separation of a degree of latitude in view units
|
||||
*/
|
||||
public GPSConverter(RaceDataSource source, double longitudeFactor, double latitudeFactor) {
|
||||
this.latTop = source.getMapTopLeft().getLatitude();
|
||||
this.longLeft = source.getMapTopLeft().getLongitude();
|
||||
this.latBottom = source.getMapBottomRight().getLatitude();
|
||||
this.longRight = source.getMapBottomRight().getLongitude();
|
||||
this.longitudeFactor = longitudeFactor;
|
||||
this.latitudeFactor = latitudeFactor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts GPS coordinates to coordinates for container.
|
||||
* It is assumed that the provided GPSCoordinate will always be within the GPSCoordinate boundaries of the RaceMap.
|
||||
*
|
||||
* @param lat GPS latitude
|
||||
* @param lon GPS longitude
|
||||
* @return GraphCoordinate (pair of doubles)
|
||||
* @see GraphCoordinate
|
||||
*/
|
||||
private GraphCoordinate convertGPS(double lat, double lon) {
|
||||
|
||||
//Calculate the width/height, in gps coordinates, of the map.
|
||||
double longWidth = longRight - longLeft;
|
||||
double latHeight = latBottom - latTop;
|
||||
|
||||
//Calculate the distance between the specified coordinate and the edge of the map.
|
||||
double longDelta = lon - longLeft;
|
||||
double latDelta = lat - latTop;
|
||||
|
||||
//Calculate the proportion along horizontally, from the left, the coordinate should be.
|
||||
double longProportion = longDelta / longWidth;
|
||||
//Calculate the proportion along vertically, from the top, the coordinate should be.
|
||||
double latProportion = latDelta / latHeight;
|
||||
|
||||
//Check which metric dimension of our map is smaller. We use this to ensure that any rendered stuff retains its correct aspect ratio, and that everything is visible on screen.
|
||||
double smallerDimension = Math.min(longitudeFactor, latitudeFactor);
|
||||
|
||||
//Calculate the x and y pixel coordinates.
|
||||
//We take the complement of latProportion to flip it.
|
||||
int x = (int) (longProportion * smallerDimension);
|
||||
int y = (int) (latProportion * smallerDimension);
|
||||
|
||||
//Because we try to maintain the correct aspect ratio, we will end up with "spare" pixels along the larger dimension (e.g., width 800, height 600, 200 extra pixels along width).
|
||||
double extraDistance = Math.abs(longitudeFactor - latitudeFactor);
|
||||
//We therefore "center" the coordinates along this larger dimension, by adding half of the extra pixels.
|
||||
if (longitudeFactor > latitudeFactor) {
|
||||
x += extraDistance / 2;
|
||||
} else {
|
||||
y += extraDistance / 2;
|
||||
}
|
||||
|
||||
|
||||
//Finally, create the GraphCoordinate.
|
||||
GraphCoordinate graphCoordinate = new GraphCoordinate(x, y);
|
||||
|
||||
|
||||
return graphCoordinate;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the GPS Coordinate to GraphCoordinate.
|
||||
* It is assumed that the provided GPSCoordinate will always be within the GPSCoordinate boundaries of the RaceMap.
|
||||
*
|
||||
* @param coordinate GPSCoordinate representation of Latitude and Longitude.
|
||||
* @return GraphCoordinate that the GPS is coordinates are to be displayed on the map.
|
||||
* @see GraphCoordinate
|
||||
* @see GPSCoordinate
|
||||
*/
|
||||
public GraphCoordinate convertGPS(GPSCoordinate coordinate) {
|
||||
return convertGPS(coordinate.getLatitude(), coordinate.getLongitude());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user