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 68107866..bd4de5ef 100644 --- a/src/main/java/seng302/App.java +++ b/src/main/java/seng302/App.java @@ -7,6 +7,7 @@ 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 { @@ -22,16 +23,24 @@ 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, "TestThread1"); + sr = new StreamReceiver("localhost", 8085, "RaceStream"); } else{ - sr = new StreamReceiver("csse-s302staff.canterbury.ac.nz", 4941,"TestThread1"); - //sr = new StreamReceiver("livedata.americascup.com", 4941, "TestThread1"); + 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("TestThread2"); + StreamParser streamParser = new StreamParser("StreamParser"); streamParser.start(); launch(args); diff --git a/src/main/java/seng302/controllers/Controller.java b/src/main/java/seng302/controllers/Controller.java index d5d662b2..2baa46c4 100644 --- a/src/main/java/seng302/controllers/Controller.java +++ b/src/main/java/seng302/controllers/Controller.java @@ -17,10 +17,13 @@ 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; @@ -66,10 +69,11 @@ public class Controller implements Initializable { } /** - * Running a timer to update the livestream status on welcome screen. Update interval is 500 miliseconds. + * 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); @@ -83,26 +87,32 @@ public class Controller implements Initializable { timeTillLive.setTextFill(Color.RED); timeTillLive.setText("Race finished! Waiting for new race..."); switchToRaceViewButton.setDisable(true); - } else if (StreamParser.getTimeSinceStart() > 0 && StreamParser.getTimeSinceStart() % 10 == 0) { + } else if (StreamParser.getTimeSinceStart() > 0) { updateTeamList(); timeTillLive.setTextFill(Color.RED); switchToRaceViewButton.setDisable(false); - Long timerMinute = StreamParser.getTimeSinceStart() / 60; - Long timerSecond = StreamParser.getTimeSinceStart() % 60; - String timerString = "-" + timerMinute + "." + timerSecond + " minutes"; + 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 if (StreamParser.getTimeSinceStart() % 10 == 0) { + } else { updateTeamList(); timeTillLive.setTextFill(Color.BLACK); switchToRaceViewButton.setDisable(false); - Long timerMinute = -1 * StreamParser.getTimeSinceStart() / 60; - Long timerSecond = -1 * StreamParser.getTimeSinceStart() % 60; - String timerString = timerMinute + "." + timerSecond + " minutes"; + 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, 500); + }, 0, 1000); } else { timeTillLive.setText("Stream not available."); timeTillLive.setTextFill(Color.RED); 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 43509252..8468376d 100644 --- a/src/main/java/seng302/controllers/RaceViewController.java +++ b/src/main/java/seng302/controllers/RaceViewController.java @@ -293,14 +293,20 @@ public class RaceViewController extends Thread{ private String currentTimer() { String timerString = "0:00 minutes"; - if (StreamParser.getTimeSinceStart() > 0 && StreamParser.getTimeSinceStart() % 10 == 0) { - Long timerMinute = StreamParser.getTimeSinceStart() / 60; - Long timerSecond = StreamParser.getTimeSinceStart() % 60; - timerString = "-" + timerMinute + "." + timerSecond + " minutes"; - } else if (StreamParser.getTimeSinceStart() % 10 == 0) { - Long timerMinute = -1 * StreamParser.getTimeSinceStart() / 60; - Long timerSecond = -1 * StreamParser.getTimeSinceStart() % 60; - timerString = timerMinute + "." + timerSecond + " 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; } diff --git a/src/main/java/seng302/models/BoatGroup.java b/src/main/java/seng302/models/BoatGroup.java index d53edfe2..ef739ae5 100644 --- a/src/main/java/seng302/models/BoatGroup.java +++ b/src/main/java/seng302/models/BoatGroup.java @@ -179,10 +179,10 @@ public class BoatGroup extends RaceObject{ * @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 groundSpeed, int... raceIds) { + public void setDestination (double newXValue, double newYValue, double rotation, double speed, int... raceIds) { if (hasRaceId(raceIds)) { destinationSet = true; - boat.setVelocity(groundSpeed); + boat.setVelocity(speed); if (currentRotation < 0) currentRotation = 360 - currentRotation; double dx = newXValue - boatPoly.getLayoutX(); @@ -226,22 +226,20 @@ public class BoatGroup extends RaceObject{ } } - public void setDestination (double newXValue, double newYValue, int... raceIDs) { + 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 setDestination (double newXValue, double newYValue, 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, raceIDs); -// } -// } public void rotateTo (double rotation) { currentRotation = rotation; diff --git a/src/main/java/seng302/models/RaceObject.java b/src/main/java/seng302/models/RaceObject.java index 21eaece9..bc061584 100644 --- a/src/main/java/seng302/models/RaceObject.java +++ b/src/main/java/seng302/models/RaceObject.java @@ -59,10 +59,9 @@ public abstract class RaceObject extends Group { * @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 groundSpeed boat groundspeed. * @param raceIds RaceID of the object to move. */ - public abstract void setDestination (double x, double y, double rotation, double groundSpeed, int... raceIds); + 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). @@ -70,7 +69,7 @@ public abstract class RaceObject extends Group { * @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, int... raceIds); + public abstract void setDestination (double x, double y, double speed, int... raceIds); public abstract void updatePosition (long timeInterval); diff --git a/src/main/java/seng302/models/mark/MarkGroup.java b/src/main/java/seng302/models/mark/MarkGroup.java index 916bcd41..16a1e06a 100644 --- a/src/main/java/seng302/models/mark/MarkGroup.java +++ b/src/main/java/seng302/models/mark/MarkGroup.java @@ -102,21 +102,21 @@ public class MarkGroup extends RaceObject { //moveTo(points[0].getX(), points[0].getY()); } - public void setDestination (double x, double y, double rotation, double groundSpeed, int... raceIds) { - setDestination(x, y, raceIds); + 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, int... raceIds) { + 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, Math.max(0, i-1)); + setDestinationChild(x, y, 0, Math.max(0, i-1)); } - private void setDestinationChild (double x, double y, int childIndex) { + 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); diff --git a/src/main/java/seng302/models/parsers/StreamParser.java b/src/main/java/seng302/models/parsers/StreamParser.java index 47f3b123..837636e7 100644 --- a/src/main/java/seng302/models/parsers/StreamParser.java +++ b/src/main/java/seng302/models/parsers/StreamParser.java @@ -33,6 +33,7 @@ public class StreamParser extends Thread{ 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; @@ -50,6 +51,7 @@ public class StreamParser extends Thread{ try { System.out.println("START OF STREAM"); streamStatus = true; + xmlObject = new XMLParser(); while (StreamReceiver.packetBuffer == null || StreamReceiver.packetBuffer.size() < 1) { Thread.sleep(1); } @@ -151,15 +153,15 @@ public class StreamParser extends Thread{ private static void extractRaceStatus(StreamPacket packet){ byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; - long currentTime = extractTimeStamp(Arrays.copyOfRange(payload,1,7), 6); + 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 = extractTimeStamp(Arrays.copyOfRange(payload,12,18), 6); + 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 && timeTillStart % 10 == 0) { + if (timeTillStart > 0) { timeSinceStart = timeTillStart; System.out.println("Time till start: " + timeTillStart + " Seconds"); } else { @@ -172,10 +174,8 @@ public class StreamParser extends Thread{ raceFinished = false; System.out.println("RACE HAS STARTED"); } - if (timeTillStart % 10 == 0){ - System.out.println("Time since start: " + -1 * timeTillStart + " Seconds"); - timeSinceStart = timeTillStart; - } + 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)); @@ -188,8 +188,8 @@ public class StreamParser extends Thread{ boatStatus += "\nLegNumber: " + (int)payload[29 + (i * 20)]; boatStatus += "\nPenaltiesAwarded: " + (int)payload[29 + (i * 20)]; boatStatus += "\nPenaltiesServed: " + (int)payload[30 + (i * 20)]; - boatStatus += "\nEstTimeAtNextMark: " + extractTimeStamp(Arrays.copyOfRange(payload,31 + (i * 20),37+ (i * 20)), 6); - boatStatus += "\nEstTimeAtFinish: " + extractTimeStamp(Arrays.copyOfRange(payload,37 + (i * 20),43+ (i * 20)), 6); + 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); } } @@ -219,41 +219,24 @@ public class StreamParser extends Thread{ private static void extractXmlMessage(StreamPacket packet){ byte[] payload = packet.getPayload(); - String xmlMessage = ""; - ByteArrayInputStream payloadStream = new ByteArrayInputStream(payload); - - //Bunch of data we don't want (Message Version Number, AckNumber, Timestamp) - payloadStream.skip(9); - int xmlMessageSubType = payloadStream.read(); - payloadStream.skip(2); - - //checks the length of the xml message itself - int xmlMessageLength = payloadStream.read() | payloadStream.read() << 8; - - //Converts XML message to string to be parsed - int currentChar; - while (payloadStream.available() > 0 && (currentChar = payloadStream.read()) != 0) { - xmlMessage += (char)currentChar; - } - - // Parse boat xml from server - if (xmlMessageSubType == 7) { - BoatsParser boatsParser = new BoatsParser(xmlMessage); - boats = boatsParser.getBoats(); - } + 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; -// try { -// db = dbf.newDocumentBuilder(); -// Document doc = db.parse(new InputSource(new StringReader(xmlMessage))); -// // TODO: 25/04/17 ajm412: Check that the object matches expected structure and return Document object. -// } catch (ParserConfigurationException | IOException | SAXException e) { -// e.printStackTrace(); -// } + 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); } /** @@ -264,8 +247,8 @@ public class StreamParser extends Thread{ private static void extractRaceStartStatus(StreamPacket packet){ byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; - long timeStamp = extractTimeStamp(Arrays.copyOfRange(payload,1,7), 6); - long raceStartTime = extractTimeStamp(Arrays.copyOfRange(payload,9,15), 6); + 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]; } @@ -278,7 +261,7 @@ public class StreamParser extends Thread{ private static void extractYachtEventCode(StreamPacket packet){ byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; - long timeStamp = extractTimeStamp(Arrays.copyOfRange(payload,1,7), 6); + 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)); @@ -359,7 +342,7 @@ public class StreamParser extends Thread{ private static void extractMarkRounding(StreamPacket packet){ byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; - long timeStamp = extractTimeStamp(Arrays.copyOfRange(payload,1,7), 6); + 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]; @@ -376,7 +359,7 @@ public class StreamParser extends Thread{ ArrayList windInfo = new ArrayList<>(); for (int i = 0; i < loopCount; i++){ String wind = "WindId: " + payload[3 + (20 * i)]; - wind += "\nTime: " + extractTimeStamp(Arrays.copyOfRange(payload,4 + (20 * i),10 + (20 * i)), 6); + 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))); @@ -390,7 +373,7 @@ public class StreamParser extends Thread{ private static void extractAvgWind(StreamPacket packet){ byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; - long timeStamp = extractTimeStamp(Arrays.copyOfRange(payload,1,7), 6); + 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)); @@ -401,16 +384,6 @@ public class StreamParser extends Thread{ long speed4 = bytesToLong(Arrays.copyOfRange(payload,21,23)); } - private static long extractTimeStamp(byte[] timeStampBytes, int noOfBytes){ - long timeStamp = 0; - long multiplier=1; - for(int i = 0;i < noOfBytes;i++) { - timeStamp += timeStampBytes[i]*multiplier; - multiplier *= 256; - } - return timeStamp; - } - /** * takes an array of up to 7 bytes and returns a positive * long constructed from the input bytes @@ -474,5 +447,9 @@ public class StreamParser extends Thread{ public static List getBoats() { return boats; } + + public static XMLParser getXmlObject() { + return xmlObject; + } } diff --git a/src/main/java/seng302/models/parsers/XMLParser.java b/src/main/java/seng302/models/parsers/XMLParser.java index ef65ea38..a50cbcbc 100644 --- a/src/main/java/seng302/models/parsers/XMLParser.java +++ b/src/main/java/seng302/models/parsers/XMLParser.java @@ -9,35 +9,51 @@ 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. + * */ -class XMLParser { +public class XMLParser { - /** - * Creates a Regatta XML Object from the data in a Regatta XML Message - * @param doc XML Document Object - * @return A new RegattaXMLObject from the input Document. - */ - RegattaXMLObject createRegattaXML(Document doc) { - return new RegattaXMLObject(doc); + private Document xmlDoc; + + private RaceXMLObject raceXML; + private RegattaXMLObject regattaXML; + private BoatXMLObject boatXML; + + public XMLParser() { } /** - * Creates a Race XML Object from the data in a Regatta XML Message - * @param doc XML Document Object - * @return A new RaceXMLObject from the input Document. + * Constructor for XMLParser + * @param doc Document to create XML object. + * @param messageType Defines if a message is a RegattaXML(5), RaceXML(6), BoatXML(7). */ - RaceXMLObject createRaceXML(Document doc) { - return new RaceXMLObject(doc); + 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; + } } - /** - * Creates a Boat XML Object from the data in a Regatta XML Message - * @param doc XML Document Object - * @return A new BoatXMLObject from the input Document. - */ - BoatXMLObject createBoatXML(Document doc) { - return new BoatXMLObject(doc); - } + 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. @@ -129,7 +145,7 @@ class XMLParser { } } - class RegattaXMLObject { + public class RegattaXMLObject { //Regatta Info private Integer regattaID; private String regattaName; @@ -163,7 +179,7 @@ class XMLParser { } - class RaceXMLObject { + public class RaceXMLObject { // Race Info private Integer raceID; @@ -266,7 +282,7 @@ class XMLParser { public ArrayList getCompoundMarkSequence() { return compoundMarkSequence; } public ArrayList getCourseLimit() { return courseLimit; } - class Participant { + public class Participant { Integer sourceID; String entry; @@ -279,7 +295,7 @@ class XMLParser { public String getEntry() { return entry; } } - class CompoundMark { + public class CompoundMark { private Integer markID; private String cMarkName; private ArrayList marks; @@ -302,7 +318,7 @@ class XMLParser { public String getcMarkName() { return cMarkName; } public ArrayList getMarks() { return marks; } - class Mark { + public class Mark { private Integer seqID; private Integer sourceID; private String markName; @@ -310,13 +326,11 @@ class XMLParser { 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; } @@ -327,7 +341,7 @@ class XMLParser { } } - class Corner { + public class Corner { private Integer seqID; private Integer compoundMarkID; private String rounding; @@ -346,7 +360,7 @@ class XMLParser { public Integer getZoneSize() { return zoneSize; } } - class Limit { + public class Limit { private Integer seqID; private Double lat; private Double lng; @@ -364,7 +378,7 @@ class XMLParser { } - class BoatXMLObject { + public class BoatXMLObject { private String lastModified; private Integer version; @@ -429,7 +443,7 @@ class XMLParser { public ArrayList getZoneLimits() { return zoneLimits; } public ArrayList getBoats() { return boats; } - class Boat { + public class Boat { private String boatType; private Integer sourceID; 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 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/MainView.fxml b/src/main/resources/views/MainView.fxml index ebea61a2..cc38f3ed 100644 --- a/src/main/resources/views/MainView.fxml +++ b/src/main/resources/views/MainView.fxml @@ -9,18 +9,18 @@ - + - - - - - - - + + + + + + +