You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
575 lines
15 KiB
575 lines
15 KiB
package visualiser.model;
|
|
|
|
|
|
import javafx.collections.ObservableList;
|
|
import javafx.scene.Node;
|
|
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.Leg;
|
|
import shared.model.Mark;
|
|
import shared.model.RaceClock;
|
|
|
|
import java.time.Duration;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* This JavaFX Canvas is used to update and display details for a
|
|
* {@link RaceMap} via the
|
|
* {@link visualiser.Controllers.RaceController}.<br>
|
|
* It fills it's parent and cannot be downsized. <br>
|
|
* 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 {
|
|
|
|
/**
|
|
* The RaceMap used for converting GPSCoordinates to GraphCoordinates.
|
|
*/
|
|
private RaceMap map;
|
|
|
|
/**
|
|
* The race we read data from and draw.
|
|
*/
|
|
private VisualiserRace visualiserRace;
|
|
|
|
/**
|
|
* The background of the race.
|
|
* We render the background whenever the race boundary changes, or the screen size changes.
|
|
*/
|
|
private Image background;
|
|
|
|
|
|
private boolean annoName = true;
|
|
private boolean annoAbbrev = true;
|
|
private boolean annoSpeed = true;
|
|
private boolean annoPath = true;
|
|
private boolean annoEstTime = true;
|
|
private boolean annoTimeSinceLastMark = true;
|
|
|
|
|
|
/**
|
|
* The wind arrow node.
|
|
*/
|
|
private Node arrow;
|
|
|
|
|
|
/**
|
|
* Constructs a {@link ResizableRaceCanvas} using a given {@link VisualiserRace}.
|
|
* @param visualiserRace The race that data is read from in order to be drawn.
|
|
* @param arrow The wind arrow's node.
|
|
*/
|
|
public ResizableRaceCanvas(VisualiserRace visualiserRace, Node arrow) {
|
|
super();
|
|
|
|
this.visualiserRace = visualiserRace;
|
|
this.arrow = arrow;
|
|
|
|
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 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 the boat.
|
|
drawBoat(boat);
|
|
|
|
//Only draw wake if they are currently racing.
|
|
if (boat.getStatus() == BoatStatusEnum.RACING) {
|
|
drawWake(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);
|
|
|
|
//Draw track.
|
|
drawTrack(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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* 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());
|
|
|
|
//Draw.
|
|
drawLine(wakeFrom, wakeTo, boat.getColor());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Displays an arrow representing wind direction on the Canvas.
|
|
* This function accepts a wind-to bearing, but displays a wind-from bearing.
|
|
*
|
|
* @param angle Angle that the arrow is to be facing in degrees 0 degrees = North (Up).
|
|
* @see GraphCoordinate
|
|
*/
|
|
private void displayWindArrow(double angle) {
|
|
|
|
//We need to display wind-from, so add 180 degrees.
|
|
angle += 180d;
|
|
|
|
//Get it within [0, 360).
|
|
while (angle >= 360d) {
|
|
angle -= 360d;
|
|
}
|
|
|
|
//Rotate the wind arrow.
|
|
if (arrow != null && arrow.getRotate() != angle) {
|
|
arrow.setRotate(angle);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* 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) {
|
|
|
|
//Calculate screen position.
|
|
GraphCoordinate mark1 = this.map.convertGPS(mark.getPosition());
|
|
|
|
//Draw.
|
|
drawPoint(mark1, Color.LIMEGREEN);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Draws the Race Map.
|
|
* Called when the canvas is resized.
|
|
*/
|
|
public void draw() {
|
|
|
|
//Clear canvas.
|
|
clear();
|
|
|
|
//Update our RaceMap using new canvas size.
|
|
this.map.setWidth((int) getWidth());
|
|
this.map.setHeight((int) getHeight());
|
|
|
|
//Redraw the boundary.
|
|
redrawBoundaryImage();
|
|
|
|
//Draw the race.
|
|
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<GPSCoordinate> 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();
|
|
|
|
//Race boundary.
|
|
drawBoundary();
|
|
|
|
//Boats.
|
|
drawBoats();
|
|
|
|
//Marks.
|
|
drawMarks();
|
|
|
|
//Wind arrow. This rotates the wind arrow node.
|
|
displayWindArrow(this.visualiserRace.getWindDirection().degrees());
|
|
|
|
}
|
|
|
|
/**
|
|
* draws a transparent line around the course that shows the paths boats must travel
|
|
*/
|
|
public void drawRaceLine(){
|
|
List<Leg> legs = this.visualiserRace.getLegs();
|
|
|
|
for (Leg leg: legs) {
|
|
//todo calculate and draw line around this leg
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* 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());
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|