Implemented observer and strategy pattern in BoatAnnotations, now renamed AnnotationsBox.

Also implemented various other small fixes and further refactored code.

#refactor
This commit is contained in:
Calum
2017-07-25 20:45:27 +12:00
parent aad93d8913
commit 6242ab0b2e
12 changed files with 663 additions and 611 deletions
+23 -14
View File
@@ -1,9 +1,11 @@
package seng302.model;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyLongProperty;
import javafx.beans.property.ReadOnlyLongWrapper;
import javafx.scene.paint.Color;
import seng302.model.mark.Mark;
import seng302.model.stream.packets.StreamPacket;
import seng302.visualiser.controllers.RaceViewController;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
@@ -30,15 +32,14 @@ public class Boat {
private Integer boatStatus;
private Integer legNumber = 0;
private Integer position = 0;
private Integer penaltiesAwarded;
private Integer penaltiesServed;
private Long estimateTimeAtFinish;
private Long markRoundTime;
private Double lat;
private Double lon;
private Double heading;
private double velocity;
private Long timeTillNext;
private Long markRoundTime;
private ReadOnlyDoubleWrapper velocity = new ReadOnlyDoubleWrapper();
private ReadOnlyLongWrapper timeTillNext = new ReadOnlyLongWrapper();
private ReadOnlyLongWrapper timeSinceLastMark = new ReadOnlyLongWrapper();
// Mark rounding
private Mark lastMarkRounded;
@@ -94,8 +95,8 @@ public class Boat {
this.legNumber = legNumber;
}
public void setEstimateTimeAtNextMark(Long estimateTimeAtNextMark) {
timeTillNext = estimateTimeAtNextMark;
public void setEstimateTimeTillNextMark(Long estimateTimeAtNextMark) {
timeTillNext.set(estimateTimeAtNextMark);
}
public String getEstimateTimeAtFinish() {
@@ -124,7 +125,7 @@ public class Boat {
}
public void setVelocity(double velocity) {
this.velocity = velocity;
this.velocity.set(velocity);
}
@@ -132,12 +133,12 @@ public class Boat {
this.markRoundTime = markRoundingTime;
}
public double getVelocity() {
return velocity;
public ReadOnlyDoubleProperty getVelocityProperty() {
return velocity.getReadOnlyProperty();
}
public Long getTimeTillNext() {
return timeTillNext;
public ReadOnlyLongProperty timeTillNextProperty() {
return timeTillNext.getReadOnlyProperty();
}
public Long getMarkRoundTime() {
@@ -189,4 +190,12 @@ public class Boat {
return boatName;
}
public void setTimeSinceLastMark (long timeSinceLastMark) {
this.timeSinceLastMark.set(timeSinceLastMark);
}
public ReadOnlyLongProperty timeSinceLastMarkProperty () {
return timeSinceLastMark.getReadOnlyProperty();
}
}
@@ -0,0 +1,72 @@
package seng302.model;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.TimeZone;
import seng302.model.stream.parsers.RaceStartData;
import seng302.model.stream.parsers.RaceStatusData;
/**
* Class for storing race data that does not relate to specific vessels or marks such as time or wind.
* Calculates the state of critical race attributes when relevant data is added.
*/
public class RaceState {
// private final DateFormat DATE_TIME_FORMAT = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
private final DateFormat DATE_TIME_FORMAT = new SimpleDateFormat("HH:mm:ss");
private double windSpeed;
private double windDirection;
private long raceTime;
private long expectedStartTime;
private boolean isRaceStarted;
// long timeTillStart;
public RaceState() {
}
public void updateState (RaceStatusData data) {
this.windSpeed = data.getWindSpeed();
this.windDirection = data.getWindDirection();
this.raceTime = data.getCurrentTime();
this.expectedStartTime = data.getExpectedStartTime();
this.isRaceStarted = data.isRaceStarted();
}
public void setTimeZone (TimeZone timeZone) {
DATE_TIME_FORMAT.setTimeZone(timeZone);
}
public void updateState (RaceStartData data) {
// this.timeTillStart = data.getRaceStartTime();
System.out.println(data.getRaceStartTime());
}
public String getRaceTimeStr () {
return DATE_TIME_FORMAT.format(raceTime);
}
public long getTimeTillStart () {
return (expectedStartTime - raceTime) / 1000;
}
public double getWindSpeed() {
return windSpeed;
}
public double getWindDirection() {
return windDirection;
}
public long getRaceTime() {
return raceTime;
}
public long getExpectedStartTime() {
return expectedStartTime;
}
public boolean isRaceStarted () {
return isRaceStarted;
}
}
@@ -1,16 +0,0 @@
package seng302.model;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleLongProperty;
/**
* Class for storing race data that does not relate to specific vessels or marks such as time or wind
*/
public class RaceStatus {
double windSpeed;
double windDirection;
long raceTime;
}
@@ -38,20 +38,6 @@ public class StreamParser {
return heartbeat;
}
private static String getTimeZoneString(RegattaXMLData regattaXML) {
Integer offset = regattaXML.getUtcOffset();
StringBuilder utcOffset = new StringBuilder();
utcOffset.append("GMT");
if (offset > 0) {
utcOffset.append("+");
utcOffset.append(offset);
} else if (offset < 0) {
utcOffset.append("-");
utcOffset.append(offset);
}
return utcOffset.toString();
}
/**
* Extracts the useful race status data from race status type packets. This method will also
* print to the console the current state of the race (if it has started/finished or is about to
@@ -112,7 +98,7 @@ public class StreamParser {
// boat.setPenaltiesServed((int) payload[31 + (i * 20)]);
Long estTimeAtNextMark = bytesToLong(
Arrays.copyOfRange(payload, 32 + (i * 20), 38 + (i * 20)));
// boat.setEstimateTimeAtNextMark(estTimeAtNextMark);
// boat.setEstimateTimeTillNextMark(estTimeAtNextMark);
Long estTimeAtFinish = bytesToLong(
Arrays.copyOfRange(payload, 38 + (i * 20), 44 + (i * 20)));
int leg = (int) payload[29 + (i * 20)];
+108 -108
View File
@@ -1,11 +1,13 @@
package seng302.visualiser;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.PriorityBlockingQueue;
import javafx.animation.AnimationTimer;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.collections.ObservableList;
@@ -21,8 +23,9 @@ import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.scene.text.Text;
import seng302.model.Limit;
import seng302.visualiser.fxObjects.BoatGroup;
import seng302.visualiser.fxObjects.MarkGroup;
import seng302.visualiser.fxObjects.AnnotationBox;
import seng302.visualiser.fxObjects.BoatObject;
import seng302.visualiser.fxObjects.MarkObject;
import seng302.model.Colors;
import seng302.model.Boat;
import seng302.model.map.Boundary;
@@ -32,10 +35,6 @@ import seng302.model.mark.Mark;
import seng302.model.mark.MarkType;
import seng302.model.mark.SingleMark;
import seng302.model.stream.parsers.StreamParser;
import seng302.model.stream.parsers.xml.XMLParser;
import seng302.model.stream.parsers.xml.XMLParser.RaceXMLObject.Limit;
import seng302.model.stream.parsers.xml.XMLParser.RaceXMLObject.Participant;
import seng302.model.stream.parsers.PositionUpdateData;
import seng302.utilities.GeoPoint;
import seng302.utilities.GeoUtility;
@@ -65,8 +64,9 @@ public class GameView extends Pane {
private double metersPerPixelX;
private double metersPerPixelY;
private List<MarkGroup> markGroups = new ArrayList<>();
private List<BoatGroup> boatGroups = new ArrayList<>();
private Map<SingleMark, MarkObject> markObjects = new HashMap<>();
private Map<Boat, BoatObject> boatObjects = new HashMap<>();
private List<AnnotationBox> annotations = new ArrayList<>();
private Text fpsDisplay = new Text();
@@ -130,17 +130,14 @@ public class GameView extends Pane {
};
}
void initializeCanvas() {
fitMarksToCanvas();
public void initializeCanvas() {
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();
@@ -206,14 +203,12 @@ public class GameView extends Pane {
* 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();
public void updateBorder(List<Limit> border) {
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) {
for (Limit limit : border) {
Point2D location = findScaledXY(limit.getLat(), limit.getLng());
boundaryPoints.add(location.getX());
boundaryPoints.add(location.getY());
@@ -222,112 +217,114 @@ public class GameView extends Pane {
}
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();
boatObjects.forEach((boat, boatObject) -> {});
markObjects.forEach((mark, markObject) -> {});
}
private void checkForCourseChanges() {
if (StreamParser.isNewRaceXmlReceived()){
addRaceBorder();
}
}
private void updateBoatGroup(BoatGroup boatGroup) {
// PriorityBlockingQueue<PositionUpdateData> movementQueue = StreamParser.boatLocations.get(boatGroup.getRaceId());
// // giving the movementQueue a 5 packet buffer to account for slightly out of order packets
// private void updateBoatGroup(BoatGroup boatGroup) {
//// PriorityBlockingQueue<PositionUpdateData> 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 {
//// PositionUpdateData 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<PositionUpdateData> movementQueue = StreamParser.markLocations.get(raceId);
// if (movementQueue.size() > 0){
// try {
// PositionUpdateData 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);
// Point2D p2d = findScaledXY(positionPacket.getLat(), positionPacket.getLon());
// markGroup.moveMarkTo(p2d.getX(), p2d.getY(), raceId);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//// }
// }
}
private void updateMarkGroup (long raceId, MarkGroup markGroup) {
PriorityBlockingQueue<PositionUpdateData> movementQueue = StreamParser.markLocations.get(raceId);
if (movementQueue.size() > 0){
try {
PositionUpdateData 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, Boat> boats = StreamParser.getBoats();
Group wakes = new Group();
Group trails = new Group();
Group annotations = new Group();
public void setBoats(List<Boat> boats) {
Group annotationsGroup = new Group();
Group wakesGroup = new Group();
Group boatObjectGroup = new Group();
ArrayList<Participant> participants = StreamParser.getXmlObject().getRaceXML()
.getParticipants();
ArrayList<Integer> participantIDs = new ArrayList<>();
for (Participant p : participants) {
participantIDs.add(p.getsourceID());
BoatObject newObject;
for (Boat boat : boats) {
newObject = new BoatObject();
// newObject.bindBoat(boat);
newObject.setFill(Colors.getColor());
createAnnotationBox(boat);
}
// Group wakes = new Group();
// Group trails = new Group();
// Group annotationsGroup = new Group();
//
// gameObjects.addAll(trails);
// gameObjects.addAll(wakes);
annotationsGroup.getChildren().addAll(annotations);
gameObjects.addAll(annotationsGroup);
gameObjects.addAll(boatObjects.values());
}
for (Boat 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());
private AnnotationBox createAnnotationBox (Boat boat) {
AnnotationBox newAnnotation;
newAnnotation = new AnnotationBox();
newAnnotation.addAnnotation("name", boat.getShortName());
// newAnnotation.addAnnotation("country", boat.getCountry());
newAnnotation.addAnnotation(
"velocity",
boat.getVelocityProperty(),
(velocity) -> String.format("%.2f ms", velocity.doubleValue())
);
newAnnotation.addAnnotation(
"nextMark",
boat.timeTillNextProperty(),
(time) -> {
DateFormat format = new SimpleDateFormat("mm:ss");
return format.format(time);
}
);
newAnnotation.addAnnotation(
"lastMark",
boat.timeTillNextProperty(),
(time) -> {
DateFormat format = new SimpleDateFormat("mm:ss");
return format.format(time);
}
gameObjects.addAll(trails);
gameObjects.addAll(wakes);
gameObjects.addAll(annotations);
gameObjects.addAll(boatGroups);
);
annotations.add(newAnnotation);
return newAnnotation;
}
private void initializeMarks() {
List<Mark> allMarks = StreamParser.getXmlObject().getRaceXML().getNonDupCompoundMarks();
for (Mark mark : allMarks) {
public void updateCourse(List<Mark> course) {
for (Mark mark : course) {
if (mark.getMarkType() == MarkType.SINGLE_MARK) {
SingleMark sMark = (SingleMark) mark;
MarkGroup markGroup = new MarkGroup(sMark, findScaledXY(sMark));
markGroups.add(markGroup);
MarkObject markObject = new MarkObject(sMark, findScaledXY(sMark));
markObjects.put(sMark, markObject);
} else {
GateMark gMark = (GateMark) mark;
MarkGroup markGroup = new MarkGroup(gMark, findScaledXY(gMark.getSingleMark1()),
MarkObject markObject = new MarkObject(gMark, findScaledXY(gMark.getSingleMark1()),
findScaledXY(gMark.getSingleMark2())); //should be 2 objects in the list.
markGroups.add(markGroup);
// markObjects.put(markObject.);
}
}
gameObjects.addAll(markGroups);
// gameObjects.addAll(markObjects);
}
private void drawFps(int fps){
@@ -340,12 +337,11 @@ public class GameView extends Pane {
*/
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();
// updateBorder();
}
@@ -356,9 +352,9 @@ public class GameView extends Pane {
*/
private void findMinMaxPoint() {
List<Limit> sortedPoints = new ArrayList<>();
for (Limit limit : raceData) {
sortedPoints.add(limit);
}
// for (Limit limit : ) {
// sortedPoints.add(limit);
// }
sortedPoints.sort(Comparator.comparingDouble(Limit::getLat));
Limit minLatMark = sortedPoints.get(0);
Limit maxLatMark = sortedPoints.get(sortedPoints.size()-1);
@@ -503,8 +499,14 @@ public class GameView extends Pane {
public void setAnnotationVisibilities (boolean teamName, boolean velocity, boolean estTime,
boolean legTime, boolean trail, boolean wake) {
for (BoatGroup boatGroup : boatGroups) {
boatGroup.setVisibility(teamName, velocity, estTime, legTime, trail, wake);
for (BoatObject boatObject : boatObjects.values()) {
boatObject.setVisibility(teamName, velocity, estTime, legTime, trail, wake);
}
for (AnnotationBox ag : annotations) {
ag.setAnnotationVisibility("name", teamName);
ag.setAnnotationVisibility("velocity", velocity);
ag.setAnnotationVisibility("nextMark", estTime);
ag.setAnnotationVisibility("lastMark", legTime);
}
}
@@ -516,8 +518,10 @@ public class GameView extends Pane {
return fpsDisplay.visibleProperty();
}
public void selectBoat (int boatId) {
public void selectBoat (Boat selectedBoat) {
boatObjects.forEach((boat, group) ->
group.setIsSelected(boat == selectedBoat)
);
}
public void pauseRace () {
@@ -527,8 +531,4 @@ public class GameView extends Pane {
public void startRace () {
timer.start();
}
public void updateBorder (List<Limit> courseLimit) {
}
}
@@ -1,16 +1,21 @@
package seng302.visualiser.controllers;
import com.sun.javafx.collections.SortableList;
import java.util.concurrent.TimeUnit;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
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.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
@@ -35,8 +40,8 @@ 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.visualiser.fxObjects.BoatObject;
import seng302.visualiser.fxObjects.MarkObject;
import seng302.model.*;
import seng302.model.mark.GateMark;
import seng302.model.mark.Mark;
@@ -45,15 +50,14 @@ import seng302.model.stream.parsers.StreamParser;
import java.io.IOException;
import java.util.*;
import seng302.model.stream.parsers.xml.XMLParser.RaceXMLObject.Participant;
/**
* Created by ptg19 on 29/03/17.
* Controller class that manages the display of a race
*/
public class RaceViewController extends Thread implements ImportantAnnotationDelegate {
@FXML
private LineChart raceSparkLine;
private LineChart<String, Double> raceSparkLine;
@FXML
private NumberAxis sparklineYAxis;
@FXML
@@ -75,15 +79,14 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
//Race Data
private Map<Integer, Boat> participants;
private Map<Integer, Mark> course;
private RaceXMLData raceData;
private Map<Integer, Mark> markers;
private RaceXMLData courseData;
private GameView gameView;
private RaceState raceState;
private Timeline timerTimeline;
private Stage stage;
private HashMap<Integer, Series<String, Double>> sparkLineData = new HashMap<>();
private ImportantAnnotationsState importantAnnotations;
private Boat selectedBoat;
public void initialize() {
// Load a default important annotation state
@@ -96,20 +99,26 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
raceSparkLine.getYAxis().setAutoRanging(false);
sparklineYAxis.setTickMarkVisible(false);
selectAnnotationBtn.setOnAction(event -> loadSelectAnnotationView());
}
public void loadRace (Map<Integer, Boat> participants, RaceXMLData raceData, RaceState raceState) {
this.participants = participants;
this.courseData = raceData;
this.markers = raceData.getCompoundMarks();
this.raceState = raceState;
initializeUpdateTimer();
initialiseFPSCheckBox();
initialiseAnnotationSlider();
initialiseBoatSelectionComboBox();
selectAnnotationBtn.setOnAction(event -> loadSelectAnnotationView());
}
public void loadRace (Map<Integer, Boat> participants, RaceXMLData raceData) {
this.participants = participants;
this.raceData = raceData;
this.course = raceData.getCompoundMarks();
gameView = new GameView();
gameView.setBoats(new ArrayList<>(participants.values()));
gameView.updateBorder(raceData.getCourseLimit());
gameView.updateCourse(new ArrayList<>(raceData.getCompoundMarks().values()));
gameView.startRace();
}
/**
@@ -169,7 +178,6 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
if (n == 2) {
return "All";
}
return "All";
}
@@ -189,7 +197,7 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
}
});
annotationSlider.valueProperty().addListener((obs, oldval, newVal) ->
annotationSlider.valueProperty().addListener((obs, oldVal, newVal) ->
setAnnotations((int) annotationSlider.getValue())
);
annotationSlider.setValue(2);
@@ -202,10 +210,11 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
private void updateSparkLine(){
// Collect the racing boats that aren't already in the chart
List<Boat> sparkLineCandidates = new ArrayList<>();
participants.forEach((id, boat) ->{
if (!sparkLineData.containsKey(id) && boat.getPosition() != null && !boat.getPosition().equals("-"))
sparkLineCandidates.add(boat);
});
// participants.forEach((id, boat) ->{
// if (!sparkLineData.containsKey(id) && boat.getPosition() != null && !boat.getPosition().equals("-"))
// sparkLineCandidates.add(boat);
// });
participants.forEach((id, boat) -> sparkLineCandidates.add(boat));
sparklineYAxis.setUpperBound(participants.size() + 1);
@@ -213,13 +222,18 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
sparkLineCandidates.stream().filter(yacht -> yacht.getPosition() != null).forEach(yacht -> {
Series<String, Double> yachtData = new Series<>();
yachtData.setName(yacht.getSourceID().toString());
yachtData.getData().add(new XYChart.Data<>(Integer.toString(yacht.getLegNumber()), 1 + participants.size() - Double.parseDouble(yacht.getPosition())));
yachtData.getData().add(
new XYChart.Data<>(
Integer.toString(yacht.getLegNumber()),
1.0 + participants.size() - 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) -> {
positions.sort((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){
@@ -247,14 +261,22 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
* @param legNumber the leg number that the position will be assigned to
*/
public void updateYachtPositionSparkline(Boat boat, Integer legNumber){
XYChart.Series<String, Double> positionData = sparkLineData.get(boat.getSourceID());
for (XYChart.Series<String, Double> positionData : sparkLineData.values()) {
positionData.getData().add(
new XYChart.Data<>(
new Data<>(
Integer.toString(legNumber),
1 + participants.size() - Double.parseDouble(boat.getPosition())
1.0 + participants.size() - boat.getPosition()
)
);
}
// XYChart.Series<String, Double> positionData = sparkLineData.get(boat.getSourceID());
// positionData.getData().add(
// new XYChart.Data<>(
// Integer.toString(legNumber),
// 1.0 + participants.size() - boat.getPosition()
// )
// );
}
/**
@@ -306,10 +328,10 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
* @param bg The BoatGroup to find the next mark of
* @return The next Mark or null if none found
*/
private Mark getNextMark(BoatGroup bg) {
private Mark getNextMark(BoatObject bg) {
Integer legNumber = bg.getBoat().getLegNumber();
List<Corner> markSequence = raceData.getMarkSequence();
List<Corner> markSequence = courseData.getMarkSequence();
if (legNumber == 0) {
return null;
@@ -319,7 +341,7 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
for (Corner corner : markSequence) {
if (legNumber + 2 == corner.getSeqID()) {
return raceData.getCompoundMarks().get(corner.getCompoundMarkID());
return courseData.getCompoundMarks().get(corner.getCompoundMarkID());
}
}
return null;
@@ -330,8 +352,8 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
* 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());
windDirectionText.setText(String.format("%.1f°", raceState.getWindDirection()));
windArrowText.setRotate(raceState.getWindDirection());
}
@@ -339,26 +361,14 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
* Updates the clock for the race
*/
private void updateRaceTime() {
if (StreamParser.isRaceFinished()) {
if (!raceState.isRaceStarted()) {
timerLabel.setFill(Color.RED);
timerLabel.setText("Race Finished!");
} else {
timerLabel.setText(getTimeSinceStartOfRace());
timerLabel.setText(raceState.getRaceTimeStr());
}
}
/**
* 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<Boat> observableBoats = FXCollections.observableArrayList();
observableBoats.addAll(participants.values());
boatSelectionComboBox.setItems(observableBoats);
}
/**
* Updates the order of the boats as from the StreamParser and sets them in the boat order
* section
@@ -369,10 +379,10 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
positionVbox.getStylesheets().add(getClass().getResource("/css/master.css").toString());
// list of racing boat id
List<Boat> sorted = new ArrayList<>(participants.values());
sorted.sort(Comparator.comparingInt(Boat::getPosition));
if (StreamParser.isRaceStarted()) {
for (Boat boat : StreamParser.getBoatsPos().values()) {
if (participantIDs.contains(boat.getSourceID())) { // check if the boat is racing
for (Boat boat : sorted) {
if (boat.getBoatStatus() == 3) { // 3 is finish status
Text textToAdd = new Text(boat.getPosition() + ". " +
boat.getShortName() + " (Finished)");
@@ -387,81 +397,78 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
positionVbox.getChildren().add(textToAdd);
}
}
}
} else {
participants.forEach((id, boat) ->{
Text textToAdd = new Text(boat.getPosition() + ". " +
boat.getShortName() + " ");
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
textToAdd.setStyle("");
positionVbox.getChildren().add(textToAdd);
});
}
// participants.forEach((id, boat) ->{
// 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 void updateLaylines(BoatObject 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(MarkObject 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){
@@ -495,17 +502,27 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
* 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) {
Boat thisBoat = (Boat) newValue;
setSelectedBoat(thisBoat);
boatSelectionComboBox.setItems(
FXCollections.observableArrayList(participants.values())
);
//Null check is if the listener is fired but nothing selected
boatSelectionComboBox.valueProperty().addListener((obs, lastSelection, selectedBoat) -> {
if (selectedBoat != null) {
gameView.selectBoat(selectedBoat);
}
});
}
/**
* 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<Boat> observableBoats = FXCollections.observableArrayList();
observableBoats.addAll(participants.values());
boatSelectionComboBox.setItems(observableBoats);
}
/**
* Display the list of boats in the order they finished the race
@@ -525,38 +542,12 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
}
}
/**
* 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;
private String getMillisToFormattedTime(long milliseconds) {
return String.format("%02d:%02d:%02d",
TimeUnit.MILLISECONDS.toHours(milliseconds),
TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60, //Modulus 60 minutes per hour
TimeUnit.MILLISECONDS.toSeconds(milliseconds) % 60 //Modulus 60 seconds per minute
);
}
private void setAnnotations(Integer annotationLevel) {
@@ -594,38 +585,21 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
* @param boat The yacht for which we want to view all annotations
*/
private void setSelectedBoat(Boat boat) {
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(boat.getHullID())) {
updateLaylines(bg);
bg.setIsSelected(true);
selectedBoat = boat;
} 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);
// for (BoatObject 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(boat.getHullID())) {
//// updateLaylines(bg);
// bg.setIsSelected(true);
//// selectedBoat = boat;
// } else {
// bg.setIsSelected(false);
// }
// }
}
public void updateRaceData (RaceXMLData raceData) {
this.raceData = raceData;
this.courseData = raceData;
gameView.updateBorder(raceData.getCourseLimit());
}
@@ -61,7 +61,6 @@ public class StartScreenController {
ClientToServerThread clientToServerThread = new ClientToServerThread("localhost", 4950);
// controller.setClientToServerThread(clientToServerThread);
clientToServerThread.start();
// new GameServerThread("Fuck you");
// get the lobby controller so that we can pass the game server thread to it
setContentPane("/views/LobbyView.fxml");
@@ -1,20 +1,18 @@
package seng302.visualiser.controllers.client;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.io.IOException;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
import javafx.fxml.FXMLLoader;
import javafx.scene.layout.Pane;
import seng302.model.Boat;
import seng302.model.RaceState;
import seng302.model.mark.Mark;
import seng302.model.stream.parsers.PositionUpdateData.DeviceType;
import seng302.model.stream.parsers.MarkRoundingData;
import seng302.model.stream.parsers.RaceStartData;
import seng302.model.stream.parsers.RaceStatusData;
import seng302.model.stream.parsers.xml.RaceXMLData;
import seng302.model.stream.parsers.StreamParser;
@@ -22,7 +20,6 @@ import seng302.model.stream.parsers.xml.RegattaXMLData;
import seng302.model.stream.parsers.xml.XMLParser;
import seng302.model.stream.parsers.PositionUpdateData;
import seng302.model.stream.packets.StreamPacket;
import seng302.visualiser.ClientSocketListener;
import seng302.visualiser.ClientToServerThread;
import seng302.visualiser.controllers.RaceViewController;
@@ -31,35 +28,37 @@ import seng302.visualiser.controllers.RaceViewController;
*/
public class ClientController {
private final DateFormat DATE_TIME_FORMAT = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
private Pane holderPane;
private ClientToServerThread socketThread;
private ClientSocketListener socketListener;
private RaceViewController raceView;
private Map<Integer, Boat> allBoatsMap;
private Map<Integer, Boat> racingBoats = new HashMap<>();
private RegattaXMLData regattaData;
private RaceXMLData raceData;
private RaceXMLData courseData;
private RaceState raceState = new RaceState();
public ClientController (String ipAddress, Pane holder) {
this.holderPane = holder;
socketThread = new ClientToServerThread(ipAddress, 4950);
socketThread.start();
socketListener = this::parsePacket;
socketThread.addStreamObserver(socketListener);
socketThread.addStreamObserver(this::parsePacket);
}
private void loadRaceView () {
allBoatsMap.forEach((id, boat) -> {
if (raceData.getParticipants().contains(id))
if (courseData.getParticipants().contains(id))
racingBoats.put(id, boat);
});
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("RaceView.fxml"));
raceView = fxmlLoader.getController();
raceView.loadRace(racingBoats, raceData);
try {
holderPane.getChildren().add(fxmlLoader.load());
} catch (IOException e) {
e.printStackTrace();
}
raceView.loadRace(racingBoats, courseData, raceState);
}
private void parsePacket(StreamPacket packet) {
@@ -72,49 +71,47 @@ public class ClientController {
regattaData = XMLParser.parseRegatta(
StreamParser.extractXmlMessage(packet)
);
DATE_TIME_FORMAT.setTimeZone(
raceState.setTimeZone(
TimeZone.getTimeZone(
ZoneId.ofOffset("UTC", ZoneOffset.ofHours(regattaData.getUtcOffset()))
)
);
startRaceIfAllDataRecieved();
startRaceIfAllDataReceived();
break;
case RACE_XML:
raceData = XMLParser.parseRace(
courseData = XMLParser.parseRace(
StreamParser.extractXmlMessage(packet)
);
if (raceView != null) {
raceView.updateRaceData(raceData);
raceView.updateRaceData(courseData);
}
startRaceIfAllDataRecieved();
startRaceIfAllDataReceived();
break;
case BOAT_XML:
allBoatsMap = XMLParser.parseBoats(
StreamParser.extractXmlMessage(packet)
);
startRaceIfAllDataRecieved();
startRaceIfAllDataReceived();
break;
case RACE_START_STATUS:
RaceStartData raceStartData = StreamParser.extractRaceStartStatus(packet);
raceState.updateState(StreamParser.extractRaceStartStatus(packet));
break;
case BOAT_LOCATION:
PositionUpdateData positionData = StreamParser.extractBoatLocation(packet);
updatePosition(positionData);
updatePosition(StreamParser.extractBoatLocation(packet));
break;
case MARK_ROUNDING:
MarkRoundingData roundingData = StreamParser.extractMarkRounding(packet);
updateMarkRounding(roundingData);
updateMarkRounding(StreamParser.extractMarkRounding(packet));
break;
}
}
private void startRaceIfAllDataRecieved () {
if (raceData != null && allBoatsMap != null && regattaData != null)
private void startRaceIfAllDataReceived() {
if (courseData != null && allBoatsMap != null && regattaData != null)
loadRaceView();
}
@@ -129,8 +126,8 @@ public class ClientController {
boat.setLat(positionData.getLat());
boat.setLon(positionData.getLon());
boat.setHeading(positionData.getHeading());
} else {
Mark mark = raceData.getCompoundMarks().get(positionData.getDeviceId());
} else if (positionData.getType() == DeviceType.MARK_TYPE) {
Mark mark = courseData.getCompoundMarks().get(positionData.getDeviceId());
}
}
@@ -142,21 +139,19 @@ public class ClientController {
private void updateMarkRounding(MarkRoundingData roundingData) {
Boat boat = racingBoats.get(roundingData.getBoatId());
boat.setMarkRoundingTime(roundingData.getTimeStamp());
boat.setTimeSinceLastMark(raceState.getRaceTime() - roundingData.getTimeStamp());
boat.setLastMarkRounded(
raceData.getCompoundMarks().get(
courseData.getCompoundMarks().get(
roundingData.getMarkId()
)
);
}
private void processRaceStatusUpdate (RaceStatusData data) {
String raceTimeStr = DATE_TIME_FORMAT.format(data.getCurrentTime());
Date date = new Date();
date.getTime();
long timeTillStart = (data.getExpectedStartTime() - data.getCurrentTime()) / 1000;
raceState.updateState(data);
for (long[] boatData : data.getBoatData()) {
Boat boat = allBoatsMap.get((int) boatData[0]);
boat.setEstimateTimeAtNextMark(boatData[1]);
boat.setEstimateTimeTillNextMark(raceState.getRaceTime() - boatData[1]);
boat.setEstimateTimeAtFinish(boatData[2]);
int legNumber = (int) boatData[3];
boat.setLegNumber(legNumber);
@@ -172,6 +167,10 @@ public class ClientController {
}
}
private void close () {
socketThread.closeSocket();
}
// /** Handle the key-pressed event from the text field. */
// public void keyPressed(KeyEvent e) {
// BoatActionMessage boatActionMessage;
@@ -201,7 +200,7 @@ public class ClientController {
// 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)
@@ -0,0 +1,185 @@
package seng302.visualiser.fxObjects;
import java.util.HashMap;
import java.util.Map;
import javafx.beans.value.ObservableValue;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
/**
* Grouping of string objects over a semi transparent background.
*/
public class AnnotationBox extends Group{
@FunctionalInterface
public interface AnnotationFormatter<T> {
String transformString (T input);
}
/**
* Class stores a text object and relationship for updating the text object if needed
*
* @param <T> The type of observable value passed to the annotation, if there is one.
*/
public class Annotation<T> {
private Text text;
private ObservableValue<T> source;
private AnnotationFormatter<T> format;
/**
* Constructor for observing annotation
* @param textObject the javaFX text object the annotation is displayed in
* @param source observable value that the annotation is taken from
* @param formatter interface describing how to format the source data if needed
*/
public Annotation (Text textObject, ObservableValue<T> source, AnnotationFormatter<T> formatter) {
this.text = textObject;
this.source = source;
this.format = formatter;
source.addListener((obs, oldVal, newVal) ->
text.setText(format.transformString(newVal))
);
}
/**
* Constructor for a static annotation
* @param textObject the javaFX text object the annotation is displayed in
* @param annotationText the static value of the test object
*/
public Annotation (Text textObject, String annotationText) {
textObject.setText(annotationText);
text = textObject;
}
private Text getText () {
return text;
}
}
//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_ARC_SIZE = 10;
private int visibleAnnotations = 0;
private double backgroundWidth = 125d;
private Rectangle background = new Rectangle();
private Paint theme = Color.BLACK;
private Map<String, Annotation> annotationsByName = new HashMap<>();
public AnnotationBox() {
this.setCache(true);
background.setX(BACKGROUND_X);
background.setY(BACKGROUND_Y);
background.setWidth(backgroundWidth);
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);
this.getChildren().add(background);
}
public void addAnnotation (String annotationName, Annotation annotation) {
annotationsByName.put(annotationName, annotation);
this.getChildren().add(annotation.getText());
visibleAnnotations++;
}
public void addAnnotation (String annotationName, String annotationText) {
Text text = getTextObject();
annotationsByName.put(annotationName, new Annotation(text, annotationText));
}
public <E> void addAnnotation (String annotationName, ObservableValue<E> observable) {
addAnnotation(annotationName, observable, E::toString);
}
public <E> void addAnnotation (String annotationName, ObservableValue<E> observable,
AnnotationFormatter<E> formatter) {
Text newText = getTextObject();
annotationsByName.put(annotationName, new Annotation<>(newText, observable, formatter));
this.getChildren().add(newText);
visibleAnnotations++;
}
public void setAnnotationVisibility (String annotationName, boolean visibility) {
Text textField = annotationsByName.get(annotationName).text;
boolean currentState = textField.visibleProperty().get();
if (visibility != currentState) {
if (visibility)
visibleAnnotations++;
else
visibleAnnotations--;
}
textField.setVisible(visibility);
update();
}
public void removeAnnotation (String annotationName) {
this.getChildren().remove(annotationsByName.remove(annotationName).getText());
annotationsByName.remove(annotationName);
visibleAnnotations--;
update();
}
public void bindLocation () {
}
public void setLocation (double x, double y) {
this.setLayoutX(x);
this.setLayoutY(y);
}
public void setWidth (double width) {
backgroundWidth = width;
background.setWidth(backgroundWidth);
}
private void update () {
background.setVisible(visibleAnnotations != 0);
background.setHeight(Math.abs(BACKGROUND_X) + TEXT_BUFFER + BACKGROUND_H_PER_TEXT * visibleAnnotations);
for (int i = 1; i <= visibleAnnotations; i++) {
Text text = (Text) this.getChildren().get(i);
if (text.visibleProperty().get())
text.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT * Y_OFFSET_PER_TEXT * (i + 1));
}
}
/**
* Return a text object with caching and a color applied
*
* @return The text object
*/
private Text getTextObject() {
Text text = new Text();
text.setFill(theme);
text.setStrokeWidth(2);
text.setCacheHint(CacheHint.SPEED);
text.setCache(true);
return text;
}
public void setFill (Paint value) {
theme = value;
background.setStroke(theme);
annotationsByName.forEach((name, annotation) -> annotation.getText().setFill(theme));
}
}
@@ -1,132 +0,0 @@
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.Boat;
import seng302.model.stream.parsers.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 Boat boat;
BoatAnnotations (Boat 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("%.2f m/s", boat.getVelocity()));
DateFormat format = new SimpleDateFormat("mm:ss");
if (boat.getTimeTillNext() != null) {
String timeToNextMark = format
.format(boat.getTimeTillNext() - StreamParser.getCurrentTimeLong());
estTimeToNextMarkObject.setText("Next mark: " + timeToNextMark);
} else {
estTimeToNextMarkObject.setText("Next mark: -");
}
if (boat.getMarkRoundTime() != null) {
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;
}
}
@@ -6,10 +6,10 @@ import javafx.geometry.Point2D;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Line;
import javafx.scene.shape.Polygon;
import javafx.scene.transform.Rotate;
import seng302.visualiser.controllers.GameViewController;
import seng302.model.Boat;
import seng302.utilities.GeoUtility;
import seng302.model.mark.GateMark;
@@ -25,7 +25,7 @@ import seng302.model.stream.parsers.StreamParser;
* minimized in which case it attempts to store animations and apply them when the window is
* maximised.
*/
public class BoatGroup extends Group {
public class BoatObject extends Group {
//Constants for drawing
private static final double BOAT_HEIGHT = 15d;
@@ -47,79 +47,57 @@ public class BoatGroup extends Group {
private Double distanceTravelled = 0.0;
private Point2D lastPoint;
private boolean destinationSet;
private BoatAnnotations boatAnnotations;
private AnnotationBox annotationBox;
private Paint colour = Color.BLACK;
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(Boat boat, Color color) {
destinationSet = false;
this.boat = boat;
initChildren(color);
public BoatObject() {
this(-BOAT_WIDTH / 2, BOAT_HEIGHT / 2,
0.0, -BOAT_HEIGHT / 2,
BOAT_WIDTH / 2, BOAT_HEIGHT / 2);
}
/**
* 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(Boat 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) {
public BoatObject(double... points) {
boatPoly = new Polygon(points);
boatPoly.setFill(color);
boatPoly.setFill(colour);
boatPoly.setOnMouseEntered(event -> {
boatPoly.setFill(Color.FLORALWHITE);
boatPoly.setStroke(Color.RED);
});
boatPoly.setOnMouseExited(event -> {
boatPoly.setFill(color);
boatPoly.setFill(colour);
boatPoly.setStroke(Color.BLACK);
});
boatPoly.setOnMouseClicked(event -> setIsSelected(!isSelected));
boatPoly.setCache(true);
boatPoly.setCacheHint(CacheHint.SPEED);
boatAnnotations = new BoatAnnotations(boat, color);
annotationBox = new AnnotationBox();
annotationBox.setFill(colour);
leftLayLine = new Line();
rightLayline = new Line();
wake = new Wake(0, -BOAT_HEIGHT);
super.getChildren().addAll(boatPoly, boatAnnotations);
super.getChildren().addAll(boatPoly, annotationBox);
}
/**
* 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);
public void setFill (Paint value) {
this.colour = value;
boatPoly.setFill(colour);
annotationBox.setFill(colour);
}
/**
@@ -132,8 +110,8 @@ public class BoatGroup extends Group {
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);
annotationBox.setLayoutX(annotationBox.getLayoutX() + dx);
annotationBox.setLayoutY(annotationBox.getLayoutY() + dy);
wake.setLayoutX(wake.getLayoutX() + dx);
wake.setLayoutY(wake.getLayoutY() + dy);
}
@@ -149,8 +127,8 @@ public class BoatGroup extends Group {
rotateTo(rotation);
boatPoly.setLayoutX(x);
boatPoly.setLayoutY(y);
boatAnnotations.setLayoutX(x);
boatAnnotations.setLayoutY(y);
annotationBox.setLayoutX(x);
annotationBox.setLayoutY(y);
wake.setLayoutX(x);
wake.setLayoutY(y);
wake.rotate(rotation);
@@ -225,54 +203,52 @@ public class BoatGroup extends Group {
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.
*/
return boatLineFuncResult.equals(windLineFuncResult);
}
// /**
// * 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.
// */
// return boatLineFuncResult.equals(windLineFuncResult);
// return true;
// }
public void setIsSelected(Boolean isSelected) {
this.isSelected = isSelected;
setLineGroupVisible(isSelected);
setWakeVisible(isSelected);
boatAnnotations.setVisible(isSelected);
annotationBox.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);
}
@@ -324,7 +300,7 @@ public class BoatGroup extends Group {
}
public Group getAnnotations() {
return boatAnnotations;
return annotationBox;
}
public Double getBoatLayoutX() {
@@ -16,7 +16,7 @@ import seng302.model.mark.SingleMark;
/**
* Grouping of javaFX objects needed to represent a Mark on screen.
*/
public class MarkGroup extends Group {
public class MarkObject extends Group {
private static int MARK_RADIUS = 5;
private static int LINE_THICKNESS = 2;
@@ -31,7 +31,7 @@ public class MarkGroup extends Group {
* @param mark
* @param points
*/
public MarkGroup (SingleMark mark, Point2D points) {
public MarkObject(SingleMark mark, Point2D points) {
marks.add(mark);
mainMark = mark;
Color color = Color.BLACK;
@@ -73,7 +73,7 @@ public class MarkGroup extends Group {
super.getChildren().removeAll(toRemove);
}
public MarkGroup(GateMark mark, Point2D points1, Point2D points2) {
public MarkObject(GateMark mark, Point2D points1, Point2D points2) {
marks.add(mark.getSingleMark1());
marks.add(mark.getSingleMark2());
mainMark = mark;