|
|
|
|
@ -1,14 +1,18 @@
|
|
|
|
|
package visualiser.model;
|
|
|
|
|
|
|
|
|
|
import javafx.application.Platform;
|
|
|
|
|
import javafx.collections.ListChangeListener;
|
|
|
|
|
import javafx.collections.ObservableList;
|
|
|
|
|
import javafx.collections.transformation.SortedList;
|
|
|
|
|
import javafx.scene.chart.LineChart;
|
|
|
|
|
import javafx.scene.chart.NumberAxis;
|
|
|
|
|
import javafx.scene.chart.XYChart;
|
|
|
|
|
import javafx.scene.paint.Color;
|
|
|
|
|
import shared.model.Leg;
|
|
|
|
|
|
|
|
|
|
import java.util.HashMap;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@ -32,10 +36,10 @@ public class Sparkline {
|
|
|
|
|
private ObservableList<VisualiserBoat> boats;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The number of legs in the race.
|
|
|
|
|
* Used to correctly scale the linechart.
|
|
|
|
|
* Race legs to observe.
|
|
|
|
|
* We need to observe legs as they may be added after the sparkline is created if race.xml is received after this is created.
|
|
|
|
|
*/
|
|
|
|
|
private Integer legNum;
|
|
|
|
|
private ObservableList<Leg> legs;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@ -53,6 +57,14 @@ public class Sparkline {
|
|
|
|
|
*/
|
|
|
|
|
private NumberAxis yAxis;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A map between a boat and its data series in the sparkline.
|
|
|
|
|
* This is used so that we can remove a series when (or if) a boat is removed from the race.
|
|
|
|
|
*/
|
|
|
|
|
private Map<VisualiserBoat, XYChart.Series<Number, Number>> boatSeriesMap;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Constructor to set up initial sparkline (LineChart) object
|
|
|
|
|
@ -62,12 +74,14 @@ public class Sparkline {
|
|
|
|
|
public Sparkline(VisualiserRaceState race, LineChart<Number,Number> sparklineChart) {
|
|
|
|
|
this.race = race;
|
|
|
|
|
this.boats = new SortedList<>(race.getBoats());
|
|
|
|
|
this.legNum = race.getLegCount();
|
|
|
|
|
this.legs = race.getLegs();
|
|
|
|
|
|
|
|
|
|
this.sparklineChart = sparklineChart;
|
|
|
|
|
this.yAxis = (NumberAxis) sparklineChart.getYAxis();
|
|
|
|
|
this.xAxis = (NumberAxis) sparklineChart.getXAxis();
|
|
|
|
|
|
|
|
|
|
this.boatSeriesMap = new HashMap<>();
|
|
|
|
|
|
|
|
|
|
createSparkline();
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
@ -79,50 +93,45 @@ public class Sparkline {
|
|
|
|
|
* Position numbers are displayed.
|
|
|
|
|
*/
|
|
|
|
|
private void createSparkline() {
|
|
|
|
|
// NOTE: Y axis is in negatives to display correct positions
|
|
|
|
|
|
|
|
|
|
//For each boat...
|
|
|
|
|
for (VisualiserBoat boat : this.boats) {
|
|
|
|
|
|
|
|
|
|
//Create data series for each boat.
|
|
|
|
|
XYChart.Series<Number, Number> series = new XYChart.Series<>();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//All boats start in "last" place.
|
|
|
|
|
series.getData().add(new XYChart.Data<>(0, boats.size()));
|
|
|
|
|
|
|
|
|
|
//Listen for changes in the boat's leg - we only update the graph when it changes leg.
|
|
|
|
|
boat.legProperty().addListener(
|
|
|
|
|
(observable, oldValue, newValue) -> {
|
|
|
|
|
//We need to dynamically update the sparkline when boats are added/removed.
|
|
|
|
|
boats.addListener((ListChangeListener.Change<? extends VisualiserBoat> c) -> {
|
|
|
|
|
|
|
|
|
|
//Get the data to plot.
|
|
|
|
|
List<VisualiserBoat> boatOrder = race.getLegCompletionOrder().get(oldValue);
|
|
|
|
|
//Find boat position in list.
|
|
|
|
|
int boatPosition = boatOrder.indexOf(boat) + 1;
|
|
|
|
|
Platform.runLater(() -> {
|
|
|
|
|
|
|
|
|
|
//Get leg number.
|
|
|
|
|
int legNumber = oldValue.getLegNumber() + 1;
|
|
|
|
|
while (c.next()) {
|
|
|
|
|
|
|
|
|
|
if (c.wasAdded()) {
|
|
|
|
|
for (VisualiserBoat boat : c.getAddedSubList()) {
|
|
|
|
|
addBoatSeries(boat);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Create new data point for boat's position at the new leg.
|
|
|
|
|
XYChart.Data<Number, Number> dataPoint = new XYChart.Data<>(legNumber, boatPosition);
|
|
|
|
|
} else if (c.wasRemoved()) {
|
|
|
|
|
for (VisualiserBoat boat : c.getRemoved()) {
|
|
|
|
|
removeBoatSeries(boat);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Add to series.
|
|
|
|
|
Platform.runLater(() -> series.getData().add(dataPoint));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Update height of y axis.
|
|
|
|
|
yAxis.setLowerBound(boats.size());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//Add to chart.
|
|
|
|
|
sparklineChart.getData().add(series);
|
|
|
|
|
|
|
|
|
|
//Color using boat's color. We need to do this after adding the series to a chart, otherwise we get null pointer exceptions.
|
|
|
|
|
series.getNode().setStyle("-fx-stroke: " + colourToHex(boat.getColor()) + ";");
|
|
|
|
|
legs.addListener((ListChangeListener.Change<? extends Leg> c) -> {
|
|
|
|
|
Platform.runLater(() -> xAxis.setUpperBound(race.getLegCount()));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//Initialise chart for existing boats.
|
|
|
|
|
for (VisualiserBoat boat : boats) {
|
|
|
|
|
addBoatSeries(boat);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sparklineChart.setCreateSymbols(false);
|
|
|
|
|
|
|
|
|
|
//Set x axis details
|
|
|
|
|
@ -131,7 +140,7 @@ public class Sparkline {
|
|
|
|
|
xAxis.setTickLabelsVisible(false);
|
|
|
|
|
xAxis.setMinorTickVisible(false);
|
|
|
|
|
xAxis.setLowerBound(0);
|
|
|
|
|
xAxis.setUpperBound(legNum + 2);
|
|
|
|
|
xAxis.setUpperBound(race.getLegCount());
|
|
|
|
|
xAxis.setTickUnit(1);
|
|
|
|
|
|
|
|
|
|
//Set y axis details
|
|
|
|
|
@ -148,6 +157,65 @@ public class Sparkline {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Removes the data series for a given boat from the sparkline.
|
|
|
|
|
* @param boat Boat to remove series for.
|
|
|
|
|
*/
|
|
|
|
|
private void removeBoatSeries(VisualiserBoat boat) {
|
|
|
|
|
sparklineChart.getData().remove(boatSeriesMap.get(boat));
|
|
|
|
|
boatSeriesMap.remove(boat);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a data series for a boat, and adds it to the sparkline.
|
|
|
|
|
* @param boat Boat to add series for.
|
|
|
|
|
*/
|
|
|
|
|
private void addBoatSeries(VisualiserBoat boat) {
|
|
|
|
|
|
|
|
|
|
//Create data series for boat.
|
|
|
|
|
XYChart.Series<Number, Number> series = new XYChart.Series<>();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//All boats start in "last" place.
|
|
|
|
|
series.getData().add(new XYChart.Data<>(0, boats.size()));
|
|
|
|
|
|
|
|
|
|
//Listen for changes in the boat's leg - we only update the graph when it changes leg.
|
|
|
|
|
boat.legProperty().addListener(
|
|
|
|
|
(observable, oldValue, newValue) -> {
|
|
|
|
|
|
|
|
|
|
//Get the data to plot.
|
|
|
|
|
List<VisualiserBoat> boatOrder = race.getLegCompletionOrder().get(oldValue);
|
|
|
|
|
//Find boat position in list.
|
|
|
|
|
int boatPosition = boatOrder.indexOf(boat) + 1;
|
|
|
|
|
|
|
|
|
|
//Get leg number.
|
|
|
|
|
int legNumber = oldValue.getLegNumber() + 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//Create new data point for boat's position at the new leg.
|
|
|
|
|
XYChart.Data<Number, Number> dataPoint = new XYChart.Data<>(legNumber, boatPosition);
|
|
|
|
|
|
|
|
|
|
//Add to series.
|
|
|
|
|
Platform.runLater(() -> series.getData().add(dataPoint));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//Add to chart.
|
|
|
|
|
sparklineChart.getData().add(series);
|
|
|
|
|
|
|
|
|
|
//Color using boat's color. We need to do this after adding the series to a chart, otherwise we get null pointer exceptions.
|
|
|
|
|
series.getNode().setStyle("-fx-stroke: " + colourToHex(boat.getColor()) + ";");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
boatSeriesMap.put(boat, series);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Converts a color to a hex string, starting with a {@literal #} symbol.
|
|
|
|
|
* @param color The color to convert.
|
|
|
|
|
|