package seng302.Controllers; import javafx.application.Platform; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.scene.control.*; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import seng302.Mock.StreamedRace; import seng302.Model.*; import seng302.VisualiserInput; import java.net.URL; import java.util.*; /** * Created by fwy13 on 15/03/2017. */ public class RaceController extends Controller { @FXML GridPane canvasBase; //user saved data for annotation display private ArrayList presetAnno; private Map importantAnno; private Map annoShownBeforeHide; private int buttonChecked;//button currently checked allows the checkboxes to know whether or not to put it's state in history (if not hidden then store) private int prevBtnChecked;//button to keep track of previous pressed button incase we want to check a checkbox straight from hidden we do not wish for all previous to come on. private static String nameCheckAnno = "name"; private static String abbrevCheckAnno = "abbrev"; private static String speedCheckAnno = "speed"; private static String pathCheckAnno = "path"; private static String timeCheckAnno = "time"; private static String estTimeCheckAnno = "est time"; private static int noBtn = 0; private static int hideBtn = 1; private static int showBtn = 2; private static int partialBtn = 3; private static int importantBtn = 4; private ArrayList startBoats; private Integer sparkLineNumber = 0; private ResizableRaceCanvas raceMap; private ResizableRaceMap raceBoundaries; private ToggleGroup annotationGroup; private ArrayList colours; private Map boatColours = new HashMap<>(); private int legNum; private RaceClock raceClock; @FXML Pane arrow; @FXML SplitPane race; @FXML StackPane arrowPane; @FXML CheckBox showFPS; @FXML CheckBox showBoatPath; @FXML CheckBox showName; @FXML CheckBox showAbbrev; @FXML CheckBox showSpeed; @FXML CheckBox showTime; @FXML CheckBox showEstTime; @FXML Label timer; @FXML Label FPS; @FXML Label timeZone; @FXML Button saveAnno; @FXML TableView boatInfoTable; @FXML TableColumn boatPlacingColumn; @FXML TableColumn boatTeamColumn; @FXML TableColumn boatMarkColumn; @FXML TableColumn boatSpeedColumn; @FXML RadioButton hideAnnoRBTN; @FXML RadioButton showAnnoRBTN; @FXML RadioButton partialAnnoRBTN; @FXML RadioButton importantAnnoRBTN; @FXML LineChart sparklineChart; @FXML NumberAxis xAxis; @FXML NumberAxis yAxis; /** * Updates the ResizableRaceCanvas (raceMap) with most recent data * * @param boats boats that are to be displayed in the race * @param boatMarkers Markers for boats * @see ResizableRaceCanvas */ public void updateMap(ObservableList boats, ObservableList boatMarkers) { raceMap.setBoats(boats); raceMap.setBoatMarkers(boatMarkers); raceMap.update(); raceBoundaries.draw(); //stop if the visualiser is no longer running } /** * Updates the array listened by the TableView (boatInfoTable) that displays the boat information. * * @param race Race to listen to. */ public void setInfoTable(StreamedRace race) { //boatInfoTable.getItems().clear(); ObservableList startingBoats = race.getStartingBoats(); boatInfoTable.setItems(race.getStartingBoats()); boatTeamColumn.setCellValueFactory(cellData -> cellData.getValue().getName()); boatSpeedColumn.setCellValueFactory(cellData -> cellData.getValue().getVelocityProp()); boatMarkColumn.setCellValueFactory(cellData -> cellData.getValue().getCurrentLegName()); boatPlacingColumn.setCellValueFactory(cellData -> cellData.getValue().positionProperty()); } @Override public void initialize(URL location, ResourceBundle resources) { //listener for fps startBoats = new ArrayList<>(); showFPS.selectedProperty().addListener((ov, old_val, new_val) -> { if (showFPS.isSelected()) { FPS.setVisible(true); } else { FPS.setVisible(false); } }); //adds all radios buttons for annotations to a group annotationGroup = new ToggleGroup(); hideAnnoRBTN.setToggleGroup(annotationGroup); showAnnoRBTN.setToggleGroup(annotationGroup); partialAnnoRBTN.setToggleGroup(annotationGroup); importantAnnoRBTN.setToggleGroup(annotationGroup); } /** * Creates and sets initial display for Sparkline for race positions. * A data series for each boat in the race is added. * Position numbers are displayed. * * @param boats boats to display on the sparkline */ public void createSparkLine(ObservableList boats){ // NOTE: Y axis is in negatives to display correct positions makeColours(); startBoats.addAll(boats); mapBoatColours(); // all boats start in 'last' place for (int i=0; i series = new XYChart.Series(); series.getData().add(new XYChart.Data(0, -startBoats.size())); series.getData().add(new XYChart.Data(0, -startBoats.size())); sparklineChart.getData().add(series); sparklineChart.getData().get(i).getNode().setStyle("-fx-stroke: " + ""+boatColours.get(startBoats.get(i).getSourceID())+";"); } sparklineChart.setCreateSymbols(false); // set x axis details xAxis.setAutoRanging(false); xAxis.setTickMarkVisible(false); xAxis.setTickLabelsVisible(false); xAxis.setMinorTickVisible(false); xAxis.setUpperBound((startBoats.size()+1)*legNum); xAxis.setTickUnit((startBoats.size()+1)*legNum); // set y axis details yAxis.setLowerBound(-(startBoats.size()+1)); yAxis.setUpperBound(0); yAxis.setAutoRanging(false); yAxis.setLabel("Position in Race"); yAxis.setTickUnit(1); yAxis.setTickMarkVisible(false); yAxis.setMinorTickVisible(false); // hide minus number from displaying on axis yAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(yAxis) { @Override public String toString(Number value) { if ((Double)value == 0.0 || (Double)value < -startBoats.size()){ return ""; } else { return String.format("%7.0f", -value.doubleValue()); } } }); } /** * Initializes and runs the race, based on the user's chosen scale factor * Currently uses an example racecourse * * @param visualiserInput input from network * @param raceClock The RaceClock to use for the race's countdown/elapsed duration + timezone. */ public void startRace(VisualiserInput visualiserInput, RaceClock raceClock) { //newRace.initialiseBoats(); legNum = visualiserInput.getCourse().getLegs().size()-1; makeArrow(); raceMap = new ResizableRaceCanvas(visualiserInput.getCourse()); raceMap.setMouseTransparent(true); raceMap.widthProperty().bind(canvasBase.widthProperty()); raceMap.heightProperty().bind(canvasBase.heightProperty()); //raceMap.setBoats(newRace.getStartingBoats()); raceMap.draw(); raceMap.setVisible(true); raceMap.setArrow(arrow.getChildren().get(0)); canvasBase.getChildren().add(0, raceMap); raceBoundaries = new ResizableRaceMap(visualiserInput.getCourse()); raceBoundaries.setMouseTransparent(true); raceBoundaries.widthProperty().bind(canvasBase.widthProperty()); raceBoundaries.heightProperty().bind(canvasBase.heightProperty()); raceBoundaries.draw(); raceBoundaries.setVisible(true); canvasBase.getChildren().add(0, raceBoundaries); race.setVisible(true); //Initialize save annotation array, fps listener, and annotation listeners timeZone.setText(raceClock.getTimeZone()); //RaceClock.duration isn't necessarily being changed in the javaFX thread, so we need to runlater the update. raceClock.durationProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> { timer.setText(newValue); }); }); this.raceClock = raceClock; raceMap.setRaceClock(raceClock); StreamedRace newRace = new StreamedRace(visualiserInput, this); initializeFPS(); initializeAnnotations(); new Thread((newRace)).start(); } /** * Finish Race View * @param boats boats there are in the race. */ public void finishRace(ObservableList boats){ race.setVisible(false); parent.enterFinish(boats); } /** * Set the value for the fps label * * @param fps fps that the label will be updated to */ public void setFrames(String fps) { FPS.setText((fps)); } /** * Set up FPS display at bottom of screen */ private void initializeFPS() { showFPS.setVisible(true); showFPS.selectedProperty().addListener((ov, old_val, new_val) -> { if (showFPS.isSelected()) { FPS.setVisible(true); } else { FPS.setVisible(false); } }); } /** * Updates the sparkline to display current boat positions. * New points are plotted to represent each boat when required. * * @param boatsInRace used for current boat positions. */ public void updateSparkline(ObservableList boatsInRace){ int placingVal = boatsInRace.size(); sparkLineNumber++; for (int i = boatsInRace.size() - 1; i >= 0; i--){ for (int j = startBoats.size() - 1; j >= 0; j--){ if (boatsInRace.get(i)==startBoats.get(j)){ // when a boat is on its first leg if (boatsInRace.get(i).getCurrentLeg().getLegNumber()==0){ // adjust boats latest point on X axis sparklineChart.getData().get(j).getData().get(1) .setXValue(sparkLineNumber); } // when a boat first enters its second leg else if (boatsInRace.get(i).getCurrentLeg().getLegNumber ()==1 && sparklineChart.getData().get(j).getData ().size()==2){ // adjust boats position from start mark sparklineChart.getData().get(j).getData().get(1) .setYValue(-placingVal); sparklineChart.getData().get(j).getData().get(1) .setXValue(sparkLineNumber); sparklineChart.getData().get(j).getData().add(new XYChart.Data<> (sparkLineNumber, -placingVal)); } // plot new point for boats current position else { sparklineChart.getData().get(j).getData().add (new XYChart.Data<>(sparkLineNumber, -placingVal)); } placingVal-=1; } } } // xAxis.setUpperBound(sparkLineNumber); // xAxis.setTickUnit(sparkLineNumber); } private void makeColours() { colours = new ArrayList<>(Arrays.asList( colourToHex(Color.BLUEVIOLET), colourToHex(Color.BLACK), colourToHex(Color.RED), colourToHex(Color.ORANGE), colourToHex(Color.DARKOLIVEGREEN), colourToHex(Color.LIMEGREEN), colourToHex(Color.PURPLE), colourToHex(Color.DARKGRAY), colourToHex(Color.YELLOW) )); } private void mapBoatColours() { int currentColour = 0; for (Boat boat : startBoats) { if (!boatColours.containsKey(boat.getSourceID())) { boatColours.put(boat.getSourceID(), colours.get(currentColour)); } currentColour = (currentColour + 1) % colours.size(); } } private void storeCurrentAnnotationState(){ annoShownBeforeHide.put(nameCheckAnno, showName.isSelected()); annoShownBeforeHide.put(abbrevCheckAnno, showAbbrev.isSelected()); annoShownBeforeHide.put(pathCheckAnno, showBoatPath.isSelected()); annoShownBeforeHide.put(speedCheckAnno, showSpeed.isSelected()); annoShownBeforeHide.put(timeCheckAnno, showTime.isSelected()); annoShownBeforeHide.put(estTimeCheckAnno, showEstTime.isSelected()); } /** * Set up boat annotations */ private void initializeAnnotations() { presetAnno = new ArrayList<>(); importantAnno = new HashMap<>(); importantAnno.put(nameCheckAnno, false); importantAnno.put(abbrevCheckAnno, false); importantAnno.put(pathCheckAnno, false); importantAnno.put(speedCheckAnno, false); importantAnno.put(timeCheckAnno, false); importantAnno.put(estTimeCheckAnno, false); annoShownBeforeHide = new HashMap<>(); annoShownBeforeHide.put(nameCheckAnno, true); annoShownBeforeHide.put(abbrevCheckAnno, true); annoShownBeforeHide.put(pathCheckAnno, true); annoShownBeforeHide.put(speedCheckAnno, true); annoShownBeforeHide.put(timeCheckAnno, true); annoShownBeforeHide.put(estTimeCheckAnno, true); //listener for show name in annotation showName.selectedProperty().addListener((ov, old_val, new_val) -> { if (old_val != new_val) { raceMap.toggleAnnoName(); } if (buttonChecked != hideBtn) { //if we are checking the box straight out of hide instead of using the radio buttons if (prevBtnChecked == hideBtn && buttonChecked != showBtn){ storeCurrentAnnotationState(); } else { annoShownBeforeHide.put(nameCheckAnno, showName.isSelected()); } if (buttonChecked == noBtn) { annotationGroup.selectToggle(showAnnoRBTN); } } raceMap.update(); prevBtnChecked = noBtn; }); //listener for show abbreviation for annotation showAbbrev.selectedProperty().addListener((ov, old_val, new_val) -> { if (old_val != new_val) { raceMap.toggleAnnoAbbrev(); } if (buttonChecked != hideBtn) { if (prevBtnChecked == hideBtn && buttonChecked != showBtn){ storeCurrentAnnotationState(); } else { annoShownBeforeHide.put(abbrevCheckAnno, showAbbrev.isSelected()); } if (buttonChecked == noBtn) { annotationGroup.selectToggle(showAnnoRBTN); } } raceMap.update(); prevBtnChecked = noBtn; }); //listener for show boat path for annotation showBoatPath.selectedProperty().addListener((ov, old_val, new_val) -> { if (old_val != new_val) { raceMap.toggleBoatPath(); } if (buttonChecked != hideBtn) { if (prevBtnChecked == hideBtn && buttonChecked != showBtn){ storeCurrentAnnotationState(); } else { annoShownBeforeHide.put(pathCheckAnno, showBoatPath.isSelected()); } if (buttonChecked == noBtn) { annotationGroup.selectToggle(showAnnoRBTN); } } raceMap.update(); prevBtnChecked = noBtn; }); //listener to show speed for annotation showSpeed.selectedProperty().addListener((ov, old_val, new_val) -> { if (old_val != new_val) { raceMap.toggleAnnoSpeed(); } if (buttonChecked != hideBtn) { if (prevBtnChecked == hideBtn && buttonChecked != showBtn){ storeCurrentAnnotationState(); } else { annoShownBeforeHide.put(speedCheckAnno, showSpeed.isSelected()); } if (buttonChecked == noBtn) { annotationGroup.selectToggle(showAnnoRBTN); } } raceMap.update(); prevBtnChecked = noBtn; }); showTime.selectedProperty().addListener((ov, old_val, new_val) -> { if (old_val != new_val) { raceMap.toggleAnnoTime(); } if (buttonChecked != hideBtn) { if (prevBtnChecked == hideBtn && buttonChecked != showBtn){ storeCurrentAnnotationState(); } else { annoShownBeforeHide.put(timeCheckAnno, showTime.isSelected()); } if (buttonChecked == noBtn) { annotationGroup.selectToggle(showAnnoRBTN); } } prevBtnChecked = noBtn; raceMap.update(); }); showEstTime.selectedProperty().addListener((ov, old_val, new_val) -> { if (old_val != new_val) { raceMap.toggleAnnoEstTime(); } if (buttonChecked != hideBtn) { if (prevBtnChecked == hideBtn && buttonChecked != showBtn){ storeCurrentAnnotationState(); } else { annoShownBeforeHide.put(estTimeCheckAnno, showEstTime.isSelected()); } if (buttonChecked == noBtn) { annotationGroup.selectToggle(showAnnoRBTN); } } prevBtnChecked = noBtn; raceMap.update(); }); //listener to save currently selected annotation saveAnno.setOnAction(event -> { presetAnno.clear(); presetAnno.add(showName.isSelected()); presetAnno.add(showAbbrev.isSelected()); presetAnno.add(showSpeed.isSelected()); presetAnno.add(showBoatPath.isSelected()); presetAnno.add(showTime.isSelected()); presetAnno.add(showEstTime.isSelected()); }); //listener for hiding hideAnnoRBTN.selectedProperty().addListener((ov, old_val, new_val) ->{ buttonChecked = hideBtn; //raceMap.hideAnnotations(); showName.setSelected(false); showAbbrev.setSelected(false); showBoatPath.setSelected(false); showSpeed.setSelected(false); showTime.setSelected(false); showEstTime.setSelected(false); annotationGroup.selectToggle(hideAnnoRBTN); raceMap.update(); buttonChecked = noBtn; prevBtnChecked = hideBtn; }); //listener for showing all annotations showAnnoRBTN.selectedProperty().addListener((ov, old_val, new_val) ->{ buttonChecked = showBtn; showName.setSelected(annoShownBeforeHide.get(nameCheckAnno)); showAbbrev.setSelected(annoShownBeforeHide.get(abbrevCheckAnno)); showBoatPath.setSelected(annoShownBeforeHide.get(pathCheckAnno)); showSpeed.setSelected(annoShownBeforeHide.get(speedCheckAnno)); showTime.setSelected(annoShownBeforeHide.get(timeCheckAnno)); showEstTime.setSelected(annoShownBeforeHide.get(estTimeCheckAnno)); raceMap.update(); buttonChecked = noBtn; prevBtnChecked = showBtn; }); //listener for showing all important partialAnnoRBTN.selectedProperty().addListener((ov, old_val, new_val) ->{ buttonChecked = partialBtn; showName.setSelected(false); showAbbrev.setSelected(true); showSpeed.setSelected(true); showBoatPath.setSelected(false); showTime.setSelected(false); showEstTime.setSelected(false); annotationGroup.selectToggle(partialAnnoRBTN); raceMap.update(); buttonChecked = noBtn; prevBtnChecked = partialBtn; }); //listener for showing all important importantAnnoRBTN.selectedProperty().addListener((ov, old_val, new_val) ->{ buttonChecked = importantBtn; if (presetAnno.size() > 0) { showName.setSelected(presetAnno.get(0)); showAbbrev.setSelected(presetAnno.get(1)); showSpeed.setSelected(presetAnno.get(2)); showBoatPath.setSelected(presetAnno.get(3)); showTime.setSelected(presetAnno.get(4)); showEstTime.setSelected(presetAnno.get(5)); annotationGroup.selectToggle(importantAnnoRBTN); raceMap.update(); } buttonChecked = noBtn; prevBtnChecked = importantBtn; }); annotationGroup.selectToggle(showAnnoRBTN); } private String colourToHex(Color color) { return String.format( "#%02X%02X%02X", (int)( color.getRed() * 255 ), (int)( color.getGreen() * 255 ), (int)( color.getBlue() * 255 ) ); } private void makeArrow() { arrowPane.getChildren().add(arrow); } public RaceClock getRaceClock() { return raceClock; } }