Changed package heirachy. Merged Controller and StartScreenController.

#refactor
This commit is contained in:
Calum
2017-07-21 09:22:55 +12:00
parent 9ecaa7c3b3
commit 881f7f8e30
62 changed files with 399 additions and 569 deletions
@@ -0,0 +1,152 @@
package seng302.visualiser;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.zip.CRC32;
import java.util.zip.Checksum;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import seng302.model.stream.StreamParser;
import seng302.model.stream.XMLParser;
import seng302.model.stream.packets.BoatPositionPacket;
import seng302.model.stream.packets.StreamPacket;
import seng302.server.messages.BoatActionMessage;
import seng302.server.messages.Message;
/**
* Created by kre39 on 13/07/17.
*/
public class ClientToServerThread extends Thread {
private Queue<StreamPacket> streamPackets = new ConcurrentLinkedQueue<>();
private List<ListChangeListener<Queue<StreamPacket>>> boatPacketListeners = new ArrayList<>();
private Socket socket;
private InputStream is;
private OutputStream os;
private final int PORT_NUMBER = 0;
private static final int LOG_LEVEL = 1;
private Boolean updateClient = true;
private ByteArrayOutputStream crcBuffer;
public ClientToServerThread(String ipAddress, Integer portNumber){
try {
socket = new Socket(ipAddress, portNumber);
is = socket.getInputStream();
os = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
}
static void serverLog(String message, int logLevel){
if(logLevel <= LOG_LEVEL){
System.out.println("[SERVER] " + message);
}
}
public void run() {
int sync1;
int sync2;
// TODO: 14/07/17 wmu16 - Work out how to fix this while loop
while(true) {
try {
crcBuffer = new ByteArrayOutputStream();
sync1 = readByte();
sync2 = readByte();
//checking if it is the start of the packet
if(sync1 == 0x47 && sync2 == 0x83) {
int type = readByte();
//No. of milliseconds since Jan 1st 1970
long timeStamp = Message.bytesToLong(getBytes(6));
skipBytes(4);
long payloadLength = Message.bytesToLong(getBytes(2));
byte[] payload = getBytes((int) payloadLength);
Checksum checksum = new CRC32();
checksum.update(crcBuffer.toByteArray(), 0, crcBuffer.size());
long computedCrc = checksum.getValue();
long packetCrc = Message.bytesToLong(getBytes(4));
if (computedCrc == packetCrc) {
streamPackets.add(new StreamPacket(type, payloadLength, timeStamp, payload));
for (ListChangeListener cl : boatPacketListeners) {
cl.onChanged();
}
} else {
System.err.println("Packet has been dropped");
}
}
} catch (Exception e) {
closeSocket();
return;
}
}
}
/**
* Send the post-start race course information
*/
public void sendBoatActionMessage(BoatActionMessage boatActionMessage) {
try {
os.write(boatActionMessage.getBuffer());
} catch (IOException e) {
e.printStackTrace();
}
}
public void closeSocket() {
try {
socket.close();
} catch (IOException e) {
System.out.println("IO error in server thread upon trying to close socket");
}
}
public void addStreamObserver (ListChangeListener<Queue<StreamPacket>> listChangeListener) {
boatPacketListeners.add(listChangeListener);
}
public void removeStreamObserver (ListChangeListener<Queue<StreamPacket>> listChangeListener) {
boatPacketListeners.remove(listChangeListener);
}
private int readByte() throws Exception {
int currentByte = -1;
try {
currentByte = is.read();
crcBuffer.write(currentByte);
} catch (IOException e) {
e.printStackTrace();
}
if (currentByte == -1){
throw new Exception();
}
return currentByte;
}
private byte[] getBytes(int n) throws Exception{
byte[] bytes = new byte[n];
for (int i = 0; i < n; i++){
bytes[i] = (byte) readByte();
}
return bytes;
}
private void skipBytes(long n) throws Exception{
for (int i=0; i < n; i++){
readByte();
}
}
}
@@ -0,0 +1,515 @@
package seng302.visualiser;
import java.io.IOException;
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.collections.ObservableList;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.scene.text.Text;
import seng302.visualiser.controllers.RaceViewController;
import seng302.visualiser.fxObjects.BoatGroup;
import seng302.visualiser.fxObjects.MarkGroup;
import seng302.model.Colors;
import seng302.model.Yacht;
import seng302.model.map.Boundary;
import seng302.model.map.CanvasMap;
import seng302.model.mark.GateMark;
import seng302.model.mark.Mark;
import seng302.model.mark.MarkType;
import seng302.model.mark.SingleMark;
import seng302.model.stream.StreamParser;
import seng302.model.stream.XMLParser;
import seng302.model.stream.XMLParser.RaceXMLObject.Limit;
import seng302.model.stream.XMLParser.RaceXMLObject.Participant;
import seng302.model.stream.packets.BoatPositionPacket;
import seng302.utilities.GeoPoint;
import seng302.utilities.GeoUtility;
/**
* Created by cir27 on 20/07/17.
*/
public class GameView extends Pane {
private RaceViewController raceViewController;
private ObservableList<Node> gameObjects;
private ImageView mapImage;
private int BUFFER_SIZE = 50;
private int panelWidth = 1260; // it should be 1280 but, minors 40 to cancel the bias.
private int panelHeight = 960;
private double canvasWidth = 720;
private double canvasHeight = 720;
private boolean horizontalInversion = false;
private double distanceScaleFactor;
private ScaleDirection scaleDirection;
private Mark minLatPoint;
private Mark minLonPoint;
private Mark maxLatPoint;
private Mark maxLonPoint;
private double referencePointX;
private double referencePointY;
private double metersPerPixelX;
private double metersPerPixelY;
private List<MarkGroup> markGroups = new ArrayList<>();
private List<BoatGroup> boatGroups = new ArrayList<>();
private Text FPSdisplay = new Text();
private Polygon raceBorder = new Polygon();
//FRAME RATE
private Double frameRate = 60.0;
private final long[] frameTimes = new long[30];
private int frameTimeIndex = 0;
private boolean arrayFilled = false;
AnimationTimer timer;
private enum ScaleDirection {
HORIZONTAL,
VERTICAL
}
void setup(RaceViewController raceViewController) {
this.raceViewController = raceViewController;
}
public void initialize() {
raceViewController = new RaceViewController();
gameObjects = this.getChildren();
// create image view for map, bind panel size to image
mapImage = new ImageView();
gameObjects.add(mapImage);
mapImage.fitWidthProperty().bind(this.widthProperty());
mapImage.fitHeightProperty().bind(this.heightProperty());
}
void initializeCanvas() {
fitMarksToCanvas();
drawGoogleMap();
FPSdisplay.setLayoutX(5);
FPSdisplay.setLayoutY(20);
FPSdisplay.setStrokeWidth(2);
gameObjects.add(FPSdisplay);
gameObjects.add(raceBorder);
initializeMarks();
initializeBoats();
this.widthProperty().addListener(resize -> {
canvasWidth = this.getWidth();
canvasHeight = this.getHeight();
fitMarksToCanvas();
});
timer = new AnimationTimer() {
private long lastTime = 0;
private int FPSCount = 30;
@Override
public void handle(long now) {
if (lastTime == 0) {
lastTime = now;
} else {
if (now - lastTime >= (1e8 / 60)) { //Fix for framerate going above 60 when minimized
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 (FPSCount-- == 0) {
FPSCount = 30;
// drawFps(frameRate.intValue());
}
}
updateGroups();
if (StreamParser.isRaceFinished()) {
this.stop();
}
lastTime = now;
}
}
if (StreamParser.isRaceFinished()) {
this.stop();
switchToFinishScreen();
}
}
};
}
private void switchToFinishScreen() {
try {
// canvas view -> anchor pane -> grid pane -> main view
GridPane gridPane = (GridPane) this.getParent().getParent();
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("/views/FinishScreenView.fxml")));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 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
GeoPoint topLeftPos = new GeoPoint(maxLatPoint.getLatitude(), minLonPoint.getLongitude());
GeoPoint originPos = GeoUtility
.getGeoCoordinate(topLeftPos, bearingFromTopLeftToOrigin, distanceFromTopLeftToOrigin);
// distance from origin corner to bottom right corner of the panel
double distanceFromOriginToBottomRight = Math.sqrt(
Math.pow(panelHeight * metersPerPixelY, 2) + Math
.pow(panelWidth * metersPerPixelX, 2));
double bearingFromOriginToBottomRight = Math
.toDegrees(Math.atan2(panelWidth, -panelHeight));
GeoPoint 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
*
* NOTE: This is quite confusing as objects are grabbed from the XMLParser such as Mark and
* CompoundMark which are named the same as those in the model package but are, however not the
* same, so they do not have things such as a type and must be derived from the number of marks
* in a compound mark etc..
*/
private void addRaceBorder() {
XMLParser.RaceXMLObject raceXMLObject = StreamParser.getXmlObject().getRaceXML();
ArrayList<Limit> courseLimits = raceXMLObject.getCourseLimit();
raceBorder.setStroke(new Color(0.0f, 0.0f, 0.74509807f, 1));
raceBorder.setStrokeWidth(3);
raceBorder.setFill(new Color(0,0,0,0));
List<Double> boundaryPoints = new ArrayList<>();
for (Limit limit : courseLimits) {
Point2D location = findScaledXY(limit.getLat(), limit.getLng());
boundaryPoints.add(location.getX());
boundaryPoints.add(location.getY());
}
raceBorder.getPoints().setAll(boundaryPoints);
}
private void updateGroups() {
for (BoatGroup boatGroup : boatGroups) {
// some raceObjects will have multiple ID's (for instance gate marks)
//checking if the current "ID" has any updates associated with it
if (StreamParser.boatLocations.containsKey(boatGroup.getRaceId())) {
if (boatGroup.isStopped()) {
updateBoatGroup(boatGroup);
}
}
boatGroup.move();
}
for (MarkGroup markGroup : markGroups) {
for (Long id : markGroup.getRaceIds()) {
if (StreamParser.markLocations.containsKey(id)) {
updateMarkGroup(id, markGroup);
}
}
}
checkForCourseChanges();
}
private void checkForCourseChanges() {
if (StreamParser.isNewRaceXmlReceived()){
addRaceBorder();
}
}
private void updateBoatGroup(BoatGroup boatGroup) {
PriorityBlockingQueue<BoatPositionPacket> movementQueue = StreamParser.boatLocations.get(boatGroup.getRaceId());
// giving the movementQueue a 5 packet buffer to account for slightly out of order packets
if (movementQueue.size() > 0) {
try {
BoatPositionPacket positionPacket = movementQueue.take();
Point2D p2d = findScaledXY(positionPacket.getLat(), positionPacket.getLon());
double heading = 360.0 / 0xffff * positionPacket.getHeading();
boatGroup.setDestination(
p2d.getX(), p2d.getY(), heading, positionPacket.getGroundSpeed(),
positionPacket.getTimeValid(), frameRate);
} catch (InterruptedException e){
e.printStackTrace();
}
// }
}
}
private void updateMarkGroup (long raceId, MarkGroup markGroup) {
PriorityBlockingQueue<BoatPositionPacket> movementQueue = StreamParser.markLocations.get(raceId);
if (movementQueue.size() > 0){
try {
BoatPositionPacket positionPacket = movementQueue.take();
Point2D p2d = findScaledXY(positionPacket.getLat(), positionPacket.getLon());
markGroup.moveMarkTo(p2d.getX(), p2d.getY(), raceId);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* Draws all the boats.
*/
private void initializeBoats() {
Map<Integer, Yacht> boats = StreamParser.getBoats();
Group wakes = new Group();
Group trails = new Group();
Group annotations = new Group();
ArrayList<Participant> participants = StreamParser.getXmlObject().getRaceXML()
.getParticipants();
ArrayList<Integer> participantIDs = new ArrayList<>();
for (Participant p : participants) {
participantIDs.add(p.getsourceID());
}
for (Yacht boat : boats.values()) {
if (participantIDs.contains(boat.getSourceID())) {
boat.setColour(Colors.getColor());
BoatGroup boatGroup = new BoatGroup(boat, boat.getColour());
boatGroups.add(boatGroup);
trails.getChildren().add(boatGroup.getTrail());
wakes.getChildren().add(boatGroup.getWake());
annotations.getChildren().add(boatGroup.getAnnotations());
}
}
gameObjects.addAll(trails);
gameObjects.addAll(wakes);
gameObjects.addAll(annotations);
gameObjects.addAll(boatGroups);
}
private void initializeMarks() {
List<Mark> allMarks = StreamParser.getXmlObject().getRaceXML().getNonDupCompoundMarks();
for (Mark mark : allMarks) {
if (mark.getMarkType() == MarkType.SINGLE_MARK) {
SingleMark sMark = (SingleMark) mark;
MarkGroup markGroup = new MarkGroup(sMark, findScaledXY(sMark));
markGroups.add(markGroup);
} else {
GateMark gMark = (GateMark) mark;
MarkGroup markGroup = new MarkGroup(gMark, findScaledXY(gMark.getSingleMark1()),
findScaledXY(gMark.getSingleMark2())); //should be 2 objects in the list.
markGroups.add(markGroup);
}
}
gameObjects.addAll(markGroups);
}
// private void drawFps(int fps){
// if (raceViewController.isDisplayFps()){
// FPSdisplay.setVisible(true);
// FPSdisplay.setText(String.format("%d FPS", fps));
// } else {
// FPSdisplay.setVisible(false);
// }
// }
/**
* Calculates x and y location for every marker that fits it to the canvas the race will be
* drawn on.
*/
private void fitMarksToCanvas() {
//Check is called once to avoid unnecessarily change the course limits once the race is running
StreamParser.isNewRaceXmlReceived();
findMinMaxPoint();
double minLonToMaxLon = scaleRaceExtremities();
calculateReferencePointLocation(minLonToMaxLon);
//givePointsXY();
addRaceBorder();
}
/**
* Sets the class variables minLatPoint, maxLatPoint, minLonPoint, maxLonPoint to the marker
* with the leftmost marker, rightmost marker, southern most marker and northern most marker
* respectively.
*/
private void findMinMaxPoint() {
List<Limit> sortedPoints = new ArrayList<>();
for (Limit limit : StreamParser.getXmlObject().getRaceXML().getCourseLimit()) {
sortedPoints.add(limit);
}
sortedPoints.sort(Comparator.comparingDouble(Limit::getLat));
Limit minLatMark = sortedPoints.get(0);
Limit maxLatMark = sortedPoints.get(sortedPoints.size()-1);
minLatPoint = new SingleMark(minLatMark.toString(), minLatMark.getLat(), minLatMark.getLng(), minLatMark.getSeqID(), minLatMark.getSeqID());
maxLatPoint = new SingleMark(maxLatMark.toString(), maxLatMark.getLat(), maxLatMark.getLng(), maxLatMark.getSeqID(), minLatMark.getSeqID());
sortedPoints.sort(Comparator.comparingDouble(Limit::getLng));
//If the course is on a point on the earth where longitudes wrap around.
Limit minLonMark = sortedPoints.get(0);
Limit maxLonMark = sortedPoints.get(sortedPoints.size()-1);
minLonPoint = new SingleMark(minLonMark.toString(), minLonMark.getLat(), minLonMark.getLng(), minLonMark.getSeqID(), minLonMark.getSeqID());
maxLonPoint = new SingleMark(maxLonMark.toString(), maxLonMark.getLat(), maxLonMark.getLng(), maxLonMark.getSeqID(), minLonMark.getSeqID());
if (maxLonPoint.getLongitude() - minLonPoint.getLongitude() > 180) {
horizontalInversion = true;
}
}
/**
* Calculates the location of a reference point, this is always the point with minimum latitude,
* in relation to the canvas.
*
* @param minLonToMaxLon The horizontal distance between the point of minimum longitude to
* maximum longitude.
*/
private void calculateReferencePointLocation(double minLonToMaxLon) {
Mark referencePoint = minLatPoint;
double referenceAngle;
if (scaleDirection == ScaleDirection.HORIZONTAL) {
referenceAngle = Math.abs(Mark.calculateHeadingRad(referencePoint, minLonPoint));
referencePointX = BUFFER_SIZE + distanceScaleFactor * Math.sin(referenceAngle) * Mark.calculateDistance(referencePoint, minLonPoint);
referenceAngle = Math.abs(Mark.calculateHeadingRad(referencePoint, maxLatPoint));
referencePointY = canvasHeight - (BUFFER_SIZE + BUFFER_SIZE);
referencePointY -= distanceScaleFactor * Math.cos(referenceAngle) * Mark.calculateDistance(referencePoint, maxLatPoint);
referencePointY = referencePointY / 2;
referencePointY += BUFFER_SIZE;
referencePointY += distanceScaleFactor * Math.cos(referenceAngle) * Mark.calculateDistance(referencePoint, maxLatPoint);
} else {
referencePointY = canvasHeight - BUFFER_SIZE;
referenceAngle = Math.abs(Mark.calculateHeadingRad(referencePoint, minLonPoint));
referencePointX = BUFFER_SIZE;
referencePointX += distanceScaleFactor * Math.sin(referenceAngle) * Mark.calculateDistance(referencePoint, minLonPoint);
referencePointX += ((canvasWidth - (BUFFER_SIZE + BUFFER_SIZE)) - (minLonToMaxLon * distanceScaleFactor)) / 2;
}
if(horizontalInversion) {
referencePointX = canvasWidth - BUFFER_SIZE - (referencePointX - BUFFER_SIZE);
}
}
/**
* Finds the scale factor necessary to fit all race markers within the onscreen map and assigns
* it to distanceScaleFactor Returns the max horizontal distance of the map.
*/
private double scaleRaceExtremities() {
double vertAngle = Math.abs(Mark.calculateHeadingRad(minLatPoint, maxLatPoint));
double vertDistance =
Math.cos(vertAngle) * Mark.calculateDistance(minLatPoint, maxLatPoint);
double horiAngle = Mark.calculateHeadingRad(minLonPoint, maxLonPoint);
if (horiAngle <= (Math.PI / 2)) {
horiAngle = (Math.PI / 2) - horiAngle;
} else {
horiAngle = horiAngle - (Math.PI / 2);
}
double horiDistance =
Math.cos(horiAngle) * Mark.calculateDistance(minLonPoint, maxLonPoint);
double vertScale = (canvasHeight - (BUFFER_SIZE + BUFFER_SIZE)) / vertDistance;
if ((horiDistance * vertScale) > (canvasWidth - (BUFFER_SIZE + BUFFER_SIZE))) {
distanceScaleFactor = (canvasWidth - (BUFFER_SIZE + BUFFER_SIZE)) / horiDistance;
scaleDirection = ScaleDirection.HORIZONTAL;
} else {
distanceScaleFactor = vertScale;
scaleDirection = ScaleDirection.VERTICAL;
}
return horiDistance;
}
private Point2D findScaledXY(Mark unscaled) {
return findScaledXY(unscaled.getLatitude(), unscaled.getLongitude());
}
public Point2D findScaledXY (double unscaledLat, double unscaledLon) {
double distanceFromReference;
double angleFromReference;
double xAxisLocation = referencePointX;
double yAxisLocation = referencePointY;
angleFromReference = Mark
.calculateHeadingRad(minLatPoint.getLatitude(), minLatPoint.getLongitude(), unscaledLat,
unscaledLon);
distanceFromReference = Mark
.calculateDistance(minLatPoint.getLatitude(), minLatPoint.getLongitude(), unscaledLat,
unscaledLon);
if (angleFromReference >= 0 && angleFromReference <= Math.PI / 2) {
xAxisLocation += Math.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference);
yAxisLocation -= Math.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference);
} else if (angleFromReference >= 0) {
angleFromReference = angleFromReference - Math.PI / 2;
xAxisLocation += Math.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference);
yAxisLocation += Math.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference);
} else if (angleFromReference < 0 && angleFromReference >= -Math.PI / 2) {
angleFromReference = Math.abs(angleFromReference);
xAxisLocation -= Math.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference);
yAxisLocation -= Math.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference);
} else {
angleFromReference = Math.abs(angleFromReference) - Math.PI / 2;
xAxisLocation -= Math.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference);
yAxisLocation += Math.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference);
}
if(horizontalInversion) {
xAxisLocation = canvasWidth - BUFFER_SIZE - (xAxisLocation - BUFFER_SIZE);
}
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, 0);
m2 = new SingleMark("m2", minLatPoint.getLatitude(), maxLonPoint.getLongitude(), 2, 0);
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;
}
}
@@ -0,0 +1,97 @@
package seng302.visualiser.controllers;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.ResourceBundle;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import seng302.model.Yacht;
import seng302.model.stream.StreamParser;
import seng302.model.stream.XMLParser.RaceXMLObject.Participant;
public class FinishScreenViewController implements Initializable {
@FXML
private GridPane finishScreenGridPane;
@FXML
private TableView<Yacht> finishOrderTable;
@FXML
private TableColumn<Yacht, String> posCol;
@FXML
private TableColumn<Yacht, String> boatNameCol;
@FXML
private TableColumn<Yacht, String> shortNameCol;
@FXML
private TableColumn<Yacht, String> countryCol;
@Override
public void initialize(URL location, ResourceBundle resources) {
finishScreenGridPane.getStylesheets()
.add(getClass().getResource("/css/master.css").toString());
finishOrderTable.getStylesheets().add(getClass().getResource("/css/master.css").toString());
// set up data for table
ObservableList<Yacht> data = FXCollections.observableArrayList();
finishOrderTable.setItems(data);
// setting table col data
posCol.setCellValueFactory(
new PropertyValueFactory<>("position")
);
boatNameCol.setCellValueFactory(
new PropertyValueFactory<>("boatName")
);
shortNameCol.setCellValueFactory(
new PropertyValueFactory<>("shortName")
);
countryCol.setCellValueFactory(
new PropertyValueFactory<>("country")
);
// check if the boat is racing
ArrayList<Participant> participants = StreamParser.getXmlObject().getRaceXML()
.getParticipants();
ArrayList<Integer> participantIDs = new ArrayList<>();
for (Participant p : participants) {
participantIDs.add(p.getsourceID());
}
// add data to table
for (Yacht boat : StreamParser.getBoatsPos().values()) {
if (participantIDs.contains(boat.getSourceID())) {
data.add(boat);
}
}
finishOrderTable.refresh();
}
private void setContentPane(String jfxUrl) {
try {
// get the main controller anchor pane (FinishView -> MainView)
AnchorPane contentPane = (AnchorPane) finishScreenGridPane.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) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public void switchToStartScreenView() {
setContentPane("/views/StartScreenView.fxml");
}
}
@@ -0,0 +1,107 @@
package seng302.visualiser.controllers;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.URL;
import java.util.Enumeration;
import java.util.ResourceBundle;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.control.ListView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.text.Text;
import seng302.gameServer.GameStages;
import seng302.gameServer.GameState;
/**
* A class describing the actions of the lobby screen
* Created by wmu16 on 10/07/17.
*/
public class LobbyController implements Initializable{
@FXML
private ListView competitorsListView;
@FXML
private GridPane lobbyScreen;
@FXML
private Text lobbyIpText;
private static ObservableList competitors;
private void setContentPane(String jfxUrl) {
try {
AnchorPane contentPane = (AnchorPane) lobbyScreen.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) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void initialize(URL location, ResourceBundle resources) {
lobbyIpText.setText("Lobby Host IP: " + getLocalHostIp());
}
public void initialize() {
competitors = FXCollections.observableArrayList();
competitorsListView.setItems(competitors);
}
private String getLocalHostIp() {
String ipAddress = null;
try {
Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();
while (e.hasMoreElements()) {
NetworkInterface ni = e.nextElement();
if (ni.isLoopback())
continue;
if(ni.isPointToPoint())
continue;
if(ni.isVirtual())
continue;
Enumeration<InetAddress> addresses = ni.getInetAddresses();
while(addresses.hasMoreElements()) {
InetAddress address = addresses.nextElement();
if(address instanceof Inet4Address) { // skip all ipv6
ipAddress = address.getHostAddress();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
if (ipAddress == null) {
System.out.println("[HOST] Cannot obtain local host ip address.");
}
return ipAddress;
}
@FXML
public void leaveLobbyButtonPressed() {
// TODO: 10/07/17 wmu16 - Finish function!
setContentPane("/views/StartScreenView.fxml");
System.out.println("Leaving lobby!");
GameState.setCurrentStage(GameStages.CANCELLED);
// TODO: 20/07/17 wmu16 - Implement some way of terminating the game
}
@FXML
public void readyButtonPressed() {
GameState.setCurrentStage(GameStages.RACING);
}
}
@@ -0,0 +1,644 @@
package seng302.visualiser.controllers;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Point2D;
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;
import javafx.scene.control.Slider;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Line;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import javafx.util.StringConverter;
import seng302.utilities.GeoUtility;
import seng302.visualiser.controllers.annotations.Annotation;
import seng302.visualiser.controllers.annotations.ImportantAnnotationController;
import seng302.visualiser.controllers.annotations.ImportantAnnotationDelegate;
import seng302.visualiser.controllers.annotations.ImportantAnnotationsState;
import seng302.visualiser.fxObjects.BoatGroup;
import seng302.visualiser.fxObjects.MarkGroup;
import seng302.model.*;
import seng302.model.mark.GateMark;
import seng302.model.mark.Mark;
import seng302.model.mark.SingleMark;
import seng302.model.stream.StreamParser;
import seng302.model.stream.XMLParser;
import java.io.IOException;
import java.util.*;
import seng302.model.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
private CheckBox toggleFps;
@FXML
private Text timerLabel;
@FXML
private AnchorPane contentAnchorPane;
@FXML
private Text windArrowText, windDirectionText;
@FXML
private Slider annotationSlider;
@FXML
private Button selectAnnotationBtn;
@FXML
private ComboBox boatSelectionComboBox;
@FXML
private GameViewController gameViewController;
private static ArrayList<Yacht> startingBoats = new ArrayList<>();
private boolean displayFps;
private Timeline timerTimeline;
private Stage stage;
private static HashMap<Integer, Series<String, Double>> sparkLineData = new HashMap<>();
private static ArrayList<Yacht> racingBoats = new ArrayList<>();
private ImportantAnnotationsState importantAnnotations;
private Yacht selectedBoat;
public void initialize() {
// 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(-5);
raceSparkLine.getYAxis().setAutoRanging(false);
sparklineYAxis.setTickMarkVisible(false);
startingBoats = new ArrayList<>(StreamParser.getBoats().values());
gameViewController.setup(this);
gameViewController.initializeCanvas();
initializeUpdateTimer();
initialiseFPSCheckBox();
initialiseAnnotationSlider();
initialiseBoatSelectionComboBox();
gameViewController.timer.start();
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) {
this.importantAnnotations = importantAnnotationsState;
setAnnotations((int) annotationSlider.getValue()); // Refresh the displayed annotations
}
/**
* Loads the "select annotations" view in a new window
*/
private void loadSelectAnnotationView() {
try {
FXMLLoader fxmlLoader = new FXMLLoader();
Stage stage = new Stage();
// Set controller
ImportantAnnotationController controller = new ImportantAnnotationController(this,
stage);
fxmlLoader.setController(controller);
// Load FXML and set CSS
fxmlLoader
.setLocation(getClass().getResource("/views/importantAnnotationSelectView.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 469, 298);
scene.getStylesheets().add(getClass().getResource("/css/master.css").toString());
stage.initStyle(StageStyle.UNDECORATED);
stage.setScene(scene);
stage.show();
controller.loadState(importantAnnotations);
} catch (IOException e) {
e.printStackTrace();
}
}
private void initialiseFPSCheckBox() {
displayFps = true;
toggleFps.selectedProperty().addListener(
(observable, oldValue, newValue) -> displayFps = !displayFps);
}
private void initialiseAnnotationSlider() {
annotationSlider.setLabelFormatter(new StringConverter<Double>() {
@Override
public String toString(Double n) {
if (n == 0) {
return "None";
}
if (n == 1) {
return "Important";
}
if (n == 2) {
return "All";
}
return "All";
}
@Override
public Double fromString(String s) {
switch (s) {
case "None":
return 0d;
case "Important":
return 1d;
case "All":
return 2d;
default:
return 2d;
}
}
});
annotationSlider.valueProperty().addListener((obs, oldval, newVal) ->
setAnnotations((int) annotationSlider.getValue()));
annotationSlider.setValue(2);
}
/**
* 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<Yacht> 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<String, Double> 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<XYChart.Series<String, Double>> 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<String, Double> 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();
}
}
if (color == null){
return String.format( "#%02X%02X%02X",255,255,255);
}
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.
* Updates of each of these attributes are called ONCE EACH SECOND
*/
private void initializeUpdateTimer() {
timerTimeline = new Timeline();
timerTimeline.setCycleCount(Timeline.INDEFINITE);
// Run timer update every second
timerTimeline.getKeyFrames().add(
new KeyFrame(Duration.seconds(1),
event -> {
updateRaceTime();
updateWindDirection();
updateOrder();
updateBoatSelectionComboBox();
})
);
// Start the timer
timerTimeline.playFromStart();
}
/**
* Iterates over all corners until ones SeqID matches with the boats current leg number.
* Then it gets the compoundMarkID of that corner and uses it to fetch the appropriate mark
* Returns null if no next mark found.
* @param bg The BoatGroup to find the next mark of
* @return The next Mark or null if none found
*/
private Mark getNextMark(BoatGroup bg) {
Integer legNumber = bg.getBoat().getLegNumber();
List<XMLParser.RaceXMLObject.Corner> markSequence = StreamParser.getXmlObject().getRaceXML().getCompoundMarkSequence();
if (legNumber == 0) {
return null;
} else if (legNumber == markSequence.size() - 1) {
return null;
}
for (XMLParser.RaceXMLObject.Corner corner : markSequence) {
if (legNumber + 2 == corner.getSeqID()) {
Integer thisCompoundMarkID = corner.getCompoundMarkID();
for (Mark mark : StreamParser.getXmlObject().getRaceXML().getAllCompoundMarks()) {
if (mark.getCompoundMarkID() == thisCompoundMarkID) {
return mark;
}
}
}
}
return null;
}
/**
* Updates the wind direction arrow and text as from info from the StreamParser
*/
private void updateWindDirection() {
windDirectionText.setText(String.format("%.1f°", StreamParser.getWindDirection()));
windArrowText.setRotate(StreamParser.getWindDirection());
}
/**
* Updates the clock for the race
*/
private void updateRaceTime() {
if (StreamParser.isRaceFinished()) {
timerLabel.setFill(Color.RED);
timerLabel.setText("Race Finished!");
} else {
timerLabel.setText(getTimeSinceStartOfRace());
}
}
/**
* Grabs the boats currently in the race as from the StreamParser and sets them to be selectable
* in the boat selection combo box
*/
private void updateBoatSelectionComboBox() {
ObservableList<Yacht> observableBoats = FXCollections
.observableArrayList(StreamParser.getBoatsPos().values());
boatSelectionComboBox.setItems(observableBoats);
}
/**
* Updates the order of the boats as from the StreamParser and sets them in the boat order
* section
*/
private void updateOrder() {
positionVbox.getChildren().clear();
positionVbox.getChildren().removeAll();
positionVbox.getStylesheets().add(getClass().getResource("/css/master.css").toString());
// list of racing boat id
ArrayList<Participant> participants = StreamParser.getXmlObject().getRaceXML()
.getParticipants();
ArrayList<Integer> participantIDs = new ArrayList<>();
for (Participant p : participants) {
participantIDs.add(p.getsourceID());
}
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);
}
}
}
}
private void updateLaylines(BoatGroup bg) {
Mark nextMark = getNextMark(bg);
Boolean isUpwind = null;
// Can only calc leg direction if there is a next mark and it is a gate mark
if (nextMark != null) {
if (nextMark instanceof GateMark) {
if (bg.isUpwindLeg(gameViewController, nextMark)) {
isUpwind = true;
} else {
isUpwind = false;
}
for(MarkGroup mg : gameViewController.getMarkGroups()) {
mg.removeLaylines();
if (mg.getMainMark().getId() == nextMark.getId()) {
SingleMark singleMark1 = ((GateMark) nextMark).getSingleMark1();
SingleMark singleMark2 = ((GateMark) nextMark).getSingleMark2();
Point2D markPoint1 = gameViewController
.findScaledXY(singleMark1.getLatitude(), singleMark1.getLongitude());
Point2D markPoint2 = gameViewController
.findScaledXY(singleMark2.getLatitude(), singleMark2.getLongitude());
HashMap<Double, Double> angleAndSpeed;
if (isUpwind) {
angleAndSpeed = PolarTable.getOptimalUpwindVMG(StreamParser.getWindSpeed());
} else {
angleAndSpeed = PolarTable.getOptimalDownwindVMG(StreamParser.getWindSpeed());
}
Double resultingAngle = angleAndSpeed.keySet().iterator().next();
Point2D boatCurrentPos = new Point2D(bg.getBoatLayoutX(), bg.getBoatLayoutY());
Point2D gateMidPoint = markPoint1.midpoint(markPoint2);
Integer lineFuncResult = GeoUtility.lineFunction(boatCurrentPos, gateMidPoint, markPoint2);
Line rightLayline = new Line();
Line leftLayline = new Line();
if (lineFuncResult == 1) {
rightLayline = makeRightLayline(markPoint2, 180 - resultingAngle, StreamParser.getWindDirection());
leftLayline = makeLeftLayline(markPoint1, 180 - resultingAngle, StreamParser.getWindDirection());
} else if (lineFuncResult == -1) {
rightLayline = makeRightLayline(markPoint1, 180 - resultingAngle, StreamParser.getWindDirection());
leftLayline = makeLeftLayline(markPoint2, 180 - resultingAngle, StreamParser.getWindDirection());
}
leftLayline.setStrokeWidth(0.5);
leftLayline.setStroke(bg.getBoat().getColour());
rightLayline.setStrokeWidth(0.5);
rightLayline.setStroke(bg.getBoat().getColour());
bg.setLaylines(leftLayline, rightLayline);
mg.addLaylines(leftLayline, rightLayline);
}
}
}
}
}
private Point2D getPointRotation(Point2D ref, Double distance, Double angle){
Double newX = ref.getX() + (ref.getX() + distance -ref.getX())*Math.cos(angle) - (ref.getY() + distance -ref.getY())*Math.sin(angle);
Double newY = ref.getY() + (ref.getX() + distance -ref.getX())*Math.sin(angle) + (ref.getY() + distance -ref.getY())*Math.cos(angle);
return new Point2D(newX, newY);
}
public Line makeLeftLayline(Point2D startPoint, Double layLineAngle, Double baseAngle) {
Point2D ep = getPointRotation(startPoint, 50.0, baseAngle + layLineAngle);
Line line = new Line(startPoint.getX(), startPoint.getY(), ep.getX(), ep.getY());
return line;
}
public Line makeRightLayline(Point2D startPoint, Double layLineAngle, Double baseAngle) {
Point2D ep = getPointRotation(startPoint, 50.0, baseAngle - layLineAngle);
Line line = new Line(startPoint.getX(), startPoint.getY(), ep.getX(), ep.getY());
return line;
}
/**
* Initialised the combo box with any boats currently in the race and adds the required listener
* for the combobox to take action upon selection
*/
private void initialiseBoatSelectionComboBox() {
updateBoatSelectionComboBox();
boatSelectionComboBox.valueProperty().addListener((observable, oldValue, newValue) -> {
//This listener is fired whenever the combo box changes. This means when the values are updated
//We dont want to set the selected value if the values are updated but nothing clicked (null)
if (newValue != null && newValue != selectedBoat) {
Yacht thisYacht = (Yacht) newValue;
setSelectedBoat(thisYacht);
}
});
}
/**
* Display the list of boats in the order they finished the race
*/
private void loadRaceResultView() {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/views/FinishView.fxml"));
try {
contentAnchorPane.getChildren().removeAll();
contentAnchorPane.getChildren().clear();
contentAnchorPane.getChildren().addAll((Pane) loader.load());
} catch (javafx.fxml.LoadException e) {
System.err.println(e.getCause());
} catch (IOException e) {
System.err.println(e);
}
}
/**
* Convert seconds to a string of the format mm:ss
*
* @param time the time in seconds
* @return a formatted string
*/
public String convertTimeToMinutesSeconds(int time) {
if (time < 0) {
return String.format("-%02d:%02d", (time * -1) / 60, (time * -1) % 60);
}
return String.format("%02d:%02d", time / 60, time % 60);
}
private String getTimeSinceStartOfRace() {
String timerString = "0:00";
if (StreamParser.getTimeSinceStart() > 0) {
String timerMinute = Long.toString(StreamParser.getTimeSinceStart() / 60);
String timerSecond = Long.toString(StreamParser.getTimeSinceStart() % 60);
if (timerSecond.length() == 1) {
timerSecond = "0" + timerSecond;
}
timerString = "-" + timerMinute + ":" + timerSecond;
} else {
String timerMinute = Long.toString(-1 * StreamParser.getTimeSinceStart() / 60);
String timerSecond = Long.toString(-1 * StreamParser.getTimeSinceStart() % 60);
if (timerSecond.length() == 1) {
timerSecond = "0" + timerSecond;
}
timerString = timerMinute + ":" + timerSecond;
}
return timerString;
}
boolean isDisplayFps() {
return displayFps;
}
private void setAnnotations(Integer annotationLevel) {
switch (annotationLevel) {
// No Annotations
case 0:
for (BoatGroup bg : gameViewController.getBoatGroups()) {
bg.setVisibility(false, false, false, false, false, false);
}
break;
// Important Annotations
case 1:
for (BoatGroup bg : gameViewController.getBoatGroups()) {
bg.setVisibility(
importantAnnotations.getAnnotationState(Annotation.NAME),
importantAnnotations.getAnnotationState(Annotation.SPEED),
importantAnnotations.getAnnotationState(Annotation.ESTTIMETONEXTMARK),
importantAnnotations.getAnnotationState(Annotation.LEGTIME),
importantAnnotations.getAnnotationState(Annotation.TRACK),
importantAnnotations.getAnnotationState(Annotation.WAKE)
);
}
break;
// All Annotations
case 2:
for (BoatGroup bg : gameViewController.getBoatGroups()) {
bg.setVisibility(true, true, true, true, true, true);
}
break;
}
}
/**
* Sets all the annotations of the selected boat to be visible and all others to be hidden
*
* @param yacht The yacht for which we want to view all annotations
*/
private void setSelectedBoat(Yacht yacht) {
for (BoatGroup bg : gameViewController.getBoatGroups()) {
//We need to iterate over all race groups to get the matching boat group belonging to this boat if we
//are to toggle its annotations, there is no other backwards knowledge of a yacht to its boatgroup.
if (bg.getBoat().getHullID().equals(yacht.getHullID())) {
updateLaylines(bg);
bg.setIsSelected(true);
selectedBoat = yacht;
} else {
bg.setIsSelected(false);
}
}
}
void setStage(Stage stage) {
this.stage = stage;
}
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);
}
}
@@ -0,0 +1,84 @@
package seng302.visualiser.controllers;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import seng302.visualiser.ClientToServerThread;
import seng302.gameServer.GameState;
import seng302.gameServer.MainServerThread;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* A Class describing the actions of the start screen controller
* Created by wmu16 on 10/07/17.
*/
public class StartScreenController {
@FXML
private TextField ipTextField;
@FXML
private GridPane startScreen2;
/**
* Loads the fxml content into the parent pane
* @param jfxUrl
* @return the controller of the fxml
*/
private Object setContentPane(String jfxUrl) {
try {
AnchorPane contentPane = (AnchorPane) startScreen2.getParent();
contentPane.getChildren().removeAll();
contentPane.getChildren().clear();
contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString());
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(jfxUrl));
contentPane.getChildren().addAll((Pane) fxmlLoader.load());
return fxmlLoader.getController();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* ATTEMPTS TO:
* Sets up a new game state with your IP address as designated as the host.
* Starts a thread to listen for incoming connections
* Switches to the lobby screen
*/
@FXML
public void hostButtonPressed() {
try {
String ipAddress = InetAddress.getLocalHost().getHostAddress();
new GameState(ipAddress);
new MainServerThread().start();
// get the lobby controller so that we can pass the game server thread to it
setContentPane("/views/LobbyView.fxml");
} catch (UnknownHostException e) {
System.err.println("COULD NOT FIND YOUR IP ADDRESS!");
e.printStackTrace();
}
}
@FXML
public void connectButtonPressed() {
// TODO: 10/07/17 wmu16 - Finish function
String ipAddress = ipTextField.getText().trim().toLowerCase();
try {
ClientToServerThread clientToServerThread = new ClientToServerThread(ipAddress, 4950);
clientToServerThread.start();
setContentPane("/views/LobbyView.fxml");
} catch (Exception e){
e.printStackTrace();
}
}
}
@@ -0,0 +1,13 @@
package seng302.visualiser.controllers.annotations;
/**
* Annotations the user can select as important
*/
public enum Annotation {
SPEED,
WAKE,
TRACK,
NAME,
ESTTIMETONEXTMARK,
LEGTIME
}
@@ -0,0 +1,141 @@
package seng302.visualiser.controllers.annotations;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
import java.net.URL;;
import java.util.ResourceBundle;
public class ImportantAnnotationController implements Initializable {
/*
* JavaFX Outlets
*/
@FXML
private CheckBox boatWakeSelect;
@FXML
private CheckBox boatSpeedSelect;
@FXML
private CheckBox boatTrackSelect;
@FXML
private CheckBox boatNameSelect;
@FXML
private CheckBox boatEstTimeToNextMarkSelect;
@FXML
private CheckBox boatElapsedTimeSelect;
@FXML
private AnchorPane annotationSelectWindow;
@FXML
private Button closeButton;
private ImportantAnnotationDelegate delegate;
private ImportantAnnotationsState importantAnnotationsState;
private Stage stage;
public ImportantAnnotationController(ImportantAnnotationDelegate delegate, Stage stage) {
this.delegate = delegate;
importantAnnotationsState = new ImportantAnnotationsState();
this.stage = stage;
}
/**
* Sets whether or not an annotation is considered important, then
* sends an update to the delegate
*
* @param annotation The annotation
* @param isSet True if annotation is important
*/
private void setAnnotation(Annotation annotation, Boolean isSet) {
importantAnnotationsState.setAnnotationState(annotation, isSet);
sendUpdate();
}
/**
* Sends an update to the delegate when the important
* annotations have changed
*/
private void sendUpdate() {
this.delegate.importantAnnotationsChanged(importantAnnotationsState);
}
/**
* Load the current state of the 'important annotations'
*
* @param currentState hashmap containing the states of each annotation
*/
public void loadState(ImportantAnnotationsState currentState) {
this.importantAnnotationsState = currentState;
// Initialise checkboxes
for (Annotation annotation : importantAnnotationsState.getAnnotations()) {
switch (annotation) {
case WAKE:
boatWakeSelect
.setSelected(importantAnnotationsState.getAnnotationState(annotation));
break;
case SPEED:
boatSpeedSelect
.setSelected(importantAnnotationsState.getAnnotationState(annotation));
break;
case TRACK:
boatTrackSelect
.setSelected(importantAnnotationsState.getAnnotationState(annotation));
break;
case NAME:
boatNameSelect
.setSelected(importantAnnotationsState.getAnnotationState(annotation));
break;
case ESTTIMETONEXTMARK:
boatEstTimeToNextMarkSelect
.setSelected(importantAnnotationsState.getAnnotationState(annotation));
break;
case LEGTIME:
boatElapsedTimeSelect
.setSelected(importantAnnotationsState.getAnnotationState(annotation));
default:
break;
}
}
}
/**
* View did load
*
* @param location .
* @param resources .
*/
@Override
public void initialize(URL location, ResourceBundle resources) {
boatWakeSelect
.setOnAction(event -> setAnnotation(Annotation.WAKE, boatWakeSelect.isSelected()));
boatSpeedSelect
.setOnAction(event -> setAnnotation(Annotation.SPEED, boatSpeedSelect.isSelected()));
boatTrackSelect
.setOnAction(event -> setAnnotation(Annotation.TRACK, boatTrackSelect.isSelected()));
boatNameSelect
.setOnAction(event -> setAnnotation(Annotation.NAME, boatNameSelect.isSelected()));
boatEstTimeToNextMarkSelect.setOnAction(event -> setAnnotation(Annotation.ESTTIMETONEXTMARK,
boatEstTimeToNextMarkSelect.isSelected()));
boatElapsedTimeSelect.setOnAction(
event -> setAnnotation(Annotation.LEGTIME, boatElapsedTimeSelect.isSelected()));
closeButton.setOnAction(event -> stage.close());
}
}
@@ -0,0 +1,16 @@
package seng302.visualiser.controllers.annotations;
/**
* An ImportantAnnotationDelegate handles updating the important annotations
* displayed to the user on behalf of the ImportantAnnotationController
*/
public interface ImportantAnnotationDelegate {
/**
* The important annotations have been changed, update the
* annotations displayed to the user
* @param importantAnnotationsState The current state of the selected annotations
*/
void importantAnnotationsChanged(ImportantAnnotationsState importantAnnotationsState);
}
@@ -0,0 +1,52 @@
package seng302.visualiser.controllers.annotations;
import java.util.HashMap;
import java.util.Map;
public class ImportantAnnotationsState {
public static final Boolean DEFAULT_ANNOTATION_STATE = true;
private Map<Annotation, Boolean> currentState;
/**
* Stores the users preference for the annotations
* they consider to be important
*/
public ImportantAnnotationsState(){
this.currentState = new HashMap<>();
initialiseState();
}
/**
* Set each annotation to the default annotation state
*/
private void initialiseState(){
for (Annotation annotation : getAnnotations()){
currentState.put(annotation, DEFAULT_ANNOTATION_STATE);
}
}
/**
* Sets the state (visibility) of an annotation
* @param annotation The annotation to set
* @param visible Whether or not the annotation should be visible
*/
public void setAnnotationState(Annotation annotation, Boolean visible){
this.currentState.put(annotation, visible);
}
/**
* Returns the state (visibility) of a specific annotation
* @param annotation The annotation to check
* @return True if visible, else false
*/
public Boolean getAnnotationState(Annotation annotation){
return this.currentState.containsKey(annotation) && this.currentState.get(annotation);
}
/**
* @return Return an array containing all defined annotations
*/
public Annotation[] getAnnotations(){
return Annotation.class.getEnumConstants();
}
}
@@ -0,0 +1,120 @@
package seng302.visualiser.controllers.client;
import javafx.beans.value.ChangeListener;
import javafx.scene.Node;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import seng302.model.stream.XMLParser;
import seng302.model.stream.packets.StreamPacket;
import seng302.server.messages.BoatActionMessage;
import seng302.server.messages.BoatActionType;
import seng302.visualiser.ClientToServerThread;
/**
* Created by cir27 on 20/07/17.
*/
public class ClientController {
Pane holderPane;
ClientToServerThread socketThread;
public ClientController (String ipAddress, Pane holder) {
this.holderPane = holder;
socketThread = new ClientToServerThread(ipAddress, 4950);
socketThread.start();
socketThread.waitForXML(event -> storeXMLData());
}
private void parsePacket(StreamPacket packet) {
try {
switch (packet.getType()) {
case HEARTBEAT:
extractHeartBeat(packet);
break;
case RACE_STATUS:
extractRaceStatus(packet);
break;
case DISPLAY_TEXT_MESSAGE:
extractDisplayMessage(packet);
break;
case XML_MESSAGE:
newRaceXmlReceived = true;
extractXmlMessage(packet);
break;
case RACE_START_STATUS:
extractRaceStartStatus(packet);
break;
case YACHT_EVENT_CODE:
extractYachtEventCode(packet);
break;
case YACHT_ACTION_CODE:
extractYachtActionCode(packet);
break;
case CHATTER_TEXT:
extractChatterText(packet);
break;
case BOAT_LOCATION:
extractBoatLocation(packet);
break;
case MARK_ROUNDING:
extractMarkRounding(packet);
break;
case COURSE_WIND:
extractCourseWind(packet);
break;
case AVG_WIND:
extractAvgWind(packet);
break;
case BOAT_ACTION:
extractBoatAction(packet);
break;
}
} catch (NullPointerException e) {
System.out.println("Error parsing packet");
e.printStackTrace();
}
}
// /** Handle the key-pressed event from the text field. */
// public void keyPressed(KeyEvent e) {
// BoatActionMessage boatActionMessage;
// switch (e.getCode()){
// case SPACE: // align with vmg
// boatActionMessage = new BoatActionMessage(BoatActionType.VMG);
// clientToServerThread.sendBoatActionMessage(boatActionMessage);
// break;
// case PAGE_UP: // upwind
// boatActionMessage = new BoatActionMessage(BoatActionType.UPWIND);
// clientToServerThread.sendBoatActionMessage(boatActionMessage);
// break;
// case PAGE_DOWN: // downwind
// boatActionMessage = new BoatActionMessage(BoatActionType.DOWNWIND);
// clientToServerThread.sendBoatActionMessage(boatActionMessage);
// break;
// case ENTER: // tack/gybe
// boatActionMessage = new BoatActionMessage(BoatActionType.TACK_GYBE);
// clientToServerThread.sendBoatActionMessage(boatActionMessage);
// break;
// //TODO Allow a zoom in and zoom out methods
// case Z: // zoom in
// System.out.println("Zoom in");
// break;
// case X: // zoom out
// System.out.println("Zoom out");
// break;
// }
// }
//
// public void keyReleased(KeyEvent e) {
// switch (e.getCode()) {
// //TODO 12/07/17 Determine the sail state and send the appropriate packet (eg. if sails are in, send a sail out packet)
// case SHIFT: // sails in/sails out
// BoatActionMessage boatActionMessage = new BoatActionMessage(BoatActionType.SAILS_IN);
// clientToServerThread.sendBoatActionMessage(boatActionMessage);
// break;
// }
// }
//
// onKeyPressed="#keyPressed" onKeyReleased="#keyReleased"
}
@@ -0,0 +1,13 @@
package seng302.visualiser.controllers.host;
import javafx.scene.layout.Pane;
/**
* Created by cir27 on 20/07/17.
*/
public class HostController {
Pane mainHolder;
public HostController (Pane holder) {
this.mainHolder = holder;
}
}
@@ -0,0 +1,133 @@
package seng302.visualiser.fxObjects;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import seng302.model.Yacht;
import seng302.model.stream.StreamParser;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
/**
* Collection of annotations for boats.
*/
public class BoatAnnotations extends Group{
//Text offset constants
private static final double X_OFFSET_TEXT = 18d;
private static final double Y_OFFSET_TEXT_INIT = -29d;
private static final double Y_OFFSET_PER_TEXT = 12d;
//Background constants
private static final double TEXT_BUFFER = 3;
private static final double BACKGROUND_X = X_OFFSET_TEXT - TEXT_BUFFER;
private static final double BACKGROUND_Y = Y_OFFSET_TEXT_INIT - TEXT_BUFFER;
private static final double BACKGROUND_H_PER_TEXT = 9.5d;
private static final double BACKGROUND_W = 125d;
private static final double BACKGROUND_ARC_SIZE = 10;
private Rectangle background = new Rectangle();
private Text teamNameObject;
private Text velocityObject;
private Text estTimeToNextMarkObject;
private Text legTimeObject;
private Yacht boat;
BoatAnnotations (Yacht boat, Color theme) {
super.setCache(true);
this.boat = boat;
background.setX(BACKGROUND_X);
background.setY(BACKGROUND_Y);
background.setWidth(BACKGROUND_W);
background.setHeight(Math.abs(BACKGROUND_X) + TEXT_BUFFER + BACKGROUND_H_PER_TEXT * 4);
background.setArcHeight(BACKGROUND_ARC_SIZE);
background.setArcWidth(BACKGROUND_ARC_SIZE);
background.setFill(new Color(1, 1, 1, 0.75));
background.setStroke(theme);
background.setStrokeWidth(2);
background.setCache(true);
background.setCacheHint(CacheHint.SPEED);
teamNameObject = getTextObject(boat.getShortName(), theme);
teamNameObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT);
velocityObject = getTextObject("0 m/s", theme);
velocityObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * 2);
estTimeToNextMarkObject = getTextObject("Next mark: ", theme);
estTimeToNextMarkObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * 3);
legTimeObject = getTextObject("Last mark: -", theme);
legTimeObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * 4);
super.getChildren().addAll(background, teamNameObject, velocityObject, estTimeToNextMarkObject, legTimeObject);
}
/**
* Return a text object with caching and a color applied
*
* @param defaultText The default text to display
* @param fill The text fill color
* @return The text object
*/
private Text getTextObject(String defaultText, Color fill) {
Text text = new Text(defaultText);
text.setFill(fill);
text.setStrokeWidth(2);
text.setCacheHint(CacheHint.SPEED);
text.setCache(true);
return text;
}
void update () {
velocityObject.setText(String.format(String.format("%.2f m/s", boat.getVelocity())));
if (boat.getTimeTillNext() != null) {
DateFormat format = new SimpleDateFormat("mm:ss");
String timeToNextMark = format
.format(boat.getTimeTillNext() - StreamParser.getCurrentTimeLong());
estTimeToNextMarkObject.setText("Next mark: " + timeToNextMark);
} else {
estTimeToNextMarkObject.setText("Next mark: -");
}
if (boat.getMarkRoundTime() != null) {
DateFormat format = new SimpleDateFormat("mm:ss");
String elapsedTime = format
.format(StreamParser.getCurrentTimeLong() - boat.getMarkRoundTime());
legTimeObject.setText("Last mark: " + elapsedTime);
}else {
legTimeObject.setText("Last mark: - ");
}
}
void setVisibile (boolean nameVisibility, boolean speedVisibility,
boolean estTimeVisibility, boolean lastMarkVisibility) {
int totalVisible = 0;
totalVisible = updateVisibility(nameVisibility, teamNameObject, totalVisible);
totalVisible = updateVisibility(speedVisibility, velocityObject, totalVisible);
totalVisible = updateVisibility(estTimeVisibility, estTimeToNextMarkObject, totalVisible);
totalVisible = updateVisibility(lastMarkVisibility, legTimeObject, totalVisible);
if (totalVisible != 0) {
background.setVisible(true);
background.setHeight(Math.abs(BACKGROUND_X) + TEXT_BUFFER + BACKGROUND_H_PER_TEXT * totalVisible);
} else {
background.setVisible(false);
}
}
private int updateVisibility (boolean visibility, Text text, int totalVisible) {
if (visibility){
totalVisible ++;
text.setVisible(true);
text.setLayoutX(X_OFFSET_TEXT);
text.setLayoutY(Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * totalVisible);
} else {
text.setVisible(false);
}
return totalVisible;
}
}
@@ -0,0 +1,352 @@
package seng302.visualiser.fxObjects;
import java.util.ArrayList;
import javafx.geometry.Point2D;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Polygon;
import javafx.scene.transform.Rotate;
import seng302.visualiser.controllers.GameViewController;
import seng302.model.Yacht;
import seng302.utilities.GeoUtility;
import seng302.model.mark.GateMark;
import seng302.model.mark.Mark;
import seng302.model.mark.SingleMark;
import seng302.model.stream.StreamParser;
/**
* BoatGroup is a javafx group that by default contains a graphical objects for representing a 2
* dimensional boat. It contains a single polygon for the boat, a group of lines to show it's path,
* a wake object and two text labels to annotate the boat teams name and the boats velocity. The
* boat will update it's position onscreen everytime UpdatePosition is called unless the window is
* minimized in which case it attempts to store animations and apply them when the window is
* maximised.
*/
public class BoatGroup extends Group {
//Constants for drawing
private static final double BOAT_HEIGHT = 15d;
private static final double BOAT_WIDTH = 10d;
//Variables for boat logic.
private boolean isStopped = true;
private double xIncrement;
private double yIncrement;
private long lastTimeValid = 0;
private Double lastRotation = 0.0;
private long framesToMove;
//Graphical objects
private Yacht boat;
private Group lineGroup = new Group();
private Polygon boatPoly;
private Wake wake;
private Line leftLayLine;
private Line rightLayline;
private Double distanceTravelled = 0.0;
private Point2D lastPoint;
private boolean destinationSet;
private BoatAnnotations boatAnnotations;
private Boolean isSelected = true; //All boats are initialised as selected
/**
* Creates a BoatGroup with the default triangular boat polygon.
*
* @param boat The boat that the BoatGroup will represent. Must contain an ID which will be used
* to tell which BoatGroup to update.
* @param color The colour of the boat polygon and the trailing line.
*/
public BoatGroup(Yacht boat, Color color) {
destinationSet = false;
this.boat = boat;
initChildren(color);
}
/**
* Creates a BoatGroup with the boat being the default polygon. The head of the boat should be
* at point (0,0).
*
* @param boat The boat that the BoatGroup will represent. Must contain an ID which will be used
* to tell which BoatGroup to update.
* @param color The colour of the boat polygon and the trailing line.
* @param points An array of co-ordinates x1,y1,x2,y2,x3,y3... that will make up the boat
* polygon.
*/
public BoatGroup(Yacht boat, Color color, double... points) {
destinationSet = false;
this.boat = boat;
initChildren(color, points);
}
/**
* Creates the javafx objects that will be the in the group by default.
*
* @param color The colour of the boat polygon and the trailing line.
* @param points An array of co-ordinates x1,y1,x2,y2,x3,y3... that will make up the boat
* polygon.
*/
private void initChildren(Color color, double... points) {
boatPoly = new Polygon(points);
boatPoly.setFill(color);
boatPoly.setOnMouseEntered(event -> {
boatPoly.setFill(Color.FLORALWHITE);
boatPoly.setStroke(Color.RED);
});
boatPoly.setOnMouseExited(event -> {
boatPoly.setFill(color);
boatPoly.setStroke(Color.BLACK);
});
boatPoly.setOnMouseClicked(event -> setIsSelected(!isSelected));
boatPoly.setCache(true);
boatPoly.setCacheHint(CacheHint.SPEED);
boatAnnotations = new BoatAnnotations(boat, color);
leftLayLine = new Line();
rightLayline = new Line();
wake = new Wake(0, -BOAT_HEIGHT);
super.getChildren().addAll(boatPoly, boatAnnotations);
}
/**
* Creates the javafx objects that will be the in the group by default.
*
* @param color The colour of the boat polygon and the trailing line.
*/
private void initChildren(Color color) {
initChildren(color,
-BOAT_WIDTH / 2, BOAT_HEIGHT / 2,
0.0, -BOAT_HEIGHT / 2,
BOAT_WIDTH / 2, BOAT_HEIGHT / 2);
}
/**
* Moves the boat and its children annotations from its current coordinates by specified
* amounts.
*
* @param dx The amount to move the X coordinate by
* @param dy The amount to move the Y coordinate by
*/
private void moveGroupBy(double dx, double dy) {
boatPoly.setLayoutX(boatPoly.getLayoutX() + dx);
boatPoly.setLayoutY(boatPoly.getLayoutY() + dy);
boatAnnotations.setLayoutX(boatAnnotations.getLayoutX() + dx);
boatAnnotations.setLayoutY(boatAnnotations.getLayoutY() + dy);
wake.setLayoutX(wake.getLayoutX() + dx);
wake.setLayoutY(wake.getLayoutY() + dy);
}
/**
* Moves the boat and its children annotations to coordinates specified
*
* @param x The X coordinate to move the boat to
* @param y The Y coordinate to move the boat to
*/
private void moveTo(double x, double y, double rotation) {
rotateTo(rotation);
boatPoly.setLayoutX(x);
boatPoly.setLayoutY(y);
boatAnnotations.setLayoutX(x);
boatAnnotations.setLayoutY(y);
wake.setLayoutX(x);
wake.setLayoutY(y);
wake.rotate(rotation);
}
private void rotateTo(double rotation) {
boatPoly.getTransforms().setAll(new Rotate(rotation));
}
public void move() {
double dx = xIncrement * framesToMove;
double dy = yIncrement * framesToMove;
distanceTravelled += Math.abs(dx) + Math.abs(dy);
moveGroupBy(xIncrement, yIncrement);
framesToMove = framesToMove - 1;
if (framesToMove <= 0) {
isStopped = true;
}
if (distanceTravelled > 70) {
distanceTravelled = 0d;
if (lastPoint != null) {
Line l = new Line(
lastPoint.getX(),
lastPoint.getY(),
boatPoly.getLayoutX(),
boatPoly.getLayoutY()
);
l.getStrokeDashArray().setAll(3d, 7d);
l.setStroke(boat.getColour());
l.setCache(true);
l.setCacheHint(CacheHint.SPEED);
lineGroup.getChildren().add(l);
}
if (destinationSet) {
lastPoint = new Point2D(boatPoly.getLayoutX(), boatPoly.getLayoutY());
}
}
wake.updatePosition();
}
/**
* Sets the destination of the boat and the headng it should have once it reaches
*
* @param newXValue The X co-ordinate the boat needs to move to.
* @param newYValue The Y co-ordinate the boat needs to move to.
* @param rotation Rotation to move graphics to.
* @param timeValid the time the position values are valid for
*/
public void setDestination(double newXValue, double newYValue, double rotation,
double groundSpeed, long timeValid, double frameRate) {
if (lastTimeValid == 0) {
lastTimeValid = timeValid - 200;
moveTo(newXValue, newYValue, rotation);
}
framesToMove = Math.round((frameRate / (1000.0f / (timeValid - lastTimeValid))));
double dx = newXValue - boatPoly.getLayoutX();
double dy = newYValue - boatPoly.getLayoutY();
xIncrement = dx / framesToMove;
yIncrement = dy / framesToMove;
destinationSet = true;
rotateTo(rotation);
wake.setRotation(rotation, groundSpeed);
boat.setVelocity(groundSpeed);
lastTimeValid = timeValid;
isStopped = false;
lastRotation = rotation;
boatAnnotations.update();
}
/**
* This function works out if a boat is going upwind or down wind. It looks at the boats current position, the next
* gates position and the current wind
* If bot the wind vector from the next gate and the boat from the next gate lay on the same side, then the boat is
* going up wind, if they are on different sides of the gate, then the boat is going downwind
* @param canvasController
*/
public Boolean isUpwindLeg(GameViewController canvasController, Mark nextMark) {
Double windAngle = StreamParser.getWindDirection();
GateMark thisGateMark = (GateMark) nextMark;
SingleMark nextMark1 = thisGateMark.getSingleMark1();
SingleMark nextMark2 = thisGateMark.getSingleMark2();
Point2D nextMarkPoint1 = canvasController.findScaledXY(nextMark1.getLatitude(), nextMark1.getLongitude());
Point2D nextMarkPoint2 = canvasController.findScaledXY(nextMark2.getLatitude(), nextMark2.getLongitude());
Point2D boatCurrentPoint = new Point2D(boatPoly.getLayoutX(), boatPoly.getLayoutY());
Point2D windTestPoint = GeoUtility.makeArbitraryVectorPoint(nextMarkPoint1, windAngle, 10d);
Integer boatLineFuncResult = GeoUtility.lineFunction(nextMarkPoint1, nextMarkPoint2, boatCurrentPoint);
Integer windLineFuncResult = GeoUtility.lineFunction(nextMarkPoint1, nextMarkPoint2, windTestPoint);
/*
If both the wind vector from the gate and the boat from the gate are on the same side of that gate, then the
boat is travelling into the wind. thus upwind. Otherwise if they are on different sides, then the boat is going
with the wind.
*/
if (boatLineFuncResult == windLineFuncResult) {
return true;
} else {
return false;
}
}
public void setIsSelected(Boolean isSelected) {
this.isSelected = isSelected;
setLineGroupVisible(isSelected);
setWakeVisible(isSelected);
boatAnnotations.setVisible(isSelected);
setLayLinesVisible(isSelected);
}
public void setVisibility (boolean teamName, boolean velocity, boolean estTime, boolean legTime, boolean trail, boolean wake) {
boatAnnotations.setVisibile(teamName, velocity, estTime, legTime);
this.wake.setVisible(wake);
this.lineGroup.setVisible(trail);
}
public void setLineGroupVisible(Boolean visible) {
lineGroup.setVisible(visible);
}
public void setWakeVisible(Boolean visible) {
wake.setVisible(visible);
}
public void setLayLinesVisible(Boolean visible) {
leftLayLine.setVisible(visible);
rightLayline.setVisible(visible);
}
public void setLaylines(Line line1, Line line2) {
this.leftLayLine = line1;
this.rightLayline = line2;
}
public ArrayList<Line> getLaylines() {
ArrayList<Line> laylines = new ArrayList<>();
laylines.add(leftLayLine);
laylines.add(rightLayline);
return laylines;
}
public Yacht getBoat() {
return boat;
}
/**
* Returns all raceIds associated with this group. For BoatGroups the ID's are for the boat.
*
* @return An array containing all ID's associated with this RaceObject.
*/
public long getRaceId() {
return boat.getSourceID();
}
public Group getWake () {
return wake;
}
public Group getTrail() {
return lineGroup;
}
public Group getAnnotations() {
return boatAnnotations;
}
public Double getBoatLayoutX() {
return boatPoly.getLayoutX();
}
public Double getBoatLayoutY() {
return boatPoly.getLayoutY();
}
public boolean isStopped() {
return isStopped;
}
@Override
public String toString() {
return boat.toString();
}
}
@@ -0,0 +1,167 @@
package seng302.visualiser.fxObjects;
import java.util.ArrayList;
import java.util.List;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import seng302.model.mark.GateMark;
import seng302.model.mark.Mark;
import seng302.model.mark.MarkType;
import seng302.model.mark.SingleMark;
/**
* Grouping of javaFX objects needed to represent a Mark on screen.
*/
public class MarkGroup extends Group {
private static int MARK_RADIUS = 5;
private static int LINE_THICKNESS = 2;
private static double DASHED_GAP_LEN = 2d;
private static double DASHED_LINE_LEN = 5d;
private List<Mark> marks = new ArrayList<>();
private Mark mainMark;
/**
* Constructor for singleMark groups
* @param mark
* @param points
*/
public MarkGroup (SingleMark mark, Point2D points) {
marks.add(mark);
mainMark = mark;
Color color = Color.BLACK;
if (mark.getName().equals("Start")){
color = Color.GREEN;
} else if (mark.getName().equals("Finish")){
color = Color.RED;
}
Circle markCircle;
markCircle = new Circle(
points.getX(),
points.getY(),
MARK_RADIUS,
color
);
super.getChildren().add(markCircle);
}
public void addLaylines(Line line1, Line line2) {
super.getChildren().addAll(line1, line2);
}
public void removeLaylines() {
ArrayList<Node> toRemove = new ArrayList<>();
for(Node node : super.getChildren()) {
if (node instanceof Line) {
Line layLine = (Line) node;
/***
* OOHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHhhh
*/
if (layLine.getStrokeWidth() == 0.5){
toRemove.add(layLine);
}
}
}
super.getChildren().removeAll(toRemove);
}
public MarkGroup(GateMark mark, Point2D points1, Point2D points2) {
marks.add(mark.getSingleMark1());
marks.add(mark.getSingleMark2());
mainMark = mark;
Color color = Color.BLACK;
if (mark.getName().equals("Start")){
color = Color.GREEN;
} else if (mark.getName().equals("Finish")){
color = Color.RED;
}
Circle markCircle;
markCircle = new Circle(
points1.getX(),
points1.getY(),
MARK_RADIUS,
color
);
super.getChildren().add(markCircle);
markCircle = new Circle(
points2.getX(),
points2.getY(),
MARK_RADIUS,
color
);
super.getChildren().add(markCircle);
Line line = new Line(
points1.getX(),
points1.getY(),
points2.getX(),
points2.getY()
);
line.setStrokeWidth(LINE_THICKNESS);
line.setStroke(color);
if (mark.getMarkType() == MarkType.OPEN_GATE) {
line.getStrokeDashArray().addAll(DASHED_GAP_LEN, DASHED_LINE_LEN);
}
super.getChildren().add(line);
}
public void moveMarkTo (double x, double y, long raceId)
{
if (mainMark.getMarkType() == MarkType.SINGLE_MARK) {
Circle markCircle = (Circle) super.getChildren().get(0);
//One of the test streams produced frequent, jittery movements. Added this as a fix.
if (Math.abs(markCircle.getCenterX() - x) > 5 || Math.abs(markCircle.getCenterY() - y) > 5) {
markCircle.setCenterX(x);
markCircle.setCenterY(y);
}
} else {
Circle markCircle1 = (Circle) super.getChildren().get(0);
Circle markCircle2 = (Circle) super.getChildren().get(1);
Line connectingLine = (Line) super.getChildren().get(2);
if (marks.get(0).getId() == raceId) {
if (Math.abs(markCircle1.getCenterX() - x) > 5 || Math.abs(markCircle1.getCenterY() - y) > 5) {
markCircle1.setCenterX(x);
markCircle1.setCenterY(y);
connectingLine.setStartX(markCircle1.getCenterX());
connectingLine.setStartY(markCircle1.getCenterY());
}
} else if (marks.get(1).getId() == raceId) {
if (Math.abs(markCircle2.getCenterX() - x) > 5 || Math.abs(markCircle2.getCenterY() - y) > 5) {
markCircle2.setCenterX(x);
markCircle2.setCenterY(y);
connectingLine.setEndX(markCircle2.getCenterX());
connectingLine.setEndY(markCircle2.getCenterY());
}
}
}
}
public boolean hasRaceId (int... raceIds) {
for (int id : raceIds)
for (Mark mark : marks)
if (id == mark.getId())
return true;
return false;
}
public long[] getRaceIds () {
long[] idArray = new long[marks.size()];
int i = 0;
for (Mark mark : marks)
idArray[i++] = mark.getId();
return idArray;
}
public Mark getMainMark() {
return mainMark;
}
}
@@ -0,0 +1,111 @@
package seng302.visualiser.fxObjects;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.transform.Rotate;
/**
* A group containing objects used to represent wakes onscreen. Contains functionality for their animation.
*/
public class Wake extends Group {
//The number of wakes
private int numWakes = 8;
//The total possible difference between the first wake and the last. Increasing/Decreasing this will make wakes fan out more/less.
private final double MAX_DIFF = 75;
//Increasing/decreasing this will alter the speed that wakes converge when the heading stop changing. Anything over about 1500 may cause oscillation.
private final int UNIFICATION_SPEED = 45;
private Arc[] arcs = new Arc[numWakes];
private double[] rotationalVelocities = new double[numWakes];
private double[] rotations = new double[numWakes];
/**
* Create a wake at the given location.
*
* @param startingX x location where the tip of wake arcs will be.
* @param startingY y location where the tip of wake arcs will be.
*/
Wake(double startingX, double startingY) {
super.setLayoutX(startingX);
super.setLayoutY(startingY);
Arc arc;
for (int i = 0; i < numWakes; i++) {
//Default triangle is -110 deg out of phase with a default wake and has angle of 40 deg.
arc = new Arc(0, 0, 0, 0, -110, 40);
arc.setCache(true);
arc.setCacheHint(CacheHint.ROTATE);
arc.setType(ArcType.OPEN);
arc.setStroke(new Color(0.18, 0.7, 1.0, 1.0 + (-0.99 / numWakes * i)));
arc.setStrokeWidth(3.0);
arc.setStrokeLineCap(StrokeLineCap.ROUND);
arc.setFill(new Color(0.0, 0.0, 0.0, 0.0));
arcs[i] = arc;
arc.getTransforms().setAll(
new Rotate(1)
);
}
super.getChildren().addAll(arcs);
}
void setRotation (double rotation, double velocity) {
if (Math.abs(rotations[0] - rotation) > 20) {
rotate(rotation);
} else {
rotations[0] = rotation;
((Rotate) arcs[0].getTransforms().get(0)).setAngle(rotation);
for (int i = 1; i < numWakes; i++) {
double wakeSeparationRad = Math.toRadians(rotations[i - 1] - rotations[i]);
double shortestDistance = Math.atan2(
Math.sin(wakeSeparationRad),
Math.cos(wakeSeparationRad)
);
double distDeg = Math.toDegrees(shortestDistance);
if (rotationalVelocities[i - 1] < 0.01 && rotationalVelocities[i - 1] > -0.01) {
rotationalVelocities[i] = distDeg / UNIFICATION_SPEED * 2 * Math.log(Math.abs(distDeg) + 1) / Math.log(MAX_DIFF / numWakes);
} else {
if (distDeg < (MAX_DIFF / numWakes)) {
rotationalVelocities[i] = distDeg / UNIFICATION_SPEED * Math.log(Math.abs(distDeg) + 1) / Math.log(MAX_DIFF / numWakes);
} else
rotationalVelocities[i] = rotationalVelocities[i - 1];
}
}
}
double rad = (14 / numWakes) + velocity;
for (Arc arc : arcs) {
arc.setRadiusX(rad);
arc.setRadiusY(rad);
rad += (14 / numWakes) + (velocity / 2.5);
}
}
/**
* Arcs rotate based on the distance they would have travelled over the supplied time interval.
*/
void updatePosition() {
for (int i = 0; i < numWakes; i++) {
rotations[i] = rotations[i] + rotationalVelocities[i];
((Rotate) arcs[i].getTransforms().get(0)).setAngle(rotations[i]);
}
}
/**
* Rotate all wakes to the given rotation.
*
* @param rotation the from north angle in degrees to rotate to.
*/
void rotate(double rotation) {
for (int i = 0; i < arcs.length; i++) {
rotations[i] = rotation;
rotationalVelocities[i] = 0;
arcs[i].getTransforms().setAll(new Rotate(rotation));
}
}
}