diff --git a/.gitignore b/.gitignore index 3c15f041..213db0db 100644 --- a/.gitignore +++ b/.gitignore @@ -180,3 +180,7 @@ local.properties .recommenders/ Makefile + +infer-out/ +infer.txt +log.log \ No newline at end of file diff --git a/.mailmap b/.mailmap index cad3a3a8..caf8a624 100644 --- a/.mailmap +++ b/.mailmap @@ -23,5 +23,5 @@ Haoming Yin Peter Galloway Peter Zhi You Tan zyt10 Zhi You Tan Ryan Tan -Alistair McIntyre alistairjmcintyre +Alistair McIntyre Calum cir27 \ No newline at end of file diff --git a/doc/design_decisions.md b/doc/design_decisions.md index 65e68153..0c1f6f00 100644 --- a/doc/design_decisions.md +++ b/doc/design_decisions.md @@ -9,7 +9,7 @@ prints out event details, including time, involved boats and legs. - Configuration file -We decided to store the team information including team names and boat velocity, as well as race configuration setting in external file. +We decided to store the team information including team names and boat currentVelocity, as well as race configuration setting in external file. To read external files, "Json-simple" library has been used to parse information. By using this library, we did not have to write our json parser and benefited from the flexibility of json files. diff --git a/doc/user_manual.md b/doc/user_manual.md index 0ca79deb..27f09fc3 100644 --- a/doc/user_manual.md +++ b/doc/user_manual.md @@ -8,7 +8,7 @@ You can specify a config file using the using the -f flag, for example 'java -ja ## The config file -The teams/boats are specified in the config file under 'teams', each team requires a team name, and a velocity (in meters per second). +The teams/boats are specified in the config file under 'teams', each team requires a team name, and a currentVelocity (in meters per second). The 'time-scale' option lets you change how long the race takes to complete. A time-scale of 1.0 is normal speed, 2.0 is 2x etc. diff --git a/pom.xml b/pom.xml index 79d2be71..fca06536 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,19 @@ test + + + info.cukes + cucumber-junit + 1.2.5 + + + + info.cukes + cucumber-java + 1.2.5 + + org.freemarker @@ -59,7 +72,6 @@ - diff --git a/src/main/java/seng302/App.java b/src/main/java/seng302/App.java index f4aad3df..8c6f85ac 100644 --- a/src/main/java/seng302/App.java +++ b/src/main/java/seng302/App.java @@ -7,14 +7,17 @@ import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.stage.Stage; -import org.apache.commons.cli.*; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import seng302.client.ClientPacketParser; -import seng302.client.ClientState; -import seng302.models.PolarTable; +import seng302.model.PolarTable; public class App extends Application { + private static Logger logger = LoggerFactory.getLogger(App.class); public static void parseArgs(String[] args) throws ParseException { @@ -22,15 +25,16 @@ public class App extends Application { CommandLineParser parser = new DefaultParser(); CommandLine cmd; - ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory + .getLogger(Logger.ROOT_LOGGER_NAME); options.addOption("debugLevel", true, "Set the application debug level"); cmd = parser.parse(options, args); - if (cmd.hasOption("debugLevel")){ + if (cmd.hasOption("debugLevel")) { - switch (cmd.getOptionValue("debugLevel")){ + switch (cmd.getOptionValue("debugLevel")) { case "DEBUG": rootLogger.setLevel(Level.DEBUG); break; @@ -56,31 +60,32 @@ public class App extends Application { default: rootLogger.setLevel(Level.ALL); } - } else{ + } else { rootLogger.setLevel(Level.WARN); } } @Override public void start(Stage primaryStage) throws Exception { - PolarTable.parsePolarFile(getClass().getResourceAsStream("/config/acc_polars.csv")); - - Parent root = FXMLLoader.load(getClass().getResource("/views/MainView.fxml")); + Parent root = FXMLLoader.load(getClass().getResource("/views/StartScreenView.fxml")); primaryStage.setTitle("RaceVision"); - primaryStage.setScene(new Scene(root, 1530, 960)); - primaryStage.setMaxWidth(1530); - primaryStage.setMaxHeight(960); + Scene scene = new Scene(root, 1530, 960); + scene.getStylesheets().add(getClass().getResource("/css/master.css").toString()); + primaryStage.setScene(scene); +// primaryStage.setMaxWidth(1530); +// primaryStage.setMaxHeight(960); primaryStage.getIcons().add(new Image(getClass().getResourceAsStream("/PP.png"))); // primaryStage.setMaximized(true); primaryStage.show(); primaryStage.setOnCloseRequest(e -> { - ClientPacketParser.appClose(); +// ClientPacketParser.appClose(); +// ClientPacketParser.appClose(); System.exit(0); }); - ClientState.primaryStage = primaryStage; +// ClientState.primaryStage = primaryStage; } public static void main(String[] args) { diff --git a/src/main/java/seng302/client/ClientPacketParser.java b/src/main/java/seng302/client/ClientPacketParser.java deleted file mode 100644 index bf58c4a1..00000000 --- a/src/main/java/seng302/client/ClientPacketParser.java +++ /dev/null @@ -1,638 +0,0 @@ -package seng302.client; - - -import java.io.IOException; -import java.io.StringReader; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Date; -import java.util.Map; -import java.util.TimeZone; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentSkipListMap; -import java.util.concurrent.PriorityBlockingQueue; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import org.w3c.dom.Document; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import seng302.models.Yacht; -import seng302.models.mark.Mark; -import seng302.models.stream.XMLParser; -import seng302.models.stream.packets.BoatPositionPacket; -import seng302.models.stream.packets.StreamPacket; - -/** - * The purpose of this class is to take in the stream of divided packets so they can be read - * and parsed in by turning the byte arrays into useful data. There are two public static hashmaps - * that are threadsafe so the visualiser can always access the latest speed and position available - * Created by kre39 on 23/04/17. - */ -public class ClientPacketParser { - - public static ConcurrentHashMap> markLocations = new ConcurrentHashMap<>(); - public static ConcurrentHashMap> boatLocations = new ConcurrentHashMap<>(); - private static boolean newRaceXmlReceived = false; - private static boolean raceStarted = false; - private static XMLParser xmlObject = new XMLParser(); - private static boolean raceFinished = false; - private static boolean streamStatus = false; - private static long timeSinceStart = -1; - private static Map boats = new ConcurrentHashMap<>(); - private static Map boatsPos = new ConcurrentSkipListMap<>(); - private static double windDirection = 0; - private static Double windSpeed = 0d; - private static Long currentTimeLong; - private static String currentTimeString; - private static boolean appRunning; - private static Map clientStateBoats = new ConcurrentHashMap<>(); - - //CONVERSION CONSTANTS - public static final Double MS_TO_KNOTS = 1.94384; - - /** - * Used to initialise the thread name and stream parser object so a thread can be executed - */ - public ClientPacketParser() { - } - - /** - * Looks at the type of the packet then sends it to the appropriate parser to extract the - * specific data associated with that packet type - * - * @param packet the packet to be looked at and processed - */ - public static void parsePacket(StreamPacket packet) { - try { - switch (packet.getType()) { - case HEARTBEAT: - extractHeartBeat(packet); - break; - case RACE_STATUS: - extractRaceStatus(packet); - break; - case DISPLAY_TEXT_MESSAGE: - extractDisplayMessage(packet); - break; - case XML_MESSAGE: - extractXmlMessage(packet); - break; - case RACE_START_STATUS: - extractRaceStartStatus(packet); - break; - case YACHT_EVENT_CODE: - extractYachtEventCode(packet); - break; - case YACHT_ACTION_CODE: - extractYachtActionCode(packet); - break; - case CHATTER_TEXT: - extractChatterText(packet); - break; - case BOAT_LOCATION: - extractBoatLocation(packet); - break; - case MARK_ROUNDING: - extractMarkRounding(packet); - break; - case COURSE_WIND: - extractCourseWind(packet); - break; - case AVG_WIND: - extractAvgWind(packet); - break; - } - } catch (NullPointerException e) { - System.out.println("Error parsing packet"); - } - } - - /** - * Extracts the seq num used in the heartbeat packet - * - * @param packet Packet parsed in to use the payload - */ - private static void extractHeartBeat(StreamPacket packet) { - long heartbeat = bytesToLong(packet.getPayload()); - } - - private static String getTimeZoneString() { - - Integer offset = xmlObject.getRegattaXML().getUtcOffset(); - StringBuilder utcOffset = new StringBuilder(); - utcOffset.append("GMT"); - if (offset > 0) { - utcOffset.append("+"); - utcOffset.append(offset); - } else if (offset < 0) { - utcOffset.append("-"); - utcOffset.append(offset); - } - return utcOffset.toString(); - - } - - /** - * Extracts the useful race status data from race status type packets. This method will also - * print to the console the current state of the race (if it has started/finished or is about to - * start), along side this it'll also display the amount of time since the race has started or - * time till it starts - * - * @param packet Packet parsed in to use the payload - */ - private static void extractRaceStatus(StreamPacket packet) { - byte[] payload = packet.getPayload(); - int messageVersionNo = payload[0]; - long currentTime = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); - long raceId = bytesToLong(Arrays.copyOfRange(payload, 7, 11)); - int raceStatus = payload[11]; - long expectedStartTime = bytesToLong(Arrays.copyOfRange(payload,12,18)); - long windDir = bytesToLong(Arrays.copyOfRange(payload,18,20)); - long rawWindSpeed = bytesToLong(Arrays.copyOfRange(payload,20,22)); - - currentTimeLong = currentTime; - DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); - if (xmlObject.getRegattaXML() != null) { - format.setTimeZone(TimeZone.getTimeZone(getTimeZoneString())); - currentTimeString = format.format((new Date(currentTime)).getTime()); - } - long timeTillStart = - ((new Date(expectedStartTime)).getTime() - (new Date(currentTime)).getTime()) / 1000; - - if (timeTillStart > 0) { - timeSinceStart = timeTillStart; - } else { - if (raceStatus == 4 || raceStatus == 8) { - raceFinished = true; - raceStarted = false; - ClientState.setRaceStarted(false); - } else if (!raceStarted) { - raceStarted = true; - ClientState.setRaceStarted(true); - raceFinished = false; - } - timeSinceStart = timeTillStart; - } - - double windDirFactor = 0x4000 / 90; //0x4000 is 90 degrees, 0x8000 is 180 degrees, etc... - windDirection = windDir / windDirFactor; - windSpeed = rawWindSpeed / 1000 * MS_TO_KNOTS; - - int noBoats = payload[22]; - int raceType = payload[23]; - for (int i = 0; i < noBoats; i++) { - long boatStatusSourceID = bytesToLong( - Arrays.copyOfRange(payload, 24 + (i * 20), 28 + (i * 20))); - int boatStatus = (int) payload[28 + (i * 20)]; - int boatLegNumber = (int) payload[29 + (i * 20)]; - int boatPenaltyAwarded = (int) payload[30 + (i * 20)]; - int boatPenaltyServed = (int) payload[31 + (i * 20)]; - long estTimeAtNextMark = bytesToLong( - Arrays.copyOfRange(payload, 32 + (i * 20), 38 + (i * 20))); - long estTimeAtFinish = bytesToLong( - Arrays.copyOfRange(payload, 38 + (i * 20), 44 + (i * 20))); - - Yacht boat = boats.get((int) boatStatusSourceID); - boat.setBoatStatus((boatStatus)); - setBoatLegPosition(boat, boatLegNumber); - boat.setPenaltiesAwarded(boatPenaltyAwarded); - boat.setPenaltiesServed(boatPenaltyServed); - boat.setEstimateTimeAtNextMark(estTimeAtNextMark); - boat.setEstimateTimeAtFinish(estTimeAtFinish); - - // Update Client State boats when receive race status packet. - // Potentially could replace boats in ClientPacketParser. - Yacht clientBoat = ClientState.getBoats().get((int) boatStatusSourceID); - clientBoat.setBoatStatus((boatStatus)); - setBoatLegPosition(clientBoat, boatLegNumber); - clientBoat.setPenaltiesAwarded(boatPenaltyAwarded); - clientBoat.setPenaltiesServed(boatPenaltyServed); - clientBoat.setEstimateTimeAtNextMark(estTimeAtNextMark); - clientBoat.setEstimateTimeAtFinish(estTimeAtFinish); - } - - // 3 is race started. - // ClientState race started flag will be set to true if race started, else set false. - if (raceStatus == 3) { - ClientState.setRaceStarted(true); - } else { - ClientState.setRaceStarted(false); - } - } - - private static void setBoatLegPosition(Yacht updatingBoat, Integer leg){ - Integer placing = 1; - - if (/* TODO implement when we are getting this data /TODO leg != updatingBoat.getLegNumber() && */(raceStarted || raceFinished)) { - for (Yacht boat : boats.values()) { - placing = boat.getSourceId(); - /* See above to-do - if (boat.getLegNumber() != null && leg <= boat.getLegNumber()){ - placing += 1; - }*/ - } - updatingBoat.setPosition(placing.toString()); - updatingBoat.setLegNumber(leg); - boatsPos.putIfAbsent(placing, updatingBoat); - boatsPos.replace(placing, updatingBoat); - } else if(updatingBoat.getLegNumber() == null){ - updatingBoat.setPosition("-"); - updatingBoat.setLegNumber(leg); - } - } - - /** - * Used to extract the messages passed through with the display message packet - * - * @param packet Packet parsed in to use the payload - */ - private static void extractDisplayMessage(StreamPacket packet) { - byte[] payload = packet.getPayload(); - int messageVersionNo = payload[0]; - int numOfLines = payload[3]; - int totalLen = 0; - for (int i = 0; i < numOfLines; i++) { - int lineNum = payload[4 + totalLen]; - int textLength = payload[5 + totalLen]; - byte[] messageTextBytes = Arrays - .copyOfRange(payload, 6 + totalLen, 6 + textLength + totalLen); - String messageText = new String(messageTextBytes); - totalLen += 2 + textLength; - } - } - - /** - * Used to read in the xml data. Will call the specific methods to create the course and boats - * - * @param packet Packet parsed in to use the payload - */ - private static void extractXmlMessage(StreamPacket packet) { - xmlObject = new XMLParser(); - byte[] payload = packet.getPayload(); - int messageType = payload[9]; - long messageLength = bytesToLong(Arrays.copyOfRange(payload, 12, 14)); - String xmlMessage = new String( - (Arrays.copyOfRange(payload, 14, (int) (14 + messageLength)))).trim(); - - //Create XML document Object - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - DocumentBuilder db = null; - Document doc = null; - try { - db = dbf.newDocumentBuilder(); - doc = db.parse(new InputSource(new StringReader(xmlMessage))); - } catch (ParserConfigurationException | IOException | SAXException e) { - System.out.println("[ClientPacketParser] ParserConfigurationException | IOException | SAXException"); - } - - xmlObject.constructXML(doc, messageType); - - if (messageType == 7) { //7 is the boat XML - boats = xmlObject.getBoatXML().getCompetingBoats(); - // Set/Update the ClientState boats after receiving new boat xml. - // Flag boatsUpdated in ClientState to true. - ClientState.setBoats(xmlObject.getBoatXML().getCompetingBoats()); - ClientState.setBoatsUpdated(true); - } - if (messageType == 6) { //6 is race info xml - newRaceXmlReceived = true; - } - } - - /** - * Extracts the race start status from the packet, currently is unused within the app but - * is here for potential future use - * - * @param packet Packet parsed in to use the payload - */ - private static void extractRaceStartStatus(StreamPacket packet) { - byte[] payload = packet.getPayload(); - int messageVersionNo = payload[0]; - long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); - long raceStartTime = bytesToLong(Arrays.copyOfRange(payload, 9, 15)); - long raceId = bytesToLong(Arrays.copyOfRange(payload, 15, 19)); - int notificationType = payload[19]; - } - - /** - * When a yacht event occurs this will parse the byte array to retrieve the necessary info, - * currently unused - * - * @param packet Packet parsed in to use the payload - */ - private static void extractYachtEventCode(StreamPacket packet) { - byte[] payload = packet.getPayload(); - int messageVersionNo = payload[0]; - long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); - long raceId = bytesToLong(Arrays.copyOfRange(payload, 9, 13)); - long subjectId = bytesToLong(Arrays.copyOfRange(payload, 13, 17)); - long incidentId = bytesToLong(Arrays.copyOfRange(payload, 17, 21)); - int eventId = payload[21]; - } - - /** - * When a yacht action occurs this will parse the parse the byte array to retrieve the necessary - * info, currently unused - * - * @param packet Packet parsed in to use the payload - */ - private static void extractYachtActionCode(StreamPacket packet) { - byte[] payload = packet.getPayload(); - int messageVersionNo = payload[0]; - long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); - long subjectId = bytesToLong(Arrays.copyOfRange(payload, 9, 13)); - long incidentId = bytesToLong(Arrays.copyOfRange(payload, 13, 17)); - int eventId = payload[17]; - } - - /** - * Strips the message from the chatter text type packets, currently the message is unused - * - * @param packet Packet parsed in to use the payload - */ - private static void extractChatterText(StreamPacket packet) { - byte[] payload = packet.getPayload(); - int messageVersionNo = payload[0]; - int messageType = payload[1]; - int length = payload[2]; - String message = new String(Arrays.copyOfRange(payload, 3, 3 + length)); - System.out.println(message); - } - - /** - * Used to breakdown the boatlocation packets so the boat coordinates, id and groundspeed are - * all used All the other extra data is still being read and translated however is unused. - * - * @param packet Packet parsed in to use the payload - */ - private static void extractBoatLocation(StreamPacket packet) { - byte[] payload = packet.getPayload(); - - int deviceType = (int) payload[15]; - long timeValid = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); - long seq = bytesToLong(Arrays.copyOfRange(payload, 11, 15)); - long boatId = bytesToLong(Arrays.copyOfRange(payload, 7, 11)); - long rawLat = bytesToLong(Arrays.copyOfRange(payload, 16, 20)); - long rawLon = bytesToLong(Arrays.copyOfRange(payload, 20, 24)); - //Converts the double to a usable lat/lon - double lat = ((180d * (double) rawLat) / Math.pow(2, 31)); - double lon = ((180d * (double) rawLon) / Math.pow(2, 31)); -// System.out.println("[CLIENT] Lat: " + lat + " Lon: " + lon); - long heading = bytesToLong(Arrays.copyOfRange(payload, 28, 30)); - double groundSpeed = bytesToLong(Arrays.copyOfRange(payload, 38, 40)) / 1000.0; - //type 1 is a racing yacht and type 3 is a mark, needed for updating positions of the mark and boat - if (deviceType == 1){ - Yacht boat = boats.get((int) boatId); - boat.setVelocity(groundSpeed); - BoatPositionPacket boatPacket = new BoatPositionPacket(boatId, timeValid, lat, lon, heading, groundSpeed); - - //add a new priority que to the boatLocations HashMap - if (!boatLocations.containsKey(boatId)) { - boatLocations.put(boatId, - new PriorityBlockingQueue<>(256, new Comparator() { - @Override - public int compare(BoatPositionPacket p1, BoatPositionPacket p2) { - return (int) (p1.getTimeValid() - p2.getTimeValid()); - } - })); - } - boatLocations.get(boatId).put(boatPacket); - } else if (deviceType == 3) { - BoatPositionPacket markPacket = new BoatPositionPacket(boatId, timeValid, lat, lon, - heading, groundSpeed); - - //add a new priority que to the boatLocations HashMap - if (!markLocations.containsKey(boatId)) { - markLocations.put(boatId, - new PriorityBlockingQueue<>(256, new Comparator() { - @Override - public int compare(BoatPositionPacket p1, BoatPositionPacket p2) { - return (int) (p1.getTimeValid() - p2.getTimeValid()); - } - })); - } - markLocations.get(boatId).put(markPacket); - } - } - - /** - * This packet type is received when a mark or gate is rounded by a boat - * - * @param packet The packet containing the payload - */ - private static void extractMarkRounding(StreamPacket packet) { - byte[] payload = packet.getPayload(); - int messageVersionNo = payload[0]; - long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); - long raceId = bytesToLong(Arrays.copyOfRange(payload, 9, 13)); - long subjectId = bytesToLong(Arrays.copyOfRange(payload, 13, 17)); - int boatStatus = payload[17]; - int roundingSide = payload[18]; - int markType = payload[19]; - int markId = payload[20]; - - // assign mark rounding time to boat - boats.get((int)subjectId).setMarkRoundingTime(timeStamp); - - for (Mark mark : xmlObject.getRaceXML().getAllCompoundMarks()) { - if (mark.getCompoundMarkID() == markId) { - boats.get((int)subjectId).setLastMarkRounded(mark); - } - } - } - - /** - * This packet type contains periodic data on the state of the wind - * - * @param packet The packet containing the payload - */ - private static void extractCourseWind(StreamPacket packet) { - byte[] payload = packet.getPayload(); - int messageVersionNo = payload[0]; - int selectedWindId = payload[1]; - int loopCount = payload[2]; - ArrayList windInfo = new ArrayList<>(); - for (int i = 0; i < loopCount; i++) { - String wind = "WindId: " + payload[3 + (20 * i)]; - wind += - "\nTime: " + bytesToLong(Arrays.copyOfRange(payload, 4 + (20 * i), 10 + (20 * i))); - wind += "\nRaceId: " + bytesToLong( - Arrays.copyOfRange(payload, 10 + (20 * i), 14 + (20 * i))); - wind += "\nWindDirection: " + bytesToLong( - Arrays.copyOfRange(payload, 14 + (20 * i), 16 + (20 * i))); - wind += "\nWindSpeed: " + bytesToLong( - Arrays.copyOfRange(payload, 16 + (20 * i), 18 + (20 * i))); - wind += "\nBestUpWindAngle: " + bytesToLong( - Arrays.copyOfRange(payload, 18 + (20 * i), 20 + (20 * i))); - wind += "\nBestDownWindAngle: " + bytesToLong( - Arrays.copyOfRange(payload, 20 + (20 * i), 22 + (20 * i))); - wind += "\nFlags: " + String - .format("%8s", Integer.toBinaryString(payload[22 + (20 * i)] & 0xFF)) - .replace(' ', '0'); - windInfo.add(wind); - } - } - - /** - * This packet conatins the average wind to ground speed - * - * @param packet The packet containing the paylaod - */ - private static void extractAvgWind(StreamPacket packet) { - byte[] payload = packet.getPayload(); - int messageVersionNo = payload[0]; - long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); - long rawPeriod = bytesToLong(Arrays.copyOfRange(payload, 7, 9)); - long rawSamplePeriod = bytesToLong(Arrays.copyOfRange(payload, 9, 11)); - long period2 = bytesToLong(Arrays.copyOfRange(payload, 11, 13)); - long speed2 = bytesToLong(Arrays.copyOfRange(payload, 13, 15)); - long period3 = bytesToLong(Arrays.copyOfRange(payload, 15, 17)); - long speed3 = bytesToLong(Arrays.copyOfRange(payload, 17, 19)); - long period4 = bytesToLong(Arrays.copyOfRange(payload, 19, 21)); - long speed4 = bytesToLong(Arrays.copyOfRange(payload, 21, 23)); - } - - /** - * takes an array of up to 7 bytes and returns a positive - * long constructed from the input bytes - * - * @return a positive long if there is less than 7 bytes -1 otherwise - */ - private static long bytesToLong(byte[] bytes) { - long partialLong = 0; - int index = 0; - for (byte b : bytes) { - if (index > 6) { - return -1; - } - partialLong = partialLong | (b & 0xFFL) << (index * 8); - index++; - } - return partialLong; - } - - /** - * returns false if race not started, true otherwise - * - * @return race started status - */ - public static boolean isRaceStarted() { - return raceStarted; - } - - /** - * returns false if stream not connected, true otherwise - * - * @return stream started status - */ - public static boolean isStreamStatus() { - return streamStatus; - } - - /** - * returns race timer - * - * @return race timer in long - */ - public static long getTimeSinceStart() { - return timeSinceStart; - } - - /** - * return false if race not finished, true otherwise - * - * @return race finished status - */ - public static boolean isRaceFinished() { - return raceFinished; - } - - /** - * return a map of boats with sourceID and the boat - * - * @return map of boats - */ - public static Map getBoats() { - return boats; - } - - - /** - * returns the latest updated object from xml parser - * - * @return the latest xml object - */ - public static XMLParser getXmlObject() { - return xmlObject; - } - - /** - * returns the wind direction in degrees - * - * @return a double wind direction value - */ - public static double getWindDirection() { - return windDirection; - } - - - /** - * Returns the wind speed in knots - * @return A double indicating the wind speed in knots - */ - public static Double getWindSpeed() { - return windSpeed; - } - - /** - * returns stream time in formatted string format - * - * @return String of stream time - */ - public static String getCurrentTimeString() { - return currentTimeString; - } - - /** - * used in boat position since tree map can sort position efficiently. - * - * @return a map of time to finish and boat. - */ - public static Map getBoatsPos() { - - return boatsPos; - } - - /** - * returns current time in stream in long - * - * @return a long value of current time - */ - public static Long getCurrentTimeLong() { - return currentTimeLong; - } - - public static void appClose() { - appRunning = false; - } - - /** - * Used to check if a new un-processed xml has been found, if so will return true before - * toggling off so that the next check will return false. - * - * @return the status of if new xml has been received - */ - public static boolean isNewRaceXmlReceived() { - if (newRaceXmlReceived) { - newRaceXmlReceived = false; - return true; - } else { - return false; - } - } -} - diff --git a/src/main/java/seng302/client/ClientState.java b/src/main/java/seng302/client/ClientState.java deleted file mode 100644 index 40656dbe..00000000 --- a/src/main/java/seng302/client/ClientState.java +++ /dev/null @@ -1,78 +0,0 @@ -package seng302.client; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import javafx.stage.Stage; -import seng302.models.Yacht; - -/** - * Used by the client to store static variables, which other threads and classes - * observer so that they can update their status accordingly. - */ -public class ClientState { - - private static String hostIp = ""; - private static Boolean isHost = false; - private static Boolean raceStarted = false; - private static Boolean connectedToHost = false; - private static Map boats = new ConcurrentHashMap<>(); - private static Boolean boatsUpdated = true; - private static String clientSourceId = ""; - public static Stage primaryStage; - - public static String getHostIp() { - return hostIp; - } - - public static void setHostIp(String hostIp) { - ClientState.hostIp = hostIp; - } - - public static Boolean isHost() { - return isHost; - } - - public static void setHost(Boolean isHost) { - ClientState.isHost = isHost; - } - - public static Boolean isRaceStarted() { - return raceStarted; - } - - public static void setRaceStarted(Boolean raceStarted) { - ClientState.raceStarted = raceStarted; - } - - public static Boolean isConnectedToHost() { - return connectedToHost; - } - - public static void setConnectedToHost(Boolean connectedToHost) { - ClientState.connectedToHost = connectedToHost; - } - - public static Map getBoats() { - return boats; - } - - public static Boolean isBoatsUpdated() { - return boatsUpdated; - } - - public static void setBoatsUpdated(Boolean boatsUpdated) { - ClientState.boatsUpdated = boatsUpdated; - } - - public static String getClientSourceId() { - return clientSourceId; - } - - public static void setClientSourceId(String clientSourceId) { - ClientState.clientSourceId = clientSourceId; - } - - public static void setBoats(Map boats) { - ClientState.boats = boats; - } -} diff --git a/src/main/java/seng302/client/ClientStateQueryingRunnable.java b/src/main/java/seng302/client/ClientStateQueryingRunnable.java deleted file mode 100644 index e704819d..00000000 --- a/src/main/java/seng302/client/ClientStateQueryingRunnable.java +++ /dev/null @@ -1,54 +0,0 @@ -package seng302.client; - -import java.util.Observable; - -/** - * Used by LobbyController to run a separate thread-loop - * updates the controller when change is detected. - */ -public class ClientStateQueryingRunnable extends Observable implements Runnable { - - private Boolean terminate = false; - - public ClientStateQueryingRunnable() {} - - /** - * Notifies observers(the lobby controller) that "game started" if ClientState - * raceStarted flag is true and terminates itself. Also, it notifies observers - * to add/remove players if ClientState boatsUpdated flag is true, then resets - * the flag to false; - */ - @Override - public void run() { - while(!terminate) { - // Sleeping the thread so it will respond to the if statement below - // if you know a better fix, pls tell me :) -ryan - try { - Thread.sleep(0); - } catch (InterruptedException e) { - continue; - } - - if (ClientState.isRaceStarted() && ClientState.isConnectedToHost()) { - setChanged(); - notifyObservers("game started"); - terminate(); - } - - if (ClientState.isBoatsUpdated()) { - setChanged(); - notifyObservers("update players"); - ClientState.setBoatsUpdated(false); - } - } - } - - /** - * Used to terminate the thread. - * - * Currently called by the main while loop when game started is detected. - */ - public void terminate() { - terminate = true; - } -} diff --git a/src/main/java/seng302/client/ClientToServerThread.java b/src/main/java/seng302/client/ClientToServerThread.java deleted file mode 100644 index 27dbcd1f..00000000 --- a/src/main/java/seng302/client/ClientToServerThread.java +++ /dev/null @@ -1,216 +0,0 @@ -package seng302.client; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; -import java.net.UnknownHostException; -import java.time.LocalDateTime; -import java.util.zip.CRC32; -import java.util.zip.Checksum; - -import javafx.application.Platform; -import javafx.scene.control.Alert; -import javafx.scene.control.Alert.AlertType; -import seng302.models.stream.packets.StreamPacket; -import seng302.server.messages.BoatActionMessage; -import seng302.server.messages.Message; - -/** - * A class describing a single connection to a Server for the purposes of sending and receiving on - * its own thread. - */ -public class ClientToServerThread implements Runnable { - - private static final int LOG_LEVEL = 1; - - private Thread thread; - - private Integer ourID; - - private Socket socket; - private InputStream is; - private OutputStream os; - - private Boolean updateClient = true; - private ByteArrayOutputStream crcBuffer; - - /** - * Constructor for ClientToServerThread which takes in ipAddress and portNumber and attempts to - * connect to the specified ipAddress and port. - * - * Upon successful socket connection, threeWayHandshake will be preformed and the instance will - * be put on a thread and run immediately. - * - * @param ipAddress a string of ip address to be connected to - * @param portNumber an integer port number - * @throws Exception SocketConnection if fail to connect to ip address and port number - * combination - */ - public ClientToServerThread(String ipAddress, Integer portNumber) throws Exception { - socket = new Socket(ipAddress, portNumber); - is = socket.getInputStream(); - os = socket.getOutputStream(); - - Integer allocatedID = threeWayHandshake(); - if (allocatedID != null) { - ourID = allocatedID; - clientLog("Successful handshake. Allocated ID: " + ourID, 0); - ClientState.setClientSourceId(String.valueOf(ourID)); - } else { - clientLog("Unsuccessful handshake", 1); - closeSocket(); - return; - } - - thread = new Thread(this); - thread.start(); - } - - /** - * Prints out log messages and the time happened. - * Only perform task if log level is below LOG_LEVEL variable. - * - * @param message a string of message to be printed out - * @param logLevel an int for log level - */ - static void clientLog(String message, int logLevel) { - if (logLevel <= LOG_LEVEL) { - System.out.println( - "[CLIENT " + LocalDateTime.now().toLocalTime().toString() + "] " + message); - } - } - - /** - * Perform the thread loop. It exits the loop if ClientState connected to host - * variable is false. - */ - public void run() { - int sync1; - int sync2; - // TODO: 14/07/17 wmu16 - Work out how to fix this while loop - while (ClientState.isConnectedToHost()) { - try { - crcBuffer = new ByteArrayOutputStream(); - sync1 = readByte(); - sync2 = readByte(); - //checking if it is the start of the packet - if (sync1 == 0x47 && sync2 == 0x83) { - int type = readByte(); - //No. of milliseconds since Jan 1st 1970 - long timeStamp = Message.bytesToLong(getBytes(6)); - skipBytes(4); - long payloadLength = Message.bytesToLong(getBytes(2)); - byte[] payload = getBytes((int) payloadLength); - Checksum checksum = new CRC32(); - checksum.update(crcBuffer.toByteArray(), 0, crcBuffer.size()); - long computedCrc = checksum.getValue(); - long packetCrc = Message.bytesToLong(getBytes(4)); - if (computedCrc == packetCrc) { - ClientPacketParser - .parsePacket(new StreamPacket(type, payloadLength, timeStamp, payload)); - } else { - clientLog("Packet has been dropped", 1); - } - } - } catch (Exception e) { - closeSocket(); - Platform.runLater(new Runnable() { - @Override - public void run() { - Alert alert = new Alert(AlertType.ERROR); - alert.setHeaderText("Host has disconnected"); - alert.setContentText("Cannot find Server"); - alert.showAndWait(); - } - }); - clientLog("Disconnected from server", 1); - return; - } - } - closeSocket(); - clientLog("Disconnected from server", 0); - } - - - /** - * Listens for an allocated sourceID and returns it to the server - * - * @return the sourceID allocated to us by the server - */ - private Integer threeWayHandshake() { - Integer ourSourceID = null; - while (true) { - try { - ourSourceID = is.read(); - } catch (IOException e) { - clientLog("Three way handshake failed", 1); - - } - if (ourSourceID != null) { - try { - os.write(ourSourceID); - return ourSourceID; - } catch (IOException e) { - clientLog("Three way handshake failed", 1); - return null; - } - } - } - } - - - /** - * Send the post-start race course information - */ - public void sendBoatActionMessage(BoatActionMessage boatActionMessage) { - try { - os.write(boatActionMessage.getBuffer()); - } catch (IOException e) { - clientLog("Could not write to server", 1); - } - } - - - public void closeSocket() { - try { - socket.close(); - } catch (IOException e) { - clientLog("Failed to close the socket", 1); - } - } - - - private int readByte() throws Exception { - int currentByte = -1; - try { - currentByte = is.read(); - crcBuffer.write(currentByte); - } catch (IOException e) { - clientLog("Read byte failed", 1); - } - if (currentByte == -1) { - throw new Exception(); - } - return currentByte; - } - - private byte[] getBytes(int n) throws Exception { - byte[] bytes = new byte[n]; - for (int i = 0; i < n; i++) { - bytes[i] = (byte) readByte(); - } - return bytes; - } - - private void skipBytes(long n) throws Exception { - for (int i = 0; i < n; i++) { - readByte(); - } - } - - public Thread getThread() { - return thread; - } -} diff --git a/src/main/java/seng302/controllers/CanvasController.java b/src/main/java/seng302/controllers/CanvasController.java deleted file mode 100644 index 4e8a8493..00000000 --- a/src/main/java/seng302/controllers/CanvasController.java +++ /dev/null @@ -1,597 +0,0 @@ -package seng302.controllers; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.PriorityBlockingQueue; -import javafx.animation.AnimationTimer; -import javafx.beans.property.SimpleDoubleProperty; -import javafx.fxml.FXML; -import javafx.fxml.FXMLLoader; -import javafx.geometry.Point2D; -import javafx.scene.Group; -import javafx.scene.canvas.Canvas; -import javafx.scene.canvas.GraphicsContext; -import javafx.scene.image.ImageView; -import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.GridPane; -import javafx.scene.layout.Pane; -import javafx.scene.paint.Color; -import javafx.scene.shape.Polygon; -import javafx.scene.text.Text; -import seng302.client.ClientPacketParser; -import seng302.client.ClientState; -import seng302.fxObjects.BoatGroup; -import seng302.models.Colors; -import seng302.models.Yacht; -import seng302.models.mark.GateMark; -import seng302.models.mark.Mark; -import seng302.fxObjects.MarkGroup; -import seng302.models.mark.MarkType; -import seng302.models.mark.SingleMark; -import seng302.models.map.Boundary; -import seng302.models.map.CanvasMap; -import seng302.models.stream.XMLParser; -import seng302.models.stream.XMLParser.RaceXMLObject.Limit; -import seng302.models.stream.XMLParser.RaceXMLObject.Participant; -import seng302.models.stream.packets.BoatPositionPacket; -import seng302.utilities.GeoPoint; -import seng302.utilities.GeoUtility; - -/** - * Created by ptg19 on 15/03/17. - * Modified by Haoming Yin (hyi25) on 20/3/2017. - */ -public class CanvasController { - - @FXML - private AnchorPane canvasPane; - - private RaceViewController raceViewController; - private ResizableCanvas canvas; - private Group group; - private GraphicsContext gc; - private ImageView mapImage; - - private final int BUFFER_SIZE = 50; - private final int PANEL_WIDTH = 1260; // it should be 1280 but, minors 40 to cancel the bias. - private final int PANEL_HEIGHT = 960; - private final int CANVAS_WIDTH = 1100; - private final int CANVAS_HEIGHT = 920; - private boolean horizontalInversion = false; - - private double distanceScaleFactor; - private ScaleDirection scaleDirection; - private Mark minLatPoint; - private Mark minLonPoint; - private Mark maxLatPoint; - private Mark maxLonPoint; - private double referencePointX; - private double referencePointY; - private double metersPerPixelX; - private double metersPerPixelY; - - private List markGroups = new ArrayList<>(); - private List boatGroups = new ArrayList<>(); - private Text FPSDisplay = new Text(); - private Polygon raceBorder = new Polygon(); - - //FRAME RATE - private Double frameRate = 60.0; - private final long[] frameTimes = new long[30]; - private int frameTimeIndex = 0; - private boolean arrayFilled = false; - - public AnimationTimer timer; - - private enum ScaleDirection { - HORIZONTAL, - VERTICAL - } - - public void setup(RaceViewController raceViewController) { - this.raceViewController = raceViewController; - } - - public void initialize() { - raceViewController = new RaceViewController(); - canvas = new ResizableCanvas(); - group = new Group(); - - // create image view for map, bind panel size to image - mapImage = new ImageView(); - canvasPane.getChildren().add(mapImage); - mapImage.fitWidthProperty().bind(canvasPane.widthProperty()); - mapImage.fitHeightProperty().bind(canvasPane.heightProperty()); - - canvasPane.getChildren().add(canvas); - canvasPane.getChildren().add(group); - // Bind canvas size to stack pane size. - canvas.widthProperty().bind(new SimpleDoubleProperty(CANVAS_WIDTH)); - canvas.heightProperty().bind(new SimpleDoubleProperty(CANVAS_HEIGHT)); - - } - - public void initializeCanvas() { - - gc = canvas.getGraphicsContext2D(); - gc.setGlobalAlpha(0.5); - fitMarksToCanvas(); - drawGoogleMap(); - FPSDisplay.setLayoutX(5); - FPSDisplay.setLayoutY(20); - FPSDisplay.setStrokeWidth(2); - group.getChildren().add(FPSDisplay); - group.getChildren().add(raceBorder); - initializeMarks(); - initializeBoats(); - - timer = new AnimationTimer() { - private long lastTime = 0; - private int FPSCount = 30; - - @Override - public void handle(long now) { - if (lastTime == 0) { - lastTime = now; - } else { - if (now - lastTime >= (1e8 / 60)) { //Fix for framerate going above 60 when minimized - long oldFrameTime = frameTimes[frameTimeIndex]; - frameTimes[frameTimeIndex] = now; - frameTimeIndex = (frameTimeIndex + 1) % frameTimes.length; - if (frameTimeIndex == 0) { - arrayFilled = true; - } - long elapsedNanos; - if (arrayFilled) { - elapsedNanos = now - oldFrameTime; - long elapsedNanosPerFrame = elapsedNanos / frameTimes.length; - frameRate = 1_000_000_000.0 / elapsedNanosPerFrame; - if (FPSCount-- == 0) { - FPSCount = 30; - drawFps(frameRate.intValue()); - } - raceViewController.updateSparkLine(); - } - updateGroups(); - if (ClientPacketParser.isRaceFinished()) { - this.stop(); - } - lastTime = now; - } - } - if (ClientPacketParser.isRaceFinished()) { - this.stop(); - switchToFinishScreen(); - } - } - }; - } - - private void switchToFinishScreen() { - try { - // canvas view -> anchor pane -> grid pane -> main view - GridPane gridPane = (GridPane) canvasPane.getParent().getParent(); - AnchorPane contentPane = (AnchorPane) gridPane.getParent(); - contentPane.getChildren().removeAll(); - contentPane.getChildren().clear(); - contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString()); - contentPane.getChildren().addAll( - (Pane) FXMLLoader.load(getClass().getResource("/views/FinishScreenView.fxml"))); - } catch (javafx.fxml.LoadException e) { - System.out.println("[Controller] FXML load exception"); - } catch (IOException e) { - System.out.println("[Controller] IO exception"); - } - } - - /** - * First find the top right and bottom left points' geo locations, then retrieve - * map from google to display on image view. - Haoming 22/5/2017 - */ - private void drawGoogleMap() { - findMetersPerPixel(); - Point2D topLeftPoint = findScaledXY(maxLatPoint.getLatitude(), minLonPoint.getLongitude()); - // distance from top left extreme to panel origin (top left corner) - double distanceFromTopLeftToOrigin = Math.sqrt( - Math.pow(topLeftPoint.getX() * metersPerPixelX, 2) + Math - .pow(topLeftPoint.getY() * metersPerPixelY, 2)); - // angle from top left extreme to panel origin - double bearingFromTopLeftToOrigin = Math - .toDegrees(Math.atan2(-topLeftPoint.getX(), topLeftPoint.getY())); - // the top left extreme - GeoPoint topLeftPos = new GeoPoint(maxLatPoint.getLatitude(), minLonPoint.getLongitude()); - GeoPoint originPos = GeoUtility - .getGeoCoordinate(topLeftPos, bearingFromTopLeftToOrigin, distanceFromTopLeftToOrigin); - - // distance from origin corner to bottom right corner of the panel - double distanceFromOriginToBottomRight = Math.sqrt( - Math.pow(PANEL_HEIGHT * metersPerPixelY, 2) + Math - .pow(PANEL_WIDTH * metersPerPixelX, 2)); - double bearingFromOriginToBottomRight = Math - .toDegrees(Math.atan2(PANEL_WIDTH, -PANEL_HEIGHT)); - GeoPoint bottomRightPos = GeoUtility - .getGeoCoordinate(originPos, bearingFromOriginToBottomRight, - distanceFromOriginToBottomRight); - - Boundary boundary = new Boundary(originPos.getLat(), bottomRightPos.getLng(), - bottomRightPos.getLat(), originPos.getLng()); - CanvasMap canvasMap = new CanvasMap(boundary); - mapImage.setImage(canvasMap.getMapImage()); - } - - /** - * Adds border marks to the canvas, taken from the XML file - * - * NOTE: This is quite confusing as objects are grabbed from the XMLParser such as Mark and - * CompoundMark which are named the same as those in the model package but are, however not the - * same, so they do not have things such as a type and must be derived from the number of marks - * in a compound mark etc.. - */ - private void addRaceBorder() { - XMLParser.RaceXMLObject raceXMLObject = ClientPacketParser.getXmlObject().getRaceXML(); - ArrayList courseLimits = raceXMLObject.getCourseLimit(); - raceBorder.setStroke(new Color(0.0f, 0.0f, 0.74509807f, 1)); - raceBorder.setStrokeWidth(3); - raceBorder.setFill(new Color(0,0,0,0)); - List boundaryPoints = new ArrayList<>(); - for (Limit limit : courseLimits) { - Point2D location = findScaledXY(limit.getLat(), limit.getLng()); - boundaryPoints.add(location.getX()); - boundaryPoints.add(location.getY()); - } - raceBorder.getPoints().setAll(boundaryPoints); - } - - private void updateGroups() { - for (BoatGroup boatGroup : boatGroups) { - // some raceObjects will have multiple ID's (for instance gate marks) - //checking if the current "ID" has any updates associated with it - if (ClientPacketParser.boatLocations.containsKey(boatGroup.getRaceId())) { - updateBoatGroup(boatGroup); - } - } - for (MarkGroup markGroup : markGroups) { - for (Long id : markGroup.getRaceIds()) { - if (ClientPacketParser.markLocations.containsKey(id)) { - updateMarkGroup(id, markGroup); - } - } - } - checkForCourseChanges(); - } - - private void checkForCourseChanges() { - if (ClientPacketParser.isNewRaceXmlReceived()) { - addRaceBorder(); - } - } - - private void updateBoatGroup(BoatGroup boatGroup) { - PriorityBlockingQueue movementQueue = ClientPacketParser.boatLocations - .get(boatGroup.getRaceId()); - // giving the movementQueue a 5 packet buffer to account for slightly out of order packets - if (movementQueue.size() > 0) { - try { - BoatPositionPacket positionPacket = movementQueue.take(); - Point2D p2d = findScaledXY(positionPacket.getLat(), positionPacket.getLon()); - double heading = 360.0 / 0xffff * positionPacket.getHeading(); - boatGroup.setDestination( - p2d.getX(), p2d.getY(), heading, positionPacket.getGroundSpeed(), - positionPacket.getTimeValid(), frameRate); - } catch (InterruptedException e){ - System.out.println("[CanvasController] Interrupted Exception"); - } - } - } - - void updateMarkGroup (long raceId, MarkGroup markGroup) { - PriorityBlockingQueue movementQueue = ClientPacketParser.markLocations - .get(raceId); - if (movementQueue.size() > 0){ - try { - BoatPositionPacket positionPacket = movementQueue.take(); - Point2D p2d = findScaledXY(positionPacket.getLat(), positionPacket.getLon()); - markGroup.moveMarkTo(p2d.getX(), p2d.getY(), raceId); - } catch (InterruptedException e) { - System.out.println("[CanvasController] Interrupted exception"); - } - } - } - - /** - * Draws all the boats. - */ - private void initializeBoats() { - Map boats = ClientPacketParser.getBoats(); - Group wakes = new Group(); - Group trails = new Group(); - Group annotations = new Group(); - - ArrayList participants = ClientPacketParser.getXmlObject().getRaceXML() - .getParticipants(); - ArrayList participantIDs = new ArrayList<>(); - for (Participant p : participants) { - participantIDs.add(p.getsourceID()); - } - - for (Yacht boat : boats.values()) { - if (participantIDs.contains(boat.getSourceId())) { - boat.setColour(Colors.getColor()); - BoatGroup boatGroup = new BoatGroup(boat, boat.getColour()); - if (boat.getSourceId().equals(Integer.parseInt(ClientState.getClientSourceId()))) { - boatGroup.setAsPlayer(); - boatGroups.add(boatGroup); - annotations.getChildren().add(boatGroup.getAnnotations()); - } else { - //Move annotations and boat to bottom of group keeping player ontop. - if (boatGroups.size() > 0) { - boatGroups.add(0, boatGroup); - } else { - boatGroups.add(boatGroup); - } - if (annotations.getChildren().size() > 0) { - annotations.getChildren().add(0, boatGroup.getAnnotations()); - } else { - annotations.getChildren().add(boatGroup.getAnnotations()); - } - } - trails.getChildren().add(boatGroup.getTrail()); - wakes.getChildren().add(boatGroup.getWake()); - } - } - group.getChildren().addAll(trails); - group.getChildren().addAll(wakes); - group.getChildren().addAll(annotations); - group.getChildren().addAll(boatGroups); - } - - private void initializeMarks() { - List allMarks = ClientPacketParser.getXmlObject().getRaceXML() - .getNonDupCompoundMarks(); - for (Mark mark : allMarks) { - if (mark.getMarkType() == MarkType.SINGLE_MARK) { - SingleMark sMark = (SingleMark) mark; - - MarkGroup markGroup = new MarkGroup(sMark, findScaledXY(sMark)); - markGroups.add(markGroup); - } else { - GateMark gMark = (GateMark) mark; - - MarkGroup markGroup = new MarkGroup(gMark, findScaledXY(gMark.getSingleMark1()), - findScaledXY(gMark.getSingleMark2())); //should be 2 objects in the list. - markGroups.add(markGroup); - } - } - group.getChildren().addAll(markGroups); - } - - class ResizableCanvas extends Canvas { - - ResizableCanvas() { - // Redraw canvas when size changes. - widthProperty().addListener(evt -> draw()); - heightProperty().addListener(evt -> draw()); - } - - private void draw() { - double width = getWidth(); - double height = getHeight(); - - GraphicsContext gc = getGraphicsContext2D(); - gc.clearRect(0, 0, width, height); - } - - @Override - public boolean isResizable() { - return true; - } - - @Override - public double prefWidth(double height) { - return getWidth(); - } - - @Override - public double prefHeight(double width) { - return getHeight(); - } - - } - - private void drawFps(int fps){ - if (raceViewController.isDisplayFps()){ - FPSDisplay.setVisible(true); - FPSDisplay.setText(String.format("%d FPS", fps)); - } else { - FPSDisplay.setVisible(false); - } - } - - /** - * Calculates x and y location for every marker that fits it to the canvas the race will be - * drawn on. - */ - private void fitMarksToCanvas() { - //Check is called once to avoid unnecessarily change the course limits once the race is running - ClientPacketParser.isNewRaceXmlReceived(); - findMinMaxPoint(); - double minLonToMaxLon = scaleRaceExtremities(); - calculateReferencePointLocation(minLonToMaxLon); - //givePointsXY(); - addRaceBorder(); - } - - - /** - * Sets the class variables minLatPoint, maxLatPoint, minLonPoint, maxLonPoint to the marker - * with the leftmost marker, rightmost marker, southern most marker and northern most marker - * respectively. - */ - private void findMinMaxPoint() { - List sortedPoints = new ArrayList<>(); - - for (Limit limit : ClientPacketParser.getXmlObject().getRaceXML().getCourseLimit()) { - sortedPoints.add(limit); - } - sortedPoints.sort(Comparator.comparingDouble(Limit::getLat)); - Limit minLatMark = sortedPoints.get(0); - Limit maxLatMark = sortedPoints.get(sortedPoints.size()-1); - minLatPoint = new SingleMark(minLatMark.toString(), minLatMark.getLat(), minLatMark.getLng(), minLatMark.getSeqID(), minLatMark.getSeqID()); - maxLatPoint = new SingleMark(maxLatMark.toString(), maxLatMark.getLat(), maxLatMark.getLng(), maxLatMark.getSeqID(), minLatMark.getSeqID()); - - sortedPoints.sort(Comparator.comparingDouble(Limit::getLng)); - //If the course is on a point on the earth where longitudes wrap around. - Limit minLonMark = sortedPoints.get(0); - Limit maxLonMark = sortedPoints.get(sortedPoints.size()-1); - minLonPoint = new SingleMark(minLonMark.toString(), minLonMark.getLat(), minLonMark.getLng(), minLonMark.getSeqID(), minLonMark.getSeqID()); - maxLonPoint = new SingleMark(maxLonMark.toString(), maxLonMark.getLat(), maxLonMark.getLng(), maxLonMark.getSeqID(), minLonMark.getSeqID()); - if (maxLonPoint.getLongitude() - minLonPoint.getLongitude() > 180) { - horizontalInversion = true; - } - } - - /** - * Calculates the location of a reference point, this is always the point with minimum latitude, - * in relation to the canvas. - * - * @param minLonToMaxLon The horizontal distance between the point of minimum longitude to - * maximum longitude. - */ - private void calculateReferencePointLocation(double minLonToMaxLon) { - Mark referencePoint = minLatPoint; - double referenceAngle; - - if (scaleDirection == ScaleDirection.HORIZONTAL) { - referenceAngle = Math.abs(Mark.calculateHeadingRad(referencePoint, minLonPoint)); - referencePointX = BUFFER_SIZE + distanceScaleFactor * Math.sin(referenceAngle) * Mark.calculateDistance(referencePoint, minLonPoint); - - referenceAngle = Math.abs(Mark.calculateHeadingRad(referencePoint, maxLatPoint)); - referencePointY = CANVAS_HEIGHT - (BUFFER_SIZE + BUFFER_SIZE); - referencePointY -= distanceScaleFactor * Math.cos(referenceAngle) * Mark.calculateDistance(referencePoint, maxLatPoint); - referencePointY = referencePointY / 2; - referencePointY += BUFFER_SIZE; - referencePointY += distanceScaleFactor * Math.cos(referenceAngle) * Mark.calculateDistance(referencePoint, maxLatPoint); - } else { - referencePointY = CANVAS_HEIGHT - BUFFER_SIZE; - - referenceAngle = Math.abs(Mark.calculateHeadingRad(referencePoint, minLonPoint)); - referencePointX = BUFFER_SIZE; - referencePointX += distanceScaleFactor * Math.sin(referenceAngle) * Mark.calculateDistance(referencePoint, minLonPoint); - referencePointX += ((CANVAS_WIDTH - (BUFFER_SIZE + BUFFER_SIZE)) - (minLonToMaxLon * distanceScaleFactor)) / 2; - } - if(horizontalInversion) { - referencePointX = CANVAS_WIDTH - BUFFER_SIZE - (referencePointX - BUFFER_SIZE); - } - } - - - /** - * Finds the scale factor necessary to fit all race markers within the onscreen map and assigns - * it to distanceScaleFactor Returns the max horizontal distance of the map. - */ - private double scaleRaceExtremities() { - - double vertAngle = Math.abs(Mark.calculateHeadingRad(minLatPoint, maxLatPoint)); - double vertDistance = - Math.cos(vertAngle) * Mark.calculateDistance(minLatPoint, maxLatPoint); - double horiAngle = Mark.calculateHeadingRad(minLonPoint, maxLonPoint); - - if (horiAngle <= (Math.PI / 2)) { - horiAngle = (Math.PI / 2) - horiAngle; - } else { - horiAngle = horiAngle - (Math.PI / 2); - } - double horiDistance = - Math.cos(horiAngle) * Mark.calculateDistance(minLonPoint, maxLonPoint); - - double vertScale = (CANVAS_HEIGHT - (BUFFER_SIZE + BUFFER_SIZE)) / vertDistance; - - if ((horiDistance * vertScale) > (CANVAS_WIDTH - (BUFFER_SIZE + BUFFER_SIZE))) { - distanceScaleFactor = (CANVAS_WIDTH - (BUFFER_SIZE + BUFFER_SIZE)) / horiDistance; - scaleDirection = ScaleDirection.HORIZONTAL; - } else { - distanceScaleFactor = vertScale; - scaleDirection = ScaleDirection.VERTICAL; - } - return horiDistance; - } - - private Point2D findScaledXY(Mark unscaled) { - return findScaledXY(unscaled.getLatitude(), unscaled.getLongitude()); - } - - public Point2D findScaledXY (double unscaledLat, double unscaledLon) { - double distanceFromReference; - double angleFromReference; - int xAxisLocation = (int) referencePointX; - int yAxisLocation = (int) referencePointY; - - angleFromReference = Mark - .calculateHeadingRad(minLatPoint.getLatitude(), minLatPoint.getLongitude(), unscaledLat, - unscaledLon); - distanceFromReference = Mark - .calculateDistance(minLatPoint.getLatitude(), minLatPoint.getLongitude(), unscaledLat, - unscaledLon); - if (angleFromReference >= 0 && angleFromReference <= Math.PI / 2) { - xAxisLocation += (int) Math - .round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); - yAxisLocation -= (int) Math - .round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); - } else if (angleFromReference >= 0) { - angleFromReference = angleFromReference - Math.PI / 2; - xAxisLocation += (int) Math - .round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); - yAxisLocation += (int) Math - .round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); - } else if (angleFromReference < 0 && angleFromReference >= -Math.PI / 2) { - angleFromReference = Math.abs(angleFromReference); - xAxisLocation -= (int) Math - .round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); - yAxisLocation -= (int) Math - .round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); - } else { - angleFromReference = Math.abs(angleFromReference) - Math.PI / 2; - xAxisLocation -= (int) Math - .round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); - yAxisLocation += (int) Math - .round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); - } - if(horizontalInversion) { - xAxisLocation = CANVAS_WIDTH - BUFFER_SIZE - (xAxisLocation - BUFFER_SIZE); - } - return new Point2D(xAxisLocation, yAxisLocation); - } - - /** - * Find the number of meters per pixel. - */ - private void findMetersPerPixel() { - Point2D p1, p2; - Mark m1, m2; - double theta, distance, dx, dy, dHorizontal, dVertical; - m1 = new SingleMark("m1", maxLatPoint.getLatitude(), minLonPoint.getLongitude(), 1, 0); - m2 = new SingleMark("m2", minLatPoint.getLatitude(), maxLonPoint.getLongitude(), 2, 0); - p1 = findScaledXY(m1); - p2 = findScaledXY(m2); - theta = Mark.calculateHeadingRad(m1, m2); - distance = Mark.calculateDistance(m1, m2); - dHorizontal = Math.abs(Math.sin(theta) * distance); - dVertical = Math.abs(Math.cos(theta) * distance); - dx = Math.abs(p1.getX() - p2.getX()); - dy = Math.abs(p1.getY() - p2.getY()); - metersPerPixelX = dHorizontal / dx; - metersPerPixelY = dVertical / dy; - } - - List getBoatGroups() { - return boatGroups; - } - - List getMarkGroups() { - return markGroups; - } - -} \ No newline at end of file diff --git a/src/main/java/seng302/controllers/Controller.java b/src/main/java/seng302/controllers/Controller.java deleted file mode 100644 index f1871192..00000000 --- a/src/main/java/seng302/controllers/Controller.java +++ /dev/null @@ -1,106 +0,0 @@ -package seng302.controllers; - -import java.io.IOException; -import java.net.URL; -import java.util.ResourceBundle; -import javafx.fxml.FXML; -import javafx.fxml.FXMLLoader; -import javafx.fxml.Initializable; -import javafx.scene.Parent; -import javafx.scene.input.KeyEvent; -import javafx.scene.layout.AnchorPane; -import seng302.client.ClientPacketParser; -import seng302.client.ClientState; -import seng302.client.ClientToServerThread; -import seng302.server.messages.BoatActionMessage; -import seng302.server.messages.BoatActionType; - -public class Controller implements Initializable { - - @FXML - private AnchorPane contentPane; - private ClientToServerThread clientToServerThread; - private long lastSendingTime; - private int KEY_STROKE_SENDING_FREQUENCY = 50; - - public Object setContentPane(String jfxUrl) { - try { - contentPane.getChildren().removeAll(); - contentPane.getChildren().clear(); - contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString()); - FXMLLoader fxmlLoader = new FXMLLoader((getClass().getResource(jfxUrl))); - Parent view = fxmlLoader.load(); - contentPane.getChildren().addAll(view); - return fxmlLoader.getController(); - } catch (javafx.fxml.LoadException e) { - System.err.println(e.getCause()); - } catch (IOException e) { - System.err.println(e); - } - return null; - } - - @Override - public void initialize(URL location, ResourceBundle resources) { - setUpStartScreen(); - lastSendingTime = System.currentTimeMillis(); - } - - void setUpStartScreen() { - contentPane.getChildren().removeAll(); - contentPane.getChildren().clear(); - contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString()); - StartScreenController startScreenController = (StartScreenController) setContentPane("/views/StartScreenView.fxml"); - startScreenController.setController(this); - ClientPacketParser.boatLocations.clear(); - } - - - /** Handle the key-pressed event from the text field. */ - public void keyPressed(KeyEvent e) { - BoatActionMessage boatActionMessage; - long currentTime = System.currentTimeMillis(); - if (currentTime - lastSendingTime > KEY_STROKE_SENDING_FREQUENCY && ClientState.isRaceStarted()) { - lastSendingTime = currentTime; - switch (e.getCode()) { - case SPACE: // align with vmg - boatActionMessage = new BoatActionMessage(BoatActionType.VMG); - clientToServerThread.sendBoatActionMessage(boatActionMessage); - break; - case PAGE_UP: // upwind - boatActionMessage = new BoatActionMessage(BoatActionType.UPWIND); - clientToServerThread.sendBoatActionMessage(boatActionMessage); - break; - case PAGE_DOWN: // downwind - boatActionMessage = new BoatActionMessage(BoatActionType.DOWNWIND); - clientToServerThread.sendBoatActionMessage(boatActionMessage); - break; - case ENTER: // tack/gybe - boatActionMessage = new BoatActionMessage(BoatActionType.TACK_GYBE); - clientToServerThread.sendBoatActionMessage(boatActionMessage); - break; - //TODO Allow a zoom in and zoom out methods - case Z: // zoom in - System.out.println("Zoom in"); - break; - case X: // zoom out - System.out.println("Zoom out"); - break; - } - } - } - - public void keyReleased(KeyEvent e) { - switch (e.getCode()) { - //TODO 12/07/17 Determine the sail state and send the appropriate packet (eg. if sails are in, send a sail out packet) - case SHIFT: // sails in/sails out - BoatActionMessage boatActionMessage = new BoatActionMessage(BoatActionType.SAILS_IN); - clientToServerThread.sendBoatActionMessage(boatActionMessage); - break; - } - } - - public void setClientToServerThread(ClientToServerThread ctt) { - clientToServerThread = ctt; - } -} diff --git a/src/main/java/seng302/controllers/LobbyController.java b/src/main/java/seng302/controllers/LobbyController.java deleted file mode 100644 index b9d407fe..00000000 --- a/src/main/java/seng302/controllers/LobbyController.java +++ /dev/null @@ -1,235 +0,0 @@ -package seng302.controllers; - -import java.io.IOException; -import java.net.URL; -import java.util.*; - -import javafx.application.Platform; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.fxml.FXML; -import javafx.fxml.FXMLLoader; -import javafx.fxml.Initializable; -import javafx.scene.control.Button; -import javafx.scene.control.ListView; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.GridPane; -import javafx.scene.layout.Pane; -import javafx.scene.text.Text; -import seng302.client.ClientState; -import seng302.client.ClientStateQueryingRunnable; -import seng302.gameServer.GameStages; -import seng302.gameServer.GameState; -import seng302.gameServer.MainServerThread; - -/** - * A class describing the actions of the lobby screen - * Created by wmu16 on 10/07/17. - */ -public class LobbyController implements Initializable, Observer{ - @FXML - private GridPane lobbyScreen; - @FXML - private Text lobbyIpText; - @FXML - private Button readyButton; - @FXML - private ListView firstListView; - @FXML - private ListView secondListView; - @FXML - private ListView thirdListView; - @FXML - private ListView fourthListView; - @FXML - private ListView fifthListView; - @FXML - private ListView sixthListView; - @FXML - private ListView seventhListView; - @FXML - private ListView eighthListView; - @FXML - private ImageView firstImageView; - @FXML - private ImageView secondImageView; - @FXML - private ImageView thirdImageView; - @FXML - private ImageView fourthImageView; - @FXML - private ImageView fifthImageView; - @FXML - private ImageView sixthImageView; - @FXML - private ImageView seventhImageView; - @FXML - private ImageView eighthImageView; - - private static List> competitors = new ArrayList<>(); - private static ObservableList firstCompetitor = FXCollections.observableArrayList(); - private static ObservableList secondCompetitor = FXCollections.observableArrayList(); - private static ObservableList thirdCompetitor = FXCollections.observableArrayList(); - private static ObservableList fourthCompetitor = FXCollections.observableArrayList(); - private static ObservableList fifthCompetitor = FXCollections.observableArrayList(); - private static ObservableList sixthCompetitor = FXCollections.observableArrayList(); - private static ObservableList seventhCompetitor = FXCollections.observableArrayList(); - private static ObservableList eighthCompetitor = FXCollections.observableArrayList(); - private ClientStateQueryingRunnable clientStateQueryingRunnable; - private static List imageViews; - private static List listViews; - - private int MAX_NUM_PLAYERS = 8; - - private Boolean switchedPane = false; - private MainServerThread mainServerThread; - private Controller controller; - - private void setContentPane(String jfxUrl) { - try { - AnchorPane contentPane = (AnchorPane) lobbyScreen.getParent(); - contentPane.getChildren().removeAll(); - contentPane.getChildren().clear(); - contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString()); - contentPane.getChildren() - .addAll((Pane) FXMLLoader.load(getClass().getResource(jfxUrl))); - } catch (javafx.fxml.LoadException e) { - System.out.println("[Controller] FXML load exception"); - } catch (IOException e) { - System.out.println("[Controller] IO exception"); - } catch (NullPointerException e) { -// System.out.println("[Controller] Null Pointer Exception"); - } - } - - @Override - public void initialize(URL location, ResourceBundle resources) { - if (ClientState.isHost()) { - lobbyIpText.setText("Lobby Host IP: " + ClientState.getHostIp()); - readyButton.setDisable(false); - } - else { - lobbyIpText.setText("Connected to IP: " + ClientState.getHostIp()); - readyButton.setDisable(true); - } - - // put all javafx objects in lists, so we can iterate though conveniently - imageViews = new ArrayList<>(); - Collections.addAll(imageViews, firstImageView, secondImageView, thirdImageView, fourthImageView, - fifthImageView, sixthImageView, seventhImageView, eighthImageView); - listViews = new ArrayList<>(); - Collections.addAll(listViews, firstListView, secondListView, thirdListView, fourthListView, fifthListView, - sixthListView, seventhListView, eighthListView); - competitors = new ArrayList<>(); - Collections.addAll(competitors, firstCompetitor, secondCompetitor, thirdCompetitor, - fourthCompetitor, fifthCompetitor, sixthCompetitor, seventhCompetitor, eighthCompetitor); - - initialiseListView(); - initialiseImageView(); // parrot gif init - - // set up client state query thread, so that when it receives the race-started packet - // it can switch to the race view - ClientStateQueryingRunnable clientStateQueryingRunnable = new ClientStateQueryingRunnable(); - clientStateQueryingRunnable.addObserver(this); - Thread clientStateQueryingThread = new Thread(clientStateQueryingRunnable, "Client State querying thread"); - clientStateQueryingThread.setDaemon(true); - clientStateQueryingThread.start(); - } - - /** - * Observers "ClientStateQueryingRunnable". - * When the clients state has been marked to "race start", the querying thread - * will notify this lobby to change the view - * @param o - * @param arg - */ - @Override - public void update(Observable o, Object arg) { - Platform.runLater(new Runnable() { - @Override - public void run() { - if (arg.equals("game started") && !switchedPane) { - switchToRaceView(); - } - if (arg.equals(("update players"))) { - initialiseListView(); - } - } - }); - } - - /** - * Reset all ListViews and ImageViews according to the current competitors - */ - private void initialiseListView() { - listViews.forEach(listView -> listView.getItems().clear()); - imageViews.forEach(gif -> gif.setVisible(false)); - competitors.forEach(ol -> ol.removeAll()); - - List ids = new ArrayList<>(ClientState.getBoats().keySet()); - for (int i = 0; i < ids.size(); i++) { - competitors.get(i).add(ClientState.getBoats().get(ids.get(i)).getBoatName()); - listViews.get(i).setItems(competitors.get(i)); - imageViews.get(i).setVisible(true); - } - } - - /** - * Loads preset images into imageViews - */ - private void initialiseImageView() { - for (int i = 0; i < MAX_NUM_PLAYERS; i++) { - imageViews.get(i).setImage(new Image(getClass().getResourceAsStream("/pics/sail.png"))); - } -// Image image1 = new Image(getClass().getResourceAsStream("/pics/sail.png")); -// firstImageView.setImage(image1); -// Image image2 = new Image(getClass().getResourceAsStream("/pics/sail.png")); -// secondImageView.setImage(image2); -// Image image3 = new Image(getClass().getResourceAsStream("/pics/sail.png")); -// thirdImageView.setImage(image3); -// Image image4 = new Image(getClass().getResourceAsStream("/pics/sail.png")); -// fourthImageView.setImage(image4); -// Image image5 = new Image(getClass().getResourceAsStream("/pics/sail.png")); -// fifthImageView.setImage(image5); -// Image image6 = new Image(getClass().getResourceAsStream("/pics/sail.png")); -// sixthImageView.setImage(image6); -// Image image7 = new Image(getClass().getResourceAsStream("/pics/sail.png")); -// seventhImageView.setImage(image7); -// Image image8 = new Image(getClass().getResourceAsStream("/pics/sail.png")); -// eighthImageView.setImage(image8); - } - - @FXML - public void leaveLobbyButtonPressed() { - if (ClientState.isHost()) { - GameState.setCurrentStage(GameStages.CANCELLED); - mainServerThread.terminate(); - } - ClientState.setConnectedToHost(false); - controller.setUpStartScreen(); - } - - @FXML - public void readyButtonPressed() { - GameState.setCurrentStage(GameStages.RACING); - mainServerThread.startGame(); - } - - - private void switchToRaceView() { - if (!switchedPane) { - switchedPane = true; - setContentPane("/views/RaceView.fxml"); - } - } - - public void setMainServerThread(MainServerThread mainServerThread) { - this.mainServerThread = mainServerThread; - } - - public void setController(Controller controller) { - this.controller = controller; - } -} diff --git a/src/main/java/seng302/controllers/RaceViewController.java b/src/main/java/seng302/controllers/RaceViewController.java deleted file mode 100644 index 57447081..00000000 --- a/src/main/java/seng302/controllers/RaceViewController.java +++ /dev/null @@ -1,668 +0,0 @@ -package seng302.controllers; - -import javafx.animation.KeyFrame; -import javafx.animation.Timeline; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.fxml.FXML; -import javafx.fxml.FXMLLoader; -import javafx.geometry.Point2D; -import javafx.scene.Scene; -import javafx.scene.chart.LineChart; -import javafx.scene.chart.NumberAxis; -import javafx.scene.chart.XYChart; -import javafx.scene.chart.XYChart.Series; -import javafx.scene.control.Button; -import javafx.scene.control.CheckBox; -import javafx.scene.control.ComboBox; -import javafx.scene.control.Slider; -import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.Pane; -import javafx.scene.layout.VBox; -import javafx.scene.paint.Color; -import javafx.scene.paint.Paint; -import javafx.scene.shape.Line; -import javafx.scene.text.Text; -import javafx.stage.Stage; -import javafx.stage.StageStyle; -import javafx.util.Duration; -import javafx.util.StringConverter; -import seng302.client.ClientPacketParser; -import seng302.utilities.GeoUtility; -import seng302.controllers.annotations.Annotation; -import seng302.controllers.annotations.ImportantAnnotationController; -import seng302.controllers.annotations.ImportantAnnotationDelegate; -import seng302.controllers.annotations.ImportantAnnotationsState; -import seng302.fxObjects.BoatGroup; -import seng302.fxObjects.MarkGroup; -import seng302.models.*; -import seng302.models.mark.GateMark; -import seng302.models.mark.Mark; -import seng302.models.mark.SingleMark; -import seng302.models.stream.XMLParser; - -import java.io.IOException; -import java.util.*; -import seng302.models.stream.XMLParser.RaceXMLObject.Participant; -import java.util.stream.Collectors; - -/** - * Created by ptg19 on 29/03/17. - */ -public class RaceViewController extends Thread implements ImportantAnnotationDelegate { - - @FXML - private Text windSpeedText; - @FXML - private LineChart raceSparkLine; - @FXML - private NumberAxis sparklineYAxis; - @FXML - private VBox positionVbox; - @FXML - private CheckBox toggleFps; - @FXML - private Text timerLabel; - @FXML - private AnchorPane contentAnchorPane; - @FXML - private Text windArrowText, windDirectionText; - @FXML - private Slider annotationSlider; - @FXML - private Button selectAnnotationBtn; - @FXML - private ComboBox boatSelectionComboBox; - @FXML - private CanvasController includedCanvasController; - - private static ArrayList startingBoats = new ArrayList<>(); - private boolean displayFps; - private Timeline timerTimeline; - private Stage stage; - private static HashMap> sparkLineData = new HashMap<>(); - private static ArrayList racingBoats = new ArrayList<>(); - private ImportantAnnotationsState importantAnnotations; - private Yacht selectedBoat; - - public void initialize() { - // Load a default important annotation state - importantAnnotations = new ImportantAnnotationsState(); - - //Formatting the y axis of the sparkline - raceSparkLine.getYAxis().setRotate(180); - raceSparkLine.getYAxis().setTickLabelRotation(180); - raceSparkLine.getYAxis().setTranslateX(-5); - raceSparkLine.getYAxis().setAutoRanging(false); - sparklineYAxis.setTickMarkVisible(false); - startingBoats = new ArrayList<>(ClientPacketParser.getBoats().values()); - - includedCanvasController.setup(this); - includedCanvasController.initializeCanvas(); - initializeUpdateTimer(); - initialiseFPSCheckBox(); - initialiseAnnotationSlider(); - initialiseBoatSelectionComboBox(); - includedCanvasController.timer.start(); - selectAnnotationBtn.setOnAction(event -> loadSelectAnnotationView()); - - } - - - /** - * The important annotations have been changed, update this view - * - * @param importantAnnotationsState The current state of the selected annotations - */ - public void importantAnnotationsChanged(ImportantAnnotationsState importantAnnotationsState) { - this.importantAnnotations = importantAnnotationsState; - setAnnotations((int) annotationSlider.getValue()); // Refresh the displayed annotations - } - - - /** - * Loads the "select annotations" view in a new window - */ - private void loadSelectAnnotationView() { - try { - FXMLLoader fxmlLoader = new FXMLLoader(); - Stage stage = new Stage(); - - // Set controller - ImportantAnnotationController controller = new ImportantAnnotationController(this, - stage); - fxmlLoader.setController(controller); - - // Load FXML and set CSS - fxmlLoader - .setLocation(getClass().getResource("/views/importantAnnotationSelectView.fxml")); - Scene scene = new Scene(fxmlLoader.load(), 469, 298); - scene.getStylesheets().add(getClass().getResource("/css/master.css").toString()); - stage.initStyle(StageStyle.UNDECORATED); - stage.setScene(scene); - stage.show(); - - controller.loadState(importantAnnotations); - - } catch (IOException e) { - System.out.println("[RaceViewController] IO exception"); - } - } - - - private void initialiseFPSCheckBox() { - displayFps = true; - toggleFps.selectedProperty().addListener( - (observable, oldValue, newValue) -> displayFps = !displayFps); - } - - private void initialiseAnnotationSlider() { - annotationSlider.setLabelFormatter(new StringConverter() { - @Override - public String toString(Double n) { - if (n == 0) { - return "None"; - } - if (n == 1) { - return "Important"; - } - if (n == 2) { - return "All"; - } - - return "All"; - } - - @Override - public Double fromString(String s) { - switch (s) { - case "None": - return 0d; - case "Important": - return 1d; - case "All": - return 2d; - - default: - return 2d; - } - } - }); - - annotationSlider.valueProperty().addListener((obs, oldval, newVal) -> - setAnnotations((int) annotationSlider.getValue())); - - annotationSlider.setValue(2); - } - - - /** - * Used to add any new boats into the race that may have started late or not have had data received yet - */ - void updateSparkLine(){ - // Collect the racing boats that aren't already in the chart - ArrayList sparkLineCandidates = startingBoats.stream().filter(yacht -> !sparkLineData.containsKey(yacht.getSourceId()) - && yacht.getPosition() != null & yacht.getPosition() != "-").collect(Collectors.toCollection(ArrayList::new)); - - // Obtain the qualifying boats to set the max on the Y axis - racingBoats = startingBoats.stream().filter(yacht -> - yacht.getPosition() != null & yacht.getPosition() != "-").collect(Collectors.toCollection(ArrayList::new)); - sparklineYAxis.setUpperBound(racingBoats.size() + 1); - - // Create a new data series for new boats - sparkLineCandidates.stream().filter(yacht -> yacht.getPosition() != null).forEach(yacht -> { - Series yachtData = new Series<>(); - yachtData.setName(yacht.getBoatName()); - yachtData.getData().add(new XYChart.Data<>(Integer.toString(yacht.getLegNumber()), 1 + racingBoats.size() - Double.parseDouble(yacht.getPosition()))); - sparkLineData.put(yacht.getSourceId(), yachtData); - }); - - // Lambda function to sort the series in order of leg (later legs shown more to the right) - List> positions = new ArrayList<>(sparkLineData.values()); - Collections.sort(positions, (o1, o2) -> { - Integer leg1 = Integer.parseInt(o1.getData().get(o1.getData().size()-1).getXValue()); - Integer leg2 = Integer.parseInt(o2.getData().get(o2.getData().size()-1).getXValue()); - if (leg2 < leg1){ - return 1; - } else { - return -1; - } - }); - - // Adds the new data series to the sparkline (and set the colour of the series) - raceSparkLine.setCreateSymbols(false); - positions.stream().filter(spark -> !raceSparkLine.getData().contains(spark)).forEach(spark -> { - raceSparkLine.getData().add(spark); - spark.getNode().lookup(".chart-series-line").setStyle("-fx-stroke:" + getBoatColorAsRGB(spark.getName())); - }); - } - - - /** - * Updates the yachts sparkline of the desired boat and using the new leg number - * @param yacht The yacht to be updated on the sparkline - * @param legNumber the leg number that the position will be assigned to - */ - public static void updateYachtPositionSparkline(Yacht yacht, Integer legNumber){ - XYChart.Series positionData = sparkLineData.get(yacht.getSourceId()); - positionData.getData().add(new XYChart.Data<>(Integer.toString(legNumber), 1 + racingBoats.size() - Double.parseDouble(yacht.getPosition()))); - } - - - /** - * gets the rgb string of the boats colour to use for the chart via css - * @param boatName boat passed in to get the boats colour - * @return the colour as an rgb string - */ - private String getBoatColorAsRGB(String boatName){ - Color color = Color.WHITE; - for (Yacht yacht: startingBoats){ - if (Objects.equals(yacht.getBoatName(), boatName)){ - color = yacht.getColour(); - } - } - if (color == null){ - return String.format( "#%02X%02X%02X",255,255,255); - } - return String.format( "#%02X%02X%02X", - (int)( color.getRed() * 255 ), - (int)( color.getGreen() * 255 ), - (int)( color.getBlue() * 255 ) ); - } - - - /** - * Initalises a timer which updates elements of the RaceView such as wind direction, boat - * orderings etc.. which are dependent on the info from the stream parser constantly. - * Updates of each of these attributes are called ONCE EACH SECOND - */ - private void initializeUpdateTimer() { - timerTimeline = new Timeline(); - timerTimeline.setCycleCount(Timeline.INDEFINITE); - // Run timer update every second - timerTimeline.getKeyFrames().add( - new KeyFrame(Duration.seconds(1), - event -> { - updateRaceTime(); - updateWindDirection(); -// updateOrder(); - updateBoatSelectionComboBox(); - updateOrder(); - }) - ); - - // Start the timer - timerTimeline.playFromStart(); - } - - - /** - * Iterates over all corners until ones SeqID matches with the boats current leg number. - * Then it gets the compoundMarkID of that corner and uses it to fetch the appropriate mark - * Returns null if no next mark found. - * @param bg The BoatGroup to find the next mark of - * @return The next Mark or null if none found - */ - private Mark getNextMark(BoatGroup bg) { - Integer legNumber = bg.getBoat().getLegNumber(); - - List markSequence = ClientPacketParser.getXmlObject() - .getRaceXML().getCompoundMarkSequence(); - - if (legNumber == 0) { - return null; - } else if (legNumber == markSequence.size() - 1) { - return null; - } - - for (XMLParser.RaceXMLObject.Corner corner : markSequence) { - if (legNumber + 2 == corner.getSeqID()) { - Integer thisCompoundMarkID = corner.getCompoundMarkID(); - - for (Mark mark : ClientPacketParser.getXmlObject().getRaceXML() - .getAllCompoundMarks()) { - if (mark.getCompoundMarkID() == thisCompoundMarkID) { - return mark; - } - } - } - } - - return null; - } - - - /** - * Updates the wind direction arrow and text as from info from the ClientPacketParser - */ - private void updateWindDirection() { - windDirectionText.setText(String.format("%.1f°", ClientPacketParser.getWindDirection())); - windArrowText.setRotate(ClientPacketParser.getWindDirection()); - windSpeedText.setText(String.format("%.1f Knots", ClientPacketParser.getWindSpeed())); - } - - - /** - * Updates the clock for the race - */ - private void updateRaceTime() { - if (ClientPacketParser.isRaceFinished()) { - timerLabel.setFill(Color.RED); - timerLabel.setText("Race Finished!"); - } else { - timerLabel.setText(getTimeSinceStartOfRace()); - } - } - - - /** - * Grabs the boats currently in the race as from the ClientPacketParser and sets them to be selectable - * in the boat selection combo box - */ - private void updateBoatSelectionComboBox() { - ObservableList observableBoats = FXCollections - .observableArrayList(ClientPacketParser.getBoatsPos().values()); - boatSelectionComboBox.setItems(observableBoats); - } - - - /** - * Updates the order of the boats as from the ClientPacketParser and sets them in the boat order - * section - */ - private void updateOrder() { - positionVbox.getChildren().clear(); - positionVbox.getChildren().removeAll(); - positionVbox.getStylesheets().add(getClass().getResource("/css/master.css").toString()); - - // list of racing boat id - ArrayList participants = ClientPacketParser.getXmlObject().getRaceXML() - .getParticipants(); - ArrayList participantIDs = new ArrayList<>(); - for (Participant p : participants) { - participantIDs.add(p.getsourceID()); - } - - if (ClientPacketParser.isRaceStarted()) { - /* - for (Yacht boat : ClientPacketParser.getBoatsPos().values()) { - System.out.println("Hi tjere" + boat.getBoatName()); - if (participantIDs.contains(boat.getSourceId()) || true - ) { // check if the boat is racing - if (boat.getBoatStatus() == 69) { // 3 is finish status - Text textToAdd = new Text(boat.getPosition() + ". " + - boat.getShortName() + " (Finished)"); - textToAdd.setFill(Paint.valueOf("#d3d3d3")); - positionVbox.getChildren().add(textToAdd); - - } else { - Text textToAdd = new Text(boat.getPosition() + ". " + - boat.getShortName() + " "); - textToAdd.setFill(Paint.valueOf("#d3d3d3")); - textToAdd.setStyle(""); - positionVbox.getChildren().add(textToAdd); - System.out.println("Adding " + textToAdd.getText()); - } - } - } - */ - for (Yacht boat : ClientPacketParser.getBoats().values()){ - Text textToAdd = new Text(boat.getSourceId() + ". " + boat.getShortName() + " "); - textToAdd.setFill(Paint.valueOf("#d3d3d3")); - textToAdd.setStyle(""); - positionVbox.getChildren().add(textToAdd); - } - } else { - for (Yacht boat : ClientPacketParser.getBoats().values()) { - if (participantIDs.contains(boat.getSourceId())) { // check if the boat is racing - Text textToAdd = new Text(boat.getPosition() + ". " + - boat.getShortName() + " "); - textToAdd.setFill(Paint.valueOf("#d3d3d3")); - textToAdd.setStyle(""); - positionVbox.getChildren().add(textToAdd); - } - } - } - } - - - private void updateLaylines(BoatGroup bg) { - - Mark nextMark = getNextMark(bg); - Boolean isUpwind = null; - // Can only calc leg direction if there is a next mark and it is a gate mark - if (nextMark != null) { - if (nextMark instanceof GateMark) { - if (bg.isUpwindLeg(includedCanvasController, nextMark)) { - isUpwind = true; - } else { - isUpwind = false; - } - - for(MarkGroup mg : includedCanvasController.getMarkGroups()) { - - mg.removeLaylines(); - - if (mg.getMainMark().getId() == nextMark.getId()) { - - SingleMark singleMark1 = ((GateMark) nextMark).getSingleMark1(); - SingleMark singleMark2 = ((GateMark) nextMark).getSingleMark2(); - Point2D markPoint1 = includedCanvasController.findScaledXY(singleMark1.getLatitude(), singleMark1.getLongitude()); - Point2D markPoint2 = includedCanvasController.findScaledXY(singleMark2.getLatitude(), singleMark2.getLongitude()); - HashMap angleAndSpeed; - if (isUpwind) { - angleAndSpeed = PolarTable - .getOptimalUpwindVMG(ClientPacketParser.getWindSpeed()); - } else { - angleAndSpeed = PolarTable - .getOptimalDownwindVMG(ClientPacketParser.getWindSpeed()); - } - - Double resultingAngle = angleAndSpeed.keySet().iterator().next(); - - - Point2D boatCurrentPos = new Point2D(bg.getBoatLayoutX(), bg.getBoatLayoutY()); - Point2D gateMidPoint = markPoint1.midpoint(markPoint2); - Integer lineFuncResult = GeoUtility.lineFunction(boatCurrentPos, gateMidPoint, markPoint2); - Line rightLayline = new Line(); - Line leftLayline = new Line(); - if (lineFuncResult == 1) { - rightLayline = makeRightLayline(markPoint2, 180 - resultingAngle, - ClientPacketParser - .getWindDirection()); - leftLayline = makeLeftLayline(markPoint1, 180 - resultingAngle, - ClientPacketParser - .getWindDirection()); - } else if (lineFuncResult == -1) { - rightLayline = makeRightLayline(markPoint1, 180 - resultingAngle, - ClientPacketParser - .getWindDirection()); - leftLayline = makeLeftLayline(markPoint2, 180 - resultingAngle, - ClientPacketParser - .getWindDirection()); - } - - leftLayline.setStrokeWidth(0.5); - leftLayline.setStroke(bg.getBoat().getColour()); - - rightLayline.setStrokeWidth(0.5); - rightLayline.setStroke(bg.getBoat().getColour()); - - bg.setLaylines(leftLayline, rightLayline); - mg.addLaylines(leftLayline, rightLayline); - - } - } - } - } - } - - - private Point2D getPointRotation(Point2D ref, Double distance, Double angle){ - Double newX = ref.getX() + (ref.getX() + distance -ref.getX())*Math.cos(angle) - (ref.getY() + distance -ref.getY())*Math.sin(angle); - Double newY = ref.getY() + (ref.getX() + distance -ref.getX())*Math.sin(angle) + (ref.getY() + distance -ref.getY())*Math.cos(angle); - - return new Point2D(newX, newY); - } - - - public Line makeLeftLayline(Point2D startPoint, Double layLineAngle, Double baseAngle) { - - Point2D ep = getPointRotation(startPoint, 50.0, baseAngle + layLineAngle); - Line line = new Line(startPoint.getX(), startPoint.getY(), ep.getX(), ep.getY()); - return line; - - } - - - public Line makeRightLayline(Point2D startPoint, Double layLineAngle, Double baseAngle) { - - Point2D ep = getPointRotation(startPoint, 50.0, baseAngle - layLineAngle); - Line line = new Line(startPoint.getX(), startPoint.getY(), ep.getX(), ep.getY()); - return line; - - } - - - /** - * Initialised the combo box with any boats currently in the race and adds the required listener - * for the combobox to take action upon selection - */ - private void initialiseBoatSelectionComboBox() { - updateBoatSelectionComboBox(); - boatSelectionComboBox.valueProperty().addListener((observable, oldValue, newValue) -> { - //This listener is fired whenever the combo box changes. This means when the values are updated - //We dont want to set the selected value if the values are updated but nothing clicked (null) - if (newValue != null && newValue != selectedBoat) { - Yacht thisYacht = (Yacht) newValue; - setSelectedBoat(thisYacht); - } - }); - } - - - /** - * Display the list of boats in the order they finished the race - */ - private void loadRaceResultView() { - FXMLLoader loader = new FXMLLoader(getClass().getResource("/views/FinishView.fxml")); - - try { - contentAnchorPane.getChildren().removeAll(); - contentAnchorPane.getChildren().clear(); - contentAnchorPane.getChildren().addAll((Pane) loader.load()); - - } catch (javafx.fxml.LoadException e) { - System.err.println(e.getCause()); - } catch (IOException e) { - System.err.println(e); - } - } - - - /** - * Convert seconds to a string of the format mm:ss - * - * @param time the time in seconds - * @return a formatted string - */ - public String convertTimeToMinutesSeconds(int time) { - if (time < 0) { - return String.format("-%02d:%02d", (time * -1) / 60, (time * -1) % 60); - } - return String.format("%02d:%02d", time / 60, time % 60); - } - - private String getTimeSinceStartOfRace() { - String timerString = "0:00"; - if (ClientPacketParser.getTimeSinceStart() > 0) { - String timerMinute = Long.toString(ClientPacketParser.getTimeSinceStart() / 60); - String timerSecond = Long.toString(ClientPacketParser.getTimeSinceStart() % 60); - if (timerSecond.length() == 1) { - timerSecond = "0" + timerSecond; - } - timerString = "-" + timerMinute + ":" + timerSecond; - } else { - String timerMinute = Long.toString(-1 * ClientPacketParser.getTimeSinceStart() / 60); - String timerSecond = Long.toString(-1 * ClientPacketParser.getTimeSinceStart() % 60); - if (timerSecond.length() == 1) { - timerSecond = "0" + timerSecond; - } - timerString = timerMinute + ":" + timerSecond; - } - return timerString; - } - - - boolean isDisplayFps() { - return displayFps; - } - - private void setAnnotations(Integer annotationLevel) { - switch (annotationLevel) { - // No Annotations - case 0: - for (BoatGroup bg : includedCanvasController.getBoatGroups()) { - bg.setVisibility(false, false, false, false, false, false); - } - break; - // Important Annotations - case 1: - for (BoatGroup bg : includedCanvasController.getBoatGroups()) { - bg.setVisibility( - importantAnnotations.getAnnotationState(Annotation.NAME), - importantAnnotations.getAnnotationState(Annotation.SPEED), - importantAnnotations.getAnnotationState(Annotation.ESTTIMETONEXTMARK), - importantAnnotations.getAnnotationState(Annotation.LEGTIME), - importantAnnotations.getAnnotationState(Annotation.TRACK), - importantAnnotations.getAnnotationState(Annotation.WAKE) - ); - } - break; - // All Annotations - case 2: - for (BoatGroup bg : includedCanvasController.getBoatGroups()) { - bg.setVisibility(true, true, true, true, true, true); - } - break; - } - } - - - /** - * Sets all the annotations of the selected boat to be visible and all others to be hidden - * - * @param yacht The yacht for which we want to view all annotations - */ - private void setSelectedBoat(Yacht yacht) { - for (BoatGroup bg : includedCanvasController.getBoatGroups()) { - //We need to iterate over all race groups to get the matching boat group belonging to this boat if we - //are to toggle its annotations, there is no other backwards knowledge of a yacht to its boatgroup. - if (bg.getBoat().getHullID().equals(yacht.getHullID())) { - updateLaylines(bg); - bg.setIsSelected(true); - selectedBoat = yacht; - } else { - bg.setIsSelected(false); - } - } - } - - void setStage(Stage stage) { - this.stage = stage; - } - - Stage getStage() { - return stage; - } - - /** - * Used for when the boat attempts to add data to the sparkline (first checks if the sparkline contains info on it) - * @param yachtId - * @return - */ - public static boolean sparkLineStatus(Integer yachtId) { - return sparkLineData.containsKey(yachtId); - } - -} \ No newline at end of file diff --git a/src/main/java/seng302/controllers/StartScreenController.java b/src/main/java/seng302/controllers/StartScreenController.java deleted file mode 100644 index b7376287..00000000 --- a/src/main/java/seng302/controllers/StartScreenController.java +++ /dev/null @@ -1,164 +0,0 @@ -package seng302.controllers; - -import java.net.Inet4Address; -import java.net.NetworkInterface; -import java.util.Enumeration; -import javafx.fxml.FXML; -import javafx.fxml.FXMLLoader; -import javafx.scene.control.Alert; -import javafx.scene.control.Alert.AlertType; -import javafx.scene.control.TextField; -import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.GridPane; -import javafx.scene.layout.Pane; -import seng302.client.ClientState; -import seng302.client.ClientToServerThread; -import seng302.gameServer.GameState; -import seng302.gameServer.MainServerThread; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; - -/** - * A Class describing the actions of the start screen controller - * Created by wmu16 on 10/07/17. - */ -public class StartScreenController { - - @FXML - private TextField ipTextField; - @FXML - private TextField portTextField; - @FXML - private GridPane startScreen2; - - private Controller controller; - - /** - * Loads the fxml content into the parent pane - * @param jfxUrl - * @return the controller of the fxml - */ - private Object setContentPane(String jfxUrl) { - try { - AnchorPane contentPane = (AnchorPane) startScreen2.getParent(); - contentPane.getChildren().removeAll(); - contentPane.getChildren().clear(); - contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString()); - FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(jfxUrl)); - contentPane.getChildren().addAll((Pane) fxmlLoader.load()); - - return fxmlLoader.getController(); - } catch (javafx.fxml.LoadException e) { - System.out.println("[Controller] FXML load exception"); - } catch (IOException e) { - System.out.println("[Controller] IO exception"); - } - return null; - } - - - /** - * ATTEMPTS TO: - * Sets up a new game state with your IP address as designated as the host. - * Starts a thread to listen for incoming connections. - * Starts a client to server thread and connects to own ip. - * Switches to the lobby screen - */ - @FXML - public void hostButtonPressed() { - try { - String ipAddress = InetAddress.getLocalHost().getHostAddress(); - // get the lobby controller so that we can pass the game server thread to it - new GameState(getLocalHostIp()); - MainServerThread mainServerThread = new MainServerThread(); - ClientState.setHost(true); - // host will connect and handshake to itself after setting up the server - // TODO: 24/07/17 wmu16 - Make port number some static global type constant? - ClientToServerThread clientToServerThread = new ClientToServerThread(ClientState.getHostIp(), 4942); - ClientState.setConnectedToHost(true); - controller.setClientToServerThread(clientToServerThread); - LobbyController lobbyController = (LobbyController) setContentPane("/views/LobbyView.fxml"); - lobbyController.setMainServerThread(mainServerThread); - lobbyController.setController(controller); - } catch (Exception e) { - Alert alert = new Alert(AlertType.ERROR); - alert.setHeaderText("Cannot host"); - alert.setContentText("Oops, failed to host, try to restart."); - alert.showAndWait(); - } - - - } - - /** - * ATTEMPTS TO: - * Connect to an ip address and port using the ip and port specified on start screen. - * Starts a Client To Server Thread to maintain connection to host. - * Switch view to lobby view. - */ - @FXML - public void connectButtonPressed() { - // TODO: 10/07/17 wmu16 - Finish function - try { - String ipAddress = ipTextField.getText().trim().toLowerCase(); - Integer port = Integer.valueOf(portTextField.getText().trim()); - - ClientToServerThread clientToServerThread = new ClientToServerThread(ipAddress, port); - ClientState.setHost(false); - ClientState.setConnectedToHost(true); - - ClientState.setHostIp(ipAddress); - controller.setClientToServerThread(clientToServerThread); - LobbyController lobbyController = (LobbyController) setContentPane("/views/LobbyView.fxml"); - lobbyController.setController(controller); - } catch (Exception e) { - Alert alert = new Alert(AlertType.ERROR); - alert.setHeaderText("Cannot reach the host"); - alert.setContentText("Please check your host IP address."); - alert.showAndWait(); - } - } - - public void setController(Controller controller) { - this.controller = controller; - } - - /** - * Gets the local host ip address and sets this ip to ClientState. - * Only runs by the host. - * - * @return the localhost ip address - */ - private String getLocalHostIp() { - String ipAddress = null; - try { - Enumeration e = NetworkInterface.getNetworkInterfaces(); - while (e.hasMoreElements()) { - NetworkInterface ni = e.nextElement(); - if (ni.isLoopback()) - continue; - if(ni.isPointToPoint()) - continue; - if(ni.isVirtual()) - continue; - - Enumeration addresses = ni.getInetAddresses(); - while(addresses.hasMoreElements()) { - InetAddress address = addresses.nextElement(); - if(address instanceof Inet4Address) { // skip all ipv6 - ipAddress = address.getHostAddress(); - } - } - } - } catch (Exception e) { - System.out.println("[StartScreenController] Exception"); - } - if (ipAddress == null) { - System.out.println("[HOST] Cannot obtain local host ip address."); - } - ClientState.setHostIp(ipAddress); - return ipAddress; - } -} diff --git a/src/main/java/seng302/fxObjects/BoatAnnotations.java b/src/main/java/seng302/fxObjects/BoatAnnotations.java deleted file mode 100644 index c9d7e90c..00000000 --- a/src/main/java/seng302/fxObjects/BoatAnnotations.java +++ /dev/null @@ -1,154 +0,0 @@ -package seng302.fxObjects; - -import javafx.scene.CacheHint; -import javafx.scene.Group; -import javafx.scene.paint.Color; -import javafx.scene.shape.Rectangle; -import javafx.scene.text.Text; -import seng302.client.ClientPacketParser; -import seng302.models.Yacht; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; - -/** - * Collection of annotations for boats. - */ -public class BoatAnnotations extends Group{ - - //Text offset constants - private static final double X_OFFSET_TEXT = 18d; - private static final double Y_OFFSET_TEXT_INIT = -29d; - private static final double Y_OFFSET_PER_TEXT = 12d; - //Background constants - private static final double TEXT_BUFFER = 3; - private static final double BACKGROUND_X = X_OFFSET_TEXT - TEXT_BUFFER; - private static final double BACKGROUND_Y = Y_OFFSET_TEXT_INIT - TEXT_BUFFER; - private static final double BACKGROUND_H_PER_TEXT = 9.5d; - private static final double BACKGROUND_W = 125d; - private static final double BACKGROUND_ARC_SIZE = 10; - - private Rectangle background = new Rectangle(); - private Text teamNameObject; - private Text velocityObject; - private Text estTimeToNextMarkObject; - private Text legTimeObject; - private boolean isPlayer = false; - private Yacht boat; - - BoatAnnotations (Yacht boat, Color theme) { - super.setCache(true); - this.boat = boat; - background.setX(BACKGROUND_X); - background.setY(BACKGROUND_Y); - background.setWidth(BACKGROUND_W); - background.setHeight(Math.abs(BACKGROUND_X) + TEXT_BUFFER + BACKGROUND_H_PER_TEXT * 4); - background.setArcHeight(BACKGROUND_ARC_SIZE); - background.setArcWidth(BACKGROUND_ARC_SIZE); - background.setFill(new Color(1, 1, 1, 0.75)); - background.setStroke(theme); - background.setStrokeWidth(2); - background.setCache(true); - background.setCacheHint(CacheHint.SPEED); - - teamNameObject = getTextObject(boat.getShortName(), theme); - teamNameObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT); - - velocityObject = getTextObject("0 m/s", theme); - velocityObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * 2); - velocityObject.setVisible(false); - - estTimeToNextMarkObject = getTextObject("Next mark: ", theme); - estTimeToNextMarkObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * 3); - estTimeToNextMarkObject.setVisible(false); - - legTimeObject = getTextObject("Last mark: -", theme); - legTimeObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * 4); - legTimeObject.setVisible(false); - - this.setVisible(true, false, false, false); - - super.getChildren().addAll(background, teamNameObject, velocityObject, estTimeToNextMarkObject, legTimeObject); - } - - /** - * Return a text object with caching and a color applied - * - * @param defaultText The default text to display - * @param fill The text fill color - * @return The text object - */ - private Text getTextObject(String defaultText, Color fill) { - Text text = new Text(defaultText); - text.setFill(fill); - text.setStrokeWidth(2); - text.setCacheHint(CacheHint.SPEED); - text.setCache(true); - return text; - } - - void update () { - teamNameObject.setText("Player: " + boat.getShortName()); - - velocityObject.setText(String.format(String.format("%.2f m/s", boat.getVelocityMMS()))); - - if (boat.getTimeTillNext() != null) { - DateFormat format = new SimpleDateFormat("mm:ss"); - String timeToNextMark = format - .format(boat.getTimeTillNext() - ClientPacketParser.getCurrentTimeLong()); - estTimeToNextMarkObject.setText("Next mark: " + timeToNextMark); - } else { - estTimeToNextMarkObject.setText("Next mark: -"); - } - - if (boat.getMarkRoundTime() != null) { - DateFormat format = new SimpleDateFormat("mm:ss"); - String elapsedTime = format - .format(ClientPacketParser.getCurrentTimeLong() - boat.getMarkRoundTime()); - legTimeObject.setText("Last mark: " + elapsedTime); - }else { - legTimeObject.setText("Last mark: - "); - } - } - - void setVisible(boolean nameVisibility, boolean speedVisibility, - boolean estTimeVisibility, boolean lastMarkVisibility) { - int totalVisible = 0; - - /* - This is a temporary fix until the new annotation group is added along with the visualiser - overhaul. - */ - totalVisible = updateVisibility(nameVisibility, teamNameObject, totalVisible); - if (isPlayer) - totalVisible = updateVisibility(speedVisibility, velocityObject, totalVisible); -// totalVisible = updateVisibility(estTimeVisibility, estTimeToNextMarkObject, totalVisible); -// totalVisible = updateVisibility(lastMarkVisibility, legTimeObject, totalVisible); - if (totalVisible != 0) { - background.setVisible(true); - background.setHeight(Math.abs(BACKGROUND_X) + TEXT_BUFFER + BACKGROUND_H_PER_TEXT * totalVisible); - } else { - background.setVisible(false); - } - } - - private int updateVisibility (boolean visibility, Text text, int totalVisible) { - if (visibility){ - totalVisible ++; - text.setVisible(true); - text.setLayoutX(X_OFFSET_TEXT); - text.setLayoutY(Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * totalVisible); - } else { - text.setVisible(false); - } - return totalVisible; - } - - /** - * Sets these annotations to show more detailed info. - */ - public void setAsPlayer () { - isPlayer = true; - velocityObject.setVisible(true); - } -} diff --git a/src/main/java/seng302/fxObjects/BoatGroup.java b/src/main/java/seng302/fxObjects/BoatGroup.java deleted file mode 100644 index 2446f9d4..00000000 --- a/src/main/java/seng302/fxObjects/BoatGroup.java +++ /dev/null @@ -1,349 +0,0 @@ -package seng302.fxObjects; - -import java.util.ArrayList; - -import javafx.geometry.Point2D; -import javafx.scene.CacheHint; -import javafx.scene.Group; -import javafx.scene.paint.Color; -import javafx.scene.shape.Circle; -import javafx.scene.shape.Line; -import javafx.scene.shape.Polygon; -import javafx.scene.transform.Rotate; -import seng302.client.ClientPacketParser; -import seng302.models.Yacht; -import seng302.utilities.GeoUtility; -import seng302.controllers.CanvasController; -import seng302.models.mark.GateMark; -import seng302.models.mark.Mark; -import seng302.models.mark.SingleMark; - -/** - * BoatGroup is a javafx group that by default contains a graphical objects for representing a 2 - * dimensional boat. It contains a single polygon for the boat, a group of lines to show it's path, - * a wake object and two text labels to annotate the boat teams name and the boats velocity. The - * boat will update it's position onscreen everytime UpdatePosition is called unless the window is - * minimized in which case it attempts to store animations and apply them when the window is - * maximised. - */ -public class BoatGroup extends Group { - - //Constants for drawing - private static final double BOAT_HEIGHT = 15d; - private static final double BOAT_WIDTH = 10d; - //Variables for boat logic. - private boolean isStopped = true; - private double xIncrement; - private double yIncrement; - private long lastTimeValid = 0; - private Double lastRotation = 0.0; - private long framesToMove; - //Graphical objects - private Yacht boat; - private Group lineGroup = new Group(); - private Polygon boatPoly; - private Wake wake; - private Line leftLayLine; - private Line rightLayline; - private Double distanceTravelled = 0.0; - private Point2D lastPoint; - private boolean destinationSet; - private BoatAnnotations boatAnnotations; - private Color color; - private Boolean isSelected = true; //All boats are initialised as selected\ - private boolean isPlayer = false; - - /** - * Creates a BoatGroup with the default triangular boat polygon. - * - * @param boat The boat that the BoatGroup will represent. Must contain an ID which will be used - * to tell which BoatGroup to update. - * @param color The colour of the boat polygon and the trailing line. - */ - public BoatGroup(Yacht boat, Color color) { - destinationSet = false; - this.boat = boat; - initChildren(color); - } - - /** - * Creates a BoatGroup with the boat being the default polygon. The head of the boat should be - * at point (0,0). - * - * @param boat The boat that the BoatGroup will represent. Must contain an ID which will be used - * to tell which BoatGroup to update. - * @param color The colour of the boat polygon and the trailing line. - * @param points An array of co-ordinates x1,y1,x2,y2,x3,y3... that will make up the boat - * polygon. - */ - public BoatGroup(Yacht boat, Color color, double... points) { - destinationSet = false; - this.boat = boat; - initChildren(color, points); - } - - /** - * Creates the javafx objects that will be the in the group by default. - * - * @param color The colour of the boat polygon and the trailing line. - * @param points An array of co-ordinates x1,y1,x2,y2,x3,y3... that will make up the boat - * polygon. - */ - private void initChildren(Color color, double... points) { - this.color = color; - boatPoly = new Polygon(points); - boatPoly.setFill(this.color); - boatPoly.setOnMouseEntered(event -> { - boatPoly.setFill(Color.FLORALWHITE); - boatPoly.setStroke(Color.RED); - }); - boatPoly.setOnMouseExited(event -> { - boatPoly.setFill(this.color); - boatPoly.setStroke(Color.BLACK); - }); - boatPoly.setOnMouseClicked(event -> setIsSelected(!isSelected)); - boatPoly.setCache(true); - boatPoly.setCacheHint(CacheHint.SPEED); - boatAnnotations = new BoatAnnotations(boat, this.color); - - leftLayLine = new Line(); - rightLayline = new Line(); - - wake = new Wake(0, -BOAT_HEIGHT); - super.getChildren().addAll(boatPoly, boatAnnotations); - } - - /** - * Creates the javafx objects that will be the in the group by default. - * - * @param color The colour of the boat polygon and the trailing line. - */ - private void initChildren(Color color) { - initChildren(color, - -BOAT_WIDTH / 2, BOAT_HEIGHT / 2, - 0.0, -BOAT_HEIGHT / 2, - BOAT_WIDTH / 2, BOAT_HEIGHT / 2); - } - - /** - * Moves the boat and its children annotations from its current coordinates by specified - * amounts. - * - * @param dx The amount to move the X coordinate by - * @param dy The amount to move the Y coordinate by - */ - private void moveGroupBy(double dx, double dy) { - boatPoly.setLayoutX(boatPoly.getLayoutX() + dx); - boatPoly.setLayoutY(boatPoly.getLayoutY() + dy); - boatAnnotations.setLayoutX(boatAnnotations.getLayoutX() + dx); - boatAnnotations.setLayoutY(boatAnnotations.getLayoutY() + dy); - wake.setLayoutX(wake.getLayoutX() + dx); - wake.setLayoutY(wake.getLayoutY() + dy); - } - - - /** - * Moves the boat and its children annotations to coordinates specified - * - * @param x The X coordinate to move the boat to - * @param y The Y coordinate to move the boat to - */ - private void moveTo(double x, double y, double rotation) { - rotateTo(rotation); - boatPoly.setLayoutX(x); - boatPoly.setLayoutY(y); - boatAnnotations.setLayoutX(x); - boatAnnotations.setLayoutY(y); - wake.setLayoutX(x); - wake.setLayoutY(y); - wake.rotate(rotation); - } - - private void rotateTo(double rotation) { - boatPoly.getTransforms().setAll(new Rotate(rotation)); - } - - /** - * Sets the destination of the boat and the headng it should have once it reaches - * - * @param newXValue The X co-ordinate the boat needs to move to. - * @param newYValue The Y co-ordinate the boat needs to move to. - * @param rotation Rotation to move graphics to. - * @param timeValid the time the position values are valid for - */ - public void setDestination(double newXValue, double newYValue, double rotation, - double groundSpeed, long timeValid, double frameRate) { - - destinationSet = true; - Double dx = Math.abs(boatPoly.getLayoutX() - newXValue); - Double dy = Math.abs(boatPoly.getLayoutY() - newYValue); - moveTo(newXValue, newYValue, rotation); - - - rotateTo(rotation); - wake.setRotation(rotation, groundSpeed); - boat.setVelocity(groundSpeed); - isStopped = false; - lastRotation = rotation; - boatAnnotations.update(); - - distanceTravelled += Math.sqrt((dx * dx) + (dy * dy)); - - if (distanceTravelled > 10 && isPlayer) { - distanceTravelled = 0d; - - if (lastPoint != null) { - Line l = new Line( - lastPoint.getX(), - lastPoint.getY(), - boatPoly.getLayoutX(), - boatPoly.getLayoutY() - ); - l.getStrokeDashArray().setAll(3d, 7d); - l.setStroke(boat.getColour()); - l.setCache(true); - l.setCacheHint(CacheHint.SPEED); - lineGroup.getChildren().add(l); - } - if (destinationSet) { - lastPoint = new Point2D(boatPoly.getLayoutX(), boatPoly.getLayoutY()); - } - } - } - - - /** - * This function works out if a boat is going upwind or down wind. It looks at the boats current position, the next - * gates position and the current wind - * If bot the wind vector from the next gate and the boat from the next gate lay on the same side, then the boat is - * going up wind, if they are on different sides of the gate, then the boat is going downwind - * @param canvasController - */ - public Boolean isUpwindLeg(CanvasController canvasController, Mark nextMark) { - - Double windAngle = ClientPacketParser.getWindDirection(); - GateMark thisGateMark = (GateMark) nextMark; - SingleMark nextMark1 = thisGateMark.getSingleMark1(); - SingleMark nextMark2 = thisGateMark.getSingleMark2(); - Point2D nextMarkPoint1 = canvasController.findScaledXY(nextMark1.getLatitude(), nextMark1.getLongitude()); - Point2D nextMarkPoint2 = canvasController.findScaledXY(nextMark2.getLatitude(), nextMark2.getLongitude()); - - Point2D boatCurrentPoint = new Point2D(boatPoly.getLayoutX(), boatPoly.getLayoutY()); - Point2D windTestPoint = GeoUtility.makeArbitraryVectorPoint(nextMarkPoint1, windAngle, 10d); - - - Integer boatLineFuncResult = GeoUtility.lineFunction(nextMarkPoint1, nextMarkPoint2, boatCurrentPoint); - Integer windLineFuncResult = GeoUtility.lineFunction(nextMarkPoint1, nextMarkPoint2, windTestPoint); - - - /* - If both the wind vector from the gate and the boat from the gate are on the same side of that gate, then the - boat is travelling into the wind. thus upwind. Otherwise if they are on different sides, then the boat is going - with the wind. - */ - if (boatLineFuncResult == windLineFuncResult) { - return true; - } else { - return false; - } - - } - - - public void setIsSelected(Boolean isSelected) { - this.isSelected = isSelected; - setLineGroupVisible(isSelected); - setWakeVisible(isSelected); - boatAnnotations.setVisible(isSelected); - setLayLinesVisible(isSelected); - } - - public void setVisibility (boolean teamName, boolean velocity, boolean estTime, boolean legTime, boolean trail, boolean wake) { - boatAnnotations.setVisible(teamName, velocity, estTime, legTime); - this.wake.setVisible(wake); - this.lineGroup.setVisible(trail); - } - - public void setLineGroupVisible(Boolean visible) { - lineGroup.setVisible(visible); - } - - public void setWakeVisible(Boolean visible) { - wake.setVisible(visible); - } - - public void setLayLinesVisible(Boolean visible) { - leftLayLine.setVisible(visible); - rightLayline.setVisible(visible); - } - - public void setLaylines(Line line1, Line line2) { - this.leftLayLine = line1; - this.rightLayline = line2; - } - - public ArrayList getLaylines() { - ArrayList laylines = new ArrayList<>(); - laylines.add(leftLayLine); - laylines.add(rightLayline); - return laylines; - } - - public Yacht getBoat() { - return boat; - } - - /** - * Returns all raceIds associated with this group. For BoatGroups the ID's are for the boat. - * - * @return An array containing all ID's associated with this RaceObject. - */ - public long getRaceId() { - return boat.getSourceId(); - } - - public Group getWake () { - return wake; - } - - public Group getTrail() { - return lineGroup; - } - - public Group getAnnotations() { - return boatAnnotations; - } - - public Double getBoatLayoutX() { - return boatPoly.getLayoutX(); - } - - - public Double getBoatLayoutY() { - return boatPoly.getLayoutY(); - } - - public boolean isStopped() { - return isStopped; - } - - @Override - public String toString() { - return boat.toString(); - } - - /** - * Sets this boat to appear highlighted - */ - public void setAsPlayer() { - boatPoly.getPoints().setAll( - -BOAT_WIDTH / 1.75, BOAT_HEIGHT / 1.75, - 0.0, -BOAT_HEIGHT / 1.75, - BOAT_WIDTH / 1.75, BOAT_HEIGHT / 1.75 - ); - boatPoly.setStroke(Color.BLACK); - boatPoly.setStrokeWidth(3); - boatAnnotations.setAsPlayer(); - isPlayer = true; - } -} \ No newline at end of file diff --git a/src/main/java/seng302/fxObjects/MarkGroup.java b/src/main/java/seng302/fxObjects/MarkGroup.java deleted file mode 100644 index 597338a1..00000000 --- a/src/main/java/seng302/fxObjects/MarkGroup.java +++ /dev/null @@ -1,167 +0,0 @@ -package seng302.fxObjects; - -import java.util.ArrayList; -import java.util.List; -import javafx.geometry.Point2D; -import javafx.scene.Group; -import javafx.scene.Node; -import javafx.scene.paint.Color; -import javafx.scene.shape.Circle; -import javafx.scene.shape.Line; -import seng302.models.mark.GateMark; -import seng302.models.mark.Mark; -import seng302.models.mark.MarkType; -import seng302.models.mark.SingleMark; - -/** - * Grouping of javaFX objects needed to represent a Mark on screen. - */ -public class MarkGroup extends Group { - - private static int MARK_RADIUS = 5; - private static int LINE_THICKNESS = 2; - private static double DASHED_GAP_LEN = 2d; - private static double DASHED_LINE_LEN = 5d; - - private List marks = new ArrayList<>(); - private Mark mainMark; - - /** - * Constructor for singleMark groups - * @param mark - * @param points - */ - public MarkGroup (SingleMark mark, Point2D points) { - marks.add(mark); - mainMark = mark; - Color color = Color.BLACK; - if (mark.getName().equals("Start")){ - color = Color.GREEN; - } else if (mark.getName().equals("Finish")){ - color = Color.RED; - } - Circle markCircle; - markCircle = new Circle( - points.getX(), - points.getY(), - MARK_RADIUS, - color - ); - super.getChildren().add(markCircle); - } - - public void addLaylines(Line line1, Line line2) { - - super.getChildren().addAll(line1, line2); - } - - - public void removeLaylines() { - ArrayList toRemove = new ArrayList<>(); - for(Node node : super.getChildren()) { - if (node instanceof Line) { - Line layLine = (Line) node; - - /*** - * OOHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHhhh - */ - if (layLine.getStrokeWidth() == 0.5){ - toRemove.add(layLine); - } - } - } - super.getChildren().removeAll(toRemove); - } - - public MarkGroup(GateMark mark, Point2D points1, Point2D points2) { - marks.add(mark.getSingleMark1()); - marks.add(mark.getSingleMark2()); - mainMark = mark; - Color color = Color.BLACK; - if (mark.getName().equals("Start")){ - color = Color.GREEN; - } else if (mark.getName().equals("Finish")){ - color = Color.RED; - } - Circle markCircle; - markCircle = new Circle( - points1.getX(), - points1.getY(), - MARK_RADIUS, - color - ); - super.getChildren().add(markCircle); - - markCircle = new Circle( - points2.getX(), - points2.getY(), - MARK_RADIUS, - color - ); - super.getChildren().add(markCircle); - Line line = new Line( - points1.getX(), - points1.getY(), - points2.getX(), - points2.getY() - ); - line.setStrokeWidth(LINE_THICKNESS); - line.setStroke(color); - if (mark.getMarkType() == MarkType.OPEN_GATE) { - line.getStrokeDashArray().addAll(DASHED_GAP_LEN, DASHED_LINE_LEN); - } - super.getChildren().add(line); - - } - - public void moveMarkTo (double x, double y, long raceId) - { - if (mainMark.getMarkType() == MarkType.SINGLE_MARK) { - Circle markCircle = (Circle) super.getChildren().get(0); - //One of the test streams produced frequent, jittery movements. Added this as a fix. - if (Math.abs(markCircle.getCenterX() - x) > 5 || Math.abs(markCircle.getCenterY() - y) > 5) { - markCircle.setCenterX(x); - markCircle.setCenterY(y); - } - } else { - Circle markCircle1 = (Circle) super.getChildren().get(0); - Circle markCircle2 = (Circle) super.getChildren().get(1); - Line connectingLine = (Line) super.getChildren().get(2); - if (marks.get(0).getId() == raceId) { - if (Math.abs(markCircle1.getCenterX() - x) > 5 || Math.abs(markCircle1.getCenterY() - y) > 5) { - markCircle1.setCenterX(x); - markCircle1.setCenterY(y); - connectingLine.setStartX(markCircle1.getCenterX()); - connectingLine.setStartY(markCircle1.getCenterY()); - } - } else if (marks.get(1).getId() == raceId) { - if (Math.abs(markCircle2.getCenterX() - x) > 5 || Math.abs(markCircle2.getCenterY() - y) > 5) { - markCircle2.setCenterX(x); - markCircle2.setCenterY(y); - connectingLine.setEndX(markCircle2.getCenterX()); - connectingLine.setEndY(markCircle2.getCenterY()); - } - } - } - } - - public boolean hasRaceId (int... raceIds) { - for (int id : raceIds) - for (Mark mark : marks) - if (id == mark.getId()) - return true; - return false; - } - - public long[] getRaceIds () { - long[] idArray = new long[marks.size()]; - int i = 0; - for (Mark mark : marks) - idArray[i++] = mark.getId(); - return idArray; - } - - public Mark getMainMark() { - return mainMark; - } -} \ No newline at end of file diff --git a/src/main/java/seng302/gameServer/ClientConnectionDelegate.java b/src/main/java/seng302/gameServer/ClientConnectionDelegate.java index fab71cd7..e44029c0 100644 --- a/src/main/java/seng302/gameServer/ClientConnectionDelegate.java +++ b/src/main/java/seng302/gameServer/ClientConnectionDelegate.java @@ -1,6 +1,6 @@ package seng302.gameServer; -import seng302.models.Player; +import seng302.model.Player; public interface ClientConnectionDelegate { /** diff --git a/src/main/java/seng302/gameServer/GameState.java b/src/main/java/seng302/gameServer/GameState.java index 16eb8050..0b36ab16 100644 --- a/src/main/java/seng302/gameServer/GameState.java +++ b/src/main/java/seng302/gameServer/GameState.java @@ -1,68 +1,155 @@ package seng302.gameServer; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import seng302.client.ClientPacketParser; -import seng302.models.Player; -import seng302.models.Yacht; -import seng302.server.messages.BoatActionType; +import java.util.Set; +import javafx.scene.paint.Color; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import seng302.gameServer.messages.BoatAction; +import seng302.gameServer.messages.BoatStatus; +import seng302.gameServer.messages.CustomizeRequestType; +import seng302.gameServer.messages.MarkRoundingMessage; +import seng302.gameServer.messages.MarkType; +import seng302.gameServer.messages.Message; +import seng302.gameServer.messages.RoundingBoatStatus; +import seng302.gameServer.messages.YachtEventCodeMessage; +import seng302.model.GeoPoint; +import seng302.model.Limit; +import seng302.model.Player; +import seng302.model.PolarTable; +import seng302.model.ServerYacht; +import seng302.model.mark.CompoundMark; +import seng302.model.mark.Mark; +import seng302.model.mark.MarkOrder; +import seng302.utilities.GeoUtility; +import seng302.utilities.XMLParser; /** * A Static class to hold information about the current state of the game (model) + * Also contains logic for updating itself on regular time intervals on its own thread * Created by wmu16 on 10/07/17. */ public class GameState implements Runnable { - private static Integer STATE_UPDATES_PER_SECOND = 60; + @FunctionalInterface + interface NewMessageListener { + + void notify(Message message); + } + + private Logger logger = LoggerFactory.getLogger(GameState.class); + + private static final Integer STATE_UPDATES_PER_SECOND = 60; + public static Integer MAX_PLAYERS = 8; + public static Double ROUNDING_DISTANCE = 50d; // TODO: 14/08/17 wmu16 - Look into this value further + public static final Double MARK_COLLISION_DISTANCE = 15d; + public static final Double YACHT_COLLISION_DISTANCE = 25.0; + public static final Double BOUNCE_DISTANCE_MARK = 20.0; + public static final Double BOUNCE_DISTANCE_YACHT = 30.0; + public static final Double COLLISION_VELOCITY_PENALTY = 0.3; private static Long previousUpdateTime; public static Double windDirection; private static Double windSpeed; + private static Boolean customizationFlag; // dirty flag to tell if a player has customized their boat. + private static String hostIpAddress; private static List players; - private static Map yachts; + private static Map yachts; private static Boolean isRaceStarted; private static GameStages currentStage; + private static MarkOrder markOrder; + private static long startTime; + private static Set marks; + private static List courseLimit; - private static long startTime = System.currentTimeMillis(); + private static List markListeners; + private static Map playerStringMap = new HashMap<>(); + /* + Ideally I would like to make this class an object instantiated by the server and given to + it's created threads if necessary. Outside of that I think the dependencies on it + (atm only Yacht & GameClient) can be removed from most other classes. The observable list of + players could be pulled directly from the server by the GameClient since it instantiates it + and it is reasonable for it to pull data. The current setup of publicly available statics is + pretty meh IMO because anything can change it making it unreliable and like people did with + the old ServerParser class everything that needs shared just gets thrown in the static + collections and things become a real mess. + */ public GameState(String hostIpAddress) { windDirection = 180d; windSpeed = 10000d; this.hostIpAddress = hostIpAddress; + yachts = new HashMap<>(); players = new ArrayList<>(); + GameState.hostIpAddress = hostIpAddress; + customizationFlag = false; + currentStage = GameStages.LOBBYING; isRaceStarted = false; - yachts = new HashMap<>(); //set this when game stage changes to prerace previousUpdateTime = System.currentTimeMillis(); - yachts = new HashMap<>(); + markOrder = new MarkOrder(); //This could be instantiated at some point with a select map? + markListeners = new ArrayList<>(); - new Thread(this).start(); + resetStartTime(); + new Thread(this, "GameState").start(); //Run the auto updates on the game state + + marks = new MarkOrder().getAllMarks(); + setCourseLimit("/server_config/race.xml"); + } + + private void setCourseLimit(String url) { + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + DocumentBuilder documentBuilder; + Document document = null; + try { + documentBuilder = documentBuilderFactory.newDocumentBuilder(); + document = documentBuilder.parse(new InputSource(getClass().getResourceAsStream(url))); + } catch (Exception e) { + // sorry, we have to catch general one, otherwise we have to catch five different exceptions. + logger.trace("Failed to load course limit for boundary collision detection.", e); + } + courseLimit = XMLParser.parseRace(document).getCourseLimit(); } public static String getHostIpAddress() { return hostIpAddress; } + public static Set getMarks() { + return Collections.unmodifiableSet(marks); + } + public static List getPlayers() { return players; } - + public static void addPlayer(Player player) { players.add(player); - } - - public static void removePlayer(Player player) { - players.remove(player); + String playerText = player.getYacht().getSourceId() + " " + player.getYacht().getBoatName() + + " " + player.getYacht().getCountry(); + playerStringMap.put(player, playerText); } - public static void addYacht(Integer sourceId, Yacht yacht) { + public static void removePlayer(Player player) { + players.remove(player); + playerStringMap.remove(player); + } + + public static void addYacht(Integer sourceId, ServerYacht yacht) { yachts.put(sourceId, yacht); } @@ -79,77 +166,49 @@ public class GameState implements Runnable { } public static void setCurrentStage(GameStages currentStage) { - if (currentStage == GameStages.RACING){ - startTime = System.currentTimeMillis(); - } - GameState.currentStage = currentStage; } - public static long getStartTime(){ + public static MarkOrder getMarkOrder() { + return markOrder; + } + + public static long getStartTime() { return startTime; } + public static void resetStartTime(){ + startTime = System.currentTimeMillis() + MainServerThread.TIME_TILL_START; + } + public static Double getWindDirection() { return windDirection; } + public static void setWindDirection(Double newWindDirection) { + windDirection = newWindDirection; + } + + public static void setWindSpeed(Double newWindSpeed) { + windSpeed = newWindSpeed; + } + public static Double getWindSpeedMMS() { return windSpeed; } public static Double getWindSpeedKnots() { - return windSpeed / 1000 * ClientPacketParser.MS_TO_KNOTS; + return GeoUtility.mmsToKnots(windSpeed); // TODO: 26/07/17 cir27 - remove magic numbers } - public static Map getYachts() { + public static Map getYachts() { return yachts; } - public static void updateBoat(Integer sourceId, BoatActionType actionType) { - Yacht playerYacht = yachts.get(sourceId); -// System.out.println("-----------------------"); - switch (actionType) { - case VMG: - playerYacht.turnToVMG(); -// System.out.println("Snapping to VMG"); - break; - case SAILS_IN: - playerYacht.toggleSailIn(); -// System.out.println("Toggling Sails"); - break; - case SAILS_OUT: - playerYacht.toggleSailIn(); -// System.out.println("Toggling Sails"); - break; - case TACK_GYBE: - playerYacht.tackGybe(windDirection); -// System.out.println("Tack/Gybe"); - break; - case UPWIND: - playerYacht.turnUpwind(); -// System.out.println("Moving upwind"); - break; - case DOWNWIND: - playerYacht.turnDownwind(); -// System.out.println("Moving downwind"); - break; - } - -// printBoatStatus(playerYacht); - } - - public static void update() { - Long timeInterval = System.currentTimeMillis() - previousUpdateTime; - previousUpdateTime = System.currentTimeMillis(); - for (Yacht yacht : yachts.values()) { - yacht.update(timeInterval); - } - } - /** * Generates a new ID based off the size of current players + 1 + * * @return a playerID to be allocated to a new connetion */ public static Integer getUniquePlayerID() { @@ -164,30 +223,480 @@ public class GameState implements Runnable { @Override public void run() { - while(true) { + while (currentStage != GameStages.FINISHED) { try { Thread.sleep(1000 / STATE_UPDATES_PER_SECOND); } catch (InterruptedException e) { System.out.println("[GameState] interrupted exception"); } - if (currentStage == GameStages.PRE_RACE) { + if (currentStage == GameStages.PRE_RACE || currentStage == GameStages.RACING) { update(); } - //RACING if (currentStage == GameStages.RACING) { update(); } } } - private static void printBoatStatus(Yacht playerYacht) { - System.out.println("-----------------------"); - System.out.println("Sails are in: " + playerYacht.getSailIn()); - System.out.println("Heading: " + playerYacht.getHeading()); - System.out.println("Velocity: " + playerYacht.getVelocityMMS() / 1000); - System.out.println("Lat: " + playerYacht.getLocation().getLat()); - System.out.println("Lng: " + playerYacht.getLocation().getLng()); - System.out.println("-----------------------\n"); + public static void updateBoat(Integer sourceId, BoatAction actionType) { + ServerYacht playerYacht = yachts.get(sourceId); + switch (actionType) { + case VMG: + playerYacht.turnToVMG(); + break; + case SAILS_IN: + playerYacht.toggleSailIn(); + break; + case SAILS_OUT: + playerYacht.toggleSailIn(); + break; + case TACK_GYBE: + playerYacht.tackGybe(windDirection); + break; + case UPWIND: + playerYacht.turnUpwind(); + break; + case DOWNWIND: + playerYacht.turnDownwind(); + break; + } + } + + /** + * Called periodically in this GameState thread to update the GameState values + */ + public void update() { + Boolean raceFinished = true; + + Double timeInterval = (System.currentTimeMillis() - previousUpdateTime) / 1000000.0; + previousUpdateTime = System.currentTimeMillis(); + if (System.currentTimeMillis() > startTime) { + GameState.setCurrentStage(GameStages.RACING); + } + for (ServerYacht yacht : yachts.values()) { + updateVelocity(yacht); + yacht.runAutoPilot(); + yacht.updateLocation(timeInterval); + if (yacht.getBoatStatus() != BoatStatus.FINISHED) { + checkCollision(yacht); + checkForLegProgression(yacht); + raceFinished = false; + } + + + } + + if (raceFinished) { + currentStage = GameStages.FINISHED; + } + } + + /** + * Check if the yacht has crossed the course limit + * + * @param yacht the yacht to be tested + * @return a boolean value of if there is a boundary collision + */ + private static Boolean checkBoundaryCollision(ServerYacht yacht) { + for (int i = 0; i < courseLimit.size() - 1; i++) { + if (GeoUtility.checkCrossedLine(courseLimit.get(i), courseLimit.get(i + 1), + yacht.getLastLocation(), yacht.getLocation()) != 0) { + return true; + } + } + if (GeoUtility.checkCrossedLine(courseLimit.get(courseLimit.size() - 1), courseLimit.get(0), + yacht.getLastLocation(), yacht.getLocation()) != 0) { + return true; + } + return false; + } + + public static void checkCollision(ServerYacht serverYacht) { + ServerYacht collidedYacht = checkYachtCollision(serverYacht); + if (collidedYacht != null) { + GeoPoint originalLocation = serverYacht.getLocation(); + serverYacht.setLocation( + calculateBounceBack(serverYacht, originalLocation, BOUNCE_DISTANCE_YACHT) + ); + serverYacht.setCurrentVelocity( + serverYacht.getCurrentVelocity() * COLLISION_VELOCITY_PENALTY + ); + collidedYacht.setLocation( + calculateBounceBack(collidedYacht, originalLocation, BOUNCE_DISTANCE_YACHT) + ); + collidedYacht.setCurrentVelocity( + collidedYacht.getCurrentVelocity() * COLLISION_VELOCITY_PENALTY + ); + notifyMessageListeners( + new YachtEventCodeMessage(serverYacht.getSourceId()) + ); + } else { + Mark collidedMark = checkMarkCollision(serverYacht); + if (collidedMark != null) { + serverYacht.setLocation( + calculateBounceBack(serverYacht, collidedMark, BOUNCE_DISTANCE_MARK) + ); + serverYacht.setCurrentVelocity( + serverYacht.getCurrentVelocity() * COLLISION_VELOCITY_PENALTY + ); + notifyMessageListeners( + new YachtEventCodeMessage(serverYacht.getSourceId()) + ); + } + else{ + if (checkBoundaryCollision(serverYacht)) { + serverYacht.setLocation( + calculateBounceBack(serverYacht, serverYacht.getLocation(), + BOUNCE_DISTANCE_YACHT) + ); + serverYacht.setCurrentVelocity( + serverYacht.getCurrentVelocity() * COLLISION_VELOCITY_PENALTY + ); + notifyMessageListeners( + new YachtEventCodeMessage(serverYacht.getSourceId()) + ); + } + } + } + } + + + private void updateVelocity(ServerYacht yacht) { + Double velocity = yacht.getCurrentVelocity(); + Double trueWindAngle = Math.abs(windDirection - yacht.getHeading()); + Double boatSpeedInKnots = PolarTable.getBoatSpeed(getWindSpeedKnots(), trueWindAngle); + Double maxBoatSpeed = GeoUtility.knotsToMMS(boatSpeedInKnots); + // TODO: 15/08/17 remove magic numbers from these equations. + if (yacht.getSailIn()) { + if (velocity < maxBoatSpeed - 500) { + yacht.changeVelocity(maxBoatSpeed / 100); + } else if (velocity > maxBoatSpeed + 500) { + yacht.changeVelocity(-velocity / 200); + } else { + yacht.setCurrentVelocity(maxBoatSpeed); + } + } else { + if (velocity > 3000) { + yacht.changeVelocity(-velocity / 200); + } else if (velocity > 100) { + yacht.changeVelocity(-velocity / 50); + } else if (velocity <= 100) { + yacht.setCurrentVelocity(0d); + } + } + } + + + /** + * Calculates the distance to the next mark (closest of the two if a gate mark). For purposes of + * mark rounding + * + * @return A distance in metres. Returns -1 if there is no next mark + * @throws IndexOutOfBoundsException If the next mark is null (ie the last mark in the race) + * Check first using {@link seng302.model.mark.MarkOrder#isLastMark(Integer)} + */ + private Double calcDistanceToCurrentMark(ServerYacht yacht) throws IndexOutOfBoundsException { + Integer currentMarkSeqID = yacht.getCurrentMarkSeqID(); + CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID); + GeoPoint location = yacht.getLocation(); + + if (currentMark.isGate()) { + Mark sub1 = currentMark.getSubMark(1); + Mark sub2 = currentMark.getSubMark(2); + Double distance1 = GeoUtility.getDistance(location, sub1); + Double distance2 = GeoUtility.getDistance(location, sub2); + if (distance1 < distance2) { + yacht.setClosestCurrentMark(sub1); + return distance1; + } else { + yacht.setClosestCurrentMark(sub2); + return distance2; + } + } else { + yacht.setClosestCurrentMark(currentMark.getSubMark(1)); + return GeoUtility.getDistance(location, currentMark.getSubMark(1)); + } + } + + + /** lobbyController.setPlayerListSource(clientLobbyList); + * 4 Different cases of progression in the race 1 - Passing the start line 2 - Passing any + * in-race Gate 3 - Passing any in-race Mark 4 - Passing the finish line + * + * @param yacht the current yacht to check for progression + */ + private void checkForLegProgression(ServerYacht yacht) { + Integer currentMarkSeqID = yacht.getCurrentMarkSeqID(); + CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID); + + Boolean hasProgressed; + if (currentMarkSeqID == 0) { + hasProgressed = checkStartLineCrossing(yacht); + } else if (markOrder.isLastMark(currentMarkSeqID)) { + hasProgressed = checkFinishLineCrossing(yacht); + } else if (currentMark.isGate()) { + hasProgressed = checkGateRounding(yacht); + } else { + hasProgressed = checkMarkRounding(yacht); + } + + if (hasProgressed) { + yacht.incrementLegNumber(); + sendMarkRoundingMessage(yacht); + logMarkRounding(yacht); + yacht.setHasPassedLine(false); + yacht.setHasEnteredRoundingZone(false); + yacht.setHasPassedThroughGate(false); + if (!markOrder.isLastMark(currentMarkSeqID)) { + yacht.incrementMarkSeqID(); + } + } + } + + + /** + * If we pass the start line gate in the correct direction, progress + * + * @param yacht The current yacht to check for + */ + private Boolean checkStartLineCrossing(ServerYacht yacht) { + Integer currentMarkSeqID = yacht.getCurrentMarkSeqID(); + CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID); + GeoPoint lastLocation = yacht.getLastLocation(); + GeoPoint location = yacht.getLocation(); + + Mark mark1 = currentMark.getSubMark(1); + Mark mark2 = currentMark.getSubMark(2); + CompoundMark nextMark = markOrder.getNextMark(currentMarkSeqID); + + Integer crossedLine = GeoUtility.checkCrossedLine(mark1, mark2, lastLocation, location); + if (crossedLine > 0) { + Boolean isClockwiseCross = GeoUtility.isClockwise(mark1, mark2, nextMark.getMidPoint()); + if (crossedLine == 2 && isClockwiseCross || crossedLine == 1 && !isClockwiseCross) { + yacht.setClosestCurrentMark(mark1); + yacht.setBoatStatus(BoatStatus.RACING); + return true; + } + } + + return false; + } + + + /** + * This algorithm checks for mark rounding. And increments the currentMarSeqID number attribute + * of the yacht if so. A visual representation of this algorithm can be seen on the Wiki under + * 'mark passing algorithm' + * + * @param yacht The current yacht to check for + */ + private Boolean checkMarkRounding(ServerYacht yacht) { + Integer currentMarkSeqID = yacht.getCurrentMarkSeqID(); + CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID); + GeoPoint lastLocation = yacht.getLastLocation(); + GeoPoint location = yacht.getLocation(); + GeoPoint nextPoint = markOrder.getNextMark(currentMarkSeqID).getMidPoint(); + GeoPoint prevPoint = markOrder.getPreviousMark(currentMarkSeqID).getMidPoint(); + GeoPoint midPoint = GeoUtility.getDirtyMidPoint(nextPoint, prevPoint); + + if (calcDistanceToCurrentMark(yacht) < ROUNDING_DISTANCE) { + yacht.setHasEnteredRoundingZone(true); + } + + //In case current mark is a gate, loop through all marks just in case + for (Mark thisCurrentMark : currentMark.getMarks()) { + if (GeoUtility.isPointInTriangle(lastLocation, location, midPoint, thisCurrentMark)) { + yacht.setHasPassedLine(true); + } + } + + return yacht.hasPassedLine() && yacht.hasEnteredRoundingZone(); + } + + + /** + * Checks if a gate line has been crossed and in the correct direction + * + * @param yacht The current yacht to check for + */ + private Boolean checkGateRounding(ServerYacht yacht) { + Integer currentMarkSeqID = yacht.getCurrentMarkSeqID(); + CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID); + GeoPoint lastLocation = yacht.getLastLocation(); + GeoPoint location = yacht.getLocation(); + + Mark mark1 = currentMark.getSubMark(1); + Mark mark2 = currentMark.getSubMark(2); + CompoundMark prevMark = markOrder.getPreviousMark(currentMarkSeqID); + CompoundMark nextMark = markOrder.getNextMark(currentMarkSeqID); + + Integer crossedLine = GeoUtility.checkCrossedLine(mark1, mark2, lastLocation, location); + + //We have crossed the line + if (crossedLine > 0) { + Boolean isClockwiseCross = GeoUtility.isClockwise(mark1, mark2, prevMark.getMidPoint()); + + //Check we cross the line in the correct direction + if (crossedLine == 1 && isClockwiseCross || crossedLine == 2 && !isClockwiseCross) { + yacht.setHasPassedThroughGate(true); + } + } + + Boolean prevMarkSide = GeoUtility.isClockwise(mark1, mark2, prevMark.getMidPoint()); + Boolean nextMarkSide = GeoUtility.isClockwise(mark1, mark2, nextMark.getMidPoint()); + + if (yacht.hasPassedThroughGate()) { + //Check if we need to round this gate after passing through + if (prevMarkSide == nextMarkSide) { + return checkMarkRounding(yacht); + } else { + return true; + } + } + + return false; + } + + /** + * If we pass the finish gate in the correct direction + * + * @param yacht The current yacht to check for + */ + private Boolean checkFinishLineCrossing(ServerYacht yacht) { + Integer currentMarkSeqID = yacht.getCurrentMarkSeqID(); + CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID); + GeoPoint lastLocation = yacht.getLastLocation(); + GeoPoint location = yacht.getLocation(); + + Mark mark1 = currentMark.getSubMark(1); + Mark mark2 = currentMark.getSubMark(2); + CompoundMark prevMark = markOrder.getPreviousMark(currentMarkSeqID); + + Integer crossedLine = GeoUtility.checkCrossedLine(mark1, mark2, lastLocation, location); + if (crossedLine > 0) { + Boolean isClockwiseCross = GeoUtility.isClockwise(mark1, mark2, prevMark.getMidPoint()); + if (crossedLine == 1 && isClockwiseCross || crossedLine == 2 && !isClockwiseCross) { + yacht.setClosestCurrentMark(mark1); + yacht.setBoatStatus(BoatStatus.FINISHED); + return true; + } + } + + return false; + } + + /** + * Handles player customization. + * + * @param playerID The ID of the player being modified. + * @param requestType the type of player customization the player wants + * @param customizeData the data related to the customization (color, name, shape) + */ + public static void customizePlayer(long playerID, CustomizeRequestType requestType, + byte[] customizeData) { + ServerYacht playerYacht = yachts.get((int) playerID); + + if (requestType.equals(CustomizeRequestType.NAME)) { + String name = new String(customizeData); + playerYacht.setBoatName(name); + } else if (requestType.equals(CustomizeRequestType.COLOR)) { + int red = customizeData[0] & 0xFF; + int green = customizeData[1] & 0xFF; + int blue = customizeData[2] & 0xFF; + Color yachtColor = Color.rgb(red, green, blue); + playerYacht.setBoatColor(yachtColor); + } + } + + private static Mark checkMarkCollision(ServerYacht yacht) { + Set marksInRace = GameState.getMarks(); + for (Mark mark : marksInRace) { + if (GeoUtility.getDistance(yacht.getLocation(), mark) + <= MARK_COLLISION_DISTANCE) { + return mark; + } + } + return null; + } + + /** + * Calculate the new position of the boat after it has had a collision + * + * @return The boats new position + */ + private static GeoPoint calculateBounceBack(ServerYacht yacht, GeoPoint collidedWith, + Double bounceDistance) { + Double heading = GeoUtility.getBearing(yacht.getLastLocation(), collidedWith); + // Invert heading + heading -= 180; + Integer newHeading = Math.floorMod(heading.intValue(), 360); + return GeoUtility + .getGeoCoordinate(yacht.getLocation(), newHeading.doubleValue(), bounceDistance); + } + + /** + * Collision detection which iterates through all the yachts and check if any yacht collided + * with this yacht. Return collided yacht or null if no collision. + * + * @return yacht to compare to all other yachts. + */ + private static ServerYacht checkYachtCollision(ServerYacht yacht) { + + for (ServerYacht otherYacht : GameState.getYachts().values()) { + if (otherYacht != yacht) { + Double distance = GeoUtility + .getDistance(otherYacht.getLocation(), yacht.getLocation()); + if (distance < YACHT_COLLISION_DISTANCE) { + return otherYacht; + } + } + } + return null; + } + + private void sendMarkRoundingMessage(ServerYacht yacht) { + Integer sourceID = yacht.getSourceId(); + Integer currentMarkSeqID = yacht.getCurrentMarkSeqID(); + CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID); + MarkType markType = (currentMark.isGate()) ? MarkType.GATE : MarkType.ROUNDING_MARK; + Mark roundingMark = yacht.getClosestCurrentMark(); + + // TODO: 13/8/17 figure out the rounding side, rounded mark source ID and boat status. + Message markRoundingMessage = new MarkRoundingMessage(0, 0, + sourceID, RoundingBoatStatus.RACING, roundingMark.getRoundingSide(), markType, + currentMarkSeqID + 1); + + notifyMessageListeners(markRoundingMessage); + } + + private static void notifyMessageListeners(Message message) { + for (NewMessageListener mpl : markListeners) { + mpl.notify(message); + } + } + + private void logMarkRounding(ServerYacht yacht) { + Mark roundingMark = yacht.getClosestCurrentMark(); + logger.debug( + String.format("Yacht srcID(%d) passed Mark srcID(%d)", yacht.getSourceId(), + roundingMark.getSourceID())); + } + + + public static void addMarkPassListener(NewMessageListener listener) { + markListeners.add(listener); + } + + public static void setCustomizationFlag() { + customizationFlag = true; + } + + public static Boolean getCustomizationFlag() { + return customizationFlag; + } + + public static void resetCustomizationFlag() { + customizationFlag = false; } } diff --git a/src/main/java/seng302/gameServer/HeartbeatThread.java b/src/main/java/seng302/gameServer/HeartbeatThread.java index 0b7ce19a..b9367134 100644 --- a/src/main/java/seng302/gameServer/HeartbeatThread.java +++ b/src/main/java/seng302/gameServer/HeartbeatThread.java @@ -1,18 +1,19 @@ package seng302.gameServer; -import seng302.models.Player; -import seng302.server.messages.Heartbeat; -import seng302.server.messages.Message; - import java.io.IOException; -import java.util.*; +import java.util.Stack; +import java.util.Timer; +import java.util.TimerTask; +import seng302.model.Player; +import seng302.gameServer.messages.Heartbeat; +import seng302.gameServer.messages.Message; /** * Send Heartbeat messages to connected player at a specified interval * Will call .clientDisconnected on the delegate when a heartbeat message * cannot be sent to a player */ -public class HeartbeatThread extends Thread{ +public class HeartbeatThread implements Runnable { private final int HEARTBEAT_PERIOD = 200; private ClientConnectionDelegate delegate; private Integer seqNum; @@ -22,6 +23,9 @@ public class HeartbeatThread extends Thread{ this.delegate = delegate; seqNum = 0; disconnectedPlayers = new Stack<>(); + + Thread thread = new Thread(this, "HeartBeat"); + thread.start(); } /** @@ -41,7 +45,6 @@ public class HeartbeatThread extends Thread{ */ private void sendHeartbeatToAllPlayers(){ Message heartbeat = new Heartbeat(seqNum); - for (Player player : GameState.getPlayers()){ if (!player.getSocket().isConnected()) { playerLostConnection(player); @@ -53,7 +56,6 @@ public class HeartbeatThread extends Thread{ playerLostConnection(player); } } - updateDelegate(); seqNum++; } @@ -70,7 +72,6 @@ public class HeartbeatThread extends Thread{ public void run(){ Timer t = new Timer(); - t.schedule(new TimerTask() { @Override public void run() { diff --git a/src/main/java/seng302/gameServer/MainServerThread.java b/src/main/java/seng302/gameServer/MainServerThread.java index 78b87123..83fb304c 100644 --- a/src/main/java/seng302/gameServer/MainServerThread.java +++ b/src/main/java/seng302/gameServer/MainServerThread.java @@ -1,30 +1,36 @@ package seng302.gameServer; -import java.time.LocalDateTime; -import java.util.Observable; -import seng302.client.ClientPacketParser; -import seng302.models.Player; -import seng302.models.stream.PacketBufferDelegate; -import seng302.models.stream.packets.StreamPacket; +import seng302.gameServer.messages.*; +import seng302.model.GeoPoint; +import seng302.model.Player; +import seng302.model.PolarTable; +import seng302.model.ServerYacht; +import seng302.model.mark.CompoundMark; +import seng302.utilities.GeoUtility; import java.io.IOException; import java.net.ServerSocket; -import java.net.Socket; -import java.util.ArrayList; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.PriorityBlockingQueue; -import java.util.logging.Logger; +import java.time.LocalDateTime; +import java.util.*; /** * A class describing the overall server, which creates and collects server threads for each client * Created by wmu16 on 13/07/17. */ -public class MainServerThread extends Observable implements Runnable, ClientConnectionDelegate{ +public class MainServerThread implements Runnable, ClientConnectionDelegate { private static final int PORT = 4942; - private static final Integer CLIENT_UPDATES_PER_SECOND = 10; + private static final Integer CLIENT_UPDATES_PER_SECOND = 60; private static final int LOG_LEVEL = 1; + private static final int WARNING_TIME = 10 * -1000; + private static final int PREPATORY_TIME = 5 * -1000; + public static final int TIME_TILL_START = 10 * 1000; + + private static final int MAX_WIND_SPEED = 12000; + private static final int MIN_WIND_SPEED = 8000; + + public static int windSpeed = 1000; + private boolean terminated; private Thread thread; @@ -33,27 +39,25 @@ public class MainServerThread extends Observable implements Runnable, ClientConn private ArrayList serverToClientThreads = new ArrayList<>(); public MainServerThread() { + new GameState("localhost"); try { serverSocket = new ServerSocket(PORT); } catch (IOException e) { serverLog("IO error in server thread handler upon trying to make new server socket", 0); } - + PolarTable.parsePolarFile(getClass().getResourceAsStream("/config/acc_polars.csv")); + GameState.addMarkPassListener(this::broadcastMessage); terminated = false; - thread = new Thread(this); + thread = new Thread(this, "MainServer"); + startUpdatingWind(); thread.start(); } public void run() { - ServerListenThread serverListenThread; - HeartbeatThread heartbeatThread; - serverListenThread = new ServerListenThread(serverSocket, this); - heartbeatThread = new HeartbeatThread(this); - - heartbeatThread.start(); - serverListenThread.start(); + new HeartbeatThread(this); + new ServerListenThread(serverSocket, this); //You should handle interrupts in some way, so that the thread won't keep on forever if you exit the app. while (!terminated) { @@ -62,6 +66,14 @@ public class MainServerThread extends Observable implements Runnable, ClientConn } catch (InterruptedException e) { serverLog("Interrupted exception in Main Server Thread thread sleep", 1); } + if (GameState.getCurrentStage() == GameStages.LOBBYING && GameState + .getCustomizationFlag()) { + // TODO: 16/08/17 ajm412: This can probably be done in a nicer way via those fancy functional interfaces. + for (ServerToClientThread thread : serverToClientThreads) { + thread.sendSetupMessages(); + } + GameState.resetCustomizationFlag(); + } if (GameState.getCurrentStage() == GameStages.PRE_RACE) { updateClients(); @@ -74,13 +86,15 @@ public class MainServerThread extends Observable implements Runnable, ClientConn //FINISHED else if (GameState.getCurrentStage() == GameStages.FINISHED) { - + terminate(); } - } // TODO: 14/07/17 wmu16 - Send out disconnect packet to clients try { + for (ServerToClientThread serverToClientThread : serverToClientThreads) { + serverToClientThread.terminate(); + } serverSocket.close(); return; } catch (IOException e) { @@ -90,68 +104,214 @@ public class MainServerThread extends Observable implements Runnable, ClientConn public void updateClients() { for (ServerToClientThread serverToClientThread : serverToClientThreads) { - serverToClientThread.updateClient(); + serverToClientThread.sendBoatLocationPackets(); } } + private void broadcastMessage(Message message) { + for (ServerToClientThread serverToClientThread : serverToClientThreads) { + serverToClientThread.sendMessage(message); + } + } - static void serverLog(String message, int logLevel){ - if(logLevel <= LOG_LEVEL){ - System.out.println("[SERVER " + LocalDateTime.now().toLocalTime().toString() + "] " + message); + private static void updateWind(){ + Integer direction = GameState.getWindDirection().intValue(); + Integer windSpeed = GameState.getWindSpeedMMS().intValue(); + + Random random = new Random(); + + if (Math.floorMod(random.nextInt(), 2) == 0){ + direction += random.nextInt(4); + windSpeed += random.nextInt(20) + 50; + } + else{ + direction -= random.nextInt(4); + windSpeed -= random.nextInt(20) + 50; + } + + direction = Math.floorMod(direction, 360); + + if (windSpeed > MAX_WIND_SPEED){ + windSpeed -= random.nextInt(1000); + } + + if (windSpeed <= MIN_WIND_SPEED){ + windSpeed += random.nextInt(1000); + } + + GameState.setWindSpeed(Double.valueOf(windSpeed)); + GameState.setWindDirection(direction.doubleValue()); + } + + private static void startUpdatingWind(){ + Timer timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + updateWind(); + } + }, 0, 500); + } + + + static void serverLog(String message, int logLevel) { + if (logLevel <= LOG_LEVEL) { + System.out.println( + "[SERVER " + LocalDateTime.now().toLocalTime().toString() + "] " + message); } } /** * A client has tried to connect to the server + * * @param serverToClientThread The player that connected */ @Override public void clientConnected(ServerToClientThread serverToClientThread) { serverLog("Player Connected From " + serverToClientThread.getThread().getName(), 0); serverToClientThreads.add(serverToClientThread); - this.addObserver(serverToClientThread); - setChanged(); - notifyObservers(); + serverToClientThread.addConnectionListener(() -> { + for (ServerToClientThread thread : serverToClientThreads) { + thread.sendSetupMessages(); + } + }); + serverToClientThread.addDisconnectListener(this::clientDisconnected); } /** * A player has left the game, remove the player from the GameState + * * @param player The player that left */ @Override public void clientDisconnected(Player player) { - try { - player.getSocket().close(); - } catch (Exception e) { - serverLog("Cannot disconnect the socket for the disconnected player.", 0); - } +// try { +// player.getSocket().close(); +// } catch (Exception e) { +// serverLog("Cannot disconnect the socket for the disconnected player.", 0); +// } serverLog("Player " + player.getYacht().getSourceId() + "'s socket disconnected", 0); GameState.removeYacht(player.getYacht().getSourceId()); GameState.removePlayer(player); + ServerToClientThread closedConnection = null; for (ServerToClientThread serverToClientThread : serverToClientThreads) { if (serverToClientThread.getSocket() == player.getSocket()) { - this.deleteObserver(serverToClientThread); + closedConnection = serverToClientThread; + } else if (GameState.getCurrentStage() != GameStages.RACING){ + serverToClientThread.sendSetupMessages(); } } - setChanged(); - notifyObservers(); + serverToClientThreads.remove(closedConnection); + closedConnection.terminate(); } public void startGame() { + initialiseBoatPositions(); Timer t = new Timer(); t.schedule(new TimerTask() { @Override public void run() { - - for (ServerToClientThread serverToClientThread : serverToClientThreads) { - serverToClientThread.sendRaceStatusMessage(); + broadcastMessage(makeRaceStatusMessage()); + if (GameState.getCurrentStage() == GameStages.PRE_RACE || GameState.getCurrentStage() == GameStages.LOBBYING) { + broadcastMessage(makeRaceStartMessage()); } } }, 0, 500); } + + private RaceStartStatusMessage makeRaceStartMessage() { + Long raceStartTime = GameState.getStartTime(); + + return new RaceStartStatusMessage(1, raceStartTime , + 1, RaceStartNotificationType.SET_RACE_START_TIME); + } + + private RaceStatusMessage makeRaceStatusMessage() { + // variables taken from GameServerThread + + List boatSubMessages = new ArrayList<>(); + RaceStatus raceStatus; + + for (Player player : GameState.getPlayers()) { + ServerYacht y = player.getYacht(); + BoatSubMessage m = new BoatSubMessage(y.getSourceId(), y.getBoatStatus(), + y.getLegNumber(), + 0, 0, 1234L, + 1234L); + boatSubMessages.add(m); + } + + long timeTillStart = System.currentTimeMillis() - GameState.getStartTime(); + + if (GameState.getCurrentStage() == GameStages.LOBBYING) { + raceStatus = RaceStatus.PRESTART; + } else if (GameState.getCurrentStage() == GameStages.PRE_RACE) { + raceStatus = RaceStatus.PRESTART; + + if (timeTillStart > WARNING_TIME) { + raceStatus = RaceStatus.WARNING; + } + + if (timeTillStart > PREPATORY_TIME) { + raceStatus = RaceStatus.PREPARATORY; + } + } else { + raceStatus = RaceStatus.STARTED; + } + + return new RaceStatusMessage(1, raceStatus, GameState.getStartTime(), + GameState.getWindDirection(), + GameState.getWindSpeedMMS().longValue(), GameState.getPlayers().size(), + RaceType.MATCH_RACE, 1, boatSubMessages); + } + public void terminate() { terminated = true; } + + /** + * Initialise boats to specific spaced out geopoints behind starting line. + */ + private void initialiseBoatPositions() { + // Getting the start line compound marks +// if (gameClient== null) { +// return; +// } + CompoundMark cm = GameState.getMarkOrder().getMarkOrder().get(0); + GeoPoint startMark1 = cm.getSubMark(1); + GeoPoint startMark2 = cm.getSubMark(2); + + // Calculating midpoint + Double perpendicularAngle = GeoUtility.getBearing(startMark1, startMark2); + Double length = GeoUtility.getDistance(startMark1, startMark2); + GeoPoint midpoint = GeoUtility.getGeoCoordinate(startMark1, perpendicularAngle, length / 2); + + // Setting each boats position side by side + double DISTANCE_FACTOR = 50.0; // distance apart in meters + int boatIndex = 0; + for (ServerYacht yacht : GameState.getYachts().values()) { + int distanceApart = boatIndex / 2; + + if (boatIndex % 2 == 1 && boatIndex != 0) { + distanceApart++; + distanceApart *= -1; + } + + GeoPoint spawnMark = GeoUtility + .getGeoCoordinate(midpoint, perpendicularAngle, distanceApart * DISTANCE_FACTOR); + + if (yacht.getHeading() < perpendicularAngle) { + spawnMark = GeoUtility + .getGeoCoordinate(spawnMark, perpendicularAngle + 90, DISTANCE_FACTOR); + } else { + spawnMark = GeoUtility + .getGeoCoordinate(spawnMark, perpendicularAngle + 270, DISTANCE_FACTOR); + } + + yacht.setLocation(spawnMark); + boatIndex++; + } + } } diff --git a/src/main/java/seng302/gameServer/ServerListenThread.java b/src/main/java/seng302/gameServer/ServerListenThread.java index 475f2cd9..2c220ca2 100644 --- a/src/main/java/seng302/gameServer/ServerListenThread.java +++ b/src/main/java/seng302/gameServer/ServerListenThread.java @@ -1,24 +1,23 @@ package seng302.gameServer; -import seng302.models.Player; - import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; /** * A class for a thread to listen to connections * Created by wmu16 on 11/07/17. */ -public class ServerListenThread extends Thread{ +public class ServerListenThread implements Runnable { private ServerSocket serverSocket; private ClientConnectionDelegate delegate; public ServerListenThread(ServerSocket serverSocket, ClientConnectionDelegate delegate){ this.serverSocket = serverSocket; this.delegate = delegate; + + Thread thread = new Thread(this, "ServerListen"); + thread.start(); } /** @@ -39,7 +38,7 @@ public class ServerListenThread extends Thread{ } public void run(){ - while (true){ + while (serverSocket != null && !serverSocket.isClosed()){ acceptConnection(); } } diff --git a/src/main/java/seng302/gameServer/ServerPacketParser.java b/src/main/java/seng302/gameServer/ServerPacketParser.java index 155ebe04..62d971ea 100644 --- a/src/main/java/seng302/gameServer/ServerPacketParser.java +++ b/src/main/java/seng302/gameServer/ServerPacketParser.java @@ -1,37 +1,32 @@ package seng302.gameServer; import java.util.Arrays; -import seng302.models.stream.packets.StreamPacket; -import seng302.server.messages.BoatActionType; +import seng302.gameServer.messages.BoatAction; +import seng302.gameServer.messages.ClientType; +import seng302.gameServer.messages.CustomizeRequestType; +import seng302.gameServer.messages.Message; +import seng302.model.stream.packets.StreamPacket; public class ServerPacketParser { - - public static BoatActionType extractBoatAction(StreamPacket packet) { + public static BoatAction extractBoatAction(StreamPacket packet) { byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; - long actionTypeValue = bytesToLong(Arrays.copyOfRange(payload, 0, 1)); - return BoatActionType.getType((int) actionTypeValue); + long actionTypeValue = Message.bytesToLong(Arrays.copyOfRange(payload, 0, 1)); + return BoatAction.getType((int) actionTypeValue); } - /** - * takes an array of up to 7 bytes and returns a positive - * long constructed from the input bytes - * - * @return a positive long if there is less than 7 bytes -1 otherwise - */ - private static long bytesToLong(byte[] bytes) { - long partialLong = 0; - int index = 0; - for (byte b : bytes) { - if (index > 6) { - return -1; - } - partialLong = partialLong | (b & 0xFFL) << (index * 8); - index++; - } - return partialLong; + public static ClientType extractClientType(StreamPacket packet){ + byte[] payload = packet.getPayload(); + long value = Message.bytesToLong(Arrays.copyOfRange(payload, 0, 1)); + return ClientType.getClientType((int) value); + } + + public static CustomizeRequestType extractCustomizationType(StreamPacket packet) { + byte[] payload = packet.getPayload(); + long type = Message.bytesToLong(Arrays.copyOfRange(payload, 4, 5)); + return CustomizeRequestType.getRequestType((int) type); } } diff --git a/src/main/java/seng302/gameServer/ServerToClientThread.java b/src/main/java/seng302/gameServer/ServerToClientThread.java index 7b2641bd..3a9b4057 100644 --- a/src/main/java/seng302/gameServer/ServerToClientThread.java +++ b/src/main/java/seng302/gameServer/ServerToClientThread.java @@ -3,42 +3,57 @@ package seng302.gameServer; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; -import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; import java.net.SocketException; -import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Observable; import java.util.Observer; -import java.util.Random; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; import java.util.zip.CRC32; import java.util.zip.Checksum; - -import seng302.models.Player; -import seng302.models.Yacht; -import seng302.models.stream.packets.PacketType; -import seng302.models.stream.packets.StreamPacket; -import seng302.models.xml.Race; -import seng302.models.xml.Regatta; -import seng302.models.xml.XMLGenerator; -import seng302.server.messages.BoatActionType; -import seng302.server.messages.BoatLocationMessage; -import seng302.server.messages.BoatStatus; -import seng302.server.messages.BoatSubMessage; -import seng302.server.messages.Message; - -import seng302.server.messages.RaceStatus; -import seng302.server.messages.RaceStatusMessage; -import seng302.server.messages.RaceType; -import seng302.server.messages.XMLMessage; -import seng302.server.messages.XMLMessageSubType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import seng302.gameServer.messages.BoatAction; +import seng302.gameServer.messages.BoatLocationMessage; +import seng302.gameServer.messages.ClientType; +import seng302.gameServer.messages.CustomizeRequestType; +import seng302.gameServer.messages.Message; +import seng302.gameServer.messages.RegistrationResponseMessage; +import seng302.gameServer.messages.RegistrationResponseStatus; +import seng302.gameServer.messages.XMLMessage; +import seng302.gameServer.messages.XMLMessageSubType; +import seng302.gameServer.messages.YachtEventCodeMessage; +import seng302.gameServer.messages.YachtEventCodeMessage; +import seng302.model.Player; +import seng302.model.ServerYacht; +import seng302.model.stream.packets.PacketType; +import seng302.model.stream.packets.StreamPacket; +import seng302.model.stream.xml.generator.Race; +import seng302.model.stream.xml.generator.Regatta; +import seng302.utilities.XMLGenerator; +import seng302.gameServer.messages.BoatAction; +import seng302.gameServer.messages.BoatLocationMessage; +import seng302.gameServer.messages.ClientType; +import seng302.gameServer.messages.Message; +import seng302.gameServer.messages.RegistrationResponseMessage; +import seng302.gameServer.messages.RegistrationResponseStatus; +import seng302.gameServer.messages.XMLMessage; +import seng302.gameServer.messages.XMLMessageSubType; +import seng302.gameServer.messages.YachtEventCodeMessage; +import seng302.model.Player; +import seng302.model.ServerYacht; +import seng302.model.stream.packets.PacketType; +import seng302.model.stream.packets.StreamPacket; +import seng302.model.stream.xml.generator.Race; +import seng302.model.stream.xml.generator.Regatta; +import seng302.utilities.XMLGenerator; /** * A class describing a single connection to a Client for the purposes of sending and receiving on @@ -47,8 +62,21 @@ import seng302.server.messages.XMLMessageSubType; */ public class ServerToClientThread implements Runnable, Observer { - private static final Integer LOG_LEVEL = 1; - private static final Integer MAX_ID_ATTEMPTS = 10; + /** + * Called to notify listeners when this thread receives a connection correctly. + */ + @FunctionalInterface + interface ConnectionListener { + void notifyConnection (); + } + + // TODO: 17/08/17 this is only temporary disconnects should be handled consistently + @FunctionalInterface + interface DisconnectListener { + void notifyDisconnect (Player player); + } + + private Logger logger = LoggerFactory.getLogger(ServerToClientThread.class); private Thread thread; @@ -58,77 +86,106 @@ public class ServerToClientThread implements Runnable, Observer { private ByteArrayOutputStream crcBuffer; - private Boolean userIdentified = false; - private Boolean connected = true; - private Boolean updateClient = true; -// private Boolean initialisedRace = true; - private Integer seqNo; private Integer sourceId; + private ClientType clientType; + private Boolean isRegistered = false; + private XMLGenerator xml; + private List connectionListeners = new ArrayList<>(); + private DisconnectListener disconnectListener; + + private ServerYacht yacht; + private Player player; + public ServerToClientThread(Socket socket) { this.socket = socket; + seqNo = 0; + + try{ + is = socket.getInputStream(); + os = socket.getOutputStream(); + } catch (IOException e) { + return; + } + + thread = new Thread(this, "ServerToClient"); + thread.start(); + } + + private void setUpPlayer(){ BufferedReader fn; String fName = ""; BufferedReader ln; String lName = ""; - try { - is = socket.getInputStream(); - os = socket.getOutputStream(); - fn = new BufferedReader( + + fn = new BufferedReader( new InputStreamReader( - ServerToClientThread.class.getResourceAsStream( - "/server_config/CSV_Database_of_First_Names.csv" - ) + ServerToClientThread.class.getResourceAsStream( + "/server_config/CSV_Database_of_First_Names.csv" + ) ) - ); - List all = fn.lines().collect(Collectors.toList()); - fName = all.get(ThreadLocalRandom.current().nextInt(0, all.size())); - ln = new BufferedReader( + ); + List all = fn.lines().collect(Collectors.toList()); + fName = all.get(ThreadLocalRandom.current().nextInt(0, all.size())); + ln = new BufferedReader( new InputStreamReader( - ServerToClientThread.class.getResourceAsStream( - "/server_config/CSV_Database_of_Last_Names.csv" - ) + ServerToClientThread.class.getResourceAsStream( + "/server_config/CSV_Database_of_Last_Names.csv" + ) ) - ); - all = ln.lines().collect(Collectors.toList()); - lName = all.get(ThreadLocalRandom.current().nextInt(0, all.size())); - } catch (IOException e) { - serverLog("IO error in server thread upon grabbing streams", 1); - } - //Attempt threeway handshake with connection - sourceId = GameState.getUniquePlayerID(); - if (threeWayHandshake(sourceId)) { - serverLog("Successful handshake. Client allocated id: " + sourceId, 0); - Yacht yacht = new Yacht( + ); + all = ln.lines().collect(Collectors.toList()); + lName = all.get(ThreadLocalRandom.current().nextInt(0, all.size())); + + ServerYacht yacht = new ServerYacht( "Yacht", sourceId, sourceId.toString(), fName, fName + " " + lName, "NZ" - ); -// Yacht yacht = new Yacht("Kappa", "Kap", new GeoPoint(57.6708220, 11.8321340), 90.0); - GameState.addYacht(sourceId, yacht); - GameState.addPlayer(new Player(socket, yacht)); - } else { - serverLog("Unsuccessful handshake. Connection rejected", 1); - closeSocket(); - return; - } + ); - seqNo = 0; - thread = new Thread(this); - thread.start(); - } - - static void serverLog(String message, int logLevel) { - if (logLevel <= LOG_LEVEL) { - System.out.println( - "[SERVER " + LocalDateTime.now().toLocalTime().toString() + "] " + message); - } + yacht.addObserver(this); // TODO: yacht can notify mark rounding message hyi25 13/8/17 + player = new Player(socket, yacht); + GameState.addYacht(sourceId, yacht); + GameState.addPlayer(player); } @Override public void update(Observable o, Object arg) { - sendSetupMessages(); + if (arg != null) { + sendMessage((Message) arg); + } else { + sendSetupMessages(); + } + } + + private void completeRegistration(ClientType clientType) throws IOException { + // Fail if not a player + if (!clientType.equals(ClientType.PLAYER)){ + RegistrationResponseMessage responseMessage = new RegistrationResponseMessage(0, RegistrationResponseStatus.FAILURE_GENERAL); + os.write(responseMessage.getBuffer()); + return; + } + + if (GameState.getPlayers().size() >= GameState.MAX_PLAYERS){ + RegistrationResponseMessage responseMessage = new RegistrationResponseMessage(0, RegistrationResponseStatus.FAILURE_FULL); + os.write(responseMessage.getBuffer()); + return; + } + + Integer sourceId = GameState.getUniquePlayerID(); + RegistrationResponseMessage responseMessage = new RegistrationResponseMessage(sourceId, RegistrationResponseStatus.SUCCESS_PLAYING); + + this.clientType = clientType; + this.sourceId = sourceId; + isRegistered = true; + os.write(responseMessage.getBuffer()); + + setUpPlayer(); + + for (ConnectionListener listener : connectionListeners) { + listener.notifyConnection(); + } } public void run() { @@ -136,23 +193,8 @@ public class ServerToClientThread implements Runnable, Observer { int sync2; // TODO: 14/07/17 wmu16 - Work out how to fix this while loop - while (socket.isConnected()) { - + while (socket.isConnected() && !socket.isClosed()) { try { - //Perform a write if it is time to as delegated by the MainServerThread - if (updateClient) { - // TODO: 13/07/17 wmu16 - Write out game state - some function that would write all appropriate messages to this output stream -// ChatterMessage chatterMessage = new ChatterMessage(4, 14, "Hello, it's me"); -// sendMessage(chatterMessage); -// try { -// GameState.outputState(os); -// } catch (IOException e) { -// System.out.println("IO error in server thread upon writing to output stream"); -// } -// sendBoatLocationPackets(); - updateClient = false; - } - crcBuffer = new ByteArrayOutputStream(); sync1 = readByte(); sync2 = readByte(); @@ -169,38 +211,59 @@ public class ServerToClientThread implements Runnable, Observer { long computedCrc = checksum.getValue(); long packetCrc = Message.bytesToLong(getBytes(4)); if (computedCrc == packetCrc) { - //System.out.println("RECEIVED A PACKET"); - switch (PacketType.assignPacketType(type)) { + switch (PacketType.assignPacketType(type, payload)) { case BOAT_ACTION: - BoatActionType actionType = ServerPacketParser - .extractBoatAction( - new StreamPacket(type, payloadLength, timeStamp, payload)); + BoatAction actionType = ServerPacketParser + .extractBoatAction( + new StreamPacket(type, payloadLength, timeStamp, payload)); GameState.updateBoat(sourceId, actionType); break; + + case RACE_REGISTRATION_REQUEST: + ClientType requestedType = ServerPacketParser.extractClientType( + new StreamPacket(type, payloadLength, timeStamp, payload)); + + completeRegistration(requestedType); + break; + + case RACE_CUSTOMIZATION_REQUEST: + Long sourceID = Message + .bytesToLong(Arrays.copyOfRange(payload, 0, 3)); + CustomizeRequestType requestType = ServerPacketParser + .extractCustomizationType( + new StreamPacket(type, payloadLength, timeStamp, payload)); + GameState.customizePlayer(sourceID, requestType, + Arrays.copyOfRange(payload, 6, payload.length)); + GameState.setCustomizationFlag(); + // TODO: 17/08/2017 ajm412: Send a response packet here, not really necessary until we do shapes. + break; } } else { - serverLog("Packet has been dropped", 1); + logger.warn("Packet has been dropped", 1); } } } catch (Exception e) { - // TODO: 24/07/17 zyt10 - fix a logic here when a client disconnected -// serverLog("ERROR OCCURRED, CLOSING SERVER CONNECTION: " + socket.getRemoteSocketAddress().toString(), 1); closeSocket(); return; } } + logger.warn("Closed serverToClientThread" + thread, 1); } - private void sendSetupMessages() { + public void sendSetupMessages() { xml = new XMLGenerator(); Race race = new Race(); - for (Yacht yacht : GameState.getYachts().values()) { + for (ServerYacht yacht : GameState.getYachts().values()) { race.addBoat(yacht); } //@TODO calculate lat/lng values - xml.setRegatta(new Regatta("RaceVision Test Game", 57.6679590, 11.8503233)); + xml.setRegatta( + new Regatta( + "Party Parrot Test Server", "Bermuda Test Course", + 57.6679590, 11.8503233) + ); xml.setRace(race); XMLMessage xmlMessage; @@ -217,44 +280,6 @@ public class ServerToClientThread implements Runnable, Observer { sendMessage(xmlMessage); } - public void updateClient() { - sendBoatLocationPackets(); - updateClient = true; - } - - - /** - * Tries to confirm the connection just accepted. - * Sends ID, expects that ID echoed for confirmation, - * if so, sends a confirmation packet back to that connection - * Creates a player instance with that ID and this thread and adds it to the GameState - * If not, close the socket and end the threads execution - * - * @param id the id to try and assign to the connection - * @return A boolean indicating if it was a successful handshake - */ - private Boolean threeWayHandshake(Integer id) { - Integer confirmationID = null; - Integer identificationAttempt = 0; - while (!userIdentified) { - try { - os.write(id); //Send out new ID looking for echo - confirmationID = is.read(); - } catch (IOException e) { - serverLog("Three way handshake failed", 1); - } - - if (id.equals(confirmationID)) { //ID is echoed back. Connection is a client - return true; - } else if (identificationAttempt > MAX_ID_ATTEMPTS) { //No response. not a client. tidy up and go home. - return false; - } - identificationAttempt++; - } - - return true; - } - private void closeSocket() { try { socket.close(); @@ -263,15 +288,16 @@ public class ServerToClientThread implements Runnable, Observer { } } - private int readByte() throws Exception { int currentByte = -1; try { - // @TODO @FIX ConnectionReset Exception when a client disconnects before it is garbage collected currentByte = is.read(); crcBuffer.write(currentByte); + } catch (SocketException se) { + disconnectListener.notifyDisconnect(this.player); } catch (IOException e) { - serverLog("Socket read failed", 1); + disconnectListener.notifyDisconnect(this.player); + logger.warn("Socket read failed", 1); } if (currentByte == -1) { throw new Exception(); @@ -297,10 +323,9 @@ public class ServerToClientThread implements Runnable, Observer { try { os.write(message.getBuffer()); } catch (SocketException e) { - //serverLog("Player " + sourceId + " side socket disconnected", 1); - return; + logger.warn("Player " + sourceId + " side socket disconnected", 1); } catch (IOException e) { - serverLog("Message send failed", 1); + logger.warn("Message send failed", 1); } } @@ -310,10 +335,9 @@ public class ServerToClientThread implements Runnable, Observer { } - private void sendBoatLocationPackets() { - ArrayList yachts = new ArrayList<>(GameState.getYachts().values()); - for (Yacht yacht : yachts) { -// System.out.println("[SERVER] Lat: " + yacht.getLocation().getLat() + " Lon: " + yacht.getLocation().getLng()); + public void sendBoatLocationPackets() { + ArrayList yachts = new ArrayList<>(GameState.getYachts().values()); + for (ServerYacht yacht : yachts) { BoatLocationMessage boatLocationMessage = new BoatLocationMessage( yacht.getSourceId(), @@ -321,7 +345,7 @@ public class ServerToClientThread implements Runnable, Observer { yacht.getLocation().getLat(), yacht.getLocation().getLng(), yacht.getHeading(), - (long) yacht.getVelocityMMS()); + yacht.getCurrentVelocity().longValue()); sendMessage(boatLocationMessage); } @@ -331,42 +355,35 @@ public class ServerToClientThread implements Runnable, Observer { return thread; } - public void sendRaceStatusMessage() { - // variables taken from GameServerThread - - - List boatSubMessages = new ArrayList<>(); - BoatStatus boatStatus; - RaceStatus raceStatus; - - for (Player player : GameState.getPlayers()) { - Yacht y = player.getYacht(); - - if (GameState.getCurrentStage() == GameStages.PRE_RACE) { - boatStatus = BoatStatus.PRESTART; - } else if (GameState.getCurrentStage() == GameStages.RACING) { - boatStatus = BoatStatus.RACING; - } else { - boatStatus = BoatStatus.UNDEFINED; - } - - BoatSubMessage m = new BoatSubMessage(y.getSourceId(), boatStatus, 0, 0, 0, 1234l, - 1234l); - boatSubMessages.add(m); - } - - if (GameState.getCurrentStage() == GameStages.RACING) { - raceStatus = RaceStatus.STARTED; - } else { - raceStatus = RaceStatus.WARNING; - } - - sendMessage(new RaceStatusMessage(1, raceStatus, GameState.getStartTime(), GameState.getWindDirection(), - GameState.getWindSpeedMMS().longValue(), GameState.getPlayers().size(), - RaceType.MATCH_RACE, 1, boatSubMessages)); - } - public Socket getSocket() { return socket; } + + public ServerYacht getYacht() { + return yacht; + } + + public void sendCollisionMessage(Integer yachtId) { + sendMessage(new YachtEventCodeMessage(yachtId)); + } + + public void addConnectionListener(ConnectionListener listener) { + connectionListeners.add(listener); + } + + public void removeConnectionListener(ConnectionListener listener) { + connectionListeners.remove(listener); + } + + public void terminate () { + try { + socket.close(); + } catch (IOException ioe) { + logger.warn("IOException attempting to terminate serverToClientThread " + this.thread); + } + } + + public void addDisconnectListener(DisconnectListener disconnectListener) { + this.disconnectListener = disconnectListener; + } } diff --git a/src/main/java/seng302/server/messages/BoatActionType.java b/src/main/java/seng302/gameServer/messages/BoatAction.java similarity index 57% rename from src/main/java/seng302/server/messages/BoatActionType.java rename to src/main/java/seng302/gameServer/messages/BoatAction.java index f8318af7..9003958a 100644 --- a/src/main/java/seng302/server/messages/BoatActionType.java +++ b/src/main/java/seng302/gameServer/messages/BoatAction.java @@ -1,4 +1,4 @@ -package seng302.server.messages; +package seng302.gameServer.messages; import java.util.HashMap; import java.util.Map; @@ -6,29 +6,30 @@ import java.util.Map; /** * Created by kre39 on 12/07/17. */ -public enum BoatActionType { +public enum BoatAction { VMG(1), SAILS_IN(2), SAILS_OUT(3), TACK_GYBE(4), UPWIND(5), - DOWNWIND(6); + DOWNWIND(6), + MAINTAIN_HEADING(7); private final int type; - private static final Map intToTypeMap = new HashMap<>(); + private static final Map intToTypeMap = new HashMap<>(); static { - for (BoatActionType type : BoatActionType.values()) { + for (BoatAction type : BoatAction.values()) { intToTypeMap.put(type.getValue(), type); } } - BoatActionType(int type){ + BoatAction(int type){ this.type = type; } - public static BoatActionType getType(int value) { + public static BoatAction getType(int value) { return intToTypeMap.get(value); } diff --git a/src/main/java/seng302/server/messages/BoatActionMessage.java b/src/main/java/seng302/gameServer/messages/BoatActionMessage.java similarity index 73% rename from src/main/java/seng302/server/messages/BoatActionMessage.java rename to src/main/java/seng302/gameServer/messages/BoatActionMessage.java index cf4ea918..419bf72e 100644 --- a/src/main/java/seng302/server/messages/BoatActionMessage.java +++ b/src/main/java/seng302/gameServer/messages/BoatActionMessage.java @@ -1,8 +1,4 @@ -package seng302.server.messages; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; +package seng302.gameServer.messages; /** * Created by kre39 on 12/07/17. @@ -10,9 +6,9 @@ import java.nio.ByteBuffer; public class BoatActionMessage extends Message{ private final MessageType MESSAGE_TYPE = MessageType.BOAT_ACTION; private final int MESSAGE_SIZE = 1; - private BoatActionType actionType; + private BoatAction actionType; - public BoatActionMessage(BoatActionType actionType) { + public BoatActionMessage(BoatAction actionType) { this.actionType = actionType; setHeader(new Header(MessageType.BOAT_ACTION, 0, (short) 1)); // the second variable is the source id allocateBuffer(); diff --git a/src/main/java/seng302/server/messages/BoatLocationMessage.java b/src/main/java/seng302/gameServer/messages/BoatLocationMessage.java similarity index 85% rename from src/main/java/seng302/server/messages/BoatLocationMessage.java rename to src/main/java/seng302/gameServer/messages/BoatLocationMessage.java index ec0a4c0e..faf344ff 100644 --- a/src/main/java/seng302/server/messages/BoatLocationMessage.java +++ b/src/main/java/seng302/gameServer/messages/BoatLocationMessage.java @@ -1,9 +1,7 @@ -package seng302.server.messages; - -import java.io.IOException; -import java.io.OutputStream; +package seng302.gameServer.messages; public class BoatLocationMessage extends Message { + private final int MESSAGE_SIZE = 56; private long messageVersionNumber; @@ -31,6 +29,7 @@ public class BoatLocationMessage extends Message { /** * 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 @@ -38,7 +37,8 @@ public class BoatLocationMessage extends Message { * @param heading The boats heading * @param boatSpeed The boats speed */ - public BoatLocationMessage(int sourceId, int sequenceNum, double latitude, double longitude, double heading, long boatSpeed){ + public BoatLocationMessage(int sourceId, int sequenceNum, double latitude, double longitude, + double heading, long boatSpeed) { messageVersionNumber = 1; time = System.currentTimeMillis(); this.sourceId = sourceId; @@ -52,7 +52,7 @@ public class BoatLocationMessage extends Message { this.roll = 0; this.boatSpeed = boatSpeed; this.COG = 2; - this.SOG = boatSpeed ; + this.SOG = boatSpeed; this.apparentWindSpeed = 0; this.apparentWindAngle = 0; this.trueWindSpeed = 0; @@ -66,7 +66,7 @@ public class BoatLocationMessage extends Message { allocateBuffer(); writeHeaderToBuffer(); - long headingToSend = (long)((heading/360.0) * 65535.0); + long headingToSend = (long) ((heading / 360.0) * 65535.0); putByte((byte) messageVersionNumber); putInt(time, 6); @@ -97,56 +97,62 @@ public class BoatLocationMessage extends Message { /** * 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; + 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; + 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; + 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); + 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); + 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); + public static long windAngleToBinaryPackedLong(double windAngle) { + return (long) ((8192 * windAngle) / 45); } @Override diff --git a/src/main/java/seng302/server/messages/BoatStatus.java b/src/main/java/seng302/gameServer/messages/BoatStatus.java similarity index 89% rename from src/main/java/seng302/server/messages/BoatStatus.java rename to src/main/java/seng302/gameServer/messages/BoatStatus.java index 94418000..7837994b 100644 --- a/src/main/java/seng302/server/messages/BoatStatus.java +++ b/src/main/java/seng302/gameServer/messages/BoatStatus.java @@ -1,4 +1,4 @@ -package seng302.server.messages; +package seng302.gameServer.messages; /** * The current status of a boat diff --git a/src/main/java/seng302/server/messages/BoatSubMessage.java b/src/main/java/seng302/gameServer/messages/BoatSubMessage.java similarity index 98% rename from src/main/java/seng302/server/messages/BoatSubMessage.java rename to src/main/java/seng302/gameServer/messages/BoatSubMessage.java index 0be35246..f6b78422 100644 --- a/src/main/java/seng302/server/messages/BoatSubMessage.java +++ b/src/main/java/seng302/gameServer/messages/BoatSubMessage.java @@ -1,4 +1,4 @@ -package seng302.server.messages; +package seng302.gameServer.messages; import java.nio.ByteBuffer; diff --git a/src/main/java/seng302/server/messages/ChatterMessage.java b/src/main/java/seng302/gameServer/messages/ChatterMessage.java similarity index 96% rename from src/main/java/seng302/server/messages/ChatterMessage.java rename to src/main/java/seng302/gameServer/messages/ChatterMessage.java index 8480a9d5..f312109f 100644 --- a/src/main/java/seng302/server/messages/ChatterMessage.java +++ b/src/main/java/seng302/gameServer/messages/ChatterMessage.java @@ -1,4 +1,4 @@ -package seng302.server.messages; +package seng302.gameServer.messages; /** * Created by kre39 on 20/07/17. diff --git a/src/main/java/seng302/gameServer/messages/ClientType.java b/src/main/java/seng302/gameServer/messages/ClientType.java new file mode 100644 index 00000000..13be441e --- /dev/null +++ b/src/main/java/seng302/gameServer/messages/ClientType.java @@ -0,0 +1,33 @@ +package seng302.gameServer.messages; + +public enum ClientType { + SPECTATOR(0x00), + PLAYER(0x01), + CONTROL_TUTORIAL(0x02), + GHOST_MODE(0x03); + + private int type; + + ClientType(int type){ + this.type = type; + } + + public int getCode(){ + return type; + } + + public static ClientType getClientType(int typeCode){ + switch (typeCode){ + case 0x00: + return SPECTATOR; + case 0x01: + return PLAYER; + case 0x02: + return CONTROL_TUTORIAL; + case 0x03: + return GHOST_MODE; + default: + return PLAYER; + } + } +} diff --git a/src/main/java/seng302/gameServer/messages/CustomizeRequestMessage.java b/src/main/java/seng302/gameServer/messages/CustomizeRequestMessage.java new file mode 100644 index 00000000..324196f6 --- /dev/null +++ b/src/main/java/seng302/gameServer/messages/CustomizeRequestMessage.java @@ -0,0 +1,35 @@ +package seng302.gameServer.messages; + +// TODO: 14/08/17 ajm412: this may eventually need adjusting due to conforming to the agreed spec. +public class CustomizeRequestMessage extends Message { + + + private static int MESSAGE_LENGTH = 6; + + //Message fields + private CustomizeRequestType customizeType; + private Integer payloadLength; + + public CustomizeRequestMessage(CustomizeRequestType customizeType, double sourceID, + byte[] payload) { + payloadLength = payload.length; + setHeader(new Header(MessageType.CUSTOMIZATION_REQUEST, 1, (short) getSize())); + allocateBuffer(); + writeHeaderToBuffer(); + + + putInt((int) sourceID, 4); + putInt((int) customizeType.getType(), 2); + putBytes(payload); + + writeCRC(); + rewind(); + } + + @Override + public int getSize() { + return MESSAGE_LENGTH + payloadLength; // placeholder + } + + +} diff --git a/src/main/java/seng302/gameServer/messages/CustomizeRequestType.java b/src/main/java/seng302/gameServer/messages/CustomizeRequestType.java new file mode 100644 index 00000000..8077c657 --- /dev/null +++ b/src/main/java/seng302/gameServer/messages/CustomizeRequestType.java @@ -0,0 +1,31 @@ +package seng302.gameServer.messages; + +// TODO: 14/08/17 ajm412: this may eventually need adjusting due to conforming to the agreed spec. +public enum CustomizeRequestType { + NAME(0x00), + COLOR(0x01), + SHAPE(0x02); + + private int type; + + CustomizeRequestType(int type) { + this.type = type; + } + + int getType() { + return this.type; + } + + public static CustomizeRequestType getRequestType(int typeCode) { + switch (typeCode) { + case 0x00: + return NAME; + case 0x01: + return COLOR; + case 0x02: + return SHAPE; + default: + return null; + } + } +} diff --git a/src/main/java/seng302/gameServer/messages/CustomizeResponseMessage.java b/src/main/java/seng302/gameServer/messages/CustomizeResponseMessage.java new file mode 100644 index 00000000..e1723623 --- /dev/null +++ b/src/main/java/seng302/gameServer/messages/CustomizeResponseMessage.java @@ -0,0 +1,28 @@ +package seng302.gameServer.messages; + +/** + * Created by ajm412 on 14/08/17. + */ +public class CustomizeResponseMessage extends Message { + + private static int MESSAGE_LENGTH = 2; + + public CustomizeResponseMessage(CustomizeResponseType responseType) { + setHeader(new Header(MessageType.CUSTOMIZATION_RESPONSE, 1, (short) getSize())); + + allocateBuffer(); + writeHeaderToBuffer(); + + putInt(responseType.getType(), 2); + + writeCRC(); + rewind(); + } + + @Override + public int getSize() { + return MESSAGE_LENGTH; // placeholder + } + + +} diff --git a/src/main/java/seng302/gameServer/messages/CustomizeResponseType.java b/src/main/java/seng302/gameServer/messages/CustomizeResponseType.java new file mode 100644 index 00000000..a935dc80 --- /dev/null +++ b/src/main/java/seng302/gameServer/messages/CustomizeResponseType.java @@ -0,0 +1,34 @@ +package seng302.gameServer.messages; + +// TODO: 14/08/17 ajm412: this may eventually need adjusting due to conforming to the agreed spec. +public enum CustomizeResponseType { + SUCCESS(0x00), + FAILURE(0x01), + FAILURE_MALFORMED_DATA(0x02), + FAILURE_INCOMPATIBLE(0x03); + + private int type; + + CustomizeResponseType(int type) { + this.type = type; + } + + int getType() { + return this.type; + } + + public static CustomizeResponseType getResponseType(int typeCode) { + switch (typeCode) { + case 0x00: + return SUCCESS; + case 0x01: + return FAILURE; + case 0x02: + return FAILURE_MALFORMED_DATA; + case 0x03: + return FAILURE_INCOMPATIBLE; + default: + return null; + } + } +} diff --git a/src/main/java/seng302/server/messages/DeviceType.java b/src/main/java/seng302/gameServer/messages/DeviceType.java similarity index 84% rename from src/main/java/seng302/server/messages/DeviceType.java rename to src/main/java/seng302/gameServer/messages/DeviceType.java index d245c2b1..54925b6d 100644 --- a/src/main/java/seng302/server/messages/DeviceType.java +++ b/src/main/java/seng302/gameServer/messages/DeviceType.java @@ -1,4 +1,4 @@ -package seng302.server.messages; +package seng302.gameServer.messages; public enum DeviceType { UNKNOWN(0), diff --git a/src/main/java/seng302/server/messages/Header.java b/src/main/java/seng302/gameServer/messages/Header.java similarity index 94% rename from src/main/java/seng302/server/messages/Header.java rename to src/main/java/seng302/gameServer/messages/Header.java index 2b520611..1e388b0d 100644 --- a/src/main/java/seng302/server/messages/Header.java +++ b/src/main/java/seng302/gameServer/messages/Header.java @@ -1,9 +1,6 @@ -package seng302.server.messages; +package seng302.gameServer.messages; -import java.math.BigInteger; import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.Collections; public class Header { // From API spec diff --git a/src/main/java/seng302/server/messages/Heartbeat.java b/src/main/java/seng302/gameServer/messages/Heartbeat.java similarity index 85% rename from src/main/java/seng302/server/messages/Heartbeat.java rename to src/main/java/seng302/gameServer/messages/Heartbeat.java index c86baac3..0a81ceec 100644 --- a/src/main/java/seng302/server/messages/Heartbeat.java +++ b/src/main/java/seng302/gameServer/messages/Heartbeat.java @@ -1,7 +1,4 @@ -package seng302.server.messages; - -import java.io.IOException; -import java.io.OutputStream; +package seng302.gameServer.messages; public class Heartbeat extends Message { private final int MESSAGE_SIZE = 4; diff --git a/src/main/java/seng302/server/messages/MarkRoundingMessage.java b/src/main/java/seng302/gameServer/messages/MarkRoundingMessage.java similarity index 77% rename from src/main/java/seng302/server/messages/MarkRoundingMessage.java rename to src/main/java/seng302/gameServer/messages/MarkRoundingMessage.java index 5a085255..c32a6927 100644 --- a/src/main/java/seng302/server/messages/MarkRoundingMessage.java +++ b/src/main/java/seng302/gameServer/messages/MarkRoundingMessage.java @@ -1,7 +1,4 @@ -package seng302.server.messages; - -import java.io.IOException; -import java.io.OutputStream; +package seng302.gameServer.messages; public class MarkRoundingMessage extends Message{ private final long MESSAGE_VERSION_NUMBER = 1; @@ -15,13 +12,20 @@ public class MarkRoundingMessage extends Message{ 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 + * @param ackNumber ackNumber + * @param raceId raceId + * @param sourceId boatSourceId + * @param roundingBoatStatus roundingBoatStatus + * @param roundingSide roundingSide + * @param markId markId */ public MarkRoundingMessage(int ackNumber, int raceId, int sourceId, RoundingBoatStatus roundingBoatStatus, - RoundingSide roundingSide, int markId){ - this.time = System.currentTimeMillis() / 1000L; + RoundingSide roundingSide, MarkType markType, int markId) { + this.time = System.currentTimeMillis(); this.ackNumber = ackNumber; this.raceId = raceId; this.sourceId = sourceId; @@ -40,6 +44,7 @@ public class MarkRoundingMessage extends Message{ putInt((int) sourceId, 4); putByte((byte) boatStatus.getCode()); putByte((byte) roundingSide.getCode()); + putByte((byte) markType.getCode()); putByte((byte) markId); writeCRC(); diff --git a/src/main/java/seng302/server/messages/MarkType.java b/src/main/java/seng302/gameServer/messages/MarkType.java similarity index 87% rename from src/main/java/seng302/server/messages/MarkType.java rename to src/main/java/seng302/gameServer/messages/MarkType.java index abbacc6f..e67a9509 100644 --- a/src/main/java/seng302/server/messages/MarkType.java +++ b/src/main/java/seng302/gameServer/messages/MarkType.java @@ -1,4 +1,4 @@ -package seng302.server.messages; +package seng302.gameServer.messages; /** * Types of marks boats can round diff --git a/src/main/java/seng302/server/messages/Message.java b/src/main/java/seng302/gameServer/messages/Message.java similarity index 97% rename from src/main/java/seng302/server/messages/Message.java rename to src/main/java/seng302/gameServer/messages/Message.java index 398628ab..691bb022 100644 --- a/src/main/java/seng302/server/messages/Message.java +++ b/src/main/java/seng302/gameServer/messages/Message.java @@ -1,7 +1,5 @@ -package seng302.server.messages; +package seng302.gameServer.messages; -import java.io.IOException; -import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; @@ -174,7 +172,7 @@ public abstract class Message { * 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 + * @return A byte array to be sent */ public static byte[] intToByteArray(long val, int len){ int index = 0; @@ -193,6 +191,7 @@ public abstract class Message { * takes an array of up to 7 bytes in little endian format and * returns a positive long constructed from the input bytes * + * @param bytes the bytes to be converted to long * @return a positive long if there is less than 8 bytes -1 otherwise */ public static long bytesToLong(byte[] bytes){ diff --git a/src/main/java/seng302/server/messages/MessageType.java b/src/main/java/seng302/gameServer/messages/MessageType.java similarity index 76% rename from src/main/java/seng302/server/messages/MessageType.java rename to src/main/java/seng302/gameServer/messages/MessageType.java index fccf8d45..9f0ddd4c 100644 --- a/src/main/java/seng302/server/messages/MessageType.java +++ b/src/main/java/seng302/gameServer/messages/MessageType.java @@ -1,4 +1,4 @@ -package seng302.server.messages; +package seng302.gameServer.messages; /** * Enum containing the types of messages @@ -17,7 +17,12 @@ public enum MessageType { MARK_ROUNDING(38), COURSE_WIND(44), AVERAGE_WIND(47), - BOAT_ACTION(100); + BOAT_ACTION(100), + REGISTRATION_REQUEST(101), + REGISTRATION_RESPONSE(102), + CUSTOMIZATION_REQUEST(103), + CUSTOMIZATION_RESPONSE(104); + private int code; @@ -32,4 +37,6 @@ public enum MessageType { int getCode(){ return this.code; } + + } diff --git a/src/main/java/seng302/server/messages/RaceStartNotificationType.java b/src/main/java/seng302/gameServer/messages/RaceStartNotificationType.java similarity index 90% rename from src/main/java/seng302/server/messages/RaceStartNotificationType.java rename to src/main/java/seng302/gameServer/messages/RaceStartNotificationType.java index 29db3f1e..4824e349 100644 --- a/src/main/java/seng302/server/messages/RaceStartNotificationType.java +++ b/src/main/java/seng302/gameServer/messages/RaceStartNotificationType.java @@ -1,4 +1,4 @@ -package seng302.server.messages; +package seng302.gameServer.messages; /** * The types of race start status messages diff --git a/src/main/java/seng302/server/messages/RaceStartStatusMessage.java b/src/main/java/seng302/gameServer/messages/RaceStartStatusMessage.java similarity index 94% rename from src/main/java/seng302/server/messages/RaceStartStatusMessage.java rename to src/main/java/seng302/gameServer/messages/RaceStartStatusMessage.java index 24158d62..9de0a7b2 100644 --- a/src/main/java/seng302/server/messages/RaceStartStatusMessage.java +++ b/src/main/java/seng302/gameServer/messages/RaceStartStatusMessage.java @@ -1,7 +1,4 @@ -package seng302.server.messages; - -import java.io.IOException; -import java.io.OutputStream; +package seng302.gameServer.messages; public class RaceStartStatusMessage extends Message { private final int MESSAGE_SIZE = 20; diff --git a/src/main/java/seng302/server/messages/RaceStatus.java b/src/main/java/seng302/gameServer/messages/RaceStatus.java similarity index 92% rename from src/main/java/seng302/server/messages/RaceStatus.java rename to src/main/java/seng302/gameServer/messages/RaceStatus.java index 7f123c2d..82adfb5b 100644 --- a/src/main/java/seng302/server/messages/RaceStatus.java +++ b/src/main/java/seng302/gameServer/messages/RaceStatus.java @@ -1,4 +1,4 @@ -package seng302.server.messages; +package seng302.gameServer.messages; /** * The current status of the race diff --git a/src/main/java/seng302/server/messages/RaceStatusMessage.java b/src/main/java/seng302/gameServer/messages/RaceStatusMessage.java similarity index 96% rename from src/main/java/seng302/server/messages/RaceStatusMessage.java rename to src/main/java/seng302/gameServer/messages/RaceStatusMessage.java index 0310216e..e1c9af55 100644 --- a/src/main/java/seng302/server/messages/RaceStatusMessage.java +++ b/src/main/java/seng302/gameServer/messages/RaceStatusMessage.java @@ -1,7 +1,5 @@ -package seng302.server.messages; +package seng302.gameServer.messages; -import java.io.IOException; -import java.io.OutputStream; import java.util.List; import java.util.zip.CRC32; @@ -41,7 +39,7 @@ public class RaceStatusMessage extends Message{ this.raceId = raceId; this.raceStatus = raceStatus; this.expectedStartTime = expectedStartTime; - this.raceWindDirection = raceWindDirection * windDirFactor; + this.raceWindDirection = raceWindDirection * windDirFactor+100.0; this.windSpeed = windSpeed; this.numBoatsInRace = numBoatsInRace; this.raceType = raceType; diff --git a/src/main/java/seng302/server/messages/RaceType.java b/src/main/java/seng302/gameServer/messages/RaceType.java similarity index 87% rename from src/main/java/seng302/server/messages/RaceType.java rename to src/main/java/seng302/gameServer/messages/RaceType.java index 182b5dfd..52e0d628 100644 --- a/src/main/java/seng302/server/messages/RaceType.java +++ b/src/main/java/seng302/gameServer/messages/RaceType.java @@ -1,4 +1,4 @@ -package seng302.server.messages; +package seng302.gameServer.messages; /** * Enum containing the types of races diff --git a/src/main/java/seng302/gameServer/messages/RegistrationRequestMessage.java b/src/main/java/seng302/gameServer/messages/RegistrationRequestMessage.java new file mode 100644 index 00000000..c7b2a1db --- /dev/null +++ b/src/main/java/seng302/gameServer/messages/RegistrationRequestMessage.java @@ -0,0 +1,22 @@ +package seng302.gameServer.messages; + + +public class RegistrationRequestMessage extends Message { + private static int MESSAGE_LENGTH = 2; + + public RegistrationRequestMessage(ClientType type){ + setHeader(new Header(MessageType.REGISTRATION_REQUEST, 1, (short) getSize())); + + allocateBuffer(); + writeHeaderToBuffer(); + + putInt(type.getCode(), 2); + + writeCRC(); + } + + @Override + public int getSize() { + return MESSAGE_LENGTH; + } +} diff --git a/src/main/java/seng302/gameServer/messages/RegistrationResponseMessage.java b/src/main/java/seng302/gameServer/messages/RegistrationResponseMessage.java new file mode 100644 index 00000000..28700210 --- /dev/null +++ b/src/main/java/seng302/gameServer/messages/RegistrationResponseMessage.java @@ -0,0 +1,20 @@ +package seng302.gameServer.messages; + +public class RegistrationResponseMessage extends Message{ + + public RegistrationResponseMessage(int clientSourceID, RegistrationResponseStatus status){ + setHeader(new Header(MessageType.REGISTRATION_RESPONSE, 1, (short) getSize())); + allocateBuffer(); + writeHeaderToBuffer(); + + putInt(clientSourceID, 4); + putInt(status.getCode(), 1); + + writeCRC(); + } + + @Override + public int getSize() { + return 5; + } +} diff --git a/src/main/java/seng302/gameServer/messages/RegistrationResponseStatus.java b/src/main/java/seng302/gameServer/messages/RegistrationResponseStatus.java new file mode 100644 index 00000000..90e579bf --- /dev/null +++ b/src/main/java/seng302/gameServer/messages/RegistrationResponseStatus.java @@ -0,0 +1,44 @@ +package seng302.gameServer.messages; + +public enum RegistrationResponseStatus { + SUCCESS_SPECTATING(0x00), + SUCCESS_PLAYING(0x01), + SUCCESS_TUTORIAL(0x02), + SUCCESS_GHOSTING(0x03), + + FAILURE_GENERAL(0x10), + FAILURE_FULL(0x11); + + private int code; + + RegistrationResponseStatus(int code){ + this.code = code; + } + + /** + * Get the message code (From the API Spec) + * @return the message code + */ + int getCode(){ + return this.code; + } + + public static RegistrationResponseStatus getResponseStatus(int typeCode){ + switch (typeCode){ + case 0x00: + return SUCCESS_SPECTATING; + case 0x01: + return SUCCESS_PLAYING; + case 0x02: + return SUCCESS_TUTORIAL; + case 0x03: + return SUCCESS_GHOSTING; + case 0x10: + return FAILURE_GENERAL; + case 0x11: + return FAILURE_FULL; + default: + return FAILURE_GENERAL; + } + } +} diff --git a/src/main/java/seng302/server/messages/RoundingBoatStatus.java b/src/main/java/seng302/gameServer/messages/RoundingBoatStatus.java similarity index 88% rename from src/main/java/seng302/server/messages/RoundingBoatStatus.java rename to src/main/java/seng302/gameServer/messages/RoundingBoatStatus.java index 32eb2447..324c03e3 100644 --- a/src/main/java/seng302/server/messages/RoundingBoatStatus.java +++ b/src/main/java/seng302/gameServer/messages/RoundingBoatStatus.java @@ -1,4 +1,4 @@ -package seng302.server.messages; +package seng302.gameServer.messages; /** * The status of a boat rounding a mark diff --git a/src/main/java/seng302/gameServer/messages/RoundingSide.java b/src/main/java/seng302/gameServer/messages/RoundingSide.java new file mode 100644 index 00000000..ffcc7f6c --- /dev/null +++ b/src/main/java/seng302/gameServer/messages/RoundingSide.java @@ -0,0 +1,52 @@ +package seng302.gameServer.messages; + +/** + * The side the boat rounded the mark + */ +public enum RoundingSide { + UNKNOWN(0, "Unknown"), + PORT(1, "Port"), + STARBOARD(2, "Stbd"), + SP(3, "SP"), + PS(4, "PS"); + + + private long code; + private String name; + + RoundingSide(long code, String name) { + this.code = code; + this.name = name; + } + + public long getCode(){ + return code; + } + + public String getName() { + return name; + } + + public static RoundingSide getRoundingSide(String identifier) { + RoundingSide roundingSide = UNKNOWN; + switch (identifier) { + case "Unknown": + roundingSide = UNKNOWN; + break; + case "Port": + roundingSide = PORT; + break; + case "Stbd": + roundingSide = STARBOARD; + break; + case "SP": + roundingSide = SP; + break; + case "PS": + roundingSide = PS; + break; + } + + return roundingSide; + } +} diff --git a/src/main/java/seng302/server/messages/XMLMessage.java b/src/main/java/seng302/gameServer/messages/XMLMessage.java similarity index 94% rename from src/main/java/seng302/server/messages/XMLMessage.java rename to src/main/java/seng302/gameServer/messages/XMLMessage.java index 57d10a00..361bac0e 100644 --- a/src/main/java/seng302/server/messages/XMLMessage.java +++ b/src/main/java/seng302/gameServer/messages/XMLMessage.java @@ -1,7 +1,4 @@ -package seng302.server.messages; - -import java.io.IOException; -import java.io.OutputStream; +package seng302.gameServer.messages; public class XMLMessage extends Message{ private final MessageType MESSAGE_TYPE = MessageType.XML_MESSAGE; @@ -20,6 +17,7 @@ public class XMLMessage extends Message{ * XML Message from the AC35 Streaming data spec * @param content The XML content * @param type The XML Message Sub Type + * @param sequenceNum sequenceNum */ public XMLMessage(String content, XMLMessageSubType type, long sequenceNum){ this.content = content; diff --git a/src/main/java/seng302/server/messages/XMLMessageSubType.java b/src/main/java/seng302/gameServer/messages/XMLMessageSubType.java similarity index 88% rename from src/main/java/seng302/server/messages/XMLMessageSubType.java rename to src/main/java/seng302/gameServer/messages/XMLMessageSubType.java index 2e146c5a..64757c5d 100644 --- a/src/main/java/seng302/server/messages/XMLMessageSubType.java +++ b/src/main/java/seng302/gameServer/messages/XMLMessageSubType.java @@ -1,4 +1,4 @@ -package seng302.server.messages; +package seng302.gameServer.messages; /** * Enum containing the types of XML messages diff --git a/src/main/java/seng302/gameServer/messages/YachtEventCodeMessage.java b/src/main/java/seng302/gameServer/messages/YachtEventCodeMessage.java new file mode 100644 index 00000000..eb9f557e --- /dev/null +++ b/src/main/java/seng302/gameServer/messages/YachtEventCodeMessage.java @@ -0,0 +1,52 @@ +package seng302.gameServer.messages; + +/** + * Created by zyt10 on 10/08/17. + */ +public class YachtEventCodeMessage extends Message { + + private final MessageType MESSAGE_TYPE = MessageType.YACHT_EVENT_CODE; + private final int MESSAGE_VERSION = 1; //Always set to 1 + private final int MESSAGE_SIZE = 22; + + // Message fields + private long timeStamp; + private long ack = 0x00; //Unused + private int raceId; + private int destSourceId; + private int incidentId; + private int eventId; + + + public YachtEventCodeMessage(Integer subjectId) { + timeStamp = System.currentTimeMillis() / 1000L; + ack = 0; + raceId = 1; + destSourceId = subjectId; // collision boat source id + incidentId = 0; + eventId = 33; + + setHeader(new Header(MESSAGE_TYPE, 0x01, (short) getSize())); + allocateBuffer(); + writeHeaderToBuffer(); + + // Write message fields + putUnsignedByte((byte) MESSAGE_VERSION); + putInt((int) timeStamp, 6); + putInt((int) ack, 2); + putInt((int) raceId, 4); + putInt((int) destSourceId, 4); + putInt((int) incidentId, 4); + putInt((int) eventId, 1); + + writeCRC(); + rewind(); + } + + /** + * @return The length of this message + */ + public int getSize() { + return MESSAGE_SIZE; + } +} diff --git a/src/main/java/seng302/model/ClientYacht.java b/src/main/java/seng302/model/ClientYacht.java new file mode 100644 index 00000000..5eccc3c5 --- /dev/null +++ b/src/main/java/seng302/model/ClientYacht.java @@ -0,0 +1,298 @@ +package seng302.model; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Observable; +import java.util.Observer; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyDoubleWrapper; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.ReadOnlyIntegerWrapper; +import javafx.beans.property.ReadOnlyLongProperty; +import javafx.beans.property.ReadOnlyLongWrapper; +import javafx.scene.paint.Color; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import seng302.model.mark.CompoundMark; + +/** + * Yacht class for the racing boat.

Class created to store more variables (eg. boat statuses) + * compared to the XMLParser boat class, also done outside Boat class because some old variables are + * not used anymore. + */ +public class ClientYacht extends Observable { + + @FunctionalInterface + public interface YachtLocationListener { + void notifyLocation(ClientYacht clientYacht, double lat, double lon, double heading, + Boolean sailsIn, double velocity); + } + + @FunctionalInterface + public interface MarkRoundingListener { + void notifyRounding(ClientYacht yacht, CompoundMark markPassed, int legNumber); + } + + private Logger logger = LoggerFactory.getLogger(ClientYacht.class); + + + private String boatType; + private Integer sourceId; + private String hullID; //matches HullNum in the XML spec. + private String shortName; + private String boatName; + private String country; + private Integer position; + + private Long estimateTimeAtFinish; + private Boolean sailIn = true; + private Integer currentMarkSeqID = 0; + private Long markRoundTime; + private Long timeTillNext; + private Double heading; + private Integer legNumber = 0; + private GeoPoint location; + private Integer boatStatus; + private Double currentVelocity; + + private List locationListeners = new ArrayList<>(); + private List markRoundingListeners = new ArrayList<>(); + private ReadOnlyDoubleWrapper velocityProperty = new ReadOnlyDoubleWrapper(); + private ReadOnlyLongWrapper timeTillNextProperty = new ReadOnlyLongWrapper(); + private ReadOnlyLongWrapper timeSinceLastMarkProperty = new ReadOnlyLongWrapper(); + private ReadOnlyIntegerWrapper placingProperty = new ReadOnlyIntegerWrapper(); + private CompoundMark lastMarkRounded; + private Color colour; + + public ClientYacht(String boatType, Integer sourceId, String hullID, String shortName, + String boatName, String country) { + this.boatType = boatType; + this.sourceId = sourceId; + this.hullID = hullID; + this.shortName = shortName; + this.boatName = boatName; + this.country = country; + this.location = new GeoPoint(57.670341, 11.826856); + this.heading = 120.0; //In degrees + this.currentVelocity = 0d; + this.boatStatus = 1; + this.colour = Color.rgb(0, 0, 0, 1.0); + } + + /** + * Add ServerToClientThread as the observer, this observer pattern mainly server for the boat + * rounding package. + */ + @Override + public void addObserver(Observer o) { + super.addObserver(o); + } + + public String getBoatType() { + return boatType; + } + + public Integer getSourceId() { + //@TODO Remove and merge with Creating Game Loop + if (sourceId == null) { + return 0; + } + return sourceId; + } + + public String getHullID() { + if (hullID == null) { + return ""; + } + return hullID; + } + + public String getShortName() { + return shortName; + } + + public String getBoatName() { + return boatName; + } + + public String getCountry() { + if (country == null) { + return ""; + } + return country; + } + + public Integer getBoatStatus() { + return boatStatus; + } + + public void setBoatStatus(Integer boatStatus) { + this.boatStatus = boatStatus; + } + + public Integer getLegNumber() { + return legNumber; + } + + public void setLegNumber(Integer legNumber) { + this.legNumber = legNumber; + } + + public void setEstimateTimeTillNextMark(Long estimateTimeTillNextMark) { + timeTillNext = estimateTimeTillNextMark; + } + + public String getEstimateTimeAtFinish() { + DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); + return format.format(estimateTimeAtFinish); + } + + public void setEstimateTimeAtFinish(Long estimateTimeAtFinish) { + this.estimateTimeAtFinish = estimateTimeAtFinish; + } + + public Integer getPlacing() { + return placingProperty.get(); + } + + public void setPlacing(Integer position) { + placingProperty.set(position); + } + + public ReadOnlyIntegerProperty placingProperty() { + return placingProperty.getReadOnlyProperty(); + } + + public void updateVelocityProperty(double velocity) { + this.velocityProperty.set(velocity); + } + + public void setMarkRoundingTime(Long markRoundingTime) { + this.markRoundTime = markRoundingTime; + } + + public ReadOnlyDoubleProperty getVelocityProperty() { + return velocityProperty.getReadOnlyProperty(); + } + + public ReadOnlyLongProperty timeTillNextProperty() { + return timeTillNextProperty.getReadOnlyProperty(); + } + + public Long getTimeTillNext() { + return timeTillNext; + } + + public Long getMarkRoundTime() { + return markRoundTime; + } + + public CompoundMark getLastMarkRounded() { + return lastMarkRounded; + } + + public void setLastMarkRounded(CompoundMark lastMarkRounded) { + this.lastMarkRounded = lastMarkRounded; + } + + public GeoPoint getLocation() { + return location; + } + + public Integer getPosition() { + return position; + } + + public void setPosition(Integer position) { + this.position = position; + } + + public void toggleSail() { + sailIn = !sailIn; + } + //// TODO: 15/08/17 asd + + /** + * Sets the current location of the boat in lat and long whilst preserving the last location + * + * @param lat Latitude + * @param lng Longitude + */ + public void setLocation(Double lat, Double lng) { + location.setLat(lat); + location.setLng(lng); + } + + public Double getHeading() { + return heading; + } + + public void setHeading(Double heading) { + this.heading = heading; + } + + @Override + public String toString() { + return boatName; + } + + public void updateTimeSinceLastMarkProperty(long timeSinceLastMark) { + this.timeSinceLastMarkProperty.set(timeSinceLastMark); + } + + public ReadOnlyLongProperty timeSinceLastMarkProperty() { + return timeSinceLastMarkProperty.getReadOnlyProperty(); + } + + public void setTimeTillNext(Long timeTillNext) { + this.timeTillNext = timeTillNext; + } + + + public Color getColour() { + return colour; + } + + public void setColour(Color colour) { + this.colour = colour; + } + + + public void updateLocation(double lat, double lng, double heading, double velocity) { + setLocation(lat, lng); + this.heading = heading; +// this.currentVelocity = velocity; + updateVelocityProperty(velocity); + for (YachtLocationListener yll : locationListeners) { + yll.notifyLocation(this, lat, lng, heading, sailIn, velocity); + } + } + + public void addLocationListener(YachtLocationListener listener) { + locationListeners.add(listener); + } + + public void addMarkRoundingListener(MarkRoundingListener listener) { + markRoundingListeners.add(listener); + } + + public void removeMarkRoundingListener(MarkRoundingListener listener) { + markRoundingListeners.remove(listener); + } + + public boolean getSailIn () { + return sailIn; + } + + public void roundMark(CompoundMark mark, long markRoundTime, long timeSinceLastMark) { + this.markRoundTime = markRoundTime; + timeSinceLastMarkProperty.set(timeSinceLastMark); + lastMarkRounded = mark; + legNumber++; + for (MarkRoundingListener listener : markRoundingListeners) { + listener.notifyRounding(this, lastMarkRounded, legNumber); + } + } +} diff --git a/src/main/java/seng302/model/Colors.java b/src/main/java/seng302/model/Colors.java new file mode 100644 index 00000000..c3afef90 --- /dev/null +++ b/src/main/java/seng302/model/Colors.java @@ -0,0 +1,14 @@ +package seng302.model; + +import javafx.scene.paint.Color; + +/** + * Enum for generating colours. + */ +public enum Colors { + RED, PERU, GOLD, GREEN, BLUE, PURPLE, DEEPPINK, GRAY; + + public static Color getColor(Integer index) { + return Color.valueOf(values()[index].toString()); + } +} diff --git a/src/main/java/seng302/utilities/GeoPoint.java b/src/main/java/seng302/model/GeoPoint.java similarity index 65% rename from src/main/java/seng302/utilities/GeoPoint.java rename to src/main/java/seng302/model/GeoPoint.java index a3d5c54b..91937663 100644 --- a/src/main/java/seng302/utilities/GeoPoint.java +++ b/src/main/java/seng302/model/GeoPoint.java @@ -1,12 +1,12 @@ -package seng302.utilities; +package seng302.model; /** - * A class represent Geo location (latitude, longitude). + * A class represent Geo location (latitude, lnggitude). * Created by Haoming on 15/5/2017 */ public class GeoPoint { - double lat, lng; + private double lat, lng; public GeoPoint(double lat, double lng) { this.lat = lat; @@ -28,4 +28,9 @@ public class GeoPoint { public void setLng(double lng) { this.lng = lng; } + + @Override + public String toString() { + return "lat: " + lat + " lng: " + lng; + } } diff --git a/src/main/java/seng302/model/Limit.java b/src/main/java/seng302/model/Limit.java new file mode 100644 index 00000000..e93ed2fe --- /dev/null +++ b/src/main/java/seng302/model/Limit.java @@ -0,0 +1,18 @@ +package seng302.model; + +/** + * Stores data on the border of a race + */ +public class Limit extends GeoPoint { + + private Integer seqID; + + public Limit(Integer seqID, Double lat, Double lng) { + super(lat, lng); + this.seqID = seqID; + } + + public Integer getSeqID() { + return seqID; + } +} \ No newline at end of file diff --git a/src/main/java/seng302/models/Player.java b/src/main/java/seng302/model/Player.java similarity index 83% rename from src/main/java/seng302/models/Player.java rename to src/main/java/seng302/model/Player.java index 71260d9c..1ed2b6dc 100644 --- a/src/main/java/seng302/models/Player.java +++ b/src/main/java/seng302/model/Player.java @@ -1,10 +1,6 @@ -package seng302.models; +package seng302.model; -import javafx.scene.paint.Color; - -import java.io.IOException; import java.net.Socket; -import java.nio.channels.SocketChannel; /** * A Class defining a player and their respective details in the game as held by the model @@ -13,11 +9,11 @@ import java.nio.channels.SocketChannel; public class Player { private Socket socket; - private Yacht yacht; + private ServerYacht yacht; private Integer lastMarkPassed; - public Player(Socket socket, Yacht yacht) { + public Player(Socket socket, ServerYacht yacht) { this.socket = socket; this.yacht = yacht; } @@ -34,7 +30,7 @@ public class Player { this.lastMarkPassed = lastMarkPassed; } - public Yacht getYacht() { + public ServerYacht getYacht() { return yacht; } diff --git a/src/main/java/seng302/models/PolarTable.java b/src/main/java/seng302/model/PolarTable.java similarity index 98% rename from src/main/java/seng302/models/PolarTable.java rename to src/main/java/seng302/model/PolarTable.java index e76dcaa3..9334cc54 100644 --- a/src/main/java/seng302/models/PolarTable.java +++ b/src/main/java/seng302/model/PolarTable.java @@ -1,4 +1,4 @@ -package seng302.models; +package seng302.model; import java.io.BufferedReader; import java.io.IOException; @@ -26,6 +26,7 @@ public final class PolarTable { * Iterates through each row of the polar table, in pairs, to extract the row into a hashmap of angle to boat speed. * These angle boatspeed hashmaps are then added to an outer hashmap at the end of wind speed key to each row hashmap * as a value + * @param polarFile polarFile to be parsed */ public static void parsePolarFile(InputStream polarFile) { polarTable = new HashMap<>(); @@ -70,8 +71,6 @@ public final class PolarTable { } catch (IOException e) { System.out.println("[PolarTable] IO exception"); } - - } @@ -154,7 +153,6 @@ public final class PolarTable { public static Double getClosestWindSpeedInPolar(Double thisWindSpeed) { Double smallestDif = Double.POSITIVE_INFINITY; Double closestWind = 0d; - for (Double polarWindSpeed : polarTable.keySet()) { Double difference = Math.abs(polarWindSpeed - thisWindSpeed); if (difference < smallestDif) { diff --git a/src/main/java/seng302/model/RaceState.java b/src/main/java/seng302/model/RaceState.java new file mode 100644 index 00000000..501a3417 --- /dev/null +++ b/src/main/java/seng302/model/RaceState.java @@ -0,0 +1,122 @@ +package seng302.model; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Observable; +import java.util.TimeZone; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyDoubleWrapper; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seng302.model.stream.parser.RaceStartData; +import seng302.model.stream.parser.RaceStatusData; + +/** + * Class for storing race data that does not relate to specific vessels or marks such as time or wind. + * Calculates the state of critical race attributes when relevant data is added. + */ +public class RaceState { + + @FunctionalInterface + public interface CollisionListener { + void notifyCollision(GeoPoint location); + } + +// private final DateFormat DATE_TIME_FORMAT = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); + private final DateFormat DATE_TIME_FORMAT = new SimpleDateFormat("HH:mm:ss"); + private ReadOnlyDoubleWrapper windSpeed = new ReadOnlyDoubleWrapper(); + private ReadOnlyDoubleWrapper windDirection = new ReadOnlyDoubleWrapper(); + private long serverSystemTime; + private long expectedStartTime; + private boolean isRaceStarted = false; + long timeTillStart; + private ObservableList playerPositions; + private List collisions = new ArrayList<>(); + private List collisionListeners = new ArrayList<>(); + + public RaceState() { + playerPositions = FXCollections.observableArrayList(); + } + + public void updateState (RaceStatusData data) { + this.windSpeed.set(data.getWindSpeed()); + this.windDirection.set(data.getWindDirection()); + this.serverSystemTime = data.getCurrentTime(); + this.expectedStartTime = data.getExpectedStartTime(); + this.isRaceStarted = data.isRaceStarted(); + } + + public void setTimeZone (TimeZone timeZone) { + DATE_TIME_FORMAT.setTimeZone(timeZone); + } + + public void updateState (RaceStartData data) { + this.timeTillStart = data.getRaceStartTime(); + } + + public String getRaceTimeStr () { + long raceTime = serverSystemTime - expectedStartTime; + if (raceTime < 0) { + return "-" + DATE_TIME_FORMAT.format(-1 * (raceTime - 1000)); + } else { + return DATE_TIME_FORMAT.format(serverSystemTime - expectedStartTime); + } + } + + public long getTimeTillStart () { + return (expectedStartTime - serverSystemTime); + } + + public double getWindSpeed() { + return windSpeed.doubleValue(); + } + + public ReadOnlyDoubleProperty windSpeedProperty() { + return windSpeed.getReadOnlyProperty(); + } + + public ReadOnlyDoubleProperty windDirectionProperty() { + return windDirection.getReadOnlyProperty(); + } + + public long getRaceTime() { + return serverSystemTime; + } + + public boolean isRaceStarted () { + return isRaceStarted; + } + + public void setBoats(Collection clientYachts) { + playerPositions.setAll(clientYachts); + } + + public void sortPlayers() { + playerPositions.sort((yacht1, yacht2) -> Integer.compare(yacht2.getLegNumber(), + yacht1.getLegNumber())); + } + + public ObservableList getPlayerPositions() { + return playerPositions; + } + + public void storeCollision(ClientYacht yacht) { + collisions.add(yacht); + for (CollisionListener collisionListener : collisionListeners) { + collisionListener.notifyCollision(yacht.getLocation()); + } + } + + public void addCollisionListener(CollisionListener collisionListener) { + collisionListeners.add(collisionListener); + } + + public void removeCollisionListener(CollisionListener collisionListener) { + collisionListeners.remove(collisionListener); + } +} diff --git a/src/main/java/seng302/model/ServerYacht.java b/src/main/java/seng302/model/ServerYacht.java new file mode 100644 index 00000000..64143023 --- /dev/null +++ b/src/main/java/seng302/model/ServerYacht.java @@ -0,0 +1,410 @@ +package seng302.model; + +import java.util.HashMap; +import java.util.Observable; +import java.util.Observer; +import javafx.scene.paint.Color; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import seng302.gameServer.GameState; +import seng302.gameServer.messages.BoatStatus; +import seng302.model.mark.Mark; +import seng302.utilities.GeoUtility; + +/** + * Yacht class for the racing boat.

Class created to store more variables (eg. boat statuses) + * compared to the XMLParser boat class, also done outside Boat class because some old variables are + * not used anymore. + */ +public class ServerYacht extends Observable { + + private Logger logger = LoggerFactory.getLogger(ClientYacht.class); + + public static final Double TURN_STEP = 5.0; + + //Boat info + private String boatType; + private Integer sourceId; + private String hullID; //matches HullNum in the XML spec. + private String shortName; + private String boatName; + private String country; + private BoatStatus boatStatus; + + private Color boatColor; + + + //Location + private Double lastHeading; + private Boolean sailIn; + private Double heading; + private GeoPoint lastLocation; + private GeoPoint location; + private Double currentVelocity; + private Boolean isAuto; + private Double autoHeading; + private Integer legNumber; + + //Mark Rounding + private Integer currentMarkSeqID; + private Boolean hasEnteredRoundingZone; + private Mark closestCurrentMark; + private Boolean hasPassedLine; + private Boolean hasPassedThroughGate; + + + public ServerYacht(String boatType, Integer sourceId, String hullID, String shortName, + String boatName, String country) { + this.boatType = boatType; + this.boatStatus = BoatStatus.PRESTART; + this.sourceId = sourceId; + this.hullID = hullID; + this.shortName = shortName; + this.boatName = boatName; + this.country = country; + this.sailIn = false; + this.isAuto = false; + this.location = new GeoPoint(57.67046, 11.83751); + this.lastLocation = location; + this.heading = 120.0; //In degrees + this.currentVelocity = 0d; //in mms-1 + this.currentMarkSeqID = 0; + this.legNumber = 0; + this.boatColor = Colors.getColor(sourceId - 1); + + this.hasEnteredRoundingZone = false; + this.hasPassedLine = false; + this.hasPassedThroughGate = false; + } + + + /** + * Changes the boats current currentVelocity by a set amount, positive or negative + * + * @param velocityChange The ammount to change the currentVelocity by, in mms-1 + */ + public void changeVelocity(Double velocityChange) { + currentVelocity += velocityChange; + } + + /** + * Updates the boat to a new GeoPoint whilst preserving the last location + * + * @param secondsElapsed The seconds elapsed since the last update of this yacht + */ + public void updateLocation(Double secondsElapsed) { + lastLocation = location; + location = GeoUtility.getGeoCoordinate(location, heading, currentVelocity * secondsElapsed); + } + + public void setLocation(GeoPoint geoPoint) { + location = geoPoint; + } + + /** + * Add ServerToClientThread as the observer, this observer pattern mainly server for the boat + * rounding package. + */ + @Override + public void addObserver(Observer o) { + super.addObserver(o); + } + + /** + * Adjusts the heading of the boat by a given amount, while recording the boats last heading. + * + * @param amount the amount by which to adjust the boat heading. + */ + public void adjustHeading(Double amount) { + Double newVal = heading + amount; + lastHeading = heading; + heading = (double) Math.floorMod(newVal.longValue(), 360L); + } + + /** + * Swaps the boats direction from one side of the wind to the other. + */ + public void tackGybe(Double windDirection) { + if (isAuto) { + disableAutoPilot(); + } else { + Double normalizedHeading = normalizeHeading(); + Double newVal = (-2 * normalizedHeading) + heading; + Double newHeading = (double) Math.floorMod(newVal.longValue(), 360L); + setAutoPilot(newHeading); + } + } + + /** + * Enables the boats auto pilot feature, which will move the boat towards a given heading. + * + * @param thisHeading The heading to move the boat towards. + */ + private void setAutoPilot(Double thisHeading) { + isAuto = true; + autoHeading = thisHeading; + } + + /** + * Disables the auto pilot function. + */ + public void disableAutoPilot() { + isAuto = false; + } + + /** + * Moves the boat towards the given heading when the auto pilot was set. Disables the auto pilot + * in the event that the boat is within the range of 1 turn step of its goal. + */ + public void runAutoPilot() { + if (isAuto) { + turnTowardsHeading(autoHeading); + if (Math.abs(heading - autoHeading) + <= TURN_STEP) { //Cancel when within 1 turn step of target. + isAuto = false; + } + } + } + + public void toggleSailIn() { + sailIn = !sailIn; + } + + public void turnUpwind() { + disableAutoPilot(); + Double normalizedHeading = normalizeHeading(); + if (normalizedHeading == 0) { + if (lastHeading < 180) { + adjustHeading(-TURN_STEP); + } else { + adjustHeading(TURN_STEP); + } + } else if (normalizedHeading == 180) { + if (lastHeading < 180) { + adjustHeading(TURN_STEP); + } else { + adjustHeading(-TURN_STEP); + } + } else if (normalizedHeading < 180) { + adjustHeading(-TURN_STEP); + } else { + adjustHeading(TURN_STEP); + } + } + + public void turnDownwind() { + disableAutoPilot(); + Double normalizedHeading = normalizeHeading(); + if (normalizedHeading == 0) { + if (lastHeading < 180) { + adjustHeading(TURN_STEP); + } else { + adjustHeading(-TURN_STEP); + } + } else if (normalizedHeading == 180) { + if (lastHeading < 180) { + adjustHeading(-TURN_STEP); + } else { + adjustHeading(TURN_STEP); + } + } else if (normalizedHeading < 180) { + adjustHeading(TURN_STEP); + } else { + adjustHeading(-TURN_STEP); + } + } + + /** + * Takes the VMG from the polartable for upwind or downwind depending on the boats direction, + * and uses this to calculate a heading to move the yacht towards. + */ + public void turnToVMG() { + if (isAuto) { + disableAutoPilot(); + } else { + Double normalizedHeading = normalizeHeading(); + Double optimalHeading; + HashMap optimalPolarMap; + + if (normalizedHeading >= 90 && normalizedHeading <= 270) { // Downwind + optimalPolarMap = PolarTable.getOptimalDownwindVMG(GameState.getWindSpeedKnots()); + } else { + optimalPolarMap = PolarTable.getOptimalUpwindVMG(GameState.getWindSpeedKnots()); + } + optimalHeading = optimalPolarMap.keySet().iterator().next(); + + if (normalizedHeading > 180) { + optimalHeading = 360 - optimalHeading; + } + + // Take optimal heading and turn into a boat heading rather than a wind heading. + optimalHeading = + optimalHeading + GameState.getWindDirection(); + + setAutoPilot(optimalHeading); + } + } + + /** + * Takes a given heading and rotates the boat towards that heading. This does not care about + * being upwind or downwind, just which direction will reach a given heading faster. + * + * @param newHeading The heading to turn the yacht towards. + */ + private void turnTowardsHeading(Double newHeading) { + Double newVal = heading - newHeading; + if (Math.floorMod(newVal.longValue(), 360L) > 180) { + adjustHeading(TURN_STEP / 5); + } else { + adjustHeading(-TURN_STEP / 5); + } + } + + /** + * Returns a heading normalized for the wind direction. Heading direction into the wind is 0, + * directly away is 180. + * + * @return The normalized heading accounting for wind direction. + */ + private Double normalizeHeading() { + Double normalizedHeading = heading - GameState.windDirection; + normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360L); + return normalizedHeading; + } + + public Integer getSourceId() { + //@TODO Remove and merge with Creating Game Loop + if (sourceId == null) { + return 0; + } + return sourceId; + } + + // TODO: 15/08/17 This method is implicitly called from the XML generator for boats DO NOT DELETE + public String getHullID() { + if (hullID == null) { + return ""; + } + return hullID; + } + + // TODO: 15/08/17 This method is implicitly called from the XML generator for boats DO NOT DELETE + public String getShortName() { + return shortName; + } + + public String getBoatName() { + return boatName; + } + + public String getCountry() { + if (country == null) { + return ""; + } + return country; + } + + public void setBoatName(String name) { + boatName = name; + shortName = name.split(" ")[0]; + } + + public GeoPoint getLocation() { + return location; + } + + + public Double getHeading() { + return heading; + } + + public void setHeading(Double heading) { + this.heading = heading; + } + + public Boolean getSailIn() { + return sailIn; + } + + @Override + public String toString() { + return boatName; + } + + public Double getCurrentVelocity() { + return currentVelocity; + } + + public void setCurrentVelocity(Double currentVelocity) { + this.currentVelocity = currentVelocity; + } + + public Integer getCurrentMarkSeqID() { + return currentMarkSeqID; + } + + public GeoPoint getLastLocation() { + return lastLocation; + } + + public Mark getClosestCurrentMark() { + return closestCurrentMark; + } + + public void setClosestCurrentMark(Mark closestCurrentMark) { + this.closestCurrentMark = closestCurrentMark; + } + + public void setHasEnteredRoundingZone(Boolean hasEnteredRoundingZone) { + this.hasEnteredRoundingZone = hasEnteredRoundingZone; + } + + public void setHasPassedLine(Boolean hasPassedLine) { + this.hasPassedLine = hasPassedLine; + } + + public void setHasPassedThroughGate(Boolean hasPassedThroughGate) { + this.hasPassedThroughGate = hasPassedThroughGate; + } + + public BoatStatus getBoatStatus() { + return boatStatus; + } + + public void setBoatStatus(BoatStatus boatStatus) { + this.boatStatus = boatStatus; + } + + public void incrementMarkSeqID() { + currentMarkSeqID++; + } + + public Boolean hasEnteredRoundingZone() { + return hasEnteredRoundingZone; + } + + public Boolean hasPassedThroughGate() { + return hasPassedThroughGate; + } + + public Boolean hasPassedLine() { + return hasPassedLine; + } + + public void incrementLegNumber() { + legNumber++; + } + + public Integer getLegNumber() { + return legNumber; + } + + public void setBoatColor(Color color) { + this.boatColor = color; + } + + public Color getBoatColor() { + return boatColor; + } + +} diff --git a/src/main/java/seng302/model/mark/CompoundMark.java b/src/main/java/seng302/model/mark/CompoundMark.java new file mode 100644 index 00000000..3f7ba027 --- /dev/null +++ b/src/main/java/seng302/model/mark/CompoundMark.java @@ -0,0 +1,130 @@ +package seng302.model.mark; + +import java.util.ArrayList; +import java.util.List; +import seng302.gameServer.messages.RoundingSide; +import seng302.model.GeoPoint; +import seng302.utilities.GeoUtility; + +public class CompoundMark { + + private int compoundMarkId; + private String name; + private List marks = new ArrayList<>(); + private GeoPoint midPoint; + + public CompoundMark(int markID, String name, List marks) { + this.compoundMarkId = markID; + this.name = name; + this.marks.addAll(marks); + if (marks.size() > 1) { + this.midPoint = GeoUtility.getDirtyMidPoint(marks.get(0), marks.get(1)); + } else { + this.midPoint = marks.get(0); + } + } + + /** + * Prints out compoundMark's info and its marks, good for testing + * @return a string showing its details + */ + @Override + public String toString(){ + String info = String.format( + "CompoundMark: %d (%s), [%s", compoundMarkId, name, marks.get(0).toString() + ); + if (marks.size() > 1) { + info += String.format(", %s", marks.get(1).toString()); + } + return info + "]"; + } + + public int getId() { + return compoundMarkId; + } + + public void setId (int markID) { + this.compoundMarkId = markID; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public void setRoundingSide(RoundingSide roundingSide) {; + switch (roundingSide) { + case SP: + getSubMark(1).setRoundingSide(RoundingSide.STARBOARD); + getSubMark(2).setRoundingSide(RoundingSide.PORT); + break; + case PS: + getSubMark(1).setRoundingSide(RoundingSide.PORT); + getSubMark(2).setRoundingSide(RoundingSide.STARBOARD); + break; + case PORT: + getSubMark(1).setRoundingSide(RoundingSide.PORT); + break; + case STARBOARD: + getSubMark(1).setRoundingSide(RoundingSide.STARBOARD); + break; + + + } + } + + /** + * Returns the mark contained in the compound mark. Marks are numbered 1 to n; + * @param singleMarkId the id of the desired mark contained in this compound mark. + * @return the desired mark. Returns null if the ID is not in range (1, NUM_MARKS) + */ + public Mark getSubMark(int singleMarkId) { + try { + return marks.get(singleMarkId - 1); + } catch (IndexOutOfBoundsException e) { + return null; + } + } + + /** + * NOTE: This is a 'dirty' mid point as it is simply calculated as an xy point would be. + * NO CHECKING FOR LAT / LNG WRAPPING IS DONE IN CREATION OF THIS MIDPOINT + * + * @return GeoPoint of the midpoint of the two marks, or the one mark if there is only one + */ + public GeoPoint getMidPoint() { + return midPoint; + } + + /** + * Returns whether or not this CompoundMark is a Gate. It is generally cleaner to program to a + * specific singleMark or the list of marks. + * + * @return True if the compound mark is a gate, false otherwise. + */ + public boolean isGate () { + return marks.size() > 1; + } + + /** + * Returns the list of marks in the compoundMark + * + * @return All marks contained in this mark. + */ + public List getMarks () { + return marks; + } + + @Override + public int hashCode() { + int hash = 0; + for (Mark mark : marks) { + hash += Double.hashCode(mark.getSourceID()) + Double.hashCode(mark.getLat()) + + Double.hashCode(mark.getLng()) + mark.getName().hashCode(); + } + return hash + getName().hashCode() + Integer.hashCode(getId()); + } +} diff --git a/src/main/java/seng302/model/mark/Corner.java b/src/main/java/seng302/model/mark/Corner.java new file mode 100644 index 00000000..c911ce9c --- /dev/null +++ b/src/main/java/seng302/model/mark/Corner.java @@ -0,0 +1,35 @@ +package seng302.model.mark; + +/** + * Stores the data for the cornering of a mark. + */ +public class Corner { + + private Integer seqID; + private Integer compoundMarkID; + private String rounding; + private Integer zoneSize; + + public Corner(Integer seqID, Integer compoundMarkID, String rounding, Integer zoneSize) { + this.seqID = seqID; + this.compoundMarkID = compoundMarkID; + this.rounding = rounding; + this.zoneSize = zoneSize; + } + + public Integer getSeqID() { + return seqID; + } + + public Integer getCompoundMarkID() { + return compoundMarkID; + } + + public String getRounding() { + return rounding; + } + + public Integer getZoneSize() { + return zoneSize; + } +} \ No newline at end of file diff --git a/src/main/java/seng302/model/mark/Mark.java b/src/main/java/seng302/model/mark/Mark.java new file mode 100644 index 00000000..b5b65e7e --- /dev/null +++ b/src/main/java/seng302/model/mark/Mark.java @@ -0,0 +1,86 @@ +package seng302.model.mark; + +import java.util.ArrayList; +import java.util.List; +import seng302.gameServer.messages.RoundingSide; +import seng302.model.GeoPoint; + +/** + * An abstract class to represent general marks + * Created by Haoming Yin (hyi25) on 17/3/17. + */ +public class Mark extends GeoPoint { + + @FunctionalInterface + public interface PositionListener { + void notifyPositionChange(Mark mark, double lat, double lon); + } + + private int seqID; + private String name; + private int sourceID; + private List positionListeners = new ArrayList<>(); + private RoundingSide roundingSide; + + public Mark(String name, int seqID, double lat, double lng, int sourceID) { + super(lat, lng); + this.name = name; + this.sourceID = sourceID; + this.seqID = seqID; + } + + /** + * 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, getLat(), getLng()); + } + + public int getSeqID() { + return seqID; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getSourceID() { + return sourceID; + } + + public RoundingSide getRoundingSide() { + return roundingSide; + } + + public void setRoundingSide(RoundingSide roundingSide) { + this.roundingSide = roundingSide; + } + + public void setSourceID(int sourceID) { + this.sourceID = sourceID; + } + + public void updatePosition (double lat, double lon) { + this.setLat(lat); + this.setLng(lon); + for (PositionListener listener : positionListeners) { + listener.notifyPositionChange(this, lat, lon); + } + } + + public void addPositionListener (PositionListener listener) { + positionListeners.add(listener); + } + + public void removePositionListener (PositionListener listener) { + positionListeners.remove(listener); + } +} + + diff --git a/src/main/java/seng302/model/mark/MarkOrder.java b/src/main/java/seng302/model/mark/MarkOrder.java new file mode 100644 index 00000000..ab3a1848 --- /dev/null +++ b/src/main/java/seng302/model/mark/MarkOrder.java @@ -0,0 +1,138 @@ +package seng302.model.mark; + +import java.io.IOException; +import java.io.StringReader; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import seng302.gameServer.messages.RoundingSide; +import seng302.model.stream.xml.generator.Race; +import seng302.model.stream.xml.parser.RaceXMLData; +import seng302.utilities.XMLGenerator; +import seng302.utilities.XMLParser; +import java.util.*; + +/** + * Class to hold the order of the marks in the race. + */ +public class MarkOrder { + private List raceMarkOrder; + private Logger logger = LoggerFactory.getLogger(MarkOrder.class); + private Set allMarks; + + public MarkOrder(){ + loadRaceProperties(); + } + + /** + * @return An ordered list of marks in the race + * OR null if the mark order could not be loaded + */ + public List getMarkOrder() { + if (raceMarkOrder == null){ + logger.warn("Race order accessed but not instantiated"); + return null; + } + + return Collections.unmodifiableList(raceMarkOrder); + } + + /** + * @param seqID The seqID of the current mark the boat is heading to + * @return A Boolean indicating if this coming mark is the last one (finish line) + */ + public Boolean isLastMark(Integer seqID) { + return seqID == raceMarkOrder.size() - 1; + } + + /** + * @param currentSeqID The seqID of the current mark the boat is heading to + * @return The mark last passed + * @throws IndexOutOfBoundsException if there is no next mark. Check seqID != 0 first + */ + public CompoundMark getPreviousMark(Integer currentSeqID) throws IndexOutOfBoundsException { + return raceMarkOrder.get(currentSeqID - 1); + } + + public CompoundMark getCurrentMark(Integer currentSeqID) { + return raceMarkOrder.get(currentSeqID); + } + + /** + * @param currentSeqID The seqID of the current mark the boat is heading to + * @return The mark following the mark that the boat is heading to + * @throws IndexOutOfBoundsException if there is no next mark. Check using {@link + * #isLastMark(Integer)} + */ + public CompoundMark getNextMark(Integer currentSeqID) throws IndexOutOfBoundsException { + return raceMarkOrder.get(currentSeqID + 1); + } + + public Set getAllMarks(){ + return Collections.unmodifiableSet(allMarks); + } + + /** + * Loads the race order from an XML string + * @param xml An AC35 RaceXML + * @return An ordered list of marks in the race + */ + private List loadRaceOrderFromXML(String xml) { + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db; + Document doc; + allMarks = new HashSet<>(); + + try { + db = dbf.newDocumentBuilder(); + doc = db.parse(new InputSource(new StringReader(xml))); + } catch (ParserConfigurationException | IOException | SAXException e) { + logger.error("Failed to read generated race XML"); + return null; + } + + RaceXMLData data = XMLParser.parseRace(doc); + + if (data != null){ + logger.debug("Loaded RaceXML for mark order"); + List corners = data.getMarkSequence(); + Map marks = data.getCompoundMarks(); + List course = new ArrayList<>(); + for (Corner corner : corners){ + CompoundMark compoundMark = marks.get(corner.getCompoundMarkID()); + compoundMark.setRoundingSide( + RoundingSide.getRoundingSide(corner.getRounding()) + ); + course.add(compoundMark); + allMarks.addAll(compoundMark.getMarks()); + } + + return course; + } + + return null; + } + + /** + * Load the raceXML and mark order + */ + private void loadRaceProperties(){ + XMLGenerator generator = new XMLGenerator(); + + generator.setRace(new Race()); + + String raceXML = generator.getRaceAsXml(); + + if (raceXML == null){ + logger.error("Failed to generate raceXML (for race properties)"); + return; + } + raceMarkOrder = loadRaceOrderFromXML(raceXML); + } +} \ No newline at end of file diff --git a/src/main/java/seng302/models/stream/packets/PacketType.java b/src/main/java/seng302/model/stream/packets/PacketType.java similarity index 54% rename from src/main/java/seng302/models/stream/packets/PacketType.java rename to src/main/java/seng302/model/stream/packets/PacketType.java index 6737d53f..baf2ff42 100644 --- a/src/main/java/seng302/models/stream/packets/PacketType.java +++ b/src/main/java/seng302/model/stream/packets/PacketType.java @@ -1,13 +1,12 @@ -package seng302.models.stream.packets; +package seng302.model.stream.packets; -/** - * Created by Kusal on 4/24/2017. - */ public enum PacketType { HEARTBEAT, RACE_STATUS, DISPLAY_TEXT_MESSAGE, - XML_MESSAGE, + RACE_XML, + REGATTA_XML, + BOAT_XML, RACE_START_STATUS, YACHT_EVENT_CODE, YACHT_ACTION_CODE, @@ -17,9 +16,13 @@ public enum PacketType { COURSE_WIND, AVG_WIND, BOAT_ACTION, - OTHER; + OTHER, + RACE_REGISTRATION_REQUEST, + RACE_REGISTRATION_RESPONSE, + RACE_CUSTOMIZATION_REQUEST, + RACE_CUSTOMIZATION_RESPONSE; - public static PacketType assignPacketType(int packetType){ + public static PacketType assignPacketType(int packetType, byte[] payload){ switch(packetType){ case 1: return HEARTBEAT; @@ -28,7 +31,14 @@ public enum PacketType { case 20: return DISPLAY_TEXT_MESSAGE; case 26: - return XML_MESSAGE; + switch (payload[9]) { //The type of XML message + case 5: + return REGATTA_XML; + case 6: + return RACE_XML; + case 7: + return BOAT_XML; + } case 27: return RACE_START_STATUS; case 29: @@ -47,6 +57,14 @@ public enum PacketType { return AVG_WIND; case 100: return BOAT_ACTION; + case 101: + return RACE_REGISTRATION_REQUEST; + case 102: + return RACE_REGISTRATION_RESPONSE; + case 103: + return RACE_CUSTOMIZATION_REQUEST; + case 104: + return RACE_CUSTOMIZATION_RESPONSE; default: } return OTHER; diff --git a/src/main/java/seng302/models/stream/packets/StreamPacket.java b/src/main/java/seng302/model/stream/packets/StreamPacket.java similarity index 87% rename from src/main/java/seng302/models/stream/packets/StreamPacket.java rename to src/main/java/seng302/model/stream/packets/StreamPacket.java index 22f2fe56..41442e84 100644 --- a/src/main/java/seng302/models/stream/packets/StreamPacket.java +++ b/src/main/java/seng302/model/stream/packets/StreamPacket.java @@ -1,4 +1,4 @@ -package seng302.models.stream.packets; +package seng302.model.stream.packets; /** * Created by kre39 on 23/04/17. @@ -13,7 +13,7 @@ public class StreamPacket { private byte[] payload; public StreamPacket(int type, long messageLength, long timeStamp, byte[] payload) { - this.type = PacketType.assignPacketType(type); + this.type = PacketType.assignPacketType(type, payload); this.messageLength = messageLength; this.timeStamp = timeStamp; this.payload = payload; diff --git a/src/main/java/seng302/model/stream/parser/MarkRoundingData.java b/src/main/java/seng302/model/stream/parser/MarkRoundingData.java new file mode 100644 index 00000000..70971317 --- /dev/null +++ b/src/main/java/seng302/model/stream/parser/MarkRoundingData.java @@ -0,0 +1,36 @@ +package seng302.model.stream.parser; + + +/** + * Simple data wrapper for mark rounding data packet. + */ +public class MarkRoundingData { + + private int boatId; + private int markId; + private int roundingSide; + private long timeStamp; + + public MarkRoundingData(int boatId, int markId, int roundingSide, long timeStamp) { + this.boatId = boatId; + this.markId = markId; + this.roundingSide = roundingSide; + this.timeStamp = timeStamp; + } + + public int getBoatId() { + return boatId; + } + + public int getMarkId() { + return markId; + } + + public int getRoundingSide() { + return roundingSide; + } + + public long getTimeStamp() { + return timeStamp; + } +} diff --git a/src/main/java/seng302/model/stream/parser/PositionUpdateData.java b/src/main/java/seng302/model/stream/parser/PositionUpdateData.java new file mode 100644 index 00000000..6f1af7c0 --- /dev/null +++ b/src/main/java/seng302/model/stream/parser/PositionUpdateData.java @@ -0,0 +1,50 @@ +package seng302.model.stream.parser; + +public class PositionUpdateData { + + public enum DeviceType { + YACHT_TYPE, + MARK_TYPE + } + + private int deviceId; + private DeviceType type; + private double lat; + private double lon; + private double heading; + private double groundSpeed; + + public PositionUpdateData(int deviceId, DeviceType type, double lat, double lon, + double heading, double groundSpeed) { + this.deviceId = deviceId; + this.type = type; + this.lat = lat; + this.lon = lon; + this.heading = heading; + this.groundSpeed = groundSpeed; + } + + public int getDeviceId() { + return deviceId; + } + + public DeviceType getType() { + return type; + } + + public double getLat() { + return lat; + } + + public double getLon() { + return lon; + } + + public double getHeading() { + return heading; + } + + public double getGroundSpeed() { + return groundSpeed; + } +} diff --git a/src/main/java/seng302/model/stream/parser/RaceStartData.java b/src/main/java/seng302/model/stream/parser/RaceStartData.java new file mode 100644 index 00000000..0bd4d926 --- /dev/null +++ b/src/main/java/seng302/model/stream/parser/RaceStartData.java @@ -0,0 +1,35 @@ +package seng302.model.stream.parser; + +/** + * Class for storing data parsed from race start status packet + */ +public class RaceStartData { + + private long raceId; + private long raceStartTime; + private int notificationType; + private long timeStamp; + + public RaceStartData (long raceId, long raceStartTime, int notificationType, long timeStamp) { + this.raceId = raceId; + this.raceStartTime = raceStartTime; + this.notificationType = notificationType; + this.timeStamp = timeStamp; + } + + public long getRaceId() { + return raceId; + } + + public long getRaceStartTime() { + return raceStartTime; + } + + public int getNotificationType() { + return notificationType; + } + + public long getTimeStamp() { + return timeStamp; + } +} diff --git a/src/main/java/seng302/model/stream/parser/RaceStatusData.java b/src/main/java/seng302/model/stream/parser/RaceStatusData.java new file mode 100644 index 00000000..ba836442 --- /dev/null +++ b/src/main/java/seng302/model/stream/parser/RaceStatusData.java @@ -0,0 +1,65 @@ +package seng302.model.stream.parser; + +import java.util.ArrayList; +import java.util.List; + +/** + * Stores parsed data from race status packets + */ +public class RaceStatusData { + + //CONVERSION CONSTANTS + private static final double WIND_DIR_FACTOR = 0x4000 / 90; //0x4000 is 90 degrees + private static final double MS_TO_KNOTS = 1.94384; + + private double windDirection; + private double windSpeed; + private boolean raceStarted = false; + private long currentTime; + private long expectedStartTime; + private List boatData = new ArrayList<>(); + + public RaceStatusData( + long windDir, long rawWindSpeed, int raceStatus, long currentTime, long expectedStartTime) { + + windDirection = windDir / WIND_DIR_FACTOR; + windSpeed = rawWindSpeed / 1000 * MS_TO_KNOTS; + raceStarted = raceStatus == 3; + this.currentTime = currentTime; + this.expectedStartTime = expectedStartTime; + } + + public void addBoatData (long boatID, long estTimeToNextMark, long estTimeToFinish, int leg, int boatStatus) { + boatData.add(new long[] {boatID, estTimeToNextMark, estTimeToFinish, leg, boatStatus}); + } + + public double getWindDirection() { + return windDirection; + } + + public double getWindSpeed() { + return windSpeed; + } + + public boolean isRaceStarted() { + return raceStarted; + } + + public long getCurrentTime() { + return currentTime; + } + + public long getExpectedStartTime() { + return expectedStartTime; + } + + /** + * Returns the data for boats collected form race status packets. + * + * @return A list of boat data. Boat data is in the form + * [boatID, estTimeToNextMark, estTimeToFinish, legNumber]. + */ + public List getBoatData () { + return boatData; + } +} diff --git a/src/main/java/seng302/model/stream/parser/YachtEventData.java b/src/main/java/seng302/model/stream/parser/YachtEventData.java new file mode 100644 index 00000000..635bd11f --- /dev/null +++ b/src/main/java/seng302/model/stream/parser/YachtEventData.java @@ -0,0 +1,34 @@ +package seng302.model.stream.parser; + +/** + * Stores parsed data from yacht event code packet + */ +public class YachtEventData { + private Long subjectId; + private Long incidentId; + private Integer eventId; + private Long timeStamp; + + public YachtEventData(Long subjectId, Long incidentId, Integer eventId, Long timeStamp) { + this.subjectId = subjectId; + this.incidentId = incidentId; + this.eventId = eventId; + this.timeStamp = timeStamp; + } + + public Long getSubjectId() { + return subjectId; + } + + public Long getIncidentId() { + return incidentId; + } + + public Integer getEventId() { + return eventId; + } + + public Long getTimeStamp() { + return timeStamp; + } +} diff --git a/src/main/java/seng302/models/xml/Race.java b/src/main/java/seng302/model/stream/xml/generator/Race.java similarity index 83% rename from src/main/java/seng302/models/xml/Race.java rename to src/main/java/seng302/model/stream/xml/generator/Race.java index 9be61f37..cf42cb2c 100644 --- a/src/main/java/seng302/models/xml/Race.java +++ b/src/main/java/seng302/model/stream/xml/generator/Race.java @@ -1,17 +1,17 @@ -package seng302.models.xml; - -import seng302.models.Yacht; +package seng302.model.stream.xml.generator; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import seng302.model.ServerYacht; /** * A Race object that can be parsed into XML */ public class Race { - private List yachts; + + private List yachts; private LocalDateTime startTime; public Race(){ @@ -23,7 +23,7 @@ public class Race { * Add a boat to the race * @param yacht The boat to add */ - public void addBoat(Yacht yacht){ + public void addBoat(ServerYacht yacht) { yachts.add(yacht); } @@ -31,7 +31,7 @@ public class Race { * Get a list of boats in the race * @return A List of boats */ - public List getBoats(){ + public List getBoats() { return Collections.unmodifiableList(yachts); } diff --git a/src/main/java/seng302/models/xml/Regatta.java b/src/main/java/seng302/model/stream/xml/generator/Regatta.java similarity index 89% rename from src/main/java/seng302/models/xml/Regatta.java rename to src/main/java/seng302/model/stream/xml/generator/Regatta.java index 733b7a0a..fa802e01 100644 --- a/src/main/java/seng302/models/xml/Regatta.java +++ b/src/main/java/seng302/model/stream/xml/generator/Regatta.java @@ -1,4 +1,4 @@ -package seng302.models.xml; +package seng302.model.stream.xml.generator; /** * A Race regatta that can be parsed into XML @@ -18,10 +18,10 @@ public class Regatta { private Integer utcOffset; private Double magneticVariation; - public Regatta(String name, Double latitude, Double longitude) { + public Regatta(String name, String courseName, Double latitude, Double longitude) { this.name = name; this.id = DEFAULT_REGATTA_ID; - this.courseName = name; + this.courseName = courseName; this.latitude = latitude; this.longitude = longitude; diff --git a/src/main/java/seng302/model/stream/xml/parser/RaceXMLData.java b/src/main/java/seng302/model/stream/xml/parser/RaceXMLData.java new file mode 100644 index 00000000..13a0bdbc --- /dev/null +++ b/src/main/java/seng302/model/stream/xml/parser/RaceXMLData.java @@ -0,0 +1,46 @@ +package seng302.model.stream.xml.parser; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import seng302.model.Limit; +import seng302.model.mark.CompoundMark; +import seng302.model.mark.Corner; + +/** + * Process a Document object containing race data in XML format and stores the data. + */ +public class RaceXMLData { + + private List participants; + private Map compoundMarks; + private List markSequence; + private List courseLimit; + + public RaceXMLData(List participants, List compoundMarks, + List markSequence, List courseLimit) { + this.participants = participants; + this.markSequence = markSequence; + this.courseLimit = courseLimit; + this.compoundMarks = new HashMap<>(); + for (CompoundMark cMark : compoundMarks) { + this.compoundMarks.put(cMark.getId(), cMark); + } + } + + public List getParticipants() { + return participants; + } + + public Map getCompoundMarks() { + return compoundMarks; + } + + public List getMarkSequence() { + return markSequence; + } + + public List getCourseLimit() { + return courseLimit; + } +} diff --git a/src/main/java/seng302/model/stream/xml/parser/RegattaXMLData.java b/src/main/java/seng302/model/stream/xml/parser/RegattaXMLData.java new file mode 100644 index 00000000..a4dc783d --- /dev/null +++ b/src/main/java/seng302/model/stream/xml/parser/RegattaXMLData.java @@ -0,0 +1,49 @@ +package seng302.model.stream.xml.parser; + +/** + * Stores data from regatta xml packet. + */ +public class RegattaXMLData { + //Regatta Info + private Integer regattaID; + private String regattaName; + private String courseName; + private Double centralLat; + private Double centralLng; + private Integer utcOffset; + + public RegattaXMLData (Integer regattaID, String regattaName, String courseName, + Double centralLat, Double centralLng, Integer utcOffset) { + this.regattaID = regattaID; + this.regattaName = regattaName; + this.courseName = courseName; + this.centralLat = centralLat; + this.centralLng = centralLng; + this.utcOffset = utcOffset; + } + + public Integer getRegattaID() { + return regattaID; + } + + public String getRegattaName() { + return regattaName; + } + + public String getCourseName() { + return courseName; + } + + public Double getCentralLat() { + return centralLat; + } + + public Double getCentralLng() { + return centralLng; + } + + public Integer getUtcOffset() { + return utcOffset; + } + +} diff --git a/src/main/java/seng302/models/Colors.java b/src/main/java/seng302/models/Colors.java deleted file mode 100644 index 0078e505..00000000 --- a/src/main/java/seng302/models/Colors.java +++ /dev/null @@ -1,19 +0,0 @@ -package seng302.models; - -import javafx.scene.paint.Color; - -/** - * Enum for randomly generating colours. - */ -public enum Colors { - RED, PERU, SEAGREEN, GREEN, BLUE, PURPLE; - - static Integer index = 0; - - public static Color getColor() { - if (index == 6) { - index = 0; - } - return Color.valueOf(values()[index++].toString()); - } -} diff --git a/src/main/java/seng302/models/Yacht.java b/src/main/java/seng302/models/Yacht.java deleted file mode 100644 index 67ab3373..00000000 --- a/src/main/java/seng302/models/Yacht.java +++ /dev/null @@ -1,404 +0,0 @@ -package seng302.models; - -import static seng302.utilities.GeoUtility.getGeoCoordinate; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.HashMap; -import javafx.scene.paint.Color; -import seng302.client.ClientPacketParser; -import seng302.controllers.RaceViewController; -import seng302.gameServer.GameState; -import seng302.models.mark.Mark; -import seng302.utilities.GeoPoint; - -/** - * Yacht class for the racing boat. - * - * Class created to store more variables (eg. boat statuses) compared to the XMLParser boat class, - * also done outside Boat class because some old variables are not used anymore. - */ -public class Yacht { - - private final Double TURN_STEP = 5.0; - - private Double lastHeading; - private Boolean sailIn; - - - // Used in boat group - private Color colour; - - private String boatType; - private Integer sourceId; - private String hullID; //matches HullNum in the XML spec. - private String shortName; - private String boatName; - private String country; - - // Situational data - - - // Boat status - private Integer boatStatus; - private Integer legNumber; - private Integer penaltiesAwarded; - private Integer penaltiesServed; - private Long estimateTimeAtFinish; - private String position; - private GeoPoint location; - private Double heading; - private Double velocity; - private Long timeTillNext; - private Long markRoundTime; - - // Mark rounding - private Mark lastMarkRounded; - private Mark nextMark; - - - /** - * @param location latlon location of the boat stored in a geopoint - * @param heading heading of the boat in degrees from 0 to 365 with 0 being north - */ - public Yacht(GeoPoint location, Double heading) { - this.location = location; - this.heading = heading; - this.velocity = 0.0; - this.sailIn = false; - } - - - /** - * Used in EventTest and RaceTest. - * - * @param boatName Create a yacht object with name. - */ - public Yacht(String boatName, String shortName, GeoPoint location, Double heading) { - this.boatName = boatName; - this.shortName = shortName; - this.location = location; - this.heading = heading; - this.velocity = 0.0; - this.sailIn = false; - } - - /** - * Used in BoatGroupTest. - * - * @param boatName The name of the team sailing the boat - * @param boatVelocity The speed of the boat in meters/second - * @param shortName A shorter version of the teams name - */ - public Yacht(String boatName, double boatVelocity, String shortName, int id) { - this.boatName = boatName; - this.velocity = boatVelocity; - this.shortName = shortName; - this.sourceId = id; - this.sailIn = false; - } - - - public Yacht(String boatType, Integer sourceId, String hullID, String shortName, - String boatName, String country) { - this.boatType = boatType; - this.sourceId = sourceId; - this.hullID = hullID; - this.shortName = shortName; - this.boatName = boatName; - this.country = country; - this.position = "-"; - this.sailIn = false; - this.location = new GeoPoint(57.670341, 11.826856); - this.heading = 120.0; //In degrees - this.velocity = 0d; //in mms-1 - } - - /** - * @param timeInterval since last update in milliseconds - */ - public void update(Long timeInterval) { - - Double secondsElapsed = timeInterval / 1000000.0; - Double windSpeedKnots = GameState.getWindSpeedKnots(); - Double trueWindAngle = Math.abs(GameState.getWindDirection() - heading); - Double boatSpeedInKnots = PolarTable.getBoatSpeed(windSpeedKnots, trueWindAngle); - Double maxBoatSpeed = boatSpeedInKnots / ClientPacketParser.MS_TO_KNOTS * 1000; - if (sailIn && velocity <= maxBoatSpeed && maxBoatSpeed != 0d) { - - if (velocity < maxBoatSpeed) { - velocity += maxBoatSpeed / 15; // Acceleration - } - if (velocity > maxBoatSpeed) { - velocity = maxBoatSpeed; // Prevent the boats from exceeding top speed - } - - } else { // Deceleration - - if (velocity > 0d) { - if (maxBoatSpeed != 0d) { - velocity -= maxBoatSpeed / 600; - } else { - velocity -= velocity / 100; - } - if (velocity < 0) { - velocity = 0d; - } - } - - } - - Double metersCovered = velocity * secondsElapsed; - location = getGeoCoordinate(location, heading, metersCovered); - } - - - public Double getHeading() { - return heading; - } - - public void adjustHeading(Double amount) { - Double newVal = heading + amount; - lastHeading = heading; - // TODO: 24/07/17 wmu16 - '%' in java does remainder, we need modulo. All this must be changed here, this is why we have neg values! - heading = (double) Math.floorMod(newVal.longValue(), 360L); - } - - public void tackGybe(Double windDirection) { - Double normalizedHeading = normalizeHeading(); - adjustHeading(-2 * normalizedHeading); - } - - public void toggleSailIn() { - sailIn = !sailIn; - } - - public void turnUpwind() { - Double normalizedHeading = normalizeHeading(); - if (normalizedHeading == 0) { - if (lastHeading < 180) { - adjustHeading(-TURN_STEP); - } else { - adjustHeading(TURN_STEP); - } - } else if (normalizedHeading == 180) { - if (lastHeading < 180) { - adjustHeading(TURN_STEP); - } else { - adjustHeading(-TURN_STEP); - } - } else if (normalizedHeading < 180) { - adjustHeading(-TURN_STEP); - } else { - adjustHeading(TURN_STEP); - } - } - - public void turnDownwind() { - Double normalizedHeading = normalizeHeading(); - if (normalizedHeading == 0) { - if (lastHeading < 180) { - adjustHeading(TURN_STEP); - } else { - adjustHeading(-TURN_STEP); - } - } else if (normalizedHeading == 180) { - if (lastHeading < 180) { - adjustHeading(-TURN_STEP); - } else { - adjustHeading(TURN_STEP); - } - } else if (normalizedHeading < 180) { - adjustHeading(TURN_STEP); - } else { - adjustHeading(-TURN_STEP); - } - } - - public void turnToVMG() { - Double normalizedHeading = normalizeHeading(); - Double optimalHeading; - HashMap optimalPolarMap; - - if (normalizedHeading >= 90 && normalizedHeading <= 270) { // Downwind - optimalPolarMap = PolarTable.getOptimalDownwindVMG(GameState.getWindSpeedKnots()); - optimalHeading = optimalPolarMap.keySet().iterator().next(); - } else { - optimalPolarMap = PolarTable.getOptimalUpwindVMG(GameState.getWindSpeedKnots()); - optimalHeading = optimalPolarMap.keySet().iterator().next(); - } - // Take optimal heading and turn into correct - optimalHeading = - optimalHeading + (double) Math.floorMod(GameState.getWindDirection().longValue(), 360L); - - turnTowardsHeading(optimalHeading); - - } - - private void turnTowardsHeading(Double newHeading) { - System.out.println(newHeading); - if (heading < 90 && newHeading > 270) { - adjustHeading(-TURN_STEP); - } else { - if (heading < newHeading) { - adjustHeading(TURN_STEP); - } else { - adjustHeading(-TURN_STEP); - } - } - } - - private Double normalizeHeading() { - Double normalizedHeading = heading - GameState.windDirection; - normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360L); - return normalizedHeading; - } - - public String getBoatType() { - return boatType; - } - - public Integer getSourceId() { - //@TODO Remove and merge with Creating Game Loop - if (sourceId == null) return 0; - return sourceId; - } - - public String getHullID() { - if (hullID == null) return ""; - return hullID; - } - - public String getShortName() { - return shortName; - } - - public String getBoatName() { - return boatName; - } - - public String getCountry() { - if (country == null) return ""; - return country; - } - - public Integer getBoatStatus() { - return boatStatus; - } - - public void setBoatStatus(Integer boatStatus) { - this.boatStatus = boatStatus; - } - - public Integer getLegNumber() { - return legNumber; - } - - public void setLegNumber(Integer legNumber) { - if (colour != null && position != "-" && legNumber != this.legNumber&& RaceViewController.sparkLineStatus( - sourceId)) { - RaceViewController.updateYachtPositionSparkline(this, legNumber); - } - this.legNumber = legNumber; - } - - public Integer getPenaltiesAwarded() { - return penaltiesAwarded; - } - - public void setPenaltiesAwarded(Integer penaltiesAwarded) { - this.penaltiesAwarded = penaltiesAwarded; - } - - public Integer getPenaltiesServed() { - return penaltiesServed; - } - - public void setPenaltiesServed(Integer penaltiesServed) { - this.penaltiesServed = penaltiesServed; - } - - public void setEstimateTimeAtNextMark(Long estimateTimeAtNextMark) { - timeTillNext = estimateTimeAtNextMark; - } - - public String getEstimateTimeAtFinish() { - DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); - return format.format(estimateTimeAtFinish); - } - - public void setEstimateTimeAtFinish(Long estimateTimeAtFinish) { - this.estimateTimeAtFinish = estimateTimeAtFinish; - } - - public String getPosition() { - return position; - } - - public void setPosition(String position) { - this.position = position; - } - - public Color getColour() { - return colour; - } - - public void setColour(Color colour) { - this.colour = colour; - } - - public void setVelocity(double velocity) { - this.velocity = velocity; - } - - - public void setMarkRoundingTime(Long markRoundingTime) { - this.markRoundTime = markRoundingTime; - } - - public double getVelocityMMS() { - return velocity; - } - - public Double getVelocityKnots() { - return velocity / 1000 * ClientPacketParser.MS_TO_KNOTS; - } - - public Long getTimeTillNext() { - return timeTillNext; - } - - public Long getMarkRoundTime() { - return markRoundTime; - } - - public Mark getLastMarkRounded() { - return lastMarkRounded; - } - - public void setLastMarkRounded(Mark lastMarkRounded) { - this.lastMarkRounded = lastMarkRounded; - } - - public void setNextMark(Mark nextMark) { - this.nextMark = nextMark; - } - - public Mark getNextMark(){ - return nextMark; - } - - public Boolean getSailIn() { - return sailIn; - } - - @Override - public String toString() { - return boatName; - } - - public GeoPoint getLocation() { - return location; - } - -} diff --git a/src/main/java/seng302/models/mark/GateMark.java b/src/main/java/seng302/models/mark/GateMark.java deleted file mode 100644 index 8459b882..00000000 --- a/src/main/java/seng302/models/mark/GateMark.java +++ /dev/null @@ -1,49 +0,0 @@ -package seng302.models.mark; - -/** - * To represent a gate mark which contains two single marks. - * Created by ptg19 on 16/03/17. - * Modified by Haoming Yin (hyi25) on 17/3/2017. - */ -public class GateMark extends Mark { - - private SingleMark singleMark1; - private SingleMark singleMark2; - - /** - * Create an instance of Gate Mark which contains two single mark - * @param name the name of the gate mark - * @param singleMark1 one single mark inside of the gate mark - * @param singleMark2 the second mark inside of the gate mark - */ - public GateMark(String name, MarkType type, SingleMark singleMark1, SingleMark singleMark2, double latitude, double longitude, int compoundMarkID) { - super(name, type, latitude, longitude, compoundMarkID); - this.singleMark1 = singleMark1; - this.singleMark2 = singleMark2; - } - - public SingleMark getSingleMark1() { - return singleMark1; - } - - public void setSingleMark1(SingleMark singleMark1) { - this.singleMark1 = singleMark1; - } - - public SingleMark getSingleMark2() { - return singleMark2; - } - - public void setSingleMark2(SingleMark singleMark2) { - this.singleMark2 = singleMark2; - } - - public double getLatitude(){ - return (this.getSingleMark1().getLatitude()); - } - - public double getLongitude(){ - return (this.getSingleMark1().getLongitude()); - } - -} diff --git a/src/main/java/seng302/models/mark/Mark.java b/src/main/java/seng302/models/mark/Mark.java deleted file mode 100644 index 027bf6d3..00000000 --- a/src/main/java/seng302/models/mark/Mark.java +++ /dev/null @@ -1,148 +0,0 @@ -package seng302.models.mark; - -/** - * An abstract class to represent general marks - * Created by Haoming Yin (hyi25) on 17/3/17. - */ -public abstract class Mark { - - private String name; - private MarkType markType; - private double latitude; - private double longitude; - private long id; - private int compoundMarkID; - - /** - * Create a mark instance by passing its name and type - * @param name the name of the mark - * @param markType the type of mark. either GATE_MARK or SINGLE_MARK. - */ - public Mark (String name, MarkType markType, int sourceID, int compoundMarkID) { - this.name = name; - this.markType = markType; - this.id = sourceID; - this.compoundMarkID = compoundMarkID; - } - - public Mark(String name, MarkType markType, double latitude, double longitude, int compoundMarkID) { - this.name = name; - this.markType = markType; - this.latitude = latitude; - this.longitude = longitude; - this.id = 0; - this.compoundMarkID = compoundMarkID; - } - - /** - * Calculated the heading in radians from first Mark to the second Mark. - * - * @param pointOne First Mark - * @param pointTwo Second Mark - * @return Heading in radians - */ - public static Double calculateHeadingRad(Mark pointOne, Mark pointTwo) { - Double longitude1 = pointOne.getLongitude(); - Double longitude2 = pointTwo.getLongitude(); - Double latitude1 = pointOne.getLatitude(); - Double latitude2 = pointTwo.getLatitude(); - return calculateHeadingRad(latitude1, longitude1, latitude2, longitude2); - } - - /** - * Calculate the heading in radians from geographical location with latitude1, longitude 1 to - * geographical latitude2, longitude 2 - * - * @param longitude1 Longitude of first point in degrees - * @param longitude2 Longitude of second point in degrees - * @param latitude1 Latitude of first point in degrees - * @param latitude2 Latitude of first point in degrees - * @return Heading in radians - */ - public static double calculateHeadingRad(Double latitude1, Double longitude1, Double latitude2, - Double longitude2) { - latitude1 = Math.toRadians(latitude1); - latitude2 = Math.toRadians(latitude2); - Double longDiff = Math.toRadians(longitude2 - longitude1); - Double y = Math.sin(longDiff) * Math.cos(latitude2); - Double x = - Math.cos(latitude1) * Math.sin(latitude2) - Math.sin(latitude1) * Math.cos(latitude2) - * Math.cos(longDiff); - return Math.atan2(y, x); - } - - /** - * Calculates the distance in meters from the first Mark to a second Mark - * - * @param pointOne First Mark - * @param pointTwo Second Mark - * @return Distance in meters - */ - public static Double calculateDistance(Mark pointOne, Mark pointTwo) { - Double longitude1 = pointOne.getLongitude(); - Double longitude2 = pointTwo.getLongitude(); - Double latitude1 = pointOne.getLatitude(); - Double latitude2 = pointTwo.getLatitude(); - return calculateDistance(latitude1, longitude1, latitude2, longitude2); - } - - /** - * Calculate the distance in meters from geographical location with latitude1, longitude 1 to - * geographical latitude2, longitude 2 - * - * @param longitude1 Longitude of first point in degrees - * @param longitude2 Longitude of second point in degrees - * @param latitude1 Latitude of first point in degrees - * @param latitude2 Latitude of first point in degrees - * @return Distance in meters - */ - public static Double calculateDistance(Double latitude1, Double longitude1, Double latitude2, - Double longitude2) { - Double theta = longitude1 - longitude2; - Double dist = Math.sin(Math.toRadians(latitude1)) * Math.sin(Math.toRadians(latitude2)) + - Math.cos(Math.toRadians(latitude1)) * Math.cos(Math.toRadians(latitude2)) * - Math.cos(Math.toRadians(theta)); - dist = Math.acos(dist); - dist = Math.toDegrees(dist); - dist = dist * 60 - * 1.1508; //nautical mile (distance between two degrees) * (degrees in a minute) - dist = dist * 1609.344; //ratio of miles to metres - return dist; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public MarkType getMarkType() { - return markType; - } - - public void setMarkType(MarkType markType) { - this.markType = markType; - } - - public double getLatitude() { - return latitude; - } - - public double getLongitude() { - return longitude; - } - - public long getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public int getCompoundMarkID() { - return compoundMarkID; - } -} diff --git a/src/main/java/seng302/models/mark/MarkType.java b/src/main/java/seng302/models/mark/MarkType.java deleted file mode 100644 index 92b3c93b..00000000 --- a/src/main/java/seng302/models/mark/MarkType.java +++ /dev/null @@ -1,9 +0,0 @@ -package seng302.models.mark; - -/** - * To represent two types of mark - * Created by Haoming Yin (hyi25) on 17/3/17. - */ -public enum MarkType { - SINGLE_MARK, OPEN_GATE -} diff --git a/src/main/java/seng302/models/mark/SingleMark.java b/src/main/java/seng302/models/mark/SingleMark.java deleted file mode 100644 index 56ba9dc6..00000000 --- a/src/main/java/seng302/models/mark/SingleMark.java +++ /dev/null @@ -1,34 +0,0 @@ -package seng302.models.mark; - -/** - * Represents the marker as a single mark - * - * Created by Haoming Yin (hyi25) on 17/3/2017 - */ -public class SingleMark extends Mark { - private double lat; - private double lon; - private String name; - - /** - * Represents a marker - * - * @param name, the name of the marker* - * @param lat, the latitude of the marker - * @param lon, the longitude of the marker - */ - public SingleMark(String name, double lat, double lon, int sourceID, int compoundMarkID) { - super(name, MarkType.SINGLE_MARK, sourceID, compoundMarkID); - this.lat = lat; - this.lon = lon; - } - - - public double getLatitude() { - return this.lat; - } - - public double getLongitude() { - return this.lon; - } -} \ No newline at end of file diff --git a/src/main/java/seng302/models/stream/PacketBufferDelegate.java b/src/main/java/seng302/models/stream/PacketBufferDelegate.java deleted file mode 100644 index 847b0de5..00000000 --- a/src/main/java/seng302/models/stream/PacketBufferDelegate.java +++ /dev/null @@ -1,7 +0,0 @@ -package seng302.models.stream; - -import seng302.models.stream.packets.StreamPacket; - -public interface PacketBufferDelegate { - boolean addToBuffer(StreamPacket streamPacket); -} diff --git a/src/main/java/seng302/models/stream/XMLParser.java b/src/main/java/seng302/models/stream/XMLParser.java deleted file mode 100644 index 733bcb54..00000000 --- a/src/main/java/seng302/models/stream/XMLParser.java +++ /dev/null @@ -1,610 +0,0 @@ -package seng302.models.stream; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import seng302.models.Yacht; -import seng302.models.mark.GateMark; -import seng302.models.mark.Mark; -import seng302.models.mark.MarkType; -import seng302.models.mark.SingleMark; - -/** - * Class to create an XML object from the XML Packet Messages. - * - * Example usage: - * - * Document doc; // some xml document - * Integer xmlMessageType; // an Integer of value 5, 6, 7 - * - * xmlP = new XMLParser(doc, xmlMessageType); - * RegattaXMLObject rXmlObj = xmlP.createRegattaXML(); // creates a regattaXML object. - */ -public class XMLParser { - - private Document xmlDoc; - - private RaceXMLObject raceXML; - private RegattaXMLObject regattaXML; - private BoatXMLObject boatXML; - - public XMLParser() { - } - - /** - * Constructor for XMLParser - * - * @param doc Document to create XML object. - * @param messageType Defines if a message is a RegattaXML(5), RaceXML(6), BoatXML(7). - */ - public void constructXML(Document doc, Integer messageType) { - this.xmlDoc = doc; - switch (messageType) { - case 5: - regattaXML = new RegattaXMLObject(this.xmlDoc); - break; - case 6: - raceXML = new RaceXMLObject(this.xmlDoc); - break; - case 7: - boatXML = new BoatXMLObject(this.xmlDoc); - break; - } - } - - public RaceXMLObject getRaceXML() { - return raceXML; - } - - public RegattaXMLObject getRegattaXML() { - return regattaXML; - } - - public BoatXMLObject getBoatXML() { - return boatXML; - } - - - /** - * Returns the text content of a given child element tag, assuming it exists, as an Integer. - * - * @param ele Document Element with child elements. - * @param tag Tag to find in document elements child elements. - * @return Text content from tag if found, null otherwise. - */ - private static Integer getElementInt(Element ele, String tag) { - NodeList tagList = ele.getElementsByTagName(tag); - if (tagList.getLength() > 0) { - return Integer.parseInt(tagList.item(0).getTextContent()); - } else { - return null; - } - } - - /** - * Returns the text content of a given child element tag, assuming it exists, as an String. - * - * @param ele Document Element with child elements. - * @param tag Tag to find in document elements child elements. - * @return Text content from tag if found, null otherwise. - */ - private static String getElementString(Element ele, String tag) { - NodeList tagList = ele.getElementsByTagName(tag); - if (tagList.getLength() > 0) { - return tagList.item(0).getTextContent(); - } else { - return null; - } - } - - /** - * Returns the text content of a given child element tag, assuming it exists, as a Double. - * - * @param ele Document Element with child elements. - * @param tag Tag to find in document elements child elements. - * @return Text content from tag if found, null otherwise. - */ - private static Double getElementDouble(Element ele, String tag) { - NodeList tagList = ele.getElementsByTagName(tag); - if (tagList.getLength() > 0) { - return Double.parseDouble(tagList.item(0).getTextContent()); - } else { - return null; - } - } - - /** - * Returns the text content of an attribute of a given Node, assuming it exists, as a String. - * - * @param n A node object that should have some attributes - * @param attr The attribute you want to get from the given node. - * @return The String representation of the text content of an attribute in the given node, else - * returns null. - */ - private static String getNodeAttributeString(Node n, String attr) { - Node attrItem = n.getAttributes().getNamedItem(attr); - if (attrItem != null) { - return attrItem.getTextContent(); - } else { - return null; - } - } - - /** - * Returns the text content of an attribute of a given Node, assuming it exists, as an Integer. - * - * @param n A node object that should have some attributes - * @param attr The attribute you want to get from the given node. - * @return The Integer representation of the text content of an attribute in the given node, - * else returns null. - */ - private static Integer getNodeAttributeInt(Node n, String attr) { - Node attrItem = n.getAttributes().getNamedItem(attr); - if (attrItem != null) { - return Integer.parseInt(attrItem.getTextContent()); - } else { - return null; - } - } - - /** - * Returns the text content of an attribute of a given Node, assuming it exists, as a Double. - * - * @param n A node object that should have some attributes - * @param attr The attribute you want to get from the given node. - * @return The Double representation of the text content of an attribute in the given node, else - * returns null. - */ - private static Double getNodeAttributeDouble(Node n, String attr) { - Node attrItem = n.getAttributes().getNamedItem(attr); - if (attrItem != null) { - return Double.parseDouble(attrItem.getTextContent()); - } else { - return null; - } - } - - public class RegattaXMLObject { - - //Regatta Info - private Integer regattaID; - private String regattaName; - private String courseName; - private Double centralLat; - private Double centralLng; - private Integer utcOffset; - - /** - * Constructor for a RegattaXMLObject. - * Takes the information from a Document object and creates a more usable format. - * - * @param doc XML Document Object - */ - RegattaXMLObject(Document doc) { - Element docEle = doc.getDocumentElement(); - - this.regattaID = getElementInt(docEle, "RegattaID"); - this.regattaName = getElementString(docEle, "RegattaName"); - this.courseName = getElementString(docEle, "CourseName"); - this.centralLat = getElementDouble(docEle, "CentralLatitude"); - this.centralLng = getElementDouble(docEle, "CentralLongitude"); - this.utcOffset = getElementInt(docEle, "UtcOffset"); - } - - public Integer getRegattaID() { - return regattaID; - } - - public String getRegattaName() { - return regattaName; - } - - public String getCourseName() { - return courseName; - } - - public Double getCentralLat() { - return centralLat; - } - - public Double getCentralLng() { - return centralLng; - } - - public Integer getUtcOffset() { - return utcOffset; - } - - } - - public class RaceXMLObject { - - // Race Info - private Integer raceID; - private String raceType; - private String creationTimeDate; // XML Creation Time - - //Race Start Details - private String raceStartTime; - private Boolean postponeStatus; - - //Non atomic race attributes - private ArrayList participants; - private ArrayList allMarks; - private ArrayList nonDuplicateMarks; - private ArrayList compoundMarkSequence; - private ArrayList courseLimit; - - // ensures there's no duplicate marks. - private List seenSourceIDs = new ArrayList(); - - /** - * Constructor for a RaceXMLObject. - * Takes the information from a Document object and creates a more usable format. - * - * @param doc XML Document Object - */ - RaceXMLObject(Document doc) { - Element docEle = doc.getDocumentElement(); - - //Atomic and Semi-Atomic Elements - this.raceID = getElementInt(docEle, "RaceID"); - this.raceType = getElementString(docEle, "RaceType"); - this.creationTimeDate = getElementString(docEle, "CreationTimeDate"); - - Node raceStart = docEle.getElementsByTagName("RaceStartTime").item(0); - this.raceStartTime = getNodeAttributeString(raceStart, "Start"); - this.postponeStatus = Boolean - .parseBoolean(getNodeAttributeString(raceStart, "Postpone")); - - //Participants - participants = new ArrayList<>(); - - NodeList pList = docEle.getElementsByTagName("Participants").item(0).getChildNodes(); - for (int i = 0; i < pList.getLength(); i++) { - Node pNode = pList.item(i); - String entry; - if (pNode.getNodeName().equals("Yacht")) { - Integer sourceID = getNodeAttributeInt(pNode, "SourceID"); - - if (pNode.getAttributes().getLength() == 2) { - entry = getNodeAttributeString(pNode, "Entry"); - } else { - entry = null; - } - - Participant pa = new Participant(sourceID, entry); - participants.add(pa); - } - } - - //Course - allMarks = new ArrayList<>(); - nonDuplicateMarks = new ArrayList<>(); - createCompoundMarks(docEle); - - //Course Mark Sequence - compoundMarkSequence = new ArrayList<>(); - - NodeList cornerList = docEle.getElementsByTagName("CompoundMarkSequence").item(0) - .getChildNodes(); - for (int i = 0; i < cornerList.getLength(); i++) { - Node cornerNode = cornerList.item(i); - if (cornerNode.getNodeName().equals("Corner")) { - Corner corner = new Corner(cornerNode); - compoundMarkSequence.add(corner); - } - } - - //Course Limits - courseLimit = new ArrayList<>(); - - NodeList limitList = docEle.getElementsByTagName("CourseLimit").item(0).getChildNodes(); - for (int i = 0; i < limitList.getLength(); i++) { - Node limitNode = limitList.item(i); - if (limitNode.getNodeName().equals("Limit")) { - Limit limit = new Limit(limitNode); - courseLimit.add(limit); - } - } - } - - - private void createCompoundMarks(Element docEle) { - - NodeList cMarkList = docEle.getElementsByTagName("Course").item(0).getChildNodes(); - for (int i = 0; i < cMarkList.getLength(); i++) { - Node cMarkNode = cMarkList.item(i); - if (cMarkNode.getNodeName().equals("CompoundMark")) { - createAndAddMark(cMarkNode); - } - } - } - - - private void createAndAddMark(Node compoundMark) { - - Boolean markSeen = false; - List marksList = new ArrayList<>(); - Integer compoundMarkID = getNodeAttributeInt(compoundMark, "CompoundMarkID"); - String cMarkName = getNodeAttributeString(compoundMark, "Name"); - - NodeList childMarks = compoundMark.getChildNodes(); - - for (int i = 0; i < childMarks.getLength(); i++) { - Node markNode = childMarks.item(i); - if (markNode.getNodeName().equals("Mark")) { - - Integer sourceID = getNodeAttributeInt(markNode, "SourceID"); - String markName = getNodeAttributeString(markNode, "Name"); - Double targetLat = getNodeAttributeDouble(markNode, "TargetLat"); - Double targetLng = getNodeAttributeDouble(markNode, "TargetLng"); - - SingleMark mark = new SingleMark(markName, targetLat, targetLng, sourceID, compoundMarkID); - marksList.add(mark); - } - } - - for (SingleMark mark : marksList) { - if (seenSourceIDs.contains(mark.getId())) { - markSeen = true; - } else { - seenSourceIDs.add(mark.getId()); - } - } - - - if (marksList.size() == 1) { - if (!markSeen) { - nonDuplicateMarks.add(marksList.get(0)); - } - allMarks.add(marksList.get(0)); - } else if (marksList.size() == 2) { - GateMark thisGateMark = new GateMark(cMarkName, MarkType.OPEN_GATE, marksList.get(0), - marksList.get(1), marksList.get(0).getLatitude(), - marksList.get(0).getLongitude(), compoundMarkID); - if(!markSeen) { - nonDuplicateMarks.add(thisGateMark); - } - allMarks.add(thisGateMark); - } - - } - - public Integer getRaceID() { - return raceID; - } - - public String getRaceType() { - return raceType; - } - - public String getCreationTimeDate() { - return creationTimeDate; - } - - public String getRaceStartTime() { - return raceStartTime; - } - - public Boolean getPostponeStatus() { - return postponeStatus; - } - - public ArrayList getParticipants() { - return participants; - } - - /** - * @return Returns ALL compound marks as stated in the RaceXML (INCLUDING DUPLICATE MARKS) - */ - public List getAllCompoundMarks() { - return allMarks; - } - - /** - * @return Returns Marks from the race XML without any duplicates - */ - public List getNonDupCompoundMarks() { - return nonDuplicateMarks; - } - - public ArrayList getCompoundMarkSequence() { - return compoundMarkSequence; - } - - public ArrayList getCourseLimit() { - return courseLimit; - } - - public class Participant { - - Integer sourceID; - String entry; - - Participant(Integer sourceID, String entry) { - this.sourceID = sourceID; - this.entry = entry; - } - - public Integer getsourceID() { - return sourceID; - } - - public String getEntry() { - return entry; - } - } - - public class Corner { - - private Integer seqID; - private Integer compoundMarkID; - private String rounding; - private Integer zoneSize; - - Corner(Node cornerNode) { - this.seqID = getNodeAttributeInt(cornerNode, "SeqID"); - this.compoundMarkID = getNodeAttributeInt(cornerNode, "CompoundMarkID"); - this.rounding = getNodeAttributeString(cornerNode, "Rounding"); - this.zoneSize = getNodeAttributeInt(cornerNode, "ZoneSize"); - } - - public Integer getSeqID() { - return seqID; - } - - public Integer getCompoundMarkID() { - return compoundMarkID; - } - - public String getRounding() { - return rounding; - } - - public Integer getZoneSize() { - return zoneSize; - } - } - - public class Limit { - - private Integer seqID; - private Double lat; - private Double lng; - - Limit(Node limitNode) { - this.seqID = getNodeAttributeInt(limitNode, "SeqID"); - this.lat = getNodeAttributeDouble(limitNode, "Lat"); - this.lng = getNodeAttributeDouble(limitNode, "Lon"); - } - - public Integer getSeqID() { - return seqID; - } - - public Double getLat() { - return lat; - } - - public Double getLng() { - return lng; - } - } - - } - - public class BoatXMLObject { - - private String lastModified; - private Integer version; - - //Settings for the boat type in the race. This may end up having to be reworked if multiple boat types compete. - private String boatType; - private Double boatLength; - private Double hullLength; - private Double markZoneSize; - private Double courseZoneSize; - private ArrayList zoneLimits;// will only contain 5 elements. Limits 1-5 - - //Boats - ArrayList boats; - //Competing boats - Map competingBoats = new HashMap<>(); - - /** - * Constructor for a BoatXMLObject. - * Takes the information from a Document object and creates a more usable format. - * - * @param doc XML Document Object - */ - BoatXMLObject(Document doc) { - - Element docEle = doc.getDocumentElement(); - - this.lastModified = getElementString(docEle, "Modified"); - this.version = getElementInt(docEle, "Version"); - - NodeList settingsList = docEle.getElementsByTagName("Settings").item(0).getChildNodes(); - this.boatType = getNodeAttributeString(settingsList.item(1), "Type"); - this.boatLength = getNodeAttributeDouble(settingsList.item(3), "BoatLength"); - this.hullLength = getNodeAttributeDouble(settingsList.item(3), "HullLength"); - this.markZoneSize = getNodeAttributeDouble(settingsList.item(5), "MarkZoneSize"); - this.courseZoneSize = getNodeAttributeDouble(settingsList.item(5), "CourseZoneSize"); - - Node zoneLimitsList = settingsList.item(7); - this.zoneLimits = new ArrayList<>(); - for (int i = 0; i < zoneLimitsList.getAttributes().getLength(); i++) { - String tag = String.format("Limit%d", i + 1); - this.zoneLimits.add(getNodeAttributeDouble(zoneLimitsList, tag)); - } - - this.boats = new ArrayList<>(); - NodeList boatsList = docEle.getElementsByTagName("Boats").item(0).getChildNodes(); - for (int i = 0; i < boatsList.getLength(); i++) { - Node currentBoat = boatsList.item(i); - if (currentBoat.getNodeName().equals("Boat")) { -// Boat boat = new Boat(currentBoat); - Yacht boat = new Yacht(getNodeAttributeString(currentBoat, "Type"), - getNodeAttributeInt(currentBoat, "SourceID"), - getNodeAttributeString(currentBoat, "HullNum"), - getNodeAttributeString(currentBoat, "ShortName"), - getNodeAttributeString(currentBoat, "BoatName"), - getNodeAttributeString(currentBoat, "Country")); - this.boats.add(boat); - if (boat.getBoatType().equals("Yacht")) { - competingBoats.put(boat.getSourceId(), boat); - } - } - } - - } - - public String getLastModified() { - return lastModified; - } - - public Integer getVersion() { - return version; - } - - public String getBoatType() { - return boatType; - } - - public Double getBoatLength() { - return boatLength; - } - - public Double getHullLength() { - return hullLength; - } - - public Double getMarkZoneSize() { - return markZoneSize; - } - - public Double getCourseZoneSize() { - return courseZoneSize; - } - - public ArrayList getZoneLimits() { - return zoneLimits; - } - - public ArrayList getBoats() { - return boats; - } - - public Map getCompetingBoats() { - return competingBoats; - } - - } - -} \ No newline at end of file diff --git a/src/main/java/seng302/models/stream/packets/BoatPositionPacket.java b/src/main/java/seng302/models/stream/packets/BoatPositionPacket.java deleted file mode 100644 index 859223e0..00000000 --- a/src/main/java/seng302/models/stream/packets/BoatPositionPacket.java +++ /dev/null @@ -1,39 +0,0 @@ -package seng302.models.stream.packets; - -public class BoatPositionPacket { - private long boatId; - private long timeValid; - private double lat; - private double lon; - private double heading; - private double groundSpeed; - - public BoatPositionPacket(long boatId, long timeValid, double lat, double lon, double heading, double groundSpeed) { - this.boatId = boatId; - this.timeValid = timeValid; - this.lat = lat; - this.lon = lon; - this.heading = heading; - this.groundSpeed = groundSpeed; - } - - public long getTimeValid() { - return timeValid; - } - - public double getLat() { - return lat; - } - - public double getLon() { - return lon; - } - - public double getHeading() { - return heading; - } - - public double getGroundSpeed() { - return groundSpeed; - } -} diff --git a/src/main/java/seng302/server/messages/RoundingSide.java b/src/main/java/seng302/server/messages/RoundingSide.java deleted file mode 100644 index 5cc4097c..00000000 --- a/src/main/java/seng302/server/messages/RoundingSide.java +++ /dev/null @@ -1,20 +0,0 @@ -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/simulator/Boat.java b/src/main/java/seng302/server/simulator/Boat.java deleted file mode 100644 index 435a70b6..00000000 --- a/src/main/java/seng302/server/simulator/Boat.java +++ /dev/null @@ -1,126 +0,0 @@ -package seng302.server.simulator; - -import seng302.server.simulator.mark.Corner; -import seng302.utilities.GeoPoint; -import seng302.utilities.GeoUtility; - -public class Boat { - - private int sourceID; - private double lat; - private double lng; - private double speed; // in mm/sec - private String boatName, shortName, shorterName; - private boolean isFinished; - private long estimatedTimeTillFinish; - - private Corner lastPassedCorner, headingCorner; - - public Boat(int sourceID, String boatName) { - this.sourceID = sourceID; - this.boatName = boatName; - this.isFinished = false; - estimatedTimeTillFinish = 0; - } - - /** - * 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 - GeoPoint originPos = new GeoPoint(lat, lng); - GeoPoint 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; - } - - public boolean isFinished() { - return isFinished; - } - - public void setFinished(boolean finished) { - isFinished = finished; - } - - public long getEstimatedTimeTillFinish(){ - return (long) (-getSpeed()) + System.currentTimeMillis(); - } -} diff --git a/src/main/java/seng302/server/simulator/Simulator.java b/src/main/java/seng302/server/simulator/Simulator.java deleted file mode 100644 index 363977c9..00000000 --- a/src/main/java/seng302/server/simulator/Simulator.java +++ /dev/null @@ -1,139 +0,0 @@ -package seng302.server.simulator; - -import seng302.server.simulator.mark.Corner; -import seng302.server.simulator.mark.Mark; -import seng302.server.simulator.parsers.RaceParser; -import seng302.utilities.GeoPoint; -import seng302.utilities.GeoUtility; - -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; - private boolean isRaceStarted; - - /** - * 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; - isRaceStarted = false; - - 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()) { - - // if race has started, then boat should start to move. - if (isRaceStarted) { - for (Boat boat : boats) { - numOfFinishedBoats += moveBoat(boat, lapse); - } - } - - setChanged(); - notifyObservers(boats); - - try { - Thread.sleep(lapse); - } catch (InterruptedException e) { - System.out.println("[Simulator] interrupted exception "); - } - } - } - - /** - * 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); - - GeoPoint boatPos = new GeoPoint(boat.getLat(), boat.getLng()); - GeoPoint 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) { - boat.setFinished(true); - return 1; - } - - // move compensate distance for the mark just passed - GeoPoint pos = GeoUtility.getGeoCoordinate( - boat.getLastPassedCorner().getCompoundMark().getMark1(), - boat.getLastPassedCorner().getBearingToNextCorner(), - compensateDistance); - boat.setLat(pos.getLat()); - boat.setLng(pos.getLng()); - distanceFromLastMark = GeoUtility.getDistance(new GeoPoint(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; - } - - public void setRaceStarted(boolean raceStarted) { - isRaceStarted = raceStarted; - } -} diff --git a/src/main/java/seng302/server/simulator/mark/CompoundMark.java b/src/main/java/seng302/server/simulator/mark/CompoundMark.java deleted file mode 100644 index 489a4a12..00000000 --- a/src/main/java/seng302/server/simulator/mark/CompoundMark.java +++ /dev/null @@ -1,70 +0,0 @@ -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 deleted file mode 100644 index 136212f2..00000000 --- a/src/main/java/seng302/server/simulator/mark/Corner.java +++ /dev/null @@ -1,89 +0,0 @@ -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 deleted file mode 100644 index 0b6f1f3b..00000000 --- a/src/main/java/seng302/server/simulator/mark/Mark.java +++ /dev/null @@ -1,55 +0,0 @@ -package seng302.server.simulator.mark; - -import seng302.utilities.GeoPoint; - -/** - * An abstract class to represent general marks - * Created by Haoming Yin (hyi25) on 17/3/17. - */ -public class Mark extends GeoPoint { - - 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, getLat(), getLng()); - } - - 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/RoundingType.java b/src/main/java/seng302/server/simulator/mark/RoundingType.java deleted file mode 100644 index de6f6133..00000000 --- a/src/main/java/seng302/server/simulator/mark/RoundingType.java +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index 5d552a00..00000000 --- a/src/main/java/seng302/server/simulator/parsers/BoatsParser.java +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index f7be46cd..00000000 --- a/src/main/java/seng302/server/simulator/parsers/CourseParser.java +++ /dev/null @@ -1,118 +0,0 @@ -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 deleted file mode 100644 index d724e0bc..00000000 --- a/src/main/java/seng302/server/simulator/parsers/FileParser.java +++ /dev/null @@ -1,52 +0,0 @@ -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) { - System.out.println("[FileParser] Exception"); - 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) { - System.out.println("[FileParser] Exception"); - } - return null; - } -} diff --git a/src/main/java/seng302/server/simulator/parsers/RaceParser.java b/src/main/java/seng302/server/simulator/parsers/RaceParser.java deleted file mode 100644 index 14bf7bb8..00000000 --- a/src/main/java/seng302/server/simulator/parsers/RaceParser.java +++ /dev/null @@ -1,66 +0,0 @@ -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/java/seng302/utilities/GeoUtility.java b/src/main/java/seng302/utilities/GeoUtility.java index 0e17ff1a..1c8c2d21 100644 --- a/src/main/java/seng302/utilities/GeoUtility.java +++ b/src/main/java/seng302/utilities/GeoUtility.java @@ -1,133 +1,254 @@ package seng302.utilities; import javafx.geometry.Point2D; +import seng302.model.GeoPoint; public class GeoUtility { - private static double EARTH_RADIUS = 6378.137; + private static double EARTH_RADIUS = 6378.137; + private static Double MS_TO_KNOTS = 1.943844492; - /** - * 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(GeoPoint p1, GeoPoint p2) { + /** + * 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(GeoPoint p1, GeoPoint p2) { - double dLat = Math.toRadians(p2.getLat() - p1.getLat()); - double dLon = Math.toRadians(p2.getLng() - p1.getLng()); + 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 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; + 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 - } + 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(GeoPoint p1, GeoPoint 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 GeoPoint getGeoCoordinate(GeoPoint 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 GeoPoint(Math.toDegrees(endLat), Math.toDegrees(endLng)); - } - - /** - * Performs the line function on two points of a line and a test point to test which side of the line that point is - * on. If the return value is - * return 1, then the point is on one side of the line, - * return -1 then the point is on the other side of the line - * return 0 then the point is exactly on the line. - * @param linePoint1 One point of the line - * @param linePoint2 Second point of the line - * @param testPoint The point to test with this line - * @return A return value indicating which side of the line the point is on - */ - public static Integer lineFunction(Point2D linePoint1, Point2D linePoint2, Point2D testPoint) { - - Double x = testPoint.getX(); - Double y = testPoint.getY(); - Double x1 = linePoint1.getX(); - Double y1 = linePoint1.getY(); - Double x2 = linePoint2.getX(); - Double y2 = linePoint2.getY(); - - Double result = (x - x1)*(y2 - y1) - (y - y1)*(x2 - x1); //Line function - - if (result > 0) { - return 1; - } - else if (result < 0) { - return -1; - } - else { - return 0; - } - } + /** + * 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(GeoPoint p1, GeoPoint p2) { + return (Math.toDegrees(getBearingRad(p1, p2)) + 360.0) % 360.0; + } - /** - * Given a point and a vector (angle and vector length) Will create a new point, that vector away from the origin - * point - * @param originPoint The point with which to use as the base for our vector addition - * @param angleInDeg (DEGREES) The angle at which our new point is being created (in degrees!) - * @param vectorLength The length out on this angle from the origin point to create the new point - * @return a Point2D - */ - public static Point2D makeArbitraryVectorPoint(Point2D originPoint, Double angleInDeg, Double vectorLength) { + /** + * WARNING: this function DOES NOT account for wrapping around on lats / longs etc. + * SO BE CAREFUL IN USING THIS FUNCTION + * + * @param p1 GeoPoint 1 + * @param p2 GeoPoint 2 + * @return GeoPoint midPoint + */ + public static GeoPoint getDirtyMidPoint(GeoPoint p1, GeoPoint p2) { + return new GeoPoint((p1.getLat() + p2.getLat()) / 2, (p1.getLng() + p2.getLng()) / 2); + } - Double endPointX = originPoint.getX() + vectorLength * Math.cos(Math.toRadians(angleInDeg)); - Double endPointY = originPoint.getY() + vectorLength * Math.sin(Math.toRadians(angleInDeg)); + /** + * Calculates the angle between to angular co-ordinates on a sphere in radians. + * + * @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 getBearingRad(GeoPoint p1, GeoPoint p2) { + double dLon = Math.toRadians(p2.getLng() - p1.getLng()); - return new Point2D(endPointX, endPointY); + 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); - } + return Math.atan2(y, x); + } + + /** + * 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 GeoPoint getGeoCoordinate(GeoPoint 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 GeoPoint(Math.toDegrees(endLat), Math.toDegrees(endLng)); + } + + /** + * Performs the line function on two points of a line and a test point to test which side of the + * line that point is on. If the return value is return 1, then the point is on one side of the + * line, return -1 then the point is on the other side of the line return 0 then the point is + * exactly on the line. + * + * @param linePoint1 One point of the line + * @param linePoint2 Second point of the line + * @param testPoint The point to test with this line + * @return A return value indicating which side of the line the point is on + */ + public static Integer lineFunction(Point2D linePoint1, Point2D linePoint2, Point2D testPoint) { + + Double x = testPoint.getX(); + Double y = testPoint.getY(); + Double x1 = linePoint1.getX(); + Double y1 = linePoint1.getY(); + Double x2 = linePoint2.getX(); + Double y2 = linePoint2.getY(); + + Double result = (x - x1) * (y2 - y1) - (y - y1) * (x2 - x1); //Line function + + if (result > 0) { + return 1; + } else if (result < 0) { + return -1; + } else { + return 0; + } + } + + /** + * Checks if the line formed by lastLocation and location doesn't intersect the line segment + * formed by mark1 and mark2 See the wiki Mark Rounding algorithm for more info + * + * @param mark1 One mark of the line + * @param mark2 The second mark of the line + * @param lastLocation The last location of the point crossing this line + * @param location The current location of the point crossing this line + * @return 0 if two line segment doesn't intersect, otherwise 1 if they intersect and + * lastLocation is on RHS of the line segment (mark1 to mark2) or 2 if lastLocation on LHS of + * the line segment (mark1 to mark2) + */ + public static Integer checkCrossedLine(GeoPoint mark1, GeoPoint mark2, GeoPoint lastLocation, + GeoPoint location) { + boolean enteredDirection = isClockwise(mark1, mark2, lastLocation); + boolean exitedDirection = isClockwise(mark1, mark2, location); + if (enteredDirection != exitedDirection) { + if (!isPointInTriangle(mark1, lastLocation, location, mark2) + && !isPointInTriangle(mark2, lastLocation, location, mark1)) { + + return enteredDirection ? 1 : 2; + } + } + return 0; + } + + /** + * Given a point and a vector (angle and vector length) Will create a new point, that vector + * away from the origin point + * + * @param originPoint The point with which to use as the base for our vector addition + * @param angleInDeg (DEGREES) The angle at which our new point is being created (in degrees!) + * @param vectorLength The length out on this angle from the origin point to create the new + * point + * @return a Point2D + */ + public static Point2D makeArbitraryVectorPoint(Point2D originPoint, Double angleInDeg, + Double vectorLength) { + + Double endPointX = originPoint.getX() + vectorLength * Math.cos(Math.toRadians(angleInDeg)); + Double endPointY = originPoint.getY() + vectorLength * Math.sin(Math.toRadians(angleInDeg)); + + return new Point2D(endPointX, endPointY); + + } + + /** + * Define vector v1 = p1 - p0 to v2 = p2- p0. This function returns the difference of bearing + * from v1 to v2. For example, if bearing of v1 is 30 deg and bearing of v2 is 90 deg, then the + * difference is 60 deg. + * + * @param bearing1 the bearing of v1 + * @param bearing2 the bearing of v2 + * @return the difference of bearing from v1 to v2 + */ + private static Double getBearingDiff(double bearing1, double bearing2) { + return ((360 - bearing1) + bearing2) % 360; + } + + /** + * Check if a geo point ins on the right hand side of the line segment, which + * formed by two geo points v1 to v2. (Algorithm: point is clockwise to the + * line if the bearing difference is less than 180 deg.) + * + * @param v1 one end of the line segment + * @param v2 another end of the line segment + * @param point the point to be tested + * @return true if the point is on the RHS of the line + */ + public static Boolean isClockwise(GeoPoint v1, GeoPoint v2, GeoPoint point) { + return getBearingDiff(getBearing(v1, v2), getBearing(v1, point)) < 180; + } + + /** + * Given three geo points to form a triangle, the method returns true if the fourth point is + * inside the triangle + * + * @param v1 the vertex of the triangle + * @param v2 the vertex of the triangle + * @param v3 the vertex of the triangle + * @param point the point to be tested + * @return true if the fourth point is inside the triangle + */ + public static Boolean isPointInTriangle(GeoPoint v1, GeoPoint v2, GeoPoint v3, GeoPoint point) { + // true, if diff of bearing from (v1 to v2) to (v1 to p) is less than 180 deg + boolean isCW = isClockwise(v1, v2, point); + + if (isClockwise(v2, v3, point) != isCW) { + return false; + } + + if (isClockwise(v3, v1, point) != isCW) { + return false; + } + + return true; + } + + /** + * @param boatSpeedInKnots Speed in knots + * @return The Boat speed in millimeters per second + */ + public static Double knotsToMMS(Double boatSpeedInKnots) { + return boatSpeedInKnots / MS_TO_KNOTS * 1000; + } + + /** + * @param boatSpeedInMMS Speed in millimeters per second + * @return The Boat speed in knots + */ + public static Double mmsToKnots(Double boatSpeedInMMS) { + return boatSpeedInMMS / 1000 * MS_TO_KNOTS; + } } diff --git a/src/main/java/seng302/utilities/StreamParser.java b/src/main/java/seng302/utilities/StreamParser.java new file mode 100644 index 00000000..1f90eac8 --- /dev/null +++ b/src/main/java/seng302/utilities/StreamParser.java @@ -0,0 +1,434 @@ +package seng302.utilities; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import seng302.model.stream.packets.PacketType; +import seng302.model.stream.packets.StreamPacket; +import seng302.model.stream.parser.*; +import seng302.model.stream.parser.PositionUpdateData.DeviceType; + +/** + * StreamParser is a utilities class for taking byte data, formatted according to the AC35 streaming + * protocol, and parsing it into basic data types or collections. + * + * Created by kre39 on 23/04/17. + */ +public class StreamParser { + + /** + * Extracts and returns the seq num used in the heartbeat packet. + * + * @param packet Packet parsed in to use the payload + * @return the packet sequence number if the packet is of type HEARTBEAT, null otherwise. + */ + public static Long extractHeartBeat(StreamPacket packet) { + if (packet.getType() != PacketType.HEARTBEAT) { + return null; + } + long heartbeat = bytesToLong(packet.getPayload()); + System.out.println("heartbeat = " + heartbeat); + return heartbeat; + } + + /** + * Extracts the useful race status data from race status type packets. This method will also + * print to the console the current state of the race (if it has started/finished or is about to + * start), along side this it'll also display the amount of time since the race has started or + * time till it starts + * + * @param packet Packet parsed in to use the payload + * @return null if the packet type is not RACE_STATUS, otherwise an instance of RaceStatusData + * containing the parsed packet data. + */ + public static RaceStatusData extractRaceStatus(StreamPacket packet) { + if (packet.getType() != PacketType.RACE_STATUS) { + return null; + } + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + long currentTime = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); + long raceId = bytesToLong(Arrays.copyOfRange(payload, 7, 11)); + int raceStatus = payload[11]; + long expectedStartTime = bytesToLong(Arrays.copyOfRange(payload, 12, 18)); + long windDir = bytesToLong(Arrays.copyOfRange(payload, 18, 20)); + long rawWindSpeed = bytesToLong(Arrays.copyOfRange(payload, 20, 22)); + +// DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); +// currentTime = format.format((new Date(currentTime))) + + RaceStatusData data = new RaceStatusData( + windDir, rawWindSpeed, raceStatus, currentTime, expectedStartTime + ); + +// long timeTillStart = +// ((new Date(expectedStartTime)).getTime() - (new Date(currentTime)).getTime()) / 1000; +// +// if (timeTillStart > 0) { +// timeSinceStart = timeTillStart; +// } else { +// if (raceStatus == 4 || raceStatus == 8) { +// raceFinished = true; +// raceStarted = false; +// } else if (!raceStarted) { +// raceStarted = true; +// raceFinished = false; +// } +// timeSinceStart = timeTillStart; +// } +// + +// + int noBoats = payload[22]; + int raceType = payload[23]; + long boatID, estTimeAtNextMark, estTimeAtFinish; + int leg, boatStatus; + for (int i = 0; i < noBoats; i++) { + boatID = bytesToLong( + Arrays.copyOfRange(payload, 24 + (i * 20), 28 + (i * 20))); + boatStatus = (int) payload[28 + (i * 20)]; + estTimeAtNextMark = bytesToLong( + Arrays.copyOfRange(payload, 32 + (i * 20), 38 + (i * 20))); + estTimeAtFinish = bytesToLong( + Arrays.copyOfRange(payload, 38 + (i * 20), 44 + (i * 20))); + leg = (int) payload[29 + (i * 20)]; +// boat.setEstimateTimeAtFinish(estTimeAtFinish); + data.addBoatData(boatID, estTimeAtNextMark, estTimeAtFinish, leg, boatStatus); + } + return data; + } + +// private static void setBoatLegPosition(Yacht updatingBoat, Integer leg){ +// Integer placing = 1; +// if (leg != updatingBoat.getLegNumber() && (raceStarted || raceFinished)) { +// for (Yacht boat : boats.values()) { +// if (boat.getLegNumber() != null && leg <= boat.getLegNumber()){ +// placing += 1; +// } +// } +// updatingBoat.setPlacing(placing.toString()); +// updatingBoat.setLegNumber(leg); +// boatsPos.putIfAbsent(placing, updatingBoat); +// boatsPos.replace(placing, updatingBoat); +// } else if(updatingBoat.getLegNumber() == null){ +// updatingBoat.setPlacing("1"); +// updatingBoat.setLegNumber(leg); +// } +// } + + /** + * Parses and returns the text from a StreamPacket containing text data for display. + * + * @param packet Packet parsed in to use the payload + * @return A list containing all display message text. Is null if the packet is not of type + * DISPLAY_TEXT_MESSAGE. + */ + public static List extractDisplayMessage(StreamPacket packet) { + if (packet.getType() != PacketType.DISPLAY_TEXT_MESSAGE) { + return null; + } + List message = new ArrayList<>(); + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + int numOfLines = payload[3]; + int totalLen = 0; + for (int i = 0; i < numOfLines; i++) { + int lineNum = payload[4 + totalLen]; + int textLength = payload[5 + totalLen]; + byte[] messageTextBytes = Arrays + .copyOfRange(payload, 6 + totalLen, 6 + textLength + totalLen); + message.add(new String(messageTextBytes)); + totalLen += 2 + textLength; + } + return message; + } + + /** + * Parses and returns an XMLParser containing XML data sent in the given StreamPacket. XML data + * can be for races, boats or the regatta. + * + * @param packet Packet parsed in to use the payload + * @return XMLParse containing xmldata. Returns null if the StreamPacket is not of type + * XML_MESSAGE. + */ + public static Document extractXmlMessage(StreamPacket packet) { + if (packet.getType() != PacketType.RACE_XML && + packet.getType() != PacketType.REGATTA_XML && + packet.getType() != PacketType.BOAT_XML) { + return null; + } + + byte[] payload = packet.getPayload(); + int messageType = payload[9]; + long messageLength = bytesToLong(Arrays.copyOfRange(payload, 12, 14)); + String xmlMessage = new String( + (Arrays.copyOfRange(payload, 14, (int) (14 + messageLength)))).trim(); + + //Create XML document Object + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db; + Document doc = null; + try { + db = dbf.newDocumentBuilder(); + doc = db.parse(new InputSource(new StringReader(xmlMessage))); + } catch (ParserConfigurationException | IOException | SAXException e) { + e.printStackTrace(); + } + return doc; + } + + /** + * Extracts the race start status from the packet and returns it as a long array. + * + * @param packet Packet parsed in to use the payload + * @return An array of form [raceID, raceStartTime, notificationType, timeStamp] or null if the + * packet type is not of RACE_START_STATUS. + */ + public static RaceStartData extractRaceStartStatus(StreamPacket packet) { + if (packet.getType() != PacketType.RACE_START_STATUS) { + return null; + } + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); + long raceStartTime = bytesToLong(Arrays.copyOfRange(payload, 9, 15)); + long raceId = bytesToLong(Arrays.copyOfRange(payload, 15, 19)); + int notificationType = payload[19]; + return new RaceStartData(raceId, raceStartTime, notificationType, timeStamp); + } + + /** + * Parses the the byte array in a StreamPacket for yacht events to retrieve the necessary info + * and returns it as YachtEventData. + * + * @param packet Packet parsed in to use the payload + * @return the event data in the form of YachtEventData. Returns null if the packet is not of + * type YACHT_EVENT_CODE. + */ + public static YachtEventData extractYachtEventCode(StreamPacket packet) { + if (packet.getType() != PacketType.YACHT_EVENT_CODE) { + return null; + } + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); + long ackNumber = bytesToLong(Arrays.copyOfRange(payload, 7, 9)); + long raceId = bytesToLong(Arrays.copyOfRange(payload, 9, 13)); + long subjectId = bytesToLong(Arrays.copyOfRange(payload, 13, 17)); + long incidentId = bytesToLong(Arrays.copyOfRange(payload, 17, 21)); + int eventId = payload[21]; + return new YachtEventData(subjectId, incidentId, eventId, timeStamp); + } + + /** + * Parses data from a StreamPacket for yacht actions and returns it in a long array. + * + * @param packet Packet parsed in to use the payload + * @return long array of packet data in the form [subjectID, incidentID, eventID, timeStamp]. + * Returns null if the packet is not of type YACHT_ACTION_CODE. + */ + public static long[] extractYachtActionCode(StreamPacket packet) { + if (packet.getType() != PacketType.YACHT_ACTION_CODE) { + return null; + } + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); + long subjectId = bytesToLong(Arrays.copyOfRange(payload, 9, 13)); + long incidentId = bytesToLong(Arrays.copyOfRange(payload, 13, 17)); + int eventId = payload[17]; + return new long[]{subjectId, incidentId, eventId, timeStamp}; + } + + /** + * Strips the message from the chatter text type packets. + * + * @param packet Packet parsed in to use the payload + * @return Chatter text message as a string. Returns null if the packet is not of type + * CHATTER_TEXT. + */ + public static String extractChatterText(StreamPacket packet) { + if (packet.getType() != PacketType.CHATTER_TEXT) { + return null; + } + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + int messageType = payload[1]; + int length = payload[2]; + return new String(Arrays.copyOfRange(payload, 3, 3 + length)); + } + + /** + * Takes the data from a bot location stream packet and parses the id, timeValid, lat, lon, + * heading and groundspeed into a BoatPositionPacket which is returned. + * + * @param packet Packet parsed in to use the payload + * @return BoatPositionPacket containing important boat information. Returns null if the packet + * is not of type BOAT_LOCATION. + */ + public static PositionUpdateData extractBoatLocation(StreamPacket packet) { + if (packet.getType() != PacketType.BOAT_LOCATION) { + return null; + } + byte[] payload = packet.getPayload(); + int deviceType = (int) payload[15]; + long timeValid = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); + long seq = bytesToLong(Arrays.copyOfRange(payload, 11, 15)); + long boatId = bytesToLong(Arrays.copyOfRange(payload, 7, 11)); + long rawLat = bytesToLong(Arrays.copyOfRange(payload, 16, 20)); + long rawLon = bytesToLong(Arrays.copyOfRange(payload, 20, 24)); + //Converts the double to a usable lat/lon + double lat = ((180d * (double) rawLat) / Math.pow(2, 31)); + double lon = ((180d * (double) rawLon) / Math.pow(2, 31)); + double heading = bytesToLong(Arrays.copyOfRange(payload, 28, 30)); + heading = 360.0 / 0xffff * heading; //Convert to degrees. + double groundSpeed = bytesToLong(Arrays.copyOfRange(payload, 38, 40)) / 1000.0; + + DeviceType type; + if (deviceType == 1) { + type = DeviceType.YACHT_TYPE; + } else { + type = DeviceType.MARK_TYPE; + } + + return new PositionUpdateData((int) boatId, type, lat, lon, heading, groundSpeed); + } + + /** + * Processes a stream packet for a mark rounding and returns the boatID, markID and timestamp. + * + * @param packet The packet containing the payload + * @return an array containing longs. The values are [boatID, markID, timeStamp]. Returns null + * if packet is not of type MARK_ROUNDING. + */ + public static MarkRoundingData extractMarkRounding(StreamPacket packet) { + if (packet.getType() != PacketType.MARK_ROUNDING) { + return null; + } + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); + long raceId = bytesToLong(Arrays.copyOfRange(payload, 9, 13)); + long subjectId = bytesToLong(Arrays.copyOfRange(payload, 13, 17)); + int boatStatus = payload[17]; + int roundingSide = payload[18]; + int markType = payload[19]; + int markId = payload[20]; + + return new MarkRoundingData((int) subjectId, markId, roundingSide, timeStamp); + } + + /** + * Returns a list containing the string value of data within the given stream packet for course + * wind. + * + * @param packet The packet containing the payload + * @return the string values of the wind packet. Returns null if the packet is not of type + * COURSE_WIND. + */ + public static List extractCourseWind(StreamPacket packet) { + if (packet.getType() != PacketType.COURSE_WIND) { + return null; + } + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + int selectedWindId = payload[1]; + int loopCount = payload[2]; + List windInfo = new ArrayList<>(); + for (int i = 0; i < loopCount; i++) { + String wind = "WindId: " + payload[3 + (20 * i)]; + wind += + "\nTime: " + bytesToLong(Arrays.copyOfRange(payload, 4 + (20 * i), 10 + (20 * i))); + wind += "\nRaceId: " + bytesToLong( + Arrays.copyOfRange(payload, 10 + (20 * i), 14 + (20 * i))); + wind += "\nWindDirection: " + bytesToLong( + Arrays.copyOfRange(payload, 14 + (20 * i), 16 + (20 * i))); + wind += "\nWindSpeed: " + bytesToLong( + Arrays.copyOfRange(payload, 16 + (20 * i), 18 + (20 * i))); + wind += "\nBestUpWindAngle: " + bytesToLong( + Arrays.copyOfRange(payload, 18 + (20 * i), 20 + (20 * i))); + wind += "\nBestDownWindAngle: " + bytesToLong( + Arrays.copyOfRange(payload, 20 + (20 * i), 22 + (20 * i))); + wind += "\nFlags: " + String + .format("%8s", Integer.toBinaryString(payload[22 + (20 * i)] & 0xFF)) + .replace(' ', '0'); + windInfo.add(wind); + } + return windInfo; + } + + /** + * Returns the parsed data from a StreamPacket for average wind data. + * + * @param packet The packet containing the payload + * @return The wind data in the form [rawPeriod, rawSamplePeriod, period2, speed2, period3, + * speed3, period4, speed4, timestamp] or null if the packet is not of type AVG_WIND. + */ + public static long[] extractAvgWind(StreamPacket packet) { + if (packet.getType() != PacketType.AVG_WIND) { + return null; + } + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); + long rawPeriod = bytesToLong(Arrays.copyOfRange(payload, 7, 9)); + long rawSamplePeriod = bytesToLong(Arrays.copyOfRange(payload, 9, 11)); + long period2 = bytesToLong(Arrays.copyOfRange(payload, 11, 13)); + long speed2 = bytesToLong(Arrays.copyOfRange(payload, 13, 15)); + long period3 = bytesToLong(Arrays.copyOfRange(payload, 15, 17)); + long speed3 = bytesToLong(Arrays.copyOfRange(payload, 17, 19)); + long period4 = bytesToLong(Arrays.copyOfRange(payload, 19, 21)); + long speed4 = bytesToLong(Arrays.copyOfRange(payload, 21, 23)); + return new long[]{ + rawPeriod, rawSamplePeriod, period2, speed2, period3, speed3, period4, speed4, timeStamp + }; + } + + + public static void extractBoatAction(StreamPacket packet) { + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + long actionType = bytesToLong(Arrays.copyOfRange(payload, 0, 1)); + if (actionType == 1) { + System.out.println("VMG"); + } else if (actionType == 2) { + System.out.println("SAILS IN"); + } else if (actionType == 3) { + System.out.println("SAILS OUT"); + } else if (actionType == 4) { + System.out.println("TACK/GYBE"); + } else if (actionType == 5) { + System.out.println("UPWIND"); + } else if (actionType == 6) { + System.out.println("DOWNWIND"); + } + } + + /** + * takes an array of up to 7 bytes and returns a positive long constructed from the input bytes + * + * @param bytes the byte array to conver to Long + * @return a positive long if there is less than 7 bytes -1 otherwise + */ + public static long bytesToLong(byte[] bytes) { + long partialLong = 0; + int index = 0; + for (byte b : bytes) { + if (index > 6) { + return -1; + } + partialLong = partialLong | (b & 0xFFL) << (index * 8); + index++; + } + return partialLong; + } +} + diff --git a/src/main/java/seng302/models/xml/XMLGenerator.java b/src/main/java/seng302/utilities/XMLGenerator.java similarity index 93% rename from src/main/java/seng302/models/xml/XMLGenerator.java rename to src/main/java/seng302/utilities/XMLGenerator.java index 04a5b5fb..7fcc8efd 100644 --- a/src/main/java/seng302/models/xml/XMLGenerator.java +++ b/src/main/java/seng302/utilities/XMLGenerator.java @@ -1,13 +1,15 @@ -package seng302.models.xml; +package seng302.utilities; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; -import org.apache.commons.io.IOUtils; -import seng302.server.messages.XMLMessageSubType; - -import java.io.*; -import java.net.URISyntaxException; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import seng302.model.stream.xml.generator.Race; +import seng302.model.stream.xml.generator.Regatta; +import seng302.gameServer.messages.XMLMessageSubType; /** * An XML generator to generate the Race, Boat, and Regatta XML dynamically diff --git a/src/main/java/seng302/utilities/XMLParser.java b/src/main/java/seng302/utilities/XMLParser.java new file mode 100644 index 00000000..d7556234 --- /dev/null +++ b/src/main/java/seng302/utilities/XMLParser.java @@ -0,0 +1,294 @@ +package seng302.utilities; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javafx.scene.paint.Color; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import seng302.model.ClientYacht; +import seng302.model.Limit; +import seng302.model.mark.CompoundMark; +import seng302.model.mark.Corner; +import seng302.model.mark.Mark; +import seng302.model.stream.xml.parser.RaceXMLData; +import seng302.model.stream.xml.parser.RegattaXMLData; + +/** + * Utilities for parsing XML documents + */ +public class XMLParser { + + /** + * Returns the text content of a given child element tag, assuming it exists, as an Integer. + * + * @param ele Document Element with child elements. + * @param tag Tag to find in document elements child elements. + * @return Text content from tag if found, null otherwise. + */ + private static Integer getElementInt(Element ele, String tag) { + NodeList tagList = ele.getElementsByTagName(tag); + if (tagList.getLength() > 0) { + return Integer.parseInt(tagList.item(0).getTextContent()); + } else { + return null; + } + } + + /** + * Returns the text content of a given child element tag, assuming it exists, as an String. + * + * @param ele Document Element with child elements. + * @param tag Tag to find in document elements child elements. + * @return Text content from tag if found, null otherwise. + */ + private static String getElementString(Element ele, String tag) { + NodeList tagList = ele.getElementsByTagName(tag); + if (tagList.getLength() > 0) { + return tagList.item(0).getTextContent(); + } else { + return null; + } + } + + /** + * Returns the text content of a given child element tag, assuming it exists, as a Double. + * + * @param ele Document Element with child elements. + * @param tag Tag to find in document elements child elements. + * @return Text content from tag if found, null otherwise. + */ + private static Double getElementDouble(Element ele, String tag) { + NodeList tagList = ele.getElementsByTagName(tag); + if (tagList.getLength() > 0) { + return Double.parseDouble(tagList.item(0).getTextContent()); + } else { + return null; + } + } + + /** + * Returns the text content of an attribute of a given Node, assuming it exists, as a String. + * + * @param n A node object that should have some attributes + * @param attr The attribute you want to get from the given node. + * @return The String representation of the text content of an attribute in the given node, else + * returns null. + */ + private static String getNodeAttributeString(Node n, String attr) { + Node attrItem = n.getAttributes().getNamedItem(attr); + if (attrItem != null) { + return attrItem.getTextContent(); + } else { + return null; + } + } + + /** + * Returns the text content of an attribute of a given Node, assuming it exists, as an Integer. + * + * @param n A node object that should have some attributes + * @param attr The attribute you want to get from the given node. + * @return The Integer representation of the text content of an attribute in the given node, + * else returns null. + */ + private static Integer getNodeAttributeInt(Node n, String attr) { + Node attrItem = n.getAttributes().getNamedItem(attr); + if (attrItem != null) { + return Integer.parseInt(attrItem.getTextContent()); + } else { + return null; + } + } + + /** + * Returns the text content of an attribute of a given Node, assuming it exists, as a Double. + * + * @param n A node object that should have some attributes + * @param attr The attribute you want to get from the given node. + * @return The Double representation of the text content of an attribute in the given node, else + * returns null. + */ + private static Double getNodeAttributeDouble(Node n, String attr) { + Node attrItem = n.getAttributes().getNamedItem(attr); + if (attrItem != null) { + return Double.parseDouble(attrItem.getTextContent()); + } else { + return null; + } + } + + /** + * Produces a mapping of boat sourceIDS to boat objects created from the given xml document. + * @param doc XML Document Object + * @return Mapping of sourceIds to Boats. + */ + public static Map parseBoats(Document doc) { + Map competingBoats = new HashMap<>(); + + Element docEle = doc.getDocumentElement(); + + NodeList boatsList = docEle.getElementsByTagName("Boats").item(0).getChildNodes(); + for (int i = 0; i < boatsList.getLength(); i++) { + Node currentBoat = boatsList.item(i); + if (currentBoat.getNodeName().equals("Boat")) { +// Boat boat = new Boat(currentBoat); + ClientYacht yacht = new ClientYacht( + XMLParser.getNodeAttributeString(currentBoat, "Type"), + XMLParser.getNodeAttributeInt(currentBoat, "SourceID"), + XMLParser.getNodeAttributeString(currentBoat, "HullNum"), + XMLParser.getNodeAttributeString(currentBoat, "ShortName"), + XMLParser.getNodeAttributeString(currentBoat, "BoatName"), + XMLParser.getNodeAttributeString(currentBoat, "Country")); + yacht.setColour(Color.web(getNodeAttributeString(currentBoat, "Color"))); + if (yacht.getBoatType().equals("Yacht")) { + competingBoats.put(yacht.getSourceId(), yacht); + } + } + } + return competingBoats; + } + + /** + * Returns an object containing the data extracted from the given xml formatted document + * + * @param doc XML Document Object + * @return Object containing regatta data + */ + public static RegattaXMLData parseRegatta(Document doc) { + + Element docEle = doc.getDocumentElement(); + Integer regattaID = XMLParser.getElementInt(docEle, "RegattaID"); + String regattaName = XMLParser.getElementString(docEle, "RegattaName"); + String courseName = XMLParser.getElementString(docEle, "CourseName"); + Double centralLat = XMLParser.getElementDouble(docEle, "CentralLatitude"); + Double centralLng = XMLParser.getElementDouble(docEle, "CentralLongitude"); + Integer utcOffset = XMLParser.getElementInt(docEle, "UtcOffset"); + return new RegattaXMLData( + regattaID, regattaName, courseName, centralLat, centralLng, utcOffset + ); + } + + /** + * Returns an object containing the data extracted from the given xml formatted document + * + * @param doc XML document + * @return object containing race data + */ + public static RaceXMLData parseRace(Document doc) { + Element docEle = doc.getDocumentElement(); + return new RaceXMLData( + extractParticpantIDs(docEle), + extractCompoundMarks(docEle), + extractMarkOrder(docEle), + extractCourseLimit(docEle) + ); + } + + /** + * Extracts course limit data + */ + private static List extractCourseLimit(Element docEle) { + List courseLimit = new ArrayList<>(); + NodeList limitList = docEle.getElementsByTagName("CourseLimit").item(0).getChildNodes(); + for (int i = 0; i < limitList.getLength(); i++) { + Node limitNode = limitList.item(i); + if (limitNode.getNodeName().equals("Limit")) { + courseLimit.add( + new Limit( + XMLParser.getNodeAttributeInt(limitNode, "SeqID"), + XMLParser.getNodeAttributeDouble(limitNode, "Lat"), + XMLParser.getNodeAttributeDouble(limitNode, "Lon") + ) + ); + } + } + return courseLimit; + } + + /** + * Extracts course order data + */ + private static List extractMarkOrder (Element docEle) { + List compoundMarkSequence = new ArrayList<>(); + NodeList cornerList = docEle.getElementsByTagName("CompoundMarkSequence").item(0) + .getChildNodes(); + for (int i = 0; i < cornerList.getLength(); i++) { + Node cornerNode = cornerList.item(i); + if (cornerNode.getNodeName().equals("Corner")) { + compoundMarkSequence.add( + new Corner( + XMLParser.getNodeAttributeInt(cornerNode, "SeqID"), + XMLParser.getNodeAttributeInt(cornerNode, "CompoundMarkID"), + XMLParser.getNodeAttributeString(cornerNode, "Rounding"), + XMLParser.getNodeAttributeInt(cornerNode, "ZoneSize") + ) + ); + } + } + return compoundMarkSequence; + } + + /** + * Extracts course participants data + */ + private static List extractParticpantIDs (Element docEle) { + List boatIDs = new ArrayList<>(); + NodeList pList = docEle.getElementsByTagName("Participants").item(0).getChildNodes(); + for (int i = 0; i < pList.getLength(); i++) { + Node pNode = pList.item(i); + if (pNode.getNodeName().equals("Yacht")) { + boatIDs.add(XMLParser.getNodeAttributeInt(pNode, "SourceID")); + } + } + return boatIDs; + } + + /** + * Extracts course mark data + */ + private static List extractCompoundMarks(Element docEle) { + List allMarks = new ArrayList<>(); + NodeList cMarkList = docEle.getElementsByTagName("Course").item(0).getChildNodes(); + CompoundMark cMark; + for (int i = 0; i < cMarkList.getLength(); i++) { + Node cMarkNode = cMarkList.item(i); + if (cMarkNode.getNodeName().equals("CompoundMark")) { + cMark = new CompoundMark( + XMLParser.getNodeAttributeInt(cMarkNode, "CompoundMarkID"), + XMLParser.getNodeAttributeString(cMarkNode, "Name"), + createMarks(cMarkNode) + ); + allMarks.add(cMark); + } + } + return allMarks; + } + + /** + * Creates marks objects from the given node + */ + private static List createMarks(Node compoundMark) { + List subMarks = new ArrayList<>(); + Integer compoundMarkID = XMLParser.getNodeAttributeInt(compoundMark, "CompoundMarkID"); + String cMarkName = XMLParser.getNodeAttributeString(compoundMark, "Name"); + + NodeList childMarks = compoundMark.getChildNodes(); + for (int i = 0; i < childMarks.getLength(); i++) { + Node markNode = childMarks.item(i); + if (markNode.getNodeName().equals("Mark")) { + Integer seqID = XMLParser.getNodeAttributeInt(markNode, "SeqID"); + Integer sourceID = XMLParser.getNodeAttributeInt(markNode, "SourceID"); + String markName = XMLParser.getNodeAttributeString(markNode, "Name"); + Double targetLat = XMLParser.getNodeAttributeDouble(markNode, "TargetLat"); + Double targetLng = XMLParser.getNodeAttributeDouble(markNode, "TargetLng"); + Mark mark = new Mark(markName, seqID, targetLat, targetLng, sourceID); + subMarks.add(mark); + } + } + return subMarks; + } +} \ No newline at end of file diff --git a/src/main/java/seng302/visualiser/ClientToServerThread.java b/src/main/java/seng302/visualiser/ClientToServerThread.java new file mode 100644 index 00000000..57256760 --- /dev/null +++ b/src/main/java/seng302/visualiser/ClientToServerThread.java @@ -0,0 +1,365 @@ +package seng302.visualiser; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Queue; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.zip.CRC32; +import java.util.zip.Checksum; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import seng302.gameServer.messages.BoatAction; +import seng302.gameServer.messages.BoatActionMessage; +import seng302.gameServer.messages.ClientType; +import seng302.gameServer.messages.CustomizeRequestMessage; +import seng302.gameServer.messages.CustomizeRequestType; +import seng302.gameServer.messages.Message; +import seng302.gameServer.messages.RegistrationRequestMessage; +import seng302.gameServer.messages.RegistrationResponseStatus; +import seng302.model.stream.packets.PacketType; +import seng302.model.stream.packets.StreamPacket; + +/** + * A class describing a single connection to a Server for the purposes of sending and receiving on + * its own thread. + */ +public class ClientToServerThread implements Runnable { + + + + /** + * Functional interface for receiving packets from client socket. + */ + @FunctionalInterface + public interface ClientSocketListener { + void newPacket(); + } + + @FunctionalInterface + public interface DisconnectedFromHostListener { + void notifYDisconnection (String message); + } + + private class ByteReadException extends Exception { + private ByteReadException(String message) { + super(message); + } + } + + private Queue streamPackets = new ConcurrentLinkedQueue<>(); + private List listeners = new ArrayList<>(); + private List disconnectionListeners = new ArrayList<>(); + private Thread thread; + + private Socket socket; + private InputStream is; + + private Logger logger = LoggerFactory.getLogger(ClientToServerThread.class); + + //Output stream + private OutputStream os; + private Timer upWindPacketTimer = new Timer(); + private Timer downWindPacketTimer = new Timer(); + private boolean upwindTimerFlag = false, downwindTimerFlag = false; + static public final int PACKET_SENDING_INTERVAL_MS = 100; + + private int clientId = -1; + + private ByteArrayOutputStream crcBuffer; + private boolean socketOpen = true; + + /** + * Constructor for ClientToServerThread which takes in ipAddress and portNumber and attempts to + * connect to the specified ipAddress and port. + * + * Upon successful socket connection, threeWayHandshake will be preformed and the instance will + * be put on a thread and run immediately. + * + * @param ipAddress a string of ip address to be connected to + * @param portNumber an integer port number + * @throws IOException SocketConnection if fail to connect to ip address and port number + * combination + */ + public ClientToServerThread(String ipAddress, Integer portNumber) throws IOException { + socket = new Socket(ipAddress, portNumber); + is = socket.getInputStream(); + os = socket.getOutputStream(); + + sendRegistrationRequest(); + + thread = new Thread(this); + thread.start(); + } + + /** + * Perform the thread loop. It exits the loop if ClientState connected to host + * variable is false. + */ + public void run() { + int sync1; + int sync2; + // TODO: 14/07/17 wmu16 - Work out how to fix this while loop + while(!socket.isClosed() && socket.isConnected() && socketOpen) { + try { + crcBuffer = new ByteArrayOutputStream(); + sync1 = readByte(); + sync2 = readByte(); + //checking if it is the start of the packet + if (sync1 == 0x47 && sync2 == 0x83) { + int type = readByte(); + //No. of milliseconds since Jan 1st 1970 + long timeStamp = Message.bytesToLong(getBytes(6)); + skipBytes(4); + long payloadLength = Message.bytesToLong(getBytes(2)); + byte[] payload = getBytes((int) payloadLength); + Checksum checksum = new CRC32(); + checksum.update(crcBuffer.toByteArray(), 0, crcBuffer.size()); + long computedCrc = checksum.getValue(); + long packetCrc = Message.bytesToLong(getBytes(4)); + if (computedCrc == packetCrc) { + if (streamPackets.size() > 0) { + streamPackets.add(new StreamPacket(type, payloadLength, timeStamp, payload)); + } else { + if (PacketType.RACE_REGISTRATION_RESPONSE == PacketType.assignPacketType(type, payload)){ + processRegistrationResponse(new StreamPacket(type, payloadLength, timeStamp, payload)); + } + else { + if (clientId == -1) continue; // Do not continue if not registered + streamPackets.add(new StreamPacket(type, payloadLength, timeStamp, payload)); + for (ClientSocketListener csl : listeners) + csl.newPacket(); + } + } + } else { + logger.warn("Packet has been dropped", 1); + } + } + } catch (ByteReadException e) { + logger.warn("Byte read exception on ClientToServerThread", 1); + notifyDisconnectListeners("Connection to server was interrupted"); + closeSocket(); + } + } + logger.warn("Closed connection to server", 1); + notifyDisconnectListeners("Connection to server was terminated"); + closeSocket(); + } + + public void sendCustomizationRequest(CustomizeRequestType reqType, byte[] payload) { + CustomizeRequestMessage requestMessage = new CustomizeRequestMessage(reqType, this.clientId, payload); + try { + os.write(requestMessage.getBuffer()); + } catch (IOException e) { + logger.error("Could not send customization request"); + notifyDisconnectListeners("Could not communicate with server"); + closeSocket(); + } + } + + private void notifyDisconnectListeners (String message) { + if (socketOpen) { + for (DisconnectedFromHostListener listener : disconnectionListeners) { + listener.notifYDisconnection(message); + } + } + } + + /** + * Sends a request to the server asking for a source ID + */ + private void sendRegistrationRequest() { + RegistrationRequestMessage requestMessage = new RegistrationRequestMessage(ClientType.PLAYER); + + try { + os.write(requestMessage.getBuffer()); + } catch (IOException e) { + logger.error("Could not send registration request. Exiting"); + notifyDisconnectListeners("Failed to register with server"); + closeSocket(); + } + } + + /** + * Accepts a response to the registration request message, and updates the client OR quits + * @param packet The registration requests packet + */ + private void processRegistrationResponse(StreamPacket packet){ + int sourceId = (int) Message.bytesToLong(Arrays.copyOfRange(packet.getPayload(), 0, 3)); + int statusCode = (int) Message.bytesToLong(Arrays.copyOfRange(packet.getPayload(), 4,5)); + + RegistrationResponseStatus status = RegistrationResponseStatus.getResponseStatus(statusCode); + + if (status.equals(RegistrationResponseStatus.SUCCESS_PLAYING)){ + clientId = sourceId; + return; + } + + logger.error("Server Denied Connection, Exiting"); + + final String alertErrorText; + + if (status.equals(RegistrationResponseStatus.FAILURE_FULL)){ + alertErrorText = "Server is full"; + } + else{ + alertErrorText = "Could not connect to server"; + } + notifyDisconnectListeners(alertErrorText); + closeSocket(); + } + + /** + * Sends packets for the given boat action. Special cases are: \n + * - DOWNWIND = Packets are sent every ClientToServerThread.PACKET_SENDING_INTERVAL_MS + * - UPWIND = Packets are sent every ClientToServerThread.PACKET_SENDING_INTERVAL_MS + * - MAINTAIN_HEADING = DOWNWIND and UPWIND packets stop being sent. + * @param actionType The boat action that will dictate packets sent. + */ + public void sendBoatAction(BoatAction actionType) { + switch (actionType) { + case MAINTAIN_HEADING: + if (upwindTimerFlag) { + cancelTimer(upWindPacketTimer); + upwindTimerFlag = false; + upWindPacketTimer = new Timer(); + } + if (downwindTimerFlag) { + cancelTimer(downWindPacketTimer); + downwindTimerFlag = false; + downWindPacketTimer = new Timer(); + } + break; + case DOWNWIND: + if (!downwindTimerFlag) { + downwindTimerFlag = true; + downWindPacketTimer.scheduleAtFixedRate( + new TimerTask() { + @Override + public void run() { + sendBoatActionMessage(new BoatActionMessage(BoatAction.DOWNWIND)); + } + }, 0, PACKET_SENDING_INTERVAL_MS + ); + } + break; + case UPWIND: + if (!upwindTimerFlag) { + upwindTimerFlag = true; + upWindPacketTimer.scheduleAtFixedRate( + new TimerTask() { + @Override + public void run() { + sendBoatActionMessage(new BoatActionMessage(BoatAction.UPWIND)); + } + }, 0, PACKET_SENDING_INTERVAL_MS + ); + } + break; + default: + sendBoatActionMessage(new BoatActionMessage(actionType)); + break; + } + } + + /** + * Cancels a packet sending timer. + * @param timer The timer to cancel. + */ + private void cancelTimer (Timer timer) { + timer.cancel(); + timer.purge(); + } + + /** + * Sends a boat action of the given message type. + * @param message The given message type. + */ + private void sendBoatActionMessage(BoatActionMessage message) { + if (clientId != -1) { + try { + os.write(message.getBuffer()); + } catch (IOException e) { + logger.warn("IOException on attempting to sendBoatAction from Client"); + notifyDisconnectListeners("Cannot communicate with server"); + closeSocket(); + } + } + } + + private void closeSocket() { + try { + socket.close(); + socketOpen = false; + } catch (IOException e) { + logger.warn("IOException on attempting to close ClientToServerSocket"); + } + } + + public void setSocketToClose () { + socketOpen = false; + } + + public Queue getPacketQueue () { + return streamPackets; + } + + public void addStreamObserver (ClientSocketListener streamListener) { + listeners.add(streamListener); + } + + public void removeStreamObserver (ClientSocketListener streamListener) { + listeners.remove(streamListener); + } + + public void addDisconnectionListener (DisconnectedFromHostListener listener) { + disconnectionListeners.add(listener); + } + + public void removeDisconnectionListener (DisconnectedFromHostListener listener) { + disconnectionListeners.remove(listener); + } + + private int readByte() throws ByteReadException { + int currentByte = -1; + try { + currentByte = is.read(); + crcBuffer.write(currentByte); + } catch (IOException e) { + logger.warn("IOException on readByte Client side", 1); + notifyDisconnectListeners("Cannot read from server."); + closeSocket(); + } + if (currentByte == -1) { + notifyDisconnectListeners("Cannot read from server."); + closeSocket(); + logger.warn("InputStream reach end of stream", 1); + } + return currentByte; + } + + private byte[] getBytes(int n) throws ByteReadException { + byte[] bytes = new byte[n]; + for (int i = 0; i < n; i++) { + bytes[i] = (byte) readByte(); + } + return bytes; + } + + private void skipBytes(long n) throws ByteReadException { + for (int i = 0; i < n; i++) { + readByte(); + } + } + + int getClientId () { + return clientId; + } +} diff --git a/src/main/java/seng302/visualiser/GameClient.java b/src/main/java/seng302/visualiser/GameClient.java new file mode 100644 index 00000000..968d9a92 --- /dev/null +++ b/src/main/java/seng302/visualiser/GameClient.java @@ -0,0 +1,419 @@ +package seng302.visualiser; + +import java.io.IOException; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.Map; +import java.util.TimeZone; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.Pane; +import seng302.gameServer.GameState; +import seng302.gameServer.MainServerThread; +import seng302.gameServer.messages.BoatAction; +import seng302.gameServer.messages.BoatStatus; +import seng302.model.ClientYacht; +import seng302.model.RaceState; +import seng302.model.stream.packets.StreamPacket; +import seng302.model.stream.parser.MarkRoundingData; +import seng302.model.stream.parser.PositionUpdateData; +import seng302.model.stream.parser.PositionUpdateData.DeviceType; +import seng302.model.stream.parser.RaceStatusData; +import seng302.model.stream.parser.YachtEventData; +import seng302.model.stream.xml.parser.RaceXMLData; +import seng302.model.stream.xml.parser.RegattaXMLData; +import seng302.utilities.StreamParser; +import seng302.utilities.XMLParser; +import seng302.visualiser.controllers.FinishScreenViewController; +import seng302.visualiser.controllers.LobbyController; +import seng302.visualiser.controllers.LobbyController.CloseStatus; +import seng302.visualiser.controllers.RaceViewController; + +/** + * This class is a client side instance of a yacht racing game in JavaFX. The game is instantiated + * with a JavaFX Pane to insert itself into. + */ +public class GameClient { + + private Pane holderPane; + private ClientToServerThread socketThread; + private MainServerThread server; + + private RaceViewController raceView; + + private Map allBoatsMap; + private RegattaXMLData regattaData; + private RaceXMLData courseData; + private RaceState raceState = new RaceState(); + private LobbyController lobbyController; + + private ObservableList clientLobbyList = FXCollections.observableArrayList(); + + /** + * Create an instance of the game client. Does not do anything until run with runAsClient() + * runAsHost(). + * @param holder The JavaFX Pane that the visual elements for the race will be inserted into. + */ + public GameClient(Pane holder) { + this.holderPane = holder; + } + + /** + * Connect to a game at the given address and starts the visualiser. + * @param ipAddress IP to connect to. + * @param portNumber Port to connect to. + */ + public void runAsClient(String ipAddress, Integer portNumber) { + try { + startClientToServerThread(ipAddress, portNumber); + socketThread.addDisconnectionListener((cause) -> { + showConnectionError(cause); + Platform.runLater(this::loadStartScreen); + }); + socketThread.addStreamObserver(this::parsePackets); + LobbyController lobbyController = loadLobby(); + lobbyController.setSocketThread(socketThread); + lobbyController.setPlayerID(socketThread.getClientId()); + lobbyController.setPlayerListSource(clientLobbyList); + lobbyController.disableReadyButton(); + if (regattaData != null){ + lobbyController.setTitle(regattaData.getRegattaName()); + lobbyController.setCourseName(regattaData.getCourseName()); + } + else{ + lobbyController.setTitle(ipAddress); + lobbyController.setCourseName(""); + } + + lobbyController.addCloseListener((exitCause) -> this.loadStartScreen()); + this.lobbyController = lobbyController; + } catch (IOException ioe) { + showConnectionError("Unable to find server"); + Platform.runLater(this::loadStartScreen); + } + } + + /** + * Connect to a game as the host at the given address and starts the visualiser. + * @param ipAddress IP to connect to. + * @param portNumber Port to connect to. + */ + public void runAsHost(String ipAddress, Integer portNumber) { + server = new MainServerThread(); + try { + startClientToServerThread(ipAddress, portNumber); + socketThread.addDisconnectionListener((cause) -> { + Platform.runLater(this::loadStartScreen); + }); + LobbyController lobbyController = loadLobby(); + lobbyController.setSocketThread(socketThread); + lobbyController.setPlayerID(socketThread.getClientId()); + lobbyController.setPlayerListSource(clientLobbyList); + if (regattaData != null) { + lobbyController.setTitle("Hosting: " + regattaData.getRegattaName()); + lobbyController.setCourseName(regattaData.getCourseName()); + } else { + lobbyController.setTitle("Hosting: " + ipAddress); + lobbyController.setCourseName(""); + } + + lobbyController.addCloseListener(exitCause -> { + if (exitCause == CloseStatus.READY) { + GameState.resetStartTime(); + lobbyController.disableReadyButton(); + server.startGame(); + } else if (exitCause == CloseStatus.LEAVE) { + server.terminate(); + server = null; + loadStartScreen(); + } + }); + this.lobbyController = lobbyController; + } catch (IOException ioe) { + showConnectionError("Cannot connect to server as host"); + Platform.runLater(this::loadStartScreen); + } + } + + private void loadStartScreen() { + socketThread.setSocketToClose(); + if (server != null) { + server.terminate(); + server = null; + } + FXMLLoader fxmlLoader = new FXMLLoader( + getClass().getResource("/views/StartScreenView.fxml")); + try { + holderPane.getChildren().clear(); + holderPane.getChildren().add(fxmlLoader.load()); + } catch (IOException e) { + showConnectionError("JavaFX crashed. Please restart the app"); + } + } + + private void showConnectionError (String message) { + Platform.runLater(() -> { + Alert alert = new Alert(AlertType.ERROR); + alert.setHeaderText("Connection Error"); + alert.setContentText(message); + alert.showAndWait(); + }); + } + + private void startClientToServerThread (String ipAddress, int portNumber) throws IOException { + socketThread = new ClientToServerThread(ipAddress, portNumber); + socketThread.addStreamObserver(this::parsePackets); + } + + /** + * Loads a view of the lobby into the clients pane + * + * @return the lobby controller. + */ + private LobbyController loadLobby() { + FXMLLoader fxmlLoader = new FXMLLoader( + GameClient.class.getResource("/views/LobbyView.fxml")); + try { + holderPane.getChildren().clear(); + holderPane.getChildren().add(fxmlLoader.load()); + } catch (IOException e) { + e.printStackTrace(); + } + return fxmlLoader.getController(); + } + + private void loadRaceView() { + FXMLLoader fxmlLoader = loadFXMLToHolder("/views/RaceView.fxml"); + holderPane.getScene().setOnKeyPressed(this::keyPressed); + holderPane.getScene().setOnKeyReleased(this::keyReleased); + raceView = fxmlLoader.getController(); + ClientYacht player = allBoatsMap.get(socketThread.getClientId()); + raceView.loadRace(allBoatsMap, courseData, raceState, player); + } + + private void loadFinishScreenView() { + FXMLLoader fxmlLoader = loadFXMLToHolder("/views/FinishScreenView.fxml"); + FinishScreenViewController controller = fxmlLoader.getController(); + controller.setFinishers(raceState.getPlayerPositions()); + } + + private FXMLLoader loadFXMLToHolder(String fxmlLocation) { + FXMLLoader fxmlLoader = new FXMLLoader( + getClass().getResource(fxmlLocation) + ); + try { + final Node fxmlLoaderFX = fxmlLoader.load(); + Platform.runLater(() -> { + holderPane.getChildren().clear(); + holderPane.getChildren().add(fxmlLoaderFX); + }); + } catch (IOException e) { + e.printStackTrace(); + } + return fxmlLoader; + } + + private void parsePackets() { + while (socketThread.getPacketQueue().peek() != null) { + StreamPacket packet = socketThread.getPacketQueue().poll(); + switch (packet.getType()) { + case RACE_STATUS: + processRaceStatusUpdate(StreamParser.extractRaceStatus(packet)); + + if (raceState.getTimeTillStart() <= 5000) { + startRaceIfAllDataReceived(); + } + + break; + + case REGATTA_XML: + regattaData = XMLParser.parseRegatta( + StreamParser.extractXmlMessage(packet) + ); + + raceState.setTimeZone( + TimeZone.getTimeZone( + ZoneId.ofOffset("UTC", ZoneOffset.ofHours(regattaData.getUtcOffset())) + ) + ); + break; + + case RACE_XML: + courseData = XMLParser.parseRace( + StreamParser.extractXmlMessage(packet) + ); + if (raceView != null) { + raceView.updateRaceData(courseData); + } + break; + + case BOAT_XML: + allBoatsMap = XMLParser.parseBoats( + StreamParser.extractXmlMessage(packet) + ); + clientLobbyList.clear(); + allBoatsMap.forEach((id, boat) -> + clientLobbyList.add(boat.getBoatName()) + ); + raceState.setBoats(allBoatsMap.values()); + break; + + case RACE_START_STATUS: + raceState.updateState(StreamParser.extractRaceStartStatus(packet)); + if (lobbyController != null) lobbyController.updateRaceState(raceState); + break; + + case BOAT_LOCATION: + updatePosition(StreamParser.extractBoatLocation(packet)); + break; + + case MARK_ROUNDING: + updateMarkRounding(StreamParser.extractMarkRounding(packet)); + break; + + case YACHT_EVENT_CODE: + showCollisionAlert(StreamParser.extractYachtEventCode(packet)); + break; + } + } + } + + private void startRaceIfAllDataReceived() { + if (allXMLReceived() && raceView == null) { + loadRaceView(); + } + } + + private boolean allXMLReceived() { + return courseData != null && allBoatsMap != null && regattaData != null; + } + + /** + * Updates the position of a boat. Boat and position are given in the provided data. + */ + private void updatePosition(PositionUpdateData positionData) { + if (positionData.getType() == DeviceType.YACHT_TYPE) { + if (allXMLReceived() && allBoatsMap.containsKey(positionData.getDeviceId())) { + ClientYacht yacht = allBoatsMap.get(positionData.getDeviceId()); + yacht.updateLocation(positionData.getLat(), + positionData.getLon(), positionData.getHeading(), + positionData.getGroundSpeed()); + } + } else if (positionData.getType() == DeviceType.MARK_TYPE) { + //CompoundMark mark = courseData.getCompoundMarks().get(positionData.getDeviceId()); + } + } + + /** + * Updates the boat as having passed the mark. Boat and mark are given by the ids in the + * provided data. + * + * @param roundingData Contains data for the rounding of a mark. + */ + private void updateMarkRounding(MarkRoundingData roundingData) { + if (allXMLReceived()) { + ClientYacht clientYacht = allBoatsMap.get(roundingData.getBoatId()); + clientYacht.roundMark( + courseData.getCompoundMarks().get(roundingData.getMarkId()), + roundingData.getTimeStamp(), + raceState.getRaceTime() - roundingData.getTimeStamp() + ); + } + updatePlayerPositions(); + } + + private void processRaceStatusUpdate(RaceStatusData data) { + if (allXMLReceived()) { + raceState.updateState(data); + boolean raceFinished = true; + for (ClientYacht yacht : allBoatsMap.values()) { + if (yacht.getBoatStatus() != BoatStatus.FINISHED.getCode()) { + raceFinished = false; + } + } + + for (long[] boatData : data.getBoatData()) { + ClientYacht clientYacht = allBoatsMap.get((int) boatData[0]); + clientYacht.setEstimateTimeTillNextMark(raceState.getRaceTime() - boatData[1]); + clientYacht.setEstimateTimeAtFinish(boatData[2]); +// int legNumber = (int) boatData[3]; + clientYacht.setBoatStatus((int) boatData[4]); +// if (legNumber != clientYacht.getLegNumber()) { +// clientYacht.setLegNumber(legNumber); +// } + } + + if (raceFinished) { + close(); + loadFinishScreenView(); + } + } + } + + private void updatePlayerPositions() { + raceState.sortPlayers(); + for (ClientYacht yacht : raceState.getPlayerPositions()) { + yacht.setPosition(raceState.getPlayerPositions().indexOf(yacht) + 1); + } + } + + private void close() { + socketThread.setSocketToClose(); + } + + + /** + * Handle the key-pressed event from the text field. + * @param e The key event triggering this call + */ + private void keyPressed(KeyEvent e) { + switch (e.getCode()) { + case SPACE: // align with vmg + socketThread.sendBoatAction(BoatAction.VMG); break; + case PAGE_UP: // upwind + socketThread.sendBoatAction(BoatAction.UPWIND); break; + case PAGE_DOWN: // downwind + socketThread.sendBoatAction(BoatAction.DOWNWIND); break; + case ENTER: // tack/gybe + socketThread.sendBoatAction(BoatAction.TACK_GYBE); break; + } + } + + + private void keyReleased(KeyEvent e) { + switch (e.getCode()) { + //TODO 12/07/17 Determine the sail state and send the appropriate packet (eg. if sails are in, send a sail out packet) + case SHIFT: // sails in/sails out + socketThread.sendBoatAction(BoatAction.SAILS_IN); + allBoatsMap.get(socketThread.getClientId()).toggleSail(); + break; + case PAGE_UP: + case PAGE_DOWN: + socketThread.sendBoatAction(BoatAction.MAINTAIN_HEADING); break; + } + } + + public RaceXMLData getCourseData() { + return courseData; + } + + /** + * Tells race view to show a collision animation. + */ + private void showCollisionAlert(YachtEventData yachtEventData) { + // 33 is the agreed code to show collision + if (yachtEventData.getEventId() == 33) { + raceState.storeCollision( + allBoatsMap.get( + yachtEventData.getSubjectId().intValue() + ) + ); + } + } +} diff --git a/src/main/java/seng302/visualiser/GameView.java b/src/main/java/seng302/visualiser/GameView.java new file mode 100644 index 00000000..99a49021 --- /dev/null +++ b/src/main/java/seng302/visualiser/GameView.java @@ -0,0 +1,847 @@ +package seng302.visualiser; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javafx.animation.AnimationTimer; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.application.Platform; +import javafx.collections.ObservableList; +import javafx.geometry.Point2D; +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; +import javafx.scene.shape.Circle; +import javafx.scene.shape.Polygon; +import javafx.scene.text.Text; +import javafx.util.Duration; +import seng302.model.ClientYacht; +import seng302.gameServer.messages.RoundingSide; +import seng302.model.ClientYacht; +import seng302.model.Colors; +import seng302.model.GeoPoint; +import seng302.model.Limit; +import seng302.model.mark.CompoundMark; +import seng302.model.mark.Corner; +import seng302.model.mark.Mark; +import seng302.utilities.GeoUtility; +import seng302.visualiser.fxObjects.AnnotationBox; +import seng302.visualiser.fxObjects.BoatObject; +import seng302.visualiser.fxObjects.CourseBoundary; +import seng302.visualiser.fxObjects.Gate; +import seng302.visualiser.fxObjects.MarkArrowFactory; +import seng302.visualiser.fxObjects.Marker; +import seng302.visualiser.map.Boundary; +import seng302.visualiser.map.CanvasMap; + +/** + * Created by cir27 on 20/07/17. + */ +public class GameView extends Pane { + + private double bufferSize = 50; + private double panelWidth = 1260; // it should be 1280 but, minors 40 to cancel the bias. + private double panelHeight = 960; + private double canvasWidth = 1100; + private double canvasHeight = 920; + private boolean horizontalInversion = false; + + private double distanceScaleFactor; + private ScaleDirection scaleDirection; + private GeoPoint minLatPoint, minLonPoint, maxLatPoint, maxLonPoint; + private double referencePointX, referencePointY; + private double metersPerPixelX, metersPerPixelY; + + final double SCALE_DELTA = 1.1; + + private Text fpsDisplay = new Text(); + private Polygon raceBorder = new CourseBoundary(); + + /* Note that if either of these is null then values for it have not been added and the other + should be used as the limits of the map. */ + private List borderPoints; + private Map markerObjects; + + private Map boatObjects = new HashMap<>(); + private Map annotations = new HashMap<>(); + private ObservableList gameObjects; + private BoatObject selectedBoat = null; + private Group annotationsGroup = new Group(); + private Group wakesGroup = new Group(); + private Group boatObjectGroup = new Group(); + private Group trails = new Group(); + private Group markers = new Group(); + private List course = new ArrayList<>(); + + private ImageView mapImage = new ImageView(); + + //FRAME RATE + + private AnimationTimer timer; + private int NUM_SAMPLES = 10; + private final long[] frameTimes = new long[NUM_SAMPLES]; + private Double frameRate = 60.0; + private int frameTimeIndex = 0; + private boolean arrayFilled = false; + private ClientYacht playerYacht; + private double windDir = 0.0; + + double scaleFactor = 1; + + private void zoomOut() { + scaleFactor = 0.1; + if (this.getScaleX() > 0.5) { + this.setScaleX(this.getScaleX() - scaleFactor); + this.setScaleY(this.getScaleY() - scaleFactor); + } + } + + private void zoomIn() { + scaleFactor = 0.10; + if (this.getScaleX() < 2.5) { + this.setScaleX(this.getScaleX() + scaleFactor); + this.setScaleY(this.getScaleY() + scaleFactor); + } + } + + private enum ScaleDirection { + HORIZONTAL, + VERTICAL + } + + + private void trackBoat() { + if (selectedBoat != null) { + double x = selectedBoat.getBoatLayoutX(); + double y = selectedBoat.getBoatLayoutY(); + double displacementX = this.getWidth(); + double displacementY = this.getHeight(); + this.setLayoutX((-x + (displacementX / 2.0)) * this.getScaleX()); + this.setLayoutY((-y + (displacementY / 2.0)) * this.getScaleY()); + } else { + this.setLayoutX(0); + this.setLayoutY(0); + } + } + + public GameView () { + gameObjects = this.getChildren(); + // create image view for map, bind panel size to image + gameObjects.add(mapImage); + gameObjects.add(raceBorder); + gameObjects.add(markers); + initializeTimer(); + } + + private void initializeTimer() { + Arrays.fill(frameTimes, 1_000_000_000 / 60); + timer = new AnimationTimer() { + private long lastTime = 0; + private int FPSCount = 30; + private Double frameRate = 60.0; + private int index = 0; + private boolean arrayFilled = false; + private long sum = 1_000_000_000 / 3; + + @Override + public void handle(long now) { + trackBoat(); + if (lastTime == 0) { + lastTime = now; + } else { + if (now - lastTime >= (1e8 / 60)) { //Fix for framerate going above 60 when minimized + long oldFrameTime = frameTimes[frameTimeIndex]; + frameTimes[frameTimeIndex] = now; + frameTimeIndex = (frameTimeIndex + 1) % frameTimes.length; + if (frameTimeIndex == 0) { + arrayFilled = true; + } + long elapsedNanos; + if (arrayFilled) { + elapsedNanos = now - oldFrameTime; + long elapsedNanosPerFrame = elapsedNanos / frameTimes.length; + frameRate = 1_000_000_000.0 / elapsedNanosPerFrame; + if (FPSCount-- == 0) { + FPSCount = 30; + drawFps(frameRate); + } + } + lastTime = now; + } + } + boatObjects.forEach((boat, boatObject) -> boatObject.updateLocation()); + } + }; + } + + /** + * First find the top right and bottom left points' geo locations, then retrieve map from google + * to display on image view. - Haoming 22/5/2017 + */ + private void drawGoogleMap() { + findMetersPerPixel(); + Point2D topLeftPoint = findScaledXY(maxLatPoint.getLat(), minLonPoint.getLng()); + // distance from top left extreme to panel origin (top left corner) + double distanceFromTopLeftToOrigin = Math.sqrt( + Math.pow(topLeftPoint.getX() * metersPerPixelX, 2) + Math + .pow(topLeftPoint.getY() * metersPerPixelY, 2)); + // angle from top left extreme to panel origin + double bearingFromTopLeftToOrigin = Math + .toDegrees(Math.atan2(-topLeftPoint.getX(), topLeftPoint.getY())); + // the top left extreme + GeoPoint topLeftPos = new GeoPoint(maxLatPoint.getLat(), minLonPoint.getLng()); + GeoPoint originPos = GeoUtility + .getGeoCoordinate(topLeftPos, bearingFromTopLeftToOrigin, distanceFromTopLeftToOrigin); + + // distance from origin corner to bottom right corner of the panel + double distanceFromOriginToBottomRight = Math.sqrt( + Math.pow(panelHeight * metersPerPixelY, 2) + Math + .pow(panelWidth * metersPerPixelX, 2)); + double bearingFromOriginToBottomRight = Math + .toDegrees(Math.atan2(panelWidth, -panelHeight)); + GeoPoint bottomRightPos = GeoUtility + .getGeoCoordinate(originPos, bearingFromOriginToBottomRight, + distanceFromOriginToBottomRight); + + Boundary boundary = new Boundary(originPos.getLat(), bottomRightPos.getLng(), + bottomRightPos.getLat(), originPos.getLng()); + CanvasMap canvasMap = new CanvasMap(boundary); + mapImage.setImage(canvasMap.getMapImage()); + mapImage.fitWidthProperty().bind(((AnchorPane) this.getParent()).heightProperty()); + mapImage.fitHeightProperty().bind(((AnchorPane) this.getParent()).heightProperty()); + } + + // TODO: 16/08/17 Break up this function + /** + * Adds a course to the GameView. The view is scaled accordingly unless a border is set in which + * case the course is added relative ot the border. + * + * @param newCourse the mark objects that make up the course. + * @param sequence The sequence the marks travel through + */ + public void updateCourse(List newCourse, List sequence) { + markerObjects = new HashMap<>(); + + for (Corner corner : sequence) { //Makes course out of all compound marks. + for (CompoundMark compoundMark : newCourse) { + if (corner.getCompoundMarkID() == compoundMark.getId()) { + course.add(compoundMark); + } + } + } + + // TODO: 16/08/17 Updating mark roundings here. It should not happen here. Nor should it be done this way. + for (Corner corner : sequence){ + CompoundMark compoundMark = course.get(corner.getSeqID() - 1); + compoundMark.setRoundingSide( + RoundingSide.getRoundingSide(corner.getRounding()) + ); + } + + final List gates = new ArrayList<>(); + Paint colour = Color.BLACK; + //Creates new markers + for (CompoundMark cMark : newCourse) { + //Set start and end colour + if (cMark.getId() == sequence.get(0).getCompoundMarkID()) { + colour = Color.GREEN; + } else if (cMark.getId() == sequence.get(sequence.size() - 1).getCompoundMarkID()) { + colour = Color.RED; + } + //Create mark dots + for (Mark mark : cMark.getMarks()) { + makeAndBindMarker(mark, colour); + } + //Create gate line + if (cMark.isGate()) { + for (int i = 1; i < cMark.getMarks().size(); i++) { + gates.add( + makeAndBindGate( + markerObjects.get(cMark.getSubMark(i)), + markerObjects.get(cMark.getSubMark(i + 1)), + colour + ) + ); + } + } + colour = Color.BLACK; + } + + createMarkArrows(sequence); + + //Scale race to markers if there is no border. + if (borderPoints == null) { + rescaleRace(new ArrayList<>(markerObjects.keySet())); + } + //Move the Markers to initial position. + markerObjects.forEach(((mark, marker) -> { + Point2D p2d = findScaledXY(mark.getLat(), mark.getLng()); + marker.setLayoutX(p2d.getX()); + marker.setLayoutY(p2d.getY()); + })); + Platform.runLater(() -> { + markers.getChildren().clear(); + markers.getChildren().addAll(gates); + markers.getChildren().addAll(markerObjects.values()); + }); + } + + /** + * Calculates all the data needed for to create mark arrows. Requires that a course has been + * added to the gameview. + * @param sequence The order in which marks are traversed. + */ + private void createMarkArrows (List sequence) { + for (int i=1; i < sequence.size()-1; i++) { //General case. + double averageLat = 0; + double averageLng = 0; + int numMarks = 0; + for (Mark mark : course.get(i-1).getMarks()) { + numMarks += 1; + averageLat += mark.getLat(); + averageLng += mark.getLng(); + } + GeoPoint lastMarkAv = new GeoPoint(averageLat / numMarks, averageLng / numMarks); + numMarks = 0; + averageLat = 0; + averageLng = 0; + for (Mark mark : course.get(i+1).getMarks()) { + numMarks += 1; + averageLat += mark.getLat(); + averageLng += mark.getLng(); + } + GeoPoint nextMarkAv = new GeoPoint(averageLat / numMarks, averageLng / numMarks); + // TODO: 16/08/17 This comparison doesn't need to exist but the alternative is to user server enum client side. + for (Mark mark : course.get(i).getMarks()) { + markerObjects.get(mark).addArrows( + mark.getRoundingSide() == RoundingSide.STARBOARD ? MarkArrowFactory.RoundingSide.STARBOARD : MarkArrowFactory.RoundingSide.PORT, + GeoUtility.getBearing(lastMarkAv, mark), + GeoUtility.getBearing(mark, nextMarkAv) + ); + } + } + createStartLineArrows(); + createFinishLineArrows(); + } + + private void createStartLineArrows () { + double averageLat = 0; + double averageLng = 0; + int numMarks = 0; + for (Mark mark : course.get(1).getMarks()) { + numMarks += 1; + averageLat += mark.getLat(); + averageLng += mark.getLng(); + } + GeoPoint firstMarkAv = new GeoPoint(averageLat / numMarks, averageLng / numMarks); + for (Mark mark : course.get(0).getMarks()) { + markerObjects.get(mark).addArrows( + mark.getRoundingSide() == RoundingSide.STARBOARD ? MarkArrowFactory.RoundingSide.STARBOARD : MarkArrowFactory.RoundingSide.PORT, + 0d, //90 + GeoUtility.getBearing(mark, firstMarkAv) + ); + } + } + + private void createFinishLineArrows () { + double numMarks = 0; + double averageLat = 0; + double averageLng = 0; + for (Mark mark : course.get(course.size()-2).getMarks()) { + numMarks += 1; + averageLat += mark.getLat(); + averageLng += mark.getLng(); + } + GeoPoint secondToLastMarkAv = new GeoPoint(averageLat / numMarks, averageLng / numMarks); + for (Mark mark : course.get(course.size()-1).getMarks()) { + markerObjects.get(mark).addArrows( + mark.getRoundingSide() == RoundingSide.STARBOARD ? MarkArrowFactory.RoundingSide.STARBOARD : MarkArrowFactory.RoundingSide.PORT, + GeoUtility.getBearing(secondToLastMarkAv, mark), + GeoUtility.getBearing(mark, mark) + ); + } + } + + /** + * Creates a new Marker and binds it's position to the given Mark. + * + * @param observableMark The mark to bind the marker to. + * @param colour The desired colour of the mark + */ + private void makeAndBindMarker(Mark observableMark, Paint colour) { + Marker marker = new Marker(colour); +// marker.addArrows(MarkArrowFactory.RoundingSide.PORT, ThreadLocalRandom.current().nextDouble(91, 180), ThreadLocalRandom.current().nextDouble(1, 90)); + markerObjects.put(observableMark, marker); + observableMark.addPositionListener((mark, lat, lon) -> { + Point2D p2d = findScaledXY(lat, lon); + markerObjects.get(mark).setLayoutX(p2d.getX()); + markerObjects.get(mark).setLayoutY(p2d.getY()); + }); + } + + /** + * Creates a new gate connecting the given marks. + * + * @param m1 The first Mark of the gate. + * @param m2 The second Mark of the gate. + * @param colour The desired colour of the gate. + * @return the new gate. + */ + private Gate makeAndBindGate(Marker m1, Marker m2, Paint colour) { + Gate gate = new Gate(colour); + gate.startXProperty().bind( + m1.layoutXProperty() + ); + gate.startYProperty().bind( + m1.layoutYProperty() + ); + gate.endXProperty().bind( + m2.layoutXProperty() + ); + gate.endYProperty().bind( + m2.layoutYProperty() + ); + return gate; + } + + /** + * Adds a border to the GameView and rescales to the size of the border, does not rescale if a + * border already exists. Assumes the border is larger than the course. + * + * @param border the race border to be drawn. + */ + public void updateBorder(List border) { + if (borderPoints == null) { + borderPoints = border; + rescaleRace(new ArrayList<>(borderPoints)); + } + List boundaryPoints = new ArrayList<>(); + for (Limit limit : border) { + Point2D location = findScaledXY(limit.getLat(), limit.getLng()); + boundaryPoints.add(location.getX()); + boundaryPoints.add(location.getY()); + } + raceBorder.getPoints().setAll(boundaryPoints); + } + + // TODO: 16/08/17 initialize zooming internal to GameView only + /** + * Enables zoom. Has to be called after this is added to a scene. + */ + public void enableZoom () { + if (this.getScene() != null) { + this.getScene().addEventHandler(KeyEvent.KEY_PRESSED, (event) -> { + if (event.getCode() == KeyCode.Z) { + zoomIn(); + } else if (event.getCode() == KeyCode.X) { + zoomOut(); + } + }); + } + } + /** + * Rescales the race to the size of the window. + * + * @param limitingCoordinates the set of geo points that contains the extremities of the race. + */ + private void rescaleRace(List limitingCoordinates) { + //Check is called once to avoid unnecessarily change the course limits once the race is running + findMinMaxPoint(limitingCoordinates); + double minLonToMaxLon = scaleRaceExtremities(); + calculateReferencePointLocation(minLonToMaxLon); +// drawGoogleMap(); + } + + private void setSelectedBoat(BoatObject bo, Boolean isSelected) { + if (this.selectedBoat == bo && !isSelected) { + this.selectedBoat = null; + boatObjects.forEach((boat, group) -> + group.setIsSelected(false) + ); + } else if (isSelected) { + this.selectedBoat = bo; + for (BoatObject group : boatObjects.values()) { + if (group != bo) { + group.setIsSelected(false); + } + } + } + } + + /** + * Draws all the boats. + * @param yachts The yachts to set in the race + */ + public void setBoats(List yachts) { + BoatObject newBoat; + final List wakes = new ArrayList<>(); + for (ClientYacht clientYacht : yachts) { + Paint colour = clientYacht.getColour(); + newBoat = new BoatObject(); + newBoat.addSelectedBoatListener(this::setSelectedBoat); + newBoat.setFill(colour); + boatObjects.put(clientYacht, newBoat); + createAndBindAnnotationBox(clientYacht, colour); +// wakesGroup.getChildren().add(newBoat.getWake()); + wakes.add(newBoat.getWake()); + boatObjectGroup.getChildren().add(newBoat); + trails.getChildren().add(newBoat.getTrail()); + // TODO: 1/08/17 Make this less vile to look at. + clientYacht.addLocationListener((boat, lat, lon, heading, sailIn, velocity) -> { + BoatObject bo = boatObjects.get(boat); + Point2D p2d = findScaledXY(lat, lon); + bo.moveTo(p2d.getX(), p2d.getY(), heading, velocity, sailIn, windDir); + annotations.get(boat).setLocation(p2d.getX(), p2d.getY()); + bo.setTrajectory( + heading, + velocity, + metersPerPixelX, + metersPerPixelY); + }); + } + annotationsGroup.getChildren().addAll(annotations.values()); + Platform.runLater(() -> { + gameObjects.addAll(trails); + gameObjects.addAll(wakes); + gameObjects.addAll(annotationsGroup); + gameObjects.addAll(boatObjectGroup); + }); + } + + private void createAndBindAnnotationBox(ClientYacht clientYacht, Paint colour) { + AnnotationBox newAnnotation = new AnnotationBox(); + newAnnotation.setFill(colour); + newAnnotation.addAnnotation( + "name", "Player: " + clientYacht.getShortName() + ); +// newAnnotation.addAnnotation( +// "velocity", +// yacht.getVelocityProperty(), +// (velocity) -> String.format("Speed: %.2f ms", velocity.doubleValue()) +// ); +// newAnnotation.addAnnotation( +// "nextMark", +// yacht.timeTillNextProperty(), +// (time) -> { +// DateFormat format = new SimpleDateFormat("mm:ss"); +// return format.format(time); +// } +// ); +// newAnnotation.addAnnotation( +// "lastMark", +// yacht.timeTillNextProperty(), +// (time) -> { +// DateFormat format = new SimpleDateFormat("mm:ss"); +// return format.format(time); +// } +// ); + annotations.put(clientYacht, newAnnotation); + } + + private void drawFps(Double fps) { + Platform.runLater(() -> fpsDisplay.setText(String.format("%d FPS", Math.round(fps)))); + } + + /** + * Sets the class variables minLatPoint, maxLatPoint, minLonPoint, maxLonPoint to the point with + * the leftmost point, rightmost point, southern most point and northern most point + * respectively. + */ + private void findMinMaxPoint(List points) { + List sortedPoints = new ArrayList<>(points); + sortedPoints.sort(Comparator.comparingDouble(GeoPoint::getLat)); + minLatPoint = new GeoPoint(sortedPoints.get(0).getLat(), sortedPoints.get(0).getLng()); + GeoPoint maxLat = sortedPoints.get(sortedPoints.size() - 1); + maxLatPoint = new GeoPoint(maxLat.getLat(), maxLat.getLng()); + + sortedPoints.sort(Comparator.comparingDouble(GeoPoint::getLng)); + minLonPoint = new GeoPoint(sortedPoints.get(0).getLat(), sortedPoints.get(0).getLng()); + GeoPoint maxLon = sortedPoints.get(sortedPoints.size() - 1); + maxLonPoint = new GeoPoint(maxLon.getLat(), maxLon.getLng()); + if (maxLonPoint.getLng() - minLonPoint.getLng() > 180) { + horizontalInversion = true; + } + } + + /** + * Calculates the location of a reference point, this is always the point with minimum latitude, + * in relation to the canvas. + * + * @param minLonToMaxLon The horizontal distance between the point of minimum longitude to + * maximum longitude. + */ + private void calculateReferencePointLocation(double minLonToMaxLon) { + GeoPoint referencePoint = minLatPoint; + double referenceAngle; + + if (scaleDirection == ScaleDirection.HORIZONTAL) { + referenceAngle = Math.abs( + GeoUtility.getBearingRad(referencePoint, minLonPoint) + ); + referencePointX = + bufferSize + distanceScaleFactor * Math.sin(referenceAngle) * GeoUtility + .getDistance(referencePoint, minLonPoint); + referenceAngle = Math.abs(GeoUtility.getDistance(referencePoint, maxLatPoint)); + referencePointY = canvasHeight - (bufferSize + bufferSize); + referencePointY -= distanceScaleFactor * Math.cos(referenceAngle) * GeoUtility + .getDistance(referencePoint, maxLatPoint); + referencePointY = referencePointY / 2; + referencePointY += bufferSize; + referencePointY += distanceScaleFactor * Math.cos(referenceAngle) * GeoUtility + .getDistance(referencePoint, maxLatPoint); + } else { + referencePointY = canvasHeight - bufferSize; + referenceAngle = Math.abs( + Math.toRadians( + GeoUtility.getDistance(referencePoint, minLonPoint) + ) + ); + referencePointX = bufferSize; + referencePointX += distanceScaleFactor * Math.sin(referenceAngle) * GeoUtility + .getDistance(referencePoint, minLonPoint); + referencePointX += + ((canvasWidth - (bufferSize + bufferSize)) - (minLonToMaxLon * distanceScaleFactor)) + / 2; + } + if (horizontalInversion) { + referencePointX = canvasWidth - bufferSize - (referencePointX - bufferSize); + } + } + + + /** + * Finds the scale factor necessary to fit all race markers within the onscreen map and assigns + * it to distanceScaleFactor Returns the max horizontal distance of the map. + */ + private double scaleRaceExtremities() { + + double vertAngle = Math.abs( + GeoUtility.getBearingRad(minLatPoint, maxLatPoint) + ); + double vertDistance = + Math.cos(vertAngle) * GeoUtility.getDistance(minLatPoint, maxLatPoint); + double horiAngle = Math.abs( + GeoUtility.getBearingRad(minLonPoint, maxLonPoint) + ); + if (horiAngle <= (Math.PI / 2)) { + horiAngle = (Math.PI / 2) - horiAngle; + } else { + horiAngle = horiAngle - (Math.PI / 2); + } + double horiDistance = + Math.cos(horiAngle) * GeoUtility.getDistance(minLonPoint, maxLonPoint); + + double vertScale = (canvasHeight - (bufferSize + bufferSize)) / vertDistance; + + if ((horiDistance * vertScale) > (canvasWidth - (bufferSize + bufferSize))) { + distanceScaleFactor = (canvasWidth - (bufferSize + bufferSize)) / horiDistance; + scaleDirection = ScaleDirection.HORIZONTAL; + } else { + distanceScaleFactor = vertScale; + scaleDirection = ScaleDirection.VERTICAL; + } + return horiDistance; + } + + private Point2D findScaledXY(GeoPoint unscaled) { + return findScaledXY(unscaled.getLat(), unscaled.getLng()); + } + + private Point2D findScaledXY(double unscaledLat, double unscaledLon) { + double distanceFromReference; + double angleFromReference; + double xAxisLocation = referencePointX; + double yAxisLocation = referencePointY; + + angleFromReference = GeoUtility.getBearingRad( + minLatPoint, new GeoPoint(unscaledLat, unscaledLon) + ); + distanceFromReference = GeoUtility.getDistance( + minLatPoint, new GeoPoint(unscaledLat, unscaledLon) + ); + if (angleFromReference >= 0 && angleFromReference <= Math.PI / 2) { + xAxisLocation += Math + .round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); + yAxisLocation -= Math + .round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); + } else if (angleFromReference >= 0) { + angleFromReference = angleFromReference - Math.PI / 2; + xAxisLocation += Math + .round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); + yAxisLocation += Math + .round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); + } else if (angleFromReference < 0 && angleFromReference >= -Math.PI / 2) { + angleFromReference = Math.abs(angleFromReference); + xAxisLocation -= Math + .round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); + yAxisLocation -= Math + .round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); + } else { + angleFromReference = Math.abs(angleFromReference) - Math.PI / 2; + xAxisLocation -= Math + .round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); + yAxisLocation += Math + .round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); + } + if (horizontalInversion) { + xAxisLocation = canvasWidth - bufferSize - (xAxisLocation - bufferSize); + } + return new Point2D(xAxisLocation, yAxisLocation); + } + + /** + * Find the number of meters per pixel. + */ + private void findMetersPerPixel() { + Point2D p1, p2; + GeoPoint g1, g2; + double theta, distance, dx, dy, dHorizontal, dVertical; + g1 = new GeoPoint(maxLatPoint.getLat(), minLonPoint.getLng()); + g2 = new GeoPoint(minLatPoint.getLat(), maxLatPoint.getLng()); + p1 = findScaledXY(new GeoPoint(maxLatPoint.getLat(), minLonPoint.getLng())); + p2 = findScaledXY(new GeoPoint(minLatPoint.getLat(), maxLatPoint.getLng())); + theta = GeoUtility.getBearingRad(g1, g2); + distance = GeoUtility.getDistance(g1, g2); + dHorizontal = Math.abs(Math.sin(theta) * distance); + dVertical = Math.abs(Math.cos(theta) * distance); + dx = Math.abs(p1.getX() - p2.getX()); + dy = Math.abs(p1.getY() - p2.getY()); + metersPerPixelX = dHorizontal / dx; + metersPerPixelY = dVertical / dy; + } + + public void setAnnotationVisibilities(boolean teamName, boolean velocity, boolean estTime, + boolean legTime, boolean trail, boolean wake) { + for (BoatObject boatObject : boatObjects.values()) { + boatObject.setVisibility(teamName, velocity, estTime, legTime, trail, wake); + } + for (AnnotationBox ag : annotations.values()) { + ag.setAnnotationVisibility("name", teamName); + ag.setAnnotationVisibility("velocity", velocity); + ag.setAnnotationVisibility("nextMark", estTime); + ag.setAnnotationVisibility("lastMark", legTime); + } + } + + public void setFPSVisibility(boolean visibility) { + fpsDisplay.setVisible(visibility); + } + + public void selectBoat(ClientYacht selectedClientYacht) { + boatObjects.forEach((boat, group) -> + group.setIsSelected(boat == selectedClientYacht) + ); + } + + public void pauseRace() { + timer.stop(); + } + + public void setWindDir(double windDir) { + this.windDir = windDir; + } + + public void startRace() { + timer.start(); + } + + public ClientYacht getPlayerYacht() { + return playerYacht; + } + + public void setBoatAsPlayer (ClientYacht playerYacht) { + this.playerYacht = playerYacht; + playerYacht.toggleSail(); + boatObjects.get(playerYacht).setAsPlayer(); + CompoundMark currentMark = course.get(playerYacht.getLegNumber()); + for (Mark mark : currentMark.getMarks()) { + markerObjects.get(mark).showNextExitArrow(); + } + annotations.get(playerYacht).addAnnotation( + "velocity", + playerYacht.getVelocityProperty(), + (velocity) -> String.format("Speed: %.2f ms", velocity.doubleValue()) + ); + Platform.runLater(() -> { + boatObjectGroup.getChildren().remove(boatObjects.get(playerYacht)); + gameObjects.add(boatObjects.get(playerYacht)); + annotationsGroup.getChildren().remove(annotations.get(playerYacht)); + gameObjects.add(annotations.get(playerYacht)); + }); + playerYacht.addMarkRoundingListener(this::updateMarkArrows); + } + + private void updateMarkArrows (ClientYacht yacht, CompoundMark compoundMark, int legNumber) { + //Only show arrows for this and next leg. + if (compoundMark != null) { + for (Mark mark : compoundMark.getMarks()) { + markerObjects.get(mark).showNextExitArrow(); + } + } + CompoundMark nextMark = null; + if (legNumber < course.size() - 1) { + nextMark = course.get(legNumber); + for (Mark mark : nextMark.getMarks()) { + markerObjects.get(mark).showNextEnterArrow(); + } + } + if (legNumber - 2 >= 0) { + CompoundMark lastMark = course.get(Math.max(0, legNumber - 2)); + if (lastMark != nextMark) { + for (Mark mark : lastMark.getMarks()) { + markerObjects.get(mark).hideAllArrows(); + } + } + } + } + + /** + * Given yacht geopoint by race view controller, drawCollision will calculate canvas X and Y and + * display a flashing red circle on collision point. + * + * @param collisionPoint yacht collision point + */ + public void drawCollision(GeoPoint collisionPoint) { + Point2D point = findScaledXY(collisionPoint); + double circleRadius = 0.0; + Circle circle = new Circle(point.getX(), point.getY(), circleRadius, Color.RED); + + circle.setFill(Color.TRANSPARENT); + circle.setStroke(Color.RED); + circle.setStrokeWidth(3); + + Timeline timeline = new Timeline(); + timeline.setCycleCount(1); + + KeyFrame keyframe1 = new KeyFrame(Duration.ZERO, + new KeyValue(circle.radiusProperty(), 0), + new KeyValue(circle.strokeProperty(), Color.TRANSPARENT)); + KeyFrame keyFrame2 = new KeyFrame(new Duration(1000), + new KeyValue(circle.radiusProperty(), 50), + new KeyValue(circle.strokeProperty(), Color.RED)); + KeyFrame keyFrame3 = new KeyFrame(new Duration(1500), + new KeyValue(circle.strokeProperty(), Color.TRANSPARENT)); + + timeline.getKeyFrames().addAll(keyframe1, keyFrame2, keyFrame3); + + Platform.runLater(() -> gameObjects.add(circle)); + timeline.setOnFinished(event -> Platform.runLater(() -> gameObjects.remove(circle))); + timeline.play(); + } + + public void setFrameRateFXText(Text fpsDisplay) { + this.fpsDisplay = null; + this.fpsDisplay = fpsDisplay; + } +} diff --git a/src/main/java/seng302/visualiser/controllers/CustomizationController.java b/src/main/java/seng302/visualiser/controllers/CustomizationController.java new file mode 100644 index 00000000..1a5b5e4d --- /dev/null +++ b/src/main/java/seng302/visualiser/controllers/CustomizationController.java @@ -0,0 +1,74 @@ +package seng302.visualiser.controllers; + +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ColorPicker; +import javafx.scene.control.TextField; +import javafx.scene.paint.Color; +import javafx.stage.Stage; +import seng302.gameServer.messages.CustomizeRequestType; +import seng302.visualiser.ClientToServerThread; + +public class CustomizationController { + + @FXML + private TextField nameField; + + @FXML + private ColorPicker boatColorPicker; + + @FXML + private Button customizeSubmit; + + private LobbyController lc; + private ClientToServerThread socketThread; + private Stage windowStage; + + public void initialize() { + + } + + public void setServerThread(ClientToServerThread ctsThread) { + this.socketThread = ctsThread; + } + + @FXML + public void submitCustomization() { + System.out.println("Attempting to send"); + socketThread.sendCustomizationRequest(CustomizeRequestType.NAME, nameField.getText().getBytes()); + // TODO: 16/08/17 ajm412: Turn colors into byte array. + Color color = boatColorPicker.getValue(); + + short red = (short) (color.getRed() * 255); + short green = (short) (color.getGreen() * 255); + short blue = (short) (color.getBlue() * 255); + + byte[] colorArray = new byte[3]; + + colorArray[0] = (byte) red; + colorArray[1] = (byte) green; + colorArray[2] = (byte) blue; + + socketThread.sendCustomizationRequest(CustomizeRequestType.COLOR, colorArray); + lc.setPlayersColor(color); + windowStage.close(); + } + + public void setLobbyController(LobbyController lc) { + this.lc = lc; + } + + public void setStage(Stage stage) { + this.windowStage = stage; + } + + public void setPlayerName(String name) { + this.nameField.setText(name); + } + + public void setPlayerColor(Color playerColor) { + this.boatColorPicker.setValue(playerColor); + } + + +} diff --git a/src/main/java/seng302/controllers/FinishScreenViewController.java b/src/main/java/seng302/visualiser/controllers/FinishScreenViewController.java similarity index 70% rename from src/main/java/seng302/controllers/FinishScreenViewController.java rename to src/main/java/seng302/visualiser/controllers/FinishScreenViewController.java index 170de47f..b2e49f1a 100644 --- a/src/main/java/seng302/controllers/FinishScreenViewController.java +++ b/src/main/java/seng302/visualiser/controllers/FinishScreenViewController.java @@ -1,8 +1,11 @@ -package seng302.controllers; +package seng302.visualiser.controllers; import java.io.IOException; import java.net.URL; import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; import java.util.ResourceBundle; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -15,24 +18,24 @@ import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; -import seng302.client.ClientPacketParser; -import seng302.models.Yacht; -import seng302.models.stream.XMLParser.RaceXMLObject.Participant; +import seng302.model.ClientYacht; public class FinishScreenViewController implements Initializable { @FXML private GridPane finishScreenGridPane; @FXML - private TableView finishOrderTable; + private TableView finishOrderTable; @FXML - private TableColumn posCol; + private TableColumn posCol; @FXML - private TableColumn boatNameCol; + private TableColumn boatNameCol; @FXML - private TableColumn shortNameCol; + private TableColumn shortNameCol; @FXML - private TableColumn countryCol; + private TableColumn countryCol; + + ObservableList data = FXCollections.observableArrayList(); @Override public void initialize(URL location, ResourceBundle resources) { @@ -41,7 +44,6 @@ public class FinishScreenViewController implements Initializable { finishOrderTable.getStylesheets().add(getClass().getResource("/css/master.css").toString()); // set up data for table - ObservableList data = FXCollections.observableArrayList(); finishOrderTable.setItems(data); // setting table col data @@ -57,24 +59,15 @@ public class FinishScreenViewController implements Initializable { countryCol.setCellValueFactory( new PropertyValueFactory<>("country") ); - - // check if the boat is racing - ArrayList participants = ClientPacketParser.getXmlObject().getRaceXML() - .getParticipants(); - ArrayList participantIDs = new ArrayList<>(); - for (Participant p : participants) { - participantIDs.add(p.getsourceID()); - } - - // add data to table - for (Yacht boat : ClientPacketParser.getBoatsPos().values()) { - if (participantIDs.contains(boat.getSourceId())) { - data.add(boat); - } - } finishOrderTable.refresh(); } + public void setFinishers(Collection participants) { + List sorted = new ArrayList<>(participants); + sorted.sort(Comparator.comparingInt(ClientYacht::getPlacing)); + finishOrderTable.getItems().setAll(sorted); + } + private void setContentPane(String jfxUrl) { try { // get the main controller anchor pane (FinishView -> MainView) diff --git a/src/main/java/seng302/visualiser/controllers/LobbyController.java b/src/main/java/seng302/visualiser/controllers/LobbyController.java new file mode 100644 index 00000000..4dc5293f --- /dev/null +++ b/src/main/java/seng302/visualiser/controllers/LobbyController.java @@ -0,0 +1,248 @@ +package seng302.visualiser.controllers; + +import com.sun.media.jfxmedia.logging.Logger; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javafx.application.Platform; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.TextArea; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.paint.Color; +import javafx.scene.text.Text; +import javafx.stage.Stage; +import seng302.gameServer.GameStages; +import seng302.gameServer.GameState; +import seng302.model.Colors; +import seng302.model.RaceState; +import seng302.visualiser.ClientToServerThread; + +/** + * A class describing the actions of the lobby screen + * Created by wmu16 on 10/07/17. + */ +public class LobbyController { + + public enum CloseStatus { + LEAVE, + READY + } + + @FunctionalInterface + public interface LobbyCloseListener { + void notify(CloseStatus exitCause); + } + + @FXML + private Text lobbyIpText; + @FXML + private Button readyButton; + @FXML + private Button customizeButton; + @FXML + private TextArea playerOneTxt; + @FXML + private TextArea playerTwoTxt; + @FXML + private TextArea playerThreeTxt; + @FXML + private TextArea playerFourTxt; + @FXML + private TextArea playerFiveTxt; + @FXML + private TextArea playerSixTxt; + @FXML + private TextArea playerSevenTxt; + @FXML + private TextArea playerEightTxt; + @FXML + private ImageView firstImageView; + @FXML + private ImageView secondImageView; + @FXML + private ImageView thirdImageView; + @FXML + private ImageView fourthImageView; + @FXML + private ImageView fifthImageView; + @FXML + private ImageView sixthImageView; + @FXML + private ImageView seventhImageView; + @FXML + private ImageView eighthImageView; + @FXML + private Text timeUntilStart; + @FXML + private Text courseNameText; + + private List imageViews = new ArrayList<>(); + private List