Copied remaining files into appropriate package. These need to be refactored and put into the shared package.

This commit is contained in:
fjc40
2017-07-04 16:22:25 +12:00
parent bbbb1f2eb0
commit d0d63ca236
36 changed files with 4571 additions and 0 deletions
@@ -0,0 +1,284 @@
package visualiser.model;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Toggle;
import javafx.scene.layout.AnchorPane;
import java.util.HashMap;
import java.util.Map;
/**
* Class that processes user selected annotation visibility options to
* display the requested information on the
* {@link seng302.Model.ResizableRaceMap ResizbleRaceMap}. These are displayed
* via the {@link seng302.Controllers.RaceController RaceController}. <br>
* Annotation options for a {@link seng302.Model.Boat Boat} include: its name,
* abbreviation, speed, the time since it passed the last
* {@link seng302.Model.Marker Marker}, estimated time to the next marker,
* and a path it has travelled made up of
* {@link seng302.Model.TrackPoint TrackPoint}s.
*/
public class Annotations {
private ResizableRaceCanvas raceMap;
// checkable objects in the anchor pane
private Map<String, CheckBox> checkBoxes = new HashMap<>();
private Map<String, Toggle> annoToggles = new HashMap<>();
// maps of selected and saved annotations
private Map<String, Boolean> importantAnno = new HashMap<>();
private Map<String, Boolean> annoShownBeforeHide = new HashMap<>();
// string values match the fx:id value of check boxes
private static String nameCheckAnno = "showName";
private static String abbrevCheckAnno = "showAbbrev";
private static String speedCheckAnno = "showSpeed";
private static String pathCheckAnno = "showBoatPath";
private static String timeCheckAnno = "showTime";
private static String estTimeCheckAnno = "showEstTime";
// string values match the fx:id value of radio buttons
private static String noBtn = "noBtn";
private static String hideBtn = "hideAnnoRBtn";
private static String showBtn = "showAnnoRBtn";
private static String partialBtn = "partialAnnoRBtn";
private static String importantBtn = "importantAnnoRBtn";
private Boolean selectShow = false;
private String buttonChecked;
private String prevBtnChecked;
@FXML Button saveAnnoBtn;
/**
* Constructor to set up and display initial annotations
* @param annotationPane javaFX pane containing annotation options
* @param raceMap the canvas to update annotation displays
*/
public Annotations(AnchorPane annotationPane, ResizableRaceCanvas raceMap){
this.raceMap = raceMap;
for (Node child : annotationPane.getChildren()) {
// collect all check boxes into a map
if (child.getClass()==CheckBox.class){
checkBoxes.put(child.getId(), (CheckBox)child);
}
// collect annotation toggle radio buttons into a map
else if (child.getClass()== RadioButton.class){
//annotationGroup.getToggles().add((RadioButton)child);
annoToggles.put(child.getId(), (RadioButton)child);
}
else if (child.getClass() == Button.class){
saveAnnoBtn = (Button)child;
}
}
initializeAnnotations();
}
/**
* Set up initial boat annotations and shows all data.
* Defines partial annotations.
* Creates listeners for when the user selects a different annotation
* visibility.
*/
public void initializeAnnotations() {
for (Map.Entry<String, CheckBox> checkBox : checkBoxes.entrySet())
{ annoShownBeforeHide.put(checkBox.getKey(), true); }
addCheckBoxListeners();
addSaveAnnoListener();
addAnnoToggleListeners();
annoToggles.get(showBtn).setSelected(true);
}
/**
* Creates listeners for each checkbox so the annotation display is
* updated when a user selects a different level of annotation visibility.
*/
private void addCheckBoxListeners(){
//listener for show name in annotation
checkBoxes.get(nameCheckAnno).selectedProperty()
.addListener((ov, old_val, new_val) -> {
if (old_val != new_val) {
raceMap.toggleAnnoName();
storeCurrentAnnotationState(nameCheckAnno, new_val);
raceMap.update();
}
});
//listener for show abbreviation for annotation
checkBoxes.get(abbrevCheckAnno).selectedProperty()
.addListener((ov, old_val, new_val) -> {
if (old_val != new_val) {
raceMap.toggleAnnoAbbrev();
storeCurrentAnnotationState(abbrevCheckAnno, new_val);
raceMap.update();
}
});
//listener for show boat path for annotation
checkBoxes.get(pathCheckAnno).selectedProperty()
.addListener((ov, old_val, new_val) -> {
if (old_val != new_val) {
raceMap.toggleBoatPath();
storeCurrentAnnotationState(pathCheckAnno, new_val);
raceMap.update();
}
});
//listener to show speed for annotation
checkBoxes.get(speedCheckAnno).selectedProperty()
.addListener((ov, old_val, new_val) -> {
if (old_val != new_val) {
raceMap.toggleAnnoSpeed();
storeCurrentAnnotationState(speedCheckAnno, new_val);
raceMap.update();
}
});
//listener to show time for annotation
checkBoxes.get(timeCheckAnno).selectedProperty()
.addListener((ov, old_val, new_val) -> {
if (old_val != new_val) {
raceMap.toggleAnnoTime();
storeCurrentAnnotationState(timeCheckAnno, new_val);
raceMap.update();
}
});
//listener to show estimated time for annotation
checkBoxes.get(estTimeCheckAnno).selectedProperty()
.addListener((ov, old_val, new_val) -> {
if (old_val != new_val) {
raceMap.toggleAnnoEstTime();
storeCurrentAnnotationState(estTimeCheckAnno, new_val);
raceMap.update();
}
});
}
/**
* Creates a listener so the system knows when to save a users currently
* selected annotation options as important for future use.
*/
private void addSaveAnnoListener(){
//listener to save currently selected annotations as important
saveAnnoBtn.setOnAction(event -> {
importantAnno.clear();
for (Map.Entry<String, CheckBox> checkBox : checkBoxes.entrySet()){
importantAnno.put(checkBox.getKey(),
checkBox.getValue().isSelected());
}
});
}
/**
* Creates listeners for each visibility option so that the annotation
* display is updated when a user selects a different level of annotation
* visibility.
*/
private void addAnnoToggleListeners(){
//listener for hiding all annotations
RadioButton hideAnnoRBtn = (RadioButton)annoToggles.get(hideBtn);
hideAnnoRBtn.setOnAction((e)->{
buttonChecked = hideBtn;
selectShow = false;
for (Map.Entry<String, CheckBox> checkBox : checkBoxes.entrySet()){
checkBox.getValue().setSelected(false);
}
raceMap.update();
buttonChecked = noBtn;
prevBtnChecked = hideBtn;
selectShow = true;
});
//listener for showing previously visible annotations
RadioButton showAnnoRBTN = (RadioButton)annoToggles.get(showBtn);
showAnnoRBTN.setOnAction((e)->{
if (selectShow) {
buttonChecked = showBtn;
for (Map.Entry<String, CheckBox> checkBox : checkBoxes.entrySet()){
checkBox.getValue().setSelected(
annoShownBeforeHide.get(checkBox.getKey()));
}
raceMap.update();
buttonChecked = noBtn;
prevBtnChecked = showBtn;
}
selectShow = true;
});
//listener for showing a predetermined subset of annotations
RadioButton partialAnnoRBTN = (RadioButton)annoToggles.get(partialBtn);
partialAnnoRBTN.setOnAction((e)->{
selectShow = false;
buttonChecked = partialBtn;
for (Map.Entry<String, CheckBox> checkBox : checkBoxes.entrySet()){
// the checkbox defaults for partial annotations
if (checkBox.getKey().equals(abbrevCheckAnno)
|| checkBox.getKey().equals(speedCheckAnno)){
checkBox.getValue().setSelected(true);
}
else { checkBox.getValue().setSelected(false); }
}
raceMap.update();
buttonChecked = noBtn;
prevBtnChecked = partialBtn;
selectShow = true;
});
//listener for showing all saved important annotations
RadioButton importantAnnoRBTN = (RadioButton)annoToggles.get(importantBtn);
importantAnnoRBTN.setOnAction((e) ->{
selectShow = false;
buttonChecked = importantBtn;
if (importantAnno.size()>0){
for (Map.Entry<String, CheckBox> checkBox : checkBoxes.entrySet()){
checkBox.getValue().setSelected
(importantAnno.get(checkBox.getKey()));
}
}
buttonChecked = noBtn;
prevBtnChecked = importantBtn;
selectShow = true;
});
}
/**
* Updates the current state of an annotation so that when a user
* deselects the hidden visibility option, the previous annotations will
* be displayed again.
* @param dictionaryAnnotationKey annotation checkbox
* @param selected boolean value representing the state of the checkbox
*/
private void storeCurrentAnnotationState(String dictionaryAnnotationKey, boolean selected){
if (buttonChecked != hideBtn) {
//if we are checking the box straight out of hide instead of using the radio buttons
annoShownBeforeHide.put(dictionaryAnnotationKey, selected);
if (prevBtnChecked == hideBtn && buttonChecked == noBtn){
storeCurrentAnnotationDictionary();
}
if (buttonChecked == noBtn) {
selectShow = false;
annoToggles.get(showBtn).setSelected(true);
}
}
}
/**
* Stores all current annotation states so that when a user
* deselects the hidden visibility option, the previous annotations will
* be displayed again.
*/
private void storeCurrentAnnotationDictionary(){
for (Map.Entry<String, CheckBox> checkBox : checkBoxes.entrySet()){
annoShownBeforeHide.put(checkBox.getKey(),
checkBox.getValue().isSelected());
}
}
}
@@ -0,0 +1,42 @@
package visualiser.model;
/**
* It is a coordinate representing a location on the
* {@link seng302.Model.ResizableRaceMap ResizableRaceMap}.
* It has been converted from a {@link seng302.GPSCoordinate GPSCoordinate}
* to display objects in their relative positions.
*/
public class GraphCoordinate {
private final int x;
private final int y;
/**
* Constructor method.
*
* @param x X coordinate.
* @param y Y coordinate.
*/
public GraphCoordinate(int x, int y) {
this.x = x;
this.y = y;
}
/**
* Returns the X coordinate.
*
* @return x axis Coordinate.
*/
public int getX() {
return x;
}
/**
* Returns the Y coordinate.
*
* @return y axis Coordinate.
*/
public int getY() {
return y;
}
}
@@ -0,0 +1,133 @@
package visualiser.model;
import com.github.bfsmith.geotimezone.TimeZoneLookup;
import com.github.bfsmith.geotimezone.TimeZoneResult;
import javafx.animation.AnimationTimer;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import seng302.GPSCoordinate;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Date;
/**
* This class is used to implement a clock which keeps track of and
* displays times relevant to a race. This is displayed on the
* {@link seng302.Model.ResizableRaceCanvas ResizableRaceCanvas} via the
* {@link seng302.Controllers.RaceController RaceController} and the
* {@link seng302.Controllers.StartController StartController}.
*/
public class RaceClock implements Runnable {
private long lastTime;
private final ZoneId zoneId;
private ZonedDateTime time;
private ZonedDateTime startingTime;
private final StringProperty timeString;
private StringProperty duration;
public RaceClock(ZonedDateTime zonedDateTime) {
this.zoneId = zonedDateTime.getZone();
this.timeString = new SimpleStringProperty();
this.duration = new SimpleStringProperty();
this.time = zonedDateTime;
setTime(time);
}
public static ZonedDateTime getCurrentZonedDateTime(GPSCoordinate gpsCoordinate) {
TimeZoneLookup timeZoneLookup = new TimeZoneLookup();
TimeZoneResult timeZoneResult = timeZoneLookup.getTimeZone(gpsCoordinate.getLatitude(), gpsCoordinate.getLongitude());
ZoneId zone = ZoneId.of(timeZoneResult.getResult());
return LocalDateTime.now(zone).atZone(zone);
}
public void run() {
new AnimationTimer() {
@Override
public void handle(long now) {
updateTime();
}
}.start();
}
/**
* Sets time to arbitrary zoned time.
*
* @param time arbitrary time with timezone.
*/
private void setTime(ZonedDateTime time) {
this.time = time;
this.timeString.set(DateTimeFormatter.ofPattern("HH:mm:ss dd/MM/YYYY Z").format(time));
this.lastTime = System.currentTimeMillis();
if(startingTime != null) {
long seconds = Duration.between(startingTime.toLocalDateTime(), time.toLocalDateTime()).getSeconds();
if(seconds < 0)
duration.set(String.format("Starting in: %02d:%02d:%02d", -seconds/3600, -(seconds%3600)/60, -seconds%60));
else
duration.set(String.format("Time: %02d:%02d:%02d", seconds/3600, (seconds%3600)/60, seconds%60));
}
}
/**
* Sets time to given UTC time in seconds from Unix epoch, preserving timezone.
* @param time UTC time
*/
public void setUTCTime(long time) {
Date utcTime = new Date(time);
setTime(utcTime.toInstant().atZone(this.zoneId));
}
public ZonedDateTime getStartingTime() {
return startingTime;
}
public void setStartingTime(ZonedDateTime startingTime) {
this.startingTime = startingTime;
}
/**
* Get ZonedDateTime corresponding to local time zone and given UTC time.
* @param time time in mills
* @return local date time
*/
public ZonedDateTime getLocalTime(long time) {
Date utcTime = new Date(time);
return utcTime.toInstant().atZone(this.zoneId);
}
/**
* Updates time by duration elapsed since last update.
*/
private void updateTime() {
this.time = this.time.plus(Duration.of(System.currentTimeMillis() - this.lastTime, ChronoUnit.MILLIS));
this.lastTime = System.currentTimeMillis();
setTime(time);
}
public String getDuration() {
return duration.get();
}
public StringProperty durationProperty() {
return duration;
}
public String getTimeZone() {
return zoneId.toString();
}
public ZonedDateTime getTime() {
return time;
}
public StringProperty timeStringProperty() {
return timeString;
}
}
@@ -0,0 +1,57 @@
package visualiser.model;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
/**
* Created by cbt24 on 3/05/17.
*/
public class RaceConnection {
private final StringProperty hostname;
private final int port;
private final StringProperty status;
public RaceConnection(String hostname, int port) {
this.hostname = new SimpleStringProperty(hostname);
this.port = port;
this.status = new SimpleStringProperty("");
check();
}
/**
* Tries to create a socket to hostname and port, indicates status after test.
* @return true if socket can connect
*/
@SuppressWarnings("unused")
public boolean check() {
InetSocketAddress i = new InetSocketAddress(hostname.get(), port);
try (Socket s = new Socket()){
s.connect(i, 5000);
status.set("Ready");
return true;
} catch (IOException e) {}
status.set("Offline");
return false;
}
public String getHostname() {
return hostname.get();
}
public StringProperty hostnameProperty() {
return hostname;
}
public int getPort() {
return port;
}
public StringProperty statusProperty() {
return status;
}
}
@@ -0,0 +1,76 @@
package visualiser.model;
/**
* The base size of the map to be used for the
* {@link seng302.Model.ResizableRaceMap ResizableRaceMap} and
* {@link seng302.Model.ResizableRaceCanvas ResizableRaceCanvas}. It is used
* to convert {@link seng302.GPSCoordinate GPSCoordinate}s to relative
* {@link seng302.GraphCoordinate GraphCoordinate}s.
*/
public class RaceMap {
private final double x1;
private final double x2;
private final double y1;
private final double y2;
private int width, height;
/**
* Constructor Method.
*
* @param x1 Longitude of the top left point.
* @param y1 Latitude of the top left point.
* @param x2 Longitude of the top right point.
* @param y2 Latitude of the top right point.
* @param width width that the Canvas the race is to be drawn on is.
* @param height height that the Canvas the race is to be drawn on is.
*/
public RaceMap(double y1, double x1, double y2, double x2, int height, int width) {
this.x1 = x1;
this.x2 = x2;
this.y1 = y1;
this.y2 = y2;
this.width = width;
this.height = height;
}
/**
* Converts GPS coordinates to coordinates for container
*
* @param lat GPS latitude
* @param lon GPS longitude
* @return GraphCoordinate (pair of doubles)
* @see GraphCoordinate
*/
private GraphCoordinate convertGPS(double lat, double lon) {
int difference = Math.abs(width - height);
int size = width;
if (width > height) {
size = height;
return new GraphCoordinate((int) ((size * (lon - x1) / (x2 - x1)) + difference / 2), (int) (size - (size * (lat - y1) / (y2 - y1))));
} else {
return new GraphCoordinate((int) (size * (lon - x1) / (x2 - x1)), (int) ((size - (size * (lat - y1) / (y2 - y1))) + difference / 2));
}
//return new GraphCoordinate((int) (width * (lon - x1) / (x2 - x1)), (int) (height - (height * (lat - y1) / (y2 - y1))));
}
/**
* Converts the GPS Coordinate to GraphCoordinates
*
* @param coordinate GPSCoordinate representation of Latitude and Longitude.
* @return GraphCoordinate that the GPS is coordinates are to be displayed on the map.
* @see GraphCoordinate
* @see GPSCoordinate
*/
public GraphCoordinate convertGPS(GPSCoordinate coordinate) {
return convertGPS(coordinate.getLatitude(), coordinate.getLongitude());
}
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
}
@@ -0,0 +1,55 @@
package visualiser.model;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
/**
* The abstract class for the resizable race canvases.
*/
public abstract class ResizableCanvas extends Canvas {
protected final GraphicsContext gc;
public ResizableCanvas(){
this.gc = this.getGraphicsContext2D();
// Redraw canvas when size changes.
widthProperty().addListener(evt -> draw());
heightProperty().addListener(evt -> draw());
}
abstract void draw();
/**
* Set the Canvas to resizable.
*
* @return That the Canvas is resizable.
*/
@Override
public boolean isResizable() {
return true;
}
/**
* Returns the preferred width of the Canvas
*
* @param width of canvas
* @return Returns the width of the Canvas
*/
@Override
public double prefWidth(double width) {
return getWidth();
}
/**
* Returns the preferred height of the Canvas
*
* @param height of canvas
* @return Returns the height of the Canvas
*/
@Override
public double prefHeight(double height) {
return getHeight();
}
}
@@ -0,0 +1,392 @@
package visualiser.model;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.transform.Rotate;
import seng302.Mock.StreamedCourse;
import seng302.RaceDataSource;
import seng302.RaceMap;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.*;
/**
* This JavaFX Canvas is used to update and display details for a
* {@link seng302.RaceMap RaceMap} via the
* {@link seng302.Controllers.RaceController RaceController}.<br>
* It fills it's parent and cannot be downsized. <br>
* Details displayed include:
* {@link seng302.Model.Boat Boats} (and their
* {@link seng302.Model.TrackPoint TrackPoint}s),
* {@link seng302.Model.Marker Markers}, a
* {@link seng302.Model.RaceClock RaceClock}, a wind direction arrow and
* various user selected {@link seng302.Model.Annotations Annotations}.
*/
public class ResizableRaceCanvas extends ResizableCanvas {
private RaceMap map;
private List<Boat> boats;
private List<Marker> boatMarkers;
private boolean annoName = true;
private boolean annoAbbrev = true;
private boolean annoSpeed = true;
private boolean annoPath = true;
private boolean annoEstTime = true;
private boolean annoTimeSinceLastMark = true;
private List<Color> colours;
private final List<Marker> markers;
private final RaceDataSource raceData;
private Map<Integer, Color> boatColours = new HashMap<>();
private Node arrow;
private RaceClock raceClock;
public ResizableRaceCanvas(RaceDataSource raceData) {
super();
double lat1 = raceData.getMapTopLeft().getLatitude();
double long1 = raceData.getMapTopLeft().getLongitude();
double lat2 = raceData.getMapBottomRight().getLatitude();
double long2 = raceData.getMapBottomRight().getLongitude();
setMap(new RaceMap(lat1, long1, lat2, long2, (int) getWidth(), (int) getHeight()));
this.markers = raceData.getMarkers();
makeColours();
this.raceData = raceData;
}
/**
* Sets the boats that are to be displayed in this race.
*
* @param boats in race
*/
public void setBoats(List<Boat> boats) {
this.boats = boats;
mapBoatColours();
}
/**
* Sets the boat markers that are to be displayed in this race.
*
* @param boatMarkers in race
*/
public void setBoatMarkers(ObservableList<Marker> boatMarkers) {
this.boatMarkers = boatMarkers;
}
/**
* Sets the RaceMap that the RaceCanvas is to be displaying for.
*
* @param map race map
*/
private void setMap(RaceMap map) {
this.map = map;
}
private void displayBoat(Boat boat, double angle, Color colour) {
if (boat.getCurrentPosition() != null) {
GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition());
double[] x = {pos.getX() - 6, pos.getX(), pos.getX() + 6};
double[] y = {pos.getY() + 12, pos.getY() - 12, pos.getY() + 12};
gc.setFill(colour);
gc.save();
rotate(angle, pos.getX(), pos.getY());
gc.fillPolygon(x, y, 3);
gc.restore();
}
}
/**
* Displays a line on the map with rectangles on the starting and ending point of the line.
*
* @param graphCoordinateA Starting Point of the line in GraphCoordinate.
* @param graphCoordinateB End Point of the line in GraphCoordinate.
* @param paint Colour the line is to coloured.
* @see GraphCoordinate
* @see Color
* @see Paint
*/
private void displayLine(GraphCoordinate graphCoordinateA, GraphCoordinate graphCoordinateB, Paint paint) {
gc.setStroke(paint);
gc.setFill(paint);
gc.fillOval(graphCoordinateA.getX() - 3, graphCoordinateA.getY() - 3, 6, 6);
gc.fillOval(graphCoordinateB.getX() - 3, graphCoordinateB.getY() - 3, 6, 6);
gc.strokeLine(graphCoordinateA.getX(), graphCoordinateA.getY(), graphCoordinateB.getX(), graphCoordinateB.getY());
}
/**
* Display a point on the Canvas
*
* @param graphCoordinate Coordinate that the point is to be displayed at.
* @param paint Colour that the boat is to be coloured.
* @see GraphCoordinate
* @see Paint
* @see Color
*/
private void displayPoint(GraphCoordinate graphCoordinate, Paint paint) {
gc.setFill(paint);
gc.fillOval(graphCoordinate.getX(), graphCoordinate.getY(), 10, 10);
}
/**
* Displays an arrow representing wind direction on the Canvas
*
* @param angle Angle that the arrow is to be facing in degrees 0 degrees = North (Up).
* @see GraphCoordinate
*/
private void displayWindArrow(double angle) {
angle = angle % 360;
// show direction wind is coming from
if (angle<180){angle = angle + 180;}
else {angle = angle - 180;}
if (arrow != null && arrow.getRotate() != angle) {
arrow.setRotate(angle);
}
}
/**
* Rotates things on the canvas Note: this must be called in between gc.save() and gc.restore() else they will rotate everything
*
* @param angle Bearing angle to rotate at in degrees
* @param px Pivot point x of rotation.
* @param py Pivot point y of rotation.
*/
private void rotate(double angle, double px, double py) {
Rotate r = new Rotate(angle, px, py);
gc.setTransform(r.getMxx(), r.getMyx(), r.getMxy(), r.getMyy(), r.getTx(), r.getTy());
}
/**
* Display given name and speed of boat at a graph coordinate
*
* @param name name of the boat
* @param abbrev abbreviation of the boat name
* @param speed speed of the boat
* @param coordinate coordinate the text appears
* @param timeSinceLastMark time since the last mark was passed
*/
private void displayText(String name, String abbrev, double speed, GraphCoordinate coordinate, String estTime, ZonedDateTime timeSinceLastMark) {
String text = "";
//Check name toggle value
if (annoName){
text += String.format("%s ", name);
}
//Check abbreviation toggle value
if (annoAbbrev){
text += String.format("%s ", abbrev);
}
//Check speed toggle value
if (annoSpeed){
text += String.format("%.2fkn ", speed);
}
if (annoEstTime) {
text += estTime;
}
//Check time since last mark toggle value
if(annoTimeSinceLastMark){
Duration timeSince = Duration.between(timeSinceLastMark, raceClock.getTime());
text += String.format(" %ds ", timeSince.getSeconds());
}
//String text = String.format("%s, %2$.2fkn", name, speed);
long xCoord = coordinate.getX() + 20;
long yCoord = coordinate.getY();
if (xCoord + (text.length() * 7) >= getWidth()) {
xCoord -= text.length() * 7;
}
if (yCoord - (text.length() * 2) <= 0) {
yCoord += 30;
}
gc.fillText(text, xCoord, yCoord);
}
/**
* Draws race map with up to date data.
*/
public void update() {
this.draw();
this.updateBoats();
}
/**
* Draw race markers
*/
private void drawMarkers() {
for(Marker marker: markers) {
GraphCoordinate mark1 = this.map.convertGPS(marker.getMark1());
// removed drawing of lines between the marks as only
// the start and finish line should have a line drawn
if(marker.isCompoundMark()) {
GraphCoordinate mark2 = this.map.convertGPS(marker.getMark2());
displayPoint(mark1, Color.LIMEGREEN);
displayPoint(mark2, Color.LIMEGREEN);
} else {
displayPoint(mark1, Color.GREEN);
}
}
}
/**
* Draws the Race Map
*/
public void draw() {
double width = getWidth();
double height = getHeight();
gc.clearRect(0, 0, width, height);
if (map == null) {
return;//TODO this should return a exception in the future
}
this.map.setHeight((int) height);
this.map.setWidth((int) width);
gc.setLineWidth(2);
updateBoats();
drawMarkers();
//display wind direction arrow - specify origin point and angle - angle now set to random angle
if (raceData instanceof StreamedCourse) {
displayWindArrow(((StreamedCourse) raceData).getWindDirection());
} else {
displayWindArrow(150);
}
}
/**
* Toggle name display in annotation
*/
public void toggleAnnoName() {
annoName = !annoName;
}
/**
* Toggle boat path display in annotation
*/
public void toggleBoatPath() {
annoPath = !annoPath;
}
public void toggleAnnoEstTime() {
annoEstTime = !annoEstTime;
}
/**
* Toggle boat time display in annotation
*/
public void toggleAnnoTime() { annoTimeSinceLastMark = !annoTimeSinceLastMark;}
/**
* Toggle abbreviation display in annotation
*/
public void toggleAnnoAbbrev() {
annoAbbrev = !annoAbbrev;
}
/**
* Toggle speed display in annotation
*/
public void toggleAnnoSpeed() {
annoSpeed = !annoSpeed;
}
/**
* Draws boats while race in progress, when leg heading is set.
*/
private void updateBoats() {
if (boats != null) {
if (boatColours.size() < boats.size()) mapBoatColours();
for (Boat boat : boats) {
boolean finished = boat.getCurrentLeg().getName().equals("Finish") || boat.getCurrentLeg().getName().equals("DNF");
boolean isStart = boat.isStarted();
int sourceID = boat.getSourceID();
if (!finished && isStart) {
displayBoat(boat, boat.getHeading(), boatColours.get(sourceID));
GraphCoordinate wakeFrom = this.map.convertGPS(boat.getCurrentPosition());
GraphCoordinate wakeTo = this.map.convertGPS(boat.getWake());
displayLine(wakeFrom, wakeTo, boatColours.get(sourceID));
} else if (!isStart) {
displayBoat(boat, boat.getHeading(), boatColours.get(sourceID));
} else {
displayBoat(boat, 0, boatColours.get(sourceID));
}
if (Duration.between(boat.getTimeSinceLastMark(), raceClock.getTime()).getSeconds() < 0) {
boat.setTimeSinceLastMark(raceClock.getTime());
}
//If the race hasn't started, we set the time since last mark to the current time, to ensure we don't start counting until the race actually starts.
if (boat.isStarted() == false) {
boat.setTimeSinceLastMark(raceClock.getTime());
}
displayText(boat.toString(), boat.getAbbrev(), boat.getVelocity(), this.map.convertGPS(boat.getCurrentPosition()), boat.getFormattedEstTime(), boat.getTimeSinceLastMark());
//TODO this needs to be fixed.
drawTrack(boat, boatColours.get(sourceID));
}
}
}
/**
* Draws all track points for a given boat. Colour is set by boat, opacity by track point.
* @param boat whose track is displayed
* @param colour The color to use for the track.
* @see seng302.Model.TrackPoint
*/
private void drawTrack(Boat boat, Color colour) {
if (annoPath) {
for (TrackPoint point : boat.getTrack()) {
GraphCoordinate scaledCoordinate = this.map.convertGPS(point.getCoordinate());
gc.setFill(new Color(colour.getRed(), colour.getGreen(), colour.getBlue(), point.getAlpha()));
gc.fillOval(scaledCoordinate.getX(), scaledCoordinate.getY(), 5, 5);
}
}
}
/**
* makes colours
*/
private void makeColours() {
colours = new ArrayList<>(Arrays.asList(
Color.BLUEVIOLET,
Color.BLACK,
Color.RED,
Color.ORANGE,
Color.DARKOLIVEGREEN,
Color.LIMEGREEN,
Color.PURPLE,
Color.DARKGRAY,
Color.YELLOW
));
}
public void setArrow(Node arrow) {
this.arrow = arrow;
}
public void setRaceClock(RaceClock raceClock) {
this.raceClock = raceClock;
}
private void mapBoatColours() {
int currentColour = 0;
for (Boat boat : boats) {
if (!boatColours.containsKey(boat.getSourceID())) {
boatColours.put(boat.getSourceID(), colours.get(currentColour));
}
currentColour = (currentColour + 1) % colours.size();
}
}
}
@@ -0,0 +1,93 @@
package visualiser.model;
import javafx.scene.paint.Color;
import seng302.GPSCoordinate;
import seng302.RaceDataSource;
import seng302.RaceMap;
import java.util.List;
/**
* This JavaFX Canvas is used to generate the size of a
* {@link seng302.RaceMap RaceMap} using co-ordinates from a
* {@link seng302.RaceDataSource RaceDataSource}. This is done via the
* {@link seng302.Controllers.RaceController RaceController}.
*/
public class ResizableRaceMap extends ResizableCanvas {
private RaceMap map;
private final List<GPSCoordinate> raceBoundaries;
private double[] xpoints = {};
private double[] ypoints = {};
/**
* Constructor
* @param raceData Race which it is taking its information to be displayed from
*/
public ResizableRaceMap(RaceDataSource raceData){
super();
raceBoundaries = raceData.getBoundary();
double lat1 = raceData.getMapTopLeft().getLatitude();
double long1 = raceData.getMapTopLeft().getLongitude();
double lat2 = raceData.getMapBottomRight().getLatitude();
double long2 = raceData.getMapBottomRight().getLongitude();
setMap(new RaceMap(lat1, long1, lat2, long2, (int) getWidth(), (int) getHeight()));
//draw();
}
/**
* Sets the map race that it is supposed to be viewing.
* @param map the map to be set
*/
private void setMap(RaceMap map) {
this.map = map;
}
/**
* Draw boundary of the race.
*/
private void drawBoundaries() {
if (this.raceBoundaries == null) {
return;
}
gc.setFill(Color.AQUA);
setRaceBoundCoordinates();
gc.setLineWidth(1);
gc.fillPolygon(xpoints, ypoints, xpoints.length);
}
/**
* Sets the coordinately of the race boundaries
*/
private void setRaceBoundCoordinates() {
xpoints = new double[this.raceBoundaries.size()];
ypoints = new double[this.raceBoundaries.size()];
for (int i = 0; i < raceBoundaries.size(); i++) {
GraphCoordinate coord = map.convertGPS(raceBoundaries.get(i));
xpoints[i] = coord.getX();
ypoints[i] = coord.getY();
}
}
/**
* Draw update for the canvas
*/
public void draw(){
double width = getWidth();
double height = getHeight();
gc.clearRect(0, 0, width, height);
if (map == null) {
return;//TODO this should return a exception in the future
}
this.map.setHeight((int) height);
this.map.setWidth((int) width);
gc.setLineWidth(2);
drawBoundaries();
}
}
@@ -0,0 +1,180 @@
package visualiser.model;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.paint.Color;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* Class to process and modify a sparkline display. This display keeps visual
* track of {@link seng302.Model.Boat Boats}s in a race and their current
* placing position as they complete each {@link seng302.Model.Leg Leg} by
* passing a course {@link seng302.Model.Marker Marker}. <br>
* This sparkline is displayed using the
* {@link seng302.Controllers.RaceController RaceController}.
*/
public class Sparkline {
private ArrayList<String> colours;
private ArrayList<Boat> startBoats = new ArrayList<>();
private Map<Integer, String> boatColours = new HashMap<>();
private Integer legNum;
private Integer sparkLineNumber = 0;
@FXML LineChart<Number, Number> sparklineChart;
@FXML NumberAxis xAxis;
@FXML NumberAxis yAxis;
/**
* Constructor to set up initial sparkline (LineChart) object
* @param boats boats to display on the sparkline
* @param legNum total number of legs in the race
* @param sparklineChart javaFX LineChart for the sparkline
*/
public Sparkline(ObservableList<Boat> boats, Integer legNum,
LineChart<Number,Number> sparklineChart) {
this.sparklineChart = sparklineChart;
this.legNum = legNum;
this.yAxis = (NumberAxis)sparklineChart.getYAxis();
this.xAxis = (NumberAxis)sparklineChart.getXAxis();
startBoats.addAll(boats);
makeColours();
mapBoatColours();
createSparkline();
}
/**
* Creates and sets initial display for Sparkline for race positions.
* A data series for each boat in the race is added.
* Position numbers are displayed.
*/
public void createSparkline(){
// NOTE: Y axis is in negatives to display correct positions
// all boats start in 'last' place
for (int i=0; i<startBoats.size(); i++){
XYChart.Series<Number, Number> series = new XYChart.Series();
series.getData().add(new XYChart.Data(0, -startBoats.size()));
series.getData().add(new XYChart.Data(0, -startBoats.size()));
sparklineChart.getData().add(series);
sparklineChart.getData().get(i).getNode().setStyle("-fx-stroke: " +
""+boatColours.get(startBoats.get(i).getSourceID())+";");
}
sparklineChart.setCreateSymbols(false);
// set x axis details
xAxis.setAutoRanging(false);
xAxis.setTickMarkVisible(false);
xAxis.setTickLabelsVisible(false);
xAxis.setMinorTickVisible(false);
xAxis.setUpperBound((startBoats.size()+1)*legNum);
xAxis.setTickUnit((startBoats.size()+1)*legNum);
// set y axis details
yAxis.setLowerBound(-(startBoats.size()+1));
yAxis.setUpperBound(0);
yAxis.setAutoRanging(false);
yAxis.setLabel("Position in Race");
yAxis.setTickUnit(1);
yAxis.setTickMarkVisible(false);
yAxis.setMinorTickVisible(false);
// hide minus number from displaying on axis
yAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(yAxis) {
@Override
public String toString(Number value) {
if ((Double)value == 0.0
|| (Double)value < -startBoats.size()){
return "";
}
else {
return String.format("%7.0f", -value.doubleValue());
}
}
});
}
/**
* Updates the sparkline to display current boat positions.
* New points are plotted to represent each boat when required.
* @param boatsInRace current position of the boats in race
*/
public void updateSparkline(ObservableList<Boat> boatsInRace){
int placingVal = boatsInRace.size();
sparkLineNumber++;
for (int i = boatsInRace.size() - 1; i >= 0; i--){
for (int j = startBoats.size() - 1; j >= 0; j--){
if (boatsInRace.get(i)==startBoats.get(j)){
// when a boat is on its first leg
if (boatsInRace.get(i).getCurrentLeg().getLegNumber()==0){
// adjust boats latest point on X axis
sparklineChart.getData().get(j).getData().get(1)
.setXValue(sparkLineNumber);
}
// when a boat first enters its second leg
else if (boatsInRace.get(i).getCurrentLeg().getLegNumber
()==1 && sparklineChart.getData().get(j).getData
().size()==2){
// adjust boats position from start mark
sparklineChart.getData().get(j).getData().get(1)
.setYValue(-placingVal);
sparklineChart.getData().get(j).getData().get(1)
.setXValue(sparkLineNumber);
sparklineChart.getData().get(j).getData().add(new XYChart.Data<>
(sparkLineNumber, -placingVal));
}
// plot new point for boats current position
else {
sparklineChart.getData().get(j).getData().add
(new XYChart.Data<>(sparkLineNumber, -placingVal));
}
placingVal-=1;
}
}
}
}
private void makeColours() {
colours = new ArrayList<>(Arrays.asList(
colourToHex(Color.BLUEVIOLET),
colourToHex(Color.BLACK),
colourToHex(Color.RED),
colourToHex(Color.ORANGE),
colourToHex(Color.DARKOLIVEGREEN),
colourToHex(Color.LIMEGREEN),
colourToHex(Color.PURPLE),
colourToHex(Color.DARKGRAY),
colourToHex(Color.YELLOW)
));
}
private String colourToHex(Color color) {
return String.format( "#%02X%02X%02X",
(int)( color.getRed() * 255 ),
(int)( color.getGreen() * 255 ),
(int)( color.getBlue() * 255 ) );
}
private void mapBoatColours() {
int currentColour = 0;
for (Boat boat : startBoats) {
if (!boatColours.containsKey(boat.getSourceID())) {
boatColours.put(boat.getSourceID(), colours.get(currentColour));
}
currentColour = (currentColour + 1) % colours.size();
}
}
}
@@ -0,0 +1,106 @@
package visualiser.model;
import seng302.GPSCoordinate;
import seng302.Model.Boat;
import seng302.Model.Leg;
import seng302.Model.Marker;
import seng302.RaceDataSource;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Observable;
/**
* COurse that the is being received.
*/
public class StreamedCourse extends Observable implements RaceDataSource {
private StreamedCourseXMLReader streamedCourseXMLReader = null;
private BoatXMLReader boatXMLReader = null;
private RegattaXMLReader regattaXMLReader = null;
private double windDirection = 0;
public StreamedCourse() {}
/**
* Read and set the new XML that has been received.
* @param boatXMLReader new XMl of the boats.
*/
public void setBoatXMLReader(BoatXMLReader boatXMLReader) {
this.boatXMLReader = boatXMLReader;
if (streamedCourseXMLReader != null && boatXMLReader != null) {
this.boatXMLReader.setParticipants(streamedCourseXMLReader.getParticipants());
boatXMLReader.read();
}
setChanged();
notifyObservers();
}
public StreamedCourseXMLReader getStreamedCourseXMLReader() {
return streamedCourseXMLReader;
}
/**
* Read and sets the new Course that has been received
* @param streamedCourseXMLReader COurse XML that has been received
*/
public void setStreamedCourseXMLReader(StreamedCourseXMLReader streamedCourseXMLReader) {
this.streamedCourseXMLReader = streamedCourseXMLReader;
if (streamedCourseXMLReader != null && boatXMLReader != null) {
boatXMLReader.setParticipants(streamedCourseXMLReader.getParticipants());
boatXMLReader.read();
}
}
/**
* Reads and sets the new Regatta that has been received
* @param regattaXMLReader Regatta XMl that has been received.
*/
public void setRegattaXMLReader(RegattaXMLReader regattaXMLReader) {
this.regattaXMLReader = regattaXMLReader;
setChanged();
notifyObservers();
}
public void setWindDirection(double windDirection) {
this.windDirection = windDirection;
}
public double getWindDirection() {
return windDirection;
}
public boolean hasReadRegatta() { return regattaXMLReader != null; }
public boolean hasReadBoats() { return boatXMLReader != null; }
public boolean hasReadCourse() { return streamedCourseXMLReader != null; }
public String getRegattaName() { return regattaXMLReader.getRegattaName(); }
public List<Boat> getBoats() {
return boatXMLReader.getBoats();
}
public List<Leg> getLegs() {
return streamedCourseXMLReader.getLegs();
}
public List<Marker> getMarkers() { return streamedCourseXMLReader.getMarkers(); }
public List<GPSCoordinate> getBoundary() {
return streamedCourseXMLReader.getBoundary();
}
public ZonedDateTime getZonedDateTime() {
return streamedCourseXMLReader.getRaceStartTime();
}
public GPSCoordinate getMapTopLeft() {
return streamedCourseXMLReader.getMapTopLeft();
}
public GPSCoordinate getMapBottomRight() {
return streamedCourseXMLReader.getMapBottomRight();
}
}
@@ -0,0 +1,285 @@
package visualiser.model;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import seng302.Controllers.FinishController;
import seng302.Controllers.RaceController;
import seng302.GPSCoordinate;
import seng302.Model.Boat;
import seng302.Model.Leg;
import seng302.Model.Marker;
import seng302.Networking.Messages.BoatLocation;
import seng302.Networking.Messages.BoatStatus;
import seng302.Networking.Messages.Enums.BoatStatusEnum;
import seng302.VisualiserInput;
import java.util.List;
/**
* The Class used to view the race streamed.
*/
public class StreamedRace implements Runnable {
private final VisualiserInput visualiserInput;
private final ObservableList<Boat> startingBoats;
private final ObservableList<Marker> boatMarkers;
private final List<Leg> legs;
private RaceController controller;
protected FinishController finishController;
private int boatsFinished = 0;
private long totalTimeElapsed;
private int lastFPS = 20;
public StreamedRace(VisualiserInput visualiserInput, RaceController controller) {
StreamedCourse course = visualiserInput.getCourse();
this.startingBoats = FXCollections.observableArrayList(course.getBoats());
this.boatMarkers = FXCollections.observableArrayList(course.getMarkers());
this.legs = course.getLegs();
this.legs.add(new Leg("Finish", this.legs.size()));
this.controller = controller;
if (startingBoats != null && startingBoats.size() > 0) {
initialiseBoats();
}
this.visualiserInput = visualiserInput;
}
private void initialiseBoats() {
Leg officialStart = legs.get(0);
String name = officialStart.getName();
Marker endCompoundMark = officialStart.getEndMarker();
for (Boat boat : startingBoats) {
if (boat != null) {
Leg startLeg = new Leg(name, 0);
startLeg.setEndMarker(endCompoundMark);
boat.setCurrentLeg(startLeg, controller.getRaceClock());
boat.setTimeSinceLastMark(controller.getRaceClock().getTime());
}
}
}
/**
* Checks if the boat cannot finish the race
* @return True if boat cannot finish the race
*/
protected boolean doNotFinish() {
// DNF is no longer random and is now determined by a dnf packet
return false;
}
/**
* Checks the position of the boat.
*
* @param boat Boat that the position is to be updated for.
* @param timeElapsed Time that has elapse since the start of the the race.
*/
private void checkPosition(Boat boat, long timeElapsed) {
boolean legChanged = false;
StreamedCourse raceData = visualiserInput.getCourse();
BoatStatus boatStatusMessage = visualiserInput.getBoatStatusMap().get(boat.getSourceID());
if (boatStatusMessage != null) {
BoatStatusEnum boatStatusEnum = BoatStatusEnum.fromByte(boatStatusMessage.getBoatStatus());
int legNumber = boatStatusMessage.getLegNumber();
if (legNumber >= 1 && legNumber < legs.size()) {
if (boat.getCurrentLeg() != legs.get(legNumber)){
boat.setCurrentLeg(legs.get(legNumber), controller.getRaceClock());
legChanged = true;
}
}
if (boatStatusEnum == BoatStatusEnum.RACING) {
boat.addTrackPoint(boat.getCurrentPosition());
} else if (boatStatusEnum == BoatStatusEnum.DNF) {
boat.setDnf(true);
} else if (boatStatusEnum == BoatStatusEnum.FINISHED || legNumber == raceData.getLegs().size()) {
boatsFinished++;
boat.setTimeFinished(timeElapsed);
boat.setFinished(true);
}
}
if (legChanged) {
//Update the boat display table in the GUI to reflect the leg change
updatePositions();
controller.updateSparkline(startingBoats);
}
}
/**
* Updates the boat's gps coordinates
*
* @param boat to be updated
*/
private void updatePosition(Boat boat) {
int sourceID = boat.getSourceID();
BoatLocation boatLocation = visualiserInput.getBoatLocationMessage(sourceID);
BoatStatus boatStatus = visualiserInput.getBoatStatusMessage(sourceID);
if(boatLocation != null) {
double lat = boatLocation.getLatitudeDouble();
double lon = boatLocation.getLongitudeDouble();
boat.setCurrentPosition(new GPSCoordinate(lat, lon));
boat.setHeading(boatLocation.getHeadingDegrees());
boat.setEstTime(convertEstTime(boatStatus.getEstTimeAtNextMark(), boatLocation.getTime()));
double MMPS_TO_KN = 0.001944;
boat.setVelocity(boatLocation.getBoatSOG() * MMPS_TO_KN);
}
}
/**
* Updates the boat's gps coordinates
*
* @param mark to be updated
*/
private void updateMarker(Marker mark) {
int sourceID = mark.getSourceId1();
BoatLocation boatLocation1 = visualiserInput.getBoatLocationMessage(sourceID);
if(boatLocation1 != null) {
double lat = boatLocation1.getLatitudeDouble();
double lon = boatLocation1.getLongitudeDouble();
mark.setCurrentPosition1(new GPSCoordinate(lat, lon));
}
int sourceID2 = mark.getSourceId2();
BoatLocation boatLocation2 = visualiserInput.getBoatLocationMessage(sourceID2);
if(boatLocation2 != null) {
double lat = boatLocation2.getLatitudeDouble();
double lon = boatLocation2.getLongitudeDouble();
mark.setCurrentPosition2(new GPSCoordinate(lat, lon));
}
}
public void setController(RaceController controller) {
this.controller = controller;
}
/**
* Runnable for the thread.
*/
public void run() {
setControllerListeners();
Platform.runLater(() -> controller.createSparkLine(startingBoats));
initialiseBoats();
startRaceStream();
}
/**
* Update the calculated fps to the fps label
*
* @param fps The new calculated fps value
*/
private void updateFPS(int fps) {
Platform.runLater(() -> controller.setFrames("FPS: " + fps));
}
/**
* Starts the Race Simulation, playing the race start to finish with the timescale.
* This prints the boats participating, the order that the events occur in time order, and the respective information of the events.
*/
private void startRaceStream() {
System.setProperty("javafx.animation.fullspeed", "true");
new AnimationTimer() {
final long timeRaceStarted = System.currentTimeMillis(); //start time of loop
int fps = 0; //init fps value
long timeCurrent = System.currentTimeMillis(); //current time
@Override
public void handle(long arg0) {
totalTimeElapsed = System.currentTimeMillis() - timeRaceStarted;
//Check if the race has actually started.
if (visualiserInput.getRaceStatus().isStarted()) {
//Set all boats to started.
for (Boat boat : startingBoats) {
boat.setStarted(true);
}
}
for (Boat boat : startingBoats) {
if (boat != null && !boat.isFinished()) {
updatePosition(boat);
checkPosition(boat, totalTimeElapsed);
}
}
for (Marker mark: boatMarkers){
if (mark != null){
updateMarker(mark);
}
}
if (visualiserInput.getRaceStatus().isFinished()) {
controller.finishRace(startingBoats);
stop();
}
controller.updateMap(startingBoats, boatMarkers);
fps++;
if ((System.currentTimeMillis() - timeCurrent) > 1000) {
updateFPS(fps);
lastFPS = fps;
fps = 0;
timeCurrent = System.currentTimeMillis();
}
}
}.start();
}
/**
* Update position of boats in race, no position if on starting leg or DNF.
*/
private void updatePositions() {
FXCollections.sort(startingBoats, (a, b) -> b.getCurrentLeg().getLegNumber() - a.getCurrentLeg().getLegNumber());
for(Boat boat: startingBoats) {
if(boat != null) {
boat.setPosition(Integer.toString(startingBoats.indexOf(boat) + 1));
if (boat.isDnf() || !boat.isStarted() || boat.getCurrentLeg().getLegNumber() < 0)
boat.setPosition("-");
}
}
}
/**
* Update call for the controller.
*/
private void setControllerListeners() {
if (controller != null) controller.setInfoTable(this);
}
/**
* Returns the boats that have started the race.
*
* @return ObservableList of Boat class that participated in the race.
* @see ObservableList
* @see Boat
*/
public ObservableList<Boat> getStartingBoats() {
return startingBoats;
}
/**
* Takes an estimated time an event will occur, and converts it to the
* number of seconds before the event will occur.
*
* @param estTimeMillis estimated time in milliseconds
* @return int difference between time the race started and the estimated time
*/
private int convertEstTime(long estTimeMillis, long currentTime) {
long estElapsedMillis = estTimeMillis - currentTime;
int estElapsedSecs = Math.round(estElapsedMillis/1000);
return estElapsedSecs;
}
}