Merged Develop onto refactored yacht branch

tags: #story[1124] #pair[hyi25, wmu16]
This commit is contained in:
Haoming Yin
2017-08-15 15:04:35 +12:00
parent 6909f99773
commit 6a9357f598
4 changed files with 466 additions and 720 deletions
+460 -25
View File
@@ -3,9 +3,8 @@ package seng302.model;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
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.ReadOnlyLongProperty;
@@ -13,24 +12,31 @@ 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;
/**
* Yacht class for the racing boat. <p> 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.
* 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 {
public class ClientYacht {
@FunctionalInterface
public interface YachtLocationListener {
void notifyLocation(ClientYacht clientYacht, double lat, double lon, double heading,
double velocity);
void notifyLocation(ClientYacht yacht, double lat, double lon, double heading,
double velocity, boolean sailIn);
}
private Logger logger = LoggerFactory.getLogger(ClientYacht.class);
private static final Double ROUNDING_DISTANCE = 50d; // TODO: 3/08/17 wmu16 - Look into this value further
//BOTH AFAIK
private String boatType;
@@ -43,12 +49,27 @@ public class ClientYacht extends Observable {
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 currentVelocity;
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;
//CLIENT SIDE
private List<YachtLocationListener> locationListeners = new ArrayList<>();
@@ -58,6 +79,7 @@ public class ClientYacht extends Observable {
private CompoundMark lastMarkRounded;
private Integer positionInt = 0;
private Color colour;
private Boolean clientSailsIn = true;
public ClientYacht(String boatType, Integer sourceId, String hullID, String shortName,
String boatName, String country) {
@@ -67,18 +89,389 @@ public class ClientYacht extends Observable {
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.currentVelocity = 0d;
this.velocity = 0d; //in mms-1
this.hasEnteredRoundingZone = false;
this.hasPassedLine = false;
this.hasPassedThroughGate = false;
this.finishedRace = false;
}
/**
* Add ServerToClientThread as the observer, this observer pattern mainly server for the boat
* rounding package.
* @param timeInterval since last update in milliseconds
*/
@Override
public void addObserver(Observer o) {
super.addObserver(o);
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 / 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;
}
}
}
runAutoPilot();
//UPDATE BOAT LOCATION
lastLocation = location;
location = GeoUtility.getGeoCoordinate(location, heading, velocity * secondsElapsed);
//CHECK FOR MARK ROUNDING
if (!finishedRace) {
checkForLegProgression();
}
// TODO: 3/08/17 wmu16 - Implement line cross check here
}
/**
* 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<Double, Double> 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() {
@@ -128,6 +521,9 @@ public class ClientYacht extends Observable {
}
public void setLegNumber(Integer legNumber) {
// if (colour != null && position != "-" && legNumber != this.legNumber) {
// RaceViewController.updateYachtPositionSparkline(this, legNumber);
// }
this.legNumber = legNumber;
}
@@ -164,10 +560,18 @@ public class ClientYacht extends Observable {
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;
}
@@ -195,6 +599,8 @@ public class ClientYacht extends Observable {
* @param lng Longitude
*/
public void setLocation(Double lat, Double lng) {
lastLocation.setLat(location.getLat());
lastLocation.setLng(location.getLng());
location.setLat(lat);
location.setLng(lng);
}
@@ -207,6 +613,10 @@ public class ClientYacht extends Observable {
this.heading = heading;
}
public Boolean getSailIn() {
return sailIn;
}
@Override
public String toString() {
return boatName;
@@ -216,7 +626,7 @@ public class ClientYacht extends Observable {
this.timeSinceLastMarkProperty.set(timeSinceLastMark);
}
public ReadOnlyLongProperty timeSinceLastMarkProperty() {
public ReadOnlyLongProperty timeSinceLastMarkProperty () {
return timeSinceLastMarkProperty.getReadOnlyProperty();
}
@@ -233,25 +643,50 @@ public class ClientYacht extends Observable {
this.colour = colour;
}
// public Double getCurrentVelocity() {
// return currentVelocity;
// }
//
// public void setCurrentVelocity(Double currentVelocity) {
// this.currentVelocity = currentVelocity;
// }
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.currentVelocity = velocity;
this.velocity = velocity;
updateVelocityProperty(velocity);
for (YachtLocationListener yll : locationListeners) {
yll.notifyLocation(this, lat, lng, heading, velocity);
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);
}
-683
View File
@@ -1,683 +0,0 @@
package seng302.model;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
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;
/**
* 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 {
@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
//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;
//CLIENT SIDE
private List<YachtLocationListener> 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;
}
/**
* @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 / 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;
}
}
}
runAutoPilot();
//UPDATE BOAT LOCATION
lastLocation = location;
location = GeoUtility.getGeoCoordinate(location, heading, velocity * secondsElapsed);
//CHECK FOR MARK ROUNDING
if (!finishedRace) {
checkForLegProgression();
}
// TODO: 3/08/17 wmu16 - Implement line cross check here
}
/**
* 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<Double, Double> 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);
}
}
@@ -1,24 +1,21 @@
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;
import seng302.model.ClientYacht;
/**
* Created by kre39 on 6/08/17.
*/
public class BoatSailAnimationToggleTest {
private Yacht yacht;
private ClientYacht yacht;
@Before
public void setup() throws Exception{
yacht = new Yacht("Yacht", 1, "YACHT", "YAC", "Test Yacht", "NZ");
yacht = new ClientYacht("Yacht", 1, "YACHT", "YAC", "Test Yacht", "NZ");
}
@Test
+3 -6
View File
@@ -9,11 +9,9 @@ import seng302.gameServer.GameStages;
import seng302.gameServer.GameState;
import seng302.gameServer.MainServerThread;
import seng302.gameServer.server.messages.BoatAction;
import seng302.model.Yacht;
import seng302.model.ServerYacht;
import seng302.visualiser.ClientToServerThread;
import java.util.ArrayList;
/**
* Created by kre39 on 7/08/17.
*/
@@ -24,7 +22,6 @@ public class ToggleSailSteps {
ClientToServerThread client;
boolean sailsIn = false;
long startTime;
private Yacht yacht;
@@ -34,7 +31,7 @@ public class ToggleSailSteps {
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);
ServerYacht yacht = (new ArrayList<>(GameState.getYachts().values())).get(0);
Assert.assertFalse(yacht.getSailIn());
}
@@ -50,7 +47,7 @@ public class ToggleSailSteps {
@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);
ServerYacht yacht = (new ArrayList<>(GameState.getYachts().values())).get(0);
if (arg1 == "in") {
Assert.assertTrue(yacht.getSailIn());
} else {