package visualiser.model; import javafx.scene.image.Image; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.scene.transform.Rotate; import network.Messages.Enums.BoatStatusEnum; import shared.dataInput.RaceDataSource; import shared.model.GPSCoordinate; import shared.model.Mark; import shared.model.RaceClock; import java.util.List; /** * This JavaFX Canvas is used to update and display details for a * {@link RaceMap} via the * {@link visualiser.Controllers.RaceController}.
* It fills it's parent and cannot be downsized.
* Details displayed include: * {@link VisualiserBoat}s (and their * {@link TrackPoint}s), * {@link shared.model.Mark}s, a * {@link RaceClock}, a wind direction arrow and * various user selected {@link Annotations}. */ public class ResizableRaceCanvas extends ResizableCanvas { private RaceMap map; // for converting GPSCoordinates to GraphCoordinates private VisualiserRace visualiserRace; // draw data read from this race private Image background; private Image sailsRight = new Image("/images/sailsRight.png"); private Image sailsLeft = new Image("/images/sailsLeft.png"); // annotations private boolean annoName = true; private boolean annoAbbrev = true; private boolean annoSpeed = true; private boolean annoPath = true; private boolean annoEstTime = true; private boolean annoTimeSinceLastMark = true; /** * Constructs a {@link ResizableRaceCanvas} using a given {@link VisualiserRace}. * @param visualiserRace The race that data is read from in order to be drawn. */ public ResizableRaceCanvas(VisualiserRace visualiserRace) { super(); this.visualiserRace = visualiserRace; RaceDataSource raceData = visualiserRace.getRaceDataSource(); double lat1 = raceData.getMapTopLeft().getLatitude(); double long1 = raceData.getMapTopLeft().getLongitude(); double lat2 = raceData.getMapBottomRight().getLatitude(); double long2 = raceData.getMapBottomRight().getLongitude(); this.map = new RaceMap( lat1, long1, lat2, long2, (int)getWidth(), (int)getHeight()); } /** * Toggle name display in annotation */ public void toggleAnnoName() { annoName = !annoName; } /** * Toggle boat path display in annotation */ public void toggleBoatPath() { annoPath = !annoPath; } public void toggleAnnoEstTime() { annoEstTime = !annoEstTime; } /** * Toggle boat time display in annotation */ public void toggleAnnoTime() { annoTimeSinceLastMark = !annoTimeSinceLastMark; } /** * Toggle abbreviation display in annotation */ public void toggleAnnoAbbrev() { annoAbbrev = !annoAbbrev; } /** * Toggle speed display in annotation */ public void toggleAnnoSpeed() { annoSpeed = !annoSpeed; } /** * Rotates things displayed on the canvas. Note: this must be called in * between gc.save() and gc.restore() else they will rotate everything * * @param degrees Bearing degrees to rotate. * @param px Pivot point x of rotation. * @param py Pivot point y of rotation. */ private void rotate(double degrees, double px, double py) { Rotate r = new Rotate(degrees, px, py); gc.setTransform(r.getMxx(), r.getMyx(), r.getMxy(), r.getMyy(), r.getTx(), r.getTy()); } /** * Draws a circle with a given diameter, centred on a given graph coordinate. * @param center The center coordinate of the circle. * @param diameter The diameter of the circle. */ private void drawCircle(GraphCoordinate center, double diameter) { //The graphCoordinates are for the center of the point, so we offset them to get the corner coordinate. gc.fillOval( center.getX() - (diameter / 2), center.getY() - (diameter / 2), diameter, diameter ); } /** * Displays a line on the map with rectangles on the starting and ending point of the line. * * @param graphCoordinateA Starting Point of the line in GraphCoordinate. * @param graphCoordinateB End Point of the line in GraphCoordinate. * @param paint Colour the line is to coloured. */ private void drawLine(GraphCoordinate graphCoordinateA, GraphCoordinate graphCoordinateB, Paint paint) { gc.setStroke(paint); gc.setFill(paint); double endPointDiameter = 6; //Draw first end-point. drawCircle(graphCoordinateA, endPointDiameter); //Draw second end-point. drawCircle(graphCoordinateB, endPointDiameter); //Draw line between them. gc.strokeLine( graphCoordinateA.getX(), graphCoordinateA.getY(), graphCoordinateB.getX(), graphCoordinateB.getY() ); } /** * Display a point on the Canvas. It has a diameter of 10 pixels. * * @param graphCoordinate Coordinate that the point is to be displayed at. * @param paint Paint to use for the point. */ private void drawPoint(GraphCoordinate graphCoordinate, Paint paint) { //Set paint. gc.setFill(paint); double pointDiameter = 10; //Draw the point. drawCircle(graphCoordinate, pointDiameter); } /** * Display given name and speed of boat at a graph coordinate * * @param name name of the boat * @param abbrev abbreviation of the boat name * @param speed speed of the boat * @param coordinate coordinate the text appears * @param timeToNextMark The time until the boat reaches the next mark. * @param timeSinceLastMark The time since the boat passed the last mark. */ private void drawText(String name, String abbrev, double speed, GraphCoordinate coordinate, String timeToNextMark, String timeSinceLastMark) { //The text to draw. Built during the function. String text = ""; //Draw name if annotation is enabled. if (annoName) { text += String.format("%s ", name); } //Draw abbreviation/country if annotation is enabled. if (annoAbbrev) { text += String.format("%s ", abbrev); } //Draw speed if annotation is enabled. if (annoSpeed){ text += String.format("%.2fkn ", speed); } //Draw time to reach next mark if annotation is enabled. if (annoEstTime) { text += timeToNextMark; } //Draw time since last mark if annotation is enabled. if(annoTimeSinceLastMark) { text += timeSinceLastMark; } //Offset by 20 pixels horizontally. long xCoord = coordinate.getX() + 20; long yCoord = coordinate.getY(); //If the text would extend out of the canvas (to the right), move it left. if (xCoord + (text.length() * 7) >= getWidth()) { xCoord -= text.length() * 7; } if (yCoord - (text.length() * 2) <= 0) { yCoord += 30; } //Draw text. gc.fillText(text, xCoord, yCoord); } /** * Draws the label for a given boat. Includes name, abbreviation, speed, time since mark, and time to next mark. * @param boat The boat to draw text for. */ private void drawBoatText(VisualiserBoat boat) { drawText( boat.getName(), boat.getCountry(), boat.getCurrentSpeed(), this.map.convertGPS(boat.getCurrentPosition()), boat.getTimeToNextMarkFormatted(this.visualiserRace.getRaceClock().getCurrentTime()), boat.getTimeSinceLastMarkFormatted(this.visualiserRace.getRaceClock().getCurrentTime()) ); } /** * Draws all of the boats on the canvas. */ private void drawBoats() { for (VisualiserBoat boat : visualiserRace.getBoats()) { //Draw track. drawTrack(boat); //Only draw wake if they are currently racing. if (boat.getStatus() == BoatStatusEnum.RACING) { drawWake(boat); } //Draw the boat. drawBoat(boat); //If the race hasn't started, we set the time since last mark to the current time, to ensure we don't start counting until the race actually starts. if ((boat.getStatus() != BoatStatusEnum.RACING) && (boat.getStatus() == BoatStatusEnum.FINISHED)) { boat.setTimeAtLastMark(visualiserRace.getRaceClock().getCurrentTime()); } //Draw boat label. drawBoatText(boat); } } /** * Draws a given boat on the canvas. * @param boat The boat to draw. */ private void drawBoat(VisualiserBoat boat) { //The position may be null if we haven't received any BoatLocation messages yet. if (boat.getCurrentPosition() != null) { //Convert position to graph coordinate. GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition()); //The x coordinates of each vertex of the boat. double[] x = { pos.getX() - 6, pos.getX(), pos.getX() + 6 }; //The y coordinates of each vertex of the boat. double[] y = { pos.getY() + 12, pos.getY() - 12, pos.getY() + 12 }; //The above shape is essentially a triangle 12px wide, and 24px long. //Draw the boat. gc.setFill(boat.getColor()); gc.save(); rotate(boat.getBearing().degrees(), pos.getX(), pos.getY()); gc.fillPolygon(x, y, 3); gc.restore(); if (boat.getSourceID() == ThisBoat.getInstance().getSourceID()) { drawSails(boat); } } } /** * Draws sails for a given boat on the canvas. Sail position is * determined by the boats heading and the current wind direction * according to the "points of sail". * @param boat boat to display sails for */ private void drawSails(VisualiserBoat boat) { GraphCoordinate boatPos = this.map.convertGPS(boat.getCurrentPosition()); double xPos = boatPos.getX(); // x pos of sail (on boat) double yPos = boatPos.getY() - 6; // y pos of sail (on boat) double boatBearing = boat.getBearing().degrees(); double windDirection = visualiserRace.getWindDirection().degrees(); double sailRotateAngle = 0; // rotation for correct sail display Image sailImage = null; Boolean rightSail = true; // Getting the correct Points of Sail if (ThisBoat.getInstance().isSailsOut()){ // correct sail and sailRotateAngle start depending on wind+bearing if ((windDirection + 180) > 360) { if ((boatBearing < windDirection) && (boatBearing > windDirection - 180)) { rightSail = false; } else { if (boatBearing < 180) { sailRotateAngle = -180; } } } else { if (!((boatBearing > windDirection) && (boatBearing < windDirection + 180))) { rightSail = false; if (boatBearing > 180) { sailRotateAngle = -180; } } } if (rightSail) { sailImage = sailsRight; xPos -= 1; // right align sail to boat edge on canvas } else { sailImage = sailsLeft; xPos -= 5; // left align sail to boat edge on canvas } } else { // TODO: display luffing sail } sailRotateAngle += ((boatBearing + windDirection) * 0.5); // System.out.println("boat: " + boatBearing + " || rotate: " + // sailRotateAngle + " || wind angle: " + windDirection); gc.save(); // rotate sails based on boats current heading rotate(sailRotateAngle, boatPos.getX(), boatPos.getY()); gc.drawImage(sailImage, xPos, yPos); gc.restore(); } /** * Draws the wake for a given boat. * @param boat Boat to draw wake for. */ private void drawWake(VisualiserBoat boat) { // Calculate either end of wake line. GraphCoordinate wakeFrom = this.map.convertGPS(boat.getCurrentPosition()); GraphCoordinate wakeTo = this.map.convertGPS(boat.getWake()); drawLine(wakeFrom, wakeTo, boat.getColor()); } /** * Draws all of the {@link Mark}s on the canvas. */ private void drawMarks() { for (Mark mark : this.visualiserRace.getMarks()) { drawMark(mark); } } /** * Draws a given mark on the canvas. * @param mark The mark to draw. */ private void drawMark(Mark mark) { GraphCoordinate markToDraw = this.map.convertGPS(mark.getPosition()); drawPoint(markToDraw, Color.LIMEGREEN); } /** * Draws the Race Map. * Called when the canvas is resized. */ public void draw() { clear(); // clear previous canvas //Update our RaceMap using new canvas size. this.map.setWidth((int) getWidth()); this.map.setHeight((int) getHeight()); redrawBoundaryImage(); drawRace(); } /** * Clears the canvas. */ private void clear() { gc.clearRect(0, 0, getWidth(), getHeight()); } /** * Draws the race boundary, and saves the image to {@link #background}. * You should call {@link #clear()} before calling this. */ private void redrawBoundaryImage() { //Prepare to draw. gc.setLineWidth(1); gc.setFill(Color.AQUA); gc.drawImage(new Image(getClass().getClassLoader().getResourceAsStream("images/WaterBackground.png")), 0, 0); //Calculate the screen coordinates of the boundary. List boundary = this.visualiserRace.getBoundary(); double[] xpoints = new double[boundary.size()]; double[] ypoints = new double[boundary.size()]; //For each boundary coordinate. for (int i = 0; i < boundary.size(); i++) { //Convert. GraphCoordinate coord = map.convertGPS(boundary.get(i)); //Use. xpoints[i] = coord.getX(); ypoints[i] = coord.getY(); } //Draw the boundary. gc.fillPolygon(xpoints, ypoints, xpoints.length); //Render boundary to image. this.background = snapshot(null, null); } /** * Draws the race. * Called once per frame, and on canvas resize. */ public void drawRace() { gc.setLineWidth(2); clear(); // clear the previous canvas drawBoundary(); drawBoats(); drawMarks(); } /** * Draws the race boundary image onto the canvas. * See {@link #background}. */ private void drawBoundary() { gc.drawImage(this.background, 0, 0); } /** * Draws all track points for a given boat. Colour is set by boat, opacity * by track point. This checks if {@link #annoPath} is enabled. * @param boat The boat to draw tracks for. * @see TrackPoint */ private void drawTrack(VisualiserBoat boat) { //Check that track points are enabled. if (this.annoPath) { //Apply the boat color. gc.setFill(boat.getColor()); //Draw each TrackPoint. for (TrackPoint point : boat.getTrack()) { //Convert the GPSCoordinate to a screen coordinate. GraphCoordinate scaledCoordinate = this.map.convertGPS(point.getCoordinate()); //Draw a circle for the trackpoint. gc.fillOval(scaledCoordinate.getX(), scaledCoordinate.getY(), point.getDiameter(), point.getDiameter()); } } } }