mirror of
https://github.com/michaelrausch/Party-Parrots-At-Sea.git
synced 2026-05-09 14:28:43 +00:00
Changed package heirachy. Merged Controller and StartScreenController.
#refactor
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package seng302.model;
|
||||
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
/**
|
||||
* Enum for randomly generating colours.
|
||||
*/
|
||||
public enum Colors {
|
||||
RED, PERU, SEAGREEN, GREEN, BLUE, PURPLE;
|
||||
|
||||
static Integer index = 0;
|
||||
|
||||
public static Color getColor() {
|
||||
if (index == 6) {
|
||||
index = 0;
|
||||
}
|
||||
return Color.valueOf(values()[index++].toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package seng302.model;
|
||||
|
||||
import java.net.Socket;
|
||||
|
||||
/**
|
||||
* A Class defining a player and their respective details in the game as held by the model
|
||||
* Created by wmu16 on 10/07/17.
|
||||
*/
|
||||
public class Player {
|
||||
|
||||
private Socket socket;
|
||||
private Yacht yacht;
|
||||
private Integer lastMarkPassed;
|
||||
|
||||
|
||||
public Player(Socket socket) {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
public Socket getSocket() {
|
||||
return socket;
|
||||
}
|
||||
|
||||
public Integer getLastMarkPassed() {
|
||||
return lastMarkPassed;
|
||||
}
|
||||
|
||||
public void setLastMarkPassed(Integer lastMarkPassed) {
|
||||
this.lastMarkPassed = lastMarkPassed;
|
||||
}
|
||||
|
||||
public Yacht getYacht() {
|
||||
return yacht;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String playerAddress = null;
|
||||
|
||||
if (socket == null){
|
||||
return "Disconnected Player";
|
||||
}
|
||||
|
||||
playerAddress = socket.getRemoteSocketAddress().toString();
|
||||
|
||||
|
||||
return playerAddress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null){
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(obj instanceof Player)){
|
||||
return false;
|
||||
}
|
||||
|
||||
return ((Player) obj).socket.equals(socket);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode(){
|
||||
return socket.hashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package seng302.model;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* A static class for parsing and storing the polars. Will parse the whole polar table and also store the optimised
|
||||
* upwind and downwind in separate tables here as well
|
||||
* Created by wmu16 on 22/05/17.
|
||||
*/
|
||||
public final class PolarTable {
|
||||
|
||||
//A Polar table will consist of a wind speed key to a hashmap value of pairs of wind angles and boat speeds
|
||||
private static HashMap<Double, HashMap<Double, Double>> polarTable;
|
||||
private static HashMap<Double, HashMap<Double, Double>> upwindOptimal;
|
||||
private static HashMap<Double, HashMap<Double, Double>> downwindOptimal;
|
||||
|
||||
private static int upTwaIndex;
|
||||
private static int dnTwaIndex;
|
||||
|
||||
|
||||
/**
|
||||
* Iterates through each row of the polar table, in pairs, to extract the row into a hashmap of angle to boat speed.
|
||||
* These angle boatspeed hashmaps are then added to an outer hashmap at the end of wind speed key to each row hashmap
|
||||
* as a value
|
||||
*/
|
||||
public static void parsePolarFile(InputStream polarFile) {
|
||||
polarTable = new HashMap<>();
|
||||
upwindOptimal = new HashMap<>();
|
||||
downwindOptimal = new HashMap<>();
|
||||
|
||||
String line;
|
||||
Boolean isHeaderLine = true;
|
||||
|
||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(polarFile))) {
|
||||
while ((line = br.readLine()) != null) {
|
||||
String[] thisLine = line.split(",");
|
||||
|
||||
//Initial line in file
|
||||
if (isHeaderLine) {
|
||||
deduceHeaders(thisLine);
|
||||
isHeaderLine = false;
|
||||
} else {
|
||||
HashMap<Double, Double> thisPolar = new HashMap<>();
|
||||
HashMap<Double, Double> thisUpWindPolar = new HashMap<>();
|
||||
HashMap<Double, Double> thisDnWindPolar = new HashMap<>();
|
||||
Double thisWindSpeed = Double.parseDouble(thisLine[0]);
|
||||
|
||||
// -3 <== -1 for length -1, and a further -2 as we iterate in pairs of 2 so finish before final 2
|
||||
for (int i = 1; i < thisLine.length; i += 2) {
|
||||
Double thisWindAngle = Double.parseDouble(thisLine[i]);
|
||||
Double thisBoatSpeed = Double.parseDouble(thisLine[i + 1]);
|
||||
thisPolar.put(thisWindAngle, thisBoatSpeed);
|
||||
if (i == upTwaIndex) {
|
||||
thisUpWindPolar.put(thisWindAngle, thisBoatSpeed);
|
||||
} else if (i == dnTwaIndex) {
|
||||
thisDnWindPolar.put(thisWindAngle, thisBoatSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
polarTable.put(thisWindSpeed, thisPolar);
|
||||
upwindOptimal.put(thisWindSpeed, thisUpWindPolar);
|
||||
downwindOptimal.put(thisWindSpeed, thisDnWindPolar);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Parses the header line of a polar file
|
||||
* @param thisLine The line which is the header of a polar file
|
||||
*/
|
||||
private static void deduceHeaders(String[] thisLine) {
|
||||
|
||||
for (int i = 0; i < thisLine.length; i++) {
|
||||
String thisItem = thisLine[i];
|
||||
if (thisItem.toLowerCase().startsWith("uptwa")) {
|
||||
upTwaIndex = i;
|
||||
}
|
||||
else if (thisItem.toLowerCase().startsWith("dntwa")) {
|
||||
dnTwaIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return The entire polar table
|
||||
*/
|
||||
public static HashMap<Double, HashMap<Double, Double>> getPolarTable() {
|
||||
return polarTable;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return The polar table just containing the optimal upwind values
|
||||
*/
|
||||
public static HashMap<Double, HashMap<Double, Double>> getUpwindOptimal() {
|
||||
return upwindOptimal;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return The polar table just containing the optimal downwind values
|
||||
*/
|
||||
public static HashMap<Double, HashMap<Double, Double>> getDownwindOptimal() {
|
||||
return downwindOptimal;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Will raise an exception if a polar table has just one row of data
|
||||
* @param thisWindSpeed The current wind speed
|
||||
* @return HashMap containing just the optimal upwind angle and resulting boat speed
|
||||
*/
|
||||
public static HashMap<Double, Double> getOptimalUpwindVMG(Double thisWindSpeed) {
|
||||
|
||||
Double polarWindSpeed = getClosestMatch(thisWindSpeed);
|
||||
return upwindOptimal.get(polarWindSpeed);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Will raise an exception if a polar table has just one row of data
|
||||
* @param thisWindSpeed The current wind speed
|
||||
* @return HashMap containing just the optimal downwind angle and resulting boat speed
|
||||
*/
|
||||
public static HashMap<Double, Double> getOptimalDownwindVMG(Double thisWindSpeed) {
|
||||
|
||||
Double polarWindSpeed = getClosestMatch(thisWindSpeed);
|
||||
return downwindOptimal.get(polarWindSpeed);
|
||||
}
|
||||
|
||||
|
||||
private static Double getClosestMatch(Double thisWindSpeed) {
|
||||
|
||||
ArrayList<Double> windValues = new ArrayList<>(polarTable.keySet());
|
||||
|
||||
Double lowerVal = windValues.get(0);
|
||||
Double upperVal = windValues.get(1);
|
||||
|
||||
for(int i = 0; i < windValues.size() - 1; i++) {
|
||||
lowerVal = windValues.get(i);
|
||||
upperVal = windValues.get(i+1);
|
||||
if (thisWindSpeed <= upperVal) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Double lowerDiff = Math.abs(lowerVal - thisWindSpeed);
|
||||
Double upperDiff = Math.abs(upperVal - thisWindSpeed);
|
||||
|
||||
return (lowerDiff <= upperDiff) ? lowerVal : upperVal;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package seng302.model;
|
||||
|
||||
import javafx.scene.paint.Color;
|
||||
import seng302.model.mark.Mark;
|
||||
import seng302.visualiser.controllers.RaceViewController;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
// Used in boat group
|
||||
private Color colour;
|
||||
|
||||
private String boatType;
|
||||
private Integer sourceID;
|
||||
private String hullID; //matches HullNum in the XML spec.
|
||||
private String shortName;
|
||||
private String boatName;
|
||||
private String country;
|
||||
|
||||
// Situational data
|
||||
|
||||
|
||||
// Boat status
|
||||
private Integer boatStatus;
|
||||
private Integer legNumber;
|
||||
private Integer penaltiesAwarded;
|
||||
private Integer penaltiesServed;
|
||||
private Long estimateTimeAtFinish;
|
||||
private String position;
|
||||
private Double lat;
|
||||
private Double lon;
|
||||
private Float heading;
|
||||
private double velocity;
|
||||
private Long timeTillNext;
|
||||
private Long markRoundTime;
|
||||
|
||||
// Mark rounding
|
||||
private Mark lastMarkRounded;
|
||||
private Mark nextMark;
|
||||
|
||||
|
||||
/**
|
||||
* Used in EventTest and RaceTest.
|
||||
*
|
||||
* @param boatName Create a yacht object with name.
|
||||
*/
|
||||
public Yacht(String boatName) {
|
||||
this.boatName = boatName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in BoatGroupTest.
|
||||
*
|
||||
* @param boatName The name of the team sailing the boat
|
||||
* @param boatVelocity The speed of the boat in meters/second
|
||||
* @param shortName A shorter version of the teams name
|
||||
*/
|
||||
public Yacht(String boatName, double boatVelocity, String shortName, int id) {
|
||||
this.boatName = boatName;
|
||||
this.velocity = boatVelocity;
|
||||
this.shortName = shortName;
|
||||
this.sourceID = id;
|
||||
}
|
||||
|
||||
public Yacht(String boatType, Integer sourceID, String hullID, String shortName,
|
||||
String boatName, String country) {
|
||||
this.boatType = boatType;
|
||||
this.sourceID = sourceID;
|
||||
this.hullID = hullID;
|
||||
this.shortName = shortName;
|
||||
this.boatName = boatName;
|
||||
this.country = country;
|
||||
this.position = "-";
|
||||
}
|
||||
|
||||
public String getBoatType() {
|
||||
return boatType;
|
||||
}
|
||||
|
||||
public Integer getSourceID() {
|
||||
return sourceID;
|
||||
}
|
||||
|
||||
public String getHullID() {
|
||||
return hullID;
|
||||
}
|
||||
|
||||
public String getShortName() {
|
||||
return shortName;
|
||||
}
|
||||
|
||||
public String getBoatName() {
|
||||
return boatName;
|
||||
}
|
||||
|
||||
public String getCountry() {
|
||||
return country;
|
||||
}
|
||||
|
||||
public Integer getBoatStatus() {
|
||||
return boatStatus;
|
||||
}
|
||||
|
||||
public void setBoatStatus(Integer boatStatus) {
|
||||
this.boatStatus = boatStatus;
|
||||
}
|
||||
|
||||
public Integer getLegNumber() {
|
||||
return legNumber;
|
||||
}
|
||||
|
||||
public void setLegNumber(Integer legNumber) {
|
||||
if (colour != null && position != "-" && legNumber != this.legNumber&& RaceViewController.sparkLineStatus(sourceID)) {
|
||||
RaceViewController.updateYachtPositionSparkline(this, legNumber);
|
||||
}
|
||||
this.legNumber = legNumber;
|
||||
}
|
||||
|
||||
public Integer getPenaltiesAwarded() {
|
||||
return penaltiesAwarded;
|
||||
}
|
||||
|
||||
public void setPenaltiesAwarded(Integer penaltiesAwarded) {
|
||||
this.penaltiesAwarded = penaltiesAwarded;
|
||||
}
|
||||
|
||||
public Integer getPenaltiesServed() {
|
||||
return penaltiesServed;
|
||||
}
|
||||
|
||||
public void setPenaltiesServed(Integer penaltiesServed) {
|
||||
this.penaltiesServed = penaltiesServed;
|
||||
}
|
||||
|
||||
public void setEstimateTimeAtNextMark(Long estimateTimeAtNextMark) {
|
||||
timeTillNext = estimateTimeAtNextMark;
|
||||
}
|
||||
|
||||
public String getEstimateTimeAtFinish() {
|
||||
DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
|
||||
return format.format(estimateTimeAtFinish);
|
||||
}
|
||||
|
||||
public void setEstimateTimeAtFinish(Long estimateTimeAtFinish) {
|
||||
this.estimateTimeAtFinish = estimateTimeAtFinish;
|
||||
}
|
||||
|
||||
public String getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public void setPosition(String position) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
public Color getColour() {
|
||||
return colour;
|
||||
}
|
||||
|
||||
public void setColour(Color colour) {
|
||||
this.colour = colour;
|
||||
}
|
||||
|
||||
public void setVelocity(double velocity) {
|
||||
this.velocity = velocity;
|
||||
}
|
||||
|
||||
|
||||
public void setMarkRoundingTime(Long markRoundingTime) {
|
||||
this.markRoundTime = markRoundingTime;
|
||||
}
|
||||
|
||||
public double getVelocity() {
|
||||
return velocity;
|
||||
}
|
||||
|
||||
public Long getTimeTillNext() {
|
||||
return timeTillNext;
|
||||
}
|
||||
|
||||
public Long getMarkRoundTime() {
|
||||
return markRoundTime;
|
||||
}
|
||||
|
||||
public Mark getLastMarkRounded() {
|
||||
return lastMarkRounded;
|
||||
}
|
||||
|
||||
public void setLastMarkRounded(Mark lastMarkRounded) {
|
||||
this.lastMarkRounded = lastMarkRounded;
|
||||
}
|
||||
|
||||
public void setNextMark(Mark nextMark) {
|
||||
this.nextMark = nextMark;
|
||||
}
|
||||
|
||||
public Mark getNextMark(){
|
||||
return nextMark;
|
||||
}
|
||||
|
||||
public Double getLat() {
|
||||
return lat;
|
||||
}
|
||||
|
||||
public void setLat(Double lat) {
|
||||
this.lat = lat;
|
||||
}
|
||||
|
||||
public Double getLon() {
|
||||
return lon;
|
||||
}
|
||||
|
||||
public void setLon(Double lon) {
|
||||
this.lon = lon;
|
||||
}
|
||||
|
||||
public Float getHeading() {
|
||||
return heading;
|
||||
}
|
||||
|
||||
public void setHeading(Float heading) {
|
||||
this.heading = heading;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return boatName;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package seng302.model.map;
|
||||
|
||||
/**
|
||||
* The Boundary class represents a rectangle territorial boundary on a map. It
|
||||
* contains four extremity double values(N, E, S, W). N and S are represented as
|
||||
* latitudes in radians. E and W are represented as longitudes in radians.
|
||||
*
|
||||
* Created by Haoming on 10/5/17
|
||||
*/
|
||||
public class Boundary {
|
||||
|
||||
private double northLat, eastLng, southLat, westLng;
|
||||
|
||||
public Boundary(double northLat, double eastLng, double southLat, double westLng) {
|
||||
this.northLat = northLat;
|
||||
this.eastLng = eastLng;
|
||||
this.southLat = southLat;
|
||||
this.westLng = westLng;
|
||||
}
|
||||
|
||||
double getCentreLat() {
|
||||
return (northLat + southLat) / 2;
|
||||
}
|
||||
|
||||
double getCentreLng() {
|
||||
return (eastLng + westLng) / 2;
|
||||
}
|
||||
|
||||
double getNorthLat() {
|
||||
return northLat;
|
||||
}
|
||||
|
||||
double getEastLng() {
|
||||
return eastLng;
|
||||
}
|
||||
|
||||
double getSouthLat() {
|
||||
return southLat;
|
||||
}
|
||||
|
||||
double getWestLng() {
|
||||
return westLng;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package seng302.model.map;
|
||||
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.image.Image;
|
||||
import seng302.utilities.GeoPoint;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
import java.lang.Math;
|
||||
|
||||
/**
|
||||
* CanvasMap retrieves a map image with given geo boundary from Google Map server.
|
||||
* By passing a rectangle like geo boundary, it returns a map image with the
|
||||
* highest resolution. However, due to free quote account usage limit, the maximum
|
||||
* resolution is only 1280 * 1280.
|
||||
*
|
||||
* Created by Haoming on 15/5/2017
|
||||
*/
|
||||
public class CanvasMap {
|
||||
|
||||
private Boundary boundary;
|
||||
private long width, height; // desired image size
|
||||
private int zoom;
|
||||
|
||||
private String KEY = "AIzaSyC-5oOShMCY5Oy_9L7guYMPUPFHDMr37wE";
|
||||
|
||||
public CanvasMap(Boundary boundary) {
|
||||
this.boundary = boundary;
|
||||
calculateOptimalMapSize();
|
||||
}
|
||||
|
||||
public Image getMapImage() {
|
||||
try {
|
||||
URL url = new URL(getRequest());
|
||||
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
||||
|
||||
return new Image(connection.getInputStream());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getRequest() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("https://maps.googleapis.com/maps/api/staticmap?");
|
||||
sb.append(String.format("center=%f,%f", boundary.getCentreLat(), boundary.getCentreLng()));
|
||||
sb.append(String.format("&zoom=%d", zoom));
|
||||
sb.append(String.format("&size=%dx%d&scale=2", width, height));
|
||||
sb.append("&style=feature:all|element:labels|visibility:off"); // hide all labels on map
|
||||
// sb.append(String.format("&markers=%f,%f", boundary.getSouthLat(), boundary.getWestLng()));
|
||||
// sb.append(String.format("&key=%s", KEY));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void calculateOptimalMapSize() {
|
||||
for (int z = 20; z > 0; z--) {
|
||||
MapSize mapSize = getMapSize(z, boundary);
|
||||
zoom = z;
|
||||
width = mapSize.width;
|
||||
height = mapSize.height;
|
||||
// if map size is valid, exit the loop as we have the highest resolution
|
||||
if (mapSize.isValid()) break;
|
||||
}
|
||||
}
|
||||
|
||||
private MapSize getMapSize(int zoom, Boundary boundary) {
|
||||
double scale = Math.pow(2, zoom);
|
||||
GeoPoint geoSW = new GeoPoint(boundary.getSouthLat(), boundary.getWestLng());
|
||||
GeoPoint geoNE = new GeoPoint(boundary.getNorthLat(), boundary.getEastLng());
|
||||
Point2D pointSW = MercatorProjection.toMapPoint(geoSW);
|
||||
Point2D pointNE = MercatorProjection.toMapPoint(geoNE);
|
||||
return new MapSize(Math.abs(pointNE.getX() - pointSW.getX()) * scale,
|
||||
Math.abs(pointNE.getY() - pointSW.getY()) * scale);
|
||||
}
|
||||
|
||||
class MapSize {
|
||||
long width, height;
|
||||
|
||||
MapSize(double width, double height) {
|
||||
this.width = Math.round(width);
|
||||
this.height = Math.round(height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map size is valid when width and height are both less than 640 pixels
|
||||
* @return true if both dimensions are less than 640px
|
||||
*/
|
||||
boolean isValid() {
|
||||
return Math.max(width, height) <= 640;
|
||||
}
|
||||
}
|
||||
|
||||
public long getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public long getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public int getZoom() {
|
||||
return zoom;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package seng302.model.map;
|
||||
|
||||
import javafx.geometry.Point2D;
|
||||
import seng302.utilities.GeoPoint;
|
||||
|
||||
/**
|
||||
* An utility class useful to convert between Geo locations and Mercator projection
|
||||
* planar coordinates.
|
||||
* Created by Haoming on 15/5/2017
|
||||
*/
|
||||
public class MercatorProjection {
|
||||
|
||||
private static final double MERCATOR_RANGE = 256;
|
||||
private static final double pixelsPerLngDegree = MERCATOR_RANGE / 360.0;
|
||||
private static final double pixelsPerLngRadian = MERCATOR_RANGE / (2 * Math.PI);
|
||||
|
||||
/**
|
||||
* A help function keeps the value in bound between -0.9999 and 0.9999.
|
||||
* @param value in bound value
|
||||
* @return the value in bound
|
||||
*/
|
||||
private static double bound(double value) {
|
||||
return Math.min(Math.max(value, -0.9999), 0.9999);
|
||||
}
|
||||
|
||||
/**
|
||||
* Projects a Geo Location (lat, lng) on a planar
|
||||
* @param geo GeoPoint (lat, lng) location to be projected
|
||||
* @return the projection Point2D (x, y) on planar
|
||||
*/
|
||||
public static Point2D toMapPoint(GeoPoint geo) {
|
||||
double x, y;
|
||||
Point2D origin = new Point2D(MERCATOR_RANGE / 2.0, MERCATOR_RANGE / 2.0);
|
||||
x = (origin.getX() + geo.getLng() * pixelsPerLngDegree);
|
||||
|
||||
// NOTE(appleton): Truncating to 0.9999 effectively limits latitude to
|
||||
// 89.189. This is about a third of a tile past the edge of the world tile.
|
||||
double sinY = bound(Math.sin(Math.toRadians(geo.getLat())));
|
||||
y = origin.getY() + 0.5 * Math.log((1 + sinY) / (1 - sinY)) * (-pixelsPerLngRadian);
|
||||
return new Point2D(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the planar projection (x, y) back to Geo Location (lat, lng)
|
||||
* @param point Point2D (x, y) to be converted back
|
||||
* @return the original Geo location converted from the given projection point
|
||||
*/
|
||||
public static GeoPoint toMapGeo(Point2D point) {
|
||||
Point2D origin = new Point2D(MERCATOR_RANGE / 2.0, MERCATOR_RANGE / 2.0);
|
||||
double lng = (point.getX() - origin.getX()) / pixelsPerLngDegree;
|
||||
double latRadians = (point.getY() - origin.getY()) / (-pixelsPerLngRadian);
|
||||
double lat = Math.toDegrees(2 * Math.atan(Math.exp(latRadians)) - Math.PI / 2.0);
|
||||
return new GeoPoint(lat, lng);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package seng302.model.map;
|
||||
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
public class TestMapController implements Initializable{
|
||||
|
||||
@FXML
|
||||
private Canvas mapCanvas;
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
GraphicsContext gc = mapCanvas.getGraphicsContext2D();
|
||||
Boundary bound = new Boundary(57.662943, 11.848501, 57.673945, 11.824966);
|
||||
CanvasMap canvasMap = new CanvasMap(bound);
|
||||
gc.drawImage(canvasMap.getMapImage(), 0, 0, canvasMap.getWidth(), canvasMap.getHeight());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package seng302.model.mark;
|
||||
|
||||
/**
|
||||
* To represent a gate mark which contains two single marks.
|
||||
* Created by ptg19 on 16/03/17.
|
||||
* Modified by Haoming Yin (hyi25) on 17/3/2017.
|
||||
*/
|
||||
public class GateMark extends Mark {
|
||||
|
||||
private SingleMark singleMark1;
|
||||
private SingleMark singleMark2;
|
||||
|
||||
/**
|
||||
* Create an instance of Gate Mark which contains two single mark
|
||||
* @param name the name of the gate mark
|
||||
* @param singleMark1 one single mark inside of the gate mark
|
||||
* @param singleMark2 the second mark inside of the gate mark
|
||||
*/
|
||||
public GateMark(String name, MarkType type, SingleMark singleMark1, SingleMark singleMark2, double latitude, double longitude, int compoundMarkID) {
|
||||
super(name, type, latitude, longitude, compoundMarkID);
|
||||
this.singleMark1 = singleMark1;
|
||||
this.singleMark2 = singleMark2;
|
||||
}
|
||||
|
||||
public SingleMark getSingleMark1() {
|
||||
return singleMark1;
|
||||
}
|
||||
|
||||
public void setSingleMark1(SingleMark singleMark1) {
|
||||
this.singleMark1 = singleMark1;
|
||||
}
|
||||
|
||||
public SingleMark getSingleMark2() {
|
||||
return singleMark2;
|
||||
}
|
||||
|
||||
public void setSingleMark2(SingleMark singleMark2) {
|
||||
this.singleMark2 = singleMark2;
|
||||
}
|
||||
|
||||
public double getLatitude(){
|
||||
return (this.getSingleMark1().getLatitude());
|
||||
}
|
||||
|
||||
public double getLongitude(){
|
||||
return (this.getSingleMark1().getLongitude());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package seng302.model.mark;
|
||||
|
||||
/**
|
||||
* An abstract class to represent general marks
|
||||
* Created by Haoming Yin (hyi25) on 17/3/17.
|
||||
*/
|
||||
public abstract class Mark {
|
||||
|
||||
private String name;
|
||||
private MarkType markType;
|
||||
private double latitude;
|
||||
private double longitude;
|
||||
private long id;
|
||||
private int compoundMarkID;
|
||||
|
||||
/**
|
||||
* Create a mark instance by passing its name and type
|
||||
* @param name the name of the mark
|
||||
* @param markType the type of mark. either GATE_MARK or SINGLE_MARK.
|
||||
*/
|
||||
public Mark (String name, MarkType markType, int sourceID, int compoundMarkID) {
|
||||
this.name = name;
|
||||
this.markType = markType;
|
||||
this.id = sourceID;
|
||||
this.compoundMarkID = compoundMarkID;
|
||||
}
|
||||
|
||||
public Mark(String name, MarkType markType, double latitude, double longitude, int compoundMarkID) {
|
||||
this.name = name;
|
||||
this.markType = markType;
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
this.id = 0;
|
||||
this.compoundMarkID = compoundMarkID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculated the heading in radians from first Mark to the second Mark.
|
||||
*
|
||||
* @param pointOne First Mark
|
||||
* @param pointTwo Second Mark
|
||||
* @return Heading in radians
|
||||
*/
|
||||
public static Double calculateHeadingRad(Mark pointOne, Mark pointTwo) {
|
||||
Double longitude1 = pointOne.getLongitude();
|
||||
Double longitude2 = pointTwo.getLongitude();
|
||||
Double latitude1 = pointOne.getLatitude();
|
||||
Double latitude2 = pointTwo.getLatitude();
|
||||
return calculateHeadingRad(latitude1, longitude1, latitude2, longitude2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the heading in radians from geographical location with latitude1, longitude 1 to
|
||||
* geographical latitude2, longitude 2
|
||||
*
|
||||
* @param longitude1 Longitude of first point in degrees
|
||||
* @param longitude2 Longitude of second point in degrees
|
||||
* @param latitude1 Latitude of first point in degrees
|
||||
* @param latitude2 Latitude of first point in degrees
|
||||
* @return Heading in radians
|
||||
*/
|
||||
public static double calculateHeadingRad(Double latitude1, Double longitude1, Double latitude2,
|
||||
Double longitude2) {
|
||||
latitude1 = Math.toRadians(latitude1);
|
||||
latitude2 = Math.toRadians(latitude2);
|
||||
Double longDiff = Math.toRadians(longitude2 - longitude1);
|
||||
Double y = Math.sin(longDiff) * Math.cos(latitude2);
|
||||
Double x =
|
||||
Math.cos(latitude1) * Math.sin(latitude2) - Math.sin(latitude1) * Math.cos(latitude2)
|
||||
* Math.cos(longDiff);
|
||||
return Math.atan2(y, x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the distance in meters from the first Mark to a second Mark
|
||||
*
|
||||
* @param pointOne First Mark
|
||||
* @param pointTwo Second Mark
|
||||
* @return Distance in meters
|
||||
*/
|
||||
public static Double calculateDistance(Mark pointOne, Mark pointTwo) {
|
||||
Double longitude1 = pointOne.getLongitude();
|
||||
Double longitude2 = pointTwo.getLongitude();
|
||||
Double latitude1 = pointOne.getLatitude();
|
||||
Double latitude2 = pointTwo.getLatitude();
|
||||
return calculateDistance(latitude1, longitude1, latitude2, longitude2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the distance in meters from geographical location with latitude1, longitude 1 to
|
||||
* geographical latitude2, longitude 2
|
||||
*
|
||||
* @param longitude1 Longitude of first point in degrees
|
||||
* @param longitude2 Longitude of second point in degrees
|
||||
* @param latitude1 Latitude of first point in degrees
|
||||
* @param latitude2 Latitude of first point in degrees
|
||||
* @return Distance in meters
|
||||
*/
|
||||
public static Double calculateDistance(Double latitude1, Double longitude1, Double latitude2,
|
||||
Double longitude2) {
|
||||
Double theta = longitude1 - longitude2;
|
||||
Double dist = Math.sin(Math.toRadians(latitude1)) * Math.sin(Math.toRadians(latitude2)) +
|
||||
Math.cos(Math.toRadians(latitude1)) * Math.cos(Math.toRadians(latitude2)) *
|
||||
Math.cos(Math.toRadians(theta));
|
||||
dist = Math.acos(dist);
|
||||
dist = Math.toDegrees(dist);
|
||||
dist = dist * 60
|
||||
* 1.1508; //nautical mile (distance between two degrees) * (degrees in a minute)
|
||||
dist = dist * 1609.344; //ratio of miles to metres
|
||||
return dist;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public MarkType getMarkType() {
|
||||
return markType;
|
||||
}
|
||||
|
||||
public void setMarkType(MarkType markType) {
|
||||
this.markType = markType;
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
|
||||
public double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getCompoundMarkID() {
|
||||
return compoundMarkID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package seng302.model.mark;
|
||||
|
||||
/**
|
||||
* To represent two types of mark
|
||||
* Created by Haoming Yin (hyi25) on 17/3/17.
|
||||
*/
|
||||
public enum MarkType {
|
||||
SINGLE_MARK, OPEN_GATE
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package seng302.model.mark;
|
||||
|
||||
/**
|
||||
* Represents the marker as a single mark
|
||||
*
|
||||
* Created by Haoming Yin (hyi25) on 17/3/2017
|
||||
*/
|
||||
public class SingleMark extends Mark {
|
||||
private double lat;
|
||||
private double lon;
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Represents a marker
|
||||
*
|
||||
* @param name, the name of the marker*
|
||||
* @param lat, the latitude of the marker
|
||||
* @param lon, the longitude of the marker
|
||||
*/
|
||||
public SingleMark(String name, double lat, double lon, int sourceID, int compoundMarkID) {
|
||||
super(name, MarkType.SINGLE_MARK, sourceID, compoundMarkID);
|
||||
this.lat = lat;
|
||||
this.lon = lon;
|
||||
}
|
||||
|
||||
|
||||
public double getLatitude() {
|
||||
return this.lat;
|
||||
}
|
||||
|
||||
public double getLongitude() {
|
||||
return this.lon;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package seng302.model.stream;
|
||||
|
||||
import seng302.model.stream.packets.StreamPacket;
|
||||
|
||||
public interface PacketBufferDelegate {
|
||||
boolean addToBuffer(StreamPacket streamPacket);
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
package seng302.model.stream;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentSkipListMap;
|
||||
import java.util.concurrent.PriorityBlockingQueue;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import org.w3c.dom.Document;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
import seng302.model.Yacht;
|
||||
import seng302.model.mark.Mark;
|
||||
import seng302.model.stream.packets.BoatPositionPacket;
|
||||
import seng302.model.stream.packets.StreamPacket;
|
||||
|
||||
/**
|
||||
* The purpose of this class is to take in the stream of divided packets so they can be read
|
||||
* and parsed in by turning the byte arrays into useful data. There are two public static hashmaps
|
||||
* that are threadsafe so the visualiser can always access the latest speed and position available
|
||||
* Created by kre39 on 23/04/17.
|
||||
*/
|
||||
public class StreamParser{
|
||||
|
||||
public static ConcurrentHashMap<Long, PriorityBlockingQueue<BoatPositionPacket>> markLocations = new ConcurrentHashMap<>();
|
||||
public static ConcurrentHashMap<Long, PriorityBlockingQueue<BoatPositionPacket>> boatLocations = new ConcurrentHashMap<>();
|
||||
private static boolean newRaceXmlReceived = false;
|
||||
private static boolean raceStarted = false;
|
||||
private static XMLParser xmlObject;
|
||||
private static boolean raceFinished = false;
|
||||
private static boolean streamStatus = false;
|
||||
private static long timeSinceStart = -1;
|
||||
private static Map<Integer, Yacht> boats = new ConcurrentHashMap<>();
|
||||
private static Map<Integer, Yacht> boatsPos = new ConcurrentSkipListMap<>();
|
||||
private static double windDirection = 0;
|
||||
private static Double windSpeed = 0d;
|
||||
private static Long currentTimeLong;
|
||||
private static String currentTimeString;
|
||||
|
||||
|
||||
//CONVERSION CONSTANTS
|
||||
private static final Double MS_TO_KNOTS = 1.94384;
|
||||
|
||||
/**
|
||||
* Used to initialise the thread name and stream parser object so a thread can be executed
|
||||
*/
|
||||
public StreamParser() {
|
||||
}
|
||||
/**
|
||||
* Looks at the type of the packet then sends it to the appropriate parser to extract the
|
||||
* specific data associated with that packet type
|
||||
*
|
||||
* @param packet the packet to be looked at and processed
|
||||
*/
|
||||
public static void parsePacket(StreamPacket packet) {
|
||||
try {
|
||||
switch (packet.getType()) {
|
||||
case HEARTBEAT:
|
||||
extractHeartBeat(packet);
|
||||
break;
|
||||
case RACE_STATUS:
|
||||
extractRaceStatus(packet);
|
||||
break;
|
||||
case DISPLAY_TEXT_MESSAGE:
|
||||
extractDisplayMessage(packet);
|
||||
break;
|
||||
case XML_MESSAGE:
|
||||
newRaceXmlReceived = true;
|
||||
extractXmlMessage(packet);
|
||||
break;
|
||||
case RACE_START_STATUS:
|
||||
extractRaceStartStatus(packet);
|
||||
break;
|
||||
case YACHT_EVENT_CODE:
|
||||
extractYachtEventCode(packet);
|
||||
break;
|
||||
case YACHT_ACTION_CODE:
|
||||
extractYachtActionCode(packet);
|
||||
break;
|
||||
case CHATTER_TEXT:
|
||||
extractChatterText(packet);
|
||||
break;
|
||||
case BOAT_LOCATION:
|
||||
extractBoatLocation(packet);
|
||||
break;
|
||||
case MARK_ROUNDING:
|
||||
extractMarkRounding(packet);
|
||||
break;
|
||||
case COURSE_WIND:
|
||||
extractCourseWind(packet);
|
||||
break;
|
||||
case AVG_WIND:
|
||||
extractAvgWind(packet);
|
||||
break;
|
||||
case BOAT_ACTION:
|
||||
extractBoatAction(packet);
|
||||
break;
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
System.out.println("Error parsing packet");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the seq num used in the heartbeat packet
|
||||
*
|
||||
* @param packet Packet parsed in to use the payload
|
||||
*/
|
||||
private static void extractHeartBeat(StreamPacket packet) {
|
||||
long heartbeat = bytesToLong(packet.getPayload());
|
||||
}
|
||||
|
||||
private static String getTimeZoneString() {
|
||||
|
||||
Integer offset = xmlObject.getRegattaXML().getUtcOffset();
|
||||
StringBuilder utcOffset = new StringBuilder();
|
||||
utcOffset.append("GMT");
|
||||
if (offset > 0) {
|
||||
utcOffset.append("+");
|
||||
utcOffset.append(offset);
|
||||
} else if (offset < 0) {
|
||||
utcOffset.append("-");
|
||||
utcOffset.append(offset);
|
||||
}
|
||||
return utcOffset.toString();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the useful race status data from race status type packets. This method will also
|
||||
* print to the console the current state of the race (if it has started/finished or is about to
|
||||
* start), along side this it'll also display the amount of time since the race has started or
|
||||
* time till it starts
|
||||
*
|
||||
* @param packet Packet parsed in to use the payload
|
||||
*/
|
||||
private static void extractRaceStatus(StreamPacket packet) {
|
||||
byte[] payload = packet.getPayload();
|
||||
int messageVersionNo = payload[0];
|
||||
long currentTime = bytesToLong(Arrays.copyOfRange(payload, 1, 7));
|
||||
long raceId = bytesToLong(Arrays.copyOfRange(payload, 7, 11));
|
||||
int raceStatus = payload[11];
|
||||
long expectedStartTime = bytesToLong(Arrays.copyOfRange(payload,12,18));
|
||||
long windDir = bytesToLong(Arrays.copyOfRange(payload,18,20));
|
||||
long rawWindSpeed = bytesToLong(Arrays.copyOfRange(payload,20,22));
|
||||
|
||||
currentTimeLong = currentTime;
|
||||
DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
|
||||
if (xmlObject.getRegattaXML() != null) {
|
||||
format.setTimeZone(TimeZone.getTimeZone(getTimeZoneString()));
|
||||
currentTimeString = format.format((new Date(currentTime)).getTime());
|
||||
}
|
||||
long timeTillStart =
|
||||
((new Date(expectedStartTime)).getTime() - (new Date(currentTime)).getTime()) / 1000;
|
||||
|
||||
if (timeTillStart > 0) {
|
||||
timeSinceStart = timeTillStart;
|
||||
} else {
|
||||
if (raceStatus == 4 || raceStatus == 8) {
|
||||
raceFinished = true;
|
||||
raceStarted = false;
|
||||
} else if (!raceStarted) {
|
||||
raceStarted = true;
|
||||
raceFinished = false;
|
||||
}
|
||||
timeSinceStart = timeTillStart;
|
||||
}
|
||||
|
||||
double windDirFactor = 0x4000 / 90; //0x4000 is 90 degrees, 0x8000 is 180 degrees, etc...
|
||||
windDirection = windDir / windDirFactor;
|
||||
windSpeed = rawWindSpeed / 1000 * MS_TO_KNOTS;
|
||||
|
||||
int noBoats = payload[22];
|
||||
int raceType = payload[23];
|
||||
for (int i = 0; i < noBoats; i++) {
|
||||
long boatStatusSourceID = bytesToLong(
|
||||
Arrays.copyOfRange(payload, 24 + (i * 20), 28 + (i * 20)));
|
||||
Yacht boat = boats.get((int) boatStatusSourceID);
|
||||
boat.setBoatStatus((int) payload[28 + (i * 20)]);
|
||||
setBoatLegPosition(boat, (int) payload[29 + (i * 20)]);
|
||||
boat.setPenaltiesAwarded((int) payload[30 + (i * 20)]);
|
||||
boat.setPenaltiesServed((int) payload[31 + (i * 20)]);
|
||||
Long estTimeAtNextMark = bytesToLong(
|
||||
Arrays.copyOfRange(payload, 32 + (i * 20), 38 + (i * 20)));
|
||||
boat.setEstimateTimeAtNextMark(estTimeAtNextMark);
|
||||
Long estTimeAtFinish = bytesToLong(
|
||||
Arrays.copyOfRange(payload, 38 + (i * 20), 44 + (i * 20)));
|
||||
boat.setEstimateTimeAtFinish(estTimeAtFinish);
|
||||
// boatsPos.put(estTimeAtFinish, boat);
|
||||
// String boatStatus = "SourceID: " + boatStatusSourceID;
|
||||
// boatStatus += "\nBoat Status: " + (int)payload[28 + (i * 20)];
|
||||
// boatStatus += "\nLegNumber: " + (int)payload[29 + (i * 20)];
|
||||
// boatStatus += "\nPenaltiesAwarded: " + (int)payload[29 + (i * 20)];
|
||||
// boatStatus += "\nPenaltiesServed: " + (int)payload[30 + (i * 20)];
|
||||
// boatStatus += "\nEstTimeAtNextMark: " + bytesToLong(Arrays.copyOfRange(payload,31 + (i * 20),37+ (i * 20)));
|
||||
// boatStatus += "\nEstTimeAtFinish: " + bytesToLong(Arrays.copyOfRange(payload,37 + (i * 20),43+ (i * 20)));
|
||||
// boatStatuses.add(boatStatus);
|
||||
}
|
||||
// if (isRaceStarted()) {
|
||||
// int pos = 1;
|
||||
// for (Yacht yacht : boatsPos.values()) {
|
||||
// yacht.setPosition(String.valueOf(pos));
|
||||
// pos++;
|
||||
// }
|
||||
// } else {
|
||||
// for (Yacht yacht : boatsPos.values()) {
|
||||
// yacht.setPosition("-");
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private static void setBoatLegPosition(Yacht updatingBoat, Integer leg){
|
||||
Integer placing = 1;
|
||||
if (leg != updatingBoat.getLegNumber() && (raceStarted || raceFinished)) {
|
||||
for (Yacht boat : boats.values()) {
|
||||
if (boat.getLegNumber() != null && leg <= boat.getLegNumber()){
|
||||
placing += 1;
|
||||
}
|
||||
}
|
||||
updatingBoat.setPosition(placing.toString());
|
||||
updatingBoat.setLegNumber(leg);
|
||||
boatsPos.putIfAbsent(placing, updatingBoat);
|
||||
boatsPos.replace(placing, updatingBoat);
|
||||
} else if(updatingBoat.getLegNumber() == null){
|
||||
updatingBoat.setPosition("1");
|
||||
updatingBoat.setLegNumber(leg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to extract the messages passed through with the display message packet
|
||||
*
|
||||
* @param packet Packet parsed in to use the payload
|
||||
*/
|
||||
private static void extractDisplayMessage(StreamPacket packet) {
|
||||
byte[] payload = packet.getPayload();
|
||||
int messageVersionNo = payload[0];
|
||||
int numOfLines = payload[3];
|
||||
int totalLen = 0;
|
||||
for (int i = 0; i < numOfLines; i++) {
|
||||
int lineNum = payload[4 + totalLen];
|
||||
int textLength = payload[5 + totalLen];
|
||||
byte[] messageTextBytes = Arrays
|
||||
.copyOfRange(payload, 6 + totalLen, 6 + textLength + totalLen);
|
||||
String messageText = new String(messageTextBytes);
|
||||
totalLen += 2 + textLength;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to read in the xml data. Will call the specific methods to create the course and boats
|
||||
*
|
||||
* @param packet Packet parsed in to use the payload
|
||||
*/
|
||||
private static void extractXmlMessage(StreamPacket packet) {
|
||||
|
||||
byte[] payload = packet.getPayload();
|
||||
|
||||
int messageType = payload[9];
|
||||
long messageLength = bytesToLong(Arrays.copyOfRange(payload, 12, 14));
|
||||
String xmlMessage = new String(
|
||||
(Arrays.copyOfRange(payload, 14, (int) (14 + messageLength)))).trim();
|
||||
|
||||
//Create XML document Object
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder db = null;
|
||||
Document doc = null;
|
||||
try {
|
||||
db = dbf.newDocumentBuilder();
|
||||
doc = db.parse(new InputSource(new StringReader(xmlMessage)));
|
||||
} catch (ParserConfigurationException | IOException | SAXException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
xmlObject.constructXML(doc, messageType);
|
||||
if (messageType == 7) { //7 is the boat XML
|
||||
boats = xmlObject.getBoatXML().getCompetingBoats();
|
||||
}
|
||||
if (messageType == 6) { //6 is race info xml
|
||||
|
||||
newRaceXmlReceived = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the race start status from the packet, currently is unused within the app but
|
||||
* is here for potential future use
|
||||
*
|
||||
* @param packet Packet parsed in to use the payload
|
||||
*/
|
||||
private static void extractRaceStartStatus(StreamPacket packet) {
|
||||
byte[] payload = packet.getPayload();
|
||||
int messageVersionNo = payload[0];
|
||||
long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7));
|
||||
long raceStartTime = bytesToLong(Arrays.copyOfRange(payload, 9, 15));
|
||||
long raceId = bytesToLong(Arrays.copyOfRange(payload, 15, 19));
|
||||
int notificationType = payload[19];
|
||||
}
|
||||
|
||||
/**
|
||||
* When a yacht event occurs this will parse the byte array to retrieve the necessary info,
|
||||
* currently unused
|
||||
*
|
||||
* @param packet Packet parsed in to use the payload
|
||||
*/
|
||||
private static void extractYachtEventCode(StreamPacket packet) {
|
||||
byte[] payload = packet.getPayload();
|
||||
int messageVersionNo = payload[0];
|
||||
long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7));
|
||||
long raceId = bytesToLong(Arrays.copyOfRange(payload, 9, 13));
|
||||
long subjectId = bytesToLong(Arrays.copyOfRange(payload, 13, 17));
|
||||
long incidentId = bytesToLong(Arrays.copyOfRange(payload, 17, 21));
|
||||
int eventId = payload[21];
|
||||
}
|
||||
|
||||
/**
|
||||
* When a yacht action occurs this will parse the parse the byte array to retrieve the necessary
|
||||
* info, currently unused
|
||||
*
|
||||
* @param packet Packet parsed in to use the payload
|
||||
*/
|
||||
private static void extractYachtActionCode(StreamPacket packet) {
|
||||
byte[] payload = packet.getPayload();
|
||||
int messageVersionNo = payload[0];
|
||||
long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7));
|
||||
long subjectId = bytesToLong(Arrays.copyOfRange(payload, 9, 13));
|
||||
long incidentId = bytesToLong(Arrays.copyOfRange(payload, 13, 17));
|
||||
int eventId = payload[17];
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips the message from the chatter text type packets, currently the message is unused
|
||||
*
|
||||
* @param packet Packet parsed in to use the payload
|
||||
*/
|
||||
private static void extractChatterText(StreamPacket packet) {
|
||||
byte[] payload = packet.getPayload();
|
||||
int messageVersionNo = payload[0];
|
||||
int messageType = payload[1];
|
||||
int length = payload[2];
|
||||
String message = new String(Arrays.copyOfRange(payload, 3, 3 + length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to breakdown the boatlocation packets so the boat coordinates, id and groundspeed are
|
||||
* all used All the other extra data is still being read and translated however is unused.
|
||||
*
|
||||
* @param packet Packet parsed in to use the payload
|
||||
*/
|
||||
private static void extractBoatLocation(StreamPacket packet) {
|
||||
byte[] payload = packet.getPayload();
|
||||
|
||||
int deviceType = (int) payload[15];
|
||||
long timeValid = bytesToLong(Arrays.copyOfRange(payload, 1, 7));
|
||||
long seq = bytesToLong(Arrays.copyOfRange(payload, 11, 15));
|
||||
long boatId = bytesToLong(Arrays.copyOfRange(payload, 7, 11));
|
||||
long rawLat = bytesToLong(Arrays.copyOfRange(payload, 16, 20));
|
||||
long rawLon = bytesToLong(Arrays.copyOfRange(payload, 20, 24));
|
||||
//Converts the double to a usable lat/lon
|
||||
double lat = ((180d * (double) rawLat) / Math.pow(2, 31));
|
||||
double lon = ((180d * (double) rawLon) / Math.pow(2, 31));
|
||||
long heading = bytesToLong(Arrays.copyOfRange(payload, 28, 30));
|
||||
double groundSpeed = bytesToLong(Arrays.copyOfRange(payload, 38, 40)) / 1000.0;
|
||||
|
||||
//type 1 is a racing yacht and type 3 is a mark, needed for updating positions of the mark and boat
|
||||
if (deviceType == 1){
|
||||
Yacht boat = boats.get((int) boatId);
|
||||
boat.setVelocity(groundSpeed);
|
||||
BoatPositionPacket boatPacket = new BoatPositionPacket(boatId, timeValid, lat, lon, heading, groundSpeed);
|
||||
|
||||
//add a new priority que to the boatLocations HashMap
|
||||
if (!boatLocations.containsKey(boatId)) {
|
||||
boatLocations.put(boatId,
|
||||
new PriorityBlockingQueue<>(256, new Comparator<BoatPositionPacket>() {
|
||||
@Override
|
||||
public int compare(BoatPositionPacket p1, BoatPositionPacket p2) {
|
||||
return (int) (p1.getTimeValid() - p2.getTimeValid());
|
||||
}
|
||||
}));
|
||||
}
|
||||
boatLocations.get(boatId).put(boatPacket);
|
||||
} else if (deviceType == 3) {
|
||||
BoatPositionPacket markPacket = new BoatPositionPacket(boatId, timeValid, lat, lon,
|
||||
heading, groundSpeed);
|
||||
|
||||
//add a new priority que to the boatLocations HashMap
|
||||
if (!markLocations.containsKey(boatId)) {
|
||||
markLocations.put(boatId,
|
||||
new PriorityBlockingQueue<>(256, new Comparator<BoatPositionPacket>() {
|
||||
@Override
|
||||
public int compare(BoatPositionPacket p1, BoatPositionPacket p2) {
|
||||
return (int) (p1.getTimeValid() - p2.getTimeValid());
|
||||
}
|
||||
}));
|
||||
}
|
||||
markLocations.get(boatId).put(markPacket);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This packet type is received when a mark or gate is rounded by a boat
|
||||
*
|
||||
* @param packet The packet containing the payload
|
||||
*/
|
||||
private static void extractMarkRounding(StreamPacket packet) {
|
||||
byte[] payload = packet.getPayload();
|
||||
int messageVersionNo = payload[0];
|
||||
long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7));
|
||||
long raceId = bytesToLong(Arrays.copyOfRange(payload, 9, 13));
|
||||
long subjectId = bytesToLong(Arrays.copyOfRange(payload, 13, 17));
|
||||
int boatStatus = payload[17];
|
||||
int roundingSide = payload[18];
|
||||
int markType = payload[19];
|
||||
int markId = payload[20];
|
||||
|
||||
// assign mark rounding time to boat
|
||||
boats.get((int)subjectId).setMarkRoundingTime(timeStamp);
|
||||
|
||||
for (Mark mark : xmlObject.getRaceXML().getAllCompoundMarks()) {
|
||||
if (mark.getCompoundMarkID() == markId) {
|
||||
boats.get((int)subjectId).setLastMarkRounded(mark);
|
||||
}
|
||||
}
|
||||
|
||||
Long[] array = new Long[]{subjectId, timeStamp, (long) markId};
|
||||
}
|
||||
|
||||
/**
|
||||
* This packet type contains periodic data on the state of the wind
|
||||
*
|
||||
* @param packet The packet containing the payload
|
||||
*/
|
||||
private static void extractCourseWind(StreamPacket packet) {
|
||||
byte[] payload = packet.getPayload();
|
||||
int messageVersionNo = payload[0];
|
||||
int selectedWindId = payload[1];
|
||||
int loopCount = payload[2];
|
||||
List<String> windInfo = new ArrayList<>();
|
||||
for (int i = 0; i < loopCount; i++) {
|
||||
String wind = "WindId: " + payload[3 + (20 * i)];
|
||||
wind +=
|
||||
"\nTime: " + bytesToLong(Arrays.copyOfRange(payload, 4 + (20 * i), 10 + (20 * i)));
|
||||
wind += "\nRaceId: " + bytesToLong(
|
||||
Arrays.copyOfRange(payload, 10 + (20 * i), 14 + (20 * i)));
|
||||
wind += "\nWindDirection: " + bytesToLong(
|
||||
Arrays.copyOfRange(payload, 14 + (20 * i), 16 + (20 * i)));
|
||||
wind += "\nWindSpeed: " + bytesToLong(
|
||||
Arrays.copyOfRange(payload, 16 + (20 * i), 18 + (20 * i)));
|
||||
wind += "\nBestUpWindAngle: " + bytesToLong(
|
||||
Arrays.copyOfRange(payload, 18 + (20 * i), 20 + (20 * i)));
|
||||
wind += "\nBestDownWindAngle: " + bytesToLong(
|
||||
Arrays.copyOfRange(payload, 20 + (20 * i), 22 + (20 * i)));
|
||||
wind += "\nFlags: " + String
|
||||
.format("%8s", Integer.toBinaryString(payload[22 + (20 * i)] & 0xFF))
|
||||
.replace(' ', '0');
|
||||
windInfo.add(wind);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This packet conatins the average wind to ground speed
|
||||
*
|
||||
* @param packet The packet containing the paylaod
|
||||
*/
|
||||
private static void extractAvgWind(StreamPacket packet) {
|
||||
byte[] payload = packet.getPayload();
|
||||
int messageVersionNo = payload[0];
|
||||
long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7));
|
||||
long rawPeriod = bytesToLong(Arrays.copyOfRange(payload, 7, 9));
|
||||
long rawSamplePeriod = bytesToLong(Arrays.copyOfRange(payload, 9, 11));
|
||||
long period2 = bytesToLong(Arrays.copyOfRange(payload, 11, 13));
|
||||
long speed2 = bytesToLong(Arrays.copyOfRange(payload, 13, 15));
|
||||
long period3 = bytesToLong(Arrays.copyOfRange(payload, 15, 17));
|
||||
long speed3 = bytesToLong(Arrays.copyOfRange(payload, 17, 19));
|
||||
long period4 = bytesToLong(Arrays.copyOfRange(payload, 19, 21));
|
||||
long speed4 = bytesToLong(Arrays.copyOfRange(payload, 21, 23));
|
||||
}
|
||||
|
||||
|
||||
private static void extractBoatAction(StreamPacket packet) {
|
||||
byte[] payload = packet.getPayload();
|
||||
int messageVersionNo = payload[0];
|
||||
long actionType = bytesToLong(Arrays.copyOfRange(payload, 0, 1));
|
||||
if (actionType == 1) {
|
||||
System.out.println("VMG");
|
||||
} else if (actionType == 2) {
|
||||
System.out.println("SAILS IN");
|
||||
} else if (actionType == 3) {
|
||||
System.out.println("SAILS OUT");
|
||||
} else if (actionType == 4) {
|
||||
System.out.println("TACK/GYBE");
|
||||
} else if (actionType == 5) {
|
||||
System.out.println("UPWIND");
|
||||
} else if (actionType == 6) {
|
||||
System.out.println("DOWNWIND");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* takes an array of up to 7 bytes and returns a positive
|
||||
* long constructed from the input bytes
|
||||
*
|
||||
* @return a positive long if there is less than 7 bytes -1 otherwise
|
||||
*/
|
||||
private static long bytesToLong(byte[] bytes) {
|
||||
long partialLong = 0;
|
||||
int index = 0;
|
||||
for (byte b : bytes) {
|
||||
if (index > 6) {
|
||||
return -1;
|
||||
}
|
||||
partialLong = partialLong | (b & 0xFFL) << (index * 8);
|
||||
index++;
|
||||
}
|
||||
return partialLong;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns false if race not started, true otherwise
|
||||
*
|
||||
* @return race started status
|
||||
*/
|
||||
public static boolean isRaceStarted() {
|
||||
return raceStarted;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns false if stream not connected, true otherwise
|
||||
*
|
||||
* @return stream started status
|
||||
*/
|
||||
public static boolean isStreamStatus() {
|
||||
return streamStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns race timer
|
||||
*
|
||||
* @return race timer in long
|
||||
*/
|
||||
public static long getTimeSinceStart() {
|
||||
return timeSinceStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* return false if race not finished, true otherwise
|
||||
*
|
||||
* @return race finished status
|
||||
*/
|
||||
public static boolean isRaceFinished() {
|
||||
return raceFinished;
|
||||
}
|
||||
|
||||
/**
|
||||
* return a map of boats with sourceID and the boat
|
||||
*
|
||||
* @return map of boats
|
||||
*/
|
||||
public static Map<Integer, Yacht> getBoats() {
|
||||
return boats;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* returns the latest updated object from xml parser
|
||||
*
|
||||
* @return the latest xml object
|
||||
*/
|
||||
public static XMLParser getXmlObject() {
|
||||
return xmlObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the wind direction in degrees
|
||||
*
|
||||
* @return a double wind direction value
|
||||
*/
|
||||
public static double getWindDirection() {
|
||||
return windDirection;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the wind speed in knots
|
||||
* @return A double indicating the wind speed in knots
|
||||
*/
|
||||
public static Double getWindSpeed() {
|
||||
return windSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns stream time in formatted string format
|
||||
*
|
||||
* @return String of stream time
|
||||
*/
|
||||
public static String getCurrentTimeString() {
|
||||
return currentTimeString;
|
||||
}
|
||||
|
||||
/**
|
||||
* used in boat position since tree map can sort position efficiently.
|
||||
*
|
||||
* @return a map of time to finish and boat.
|
||||
*/
|
||||
public static Map<Integer, Yacht> getBoatsPos() {
|
||||
|
||||
return boatsPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns current time in stream in long
|
||||
*
|
||||
* @return a long value of current time
|
||||
*/
|
||||
public static Long getCurrentTimeLong() {
|
||||
return currentTimeLong;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check if a new un-processed xml has been found, if so will return true before
|
||||
* toggling off so that the next check will return false.
|
||||
*
|
||||
* @return the status of if new xml has been received
|
||||
*/
|
||||
public static boolean isNewRaceXmlReceived() {
|
||||
if (newRaceXmlReceived) {
|
||||
newRaceXmlReceived = false;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package seng302.model.stream;
|
||||
|
||||
import seng302.model.stream.packets.StreamPacket;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.util.Comparator;
|
||||
import java.util.concurrent.PriorityBlockingQueue;
|
||||
|
||||
|
||||
public class StreamReceiver extends Thread {
|
||||
private InputStream inputStream;
|
||||
private OutputStream outputStream;
|
||||
private Socket host;
|
||||
private ByteArrayOutputStream crcBuffer;
|
||||
private Thread t;
|
||||
private String threadName;
|
||||
public static PriorityBlockingQueue<StreamPacket> packetBuffer;
|
||||
private static boolean moreBytes;
|
||||
|
||||
public StreamReceiver(String hostAddress, int hostPort, String threadName) {
|
||||
this.threadName = threadName;
|
||||
this.setDaemon(true);
|
||||
try {
|
||||
host = new Socket(hostAddress, hostPort);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
public void run(){
|
||||
PriorityBlockingQueue<StreamPacket> pq = new PriorityBlockingQueue<>(256, new Comparator<StreamPacket>() {
|
||||
@Override
|
||||
public int compare(StreamPacket s1, StreamPacket s2) {
|
||||
return (int) (s1.getTimeStamp() - s2.getTimeStamp());
|
||||
}
|
||||
});
|
||||
packetBuffer = pq;
|
||||
connect();
|
||||
}
|
||||
|
||||
public void start () {
|
||||
if (t == null) {
|
||||
t = new Thread (this, threadName);
|
||||
t.start ();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public StreamReceiver(Socket host, PriorityBlockingQueue packetBuffer){
|
||||
this.host=host;
|
||||
this.packetBuffer = packetBuffer;
|
||||
}
|
||||
|
||||
|
||||
public void connect(){
|
||||
|
||||
// int sync1;
|
||||
// int sync2;
|
||||
// moreBytes = true;
|
||||
// while(moreBytes) {
|
||||
// try {
|
||||
// crcBuffer = new ByteArrayOutputStream();
|
||||
// sync1 = readByte();
|
||||
// sync2 = readByte();
|
||||
// //checking if it is the start of the packet
|
||||
// if(sync1 == 0x47 && sync2 == 0x83) {
|
||||
// int type = readByte();
|
||||
// //No. of milliseconds since Jan 1st 1970
|
||||
// long timeStamp = bytesToLong(getBytes(6));
|
||||
// skipBytes(4);
|
||||
// long payloadLength = bytesToLong(getBytes(2));
|
||||
// byte[] payload = getBytes((int) payloadLength);
|
||||
// Checksum checksum = new CRC32();
|
||||
// checksum.update(crcBuffer.toByteArray(), 0, crcBuffer.size());
|
||||
// long computedCrc = checksum.getValue();
|
||||
// long packetCrc = bytesToLong(getBytes(4));
|
||||
// if (computedCrc == packetCrc) {
|
||||
// packetBuffer.add(new StreamPacket(type, payloadLength, timeStamp, payload));
|
||||
// } else {
|
||||
// System.err.println("Packet has been dropped");
|
||||
// }
|
||||
// }
|
||||
// } catch (Exception e) {
|
||||
// moreBytes = false;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private int readByte() throws Exception {
|
||||
int currentByte = -1;
|
||||
try {
|
||||
currentByte = inputStream.read();
|
||||
crcBuffer.write(currentByte);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (currentByte == -1){
|
||||
throw new Exception();
|
||||
}
|
||||
return currentByte;
|
||||
}
|
||||
|
||||
private byte[] getBytes(int n) throws Exception{
|
||||
byte[] bytes = new byte[n];
|
||||
for (int i = 0; i < n; i++){
|
||||
bytes[i] = (byte) readByte();
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private void skipBytes(long n) throws Exception{
|
||||
for (int i=0; i < n; i++){
|
||||
readByte();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* takes an array of up to 7 bytes in little endian format and
|
||||
* returns a positive long constructed from the input bytes
|
||||
*
|
||||
* @return a positive long if there is less than 8 bytes -1 otherwise
|
||||
*/
|
||||
private long bytesToLong(byte[] bytes){
|
||||
long partialLong = 0;
|
||||
int index = 0;
|
||||
for (byte b: bytes){
|
||||
if (index > 6){
|
||||
return -1;
|
||||
}
|
||||
partialLong = partialLong | (b & 0xFFL) << (index * 8);
|
||||
index++;
|
||||
}
|
||||
return partialLong;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
StreamReceiver sr = new StreamReceiver("csse-s302staff.canterbury.ac.nz", 4941,"TestThread1");
|
||||
//StreamReceiver sr = new StreamReceiver("livedata.americascup.com", 4941, "TestThread2");
|
||||
sr.start();
|
||||
|
||||
}
|
||||
|
||||
public static void noMoreBytes(){
|
||||
moreBytes = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,610 @@
|
||||
package seng302.model.stream;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import seng302.model.Yacht;
|
||||
import seng302.model.mark.GateMark;
|
||||
import seng302.model.mark.Mark;
|
||||
import seng302.model.mark.MarkType;
|
||||
import seng302.model.mark.SingleMark;
|
||||
|
||||
/**
|
||||
* Class to create an XML object from the XML Packet Messages.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* Document doc; // some xml document
|
||||
* Integer xmlMessageType; // an Integer of value 5, 6, 7
|
||||
*
|
||||
* xmlP = new XMLParser(doc, xmlMessageType);
|
||||
* RegattaXMLObject rXmlObj = xmlP.createRegattaXML(); // creates a regattaXML object.
|
||||
*/
|
||||
public class XMLParser {
|
||||
|
||||
private Document xmlDoc;
|
||||
|
||||
private RaceXMLObject raceXML;
|
||||
private RegattaXMLObject regattaXML;
|
||||
private BoatXMLObject boatXML;
|
||||
|
||||
public XMLParser() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for XMLParser
|
||||
*
|
||||
* @param doc Document to create XML object.
|
||||
* @param messageType Defines if a message is a RegattaXML(5), RaceXML(6), BoatXML(7).
|
||||
*/
|
||||
public void constructXML(Document doc, Integer messageType) {
|
||||
this.xmlDoc = doc;
|
||||
switch (messageType) {
|
||||
case 5:
|
||||
regattaXML = new RegattaXMLObject(this.xmlDoc);
|
||||
break;
|
||||
case 6:
|
||||
raceXML = new RaceXMLObject(this.xmlDoc);
|
||||
break;
|
||||
case 7:
|
||||
boatXML = new BoatXMLObject(this.xmlDoc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public RaceXMLObject getRaceXML() {
|
||||
return raceXML;
|
||||
}
|
||||
|
||||
public RegattaXMLObject getRegattaXML() {
|
||||
return regattaXML;
|
||||
}
|
||||
|
||||
public BoatXMLObject getBoatXML() {
|
||||
return boatXML;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the text content of a given child element tag, assuming it exists, as an Integer.
|
||||
*
|
||||
* @param ele Document Element with child elements.
|
||||
* @param tag Tag to find in document elements child elements.
|
||||
* @return Text content from tag if found, null otherwise.
|
||||
*/
|
||||
private static Integer getElementInt(Element ele, String tag) {
|
||||
NodeList tagList = ele.getElementsByTagName(tag);
|
||||
if (tagList.getLength() > 0) {
|
||||
return Integer.parseInt(tagList.item(0).getTextContent());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text content of a given child element tag, assuming it exists, as an String.
|
||||
*
|
||||
* @param ele Document Element with child elements.
|
||||
* @param tag Tag to find in document elements child elements.
|
||||
* @return Text content from tag if found, null otherwise.
|
||||
*/
|
||||
private static String getElementString(Element ele, String tag) {
|
||||
NodeList tagList = ele.getElementsByTagName(tag);
|
||||
if (tagList.getLength() > 0) {
|
||||
return tagList.item(0).getTextContent();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text content of a given child element tag, assuming it exists, as a Double.
|
||||
*
|
||||
* @param ele Document Element with child elements.
|
||||
* @param tag Tag to find in document elements child elements.
|
||||
* @return Text content from tag if found, null otherwise.
|
||||
*/
|
||||
private static Double getElementDouble(Element ele, String tag) {
|
||||
NodeList tagList = ele.getElementsByTagName(tag);
|
||||
if (tagList.getLength() > 0) {
|
||||
return Double.parseDouble(tagList.item(0).getTextContent());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text content of an attribute of a given Node, assuming it exists, as a String.
|
||||
*
|
||||
* @param n A node object that should have some attributes
|
||||
* @param attr The attribute you want to get from the given node.
|
||||
* @return The String representation of the text content of an attribute in the given node, else
|
||||
* returns null.
|
||||
*/
|
||||
private static String getNodeAttributeString(Node n, String attr) {
|
||||
Node attrItem = n.getAttributes().getNamedItem(attr);
|
||||
if (attrItem != null) {
|
||||
return attrItem.getTextContent();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text content of an attribute of a given Node, assuming it exists, as an Integer.
|
||||
*
|
||||
* @param n A node object that should have some attributes
|
||||
* @param attr The attribute you want to get from the given node.
|
||||
* @return The Integer representation of the text content of an attribute in the given node,
|
||||
* else returns null.
|
||||
*/
|
||||
private static Integer getNodeAttributeInt(Node n, String attr) {
|
||||
Node attrItem = n.getAttributes().getNamedItem(attr);
|
||||
if (attrItem != null) {
|
||||
return Integer.parseInt(attrItem.getTextContent());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text content of an attribute of a given Node, assuming it exists, as a Double.
|
||||
*
|
||||
* @param n A node object that should have some attributes
|
||||
* @param attr The attribute you want to get from the given node.
|
||||
* @return The Double representation of the text content of an attribute in the given node, else
|
||||
* returns null.
|
||||
*/
|
||||
private static Double getNodeAttributeDouble(Node n, String attr) {
|
||||
Node attrItem = n.getAttributes().getNamedItem(attr);
|
||||
if (attrItem != null) {
|
||||
return Double.parseDouble(attrItem.getTextContent());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class RegattaXMLObject {
|
||||
|
||||
//Regatta Info
|
||||
private Integer regattaID;
|
||||
private String regattaName;
|
||||
private String courseName;
|
||||
private Double centralLat;
|
||||
private Double centralLng;
|
||||
private Integer utcOffset;
|
||||
|
||||
/**
|
||||
* Constructor for a RegattaXMLObject.
|
||||
* Takes the information from a Document object and creates a more usable format.
|
||||
*
|
||||
* @param doc XML Document Object
|
||||
*/
|
||||
RegattaXMLObject(Document doc) {
|
||||
Element docEle = doc.getDocumentElement();
|
||||
|
||||
this.regattaID = getElementInt(docEle, "RegattaID");
|
||||
this.regattaName = getElementString(docEle, "RegattaName");
|
||||
this.courseName = getElementString(docEle, "CourseName");
|
||||
this.centralLat = getElementDouble(docEle, "CentralLatitude");
|
||||
this.centralLng = getElementDouble(docEle, "CentralLongitude");
|
||||
this.utcOffset = getElementInt(docEle, "UtcOffset");
|
||||
}
|
||||
|
||||
public Integer getRegattaID() {
|
||||
return regattaID;
|
||||
}
|
||||
|
||||
public String getRegattaName() {
|
||||
return regattaName;
|
||||
}
|
||||
|
||||
public String getCourseName() {
|
||||
return courseName;
|
||||
}
|
||||
|
||||
public Double getCentralLat() {
|
||||
return centralLat;
|
||||
}
|
||||
|
||||
public Double getCentralLng() {
|
||||
return centralLng;
|
||||
}
|
||||
|
||||
public Integer getUtcOffset() {
|
||||
return utcOffset;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class RaceXMLObject {
|
||||
|
||||
// Race Info
|
||||
private Integer raceID;
|
||||
private String raceType;
|
||||
private String creationTimeDate; // XML Creation Time
|
||||
|
||||
//Race Start Details
|
||||
private String raceStartTime;
|
||||
private Boolean postponeStatus;
|
||||
|
||||
//Non atomic race attributes
|
||||
private ArrayList<Participant> participants;
|
||||
private ArrayList<Mark> allMarks;
|
||||
private ArrayList<Mark> nonDuplicateMarks;
|
||||
private ArrayList<Corner> compoundMarkSequence;
|
||||
private ArrayList<Limit> courseLimit;
|
||||
|
||||
// ensures there's no duplicate marks.
|
||||
private List<Long> seenSourceIDs = new ArrayList<Long>();
|
||||
|
||||
/**
|
||||
* Constructor for a RaceXMLObject.
|
||||
* Takes the information from a Document object and creates a more usable format.
|
||||
*
|
||||
* @param doc XML Document Object
|
||||
*/
|
||||
RaceXMLObject(Document doc) {
|
||||
Element docEle = doc.getDocumentElement();
|
||||
|
||||
//Atomic and Semi-Atomic Elements
|
||||
this.raceID = getElementInt(docEle, "RaceID");
|
||||
this.raceType = getElementString(docEle, "RaceType");
|
||||
this.creationTimeDate = getElementString(docEle, "CreationTimeDate");
|
||||
|
||||
Node raceStart = docEle.getElementsByTagName("RaceStartTime").item(0);
|
||||
this.raceStartTime = getNodeAttributeString(raceStart, "Start");
|
||||
this.postponeStatus = Boolean
|
||||
.parseBoolean(getNodeAttributeString(raceStart, "Postpone"));
|
||||
|
||||
//Participants
|
||||
participants = new ArrayList<>();
|
||||
|
||||
NodeList pList = docEle.getElementsByTagName("Participants").item(0).getChildNodes();
|
||||
for (int i = 0; i < pList.getLength(); i++) {
|
||||
Node pNode = pList.item(i);
|
||||
String entry;
|
||||
if (pNode.getNodeName().equals("Yacht")) {
|
||||
Integer sourceID = getNodeAttributeInt(pNode, "SourceID");
|
||||
|
||||
if (pNode.getAttributes().getLength() == 2) {
|
||||
entry = getNodeAttributeString(pNode, "Entry");
|
||||
} else {
|
||||
entry = null;
|
||||
}
|
||||
|
||||
Participant pa = new Participant(sourceID, entry);
|
||||
participants.add(pa);
|
||||
}
|
||||
}
|
||||
|
||||
//Course
|
||||
allMarks = new ArrayList<>();
|
||||
nonDuplicateMarks = new ArrayList<>();
|
||||
createCompoundMarks(docEle);
|
||||
|
||||
//Course Mark Sequence
|
||||
compoundMarkSequence = new ArrayList<>();
|
||||
|
||||
NodeList cornerList = docEle.getElementsByTagName("CompoundMarkSequence").item(0)
|
||||
.getChildNodes();
|
||||
for (int i = 0; i < cornerList.getLength(); i++) {
|
||||
Node cornerNode = cornerList.item(i);
|
||||
if (cornerNode.getNodeName().equals("Corner")) {
|
||||
Corner corner = new Corner(cornerNode);
|
||||
compoundMarkSequence.add(corner);
|
||||
}
|
||||
}
|
||||
|
||||
//Course Limits
|
||||
courseLimit = new ArrayList<>();
|
||||
|
||||
NodeList limitList = docEle.getElementsByTagName("CourseLimit").item(0).getChildNodes();
|
||||
for (int i = 0; i < limitList.getLength(); i++) {
|
||||
Node limitNode = limitList.item(i);
|
||||
if (limitNode.getNodeName().equals("Limit")) {
|
||||
Limit limit = new Limit(limitNode);
|
||||
courseLimit.add(limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void createCompoundMarks(Element docEle) {
|
||||
|
||||
NodeList cMarkList = docEle.getElementsByTagName("Course").item(0).getChildNodes();
|
||||
for (int i = 0; i < cMarkList.getLength(); i++) {
|
||||
Node cMarkNode = cMarkList.item(i);
|
||||
if (cMarkNode.getNodeName().equals("CompoundMark")) {
|
||||
createAndAddMark(cMarkNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void createAndAddMark(Node compoundMark) {
|
||||
|
||||
Boolean markSeen = false;
|
||||
List<SingleMark> marksList = new ArrayList<>();
|
||||
Integer compoundMarkID = getNodeAttributeInt(compoundMark, "CompoundMarkID");
|
||||
String cMarkName = getNodeAttributeString(compoundMark, "Name");
|
||||
|
||||
NodeList childMarks = compoundMark.getChildNodes();
|
||||
|
||||
for (int i = 0; i < childMarks.getLength(); i++) {
|
||||
Node markNode = childMarks.item(i);
|
||||
if (markNode.getNodeName().equals("Mark")) {
|
||||
|
||||
Integer sourceID = getNodeAttributeInt(markNode, "SourceID");
|
||||
String markName = getNodeAttributeString(markNode, "Name");
|
||||
Double targetLat = getNodeAttributeDouble(markNode, "TargetLat");
|
||||
Double targetLng = getNodeAttributeDouble(markNode, "TargetLng");
|
||||
|
||||
SingleMark mark = new SingleMark(markName, targetLat, targetLng, sourceID, compoundMarkID);
|
||||
marksList.add(mark);
|
||||
}
|
||||
}
|
||||
|
||||
for (SingleMark mark : marksList) {
|
||||
if (seenSourceIDs.contains(mark.getId())) {
|
||||
markSeen = true;
|
||||
} else {
|
||||
seenSourceIDs.add(mark.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (marksList.size() == 1) {
|
||||
if (!markSeen) {
|
||||
nonDuplicateMarks.add(marksList.get(0));
|
||||
}
|
||||
allMarks.add(marksList.get(0));
|
||||
} else if (marksList.size() == 2) {
|
||||
GateMark thisGateMark = new GateMark(cMarkName, MarkType.OPEN_GATE, marksList.get(0),
|
||||
marksList.get(1), marksList.get(0).getLatitude(),
|
||||
marksList.get(0).getLongitude(), compoundMarkID);
|
||||
if(!markSeen) {
|
||||
nonDuplicateMarks.add(thisGateMark);
|
||||
}
|
||||
allMarks.add(thisGateMark);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Integer getRaceID() {
|
||||
return raceID;
|
||||
}
|
||||
|
||||
public String getRaceType() {
|
||||
return raceType;
|
||||
}
|
||||
|
||||
public String getCreationTimeDate() {
|
||||
return creationTimeDate;
|
||||
}
|
||||
|
||||
public String getRaceStartTime() {
|
||||
return raceStartTime;
|
||||
}
|
||||
|
||||
public Boolean getPostponeStatus() {
|
||||
return postponeStatus;
|
||||
}
|
||||
|
||||
public ArrayList<Participant> getParticipants() {
|
||||
return participants;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns ALL compound marks as stated in the RaceXML (INCLUDING DUPLICATE MARKS)
|
||||
*/
|
||||
public List<Mark> getAllCompoundMarks() {
|
||||
return allMarks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns Marks from the race XML without any duplicates
|
||||
*/
|
||||
public List<Mark> getNonDupCompoundMarks() {
|
||||
return nonDuplicateMarks;
|
||||
}
|
||||
|
||||
public ArrayList<Corner> getCompoundMarkSequence() {
|
||||
return compoundMarkSequence;
|
||||
}
|
||||
|
||||
public ArrayList<Limit> getCourseLimit() {
|
||||
return courseLimit;
|
||||
}
|
||||
|
||||
public class Participant {
|
||||
|
||||
Integer sourceID;
|
||||
String entry;
|
||||
|
||||
Participant(Integer sourceID, String entry) {
|
||||
this.sourceID = sourceID;
|
||||
this.entry = entry;
|
||||
}
|
||||
|
||||
public Integer getsourceID() {
|
||||
return sourceID;
|
||||
}
|
||||
|
||||
public String getEntry() {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
public class Corner {
|
||||
|
||||
private Integer seqID;
|
||||
private Integer compoundMarkID;
|
||||
private String rounding;
|
||||
private Integer zoneSize;
|
||||
|
||||
Corner(Node cornerNode) {
|
||||
this.seqID = getNodeAttributeInt(cornerNode, "SeqID");
|
||||
this.compoundMarkID = getNodeAttributeInt(cornerNode, "CompoundMarkID");
|
||||
this.rounding = getNodeAttributeString(cornerNode, "Rounding");
|
||||
this.zoneSize = getNodeAttributeInt(cornerNode, "ZoneSize");
|
||||
}
|
||||
|
||||
public Integer getSeqID() {
|
||||
return seqID;
|
||||
}
|
||||
|
||||
public Integer getCompoundMarkID() {
|
||||
return compoundMarkID;
|
||||
}
|
||||
|
||||
public String getRounding() {
|
||||
return rounding;
|
||||
}
|
||||
|
||||
public Integer getZoneSize() {
|
||||
return zoneSize;
|
||||
}
|
||||
}
|
||||
|
||||
public class Limit {
|
||||
|
||||
private Integer seqID;
|
||||
private Double lat;
|
||||
private Double lng;
|
||||
|
||||
Limit(Node limitNode) {
|
||||
this.seqID = getNodeAttributeInt(limitNode, "SeqID");
|
||||
this.lat = getNodeAttributeDouble(limitNode, "Lat");
|
||||
this.lng = getNodeAttributeDouble(limitNode, "Lon");
|
||||
}
|
||||
|
||||
public Integer getSeqID() {
|
||||
return seqID;
|
||||
}
|
||||
|
||||
public Double getLat() {
|
||||
return lat;
|
||||
}
|
||||
|
||||
public Double getLng() {
|
||||
return lng;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class BoatXMLObject {
|
||||
|
||||
private String lastModified;
|
||||
private Integer version;
|
||||
|
||||
//Settings for the boat type in the race. This may end up having to be reworked if multiple boat types compete.
|
||||
private String boatType;
|
||||
private Double boatLength;
|
||||
private Double hullLength;
|
||||
private Double markZoneSize;
|
||||
private Double courseZoneSize;
|
||||
private ArrayList<Double> zoneLimits;// will only contain 5 elements. Limits 1-5
|
||||
|
||||
//Boats
|
||||
ArrayList<Yacht> boats;
|
||||
//Competing boats
|
||||
Map<Integer, Yacht> competingBoats = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Constructor for a BoatXMLObject.
|
||||
* Takes the information from a Document object and creates a more usable format.
|
||||
*
|
||||
* @param doc XML Document Object
|
||||
*/
|
||||
BoatXMLObject(Document doc) {
|
||||
|
||||
Element docEle = doc.getDocumentElement();
|
||||
|
||||
this.lastModified = getElementString(docEle, "Modified");
|
||||
this.version = getElementInt(docEle, "Version");
|
||||
|
||||
NodeList settingsList = docEle.getElementsByTagName("Settings").item(0).getChildNodes();
|
||||
this.boatType = getNodeAttributeString(settingsList.item(1), "Type");
|
||||
this.boatLength = getNodeAttributeDouble(settingsList.item(3), "BoatLength");
|
||||
this.hullLength = getNodeAttributeDouble(settingsList.item(3), "HullLength");
|
||||
this.markZoneSize = getNodeAttributeDouble(settingsList.item(5), "MarkZoneSize");
|
||||
this.courseZoneSize = getNodeAttributeDouble(settingsList.item(5), "CourseZoneSize");
|
||||
|
||||
Node zoneLimitsList = settingsList.item(7);
|
||||
this.zoneLimits = new ArrayList<>();
|
||||
for (int i = 0; i < zoneLimitsList.getAttributes().getLength(); i++) {
|
||||
String tag = String.format("Limit%d", i + 1);
|
||||
this.zoneLimits.add(getNodeAttributeDouble(zoneLimitsList, tag));
|
||||
}
|
||||
|
||||
this.boats = new ArrayList<>();
|
||||
NodeList boatsList = docEle.getElementsByTagName("Boats").item(0).getChildNodes();
|
||||
for (int i = 0; i < boatsList.getLength(); i++) {
|
||||
Node currentBoat = boatsList.item(i);
|
||||
if (currentBoat.getNodeName().equals("Boat")) {
|
||||
// Boat boat = new Boat(currentBoat);
|
||||
Yacht boat = new Yacht(getNodeAttributeString(currentBoat, "Type"),
|
||||
getNodeAttributeInt(currentBoat, "SourceID"),
|
||||
getNodeAttributeString(currentBoat, "HullNum"),
|
||||
getNodeAttributeString(currentBoat, "ShortName"),
|
||||
getNodeAttributeString(currentBoat, "BoatName"),
|
||||
getNodeAttributeString(currentBoat, "Country"));
|
||||
this.boats.add(boat);
|
||||
if (boat.getBoatType().equals("Yacht")) {
|
||||
competingBoats.put(boat.getSourceID(), boat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public String getLastModified() {
|
||||
return lastModified;
|
||||
}
|
||||
|
||||
public Integer getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getBoatType() {
|
||||
return boatType;
|
||||
}
|
||||
|
||||
public Double getBoatLength() {
|
||||
return boatLength;
|
||||
}
|
||||
|
||||
public Double getHullLength() {
|
||||
return hullLength;
|
||||
}
|
||||
|
||||
public Double getMarkZoneSize() {
|
||||
return markZoneSize;
|
||||
}
|
||||
|
||||
public Double getCourseZoneSize() {
|
||||
return courseZoneSize;
|
||||
}
|
||||
|
||||
public ArrayList<Double> getZoneLimits() {
|
||||
return zoneLimits;
|
||||
}
|
||||
|
||||
public ArrayList<Yacht> getBoats() {
|
||||
return boats;
|
||||
}
|
||||
|
||||
public Map<Integer, Yacht> getCompetingBoats() {
|
||||
return competingBoats;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package seng302.model.stream.packets;
|
||||
|
||||
public class BoatPositionPacket {
|
||||
private long boatId;
|
||||
private long timeValid;
|
||||
private double lat;
|
||||
private double lon;
|
||||
private double heading;
|
||||
private double groundSpeed;
|
||||
|
||||
public BoatPositionPacket(long boatId, long timeValid, double lat, double lon, double heading, double groundSpeed) {
|
||||
this.boatId = boatId;
|
||||
this.timeValid = timeValid;
|
||||
this.lat = lat;
|
||||
this.lon = lon;
|
||||
this.heading = heading;
|
||||
this.groundSpeed = groundSpeed;
|
||||
}
|
||||
|
||||
public long getTimeValid() {
|
||||
return timeValid;
|
||||
}
|
||||
|
||||
public double getLat() {
|
||||
return lat;
|
||||
}
|
||||
|
||||
public double getLon() {
|
||||
return lon;
|
||||
}
|
||||
|
||||
public double getHeading() {
|
||||
return heading;
|
||||
}
|
||||
|
||||
public double getGroundSpeed() {
|
||||
return groundSpeed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package seng302.model.stream.packets;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
/**
|
||||
* Created by Kusal on 4/24/2017.
|
||||
*/
|
||||
public enum PacketType {
|
||||
HEARTBEAT,
|
||||
RACE_STATUS,
|
||||
DISPLAY_TEXT_MESSAGE,
|
||||
XML_MESSAGE,
|
||||
RACE_START_STATUS,
|
||||
YACHT_EVENT_CODE,
|
||||
YACHT_ACTION_CODE,
|
||||
CHATTER_TEXT,
|
||||
BOAT_LOCATION,
|
||||
MARK_ROUNDING,
|
||||
COURSE_WIND,
|
||||
AVG_WIND,
|
||||
BOAT_ACTION,
|
||||
OTHER;
|
||||
|
||||
public static PacketType assignPacketType(int packetType){
|
||||
switch(packetType){
|
||||
case 1:
|
||||
return HEARTBEAT;
|
||||
case 12:
|
||||
return RACE_STATUS;
|
||||
case 20:
|
||||
return DISPLAY_TEXT_MESSAGE;
|
||||
case 26:
|
||||
return XML_MESSAGE;
|
||||
case 27:
|
||||
return RACE_START_STATUS;
|
||||
case 29:
|
||||
return YACHT_EVENT_CODE;
|
||||
case 31:
|
||||
return YACHT_ACTION_CODE;
|
||||
case 36:
|
||||
return CHATTER_TEXT;
|
||||
case 37:
|
||||
return BOAT_LOCATION;
|
||||
case 38:
|
||||
return MARK_ROUNDING;
|
||||
case 44:
|
||||
return COURSE_WIND;
|
||||
case 47:
|
||||
return AVG_WIND;
|
||||
case 100:
|
||||
return BOAT_ACTION;
|
||||
default:
|
||||
}
|
||||
return OTHER;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package seng302.model.stream.packets;
|
||||
|
||||
/**
|
||||
* Created by kre39 on 23/04/17.
|
||||
*/
|
||||
public class StreamPacket {
|
||||
|
||||
//Change int to an ENUM for the type
|
||||
private PacketType type;
|
||||
|
||||
private long messageLength;
|
||||
private long timeStamp;
|
||||
private byte[] payload;
|
||||
|
||||
public StreamPacket(int type, long messageLength, long timeStamp, byte[] payload) {
|
||||
this.type = PacketType.assignPacketType(type);
|
||||
this.messageLength = messageLength;
|
||||
this.timeStamp = timeStamp;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public PacketType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public long getMessageLength() {
|
||||
return messageLength;
|
||||
}
|
||||
|
||||
public byte[] getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
public long getTimeStamp() {
|
||||
return timeStamp;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user