diff --git a/src/main/java/seng302/App.java b/src/main/java/seng302/App.java index 57a5e1e6..64cf5e2c 100644 --- a/src/main/java/seng302/App.java +++ b/src/main/java/seng302/App.java @@ -15,8 +15,10 @@ public class App extends Application { public void start(Stage primaryStage) throws Exception { Parent root = FXMLLoader.load(getClass().getResource("/views/MainView.fxml")); primaryStage.setTitle("RaceVision"); - primaryStage.setScene(new Scene(root)); - primaryStage.setMaximized(true); + primaryStage.setScene(new Scene(root, 1530, 960)); + primaryStage.setMaxWidth(1530); + primaryStage.setMaxHeight(960); +// primaryStage.setMaximized(true); primaryStage.show(); primaryStage.setOnCloseRequest(e -> { @@ -63,8 +65,8 @@ public class App extends Application { //Change the StreamReceiver in this else block to change the default data source. else{ // sr = new StreamReceiver("localhost", 4949, "RaceStream"); -// sr = new StreamReceiver("livedata.americascup.com", 4941, "RaceStream"); - sr = new StreamReceiver("csse-s302staff.canterbury.ac.nz", 4941, "RaceStream"); +// sr = new StreamReceiver("csse-s302staff.canterbury.ac.nz", 4941, "RaceStream"); + sr = new StreamReceiver("livedata.americascup.com", 4941, "RaceStream"); } sr.start(); diff --git a/src/main/java/seng302/controllers/CanvasController.java b/src/main/java/seng302/controllers/CanvasController.java index 13340429..daa9a4fa 100644 --- a/src/main/java/seng302/controllers/CanvasController.java +++ b/src/main/java/seng302/controllers/CanvasController.java @@ -1,10 +1,5 @@ package seng302.controllers; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.PriorityBlockingQueue; import javafx.animation.AnimationTimer; import javafx.beans.property.SimpleDoubleProperty; import javafx.fxml.FXML; @@ -12,6 +7,7 @@ import javafx.geometry.Point2D; import javafx.scene.Group; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; +import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.paint.Color; import javafx.scene.shape.Polygon; @@ -24,11 +20,22 @@ import seng302.models.mark.Mark; import seng302.fxObjects.MarkGroup; import seng302.models.mark.MarkType; import seng302.models.mark.SingleMark; +import seng302.models.map.Boundary; +import seng302.models.map.CanvasMap; +import seng302.models.mark.*; import seng302.models.stream.StreamParser; import seng302.models.stream.XMLParser; import seng302.models.stream.XMLParser.RaceXMLObject.Limit; import seng302.models.stream.XMLParser.RaceXMLObject.Participant; import seng302.models.stream.packets.BoatPositionPacket; +import seng302.server.simulator.GeoUtility; +import seng302.server.simulator.mark.Position; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.PriorityBlockingQueue; /** * Created by ptg19 on 15/03/17. @@ -43,15 +50,18 @@ public class CanvasController { private ResizableCanvas canvas; private Group group; private GraphicsContext gc; + private ImageView mapImage; private final int MARK_SIZE = 10; private final int BUFFER_SIZE = 50; + private final int PANEL_WIDTH = 1260; // it should be 1280 but, minors 40 to cancel the bias. + private final int PANEL_HEIGHT = 960; private final int CANVAS_WIDTH = 720; private final int CANVAS_HEIGHT = 720; private final int LHS_BUFFER = BUFFER_SIZE; - private final int RHS_BUFFER = BUFFER_SIZE + MARK_SIZE / 2; + private final int RHS_BUFFER = BUFFER_SIZE; private final int TOP_BUFFER = BUFFER_SIZE; - private final int BOT_BUFFER = TOP_BUFFER + MARK_SIZE / 2; + private final int BOT_BUFFER = TOP_BUFFER; private boolean horizontalInversion = false; private double distanceScaleFactor; @@ -62,6 +72,8 @@ public class CanvasController { private Mark maxLonPoint; private double referencePointX; private double referencePointY; + private double metersPerPixelX; + private double metersPerPixelY; private List markGroups = new ArrayList<>(); private List boatGroups = new ArrayList<>(); @@ -90,6 +102,12 @@ public class CanvasController { canvas = new ResizableCanvas(); group = new Group(); + // create image view for map, bind panel size to image + mapImage = new ImageView(); + canvasPane.getChildren().add(mapImage); + mapImage.fitWidthProperty().bind(canvasPane.widthProperty()); + mapImage.fitHeightProperty().bind(canvasPane.heightProperty()); + canvasPane.getChildren().add(canvas); canvasPane.getChildren().add(group); // Bind canvas size to stack pane size. @@ -100,11 +118,9 @@ public class CanvasController { public void initializeCanvas (){ gc = canvas.getGraphicsContext2D(); - gc.save(); - gc.setFill(Color.SKYBLUE); - gc.fillRect(0,0, CANVAS_WIDTH, CANVAS_HEIGHT); - gc.restore(); + gc.setGlobalAlpha(0.5); fitMarksToCanvas(); + drawGoogleMap(); FPSdisplay.setLayoutX(5); FPSdisplay.setLayoutY(20); FPSdisplay.setStrokeWidth(2); @@ -117,11 +133,37 @@ public class CanvasController { initializeMarks(); timer = new AnimationTimer() { + private int UPDATE_FPM_PERIOD = 50; // update FPM label every 50 frames + private int updateFPMCounter = 100; + private long lastTime = 0; @Override public void handle(long now) { //fps stuff + long oldFrameTime = frameTimes[frameTimeIndex] ; + frameTimes[frameTimeIndex] = now ; + frameTimeIndex = (frameTimeIndex + 1) % frameTimes.length ; + if (frameTimeIndex == 0) { + arrayFilled = true ; + } + long elapsedNanos; + if (arrayFilled) { + elapsedNanos = now - oldFrameTime ; + long elapsedNanosPerFrame = elapsedNanos / frameTimes.length ; + frameRate = 1_000_000_000.0 / elapsedNanosPerFrame ; + if (updateFPMCounter++ > UPDATE_FPM_PERIOD) { + updateFPMCounter = 0; + drawFps(frameRate.intValue()); + } + raceViewController.updateSparkLine(); + } + + // TODO: 1/05/17 cir27 - Make the RaceObjects update on the actual delay. + elapsedNanos = 1000 / 60; + updateGroups(); + if (StreamParser.isRaceFinished()) { + this.stop(); if (lastTime == 0) { lastTime = now; } else { @@ -150,6 +192,30 @@ public class CanvasController { }; } + /** + * First find the top right and bottom left points' geo locations, then retrieve + * map from google to display on image view. - Haoming 22/5/2017 + */ + private void drawGoogleMap() { + findMetersPerPixel(); + Point2D topLeftPoint = findScaledXY(maxLatPoint.getLatitude(), minLonPoint.getLongitude()); + // distance from top left extreme to panel origin (top left corner) + double distanceFromTopLeftToOrigin = Math.sqrt(Math.pow(topLeftPoint.getX() * metersPerPixelX, 2) + Math.pow(topLeftPoint.getY() * metersPerPixelY, 2)); + // angle from top left extreme to panel origin + double bearingFromTopLeftToOrigin = Math.toDegrees(Math.atan2(-topLeftPoint.getX(), topLeftPoint.getY())); + // the top left extreme + Position topLeftPos = new Position(maxLatPoint.getLatitude(), minLonPoint.getLongitude()); + Position originPos = GeoUtility.getGeoCoordinate(topLeftPos, bearingFromTopLeftToOrigin, distanceFromTopLeftToOrigin); + + // distance from origin corner to bottom right corner of the panel + double distanceFromOriginToBottomRight = Math.sqrt(Math.pow(PANEL_HEIGHT* metersPerPixelY, 2) + Math.pow(PANEL_WIDTH * metersPerPixelX, 2)); + double bearingFromOriginToBottomRight = Math.toDegrees(Math.atan2(PANEL_WIDTH, -PANEL_HEIGHT)); + Position bottomRightPos = GeoUtility.getGeoCoordinate(originPos, bearingFromOriginToBottomRight, distanceFromOriginToBottomRight); + + Boundary boundary = new Boundary(originPos.getLat(), bottomRightPos.getLng(), bottomRightPos.getLat(), originPos.getLng()); + CanvasMap canvasMap = new CanvasMap(boundary); + mapImage.setImage(canvasMap.getMapImage()); + } /** * Adds border marks to the canvas, taken from the XML file @@ -196,11 +262,9 @@ public class CanvasController { private void checkForCourseChanges() { if (StreamParser.isNewRaceXmlReceived()){ - gc.setFill(Color.SKYBLUE); - gc.fillRect(0,0, CANVAS_WIDTH, CANVAS_HEIGHT); - gc.restore(); + gc.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + drawGoogleMap(); addRaceBorder(); - canvas.toBack(); } } @@ -453,6 +517,27 @@ public class CanvasController { return new Point2D(xAxisLocation, yAxisLocation); } + /** + * Find the number of meters per pixel. + */ + private void findMetersPerPixel () { + Point2D p1, p2; + Mark m1, m2; + double theta, distance, dx, dy, dHorizontal, dVertical; + m1 = new SingleMark("m1", maxLatPoint.getLatitude(), minLonPoint.getLongitude(), 1); + m2 = new SingleMark("m2", minLatPoint.getLatitude(), maxLonPoint.getLongitude(), 2); + p1 = findScaledXY(m1); + p2 = findScaledXY(m2); + theta = Mark.calculateHeadingRad(m1, m2); + distance = Mark.calculateDistance(m1, m2); + dHorizontal = Math.abs(Math.sin(theta) * distance); + dVertical = Math.abs(Math.cos(theta) * distance); + dx = Math.abs(p1.getX() - p2.getX()); + dy = Math.abs(p1.getY() - p2.getY()); + metersPerPixelX = dHorizontal / dx; + metersPerPixelY = dVertical / dy; + } + List getBoatGroups() { return boatGroups; } diff --git a/src/main/java/seng302/controllers/RaceController.java b/src/main/java/seng302/controllers/RaceController.java new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/seng302/controllers/RaceViewController.java b/src/main/java/seng302/controllers/RaceViewController.java index 9c55f9a2..57793426 100644 --- a/src/main/java/seng302/controllers/RaceViewController.java +++ b/src/main/java/seng302/controllers/RaceViewController.java @@ -6,7 +6,12 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; +import javafx.geometry.Side; import javafx.scene.Scene; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.chart.XYChart.Series; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; @@ -31,12 +36,19 @@ import seng302.models.stream.StreamParser; import java.io.IOException; import java.util.*; +import seng302.models.stream.XMLParser.RaceXMLObject.Participant; +import java.util.stream.Collectors; /** + * * Created by ptg19 on 29/03/17. */ public class RaceViewController extends Thread implements ImportantAnnotationDelegate { + @FXML + private LineChart raceSparkLine; + @FXML + private NumberAxis sparklineYAxis; @FXML private VBox positionVbox; @FXML @@ -60,7 +72,8 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel private boolean displayFps; private Timeline timerTimeline; private Stage stage; - + private static HashMap> sparkLineData = new HashMap<>(); + private static ArrayList racingBoats = new ArrayList<>(); private ImportantAnnotationsState importantAnnotations; private Yacht selectedBoat; @@ -68,6 +81,14 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel // Load a default important annotation state importantAnnotations = new ImportantAnnotationsState(); + //Formatting the y axis of the sparkline + raceSparkLine.getYAxis().setRotate(180); + raceSparkLine.getYAxis().setTickLabelRotation(180); + raceSparkLine.getYAxis().setTranslateX(15); + raceSparkLine.getYAxis().setAutoRanging(false); + + startingBoats = new ArrayList<>(StreamParser.getBoats().values()); + includedCanvasController.setup(this); includedCanvasController.initializeCanvas(); initializeUpdateTimer(); @@ -75,14 +96,13 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel initialiseAnnotationSlider(); initialiseBoatSelectionComboBox(); includedCanvasController.timer.start(); - - selectAnnotationBtn.setOnAction(event -> { - loadSelectAnnotationView(); - }); + selectAnnotationBtn.setOnAction(event -> loadSelectAnnotationView()); } + /** * The important annotations have been changed, update this view + * * @param importantAnnotationsState The current state of the selected annotations */ public void importantAnnotationsChanged(ImportantAnnotationsState importantAnnotationsState) { @@ -90,6 +110,7 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel setAnnotations((int) annotationSlider.getValue()); // Refresh the displayed annotations } + /** * Loads the "select annotations" view in a new window */ @@ -127,6 +148,7 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel (observable, oldValue, newValue) -> displayFps = !displayFps); } + private void initialiseAnnotationSlider() { annotationSlider.setLabelFormatter(new StringConverter() { @Override @@ -167,6 +189,79 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel } + /** + * Used to add any new boats into the race that may have started late or not have had data received yet + */ + void updateSparkLine(){ + // Collect the racing boats that aren't already in the chart + ArrayList sparkLineCandidates = startingBoats.stream().filter(yacht -> !sparkLineData.containsKey(yacht.getSourceID()) + && yacht.getPosition() != null & yacht.getPosition() != "-").collect(Collectors.toCollection(ArrayList::new)); + + // Obtain the qualifying boats to set the max on the Y axis + racingBoats = startingBoats.stream().filter(yacht -> + yacht.getPosition() != null & yacht.getPosition() != "-").collect(Collectors.toCollection(ArrayList::new)); + sparklineYAxis.setUpperBound(racingBoats.size() + 1); + + // Create a new data series for new boats + sparkLineCandidates.stream().filter(yacht -> yacht.getPosition() != null).forEach(yacht -> { + Series yachtData = new Series<>(); + yachtData.setName(yacht.getBoatName()); + yachtData.getData().add(new XYChart.Data<>(Integer.toString(yacht.getLegNumber()), 1 + racingBoats.size() - Double.parseDouble(yacht.getPosition()))); + sparkLineData.put(yacht.getSourceID(), yachtData); + }); + + // Lambda function to sort the series in order of leg (later legs shown more to the right) + List> positions = new ArrayList<>(sparkLineData.values()); + Collections.sort(positions, (o1, o2) -> { + Integer leg1 = Integer.parseInt(o1.getData().get(o1.getData().size()-1).getXValue()); + Integer leg2 = Integer.parseInt(o2.getData().get(o2.getData().size()-1).getXValue()); + if (leg2 < leg1){ + return 1; + } else { + return -1; + } + }); + + + // Adds the new data series to the sparkline (and set the colour of the series) + raceSparkLine.setCreateSymbols(false); + positions.stream().filter(spark -> !raceSparkLine.getData().contains(spark)).forEach(spark -> { + raceSparkLine.getData().add(spark); + spark.getNode().lookup(".chart-series-line").setStyle("-fx-stroke:" + getBoatColorAsRGB(spark.getName())); + }); + } + + + /** + * Updates the yachts sparkline of the desired boat and using the new leg number + * @param yacht The yacht to be updated on the sparkline + * @param legNumber the leg number that the position will be assigned to + */ + public static void updateYachtPositionSparkline(Yacht yacht, Integer legNumber){ + XYChart.Series positionData = sparkLineData.get(yacht.getSourceID()); + positionData.getData().add(new XYChart.Data<>(Integer.toString(legNumber), 1 + racingBoats.size() - Double.parseDouble(yacht.getPosition()))); + } + + + /** + * gets the rgb string of the boats colour to use for the chart via css + * @param boatName boat passed in to get the boats colour + * @return the colour as an rgb string + */ + private String getBoatColorAsRGB(String boatName){ + Color color = Color.WHITE; + for (Yacht yacht: startingBoats){ + if (Objects.equals(yacht.getBoatName(), boatName)){ + color = yacht.getColour(); + } + } + return String.format( "#%02X%02X%02X", + (int)( color.getRed() * 255 ), + (int)( color.getGreen() * 255 ), + (int)( color.getBlue() * 255 ) ); + } + + /** * Initalises a timer which updates elements of the RaceView such as wind direction, boat * orderings etc.. which are dependent on the info from the stream parser constantly. @@ -183,7 +278,6 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel updateWindDirection(); updateOrder(); updateBoatSelectionComboBox(); - }) ); @@ -234,21 +328,42 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel positionVbox.getChildren().removeAll(); positionVbox.getStylesheets().add(getClass().getResource("/css/master.css").toString()); - for (Yacht boat : StreamParser.getBoatsPos().values()) { - if (boat.getBoatStatus() == 3) { // 3 is finish status - Text textToAdd = new Text(boat.getPosition() + ". " + - boat.getShortName() + " (Finished)"); - textToAdd.setFill(Paint.valueOf("#d3d3d3")); - positionVbox.getChildren().add(textToAdd); + // list of racing boat id + ArrayList participants = StreamParser.getXmlObject().getRaceXML() + .getParticipants(); + ArrayList participantIDs = new ArrayList<>(); + for (Participant p : participants) { + participantIDs.add(p.getsourceID()); + } - } else { - Text textToAdd = new Text(boat.getPosition() + ". " + - boat.getShortName() + " "); - textToAdd.setFill(Paint.valueOf("#d3d3d3")); - textToAdd.setStyle(""); - positionVbox.getChildren().add(textToAdd); + if (StreamParser.isRaceStarted()) { + for (Yacht boat : StreamParser.getBoatsPos().values()) { + if (participantIDs.contains(boat.getSourceID())) { // check if the boat is racing + if (boat.getBoatStatus() == 3) { // 3 is finish status + Text textToAdd = new Text(boat.getPosition() + ". " + + boat.getShortName() + " (Finished)"); + textToAdd.setFill(Paint.valueOf("#d3d3d3")); + positionVbox.getChildren().add(textToAdd); + + } else { + Text textToAdd = new Text(boat.getPosition() + ". " + + boat.getShortName() + " "); + textToAdd.setFill(Paint.valueOf("#d3d3d3")); + textToAdd.setStyle(""); + positionVbox.getChildren().add(textToAdd); + } + } + } + } else { + for (Yacht boat : StreamParser.getBoats().values()) { + if (participantIDs.contains(boat.getSourceID())) { // check if the boat is racing + Text textToAdd = new Text(boat.getPosition() + ". " + + boat.getShortName() + " "); + textToAdd.setFill(Paint.valueOf("#d3d3d3")); + textToAdd.setStyle(""); + positionVbox.getChildren().add(textToAdd); + } } - } } @@ -323,10 +438,53 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel } - public boolean isDisplayFps() { + boolean isDisplayFps() { return displayFps; } + /** + * Display the important annotations for a specific BoatGroup + * + * @param bg The boat group to set the annotations for + */ + private void setBoatGroupImportantAnnotations(BoatGroup bg) { + if (importantAnnotations.getAnnotationState(Annotation.NAME)) { + bg.setTeamNameObjectVisible(true); + } else { + bg.setTeamNameObjectVisible(false); + } + + if (importantAnnotations.getAnnotationState(Annotation.SPEED)) { + bg.setVelocityObjectVisible(true); + } else { + bg.setVelocityObjectVisible(false); + } + + if (importantAnnotations.getAnnotationState(Annotation.TRACK)) { + bg.setLineGroupVisible(true); + } else { + bg.setLineGroupVisible(false); + } + + if (importantAnnotations.getAnnotationState(Annotation.WAKE)) { + bg.setWakeVisible(true); + } else { + bg.setWakeVisible(false); + } + //TODO fix boat annotations with new boatgroup + if (importantAnnotations.getAnnotationState(Annotation.ESTTIMETONEXTMARK)) { + bg.setEstTimeToNextMarkObjectVisible(true); + } else { + bg.setEstTimeToNextMarkObjectVisible(false); + } + + if (importantAnnotations.getAnnotationState(Annotation.LEGTIME)) { + bg.setLegTimeObjectVisible(true); + } else { + bg.setLegTimeObjectVisible(false); + } + } + private void setAnnotations(Integer annotationLevel) { switch (annotationLevel) { // No Annotations @@ -383,4 +541,13 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel Stage getStage() { return stage; } + + /** + * Used for when the boat attempts to add data to the sparkline (first checks if the sparkline contains info on it) + * @param yachtId + * @return + */ + public static boolean sparkLineStatus(Integer yachtId) { + return sparkLineData.containsKey(yachtId); + } } \ No newline at end of file diff --git a/src/main/java/seng302/controllers/StartScreenController.java b/src/main/java/seng302/controllers/StartScreenController.java index debeb371..22e5dd1b 100644 --- a/src/main/java/seng302/controllers/StartScreenController.java +++ b/src/main/java/seng302/controllers/StartScreenController.java @@ -2,6 +2,7 @@ package seng302.controllers; import java.io.IOException; import java.net.URL; +import java.util.ArrayList; import java.util.ResourceBundle; import java.util.Timer; import java.util.TimerTask; @@ -23,8 +24,10 @@ import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import seng302.models.Yacht; import seng302.models.stream.StreamParser; +import seng302.models.stream.XMLParser.RaceXMLObject.Participant; public class StartScreenController implements Initializable { + @FXML private GridPane gridPane; @FXML @@ -48,19 +51,18 @@ public class StartScreenController implements Initializable { private boolean switchedToRaceView = false; - private void setContentPane(String jfxUrl){ - try{ + private void setContentPane(String jfxUrl) { + try { // get the main controller anchor pane (MainView.fxml) AnchorPane contentPane = (AnchorPane) gridPane.getParent(); contentPane.getChildren().removeAll(); contentPane.getChildren().clear(); contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString()); - contentPane.getChildren().addAll((Pane) FXMLLoader.load(getClass().getResource(jfxUrl))); - } - catch(javafx.fxml.LoadException e){ + contentPane.getChildren() + .addAll((Pane) FXMLLoader.load(getClass().getResource(jfxUrl))); + } catch (javafx.fxml.LoadException e) { e.printStackTrace(); - } - catch(IOException e){ + } catch (IOException e) { e.printStackTrace(); } } @@ -72,7 +74,8 @@ public class StartScreenController implements Initializable { } /** - * Running a timer to update the livestream status on welcome screen. Update interval is 1 second. + * Running a timer to update the livestream status on welcome screen. Update interval is 1 + * second. */ public void startStream() { if (StreamParser.isStreamStatus()) { @@ -102,8 +105,10 @@ public class StartScreenController implements Initializable { updateTeamList(); timeTillLive.setTextFill(Color.RED); switchToRaceViewButton.setDisable(false); - String timerMinute = Long.toString(StreamParser.getTimeSinceStart() / 60); - String timerSecond = Long.toString(StreamParser.getTimeSinceStart() % 60); + String timerMinute = Long + .toString(StreamParser.getTimeSinceStart() / 60); + String timerSecond = Long + .toString(StreamParser.getTimeSinceStart() % 60); if (timerSecond.length() == 1) { timerSecond = "0" + timerSecond; } @@ -114,8 +119,10 @@ public class StartScreenController implements Initializable { updateTeamList(); timeTillLive.setTextFill(Color.BLACK); switchToRaceViewButton.setDisable(false); - String timerMinute = Long.toString(-1 * StreamParser.getTimeSinceStart() / 60); - String timerSecond = Long.toString(-1 * StreamParser.getTimeSinceStart() % 60); + String timerMinute = Long + .toString(-1 * StreamParser.getTimeSinceStart() / 60); + String timerSecond = Long + .toString(-1 * StreamParser.getTimeSinceStart() % 60); if (timerSecond.length() == 1) { timerSecond = "0" + timerSecond; } @@ -143,19 +150,40 @@ public class StartScreenController implements Initializable { teamList.setItems(data); boatNameCol.setCellValueFactory( - new PropertyValueFactory<>("boatName") + new PropertyValueFactory<>("boatName") ); shortNameCol.setCellValueFactory( - new PropertyValueFactory<>("shortName") + new PropertyValueFactory<>("shortName") ); countryCol.setCellValueFactory( - new PropertyValueFactory<>("country") + new PropertyValueFactory<>("country") ); posCol.setCellValueFactory( - new PropertyValueFactory<>("position") + new PropertyValueFactory<>("position") ); - data.addAll(StreamParser.getBoatsPos().values()); + // check if the boat is racing + ArrayList participants = StreamParser.getXmlObject().getRaceXML() + .getParticipants(); + ArrayList participantIDs = new ArrayList<>(); + for (Participant p : participants) { + participantIDs.add(p.getsourceID()); + } + + // add boats to the start screen list + if (StreamParser.isRaceStarted()) { // if race is started, use StreamParser.getBoatsPos() + for (Yacht boat : StreamParser.getBoatsPos().values()) { + if (participantIDs.contains(boat.getSourceID())) { + data.add(boat); + } + } + } else { // else use StreamParser.getBoats() + for (Yacht boat : StreamParser.getBoats().values()) { + if (participantIDs.contains(boat.getSourceID())) { + data.add(boat); + } + } + } teamList.refresh(); } } diff --git a/src/main/java/seng302/models/Yacht.java b/src/main/java/seng302/models/Yacht.java index 90e24739..fbbf9849 100644 --- a/src/main/java/seng302/models/Yacht.java +++ b/src/main/java/seng302/models/Yacht.java @@ -5,7 +5,9 @@ import javafx.beans.property.*; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.fxml.Initializable; + import javafx.scene.paint.Color; +import seng302.controllers.RaceViewController; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -17,9 +19,10 @@ import java.util.List; * Yacht class for the racing boat. * * Class created to store more variables (eg. boat statuses) compared to the XMLParser boat class, - * also done outside Boat class because some old variables are not used anymore. + * also done outside Boat class because some old variables are not used anymore. */ public class Yacht { + // Used in boat group private Color colour; @@ -41,21 +44,22 @@ public class Yacht { private Long markRoundTime; + /** * Used in EventTest and RaceTest. * * @param boatName Create a yacht object with name. */ - public Yacht (String boatName) { + public Yacht(String boatName) { this.boatName = boatName; } /** * Used in BoatGroupTest. * - * @param boatName The name of the team sailing the boat + * @param boatName The name of the team sailing the boat * @param boatVelocity The speed of the boat in meters/second - * @param shortName A shorter version of the teams name + * @param shortName A shorter version of the teams name */ public Yacht(String boatName, double boatVelocity, String shortName, int id) { this.boatName = boatName; @@ -64,30 +68,37 @@ public class Yacht { this.sourceID = id; } - public Yacht(String boatType, Integer sourceID, String hullID, String shortName, String boatName, String country) { + public Yacht(String boatType, Integer sourceID, String hullID, String shortName, + String boatName, String country) { this.boatType = boatType; this.sourceID = sourceID; this.hullID = hullID; this.shortName = shortName; this.boatName = boatName; this.country = country; + this.position = "-"; } public String getBoatType() { return boatType; } + public Integer getSourceID() { return sourceID; } + public String getHullID() { return hullID; } + public String getShortName() { return shortName; } + public String getBoatName() { return boatName; } + public String getCountry() { return country; } @@ -105,6 +116,9 @@ public class Yacht { } public void setLegNumber(Integer legNumber) { + if (colour != null && position != "-" && legNumber != this.legNumber&& RaceViewController.sparkLineStatus(sourceID)) { + RaceViewController.updateYachtPositionSparkline(this, legNumber); + } this.legNumber = legNumber; } diff --git a/src/main/java/seng302/models/map/Boundary.java b/src/main/java/seng302/models/map/Boundary.java new file mode 100644 index 00000000..4396d95d --- /dev/null +++ b/src/main/java/seng302/models/map/Boundary.java @@ -0,0 +1,44 @@ +package seng302.models.map; + +/** + * The Boundary class represents a rectangle territorial boundary on a map. It + * contains four extremity double values(N, E, S, W). N and S are represented as + * latitudes in radians. E and W are represented as longitudes in radians. + * + * Created by Haoming on 10/5/17 + */ +public class Boundary { + + private double northLat, eastLng, southLat, westLng; + + public Boundary(double northLat, double eastLng, double southLat, double westLng) { + this.northLat = northLat; + this.eastLng = eastLng; + this.southLat = southLat; + this.westLng = westLng; + } + + double getCentreLat() { + return (northLat + southLat) / 2; + } + + double getCentreLng() { + return (eastLng + westLng) / 2; + } + + double getNorthLat() { + return northLat; + } + + double getEastLng() { + return eastLng; + } + + double getSouthLat() { + return southLat; + } + + double getWestLng() { + return westLng; + } +} diff --git a/src/main/java/seng302/models/map/CanvasMap.java b/src/main/java/seng302/models/map/CanvasMap.java new file mode 100644 index 00000000..162358a1 --- /dev/null +++ b/src/main/java/seng302/models/map/CanvasMap.java @@ -0,0 +1,104 @@ +package seng302.models.map; + +import javafx.scene.image.Image; + +import javax.net.ssl.HttpsURLConnection; +import java.net.URL; + +import java.lang.Math; + +/** + * CanvasMap retrieves a map image with given geo boundary from Google Map server. + * By passing a rectangle like geo boundary, it returns a map image with the + * highest resolution. However, due to free quote account usage limit, the maximum + * resolution is only 1280 * 1280. + * + * Created by Haoming on 15/5/2017 + */ +public class CanvasMap { + + private Boundary boundary; + private long width, height; // desired image size + private int zoom; + + private String KEY = "AIzaSyC-5oOShMCY5Oy_9L7guYMPUPFHDMr37wE"; + + public CanvasMap(Boundary boundary) { + this.boundary = boundary; + calculateOptimalMapSize(); + } + + public Image getMapImage() { + try { + URL url = new URL(getRequest()); + HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + + return new Image(connection.getInputStream()); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + private String getRequest() { + StringBuilder sb = new StringBuilder(); + sb.append("https://maps.googleapis.com/maps/api/staticmap?"); + sb.append(String.format("center=%f,%f", boundary.getCentreLat(), boundary.getCentreLng())); + sb.append(String.format("&zoom=%d", zoom)); + sb.append(String.format("&size=%dx%d&scale=2", width, height)); + sb.append("&style=feature:all|element:labels|visibility:off"); // hide all labels on map +// sb.append(String.format("&markers=%f,%f", boundary.getSouthLat(), boundary.getWestLng())); +// sb.append(String.format("&key=%s", KEY)); + return sb.toString(); + } + + private void calculateOptimalMapSize() { + for (int z = 20; z > 0; z--) { + MapSize mapSize = getMapSize(z, boundary); + zoom = z; + width = mapSize.width; + height = mapSize.height; + // if map size is valid, exit the loop as we have the highest resolution + if (mapSize.isValid()) break; + } + } + + private MapSize getMapSize(int zoom, Boundary boundary) { + double scale = Math.pow(2, zoom); + MapGeo geoSW = new MapGeo(boundary.getSouthLat(), boundary.getWestLng()); + MapGeo geoNE = new MapGeo(boundary.getNorthLat(), boundary.getEastLng()); + MapPoint pointSW = MercatorProjection.toMapPoint(geoSW); + MapPoint pointNE = MercatorProjection.toMapPoint(geoNE); + return new MapSize(Math.abs(pointNE.getX() - pointSW.getX()) * scale, + Math.abs(pointNE.getY() - pointSW.getY()) * scale); + } + + class MapSize { + long width, height; + + MapSize(double width, double height) { + this.width = Math.round(width); + this.height = Math.round(height); + } + + /** + * Map size is valid when width and height are both less than 640 pixels + * @return true if both dimensions are less than 640px + */ + boolean isValid() { + return Math.max(width, height) <= 640; + } + } + + public long getWidth() { + return width; + } + + public long getHeight() { + return height; + } + + public int getZoom() { + return zoom; + } +} diff --git a/src/main/java/seng302/models/map/MapGeo.java b/src/main/java/seng302/models/map/MapGeo.java new file mode 100644 index 00000000..43d02565 --- /dev/null +++ b/src/main/java/seng302/models/map/MapGeo.java @@ -0,0 +1,31 @@ +package seng302.models.map; + +/** + * A class represent Geo location (latitude, longitude). + * Created by Haoming on 15/5/2017 + */ +class MapGeo { + + private double lat, lng; + + MapGeo(double lat, double lng) { + this.lat = lat; + this.lng = lng; + } + + double getLat() { + return lat; + } + + void setLat(double lat) { + this.lat = lat; + } + + double getLng() { + return lng; + } + + void setLng(double lng) { + this.lng = lng; + } +} diff --git a/src/main/java/seng302/models/map/MapPoint.java b/src/main/java/seng302/models/map/MapPoint.java new file mode 100644 index 00000000..41be919a --- /dev/null +++ b/src/main/java/seng302/models/map/MapPoint.java @@ -0,0 +1,31 @@ +package seng302.models.map; + +/** + * A class represent euclidean planar point (x, y) + * Created by Haoming on 15/5/2017 + */ +class MapPoint { + + private double x, y; + + MapPoint(double x, double y) { + this.x = x; + this.y = y; + } + + double getX() { + return x; + } + + void setX(double x) { + this.x = x; + } + + double getY() { + return y; + } + + void setY(double y) { + this.y = y; + } +} diff --git a/src/main/java/seng302/models/map/MercatorProjection.java b/src/main/java/seng302/models/map/MercatorProjection.java new file mode 100644 index 00000000..b4bf647d --- /dev/null +++ b/src/main/java/seng302/models/map/MercatorProjection.java @@ -0,0 +1,52 @@ +package seng302.models.map; + +/** + * An utility class useful to convert between Geo locations and Mercator projection + * planar coordinates. + * Created by Haoming on 15/5/2017 + */ +public class MercatorProjection { + + private static final double MERCATOR_RANGE = 256; + private static final double pixelsPerLngDegree = MERCATOR_RANGE / 360.0; + private static final double pixelsPerLngRadian = MERCATOR_RANGE / (2 * Math.PI); + + /** + * A help function keeps the value in bound between -0.9999 and 0.9999. + * @param value in bound value + * @return the value in bound + */ + private static double bound(double value) { + return Math.min(Math.max(value, -0.9999), 0.9999); + } + + /** + * Projects a Geo Location (lat, lng) on a planar + * @param geo MapGeo (lat, lng) location to be projected + * @return the projection GeoPoint (x, y) on planar + */ + public static MapPoint toMapPoint(MapGeo geo) { + MapPoint point = new MapPoint(0, 0); + MapPoint origin = new MapPoint(MERCATOR_RANGE / 2.0, MERCATOR_RANGE / 2.0); + point.setX(origin.getX() + geo.getLng() * pixelsPerLngDegree); + +// NOTE(appleton): Truncating to 0.9999 effectively limits latitude to +// 89.189. This is about a third of a tile past the edge of the world tile. + double sinY = bound(Math.sin(Math.toRadians(geo.getLat()))); + point.setY(origin.getY() + 0.5 * Math.log((1 + sinY) / (1 - sinY)) * (-pixelsPerLngRadian)); + return point; + } + + /** + * Converts the planar projection (x, y) back to Geo Location (lat, lng) + * @param point MapPoint (x, y) to be converted back + * @return the original Geo location converted from the given projection point + */ + public static MapGeo toMapGeo(MapPoint point) { + MapPoint origin = new MapPoint(MERCATOR_RANGE / 2.0, MERCATOR_RANGE / 2.0); + double lng = (point.getX() - origin.getX()) / pixelsPerLngDegree; + double latRadians = (point.getY() - origin.getY()) / (-pixelsPerLngRadian); + double lat = Math.toDegrees(2 * Math.atan(Math.exp(latRadians)) - Math.PI / 2.0); + return new MapGeo(lat, lng); + } +} diff --git a/src/main/java/seng302/models/map/TestMapController.java b/src/main/java/seng302/models/map/TestMapController.java new file mode 100644 index 00000000..fc319bcc --- /dev/null +++ b/src/main/java/seng302/models/map/TestMapController.java @@ -0,0 +1,23 @@ +package seng302.models.map; + +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; + +import java.net.URL; +import java.util.ResourceBundle; + +public class TestMapController implements Initializable{ + + @FXML + private Canvas mapCanvas; + + @Override + public void initialize(URL location, ResourceBundle resources) { + GraphicsContext gc = mapCanvas.getGraphicsContext2D(); + Boundary bound = new Boundary(57.662943, 11.848501, 57.673945, 11.824966); + CanvasMap canvasMap = new CanvasMap(bound); + gc.drawImage(canvasMap.getMapImage(), 0, 0, canvasMap.getWidth(), canvasMap.getHeight()); + } +} diff --git a/src/main/resources/css/master.css b/src/main/resources/css/master.css index 8223fc00..bef93923 100644 --- a/src/main/resources/css/master.css +++ b/src/main/resources/css/master.css @@ -181,4 +181,8 @@ Remove scroll bars -fx-background-radius: 0; -fx-background-insets: 0; -fx-padding: 0; -} \ No newline at end of file +} + +.chart{ + -fx-background-color: #ffffff; +} diff --git a/src/main/resources/views/CanvasView.fxml b/src/main/resources/views/CanvasView.fxml index bc16ad7e..94c08695 100644 --- a/src/main/resources/views/CanvasView.fxml +++ b/src/main/resources/views/CanvasView.fxml @@ -4,4 +4,4 @@ - + diff --git a/src/main/resources/views/RaceView.fxml b/src/main/resources/views/RaceView.fxml index 7b81dd19..b225ec98 100644 --- a/src/main/resources/views/RaceView.fxml +++ b/src/main/resources/views/RaceView.fxml @@ -1,11 +1,13 @@ + + - + @@ -16,7 +18,7 @@ - +