You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
472 lines
15 KiB
472 lines
15 KiB
package visualiser.model;
|
|
|
|
import javafx.animation.AnimationTimer;
|
|
import javafx.collections.FXCollections;
|
|
import javafx.collections.ObservableList;
|
|
import javafx.scene.paint.Color;
|
|
import network.Messages.BoatLocation;
|
|
import network.Messages.BoatStatus;
|
|
import network.Messages.Enums.BoatStatusEnum;
|
|
import network.Messages.Enums.RaceStatusEnum;
|
|
import network.Messages.LatestMessages;
|
|
import network.Messages.RaceStatus;
|
|
import shared.dataInput.BoatDataSource;
|
|
import shared.dataInput.RaceDataSource;
|
|
import shared.dataInput.RegattaDataSource;
|
|
import shared.model.*;
|
|
|
|
import java.time.Duration;
|
|
import java.time.ZonedDateTime;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* The Class used to view the race streamed.
|
|
* Has a course, boats, boundaries, etc...
|
|
* Observes LatestMessages and updates its state based on new messages.
|
|
*/
|
|
public class VisualiserRace extends Race {
|
|
|
|
|
|
/**
|
|
* An observable list of boats in the race.
|
|
*/
|
|
private final ObservableList<VisualiserBoat> boats;
|
|
|
|
/**
|
|
* An observable list of marker boats in the race.
|
|
*/
|
|
private ObservableList<Mark> boatMarkers;
|
|
|
|
|
|
/**
|
|
* Maps between a Leg to a list of boats, in the order that they finished the leg.
|
|
* Used by the Sparkline to ensure it has correct information.
|
|
*/
|
|
private Map<Leg, List<VisualiserBoat>> legCompletionOrder = new HashMap<>();
|
|
|
|
|
|
|
|
/**
|
|
* Constructs a race object with a given RaceDataSource, BoatDataSource, and RegattaDataSource and receives events from LatestMessages.
|
|
* @param boatDataSource Data source for boat related data (yachts and marker boats).
|
|
* @param raceDataSource Data source for race related data (participating boats, legs, etc...).
|
|
* @param regattaDataSource Data source for race related data (course name, location, timezone, etc...).
|
|
* @param latestMessages The LatestMessages to send events to.
|
|
* @param colors A collection of colors used to assign a color to each boat.
|
|
*/
|
|
public VisualiserRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, List<Color> colors) {
|
|
|
|
super(boatDataSource, raceDataSource, regattaDataSource, latestMessages);
|
|
|
|
|
|
this.boats = FXCollections.observableArrayList(this.generateVisualiserBoats(boatDataSource.getBoats(), raceDataSource.getParticipants(), colors));
|
|
|
|
this.boatMarkers = FXCollections.observableArrayList(boatDataSource.getMarkerBoats().values());
|
|
|
|
|
|
//Initialise the leg completion order map.
|
|
for (Leg leg : this.legs) {
|
|
this.legCompletionOrder.put(leg, new ArrayList<>(this.boats.size()));
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets the race data source for this race to a new RaceDataSource.
|
|
* Uses the boundary and legs specified by the new RaceDataSource.
|
|
* @param raceDataSource The new RaceDataSource to use.
|
|
*/
|
|
public void setRaceDataSource(RaceDataSource raceDataSource) {
|
|
this.raceDataSource = raceDataSource;
|
|
|
|
this.boundary = raceDataSource.getBoundary();
|
|
|
|
this.useLegsList(raceDataSource.getLegs());
|
|
}
|
|
|
|
/**
|
|
* Sets the boat data source for this race to a new BoatDataSource.
|
|
* Uses the marker boats specified by the new BoatDataSource.
|
|
* @param boatDataSource The new BoatDataSource to use.
|
|
*/
|
|
public void setBoatDataSource(BoatDataSource boatDataSource) {
|
|
this.boatDataSource = boatDataSource;
|
|
|
|
this.boatMarkers = FXCollections.observableArrayList(boatDataSource.getMarkerBoats().values());
|
|
}
|
|
|
|
/**
|
|
* Sets the regatta data source for this race to a new RegattaDataSource.
|
|
* @param regattaDataSource The new RegattaDataSource to use.
|
|
*/
|
|
public void setRegattaDataSource(RegattaDataSource regattaDataSource) {
|
|
this.regattaDataSource = regattaDataSource;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns a list of {@link Mark} boats.
|
|
* @return List of mark boats.
|
|
*/
|
|
public ObservableList<Mark> getMarks() {
|
|
return boatMarkers;
|
|
}
|
|
|
|
/**
|
|
* Generates a list of VisualiserBoats given a list of Boats, and a list of participating boats.
|
|
* @param boats The map of Boats describing boats that are potentially in the race. Maps boat sourceID to boat.
|
|
* @param sourceIDs The list of boat sourceIDs describing which specific boats are actually participating.
|
|
* @param colors The list of colors to be used for the boats.
|
|
* @return A list of MockBoats that are participating in the race.
|
|
*/
|
|
private List<VisualiserBoat> generateVisualiserBoats(Map<Integer, Boat> boats, List<Integer> sourceIDs, List<Color> colors) {
|
|
|
|
List<VisualiserBoat> visualiserBoats = new ArrayList<>(sourceIDs.size());
|
|
|
|
//For each sourceID participating...
|
|
int colorIndex = 0;
|
|
for (int sourceID : sourceIDs) {
|
|
|
|
//Get the boat associated with the sourceID.
|
|
Boat boat = boats.get(sourceID);
|
|
|
|
//Get a color for the boat.
|
|
Color color = colors.get(colorIndex);
|
|
|
|
//Construct a VisualiserBoat using the Boat and Polars.
|
|
VisualiserBoat visualiserBoat = new VisualiserBoat(boat, color);
|
|
|
|
visualiserBoats.add(visualiserBoat);
|
|
|
|
//Next color.
|
|
colorIndex++;
|
|
|
|
}
|
|
|
|
return visualiserBoats;
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Initialise the boats in the race.
|
|
* This sets their current leg.
|
|
*/
|
|
@Override
|
|
protected void initialiseBoats() {
|
|
|
|
Leg startingLeg = legs.get(0);
|
|
|
|
for (VisualiserBoat boat : boats) {
|
|
|
|
boat.setCurrentLeg(startingLeg);
|
|
boat.setTimeAtLastMark(this.raceClock.getCurrentTime());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates all of the racing boats based on messages received.
|
|
* @param boats The list of racing boats.
|
|
* @param boatLocationMap A map between boat sourceIDs and BoatLocation messages.
|
|
* @param boatStatusMap A map between boat sourceIDs and BoatStatus messages.
|
|
*/
|
|
private void updateBoats(ObservableList<VisualiserBoat> boats, Map<Integer, BoatLocation> boatLocationMap, Map<Integer, BoatStatus> boatStatusMap) {
|
|
|
|
for (VisualiserBoat boat : boats) {
|
|
BoatLocation boatLocation = boatLocationMap.get(boat.getSourceID());
|
|
BoatStatus boatStatus = boatStatusMap.get(boat.getSourceID());
|
|
updateBoat(boat, boatLocation, boatStatus);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates an individual racing boat based on messages received.
|
|
* @param boat The boat to update.
|
|
* @param boatLocation The BoatLocation message to use.
|
|
* @param boatStatus The BoatStatus message to use.
|
|
*/
|
|
private void updateBoat(VisualiserBoat boat, BoatLocation boatLocation, BoatStatus boatStatus) {
|
|
|
|
if (boatLocation != null && boatStatus != null) {
|
|
|
|
//Get the new position.
|
|
double latitude = boatLocation.getLatitudeDouble();
|
|
double longitude = boatLocation.getLongitudeDouble();
|
|
GPSCoordinate gpsCoordinate = new GPSCoordinate(latitude, longitude);
|
|
|
|
boat.setCurrentPosition(gpsCoordinate);
|
|
|
|
//Bearing.
|
|
boat.setBearing(Bearing.fromDegrees(boatLocation.getHeadingDegrees()));
|
|
|
|
//Time until next mark.
|
|
boat.setEstimatedTimeAtNextMark(raceClock.getLocalTime(boatStatus.getEstTimeAtNextMark()));
|
|
|
|
//Speed.
|
|
boat.setCurrentSpeed(boatLocation.getBoatSOG() / Constants.KnotsToMMPerSecond);
|
|
|
|
|
|
//Boat status.
|
|
BoatStatusEnum newBoatStatusEnum = BoatStatusEnum.fromByte(boatStatus.getBoatStatus());
|
|
|
|
//If we are changing from non-racing to racing, we need to initialise boat with their time at last mark.
|
|
if ((boat.getStatus() != BoatStatusEnum.RACING) && (newBoatStatusEnum == BoatStatusEnum.RACING)) {
|
|
boat.setTimeAtLastMark(this.raceClock.getCurrentTime());
|
|
}
|
|
|
|
boat.setStatus(newBoatStatusEnum);
|
|
|
|
|
|
//Leg.
|
|
int legNumber = boatStatus.getLegNumber();
|
|
|
|
if (legNumber >= 1 && legNumber < legs.size()) {
|
|
if (boat.getCurrentLeg() != legs.get(legNumber)) {
|
|
boatFinishedLeg(boat, legs.get(legNumber));
|
|
}
|
|
}
|
|
|
|
|
|
//Attempt to add a track point.
|
|
if (newBoatStatusEnum == BoatStatusEnum.RACING) {
|
|
boat.addTrackPoint(boat.getCurrentPosition(), raceClock.getCurrentTime());
|
|
}
|
|
|
|
//Set finish time if boat finished.
|
|
if (newBoatStatusEnum == BoatStatusEnum.FINISHED || legNumber == this.legs.size()) {
|
|
boat.setTimeFinished(boatLocation.getTime());
|
|
boat.setStatus(BoatStatusEnum.FINISHED);
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates a boat's leg to a specified leg. Also records the order in which the boat passed the leg.
|
|
* @param boat The boat to update.
|
|
* @param leg The leg to use.
|
|
*/
|
|
private void boatFinishedLeg(VisualiserBoat boat, Leg leg) {
|
|
|
|
//Record order in which boat finished leg.
|
|
this.legCompletionOrder.get(boat.getCurrentLeg()).add(boat);
|
|
|
|
//Update boat.
|
|
boat.setCurrentLeg(leg);
|
|
boat.setTimeAtLastMark(this.raceClock.getCurrentTime());
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates all of the marker boats based on messages received.
|
|
* @param boatMarkers The list of marker boats.
|
|
* @param boatLocationMap A map between boat sourceIDs and BoatLocation messages.
|
|
* @param boatStatusMap A map between boat sourceIDs and BoatStatus messages.
|
|
*/
|
|
private void updateMarkers(ObservableList<Mark> boatMarkers, Map<Integer, BoatLocation> boatLocationMap, Map<Integer, BoatStatus> boatStatusMap) {
|
|
|
|
for (Mark mark : boatMarkers) {
|
|
BoatLocation boatLocation = boatLocationMap.get(mark.getSourceID());
|
|
updateMark(mark, boatLocation);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates an individual marker boat based on messages received.
|
|
* @param mark The marker boat to be updated.
|
|
* @param boatLocation The message describing the boat's new location.
|
|
*/
|
|
private void updateMark(Mark mark, BoatLocation boatLocation) {
|
|
|
|
if (boatLocation != null) {
|
|
|
|
//We only update the boat's position.
|
|
double latitude = boatLocation.getLatitudeDouble();
|
|
double longitude = boatLocation.getLongitudeDouble();
|
|
GPSCoordinate gpsCoordinate = new GPSCoordinate(latitude, longitude);
|
|
|
|
mark.setPosition(gpsCoordinate);
|
|
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates the race status (RaceStatusEnum, wind bearing, wind speed) based on received messages.
|
|
* @param raceStatus The RaceStatus message received.
|
|
*/
|
|
private void updateRaceStatus(RaceStatus raceStatus) {
|
|
|
|
//Race status enum.
|
|
this.raceStatusEnum = RaceStatusEnum.fromByte(raceStatus.getRaceStatus());
|
|
|
|
//Wind bearing.
|
|
this.windDirection.setDegrees(raceStatus.getScaledWindDirection());
|
|
|
|
//Wind speed.
|
|
this.windSpeed = raceStatus.getWindSpeedKnots();
|
|
|
|
//Current race time.
|
|
this.raceClock.setUTCTime(raceStatus.getCurrentTime());
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Runnable for the thread.
|
|
*/
|
|
public void run() {
|
|
initialiseBoats();
|
|
startRaceStream();
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Starts the race.
|
|
* This updates the race based on {@link #latestMessages}.
|
|
*/
|
|
private void startRaceStream() {
|
|
|
|
new AnimationTimer() {
|
|
|
|
long lastFrameTime = System.currentTimeMillis();
|
|
|
|
@Override
|
|
public void handle(long arg0) {
|
|
|
|
//Calculate the frame period.
|
|
long currentFrameTime = System.currentTimeMillis();
|
|
long framePeriod = currentFrameTime - lastFrameTime;
|
|
|
|
|
|
//Update race status.
|
|
updateRaceStatus(latestMessages.getRaceStatus());
|
|
|
|
|
|
//Update racing boats.
|
|
updateBoats(boats, latestMessages.getBoatLocationMap(), latestMessages.getBoatStatusMap());
|
|
//And their positions (e.g., 5th).
|
|
updateBoatPositions(boats);
|
|
|
|
|
|
//Update marker boats.
|
|
updateMarkers(boatMarkers, latestMessages.getBoatLocationMap(), latestMessages.getBoatStatusMap());
|
|
|
|
|
|
|
|
|
|
if (getRaceStatusEnum() == RaceStatusEnum.FINISHED) {
|
|
stop();
|
|
}
|
|
|
|
lastFrameTime = currentFrameTime;
|
|
|
|
//Increment fps.
|
|
incrementFps(framePeriod);
|
|
|
|
}
|
|
}.start();
|
|
}
|
|
|
|
/**
|
|
* Update position of boats in race (e.g, 5th), no position if on starting leg or DNF.
|
|
* @param boats The list of boats to update.
|
|
*/
|
|
private void updateBoatPositions(ObservableList<VisualiserBoat> boats) {
|
|
|
|
//Sort boats.
|
|
sortBoatsByPosition(boats);
|
|
|
|
//Assign new positions.
|
|
for (int i = 0; i < boats.size(); i++) {
|
|
VisualiserBoat boat = boats.get(i);
|
|
|
|
|
|
if ((boat.getStatus() == BoatStatusEnum.DNF) || (boat.getStatus() == BoatStatusEnum.PRESTART) || (boat.getCurrentLeg().getLegNumber() < 0)) {
|
|
|
|
boat.setPosition("-");
|
|
|
|
} else {
|
|
boat.setPosition(Integer.toString(i + 1));
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Sorts the list of boats by their position within the race.
|
|
* @param boats The list of boats in the race.
|
|
*/
|
|
private void sortBoatsByPosition(ObservableList<VisualiserBoat> boats) {
|
|
|
|
FXCollections.sort(boats, (a, b) -> {
|
|
//Get the difference in leg numbers.
|
|
int legNumberDelta = b.getCurrentLeg().getLegNumber() - a.getCurrentLeg().getLegNumber();
|
|
|
|
//If they're on the same leg, we need to compare time to finish leg.
|
|
if (legNumberDelta == 0) {
|
|
return (int) Duration.between(b.getEstimatedTimeAtNextMark(), a.getEstimatedTimeAtNextMark()).toMillis();
|
|
} else {
|
|
return legNumberDelta;
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Returns the boats participating in the race.
|
|
* @return ObservableList of boats participating in the race.
|
|
*/
|
|
public ObservableList<VisualiserBoat> getBoats() {
|
|
return boats;
|
|
}
|
|
|
|
/**
|
|
* Returns the order in which boats completed each leg. Maps the leg to a list of boats, ordered by the order in which they finished the leg.
|
|
* @return Leg completion order for each leg.
|
|
*/
|
|
public Map<Leg, List<VisualiserBoat>> getLegCompletionOrder() {
|
|
return legCompletionOrder;
|
|
}
|
|
|
|
/**
|
|
* Takes an estimated time an event will occur, and converts it to the
|
|
* number of seconds before the event will occur.
|
|
*
|
|
* @param estTimeMillis The estimated time, in milliseconds.
|
|
* @param currentTime The current time, in milliseconds.
|
|
* @return int difference between time the race started and the estimated time
|
|
*/
|
|
private int convertEstTime(long estTimeMillis, long currentTime) {
|
|
|
|
//Calculate millisecond delta.
|
|
long estElapsedMillis = estTimeMillis - currentTime;
|
|
|
|
//Convert milliseconds to seconds.
|
|
int estElapsedSecs = Math.round(estElapsedMillis / 1000);
|
|
|
|
return estElapsedSecs;
|
|
|
|
}
|
|
|
|
}
|