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 boats; /** * An observable list of marker boats in the race. */ private ObservableList 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> 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 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 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 generateVisualiserBoats(Map boats, List sourceIDs, List colors) { List 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 boats, Map boatLocationMap, Map 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 boatMarkers, Map boatLocationMap, Map 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) { if (raceStatus != null) { //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 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 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 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> 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; } }