diff --git a/racevisionGame/src/main/java/shared/model/RaceState.java b/racevisionGame/src/main/java/shared/model/RaceState.java index d77a4129..19a1a52c 100644 --- a/racevisionGame/src/main/java/shared/model/RaceState.java +++ b/racevisionGame/src/main/java/shared/model/RaceState.java @@ -2,6 +2,8 @@ package shared.model; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import network.Messages.Enums.RaceStatusEnum; import network.Messages.Enums.RaceTypeEnum; import shared.dataInput.BoatDataSource; @@ -37,6 +39,12 @@ public abstract class RaceState { */ private RegattaDataSource regattaDataSource; + /** + * Legs in the race. + * We have this in a separate list so that it can be observed. + */ + private ObservableList legs; + /** @@ -65,6 +73,9 @@ public abstract class RaceState { */ public RaceState() { + //Legs. + this.legs = FXCollections.observableArrayList(); + //Race clock. this.raceClock = new RaceClock(ZonedDateTime.now()); @@ -90,8 +101,9 @@ public abstract class RaceState { * @param legs The new list of legs to use. */ protected void useLegsList(List legs) { + this.legs.setAll(legs); //We add a "dummy" leg at the end of the race. - if (legs.size() > 0) { + if (getLegs().size() > 0) { getLegs().add(new Leg("Finish", getLegs().size())); } } @@ -126,6 +138,7 @@ public abstract class RaceState { public void setRaceDataSource(RaceDataSource raceDataSource) { this.raceDataSource = raceDataSource; this.getRaceClock().setStartingTime(raceDataSource.getStartDateTime()); + useLegsList(raceDataSource.getLegs()); } /** @@ -309,8 +322,8 @@ public abstract class RaceState { * Returns the legs of the race. * @return Legs of the race. */ - public List getLegs() { - return raceDataSource.getLegs(); + public ObservableList getLegs() { + return legs; } diff --git a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java index d06ef6f4..d58e2afd 100644 --- a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java @@ -531,6 +531,7 @@ public class ResizableRaceCanvas extends ResizableCanvas { * draws the line leg by leg * @param legs the legs of a race * @param index the index of the current leg to use + * @param legStartPoint The position the current leg. * @return the end point of the current leg that has been drawn */ private GPSCoordinate drawLineRounding(List legs, int index, GPSCoordinate legStartPoint){ diff --git a/racevisionGame/src/main/java/visualiser/model/Sparkline.java b/racevisionGame/src/main/java/visualiser/model/Sparkline.java index d8f21f77..3e49c5a3 100644 --- a/racevisionGame/src/main/java/visualiser/model/Sparkline.java +++ b/racevisionGame/src/main/java/visualiser/model/Sparkline.java @@ -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 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 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> boatSeriesMap; + + + /** * Constructor to set up initial sparkline (LineChart) object @@ -62,12 +74,14 @@ public class Sparkline { public Sparkline(VisualiserRaceState race, LineChart 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 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 c) -> { - //Get the data to plot. - List 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 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 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 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 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 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. diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java index faefa467..0398e4e5 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java @@ -97,7 +97,7 @@ public class VisualiserRaceState extends RaceState { this.generateVisualiserBoats(this.boats, getBoatDataSource().getBoats(), raceDataSource.getParticipants(), unassignedColors); } - useLegsList(raceDataSource.getLegs()); + initialiseLegCompletionOrder(); } /** @@ -125,14 +125,9 @@ public class VisualiserRaceState extends RaceState { /** - * See {@link RaceState#useLegsList(List)}. - * Also initialises the {@link #legCompletionOrder} map. - * @param legs The new list of legs to use. + * Initialises the {@link #legCompletionOrder} map. */ - @Override - public void useLegsList(List legs) { - super.useLegsList(legs); - + public void initialiseLegCompletionOrder() { //Initialise the leg completion order map. for (Leg leg : getLegs()) { this.legCompletionOrder.put(leg, new ArrayList<>(this.boats.size()));