diff --git a/src/main/java/seng302/model/Boat.java b/src/main/java/seng302/model/Boat.java index d372f695..caed3621 100644 --- a/src/main/java/seng302/model/Boat.java +++ b/src/main/java/seng302/model/Boat.java @@ -1,9 +1,11 @@ package seng302.model; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyDoubleWrapper; +import javafx.beans.property.ReadOnlyLongProperty; +import javafx.beans.property.ReadOnlyLongWrapper; import javafx.scene.paint.Color; import seng302.model.mark.Mark; -import seng302.model.stream.packets.StreamPacket; -import seng302.visualiser.controllers.RaceViewController; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -30,15 +32,14 @@ public class Boat { private Integer boatStatus; private Integer legNumber = 0; private Integer position = 0; - private Integer penaltiesAwarded; - private Integer penaltiesServed; private Long estimateTimeAtFinish; + private Long markRoundTime; private Double lat; private Double lon; private Double heading; - private double velocity; - private Long timeTillNext; - private Long markRoundTime; + private ReadOnlyDoubleWrapper velocity = new ReadOnlyDoubleWrapper(); + private ReadOnlyLongWrapper timeTillNext = new ReadOnlyLongWrapper(); + private ReadOnlyLongWrapper timeSinceLastMark = new ReadOnlyLongWrapper(); // Mark rounding private Mark lastMarkRounded; @@ -94,8 +95,8 @@ public class Boat { this.legNumber = legNumber; } - public void setEstimateTimeAtNextMark(Long estimateTimeAtNextMark) { - timeTillNext = estimateTimeAtNextMark; + public void setEstimateTimeTillNextMark(Long estimateTimeAtNextMark) { + timeTillNext.set(estimateTimeAtNextMark); } public String getEstimateTimeAtFinish() { @@ -124,7 +125,7 @@ public class Boat { } public void setVelocity(double velocity) { - this.velocity = velocity; + this.velocity.set(velocity); } @@ -132,12 +133,12 @@ public class Boat { this.markRoundTime = markRoundingTime; } - public double getVelocity() { - return velocity; + public ReadOnlyDoubleProperty getVelocityProperty() { + return velocity.getReadOnlyProperty(); } - public Long getTimeTillNext() { - return timeTillNext; + public ReadOnlyLongProperty timeTillNextProperty() { + return timeTillNext.getReadOnlyProperty(); } public Long getMarkRoundTime() { @@ -189,4 +190,12 @@ public class Boat { return boatName; } + public void setTimeSinceLastMark (long timeSinceLastMark) { + this.timeSinceLastMark.set(timeSinceLastMark); + } + + public ReadOnlyLongProperty timeSinceLastMarkProperty () { + return timeSinceLastMark.getReadOnlyProperty(); + } + } diff --git a/src/main/java/seng302/model/RaceState.java b/src/main/java/seng302/model/RaceState.java new file mode 100644 index 00000000..99a72c22 --- /dev/null +++ b/src/main/java/seng302/model/RaceState.java @@ -0,0 +1,72 @@ +package seng302.model; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.TimeZone; +import seng302.model.stream.parsers.RaceStartData; +import seng302.model.stream.parsers.RaceStatusData; + +/** + * Class for storing race data that does not relate to specific vessels or marks such as time or wind. + * Calculates the state of critical race attributes when relevant data is added. + */ +public class RaceState { + +// private final DateFormat DATE_TIME_FORMAT = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); + private final DateFormat DATE_TIME_FORMAT = new SimpleDateFormat("HH:mm:ss"); + + private double windSpeed; + private double windDirection; + private long raceTime; + private long expectedStartTime; + private boolean isRaceStarted; +// long timeTillStart; + + public RaceState() { + } + + public void updateState (RaceStatusData data) { + this.windSpeed = data.getWindSpeed(); + this.windDirection = data.getWindDirection(); + this.raceTime = data.getCurrentTime(); + this.expectedStartTime = data.getExpectedStartTime(); + this.isRaceStarted = data.isRaceStarted(); + } + + public void setTimeZone (TimeZone timeZone) { + DATE_TIME_FORMAT.setTimeZone(timeZone); + } + + public void updateState (RaceStartData data) { +// this.timeTillStart = data.getRaceStartTime(); + System.out.println(data.getRaceStartTime()); + } + + public String getRaceTimeStr () { + return DATE_TIME_FORMAT.format(raceTime); + } + + public long getTimeTillStart () { + return (expectedStartTime - raceTime) / 1000; + } + + public double getWindSpeed() { + return windSpeed; + } + + public double getWindDirection() { + return windDirection; + } + + public long getRaceTime() { + return raceTime; + } + + public long getExpectedStartTime() { + return expectedStartTime; + } + + public boolean isRaceStarted () { + return isRaceStarted; + } +} diff --git a/src/main/java/seng302/model/RaceStatus.java b/src/main/java/seng302/model/RaceStatus.java deleted file mode 100644 index 62a336b0..00000000 --- a/src/main/java/seng302/model/RaceStatus.java +++ /dev/null @@ -1,16 +0,0 @@ -package seng302.model; - -import javafx.beans.property.DoubleProperty; -import javafx.beans.property.LongProperty; -import javafx.beans.property.SimpleDoubleProperty; -import javafx.beans.property.SimpleLongProperty; - -/** - * Class for storing race data that does not relate to specific vessels or marks such as time or wind - */ -public class RaceStatus { - double windSpeed; - double windDirection; - long raceTime; - -} diff --git a/src/main/java/seng302/model/stream/parsers/StreamParser.java b/src/main/java/seng302/model/stream/parsers/StreamParser.java index e46ceadf..e30009c2 100644 --- a/src/main/java/seng302/model/stream/parsers/StreamParser.java +++ b/src/main/java/seng302/model/stream/parsers/StreamParser.java @@ -38,20 +38,6 @@ public class StreamParser { return heartbeat; } - private static String getTimeZoneString(RegattaXMLData regattaXML) { - Integer offset = regattaXML.getUtcOffset(); - StringBuilder utcOffset = new StringBuilder(); - utcOffset.append("GMT"); - if (offset > 0) { - utcOffset.append("+"); - utcOffset.append(offset); - } else if (offset < 0) { - utcOffset.append("-"); - utcOffset.append(offset); - } - return utcOffset.toString(); - } - /** * Extracts the useful race status data from race status type packets. This method will also * print to the console the current state of the race (if it has started/finished or is about to @@ -112,7 +98,7 @@ public class StreamParser { // boat.setPenaltiesServed((int) payload[31 + (i * 20)]); Long estTimeAtNextMark = bytesToLong( Arrays.copyOfRange(payload, 32 + (i * 20), 38 + (i * 20))); -// boat.setEstimateTimeAtNextMark(estTimeAtNextMark); +// boat.setEstimateTimeTillNextMark(estTimeAtNextMark); Long estTimeAtFinish = bytesToLong( Arrays.copyOfRange(payload, 38 + (i * 20), 44 + (i * 20))); int leg = (int) payload[29 + (i * 20)]; diff --git a/src/main/java/seng302/visualiser/GameView.java b/src/main/java/seng302/visualiser/GameView.java index 3a2f4822..836d04e1 100644 --- a/src/main/java/seng302/visualiser/GameView.java +++ b/src/main/java/seng302/visualiser/GameView.java @@ -1,11 +1,13 @@ package seng302.visualiser; import java.io.IOException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.PriorityBlockingQueue; import javafx.animation.AnimationTimer; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.collections.ObservableList; @@ -21,8 +23,9 @@ import javafx.scene.paint.Color; import javafx.scene.shape.Polygon; import javafx.scene.text.Text; import seng302.model.Limit; -import seng302.visualiser.fxObjects.BoatGroup; -import seng302.visualiser.fxObjects.MarkGroup; +import seng302.visualiser.fxObjects.AnnotationBox; +import seng302.visualiser.fxObjects.BoatObject; +import seng302.visualiser.fxObjects.MarkObject; import seng302.model.Colors; import seng302.model.Boat; import seng302.model.map.Boundary; @@ -32,10 +35,6 @@ import seng302.model.mark.Mark; import seng302.model.mark.MarkType; import seng302.model.mark.SingleMark; import seng302.model.stream.parsers.StreamParser; -import seng302.model.stream.parsers.xml.XMLParser; -import seng302.model.stream.parsers.xml.XMLParser.RaceXMLObject.Limit; -import seng302.model.stream.parsers.xml.XMLParser.RaceXMLObject.Participant; -import seng302.model.stream.parsers.PositionUpdateData; import seng302.utilities.GeoPoint; import seng302.utilities.GeoUtility; @@ -65,8 +64,9 @@ public class GameView extends Pane { private double metersPerPixelX; private double metersPerPixelY; - private List markGroups = new ArrayList<>(); - private List boatGroups = new ArrayList<>(); + private Map markObjects = new HashMap<>(); + private Map boatObjects = new HashMap<>(); + private List annotations = new ArrayList<>(); private Text fpsDisplay = new Text(); @@ -130,17 +130,14 @@ public class GameView extends Pane { }; } - void initializeCanvas() { - - fitMarksToCanvas(); + public void initializeCanvas() { drawGoogleMap(); fpsDisplay.setLayoutX(5); fpsDisplay.setLayoutY(20); fpsDisplay.setStrokeWidth(2); gameObjects.add(fpsDisplay); gameObjects.add(raceBorder); - initializeMarks(); - initializeBoats(); + this.widthProperty().addListener(resize -> { canvasWidth = this.getWidth(); canvasHeight = this.getHeight(); @@ -206,14 +203,12 @@ public class GameView extends Pane { * same, so they do not have things such as a type and must be derived from the number of marks * in a compound mark etc.. */ - private void addRaceBorder() { - XMLParser.RaceXMLObject raceXMLObject = StreamParser.getXmlObject().getRaceXML(); - ArrayList courseLimits = raceXMLObject.getCourseLimit(); + public void updateBorder(List border) { raceBorder.setStroke(new Color(0.0f, 0.0f, 0.74509807f, 1)); raceBorder.setStrokeWidth(3); raceBorder.setFill(new Color(0,0,0,0)); List boundaryPoints = new ArrayList<>(); - for (Limit limit : courseLimits) { + for (Limit limit : border) { Point2D location = findScaledXY(limit.getLat(), limit.getLng()); boundaryPoints.add(location.getX()); boundaryPoints.add(location.getY()); @@ -222,112 +217,114 @@ public class GameView extends Pane { } private void updateGroups() { - for (BoatGroup boatGroup : boatGroups) { - // some raceObjects will have multiple ID's (for instance gate marks) - //checking if the current "ID" has any updates associated with it - if (StreamParser.boatLocations.containsKey(boatGroup.getRaceId())) { - if (boatGroup.isStopped()) { - updateBoatGroup(boatGroup); - } - } - boatGroup.move(); - } - for (MarkGroup markGroup : markGroups) { - for (Long id : markGroup.getRaceIds()) { - if (StreamParser.markLocations.containsKey(id)) { - updateMarkGroup(id, markGroup); - } - } - } - checkForCourseChanges(); + boatObjects.forEach((boat, boatObject) -> {}); + markObjects.forEach((mark, markObject) -> {}); } - private void checkForCourseChanges() { - if (StreamParser.isNewRaceXmlReceived()){ - addRaceBorder(); - } - } - - private void updateBoatGroup(BoatGroup boatGroup) { -// PriorityBlockingQueue movementQueue = StreamParser.boatLocations.get(boatGroup.getRaceId()); -// // giving the movementQueue a 5 packet buffer to account for slightly out of order packets -// if (movementQueue.size() > 0) { +// private void updateBoatGroup(BoatGroup boatGroup) { +//// PriorityBlockingQueue movementQueue = StreamParser.boatLocations.get(boatGroup.getRaceId()); +//// // giving the movementQueue a 5 packet buffer to account for slightly out of order packets +//// if (movementQueue.size() > 0) { +//// try { +//// PositionUpdateData positionPacket = movementQueue.take(); +// Point2D p2d = findScaledXY(positionPacket.getLat(), positionPacket.getLon()); +//// double heading = 360.0 / 0xffff * positionPacket.getHeading(); +// boatGroup.setDestination( +// p2d.getX(), p2d.getY(), heading, positionPacket.getGroundSpeed(), +// positionPacket.getTimeValid(), frameRate); +//// } catch (InterruptedException e){ +//// e.printStackTrace(); +//// } +////// } +//// } +// } +// +// private void updateMarkGroup (long raceId, MarkGroup markGroup) { +// PriorityBlockingQueue movementQueue = StreamParser.markLocations.get(raceId); +// if (movementQueue.size() > 0){ // try { // PositionUpdateData positionPacket = movementQueue.take(); - Point2D p2d = findScaledXY(positionPacket.getLat(), positionPacket.getLon()); -// double heading = 360.0 / 0xffff * positionPacket.getHeading(); - boatGroup.setDestination( - p2d.getX(), p2d.getY(), heading, positionPacket.getGroundSpeed(), - positionPacket.getTimeValid(), frameRate); -// } catch (InterruptedException e){ +// Point2D p2d = findScaledXY(positionPacket.getLat(), positionPacket.getLon()); +// markGroup.moveMarkTo(p2d.getX(), p2d.getY(), raceId); +// } catch (InterruptedException e) { // e.printStackTrace(); // } -//// } // } - } - - private void updateMarkGroup (long raceId, MarkGroup markGroup) { - PriorityBlockingQueue movementQueue = StreamParser.markLocations.get(raceId); - if (movementQueue.size() > 0){ - try { - PositionUpdateData positionPacket = movementQueue.take(); - Point2D p2d = findScaledXY(positionPacket.getLat(), positionPacket.getLon()); - markGroup.moveMarkTo(p2d.getX(), p2d.getY(), raceId); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } +// } /** * Draws all the boats. */ - private void initializeBoats() { - Map boats = StreamParser.getBoats(); - Group wakes = new Group(); - Group trails = new Group(); - Group annotations = new Group(); + public void setBoats(List boats) { + Group annotationsGroup = new Group(); + Group wakesGroup = new Group(); + Group boatObjectGroup = new Group(); - ArrayList participants = StreamParser.getXmlObject().getRaceXML() - .getParticipants(); - ArrayList participantIDs = new ArrayList<>(); - for (Participant p : participants) { - participantIDs.add(p.getsourceID()); - } + BoatObject newObject; + for (Boat boat : boats) { + newObject = new BoatObject(); +// newObject.bindBoat(boat); + newObject.setFill(Colors.getColor()); + createAnnotationBox(boat); - for (Boat boat : boats.values()) { - if (participantIDs.contains(boat.getSourceID())) { - boat.setColour(Colors.getColor()); - BoatGroup boatGroup = new BoatGroup(boat, boat.getColour()); - boatGroups.add(boatGroup); - trails.getChildren().add(boatGroup.getTrail()); - wakes.getChildren().add(boatGroup.getWake()); - annotations.getChildren().add(boatGroup.getAnnotations()); - } } - gameObjects.addAll(trails); - gameObjects.addAll(wakes); - gameObjects.addAll(annotations); - gameObjects.addAll(boatGroups); +// Group wakes = new Group(); +// Group trails = new Group(); +// Group annotationsGroup = new Group(); +// +// gameObjects.addAll(trails); +// gameObjects.addAll(wakes); + annotationsGroup.getChildren().addAll(annotations); + gameObjects.addAll(annotationsGroup); + gameObjects.addAll(boatObjects.values()); } - private void initializeMarks() { - List allMarks = StreamParser.getXmlObject().getRaceXML().getNonDupCompoundMarks(); - for (Mark mark : allMarks) { + private AnnotationBox createAnnotationBox (Boat boat) { + AnnotationBox newAnnotation; + newAnnotation = new AnnotationBox(); + newAnnotation.addAnnotation("name", boat.getShortName()); +// newAnnotation.addAnnotation("country", boat.getCountry()); + newAnnotation.addAnnotation( + "velocity", + boat.getVelocityProperty(), + (velocity) -> String.format("%.2f ms", velocity.doubleValue()) + ); + newAnnotation.addAnnotation( + "nextMark", + boat.timeTillNextProperty(), + (time) -> { + DateFormat format = new SimpleDateFormat("mm:ss"); + return format.format(time); + } + ); + newAnnotation.addAnnotation( + "lastMark", + boat.timeTillNextProperty(), + (time) -> { + DateFormat format = new SimpleDateFormat("mm:ss"); + return format.format(time); + } + ); + annotations.add(newAnnotation); + return newAnnotation; + } + + public void updateCourse(List course) { + for (Mark mark : course) { if (mark.getMarkType() == MarkType.SINGLE_MARK) { SingleMark sMark = (SingleMark) mark; - MarkGroup markGroup = new MarkGroup(sMark, findScaledXY(sMark)); - markGroups.add(markGroup); + MarkObject markObject = new MarkObject(sMark, findScaledXY(sMark)); + markObjects.put(sMark, markObject); } else { GateMark gMark = (GateMark) mark; - MarkGroup markGroup = new MarkGroup(gMark, findScaledXY(gMark.getSingleMark1()), + MarkObject markObject = new MarkObject(gMark, findScaledXY(gMark.getSingleMark1()), findScaledXY(gMark.getSingleMark2())); //should be 2 objects in the list. - markGroups.add(markGroup); +// markObjects.put(markObject.); } } - gameObjects.addAll(markGroups); +// gameObjects.addAll(markObjects); } private void drawFps(int fps){ @@ -340,12 +337,11 @@ public class GameView extends Pane { */ private void fitMarksToCanvas() { //Check is called once to avoid unnecessarily change the course limits once the race is running - StreamParser.isNewRaceXmlReceived(); findMinMaxPoint(); double minLonToMaxLon = scaleRaceExtremities(); calculateReferencePointLocation(minLonToMaxLon); //givePointsXY(); - addRaceBorder(); +// updateBorder(); } @@ -356,9 +352,9 @@ public class GameView extends Pane { */ private void findMinMaxPoint() { List sortedPoints = new ArrayList<>(); - for (Limit limit : raceData) { - sortedPoints.add(limit); - } +// for (Limit limit : ) { +// sortedPoints.add(limit); +// } sortedPoints.sort(Comparator.comparingDouble(Limit::getLat)); Limit minLatMark = sortedPoints.get(0); Limit maxLatMark = sortedPoints.get(sortedPoints.size()-1); @@ -503,8 +499,14 @@ public class GameView extends Pane { public void setAnnotationVisibilities (boolean teamName, boolean velocity, boolean estTime, boolean legTime, boolean trail, boolean wake) { - for (BoatGroup boatGroup : boatGroups) { - boatGroup.setVisibility(teamName, velocity, estTime, legTime, trail, wake); + for (BoatObject boatObject : boatObjects.values()) { + boatObject.setVisibility(teamName, velocity, estTime, legTime, trail, wake); + } + for (AnnotationBox ag : annotations) { + ag.setAnnotationVisibility("name", teamName); + ag.setAnnotationVisibility("velocity", velocity); + ag.setAnnotationVisibility("nextMark", estTime); + ag.setAnnotationVisibility("lastMark", legTime); } } @@ -516,8 +518,10 @@ public class GameView extends Pane { return fpsDisplay.visibleProperty(); } - public void selectBoat (int boatId) { - + public void selectBoat (Boat selectedBoat) { + boatObjects.forEach((boat, group) -> + group.setIsSelected(boat == selectedBoat) + ); } public void pauseRace () { @@ -527,8 +531,4 @@ public class GameView extends Pane { public void startRace () { timer.start(); } - - public void updateBorder (List courseLimit) { - - } } diff --git a/src/main/java/seng302/visualiser/controllers/RaceViewController.java b/src/main/java/seng302/visualiser/controllers/RaceViewController.java index ba0557bb..4a888f99 100644 --- a/src/main/java/seng302/visualiser/controllers/RaceViewController.java +++ b/src/main/java/seng302/visualiser/controllers/RaceViewController.java @@ -1,16 +1,21 @@ package seng302.visualiser.controllers; +import com.sun.javafx.collections.SortableList; +import java.util.concurrent.TimeUnit; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.geometry.Point2D; import javafx.scene.Scene; + import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; +import javafx.scene.chart.XYChart.Data; import javafx.scene.chart.XYChart.Series; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; @@ -35,8 +40,8 @@ import seng302.visualiser.controllers.annotations.Annotation; import seng302.visualiser.controllers.annotations.ImportantAnnotationController; import seng302.visualiser.controllers.annotations.ImportantAnnotationDelegate; import seng302.visualiser.controllers.annotations.ImportantAnnotationsState; -import seng302.visualiser.fxObjects.BoatGroup; -import seng302.visualiser.fxObjects.MarkGroup; +import seng302.visualiser.fxObjects.BoatObject; +import seng302.visualiser.fxObjects.MarkObject; import seng302.model.*; import seng302.model.mark.GateMark; import seng302.model.mark.Mark; @@ -45,15 +50,14 @@ import seng302.model.stream.parsers.StreamParser; import java.io.IOException; import java.util.*; -import seng302.model.stream.parsers.xml.XMLParser.RaceXMLObject.Participant; /** - * Created by ptg19 on 29/03/17. + * Controller class that manages the display of a race */ public class RaceViewController extends Thread implements ImportantAnnotationDelegate { @FXML - private LineChart raceSparkLine; + private LineChart raceSparkLine; @FXML private NumberAxis sparklineYAxis; @FXML @@ -75,15 +79,14 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel //Race Data private Map participants; - private Map course; - private RaceXMLData raceData; + private Map markers; + private RaceXMLData courseData; private GameView gameView; + private RaceState raceState; private Timeline timerTimeline; - private Stage stage; private HashMap> sparkLineData = new HashMap<>(); private ImportantAnnotationsState importantAnnotations; - private Boat selectedBoat; public void initialize() { // Load a default important annotation state @@ -96,20 +99,26 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel raceSparkLine.getYAxis().setAutoRanging(false); sparklineYAxis.setTickMarkVisible(false); + selectAnnotationBtn.setOnAction(event -> loadSelectAnnotationView()); + } + + public void loadRace (Map participants, RaceXMLData raceData, RaceState raceState) { + this.participants = participants; + this.courseData = raceData; + this.markers = raceData.getCompoundMarks(); + this.raceState = raceState; + initializeUpdateTimer(); initialiseFPSCheckBox(); initialiseAnnotationSlider(); initialiseBoatSelectionComboBox(); - selectAnnotationBtn.setOnAction(event -> loadSelectAnnotationView()); - } - - public void loadRace (Map participants, RaceXMLData raceData) { - this.participants = participants; - this.raceData = raceData; - this.course = raceData.getCompoundMarks(); gameView = new GameView(); + gameView.setBoats(new ArrayList<>(participants.values())); + gameView.updateBorder(raceData.getCourseLimit()); + gameView.updateCourse(new ArrayList<>(raceData.getCompoundMarks().values())); gameView.startRace(); + } /** @@ -169,7 +178,6 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel if (n == 2) { return "All"; } - return "All"; } @@ -189,7 +197,7 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel } }); - annotationSlider.valueProperty().addListener((obs, oldval, newVal) -> + annotationSlider.valueProperty().addListener((obs, oldVal, newVal) -> setAnnotations((int) annotationSlider.getValue()) ); annotationSlider.setValue(2); @@ -202,10 +210,11 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel private void updateSparkLine(){ // Collect the racing boats that aren't already in the chart List sparkLineCandidates = new ArrayList<>(); - participants.forEach((id, boat) ->{ - if (!sparkLineData.containsKey(id) && boat.getPosition() != null && !boat.getPosition().equals("-")) - sparkLineCandidates.add(boat); - }); +// participants.forEach((id, boat) ->{ +// if (!sparkLineData.containsKey(id) && boat.getPosition() != null && !boat.getPosition().equals("-")) +// sparkLineCandidates.add(boat); +// }); + participants.forEach((id, boat) -> sparkLineCandidates.add(boat)); sparklineYAxis.setUpperBound(participants.size() + 1); @@ -213,13 +222,18 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel sparkLineCandidates.stream().filter(yacht -> yacht.getPosition() != null).forEach(yacht -> { Series yachtData = new Series<>(); yachtData.setName(yacht.getSourceID().toString()); - yachtData.getData().add(new XYChart.Data<>(Integer.toString(yacht.getLegNumber()), 1 + participants.size() - Double.parseDouble(yacht.getPosition()))); + yachtData.getData().add( + new XYChart.Data<>( + Integer.toString(yacht.getLegNumber()), + 1.0 + participants.size() - yacht.getPosition() + ) + ); sparkLineData.put(yacht.getSourceID(), yachtData); }); // Lambda function to sort the series in order of leg (later legs shown more to the right) List> positions = new ArrayList<>(sparkLineData.values()); - Collections.sort(positions, (o1, o2) -> { + positions.sort((o1, o2) -> { Integer leg1 = Integer.parseInt(o1.getData().get(o1.getData().size()-1).getXValue()); Integer leg2 = Integer.parseInt(o2.getData().get(o2.getData().size()-1).getXValue()); if (leg2 < leg1){ @@ -247,13 +261,21 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel * @param legNumber the leg number that the position will be assigned to */ public void updateYachtPositionSparkline(Boat boat, Integer legNumber){ - XYChart.Series positionData = sparkLineData.get(boat.getSourceID()); - positionData.getData().add( - new XYChart.Data<>( - Integer.toString(legNumber), - 1 + participants.size() - Double.parseDouble(boat.getPosition()) - ) - ); + for (XYChart.Series positionData : sparkLineData.values()) { + positionData.getData().add( + new Data<>( + Integer.toString(legNumber), + 1.0 + participants.size() - boat.getPosition() + ) + ); + } +// XYChart.Series positionData = sparkLineData.get(boat.getSourceID()); +// positionData.getData().add( +// new XYChart.Data<>( +// Integer.toString(legNumber), +// 1.0 + participants.size() - boat.getPosition() +// ) +// ); } @@ -306,10 +328,10 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel * @param bg The BoatGroup to find the next mark of * @return The next Mark or null if none found */ - private Mark getNextMark(BoatGroup bg) { + private Mark getNextMark(BoatObject bg) { Integer legNumber = bg.getBoat().getLegNumber(); - List markSequence = raceData.getMarkSequence(); + List markSequence = courseData.getMarkSequence(); if (legNumber == 0) { return null; @@ -319,7 +341,7 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel for (Corner corner : markSequence) { if (legNumber + 2 == corner.getSeqID()) { - return raceData.getCompoundMarks().get(corner.getCompoundMarkID()); + return courseData.getCompoundMarks().get(corner.getCompoundMarkID()); } } return null; @@ -330,8 +352,8 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel * Updates the wind direction arrow and text as from info from the StreamParser */ private void updateWindDirection() { - windDirectionText.setText(String.format("%.1f°", StreamParser.getWindDirection())); - windArrowText.setRotate(StreamParser.getWindDirection()); + windDirectionText.setText(String.format("%.1f°", raceState.getWindDirection())); + windArrowText.setRotate(raceState.getWindDirection()); } @@ -339,26 +361,14 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel * Updates the clock for the race */ private void updateRaceTime() { - if (StreamParser.isRaceFinished()) { + if (!raceState.isRaceStarted()) { timerLabel.setFill(Color.RED); timerLabel.setText("Race Finished!"); } else { - timerLabel.setText(getTimeSinceStartOfRace()); + timerLabel.setText(raceState.getRaceTimeStr()); } } - - /** - * Grabs the boats currently in the race as from the StreamParser and sets them to be selectable - * in the boat selection combo box - */ - private void updateBoatSelectionComboBox() { - ObservableList observableBoats = FXCollections.observableArrayList(); - observableBoats.addAll(participants.values()); - boatSelectionComboBox.setItems(observableBoats); - } - - /** * Updates the order of the boats as from the StreamParser and sets them in the boat order * section @@ -369,101 +379,98 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel positionVbox.getStylesheets().add(getClass().getResource("/css/master.css").toString()); // list of racing boat id + List sorted = new ArrayList<>(participants.values()); + sorted.sort(Comparator.comparingInt(Boat::getPosition)); - if (StreamParser.isRaceStarted()) { - for (Boat boat : StreamParser.getBoatsPos().values()) { - if (participantIDs.contains(boat.getSourceID())) { // check if the boat is racing - if (boat.getBoatStatus() == 3) { // 3 is finish status - Text textToAdd = new Text(boat.getPosition() + ". " + - boat.getShortName() + " (Finished)"); - textToAdd.setFill(Paint.valueOf("#d3d3d3")); - positionVbox.getChildren().add(textToAdd); + for (Boat boat : sorted) { + if (boat.getBoatStatus() == 3) { // 3 is finish status + Text textToAdd = new Text(boat.getPosition() + ". " + + boat.getShortName() + " (Finished)"); + textToAdd.setFill(Paint.valueOf("#d3d3d3")); + positionVbox.getChildren().add(textToAdd); - } else { - Text textToAdd = new Text(boat.getPosition() + ". " + - boat.getShortName() + " "); - textToAdd.setFill(Paint.valueOf("#d3d3d3")); - textToAdd.setStyle(""); - positionVbox.getChildren().add(textToAdd); - } - } - } - } else { - participants.forEach((id, boat) ->{ + } else { Text textToAdd = new Text(boat.getPosition() + ". " + boat.getShortName() + " "); textToAdd.setFill(Paint.valueOf("#d3d3d3")); textToAdd.setStyle(""); positionVbox.getChildren().add(textToAdd); - }); - } - } - - - private void updateLaylines(BoatGroup bg) { - - Mark nextMark = getNextMark(bg); - Boolean isUpwind = null; - // Can only calc leg direction if there is a next mark and it is a gate mark - if (nextMark != null) { - if (nextMark instanceof GateMark) { - if (bg.isUpwindLeg(gameViewController, nextMark)) { - isUpwind = true; - } else { - isUpwind = false; - } - - for(MarkGroup mg : gameViewController.getMarkGroups()) { - - mg.removeLaylines(); - - if (mg.getMainMark().getId() == nextMark.getId()) { - - SingleMark singleMark1 = ((GateMark) nextMark).getSingleMark1(); - SingleMark singleMark2 = ((GateMark) nextMark).getSingleMark2(); - Point2D markPoint1 = gameViewController - .findScaledXY(singleMark1.getLatitude(), singleMark1.getLongitude()); - Point2D markPoint2 = gameViewController - .findScaledXY(singleMark2.getLatitude(), singleMark2.getLongitude()); - HashMap angleAndSpeed; - if (isUpwind) { - angleAndSpeed = PolarTable.getOptimalUpwindVMG(StreamParser.getWindSpeed()); - } else { - angleAndSpeed = PolarTable.getOptimalDownwindVMG(StreamParser.getWindSpeed()); - } - - Double resultingAngle = angleAndSpeed.keySet().iterator().next(); - - - Point2D boatCurrentPos = new Point2D(bg.getBoatLayoutX(), bg.getBoatLayoutY()); - Point2D gateMidPoint = markPoint1.midpoint(markPoint2); - Integer lineFuncResult = GeoUtility.lineFunction(boatCurrentPos, gateMidPoint, markPoint2); - Line rightLayline = new Line(); - Line leftLayline = new Line(); - if (lineFuncResult == 1) { - rightLayline = makeRightLayline(markPoint2, 180 - resultingAngle, StreamParser.getWindDirection()); - leftLayline = makeLeftLayline(markPoint1, 180 - resultingAngle, StreamParser.getWindDirection()); - } else if (lineFuncResult == -1) { - rightLayline = makeRightLayline(markPoint1, 180 - resultingAngle, StreamParser.getWindDirection()); - leftLayline = makeLeftLayline(markPoint2, 180 - resultingAngle, StreamParser.getWindDirection()); - } - - leftLayline.setStrokeWidth(0.5); - leftLayline.setStroke(bg.getBoat().getColour()); - - rightLayline.setStrokeWidth(0.5); - rightLayline.setStroke(bg.getBoat().getColour()); - - bg.setLaylines(leftLayline, rightLayline); - mg.addLaylines(leftLayline, rightLayline); - - } - } } } +// participants.forEach((id, boat) ->{ +// Text textToAdd = new Text(boat.getPosition() + ". " + +// boat.getShortName() + " "); +// textToAdd.setFill(Paint.valueOf("#d3d3d3")); +// textToAdd.setStyle(""); +// positionVbox.getChildren().add(textToAdd); +// }); } +// private void updateLaylines(BoatObject bg) { +// +// Mark nextMark = getNextMark(bg); +// Boolean isUpwind = null; +// // Can only calc leg direction if there is a next mark and it is a gate mark +// if (nextMark != null) { +// if (nextMark instanceof GateMark) { +// if (bg.isUpwindLeg(gameViewController, nextMark)) { +// isUpwind = true; +// } else { +// isUpwind = false; +// } +// +// for(MarkObject mg : gameViewController.getMarkGroups()) { +// +// mg.removeLaylines(); +// +// if (mg.getMainMark().getId() == nextMark.getId()) { +// +// SingleMark singleMark1 = ((GateMark) nextMark).getSingleMark1(); +// SingleMark singleMark2 = ((GateMark) nextMark).getSingleMark2(); +// Point2D markPoint1 = gameViewController +// .findScaledXY(singleMark1.getLatitude(), singleMark1.getLongitude()); +// Point2D markPoint2 = gameViewController +// .findScaledXY(singleMark2.getLatitude(), singleMark2.getLongitude()); +// HashMap angleAndSpeed; +// if (isUpwind) { +// angleAndSpeed = PolarTable.getOptimalUpwindVMG(StreamParser.getWindSpeed()); +// } else { +// angleAndSpeed = PolarTable.getOptimalDownwindVMG(StreamParser.getWindSpeed()); +// } +// +// Double resultingAngle = angleAndSpeed.keySet().iterator().next(); +// +// +// Point2D boatCurrentPos = new Point2D(bg.getBoatLayoutX(), bg.getBoatLayoutY()); +// Point2D gateMidPoint = markPoint1.midpoint(markPoint2); +// Integer lineFuncResult = GeoUtility.lineFunction(boatCurrentPos, gateMidPoint, markPoint2); +// Line rightLayline = new Line(); +// Line leftLayline = new Line(); +// if (lineFuncResult == 1) { +// rightLayline = makeRightLayline(markPoint2, 180 - resultingAngle, StreamParser.getWindDirection()); +// leftLayline = makeLeftLayline(markPoint1, 180 - resultingAngle, StreamParser.getWindDirection()); +// } else if (lineFuncResult == -1) { +// rightLayline = makeRightLayline(markPoint1, 180 - resultingAngle, StreamParser.getWindDirection()); +// leftLayline = makeLeftLayline(markPoint2, 180 - resultingAngle, StreamParser.getWindDirection()); +// } +// +// leftLayline.setStrokeWidth(0.5); +// leftLayline.setStroke(bg.getBoat().getColour()); +// +// rightLayline.setStrokeWidth(0.5); +// rightLayline.setStroke(bg.getBoat().getColour()); +// +// bg.setLaylines(leftLayline, rightLayline); +// mg.addLaylines(leftLayline, rightLayline); +// +// } +// } +// } +// } +// } + + private Point2D getPointRotation(Point2D ref, Double distance, Double angle){ Double newX = ref.getX() + (ref.getX() + distance -ref.getX())*Math.cos(angle) - (ref.getY() + distance -ref.getY())*Math.sin(angle); Double newY = ref.getY() + (ref.getX() + distance -ref.getX())*Math.sin(angle) + (ref.getY() + distance -ref.getY())*Math.cos(angle); @@ -495,17 +502,27 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel * for the combobox to take action upon selection */ private void initialiseBoatSelectionComboBox() { - updateBoatSelectionComboBox(); - boatSelectionComboBox.valueProperty().addListener((observable, oldValue, newValue) -> { - //This listener is fired whenever the combo box changes. This means when the values are updated - //We dont want to set the selected value if the values are updated but nothing clicked (null) - if (newValue != null && newValue != selectedBoat) { - Boat thisBoat = (Boat) newValue; - setSelectedBoat(thisBoat); + boatSelectionComboBox.setItems( + FXCollections.observableArrayList(participants.values()) + ); + //Null check is if the listener is fired but nothing selected + boatSelectionComboBox.valueProperty().addListener((obs, lastSelection, selectedBoat) -> { + if (selectedBoat != null) { + gameView.selectBoat(selectedBoat); } }); } + /** + * Grabs the boats currently in the race as from the StreamParser and sets them to be selectable + * in the boat selection combo box + */ + private void updateBoatSelectionComboBox() { + ObservableList observableBoats = FXCollections.observableArrayList(); + observableBoats.addAll(participants.values()); + boatSelectionComboBox.setItems(observableBoats); + } + /** * Display the list of boats in the order they finished the race @@ -525,38 +542,12 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel } } - - /** - * Convert seconds to a string of the format mm:ss - * - * @param time the time in seconds - * @return a formatted string - */ - public String convertTimeToMinutesSeconds(int time) { - if (time < 0) { - return String.format("-%02d:%02d", (time * -1) / 60, (time * -1) % 60); - } - return String.format("%02d:%02d", time / 60, time % 60); - } - - private String getTimeSinceStartOfRace() { - String timerString = "0:00"; - if (StreamParser.getTimeSinceStart() > 0) { - String timerMinute = Long.toString(StreamParser.getTimeSinceStart() / 60); - String timerSecond = Long.toString(StreamParser.getTimeSinceStart() % 60); - if (timerSecond.length() == 1) { - timerSecond = "0" + timerSecond; - } - timerString = "-" + timerMinute + ":" + timerSecond; - } else { - String timerMinute = Long.toString(-1 * StreamParser.getTimeSinceStart() / 60); - String timerSecond = Long.toString(-1 * StreamParser.getTimeSinceStart() % 60); - if (timerSecond.length() == 1) { - timerSecond = "0" + timerSecond; - } - timerString = timerMinute + ":" + timerSecond; - } - return timerString; + private String getMillisToFormattedTime(long milliseconds) { + return String.format("%02d:%02d:%02d", + TimeUnit.MILLISECONDS.toHours(milliseconds), + TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60, //Modulus 60 minutes per hour + TimeUnit.MILLISECONDS.toSeconds(milliseconds) % 60 //Modulus 60 seconds per minute + ); } private void setAnnotations(Integer annotationLevel) { @@ -594,38 +585,21 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel * @param boat The yacht for which we want to view all annotations */ private void setSelectedBoat(Boat boat) { - for (BoatGroup bg : gameViewController.getBoatGroups()) { - //We need to iterate over all race groups to get the matching boat group belonging to this boat if we - //are to toggle its annotations, there is no other backwards knowledge of a yacht to its boatgroup. - if (bg.getBoat().getHullID().equals(boat.getHullID())) { - updateLaylines(bg); - bg.setIsSelected(true); - selectedBoat = boat; - } else { - bg.setIsSelected(false); - } - } - } - - void setStage(Stage stage) { - this.stage = stage; - } - - Stage getStage() { - return stage; - } - - /** - * Used for when the boat attempts to add data to the sparkline (first checks if the sparkline contains info on it) - * @param yachtId - * @return - */ - public static boolean sparkLineStatus(Integer yachtId) { - return sparkLineData.containsKey(yachtId); +// for (BoatObject bg : gameViewController.getBoatGroups()) { +// //We need to iterate over all race groups to get the matching boat group belonging to this boat if we +// //are to toggle its annotations, there is no other backwards knowledge of a yacht to its boatgroup. +// if (bg.getBoat().getHullID().equals(boat.getHullID())) { +//// updateLaylines(bg); +// bg.setIsSelected(true); +//// selectedBoat = boat; +// } else { +// bg.setIsSelected(false); +// } +// } } public void updateRaceData (RaceXMLData raceData) { - this.raceData = raceData; + this.courseData = raceData; gameView.updateBorder(raceData.getCourseLimit()); } diff --git a/src/main/java/seng302/visualiser/controllers/StartScreenController.java b/src/main/java/seng302/visualiser/controllers/StartScreenController.java index 3ce16e52..90e2f2a6 100644 --- a/src/main/java/seng302/visualiser/controllers/StartScreenController.java +++ b/src/main/java/seng302/visualiser/controllers/StartScreenController.java @@ -61,7 +61,6 @@ public class StartScreenController { ClientToServerThread clientToServerThread = new ClientToServerThread("localhost", 4950); // controller.setClientToServerThread(clientToServerThread); clientToServerThread.start(); -// new GameServerThread("Fuck you"); // get the lobby controller so that we can pass the game server thread to it setContentPane("/views/LobbyView.fxml"); diff --git a/src/main/java/seng302/visualiser/controllers/client/ClientController.java b/src/main/java/seng302/visualiser/controllers/client/ClientController.java index a9011680..c22d305b 100644 --- a/src/main/java/seng302/visualiser/controllers/client/ClientController.java +++ b/src/main/java/seng302/visualiser/controllers/client/ClientController.java @@ -1,20 +1,18 @@ package seng302.visualiser.controllers.client; -import java.text.DateFormat; -import java.text.SimpleDateFormat; +import java.io.IOException; import java.time.ZoneId; import java.time.ZoneOffset; -import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.TimeZone; import javafx.fxml.FXMLLoader; import javafx.scene.layout.Pane; import seng302.model.Boat; +import seng302.model.RaceState; import seng302.model.mark.Mark; import seng302.model.stream.parsers.PositionUpdateData.DeviceType; import seng302.model.stream.parsers.MarkRoundingData; -import seng302.model.stream.parsers.RaceStartData; import seng302.model.stream.parsers.RaceStatusData; import seng302.model.stream.parsers.xml.RaceXMLData; import seng302.model.stream.parsers.StreamParser; @@ -22,7 +20,6 @@ import seng302.model.stream.parsers.xml.RegattaXMLData; import seng302.model.stream.parsers.xml.XMLParser; import seng302.model.stream.parsers.PositionUpdateData; import seng302.model.stream.packets.StreamPacket; -import seng302.visualiser.ClientSocketListener; import seng302.visualiser.ClientToServerThread; import seng302.visualiser.controllers.RaceViewController; @@ -31,35 +28,37 @@ import seng302.visualiser.controllers.RaceViewController; */ public class ClientController { - private final DateFormat DATE_TIME_FORMAT = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); - private Pane holderPane; private ClientToServerThread socketThread; - private ClientSocketListener socketListener; private RaceViewController raceView; private Map allBoatsMap; private Map racingBoats = new HashMap<>(); private RegattaXMLData regattaData; - private RaceXMLData raceData; + private RaceXMLData courseData; + private RaceState raceState = new RaceState(); public ClientController (String ipAddress, Pane holder) { this.holderPane = holder; socketThread = new ClientToServerThread(ipAddress, 4950); socketThread.start(); - socketListener = this::parsePacket; - socketThread.addStreamObserver(socketListener); + socketThread.addStreamObserver(this::parsePacket); } private void loadRaceView () { allBoatsMap.forEach((id, boat) -> { - if (raceData.getParticipants().contains(id)) + if (courseData.getParticipants().contains(id)) racingBoats.put(id, boat); }); FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("RaceView.fxml")); raceView = fxmlLoader.getController(); - raceView.loadRace(racingBoats, raceData); + try { + holderPane.getChildren().add(fxmlLoader.load()); + } catch (IOException e) { + e.printStackTrace(); + } + raceView.loadRace(racingBoats, courseData, raceState); } private void parsePacket(StreamPacket packet) { @@ -72,49 +71,47 @@ public class ClientController { regattaData = XMLParser.parseRegatta( StreamParser.extractXmlMessage(packet) ); - DATE_TIME_FORMAT.setTimeZone( + raceState.setTimeZone( TimeZone.getTimeZone( ZoneId.ofOffset("UTC", ZoneOffset.ofHours(regattaData.getUtcOffset())) ) ); - startRaceIfAllDataRecieved(); + startRaceIfAllDataReceived(); break; case RACE_XML: - raceData = XMLParser.parseRace( + courseData = XMLParser.parseRace( StreamParser.extractXmlMessage(packet) ); if (raceView != null) { - raceView.updateRaceData(raceData); + raceView.updateRaceData(courseData); } - startRaceIfAllDataRecieved(); + startRaceIfAllDataReceived(); break; case BOAT_XML: allBoatsMap = XMLParser.parseBoats( StreamParser.extractXmlMessage(packet) ); - startRaceIfAllDataRecieved(); + startRaceIfAllDataReceived(); break; case RACE_START_STATUS: - RaceStartData raceStartData = StreamParser.extractRaceStartStatus(packet); + raceState.updateState(StreamParser.extractRaceStartStatus(packet)); break; case BOAT_LOCATION: - PositionUpdateData positionData = StreamParser.extractBoatLocation(packet); - updatePosition(positionData); + updatePosition(StreamParser.extractBoatLocation(packet)); break; case MARK_ROUNDING: - MarkRoundingData roundingData = StreamParser.extractMarkRounding(packet); - updateMarkRounding(roundingData); + updateMarkRounding(StreamParser.extractMarkRounding(packet)); break; } } - private void startRaceIfAllDataRecieved () { - if (raceData != null && allBoatsMap != null && regattaData != null) + private void startRaceIfAllDataReceived() { + if (courseData != null && allBoatsMap != null && regattaData != null) loadRaceView(); } @@ -129,8 +126,8 @@ public class ClientController { boat.setLat(positionData.getLat()); boat.setLon(positionData.getLon()); boat.setHeading(positionData.getHeading()); - } else { - Mark mark = raceData.getCompoundMarks().get(positionData.getDeviceId()); + } else if (positionData.getType() == DeviceType.MARK_TYPE) { + Mark mark = courseData.getCompoundMarks().get(positionData.getDeviceId()); } } @@ -142,21 +139,19 @@ public class ClientController { private void updateMarkRounding(MarkRoundingData roundingData) { Boat boat = racingBoats.get(roundingData.getBoatId()); boat.setMarkRoundingTime(roundingData.getTimeStamp()); + boat.setTimeSinceLastMark(raceState.getRaceTime() - roundingData.getTimeStamp()); boat.setLastMarkRounded( - raceData.getCompoundMarks().get( + courseData.getCompoundMarks().get( roundingData.getMarkId() ) ); } private void processRaceStatusUpdate (RaceStatusData data) { - String raceTimeStr = DATE_TIME_FORMAT.format(data.getCurrentTime()); - Date date = new Date(); - date.getTime(); - long timeTillStart = (data.getExpectedStartTime() - data.getCurrentTime()) / 1000; + raceState.updateState(data); for (long[] boatData : data.getBoatData()) { Boat boat = allBoatsMap.get((int) boatData[0]); - boat.setEstimateTimeAtNextMark(boatData[1]); + boat.setEstimateTimeTillNextMark(raceState.getRaceTime() - boatData[1]); boat.setEstimateTimeAtFinish(boatData[2]); int legNumber = (int) boatData[3]; boat.setLegNumber(legNumber); @@ -172,6 +167,10 @@ public class ClientController { } } + private void close () { + socketThread.closeSocket(); + } + // /** Handle the key-pressed event from the text field. */ // public void keyPressed(KeyEvent e) { // BoatActionMessage boatActionMessage; @@ -201,7 +200,7 @@ public class ClientController { // break; // } // } -// + // public void keyReleased(KeyEvent e) { // switch (e.getCode()) { // //TODO 12/07/17 Determine the sail state and send the appropriate packet (eg. if sails are in, send a sail out packet) diff --git a/src/main/java/seng302/visualiser/fxObjects/AnnotationBox.java b/src/main/java/seng302/visualiser/fxObjects/AnnotationBox.java new file mode 100644 index 00000000..3c257e71 --- /dev/null +++ b/src/main/java/seng302/visualiser/fxObjects/AnnotationBox.java @@ -0,0 +1,185 @@ +package seng302.visualiser.fxObjects; + +import java.util.HashMap; +import java.util.Map; +import javafx.beans.value.ObservableValue; +import javafx.scene.CacheHint; +import javafx.scene.Group; +import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; +import javafx.scene.shape.Rectangle; +import javafx.scene.text.Text; + +/** + * Grouping of string objects over a semi transparent background. + */ +public class AnnotationBox extends Group{ + + @FunctionalInterface + public interface AnnotationFormatter { + String transformString (T input); + } + + /** + * Class stores a text object and relationship for updating the text object if needed + * + * @param The type of observable value passed to the annotation, if there is one. + */ + public class Annotation { + private Text text; + private ObservableValue source; + private AnnotationFormatter format; + + /** + * Constructor for observing annotation + * @param textObject the javaFX text object the annotation is displayed in + * @param source observable value that the annotation is taken from + * @param formatter interface describing how to format the source data if needed + */ + public Annotation (Text textObject, ObservableValue source, AnnotationFormatter formatter) { + this.text = textObject; + this.source = source; + this.format = formatter; + source.addListener((obs, oldVal, newVal) -> + text.setText(format.transformString(newVal)) + ); + } + + /** + * Constructor for a static annotation + * @param textObject the javaFX text object the annotation is displayed in + * @param annotationText the static value of the test object + */ + public Annotation (Text textObject, String annotationText) { + textObject.setText(annotationText); + text = textObject; + } + + private Text getText () { + return text; + } + } + + //Text offset constants + private static final double X_OFFSET_TEXT = 18d; + private static final double Y_OFFSET_TEXT_INIT = -29d; + private static final double Y_OFFSET_PER_TEXT = 12d; + //Background constants + private static final double TEXT_BUFFER = 3; + private static final double BACKGROUND_X = X_OFFSET_TEXT - TEXT_BUFFER; + private static final double BACKGROUND_Y = Y_OFFSET_TEXT_INIT - TEXT_BUFFER; + private static final double BACKGROUND_H_PER_TEXT = 9.5d; + private static final double BACKGROUND_ARC_SIZE = 10; + + private int visibleAnnotations = 0; + private double backgroundWidth = 125d; + + private Rectangle background = new Rectangle(); + private Paint theme = Color.BLACK; + + private Map annotationsByName = new HashMap<>(); + + public AnnotationBox() { + this.setCache(true); + background.setX(BACKGROUND_X); + background.setY(BACKGROUND_Y); + background.setWidth(backgroundWidth); + background.setHeight(Math.abs(BACKGROUND_X) + TEXT_BUFFER + BACKGROUND_H_PER_TEXT * 4); + background.setArcHeight(BACKGROUND_ARC_SIZE); + background.setArcWidth(BACKGROUND_ARC_SIZE); + background.setFill(new Color(1, 1, 1, 0.75)); + background.setStroke(theme); + background.setStrokeWidth(2); + background.setCache(true); + background.setCacheHint(CacheHint.SPEED); + this.getChildren().add(background); + } + + public void addAnnotation (String annotationName, Annotation annotation) { + annotationsByName.put(annotationName, annotation); + this.getChildren().add(annotation.getText()); + visibleAnnotations++; + } + + public void addAnnotation (String annotationName, String annotationText) { + Text text = getTextObject(); + annotationsByName.put(annotationName, new Annotation(text, annotationText)); + } + + public void addAnnotation (String annotationName, ObservableValue observable) { + addAnnotation(annotationName, observable, E::toString); + } + + public void addAnnotation (String annotationName, ObservableValue observable, + AnnotationFormatter formatter) { + Text newText = getTextObject(); + annotationsByName.put(annotationName, new Annotation<>(newText, observable, formatter)); + this.getChildren().add(newText); + visibleAnnotations++; + } + + public void setAnnotationVisibility (String annotationName, boolean visibility) { + Text textField = annotationsByName.get(annotationName).text; + boolean currentState = textField.visibleProperty().get(); + + if (visibility != currentState) { + if (visibility) + visibleAnnotations++; + else + visibleAnnotations--; + } + textField.setVisible(visibility); + update(); + } + + public void removeAnnotation (String annotationName) { + this.getChildren().remove(annotationsByName.remove(annotationName).getText()); + annotationsByName.remove(annotationName); + visibleAnnotations--; + update(); + } + + public void bindLocation () { + + } + + public void setLocation (double x, double y) { + this.setLayoutX(x); + this.setLayoutY(y); + } + + public void setWidth (double width) { + backgroundWidth = width; + background.setWidth(backgroundWidth); + } + + private void update () { + background.setVisible(visibleAnnotations != 0); + background.setHeight(Math.abs(BACKGROUND_X) + TEXT_BUFFER + BACKGROUND_H_PER_TEXT * visibleAnnotations); + for (int i = 1; i <= visibleAnnotations; i++) { + Text text = (Text) this.getChildren().get(i); + if (text.visibleProperty().get()) + text.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT * Y_OFFSET_PER_TEXT * (i + 1)); + } + } + + /** + * Return a text object with caching and a color applied + * + * @return The text object + */ + private Text getTextObject() { + Text text = new Text(); + text.setFill(theme); + text.setStrokeWidth(2); + text.setCacheHint(CacheHint.SPEED); + text.setCache(true); + return text; + } + + public void setFill (Paint value) { + theme = value; + background.setStroke(theme); + annotationsByName.forEach((name, annotation) -> annotation.getText().setFill(theme)); + } +} diff --git a/src/main/java/seng302/visualiser/fxObjects/BoatAnnotations.java b/src/main/java/seng302/visualiser/fxObjects/BoatAnnotations.java deleted file mode 100644 index e0e3a040..00000000 --- a/src/main/java/seng302/visualiser/fxObjects/BoatAnnotations.java +++ /dev/null @@ -1,132 +0,0 @@ -package seng302.visualiser.fxObjects; - -import javafx.scene.CacheHint; -import javafx.scene.Group; -import javafx.scene.paint.Color; -import javafx.scene.shape.Rectangle; -import javafx.scene.text.Text; -import seng302.model.Boat; -import seng302.model.stream.parsers.StreamParser; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; - -/** - * Collection of annotations for boats. - */ -public class BoatAnnotations extends Group{ - - //Text offset constants - private static final double X_OFFSET_TEXT = 18d; - private static final double Y_OFFSET_TEXT_INIT = -29d; - private static final double Y_OFFSET_PER_TEXT = 12d; - //Background constants - private static final double TEXT_BUFFER = 3; - private static final double BACKGROUND_X = X_OFFSET_TEXT - TEXT_BUFFER; - private static final double BACKGROUND_Y = Y_OFFSET_TEXT_INIT - TEXT_BUFFER; - private static final double BACKGROUND_H_PER_TEXT = 9.5d; - private static final double BACKGROUND_W = 125d; - private static final double BACKGROUND_ARC_SIZE = 10; - - private Rectangle background = new Rectangle(); - private Text teamNameObject; - private Text velocityObject; - private Text estTimeToNextMarkObject; - private Text legTimeObject; - - private Boat boat; - - BoatAnnotations (Boat boat, Color theme) { - super.setCache(true); - this.boat = boat; - background.setX(BACKGROUND_X); - background.setY(BACKGROUND_Y); - background.setWidth(BACKGROUND_W); - background.setHeight(Math.abs(BACKGROUND_X) + TEXT_BUFFER + BACKGROUND_H_PER_TEXT * 4); - background.setArcHeight(BACKGROUND_ARC_SIZE); - background.setArcWidth(BACKGROUND_ARC_SIZE); - background.setFill(new Color(1, 1, 1, 0.75)); - background.setStroke(theme); - background.setStrokeWidth(2); - background.setCache(true); - background.setCacheHint(CacheHint.SPEED); - - teamNameObject = getTextObject(boat.getShortName(), theme); - teamNameObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT); - - velocityObject = getTextObject("0 m/s", theme); - velocityObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * 2); - - estTimeToNextMarkObject = getTextObject("Next mark: ", theme); - estTimeToNextMarkObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * 3); - - legTimeObject = getTextObject("Last mark: -", theme); - legTimeObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * 4); - - super.getChildren().addAll(background, teamNameObject, velocityObject, estTimeToNextMarkObject, legTimeObject); - } - - /** - * Return a text object with caching and a color applied - * - * @param defaultText The default text to display - * @param fill The text fill color - * @return The text object - */ - private Text getTextObject(String defaultText, Color fill) { - Text text = new Text(defaultText); - text.setFill(fill); - text.setStrokeWidth(2); - text.setCacheHint(CacheHint.SPEED); - text.setCache(true); - return text; - } - - void update () { - velocityObject.setText(String.format("%.2f m/s", boat.getVelocity())); - - DateFormat format = new SimpleDateFormat("mm:ss"); - if (boat.getTimeTillNext() != null) { - String timeToNextMark = format - .format(boat.getTimeTillNext() - StreamParser.getCurrentTimeLong()); - estTimeToNextMarkObject.setText("Next mark: " + timeToNextMark); - } else { - estTimeToNextMarkObject.setText("Next mark: -"); - } - - if (boat.getMarkRoundTime() != null) { - String elapsedTime = format - .format(StreamParser.getCurrentTimeLong() - boat.getMarkRoundTime()); - legTimeObject.setText("Last mark: " + elapsedTime); - }else { - legTimeObject.setText("Last mark: - "); - } - } - - void setVisibile (boolean nameVisibility, boolean speedVisibility, - boolean estTimeVisibility, boolean lastMarkVisibility) { - int totalVisible = 0; - totalVisible = updateVisibility(nameVisibility, teamNameObject, totalVisible); - totalVisible = updateVisibility(speedVisibility, velocityObject, totalVisible); - totalVisible = updateVisibility(estTimeVisibility, estTimeToNextMarkObject, totalVisible); - totalVisible = updateVisibility(lastMarkVisibility, legTimeObject, totalVisible); - if (totalVisible != 0) { - background.setVisible(true); - background.setHeight(Math.abs(BACKGROUND_X) + TEXT_BUFFER + BACKGROUND_H_PER_TEXT * totalVisible); - } else { - background.setVisible(false); - } - } - - private int updateVisibility (boolean visibility, Text text, int totalVisible) { - if (visibility){ - totalVisible ++; - text.setVisible(true); - text.setLayoutX(X_OFFSET_TEXT); - text.setLayoutY(Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * totalVisible); - } else { - text.setVisible(false); - } - return totalVisible; - } -} diff --git a/src/main/java/seng302/visualiser/fxObjects/BoatGroup.java b/src/main/java/seng302/visualiser/fxObjects/BoatObject.java similarity index 67% rename from src/main/java/seng302/visualiser/fxObjects/BoatGroup.java rename to src/main/java/seng302/visualiser/fxObjects/BoatObject.java index cbf51e9f..edf7640c 100644 --- a/src/main/java/seng302/visualiser/fxObjects/BoatGroup.java +++ b/src/main/java/seng302/visualiser/fxObjects/BoatObject.java @@ -6,10 +6,10 @@ import javafx.geometry.Point2D; import javafx.scene.CacheHint; import javafx.scene.Group; import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; import javafx.scene.shape.Line; import javafx.scene.shape.Polygon; import javafx.scene.transform.Rotate; -import seng302.visualiser.controllers.GameViewController; import seng302.model.Boat; import seng302.utilities.GeoUtility; import seng302.model.mark.GateMark; @@ -25,7 +25,7 @@ import seng302.model.stream.parsers.StreamParser; * minimized in which case it attempts to store animations and apply them when the window is * maximised. */ -public class BoatGroup extends Group { +public class BoatObject extends Group { //Constants for drawing private static final double BOAT_HEIGHT = 15d; @@ -47,79 +47,57 @@ public class BoatGroup extends Group { private Double distanceTravelled = 0.0; private Point2D lastPoint; private boolean destinationSet; - private BoatAnnotations boatAnnotations; + private AnnotationBox annotationBox; + + private Paint colour = Color.BLACK; private Boolean isSelected = true; //All boats are initialised as selected /** * Creates a BoatGroup with the default triangular boat polygon. - * - * @param boat The boat that the BoatGroup will represent. Must contain an ID which will be used - * to tell which BoatGroup to update. - * @param color The colour of the boat polygon and the trailing line. */ - public BoatGroup(Boat boat, Color color) { - destinationSet = false; - this.boat = boat; - initChildren(color); + public BoatObject() { + this(-BOAT_WIDTH / 2, BOAT_HEIGHT / 2, + 0.0, -BOAT_HEIGHT / 2, + BOAT_WIDTH / 2, BOAT_HEIGHT / 2); } /** * Creates a BoatGroup with the boat being the default polygon. The head of the boat should be * at point (0,0). * - * @param boat The boat that the BoatGroup will represent. Must contain an ID which will be used - * to tell which BoatGroup to update. - * @param color The colour of the boat polygon and the trailing line. * @param points An array of co-ordinates x1,y1,x2,y2,x3,y3... that will make up the boat * polygon. */ - public BoatGroup(Boat boat, Color color, double... points) { - destinationSet = false; - this.boat = boat; - initChildren(color, points); - } - - /** - * Creates the javafx objects that will be the in the group by default. - * - * @param color The colour of the boat polygon and the trailing line. - * @param points An array of co-ordinates x1,y1,x2,y2,x3,y3... that will make up the boat - * polygon. - */ - private void initChildren(Color color, double... points) { + public BoatObject(double... points) { boatPoly = new Polygon(points); - boatPoly.setFill(color); + boatPoly.setFill(colour); boatPoly.setOnMouseEntered(event -> { boatPoly.setFill(Color.FLORALWHITE); boatPoly.setStroke(Color.RED); }); boatPoly.setOnMouseExited(event -> { - boatPoly.setFill(color); + boatPoly.setFill(colour); boatPoly.setStroke(Color.BLACK); }); boatPoly.setOnMouseClicked(event -> setIsSelected(!isSelected)); boatPoly.setCache(true); boatPoly.setCacheHint(CacheHint.SPEED); - boatAnnotations = new BoatAnnotations(boat, color); + + annotationBox = new AnnotationBox(); + annotationBox.setFill(colour); leftLayLine = new Line(); rightLayline = new Line(); wake = new Wake(0, -BOAT_HEIGHT); - super.getChildren().addAll(boatPoly, boatAnnotations); + super.getChildren().addAll(boatPoly, annotationBox); } - /** - * Creates the javafx objects that will be the in the group by default. - * - * @param color The colour of the boat polygon and the trailing line. - */ - private void initChildren(Color color) { - initChildren(color, - -BOAT_WIDTH / 2, BOAT_HEIGHT / 2, - 0.0, -BOAT_HEIGHT / 2, - BOAT_WIDTH / 2, BOAT_HEIGHT / 2); + public void setFill (Paint value) { + this.colour = value; + boatPoly.setFill(colour); + annotationBox.setFill(colour); } /** @@ -132,8 +110,8 @@ public class BoatGroup extends Group { private void moveGroupBy(double dx, double dy) { boatPoly.setLayoutX(boatPoly.getLayoutX() + dx); boatPoly.setLayoutY(boatPoly.getLayoutY() + dy); - boatAnnotations.setLayoutX(boatAnnotations.getLayoutX() + dx); - boatAnnotations.setLayoutY(boatAnnotations.getLayoutY() + dy); + annotationBox.setLayoutX(annotationBox.getLayoutX() + dx); + annotationBox.setLayoutY(annotationBox.getLayoutY() + dy); wake.setLayoutX(wake.getLayoutX() + dx); wake.setLayoutY(wake.getLayoutY() + dy); } @@ -149,8 +127,8 @@ public class BoatGroup extends Group { rotateTo(rotation); boatPoly.setLayoutX(x); boatPoly.setLayoutY(y); - boatAnnotations.setLayoutX(x); - boatAnnotations.setLayoutY(y); + annotationBox.setLayoutX(x); + annotationBox.setLayoutY(y); wake.setLayoutX(x); wake.setLayoutY(y); wake.rotate(rotation); @@ -225,54 +203,52 @@ public class BoatGroup extends Group { lastTimeValid = timeValid; isStopped = false; lastRotation = rotation; - boatAnnotations.update(); } - /** - * This function works out if a boat is going upwind or down wind. It looks at the boats current position, the next - * gates position and the current wind - * If bot the wind vector from the next gate and the boat from the next gate lay on the same side, then the boat is - * going up wind, if they are on different sides of the gate, then the boat is going downwind - * @param canvasController - */ - public Boolean isUpwindLeg(GameViewController canvasController, Mark nextMark) { - - Double windAngle = StreamParser.getWindDirection(); - GateMark thisGateMark = (GateMark) nextMark; - SingleMark nextMark1 = thisGateMark.getSingleMark1(); - SingleMark nextMark2 = thisGateMark.getSingleMark2(); - Point2D nextMarkPoint1 = canvasController.findScaledXY(nextMark1.getLatitude(), nextMark1.getLongitude()); - Point2D nextMarkPoint2 = canvasController.findScaledXY(nextMark2.getLatitude(), nextMark2.getLongitude()); - - Point2D boatCurrentPoint = new Point2D(boatPoly.getLayoutX(), boatPoly.getLayoutY()); - Point2D windTestPoint = GeoUtility.makeArbitraryVectorPoint(nextMarkPoint1, windAngle, 10d); - - - Integer boatLineFuncResult = GeoUtility.lineFunction(nextMarkPoint1, nextMarkPoint2, boatCurrentPoint); - Integer windLineFuncResult = GeoUtility.lineFunction(nextMarkPoint1, nextMarkPoint2, windTestPoint); - - - /* - If both the wind vector from the gate and the boat from the gate are on the same side of that gate, then the - boat is travelling into the wind. thus upwind. Otherwise if they are on different sides, then the boat is going - with the wind. - */ - return boatLineFuncResult.equals(windLineFuncResult); - } +// /** +// * This function works out if a boat is going upwind or down wind. It looks at the boats current position, the next +// * gates position and the current wind +// * If bot the wind vector from the next gate and the boat from the next gate lay on the same side, then the boat is +// * going up wind, if they are on different sides of the gate, then the boat is going downwind +// * @param canvasController +// */ +// public Boolean isUpwindLeg(GameViewController canvasController, Mark nextMark) { +// +// Double windAngle = StreamParser.getWindDirection(); +// GateMark thisGateMark = (GateMark) nextMark; +// SingleMark nextMark1 = thisGateMark.getSingleMark1(); +// SingleMark nextMark2 = thisGateMark.getSingleMark2(); +// Point2D nextMarkPoint1 = canvasController.findScaledXY(nextMark1.getLatitude(), nextMark1.getLongitude()); +// Point2D nextMarkPoint2 = canvasController.findScaledXY(nextMark2.getLatitude(), nextMark2.getLongitude()); +// +// Point2D boatCurrentPoint = new Point2D(boatPoly.getLayoutX(), boatPoly.getLayoutY()); +// Point2D windTestPoint = GeoUtility.makeArbitraryVectorPoint(nextMarkPoint1, windAngle, 10d); +// +// +// Integer boatLineFuncResult = GeoUtility.lineFunction(nextMarkPoint1, nextMarkPoint2, boatCurrentPoint); +// Integer windLineFuncResult = GeoUtility.lineFunction(nextMarkPoint1, nextMarkPoint2, windTestPoint); +// +// +// /* +// If both the wind vector from the gate and the boat from the gate are on the same side of that gate, then the +// boat is travelling into the wind. thus upwind. Otherwise if they are on different sides, then the boat is going +// with the wind. +// */ +// return boatLineFuncResult.equals(windLineFuncResult); +// return true; +// } public void setIsSelected(Boolean isSelected) { this.isSelected = isSelected; setLineGroupVisible(isSelected); setWakeVisible(isSelected); - boatAnnotations.setVisible(isSelected); + annotationBox.setVisible(isSelected); setLayLinesVisible(isSelected); } public void setVisibility (boolean teamName, boolean velocity, boolean estTime, boolean legTime, boolean trail, boolean wake) { - - boatAnnotations.setVisibile(teamName, velocity, estTime, legTime); this.wake.setVisible(wake); this.lineGroup.setVisible(trail); } @@ -324,7 +300,7 @@ public class BoatGroup extends Group { } public Group getAnnotations() { - return boatAnnotations; + return annotationBox; } public Double getBoatLayoutX() { diff --git a/src/main/java/seng302/visualiser/fxObjects/MarkGroup.java b/src/main/java/seng302/visualiser/fxObjects/MarkObject.java similarity index 96% rename from src/main/java/seng302/visualiser/fxObjects/MarkGroup.java rename to src/main/java/seng302/visualiser/fxObjects/MarkObject.java index f615d3e3..21515cad 100644 --- a/src/main/java/seng302/visualiser/fxObjects/MarkGroup.java +++ b/src/main/java/seng302/visualiser/fxObjects/MarkObject.java @@ -16,7 +16,7 @@ import seng302.model.mark.SingleMark; /** * Grouping of javaFX objects needed to represent a Mark on screen. */ -public class MarkGroup extends Group { +public class MarkObject extends Group { private static int MARK_RADIUS = 5; private static int LINE_THICKNESS = 2; @@ -31,7 +31,7 @@ public class MarkGroup extends Group { * @param mark * @param points */ - public MarkGroup (SingleMark mark, Point2D points) { + public MarkObject(SingleMark mark, Point2D points) { marks.add(mark); mainMark = mark; Color color = Color.BLACK; @@ -73,7 +73,7 @@ public class MarkGroup extends Group { super.getChildren().removeAll(toRemove); } - public MarkGroup(GateMark mark, Point2D points1, Point2D points2) { + public MarkObject(GateMark mark, Point2D points1, Point2D points2) { marks.add(mark.getSingleMark1()); marks.add(mark.getSingleMark2()); mainMark = mark;