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/controllers/RaceController.java b/src/main/java/seng302/controllers/RaceController.java deleted file mode 100644 index b5fa2847..00000000 --- a/src/main/java/seng302/controllers/RaceController.java +++ /dev/null @@ -1,80 +0,0 @@ -package seng302.controllers; - -import seng302.models.Boat; -import seng302.models.Race; -import seng302.models.parsers.ConfigParser; -import seng302.models.parsers.CourseParser; -import seng302.models.parsers.TeamsParser; - -import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Random; - -/** - * Created by zyt10 on 17/03/17. - * run before CanvasController to initialize race events - * the CanvasController then uses the event data to make the animations - */ -public class RaceController { - Race race = null; - - public void initializeRace() { - String raceConfigFile = "/config/config.xml"; - String teamsConfigFile = "/config/teams.xml"; - - try { - race = createRace(raceConfigFile, teamsConfigFile); - } catch (Exception e) { - System.out.println("There was an error creating the race."); - } - - if (race != null) { - race.startRace(); - } else { - System.out.println("There was an error creating the race. Exiting."); - } - } - - public Race createRace(String configFile, String teamsConfigFile) throws Exception { - Race race = new Race(); - - // Read team names from file - TeamsParser tp = new TeamsParser(teamsConfigFile); - - // Read course from file - ConfigParser config = new ConfigParser(configFile); - - ArrayList boatNames = new ArrayList<>(); - ArrayList teams = tp.getBoats(); - - //get race size - int numberOfBoats = teams.size(); - - //get time scale - double timeScale = config.getTimeScale(); - race.setTimeScale(timeScale); - - for (Boat boat : teams) { - boatNames.add(boat.getTeamName()); - race.addBoat(boat); - } - - // Shuffle team names - long seed = System.nanoTime(); - Collections.shuffle(boatNames, new Random(seed)); - - if (numberOfBoats > Array.getLength(boatNames.toArray())) { - return null; - } - - CourseParser course = new CourseParser("/config/course.xml"); - race.addCourse(course.getCourse()); - - return race; - } - - public Race getRace() { - return race; - } -} diff --git a/src/main/java/seng302/controllers/RaceResultController.java b/src/main/java/seng302/controllers/RaceResultController.java deleted file mode 100644 index 7378fa68..00000000 --- a/src/main/java/seng302/controllers/RaceResultController.java +++ /dev/null @@ -1,37 +0,0 @@ -package seng302.controllers; - -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.VBox; -import javafx.scene.text.Text; -import seng302.models.Race; - -import java.net.URL; -import java.util.ResourceBundle; - -/** - * Created by ptg19 on 20/03/17. - */ -public class RaceResultController implements Initializable{ - @FXML private AnchorPane window; - @FXML private VBox resultsVBox; - private Race race; - - RaceResultController(Race race){ - this.race = race; - } - - @Override - public void initialize(URL location, ResourceBundle resources) { - int boatPosition = this.race.getFinishedBoats().length; - - for (int i = this.race.getFinishedBoats().length - 1; i >= 0; i--){ - resultsVBox.getChildren().add(0, new Text(boatPosition + ": " + this.race.getFinishedBoats()[i].getTeamName())); - boatPosition--; - } - - - - } -} diff --git a/src/main/java/seng302/models/Leg.java b/src/main/java/seng302/models/Leg.java deleted file mode 100644 index 8f21a6ea..00000000 --- a/src/main/java/seng302/models/Leg.java +++ /dev/null @@ -1,116 +0,0 @@ -package seng302.models; - -import seng302.models.mark.SingleMark; - -/** -* Represents the leg of a race. -*/ -public class Leg { - private int heading; - private int distance; - private boolean isFinishingLeg; - private SingleMark startingSingleMark; - - /** - * Create a new leg - * - * @param heading, the magnetic heading of this leg - * @param distance, the total distance of this leg in meters - * @param singleMark, the singleMark this leg starts on - */ - public Leg(int heading, int distance, SingleMark singleMark) { - this.heading = heading; - this.distance = distance; - this.startingSingleMark = singleMark; - this.isFinishingLeg = false; - } - - /** - * Create a new leg - * - * @param heading, the magnetic heading of this leg - * @param distance, the total distance of this leg in meters - * @param markerName, the name of the marker this leg starts on - */ - public Leg(int heading, int distance, String markerName) { - this.heading = heading; - this.distance = distance; - this.startingSingleMark = new SingleMark(markerName); - this.isFinishingLeg = false; - } - - /** - * Get the heading of this leg - * @return int - */ - public int getHeading() { - return this.heading; - } - - /** - * Set the heading for this leg - * @param heading - */ - public void setHeading(int heading) { - this.heading = heading; - } - - /** - * Get the total distance of this leg in meters - * @return int - */ - public int getDistance() { - return this.distance; - } - - /** - * Set the distance of this leg in meters - * @param distance - */ - public void setDistance(int distance) { - this.distance = distance; - } - - /** - * Returns the marker this leg started on - * @return SingleMark - */ - public SingleMark getMarker() { - return this.startingSingleMark; - } - - /** - * Set the singleMark this leg starts on - * @param singleMark - */ - public void setMarker(SingleMark singleMark) { - this.startingSingleMark = singleMark; - } - - /** - * Returns the name of the marker this leg started on - * @return String - */ - public String getMarkerLabel() { - return this.startingSingleMark.getName(); - } - - - - /** - * Specify whether or not the race finishes on this leg - * - * @param isFinishingLeg whether or not the race finishes on this leg - */ - public void setFinishingLeg(boolean isFinishingLeg) { - this.isFinishingLeg = isFinishingLeg; - } - - /** - * Returns whether or not the race finishes after this leg - * @return true if this the race finishes after this leg - */ - public boolean getIsFinishingLeg() { - return this.isFinishingLeg; - } -} \ No newline at end of file diff --git a/src/main/java/seng302/models/TimelineInfo.java b/src/main/java/seng302/models/TimelineInfo.java deleted file mode 100644 index 867ce67b..00000000 --- a/src/main/java/seng302/models/TimelineInfo.java +++ /dev/null @@ -1,31 +0,0 @@ -package seng302.models; - -import javafx.animation.Timeline; -import javafx.beans.property.DoubleProperty; - - -/** - * Created by zyt10 on 17/03/17. - * this class is literally just to associate a timeline with a DoubleProperty x and y - */ -public class TimelineInfo { - private Timeline timeline; - private DoubleProperty x; - private DoubleProperty y; - - public TimelineInfo(Timeline timeline, DoubleProperty x, DoubleProperty y) { - this.timeline = timeline; - this.x = x; - this.y = y; - } - - public Timeline getTimeline() { - return timeline; - } - public DoubleProperty getX() { - return x; - } - public DoubleProperty getY() { - return y; - } -} diff --git a/src/main/java/seng302/models/parsers/ConfigParser.java b/src/main/java/seng302/models/parsers/ConfigParser.java deleted file mode 100644 index 1d870c67..00000000 --- a/src/main/java/seng302/models/parsers/ConfigParser.java +++ /dev/null @@ -1,78 +0,0 @@ -package seng302.models.parsers; - - -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -import java.util.DoubleSummaryStatistics; - -public class ConfigParser extends FileParser { - - private Document doc; - - public ConfigParser(String path) { - super(path); - this.doc = this.parseFile(); - } - - /** - * Gets wind direction from config file. - * - * @return a double type degree, or 0 if no value or invalid value is found - */ - public double getWindDirection() { - return getDoubleByTagName("wind-direction", 0.0); - } - - /** - * Gets a non negative time scale for the race - * - * @return a double type scale, or 0 if no scale or invalid scale is found - */ - public double getTimeScale() { - return getDoubleByTagName("time-scale", 1.0); - } - - /** - * Gets a double type number by given tag name found in xml file - * - * @param tagName a string of tag name - * @param defaultVal value returned if no value or invalid value is found - * @return value found - */ - public double getDoubleByTagName(String tagName, double defaultVal) { - double val = defaultVal; - try { - Node node = this.doc.getElementsByTagName(tagName).item(0); - if (node.getNodeType() == Node.ELEMENT_NODE) { - Element element = (Element) node; - val = Double.valueOf(element.getTextContent()); - } - } catch (Exception e) { - } finally { - return val; - } - } - - /** - * Gets a string by given tag name found in xml file - * - * @param tagName a string of tag name - * @param defaultVal a string returned if no value or invalid value is found - * @return string found - */ - public String getStringByTagName(String tagName, String defaultVal) { - String string = defaultVal; - try { - Node node = this.doc.getElementsByTagName(tagName).item(0); - if (node.getNodeType() == Node.ELEMENT_NODE) { - Element element = (Element) node; - string = element.getTextContent(); - } - } catch (Exception e) { - } finally { - return string; - } - } -} 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/models/parsers/FileParser.java b/src/main/java/seng302/server/simulator/parsers/FileParser.java similarity index 95% rename from src/main/java/seng302/models/parsers/FileParser.java rename to src/main/java/seng302/server/simulator/parsers/FileParser.java index be162b9e..94910720 100644 --- a/src/main/java/seng302/models/parsers/FileParser.java +++ b/src/main/java/seng302/server/simulator/parsers/FileParser.java @@ -1,12 +1,10 @@ -package seng302.models.parsers; +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.File; -import java.io.IOException; import java.io.InputStream; import java.io.StringReader; 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 6e1a72fb..cec726ad 100644 --- a/src/main/resources/config/course.xml +++ b/src/main/resources/config/course.xml @@ -1,6 +1,6 @@ - + Start @@ -77,4 +77,4 @@ Leeward Gate Finish - + 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/CanvasView.fxml b/src/main/resources/views/CanvasView.fxml deleted file mode 100644 index bc16ad7e..00000000 --- a/src/main/resources/views/CanvasView.fxml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/main/resources/views/FinishView.fxml b/src/main/resources/views/FinishView.fxml deleted file mode 100644 index debdea26..00000000 --- a/src/main/resources/views/FinishView.fxml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/test/java/seng302/LegTest.java b/src/test/java/seng302/LegTest.java deleted file mode 100644 index 9bb64b6c..00000000 --- a/src/test/java/seng302/LegTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package seng302; - -import org.junit.Test; -import seng302.models.Leg; -import seng302.models.mark.SingleMark; - -import static org.junit.Assert.assertEquals; - -/** - * Unit test for the Leg class. - */ -public class LegTest { - - /** - * Test creation of the leg by specifying a string - * for the marker label - */ - @Test - public void testLegCreationUsingMarkerLabel() { - Leg leg = new Leg(010, 100, "SingleMark"); - - assertEquals(leg.getHeading(), 010); - assertEquals(leg.getDistance(), 100); - assertEquals(leg.getMarkerLabel(), "SingleMark"); - assertEquals(leg.getIsFinishingLeg(), false); - } - - /** - * Test creation of the leg by providing a - * SingleMark object - */ - @Test - public void testLegCreation() { - Leg leg = new Leg(010, 100, new SingleMark("SingleMark")); - - assertEquals(leg.getHeading(), 010); - assertEquals(leg.getDistance(), 100); - assertEquals(leg.getMarkerLabel(), "SingleMark"); - assertEquals(leg.getIsFinishingLeg(), false); - } - - /** - * Test changing whether or not a - * leg is the finishing leg - */ - @Test - public void testSetFinishLeg() { - Leg leg = new Leg(010, 100, "SingleMark"); - - leg.setFinishingLeg(true); - assertEquals(leg.getIsFinishingLeg(), true); - } - -} diff --git a/src/test/java/seng302/RaceTest.java b/src/test/java/seng302/RaceTest.java deleted file mode 100644 index ab318331..00000000 --- a/src/test/java/seng302/RaceTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package seng302; - -import org.junit.Test; -import seng302.models.Boat; -import seng302.models.Race; - -import java.lang.reflect.Array; - -import static org.junit.Assert.assertEquals; - -/** - * Unit test for the Race class. - */ -public class RaceTest { - /** - * Test that all boats were added to the race - */ - @Test - public void testAddingBoatsToRace() { - Boat boat1 = new Boat("Team 1"); - Boat boat2 = new Boat("Team 2"); - - Race race = new Race(); - race.addBoat(boat1); - race.addBoat(boat2); - - assertEquals(Array.getLength(race.getBoats()), 2); - } - - @Test - public void testGetShuffledBoats(){ - Boat boat1 = new Boat("Team 1"); - Boat boat2 = new Boat("Team 2"); - - Race race = new Race(); - race.addBoat(boat1); - race.addBoat(boat2); - - assertEquals(Array.getLength(race.getShuffledBoats()), 2); - } -} diff --git a/src/test/java/seng302/TestRaceTimer.java b/src/test/java/seng302/TestRaceTimer.java deleted file mode 100644 index 542405b1..00000000 --- a/src/test/java/seng302/TestRaceTimer.java +++ /dev/null @@ -1,25 +0,0 @@ -package seng302; - -import org.junit.Test; -import seng302.controllers.RaceViewController; - -import static org.junit.Assert.assertTrue; - - -public class TestRaceTimer { - @Test - public void testPositiveTimeString(){ - RaceViewController controller = new RaceViewController(); - String result = controller.convertTimeToMinutesSeconds(61); - - assertTrue(result.equals("01:01")); - } - - @Test - public void testNegativeTimeString(){ - RaceViewController controller = new RaceViewController(); - String result = controller.convertTimeToMinutesSeconds(-61); - - assertTrue(result.equals("-01:01")); - } -} diff --git a/src/test/java/seng302/models/parsers/ConfigParserTest.java b/src/test/java/seng302/models/parsers/ConfigParserTest.java deleted file mode 100644 index 8a0c0c8c..00000000 --- a/src/test/java/seng302/models/parsers/ConfigParserTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package seng302.models.parsers; - -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Created by Haoming on 23/03/17. - */ -public class ConfigParserTest { - - private ConfigParser cp; - - @Before - public void initializeParser() throws Exception { - cp = new ConfigParser("/config/config.xml"); - } - - @Test - public void getWindDirection() throws Exception { - assertEquals(135, cp.getWindDirection(), 1e-10); - } - - @Test - public void getTimeScale() throws Exception { - assertEquals(10.0, cp.getTimeScale(), 1e-10); - } - - @Test - public void getDoubleByTagName() throws Exception { - assertEquals(6, cp.getDoubleByTagName("race-size", 0), 1e-10); - assertEquals(100, cp.getDoubleByTagName("noTag", 100), 1e-10); - } - - @Test - public void getStringByTagName() throws Exception { - assertEquals("AC35", cp.getStringByTagName("race-name", "11")); - assertEquals("oops", cp.getStringByTagName("noTag", "oops")); - } - -} \ No newline at end of file diff --git a/src/test/java/seng302/models/parsers/TeamsParserTest.java b/src/test/java/seng302/models/parsers/TeamsParserTest.java deleted file mode 100644 index 3c31b519..00000000 --- a/src/test/java/seng302/models/parsers/TeamsParserTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package seng302.models.parsers; - -import org.junit.Before; -import org.junit.Test; -import seng302.models.Boat; - -import java.util.ArrayList; - -import static org.junit.Assert.*; - -/** - * Created by Haoming on 18/03/17. - */ -public class TeamsParserTest { - - private TeamsParser tp; - @Before - public void readFile() { - tp = new TeamsParser("/config/teams.xml"); - } - - @Test - public void getBoats() throws Exception { - ArrayList boats = tp.getBoats(); - - assertEquals(6, boats.size(), 1e-10); - - assertEquals("Oracle Team USA", boats.get(0).getTeamName()); - //assertEquals(30.9, boats.get(0).getVelocity(), 1e-10); - - assertEquals("Groupama Team France", boats.get(5).getTeamName()); - //assertEquals(45.6, boats.get(5).getVelocity(), 1e-10); - } - -} \ No newline at end of file diff --git a/src/test/java/seng302/server/TestConversions.java b/src/test/java/seng302/server/TestConversions.java new file mode 100644 index 00000000..91bf44b3 --- /dev/null +++ b/src/test/java/seng302/server/TestConversions.java @@ -0,0 +1,35 @@ +package seng302.server; + +import org.junit.Test; +import seng302.server.messages.BoatLocationMessage; + +import static junit.framework.TestCase.assertEquals; + +/** + * Test conversions used by the boat location messages + */ +public class TestConversions { + @Test + public void testLatLonConversion(){ + long binaryPacked = BoatLocationMessage.latLonToBinaryPackedLong(3232.323); + double original = BoatLocationMessage.binaryPackedToLatLon(binaryPacked); + + assertEquals(3232.323, original, 0.01); + } + + @Test + public void testWindAngleConversion(){ + long binaryPacked = BoatLocationMessage.windAngleToBinaryPackedLong(3232.323); + double original = BoatLocationMessage.binaryPackedWindAngleToDouble(binaryPacked); + + assertEquals(3232.323, original, 0.01); + } + + @Test + public void testHeadingConversion(){ + long binaryPacked = BoatLocationMessage.headingToBinaryPackedLong(3232.323); + double original = BoatLocationMessage.binaryPackedHeadingToDouble(binaryPacked); + + assertEquals(3232.323, original, 0.01); + } +} diff --git a/src/test/java/seng302/server/TestHeader.java b/src/test/java/seng302/server/TestHeader.java new file mode 100644 index 00000000..3655bc03 --- /dev/null +++ b/src/test/java/seng302/server/TestHeader.java @@ -0,0 +1,26 @@ +package seng302.server; + +import org.junit.Test; +import seng302.server.messages.*; + +import static junit.framework.TestCase.assertTrue; + +/** + * Tests message header + */ +public class TestHeader { + + @Test + public void testHeaderSizeEqualsActualSize(){ + Header h = new Header(MessageType.DISPLAY_TEXT_MESSAGE, 1, (short) 1); + assertTrue(h.getSize() == h.getByteBuffer().array().length); + + } + + @Test + public void headerSizeIsSameAsSpec(){ + Header h = new Header(MessageType.DISPLAY_TEXT_MESSAGE, 1, (short) 1); + assertTrue(h.getSize() == 15); // Spec specifies 15 bytes + } + +} diff --git a/src/test/java/seng302/server/TestMessage.java b/src/test/java/seng302/server/TestMessage.java new file mode 100644 index 00000000..6ba3a735 --- /dev/null +++ b/src/test/java/seng302/server/TestMessage.java @@ -0,0 +1,56 @@ +package seng302.server; + +import org.junit.Test; +import seng302.server.messages.*; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertTrue; + +public class TestMessage { + private static int XML_MESSAGE_LEN = 14; + private static int CRC_LEN = 4; + + + /** + * Test output expected is the same as the spec + */ + @Test + public void testXmlMessageSize(){ + Message m = new XMLMessage("12345", XMLMessageSubType.BOAT, 1); + assertTrue(m.getSize() == (XML_MESSAGE_LEN + "12345".length())); + } + + @Test + public void testMessageBytesReverse(){ + byte[] bytes = {1,2,3,4,5}; + Message.reverse(bytes); + + int testValue = 1; + for (int i = 5; i > 0; i--){ + assertEquals((byte) testValue, bytes[i-1]); + testValue++; + } + } + + @Test + public void testIntToByteArray(){ + long originalValue = 0x5050; + long testValue = 0; + + byte[] bytes = Message.intToByteArray(originalValue, 6); + Message.reverse(bytes); + + for (int i = 0; i < bytes.length; i++){ + testValue += ((long) bytes[i] & 0xffL) << (8 * i); + } + + assertEquals(originalValue, testValue); + } + +} diff --git a/src/test/java/seng302/server/simulator/GeoUtilityTest.java b/src/test/java/seng302/server/simulator/GeoUtilityTest.java new file mode 100644 index 00000000..4cdf01df --- /dev/null +++ b/src/test/java/seng302/server/simulator/GeoUtilityTest.java @@ -0,0 +1,75 @@ +package seng302.server.simulator; + +import org.junit.Test; +import seng302.server.simulator.mark.Position; + +import static org.junit.Assert.*; + +/** + * To test methods in GeoUtility. + * Created by Haoming on 28/04/17. + */ +public class GeoUtilityTest { + + private Position p1 = new Position(57.670333, 11.827833); + private Position p2 = new Position(57.671524, 11.844495); + private Position p3 = new Position(57.670822, 11.843392); + private Position p4 = new Position(25.694829, 98.392049); + + private double toleranceRate = 0.01; + + @Test + public void getDistance() throws Exception { + double expected, actual; + + actual = GeoUtility.getDistance(p1, p2); + expected = 1000; + assertEquals(expected, actual, expected * toleranceRate); + + actual = GeoUtility.getDistance(p1, p3); + expected = 927; + assertEquals(expected, actual, expected * toleranceRate); + + actual = GeoUtility.getDistance(p2, p4); + expected = 7430180; + assertEquals(expected, actual, expected * toleranceRate); + } + + @Test + public void getBearing() throws Exception { + double expected, actual; + + actual = GeoUtility.getBearing(p1, p2); + expected = 82; + assertEquals(expected, actual, expected * toleranceRate); + + actual = GeoUtility.getBearing(p1, p3); + expected = 86; + assertEquals(expected, actual, expected * toleranceRate); + + actual = GeoUtility.getBearing(p2, p4); + expected = 78; + assertEquals(expected, actual, expected * toleranceRate); + } + + @Test + public void getGeoCoordinate() throws Exception { + Position expected, actual; + + actual = GeoUtility.getGeoCoordinate(p1, 82.0, 1000.0); + expected = p2; + assertEquals(expected.getLat(), actual.getLat(), expected.getLat() * toleranceRate); + assertEquals(expected.getLng(), actual.getLng(), expected.getLng() * toleranceRate); + + actual = GeoUtility.getGeoCoordinate(p1, 86.0, 927.0); + expected = p3; + assertEquals(expected.getLat(), actual.getLat(), expected.getLat() * toleranceRate); + assertEquals(expected.getLng(), actual.getLng(), expected.getLng() * toleranceRate); + + actual = GeoUtility.getGeoCoordinate(p2, 78.0, 7430180.0); + expected = p4; + assertEquals(expected.getLat(), actual.getLat(), expected.getLat() * toleranceRate); + assertEquals(expected.getLng(), actual.getLng(), expected.getLng() * toleranceRate); + } + +} \ No newline at end of file