diff --git a/.mailmap b/.mailmap index 99d9ff08..97b5f43d 100644 --- a/.mailmap +++ b/.mailmap @@ -16,4 +16,4 @@ # https://www.kernel.org/pub/software/scm/git/docs/git-shortlog.html # http://stacktoheap.com/blog/2013/01/06/using-mailmap-to-fix-authors-list-in-git/ -Michael Rausch \ No newline at end of file +Michael Rausch \ No newline at end of file diff --git a/pom.xml b/pom.xml index 524b4562..8d10c6fc 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,11 @@ 4.12 test + + org.apache.commons + commons-io + 1.3.2 + com.googlecode.json-simple json-simple diff --git a/src/main/java/seng302/App.java b/src/main/java/seng302/App.java index 0637d2cc..bd4de5ef 100644 --- a/src/main/java/seng302/App.java +++ b/src/main/java/seng302/App.java @@ -5,6 +5,9 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; +import seng302.models.parsers.StreamParser; +import seng302.models.parsers.StreamReceiver; +import seng302.server.ServerThread; public class App extends Application { @@ -18,6 +21,28 @@ public class App extends Application } public static void main(String[] args) { + StreamReceiver sr; + + new ServerThread("Racevision Test Server"); + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + if (args.length > 1){ + sr = new StreamReceiver("localhost", 8085, "RaceStream"); + } + else{ + sr = new StreamReceiver("csse-s302staff.canterbury.ac.nz", 4941,"RaceStream"); +// sr = new StreamReceiver("livedata.americascup.com", 4941, "RaceStream"); +// sr = new StreamReceiver("localhost", 8085, "RaceStream"); + } + + sr.start(); + StreamParser streamParser = new StreamParser("StreamParser"); + streamParser.start(); + launch(args); } } diff --git a/src/main/java/seng302/controllers/CanvasController.java b/src/main/java/seng302/controllers/CanvasController.java index c0b20e26..94721e39 100644 --- a/src/main/java/seng302/controllers/CanvasController.java +++ b/src/main/java/seng302/controllers/CanvasController.java @@ -1,20 +1,31 @@ package seng302.controllers; import javafx.animation.*; +import javafx.beans.property.SimpleDoubleProperty; import javafx.fxml.FXML; +import javafx.geometry.Point2D; +import javafx.geometry.Point3D; +import javafx.scene.Group; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.layout.AnchorPane; import javafx.scene.paint.Color; +import javafx.scene.shape.Arc; +import javafx.scene.shape.ArcType; import javafx.scene.text.Font; import seng302.models.Boat; -import seng302.models.TimelineInfo; -import seng302.models.mark.GateMark; -import seng302.models.mark.Mark; -import seng302.models.mark.MarkType; -import seng302.models.mark.SingleMark; +import seng302.models.BoatGroup; +import seng302.models.Colors; +import seng302.models.RaceObject; +import seng302.models.mark.*; +import seng302.models.parsers.StreamPacket; +import seng302.models.parsers.StreamParser; +import seng302.models.parsers.packets.BoatPositionPacket; +import java.sql.Time; +import java.text.DecimalFormat; import java.util.*; +import java.util.concurrent.PriorityBlockingQueue; /** * Created by ptg19 on 15/03/17. @@ -27,66 +38,145 @@ public class CanvasController { private RaceViewController raceViewController; private ResizableCanvas canvas; + private Group group; private GraphicsContext gc; - private final double ORIGIN_LAT = 32.321504; - private final double ORIGIN_LON = -64.857063; - private final int SCALE = 16000; + private final int MARK_SIZE = 10; + private final int BUFFER_SIZE = 150; + private final int CANVAS_WIDTH = 1000; + private final int CANVAS_HEIGHT = 1000; + private final int LHS_BUFFER = BUFFER_SIZE; + private final int RHS_BUFFER = BUFFER_SIZE + MARK_SIZE / 2; + private final int TOP_BUFFER = BUFFER_SIZE; + private final int BOT_BUFFER = TOP_BUFFER + MARK_SIZE / 2; + + private double distanceScaleFactor; + private ScaleDirection scaleDirection; + private Mark minLatPoint; + private Mark minLonPoint; + private Mark maxLatPoint; + private Mark maxLonPoint; + private double referencePointX; + private double referencePointY; + private double metersToPixels; + private List raceObjects = new ArrayList<>(); + + //FRAME RATE + private static final double UPDATE_TIME = 0.016666; // 1 / 60 ie 60fps + private final long[] frameTimes = new long[30]; + private int frameTimeIndex = 0; + private boolean arrayFilled = false; + private DecimalFormat decimalFormat2dp = new DecimalFormat("0.00"); + + public AnimationTimer timer; + + private enum ScaleDirection { + HORIZONTAL, + VERTICAL + } public void setup(RaceViewController raceViewController){ this.raceViewController = raceViewController; } public void initialize() { + raceViewController = new RaceViewController(); canvas = new ResizableCanvas(); + group = new Group(); + canvasPane.getChildren().add(canvas); + canvasPane.getChildren().add(group); // Bind canvas size to stack pane size. - canvas.widthProperty().bind(canvasPane.widthProperty()); - canvas.heightProperty().bind(canvasPane.heightProperty()); + canvas.widthProperty().bind(new SimpleDoubleProperty(CANVAS_WIDTH)); + canvas.heightProperty().bind(new SimpleDoubleProperty(CANVAS_HEIGHT)); + //group.minWidth(CANVAS_WIDTH); + //group.minHeight(CANVAS_HEIGHT); + } + + public void initializeCanvas (){ + gc = canvas.getGraphicsContext2D(); - - - // overriding the handle so that it can clean canvas and redraw boats and course marks - AnimationTimer timer = new AnimationTimer() { - private long lastUpdate = 0; - private long lastFpsUpdate = 0; - private int lastFpsCount = 0; - private int fpsCount = 0; + gc.save(); + gc.setFill(Color.SKYBLUE); + gc.fillRect(0,0, CANVAS_WIDTH, CANVAS_HEIGHT); + gc.restore(); + fitMarksToCanvas(); + drawBoats(); + timer = new AnimationTimer() { @Override public void handle(long now) { - if (true){ //if statement for limiting refresh rate if needed - gc.clearRect(0, 0, canvas.getWidth(),canvas.getHeight()); - gc.setFill(Color.SKYBLUE); - gc.fillRect(0,0,canvas.getWidth(),canvas.getHeight()); - drawCourse(); - drawBoats(); - drawFps(lastFpsCount); - // If race has started, draw the boats and play the timeline - if (raceViewController.getRace().getRaceTime() > 1){ - raceViewController.playTimelines(); - } - // Race has not started, pause the timelines - else { - raceViewController.pauseTimelines(); - } - lastUpdate = now; - fpsCount ++; - if (now - lastFpsUpdate >= 1000000000){ - lastFpsCount = fpsCount; - fpsCount = 0; - lastFpsUpdate = now; - } + //fps stuff + long oldFrameTime = frameTimes[frameTimeIndex] ; + frameTimes[frameTimeIndex] = now ; + frameTimeIndex = (frameTimeIndex + 1) % frameTimes.length ; + if (frameTimeIndex == 0) { + arrayFilled = true ; } + long elapsedNanos; + if (arrayFilled) { + elapsedNanos = now - oldFrameTime ; + long elapsedNanosPerFrame = elapsedNanos / frameTimes.length ; + Double frameRate = 1_000_000_000.0 / elapsedNanosPerFrame ; + drawFps(frameRate.intValue()); + } + + // TODO: 1/05/17 cir27 - Make the RaceObjects update on the actual delay. + elapsedNanos = 1000 / 60; + updateRaceObjects(); + } }; - timer.start(); + for (Mark m : raceViewController.getRace().getCourse()) { + System.out.println(m.getName()); + } + //timer.start(); + } + + private void updateRaceObjects(){ + for (RaceObject raceObject : raceObjects) { + raceObject.updatePosition(1000 / 60); + // some raceObjects will have multiply ID's (for instance gate marks) + for (long id : raceObject.getRaceIds()) { + //checking if the current "ID" has any updates associated with it + if (StreamParser.boatPositions.containsKey(id)) { + move(id, raceObject); + } + } + } + } + + private void move(long id, RaceObject raceObject){ + PriorityBlockingQueue movementQueue = StreamParser.boatPositions.get(id); + if (movementQueue.size() > 0){ + BoatPositionPacket positionPacket = movementQueue.peek(); + + //this code adds a delay to reading from the movementQueue + //in case things being put into the movement queue are slightly + //out of order + int delayTime = 1000; + int loopTime = delayTime * 10; + long timeDiff = (System.currentTimeMillis()%loopTime - positionPacket.getTimeValid()%loopTime); + if (timeDiff < 0){ + timeDiff = loopTime + timeDiff; + } + if (timeDiff > delayTime) { + try { + positionPacket = movementQueue.take(); + Point2D p2d = latLonToXY(positionPacket.getLat(), positionPacket.getLon()); + double heading = 360.0 / 0xffff * positionPacket.getHeading(); + raceObject.setDestination(p2d.getX(), p2d.getY(), heading, positionPacket.getGroundSpeed(), (int) id); + } catch (InterruptedException e){ + e.printStackTrace(); + } + } + } } class ResizableCanvas extends Canvas { - public ResizableCanvas() { + ResizableCanvas() { // Redraw canvas when size changes. widthProperty().addListener(evt -> draw()); heightProperty().addListener(evt -> draw()); @@ -118,10 +208,15 @@ public class CanvasController { private void drawFps(int fps){ if (raceViewController.isDisplayFps()){ + gc.clearRect(5,5,50,20); gc.setFill(Color.BLACK); gc.setFont(new Font(14)); gc.setLineWidth(3); gc.fillText(fps + " FPS", 5, 20); + } else { + gc.clearRect(5,5,50,20); + gc.setFill(Color.SKYBLUE); + gc.fillRect(4,4,51,21); } } @@ -129,155 +224,212 @@ public class CanvasController { * Draws all the boats. */ private void drawBoats() { - Map timelineInfos = raceViewController.getTimelineInfos(); - for (Boat boat : timelineInfos.keySet()) { - TimelineInfo timelineInfo = timelineInfos.get(boat); +// Map timelineInfos = raceViewController.getTimelineInfos(); + List boats = raceViewController.getStartingBoats(); + Double startingX = raceObjects.get(0).getLayoutX(); + Double startingY = raceObjects.get(0).getLayoutY(); + Group boatAnnotations = new Group(); - boat.setLocation(timelineInfo.getY().doubleValue(), timelineInfo.getX().doubleValue()); - - drawBoat(boat.getLongitude(), boat.getLatitude(), boat.getColor(), boat.getShortName(), boat.getSpeedInKnots(), boat.getHeading()); + for (Boat boat : boats) { + BoatGroup boatGroup = new BoatGroup(boat, Colors.getColor()); + boatGroup.moveTo(startingX, startingY, 0d); + boatGroup.forceRotation(); + raceObjects.add(boatGroup); + boatAnnotations.getChildren().add(boatGroup.getLowPriorityAnnotations()); } + group.getChildren().add(boatAnnotations); + group.getChildren().addAll(raceObjects); } /** - * Draw the wake line behind a boat - * @param gc The graphics context used for drawing the wake - * @param x the x position of the boat - * @param y the y position of the boat - * @param speed the speed of the boat - * @param color the color of the wake line - * @param heading the heading of the boat + * Calculates x and y location for every marker that fits it to the canvas the race will be drawn on. */ - private void drawWake(GraphicsContext gc, double x, double y, double speed, Color color, double heading){ - double angle = Math.toRadians(heading); - speed = speed * 2; - Point newP = new Point(0, speed); - newP.rotate(angle); + private void fitMarksToCanvas() { + findMinMaxPoint(); + double minLonToMaxLon = scaleRaceExtremities(); + calculateReferencePointLocation(minLonToMaxLon); + givePointsXY(); + findMetersToPixels(); + } - gc.setStroke(color); - gc.setLineWidth(1.0); - gc.strokeLine(x, y, newP.x + x, newP.y + y); + + /** + * Sets the class variables minLatPoint, maxLatPoint, minLonPoint, maxLonPoint to the marker with the leftmost + * marker, rightmost marker, southern most marker and northern most marker respectively. + */ + private void findMinMaxPoint() { + List sortedPoints = new ArrayList<>(); + for (Mark mark : raceViewController.getRace().getCourse()) + { + if (mark.getMarkType() == MarkType.SINGLE_MARK) + sortedPoints.add(mark); + else { + sortedPoints.add(((GateMark) mark).getSingleMark1()); + sortedPoints.add(((GateMark) mark).getSingleMark2()); + } + } + sortedPoints.sort(Comparator.comparingDouble(Mark::getLatitude)); + minLatPoint = sortedPoints.get(0); + maxLatPoint = sortedPoints.get(sortedPoints.size()-1); + + sortedPoints.sort(Comparator.comparingDouble(Mark::getLongitude)); + //If the course is on a point on the earth where longitudes wrap around. + // TODO: 30/03/17 cir27 - Correctly account for longitude wrapping around. + if (sortedPoints.get(sortedPoints.size()-1).getLongitude() - sortedPoints.get(0).getLongitude() > 180) + Collections.reverse(sortedPoints); + minLonPoint = sortedPoints.get(0); + maxLonPoint = sortedPoints.get(sortedPoints.size()-1); } /** - * Draws a boat with given (x, y) position in the given color + * Calculates the location of a reference point, this is always the point with minimum latitude, in relation to the + * canvas. * - * @param lat - * @param lon - * @param color - * @param name - * @param speed + * @param minLonToMaxLon The horizontal distance between the point of minimum longitude to maximum longitude. */ - private void drawBoat(double lat, double lon, Color color, String name, double speed, double heading) { - // Latitude - double x = (lon - ORIGIN_LON) * SCALE; - double y = (ORIGIN_LAT - lat) * SCALE; + private void calculateReferencePointLocation (double minLonToMaxLon) { + Mark referencePoint = minLatPoint; + double referenceAngle; - gc.setFill(color); + if (scaleDirection == ScaleDirection.HORIZONTAL) { + referenceAngle = Math.abs(Mark.calculateHeadingRad(referencePoint, minLonPoint)); + referencePointX = LHS_BUFFER + distanceScaleFactor * Math.sin(referenceAngle) * Mark.calculateDistance(referencePoint, minLonPoint); - if (raceViewController.isDisplayAnnotations()) { - // Set boat text - gc.setFont(new Font(14)); - gc.setLineWidth(3); - gc.fillText(name + ", " + speed + " knots", x + 15, y + 15); - } -// double diameter = 9; -// gc.fillOval(x, y, diameter, diameter); - double angle = Math.toRadians(heading); + referenceAngle = Math.abs(Mark.calculateHeadingRad(referencePoint, maxLatPoint)); + referencePointY = CANVAS_HEIGHT - (TOP_BUFFER + BOT_BUFFER); + referencePointY -= distanceScaleFactor * Math.cos(referenceAngle) * Mark.calculateDistance(referencePoint, maxLatPoint); + referencePointY = referencePointY / 2; + referencePointY += TOP_BUFFER; + referencePointY += distanceScaleFactor * Math.cos(referenceAngle) * Mark.calculateDistance(referencePoint, maxLatPoint); + } else { + referencePointY = CANVAS_HEIGHT - BOT_BUFFER; - Point p1 = new Point(0, -15); // apex point - Point p2 = new Point(7, 4); // base point - Point p3 = new Point(-7, 4); // base point - p1.rotate(angle); - p2.rotate(angle); - p3.rotate(angle); - double[] xx = new double[] {p1.x + x, p2.x + x, x, p3.x + x}; - double[] yy = new double[] {p1.y + y, p2.y + y, y, p3.y + y}; - gc.fillPolygon(xx, yy, 4); - - if (raceViewController.isDisplayAnnotations()){ - drawWake(gc, x, y, speed, color, heading); + referenceAngle = Math.abs(Mark.calculateHeadingRad(referencePoint, minLonPoint)); + referencePointX = LHS_BUFFER; + referencePointX += distanceScaleFactor * Math.sin(referenceAngle) * Mark.calculateDistance(referencePoint, minLonPoint); + referencePointX += ((CANVAS_WIDTH - (LHS_BUFFER + RHS_BUFFER)) - (minLonToMaxLon * distanceScaleFactor)) / 2; } } /** - * Inner class for creating point so that you can rotate it around origin point. + * Finds the scale factor necessary to fit all race markers within the onscreen map and assigns it to distanceScaleFactor + * Returns the max horizontal distance of the map. */ - class Point { + private double scaleRaceExtremities () { - double x, y; - - Point (double x, double y) { - this.x = x; - this.y = y; - } - - void rotate(double angle) { - double oldX = x; - double oldY = y; - this.x = oldX * Math.cos(angle) - oldY * Math.sin(angle); - this.y = oldX * Math.sin(angle) + oldY * Math.cos(angle); + double vertAngle = Math.abs(Mark.calculateHeadingRad(minLatPoint, maxLatPoint)); + double vertDistance = Math.cos(vertAngle) * Mark.calculateDistance(minLatPoint, maxLatPoint); + double horiAngle = Mark.calculateHeadingRad(minLonPoint, maxLonPoint); + if (horiAngle <= (Math.PI / 2)) + horiAngle = (Math.PI / 2) - horiAngle; + else + horiAngle = horiAngle - (Math.PI / 2); + double horiDistance = Math.cos(horiAngle) * Mark.calculateDistance(minLonPoint, maxLonPoint); + + double vertScale = (CANVAS_HEIGHT - (TOP_BUFFER + BOT_BUFFER)) / vertDistance; + + if ((horiDistance * vertScale) > (CANVAS_WIDTH - (RHS_BUFFER + LHS_BUFFER))) { + distanceScaleFactor = (CANVAS_WIDTH - (RHS_BUFFER + LHS_BUFFER)) / horiDistance; + scaleDirection = ScaleDirection.HORIZONTAL; + } else { + distanceScaleFactor = vertScale; + scaleDirection = ScaleDirection.VERTICAL; } + return horiDistance; } /** - * Draws the course. + * Give all markers in the course an x,y location relative to a given reference with a known x,y location. Distances + * are scaled according to the distanceScaleFactor variable. */ - private void drawCourse() { - for (Mark mark : raceViewController.getRace().getCourse()) { - if (mark.getMarkType() == MarkType.SINGLE_MARK) { - drawSingleMark((SingleMark) mark, Color.BLACK); - } else if (mark.getMarkType() == MarkType.GATE_MARK) { - drawGateMark((GateMark) mark); + private void givePointsXY() { + List allPoints = new ArrayList<>(raceViewController.getRace().getCourse()); + List processed = new ArrayList<>(); + RaceObject markGroup; + + for (Mark mark : allPoints) { + if (!processed.contains(mark)) { + if (mark.getMarkType() != MarkType.SINGLE_MARK) { + GateMark gateMark = (GateMark) mark; + markGroup = new MarkGroup(mark, findScaledXY(gateMark.getSingleMark1()), findScaledXY(gateMark.getSingleMark2())); + raceObjects.add(markGroup); + } else { + markGroup = new MarkGroup(mark, findScaledXY(mark)); + raceObjects.add(markGroup); + } + processed.add(mark); } } } - /** - * Draw a given mark on canvas - * - * @param singleMark - */ - private void drawSingleMark(SingleMark singleMark, Color color) { - double x = (singleMark.getLongitude() - ORIGIN_LON) * SCALE; - double y = (ORIGIN_LAT - singleMark.getLatitude()) * SCALE; - - gc.setFill(color); - gc.fillRect(x,y,5.5,5.5); + private Point2D findScaledXY (Mark unscaled) { + return findScaledXY (minLatPoint.getLatitude(), minLatPoint.getLongitude(), + unscaled.getLatitude(), unscaled.getLongitude()); } + private Point2D findScaledXY (double latA, double lonA, double latB, double lonB) { + double distanceFromReference; + double angleFromReference; + int xAxisLocation = (int) referencePointX; + int yAxisLocation = (int) referencePointY; + + angleFromReference = Mark.calculateHeadingRad(latA, lonA, latB, lonB); + distanceFromReference = Mark.calculateDistance(latA, lonA, latB, lonB); + if (angleFromReference >= 0 && angleFromReference <= Math.PI / 2) { + xAxisLocation += (int) Math.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); + yAxisLocation -= (int) Math.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); + } else if (angleFromReference >= 0) { + angleFromReference = angleFromReference - Math.PI / 2; + xAxisLocation += (int) Math.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); + yAxisLocation += (int) Math.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); + } else if (angleFromReference < 0 && angleFromReference >= -Math.PI / 2) { + angleFromReference = Math.abs(angleFromReference); + xAxisLocation -= (int) Math.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); + yAxisLocation -= (int) Math.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); + } else { + angleFromReference = Math.abs(angleFromReference) - Math.PI / 2; + xAxisLocation -= (int) Math.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); + yAxisLocation += (int) Math.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); + } + return new Point2D(xAxisLocation, yAxisLocation); + } + + + /** - * Draw a gate mark which contains two single marks - * - * @param gateMark + * Find the number of meters per pixel. */ - private void drawGateMark(GateMark gateMark) { - Color color = Color.BLUE; - - if (gateMark.getName().equals("Start")){ - color = Color.RED; + private void findMetersToPixels () { + Double angularDistance; + Double angle; + Double straightLineDistance; + if (scaleDirection == ScaleDirection.HORIZONTAL) { + angularDistance = Mark.calculateDistance(minLonPoint, maxLonPoint); + angle = Mark.calculateHeadingRad(minLonPoint, maxLonPoint); + if (angle > Math.PI / 2) { + straightLineDistance = Math.cos(angle - Math.PI) * angularDistance; + } else { + straightLineDistance = Math.cos(angle) * angularDistance; + } + metersToPixels = (CANVAS_WIDTH - RHS_BUFFER - LHS_BUFFER) / straightLineDistance; + } else { + angularDistance = Mark.calculateDistance(minLatPoint, maxLatPoint); + angle = Mark.calculateHeadingRad(minLatPoint, maxLatPoint); + if (angle < Math.PI / 2) { + straightLineDistance = Math.cos(angle) * angularDistance; + } else { + straightLineDistance = Math.cos(-angle + Math.PI * 2) * angularDistance; + } + metersToPixels = (CANVAS_HEIGHT - TOP_BUFFER - BOT_BUFFER) / straightLineDistance; } + } - if (gateMark.getName().equals("Finish")){ - color = Color.GREEN; - } + private Point2D latLonToXY (double latitude, double longitude) { + return findScaledXY(minLatPoint.getLatitude(), minLatPoint.getLongitude(), latitude, longitude); + } - drawSingleMark(gateMark.getSingleMark1(), color); - drawSingleMark(gateMark.getSingleMark2(), color); - - GraphicsContext gc = canvas.getGraphicsContext2D(); - - gc.setStroke(color); - - // Convert lat/lon to x,y - double x1 = (gateMark.getSingleMark1().getLongitude()- ORIGIN_LON) * SCALE; - double y1 = (ORIGIN_LAT - gateMark.getSingleMark1().getLatitude()) * SCALE; - - double x2 = (gateMark.getSingleMark2().getLongitude() - ORIGIN_LON) * SCALE; - double y2 = (ORIGIN_LAT - gateMark.getSingleMark2().getLatitude()) * SCALE; - - gc.setLineWidth(1); - gc.strokeLine(x1, y1, x2, y2); + List getRaceObjects() { + return raceObjects; } } \ No newline at end of file diff --git a/src/main/java/seng302/controllers/Controller.java b/src/main/java/seng302/controllers/Controller.java index 1892d472..2baa46c4 100644 --- a/src/main/java/seng302/controllers/Controller.java +++ b/src/main/java/seng302/controllers/Controller.java @@ -1,14 +1,32 @@ package seng302.controllers; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import seng302.models.Boat; +import seng302.models.parsers.StreamParser; +import seng302.models.parsers.XMLParser; + +import javax.xml.crypto.dsig.XMLObject; import java.io.IOException; import java.net.URL; +import java.util.ArrayList; import java.util.ResourceBundle; +import java.util.Timer; +import java.util.TimerTask; /** * Created by michaelrausch on 21/03/17. @@ -16,6 +34,20 @@ import java.util.ResourceBundle; public class Controller implements Initializable { @FXML private AnchorPane contentPane; + @FXML + private Label timeTillLive; + @FXML + private Button streamButton; + @FXML + private Button switchToRaceViewButton; + @FXML + private TableView teamList; + @FXML + private TableColumn boatNameCol; + @FXML + private TableColumn shortNameCol; + @FXML + private TableColumn countryCol; private void setContentPane(String jfxUrl){ try{ @@ -33,6 +65,78 @@ public class Controller implements Initializable { @Override public void initialize(URL location, ResourceBundle resources) { + + } + + /** + * Running a timer to update the livestream status on welcome screen. Update interval is 1 second. + */ + public void startStream() { + if (StreamParser.isStreamStatus()) { + XMLParser xmlParser = StreamParser.getXmlObject(); + streamButton.setVisible(false); + timeTillLive.setVisible(true); + timeTillLive.setTextFill(Color.GREEN); + timeTillLive.setText("Connecting..."); + Timer timer = new Timer(); + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + Platform.runLater(() -> { + if (StreamParser.isRaceFinished()) { + timeTillLive.setTextFill(Color.RED); + timeTillLive.setText("Race finished! Waiting for new race..."); + switchToRaceViewButton.setDisable(true); + } else if (StreamParser.getTimeSinceStart() > 0) { + updateTeamList(); + timeTillLive.setTextFill(Color.RED); + switchToRaceViewButton.setDisable(false); + String timerMinute = Long.toString(StreamParser.getTimeSinceStart() / 60); + String timerSecond = Long.toString(StreamParser.getTimeSinceStart() % 60); + if (timerSecond.length() == 1) { + timerSecond = "0" + timerSecond; + } + String timerString = "-" + timerMinute + ":" + timerSecond + " minutes"; + timeTillLive.setText(timerString); + } else { + updateTeamList(); + timeTillLive.setTextFill(Color.BLACK); + switchToRaceViewButton.setDisable(false); + String timerMinute = Long.toString(-1 * StreamParser.getTimeSinceStart() / 60); + String timerSecond = Long.toString(-1 * StreamParser.getTimeSinceStart() % 60); + if (timerSecond.length() == 1) { + timerSecond = "0" + timerSecond; + } + String timerString = timerMinute + ":" + timerSecond + " minutes"; + timeTillLive.setText(timerString); + } + }); + } + }, 0, 1000); + } else { + timeTillLive.setText("Stream not available."); + timeTillLive.setTextFill(Color.RED); + } + } + + public void switchToRaceView() { setContentPane("/views/RaceView.fxml"); } + + private void updateTeamList() { + ObservableList data = FXCollections.observableArrayList(); + teamList.setItems(data); + boatNameCol.setCellValueFactory( + new PropertyValueFactory("boatName") + ); + shortNameCol.setCellValueFactory( + new PropertyValueFactory("shortName") + ); + countryCol.setCellValueFactory( + new PropertyValueFactory("country") + ); + for (Boat boat : StreamParser.getBoats()) { + data.add(boat); + } + } } diff --git a/src/main/java/seng302/controllers/RaceController.java b/src/main/java/seng302/controllers/RaceController.java index b5fa2847..f595e5e9 100644 --- a/src/main/java/seng302/controllers/RaceController.java +++ b/src/main/java/seng302/controllers/RaceController.java @@ -4,6 +4,7 @@ import seng302.models.Boat; import seng302.models.Race; import seng302.models.parsers.ConfigParser; import seng302.models.parsers.CourseParser; +import seng302.models.parsers.StreamParser; import seng302.models.parsers.TeamsParser; import java.lang.reflect.Array; @@ -38,7 +39,7 @@ public class RaceController { public Race createRace(String configFile, String teamsConfigFile) throws Exception { Race race = new Race(); - +// StreamParser.xmlObject // Read team names from file TeamsParser tp = new TeamsParser(teamsConfigFile); diff --git a/src/main/java/seng302/controllers/RaceViewController.java b/src/main/java/seng302/controllers/RaceViewController.java index 965fbc7d..8468376d 100644 --- a/src/main/java/seng302/controllers/RaceViewController.java +++ b/src/main/java/seng302/controllers/RaceViewController.java @@ -2,25 +2,24 @@ package seng302.controllers; import javafx.animation.Animation; import javafx.animation.KeyFrame; -import javafx.animation.KeyValue; import javafx.animation.Timeline; -import javafx.beans.property.DoubleProperty; -import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; +import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.control.CheckBox; +import javafx.scene.control.Slider; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; import javafx.scene.text.Text; import javafx.util.Duration; -import seng302.models.Boat; -import seng302.models.Event; -import seng302.models.Race; -import seng302.models.TimelineInfo; +import javafx.util.StringConverter; +import seng302.models.*; import seng302.models.parsers.ConfigParser; +import seng302.models.parsers.StreamParser; import java.io.IOException; import java.util.*; @@ -28,11 +27,11 @@ import java.util.*; /** * Created by ptg19 on 29/03/17. */ -public class RaceViewController { +public class RaceViewController extends Thread{ @FXML private VBox positionVbox; @FXML - private CheckBox toggleAnnotation, toggleFps; + private CheckBox toggleFps; @FXML private Text timerLabel; @FXML @@ -40,9 +39,11 @@ public class RaceViewController { @FXML private Text windArrowText, windDirectionText; @FXML + private Slider annotationSlider; + @FXML private CanvasController includedCanvasController; - private boolean displayAnnotations; + private ArrayList startingBoats = new ArrayList<>(); private boolean displayFps; private Timeline timerTimeline; private Map timelineInfos = new HashMap<>(); @@ -50,42 +51,78 @@ public class RaceViewController { private Race race; public void initialize() { - includedCanvasController.setup(this); + RaceController raceController = new RaceController(); raceController.initializeRace(); race = raceController.getRace(); + for (Boat boat : race.getBoats()) { + startingBoats.add(boat); + } +// try{ +// initializeTimelines(); +// } +// catch (Exception e){ +// e.printStackTrace(); +// } + includedCanvasController.setup(this); + includedCanvasController.initializeCanvas(); initializeTimer(); initializeSettings(); - try{ - initializeTimelines(); - } - catch (Exception e){ - e.printStackTrace(); - } //set wind direction!!!!!!! can't find another place to put my code --haoming double windDirection = new ConfigParser("/config/config.xml").getWindDirection(); windDirectionText.setText(String.format("%.1f°", windDirection)); windArrowText.setRotate(windDirection); + includedCanvasController.timer.start(); } - private void initializeSettings(){ - displayAnnotations = true; + + + private void initializeSettings() { displayFps = true; - toggleAnnotation.selectedProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) { - displayAnnotations = !displayAnnotations; - } - }); toggleFps.selectedProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) { displayFps = !displayFps; } }); + + //SLIFER STUFF BELOW + annotationSlider.setLabelFormatter(new StringConverter() { + @Override + public String toString(Double n) { + if (n == 0) return "None"; + if (n == 1) return "Low"; + if (n == 2) return "Medium"; + if (n == 3) return "All"; + + return "All"; + } + + @Override + public Double fromString(String s) { + switch (s) { + case "None": + return 0d; + case "Low": + return 1d; + case "Medium": + return 2d; + case "All": + return 3d; + + default: + return 3d; + } + } + }); + + annotationSlider.valueProperty().addListener((obs, oldval, newVal) -> + setAnnotations((int)annotationSlider.getValue())); + + annotationSlider.setValue(3); } private void initializeTimer(){ @@ -95,12 +132,11 @@ public class RaceViewController { timerTimeline.getKeyFrames().add( new KeyFrame(Duration.seconds(1), event -> { - // Stop timer if race is finished - if (this.race.isRaceFinished()) { - this.timerTimeline.stop(); + if (StreamParser.isRaceFinished()) { + timerLabel.setFill(Color.RED); + timerLabel.setText("Race Finished!"); } else { - timerLabel.setText(convertTimeToMinutesSeconds(race.getRaceTime())); - this.race.incrementRaceTime(); + timerLabel.setText(currentTimer()); } }) ); @@ -114,39 +150,39 @@ public class RaceViewController { */ private void initializeTimelines() { HashMap boat_events = race.getEvents(); - for (Boat boat : boat_events.keySet()) { - // x, y are the real time coordinates - DoubleProperty x = new SimpleDoubleProperty(); - DoubleProperty y = new SimpleDoubleProperty(); - - List keyFrames = new ArrayList<>(); - List events = boat_events.get(boat); - - // iterates all events and convert each event to keyFrame, then add them into a list - for (Event event : events) { - if (event.getIsFinishingEvent()) { - keyFrames.add( - new KeyFrame(Duration.seconds(event.getTime()), - onFinished -> {race.setBoatFinished(boat); handleEvent(event);}, - new KeyValue(x, event.getThisMark().getLatitude()), - new KeyValue(y, event.getThisMark().getLongitude()) - ) - ); - } else { - keyFrames.add( - new KeyFrame(Duration.seconds(event.getTime()), - onFinished ->{ - handleEvent(event); - boat.setHeading(event.getBoatHeading()); - }, - new KeyValue(x, event.getThisMark().getLatitude()), - new KeyValue(y, event.getThisMark().getLongitude()) - ) - ); - } - } - timelineInfos.put(boat, new TimelineInfo(new Timeline(keyFrames.toArray(new KeyFrame[keyFrames.size()])), x, y)); + startingBoats.add(boat); +// // x, y are the real time coordinates +// DoubleProperty x = new SimpleDoubleProperty(); +// DoubleProperty y = new SimpleDoubleProperty(); +// +// List keyFrames = new ArrayList<>(); +// List events = boat_events.get(boat); +// +// // iterates all events and convert each event to keyFrame, then add them into a list +// for (Event event : events) { +// if (event.getIsFinishingEvent()) { +// keyFrames.add( +// new KeyFrame(Duration.seconds(event.getTime()), +// onFinished -> {race.setBoatFinished(boat); handleEvent(event);}, +// new KeyValue(x, event.getThisMark().getLatitude()), +// new KeyValue(y, event.getThisMark().getLongitude()) +// ) +// ); +// } else { +// keyFrames.add( +// new KeyFrame(Duration.seconds(event.getTime()), +// onFinished ->{ +// handleEvent(event); +// boat.setHeading(event.getBoatHeading()); +// }, +// new KeyValue(x, event.getThisMark().getLatitude()), +// new KeyValue(y, event.getThisMark().getLongitude()) +// ) +// ); +// } +// } +// timelineInfos.put(boat, new TimelineInfo(new Timeline(keyFrames.toArray(new KeyFrame[keyFrames.size()])), x, y)); } setRaceDuration(); } @@ -255,6 +291,26 @@ public class RaceViewController { return String.format("%02d:%02d", time / 60, time % 60); } + private String currentTimer() { + String timerString = "0:00 minutes"; + 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 + " minutes"; + } 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 + " minutes"; + } + return timerString; + } + public void stopTimer() { timerTimeline.stop(); } @@ -266,10 +322,6 @@ public class RaceViewController { return displayFps; } - public boolean isDisplayAnnotations() { - return displayAnnotations; - } - public Race getRace() { return race; } @@ -277,4 +329,59 @@ public class RaceViewController { public Map getTimelineInfos() { return timelineInfos; } + + public ArrayList getStartingBoats(){ + return startingBoats; + } + + + private void setAnnotations(Integer annotationLevel) { + switch (annotationLevel) { + case 0: + for (RaceObject ro : includedCanvasController.getRaceObjects()) { + if(ro instanceof BoatGroup) { + BoatGroup bg = (BoatGroup) ro; + bg.setTeamNameObjectVisible(false); + bg.setVelocityObjectVisible(false); + bg.setLineGroupVisible(false); + bg.setWakeVisible(false); + } + } + break; + case 1: + for (RaceObject ro : includedCanvasController.getRaceObjects()) { + if(ro instanceof BoatGroup) { + BoatGroup bg = (BoatGroup) ro; + bg.setTeamNameObjectVisible(true); + bg.setVelocityObjectVisible(false); + bg.setLineGroupVisible(false); + bg.setWakeVisible(false); + } + } + break; + case 2: + for (RaceObject ro : includedCanvasController.getRaceObjects()) { + if(ro instanceof BoatGroup) { + BoatGroup bg = (BoatGroup) ro; + bg.setTeamNameObjectVisible(true); + bg.setVelocityObjectVisible(false); + bg.setLineGroupVisible(true); + bg.setWakeVisible(false); + } + } + break; + case 3: + for (RaceObject ro : includedCanvasController.getRaceObjects()) { + if(ro instanceof BoatGroup) { + BoatGroup bg = (BoatGroup) ro; + bg.setTeamNameObjectVisible(true); + bg.setVelocityObjectVisible(true); + bg.setLineGroupVisible(true); + bg.setWakeVisible(true); + } + } + break; + } + } + } \ No newline at end of file diff --git a/src/main/java/seng302/models/Boat.java b/src/main/java/seng302/models/Boat.java index 0973ad38..dc3aa7b7 100644 --- a/src/main/java/seng302/models/Boat.java +++ b/src/main/java/seng302/models/Boat.java @@ -1,31 +1,40 @@ package seng302.models; +import javafx.geometry.Point2D; import javafx.scene.paint.Color; +import javafx.scene.shape.Polygon; +import javafx.scene.text.Text; +import javafx.scene.transform.Rotate; +import javafx.scene.transform.Translate; +import javafx.util.Pair; /** * Represents a boat in the race. */ public class Boat { - private String teamName; // The name of the team, this is also the name of the boat - private double velocity; // In meters/second - private double lat; // Boats position - private double lon; // - - private double distanceToNextMark; - private Color color; - private int markLastPast; + private String teamName; + private double velocity; + private double lat; + private double lon; private double heading; + private int markLastPast; private String shortName; + private int id; + // new attributes to boat + private int sourceID; + private String boatName; + private String country; public Boat(String teamName) { this.teamName = teamName; this.velocity = 10; // Default velocity this.lat = 0.0; this.lon = 0.0; - this.distanceToNextMark = 0.0; this.shortName = ""; } + /** * Represents a boat in the race. * @@ -33,12 +42,26 @@ public class Boat { * @param boatVelocity The speed of the boat in meters/second * @param shortName A shorter version of the teams name */ - public Boat(String teamName, double boatVelocity, String shortName) { + public Boat(String teamName, double boatVelocity, String shortName, int id) { this.teamName = teamName; this.velocity = boatVelocity; - this.distanceToNextMark = 0.0; - this.color = Colors.getColor(); this.shortName = shortName; + this.id = id; + } + + /** + * New instance created by BoatsParser. + * + * @param sourceID source ID of the boat + * @param boatName full name of the boat + * @param shortName short name of the boat + * @param country country of the boat + */ + public Boat(int sourceID, String boatName, String shortName, String country) { + this.sourceID = sourceID; + this.boatName = boatName; + this.shortName = shortName; + this.country = country; } /** @@ -88,8 +111,9 @@ public class Boat { this.lon = lon; } - public void setDistanceToNextMark(double distance){ - this.distanceToNextMark = distance; + public Pair getLocation () + { + return new Pair<>(this.lat, this.lon); } public double getLatitude(){ @@ -100,8 +124,12 @@ public class Boat { return this.lon; } - public Color getColor() { - return color; + public void setLatitude (double latitude) { + this.lat = latitude; + } + + public void setlongitude (double longitude) { + this.lon =longitude; } public double getSpeedInKnots(){ @@ -116,15 +144,31 @@ public class Boat { return markLastPast; } - public void setHeading(double heading){ - this.heading = heading; - } - public double getHeading(){ return this.heading; } + public void setHeading(double heading) { + this.heading = heading; + } + public String getShortName(){ return this.shortName; } + + public int getId() { + return id; + } + + public int getSourceID() { + return sourceID; + } + + public String getBoatName() { + return boatName; + } + + public String getCountry() { + return country; + } } \ No newline at end of file diff --git a/src/main/java/seng302/models/BoatGroup.java b/src/main/java/seng302/models/BoatGroup.java new file mode 100644 index 00000000..ef739ae5 --- /dev/null +++ b/src/main/java/seng302/models/BoatGroup.java @@ -0,0 +1,310 @@ +package seng302.models; + +import javafx.geometry.Point2D; +import javafx.scene.Group; +import javafx.scene.paint.Color; +import javafx.scene.shape.Line; +import javafx.scene.shape.Polygon; +import javafx.scene.text.Text; +import javafx.scene.transform.Rotate; +import seng302.models.parsers.StreamParser; + +/** + * BoatGroup is a javafx group that by default contains a graphical objects for representing a 2 dimensional boat. + * It contains a single polygon for the boat, a group of lines to show it's path, a wake object and two text labels to + * annotate the boat teams name and the boats velocity. + */ +public class BoatGroup extends RaceObject{ + + private static final double TEAMNAME_X_OFFSET = 10d; + private static final double TEAMNAME_Y_OFFSET = -15d; + private static final double VELOCITY_X_OFFSET = 10d; + private static final double VELOCITY_Y_OFFSET = -5d; + private static final double BOAT_HEIGHT = 15d; + private static final double BOAT_WIDTH = 10d; + private static double expectedUpdateInterval = 200; + private boolean destinationSet; + private Point2D lastPoint; + private int wakeGenerationDelay = 10; + private double distanceTravelled; + + private Boat boat; + private Group lineGroup = new Group(); + private Polygon boatPoly; + private Text teamNameObject; + private Text velocityObject; + private Wake wake; + + /** + * 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){ + this.boat = boat; + initChildren(color); + } + + /** + * 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) + { + 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) { + boatPoly = new Polygon(points); + boatPoly.setFill(color); + + teamNameObject = new Text(boat.getShortName()); + velocityObject = new Text(String.valueOf(boat.getVelocity())); + + teamNameObject.setX(TEAMNAME_X_OFFSET); + teamNameObject.setY(TEAMNAME_Y_OFFSET); + teamNameObject.relocate(teamNameObject.getX(), teamNameObject.getY()); + + velocityObject.setX(VELOCITY_X_OFFSET); + velocityObject.setY(VELOCITY_Y_OFFSET); + velocityObject.relocate(velocityObject.getX(), velocityObject.getY()); + destinationSet = false; + + wake = new Wake(0, -BOAT_HEIGHT); + super.getChildren().addAll(teamNameObject, velocityObject, boatPoly); + } + + /** + * 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); + } + + /** + * Moves the boat and its children annotations from its current coordinates by specified amounts. + * @param dx The amount to move the X coordinate by + * @param dy The amount to move the Y coordinate by + */ + public void moveGroupBy(double dx, double dy, double rotation) { + boatPoly.setLayoutX(boatPoly.getLayoutX() + dx); + boatPoly.setLayoutY(boatPoly.getLayoutY() + dy); + teamNameObject.setLayoutX(teamNameObject.getLayoutX() + dx); + teamNameObject.setLayoutY(teamNameObject.getLayoutY() + dy); + velocityObject.setLayoutX(velocityObject.getLayoutX() + dx); + velocityObject.setLayoutY(velocityObject.getLayoutY() + dy); + wake.setLayoutX(wake.getLayoutX() + dx); + wake.setLayoutY(wake.getLayoutY() + dy); + rotateTo(rotation + currentRotation); + } + + /** + * Moves the boat and its children annotations to coordinates specified + * @param x The X coordinate to move the boat to + * @param y The Y coordinate to move the boat to + * @param rotation The heading in degrees from north the boat should rotate to. + */ + public void moveTo (double x, double y, double rotation) { + rotateTo(rotation); + moveTo(x, y); + } + + /** + * Moves the boat and its children annotations to coordinates specified + * @param x The X coordinate to move the boat to + * @param y The Y coordinate to move the boat to + */ + public void moveTo (double x, double y) { + boatPoly.setLayoutX(x); + boatPoly.setLayoutY(y); + teamNameObject.setLayoutX(x); + teamNameObject.setLayoutY(y); + velocityObject.setLayoutX(x); + velocityObject.setLayoutY(y); + wake.setLayoutX(x); + wake.setLayoutY(y); + wake.rotate(currentRotation); + } + + /** + * Updates the position of all graphics in the BoatGroup based off of the given time interval. + * @param timeInterval The interval, in milliseconds, the boat should update it's position based on. + */ + public void updatePosition (long timeInterval) { + //Calculate the movement of the boat. + double dx = pixelVelocityX * timeInterval; + double dy = pixelVelocityY * timeInterval; + double rotation = rotationalVelocity * timeInterval; + distanceTravelled += Math.abs(dx) + Math.abs(dy); + moveGroupBy(dx, dy, rotation); + //Draw a new section of the trail every 20 pixels of movement. + if (distanceTravelled > 20) { + distanceTravelled = 0; + if (lastPoint != null) { + Line l = new Line( + lastPoint.getX(), + lastPoint.getY(), + boatPoly.getLayoutX(), + boatPoly.getLayoutY() + ); + l.getStrokeDashArray().setAll(3d, 7d); + l.setStroke(boatPoly.getFill()); + lineGroup.getChildren().add(l); + } + if (destinationSet){ //Only begin drawing after the first destination is set + lastPoint = new Point2D(boatPoly.getLayoutX(), boatPoly.getLayoutY()); + } + } + wake.updatePosition(timeInterval); + } + + /** + * Sets the destination of the boat and the headng it should have once it reaches + * @param newXValue + * @param newYValue + * @param rotation Rotation to move graphics to. + * @param raceIds RaceID of the object to move. + */ + public void setDestination (double newXValue, double newYValue, double rotation, double speed, int... raceIds) { + if (hasRaceId(raceIds)) { + destinationSet = true; + boat.setVelocity(speed); + if (currentRotation < 0) + currentRotation = 360 - currentRotation; + double dx = newXValue - boatPoly.getLayoutX(); + if ((dx > 0 && pixelVelocityX < 0) || (dx < 0 && pixelVelocityX > 0)) { + pixelVelocityX = 0; + } else { + pixelVelocityX = dx / expectedUpdateInterval; + } + double dy = newYValue - boatPoly.getLayoutY(); + //Check movement is reasonable. Assumes a 1000 * 1000 canvas + if (Math.abs(dx) > 50 || Math.abs(dy) > 50) { +// System.out.println("dx = " + dx); +// System.out.println("dy = " + dy); + dx = 0; + dy = 0; + moveTo(newXValue, newYValue); + } + //Slight delay on changing X/Y direction that could help jitter. Disabled since there was an issue with + //packets that might be causing it. +// if ((dx > 0 && pixelVelocityX < 0) || (dx < 0 && pixelVelocityX > 0)) { +// pixelVelocityX = 0; +// } else { +// pixelVelocityX = dx / expectedUpdateInterval; +// } +// if ((dy > 0 && pixelVelocityY < 0) || (dy < 0 && pixelVelocityY > 0)) { +// pixelVelocityY = 0; +// } else { +// pixelVelocityY = dy / expectedUpdateInterval; +// } + pixelVelocityX = dx / expectedUpdateInterval; + pixelVelocityY = dy / expectedUpdateInterval; + rotationalGoal = rotation; + calculateRotationalVelocity(); + if (wakeGenerationDelay > 0) { + wake.rotate(rotationalGoal); + wakeGenerationDelay--; + } else { + wake.setRotationalVelocity(rotationalVelocity, currentRotation, boat.getVelocity()); + } + velocityObject.setText(String.format("%.2f m/s", boat.getVelocity())); + } + } + + public void setDestination (double newXValue, double newYValue, double speed, int... raceIDs) { + destinationSet = true; + + if (hasRaceId(raceIDs)) { + double rotation = Math.abs( + Math.toDegrees( + Math.atan( + (newYValue - boatPoly.getLayoutY()) / (newXValue - boatPoly.getLayoutX()) + ) + ) + ); + setDestination(newXValue, newYValue, rotation, speed, raceIDs); + } + } + + public void rotateTo (double rotation) { + currentRotation = rotation; + boatPoly.getTransforms().clear(); + boatPoly.getTransforms().add(new Rotate(rotation)); + } + + public void forceRotation () { + rotateTo (rotationalGoal); + wake.rotate(rotationalGoal); + } + + public void setTeamNameObjectVisible(Boolean visible) { + teamNameObject.setVisible(visible); + } + + public void setVelocityObjectVisible(Boolean visible) { + velocityObject.setVisible(visible); + } + + public void setLineGroupVisible(Boolean visible) { + lineGroup.setVisible(visible); + } + + public void setWakeVisible(Boolean visible) { + wake.setVisible(visible); + } + + public Boat getBoat() { + return boat; + } + + /** + * Returns true if this BoatGroup contains at least one of the given IDs. + * + * @param raceIds The ID's to check the BoatGroup for. + * @return True if the BoatGroup contains at east one of the given IDs, false otherwise. + */ + public boolean hasRaceId (int... raceIds) { + for (int id : raceIds) { + if (id == boat.getId()) + return true; + } + return false; + } + + /** + * Returns all raceIds associated with this group. For BoatGroups the ID's are for the boat. + * + * @return An array containing all ID's associated with this RaceObject. + */ + public int[] getRaceIds () { + return new int[] {boat.getId()}; + } + + /** + * Due to javaFX limitations annotations associated with a boat that you want to appear below all boats in the + * Z-axis need to be pulled out of the BoatGroup and added to the parent group of the BoatGroups. This function + * returns these annotations as a group. + * + * @return A group containing low priority annotations. + */ + public Group getLowPriorityAnnotations () { + Group group = new Group(); + group.getChildren().addAll(wake, lineGroup); + return group; + } +} diff --git a/src/main/java/seng302/models/Colors.java b/src/main/java/seng302/models/Colors.java index 419753dc..23ef8f4e 100644 --- a/src/main/java/seng302/models/Colors.java +++ b/src/main/java/seng302/models/Colors.java @@ -11,10 +11,9 @@ public enum Colors { static Integer index = 0; public static Color getColor() { - index++; - if (index > 6) { - index = 1; + if (index == 6) { + index = 0; } - return Color.valueOf(values()[index-1].toString()); + return Color.valueOf(values()[index++].toString()); } } diff --git a/src/main/java/seng302/models/Event.java b/src/main/java/seng302/models/Event.java index e803845d..df298741 100644 --- a/src/main/java/seng302/models/Event.java +++ b/src/main/java/seng302/models/Event.java @@ -16,7 +16,7 @@ public class Event { private Mark mark1; // This mark private Mark mark2; // Next mark private int markPosInRace; // the position of the current mark in the race course - + private double heading; private final double ORIGIN_LAT = 32.320504; private final double ORIGIN_LON = -64.857063; private final double SCALE = 16000; @@ -36,6 +36,8 @@ public class Event { this.mark1 = mark1; this.mark2 = mark2; this.markPosInRace = markPosInRace; + this.heading = angleFromCoordinate(mark1, mark2); + } /** @@ -92,7 +94,7 @@ public class Event { if (this.isFinishingEvent) { return (this.getTimeString() + ", " + this.getBoat().getTeamName() + " finished the race"); } - System.out.println(this.getDistanceBetweenMarks()); +// System.out.println(this.getDistanceBetweenMarks()); return (this.getTimeString() + ", " + this.getBoat().getTeamName() + " passed " + this.mark1.getName() + " going heading " + this.getBoatHeading() + "°"); } @@ -138,6 +140,30 @@ public class Event { } + /** + * Calculates the angle between to angular co-ordinates on a sphere. + * + * @param geoPointOne first geographical location + * @param geoPointTwo second geographical location + * @return the angle from point one to point two + */ + private Double angleFromCoordinate(Mark geoPointOne, Mark geoPointTwo) { + if (geoPointTwo == null) + return null; + + double x1 = geoPointOne.getLatitude(); + double y1 = -geoPointOne.getLongitude(); + double x2 = geoPointTwo.getLatitude(); + double y2 = -geoPointTwo.getLongitude(); + + return Math.toDegrees(Math.atan2(x2-x1, y2-y1)); + + } + + public double getHeading() { + return heading; + } + public Mark getThisMark() { return this.mark1; } diff --git a/src/main/java/seng302/models/Race.java b/src/main/java/seng302/models/Race.java index 165d9468..01d1adf8 100644 --- a/src/main/java/seng302/models/Race.java +++ b/src/main/java/seng302/models/Race.java @@ -9,6 +9,7 @@ import java.util.*; * Created by mra106 on 8/3/2017. */ public class Race { + private ArrayList boats; // The boats in the race private ArrayList finishingOrder; // The order in which the boats finish the race private HashMap events = new HashMap<>(); // The events that occur in the race diff --git a/src/main/java/seng302/models/RaceObject.java b/src/main/java/seng302/models/RaceObject.java new file mode 100644 index 00000000..bc061584 --- /dev/null +++ b/src/main/java/seng302/models/RaceObject.java @@ -0,0 +1,87 @@ +package seng302.models; + +import javafx.geometry.Point2D; +import javafx.scene.Group; + +/** + * RaceObject defines the behaviour that animated objects whose position is updated from a yacht race data stream must + * adhere to. + */ +public abstract class RaceObject extends Group { + + //Time between sections of race + protected static double expectedUpdateInterval = 200; + + protected double rotationalGoal; + protected double currentRotation; + protected double rotationalVelocity; + protected double pixelVelocityX; + protected double pixelVelocityY; + + public Point2D getPosition () { + return new Point2D(super.getLayoutX(), getLayoutY()); + } + + public static double getExpectedUpdateInterval() { + return expectedUpdateInterval; + } + + /** + * + */ + public static void setExpectedUpdateInterval(double expectedUpdateInterval) { + RaceObject.expectedUpdateInterval = expectedUpdateInterval; + } + + /** + * Calculates the rotational velocity required to reach the rotationalGoal from the currentRotation. + */ + protected void calculateRotationalVelocity () { + if (Math.abs(rotationalGoal - currentRotation) > 180) { + if (rotationalGoal - currentRotation >= 0) { + this.rotationalVelocity = ((rotationalGoal - currentRotation) - 360) / expectedUpdateInterval; + } else { + this.rotationalVelocity = (360 + (rotationalGoal - currentRotation)) / expectedUpdateInterval; + } + } else { + this.rotationalVelocity = (rotationalGoal - currentRotation) / expectedUpdateInterval; + } + //Sometimes the rotation is too large to be realistic. In that case just do it instantly. + if (Math.abs(rotationalVelocity) > 1) { + rotationalVelocity = 0; + rotateTo(rotationalGoal); + } + } + + /** + * Sets the destination of everything within the RaceObject that has an ID in the array raceIds. The destination is + * set to the co-ordinates (x, y) with the given rotation. + * @param x X co-ordinate to move the graphics to. + * @param y Y co-ordinate to move the graphics to. + * @param rotation Rotation to move graphics to. + * @param raceIds RaceID of the object to move. + */ + public abstract void setDestination (double x, double y, double rotation, double speed, int... raceIds); + /** + * Sets the destination of everything within the RaceObject that has an ID in the array raceIds. The destination is + * set to the co-ordinates (x, y). + * @param x X co-ordinate to move the graphic to. + * @param y Y co-ordinate to move the graphic to. + * @param raceIds RaceID to the object to move. + */ + public abstract void setDestination (double x, double y, double speed, int... raceIds); + + public abstract void updatePosition (long timeInterval); + + public abstract void moveTo (double x, double y, double rotation); + + public abstract void moveTo (double x, double y); + + public abstract void moveGroupBy(double x, double y, double rotation); + + public abstract void rotateTo (double rotation); + + public abstract boolean hasRaceId (int... raceIds); + + public abstract int[] getRaceIds (); +} diff --git a/src/main/java/seng302/models/Wake.java b/src/main/java/seng302/models/Wake.java new file mode 100644 index 00000000..d389e8e7 --- /dev/null +++ b/src/main/java/seng302/models/Wake.java @@ -0,0 +1,106 @@ +package seng302.models; + +import javafx.scene.Group; +import javafx.scene.paint.Color; +import javafx.scene.shape.Arc; +import javafx.scene.shape.ArcType; +import javafx.scene.transform.Rotate; + +/** + * By default wake is a group containing 5 arcs. Each arc starts from the same point. Each arc is larger and more + * transparent than the last. On calling updatePositions() arcs rotate at velocities given by setRotationalVelocity(). + * The larger and more transparent an arc is the longer the delay before it rotates at the latest velocity. It is + * assumed that rotationalVelocities() are set regularly as wakes do not stop rotating and an array of velocities needs + * to be populated for the class to work as expected. + */ +class Wake extends Group { + + private int numWakes = 5; + private double[] velocities = new double[13]; + private Arc[] arcs = new Arc[numWakes]; + private double[] rotations = new double[numWakes]; + private int[] velocityIndices = new int[numWakes]; + private double sum = 0; + private static double max; + + /** + * Create a wake at the given location. + * @param startingX x location where the tip of wake arcs will be. + * @param startingY y location where the tip of wake arcs will be. + */ + Wake(double startingX, double startingY) { + super.setLayoutX(startingX); + super.setLayoutY(startingY); + Arc arc; + for (int i = 0; i < numWakes; i++) { + //Default triangle is -110 deg out of phase with a default wake and has angle of 40 deg. + arc = new Arc(0,0,0,0,-110,40); + //Opacity increases from 0.5 -> 0 evenly over the 5 wake arcs. + arc.setFill(new Color(0.18, 0.7, 1.0, 0.50 + -0.1 * i)); + arc.setType(ArcType.ROUND); + arcs[i] = arc; + } + super.getChildren().addAll(arcs); + } + + /** + * Sets the rotationalVelocity of each arc. Each arc is 3 velocities behind the next smallest arc. The smallest uses + * the latest given velocity. + * @param rotationalVelocity The rotationalVelocity the wake should move at. + * @param rotationGoal Where the wake will rotate to if the wake is calculated to be on a straight section. This is + * used to prevent desynchronisation with the Boat polygon. + * @param velocity The real world velocity of the boat in m/s. + */ + void setRotationalVelocity (double rotationalVelocity, double rotationGoal, double velocity) { +// if (Math.abs(rotationalVelocity) > 0.5) { +// rotationalVelocity = 0; +// } + sum -= Math.abs(velocities[(velocityIndices[0] + 10) % 13]); + sum += Math.abs(rotationalVelocity); +// System.out.println("sum = " + sum); + max = Math.max(max, rotationalVelocity); + if (sum < max) + rotate (rotationGoal); //In relatively straight segments the wake snaps to match the boats current position. + //This stops the wake from eventually becoming out of sync with the boat. + + //Update the index of the array of recent velocities that each wake uses. Each wake is 3 velocities behind the + //next smallest wake. + velocityIndices[0] = (13 + (velocityIndices[0] - 1) % 13) % 13; + velocities[velocityIndices[0]] = rotationalVelocity; + for (int i = 1; i < numWakes; i++) + velocityIndices[i] = (velocityIndices[0] + 3 * i) % 13; + + //Scale wakes based on velocity. + double baseRad = 20; + double rad; + for (Arc arc :arcs) { + rad = baseRad + velocity; + arc.setRadiusX(rad); + arc.setRadiusY(rad); + baseRad += 5 + (velocity / 2); + } + } + + /** + * Arcs rotate based on the distance they would have travelled over the supplied time interval. + * @param timeInterval the time interval, in microseconds, that the wake should move. + */ + void updatePosition (long timeInterval) { + for (int i = 0; i < numWakes; i++) { + rotations[i] = rotations[i] + velocities[velocityIndices[i]] * timeInterval; + arcs[i].getTransforms().setAll(new Rotate(rotations[i])); + } + } + + /** + * Rotate all wakes to the given rotation. + * @param rotation the from north angle in degrees to rotate to. + */ + void rotate (double rotation) { + for (int i = 0; i < arcs.length; i++) { + rotations[i] = rotation; + arcs[i].getTransforms().setAll(new Rotate(rotation)); + } + } + +} diff --git a/src/main/java/seng302/models/mark/GateMark.java b/src/main/java/seng302/models/mark/GateMark.java index 2b152e65..ffcf2b51 100644 --- a/src/main/java/seng302/models/mark/GateMark.java +++ b/src/main/java/seng302/models/mark/GateMark.java @@ -16,8 +16,8 @@ public class GateMark extends Mark { * @param singleMark1 one single mark inside of the gate mark * @param singleMark2 the second mark inside of the gate mark */ - public GateMark(String name, SingleMark singleMark1, SingleMark singleMark2, double latitude, double longitude) { - super(name, MarkType.GATE_MARK, latitude, longitude); + public GateMark(String name, MarkType type, SingleMark singleMark1, SingleMark singleMark2, double latitude, double longitude) { + super(name, type, latitude, longitude); this.singleMark1 = singleMark1; this.singleMark2 = singleMark2; } @@ -47,4 +47,5 @@ public class GateMark extends Mark { //return (this.getSingleMark1().getLongitude() + this.getSingleMark2().getLongitude()) / 2; return (this.getSingleMark1().getLongitude()); } + } diff --git a/src/main/java/seng302/models/mark/Mark.java b/src/main/java/seng302/models/mark/Mark.java index 3e635856..a32ba20f 100644 --- a/src/main/java/seng302/models/mark/Mark.java +++ b/src/main/java/seng302/models/mark/Mark.java @@ -10,15 +10,17 @@ public abstract class Mark { private MarkType markType; private double latitude; private double longitude; + private int id; /** * Create a mark instance by passing its name and type * @param name the name of the mark * @param markType the type of mark. either GATE_MARK or SINGLE_MARK. */ - public Mark (String name, MarkType markType) { + public Mark (String name, MarkType markType, int id) { this.name = name; this.markType = markType; + this.id = id; } public Mark(String name, MarkType markType, double latitude, double longitude) { @@ -26,8 +28,79 @@ public abstract class Mark { this.markType = markType; this.latitude = latitude; this.longitude = longitude; + id = 0; } + /** + * Calculated the heading in radians from first Mark to the second Mark. + * + * @param pointOne First Mark + * @param pointTwo Second Mark + * @return Heading in radians + */ + public static Double calculateHeadingRad(Mark pointOne, Mark pointTwo) { + Double longitude1 = pointOne.getLongitude(); + Double longitude2 = pointTwo.getLongitude(); + Double latitude1 = pointOne.getLatitude(); + Double latitude2 = pointTwo.getLatitude(); + return calculateHeadingRad(latitude1, longitude1, latitude2, longitude2); + } + + /** + * Calculate the heading in radians from geographical location with latitude1, longitude 1 to geographical + * latitude2, longitude 2 + * @param longitude1 Longitude of first point in degrees + * @param longitude2 Longitude of second point in degrees + * @param latitude1 Latitude of first point in degrees + * @param latitude2 Latitude of first point in degrees + * @return Heading in radians + */ + public static double calculateHeadingRad (Double latitude1, Double longitude1, Double latitude2, Double longitude2) { + latitude1 = Math.toRadians(latitude1); + latitude2 = Math.toRadians(latitude2); + Double longDiff= Math.toRadians(longitude2-longitude1); + Double y = Math.sin(longDiff)*Math.cos(latitude2); + Double x = Math.cos(latitude1)*Math.sin(latitude2)-Math.sin(latitude1)*Math.cos(latitude2)*Math.cos(longDiff); + return Math.atan2(y, x); + } + + /** + * Calculates the distance in meters from the first Mark to a second Mark + * + * @param pointOne First Mark + * @param pointTwo Second Mark + * @return Distance in meters + */ + public static Double calculateDistance(Mark pointOne, Mark pointTwo) { + Double longitude1 = pointOne.getLongitude(); + Double longitude2 = pointTwo.getLongitude(); + Double latitude1 = pointOne.getLatitude(); + Double latitude2 = pointTwo.getLatitude(); + return calculateDistance(latitude1, longitude1, latitude2, longitude2); + } + + /** + * Calculate the distance in meters from geographical location with latitude1, longitude 1 to geographical + * latitude2, longitude 2 + * + * @param longitude1 Longitude of first point in degrees + * @param longitude2 Longitude of second point in degrees + * @param latitude1 Latitude of first point in degrees + * @param latitude2 Latitude of first point in degrees + * @return Distance in meters + */ + public static Double calculateDistance (Double latitude1, Double longitude1, Double latitude2, Double longitude2) { + Double theta = longitude1 - longitude2; + Double dist = Math.sin(Math.toRadians(latitude1)) * Math.sin(Math.toRadians(latitude2)) + + Math.cos(Math.toRadians(latitude1)) * Math.cos(Math.toRadians(latitude2)) * + Math.cos(Math.toRadians(theta)); + dist = Math.acos(dist); + dist = Math.toDegrees(dist); + dist = dist * 60 * 1.1508; //nautical mile (distance between two degrees) * (degrees in a minute) + dist = dist * 1609.344; //ratio of miles to metres + return dist; + } + public String getName() { return name; } @@ -51,4 +124,13 @@ public abstract class Mark { public double getLongitude() { return longitude; } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + } diff --git a/src/main/java/seng302/models/mark/MarkGroup.java b/src/main/java/seng302/models/mark/MarkGroup.java new file mode 100644 index 00000000..16a1e06a --- /dev/null +++ b/src/main/java/seng302/models/mark/MarkGroup.java @@ -0,0 +1,242 @@ +package seng302.models.mark; + +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; +import javafx.scene.shape.Line; +import javafx.scene.transform.Rotate; +import seng302.models.RaceObject; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by CJIRWIN on 26/04/2017. + */ +public class MarkGroup extends RaceObject { + + private static int MARK_RADIUS = 5; + private static int LINE_THICKNESS = 2; + private static double DASHED_GAP_LEN = 2d; + private static double DASHED_LINE_LEN = 5d; + + private List marks = new ArrayList<>(); + private Mark mainMark; + private double[] nodePixelVelocitiesX; + private double[] nodePixelVelocitiesY; + private Point2D[] nodeDestinations; + + public MarkGroup (Mark mark, Point2D... points) { + nodePixelVelocitiesX = new double[points.length]; + nodePixelVelocitiesY = new double[points.length]; + nodeDestinations = new Point2D[points.length]; + marks.add(mark); + mainMark = mark; + Color color = Color.BLACK; + if (mark.getName().equals("Start")){ + color = Color.GREEN; + } else if (mark.getName().equals("Finish")){ + color = Color.RED; + } + Circle markCircle; + if (mark.getMarkType() == MarkType.SINGLE_MARK) { + markCircle = new Circle( + points[0].getX(), + points[0].getY(), + MARK_RADIUS, + color + ); + nodeDestinations = new Point2D[]{ + new Point2D(markCircle.getCenterX(), markCircle.getCenterY() + ) + }; + super.getChildren().add(markCircle); + } else { + marks.add(((GateMark) mark).getSingleMark1()); + marks.add(((GateMark) mark).getSingleMark2()); + nodePixelVelocitiesX = new double[]{0d,0d}; + nodePixelVelocitiesY = new double[]{0d,0d}; + nodeDestinations = new Point2D[2]; +// markCircle = new Circle( +// (points[1].getX() - points[0].getX()) / 2d, +// (points[1].getY() - points[0].getY()) / 2d, +// MARK_RADIUS, +// color +// + markCircle = new Circle( + points[0].getX(), + points[0].getY(), + MARK_RADIUS, + color + ); + nodeDestinations[0] = new Point2D(markCircle.getCenterX(), markCircle.getCenterY()); + super.getChildren().add(markCircle); +// markCircle = new Circle( +// -(points[1].getX() - points[0].getX()) / 2d, +// -(points[1].getY() - points[0].getY()) / 2d, +// MARK_RADIUS, +// color +// ); + markCircle = new Circle( + points[1].getX(), + points[1].getY(), + MARK_RADIUS, + color + ); + nodeDestinations[1] = new Point2D(markCircle.getCenterX(), markCircle.getCenterY()); + super.getChildren().add(markCircle); + Line line = new Line( + points[0].getX(), + points[0].getY(), + points[1].getX(), + points[1].getY() + ); + line.setStrokeWidth(LINE_THICKNESS); + line.setStroke(color); + if (mark.getMarkType() == MarkType.OPEN_GATE) { + line.getStrokeDashArray().addAll(DASHED_GAP_LEN, DASHED_LINE_LEN); + } + super.getChildren().add(line); + } + //moveTo(points[0].getX(), points[0].getY()); + } + + public void setDestination (double x, double y, double rotation, double speed, int... raceIds) { + setDestination(x, y, 0, raceIds); + this.rotationalGoal = rotation; + calculateRotationalVelocity(); + } + + public void setDestination (double x, double y, double speed, int... raceIds) { + for (int i = 0; i < marks.size(); i++) + for (int id : raceIds) + if (id == marks.get(i).getId()) + setDestinationChild(x, y, 0, Math.max(0, i-1)); + } + + + private void setDestinationChild (double x, double y, double speed, int childIndex) { + //double relativeX = x - super.getLayoutX(); + //double relativeY = y - super.getLayoutY(); + Circle markCircle = (Circle) super.getChildren().get(childIndex); + this.nodeDestinations[childIndex] = new Point2D(x, y); + //if (Math.abs(relativeX - markCircle.getCenterX()) > 30 && Math.abs(relativeY - markCircle.getCenterY()) > 30) { + this.nodePixelVelocitiesX[childIndex] = (x - markCircle.getCenterX()) / expectedUpdateInterval; + this.nodePixelVelocitiesY[childIndex] = (y - markCircle.getCenterY()) / expectedUpdateInterval; + //} + } + + public void rotateTo (double rotation) { + if (mainMark.getMarkType() != MarkType.SINGLE_MARK) { + Line line = (Line) super.getChildren().get(2); + double xCenter = Math.abs(line.getEndX() - line.getStartX()); + double yCenter = Math.abs(line.getEndY() - line.getStartY()); + super.getTransforms().setAll(new Rotate(rotation, xCenter, yCenter)); + } + } + + public void updatePosition (long timeInterval) { + Circle markCircle = (Circle) super.getChildren().get(0); + + if (nodePixelVelocitiesX[0] > 0 && markCircle.getCenterX() > nodeDestinations[0].getX() || + nodePixelVelocitiesX[0] < 0 && markCircle.getCenterX() < nodeDestinations[0].getY()) + nodePixelVelocitiesX[0] = 0; + else if (nodePixelVelocitiesX[0] != 0) + markCircle.setCenterX(markCircle.getCenterX() + nodePixelVelocitiesX[0] * timeInterval); + + if (nodePixelVelocitiesY[0] > 0 && markCircle.getCenterY() > nodeDestinations[0].getY() || + nodePixelVelocitiesY[0] < 0 && markCircle.getCenterY() < nodeDestinations[0].getY()) + nodePixelVelocitiesY[0] = 0; + else if (nodePixelVelocitiesY[0] != 0) + markCircle.setCenterY(markCircle.getCenterY() + nodePixelVelocitiesY[0] * timeInterval); + + if (mainMark.getMarkType() != MarkType.SINGLE_MARK) { + + Line line = (Line) super.getChildren().get(2); + line.setStartX(markCircle.getCenterX()); + line.setStartY(markCircle.getCenterY()); + + markCircle = (Circle) super.getChildren().get(1); + + if (nodePixelVelocitiesX[1] > 0 && markCircle.getCenterX() >= nodeDestinations[1].getX() || + nodePixelVelocitiesX[1] < 0 && markCircle.getCenterX() <= nodeDestinations[1].getX()) + nodePixelVelocitiesX[1] = 0; + else if (nodePixelVelocitiesX[1] != 0) + markCircle.setCenterX(markCircle.getCenterX() + nodePixelVelocitiesX[1] * timeInterval); + + if (nodePixelVelocitiesY[1] > 0 && markCircle.getCenterY() > nodeDestinations[1].getY() || + nodePixelVelocitiesY[1] < 0 && markCircle.getCenterY() < nodeDestinations[1].getY()) + nodePixelVelocitiesY[1] = 0; + else if (nodePixelVelocitiesY[1] != 0) + markCircle.setCenterY(markCircle.getCenterY() + nodePixelVelocitiesY[1] * timeInterval); + line.setEndX(markCircle.getCenterX()); + line.setEndY(markCircle.getCenterY()); + } + } + + public void moveGroupBy (double x, double y, double rotation) { + if (mainMark.getMarkType() != MarkType.SINGLE_MARK) { + Line line = (Line) super.getChildren().get(2); + for (int childIndex = 0; childIndex < 2; childIndex++){ + Circle mark = (Circle) super.getChildren().get(childIndex); + mark.setCenterY(mark.getCenterY() + y); + mark.setCenterX(mark.getCenterX() + x); + } + line.setStartX(line.getStartX() + x); + line.setStartY(line.getStartY() + y); + line.setEndX(line.getEndX() + x); + line.setEndY(line.getEndY() + y); + } else { + Circle mark = (Circle) super.getChildren().get(0); + mark.setCenterY(mark.getCenterY() + y); + mark.setCenterX(mark.getCenterX() + x); + } + rotateTo(currentRotation + rotation); + } + + public void moveTo (double x, double y, double rotation) { + moveTo(x, y); + rotateTo(rotation); + } + + public void moveTo (double x, double y) { + Circle markCircle = (Circle) super.getChildren().get(0); + markCircle.setCenterX(x); + markCircle.setCenterY(y); + if (mainMark.getMarkType() != MarkType.SINGLE_MARK) { + markCircle = (Circle) super.getChildren().get(1); + markCircle.setCenterX(x); + markCircle.setCenterY(y); + Line line = (Line) super.getChildren().get(2); + line.setStartX(x); + line.setStartY(y); + line.setEndX(x); + line.setEndY(y); + } + } + + public boolean hasRaceId (int... raceIds) { + for (int id : raceIds) + for (Mark mark : marks) + if (id == mark.getId()) + return true; + return false; + } + + public static int getMarkRadius() { + return MARK_RADIUS; + } + + public static void setMarkRadius(int markRadius) { + MARK_RADIUS = markRadius; + } + + public int[] getRaceIds () { + int[] idArray = new int[marks.size()]; + int i = 0; + for (Mark mark : marks) + idArray[i++] = mark.getId(); + return idArray; + } +} diff --git a/src/main/java/seng302/models/mark/MarkType.java b/src/main/java/seng302/models/mark/MarkType.java index 3de5cba3..4ac6a9e3 100644 --- a/src/main/java/seng302/models/mark/MarkType.java +++ b/src/main/java/seng302/models/mark/MarkType.java @@ -5,5 +5,5 @@ package seng302.models.mark; * Created by Haoming Yin (hyi25) on 17/3/17. */ public enum MarkType { - SINGLE_MARK, GATE_MARK + SINGLE_MARK, OPEN_GATE, CLOSED_GATE } diff --git a/src/main/java/seng302/models/mark/SingleMark.java b/src/main/java/seng302/models/mark/SingleMark.java index 81f6f0b4..d4b4f3f2 100644 --- a/src/main/java/seng302/models/mark/SingleMark.java +++ b/src/main/java/seng302/models/mark/SingleMark.java @@ -9,6 +9,7 @@ public class SingleMark extends Mark { private double lat; private double lon; private String name; + private int id; /** @@ -18,10 +19,11 @@ public class SingleMark extends Mark { * @param lat, the latitude of the marker * @param lon, the longitude of the marker */ - public SingleMark(String name, double lat, double lon) { - super(name, MarkType.SINGLE_MARK); + public SingleMark(String name, double lat, double lon, int id) { + super(name, MarkType.SINGLE_MARK, id); this.lat = lat; this.lon = lon; + this.id = id; } /** @@ -30,9 +32,10 @@ public class SingleMark extends Mark { * @param name, the name of the marker */ public SingleMark(String name) { - super(name, MarkType.SINGLE_MARK); + super(name, MarkType.SINGLE_MARK, 0); this.lat = 0; this.lon = 0; + this.id = 0; } public double getLatitude() { diff --git a/src/main/java/seng302/models/parsers/BoatsParser.java b/src/main/java/seng302/models/parsers/BoatsParser.java new file mode 100644 index 00000000..8180bde8 --- /dev/null +++ b/src/main/java/seng302/models/parsers/BoatsParser.java @@ -0,0 +1,77 @@ +package seng302.models.parsers; + +import org.w3c.dom.*; +import org.xml.sax.InputSource; +import seng302.models.Boat; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.InputStream; +import java.io.StringBufferInputStream; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * Created by ryan_ on 30/04/2017. + */ +public class BoatsParser extends FileParser { + private Document doc; + + public BoatsParser(String xmlString) { + this.doc = this.parseFile(xmlString); + } + + /** + * Create a boat instance from a given node if 'Type' is 'Yacht' + * + * @param node a boat node + * @return an instance of Boat + */ + private Boat parseBoat(Node node) { + try { + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) node; + if (element.getAttribute("Type").equals("Yacht")) { + String sourceID = element.getAttribute("SourceID"); + String boatName = element.getAttribute("BoatName"); + String shortName = element.getAttribute("ShortName"); + String stoweName = element.getAttribute("StoweName"); + String country = element.getAttribute("Country"); + Boat boat = new Boat(Integer.parseInt(sourceID), boatName, shortName, country); + return boat; + } + } else { + throw new NoSuchElementException("Cannot generate a boat by given node"); + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * Returns a list of boats from the xml. + * + * @return a list of boats + */ + public List getBoats() { + ArrayList boats = new ArrayList<>(); + + try { + NodeList nodes = this.doc.getElementsByTagName("Boat"); + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + Boat boat = parseBoat(node); + if (!(boat == null)) { + boats.add(boat); + } + } + return boats; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/src/main/java/seng302/models/parsers/CourseParser.java b/src/main/java/seng302/models/parsers/CourseParser.java index 0ad0c471..ae7f7856 100644 --- a/src/main/java/seng302/models/parsers/CourseParser.java +++ b/src/main/java/seng302/models/parsers/CourseParser.java @@ -35,7 +35,8 @@ public class CourseParser extends FileParser { String name = element.getElementsByTagName("name").item(0).getTextContent(); double lat = Double.valueOf(element.getElementsByTagName("latitude").item(0).getTextContent()); double lon = Double.valueOf(element.getElementsByTagName("longitude").item(0).getTextContent()); - SingleMark singleMark = new SingleMark(name, lat, lon); + int id = Integer.valueOf(element.getElementsByTagName("id").item(0).getTextContent()); + SingleMark singleMark = new SingleMark(name, lat, lon, id); return singleMark; } else { throw new NoSuchElementException("Cannot generate a mark by given node."); @@ -65,7 +66,11 @@ public class CourseParser extends FileParser { String name = element.getElementsByTagName("name").item(0).getTextContent(); SingleMark mark1 = generateSingleMark(element.getElementsByTagName("mark").item(0)); SingleMark mark2 = generateSingleMark(element.getElementsByTagName("mark").item(1)); - GateMark gateMark = new GateMark(name, mark1, mark2, mark1.getLatitude(), mark1.getLongitude()); + GateMark gateMark; + if (name.equals("Start") || name.equals("Finish")) + gateMark = new GateMark(name, MarkType.CLOSED_GATE, mark1, mark2, mark1.getLatitude(), mark1.getLongitude()); + else + gateMark = new GateMark(name, MarkType.OPEN_GATE, mark1, mark2, mark1.getLatitude(), mark1.getLongitude()); marks.put(name, gateMark); } } diff --git a/src/main/java/seng302/models/parsers/FileParser.java b/src/main/java/seng302/models/parsers/FileParser.java index b3d66b05..be162b9e 100644 --- a/src/main/java/seng302/models/parsers/FileParser.java +++ b/src/main/java/seng302/models/parsers/FileParser.java @@ -1,12 +1,14 @@ package seng302.models.parsers; import org.w3c.dom.Document; +import org.xml.sax.InputSource; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.StringReader; /** * Created by Haoming Yin (hyi25) on 16/3/2017 @@ -15,6 +17,8 @@ public abstract class FileParser { private String filePath; + public FileParser() {} + public FileParser(String path) { this.filePath = path; } @@ -32,6 +36,19 @@ public abstract class FileParser { e.printStackTrace(); return null; } + } + protected Document parseFile(String xmlString) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(new InputSource(new StringReader(xmlString))); + // optional, in order to recover info from broken line. + doc.getDocumentElement().normalize(); + return doc; + } catch (Exception e) { + e.printStackTrace(); + } + return null; } } diff --git a/src/main/java/seng302/models/parsers/PacketType.java b/src/main/java/seng302/models/parsers/PacketType.java new file mode 100644 index 00000000..66b86207 --- /dev/null +++ b/src/main/java/seng302/models/parsers/PacketType.java @@ -0,0 +1,53 @@ +package seng302.models.parsers; + +/** + * Created by Kusal on 4/24/2017. + */ +public enum PacketType { + HEARTBEAT, + RACE_STATUS, + DISPLAY_TEXT_MESSAGE, + XML_MESSAGE, + RACE_START_STATUS, + YACHT_EVENT_CODE, + YACHT_ACTION_CODE, + CHATTER_TEXT, + BOAT_LOCATION, + MARK_ROUNDING, + COURSE_WIND, + AVG_WIND, + OTHER; + + static PacketType assignPacketType(int packetType){ + switch(packetType){ + case 1: + return HEARTBEAT; + case 12: + return RACE_STATUS; + case 20: + return DISPLAY_TEXT_MESSAGE; + case 26: + return XML_MESSAGE; + case 27: + return RACE_START_STATUS; + case 29: + return YACHT_EVENT_CODE; + case 31: + return YACHT_ACTION_CODE; + case 36: + return CHATTER_TEXT; + case 37: + return BOAT_LOCATION; + case 38: + return MARK_ROUNDING; + case 44: + return COURSE_WIND; + case 47: + return AVG_WIND; + default: + } + return OTHER; + } + + +} diff --git a/src/main/java/seng302/models/parsers/StreamPacket.java b/src/main/java/seng302/models/parsers/StreamPacket.java new file mode 100644 index 00000000..5c2c0706 --- /dev/null +++ b/src/main/java/seng302/models/parsers/StreamPacket.java @@ -0,0 +1,44 @@ +package seng302.models.parsers; + +/** + * Created by kre39 on 23/04/17. + */ +public class StreamPacket { + + //Change int to an ENUM for the type + private PacketType type; + + private long messageLength; + private long timeStamp; + private byte[] payload; + + StreamPacket(int type, long messageLength, long timeStamp, byte[] payload) { + this.type = PacketType.assignPacketType(type); + this.messageLength = messageLength; + this.timeStamp = timeStamp; + this.payload = payload; +// System.out.println("type = " + this.type.toString()); + //switch the packet type to deal with what ever specific packet you want to deal with +// if (this.type == PacketType.XML_MESSAGE){ +// //System.out.println("--------"); +// System.out.println(new String(payload)); +// //StreamParser.parsePacket(this); +// } + } + + PacketType getType() { + return type; + } + + public long getMessageLength() { + return messageLength; + } + + byte[] getPayload() { + return payload; + } + + long getTimeStamp() { + return timeStamp; + } +} diff --git a/src/main/java/seng302/models/parsers/StreamParser.java b/src/main/java/seng302/models/parsers/StreamParser.java new file mode 100644 index 00000000..837636e7 --- /dev/null +++ b/src/main/java/seng302/models/parsers/StreamParser.java @@ -0,0 +1,455 @@ +package seng302.models.parsers; + + +import javafx.geometry.Point3D; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import seng302.models.Boat; +import seng302.models.parsers.packets.BoatPositionPacket; +import seng302.models.parsers.packets.StreamPacket; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringReader; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.PriorityBlockingQueue; + +/** + * The purpose of this class is to take in the stream of divided packets so they can be read + * and parsed in by turning the byte arrays into useful data. There are two public static hashmaps + * that are threadsafe so the visualiser can always access the latest speed and position available + * Created by kre39 on 23/04/17. + */ +public class StreamParser extends Thread{ + + public static ConcurrentHashMap> boatPositions = new ConcurrentHashMap<>(); + private String threadName; + private Thread t; + private static boolean raceStarted = false; + public static XMLParser xmlObject; + private static boolean raceFinished = false; + private static boolean streamStatus = false; + private static long timeSinceStart = -1; + private static List boats = new ArrayList<>(); + + public StreamParser(String threadName){ + this.threadName = threadName; + } + + /** + * Used to within threading so when the stream parser thread runs, it will keep looking for a packet to + * process until it is unable to find anymore packets + */ + public void run(){ + try { + System.out.println("START OF STREAM"); + streamStatus = true; + xmlObject = new XMLParser(); + while (StreamReceiver.packetBuffer == null || StreamReceiver.packetBuffer.size() < 1) { + Thread.sleep(1); + } + while (true){ + StreamPacket packet = StreamReceiver.packetBuffer.peek(); + //this code adds a delay to reading from the packetBuffer so + //out of order packets have time to order themselves in the queue + int delayTime = 1000; + int loopTime = delayTime * 10; + long transitTime = (System.currentTimeMillis()%loopTime - packet.getTimeStamp()%loopTime); + if (transitTime < 0){ + transitTime = loopTime + transitTime; + } + if (transitTime < delayTime) { + long sleepTime = delayTime - (transitTime); + Thread.sleep(sleepTime); + } + packet = StreamReceiver.packetBuffer.take(); + parsePacket(packet); + Thread.sleep(1); + while (StreamReceiver.packetBuffer.peek() == null) { + Thread.sleep(1); + } + } + } catch (Exception e){ + e.printStackTrace(); + } + } + + /** + * Used to start the stream parser thread when multithreading + */ + public void start () { + System.out.println("Starting " + threadName ); + if (t == null) { + t = new Thread (this, threadName); + t.start (); + } + } + + private static void parsePacket(StreamPacket packet) { + switch (packet.getType()){ + case HEARTBEAT: + extractHeartBeat(packet); + break; + case RACE_STATUS: + extractRaceStatus(packet); + break; + case DISPLAY_TEXT_MESSAGE: + extractDisplayMessage(packet); + break; + case XML_MESSAGE: + extractXmlMessage(packet); + break; + case RACE_START_STATUS: + extractRaceStartStatus(packet); + break; + case YACHT_EVENT_CODE: + extractYachtEventCode(packet); + break; + case YACHT_ACTION_CODE: + extractYachtActionCode(packet); + break; + case CHATTER_TEXT: + extractChatterText(packet); + break; + case BOAT_LOCATION: + extractBoatLocation(packet); + break; + case MARK_ROUNDING: + extractMarkRounding(packet); + break; + case COURSE_WIND: + extractCourseWind(packet); + break; + case AVG_WIND: + extractAvgWind(packet); + break; + default: + break; + //System.out.println(packet.getType().toString()); + } + } + + /** + * Extracts the seq num used in the heartbeat packet + * @param packet Packet parsed in to use the payload + */ + private static void extractHeartBeat(StreamPacket packet) { + long heartbeat = bytesToLong(packet.getPayload()); + } + + /** + * 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 start), along side + * this it'll also display the amount of time since the race has started or time till it starts + * @param packet Packet parsed in to use the payload + */ + private static void extractRaceStatus(StreamPacket packet){ + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + long currentTime = bytesToLong(Arrays.copyOfRange(payload,1,7)); + long raceId = bytesToLong(Arrays.copyOfRange(payload,7,11)); + int raceStatus = payload[11]; +// System.out.println("raceStatus = " + raceStatus); + long expectedStartTime = bytesToLong(Arrays.copyOfRange(payload,12,18)); + DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + long timeTillStart = ((new Date (expectedStartTime)).getTime() - (new Date (currentTime)).getTime())/1000; + if (timeTillStart > 0) { + timeSinceStart = timeTillStart; + System.out.println("Time till start: " + timeTillStart + " Seconds"); + } else { + if (raceStatus == 4 || raceStatus == 8){ + raceFinished = true; + raceStarted = false; + System.out.println("RACE HAS FINISHED"); + } else if (!raceStarted){ + raceStarted = true; + raceFinished = false; + System.out.println("RACE HAS STARTED"); + } + System.out.println("Time since start: " + -1 * timeTillStart + " Seconds"); + timeSinceStart = timeTillStart; + } + long windDir = bytesToLong(Arrays.copyOfRange(payload,18,20)); + long windSpeed = bytesToLong(Arrays.copyOfRange(payload,20,22)); + int noBoats = payload[22]; + int raceType = payload[23]; + ArrayList boatStatuses = new ArrayList<>(); + for (int i = 0; i < noBoats; i++){ + String boatStatus = "SourceID: " + bytesToLong(Arrays.copyOfRange(payload,24 + (i * 20),28+ (i * 20))); + boatStatus += "\nBoat Status: " + (int)payload[28 + (i * 20)]; + boatStatus += "\nLegNumber: " + (int)payload[29 + (i * 20)]; + boatStatus += "\nPenaltiesAwarded: " + (int)payload[29 + (i * 20)]; + boatStatus += "\nPenaltiesServed: " + (int)payload[30 + (i * 20)]; + boatStatus += "\nEstTimeAtNextMark: " + bytesToLong(Arrays.copyOfRange(payload,31 + (i * 20),37+ (i * 20))); + boatStatus += "\nEstTimeAtFinish: " + bytesToLong(Arrays.copyOfRange(payload,37 + (i * 20),43+ (i * 20))); + boatStatuses.add(boatStatus); + } + } + + /** + * Used to extract the messages passed through with the display message packet + * @param packet Packet parsed in to use the payload + */ + private static void extractDisplayMessage(StreamPacket packet){ + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + int numOfLines = payload[3]; + int totalLen = 0; + for (int i = 0; i < numOfLines; i++){ + int lineNum = payload[4 + totalLen]; + int textLength = payload[5 + totalLen]; + byte[] messageTextBytes = Arrays.copyOfRange(payload,6 + totalLen,6 + textLength + totalLen); + String messageText = new String(messageTextBytes); + totalLen += 2 + textLength; + } + } + + /** + * Used to read in the xml data. Will call the specific methods to create the course and boats + * @param packet Packet parsed in to use the payload + */ + private static void extractXmlMessage(StreamPacket packet){ + + byte[] payload = packet.getPayload(); + + int messageType = payload[9]; + long messagelength = bytesToLong(Arrays.copyOfRange(payload,12,14)); + String xmlMessage = new String((Arrays.copyOfRange(payload,14,(int) (14 + messagelength)))).trim(); + //System.out.println("xmlMessage2 = " + xmlMessage); + + //Create XML document Object + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = null; + Document doc = null; + try { + db = dbf.newDocumentBuilder(); + doc = db.parse(new InputSource(new StringReader(xmlMessage))); + } catch (ParserConfigurationException | IOException | SAXException e) { + e.printStackTrace(); + } + + xmlObject.constructXML(doc, messageType); + } + + /** + * Extracts the race start status from the packet, currently is unused within the app but + * is here for potential future use + * @param packet Packet parsed in to use the payload + */ + private static void extractRaceStartStatus(StreamPacket packet){ + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + long timeStamp = bytesToLong(Arrays.copyOfRange(payload,1,7)); + long raceStartTime = bytesToLong(Arrays.copyOfRange(payload,9,15)); + long raceId = bytesToLong(Arrays.copyOfRange(payload,15,19)); + int notificationType = payload[19]; + } + + /** + * When a yacht event occurs this will parse the byte array to retrieve the necessary info, + * currently unused + * @param packet Packet parsed in to use the payload + */ + private static void extractYachtEventCode(StreamPacket packet){ + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + long timeStamp = bytesToLong(Arrays.copyOfRange(payload,1,7)); + long raceId = bytesToLong(Arrays.copyOfRange(payload,9,13)); + long subjectId = bytesToLong(Arrays.copyOfRange(payload,13,17)); + long incidentId = bytesToLong(Arrays.copyOfRange(payload,17,21)); + int eventId = payload[21]; + } + + /** + * When a yacht action occurs this will parse the parse the byte array to retrieve the necessary info, + * currently unused + * @param packet Packet parsed in to use the payload + */ + private static void extractYachtActionCode(StreamPacket packet){ + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + long timeStamp = extractTimeStamp(Arrays.copyOfRange(payload,1,7), 6); + long subjectId = bytesToLong(Arrays.copyOfRange(payload,9,13)); + long incidentId = bytesToLong(Arrays.copyOfRange(payload,13,17)); + int eventId = payload[17]; +// System.out.println("eventId = " + eventId); + } + + /** + * Strips the message from the chatter text type packets, currently the message is unused + * @param packet Packet parsed in to use the payload + */ + private static void extractChatterText(StreamPacket packet){ + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + int messageType = payload[1]; + int length = payload[2]; + String message = new String(Arrays.copyOfRange(payload,3,3 + length)); + } + + /** + * Used to breakdown the boatlocation packets so the boat coordinates, id and groundspeed are all used + * All the other extra data is still being read and translated however is unused. + * @param packet Packet parsed in to use the payload + */ + private static void extractBoatLocation(StreamPacket packet){ + byte[] payload = packet.getPayload(); + byte deviceType = payload[15]; + byte[] seqBytes = Arrays.copyOfRange(payload,11,15); + byte[] latBytes = Arrays.copyOfRange(payload,16,20); + byte[] lonBytes = Arrays.copyOfRange(payload,20,24); + byte[] boatIdBytes = Arrays.copyOfRange(payload,7,11); + byte[] headingBytes = Arrays.copyOfRange(payload,28,30); + byte[] groundSpeedBytes = Arrays.copyOfRange(payload,38,40); + + long timeValid = bytesToLong(Arrays.copyOfRange(payload,1,7)); +// int boatSeq = ByteBuffer.wrap(seqBytes).getInt(); + long seq = bytesToLong(seqBytes); + long boatId = bytesToLong(boatIdBytes); + long rawLat = bytesToLong(latBytes); + long rawLon = bytesToLong(lonBytes); + double lat = ((180d * (double)rawLat)/Math.pow(2,31)); + double lon = ((180d *(double)rawLon)/Math.pow(2,31)); + long heading = bytesToLong(headingBytes); + double groundSpeed = bytesToLong(groundSpeedBytes)/1000.0; + short s = (short) ((groundSpeedBytes[1] & 0xFF) << 8 | (groundSpeedBytes[0] & 0xFF)); + if ((int)deviceType == 1 || (int)deviceType == 3){ + + BoatPositionPacket boatPacket = new BoatPositionPacket(boatId, timeValid, lat, lon, heading, groundSpeed); + + if (!boatPositions.containsKey(boatId)){ + boatPositions.put(boatId, new PriorityBlockingQueue(256, new Comparator() { + @Override + public int compare(BoatPositionPacket p1, BoatPositionPacket p2) { + return (int) (p1.getTimeValid() - p2.getTimeValid()); + } + })); + } + boatPositions.get(boatId).put(boatPacket); + } + } + + + + private static void extractMarkRounding(StreamPacket packet){ + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + long timeStamp = bytesToLong(Arrays.copyOfRange(payload,1,7)); + long raceId = bytesToLong(Arrays.copyOfRange(payload,9,13)); + long subjectId = bytesToLong(Arrays.copyOfRange(payload,13,17)); + int boatStatus = payload[17]; + int roundingSide = payload[18]; + int markType = payload[19]; + int markId = payload[20]; + } + + private static void extractCourseWind(StreamPacket packet){ + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + int selectedWindId = payload[1]; + int loopCount = payload[2]; + ArrayList windInfo = new ArrayList<>(); + for (int i = 0; i < loopCount; i++){ + String wind = "WindId: " + payload[3 + (20 * i)]; + wind += "\nTime: " + bytesToLong(Arrays.copyOfRange(payload,4 + (20 * i),10 + (20 * i))); + wind += "\nRaceId: " + bytesToLong(Arrays.copyOfRange(payload,10 + (20 * i),14 + (20 * i))); + wind += "\nWindDirection: " + bytesToLong(Arrays.copyOfRange(payload,14 + (20 * i),16 + (20 * i))); + wind += "\nWindSpeed: " + bytesToLong(Arrays.copyOfRange(payload,16 + (20 * i),18 + (20 * i))); + wind += "\nBestUpWindAngle: " + bytesToLong(Arrays.copyOfRange(payload,18 + (20 * i),20 + (20 * i))); + wind += "\nBestDownWindAngle: " + bytesToLong(Arrays.copyOfRange(payload,20 + (20 * i),22 + (20 * i))); + wind += "\nFlags: " + String.format("%8s", Integer.toBinaryString(payload[22 + (20 * i)] & 0xFF)).replace(' ', '0'); + windInfo.add(wind); + } + } + + private static void extractAvgWind(StreamPacket packet){ + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + long timeStamp = bytesToLong(Arrays.copyOfRange(payload,1,7)); + long rawPeriod = bytesToLong(Arrays.copyOfRange(payload,7,9)); + long rawSamplePeriod = bytesToLong(Arrays.copyOfRange(payload,9,11)); + long period2 = bytesToLong(Arrays.copyOfRange(payload,11,13)); + long speed2 = bytesToLong(Arrays.copyOfRange(payload,13,15)); + long period3 = bytesToLong(Arrays.copyOfRange(payload,15,17)); + long speed3 = bytesToLong(Arrays.copyOfRange(payload,17,19)); + long period4 = bytesToLong(Arrays.copyOfRange(payload,19,21)); + long speed4 = bytesToLong(Arrays.copyOfRange(payload,21,23)); + } + + /** + * takes an array of up to 7 bytes and returns a positive + * long constructed from the input bytes + * + * @return a positive long if there is less than 7 bytes -1 otherwise + */ + private static long bytesToLong(byte[] bytes){ + long partialLong = 0; + int index = 0; + for (byte b: bytes){ + if (index > 6){ + return -1; + } + partialLong = partialLong | (b & 0xFFL) << (index * 8); + index++; + } + return partialLong; + } + + /** + * returns false if race not started, true otherwise + * + * @return race started status + */ + public static boolean isRaceStarted() { + return raceStarted; + } + + /** + * returns false if stream not connected, true otherwise + * + * @return stream started status + */ + public static boolean isStreamStatus() { + return streamStatus; + } + + /** + * returns race timer + * + * @return race timer in long + */ + public static long getTimeSinceStart() { + return timeSinceStart; + } + + /** + * return false if race not finished, true otherwise + * + * @return race finished status + */ + public static boolean isRaceFinished() { + return raceFinished; + } + + /** + * return list of boats from the server + * + * @return list of boats + */ + public static List getBoats() { + return boats; + } + + public static XMLParser getXmlObject() { + return xmlObject; + } +} + diff --git a/src/main/java/seng302/models/parsers/StreamReceiver.java b/src/main/java/seng302/models/parsers/StreamReceiver.java new file mode 100644 index 00000000..4c16991e --- /dev/null +++ b/src/main/java/seng302/models/parsers/StreamReceiver.java @@ -0,0 +1,158 @@ +package seng302.models.parsers; + +import seng302.models.parsers.packets.StreamPacket; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.zip.CRC32; +import java.util.zip.Checksum; + + +public class StreamReceiver extends Thread { + private InputStream stream; + private Socket host; + private ByteArrayOutputStream crcBuffer; + private Thread t; + private String threadName; + public static PriorityBlockingQueue packetBuffer; + + public StreamReceiver(String hostAddress, int hostPort, String threadName) { + this.threadName = threadName; + try { + host = new Socket(hostAddress, hostPort); + } catch (IOException e) { + e.printStackTrace(); + System.exit(1); + } + } + + public void run(){ + PriorityBlockingQueue pq = new PriorityBlockingQueue<>(256, new Comparator() { + @Override + public int compare(StreamPacket s1, StreamPacket s2) { + return (int) (s1.getTimeStamp() - s2.getTimeStamp()); + } + }); + packetBuffer = pq; + connect(); + } + + public void start () { + System.out.println("Starting " + threadName ); + if (t == null) { + t = new Thread (this, threadName); + t.start (); + } + } + + + public StreamReceiver(Socket host, PriorityBlockingQueue packetBuffer){ + this.host=host; + this.packetBuffer = packetBuffer; + } + + + public void connect(){ + try { + stream = host.getInputStream(); + } catch (IOException e) { + e.printStackTrace(); + System.exit(1); + } + + int sync1; + int sync2; + boolean moreBytes = true; + while(moreBytes) { + try { + crcBuffer = new ByteArrayOutputStream(); + sync1 = readByte(); + sync2 = readByte(); + //checking if it is the start of the packet + if(sync1 == 0x47 && sync2 == 0x83) { + int type = readByte(); + //No. of milliseconds since Jan 1st 1970 + long timeStamp = bytesToLong(getBytes(6)); + skipBytes(4); + long payloadLength = bytesToLong(getBytes(2)); + byte[] payload = getBytes((int) payloadLength); + Checksum checksum = new CRC32(); + checksum.update(crcBuffer.toByteArray(), 0, crcBuffer.size()); + long computedCrc = checksum.getValue(); + long packetCrc = bytesToLong(getBytes(4)); + if (computedCrc == packetCrc) { + packetBuffer.add(new StreamPacket(type, payloadLength, timeStamp, payload)); + } else { + System.err.println("Packet has been dropped"); + } + } + } catch (Exception e) { + moreBytes = false; + } + + } + } + + private int readByte() throws Exception { + int currentByte = -1; + try { + currentByte = stream.read(); + crcBuffer.write(currentByte); + } catch (IOException e) { + e.printStackTrace(); + } + if (currentByte == -1){ + throw new Exception(); + } + return currentByte; + } + + private byte[] getBytes(int n) throws Exception{ + byte[] bytes = new byte[n]; + for (int i = 0; i < n; i++){ + bytes[i] = (byte) readByte(); + } + return bytes; + } + + + private void skipBytes(long n) throws Exception{ + for (int i=0; i < n; i++){ + readByte(); + } + } + + /** + * takes an array of up to 7 bytes in little endian format and + * returns a positive long constructed from the input bytes + * + * @return a positive long if there is less than 8 bytes -1 otherwise + */ + private long bytesToLong(byte[] bytes){ + long partialLong = 0; + int index = 0; + for (byte b: bytes){ + if (index > 6){ + return -1; + } + partialLong = partialLong | (b & 0xFFL) << (index * 8); + index++; + } + return partialLong; + } + + + public static void main(String[] args) { + + StreamReceiver sr = new StreamReceiver("csse-s302staff.canterbury.ac.nz", 4941,"TestThread1"); + //StreamReceiver sr = new StreamReceiver("livedata.americascup.com", 4941, "TestThread2"); + sr.start(); + + } +} diff --git a/src/main/java/seng302/models/parsers/TeamsParser.java b/src/main/java/seng302/models/parsers/TeamsParser.java index 2986f448..afa31410 100644 --- a/src/main/java/seng302/models/parsers/TeamsParser.java +++ b/src/main/java/seng302/models/parsers/TeamsParser.java @@ -27,7 +27,8 @@ public class TeamsParser extends FileParser { String name = element.getElementsByTagName("name").item(0).getTextContent(); String alias = element.getElementsByTagName("alias").item(0).getTextContent(); double velocity = Double.valueOf(element.getElementsByTagName("velocity").item(0).getTextContent()); - Boat boat = new Boat(name, velocity, alias); + int id = Integer.valueOf(element.getElementsByTagName("id").item(0).getTextContent()); + Boat boat = new Boat(name, velocity, alias, id); return boat; } else { throw new NoSuchElementException("Cannot generate a boat by given node"); @@ -44,11 +45,11 @@ public class TeamsParser extends FileParser { */ public ArrayList getBoats() { ArrayList boats = new ArrayList<>(); + try { NodeList nodes = this.doc.getElementsByTagName("team"); for (int i = 0; i < nodes.getLength(); i++) { Node node = nodes.item(i); - boats.add(parseBoat(node)); } return boats; diff --git a/src/main/java/seng302/models/parsers/XMLParser.java b/src/main/java/seng302/models/parsers/XMLParser.java new file mode 100644 index 00000000..a50cbcbc --- /dev/null +++ b/src/main/java/seng302/models/parsers/XMLParser.java @@ -0,0 +1,475 @@ +package seng302.models.parsers; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.ArrayList; + +/** + * Class to create an XML object from the XML Packet Messages. + * + * Example usage: + * + * Document doc; // some xml document + * Integer xmlMessageType; // an Integer of value 5, 6, 7 + * + * xmlP = new XMLParser(doc, xmlMessageType); + * RegattaXMLObject rXmlObj = xmlP.createRegattaXML(); // creates a regattaXML object. + * + */ +public class XMLParser { + + private Document xmlDoc; + + private RaceXMLObject raceXML; + private RegattaXMLObject regattaXML; + private BoatXMLObject boatXML; + + public XMLParser() { + } + + /** + * Constructor for XMLParser + * @param doc Document to create XML object. + * @param messageType Defines if a message is a RegattaXML(5), RaceXML(6), BoatXML(7). + */ + public void constructXML(Document doc, Integer messageType) { + this.xmlDoc = doc; + switch (messageType) { + case 5: + regattaXML = new RegattaXMLObject(this.xmlDoc); + break; + case 6: + raceXML = new RaceXMLObject(this.xmlDoc); + break; + case 7: + boatXML = new BoatXMLObject(this.xmlDoc); + break; + } + } + + public RaceXMLObject getRaceXML() { return raceXML; } + public RegattaXMLObject getRegattaXML() { return regattaXML; } + public BoatXMLObject getBoatXML() { return boatXML; } + + + /** + * Returns the text content of a given child element tag, assuming it exists, as an Integer. + * @param ele Document Element with child elements. + * @param tag Tag to find in document elements child elements. + * @return Text content from tag if found, null otherwise. + */ + private static Integer getElementInt(Element ele, String tag) { + NodeList tagList = ele.getElementsByTagName(tag); + if (tagList.getLength() > 0) { + return Integer.parseInt(tagList.item(0).getTextContent()); + } else { + return null; + } + } + + /** + * Returns the text content of a given child element tag, assuming it exists, as an String. + * @param ele Document Element with child elements. + * @param tag Tag to find in document elements child elements. + * @return Text content from tag if found, null otherwise. + */ + private static String getElementString(Element ele, String tag) { + NodeList tagList = ele.getElementsByTagName(tag); + if (tagList.getLength() > 0) { + return tagList.item(0).getTextContent(); + } else { + return null; + } + } + + /** + * Returns the text content of a given child element tag, assuming it exists, as a Double. + * @param ele Document Element with child elements. + * @param tag Tag to find in document elements child elements. + * @return Text content from tag if found, null otherwise. + */ + private static Double getElementDouble(Element ele, String tag) { + NodeList tagList = ele.getElementsByTagName(tag); + if (tagList.getLength() > 0) { + return Double.parseDouble(tagList.item(0).getTextContent()); + } else { + return null; + } + } + + /** + * Returns the text content of an attribute of a given Node, assuming it exists, as a String. + * @param n A node object that should have some attributes + * @param attr The attribute you want to get from the given node. + * @return The String representation of the text content of an attribute in the given node, else returns null. + */ + private static String getNodeAttributeString(Node n, String attr) { + Node attrItem = n.getAttributes().getNamedItem(attr); + if (attrItem != null) { + return attrItem.getTextContent(); + } else { + return null; + } + } + + /** + * Returns the text content of an attribute of a given Node, assuming it exists, as an Integer. + * @param n A node object that should have some attributes + * @param attr The attribute you want to get from the given node. + * @return The Integer representation of the text content of an attribute in the given node, else returns null. + */ + private static Integer getNodeAttributeInt(Node n, String attr) { + Node attrItem = n.getAttributes().getNamedItem(attr); + if (attrItem != null) { + return Integer.parseInt(attrItem.getTextContent()); + } else { + return null; + } + } + + /** + * Returns the text content of an attribute of a given Node, assuming it exists, as a Double. + * @param n A node object that should have some attributes + * @param attr The attribute you want to get from the given node. + * @return The Double representation of the text content of an attribute in the given node, else returns null. + */ + private static Double getNodeAttributeDouble(Node n, String attr) { + Node attrItem = n.getAttributes().getNamedItem(attr); + if (attrItem != null) { + return Double.parseDouble(attrItem.getTextContent()); + } else { + return null; + } + } + + public class RegattaXMLObject { + //Regatta Info + private Integer regattaID; + private String regattaName; + private String courseName; + private Double centralLat; + private Double centralLng; + private Integer utcOffset; + + /** + * Constructor for a RegattaXMLObject. + * Takes the information from a Document object and creates a more usable format. + * @param doc XML Document Object + */ + RegattaXMLObject(Document doc) { + Element docEle = doc.getDocumentElement(); + + this.regattaID = getElementInt(docEle, "RegattaID"); + this.regattaName = getElementString(docEle, "RegattaName"); + this.courseName = getElementString(docEle, "CourseName"); + this.centralLat = getElementDouble(docEle, "CentralLatitude"); + this.centralLng = getElementDouble(docEle, "CentralLongitude"); + this.utcOffset = getElementInt(docEle, "UtcOffset"); + } + + public Integer getRegattaID() { return regattaID; } + public String getRegattaName() { return regattaName; } + public String getCourseName() { return courseName; } + public Double getCentralLat() { return centralLat; } + public Double getCentralLng() { return centralLng; } + public Integer getUtcOffset() { return utcOffset; } + + } + + public class RaceXMLObject { + + // Race Info + private Integer raceID; + private String raceType; + private String creationTimeDate; // XML Creation Time + + //Race Start Details + private String raceStartTime; + private Boolean postponeStatus; + + //Non atomic race attributes + private ArrayList participants; + private ArrayList course; + private ArrayList compoundMarkSequence; + private ArrayList courseLimit; + + /** + * Constructor for a RaceXMLObject. + * Takes the information from a Document object and creates a more usable format. + * @param doc XML Document Object + */ + RaceXMLObject(Document doc) { + Element docEle = doc.getDocumentElement(); + + //Atomic and Semi-Atomic Elements + this.raceID = getElementInt(docEle, "RaceID"); + this.raceType = getElementString(docEle, "RaceType"); + this.creationTimeDate = getElementString(docEle, "CreationTimeDate"); + + Node raceStart = docEle.getElementsByTagName("RaceStartTime").item(0); + this.raceStartTime = getNodeAttributeString(raceStart, "Start") ; + this.postponeStatus = Boolean.parseBoolean(getNodeAttributeString(raceStart, "Postpone")); + + //Participants + participants = new ArrayList<>(); + + NodeList pList = docEle.getElementsByTagName("Participants").item(0).getChildNodes(); + for (int i = 0; i < pList.getLength(); i++) { + Node pNode = pList.item(i); + String entry; + if (pNode.getNodeName().equals("Yacht")) { + Integer sourceID = getNodeAttributeInt(pNode, "SourceID"); + + if (pNode.getAttributes().getLength() == 2) { + entry = getNodeAttributeString(pNode, "Entry"); + } else { + entry = null; + } + + Participant pa = new Participant(sourceID, entry); + participants.add(pa); + } + } + + //Course + course = new ArrayList<>(); + + NodeList cMarkList = docEle.getElementsByTagName("Course").item(0).getChildNodes(); + for (int i = 0; i < cMarkList.getLength(); i++) { + Node cMarkNode = cMarkList.item(i); + if (cMarkNode.getNodeName().equals("CompoundMark")) { + CompoundMark cMark = new CompoundMark(cMarkNode); + course.add(cMark); + } + } + + //Course Mark Sequence + compoundMarkSequence = new ArrayList<>(); + + NodeList cornerList = docEle.getElementsByTagName("CompoundMarkSequence").item(0).getChildNodes(); + for (int i = 0; i < cornerList.getLength(); i++) { + Node cornerNode = cornerList.item(i); + if (cornerNode.getNodeName().equals("Corner")) { + Corner corner = new Corner(cornerNode); + compoundMarkSequence.add(corner); + } + } + + //Course Limits + courseLimit = new ArrayList<>(); + + NodeList limitList = docEle.getElementsByTagName("CourseLimit").item(0).getChildNodes(); + for (int i = 0; i < limitList.getLength(); i++) { + Node limitNode = limitList.item(i); + if (limitNode.getNodeName().equals("Limit")) { + Limit limit = new Limit(limitNode); + courseLimit.add(limit); + } + } + } + + public Integer getRaceID() { return raceID; } + public String getRaceType() { return raceType; } + public String getCreationTimeDate() { return creationTimeDate; } + public String getRaceStartTime() { return raceStartTime; } + public Boolean getPostponeStatus() { return postponeStatus; } + + public ArrayList getParticipants() { return participants; } + public ArrayList getCompoundMarks() { return course; } + public ArrayList getCompoundMarkSequence() { return compoundMarkSequence; } + public ArrayList getCourseLimit() { return courseLimit; } + + public class Participant { + Integer sourceID; + String entry; + + Participant(Integer sourceID, String entry) { + this.sourceID = sourceID; + this.entry = entry; + } + + public Integer getsourceID() { return sourceID; } + public String getEntry() { return entry; } + } + + public class CompoundMark { + private Integer markID; + private String cMarkName; + private ArrayList marks; + + CompoundMark(Node compoundMark) { + marks = new ArrayList<>(); + this.markID = getNodeAttributeInt(compoundMark, "CompoundMarkID"); + this.cMarkName = getNodeAttributeString(compoundMark, "Name"); + NodeList childMarks = compoundMark.getChildNodes(); + for (int i = 0; i < childMarks.getLength(); i++) { + Node markNode = childMarks.item(i); + if (markNode.getNodeName().equals("Mark")) { + Mark mark = new Mark(markNode); + marks.add(mark); + } + } + } + + public Integer getMarkID() { return markID; } + public String getcMarkName() { return cMarkName; } + public ArrayList getMarks() { return marks; } + + public class Mark { + private Integer seqID; + private Integer sourceID; + private String markName; + private Double targetLat; + private Double targetLng; + + Mark(Node markNode) { + this.seqID = getNodeAttributeInt(markNode, "SeqID"); + this.sourceID = getNodeAttributeInt(markNode, "SourceID"); + this.markName = getNodeAttributeString(markNode, "Name"); + this.targetLat = getNodeAttributeDouble(markNode, "TargetLat"); + this.targetLng = getNodeAttributeDouble(markNode, "TargetLng"); + } + + public Integer getSeqID() { return seqID; } + public Integer getSourceID() { return sourceID; } + public String getMarkName() { return markName; } + public Double getTargetLat() { return targetLat; } + public Double getTargetLng() { return targetLng; } + } + } + + public class Corner { + private Integer seqID; + private Integer compoundMarkID; + private String rounding; + private Integer zoneSize; + + Corner(Node cornerNode) { + this.seqID = getNodeAttributeInt(cornerNode, "SeqID"); + this.compoundMarkID = getNodeAttributeInt(cornerNode, "CompoundMarkID"); + this.rounding = getNodeAttributeString(cornerNode, "Rounding"); + this.zoneSize = getNodeAttributeInt(cornerNode, "ZoneSize"); + } + + public Integer getSeqID() { return seqID; } + public Integer getCompoundMarkID() { return compoundMarkID; } + public String getRounding() { return rounding; } + public Integer getZoneSize() { return zoneSize; } + } + + public class Limit { + private Integer seqID; + private Double lat; + private Double lng; + + Limit(Node limitNode) { + this.seqID = getNodeAttributeInt(limitNode, "SeqID"); + this.lat = getNodeAttributeDouble(limitNode, "Lat"); + this.lng = getNodeAttributeDouble(limitNode, "Lon"); + } + + public Integer getSeqID() { return seqID; } + public Double getLat() { return lat; } + public Double getLng() { return lng; } + } + + } + + public class BoatXMLObject { + + private String lastModified; + private Integer version; + + //Settings for the boat type in the race. This may end up having to be reworked if multiple boat types compete. + private String boatType; + private Double boatLength; + private Double hullLength; + private Double markZoneSize; + private Double courseZoneSize; + private ArrayList zoneLimits;// will only contain 5 elements. Limits 1-5 + + //Boats + ArrayList boats; + + /** + * Constructor for a BoatXMLObject. + * Takes the information from a Document object and creates a more usable format. + * @param doc XML Document Object + */ + BoatXMLObject(Document doc) { + + Element docEle = doc.getDocumentElement(); + + this.lastModified = getElementString(docEle, "Modified"); + this.version = getElementInt(docEle, "Version"); + + NodeList settingsList = docEle.getElementsByTagName("Settings").item(0).getChildNodes(); + this.boatType = getNodeAttributeString(settingsList.item(1), "Type"); + this.boatLength = getNodeAttributeDouble(settingsList.item(3), "BoatLength"); + this.hullLength = getNodeAttributeDouble(settingsList.item(3), "HullLength"); + this.markZoneSize = getNodeAttributeDouble(settingsList.item(5), "MarkZoneSize"); + this.courseZoneSize = getNodeAttributeDouble(settingsList.item(5), "CourseZoneSize"); + + Node zoneLimitsList = settingsList.item(7); + this.zoneLimits = new ArrayList<>(); + for (int i = 0; i < zoneLimitsList.getAttributes().getLength(); i++) { + String tag = String.format("Limit%d", i+1); + this.zoneLimits.add(getNodeAttributeDouble(zoneLimitsList, tag)); + } + + this.boats = new ArrayList<>(); + NodeList boatsList = docEle.getElementsByTagName("Boats").item(0).getChildNodes(); + for (int i = 0; i < boatsList.getLength(); i++) { + Node currentBoat = boatsList.item(i); + if (currentBoat.getNodeName().equals("Boat")) { + Boat boat = new Boat(currentBoat); + this.boats.add(boat); + } + //System.out.println(this.getBoats()); + } + + } + + public String getLastModified() { return lastModified; } + public Integer getVersion() { return version; } + public String getBoatType() { return boatType; } + public Double getBoatLength() { return boatLength; } + public Double getHullLength() { return hullLength; } + public Double getMarkZoneSize() { return markZoneSize; } + public Double getCourseZoneSize() { return courseZoneSize; } + public ArrayList getZoneLimits() { return zoneLimits; } + public ArrayList getBoats() { return boats; } + + public class Boat { + + private String boatType; + private Integer sourceID; + private String hullID; //matches HullNum in the XML spec. + private String shortName; + private String boatName; + private String country; + + Boat(Node boatNode) { + this.boatType = getNodeAttributeString(boatNode, "Type"); + this.sourceID = getNodeAttributeInt(boatNode, "SourceID"); + this.hullID = getNodeAttributeString(boatNode, "HullNum"); + this.shortName = getNodeAttributeString(boatNode, "ShortName"); + this.boatName = getNodeAttributeString(boatNode, "BoatName"); + this.country = getNodeAttributeString(boatNode, "Country"); + } + + public String getBoatType() { return boatType; } + public Integer getSourceID() { return sourceID; } + public String getHullID() { return hullID; } + public String getShortName() { return shortName; } + public String getBoatName() { return boatName; } + public String getCountry() { return country; } + + } + + } + +} \ No newline at end of file diff --git a/src/main/java/seng302/models/parsers/packets/BoatPositionPacket.java b/src/main/java/seng302/models/parsers/packets/BoatPositionPacket.java new file mode 100644 index 00000000..d6f0700d --- /dev/null +++ b/src/main/java/seng302/models/parsers/packets/BoatPositionPacket.java @@ -0,0 +1,39 @@ +package seng302.models.parsers.packets; + +public class BoatPositionPacket { + private long boatId; + private long timeValid; + private double lat; + private double lon; + private double heading; + private double groundSpeed; + + public BoatPositionPacket(long boatId, long timeValid, double lat, double lon, double heading, double groundSpeed) { + this.boatId = boatId; + this.timeValid = timeValid; + this.lat = lat; + this.lon = lon; + this.heading = heading; + this.groundSpeed = groundSpeed; + } + + public long getTimeValid() { + return timeValid; + } + + public double getLat() { + return lat; + } + + public double getLon() { + return lon; + } + + public double getHeading() { + return heading; + } + + public double getGroundSpeed() { + return groundSpeed; + } +} diff --git a/src/main/java/seng302/models/parsers/packets/PacketType.java b/src/main/java/seng302/models/parsers/packets/PacketType.java new file mode 100644 index 00000000..f522dec9 --- /dev/null +++ b/src/main/java/seng302/models/parsers/packets/PacketType.java @@ -0,0 +1,53 @@ +package seng302.models.parsers.packets; + +/** + * Created by Kusal on 4/24/2017. + */ +public enum PacketType { + HEARTBEAT, + RACE_STATUS, + DISPLAY_TEXT_MESSAGE, + XML_MESSAGE, + RACE_START_STATUS, + YACHT_EVENT_CODE, + YACHT_ACTION_CODE, + CHATTER_TEXT, + BOAT_LOCATION, + MARK_ROUNDING, + COURSE_WIND, + AVG_WIND, + OTHER; + + public static PacketType assignPacketType(int packetType){ + switch(packetType){ + case 1: + return HEARTBEAT; + case 12: + return RACE_STATUS; + case 20: + return DISPLAY_TEXT_MESSAGE; + case 26: + return XML_MESSAGE; + case 27: + return RACE_START_STATUS; + case 29: + return YACHT_EVENT_CODE; + case 31: + return YACHT_ACTION_CODE; + case 36: + return CHATTER_TEXT; + case 37: + return BOAT_LOCATION; + case 38: + return MARK_ROUNDING; + case 44: + return COURSE_WIND; + case 47: + return AVG_WIND; + default: + } + return OTHER; + } + + +} diff --git a/src/main/java/seng302/models/parsers/packets/StreamPacket.java b/src/main/java/seng302/models/parsers/packets/StreamPacket.java new file mode 100644 index 00000000..4f10008c --- /dev/null +++ b/src/main/java/seng302/models/parsers/packets/StreamPacket.java @@ -0,0 +1,37 @@ +package seng302.models.parsers.packets; + +/** + * Created by kre39 on 23/04/17. + */ +public class StreamPacket { + + //Change int to an ENUM for the type + private PacketType type; + + private long messageLength; + private long timeStamp; + private byte[] payload; + + public StreamPacket(int type, long messageLength, long timeStamp, byte[] payload) { + this.type = PacketType.assignPacketType(type); + this.messageLength = messageLength; + this.timeStamp = timeStamp; + this.payload = payload; + } + + public PacketType getType() { + return type; + } + + public long getMessageLength() { + return messageLength; + } + + public byte[] getPayload() { + return payload; + } + + public long getTimeStamp() { + return timeStamp; + } +} diff --git a/src/main/java/seng302/server/ServerThread.java b/src/main/java/seng302/server/ServerThread.java new file mode 100644 index 00000000..e4e5a84a --- /dev/null +++ b/src/main/java/seng302/server/ServerThread.java @@ -0,0 +1,281 @@ +package seng302.server; + +import seng302.server.messages.*; +import seng302.server.simulator.Boat; +import seng302.server.simulator.Simulator; +import sun.misc.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; + +public class ServerThread implements Runnable, Observer { + private Thread runner; + private StreamingServerSocket server; + private long startTime; + boolean raceStarted = false; + Map boatsFinished = new HashMap<>(); + private List boats; + private Simulator raceSimulator; + + private final int HEARTBEAT_PERIOD = 5000; + private final int RACE_STATUS_PERIOD = 1000; + private final int RACE_START_STATUS_PERIOD = 1000; + private final int BOAT_LOCATION_PERIOD = 1000/5; + private final int PORT_NUMBER = 8085; + private final int TIME_TILL_RACE_START = 20*1000; + private static final int LOG_LEVEL = 1; + + public ServerThread(String threadName){ + runner = new Thread(this, threadName); + serverLog("Spawning Server", 0); + raceSimulator = new Simulator(BOAT_LOCATION_PERIOD); + boats = raceSimulator.getBoats(); + + for (Boat b : boats){ + boatsFinished.put(b.getSourceID(), false); + } + + runner.start(); + } + + public static void serverLog(String message, int logLevel){ + if(logLevel <= LOG_LEVEL){ + System.out.println("[SERVER] " + message); + } + } + + /** + * Creates and returns an XML Message from the file specified + * @param fileName The source XML file + * @param type The XML Message type + * @return The XML Message + */ + public Message getXmlMessage(String fileName, XMLMessageSubType type){ + String fileContents = null; + + try { + InputStream thisStream = this.getClass().getResourceAsStream(fileName); + fileContents = new String(org.apache.commons.io.IOUtils.toByteArray(thisStream)); + } catch (IOException e) { + e.printStackTrace(); + } catch (NullPointerException e){ + return null; + } + + if (fileContents != null){ + return new XMLMessage(fileContents, type, server.getSequenceNumber()); + } + + return null; + } + + /** + * @return Get a race status message for the current race + */ + public Message getRaceStatusMessage(){ + List boatSubMessages = new ArrayList(); + BoatStatus boatStatus; + RaceStatus raceStatus; + boolean thereAreBoatsNotFinished = false; + + for (Boat b : boats){ + if (!raceStarted){ + boatStatus = BoatStatus.PRESTART; + thereAreBoatsNotFinished = true; + } + else if(boatsFinished.get(b.getSourceID())){ + boatStatus = BoatStatus.FINISHED; + } + else{ + boatStatus = BoatStatus.PRESTART; + thereAreBoatsNotFinished = true; + } + + BoatSubMessage m = new BoatSubMessage(b.getSourceID(), boatStatus, b.getLastPassedCorner().getSeqID(), 0, 0, 0, 0); + boatSubMessages.add(m); + } + + if (thereAreBoatsNotFinished){ + if (raceStarted){ + raceStatus = RaceStatus.STARTED; + } + else{ + long currentTime = System.currentTimeMillis(); + long timeDifference = startTime - currentTime; + + if (timeDifference > 60*3){ + raceStatus = RaceStatus.PRESTART; + } + else if (timeDifference > 60){ + raceStatus = RaceStatus.WARNING; + } + else{ + raceStatus = RaceStatus.PREPARATORY; + } + } + } + else{ + raceStatus = RaceStatus.TERMINATED; + } + + return new RaceStatusMessage(1, raceStatus, startTime, WindDirection.EAST, + 100, boats.size(), RaceType.MATCH_RACE, 1, boatSubMessages); + } + + /** + * Starts an instance of the race simulator + */ + private void startRaceSim(){ + serverLog("Starting Race Simulator", 0); + raceSimulator.addObserver(this); + new Thread(raceSimulator).start(); + } + + /** + * Starts sending heartbeat messages to the client + */ + private void startSendingHeartbeats(){ + serverLog("Sending Heartbeats", 0); + Timer t = new Timer(); + + t.schedule(new TimerTask() { + @Override + public void run() { + Message heartbeat = new Heartbeat(server.getSequenceNumber()); + + try { + server.send(heartbeat); + } catch (IOException e) { + System.out.print(""); + } + } + }, 0, HEARTBEAT_PERIOD); + } + + /** + * Start sending race start status messages until race starts + */ + private void startSendingRaceStartStatusMessages(){ + Timer t = new Timer(); + t.schedule(new TimerTask() { + @Override + public void run() { + Message raceStartStatusMessage = new RaceStartStatusMessage(server.getSequenceNumber(), startTime , 1, + RaceStartNotificationType.SET_RACE_START_TIME); + try { + if (startTime < System.currentTimeMillis() && !raceStarted){ + startRaceSim(); + raceStarted = true; + serverLog("Race Started", 0); + } + else{ + server.send(raceStartStatusMessage); + } + + } catch (IOException e) { + System.out.print(""); + } + } + }, 0, RACE_START_STATUS_PERIOD); + } + + /** + * Start sending race start status messages until race starts + */ + private void startSendingRaceStatusMessages(){ + serverLog("Sending race status messages", 0); + Timer t = new Timer(); + t.schedule(new TimerTask() { + @Override + public void run() { + Message raceStatusMessage = getRaceStatusMessage(); + try { + server.send(raceStatusMessage); + + } catch (IOException e) { + System.out.print(""); + } + } + }, 0, RACE_STATUS_PERIOD); + } + + /** + * Sends the race, boat, and regatta XML files to the client + */ + private void sendXml(){ + try{ + Message raceData = getXmlMessage("/server_config/race.xml", XMLMessageSubType.RACE); + Message boatData = getXmlMessage("/server_config/boats.xml", XMLMessageSubType.BOAT); + Message regatta = getXmlMessage("/server_config/regatta.xml", XMLMessageSubType.REGATTA); + + if (raceData != null){ + server.send(raceData); + serverLog("Sending race data", 0); + } + + if (boatData != null){ + server.send(boatData); + serverLog("Sending boat data", 0); + } + + if (regatta != null){ + server.send(regatta); + serverLog("Sending regatta data", 0); + } + } catch (IOException e) { + serverLog("Couldn't send an XML Message: " + e.getMessage(), 0); + } + } + + public void run() { + try{ + server = new StreamingServerSocket(PORT_NUMBER); + } + catch (IOException e){ + serverLog("Failed to bind socket: " + e.getMessage(), 0); + } + + // Wait for client to connect + server.start(); + + startTime = System.currentTimeMillis() + TIME_TILL_RACE_START; + + startSendingHeartbeats(); + sendXml(); + startSendingRaceStartStatusMessages(); + startSendingRaceStatusMessages(); + } + + /** + * Send a boat location message when they are updated by the simulator + * @param o . + * @param arg . + */ + @Override + public void update(Observable o, Object arg) { + // Only send if server started + if(!server.isStarted()){ + return; + } + + for (Boat b : ((Simulator) o).getBoats()){ + try { + Message m = new BoatLocationMessage(b.getSourceID(), 1, b.getLat(), + b.getLng(), b.getHeadingCorner().getBearingToNextCorner(), + ((long) b.getSpeed())); + server.send(m); + } catch (IOException e) { + serverLog("Couldn't send a boat status message", 3); + return; + } + catch (NullPointerException e){ + serverLog("Boat " + b.getSourceID() + " finished the race", 1); + boatsFinished.put(b.getSourceID(), true); + } + } + } +} diff --git a/src/main/java/seng302/server/StreamingServerSocket.java b/src/main/java/seng302/server/StreamingServerSocket.java new file mode 100644 index 00000000..dc249ea4 --- /dev/null +++ b/src/main/java/seng302/server/StreamingServerSocket.java @@ -0,0 +1,63 @@ +package seng302.server; + +import seng302.server.messages.Message; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.channels.Channels; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.nio.channels.WritableByteChannel; +import java.util.ArrayList; +import java.util.List; + +class StreamingServerSocket { + private ServerSocketChannel socket; + private SocketChannel client; + private short seqNum; + private boolean isServerStarted; + + StreamingServerSocket(int port) throws IOException{ + socket = ServerSocketChannel.open(); + socket.socket().bind(new InetSocketAddress("localhost", port)); + //socket.setSoTimeout(10000); + seqNum = 0; + isServerStarted = false; + } + + void start(){ + ServerThread.serverLog("Listening For Connections",0); + try { + client = socket.accept(); + } catch (IOException e) { + e.getMessage(); + } + if (client.socket() == null){ + start(); + } + else{ + isServerStarted = true; + ServerThread.serverLog("client connected from " + client.socket().getInetAddress(),0); + } + } + + void send(Message message) throws IOException{ + if (client == null){ + return; + } + + message.send(client); + + seqNum++; + } + + public short getSequenceNumber(){ + return seqNum; + } + + public boolean isStarted(){ + return isServerStarted; + } +} diff --git a/src/main/java/seng302/server/messages/BoatLocationMessage.java b/src/main/java/seng302/server/messages/BoatLocationMessage.java new file mode 100644 index 00000000..2bffdc72 --- /dev/null +++ b/src/main/java/seng302/server/messages/BoatLocationMessage.java @@ -0,0 +1,166 @@ +package seng302.server.messages; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.channels.Channels; +import java.nio.channels.SocketChannel; +import java.nio.channels.WritableByteChannel; + +public class BoatLocationMessage extends Message { + private final int MESSAGE_SIZE = 56; + + private long messageVersionNumber; + private long time; + private long sourceId; + private long sequenceNum; + private DeviceType deviceType; + private double latitude; + private double longitude; + private long altitude; + private Double heading; + private long pitch; + private long roll; + private long boatSpeed; + private long COG; + private long SOG; + private long apparentWindSpeed; + private long apparentWindAngle; + private long trueWindSpeed; + private long trueWindDirection; + private long trueWindAngle; + private long currentDrift; + private long currentSet; + private long rudderAngle; + + /** + * Describes the location, altitude and sensor data from the boat. + * @param sourceId ID of the boat + * @param sequenceNum Sequence number of the message + * @param latitude The boats latitude + * @param longitude The boats longitude + * @param heading The boats heading + * @param boatSpeed The boats speed + */ + public BoatLocationMessage(int sourceId, int sequenceNum, double latitude, double longitude, double heading, long boatSpeed){ + messageVersionNumber = 1; + time = System.currentTimeMillis() / 1000L; + this.sourceId = sourceId; + this.sequenceNum = sequenceNum; + this.deviceType = DeviceType.RACING_YACHT; + this.latitude = latitude; + this.longitude = longitude; + this.altitude = 0; + this.heading = heading; + this.pitch = 0; + this.roll = 0; + this.boatSpeed = boatSpeed; + this.COG = 0; + this.SOG = 0; + this.apparentWindSpeed = 0; + this.apparentWindAngle = 0; + this.trueWindSpeed = 0; + this.trueWindDirection = 0; + this.trueWindAngle = 0; + this.currentDrift = 0; + this.currentSet = 0; + this.rudderAngle = 0; + + setHeader(new Header(MessageType.BOAT_LOCATION, 1, (short) getSize())); + } + + /** + * Convert binary latitude or longitude to floating point number + * @param binaryPackedLatLon Binary packed lat OR lon + * @return Floating point lat/lon + */ + public static double binaryPackedToLatLon(long binaryPackedLatLon){ + return (double)binaryPackedLatLon * 180.0 / 2147483648.0; + } + + /** + * Convert binary packed heading to floating point number + * @param binaryPackedHeading Binary packed heading + * @return heading as a decimal + */ + public static double binaryPackedHeadingToDouble(long binaryPackedHeading){ + return (double)binaryPackedHeading * 360.0 / 65536.0; + } + + /** + * Convert binary packed wind angle to floating point number + * @param binaryPackedWindAngle Binary packed wind angle + * @return wind angle as a decimal + */ + public static double binaryPackedWindAngleToDouble(long binaryPackedWindAngle){ + return (double)binaryPackedWindAngle*180.0/32768.0; + } + + /** + * Convert a latitude or longitude to a binary packed long + * @param latLon A floating point latitude/longitude + * @return A binary packed lat/lon + */ + public static long latLonToBinaryPackedLong(double latLon){ + return (long)((536870912 * latLon) / 45); + } + + /** + * Convert a heading to a binary packed long + * @param heading A floating point heading + * @return A binary packed heading + */ + public static long headingToBinaryPackedLong(double heading){ + return (long)((8192*heading)/45); + } + + /** + * Convert a wind angle to a binary packed long + * @param windAngle Floating point wind angle + * @return A binary packed wind angle + */ + public static long windAngleToBinaryPackedLong(double windAngle){ + return (long)((8192*windAngle)/45); + } + + @Override + public int getSize() { + return MESSAGE_SIZE; + } + + + @Override + public void send(SocketChannel outputStream) throws IOException{ + allocateBuffer(); + writeHeaderToBuffer(); + + long headingToSend = (long)((heading/360.0) * 65535.0); + + putByte((byte) messageVersionNumber); + putInt(time, 6); + putInt((int) sourceId, 4); + putUnsignedInt((int) sequenceNum, 4); + putByte((byte) deviceType.getCode()); + putInt((int) latLonToBinaryPackedLong(latitude), 4); + putInt((int) latLonToBinaryPackedLong(longitude), 4); + putInt((int) altitude, 4); + putInt(headingToSend, 2); + putInt((int) pitch, 2); + putInt((int) roll, 2); + putUnsignedInt((int) boatSpeed, 2); + putUnsignedInt((int) COG, 2); + putUnsignedInt((int) SOG, 2); + putUnsignedInt((int) apparentWindSpeed, 2); + putInt((int) apparentWindAngle, 2); + putUnsignedInt((int) trueWindSpeed, 2); + putUnsignedInt((int) trueWindDirection, 2); + putInt((int) trueWindAngle, 2); + putUnsignedInt((int) currentDrift, 2); + putUnsignedInt((int) currentSet, 2); + putInt((int) rudderAngle, 2); + + writeCRC(); + rewind(); + + outputStream.write(getBuffer()); + } +} diff --git a/src/main/java/seng302/server/messages/BoatStatus.java b/src/main/java/seng302/server/messages/BoatStatus.java new file mode 100644 index 00000000..94418000 --- /dev/null +++ b/src/main/java/seng302/server/messages/BoatStatus.java @@ -0,0 +1,25 @@ +package seng302.server.messages; + +/** + * The current status of a boat + */ +public enum BoatStatus { + UNDEFINED(0), + PRESTART(1), + RACING(2), + FINISHED(3), + DNS(4), + DNF(5), + DSQ(6), + CS(7); + + private long code; + + BoatStatus(long code) { + this.code = code; + } + + public long getCode(){ + return code; + } +} diff --git a/src/main/java/seng302/server/messages/BoatSubMessage.java b/src/main/java/seng302/server/messages/BoatSubMessage.java new file mode 100644 index 00000000..ebe0f6a2 --- /dev/null +++ b/src/main/java/seng302/server/messages/BoatSubMessage.java @@ -0,0 +1,92 @@ +package seng302.server.messages; + +import java.io.DataOutputStream; +import java.nio.ByteBuffer; + +/** + * The status of each boat, sent within a race status message + */ +public class BoatSubMessage{ + private final int MESSAGE_SIZE = 20; + + private long sourceId; + private BoatStatus boatStatus; + private long legNumber; + private long numberPenaltiesAwarded; + private long numberPenaltiesServed; + private long estimatedTimeAtNextMark; + private long estimatedTimeAtFinish; + + /** + * Boat Sub message from section 4.2 of the AC35 streaming data interface spec + * @param sourceId The source ID of the boat + * @param boatStatus The boats status + * @param legNumber The leg the boat is on (0= prestart, 1=start to first mark etc) + * @param numberPenaltiesAwarded The number of penalties awarded to the boat + * @param numberPenaltiesServed The number of penalties served to the boat + * @param estimatedTimeAtFinish The estimated time (UTC) the boat will finish the race + * @param estimatedTimeAtNextMark The estimated time (UTC) the boat will arrive at the next mark + */ + public BoatSubMessage(long sourceId, BoatStatus boatStatus, long legNumber, long numberPenaltiesAwarded, long numberPenaltiesServed, + long estimatedTimeAtFinish, long estimatedTimeAtNextMark){ + this.sourceId = sourceId; + this.boatStatus = boatStatus; + this.legNumber = legNumber; + this.numberPenaltiesAwarded = numberPenaltiesAwarded; + this.numberPenaltiesServed = numberPenaltiesServed; + this.estimatedTimeAtFinish = estimatedTimeAtFinish; + this.estimatedTimeAtNextMark = estimatedTimeAtNextMark; + } + + /** + * @return The size of this message in bytes + */ + public int getSize(){ + return MESSAGE_SIZE; + } + + /** + * @return a ByteBuffer containing this boat status message + */ + public ByteBuffer getByteBuffer(){ + ByteBuffer buff = ByteBuffer.allocate(getSize()); + int buffPos = 0; + + // Source ID, 4 bytes + buff.put(ByteBuffer.allocate(4).putInt((int) sourceId).array()); + buffPos += 4; + buff.position(buffPos); + + // Boat Status, 1 byte + buff.put(ByteBuffer.allocate(1).put((byte) (boatStatus.getCode() & 0xff)).array()); + buffPos += 1; + buff.position(buffPos); + + // Leg number, 1 byte + buff.put(ByteBuffer.allocate(1).put((byte) (legNumber & 0xff)).array()); + buffPos += 1; + buff.position(buffPos); + + // Number of penalties awarded, 1 byte + buff.put(ByteBuffer.allocate(1).put((byte) (numberPenaltiesAwarded & 0xff)).array()); + buffPos += 1; + buff.position(buffPos); + + // Number of penalties served, 1 byte + buff.put(ByteBuffer.allocate(1).put((byte) (numberPenaltiesServed & 0xff)).array()); + buffPos += 1; + buff.position(buffPos); + + // Estimated time at next mark, 6 bytes + buff.put(ByteBuffer.allocate(6).putInt((int) estimatedTimeAtNextMark).array()); + buffPos += 6; + buff.position(buffPos); + + // Estimated time at finish, 6 bytes + buff.put(ByteBuffer.allocate(6).putInt((int) estimatedTimeAtFinish).array()); + buffPos += 6; + buff.position(buffPos); + + return buff; + } +} diff --git a/src/main/java/seng302/server/messages/DeviceType.java b/src/main/java/seng302/server/messages/DeviceType.java new file mode 100644 index 00000000..d245c2b1 --- /dev/null +++ b/src/main/java/seng302/server/messages/DeviceType.java @@ -0,0 +1,16 @@ +package seng302.server.messages; + +public enum DeviceType { + UNKNOWN(0), + RACING_YACHT(1); + + private long code; + + DeviceType(long code) { + this.code = code; + } + + public long getCode(){ + return code; + } +} diff --git a/src/main/java/seng302/server/messages/Header.java b/src/main/java/seng302/server/messages/Header.java new file mode 100644 index 00000000..c4dc6251 --- /dev/null +++ b/src/main/java/seng302/server/messages/Header.java @@ -0,0 +1,72 @@ +package seng302.server.messages; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Collections; + +public class Header { + // From API spec + private final int syncByte1 = 0x47; + private final int syncByte2 = 0x83; + + private MessageType messageType; + private int timeStamp; + private int sourceId; + private short messageLength; + private static final int MESSAGE_LEN = 15; + private ByteBuffer buff; + private int buffPos; + + /** + * Message Header from section 3.2 of the AC35 Streaming + * Data spec + * @param messageType The type of the message following this header + * @param sourceId The message source (as defined in the spec) + * @param messageLength The length of the message following this header + */ + public Header(MessageType messageType, int sourceId, Short messageLength){ + this.messageType = messageType; + this.sourceId = sourceId; + this.messageLength = messageLength; + timeStamp = (int) (System.currentTimeMillis() / 1000L); + buff = ByteBuffer.allocate(MESSAGE_LEN); + buffPos = 0; + } + + private void putInBuffer(byte[] bytes, long val){ + byte[] tmp = bytes.clone(); + Message.reverse(tmp); + + buff.put(tmp); + buffPos += tmp.length; + buff.position(buffPos); + } + + /** + * @return a ByteBuffer containing the message header + */ + public ByteBuffer getByteBuffer(){ + putInBuffer(ByteBuffer.allocate(1).put((byte)syncByte1).array(), syncByte1); + + putInBuffer(ByteBuffer.allocate(1).put((byte)syncByte2).array(), syncByte2); + + putInBuffer(ByteBuffer.allocate(1).put((byte)messageType.getCode()).array(), messageType.getCode()); + + putInBuffer(Message.intToByteArray(timeStamp, 6), timeStamp); + + putInBuffer(Message.intToByteArray(sourceId, 4), sourceId); + + putInBuffer(Message.intToByteArray(messageLength, 2), messageLength); + + return buff; + } + + /** + * Returns the size of this message + * @return the size of the message + */ + public static Integer getSize(){ + return MESSAGE_LEN; + } +} diff --git a/src/main/java/seng302/server/messages/Heartbeat.java b/src/main/java/seng302/server/messages/Heartbeat.java new file mode 100644 index 00000000..8e619107 --- /dev/null +++ b/src/main/java/seng302/server/messages/Heartbeat.java @@ -0,0 +1,42 @@ +package seng302.server.messages; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.SocketChannel; +import java.nio.channels.WritableByteChannel; +import java.util.zip.CRC32; + +public class Heartbeat extends Message { + private final int MESSAGE_SIZE = 4; + private int seqNo; + + /** + * Heartbeat from the AC35 Streaming data spec + * @param seqNo Increment every time a message is sent + */ + public Heartbeat(int seqNo){ + this.seqNo = seqNo; + } + + @Override + public int getSize() { + return MESSAGE_SIZE; + } + + @Override + public void send(SocketChannel outputStream) throws IOException { + setHeader(new Header(MessageType.HEARTBEAT, 0x01, (short) getSize())); + + allocateBuffer(); + writeHeaderToBuffer(); + + putUnsignedInt(seqNo, 4); + + writeCRC(); + rewind(); + + outputStream.write(getBuffer()); + } +} \ No newline at end of file diff --git a/src/main/java/seng302/server/messages/MarkRoundingMessage.java b/src/main/java/seng302/server/messages/MarkRoundingMessage.java new file mode 100644 index 00000000..750efb22 --- /dev/null +++ b/src/main/java/seng302/server/messages/MarkRoundingMessage.java @@ -0,0 +1,62 @@ +package seng302.server.messages; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.channels.Channels; +import java.nio.channels.SocketChannel; +import java.nio.channels.WritableByteChannel; + +public class MarkRoundingMessage extends Message{ + private final long MESSAGE_VERSION_NUMBER = 1; + private final int MESSAGE_SIZE = 21; + + private long time; + private long ackNumber; + private long raceId; + private long sourceId; + private RoundingBoatStatus boatStatus; + private RoundingSide roundingSide; + private long markId; + + /** + * This message is sent when a boat passes a mark, start line, or finish line + * The purpose of this is to record the time when yachts cross marks + */ + public MarkRoundingMessage(int ackNumber, int raceId, int sourceId, RoundingBoatStatus roundingBoatStatus, + RoundingSide roundingSide, int markId){ + this.time = System.currentTimeMillis() / 1000L; + this.ackNumber = ackNumber; + this.raceId = raceId; + this.sourceId = sourceId; + this.boatStatus = roundingBoatStatus; + this.roundingSide = roundingSide; + this.markId = markId; + + setHeader(new Header(MessageType.MARK_ROUNDING, 1, (short) getSize())); + } + + @Override + public int getSize() { + return MESSAGE_SIZE; + } + + @Override + public void send(SocketChannel outputStream) throws IOException { + allocateBuffer(); + writeHeaderToBuffer(); + + putByte((byte) MESSAGE_VERSION_NUMBER); + putInt((int) time, 6); + putInt((int) ackNumber, 2); + putInt((int) raceId, 4); + putInt((int) sourceId, 4); + putByte((byte) boatStatus.getCode()); + putByte((byte) roundingSide.getCode()); + putByte((byte) markId); + + writeCRC(); + rewind(); + + outputStream.write(getBuffer()); + } +} diff --git a/src/main/java/seng302/server/messages/MarkType.java b/src/main/java/seng302/server/messages/MarkType.java new file mode 100644 index 00000000..abbacc6f --- /dev/null +++ b/src/main/java/seng302/server/messages/MarkType.java @@ -0,0 +1,20 @@ +package seng302.server.messages; + +/** + * Types of marks boats can round + */ +public enum MarkType { + UNKNOWN(0), + ROUNDING_MARK(1), + GATE(2); + + private long code; + + MarkType(long code) { + this.code = code; + } + + public long getCode(){ + return code; + } +} diff --git a/src/main/java/seng302/server/messages/Message.java b/src/main/java/seng302/server/messages/Message.java new file mode 100644 index 00000000..e7dd6f74 --- /dev/null +++ b/src/main/java/seng302/server/messages/Message.java @@ -0,0 +1,213 @@ +package seng302.server.messages; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.reflect.Array; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.zip.CRC32; + +public abstract class Message { + private final int CRC_SIZE = 4; + private Header header; + private ByteBuffer buffer; + private int bufferPosition; + private CRC32 crc; + + /** + * @param header Set the header for this message + */ + void setHeader(Header header){ + this.header = header; + } + + /** + * @return the header specified for this message + */ + Header getHeader(){ + return header; + } + + /** + * @return the size of the message + */ + public abstract int getSize(); + + /** + * Send the message as through the outputStream + */ + public abstract void send(SocketChannel outputStream) throws IOException; + + /** + * Allocate byte buffer to correct size + */ + void allocateBuffer(){ + buffer = ByteBuffer.allocate(Header.getSize() + getSize() + CRC_SIZE); + buffer.order(ByteOrder.LITTLE_ENDIAN); + bufferPosition = 0; + } + + /** + * Write the set header to the byte buffer + */ + void writeHeaderToBuffer(){ + buffer.put(getHeader().getByteBuffer().array()); + bufferPosition += Header.getSize(); + buffer.position(bufferPosition); + } + + /** + * Move the buffer position by n bytes + * @param size Number of bytes to move the buffer by + */ + private void moveBufferPositionBy(int size){ + bufferPosition += size; + buffer.position(bufferPosition); + } + + /** + * Put an unsigned byte in the buffer + */ + void putUnsignedByte(byte b){ + buffer.put(ByteBuffer.allocate(1).put((byte) (b & 0xff)).array()); + moveBufferPositionBy(1); + } + + /** + * Put an signed byte in the buffer + */ + void putByte(byte b){ + buffer.put(ByteBuffer.allocate(1).put(b).array()); + moveBufferPositionBy(1); + } + + /** + * Place an unsigned integer of the specified length in the buffer + * @param val The integer value to add (Note: This must be long due to java not supporting unsigned integers) + * @param size The size of the int to be added to the buffer + */ + void putUnsignedInt(long val, int size){ + if (size <= 1){ + putUnsignedByte((byte) val); + + } + else if (size < 4){ + // Use short + byte[] tmp = Message.intToByteArray(val, size); //ByteBuffer.allocate(size).putShort((short) (val & 0xffff)).array(); + reverse(tmp); + buffer.put(tmp); + moveBufferPositionBy(size); + } + else{ + // Use int + byte[] tmp = Message.intToByteArray(val, size); + reverse(tmp); + moveBufferPositionBy(size); + } + } + + /** + * Put a signed int of a specified length in the buffer + * @param val The integer value to add + * @param size The size of the integer to be added to the buffer + */ + void putInt(long val, int size){ + if (size < 4){ + byte[] tmp = Message.intToByteArray(val, size); + reverse(tmp); + buffer.put(tmp); + } + else{ + byte[] tmp = Message.intToByteArray(val, size); + reverse(tmp); + buffer.put(tmp); + } + moveBufferPositionBy(size); + } + + /** + * Write an array of bytes to the buffer + * @param bytes to write + */ + void putBytes(byte[] bytes){ + buffer.put(bytes); + moveBufferPositionBy(bytes.length); + } + + /** + * Write a ByteBuffer of bytes to the buffer + * @param bytes to write + * @param size number of bytes + */ + void putBytes(ByteBuffer bytes, int size){ + buffer.put(bytes); + moveBufferPositionBy(size); + } + + + /** + * Calculate the CRC of the buffer and append it to the end of the buffer + */ + void writeCRC(){ + crc = new CRC32(); + + buffer.position(0); + + byte[] data = Arrays.copyOfRange(buffer.array(), 0, buffer.array().length-CRC_SIZE); + crc.update(data); + buffer.position(bufferPosition); + + putInt((int) crc.getValue(), CRC_SIZE); + } + + /** + * @return The current buffer + */ + public ByteBuffer getBuffer(){ + return buffer; + } + + /** + * Rewind the buffer to the beginning + */ + void rewind(){ + buffer.flip(); + } + + /** + * Convert an integer to an array of bytes + * @param val The value to add + * @param len The width of the integer in the buffer + * @return + */ + public static byte[] intToByteArray(long val, int len){ + long vor = val; + int index = 0; + byte[] data = new byte[len]; + + for (int i = 0; i < len; i++){ + data[len - index - 1] = (byte) (val & 0xFF); + val >>>= 8; + index++; + } + + return data; + } + + /** + * Reverse an array of bytes + * @param data The byte[] to reverse + */ + public static void reverse(byte[] data) { + for (int left = 0, right = data.length - 1; left < right; left++, right--) { + byte temp = (byte) (data[left] & 0xff); + data[left] = (byte) (data[right] & 0xff); + data[right] = (byte) (temp & 0xff); + } + } + +} diff --git a/src/main/java/seng302/server/messages/MessageType.java b/src/main/java/seng302/server/messages/MessageType.java new file mode 100644 index 00000000..be856dac --- /dev/null +++ b/src/main/java/seng302/server/messages/MessageType.java @@ -0,0 +1,34 @@ +package seng302.server.messages; + +/** + * Enum containing the types of messages + * sent by the server + */ +public enum MessageType { + HEARTBEAT(1), + RACE_STATUS(12), + DISPLAY_TEXT_MESSAGE(20), + XML_MESSAGE(26), + RACE_START_STATUS(27), + YACHT_EVENT_CODE(29), + YACHT_ACTION_CODE(31), + CHATTER_TEXT(36), + BOAT_LOCATION(37), + MARK_ROUNDING(38), + COURSE_WIND(44), + AVERAGE_WIND(47); + + private int code; + + MessageType(int code){ + this.code = code; + } + + /** + * Get the message code (From the API Spec) + * @return the message code + */ + int getCode(){ + return this.code; + } +} diff --git a/src/main/java/seng302/server/messages/RaceStartNotificationType.java b/src/main/java/seng302/server/messages/RaceStartNotificationType.java new file mode 100644 index 00000000..29db3f1e --- /dev/null +++ b/src/main/java/seng302/server/messages/RaceStartNotificationType.java @@ -0,0 +1,21 @@ +package seng302.server.messages; + +/** + * The types of race start status messages + */ +public enum RaceStartNotificationType { + SET_RACE_START_TIME(1), + RACE_POSTPONED(2), + RACE_ABANDONED(3), + RACE_TERMINATED(4); + + private final long type; + + RaceStartNotificationType(long type) { + this.type = type; + } + + long getType(){ + return type; + } +} diff --git a/src/main/java/seng302/server/messages/RaceStartStatusMessage.java b/src/main/java/seng302/server/messages/RaceStartStatusMessage.java new file mode 100644 index 00000000..368a18fd --- /dev/null +++ b/src/main/java/seng302/server/messages/RaceStartStatusMessage.java @@ -0,0 +1,59 @@ +package seng302.server.messages; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.channels.Channels; +import java.nio.channels.SocketChannel; +import java.nio.channels.WritableByteChannel; + +public class RaceStartStatusMessage extends Message { + private final int MESSAGE_SIZE = 20; + + private long version; + private long timeStamp; + private long ackNumber; + private long raceStartTime; + private long raceId; + private RaceStartNotificationType notificationType; + + /** + * Message sent to clients with the expected start time of the race + * @param ackNumber Sequence number of message. + * @param raceStartTime Expected race start time + * @param raceId Race ID# + * @param notificationType Type of this notification + */ + public RaceStartStatusMessage(long ackNumber, long raceStartTime, long raceId, RaceStartNotificationType notificationType){ + this.version = 1; + this.timeStamp = System.currentTimeMillis() / 1000L; + this.ackNumber = ackNumber; + this.raceStartTime = raceStartTime; + this.notificationType = notificationType; + this.raceId = raceId; + + setHeader(new Header(MessageType.RACE_START_STATUS, 1, (short) getSize())); + } + + @Override + public int getSize() { + return MESSAGE_SIZE; + } + + @Override + public void send(SocketChannel outputStream) throws IOException { + allocateBuffer(); + writeHeaderToBuffer(); + + putUnsignedByte((byte) version); + putInt((int) timeStamp, 6); + putInt((int) ackNumber, 2); + putInt((int) raceStartTime, 6); + putInt((int) raceId, 4); + putUnsignedByte((byte) notificationType.getType()); + + writeCRC(); + rewind(); + + outputStream.write(getBuffer()); + } +} diff --git a/src/main/java/seng302/server/messages/RaceStatus.java b/src/main/java/seng302/server/messages/RaceStatus.java new file mode 100644 index 00000000..7f123c2d --- /dev/null +++ b/src/main/java/seng302/server/messages/RaceStatus.java @@ -0,0 +1,26 @@ +package seng302.server.messages; + +/** + * The current status of the race + */ +public enum RaceStatus { + NOTACTIVE(0), + WARNING(1), // Between 3:00 and 1:00 before start + PREPARATORY(2), // Less than 1:00 before start + STARTED(3), + ABANDONED(6), + POSTPONED(7), + TERMINATED(8), + RACE_START_TIME_NOT_SET(9), + PRESTART(10); // More than 3:00 before start + + private int code; + + RaceStatus(int code){ + this.code = code; + } + + public int getCode(){ + return this.code; + } +} diff --git a/src/main/java/seng302/server/messages/RaceStatusMessage.java b/src/main/java/seng302/server/messages/RaceStatusMessage.java new file mode 100644 index 00000000..32ea9abd --- /dev/null +++ b/src/main/java/seng302/server/messages/RaceStatusMessage.java @@ -0,0 +1,88 @@ +package seng302.server.messages; + +import java.io.IOException; +import java.nio.channels.SocketChannel; +import java.util.List; +import java.util.zip.CRC32; + +public class RaceStatusMessage extends Message{ + private final MessageType MESSAGE_TYPE = MessageType.RACE_STATUS; + private final int MESSAGE_VERSION = 2; //Always set to 1 + private final int MESSAGE_BASE_SIZE = 24; + + private long currentTime; + private long raceId; + private RaceStatus raceStatus; + private long expectedStartTime; + private WindDirection raceWindDirection; + private long windSpeed; + private long numBoatsInRace; + private RaceType raceType; + private List boats; + private CRC32 crc; + + /** + * A message containing the current status of the race + * @param raceId The ID of the current race + * @param raceStatus The status of the race + * @param expectedStartTime The expected start time + * @param raceWindDirection The wind direction (north, east, south) + * @param windSpeed The wind speed in mm/sec + * @param numBoatsInRace The number of boats in the race + * @param raceType The race type (Match/fleet) + * @param sourceId The source of this message + * @param boats A list of boat status sub messages + */ + public RaceStatusMessage(long raceId, RaceStatus raceStatus, long expectedStartTime, WindDirection raceWindDirection, + long windSpeed, long numBoatsInRace, RaceType raceType, long sourceId, List boats){ + currentTime = System.currentTimeMillis(); + this.raceId = raceId; + this.raceStatus = raceStatus; + this.expectedStartTime = expectedStartTime; + this.raceWindDirection = raceWindDirection; + this.windSpeed = windSpeed; + this.numBoatsInRace = numBoatsInRace; + this.raceType = raceType; + this.boats = boats; + crc = new CRC32(); + + setHeader(new Header(MESSAGE_TYPE, (int) sourceId, (short) getSize())); + } + + /** + * @return the size of this message in bytes + */ + @Override + public int getSize() { + return MESSAGE_BASE_SIZE + (20 * ((int) numBoatsInRace)); + } + + /** + * Send this message as a stream of bytes + * @param outputStream The output stream to send the message + */ + @Override + public void send(SocketChannel outputStream) throws IOException { + allocateBuffer(); + writeHeaderToBuffer(); + + putByte((byte) MESSAGE_VERSION); + putInt(currentTime, 6); + putInt((int) raceId, 4); + putByte((byte) raceStatus.getCode()); + putInt(expectedStartTime, 6); + putInt((int) raceWindDirection.getCode(), 2); + putInt((int) windSpeed, 2); + putByte((byte) numBoatsInRace); + putByte((byte) raceType.getCode()); + + for (BoatSubMessage boatSubMessage : boats){ + putBytes(boatSubMessage.getByteBuffer(), boatSubMessage.getSize()); + } + + writeCRC(); + rewind(); + + outputStream.write(getBuffer()); + } +} diff --git a/src/main/java/seng302/server/messages/RaceType.java b/src/main/java/seng302/server/messages/RaceType.java new file mode 100644 index 00000000..182b5dfd --- /dev/null +++ b/src/main/java/seng302/server/messages/RaceType.java @@ -0,0 +1,20 @@ +package seng302.server.messages; + +/** + * Enum containing the types of races + * sent by the server + */ +public enum RaceType { + MATCH_RACE(1), + FLEET_RACE(2); + + private long code; + + RaceType(long code){ + this.code = code; + } + + public long getCode(){ + return code; + } +} diff --git a/src/main/java/seng302/server/messages/RoundingBoatStatus.java b/src/main/java/seng302/server/messages/RoundingBoatStatus.java new file mode 100644 index 00000000..32eb2447 --- /dev/null +++ b/src/main/java/seng302/server/messages/RoundingBoatStatus.java @@ -0,0 +1,21 @@ +package seng302.server.messages; + +/** + * The status of a boat rounding a mark + */ +public enum RoundingBoatStatus { + UNKNOWN(0), + RACING(1), + DSQ(2), + WITHDRAWN(3); + + private long code; + + RoundingBoatStatus(long code) { + this.code = code; + } + + public long getCode(){ + return code; + } +} diff --git a/src/main/java/seng302/server/messages/RoundingSide.java b/src/main/java/seng302/server/messages/RoundingSide.java new file mode 100644 index 00000000..5cc4097c --- /dev/null +++ b/src/main/java/seng302/server/messages/RoundingSide.java @@ -0,0 +1,20 @@ +package seng302.server.messages; + +/** + * The side the boat rounded the mark + */ +public enum RoundingSide { + UNKNOWN(0), + PORT(1), + STARBOARD(2); + + private long code; + + RoundingSide(long code) { + this.code = code; + } + + public long getCode(){ + return code; + } +} diff --git a/src/main/java/seng302/server/messages/WindDirection.java b/src/main/java/seng302/server/messages/WindDirection.java new file mode 100644 index 00000000..c0b8d767 --- /dev/null +++ b/src/main/java/seng302/server/messages/WindDirection.java @@ -0,0 +1,20 @@ +package seng302.server.messages; + +/** + * Enum containing the supported wind directions + */ +public enum WindDirection { + NORTH(0x0000L), + EAST(0x4000L), + SOUTH(0x8000L); + + private long code; + + WindDirection(long code) { + this.code = code; + } + + public long getCode() { + return code; + } +} diff --git a/src/main/java/seng302/server/messages/XMLMessage.java b/src/main/java/seng302/server/messages/XMLMessage.java new file mode 100644 index 00000000..2cf3a5b5 --- /dev/null +++ b/src/main/java/seng302/server/messages/XMLMessage.java @@ -0,0 +1,69 @@ +package seng302.server.messages; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.SocketChannel; +import java.nio.channels.WritableByteChannel; +import java.util.zip.CRC32; + +public class XMLMessage extends Message{ + private final MessageType MESSAGE_TYPE = MessageType.XML_MESSAGE; + private final int MESSAGE_VERSION = 1; //Always set to 1 + private final int MESSAGE_SIZE = 14; + + // Message fields + private long timeStamp; + private long ack = 0x00; //Unused + private XMLMessageSubType xmlMessageSubType; + private long length; + private long sequence; + private String content; + + /** + * XML Message from the AC35 Streaming data spec + * @param content The XML content + * @param type The XML Message Sub Type + */ + public XMLMessage(String content, XMLMessageSubType type, long sequenceNum){ + this.content = content; + this.xmlMessageSubType = type; + timeStamp = System.currentTimeMillis() / 1000L; + ack = 0; + length = this.content.length(); + sequence = sequenceNum; + + setHeader(new Header(MESSAGE_TYPE, 0x01, (short) getSize())); + } + + /** + * @return The length of this message + */ + public int getSize(){ + return MESSAGE_SIZE + content.length(); + } + + /** + * Send this message as a stream of bytes + * @param outputStream The output stream to send the message + */ + public void send(SocketChannel outputStream) throws IOException { + allocateBuffer(); + writeHeaderToBuffer(); + + // Write message fields + putUnsignedByte((byte) MESSAGE_VERSION); + putInt((int) ack, 2); + putInt((int) timeStamp, 6); + putByte((byte)xmlMessageSubType.getType()); + putInt((int) sequence, 2); + putInt((int) length, 2); + putBytes(content.getBytes()); + + writeCRC(); + rewind(); + + outputStream.write(getBuffer()); + } +} diff --git a/src/main/java/seng302/server/messages/XMLMessageSubType.java b/src/main/java/seng302/server/messages/XMLMessageSubType.java new file mode 100644 index 00000000..2e146c5a --- /dev/null +++ b/src/main/java/seng302/server/messages/XMLMessageSubType.java @@ -0,0 +1,20 @@ +package seng302.server.messages; + +/** + * Enum containing the types of XML messages + */ +public enum XMLMessageSubType { + REGATTA(5), + RACE(6), + BOAT(7); + + private int type; + + XMLMessageSubType(int type){ + this.type = type; + } + + public int getType(){ + return this.type; + } +} diff --git a/src/main/java/seng302/server/simulator/Boat.java b/src/main/java/seng302/server/simulator/Boat.java new file mode 100644 index 00000000..c5b821bf --- /dev/null +++ b/src/main/java/seng302/server/simulator/Boat.java @@ -0,0 +1,109 @@ +package seng302.server.simulator; + +import seng302.server.simulator.mark.Corner; +import seng302.server.simulator.mark.Position; + +public class Boat { + + private int sourceID; + private double lat; + private double lng; + private double speed; // in mm/sec + private String boatName, shortName, shorterName; + + private Corner lastPassedCorner, headingCorner; + + public Boat(int sourceID, String boatName) { + this.sourceID = sourceID; + this.boatName = boatName; + } + + /** + * Moves boat to the heading direction for a given time duration + * @param heading moving direction in degree. + * @param duration moving duration in millisecond. + */ + public void move(double heading, double duration) { + Double distance = speed * duration / 1000000; // convert mm to meter + Position originPos = new Position(lat, lng); + Position newPos = GeoUtility.getGeoCoordinate(originPos, heading, distance); + this.lat = newPos.getLat(); + this.lng = newPos.getLng(); + } + + public String toString() { + return String.format("Boat (%d): lat: %f, lng: %f", sourceID, lat, lng); + } + + public int getSourceID() { + return sourceID; + } + + public void setSourceID(int sourceID) { + this.sourceID = sourceID; + } + + public double getLat() { + return lat; + } + + public void setLat(double lat) { + this.lat = lat; + } + + public double getLng() { + return lng; + } + + public void setLng(double lng) { + this.lng = lng; + } + + public double getSpeed() { + return speed; + } + + public void setSpeed(double speed) { + this.speed = speed; + } + + public String getBoatName() { + return boatName; + } + + public void setBoatName(String boatName) { + this.boatName = boatName; + } + + public String getShortName() { + return shortName; + } + + public void setShortName(String shortName) { + this.shortName = shortName; + } + + public String getShorterName() { + return shorterName; + } + + public void setShorterName(String shorterName) { + this.shorterName = shorterName; + } + + public Corner getLastPassedCorner() { + return lastPassedCorner; + } + + public void setLastPassedCorner(Corner lastPassedCorner) { + this.lastPassedCorner = lastPassedCorner; + } + + public Corner getHeadingCorner() { + return headingCorner; + } + + public void setHeadingCorner(Corner headingCorner) { + this.headingCorner = headingCorner; + } +} diff --git a/src/main/java/seng302/server/simulator/GeoUtility.java b/src/main/java/seng302/server/simulator/GeoUtility.java new file mode 100644 index 00000000..dff67e50 --- /dev/null +++ b/src/main/java/seng302/server/simulator/GeoUtility.java @@ -0,0 +1,82 @@ +package seng302.server.simulator; + +import seng302.server.simulator.mark.Position; + +public class GeoUtility { + + private static double EARTH_RADIUS = 6378.137; + + /** + * Calculates the euclidean distance between two markers on the canvas using xy coordinates + * + * @param p1 first geographical position + * @param p2 second geographical position + * @return the distance in meter between two points in meters + */ + public static Double getDistance(Position p1, Position p2) { + + double dLat = Math.toRadians(p2.getLat() - p1.getLat()); + double dLon = Math.toRadians(p2.getLng() - p1.getLng()); + + double a = Math.pow(Math.sin(dLat / 2), 2.0) + + Math.cos(Math.toRadians(p1.getLat())) * Math.cos(Math.toRadians(p2.getLat())) + * Math.pow(Math.sin(dLon / 2), 2.0); + + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + double d = EARTH_RADIUS * c; + + return d * 1000; // distance from km to meter + } + + /** + * Calculates the angle between to angular co-ordinates on a sphere. + * + * @param p1 the first geographical position, start point + * @param p2 the second geographical position, end point + * @return the initial bearing in degree from p1 to p2, value range (0 ~ 360 deg.). + * vertical up is 0 deg. horizontal right is 90 deg. + * + * NOTE: + * The final bearing will differ from the initial bearing by varying degrees + * according to distance and latitude (if you were to go from say 35°N,45°E + * (≈ Baghdad) to 35°N,135°E (≈ Osaka), you would start on a heading of 60° + * and end up on a heading of 120° + */ + public static Double getBearing(Position p1, Position p2) { + + double dLon = Math.toRadians(p2.getLng() - p1.getLng()); + + double y = Math.sin(dLon) * Math.cos(Math.toRadians(p2.getLat())); + double x = Math.cos(Math.toRadians(p1.getLat())) * Math.sin(Math.toRadians(p2.getLat())) + - Math.sin(Math.toRadians(p1.getLat())) * Math.cos(Math.toRadians(p2.getLat())) * Math.cos(dLon); + + double bearing = Math.toDegrees(Math.atan2(y, x)); + + return (bearing + 360.0) % 360.0; + } + + /** + * Given an existing point in lat/lng, distance in (in meter) and bearing + * (in degrees), calculates the new lat/lng. + * + * @param origin the original position within lat / lng + * @param bearing the bearing in degree, from original position to the new position + * @param distance the distance in meter, from original position to the new position + * @return the new position + */ + public static Position getGeoCoordinate(Position origin, Double bearing, Double distance) { + double b = Math.toRadians(bearing); // bearing to radians + double d = distance / 1000.0; // distance to km + + double originLat = Math.toRadians(origin.getLat()); + double originLng = Math.toRadians(origin.getLng()); + + double endLat = Math.asin(Math.sin(originLat) * Math.cos(d / EARTH_RADIUS) + + Math.cos(originLat) * Math.sin(d / EARTH_RADIUS) * Math.cos(b)); + double endLng = originLng + + Math.atan2(Math.sin(b) * Math.sin(d / EARTH_RADIUS) * Math.cos(originLat), + Math.cos(d / EARTH_RADIUS) - Math.sin(originLat) * Math.sin(endLat)); + + return new Position(Math.toDegrees(endLat), Math.toDegrees(endLng)); + } +} diff --git a/src/main/java/seng302/server/simulator/Simulator.java b/src/main/java/seng302/server/simulator/Simulator.java new file mode 100644 index 00000000..d7f1d72c --- /dev/null +++ b/src/main/java/seng302/server/simulator/Simulator.java @@ -0,0 +1,129 @@ +package seng302.server.simulator; + +import seng302.server.simulator.mark.Corner; +import seng302.server.simulator.mark.Mark; +import seng302.server.simulator.mark.Position; +import seng302.server.simulator.parsers.RaceParser; + +import java.util.List; +import java.util.Observable; +import java.util.concurrent.ThreadLocalRandom; + +public class Simulator extends Observable implements Runnable { + + private List course; + private List boats; + private long lapse; + + /** + * Creates a simulator instance with given time lapse. + * @param lapse time duration in millisecond. + */ + public Simulator(long lapse) { + RaceParser rp = new RaceParser("/server_config/race.xml"); + course = rp.getCourse(); + boats = rp.getBoats(); + this.lapse = lapse; + + setLegs(); + + // set start line's coordinate to boats + Double startLat = course.get(0).getCompoundMark().getMark1().getLat(); + Double startLng = course.get(0).getCompoundMark().getMark1().getLng(); + for (Boat boat : boats) { + boat.setLat(startLat); + boat.setLng(startLng); + boat.setLastPassedCorner(course.get(0)); + boat.setHeadingCorner(course.get(1)); + boat.setSpeed(ThreadLocalRandom.current().nextInt(40000, 60000 + 1)); + } + } + + @Override + public void run() { + + int numOfFinishedBoats = 0; + + while (numOfFinishedBoats < boats.size()) { + for (Boat boat : boats) { + numOfFinishedBoats += moveBoat(boat, lapse); + } + //System.out.println(boats.get(0)); + + setChanged(); + notifyObservers(boats); + + // sleep for 1 second. + try { + Thread.sleep(lapse); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + /** + * Moves a boat with given time duration. + * + * @param boat the boat to be moved + * @param duration the moving duration in milliseconds + * @return 1 if the boat has reached the final line, otherwise return 0 + */ + private int moveBoat(Boat boat, double duration) { + if (boat.getHeadingCorner() != null) { + + boat.move(boat.getLastPassedCorner().getBearingToNextCorner(), duration); + + Position boatPos = new Position(boat.getLat(), boat.getLng()); + Position lastMarkPos = boat.getLastPassedCorner().getCompoundMark().getMark1(); + + double distanceFromLastMark = GeoUtility.getDistance(boatPos, lastMarkPos); + // if a boat passes its heading mark + while (distanceFromLastMark >= boat.getLastPassedCorner().getDistanceToNextCorner()) { + double compensateDistance = distanceFromLastMark - boat.getLastPassedCorner().getDistanceToNextCorner(); + boat.setLastPassedCorner(boat.getHeadingCorner()); + boat.setHeadingCorner(boat.getLastPassedCorner().getNextCorner()); + + // heading corner == null means boat has reached the final mark + if (boat.getHeadingCorner() == null) return 1; + + // move compensate distance for the mark just passed + Position pos = GeoUtility.getGeoCoordinate( + boat.getLastPassedCorner().getCompoundMark().getMark1(), + boat.getLastPassedCorner().getBearingToNextCorner(), + compensateDistance); + boat.setLat(pos.getLat()); + boat.setLng(pos.getLng()); + distanceFromLastMark = GeoUtility.getDistance(new Position(boat.getLat(), boat.getLng()), + boat.getLastPassedCorner().getCompoundMark().getMark1()); + } + } + return 0; + } + + /** + * Link all the corners in the course list so that every corner knows its next + * corner, as well as the distance and bearing to its next corner. However, + * the last corner's heading is null, which means it is the final line. + */ + private void setLegs() { + // get the bearing from one mark to the next heading mark + for (int i = 0; i < course.size() - 1; i++) { + + Mark mark1 = course.get(i).getCompoundMark().getMark1(); + Mark mark2 = course.get(i + 1).getCompoundMark().getMark1(); + course.get(i).setDistanceToNextCorner(GeoUtility.getDistance(mark1, mark2)); + + course.get(i).setNextCorner(course.get(i + 1)); + + course.get(i).setBearingToNextCorner( + GeoUtility.getBearing(course.get(i).getCompoundMark().getMark1(), + course.get(i + 1).getCompoundMark().getMark1())); + } + } + + public List getBoats(){ + return boats; + } + +} diff --git a/src/main/java/seng302/server/simulator/mark/CompoundMark.java b/src/main/java/seng302/server/simulator/mark/CompoundMark.java new file mode 100644 index 00000000..489a4a12 --- /dev/null +++ b/src/main/java/seng302/server/simulator/mark/CompoundMark.java @@ -0,0 +1,70 @@ +package seng302.server.simulator.mark; + +public class CompoundMark { + + private int markID; + private String name; + + private Mark mark1; + private Mark mark2; + + public CompoundMark(int markID, String name) { + this.markID = markID; + this.name = name; + } + + public void addMark(int seqId, Mark mark) { + if (seqId == 1) { + setMark1(mark); + } else if (seqId == 2) { + setMark2(mark); + } + } + + /** + * Prints out compoundMark's info and its marks, good for testing + * @return a string showing its details + */ + @Override + public String toString(){ + if (mark2 == null) + return String.format("CompoundMark: %d (%s), [%s]", + markID, name, mark1.toString()); + return String.format("CompoundMark: %d (%s), [%s; %s]", + markID, name, mark1.toString(), mark2.toString()); + } + + public int getMarkID() { + return markID; + } + + public void setMarkID(int markID) { + this.markID = markID; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Mark getMark1() { + return mark1; + } + + public void setMark1(Mark mark1) { + this.mark1 = mark1; + mark1.setSeqID(1); + } + + public Mark getMark2() { + return mark2; + } + + public void setMark2(Mark mark2) { + this.mark2 = mark2; + mark2.setSeqID(2); + } +} diff --git a/src/main/java/seng302/server/simulator/mark/Corner.java b/src/main/java/seng302/server/simulator/mark/Corner.java new file mode 100644 index 00000000..136212f2 --- /dev/null +++ b/src/main/java/seng302/server/simulator/mark/Corner.java @@ -0,0 +1,89 @@ +package seng302.server.simulator.mark; + +public class Corner { + + private int seqID; + private CompoundMark compoundMark; + //private int CompoundMarkID; + private RoundingType roundingType; + private int zoneSize; // size of the zone around a mark in boat-lengths. + + // TODO: this shouldn't be used in the future!!!! + private double bearingToNextCorner, distanceToNextCorner; + private Corner nextCorner; + + public Corner(int seqID, CompoundMark compoundMark, RoundingType roundingType, int zoneSize) { + this.seqID = seqID; + this.compoundMark = compoundMark; + this.roundingType = roundingType; + this.zoneSize = zoneSize; + } + + /** + * Prints out corner's info and its compound mark, good for testing + * @return a string showing its details + */ + @Override + public String toString() { + return String.format("Corner: %d - %s - %d, %s\n", + seqID, roundingType.getType(), zoneSize, compoundMark.toString()); + } + + public int getSeqID() { + return seqID; + } + + public void setSeqID(int seqID) { + this.seqID = seqID; + } + + public CompoundMark getCompoundMark() { + return compoundMark; + } + + public void setCompoundMark(CompoundMark compoundMark) { + this.compoundMark = compoundMark; + } + + public RoundingType getRoundingType() { + return roundingType; + } + + public void setRoundingType(RoundingType roundingType) { + this.roundingType = roundingType; + } + + public int getZoneSize() { + return zoneSize; + } + + public void setZoneSize(int zoneSize) { + this.zoneSize = zoneSize; + } + + + // TODO: next six setters & getters shouldn't be used in the future. + public double getBearingToNextCorner() { + return bearingToNextCorner; + } + + public void setBearingToNextCorner(double bearingToNextCorner) { + this.bearingToNextCorner = bearingToNextCorner; + } + + public double getDistanceToNextCorner() { + return distanceToNextCorner; + } + + public void setDistanceToNextCorner(double distanceToNextCorner) { + this.distanceToNextCorner = distanceToNextCorner; + } + + public Corner getNextCorner() { + return nextCorner; + } + + public void setNextCorner(Corner nextCorner) { + this.nextCorner = nextCorner; + } +} diff --git a/src/main/java/seng302/server/simulator/mark/Mark.java b/src/main/java/seng302/server/simulator/mark/Mark.java new file mode 100644 index 00000000..41f00bb6 --- /dev/null +++ b/src/main/java/seng302/server/simulator/mark/Mark.java @@ -0,0 +1,53 @@ +package seng302.server.simulator.mark; + +/** + * An abstract class to represent general marks + * Created by Haoming Yin (hyi25) on 17/3/17. + */ +public class Mark extends Position { + + private int seqID; + private String name; + private int sourceID; + + public Mark(String name, double lat, double lng, int sourceID) { + super(lat, lng); + this.name = name; + this.sourceID = sourceID; + } + + /** + * Prints out mark's info and its geo location, good for testing + * @return a string showing its details + */ + @Override + public String toString() { + return String.format("Mark%d: %s, source: %d, lat: %f, lng: %f", seqID, name, sourceID, lat, lng); + } + + public int getSeqID() { + return seqID; + } + + public void setSeqID(int seqID) { + this.seqID = seqID; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getSourceID() { + return sourceID; + } + + public void setSourceID(int sourceID) { + this.sourceID = sourceID; + } +} + + diff --git a/src/main/java/seng302/server/simulator/mark/Position.java b/src/main/java/seng302/server/simulator/mark/Position.java new file mode 100644 index 00000000..74200e9d --- /dev/null +++ b/src/main/java/seng302/server/simulator/mark/Position.java @@ -0,0 +1,31 @@ +package seng302.server.simulator.mark; + +public class Position { + + double lat, lng; + + public Position(double lat, double lng) { + this.lat = lat; + this.lng = lng; + } + + public String toString() { + return String.format("Position at lat:%f lng:%f.", lat, lng); + } + + public double getLat() { + return lat; + } + + public void setLat(double lat) { + this.lat = lat; + } + + public double getLng() { + return lng; + } + + public void setLng(double lng) { + this.lng = lng; + } +} diff --git a/src/main/java/seng302/server/simulator/mark/RoundingType.java b/src/main/java/seng302/server/simulator/mark/RoundingType.java new file mode 100644 index 00000000..de6f6133 --- /dev/null +++ b/src/main/java/seng302/server/simulator/mark/RoundingType.java @@ -0,0 +1,43 @@ +package seng302.server.simulator.mark; + +public enum RoundingType { + + // the mark should be rounded to port (boat's left) + PORT("Port"), + + // the mark should be rounded to starboard (boat's right) + STARBOARD("Stbd"), + + // the boat within the compound mark with the SeqID of 1 should be rounded + // to starboard and the boat within the compound mark with the SeqID of 2 + // should be rounded to port. + SP("SP"), + + // the opposite of SP + PS("PS"); + + private String type; + + RoundingType(String type) { + this.type = type; + } + + public String getType() { + return this.type; + } + + public static RoundingType typeOf(String type) { + switch (type) { + case "Port": + return PORT; + case "Stbd": + return STARBOARD; + case "SP": + return SP; + case "PS": + return PS; + default: + return null; + } + } +} diff --git a/src/main/java/seng302/server/simulator/parsers/BoatsParser.java b/src/main/java/seng302/server/simulator/parsers/BoatsParser.java new file mode 100644 index 00000000..5d552a00 --- /dev/null +++ b/src/main/java/seng302/server/simulator/parsers/BoatsParser.java @@ -0,0 +1,20 @@ +package seng302.server.simulator.parsers; + +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; + + +/** + * Parses the race xml file to get course details + * Created by Haoming Yin (hyi25) on 16/3/2017 + */ +public class BoatsParser extends FileParser { + + private Document doc; + + public BoatsParser(String path) { + super(path); + this.doc = this.parseFile(); + } + +} diff --git a/src/main/java/seng302/server/simulator/parsers/CourseParser.java b/src/main/java/seng302/server/simulator/parsers/CourseParser.java new file mode 100644 index 00000000..f7be46cd --- /dev/null +++ b/src/main/java/seng302/server/simulator/parsers/CourseParser.java @@ -0,0 +1,118 @@ +package seng302.server.simulator.parsers; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import seng302.server.simulator.mark.CompoundMark; +import seng302.server.simulator.mark.Corner; +import seng302.server.simulator.mark.Mark; +import seng302.server.simulator.mark.RoundingType; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Parses the race xml file to get course details + * Created by Haoming Yin (hyi25) on 16/3/2017 + */ +public class CourseParser extends FileParser { + + private Document doc; + private Map compoundMarksMap; + + public CourseParser(String path) { + super(path); + this.doc = this.parseFile(); + } + + // TODO: should handle error / invalid file gracefully + protected List getCourse() { + compoundMarksMap = getCompoundMarks(doc.getDocumentElement()); + List corners = new ArrayList<>(); + NodeList cMarksSequence = doc.getElementsByTagName("Corner"); + + for (int i = 0; i < cMarksSequence.getLength(); i++) { + corners.add(getCorner(cMarksSequence.item(i))); + } + return corners; + } + + + private Corner getCorner(Node node) { + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element e = (Element) node; + + Integer seqId = Integer.valueOf(e.getAttribute("SeqID")); + Integer cMarkId = Integer.valueOf(e.getAttribute("CompoundMarkID")); + CompoundMark cMark = compoundMarksMap.get(cMarkId); + RoundingType roundingType = RoundingType.typeOf(e.getAttribute("Rounding")); + Integer zoneSize = Integer.valueOf(e.getAttribute("ZoneSize")); + + return new Corner(seqId, cMark, roundingType, zoneSize); + } + return null; + } + + private Map getCompoundMarks(Node node) { + Map compoundMarksMap = new HashMap<>(); + + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) node; + NodeList cMarks = element.getElementsByTagName("CompoundMark"); + + // loop through all compound marks who are the children of course node + for (int i = 0; i < cMarks.getLength(); i++) { + CompoundMark cMark = getCompoundMark(cMarks.item(i)); + if (cMark != null) + compoundMarksMap.put(cMark.getMarkID(), cMark); + } + + return compoundMarksMap; + } + return null; + } + + + private CompoundMark getCompoundMark(Node node) { + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element e = (Element) node; + Integer markID = Integer.valueOf(e.getAttribute("CompoundMarkID")); + + String name = e.getAttribute("Name"); + CompoundMark cMark = new CompoundMark(markID, name); + + NodeList marks = e.getElementsByTagName("Mark"); + for (int i = 0; i < marks.getLength(); i++) { + Mark mark = getMark(marks.item(i)); + if (mark != null) + cMark.addMark(mark.getSeqID(), mark); + } + return cMark; + } + System.out.println("Failed to create compound mark."); + return null; + } + + + private Mark getMark(Node node) { + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element e = (Element) node; + Integer seqId = Integer.valueOf(e.getAttribute("SeqID")); + String name = e.getAttribute("Name"); + Double lat = Double.valueOf(e.getAttribute("TargetLat")); + Double lng = Double.valueOf(e.getAttribute("TargetLng")); + Integer sourceId = Integer.valueOf(e.getAttribute("SourceID")); + + Mark mark = new Mark(name, lat, lng, sourceId); + mark.setSeqID(seqId); + + return mark; + } + System.out.println("Failed to create mark."); + return null; + } + +} diff --git a/src/main/java/seng302/server/simulator/parsers/FileParser.java b/src/main/java/seng302/server/simulator/parsers/FileParser.java new file mode 100644 index 00000000..94910720 --- /dev/null +++ b/src/main/java/seng302/server/simulator/parsers/FileParser.java @@ -0,0 +1,52 @@ +package seng302.server.simulator.parsers; + +import org.w3c.dom.Document; +import org.xml.sax.InputSource; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.InputStream; +import java.io.StringReader; + +/** + * Created by Haoming Yin (hyi25) on 16/3/2017 + */ +public abstract class FileParser { + + private String filePath; + + public FileParser() {} + + public FileParser(String path) { + this.filePath = path; + } + + protected Document parseFile() { + try { + InputStream is = getClass().getResourceAsStream(this.filePath); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(is); + // optional, in order to recover info from broken line. + doc.getDocumentElement().normalize(); + return doc; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + protected Document parseFile(String xmlString) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(new InputSource(new StringReader(xmlString))); + // optional, in order to recover info from broken line. + doc.getDocumentElement().normalize(); + return doc; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/src/main/java/seng302/server/simulator/parsers/RaceParser.java b/src/main/java/seng302/server/simulator/parsers/RaceParser.java new file mode 100644 index 00000000..14bf7bb8 --- /dev/null +++ b/src/main/java/seng302/server/simulator/parsers/RaceParser.java @@ -0,0 +1,66 @@ +package seng302.server.simulator.parsers; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import seng302.server.simulator.Boat; +import seng302.server.simulator.mark.Corner; + +import java.util.ArrayList; +import java.util.List; + +/** + * Parses the race xml file to get course details + * Created by Haoming Yin (hyi25) on 16/3/2017 + */ +public class RaceParser extends FileParser { + + private Document doc; + private String path; + + public RaceParser(String path) { + super(path); + this.path = path; + this.doc = this.parseFile(); + } + + /** + * Parses race.xml file and returns a list of corner which is the race course. + * @return a list of ordered corner to represent the course. + */ + public List getCourse() { + CourseParser cp = new CourseParser(path); + return cp.getCourse(); + } + + /** + * Parses race.xml file and return a list of boats which will compete in the + * race. + * @return a list of boats that are going to compete in the race. + */ + public List getBoats() { + NodeList yachts = doc.getDocumentElement().getElementsByTagName("Yacht"); + List boats = new ArrayList<>(); + + for (int i = 0; i < yachts.getLength(); i++) { + boats.add(getBoat(yachts.item(i))); + } + return boats; + } + + /** + * Parses a single boat from the given node + * @param node a node within a boat tag + * @return a boat instance parsed from the given node + */ + private Boat getBoat(Node node) { + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element e = (Element) node; + + Integer sourceId = Integer.valueOf(e.getAttribute("SourceID")); + return new Boat(sourceId, "Test Boat"); + } + return null; + } +} diff --git a/src/main/resources/config/course.xml b/src/main/resources/config/course.xml index f8bfa00e..cec726ad 100644 --- a/src/main/resources/config/course.xml +++ b/src/main/resources/config/course.xml @@ -1,62 +1,71 @@ - + Start Start1 - 32.296577 - -64.854304 + 57.6703330 + 11.8278330 + 122 Start2 - 32.293771 - -64.855242 + 57.6706330 + 11.8281330 + 123 Mid Mark - 32.293039 - -64.843983 + 57.6675700 + 11.8359880 + 131 Leeward Gate Leeward Gate1 - 32.284680 - -64.850045 + 57.6708220 + 11.8433900 + 124 Leeward Gate2 - 32.280164 - -64.847591 + 57.6711220 + 11.8436900 + 125 Windward Gate Windward Gate1 - 32.309693 - -64.835249 + 57.6650170 + 11.8279170 + 126 Windward Gate2 - 32.308046 - -64.831785 + 57.6653170 + 11.8282170 + 127 Finish Finish1 - 32.317379 - -64.839291 + 57.6715240 + 11.8444950 + 128 Finish2 - 32.317257 - -64.836260 + 57.6718240 + 11.8447950 + 129 @@ -68,4 +77,4 @@ Leeward Gate Finish - + diff --git a/src/main/resources/config/courseAlt.xml b/src/main/resources/config/courseAlt.xml new file mode 100644 index 00000000..c0f83b32 --- /dev/null +++ b/src/main/resources/config/courseAlt.xml @@ -0,0 +1,72 @@ + + + + + + Start + + Start1 + 32.296577 + -64.854304 + + + Start2 + 32.293771 + -64.855242 + + + + Mid Mark + 32.293039 + -64.843983 + + + Leeward Gate + + Leeward Gate1 + 32.284680 + -64.850045 + + + Leeward Gate2 + 32.280164 + -64.847591 + + + + Windward Gate + + Windward Gate1 + 32.309693 + -64.835249 + + + Windward Gate2 + 32.308046 + -64.831785 + + + + Finish + + Finish1 + 32.317379 + -64.839291 + + + Finish2 + 32.317257 + -64.836260 + + + + + Start + Mid Mark + Leeward Gate + Windward Gate + Leeward Gate + Finish + + + diff --git a/src/main/resources/config/teams.xml b/src/main/resources/config/teams.xml index 582f9e51..0ac01cac 100644 --- a/src/main/resources/config/teams.xml +++ b/src/main/resources/config/teams.xml @@ -4,31 +4,37 @@ Oracle Team USA USA - 12.9 + 0.0 + 102 Artemis Racing ART - 13.1 + 0.0 + 101 Emirates Team New Zealand NZL - 15.6 + 0.0 + 103 Land Rover BAR BAR - 13.3 + 0.0 + 104 SoftBank Team Japan JAP - 14.7 + 0.0 + 105 Groupama Team France FRC - 11.4 + 0.0 + 106 \ No newline at end of file diff --git a/src/main/resources/server_config/boats.xml b/src/main/resources/server_config/boats.xml new file mode 100644 index 00000000..f5e1e1fb --- /dev/null +++ b/src/main/resources/server_config/boats.xml @@ -0,0 +1,171 @@ + + + 2015-08-28T17:32:59+0100 + 12 + 219 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/server_config/race.xml b/src/main/resources/server_config/race.xml new file mode 100644 index 00000000..845f2044 --- /dev/null +++ b/src/main/resources/server_config/race.xml @@ -0,0 +1,85 @@ + + + 2015-08-29T13:12:40+02:00 + + 15082901 + Fleet + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/server_config/regatta.xml b/src/main/resources/server_config/regatta.xml new file mode 100644 index 00000000..6abcf2da --- /dev/null +++ b/src/main/resources/server_config/regatta.xml @@ -0,0 +1,12 @@ + + + 24 + Gothenburg World Series 2015 + Gothenburg + 57.6679590 + 11.8503233 + 6.95 + 2 + 3.2 + gothenburg_shoreline + \ No newline at end of file diff --git a/src/main/resources/views/MainView.fxml b/src/main/resources/views/MainView.fxml index ac0b944e..cc38f3ed 100644 --- a/src/main/resources/views/MainView.fxml +++ b/src/main/resources/views/MainView.fxml @@ -1,11 +1,62 @@ + + + - + + + + + + + + + + + + + + + + + + +