diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index 6f778852..703686bc 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -180,7 +180,7 @@ public class Event { public static String setRaceXMLAtCurrentTimeToNow(String raceXML) { //The start time is current time + 4 minutes. prestart is 3 minutes, and we add another minute. - long millisecondsToAdd = Constants.RacePreStartTime + (1 * 60 * 1000); + long millisecondsToAdd = Constants.RacePreStartTime + Constants.RacePreparatoryTime; long secondsToAdd = millisecondsToAdd / 1000; //Scale the time using our time scalar. secondsToAdd = secondsToAdd / Constants.RaceTimeScale; diff --git a/racevisionGame/src/main/java/mock/xml/RaceXMLCreator.java b/racevisionGame/src/main/java/mock/xml/RaceXMLCreator.java index 5cb74625..b168862f 100644 --- a/racevisionGame/src/main/java/mock/xml/RaceXMLCreator.java +++ b/racevisionGame/src/main/java/mock/xml/RaceXMLCreator.java @@ -187,7 +187,7 @@ public class RaceXMLCreator { public static void setRaceXMLAtCurrentTimeToNow(XMLRace raceXML) { //The start time is current time + 4 minutes. prestart is 3 minutes, and we add another minute. - long millisecondsToAdd = Constants.RacePreStartTime + (1 * 60 * 1000); + long millisecondsToAdd = Constants.RacePreStartTime + Constants.RacePreparatoryTime; long secondsToAdd = millisecondsToAdd / 1000; //Scale the time using our time scalar. secondsToAdd = secondsToAdd / Constants.RaceTimeScale; diff --git a/racevisionGame/src/main/java/shared/model/Constants.java b/racevisionGame/src/main/java/shared/model/Constants.java index bb7ec598..18a2aaed 100644 --- a/racevisionGame/src/main/java/shared/model/Constants.java +++ b/racevisionGame/src/main/java/shared/model/Constants.java @@ -36,15 +36,18 @@ public class Constants { public static final int RaceTimeScale = 2;//10; /** - * The race pre-start time, in milliseconds. 3 minutes. + * The race pre-start time, in milliseconds. 3 minutes (30 seconds for development). */ - public static final long RacePreStartTime = 3 * 60 * 1000; +// public static final long RacePreStartTime = 30 * 1000; + public static final long RacePreStartTime = 1000; /** * The race preparatory time, in milliseconds. 1 minute. */ - public static final long RacePreparatoryTime = 1 * 60 * 1000; +// public static final long RacePreparatoryTime = 60 * 1000; + public static final long RacePreparatoryTime = 3 * 1000; + diff --git a/racevisionGame/src/main/java/visualiser/Controllers/HostController.java b/racevisionGame/src/main/java/visualiser/Controllers/HostController.java index 008276ef..10bc9dbe 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/HostController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/HostController.java @@ -11,14 +11,11 @@ import javafx.scene.control.SplitPane; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; -import javafx.scene.shape.Box; -import javafx.scene.shape.Mesh; import javafx.scene.shape.MeshView; -import javafx.scene.shape.Shape3D; -import javafx.scene.transform.Rotate; import mock.app.Event; import mock.exceptions.EventConstructionException; -import visualiser.model.View3D; +import visualiser.layout.Subject3D; +import visualiser.layout.View3D; import java.io.IOException; import java.net.Socket; @@ -64,31 +61,28 @@ public class HostController extends Controller { @Override public void initialize(URL location, ResourceBundle resources) { - ObservableList shapes = FXCollections.observableArrayList(); + ObservableList subjects = FXCollections.observableArrayList(); view3D = new View3D(); - view3D.setItems(shapes); + 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); - MeshView mesh = new MeshView(importer.getImport()); - shapes.add(mesh); + Subject3D subject = new Subject3D(new MeshView(importer.getImport())); + + subjects.add(subject); - view3D.setPivot(mesh); view3D.setDistance(50); view3D.setYaw(45); view3D.setPitch(20); - Rotate rotation = new Rotate(0, Rotate.Y_AXIS); - mesh.getTransforms().addAll(rotation, new Rotate(-90, Rotate.X_AXIS)); - AnimationTimer rotate = new AnimationTimer() { @Override public void handle(long now) { - rotation.setAngle(rotation.getAngle() + 0.1); + subject.setHeading(subject.getHeading().getAngle() + 0.1); } }; rotate.start(); diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java index 043726f7..e864993d 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java @@ -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; @@ -12,17 +12,25 @@ import javafx.scene.chart.LineChart; import javafx.scene.control.*; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; -import javafx.scene.layout.AnchorPane; +import javafx.scene.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; @@ -38,53 +46,34 @@ 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 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. @@ -100,9 +89,6 @@ public class RaceController extends Controller { @FXML private TableColumn boatMarkColumn; @FXML private TableColumn boatSpeedColumn; @FXML private LineChart sparklineChart; - @FXML private AnchorPane annotationPane; - - /** * Ctor. @@ -112,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();} @@ -142,7 +127,7 @@ public class RaceController extends Controller { Optional result = alert.showAndWait(); if (result.get() == ButtonType.OK) { parent.endEvent(); - race.setVisible(false); + racePane.setVisible(false); App.app.showMainStage(App.getStage()); } } else { @@ -151,7 +136,7 @@ public class RaceController extends Controller { alert.setContentText("Do you wish to quit the race?"); Optional result = alert.showAndWait(); if (result.get() == ButtonType.OK) { - race.setVisible(false); + racePane.setVisible(false); App.app.showMainStage(App.getStage()); } } @@ -175,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); @@ -190,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; + } + } + }); + } /** @@ -337,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.getVisualiserRaceState()); - - //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) { @@ -410,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); } /** @@ -422,7 +433,7 @@ public class RaceController extends Controller { * @param boats boats there are in the race. */ public void finishRace(ObservableList boats) { - race.setVisible(false); + racePane.setVisible(false); parent.enterFinish(boats); } @@ -457,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()); @@ -490,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); @@ -501,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); diff --git a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java index 890eb816..b46bf79f 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java @@ -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(); diff --git a/racevisionGame/src/main/java/visualiser/gameController/Keys/ControlKey.java b/racevisionGame/src/main/java/visualiser/gameController/Keys/ControlKey.java index dd489f73..ce4b341e 100644 --- a/racevisionGame/src/main/java/visualiser/gameController/Keys/ControlKey.java +++ b/racevisionGame/src/main/java/visualiser/gameController/Keys/ControlKey.java @@ -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 diff --git a/racevisionGame/src/main/java/visualiser/layout/Subject3D.java b/racevisionGame/src/main/java/visualiser/layout/Subject3D.java new file mode 100644 index 00000000..af76f4f4 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/layout/Subject3D.java @@ -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); + } +} diff --git a/racevisionGame/src/main/java/visualiser/layout/View3D.java b/racevisionGame/src/main/java/visualiser/layout/View3D.java new file mode 100644 index 00000000..27fe6086 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/layout/View3D.java @@ -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 items; + /** + * Map for selecting Subject3D from Shape3D + */ + private Map 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 pivotHeading = (o, prev, curr) -> yaw.setAngle((double)curr); + /** + * Single listener for subject position (x) changes + */ + private ChangeListener pivotX = (o, prev, curr) -> pivot.setX((double)curr); + /** + * Single listener for subject position (y) changes + */ + private ChangeListener pivotY = (o, prev, curr) -> pivot.setY((double)curr); + /** + * Single listener for subject position (z) changes + */ + private ChangeListener 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 items) { + this.items = items; + this.items.addListener((ListChangeListener) 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); + } +} diff --git a/racevisionGame/src/main/java/visualiser/model/BoatDisplay3D.java b/racevisionGame/src/main/java/visualiser/model/BoatDisplay3D.java deleted file mode 100644 index 9314f5cd..00000000 --- a/racevisionGame/src/main/java/visualiser/model/BoatDisplay3D.java +++ /dev/null @@ -1,18 +0,0 @@ -package visualiser.model; - -import com.interactivemesh.jfx.importer.Importer; -import javafx.scene.layout.Pane; - -/** - * Created by fwy13 on 29/08/17. - */ -public class BoatDisplay3D extends Pane { - - - public BoatDisplay3D(String filePath){ - super(); -// Shape3D -// this.getChildren().add(); - } - -} diff --git a/racevisionGame/src/main/java/visualiser/model/View3D.java b/racevisionGame/src/main/java/visualiser/model/View3D.java deleted file mode 100644 index 6affb906..00000000 --- a/racevisionGame/src/main/java/visualiser/model/View3D.java +++ /dev/null @@ -1,145 +0,0 @@ -package visualiser.model; - -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; -import javafx.scene.Group; -import javafx.scene.PerspectiveCamera; -import javafx.scene.SubScene; -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; - -/** - * 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 { - /** - * Observable list of renderable items - */ - private ObservableList items; - /** - * Rendering container for shapes - */ - private Group world; - /** - * Near limit of view frustum - */ - private double nearClip; - /** - * Far limit of view frustum - */ - private double farClip; - /** - * Position camera pivots around - */ - 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; - - /** - * Default constructor for View3D. Sets up Scene and PerspectiveCamera. - */ - public View3D() { - world = new Group(); - - SubScene scene = new SubScene(world, 300, 300); - scene.widthProperty().bind(this.widthProperty()); - scene.heightProperty().bind(this.heightProperty()); - scene.setFill(Color.BLACK); - - 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 = 1000.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; - } - - public void setItems(ObservableList items) { - this.items = items; - this.items.addListener((ListChangeListener) c -> { - while(c.next()) { - if (c.wasRemoved() || c.wasAdded()) { - for (Shape3D shape : c.getRemoved()) world.getChildren().remove(shape); - for (Shape3D shape : c.getAddedSubList()) world.getChildren().add(shape); - } - } - }); - } - - public void setNearClip(double nearClip) { - this.nearClip = nearClip; - } - - public void setFarClip(double farClip) { - this.farClip = farClip; - } - - /** - * Set object to centre on camera - * @param pivot centred object - */ - public void setPivot(Shape3D pivot) { - this.pivot.setX(pivot.getTranslateX()); - this.pivot.setY(pivot.getTranslateY()); - this.pivot.setZ(pivot.getTranslateZ()); - } - - /** - * Set distance of camera from pivot - * @param distance in units - */ - public void setDistance(double distance) { - this.distance.setZ(-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); - } -} diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java index 6cbfdaa3..555def15 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java @@ -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 positionProperty; + private ObjectProperty 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 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 bearingProperty() { + return bearingProperty; + } } diff --git a/racevisionGame/src/main/java/visualiser/utils/GPSConverter.java b/racevisionGame/src/main/java/visualiser/utils/GPSConverter.java new file mode 100644 index 00000000..22dd937f --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/utils/GPSConverter.java @@ -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()); + } + +} diff --git a/racevisionGame/src/main/resources/visualiser/scenes/race.fxml b/racevisionGame/src/main/resources/visualiser/scenes/race.fxml index bbc1c077..b6496743 100644 --- a/racevisionGame/src/main/resources/visualiser/scenes/race.fxml +++ b/racevisionGame/src/main/resources/visualiser/scenes/race.fxml @@ -22,7 +22,7 @@ - +