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/src/main/java/seng302/gameServer/GameState.java b/src/main/java/seng302/gameServer/GameState.java index c67109fd..47548bdf 100644 --- a/src/main/java/seng302/gameServer/GameState.java +++ b/src/main/java/seng302/gameServer/GameState.java @@ -4,6 +4,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; + +import seng302.gameServer.server.messages.BoatAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import seng302.gameServer.server.messages.BoatAction; @@ -20,6 +22,13 @@ import seng302.model.mark.CompoundMark; import seng302.model.mark.Mark; import seng302.model.mark.MarkOrder; import seng302.utilities.GeoUtility; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seng302.model.GeoPoint; +import seng302.model.mark.CompoundMark; +import seng302.model.mark.Mark; +import seng302.utilities.GeoUtility; /** * A Static class to hold information about the current state of the game (model) @@ -50,6 +59,7 @@ public class GameState implements Runnable { private static GameStages currentStage; private static MarkOrder markOrder; private static long startTime; + private static Set marks; private static List markListeners; @@ -81,12 +91,18 @@ public class GameState implements Runnable { markListeners = new ArrayList<>(); new Thread(this).start(); //Run the auto updates on the game state + + marks = new MarkOrder().getAllMarks(); } public static String getHostIpAddress() { return hostIpAddress; } + public static Set getMarks(){ + return Collections.unmodifiableSet(marks); + } + public static List getPlayers() { return players; } @@ -97,7 +113,7 @@ public class GameState implements Runnable { + " " + player.getYacht().getCountry(); playerStringMap.put(player, playerText); } - + public static void removePlayer(Player player) { players.remove(player); playerStringMap.remove(player); @@ -120,7 +136,7 @@ public class GameState implements Runnable { } public static void setCurrentStage(GameStages currentStage) { - if (currentStage == GameStages.RACING){ + if (currentStage == GameStages.RACING) { startTime = System.currentTimeMillis(); } diff --git a/src/main/java/seng302/gameServer/MainServerThread.java b/src/main/java/seng302/gameServer/MainServerThread.java index 65ca99dd..9efcd65c 100644 --- a/src/main/java/seng302/gameServer/MainServerThread.java +++ b/src/main/java/seng302/gameServer/MainServerThread.java @@ -1,21 +1,29 @@ package seng302.gameServer; +import com.sun.corba.se.spi.activation.Server; import java.io.IOException; import java.net.ServerSocket; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Observable; +import java.util.Observer; import java.util.Timer; import java.util.TimerTask; import seng302.gameServer.server.messages.Message; +import seng302.model.GeoPoint; import seng302.model.Player; +import seng302.model.Yacht; +import seng302.model.mark.CompoundMark; +import seng302.utilities.GeoUtility; +import seng302.visualiser.GameClient; import seng302.model.PolarTable; /** * 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 extends Observable implements Runnable, ClientConnectionDelegate, + Observer { private static final int PORT = 4942; private static final Integer CLIENT_UPDATES_PER_SECOND = 10; @@ -27,7 +35,10 @@ public class MainServerThread extends Observable implements Runnable, ClientConn private ServerSocket serverSocket = null; private ArrayList serverToClientThreads = new ArrayList<>(); + private GameClient gameClient; + public MainServerThread() { + new GameState("localhost"); try { serverSocket = new ServerSocket(PORT); } catch (IOException e) { @@ -46,7 +57,6 @@ public class MainServerThread extends Observable implements Runnable, ClientConn HeartbeatThread heartbeatThread; serverListenThread = new ServerListenThread(serverSocket, this); - heartbeatThread = new HeartbeatThread(this); heartbeatThread.start(); @@ -97,14 +107,16 @@ public class MainServerThread extends Observable implements Runnable, ClientConn } - static void serverLog(String message, int logLevel){ - if(logLevel <= LOG_LEVEL){ - System.out.println("[SERVER " + LocalDateTime.now().toLocalTime().toString() + "] " + message); + 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 @@ -120,6 +132,7 @@ public class MainServerThread extends Observable implements Runnable, ClientConn /** * A player has left the game, remove the player from the GameState + * * @param player The player that left */ @Override @@ -141,11 +154,12 @@ public class MainServerThread extends Observable implements Runnable, ClientConn } } serverToClientThreads.remove(closedConnection); - setChanged(); - notifyObservers(); } public void startGame() { + initialiseBoatPositions(); + setupYachtObserver(); + Timer t = new Timer(); t.schedule(new TimerTask() { @@ -162,4 +176,69 @@ public class MainServerThread extends Observable implements Runnable, ClientConn public void terminate() { terminated = true; } + + /** + * Pass GameClient to main server thread so it can access the properties inside. + * + * @param gameClient gameClient + */ + public void setGameClient(GameClient gameClient) { + this.gameClient = gameClient; + } + + /** + * Initialise boats to specific spaced out geopoints behind starting line. + */ + private void initialiseBoatPositions() { + // Getting the start line compound marks + CompoundMark cm = gameClient.getCourseData().getCompoundMarks().get(1); + GeoPoint startMark1 = new GeoPoint(cm.getMarks().get(0).getLat(), + cm.getMarks().get(0).getLng()); + GeoPoint startMark2 = new GeoPoint(cm.getMarks().get(1).getLat(), + cm.getMarks().get(1).getLng()); + + // 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 DISTANCEFACTOR = 50.0; // distance apart in meters + int boatIndex = 0; + for (Yacht yacht : GameState.getYachts().values()) { + int distanceApart = boatIndex / 2; + + if (boatIndex % 2 == 1 && boatIndex != 0) { + distanceApart++; + distanceApart *= -1; + } + + GeoPoint spawnMark = GeoUtility + .getGeoCoordinate(midpoint, perpendicularAngle, distanceApart * DISTANCEFACTOR); + + if (yacht.getHeading() < perpendicularAngle) { + spawnMark = GeoUtility + .getGeoCoordinate(spawnMark, perpendicularAngle + 90, DISTANCEFACTOR); + } else { + spawnMark = GeoUtility + .getGeoCoordinate(spawnMark, perpendicularAngle + 270, DISTANCEFACTOR); + } + + yacht.setLocation(spawnMark); + boatIndex++; + } + } + + @Override + public void update(Observable o, Object arg) { + for (ServerToClientThread serverToClientThread : serverToClientThreads) { + serverToClientThread.sendCollisionMessage((Integer) arg); + } + } + + private void setupYachtObserver() { + for (ServerToClientThread serverToClientThread : serverToClientThreads) { + serverToClientThread.getYacht().addObserver(this); + } + } } diff --git a/src/main/java/seng302/gameServer/ServerToClientThread.java b/src/main/java/seng302/gameServer/ServerToClientThread.java index f55be085..d24ddfb2 100644 --- a/src/main/java/seng302/gameServer/ServerToClientThread.java +++ b/src/main/java/seng302/gameServer/ServerToClientThread.java @@ -19,6 +19,14 @@ import java.util.zip.CRC32; import java.util.zip.Checksum; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import seng302.gameServer.server.messages.YachtEventCodeMessage; +import seng302.model.Player; +import seng302.model.Yacht; +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.server.messages.BoatAction; import seng302.gameServer.server.messages.BoatLocationMessage; import seng302.gameServer.server.messages.BoatStatus; @@ -75,6 +83,8 @@ public class ServerToClientThread implements Runnable, Observer { private List connectionListeners = new ArrayList<>(); + private Yacht yacht; + public ServerToClientThread(Socket socket) { this.socket = socket; seqNo = 0; @@ -167,7 +177,6 @@ public class ServerToClientThread implements Runnable, Observer { int sync2; // TODO: 14/07/17 wmu16 - Work out how to fix this while loop - while (socket.isConnected()) { try { @@ -346,6 +355,14 @@ public class ServerToClientThread implements Runnable, Observer { return socket; } + public Yacht getYacht() { + return yacht; + } + + public void sendCollisionMessage(Integer yachtId) { + sendMessage(new YachtEventCodeMessage(yachtId)); + } + public void addConnectionListener(ConnectionListener listener) { connectionListeners.add(listener); } diff --git a/src/main/java/seng302/gameServer/server/messages/YachtEventCodeMessage.java b/src/main/java/seng302/gameServer/server/messages/YachtEventCodeMessage.java new file mode 100644 index 00000000..08db575a --- /dev/null +++ b/src/main/java/seng302/gameServer/server/messages/YachtEventCodeMessage.java @@ -0,0 +1,52 @@ +package seng302.gameServer.server.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/Colors.java b/src/main/java/seng302/model/Colors.java index 72ff3ba5..81829262 100644 --- a/src/main/java/seng302/model/Colors.java +++ b/src/main/java/seng302/model/Colors.java @@ -6,12 +6,12 @@ import javafx.scene.paint.Color; * Enum for generating colours. */ public enum Colors { - RED, PERU, SEAGREEN, GREEN, BLUE, PURPLE; + RED, PERU, GOLD, GREEN, BLUE, PURPLE, DEEPPINK, GRAY; static Integer index = 0; public static Color getColor() { - if (index == 6) { + if (index == 8) { index = 0; } return Color.valueOf(values()[index++].toString()); diff --git a/src/main/java/seng302/model/Yacht.java b/src/main/java/seng302/model/Yacht.java new file mode 100644 index 00000000..94cb4948 --- /dev/null +++ b/src/main/java/seng302/model/Yacht.java @@ -0,0 +1,805 @@ +package seng302.model; + +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyDoubleWrapper; +import javafx.beans.property.ReadOnlyLongProperty; +import javafx.beans.property.ReadOnlyLongWrapper; +import javafx.scene.paint.Color; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import seng302.gameServer.GameState; +import seng302.model.mark.CompoundMark; +import seng302.model.mark.Mark; +import seng302.utilities.GeoUtility; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.Observable; + +import static seng302.utilities.GeoUtility.getGeoCoordinate; + +/** + * 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 extends Observable { + + @FunctionalInterface + public interface YachtLocationListener { + void notifyLocation(Yacht yacht, double lat, double lon, double heading, double velocity, boolean sailIn); + } + + private Logger logger = LoggerFactory.getLogger(Yacht.class); + + private static final Double ROUNDING_DISTANCE = 50d; // TODO: 3/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; + private static final Double BOUNCE_DISTANCE_MARK = 20.0; + private static final Double BOUNCE_DISTANCE_YACHT = 30.0; + private static final Integer COLLISION_UPDATE_INTERVAL = 100; + private static final Double COLLISION_VELOCITY_PENALTY = 0.3; + + //BOTH AFAIK + 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 Long estimateTimeAtFinish; + private Integer currentMarkSeqID = 0; + private Long markRoundTime; + private Double distanceToCurrentMark; + private Long timeTillNext; + private Double heading; + private Integer legNumber = 0; + + //SERVER SIDE + public static final Double TURN_STEP = 5.0; //This should be in some utils class somewhere 2bh. Public for tests sake. + private Double lastHeading; + private Boolean sailIn = false; + private GeoPoint location; + private Integer boatStatus; + private Double velocity; + private Boolean isAuto; + private Double autoHeading; + + //MARK ROUNDING INFO + private GeoPoint lastLocation; //For purposes of mark rounding calculations + private Boolean hasEnteredRoundingZone; //The distance that the boat must be from the mark to round + private Boolean hasPassedLine; + private Boolean hasPassedThroughGate; + private Boolean finishedRace; + private Long lastCollisionUpdate; + + //CLIENT SIDE + private List locationListeners = new ArrayList<>(); + private ReadOnlyDoubleWrapper velocityProperty = new ReadOnlyDoubleWrapper(); + private ReadOnlyLongWrapper timeTillNextProperty = new ReadOnlyLongWrapper(); + private ReadOnlyLongWrapper timeSinceLastMarkProperty = new ReadOnlyLongWrapper(); + private CompoundMark lastMarkRounded; + private Integer positionInt = 0; + private Color colour; + private Boolean clientSailsIn = true; + + 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.sailIn = false; + this.isAuto = false; + this.location = new GeoPoint(57.670341, 11.826856); + this.lastLocation = location; + this.heading = 120.0; //In degrees + this.velocity = 0d; //in mms-1 + + this.hasEnteredRoundingZone = false; + this.hasPassedLine = false; + this.hasPassedThroughGate = false; + this.finishedRace = false; + } + + public Mark markCollidedWith() { + Set marksInRace = GameState.getMarks(); + + for (Mark mark : marksInRace) { + if (GeoUtility.getDistance(getLocation(), new GeoPoint(mark.getLat(), mark.getLng())) + <= MARK_COLLISION_DISTANCE) { + return mark; + } + } + + return null; + } + + /** + * @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 / 1.943844492 * 1000; + if (sailIn && velocity <= maxBoatSpeed && maxBoatSpeed != 0d) { + + if (velocity < maxBoatSpeed) { + velocity += maxBoatSpeed / 120; // 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; + } + } + } + + runAutoPilot(); + + //UPDATE BOAT LOCATION + lastLocation = location; + location = GeoUtility.getGeoCoordinate(location, heading, velocity * secondsElapsed); + Double metersCovered = velocity * secondsElapsed; + GeoPoint calculatedPoint = getGeoCoordinate(location, heading, metersCovered); + + if (shouldDoCollisionUpdate()) { + Yacht collidedYacht = checkCollision(calculatedPoint); + + if (collidedYacht != null) { + location = calculateBounceBackYacht(this, collidedYacht, BOUNCE_DISTANCE_YACHT); + velocity *= COLLISION_VELOCITY_PENALTY; + collidedYacht.setLocation( + calculateBounceBackYacht(collidedYacht, this, BOUNCE_DISTANCE_YACHT)); + collidedYacht.setVelocity(collidedYacht.getVelocity() * COLLISION_VELOCITY_PENALTY); + setChanged(); + notifyObservers(this.sourceId); + } else if (markCollidedWith() != null) { + location = calculateBounceBack( + new GeoPoint(markCollidedWith().getLat(), markCollidedWith().getLng()), + BOUNCE_DISTANCE_MARK); + velocity *= COLLISION_VELOCITY_PENALTY; + setChanged(); + notifyObservers(this.sourceId); + } else { + location = calculatedPoint; + } + + lastCollisionUpdate = System.currentTimeMillis(); + } else { + location = calculatedPoint; + } + + //CHECK FOR MARK ROUNDING + if (!finishedRace) { + checkForLegProgression(); + } + + // TODO: 3/08/17 wmu16 - Implement line cross check here + } + + /** + * @return true if COLLISION_UPDATE_INTERVAL has elapsed since the last collision update + */ + private Boolean shouldDoCollisionUpdate() { + if (lastCollisionUpdate == null) { + lastCollisionUpdate = System.currentTimeMillis(); + } + + return System.currentTimeMillis() - lastCollisionUpdate > COLLISION_UPDATE_INTERVAL; + } + + /** + * Calculate the new position of the boat after it has had a collision + * + * @return The boats new position + */ + private GeoPoint calculateBounceBack(GeoPoint collidedWith, Double bounceDistance) { + Double heading = GeoUtility.getBearing(location, collidedWith); + + // Invert heading + heading -= 180; + Integer newHeading = Math.floorMod(heading.intValue(), 360); + + return GeoUtility.getGeoCoordinate(location, newHeading.doubleValue(), bounceDistance); + } + + private GeoPoint calculateBounceBackYacht(Yacht collidingYacht, Yacht collidedYacht, + Double bounceDistance) { + Double heading = GeoUtility + .getBearing(collidingYacht.getLocation(), collidedYacht.getLocation()); + + heading -= 180; + Integer faultYachtHeading = Math.floorMod(heading.intValue(), 360); + + return GeoUtility + .getGeoCoordinate(collidingYacht.getLocation(), faultYachtHeading.doubleValue(), + bounceDistance); + } + + + /** + * 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)} + */ + public Double calcDistanceToCurrentMark() throws IndexOutOfBoundsException { + CompoundMark nextMark = GameState.getMarkOrder().getCurrentMark(currentMarkSeqID); + + if (nextMark.isGate()) { + Mark sub1 = nextMark.getSubMark(1); + Mark sub2 = nextMark.getSubMark(2); + Double distance1 = GeoUtility.getDistance(location, sub1); + Double distance2 = GeoUtility.getDistance(location, sub2); + return (distance1 < distance2) ? distance1 : distance2; + } else { + return GeoUtility.getDistance(location, nextMark.getSubMark(1)); + } + } + + + /** + * 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 + */ + private void checkForLegProgression() { + CompoundMark currentMark = GameState.getMarkOrder().getCurrentMark(currentMarkSeqID); + if (currentMarkSeqID == 0) { + checkStartLineCrossing(currentMark); + } else if (GameState.getMarkOrder().isLastMark(currentMarkSeqID)) { + checkFinishLineCrossing(currentMark); + } else if (currentMark.isGate()) { + checkGateRounding(currentMark); + } else { + checkMarkRounding(currentMark); + } + } + + /** + * If we pass the start line gate in the correct direction, progress + * + * @param currentMark The current gate + */ + private void checkStartLineCrossing(CompoundMark currentMark) { + Mark mark1 = currentMark.getSubMark(1); + Mark mark2 = currentMark.getSubMark(2); + CompoundMark nextMark = GameState.getMarkOrder().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) { + currentMarkSeqID++; + logMarkRounding(currentMark); + } + } + } + + + /** + * 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' + */ + private void checkMarkRounding(CompoundMark currentMark) { + distanceToCurrentMark = calcDistanceToCurrentMark(); + GeoPoint nextPoint = GameState.getMarkOrder().getNextMark(currentMarkSeqID).getMidPoint(); + GeoPoint prevPoint = GameState.getMarkOrder().getPreviousMark(currentMarkSeqID) + .getMidPoint(); + GeoPoint midPoint = GeoUtility.getDirtyMidPoint(nextPoint, prevPoint); + + //1 TEST FOR ENTERING THE ROUNDING DISTANCE + if (distanceToCurrentMark < ROUNDING_DISTANCE) { + hasEnteredRoundingZone = 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)) { + hasPassedLine = true; + } + } + + if (hasPassedLine && hasEnteredRoundingZone) { + currentMarkSeqID++; + hasPassedLine = false; + hasEnteredRoundingZone = false; + hasPassedThroughGate = false; + logMarkRounding(currentMark); + } + } + + + /** + * Checks if a gate line has been crossed and in the correct direction + * + * @param currentMark The current gate + */ + private void checkGateRounding(CompoundMark currentMark) { + Mark mark1 = currentMark.getSubMark(1); + Mark mark2 = currentMark.getSubMark(2); + CompoundMark prevMark = GameState.getMarkOrder().getPreviousMark(currentMarkSeqID); + CompoundMark nextMark = GameState.getMarkOrder().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) { + hasPassedThroughGate = true; + } + } + + Boolean prevMarkSide = GeoUtility.isClockwise(mark1, mark2, prevMark.getMidPoint()); + Boolean nextMarkSide = GeoUtility.isClockwise(mark1, mark2, nextMark.getMidPoint()); + + if (hasPassedThroughGate) { + //Check if we need to round this gate after passing through + if (prevMarkSide == nextMarkSide) { + checkMarkRounding(currentMark); + } else { + currentMarkSeqID++; + logMarkRounding(currentMark); + } + } + } + + /** + * If we pass the finish gate in the correct direction + * + * @param currentMark The current gate + */ + private void checkFinishLineCrossing(CompoundMark currentMark) { + Mark mark1 = currentMark.getSubMark(1); + Mark mark2 = currentMark.getSubMark(2); + CompoundMark prevMark = GameState.getMarkOrder().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) { + currentMarkSeqID++; + finishedRace = true; + logMarkRounding(currentMark); + logger.debug(sourceId + " finished"); + // TODO: 8/08/17 wmu16 - Do something! + } + } + } + + + /** + * 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 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.updateYachtPositionSparkline(this, 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 getPositionInteger() { + return positionInt; + } + + public void setPositionInteger(Integer position) { + this.positionInt = position; + } + + public void updateVelocityProperty(double velocity) { + this.velocityProperty.set(velocity); + } + + public void setMarkRoundingTime(Long markRoundingTime) { + this.markRoundTime = markRoundingTime; + } + + public ReadOnlyDoubleProperty getVelocityProperty() { + return velocityProperty.getReadOnlyProperty(); + } + + public double getVelocityMMS() { + return velocity; + } + + public ReadOnlyLongProperty timeTillNextProperty() { + return timeTillNextProperty.getReadOnlyProperty(); + } + + public Double getVelocityKnots() { + return velocity / 1000 * 1.943844492; // TODO: 26/07/17 cir27 - remove magic number + } + + 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; + } + + /** + * 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) { + lastLocation.setLat(location.getLat()); + lastLocation.setLng(location.getLng()); + location.setLat(lat); + location.setLng(lng); + } + + 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 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 toggleClientSail() { + clientSailsIn = !clientSailsIn; + } + + public Double getVelocity() { + return velocity; + } + + public void setVelocity(Double velocity) { + this.velocity = velocity; + } + + public Double getDistanceToCurrentMark() { + return distanceToCurrentMark; + } + + public Boolean getClientSailsIn(){ + return clientSailsIn; + } + + public void updateLocation(double lat, double lng, double heading, double velocity) { + setLocation(lat, lng); + this.heading = heading; + this.velocity = velocity; + updateVelocityProperty(velocity); + for (YachtLocationListener yll : locationListeners) { + yll.notifyLocation(this, lat, lng, heading, velocity, clientSailsIn); + } + } + + private void logMarkRounding(CompoundMark currentMark) { + String typeString = "mark"; + if (currentMark.isGate()) { + typeString = "gate"; + } + logger.debug( + String.format("BoatID %d passed %s %s with id %d. Now on leg %d", + sourceId, + typeString, + currentMark.getMarks().get(0).getName(), + currentMark.getId(), + currentMarkSeqID)); + } + + public void addLocationListener(YachtLocationListener listener) { + locationListeners.add(listener); + } + + public void setLocation(GeoPoint geoPoint) { + location = geoPoint; + } + + /** + * 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. + * + * @param calculatedPoint point will the yacht will move next + * @return yacht which collided with this yacht + */ + private Yacht checkCollision(GeoPoint calculatedPoint) { + + for (Yacht yacht : GameState.getYachts().values()) { + if (yacht != this) { + Double distance = GeoUtility.getDistance(yacht.getLocation(), calculatedPoint); + if (distance < YACHT_COLLISION_DISTANCE) { + return yacht; + } + } + } + return null; + } +} diff --git a/src/main/java/seng302/model/mark/MarkOrder.java b/src/main/java/seng302/model/mark/MarkOrder.java index 800e4e76..aa75b494 100644 --- a/src/main/java/seng302/model/mark/MarkOrder.java +++ b/src/main/java/seng302/model/mark/MarkOrder.java @@ -2,10 +2,6 @@ package seng302.model.mark; import java.io.IOException; import java.io.StringReader; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -19,14 +15,15 @@ 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(); @@ -76,6 +73,10 @@ public class MarkOrder { 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 @@ -86,6 +87,7 @@ public class MarkOrder { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db; Document doc; + allMarks = new HashSet<>(); try { db = dbf.newDocumentBuilder(); @@ -107,6 +109,7 @@ public class MarkOrder { CompoundMark compoundMark = marks.get(corner.getCompoundMarkID()); compoundMark.setRoundingSide(RoundingSide.getRoundingSide(corner.getRounding())); course.add(compoundMark); + allMarks.addAll(compoundMark.getMarks()); } return course; 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/utilities/StreamParser.java b/src/main/java/seng302/utilities/StreamParser.java index 0f4c48c0..304b105d 100644 --- a/src/main/java/seng302/utilities/StreamParser.java +++ b/src/main/java/seng302/utilities/StreamParser.java @@ -13,15 +13,12 @@ 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.MarkRoundingData; -import seng302.model.stream.parser.PositionUpdateData; +import seng302.model.stream.parser.*; import seng302.model.stream.parser.PositionUpdateData.DeviceType; -import seng302.model.stream.parser.RaceStartData; -import seng302.model.stream.parser.RaceStatusData; /** - * 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. + * 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. */ @@ -34,8 +31,9 @@ public class StreamParser { * @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) + if (packet.getType() != PacketType.HEARTBEAT) { return null; + } long heartbeat = bytesToLong(packet.getPayload()); System.out.println("heartbeat = " + heartbeat); return heartbeat; @@ -52,16 +50,17 @@ public class StreamParser { * containing the parsed packet data. */ public static RaceStatusData extractRaceStatus(StreamPacket packet) { - if (packet.getType() != PacketType.RACE_STATUS) + 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)); + 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))) @@ -70,7 +69,6 @@ public class StreamParser { windDir, rawWindSpeed, raceStatus, currentTime, expectedStartTime ); - // long timeTillStart = // ((new Date(expectedStartTime)).getTime() - (new Date(currentTime)).getTime()) / 1000; // @@ -110,7 +108,7 @@ public class StreamParser { // boat.setEstimateTimeAtFinish(estTimeAtFinish); data.addBoatData(boatID, estTimeAtNextMark, estTimeAtFinish, leg, boatStatus); } - return data; + return data; } // private static void setBoatLegPosition(Yacht updatingBoat, Integer leg){ @@ -139,8 +137,9 @@ public class StreamParser { * DISPLAY_TEXT_MESSAGE. */ public static List extractDisplayMessage(StreamPacket packet) { - if (packet.getType() != PacketType.DISPLAY_TEXT_MESSAGE) + if (packet.getType() != PacketType.DISPLAY_TEXT_MESSAGE) { return null; + } List message = new ArrayList<>(); byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; @@ -166,10 +165,11 @@ public class StreamParser { * XML_MESSAGE. */ public static Document extractXmlMessage(StreamPacket packet) { - if ( packet.getType() != PacketType.RACE_XML && - packet.getType() != PacketType.REGATTA_XML && - packet.getType() != PacketType.BOAT_XML ) + 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]; @@ -194,8 +194,8 @@ public class StreamParser { * 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. + * @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) { @@ -212,23 +212,25 @@ public class StreamParser { /** * Parses the the byte array in a StreamPacket for yacht events to retrieve the necessary info - * and returns it a an array of longs. + * and returns it as YachtEventData. * * @param packet Packet parsed in to use the payload - * @return the event data in the form [boatID, incidentID, eventID, timeStamp]. Returns null if - * the packet is not of type YACHT_EVENT_CODE. + * @return the event data in the form of YachtEventData. Returns null if the packet is not of + * type YACHT_EVENT_CODE. */ - public static long[] extractYachtEventCode(StreamPacket packet) { - if (packet.getType() != PacketType.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 long[] {subjectId, incidentId, eventId, timeStamp}; + return new YachtEventData(subjectId, incidentId, eventId, timeStamp); } /** @@ -239,15 +241,16 @@ public class StreamParser { * 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) + 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}; + return new long[]{subjectId, incidentId, eventId, timeStamp}; } /** @@ -258,8 +261,9 @@ public class StreamParser { * CHATTER_TEXT. */ public static String extractChatterText(StreamPacket packet) { - if (packet.getType() != PacketType.CHATTER_TEXT) + if (packet.getType() != PacketType.CHATTER_TEXT) { return null; + } byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; int messageType = payload[1]; @@ -276,8 +280,9 @@ public class StreamParser { * is not of type BOAT_LOCATION. */ public static PositionUpdateData extractBoatLocation(StreamPacket packet) { - if (packet.getType() != PacketType.BOAT_LOCATION) + 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)); @@ -293,10 +298,11 @@ public class StreamParser { double groundSpeed = bytesToLong(Arrays.copyOfRange(payload, 38, 40)) / 1000.0; DeviceType type; - if (deviceType == 1) + if (deviceType == 1) { type = DeviceType.YACHT_TYPE; - else + } else { type = DeviceType.MARK_TYPE; + } return new PositionUpdateData((int) boatId, type, lat, lon, heading, groundSpeed); } @@ -309,8 +315,9 @@ public class StreamParser { * if packet is not of type MARK_ROUNDING. */ public static MarkRoundingData extractMarkRounding(StreamPacket packet) { - if (packet.getType() != PacketType.MARK_ROUNDING) + if (packet.getType() != PacketType.MARK_ROUNDING) { return null; + } byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); @@ -325,16 +332,17 @@ public class StreamParser { } /** - * Returns a list containing the string value of data within the given stream packet for - * course wind. + * 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) + if (packet.getType() != PacketType.COURSE_WIND) { return null; + } byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; int selectedWindId = payload[1]; @@ -366,13 +374,13 @@ public class StreamParser { * 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. + * @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) + if (packet.getType() != PacketType.AVG_WIND) { return null; + } byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); @@ -384,7 +392,7 @@ public class StreamParser { 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[] { + return new long[]{ rawPeriod, rawSamplePeriod, period2, speed2, period3, speed3, period4, speed4, timeStamp }; } @@ -410,8 +418,7 @@ public class StreamParser { } /** - * takes an array of up to 7 bytes and returns a positive - * long constructed from the input bytes + * 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 diff --git a/src/main/java/seng302/visualiser/ClientToServerThread.java b/src/main/java/seng302/visualiser/ClientToServerThread.java index dc38e129..9f76d37c 100644 --- a/src/main/java/seng302/visualiser/ClientToServerThread.java +++ b/src/main/java/seng302/visualiser/ClientToServerThread.java @@ -36,6 +36,8 @@ import seng302.model.stream.packets.StreamPacket; */ public class ClientToServerThread implements Runnable { + + /** * Functional interface for receiving packets from client socket. */ diff --git a/src/main/java/seng302/visualiser/GameClient.java b/src/main/java/seng302/visualiser/GameClient.java index 09327fe2..ae3d2549 100644 --- a/src/main/java/seng302/visualiser/GameClient.java +++ b/src/main/java/seng302/visualiser/GameClient.java @@ -21,6 +21,7 @@ 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; @@ -102,6 +103,8 @@ public class GameClient { loadStartScreen(); } }); + + server.setGameClient(this); } private void loadStartScreen() { @@ -126,7 +129,8 @@ public class GameClient { * @return the lobby controller. */ private LobbyController loadLobby() { - FXMLLoader fxmlLoader = new FXMLLoader(GameClient.class.getResource("/views/LobbyView.fxml")); + FXMLLoader fxmlLoader = new FXMLLoader( + GameClient.class.getResource("/views/LobbyView.fxml")); try { holderPane.getChildren().clear(); holderPane.getChildren().add(fxmlLoader.load()); @@ -219,13 +223,18 @@ public class GameClient { case MARK_ROUNDING: updateMarkRounding(StreamParser.extractMarkRounding(packet)); break; + + case YACHT_EVENT_CODE: + showCollisionAlert(StreamParser.extractYachtEventCode(packet)); + break; } } } private void startRaceIfAllDataReceived() { - if (allXMLReceived() && raceView == null) + if (allXMLReceived() && raceView == null) { loadRaceView(); + } } private boolean allXMLReceived() { @@ -271,7 +280,9 @@ public class GameClient { private void processRaceStatusUpdate(RaceStatusData data) { if (allXMLReceived()) { raceState.updateState(data); - + if (raceView != null) { + raceView.getGameView().setWindDir(raceState.getWindDirection()); + } boolean raceFinished = true; for (ClientYacht yacht : allBoatsMap.values()) { if (yacht.getBoatStatus() != 3) { @@ -335,10 +346,26 @@ public class GameClient { 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); break; + socketThread.sendBoatAction(BoatAction.SAILS_IN); + raceView.getGameView().getPlayerYacht().toggleClientSail(); + 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) { + raceView.showCollision(yachtEventData.getSubjectId()); + } + } } diff --git a/src/main/java/seng302/visualiser/GameView.java b/src/main/java/seng302/visualiser/GameView.java index 24f42822..8e1260db 100644 --- a/src/main/java/seng302/visualiser/GameView.java +++ b/src/main/java/seng302/visualiser/GameView.java @@ -6,20 +6,34 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import javafx.animation.Animation; 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.event.ActionEvent; +import javafx.event.Event; +import javafx.event.EventHandler; 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.input.ScrollEvent; 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.shape.StrokeType; import javafx.scene.text.Text; import seng302.model.ClientYacht; +import javafx.util.Duration; import seng302.model.Colors; import seng302.model.GeoPoint; import seng302.model.Limit; @@ -40,10 +54,10 @@ import seng302.visualiser.map.CanvasMap; */ 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 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; @@ -53,6 +67,8 @@ public class GameView extends Pane { private double referencePointX, referencePointY; private double metersPerPixelX, metersPerPixelY; + final double SCALE_DELTA = 1.1; + private Text fpsDisplay = new Text(); private Polygon raceBorder = new CourseBoundary(); @@ -80,13 +96,33 @@ public class GameView extends Pane { private Double frameRate = 60.0; private int frameTimeIndex = 0; private boolean arrayFilled = false; + private Yacht playerYacht; + private double windDir = 0.0; + + double scaleFactor = 1; + + public void zoomOut() { + scaleFactor = 0.95; + for (Node child : getChildren()) { + child.setScaleX(child.getScaleX() * scaleFactor); + child.setScaleY(child.getScaleY() * scaleFactor); + } + } + + public void zoomIn() { + scaleFactor = 1.05; + for (Node child : getChildren()) { + child.setScaleX(child.getScaleX() * scaleFactor); + child.setScaleY(child.getScaleY() * scaleFactor); + } + } private enum ScaleDirection { HORIZONTAL, VERTICAL } - public GameView () { + public GameView() { gameObjects = this.getChildren(); // create image view for map, bind panel size to image gameObjects.add(mapImage); @@ -99,7 +135,7 @@ public class GameView extends Pane { initializeTimer(); } - private void initializeTimer () { + private void initializeTimer() { Arrays.fill(frameTimes, 1_000_000_000 / 60); timer = new AnimationTimer() { private long lastTime = 0; @@ -142,8 +178,8 @@ public class GameView extends Pane { } /** - * 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 + * 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(); @@ -217,7 +253,7 @@ public class GameView extends Pane { gates.add( makeAndBindGate( markerObjects.get(cMark.getSubMark(i)), - markerObjects.get(cMark.getSubMark(i+1)), + markerObjects.get(cMark.getSubMark(i + 1)), colour ) ); @@ -280,7 +316,7 @@ public class GameView extends Pane { gate.endYProperty().bind( m2.centerYProperty() ); - return gate; + return gate; } /** @@ -334,10 +370,10 @@ public class GameView extends Pane { 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, velocity) -> { + clientYacht.addLocationListener((boat, lat, lon, heading, velocity, sailIn) -> { BoatObject bo = boatObjects.get(boat); Point2D p2d = findScaledXY(lat, lon); - bo.moveTo(p2d.getX(), p2d.getY(), heading, velocity); + bo.moveTo(p2d.getX(), p2d.getY(), heading, velocity, sailIn, windDir); // annotations.get(boat).setLayoutX(p2d.getX()); // annotations.get(boat).setLayoutY(p2d.getY()); // annotations.get(boat).setLocation(100d, 100d); @@ -388,25 +424,25 @@ public class GameView extends Pane { annotations.put(clientYacht, newAnnotation); } - private void drawFps(Double fps){ + 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 + * 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); + 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); + GeoPoint maxLon = sortedPoints.get(sortedPoints.size() - 1); maxLonPoint = new GeoPoint(maxLon.getLat(), maxLon.getLng()); if (maxLonPoint.getLng() - minLonPoint.getLng() > 180) { horizontalInversion = true; @@ -426,15 +462,19 @@ public class GameView extends Pane { if (scaleDirection == ScaleDirection.HORIZONTAL) { referenceAngle = Math.abs( - GeoUtility.getBearingRad(referencePoint, minLonPoint) + GeoUtility.getBearingRad(referencePoint, minLonPoint) ); - referencePointX = bufferSize + distanceScaleFactor * Math.sin(referenceAngle) * GeoUtility.getDistance(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 = 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); + referencePointY += distanceScaleFactor * Math.cos(referenceAngle) * GeoUtility + .getDistance(referencePoint, maxLatPoint); } else { referencePointY = canvasHeight - bufferSize; referenceAngle = Math.abs( @@ -442,11 +482,14 @@ public class GameView extends Pane { GeoUtility.getDistance(referencePoint, minLonPoint) ) ); - referencePointX = bufferSize; - referencePointX += distanceScaleFactor * Math.sin(referenceAngle) * GeoUtility.getDistance(referencePoint, minLonPoint); - referencePointX += ((canvasWidth - (bufferSize + bufferSize)) - (minLonToMaxLon * distanceScaleFactor)) / 2; + referencePointX = bufferSize; + referencePointX += distanceScaleFactor * Math.sin(referenceAngle) * GeoUtility + .getDistance(referencePoint, minLonPoint); + referencePointX += + ((canvasWidth - (bufferSize + bufferSize)) - (minLonToMaxLon * distanceScaleFactor)) + / 2; } - if(horizontalInversion) { + if (horizontalInversion) { referencePointX = canvasWidth - bufferSize - (referencePointX - bufferSize); } } @@ -459,12 +502,12 @@ public class GameView extends Pane { private double scaleRaceExtremities() { double vertAngle = Math.abs( - GeoUtility.getBearingRad(minLatPoint, maxLatPoint) + GeoUtility.getBearingRad(minLatPoint, maxLatPoint) ); double vertDistance = Math.cos(vertAngle) * GeoUtility.getDistance(minLatPoint, maxLatPoint); double horiAngle = Math.abs( - GeoUtility.getBearingRad(minLonPoint, maxLonPoint) + GeoUtility.getBearingRad(minLonPoint, maxLonPoint) ); if (horiAngle <= (Math.PI / 2)) { horiAngle = (Math.PI / 2) - horiAngle; @@ -490,35 +533,43 @@ public class GameView extends Pane { return findScaledXY(unscaled.getLat(), unscaled.getLng()); } - private Point2D findScaledXY (double unscaledLat, double unscaledLon) { + 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) + 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); + 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); + 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); + 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); + xAxisLocation -= Math + .round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); + yAxisLocation += Math + .round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); } - if(horizontalInversion) { + if (horizontalInversion) { xAxisLocation = canvasWidth - bufferSize - (xAxisLocation - bufferSize); } return new Point2D(xAxisLocation, yAxisLocation); @@ -545,7 +596,7 @@ public class GameView extends Pane { metersPerPixelY = dVertical / dy; } - public void setAnnotationVisibilities (boolean teamName, boolean velocity, boolean estTime, + 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); @@ -558,7 +609,7 @@ public class GameView extends Pane { } } - public void setFPSVisibility (boolean visibility) { + public void setFPSVisibility(boolean visibility) { fpsDisplay.setVisible(visibility); } @@ -568,15 +619,27 @@ public class GameView extends Pane { ); } - public void pauseRace () { + public void pauseRace() { timer.stop(); } - public void startRace () { + + public void setWindDir(double windDir) { + this.windDir = windDir; + } + + public void startRace() { timer.start(); } - public void setBoatAsPlayer(ClientYacht playerYacht) { + public ClientYacht getPlayerYacht() { + return playerYacht; + } + + + public void setBoatAsPlayer (ClientYacht playerYacht) { + this.playerYacht = playerYacht; + this.playerYacht.toggleClientSail(); boatObjects.get(playerYacht).setAsPlayer(); annotations.get(playerYacht).addAnnotation( "velocity", @@ -590,4 +653,40 @@ public class GameView extends Pane { gameObjects.add(annotations.get(playerYacht)); }); } + + /** + * 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) { + Platform.runLater(() -> { + Point2D point = findScaledXY(collisionPoint); + double circleRadius = 0.0; + Circle circle = new Circle(point.getX(), point.getY(), circleRadius, Color.RED); + gameObjects.add(circle); + + 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); + timeline.play(); + + timeline.setOnFinished(event -> gameObjects.remove(circle)); + }); + } } diff --git a/src/main/java/seng302/visualiser/controllers/RaceViewController.java b/src/main/java/seng302/visualiser/controllers/RaceViewController.java index b8d529b7..0eae1d4c 100644 --- a/src/main/java/seng302/visualiser/controllers/RaceViewController.java +++ b/src/main/java/seng302/visualiser/controllers/RaceViewController.java @@ -208,9 +208,10 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel /** - * Used to add any new yachts into the race that may have started late or not have had data received yet + * Used to add any new yachts into the race that may have started late or not have had data + * received yet */ - private void updateSparkLine(){ + private void updateSparkLine() { // TODO: 2/08/17 there is about 0 chance of this working. Once we are keeping track of boat positions it can be fixed. // Collect the racing yachts that aren't already in the chart sparkLineData.clear(); @@ -228,14 +229,14 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel 1.0 + participants.size() - yacht.getPositionInteger() ) ); - sparkLineData.add(yachtData); + sparkLineData.add(yachtData); }); // Lambda function to sort the series in order of leg (later legs shown more to the right) sparkLineData.sort((o1, o2) -> { - Integer leg1 = Integer.parseInt(o1.getData().get(o1.getData().size()-1).getXValue()); - Integer leg2 = Integer.parseInt(o2.getData().get(o2.getData().size()-1).getXValue()); - if (leg2 < leg1){ + 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; @@ -248,7 +249,8 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel .filter(spark -> !raceSparkLine.getData().contains(spark)) .forEach(spark -> { raceSparkLine.getData().add(spark); - spark.getNode().lookup(".chart-series-line").setStyle("-fx-stroke:" + getBoatColorAsRGB(spark.getName())); + spark.getNode().lookup(".chart-series-line") + .setStyle("-fx-stroke:" + getBoatColorAsRGB(spark.getName())); }) ); } @@ -284,26 +286,27 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel /** * gets the rgb string of the yachts colour to use for the chart via css + * * @param yachtId id of yacht passed in to get the yachts colour * @return the colour as an rgb string */ - private String getBoatColorAsRGB(String yachtId){ + private String getBoatColorAsRGB(String yachtId) { Color color = participants.get(Integer.valueOf(yachtId)).getColour(); - if (color == null){ - return String.format("#%02X%02X%02X",255,255,255); + 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 ) + return String.format("#%02X%02X%02X", + (int) (color.getRed() * 255), + (int) (color.getGreen() * 255), + (int) (color.getBlue() * 255) ); } /** * Initialises a timer which updates elements of the RaceView such as wind direction, yacht - * orderings etc.. which are dependent on the info from the stream parser constantly. - * Updates of each of these attributes are called ONCE EACH SECOND + * 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() { timer.scheduleAtFixedRate(new TimerTask() { @@ -318,9 +321,10 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel } /** - * Iterates over all corners until ones SeqID matches with the yachts 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. + * Iterates over all corners until ones SeqID matches with the yachts 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 */ @@ -474,15 +478,17 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel } - 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); + 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) { + 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()); @@ -501,8 +507,8 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel /** - * Initialised the combo box with any yachts currently in the race and adds the required listener - * for the combobox to take action upon selection + * Initialised the combo box with any yachts currently in the race and adds the required + * listener for the combobox to take action upon selection */ private void initialiseBoatSelectionComboBox() { yachtSelectionComboBox.setItems( @@ -539,7 +545,7 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel TimeUnit.MILLISECONDS.toHours(milliseconds), TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60, //Modulus 60 minutes per hour TimeUnit.MILLISECONDS.toSeconds(milliseconds) % 60 //Modulus 60 seconds per minute - ); + ); } private void setAnnotations(Integer annotationLevel) { @@ -590,8 +596,23 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel // } } - public void updateRaceData (RaceXMLData raceData) { + public void updateRaceData(RaceXMLData raceData) { this.courseData = raceData; gameView.updateBorder(raceData.getCourseLimit()); } + + /** + * Called by game client after receiving yacht event packet. Parameter subject id is the + * offending yacht. This function in turn will pass the yacht location to game view to display a + * collision alert. + * + * @param subjectId source id of offending yacht + */ + public void showCollision(Long subjectId) { + gameView.drawCollision(participants.get((int) (long) subjectId).getLocation()); + } + + public GameView getGameView() { + return gameView; + } } \ No newline at end of file diff --git a/src/main/java/seng302/visualiser/controllers/StartScreenController.java b/src/main/java/seng302/visualiser/controllers/StartScreenController.java index 87199442..1a9db1a1 100644 --- a/src/main/java/seng302/visualiser/controllers/StartScreenController.java +++ b/src/main/java/seng302/visualiser/controllers/StartScreenController.java @@ -66,7 +66,7 @@ public class StartScreenController implements Initializable { */ @FXML public void hostButtonPressed() { - new GameState(getLocalHostIp()); +// new GameState(getLocalHostIp()); gameClient = new GameClient(holder); gameClient.runAsHost(getLocalHostIp(), 4942); // try { diff --git a/src/main/java/seng302/visualiser/fxObjects/BoatObject.java b/src/main/java/seng302/visualiser/fxObjects/BoatObject.java index d2ab40c2..b04fcaae 100644 --- a/src/main/java/seng302/visualiser/fxObjects/BoatObject.java +++ b/src/main/java/seng302/visualiser/fxObjects/BoatObject.java @@ -30,9 +30,11 @@ public class BoatObject extends Group { private double xVelocity; private double yVelocity; private double lastHeading; + private double sailState; //Graphical objects private Polyline trail = new Polyline(); private Polygon boatPoly; + private Polygon sail; private Wake wake; private Line leftLayLine; private Line rightLayline; @@ -94,7 +96,16 @@ public class BoatObject extends Group { trail.setCache(true); wake = new Wake(0, -BOAT_HEIGHT); wake.setVisible(true); - super.getChildren().addAll(boatPoly);//, annotationBox); + + sail = new Polygon(0.0,BOAT_HEIGHT / 4, + 0.0, BOAT_HEIGHT); + sailState = 0; + sail.setStrokeWidth(2.0); + sail.setStroke(Color.BLACK); + sail.setFill(Color.TRANSPARENT); + sail.setCache(true); + super.getChildren().clear(); + super.getChildren().addAll(boatPoly, sail); } public void setFill (Paint value) { @@ -105,19 +116,30 @@ public class BoatObject extends Group { /** * Moves the boat and its children annotations to coordinates specified - * - * @param x The X coordinate to move the boat to + * @param x The X coordinate to move the boat to * @param y The Y coordinate to move the boat to * @param rotation The rotation by which the boat moves * @param velocity The velocity the boat is moving + * @param sailIn */ - public void moveTo(double x, double y, double rotation, double velocity) { + public void moveTo(double x, double y, double rotation, double velocity, Boolean sailIn, double windDir) { Double dx = Math.abs(boatPoly.getLayoutX() - x); Double dy = Math.abs(boatPoly.getLayoutY() - y); Platform.runLater(() -> { - rotateTo(rotation); + rotateTo(rotation, sailIn, windDir); boatPoly.setLayoutX(x); boatPoly.setLayoutY(y); + if (sailIn) { +// sail.getPoints().clear(); +// sail.getPoints().addAll(0.0, 0.0, 4.0, 1.5, 8.0, 3.0, 12.0, 3.5, 16.0, 3.0, 20.0, 1.5, 24.0, 0.0); +// sail.getPoints().addAll(0.0, 0.0, 24.0, 0.0); + sail.setLayoutX(x); + sail.setLayoutY(y); + } else { + animateSail(); + sail.setLayoutX(x); + sail.setLayoutY(y); + } wake.setLayoutX(x); wake.setLayoutY(y); }); @@ -142,8 +164,65 @@ public class BoatObject extends Group { } } - private void rotateTo(double rotation) { - boatPoly.getTransforms().setAll(new Rotate(rotation)); + private Double normalizeHeading(double heading, double windDirection) { + Double normalizedHeading = heading - windDirection; + normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360L); + return normalizedHeading; + } + + + private void rotateTo(double heading, boolean sailsIn, double windDir) { + boatPoly.getTransforms().setAll(new Rotate(heading)); + if (sailsIn) { + Double sailWindOffset = 30.0; + Double upwindAngleLimit = 15.0; + Double downwindAngleLimit = 10.0; //Upwind from normalised horizontal + Double normalizedHeading = normalizeHeading(heading, windDir); + if (normalizedHeading < 180) { + sail.getTransforms().setAll(new Rotate(windDir + 90 + sailWindOffset)); + sail.getPoints().clear(); + sail.getPoints().addAll(0.0, 0.0, 4.0, -1.5, 8.0, -3.0, 12.0, -3.5, 16.0, -3.0, 20.0, -1.5, 24.0, 0.0); + if (normalizedHeading > 90 + sailWindOffset){ + sail.getTransforms().setAll(new Rotate(heading + downwindAngleLimit)); + } + if (normalizedHeading < sailWindOffset + upwindAngleLimit){ + sail.getTransforms().setAll(new Rotate(heading + 90 - upwindAngleLimit)); + } + } else { + sail.getTransforms().setAll(new Rotate(windDir + 90 - sailWindOffset)); + sail.getPoints().clear(); + sail.getPoints().addAll(0.0, 0.0, 4.0, 1.5, 8.0, 3.0, 12.0, 3.5, 16.0, 3.0, 20.0, 1.5, 24.0, 0.0); + if (normalizedHeading < 270 - sailWindOffset){ + sail.getTransforms().setAll(new Rotate(heading + 180 - downwindAngleLimit)); + } + if (normalizedHeading > 360 - (sailWindOffset + upwindAngleLimit)){ + sail.getTransforms().setAll(new Rotate(heading + 90 + upwindAngleLimit)); + } + } + } else { + sail.getTransforms().setAll(new Rotate(windDir)); + } + } + + + private void animateSail(){ + Double[] points = new Double[200]; + double amplitude = 2.0; + double period = 10; + for (int i = 0; i < 50; i++) { + points[i * 2] = amplitude * Math.sin(((Math.PI * i) / period + sailState)); + points[i * 2 + 1] = (BOAT_HEIGHT * i) / BOAT_HEIGHT / 2; + points[199 - (i * 2)] = (BOAT_HEIGHT * i) / BOAT_HEIGHT / 2; + points[199 - (i * 2 + 1)] = amplitude * Math.sin(((Math.PI * i) / period + sailState)); + } + if (sailState == - 2 * Math.PI) { + sailState = 0; + } else { + sailState = sailState - Math.PI / 5; + } + sail.getPoints().clear(); + sail.getPoints().addAll(points); + } public void updateLocation() { @@ -275,11 +354,12 @@ public class BoatObject extends Group { boatPoly.setStroke(Color.BLACK); boatPoly.setStrokeWidth(3); isPlayer = true; + animateSail(); } - public void setTrajectory(double heading, double velocity) { + public void setTrajectory(double heading, double velocity, double windDir) { wake.setRotation(lastHeading - heading, velocity); - rotateTo(heading); + rotateTo(heading, false, windDir); xVelocity = Math.cos(Math.toRadians(heading)) * velocity; yVelocity = Math.sin(Math.toRadians(heading)) * velocity; lastHeading = heading; diff --git a/src/main/resources/config/config.xml b/src/main/resources/config/config.xml index b5c90704..4f002974 100644 --- a/src/main/resources/config/config.xml +++ b/src/main/resources/config/config.xml @@ -4,6 +4,6 @@ AC35 6 10.0 - 135 + 135 diff --git a/src/main/resources/server_config/boats1.xml b/src/main/resources/server_config/boats1.xml deleted file mode 100644 index 401e7bf6..00000000 --- a/src/main/resources/server_config/boats1.xml +++ /dev/null @@ -1,171 +0,0 @@ - - - 2015-08-28T17:32:59+0100 - 12 - 219 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/server_config/boats2.xml b/src/main/resources/server_config/boats2.xml deleted file mode 100644 index c7255771..00000000 --- a/src/main/resources/server_config/boats2.xml +++ /dev/null @@ -1,161 +0,0 @@ - - - 2015-08-28T17:32:59+0100 - 12 - 219 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/server_config/boats3.xml b/src/main/resources/server_config/boats3.xml deleted file mode 100644 index 401e7bf6..00000000 --- a/src/main/resources/server_config/boats3.xml +++ /dev/null @@ -1,171 +0,0 @@ - - - 2015-08-28T17:32:59+0100 - 12 - 219 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/views/StartScreenView.fxml b/src/main/resources/views/StartScreenView.fxml index ec629307..4bd0a808 100644 --- a/src/main/resources/views/StartScreenView.fxml +++ b/src/main/resources/views/StartScreenView.fxml @@ -1,5 +1,10 @@ + + + + + @@ -10,6 +15,7 @@ + @@ -25,7 +31,7 @@ - + diff --git a/src/test/java/RunCucumberTests.java b/src/test/java/RunCucumberTests.java new file mode 100644 index 00000000..24b2ae54 --- /dev/null +++ b/src/test/java/RunCucumberTests.java @@ -0,0 +1,12 @@ +import cucumber.api.CucumberOptions; +import cucumber.api.junit.Cucumber; +import org.junit.runner.RunWith; + +/** + * Created by kre39 on 7/08/17. + */ + +@RunWith(Cucumber.class) +@CucumberOptions(features = "src/test/java/features") +public class RunCucumberTests { +} diff --git a/src/test/java/features/toggleSail.feature b/src/test/java/features/toggleSail.feature new file mode 100644 index 00000000..a3fb4598 --- /dev/null +++ b/src/test/java/features/toggleSail.feature @@ -0,0 +1,5 @@ +Feature: SailsToggle + Scenario: User toggles in sail + Given The game is running + When the user has pressed "shift" + Then the sails are "in" \ No newline at end of file diff --git a/src/test/java/seng302/ColorsTest.java b/src/test/java/seng302/ColorsTest.java index d9f1ee4f..0b4b4238 100644 --- a/src/test/java/seng302/ColorsTest.java +++ b/src/test/java/seng302/ColorsTest.java @@ -9,9 +9,9 @@ public class ColorsTest { @Test public void testNextColor() { - Color expectedColors[] = {Color.RED, Color.PERU, Color.SEAGREEN, Color.GREEN, Color.BLUE, Color.PURPLE}; - for (int i = 0; i<6; i++) - { + Color expectedColors[] = {Color.RED, Color.PERU, Color.GOLD, Color.GREEN, Color.BLUE, + Color.PURPLE, Color.DEEPPINK, Color.GRAY}; + for (int i = 0; i < 8; i++) { Assert.assertEquals(expectedColors[i], Colors.getColor()); } } diff --git a/src/test/java/seng302/model/UpdateYachtTest.java b/src/test/java/seng302/model/UpdateYachtTest.java new file mode 100644 index 00000000..45fffa8d --- /dev/null +++ b/src/test/java/seng302/model/UpdateYachtTest.java @@ -0,0 +1,92 @@ +package seng302.model; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import seng302.gameServer.GameState; +import seng302.utilities.GeoUtility; + +/** + * Test update function in Yacht.java to make sure yacht will not be collide each other within 25.0 + * meters. + */ +public class UpdateYachtTest { + + private Yacht yacht1 = new Yacht("Yacht", 1, "1", "Yacht" + 1, "Yacht" + 1, "Test1"); + private Yacht yacht2 = new Yacht("Yacht", 2, "2", "Yacht" + 2, "Yacht" + 2, "Test2"); + private GeoPoint geoPoint1 = new GeoPoint(50.0, 50.0); + private GeoPoint geoPoint2 = GeoUtility.getGeoCoordinate(geoPoint1, 90.0, 50.0); + + @Before + public void setUpRace() { + new GameState(""); + GameState.addYacht(1, yacht1); + GameState.addYacht(2, yacht2); + PolarTable.parsePolarFile(getClass().getResourceAsStream("/config/acc_polars.csv")); + } + + @Test + public void testUpdateYachtWithCollision() { + // Yacht 1 heading towards 90 degrees heading + yacht1.setLocation(geoPoint1); + yacht1.updateLocation(geoPoint1.getLat(), geoPoint1.getLng(), 90.0, 5.0); + + // Yacht 2 heading towards 270 degrees heading + yacht2.setLocation(geoPoint2); + yacht2.updateLocation(geoPoint2.getLat(), geoPoint2.getLng(), 270.0, 5.0); + + // Start yacht 1 and rest yacht 2 + if (!yacht1.getSailIn()) { + yacht1.toggleSailIn(); + } + + for (int i = 0; i < 6; i++) { + yacht1.update((long) 1000); + + // Making sure boat is moving + double moved = GeoUtility.getDistance(yacht1.getLocation(), geoPoint1); + Assert.assertTrue(moved > 0); + + // Making sure no collision + Double distance = GeoUtility.getDistance(yacht1.getLocation(), geoPoint2); + + Assert.assertTrue(distance > Math.min(Yacht.MARK_COLLISION_DISTANCE, Yacht.YACHT_COLLISION_DISTANCE)); + } + } + + @Test + public void testUpdateYachtWithoutCollision() { + // Yacht 1 heading towards 90 degrees heading + yacht1.setLocation(geoPoint1); + yacht1.updateLocation(geoPoint1.getLat(), geoPoint1.getLng(), 90.0, 5.0); + + // Yacht 2 heading towards 90 degrees heading + yacht2.setLocation(geoPoint2); + yacht2.updateLocation(geoPoint2.getLat(), geoPoint2.getLng(), 90.0, 5.0); + + // Start yacht 1 and yacht 2 + if (!yacht1.getSailIn()) { + yacht1.toggleSailIn(); + } + if (!yacht2.getSailIn()) { + yacht2.toggleSailIn(); + } + + double previousDistance1 = 0; + double previousDistance2 = 0; + + for (int i = 0; i < 6; i++) { + yacht1.update((long) 1000); + yacht2.update((long) 1000); + + // Making sure boat is moving + double yachtMoved1 = GeoUtility.getDistance(yacht1.getLocation(), geoPoint1); + Assert.assertTrue(yachtMoved1 > previousDistance1); + previousDistance1 = yachtMoved1; + + double yachtMoved2 = GeoUtility.getDistance(yacht2.getLocation(), geoPoint2); + Assert.assertTrue(yachtMoved2 > previousDistance2); + previousDistance2 = yachtMoved2; + } + } +} diff --git a/src/test/java/seng302/visualiser/map/BoatSailAnimationToggleTest.java b/src/test/java/seng302/visualiser/map/BoatSailAnimationToggleTest.java new file mode 100644 index 00000000..cccea5c6 --- /dev/null +++ b/src/test/java/seng302/visualiser/map/BoatSailAnimationToggleTest.java @@ -0,0 +1,31 @@ +package seng302.visualiser.map; + +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import seng302.model.Yacht; +import seng302.visualiser.fxObjects.BoatObject; + +/** + * Created by kre39 on 6/08/17. + */ +public class BoatSailAnimationToggleTest { + + private Yacht yacht; + + @Before + public void setup() throws Exception{ + yacht = new Yacht("Yacht", 1, "YACHT", "YAC", "Test Yacht", "NZ"); + } + + @Test + public void sailToggleTest() throws Exception { + assertFalse(yacht.getSailIn()); + yacht.toggleClientSail(); + assertFalse(yacht.getSailIn()); + } + +} diff --git a/src/test/java/steps/ToggleSailSteps.java b/src/test/java/steps/ToggleSailSteps.java new file mode 100644 index 00000000..5347224d --- /dev/null +++ b/src/test/java/steps/ToggleSailSteps.java @@ -0,0 +1,60 @@ +package steps; + +import cucumber.api.java.en.Given; +import cucumber.api.java.en.Then; +import cucumber.api.java.en.When; +import java.util.ArrayList; +import org.junit.Assert; +import seng302.gameServer.GameStages; +import seng302.gameServer.GameState; +import seng302.gameServer.MainServerThread; +import seng302.gameServer.server.messages.BoatAction; +import seng302.model.Yacht; +import seng302.visualiser.ClientToServerThread; + +import java.util.ArrayList; + +/** + * Created by kre39 on 7/08/17. + */ +public class ToggleSailSteps { + + + MainServerThread mst; + ClientToServerThread client; + boolean sailsIn = false; + long startTime; + private Yacht yacht; + + + + @Given("^The game is running$") + public void the_game_is_running() throws Throwable { + mst = new MainServerThread(); + client = new ClientToServerThread("localhost", 4942); + GameState.setCurrentStage(GameStages.RACING); + Thread.sleep(200); // Sleep needed to help the threads all be up to speed with each other + Yacht yacht = (new ArrayList<>(GameState.getYachts().values())).get(0); + Assert.assertFalse(yacht.getSailIn()); + } + + + @When("^the user has pressed \"([^\"]*)\"$") + public void the_user_has_pressed(String arg1) throws Throwable { + startTime = System.currentTimeMillis(); + if (arg1 == "shift") { + client.sendBoatAction(BoatAction.SAILS_IN); + } + } + + @Then("^the sails are \"([^\"]*)\"$") + public void the_sails_are(String arg1) throws Throwable { + Thread.sleep(200); // Sleep needed to help the threads all be up to speed with each other + Yacht yacht = (new ArrayList<>(GameState.getYachts().values())).get(0); + if (arg1 == "in") { + Assert.assertTrue(yacht.getSailIn()); + } else { + Assert.assertFalse(yacht.getSailIn()); + } + } +}