diff --git a/racevisionGame/src/main/java/visualiser/Controllers/HostController.java b/racevisionGame/src/main/java/visualiser/Controllers/HostController.java index b2a91889..454b6cdb 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/HostController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/HostController.java @@ -82,7 +82,7 @@ public class HostController extends Controller { AnimationTimer rotate = new AnimationTimer() { @Override public void handle(long now) { - subject.setHeading(subject.getHeading() + 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 ba95317f..64481229 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java @@ -13,7 +13,6 @@ import javafx.scene.control.*; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; 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; @@ -21,13 +20,12 @@ import javafx.scene.transform.Translate; import javafx.util.Callback; import network.Messages.Enums.RaceStatusEnum; import shared.dataInput.RaceDataSource; -import shared.exceptions.BoatNotFoundException; -import shared.exceptions.MarkNotFoundException; 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.MarkRadius; import visualiser.layout.Subject3D; import visualiser.layout.View3D; @@ -36,7 +34,6 @@ import visualiser.utils.GPSConverter; import java.io.IOException; import java.net.URL; -import java.util.List; import java.util.Optional; import java.util.ResourceBundle; import java.util.logging.Level; @@ -76,7 +73,7 @@ public class RaceController extends Controller { @FXML private GridPane canvasBase; - @FXML private SplitPane race; + @FXML private SplitPane racePane; /** * This is the pane we place the actual arrow control inside of. @@ -104,7 +101,7 @@ public class RaceController extends Controller { 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();} @@ -130,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 { @@ -139,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()); } } @@ -179,13 +176,14 @@ public class RaceController extends Controller { } private void initialiseView3D(VisualiserRaceEvent race) { - ObservableList subjects = FXCollections.observableArrayList(); + viewSubjects = FXCollections.observableArrayList(); - //read 3d Assets + // Import boat mesh URL asset = HostController.class.getClassLoader().getResource("assets/V1.2 Complete Boat.stl"); StlMeshImporter importer = new StlMeshImporter(); importer.read(asset); + // Configure camera angles and control URL markerAsset = HostController.class.getClassLoader().getResource("assets/Bouy V1.1.stl"); StlMeshImporter importerMark = new StlMeshImporter(); importerMark.read(markerAsset); @@ -198,22 +196,19 @@ public class RaceController extends Controller { view3D.setDistance(1050); view3D.setYaw(0); view3D.setPitch(60); - //view3D.rotateCamera(-90, 1, 0, 0); - //view3D.updatePosition(0, 200, 0); - RaceDataSource raceData = visualiserRace.getVisualiserRaceState().getRaceDataSource(); - - double lat1 = raceData.getMapTopLeft().getLatitude(); - double long1 = raceData.getMapTopLeft().getLongitude(); - double lat2 = raceData.getMapBottomRight().getLatitude(); - double long2 = raceData.getMapBottomRight().getLongitude(); - System.out.println(view3D.getWidth()); - System.out.println(view3D.getHeight()); - final GPSConverter gpsConverter = new GPSConverter(lat1, long1, lat2, long2, (int)450, (int)450); + view3D.enableTracking(); + canvasBase.add(view3D, 0, 0); - view3D.setItems(subjects); - canvasBase.getChildren().add(0, view3D); + // 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()); MeshView mesh = new MeshView(importerMark.getImport()); Subject3D markModel = new Subject3D(mesh); Subject3D markRadius = new MarkRadius(3); @@ -224,34 +219,53 @@ public class RaceController extends Controller { markRadius.setX(x); markRadius.setZ(z); - subjects.add(markModel); - subjects.add(markRadius); + viewSubjects.add(markModel); + viewSubjects.add(markRadius); } - + // Position and add each boat to view for(VisualiserBoat boat: race.getVisualiserRaceState().getBoats()) { - MeshView mesh = null; + MeshView mesh; if(boat.getSourceID() == race.getVisualiserRaceState().getPlayerBoatID()) { mesh = new MeshView(importer.getImport()); } else { mesh = new MeshView(importerBurgerBoat.getImport()); } Subject3D subject = new Subject3D(mesh); - subjects.add(subject); + 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()); - if(boat.getSourceID() == race.getVisualiserRaceState().getPlayerBoatID()) { - //view3D.updatePivot(subject.getPosition()); - } - //view3D.setYaw(boat.getBearing().degrees()); } }; 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; + } + } + }); } @@ -434,7 +448,7 @@ public class RaceController extends Controller { initialiseRace(); //Display this controller. - race.setVisible(true); + racePane.setVisible(true); } /** @@ -442,7 +456,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); } @@ -483,7 +497,7 @@ public class RaceController extends Controller { //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()); @@ -505,10 +519,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); @@ -516,7 +530,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/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 index 63f3b3c8..af76f4f4 100644 --- a/racevisionGame/src/main/java/visualiser/layout/Subject3D.java +++ b/racevisionGame/src/main/java/visualiser/layout/Subject3D.java @@ -33,9 +33,6 @@ public class Subject3D { 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)); -// this.position.xProperty().addListener(((observable, oldValue, newValue) -> System.out.println("Boat x: " + newValue))); -// this.position.yProperty().addListener(((observable, oldValue, newValue) -> System.out.println("Boat y: " + newValue))); -// this.position.zProperty().addListener(((observable, oldValue, newValue) -> System.out.println("Boat z: " + newValue))); } public Shape3D getMesh() { @@ -46,6 +43,10 @@ public class Subject3D { return this.position; } + public Rotate getHeading() { + return heading; + } + public void setX(double x) { position.setX(x); } @@ -58,10 +59,6 @@ public class Subject3D { position.setZ(z); } - public double getHeading() { - return heading.getAngle(); - } - 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 index 95a6cfeb..27fe6086 100644 --- a/racevisionGame/src/main/java/visualiser/layout/View3D.java +++ b/racevisionGame/src/main/java/visualiser/layout/View3D.java @@ -1,27 +1,43 @@ package visualiser.layout; +import javafx.beans.value.ChangeListener; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; -import javafx.geometry.Point3D; 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 */ @@ -50,16 +66,36 @@ public class View3D extends Pane { * Angle between ground plane and camera direction */ private Rotate pitch; - - private PerspectiveCamera camera; + /** + * 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() { - world = new Group(); + this.world = new Group(); + this.selectionMap = new HashMap<>(); + this.target = null; + this.scene = new SubScene(world, 300, 300); - SubScene 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)); @@ -88,24 +124,77 @@ public class View3D extends Pane { yaw = new Rotate(0, Rotate.Y_AXIS); pitch = new Rotate(0, Rotate.X_AXIS); camera.getTransforms().addAll(pivot, yaw, pitch, distance); - centerCamera(); - this.camera = camera; 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()); - for (Subject3D shape : c.getAddedSubList()) world.getChildren().add(shape.getMesh()); + 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; } @@ -114,18 +203,16 @@ public class View3D extends Pane { 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()); } - public void updatePosition(double x, double y, double z) { - this.distance.setX(x); - this.distance.setY(y); - this.distance.setZ(z); - } - /** * Set distance of camera from pivot * @param distance in units @@ -134,6 +221,27 @@ public class View3D extends Pane { 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 @@ -149,13 +257,4 @@ public class View3D extends Pane { public void setPitch(double pitch) { this.pitch.setAngle(-pitch); } - - public void centerCamera(){ - } - - public void rotateCamera(double angle, double x, double y, double z){ - camera.setRotationAxis(new Point3D(x, y, z)); - camera.setRotate(-90); - } - } diff --git a/racevisionGame/src/main/java/visualiser/utils/GPSConverter.java b/racevisionGame/src/main/java/visualiser/utils/GPSConverter.java index 55a162b2..22dd937f 100644 --- a/racevisionGame/src/main/java/visualiser/utils/GPSConverter.java +++ b/racevisionGame/src/main/java/visualiser/utils/GPSConverter.java @@ -1,26 +1,41 @@ package visualiser.utils; +import shared.dataInput.RaceDataSource; import shared.model.GPSCoordinate; import visualiser.model.GraphCoordinate; /** - * Created by fwy13 on 7/09/17. + * 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 { - double longRight; - double longLeft; - double latBottom; - double latTop; - int paneWidth; - int paneHeight; - - public GPSConverter(double latTop, double longLeft, double latBottom, double longRight, double paneWidth, double paneHeight){ - this.longRight = longRight; - this.longLeft = longLeft; - this.latBottom = latBottom; - this.latTop = latTop; - this.paneWidth = (int)paneWidth; - this.paneHeight = (int)paneHeight; + 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; } /** @@ -46,11 +61,9 @@ public class GPSConverter { double longProportion = longDelta / longWidth; //Calculate the proportion along vertically, from the top, the coordinate should be. double latProportion = latDelta / latHeight; - //System.out.println(latProportion + " " + longProportion); - - //Check which pixel 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. - int smallerDimension = Math.min(paneWidth, paneHeight); + //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. @@ -58,12 +71,12 @@ public class GPSConverter { 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). - int extraPixels = Math.abs(paneWidth - paneHeight); + double extraDistance = Math.abs(longitudeFactor - latitudeFactor); //We therefore "center" the coordinates along this larger dimension, by adding half of the extra pixels. - if (paneWidth > paneHeight) { - x += extraPixels / 2; + if (longitudeFactor > latitudeFactor) { + x += extraDistance / 2; } else { - y += extraPixels / 2; + y += extraDistance / 2; } diff --git a/racevisionGame/src/main/resources/visualiser/scenes/hostlobby.fxml b/racevisionGame/src/main/resources/visualiser/scenes/hostlobby.fxml index c4198bf3..044e7078 100644 --- a/racevisionGame/src/main/resources/visualiser/scenes/hostlobby.fxml +++ b/racevisionGame/src/main/resources/visualiser/scenes/hostlobby.fxml @@ -14,7 +14,7 @@ - + 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 @@ - +