Copied remaining files into appropriate package. These need to be refactored and put into the shared package.
This commit is contained in:
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user