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

Also implemented various other small fixes and further refactored code.

#refactor
This commit is contained in:
Calum
2017-07-25 20:45:27 +12:00
parent aad93d8913
commit 6242ab0b2e
12 changed files with 663 additions and 611 deletions
@@ -1,16 +1,21 @@
package seng302.visualiser.controllers;
import com.sun.javafx.collections.SortableList;
import java.util.concurrent.TimeUnit;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
@@ -35,8 +40,8 @@ import seng302.visualiser.controllers.annotations.Annotation;
import seng302.visualiser.controllers.annotations.ImportantAnnotationController;
import seng302.visualiser.controllers.annotations.ImportantAnnotationDelegate;
import seng302.visualiser.controllers.annotations.ImportantAnnotationsState;
import seng302.visualiser.fxObjects.BoatGroup;
import seng302.visualiser.fxObjects.MarkGroup;
import seng302.visualiser.fxObjects.BoatObject;
import seng302.visualiser.fxObjects.MarkObject;
import seng302.model.*;
import seng302.model.mark.GateMark;
import seng302.model.mark.Mark;
@@ -45,15 +50,14 @@ import seng302.model.stream.parsers.StreamParser;
import java.io.IOException;
import java.util.*;
import seng302.model.stream.parsers.xml.XMLParser.RaceXMLObject.Participant;
/**
* Created by ptg19 on 29/03/17.
* Controller class that manages the display of a race
*/
public class RaceViewController extends Thread implements ImportantAnnotationDelegate {
@FXML
private LineChart raceSparkLine;
private LineChart<String, Double> raceSparkLine;
@FXML
private NumberAxis sparklineYAxis;
@FXML
@@ -75,15 +79,14 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
//Race Data
private Map<Integer, Boat> participants;
private Map<Integer, Mark> course;
private RaceXMLData raceData;
private Map<Integer, Mark> markers;
private RaceXMLData courseData;
private GameView gameView;
private RaceState raceState;
private Timeline timerTimeline;
private Stage stage;
private HashMap<Integer, Series<String, Double>> sparkLineData = new HashMap<>();
private ImportantAnnotationsState importantAnnotations;
private Boat selectedBoat;
public void initialize() {
// Load a default important annotation state
@@ -96,20 +99,26 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
raceSparkLine.getYAxis().setAutoRanging(false);
sparklineYAxis.setTickMarkVisible(false);
selectAnnotationBtn.setOnAction(event -> loadSelectAnnotationView());
}
public void loadRace (Map<Integer, Boat> participants, RaceXMLData raceData, RaceState raceState) {
this.participants = participants;
this.courseData = raceData;
this.markers = raceData.getCompoundMarks();
this.raceState = raceState;
initializeUpdateTimer();
initialiseFPSCheckBox();
initialiseAnnotationSlider();
initialiseBoatSelectionComboBox();
selectAnnotationBtn.setOnAction(event -> loadSelectAnnotationView());
}
public void loadRace (Map<Integer, Boat> participants, RaceXMLData raceData) {
this.participants = participants;
this.raceData = raceData;
this.course = raceData.getCompoundMarks();
gameView = new GameView();
gameView.setBoats(new ArrayList<>(participants.values()));
gameView.updateBorder(raceData.getCourseLimit());
gameView.updateCourse(new ArrayList<>(raceData.getCompoundMarks().values()));
gameView.startRace();
}
/**
@@ -169,7 +178,6 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
if (n == 2) {
return "All";
}
return "All";
}
@@ -189,7 +197,7 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
}
});
annotationSlider.valueProperty().addListener((obs, oldval, newVal) ->
annotationSlider.valueProperty().addListener((obs, oldVal, newVal) ->
setAnnotations((int) annotationSlider.getValue())
);
annotationSlider.setValue(2);
@@ -202,10 +210,11 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
private void updateSparkLine(){
// Collect the racing boats that aren't already in the chart
List<Boat> sparkLineCandidates = new ArrayList<>();
participants.forEach((id, boat) ->{
if (!sparkLineData.containsKey(id) && boat.getPosition() != null && !boat.getPosition().equals("-"))
sparkLineCandidates.add(boat);
});
// participants.forEach((id, boat) ->{
// if (!sparkLineData.containsKey(id) && boat.getPosition() != null && !boat.getPosition().equals("-"))
// sparkLineCandidates.add(boat);
// });
participants.forEach((id, boat) -> sparkLineCandidates.add(boat));
sparklineYAxis.setUpperBound(participants.size() + 1);
@@ -213,13 +222,18 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
sparkLineCandidates.stream().filter(yacht -> yacht.getPosition() != null).forEach(yacht -> {
Series<String, Double> yachtData = new Series<>();
yachtData.setName(yacht.getSourceID().toString());
yachtData.getData().add(new XYChart.Data<>(Integer.toString(yacht.getLegNumber()), 1 + participants.size() - Double.parseDouble(yacht.getPosition())));
yachtData.getData().add(
new XYChart.Data<>(
Integer.toString(yacht.getLegNumber()),
1.0 + participants.size() - yacht.getPosition()
)
);
sparkLineData.put(yacht.getSourceID(), yachtData);
});
// Lambda function to sort the series in order of leg (later legs shown more to the right)
List<XYChart.Series<String, Double>> positions = new ArrayList<>(sparkLineData.values());
Collections.sort(positions, (o1, o2) -> {
positions.sort((o1, o2) -> {
Integer leg1 = Integer.parseInt(o1.getData().get(o1.getData().size()-1).getXValue());
Integer leg2 = Integer.parseInt(o2.getData().get(o2.getData().size()-1).getXValue());
if (leg2 < leg1){
@@ -247,13 +261,21 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
* @param legNumber the leg number that the position will be assigned to
*/
public void updateYachtPositionSparkline(Boat boat, Integer legNumber){
XYChart.Series<String, Double> positionData = sparkLineData.get(boat.getSourceID());
positionData.getData().add(
new XYChart.Data<>(
Integer.toString(legNumber),
1 + participants.size() - Double.parseDouble(boat.getPosition())
)
);
for (XYChart.Series<String, Double> positionData : sparkLineData.values()) {
positionData.getData().add(
new Data<>(
Integer.toString(legNumber),
1.0 + participants.size() - boat.getPosition()
)
);
}
// XYChart.Series<String, Double> positionData = sparkLineData.get(boat.getSourceID());
// positionData.getData().add(
// new XYChart.Data<>(
// Integer.toString(legNumber),
// 1.0 + participants.size() - boat.getPosition()
// )
// );
}
@@ -306,10 +328,10 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
* @param bg The BoatGroup to find the next mark of
* @return The next Mark or null if none found
*/
private Mark getNextMark(BoatGroup bg) {
private Mark getNextMark(BoatObject bg) {
Integer legNumber = bg.getBoat().getLegNumber();
List<Corner> markSequence = raceData.getMarkSequence();
List<Corner> markSequence = courseData.getMarkSequence();
if (legNumber == 0) {
return null;
@@ -319,7 +341,7 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
for (Corner corner : markSequence) {
if (legNumber + 2 == corner.getSeqID()) {
return raceData.getCompoundMarks().get(corner.getCompoundMarkID());
return courseData.getCompoundMarks().get(corner.getCompoundMarkID());
}
}
return null;
@@ -330,8 +352,8 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
* Updates the wind direction arrow and text as from info from the StreamParser
*/
private void updateWindDirection() {
windDirectionText.setText(String.format("%.1f°", StreamParser.getWindDirection()));
windArrowText.setRotate(StreamParser.getWindDirection());
windDirectionText.setText(String.format("%.1f°", raceState.getWindDirection()));
windArrowText.setRotate(raceState.getWindDirection());
}
@@ -339,26 +361,14 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
* Updates the clock for the race
*/
private void updateRaceTime() {
if (StreamParser.isRaceFinished()) {
if (!raceState.isRaceStarted()) {
timerLabel.setFill(Color.RED);
timerLabel.setText("Race Finished!");
} else {
timerLabel.setText(getTimeSinceStartOfRace());
timerLabel.setText(raceState.getRaceTimeStr());
}
}
/**
* Grabs the boats currently in the race as from the StreamParser and sets them to be selectable
* in the boat selection combo box
*/
private void updateBoatSelectionComboBox() {
ObservableList<Boat> observableBoats = FXCollections.observableArrayList();
observableBoats.addAll(participants.values());
boatSelectionComboBox.setItems(observableBoats);
}
/**
* Updates the order of the boats as from the StreamParser and sets them in the boat order
* section
@@ -369,101 +379,98 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
positionVbox.getStylesheets().add(getClass().getResource("/css/master.css").toString());
// list of racing boat id
List<Boat> sorted = new ArrayList<>(participants.values());
sorted.sort(Comparator.comparingInt(Boat::getPosition));
if (StreamParser.isRaceStarted()) {
for (Boat boat : StreamParser.getBoatsPos().values()) {
if (participantIDs.contains(boat.getSourceID())) { // check if the boat is racing
if (boat.getBoatStatus() == 3) { // 3 is finish status
Text textToAdd = new Text(boat.getPosition() + ". " +
boat.getShortName() + " (Finished)");
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
positionVbox.getChildren().add(textToAdd);
for (Boat boat : sorted) {
if (boat.getBoatStatus() == 3) { // 3 is finish status
Text textToAdd = new Text(boat.getPosition() + ". " +
boat.getShortName() + " (Finished)");
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
positionVbox.getChildren().add(textToAdd);
} else {
Text textToAdd = new Text(boat.getPosition() + ". " +
boat.getShortName() + " ");
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
textToAdd.setStyle("");
positionVbox.getChildren().add(textToAdd);
}
}
}
} else {
participants.forEach((id, boat) ->{
} else {
Text textToAdd = new Text(boat.getPosition() + ". " +
boat.getShortName() + " ");
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
textToAdd.setStyle("");
positionVbox.getChildren().add(textToAdd);
});
}
}
private void updateLaylines(BoatGroup bg) {
Mark nextMark = getNextMark(bg);
Boolean isUpwind = null;
// Can only calc leg direction if there is a next mark and it is a gate mark
if (nextMark != null) {
if (nextMark instanceof GateMark) {
if (bg.isUpwindLeg(gameViewController, nextMark)) {
isUpwind = true;
} else {
isUpwind = false;
}
for(MarkGroup mg : gameViewController.getMarkGroups()) {
mg.removeLaylines();
if (mg.getMainMark().getId() == nextMark.getId()) {
SingleMark singleMark1 = ((GateMark) nextMark).getSingleMark1();
SingleMark singleMark2 = ((GateMark) nextMark).getSingleMark2();
Point2D markPoint1 = gameViewController
.findScaledXY(singleMark1.getLatitude(), singleMark1.getLongitude());
Point2D markPoint2 = gameViewController
.findScaledXY(singleMark2.getLatitude(), singleMark2.getLongitude());
HashMap<Double, Double> angleAndSpeed;
if (isUpwind) {
angleAndSpeed = PolarTable.getOptimalUpwindVMG(StreamParser.getWindSpeed());
} else {
angleAndSpeed = PolarTable.getOptimalDownwindVMG(StreamParser.getWindSpeed());
}
Double resultingAngle = angleAndSpeed.keySet().iterator().next();
Point2D boatCurrentPos = new Point2D(bg.getBoatLayoutX(), bg.getBoatLayoutY());
Point2D gateMidPoint = markPoint1.midpoint(markPoint2);
Integer lineFuncResult = GeoUtility.lineFunction(boatCurrentPos, gateMidPoint, markPoint2);
Line rightLayline = new Line();
Line leftLayline = new Line();
if (lineFuncResult == 1) {
rightLayline = makeRightLayline(markPoint2, 180 - resultingAngle, StreamParser.getWindDirection());
leftLayline = makeLeftLayline(markPoint1, 180 - resultingAngle, StreamParser.getWindDirection());
} else if (lineFuncResult == -1) {
rightLayline = makeRightLayline(markPoint1, 180 - resultingAngle, StreamParser.getWindDirection());
leftLayline = makeLeftLayline(markPoint2, 180 - resultingAngle, StreamParser.getWindDirection());
}
leftLayline.setStrokeWidth(0.5);
leftLayline.setStroke(bg.getBoat().getColour());
rightLayline.setStrokeWidth(0.5);
rightLayline.setStroke(bg.getBoat().getColour());
bg.setLaylines(leftLayline, rightLayline);
mg.addLaylines(leftLayline, rightLayline);
}
}
}
}
// participants.forEach((id, boat) ->{
// Text textToAdd = new Text(boat.getPosition() + ". " +
// boat.getShortName() + " ");
// textToAdd.setFill(Paint.valueOf("#d3d3d3"));
// textToAdd.setStyle("");
// positionVbox.getChildren().add(textToAdd);
// });
}
// private void updateLaylines(BoatObject bg) {
//
// Mark nextMark = getNextMark(bg);
// Boolean isUpwind = null;
// // Can only calc leg direction if there is a next mark and it is a gate mark
// if (nextMark != null) {
// if (nextMark instanceof GateMark) {
// if (bg.isUpwindLeg(gameViewController, nextMark)) {
// isUpwind = true;
// } else {
// isUpwind = false;
// }
//
// for(MarkObject mg : gameViewController.getMarkGroups()) {
//
// mg.removeLaylines();
//
// if (mg.getMainMark().getId() == nextMark.getId()) {
//
// SingleMark singleMark1 = ((GateMark) nextMark).getSingleMark1();
// SingleMark singleMark2 = ((GateMark) nextMark).getSingleMark2();
// Point2D markPoint1 = gameViewController
// .findScaledXY(singleMark1.getLatitude(), singleMark1.getLongitude());
// Point2D markPoint2 = gameViewController
// .findScaledXY(singleMark2.getLatitude(), singleMark2.getLongitude());
// HashMap<Double, Double> angleAndSpeed;
// if (isUpwind) {
// angleAndSpeed = PolarTable.getOptimalUpwindVMG(StreamParser.getWindSpeed());
// } else {
// angleAndSpeed = PolarTable.getOptimalDownwindVMG(StreamParser.getWindSpeed());
// }
//
// Double resultingAngle = angleAndSpeed.keySet().iterator().next();
//
//
// Point2D boatCurrentPos = new Point2D(bg.getBoatLayoutX(), bg.getBoatLayoutY());
// Point2D gateMidPoint = markPoint1.midpoint(markPoint2);
// Integer lineFuncResult = GeoUtility.lineFunction(boatCurrentPos, gateMidPoint, markPoint2);
// Line rightLayline = new Line();
// Line leftLayline = new Line();
// if (lineFuncResult == 1) {
// rightLayline = makeRightLayline(markPoint2, 180 - resultingAngle, StreamParser.getWindDirection());
// leftLayline = makeLeftLayline(markPoint1, 180 - resultingAngle, StreamParser.getWindDirection());
// } else if (lineFuncResult == -1) {
// rightLayline = makeRightLayline(markPoint1, 180 - resultingAngle, StreamParser.getWindDirection());
// leftLayline = makeLeftLayline(markPoint2, 180 - resultingAngle, StreamParser.getWindDirection());
// }
//
// leftLayline.setStrokeWidth(0.5);
// leftLayline.setStroke(bg.getBoat().getColour());
//
// rightLayline.setStrokeWidth(0.5);
// rightLayline.setStroke(bg.getBoat().getColour());
//
// bg.setLaylines(leftLayline, rightLayline);
// mg.addLaylines(leftLayline, rightLayline);
//
// }
// }
// }
// }
// }
private Point2D getPointRotation(Point2D ref, Double distance, Double angle){
Double newX = ref.getX() + (ref.getX() + distance -ref.getX())*Math.cos(angle) - (ref.getY() + distance -ref.getY())*Math.sin(angle);
Double newY = ref.getY() + (ref.getX() + distance -ref.getX())*Math.sin(angle) + (ref.getY() + distance -ref.getY())*Math.cos(angle);
@@ -495,17 +502,27 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
* for the combobox to take action upon selection
*/
private void initialiseBoatSelectionComboBox() {
updateBoatSelectionComboBox();
boatSelectionComboBox.valueProperty().addListener((observable, oldValue, newValue) -> {
//This listener is fired whenever the combo box changes. This means when the values are updated
//We dont want to set the selected value if the values are updated but nothing clicked (null)
if (newValue != null && newValue != selectedBoat) {
Boat thisBoat = (Boat) newValue;
setSelectedBoat(thisBoat);
boatSelectionComboBox.setItems(
FXCollections.observableArrayList(participants.values())
);
//Null check is if the listener is fired but nothing selected
boatSelectionComboBox.valueProperty().addListener((obs, lastSelection, selectedBoat) -> {
if (selectedBoat != null) {
gameView.selectBoat(selectedBoat);
}
});
}
/**
* Grabs the boats currently in the race as from the StreamParser and sets them to be selectable
* in the boat selection combo box
*/
private void updateBoatSelectionComboBox() {
ObservableList<Boat> observableBoats = FXCollections.observableArrayList();
observableBoats.addAll(participants.values());
boatSelectionComboBox.setItems(observableBoats);
}
/**
* Display the list of boats in the order they finished the race
@@ -525,38 +542,12 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
}
}
/**
* Convert seconds to a string of the format mm:ss
*
* @param time the time in seconds
* @return a formatted string
*/
public String convertTimeToMinutesSeconds(int time) {
if (time < 0) {
return String.format("-%02d:%02d", (time * -1) / 60, (time * -1) % 60);
}
return String.format("%02d:%02d", time / 60, time % 60);
}
private String getTimeSinceStartOfRace() {
String timerString = "0:00";
if (StreamParser.getTimeSinceStart() > 0) {
String timerMinute = Long.toString(StreamParser.getTimeSinceStart() / 60);
String timerSecond = Long.toString(StreamParser.getTimeSinceStart() % 60);
if (timerSecond.length() == 1) {
timerSecond = "0" + timerSecond;
}
timerString = "-" + timerMinute + ":" + timerSecond;
} else {
String timerMinute = Long.toString(-1 * StreamParser.getTimeSinceStart() / 60);
String timerSecond = Long.toString(-1 * StreamParser.getTimeSinceStart() % 60);
if (timerSecond.length() == 1) {
timerSecond = "0" + timerSecond;
}
timerString = timerMinute + ":" + timerSecond;
}
return timerString;
private String getMillisToFormattedTime(long milliseconds) {
return String.format("%02d:%02d:%02d",
TimeUnit.MILLISECONDS.toHours(milliseconds),
TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60, //Modulus 60 minutes per hour
TimeUnit.MILLISECONDS.toSeconds(milliseconds) % 60 //Modulus 60 seconds per minute
);
}
private void setAnnotations(Integer annotationLevel) {
@@ -594,38 +585,21 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
* @param boat The yacht for which we want to view all annotations
*/
private void setSelectedBoat(Boat boat) {
for (BoatGroup bg : gameViewController.getBoatGroups()) {
//We need to iterate over all race groups to get the matching boat group belonging to this boat if we
//are to toggle its annotations, there is no other backwards knowledge of a yacht to its boatgroup.
if (bg.getBoat().getHullID().equals(boat.getHullID())) {
updateLaylines(bg);
bg.setIsSelected(true);
selectedBoat = boat;
} else {
bg.setIsSelected(false);
}
}
}
void setStage(Stage stage) {
this.stage = stage;
}
Stage getStage() {
return stage;
}
/**
* Used for when the boat attempts to add data to the sparkline (first checks if the sparkline contains info on it)
* @param yachtId
* @return
*/
public static boolean sparkLineStatus(Integer yachtId) {
return sparkLineData.containsKey(yachtId);
// for (BoatObject bg : gameViewController.getBoatGroups()) {
// //We need to iterate over all race groups to get the matching boat group belonging to this boat if we
// //are to toggle its annotations, there is no other backwards knowledge of a yacht to its boatgroup.
// if (bg.getBoat().getHullID().equals(boat.getHullID())) {
//// updateLaylines(bg);
// bg.setIsSelected(true);
//// selectedBoat = boat;
// } else {
// bg.setIsSelected(false);
// }
// }
}
public void updateRaceData (RaceXMLData raceData) {
this.raceData = raceData;
this.courseData = raceData;
gameView.updateBorder(raceData.getCourseLimit());
}