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.

751 lines
24 KiB

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.enums.RoundingType;
import shared.model.*;
import java.util.ArrayList;
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;
private Image sailsRight = new Image("/images/sailsRight.png");
private Image sailsLeft = new Image("/images/sailsLeft.png");
private Image sailsLuff = new Image("/images/sailsLuff.gif", 25, 10, false, false);
/**
* The race we read data from and draw.
*/
private VisualiserRaceEvent visualiserRace;
private boolean annoName = true;
private boolean annoAbbrev = true;
private boolean annoSpeed = true;
private boolean annoPath = true;
private boolean annoEstTime = true;
private boolean annoTimeSinceLastMark = true;
private boolean annoGuideLine = false;
/**
* Constructs a {@link ResizableRaceCanvas} using a given {@link VisualiserRaceEvent}.
* @param visualiserRace The race that data is read from in order to be drawn.
*/
public ResizableRaceCanvas(VisualiserRaceEvent visualiserRace) {
super();
this.visualiserRace = visualiserRace;
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();
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;
}
/**
* Toggle the guideline annotation
*/
public void toggleGuideLine() {
annoGuideLine = !annoGuideLine;
}
/**
* 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.getPosition()),
boat.getTimeToNextMarkFormatted(this.visualiserRace.getVisualiserRaceState().getRaceClock().getCurrentTime()),
boat.getTimeSinceLastMarkFormatted(this.visualiserRace.getVisualiserRaceState().getRaceClock().getCurrentTime()) );
}
/**
* Draws all of the boats on the canvas.
*/
private void drawBoats() {
for (VisualiserBoat boat : new ArrayList<>(visualiserRace.getVisualiserRaceState().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.getVisualiserRaceState().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) {
if (boat.isClientBoat()) {
drawClientBoat(boat);
}
//Convert position to graph coordinate.
GraphCoordinate pos = this.map.convertGPS(boat.getPosition());
//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 extra decorations to show which boat has been assigned to the client.
* @param boat The client's boat.
*/
private void drawClientBoat(VisualiserBoat boat) {
//Convert position to graph coordinate.
GraphCoordinate pos = this.map.convertGPS(boat.getPosition());
//The x coordinates of each vertex of the boat.
double[] x = {
pos.getX() - 9,
pos.getX(),
pos.getX() + 9 };
//The y coordinates of each vertex of the boat.
double[] y = {
pos.getY() + 15,
pos.getY() - 15,
pos.getY() + 15 };
//The above shape is essentially a triangle 24px wide, and 48 long.
//Draw the boat.
gc.setFill(Color.BLACK);
gc.save();
rotate(boat.getBearing().degrees(), pos.getX(), pos.getY());
gc.fillPolygon(x, y, 3);
gc.restore();
}
/**
* 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 = 0; //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
}
sailRotateAngle += ((boatBearing + windDirection) * 0.5);
}
// Sails in = luffing sail
else {
xPos -= 6;
yPos += 1;
sailImage = sailsLuff;
sailRotateAngle = boatBearing + 90;
}
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.getPosition());
GraphCoordinate wakeTo = this.map.convertGPS(boat.getWake());
//Draw.
drawLine(wakeFrom, wakeTo, boat.getColor());
}
/**
* Draws all of the {@link Mark}s on the canvas.
*/
private void drawMarks() {
for (Mark mark : new ArrayList<>(visualiserRace.getVisualiserRaceState().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());
//Draw the race.
drawRace();
}
/**
* Clears the canvas.
*/
private void clear() {
gc.clearRect(0, 0, getWidth(), getHeight());
}
/**
* Draws the race boundary.
*/
private void drawBoundary() {
//Prepare to draw.
gc.setLineWidth(1);
gc.setFill(Color.AQUA);
//Calculate the screen coordinates of the boundary.
List<GPSCoordinate> boundary = new ArrayList<>(visualiserRace.getVisualiserRaceState().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);
}
/**
* Draws the race.
* Called once per frame, and on canvas resize.
*/
public void drawRace() {
//Update RaceMap with new GPS values of race.
this.map.setGPSTopLeft(visualiserRace.getVisualiserRaceState().getRaceDataSource().getMapTopLeft());
this.map.setGPSBotRight(visualiserRace.getVisualiserRaceState().getRaceDataSource().getMapBottomRight());
gc.setLineWidth(2);
clear();
//Race boundary.
drawBoundary();
//Guiding Line
if (annoGuideLine){
drawRaceLine();
}
//Boats.
drawBoats();
//Marks.
drawMarks();
}
/**
* draws a transparent line around the course that shows the paths boats must travel
*/
public void drawRaceLine(){
List<Leg> legs = this.visualiserRace.getVisualiserRaceState().getLegs();
GPSCoordinate legStartPoint = legs.get(0).getStartCompoundMark().getAverageGPSCoordinate();
GPSCoordinate nextStartPoint;
for (int i = 0; i < legs.size() -1; i++) {
nextStartPoint = drawLineRounding(legs, i, legStartPoint);
legStartPoint = nextStartPoint;
}
}
/**
* Draws a line around a course that shows where boats need to go. This method
* draws the line leg by leg
* @param legs the legs of a race
* @param index the index of the current leg to use
* @param legStartPoint The position the current leg.
* @return the end point of the current leg that has been drawn
*/
private GPSCoordinate drawLineRounding(List<Leg> legs, int index, GPSCoordinate legStartPoint){
GPSCoordinate startDirectionLinePoint;
GPSCoordinate endDirectionLinePoint;
Bearing bearingOfDirectionLine;
GPSCoordinate startNextDirectionLinePoint;
GPSCoordinate endNextDirectionLinePoint;
Bearing bearingOfNextDirectionLine;
//finds the direction of the current leg as a bearing
startDirectionLinePoint = legStartPoint;
GPSCoordinate tempEndDirectionLinePoint = legs.get(index).getEndCompoundMark().getAverageGPSCoordinate();
bearingOfDirectionLine = GPSCoordinate.calculateBearing(startDirectionLinePoint, tempEndDirectionLinePoint);
//after finding the initial bearing pick the mark used for rounding
endDirectionLinePoint = legs.get(index).getEndCompoundMark().getMarkForRounding(bearingOfDirectionLine).getPosition();
bearingOfDirectionLine = GPSCoordinate.calculateBearing(startDirectionLinePoint, endDirectionLinePoint);
//finds the direction of the next leg as a bearing
if (index < legs.size() -2){ // not last leg
startNextDirectionLinePoint = legs.get(index + 1).getStartCompoundMark().getMark1Position();
endNextDirectionLinePoint = legs.get(index + 1).getEndCompoundMark().getMark1Position();
bearingOfNextDirectionLine = GPSCoordinate.calculateBearing(startNextDirectionLinePoint, endNextDirectionLinePoint);
double degreesToAdd;
//find which side is need to be used
if (legs.get(index).getEndCompoundMark().getRoundingType() == RoundingType.Port ||
legs.get(index).getEndCompoundMark().getRoundingType() == RoundingType.SP){
degreesToAdd = 90;
}else{
degreesToAdd = -90;
}
//use the direction line to find a point parallel to it by the mark
GPSCoordinate pointToStartCurve = GPSCoordinate.calculateNewPosition(endDirectionLinePoint,
100, Azimuth.fromDegrees(bearingOfDirectionLine.degrees()+degreesToAdd));
//use the direction line to find a point to curve too
GPSCoordinate pointToEndCurve = GPSCoordinate.calculateNewPosition(endDirectionLinePoint,
100, Azimuth.fromDegrees(bearingOfNextDirectionLine.degrees()+degreesToAdd));
//use the curve points to find the two control points for the bezier curve
GPSCoordinate controlPoint;
GPSCoordinate controlPoint2;
Bearing bearingOfCurveLine = GPSCoordinate.calculateBearing(pointToStartCurve, pointToEndCurve);
if ((bearingOfDirectionLine.degrees() - bearingOfNextDirectionLine.degrees() +360)%360< 145){
//small turn
controlPoint = GPSCoordinate.calculateNewPosition(pointToStartCurve,
50, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+(degreesToAdd/2)));
controlPoint2 = controlPoint;
}else{
//large turn
controlPoint = GPSCoordinate.calculateNewPosition(pointToStartCurve,
150, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+degreesToAdd));
controlPoint2 = GPSCoordinate.calculateNewPosition(pointToEndCurve,
150, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+degreesToAdd));
}
//change all gps into graph coordinate
GraphCoordinate startPath = this.map.convertGPS(startDirectionLinePoint);
GraphCoordinate curvePoint = this.map.convertGPS(pointToStartCurve);
GraphCoordinate curvePointEnd = this.map.convertGPS(pointToEndCurve);
GraphCoordinate c1 = this.map.convertGPS(controlPoint);
GraphCoordinate c2 = this.map.convertGPS(controlPoint2);
gc.setLineWidth(2);
gc.setStroke(Color.MEDIUMAQUAMARINE);
gc.beginPath();
gc.moveTo(startPath.getX(), startPath.getY());
gc.lineTo(curvePoint.getX(), curvePoint.getY());
drawArrowHead(startDirectionLinePoint, pointToStartCurve);
gc.bezierCurveTo(c1.getX(), c1.getY(), c2.getX(), c2.getY(), curvePointEnd.getX(), curvePointEnd.getY());
gc.stroke();
gc.closePath();
gc.save();
return pointToEndCurve;
}else{//last leg so no curve
GraphCoordinate startPath = this.map.convertGPS(legStartPoint);
GraphCoordinate endPath = this.map.convertGPS(legs.get(index).getEndCompoundMark().getAverageGPSCoordinate());
gc.beginPath();
gc.moveTo(startPath.getX(), startPath.getY());
gc.lineTo(endPath.getX(), endPath.getY());
gc.stroke();
gc.closePath();
gc.save();
drawArrowHead(legStartPoint, legs.get(index).getEndCompoundMark().getAverageGPSCoordinate());
return null;
}
}
private void drawArrowHead(GPSCoordinate start, GPSCoordinate end){
GraphCoordinate lineStart = this.map.convertGPS(start);
GraphCoordinate lineEnd = this.map.convertGPS(end);
double arrowAngle = Math.toRadians(45.0);
double arrowLength = 10.0;
double dx = lineStart.getX() - lineEnd.getX();
double dy = lineStart.getY() - lineEnd.getY();
double angle = Math.atan2(dy, dx);
double x1 = Math.cos(angle + arrowAngle) * arrowLength + lineEnd.getX();
double y1 = Math.sin(angle + arrowAngle) * arrowLength + lineEnd.getY();
double x2 = Math.cos(angle - arrowAngle) * arrowLength + lineEnd.getX();
double y2 = Math.sin(angle - arrowAngle) * arrowLength + lineEnd.getY();
gc.strokeLine(lineEnd.getX(), lineEnd.getY(), x1, y1);
gc.strokeLine(lineEnd.getX(), lineEnd.getY(), x2, y2);
}
/**
* 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 : new ArrayList<>(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());
}
}
}
}