Copied remaining files into appropriate package. These need to be refactored and put into the shared package.
parent
bbbb1f2eb0
commit
d0d63ca236
@ -0,0 +1,73 @@
|
||||
package mock.app;
|
||||
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.stage.Stage;
|
||||
import mock.dataInput.PolarParser;
|
||||
import mock.model.Polars;
|
||||
import org.w3c.dom.Document;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.transform.TransformerException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class App extends Application {
|
||||
|
||||
/**
|
||||
* Entry point for running the programme
|
||||
*
|
||||
* @param args for starting the programme
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
launch(args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Stage primaryStage) {
|
||||
try {
|
||||
Polars boatPolars = PolarParser.parse("polars/acc_polars.csv");
|
||||
|
||||
String regattaXML = readFile("mockXML/regattaTest.xml", StandardCharsets.UTF_8);
|
||||
String raceXML = readFile("mockXML/raceTest.xml", StandardCharsets.UTF_8);
|
||||
String boatXML = readFile("mockXML/boatTest.xml", StandardCharsets.UTF_8);
|
||||
|
||||
Event raceEvent = new Event(raceXML, regattaXML, boatXML, boatPolars);
|
||||
raceEvent.start();
|
||||
|
||||
} catch (Exception e) {
|
||||
//Catch all exceptions, print, and exit.
|
||||
e.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the Initial Race XML files that are necessary to run the mock.
|
||||
* @param path path of the XML
|
||||
* @param encoding encoding of the xml
|
||||
* @return
|
||||
* @throws IOException No file etc
|
||||
* @throws ParserConfigurationException Issue with the XML formatting
|
||||
* @throws SAXException Issue with XML formatting
|
||||
* @throws TransformerException Issue with the XML format
|
||||
*/
|
||||
private String readFile(String path, Charset encoding) throws IOException, ParserConfigurationException, SAXException, TransformerException {
|
||||
|
||||
InputSource fXmlFile = new InputSource(getClass().getClassLoader().getResourceAsStream(path));
|
||||
|
||||
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
|
||||
Document doc = dBuilder.parse(fXmlFile);
|
||||
doc.getDocumentElement().normalize();
|
||||
|
||||
return XMLReader.getContents(doc);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
package mock.app;
|
||||
|
||||
import mock.model.Polars;
|
||||
import network.Messages.Enums.MessageType;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
|
||||
/**
|
||||
* A Race Event, this holds all of the race's information as well as handling the connection to its clients.
|
||||
*/
|
||||
public class Event {
|
||||
|
||||
String raceXML;
|
||||
String regattaXML;
|
||||
String boatXML;
|
||||
Polars boatPolars;
|
||||
MockOutput mockOutput;
|
||||
|
||||
public Event(String raceXML, String regattaXML, String boatXML, Polars boatPolars) {
|
||||
|
||||
this.raceXML = getRaceXMLAtCurrentTime(raceXML);
|
||||
this.boatXML = boatXML;
|
||||
this.regattaXML = regattaXML;
|
||||
this.boatPolars = boatPolars;
|
||||
try {
|
||||
mockOutput = new MockOutput();
|
||||
new Thread(mockOutput).start();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the initial race data and then begins race simulation
|
||||
*/
|
||||
public void start() {
|
||||
try {
|
||||
sendXMLs();
|
||||
Race newRace = new Race(new RaceXMLReader(this.raceXML, new BoatXMLReader(boatXML, this.boatPolars)), mockOutput);
|
||||
new Thread((newRace)).start();
|
||||
|
||||
} catch (ParserConfigurationException | IOException | SAXException | ParseException | StreamedCourseXMLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends out each xml string, via the mock output
|
||||
*/
|
||||
private void sendXMLs() {
|
||||
|
||||
mockOutput.setRegattaXml(regattaXML);
|
||||
mockOutput.parseXMLString(regattaXML, MessageType.XMLMESSAGE.getValue());
|
||||
|
||||
mockOutput.setRaceXml(raceXML);
|
||||
mockOutput.parseXMLString(raceXML, MessageType.XMLMESSAGE.getValue());
|
||||
|
||||
mockOutput.setBoatsXml(boatXML);
|
||||
mockOutput.parseXMLString(boatXML, MessageType.XMLMESSAGE.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the xml description of the race to show the race was created now, and starts in 3 minutes
|
||||
* @param raceXML
|
||||
* @return String containing edited xml
|
||||
*/
|
||||
private String getRaceXMLAtCurrentTime(String raceXML) {
|
||||
DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
|
||||
ZonedDateTime creationTime = ZonedDateTime.now();
|
||||
return raceXML.replace("CREATION_TIME", dateFormat.format(creationTime))
|
||||
.replace("START_TIME", dateFormat.format(creationTime.plusMinutes(3)));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,254 @@
|
||||
package mock.app;
|
||||
|
||||
|
||||
|
||||
import network.BinaryMessageEncoder;
|
||||
import network.MessageEncoders.RaceVisionByteEncoder;
|
||||
import network.MessageEncoders.XMLMessageEncoder;
|
||||
import network.Messages.BoatLocation;
|
||||
import network.Messages.Enums.MessageType;
|
||||
import network.Messages.RaceStatus;
|
||||
import network.Messages.XMLMessage;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
|
||||
/**
|
||||
* TCP server to send race information to connected clients.
|
||||
*/
|
||||
public class MockOutput implements Runnable
|
||||
{
|
||||
///Timestamp of the last sent heartbeat message.
|
||||
private long lastHeartbeatTime;
|
||||
|
||||
///Period for the heartbeat - that is, how often we send it.
|
||||
private double heartbeatPeriod = 5.0;
|
||||
|
||||
///Port to expose server on.
|
||||
private int serverPort = 4942;
|
||||
///Socket used to listen for clients on.
|
||||
private ServerSocket serverSocket;
|
||||
///Socket used to communicate with a client.
|
||||
private Socket mockSocket;
|
||||
///Output stream which wraps around mockSocket outstream.
|
||||
private DataOutputStream outToVisualiser;
|
||||
|
||||
///A queue that contains items that are waiting to be sent.
|
||||
private ArrayBlockingQueue<byte[]> messagesToSendQueue = new ArrayBlockingQueue<>(99999999);
|
||||
|
||||
///Sequence numbers used in messages.
|
||||
private short messageNumber = 1;
|
||||
private short xmlSequenceNumber = 1;
|
||||
private int heartbeatSequenceNum = 1;
|
||||
private int boatLocationSequenceNumber = 1;
|
||||
private int raceStatusSequenceNumber = 1;
|
||||
|
||||
///Strings containing XML data as strings.
|
||||
private String raceXml;
|
||||
private String regattaXml;
|
||||
private String boatsXml;
|
||||
|
||||
private boolean stop = false; //whether or not hte thread keeps running
|
||||
|
||||
/**
|
||||
* Ctor.
|
||||
* @throws IOException if server socket cannot be opened.
|
||||
*/
|
||||
public MockOutput() throws IOException {
|
||||
lastHeartbeatTime = System.currentTimeMillis();
|
||||
serverSocket = new ServerSocket(serverPort);
|
||||
|
||||
}
|
||||
/**
|
||||
* Calculates the time since last heartbeat message, in seconds.
|
||||
* @return Time since last heartbeat message, in seconds.
|
||||
*/
|
||||
private double timeSinceHeartbeat() {
|
||||
long now = System.currentTimeMillis();
|
||||
return (now - lastHeartbeatTime) / 1000.0;
|
||||
}
|
||||
|
||||
//returns the heartbeat message
|
||||
|
||||
/**
|
||||
* Increment the heartbeat value
|
||||
* @return message for heartbeat data
|
||||
*/
|
||||
private byte[] heartbeat(){
|
||||
byte[] heartbeatMessage = RaceVisionByteEncoder.heartBeat(heartbeatSequenceNum);
|
||||
heartbeatSequenceNum++;
|
||||
BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(MessageType.HEARTBEAT, System.currentTimeMillis(), messageNumber, (short)heartbeatMessage.length, heartbeatMessage);
|
||||
messageNumber++;
|
||||
return binaryMessageEncoder.getFullMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to give the mockOutput an xml string to be made into a message and sent
|
||||
* @param xmlString the xml string to send
|
||||
* @param messageType the kind of xml string, values given in AC35 spec (5 regatta, 6 race, 7 boat)
|
||||
*/
|
||||
public synchronized void parseXMLString(String xmlString, int messageType){
|
||||
XMLMessageEncoder encoder = new XMLMessageEncoder(messageNumber, System.currentTimeMillis(), messageType, xmlSequenceNumber,(short) xmlString.length(), xmlString);
|
||||
//iterates the sequence numbers
|
||||
xmlSequenceNumber++;
|
||||
byte[] encodedXML = encoder.encode();
|
||||
|
||||
BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(MessageType.XMLMESSAGE, System.currentTimeMillis(), messageNumber, (short)encodedXML.length, encodedXML);
|
||||
//iterates the message number
|
||||
messageNumber++;
|
||||
|
||||
addMessageToBufferToSend(binaryMessageEncoder.getFullMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to give the mocOutput information about boat location to be made into a message and sent
|
||||
* @param sourceID id of the boat
|
||||
* @param lat latitude of boat
|
||||
* @param lon longitude of boat
|
||||
* @param heading heading of boat
|
||||
* @param speed speed of boat
|
||||
* @param time historical time of race
|
||||
*/
|
||||
public synchronized void parseBoatLocation(int sourceID, double lat, double lon, double heading, double speed, long time){
|
||||
|
||||
BoatLocation boatLocation = new BoatLocation(sourceID, lat, lon, boatLocationSequenceNumber, heading, speed, time);
|
||||
//iterates the sequence number
|
||||
boatLocationSequenceNumber++;
|
||||
|
||||
//encodeds the messages
|
||||
byte[] encodedBoatLoc = RaceVisionByteEncoder.boatLocation(boatLocation);
|
||||
|
||||
//encodeds the full message with header
|
||||
BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(MessageType.BOATLOCATION, System.currentTimeMillis(), messageNumber, (short)encodedBoatLoc.length,
|
||||
encodedBoatLoc);
|
||||
|
||||
//iterates the message number
|
||||
messageNumber++;
|
||||
|
||||
addMessageToBufferToSend(binaryMessageEncoder.getFullMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the race status data and add it to the buffer to be sent
|
||||
* @param raceStatus race status to parses
|
||||
*/
|
||||
public synchronized void parseRaceStatus(RaceStatus raceStatus){
|
||||
|
||||
//iterates the sequence number
|
||||
raceStatusSequenceNumber++;
|
||||
|
||||
//encodeds the messages
|
||||
byte[] encodedRaceStatus = RaceVisionByteEncoder.raceStatus(raceStatus);
|
||||
|
||||
//encodeds the full message with header
|
||||
BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(MessageType.RACESTATUS, System.currentTimeMillis(), messageNumber, (short)encodedRaceStatus.length,
|
||||
encodedRaceStatus);
|
||||
|
||||
//iterates the message number
|
||||
messageNumber++;
|
||||
|
||||
addMessageToBufferToSend(binaryMessageEncoder.getFullMessage());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message to the buffer to be sent
|
||||
* @param messagesToSendBuffer message to add to the buffer
|
||||
*/
|
||||
private synchronized void addMessageToBufferToSend(byte[] messagesToSendBuffer) {
|
||||
this.messagesToSendQueue.add(messagesToSendBuffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sending loop of the Server
|
||||
*/
|
||||
public void run() {
|
||||
|
||||
try {
|
||||
while (!stop){
|
||||
System.out.println("Waiting for a connection...");//TEMP DEBUG REMOVE
|
||||
mockSocket = serverSocket.accept();
|
||||
|
||||
outToVisualiser = new DataOutputStream(mockSocket.getOutputStream());
|
||||
|
||||
if (boatsXml == null || regattaXml == null || raceXml == null){
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
parseXMLString(raceXml, XMLMessage.XMLTypeRace);
|
||||
parseXMLString(regattaXml, XMLMessage.XMLTypeRegatta);
|
||||
parseXMLString(boatsXml, XMLMessage.XMLTypeBoat);
|
||||
|
||||
|
||||
while(true) {
|
||||
try {
|
||||
//Sends a heartbeat every so often.
|
||||
if (timeSinceHeartbeat() >= heartbeatPeriod) {
|
||||
outToVisualiser.write(heartbeat());
|
||||
lastHeartbeatTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
//Checks the buffer to see if there is anything to send.
|
||||
while (messagesToSendQueue.size() > 0) {
|
||||
//Grabs message from head of queue.
|
||||
byte[] binaryMessage = messagesToSendQueue.remove();
|
||||
|
||||
//sends the message to the visualiser
|
||||
outToVisualiser.write(binaryMessage);
|
||||
|
||||
}
|
||||
}catch(SocketException e){
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void stop(){
|
||||
stop = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Race XML to send
|
||||
* @param raceXml XML to send to the CLient
|
||||
*/
|
||||
public void setRaceXml(String raceXml) {
|
||||
this.raceXml = raceXml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Regatta XMl to send
|
||||
* @param regattaXml XML to send to CLient
|
||||
*/
|
||||
public void setRegattaXml(String regattaXml) {
|
||||
this.regattaXml = regattaXml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Boats XML to send
|
||||
* @param boatsXml XMl to send to the CLient
|
||||
*/
|
||||
public void setBoatsXml(String boatsXml) {
|
||||
this.boatsXml = boatsXml;
|
||||
}
|
||||
|
||||
public static void main(String argv[]) throws Exception
|
||||
{
|
||||
MockOutput client = new MockOutput();
|
||||
client.run();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package mock.dataInput;
|
||||
|
||||
import seng302.Model.Boat;
|
||||
import seng302.Model.Mark;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Boats Data
|
||||
*/
|
||||
public interface BoatDataSource {
|
||||
Map<Integer, Boat> getBoats();
|
||||
Map<Integer, Mark> getMarkerBoats();
|
||||
}
|
||||
@ -0,0 +1,149 @@
|
||||
package mock.dataInput;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.xml.sax.SAXException;
|
||||
import seng302.Model.Boat;
|
||||
import seng302.Model.GPSCoordinate;
|
||||
import seng302.Model.Mark;
|
||||
import seng302.Model.Polars;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Xml Reader class for Boat XML used for the race
|
||||
*/
|
||||
public class BoatXMLReader extends XMLReader implements BoatDataSource {
|
||||
|
||||
private final Map<Integer, Boat> boatMap = new HashMap<>();
|
||||
private final Map<Integer, Mark> markerMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Polars table to assign to each boat.
|
||||
*/
|
||||
Polars boatPolars;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor for Boat XML
|
||||
*
|
||||
* @param filePath Name/path of file to read. Read as a resource.
|
||||
* @param boatPolars polars used by the boats
|
||||
* @throws IOException error
|
||||
* @throws SAXException error
|
||||
* @throws ParserConfigurationException error
|
||||
*/
|
||||
public BoatXMLReader(String filePath, Polars boatPolars) throws IOException, SAXException, ParserConfigurationException {
|
||||
super(filePath);
|
||||
this.boatPolars = boatPolars;
|
||||
read();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the XML
|
||||
*/
|
||||
public void read() {
|
||||
readSettings();
|
||||
readShapes();
|
||||
readBoats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the Boats
|
||||
*/
|
||||
private void readBoats() {
|
||||
Element nBoats = (Element) doc.getElementsByTagName("Boats").item(0);
|
||||
for (int i = 0; i < nBoats.getChildNodes().getLength(); i++) {
|
||||
Node boat = nBoats.getChildNodes().item(i);
|
||||
if (boat.getNodeName().equals("Boat")) {
|
||||
readBoatNode(boat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignored data
|
||||
*/
|
||||
private void readShapes() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignored data
|
||||
*/
|
||||
private void readSettings() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node (XMl data node) is a Yacht or not
|
||||
* @param boatNode Node from the XML
|
||||
* @return Whether the node is a yacht node or not
|
||||
*/
|
||||
private boolean isYachtNode(Node boatNode) {
|
||||
return boatNode.getAttributes().getNamedItem("Type").getTextContent().toLowerCase().equals("yacht");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the information about one boat
|
||||
* Ignored values: ShapeID, StoweName, HullNum, Skipper, Type
|
||||
*/
|
||||
private void readBoatNode(Node boatNode) {
|
||||
int sourceID = Integer.parseInt(boatNode.getAttributes().getNamedItem("SourceID").getTextContent());
|
||||
String name = boatNode.getAttributes().getNamedItem("BoatName").getTextContent();
|
||||
|
||||
if (isYachtNode(boatNode)) readYacht(boatNode, sourceID, name);
|
||||
else readMark(boatNode, sourceID, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a Yacht Node
|
||||
* @param boatNode Node to be read
|
||||
* @param sourceID Source ID of the Yacht
|
||||
* @param name Name of the Boat
|
||||
*/
|
||||
private void readYacht(Node boatNode, int sourceID, String name) {
|
||||
String shortName = boatNode.getAttributes().getNamedItem("ShortName").getTextContent();
|
||||
if (exists(boatNode, "Country")) {
|
||||
String country = boatNode.getAttributes().getNamedItem("Country").getTextContent();
|
||||
boatMap.put(sourceID, new Boat(sourceID, name, country, this.boatPolars));
|
||||
} else {
|
||||
boatMap.put(sourceID, new Boat(sourceID, name, shortName, this.boatPolars));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read Marker Boats
|
||||
* @param boatNode Node to be read
|
||||
* @param sourceID Source ID of the boat
|
||||
* @param name Name of the Marker Boat
|
||||
*/
|
||||
private void readMark(Node boatNode, int sourceID, String name) {
|
||||
Node nCoord = ((Element)boatNode).getElementsByTagName("GPSposition").item(0);
|
||||
double x = Double.parseDouble(nCoord.getAttributes().getNamedItem("X").getTextContent());
|
||||
double y = Double.parseDouble(nCoord.getAttributes().getNamedItem("Y").getTextContent());
|
||||
Mark mark = new Mark(sourceID, name, new GPSCoordinate(y,x));
|
||||
markerMap.put(sourceID, mark);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the boats that are going to participate in this race
|
||||
* @return Dictionary of boats that are to participate in this race indexed by SourceID
|
||||
*/
|
||||
@Override
|
||||
public Map<Integer, Boat> getBoats() {
|
||||
return boatMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the marker BOats that are participating in this race
|
||||
* @return Dictionary of the Markers BOats that are in this race indexed by their Source ID.
|
||||
*/
|
||||
@Override
|
||||
public Map<Integer, Mark> getMarkerBoats() {
|
||||
return markerMap;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package mock.dataInput;
|
||||
|
||||
import seng302.Model.Boat;
|
||||
import seng302.Model.CompoundMark;
|
||||
import seng302.Model.GPSCoordinate;
|
||||
import seng302.Model.Leg;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Data Class for a Race
|
||||
*/
|
||||
public interface RaceDataSource {
|
||||
List<Boat> getBoats();
|
||||
|
||||
List<Leg> getLegs();
|
||||
|
||||
List<GPSCoordinate> getBoundary();
|
||||
|
||||
List<CompoundMark> getCompoundMarks();
|
||||
|
||||
int getRaceId();
|
||||
|
||||
String getRaceType();
|
||||
|
||||
ZonedDateTime getZonedDateTime();
|
||||
|
||||
GPSCoordinate getMapTopLeft();
|
||||
|
||||
GPSCoordinate getMapBottomRight();
|
||||
}
|
||||
@ -0,0 +1,287 @@
|
||||
package mock.dataInput;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
import seng302.Exceptions.StreamedCourseXMLException;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* XML Reader that reads in the race data required for this race
|
||||
*/
|
||||
public class RaceXMLReader extends XMLReader implements RaceDataSource {
|
||||
private static final double COORDINATEPADDING = 0.000;
|
||||
private GPSCoordinate mapTopLeft, mapBottomRight;
|
||||
private final List<GPSCoordinate> boundary = new ArrayList<>();
|
||||
private final Map<Integer,Element> compoundMarkMap = new HashMap<>();
|
||||
private final Map<Integer, Boat> participants = new HashMap<>();
|
||||
private final List<Leg> legs = new ArrayList<>();
|
||||
private final List<CompoundMark> compoundMarks = new ArrayList<>();
|
||||
private ZonedDateTime creationTimeDate;
|
||||
private ZonedDateTime raceStartTime;
|
||||
private int raceID;
|
||||
private String raceType;
|
||||
private boolean postpone;
|
||||
|
||||
private Map<Integer, Boat> boats;
|
||||
private Map<Integer, Mark> marks;
|
||||
|
||||
/**
|
||||
* Constructor for Streamed Race XML
|
||||
* @param filePath path of the file
|
||||
* @param boatData data for boats in race
|
||||
* @throws IOException error
|
||||
* @throws SAXException error
|
||||
* @throws ParserConfigurationException error
|
||||
* @throws ParseException error
|
||||
* @throws StreamedCourseXMLException error
|
||||
*/
|
||||
public RaceXMLReader(String filePath, BoatDataSource boatData) throws IOException, SAXException, ParserConfigurationException, ParseException, StreamedCourseXMLException {
|
||||
this(filePath, boatData, true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Constructor for Streamed Race XML
|
||||
* @param filePath file path to read
|
||||
* @param boatData data of the boats in race
|
||||
* @param read whether or not to read and store the files straight away.
|
||||
* @throws IOException error
|
||||
* @throws SAXException error
|
||||
* @throws ParserConfigurationException error
|
||||
* @throws ParseException error
|
||||
* @throws StreamedCourseXMLException error
|
||||
*/
|
||||
public RaceXMLReader(String filePath, BoatDataSource boatData, boolean read) throws IOException, SAXException, ParserConfigurationException, ParseException, StreamedCourseXMLException {
|
||||
super(filePath);
|
||||
this.boats = boatData.getBoats();
|
||||
this.marks = boatData.getMarkerBoats();
|
||||
if (read) {
|
||||
read();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* reads
|
||||
* @throws StreamedCourseXMLException error
|
||||
*/
|
||||
private void read() throws StreamedCourseXMLException {
|
||||
readRace();
|
||||
readParticipants();
|
||||
readCourse();
|
||||
}
|
||||
|
||||
/**
|
||||
* reads a race
|
||||
*/
|
||||
private void readRace() {
|
||||
DateTimeFormatter dateFormat = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
||||
Element settings = (Element) doc.getElementsByTagName("Race").item(0);
|
||||
NamedNodeMap raceTimeTag = doc.getElementsByTagName("RaceStartTime").item(0).getAttributes();
|
||||
|
||||
if (raceTimeTag.getNamedItem("Time") != null) dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
|
||||
|
||||
|
||||
raceID = Integer.parseInt(getTextValueOfNode(settings, "RaceID"));
|
||||
raceType = getTextValueOfNode(settings, "RaceType");
|
||||
|
||||
creationTimeDate = ZonedDateTime.parse(getTextValueOfNode(settings, "CreationTimeDate"), dateFormat);
|
||||
|
||||
if (raceTimeTag.getNamedItem("Time") != null) raceStartTime = ZonedDateTime.parse(raceTimeTag.getNamedItem("Time").getTextContent(), dateFormat);
|
||||
else raceStartTime = ZonedDateTime.parse(raceTimeTag.getNamedItem("Start").getTextContent(), dateFormat);
|
||||
|
||||
postpone = Boolean.parseBoolean(raceTimeTag.getNamedItem("Postpone").getTextContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads in the participants for htis race
|
||||
*/
|
||||
private void readParticipants() {
|
||||
Element nParticipants = (Element) doc.getElementsByTagName("Participants").item(0);
|
||||
nParticipants.getChildNodes().getLength();
|
||||
for (int i = 0; i < nParticipants.getChildNodes().getLength(); i++) {
|
||||
int sourceID;
|
||||
Node yacht = nParticipants.getChildNodes().item(i);
|
||||
if (yacht.getNodeName().equals("Yacht")) {
|
||||
if (exists(yacht, "SourceID")) {
|
||||
sourceID = Integer.parseInt(yacht.getAttributes().getNamedItem("SourceID").getTextContent());
|
||||
participants.put(sourceID, boats.get(sourceID));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* reads a course
|
||||
* @throws StreamedCourseXMLException error
|
||||
*/
|
||||
private void readCourse() throws StreamedCourseXMLException {
|
||||
readCompoundMarks();
|
||||
readCompoundMarkSequence();
|
||||
readCourseLimit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indexes CompoundMark elements by their ID for use in generating the course, and populates list of Markers.
|
||||
* @see CompoundMark
|
||||
*/
|
||||
private void readCompoundMarks() throws StreamedCourseXMLException {
|
||||
Element nCourse = (Element) doc.getElementsByTagName("Course").item(0);
|
||||
for(int i = 0; i < nCourse.getChildNodes().getLength(); i++) {
|
||||
Node compoundMark = nCourse.getChildNodes().item(i);
|
||||
if(compoundMark.getNodeName().equals("CompoundMark")) {
|
||||
int compoundMarkID = getCompoundMarkID((Element) compoundMark);
|
||||
compoundMarkMap.put(compoundMarkID, (Element)compoundMark);
|
||||
compoundMarks.add(getCompoundMark(compoundMarkID));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a CompoundMark from the CompoundMark element with given ID.
|
||||
* @param compoundMarkID index of required CompoundMark element
|
||||
* @return generated CompoundMark
|
||||
* @throws StreamedCourseXMLException if CompoundMark element contains unhandled number of compoundMarks
|
||||
* @see CompoundMark
|
||||
*/
|
||||
private CompoundMark getCompoundMark(int compoundMarkID) throws StreamedCourseXMLException {
|
||||
Element compoundMark = compoundMarkMap.get(compoundMarkID);
|
||||
NodeList nMarks = compoundMark.getElementsByTagName("Mark");
|
||||
CompoundMark marker;
|
||||
|
||||
switch(nMarks.getLength()) {
|
||||
case 1: marker = new CompoundMark(getMark((Element)nMarks.item(0)));
|
||||
break;
|
||||
case 2: marker = new CompoundMark(getMark((Element)nMarks.item(0)), getMark((Element)nMarks.item(1))); break;
|
||||
default: throw new StreamedCourseXMLException();
|
||||
}
|
||||
|
||||
return marker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a mark from an Element
|
||||
* @param mark Element the mark is suppose to be part of
|
||||
* @return a Mark that existed in the element
|
||||
*/
|
||||
private Mark getMark(Element mark) {
|
||||
int sourceID = Integer.parseInt(mark.getAttribute("SourceID"));
|
||||
return marks.get(sourceID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads "compoundMarkID" attribute of CompoundMark or Corner element
|
||||
* @param element with "compoundMarkID" attribute
|
||||
* @return value of "compoundMarkID" attribute
|
||||
*/
|
||||
private int getCompoundMarkID(Element element) {
|
||||
return Integer.parseInt(element.getAttribute("CompoundMarkID"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads "name" attribute of CompoundMark element with corresponding CompoundMarkID
|
||||
* @param compoundMarkID unique ID for CompoundMark element
|
||||
* @return value of "name" attribute
|
||||
*/
|
||||
private String getCompoundMarkName(int compoundMarkID) {
|
||||
return compoundMarkMap.get(compoundMarkID).getAttribute("Name");
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates list of legs given CompoundMarkSequence element and referenced CompoundMark elements.
|
||||
* @throws StreamedCourseXMLException if compoundMarks cannot be resolved from CompoundMark
|
||||
*/
|
||||
private void readCompoundMarkSequence() throws StreamedCourseXMLException {
|
||||
Element nCompoundMarkSequence = (Element) doc.getElementsByTagName("CompoundMarkSequence").item(0);
|
||||
NodeList nCorners = nCompoundMarkSequence.getElementsByTagName("Corner");
|
||||
Element markXML = (Element)nCorners.item(0);
|
||||
CompoundMark lastCompoundMark = getCompoundMark(getCompoundMarkID(markXML));
|
||||
String legName = getCompoundMarkName(getCompoundMarkID(markXML));
|
||||
for(int i = 1; i < nCorners.getLength(); i++) {
|
||||
markXML = (Element)nCorners.item(i);
|
||||
CompoundMark currentCompoundMark = getCompoundMark(getCompoundMarkID(markXML));
|
||||
legs.add(new Leg(legName, lastCompoundMark, currentCompoundMark, i-1));
|
||||
lastCompoundMark = currentCompoundMark;
|
||||
legName = getCompoundMarkName(getCompoundMarkID(markXML));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the boundary limits of the course
|
||||
*/
|
||||
private void readCourseLimit() {
|
||||
Element nCourseLimit = (Element) doc.getElementsByTagName("CourseLimit").item(0);
|
||||
for(int i = 0; i < nCourseLimit.getChildNodes().getLength(); i++) {
|
||||
Node limit = nCourseLimit.getChildNodes().item(i);
|
||||
if (limit.getNodeName().equals("Limit")) {
|
||||
double lat = Double.parseDouble(limit.getAttributes().getNamedItem("Lat").getTextContent());
|
||||
double lon = Double.parseDouble(limit.getAttributes().getNamedItem("Lon").getTextContent());
|
||||
boundary.add(new GPSCoordinate(lat, lon));
|
||||
}
|
||||
}
|
||||
|
||||
double maxLatitude = boundary.stream().max(Comparator.comparingDouble(GPSCoordinate::getLatitude)).get().getLatitude() + COORDINATEPADDING;
|
||||
double maxLongitude = boundary.stream().max(Comparator.comparingDouble(GPSCoordinate::getLongitude)).get().getLongitude() + COORDINATEPADDING;
|
||||
double minLatitude = boundary.stream().min(Comparator.comparingDouble(GPSCoordinate::getLatitude)).get().getLatitude() + COORDINATEPADDING;
|
||||
double minLongitude = boundary.stream().min(Comparator.comparingDouble(GPSCoordinate::getLongitude)).get().getLongitude() + COORDINATEPADDING;
|
||||
|
||||
mapTopLeft = new GPSCoordinate(minLatitude, minLongitude);
|
||||
mapBottomRight = new GPSCoordinate(maxLatitude, maxLongitude);
|
||||
}
|
||||
|
||||
public List<GPSCoordinate> getBoundary() {
|
||||
return boundary;
|
||||
}
|
||||
|
||||
public GPSCoordinate getMapTopLeft() {
|
||||
return mapTopLeft;
|
||||
}
|
||||
|
||||
public GPSCoordinate getMapBottomRight() {
|
||||
return mapBottomRight;
|
||||
}
|
||||
|
||||
public List<Leg> getLegs() {
|
||||
return legs;
|
||||
}
|
||||
|
||||
public List<CompoundMark> getCompoundMarks() { return compoundMarks; }
|
||||
|
||||
public Double getPadding() {
|
||||
return COORDINATEPADDING;
|
||||
}
|
||||
|
||||
public ZonedDateTime getCreationTimeDate() {
|
||||
return creationTimeDate;
|
||||
}
|
||||
|
||||
public ZonedDateTime getZonedDateTime() {
|
||||
return raceStartTime;
|
||||
}
|
||||
|
||||
public int getRaceId() {
|
||||
return raceID;
|
||||
}
|
||||
|
||||
public String getRaceType() {
|
||||
return raceType;
|
||||
}
|
||||
|
||||
public boolean isPostpone() {
|
||||
return postpone;
|
||||
}
|
||||
|
||||
public List<Boat> getBoats() {
|
||||
return new ArrayList<>(participants.values());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
package mock.dataInput;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.transform.OutputKeys;
|
||||
import javax.xml.transform.Transformer;
|
||||
import javax.xml.transform.TransformerException;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
|
||||
/**
|
||||
* Base Reader for XML Files
|
||||
*/
|
||||
public abstract class XMLReader {
|
||||
|
||||
protected Document doc;
|
||||
|
||||
/**
|
||||
* Read in XML file
|
||||
* @param filePath filepath for XML file
|
||||
* @throws ParserConfigurationException If a document builder cannot be created.
|
||||
* @throws IOException If any IO errors occur while parsing the XML file.
|
||||
* @throws SAXException If any parse error occurs while parsing.
|
||||
*/
|
||||
public XMLReader(String filePath) throws ParserConfigurationException, IOException, SAXException {
|
||||
|
||||
InputSource fXmlFile;
|
||||
if (filePath.contains("<")) {
|
||||
fXmlFile = new InputSource();
|
||||
fXmlFile.setCharacterStream(new StringReader(filePath));
|
||||
|
||||
} else {
|
||||
fXmlFile = new InputSource(getClass().getClassLoader().getResourceAsStream(filePath));
|
||||
}
|
||||
|
||||
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
|
||||
doc = dBuilder.parse(fXmlFile);
|
||||
doc.getDocumentElement().normalize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternate constructor
|
||||
* @param xmlFile File to be read
|
||||
* @param isWholeFile boolean value whether entire file is being passed
|
||||
*/
|
||||
public XMLReader(String xmlFile, Boolean isWholeFile) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Document data of the read-in XML
|
||||
* @return XML document
|
||||
*/
|
||||
public Document getDocument() {
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content of a tag in an element
|
||||
* @param n Element to read tags from
|
||||
* @param tagName Name of the tag
|
||||
* @return Content of the tag
|
||||
*/
|
||||
public String getTextValueOfNode(Element n, String tagName) {
|
||||
return n.getElementsByTagName(tagName).item(0).getTextContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attributes for an element
|
||||
* @param n Element to read attributes from
|
||||
* @param attr Attributes of element
|
||||
* @return Attributes of element
|
||||
*/
|
||||
public String getAttribute(Element n, String attr) {
|
||||
return n.getAttribute(attr);
|
||||
}
|
||||
|
||||
protected boolean exists(Node node, String attribute) {
|
||||
return node.getAttributes().getNamedItem(attribute) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of the XML FILe.
|
||||
* @param document holds all xml information
|
||||
* @return String representation of document
|
||||
* @throws TransformerException when document is malformed, and cannot be turned into a string
|
||||
*/
|
||||
public static String getContents(Document document) throws TransformerException {
|
||||
DOMSource source = new DOMSource(document);
|
||||
|
||||
TransformerFactory transformerFactory = TransformerFactory.newInstance();
|
||||
Transformer transformer = transformerFactory.newTransformer();
|
||||
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
|
||||
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
StreamResult result = new StreamResult(stringWriter);
|
||||
transformer.transform(source, result);
|
||||
|
||||
return stringWriter.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package mock.exceptions;
|
||||
|
||||
/**
|
||||
* An exception thrown when we cannot generate Boats.xml and send an XML message.
|
||||
*/
|
||||
public class InvalidBoatDataException extends RuntimeException {
|
||||
|
||||
public InvalidBoatDataException() {
|
||||
}
|
||||
|
||||
public InvalidBoatDataException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package mock.exceptions;
|
||||
|
||||
/**
|
||||
* Exception thrown when we cannot generate Race.xml data, and send an XML message.
|
||||
*/
|
||||
public class InvalidRaceDataException extends RuntimeException {
|
||||
public InvalidRaceDataException() {
|
||||
}
|
||||
|
||||
public InvalidRaceDataException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package mock.exceptions;
|
||||
|
||||
/**
|
||||
* Created by cbt24 on 25/04/17.
|
||||
*/
|
||||
public class StreamedCourseXMLException extends Throwable {
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
package visualiser.Controllers;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import seng302.RaceConnection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
/**
|
||||
* Controls the connection that the VIsualiser can connect to.
|
||||
*/
|
||||
public class ConnectionController extends Controller {
|
||||
@FXML
|
||||
private AnchorPane connectionWrapper;
|
||||
@FXML
|
||||
private TableView connectionTable;
|
||||
@FXML
|
||||
private TableColumn<RaceConnection, String> hostnameColumn;
|
||||
@FXML
|
||||
private TableColumn<RaceConnection, String> statusColumn;
|
||||
@FXML
|
||||
private Button connectButton;
|
||||
|
||||
@FXML
|
||||
private TextField urlField;
|
||||
@FXML
|
||||
private TextField portField;
|
||||
|
||||
private ObservableList<RaceConnection> connections;
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
// TODO - replace with config file
|
||||
connections = FXCollections.observableArrayList();
|
||||
connections.add(new RaceConnection("livedata.americascup.com", 4941));
|
||||
connections.add(new RaceConnection("localhost", 4942));
|
||||
|
||||
connectionTable.setItems(connections);
|
||||
hostnameColumn.setCellValueFactory(cellData -> cellData.getValue().hostnameProperty());
|
||||
statusColumn.setCellValueFactory(cellData -> cellData.getValue().statusProperty());
|
||||
|
||||
connectionTable.getSelectionModel().selectedItemProperty().addListener((obs, prev, curr) -> {
|
||||
if (curr != null && ((RaceConnection)curr).check()) connectButton.setDisable(false);
|
||||
else connectButton.setDisable(true);
|
||||
});
|
||||
connectButton.setDisable(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets current status of all connections.
|
||||
*/
|
||||
public void checkConnections() {
|
||||
for(RaceConnection connection: connections) {
|
||||
connection.check();
|
||||
}
|
||||
}
|
||||
|
||||
public AnchorPane startWrapper(){
|
||||
return connectionWrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to host currently selected in table. Button enabled only if host is ready.
|
||||
*/
|
||||
public void connectSocket() {
|
||||
try{
|
||||
RaceConnection connection = (RaceConnection)connectionTable.getSelectionModel().getSelectedItem();
|
||||
Socket socket = new Socket(connection.getHostname(), connection.getPort());
|
||||
connectionWrapper.setVisible(false);
|
||||
parent.enterLobby(socket);
|
||||
} catch (IOException e) { /* Never reached */ }
|
||||
}
|
||||
|
||||
/**
|
||||
* adds a new connection
|
||||
*/
|
||||
public void addConnection(){
|
||||
String hostName = urlField.getText();
|
||||
String portString = portField.getText();
|
||||
try{
|
||||
int port = Integer.parseInt(portString);
|
||||
connections.add(new RaceConnection(hostName, port));
|
||||
}catch(NumberFormatException e){
|
||||
System.err.println("Port number entered is not a number");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package visualiser.Controllers;
|
||||
|
||||
import javafx.fxml.Initializable;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
/**
|
||||
* Controller parent for app controllers.
|
||||
* Created by fwy13 on 15/03/2017.
|
||||
*/
|
||||
public abstract class Controller implements Initializable {
|
||||
protected MainController parent;
|
||||
|
||||
/**
|
||||
* Sets the parent of the application
|
||||
*
|
||||
* @param parent controller
|
||||
*/
|
||||
public void setParent(MainController parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisation class that is run on start up.
|
||||
*
|
||||
* @param location resources location
|
||||
* @param resources resources bundle
|
||||
*/
|
||||
@Override
|
||||
public abstract void initialize(URL location, ResourceBundle resources);
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package visualiser.Controllers;
|
||||
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import seng302.Model.Boat;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
|
||||
/**
|
||||
* Finish Screen for when the race finishs.
|
||||
*/
|
||||
public class FinishController extends Controller {
|
||||
|
||||
@FXML
|
||||
AnchorPane finishWrapper;
|
||||
|
||||
@FXML
|
||||
TableView<Boat> boatInfoTable;
|
||||
|
||||
@FXML
|
||||
TableColumn<Boat, String> boatRankColumn;
|
||||
|
||||
@FXML
|
||||
TableColumn<Boat, String> boatNameColumn;
|
||||
|
||||
@FXML
|
||||
Label raceWinnerLabel;
|
||||
|
||||
/**
|
||||
* Sets up the finish table
|
||||
* @param boats Boats to display
|
||||
*/
|
||||
private void setFinishTable(ObservableList<Boat> boats){
|
||||
boatInfoTable.setItems(boats);
|
||||
boatNameColumn.setCellValueFactory(cellData -> cellData.getValue().getName());
|
||||
boatRankColumn.setCellValueFactory(cellData -> cellData.getValue().positionProperty());
|
||||
|
||||
raceWinnerLabel.setText("Winner: "+ boatNameColumn.getCellObservableValue(0).getValue());
|
||||
raceWinnerLabel.setWrapText(true);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources){
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the table
|
||||
* @param boats boats to display on the table.
|
||||
*/
|
||||
public void enterFinish(ObservableList<Boat> boats){
|
||||
finishWrapper.setVisible(true);
|
||||
setFinishTable(boats);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package visualiser.Controllers;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import seng302.Model.Boat;
|
||||
import seng302.Model.RaceClock;
|
||||
import seng302.VisualiserInput;
|
||||
|
||||
import java.net.Socket;
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
/**
|
||||
* Controller that everything is overlayed onto. This makes it so that changing scenes does not resize your stage.
|
||||
*/
|
||||
public class MainController extends Controller {
|
||||
@FXML private StartController startController;
|
||||
@FXML private RaceController raceController;
|
||||
@FXML private ConnectionController connectionController;
|
||||
@FXML private FinishController finishController;
|
||||
|
||||
public void beginRace(VisualiserInput visualiserInput, RaceClock raceClock) {
|
||||
raceController.startRace(visualiserInput, raceClock);
|
||||
}
|
||||
|
||||
public void enterLobby(Socket socket) {
|
||||
startController.enterLobby(socket);
|
||||
}
|
||||
|
||||
public void enterFinish(ObservableList<Boat> boats) { finishController.enterFinish(boats); }
|
||||
|
||||
/**
|
||||
* Main Controller for the applications will house the menu and the displayed pane.
|
||||
*
|
||||
* @param location of resources
|
||||
* @param resources bundle
|
||||
*/
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
startController.setParent(this);
|
||||
raceController.setParent(this);
|
||||
connectionController.setParent(this);
|
||||
finishController.setParent(this);
|
||||
AnchorPane.setTopAnchor(startController.startWrapper(), 0.0);
|
||||
AnchorPane.setBottomAnchor(startController.startWrapper(), 0.0);
|
||||
AnchorPane.setLeftAnchor(startController.startWrapper(), 0.0);
|
||||
AnchorPane.setRightAnchor(startController.startWrapper(), 0.0);
|
||||
|
||||
AnchorPane.setTopAnchor(connectionController.startWrapper(), 0.0);
|
||||
AnchorPane.setBottomAnchor(connectionController.startWrapper(), 0.0);
|
||||
AnchorPane.setLeftAnchor(connectionController.startWrapper(), 0.0);
|
||||
AnchorPane.setRightAnchor(connectionController.startWrapper(), 0.0);
|
||||
|
||||
AnchorPane.setTopAnchor(finishController.finishWrapper, 0.0);
|
||||
AnchorPane.setBottomAnchor(finishController.finishWrapper, 0.0);
|
||||
AnchorPane.setLeftAnchor(finishController.finishWrapper, 0.0);
|
||||
AnchorPane.setRightAnchor(finishController.finishWrapper, 0.0);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,199 @@
|
||||
package visualiser.Controllers;
|
||||
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.chart.LineChart;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import seng302.Mock.StreamedRace;
|
||||
import seng302.VisualiserInput;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
/**
|
||||
* Created by fwy13 on 15/03/2017.
|
||||
*/
|
||||
public class RaceController extends Controller {
|
||||
private ResizableRaceCanvas raceMap;
|
||||
private ResizableRaceMap raceBoundaries;
|
||||
|
||||
private RaceClock raceClock;
|
||||
private Sparkline sparkline;
|
||||
|
||||
private int legNum;
|
||||
|
||||
@FXML GridPane canvasBase;
|
||||
@FXML Pane arrow;
|
||||
@FXML SplitPane race;
|
||||
@FXML StackPane arrowPane;
|
||||
@FXML Label timer;
|
||||
@FXML Label FPS;
|
||||
@FXML Label timeZone;
|
||||
@FXML CheckBox showFPS;
|
||||
@FXML TableView<Boat> boatInfoTable;
|
||||
@FXML TableColumn<Boat, String> boatPlacingColumn;
|
||||
@FXML TableColumn<Boat, String> boatTeamColumn;
|
||||
@FXML TableColumn<Boat, String> boatMarkColumn;
|
||||
@FXML TableColumn<Boat, String> boatSpeedColumn;
|
||||
@FXML LineChart<Number, Number> sparklineChart;
|
||||
@FXML AnchorPane annotationPane;
|
||||
|
||||
/**
|
||||
* Updates the ResizableRaceCanvas (raceMap) with most recent data
|
||||
*
|
||||
* @param boats boats that are to be displayed in the race
|
||||
* @param boatMarkers Markers for boats
|
||||
* @see ResizableRaceCanvas
|
||||
*/
|
||||
public void updateMap(ObservableList<Boat> boats, ObservableList<Marker> boatMarkers) {
|
||||
raceMap.setBoats(boats);
|
||||
raceMap.setBoatMarkers(boatMarkers);
|
||||
raceMap.update();
|
||||
raceBoundaries.draw();
|
||||
//stop if the visualiser is no longer running
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the array listened by the TableView (boatInfoTable) that displays the boat information.
|
||||
*
|
||||
* @param race Race to listen to.
|
||||
*/
|
||||
public void setInfoTable(StreamedRace race) {
|
||||
boatInfoTable.setItems(race.getStartingBoats());
|
||||
boatTeamColumn.setCellValueFactory(cellData -> cellData.getValue().getName());
|
||||
boatSpeedColumn.setCellValueFactory(cellData -> cellData.getValue().getVelocityProp());
|
||||
boatMarkColumn.setCellValueFactory(cellData -> cellData.getValue().getCurrentLegName());
|
||||
boatPlacingColumn.setCellValueFactory(cellData -> cellData.getValue().positionProperty());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
//listener for fps
|
||||
showFPS.selectedProperty().addListener((ov, old_val, new_val) -> {
|
||||
if (showFPS.isSelected()) {
|
||||
FPS.setVisible(true);
|
||||
} else {
|
||||
FPS.setVisible(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and sets initial display for Sparkline for race positions.
|
||||
* @param boats boats to display on the sparkline
|
||||
*/
|
||||
public void createSparkLine(ObservableList<Boat> boats){
|
||||
sparkline = new Sparkline(boats, legNum, sparklineChart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the sparkline to display current boat positions.
|
||||
* @param boatsInRace used for current boat positions.
|
||||
*/
|
||||
public void updateSparkline(ObservableList<Boat> boatsInRace){
|
||||
sparkline.updateSparkline(boatsInRace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes and runs the race, based on the user's chosen scale factor
|
||||
* Currently uses an example racecourse
|
||||
*
|
||||
* @param visualiserInput input from network
|
||||
* @param raceClock The RaceClock to use for the race's countdown/elapsed duration + timezone.
|
||||
*/
|
||||
public void startRace(VisualiserInput visualiserInput, RaceClock raceClock) {
|
||||
|
||||
legNum = visualiserInput.getCourse().getLegs().size()-1;
|
||||
|
||||
makeArrow();
|
||||
|
||||
raceMap = new ResizableRaceCanvas(visualiserInput.getCourse());
|
||||
raceMap.setMouseTransparent(true);
|
||||
raceMap.widthProperty().bind(canvasBase.widthProperty());
|
||||
raceMap.heightProperty().bind(canvasBase.heightProperty());
|
||||
raceMap.draw();
|
||||
raceMap.setVisible(true);
|
||||
raceMap.setArrow(arrow.getChildren().get(0));
|
||||
|
||||
canvasBase.getChildren().add(0, raceMap);
|
||||
|
||||
raceBoundaries = new ResizableRaceMap(visualiserInput.getCourse());
|
||||
raceBoundaries.setMouseTransparent(true);
|
||||
raceBoundaries.widthProperty().bind(canvasBase.widthProperty());
|
||||
raceBoundaries.heightProperty().bind(canvasBase.heightProperty());
|
||||
raceBoundaries.draw();
|
||||
raceBoundaries.setVisible(true);
|
||||
|
||||
canvasBase.getChildren().add(0, raceBoundaries);
|
||||
|
||||
race.setVisible(true);
|
||||
|
||||
timeZone.setText(raceClock.getTimeZone());
|
||||
|
||||
//RaceClock.duration isn't necessarily being changed in the javaFX thread, so we need to runlater the update.
|
||||
raceClock.durationProperty().addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> {
|
||||
timer.setText(newValue);
|
||||
});
|
||||
});
|
||||
|
||||
this.raceClock = raceClock;
|
||||
raceMap.setRaceClock(raceClock);
|
||||
|
||||
StreamedRace newRace = new StreamedRace(visualiserInput, this);
|
||||
|
||||
initializeFPS();
|
||||
|
||||
// set up annotation displays
|
||||
new Annotations(annotationPane, raceMap);
|
||||
|
||||
new Thread((newRace)).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish Race View
|
||||
* @param boats boats there are in the race.
|
||||
*/
|
||||
public void finishRace(ObservableList<Boat> boats){
|
||||
race.setVisible(false);
|
||||
parent.enterFinish(boats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for the fps label
|
||||
*
|
||||
* @param fps fps that the label will be updated to
|
||||
*/
|
||||
public void setFrames(String fps) {
|
||||
FPS.setText((fps));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up FPS display at bottom of screen
|
||||
*/
|
||||
private void initializeFPS() {
|
||||
showFPS.setVisible(true);
|
||||
showFPS.selectedProperty().addListener((ov, old_val, new_val) -> {
|
||||
if (showFPS.isSelected()) {
|
||||
FPS.setVisible(true);
|
||||
} else {
|
||||
FPS.setVisible(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void makeArrow() {
|
||||
arrowPane.getChildren().add(arrow);
|
||||
}
|
||||
|
||||
public RaceClock getRaceClock() {
|
||||
return raceClock;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,212 @@
|
||||
package visualiser.Controllers;
|
||||
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.cell.PropertyValueFactory;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import seng302.Mock.StreamedCourse;
|
||||
import seng302.Model.Boat;
|
||||
import seng302.Model.RaceClock;
|
||||
import seng302.VisualiserInput;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.net.URL;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Observable;
|
||||
import java.util.Observer;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
/**
|
||||
* Controller to for waiting for the race to start
|
||||
*/
|
||||
public class StartController extends Controller implements Observer {
|
||||
|
||||
@FXML private GridPane start;
|
||||
@FXML private AnchorPane startWrapper;
|
||||
@FXML private Label raceTitleLabel;
|
||||
@FXML private Label raceStartLabel;
|
||||
|
||||
@FXML private TableView<Boat> boatNameTable;
|
||||
@FXML private TableColumn<Boat, String> boatNameColumn;
|
||||
@FXML private TableColumn<Boat, String> boatCodeColumn;
|
||||
@FXML private Label timeZoneTime;
|
||||
@FXML private Label timer;
|
||||
@FXML private Label raceStatusLabel;
|
||||
|
||||
//@FXML Button fifteenMinButton;
|
||||
|
||||
private RaceClock raceClock;
|
||||
|
||||
private StreamedCourse raceData;
|
||||
private int raceStat;
|
||||
|
||||
private VisualiserInput visualiserInput;
|
||||
|
||||
///Tracks whether the race has been started (that is, has startRaceNoScaling() be called).
|
||||
private boolean hasRaceStarted = false;
|
||||
|
||||
//Tracks whether or not a clock has been created and setup, which occurs after receiving enough information.
|
||||
private boolean hasCreatedClock = false;
|
||||
|
||||
/**
|
||||
* Begins the race with a scale factor of 1
|
||||
*/
|
||||
private void startRaceNoScaling() {
|
||||
//while(visualiserInput.getRaceStatus() == null);//TODO probably remove this.
|
||||
|
||||
countdownTimer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources){
|
||||
raceData = new StreamedCourse();
|
||||
raceData.addObserver(this);
|
||||
}
|
||||
|
||||
public AnchorPane startWrapper(){
|
||||
return startWrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiliases the tables that are to be shown on the pane
|
||||
*/
|
||||
private void initialiseTables() {
|
||||
List<Boat> boats = raceData.getBoats();
|
||||
ObservableList<Boat> observableBoats = FXCollections.observableArrayList(boats);
|
||||
|
||||
boatNameTable.setItems(observableBoats);
|
||||
boatNameColumn.setCellValueFactory(cellData -> cellData.getValue().getName());
|
||||
boatCodeColumn.setCellValueFactory(new PropertyValueFactory<>("abbrev"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Countdown timer until race starts.
|
||||
*/
|
||||
private void countdownTimer() {
|
||||
new AnimationTimer() {
|
||||
@Override
|
||||
public void handle(long arg0) {
|
||||
raceStat = visualiserInput.getRaceStatus().getRaceStatus();
|
||||
raceStatusLabel.setText("Race Status: " + visualiserInput.getRaceStatus().getRaceStatus());
|
||||
if (raceStat==2 || raceStat == 3) {
|
||||
stop();
|
||||
|
||||
startWrapper.setVisible(false);
|
||||
start.setVisible(false);
|
||||
|
||||
parent.beginRace(visualiserInput, raceClock);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the clock that displays the time of at the current race venue.
|
||||
*/
|
||||
private void setRaceClock() {
|
||||
raceClock = new RaceClock(raceData.getZonedDateTime());
|
||||
|
||||
raceClock.timeStringProperty().addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> {
|
||||
timeZoneTime.setText(newValue);
|
||||
});
|
||||
});
|
||||
//TEMP REMOVE
|
||||
//timeZoneTime.textProperty().bind(raceClock.timeStringProperty());
|
||||
raceClock.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time that the race is going to start.
|
||||
*/
|
||||
private void setStartingTime() {
|
||||
String dateFormat = "'Starting time:' HH:mm dd/MM/YYYY";
|
||||
Platform.runLater(()-> {
|
||||
long utcTime = visualiserInput.getRaceStatus().getExpectedStartTime();
|
||||
raceClock.setStartingTime(raceClock.getLocalTime(utcTime));
|
||||
raceStartLabel.setText(DateTimeFormatter.ofPattern(dateFormat).format(raceClock.getStartingTime()));
|
||||
|
||||
raceClock.durationProperty().addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> {
|
||||
timer.setText(newValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* set the current time, may be used to update the time on the clock.
|
||||
*/
|
||||
private void setCurrentTime() {
|
||||
Platform.runLater(()->
|
||||
raceClock.setUTCTime(visualiserInput.getRaceStatus().getCurrentTime())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Observable o, Object arg) {
|
||||
if(o instanceof StreamedCourse) {
|
||||
StreamedCourse streamedCourse = (StreamedCourse) o;
|
||||
if(streamedCourse.hasReadBoats()) {
|
||||
initialiseTables();
|
||||
}
|
||||
if(streamedCourse.hasReadRegatta()) {
|
||||
Platform.runLater(() -> raceTitleLabel.setText(streamedCourse.getRegattaName()));
|
||||
}
|
||||
if (streamedCourse.hasReadCourse()) {
|
||||
Platform.runLater(() -> {
|
||||
if (!this.hasCreatedClock) {
|
||||
this.hasCreatedClock = true;
|
||||
setRaceClock();
|
||||
if (visualiserInput.getRaceStatus() == null) {
|
||||
return;//TEMP BUG FIX if the race isn't sending race status messages (our mock currently doesn't), then it would block the javafx thread with the previous while loop.
|
||||
}// TODO - replace with observer on VisualiserInput
|
||||
setStartingTime();
|
||||
setCurrentTime();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//TODO this is a somewhat temporary fix for when not all of the race data (boats, course, regatta) is received in time.
|
||||
//Previously, startRaceNoScaling was called in the enterLobby function after the visualiserInput was started, but when connecting to the official data source it sometimes didn't send all of the race data, causing startRaceNoScaling to start, even though we didn't have enough information to start it.
|
||||
if (streamedCourse.hasReadBoats() && streamedCourse.hasReadCourse() && streamedCourse.hasReadRegatta()) {
|
||||
Platform.runLater(() -> {
|
||||
if (!this.hasRaceStarted) {
|
||||
if(visualiserInput.getRaceStatus() == null) {
|
||||
}
|
||||
else {
|
||||
this.hasRaceStarted = true;
|
||||
startRaceNoScaling();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show starting information for a race given a socket.
|
||||
* @param socket network source of information
|
||||
*/
|
||||
public void enterLobby(Socket socket) {
|
||||
startWrapper.setVisible(true);
|
||||
try {
|
||||
visualiserInput = new VisualiserInput(socket, raceData);
|
||||
new Thread(visualiserInput).start();
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package visualiser.app;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
|
||||
public class App extends Application {
|
||||
|
||||
/**
|
||||
* Entry point for running the programme
|
||||
*
|
||||
* @param args for starting the programme
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
launch(args);
|
||||
}
|
||||
|
||||
public void start(Stage stage) throws Exception {
|
||||
stage.setOnCloseRequest(new EventHandler<WindowEvent>() {
|
||||
@Override
|
||||
public void handle(WindowEvent event) {
|
||||
Platform.exit();
|
||||
System.exit(0);
|
||||
}
|
||||
});
|
||||
FXMLLoader loader = new FXMLLoader(getClass().getResource("/scenes/main.fxml"));
|
||||
Parent root = loader.load();
|
||||
Scene scene = new Scene(root, 1200, 800);
|
||||
stage.setScene(scene);
|
||||
stage.setTitle("RaceVision - Team 7");
|
||||
stage.show();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,432 @@
|
||||
package visualiser.app;
|
||||
import javafx.application.Platform;
|
||||
import org.xml.sax.SAXException;
|
||||
import seng302.Networking.BinaryMessageDecoder;
|
||||
import seng302.Networking.Exceptions.InvalidMessageException;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static seng302.Networking.Utils.ByteConverter.bytesToShort;
|
||||
|
||||
/**
|
||||
* TCP client which receives packets/messages from a race data source
|
||||
* (e.g., mock source, official source), and exposes them to any observers.
|
||||
* @see seng302.Mock.StreamedCourse
|
||||
*/
|
||||
public class VisualiserInput implements Runnable {
|
||||
|
||||
///Timestamp of the last heartbeat.
|
||||
private long lastHeartbeatTime = -1;
|
||||
///Sequence number of the last heartbeat.
|
||||
private long lastHeartbeatSequenceNum = -1;
|
||||
|
||||
///The socket that we have connected to.
|
||||
private Socket connectionSocket;
|
||||
|
||||
///Object to store parsed course data. //TODO comment?
|
||||
private StreamedCourse course;
|
||||
|
||||
///The last RaceStatus message received.
|
||||
private RaceStatus raceStatus;
|
||||
|
||||
///A map of the last BoatStatus message received, for each boat.
|
||||
private final Map<Integer, BoatStatus> boatStatusMap = new HashMap<>();
|
||||
|
||||
///A map of the last BoatLocation message received, for each boat.
|
||||
private final Map<Integer, BoatLocation> boatLocationMap = new HashMap<>();
|
||||
|
||||
///The last AverageWind message received.
|
||||
private AverageWind averageWind;
|
||||
|
||||
///The last CourseWinds message received.
|
||||
private CourseWinds courseWinds;
|
||||
|
||||
///A map of the last MarkRounding message received, for each boat.
|
||||
private final Map<Integer, MarkRounding> markRoundingMap = new HashMap<>();
|
||||
|
||||
///InputStream (from the socket).
|
||||
private DataInputStream inStream;
|
||||
|
||||
/**
|
||||
* Ctor.
|
||||
* @param socket Socket from which we will receive race data.
|
||||
* @param course TODO comment?
|
||||
* @throws IOException If there is something wrong with the socket's input stream.
|
||||
*/
|
||||
public VisualiserInput(Socket socket, StreamedCourse course) throws IOException {
|
||||
this.connectionSocket = socket;
|
||||
//We wrap a DataInputStream around the socket's InputStream because it has the stream.readFully(buffer) function, which is a blocking read until the buffer has been filled.
|
||||
this.inStream = new DataInputStream(connectionSocket.getInputStream());
|
||||
this.course = course;
|
||||
|
||||
this.lastHeartbeatTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides StreamedCourse container for fixed course data.
|
||||
* @return Course for current VisualiserInput instance.
|
||||
* @see seng302.Mock.StreamedCourse
|
||||
*/
|
||||
public StreamedCourse getCourse() {
|
||||
return course;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last boat location message associated with the given boat source ID.
|
||||
* @param sourceID Unique global identifier for the boat.
|
||||
* @return The most recent location message.
|
||||
*/
|
||||
public BoatLocation getBoatLocationMessage(int sourceID) {
|
||||
return boatLocationMap.get(sourceID);
|
||||
}
|
||||
|
||||
public BoatStatus getBoatStatusMessage(int sourceID) {
|
||||
return boatStatusMap.get(sourceID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the time since last heartbeat, in milliseconds.
|
||||
* @return Time since last heartbeat, in milliseconds..
|
||||
*/
|
||||
private double timeSinceHeartbeat() {
|
||||
long now = System.currentTimeMillis();
|
||||
return (now - lastHeartbeatTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the boat locations map. Maps from Integer (Boat ID) to BoatLocation.
|
||||
* @return Map of boat locations.
|
||||
*/
|
||||
public Map<Integer, BoatLocation> getBoatLocationMap() {
|
||||
return boatLocationMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the status of the race.
|
||||
* @return The status of the race.
|
||||
*/
|
||||
public RaceStatus getRaceStatus() {
|
||||
return raceStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the boat statuses map. Maps from Integer (Boat ID) to BoatStatus.
|
||||
* @return Map of boat statuses.
|
||||
*/
|
||||
public Map<Integer, BoatStatus> getBoatStatusMap() {
|
||||
return boatStatusMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the average wind of the race.
|
||||
* @return Average wind in the race.
|
||||
*/
|
||||
public AverageWind getAverageWind() {
|
||||
return averageWind;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns winds in the course.
|
||||
* @return Winds that are in the course.
|
||||
*/
|
||||
public CourseWinds getCourseWinds() {
|
||||
return courseWinds;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the mark roundings map. Maps from Integer (Boat ID) to MarkRounding.
|
||||
* @return Map of mark roundings.
|
||||
*/
|
||||
public Map<Integer, MarkRounding> getMarkRoundingMap() {
|
||||
return markRoundingMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the wind direction for the current course.
|
||||
* @param direction The new wind direction for the course.
|
||||
*/
|
||||
private void setCourseWindDirection(double direction) {
|
||||
this.course.setWindDirection(direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and returns the next message as an array of bytes from the socket. Use getNextMessage() to get the actual message object instead.
|
||||
* @return Encoded binary message bytes.
|
||||
* @throws IOException Thrown when an error occurs while reading from the socket.
|
||||
*/
|
||||
private byte[] getNextMessageBytes() throws IOException {
|
||||
inStream.mark(0);
|
||||
short CRCLength = 4;
|
||||
short headerLength = 15;
|
||||
|
||||
//Read the header of the next message.
|
||||
byte[] headerBytes = new byte[headerLength];
|
||||
inStream.readFully(headerBytes);
|
||||
|
||||
//Read the message body length.
|
||||
byte[] messageBodyLengthBytes = Arrays.copyOfRange(headerBytes, headerLength - 2, headerLength);
|
||||
short messageBodyLength = bytesToShort(messageBodyLengthBytes);
|
||||
|
||||
//Read the message body.
|
||||
byte[] messageBodyBytes = new byte[messageBodyLength];
|
||||
inStream.readFully(messageBodyBytes);
|
||||
|
||||
//Read the message CRC.
|
||||
byte[] messageCRCBytes = new byte[CRCLength];
|
||||
inStream.readFully(messageCRCBytes);
|
||||
|
||||
//Put the head + body + crc into one large array.
|
||||
ByteBuffer messageBytes = ByteBuffer.allocate(headerBytes.length + messageBodyBytes.length + messageCRCBytes.length);
|
||||
messageBytes.put(headerBytes);
|
||||
messageBytes.put(messageBodyBytes);
|
||||
messageBytes.put(messageCRCBytes);
|
||||
|
||||
return messageBytes.array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and returns the next message object from the socket.
|
||||
* @return The message object. Use instanceof for concrete type.
|
||||
* @throws IOException Thrown when an error occurs while reading from the socket.
|
||||
* @throws InvalidMessageException Thrown when the message is invalid in some way.
|
||||
*/
|
||||
private AC35Data getNextMessage() throws IOException, InvalidMessageException
|
||||
{
|
||||
//Get the next message from the socket as a block of bytes.
|
||||
byte[] messageBytes = this.getNextMessageBytes();
|
||||
|
||||
//Decode the binary message into an appropriate message object.
|
||||
BinaryMessageDecoder decoder = new BinaryMessageDecoder(messageBytes);
|
||||
|
||||
return decoder.decode();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Main loop which reads messages from the socket, and exposes them.
|
||||
*/
|
||||
public void run(){
|
||||
boolean receiverLoop = true;
|
||||
//receiver loop that gets the input
|
||||
while (receiverLoop) {
|
||||
|
||||
//If no heartbeat has been received in more the heartbeat period
|
||||
//then the connection will need to be restarted.
|
||||
//System.out.println("time since last heartbeat: " + timeSinceHeartbeat());//TEMP REMOVE
|
||||
long heartBeatPeriod = 10 * 1000;
|
||||
if (timeSinceHeartbeat() > heartBeatPeriod) {
|
||||
System.out.println("Connection has stopped, trying to reconnect.");
|
||||
|
||||
//Attempt to reconnect the socket.
|
||||
try {//This attempt doesn't really work. Under what circumstances would
|
||||
this.connectionSocket = new Socket(this.connectionSocket.getInetAddress(), this.connectionSocket.getPort());
|
||||
//this.connectionSocket.connect(this.connectionSocket.getRemoteSocketAddress());
|
||||
//Reset the heartbeat timer.
|
||||
this.lastHeartbeatTime = System.currentTimeMillis();
|
||||
}
|
||||
catch (IOException e) {
|
||||
System.err.println("Unable to reconnect.");
|
||||
|
||||
//Wait 500ms. Ugly hack, should refactor.
|
||||
long waitPeriod = 500;
|
||||
long waitTimeStart = System.currentTimeMillis() + waitPeriod;
|
||||
|
||||
while (System.currentTimeMillis() < waitTimeStart){
|
||||
//Nothing. Busyloop.
|
||||
}
|
||||
|
||||
//Swallow the exception.
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Reads the next message.
|
||||
AC35Data message;
|
||||
try {
|
||||
message = this.getNextMessage();
|
||||
}
|
||||
catch (InvalidMessageException | IOException e) {
|
||||
//Prints exception to stderr, and iterate loop (that is, read the next message).
|
||||
System.err.println("Unable to read message: " + e.getMessage());
|
||||
try {
|
||||
inStream.reset();
|
||||
} catch (IOException e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
//Continue to the next loop iteration/message.
|
||||
continue;
|
||||
}/*
|
||||
|
||||
//Add it to message queue.
|
||||
this.messagesReceivedQueue.add(message);*/
|
||||
|
||||
|
||||
//Checks which message is being received and does what is needed for that message.
|
||||
//Heartbeat.
|
||||
if (message instanceof Heartbeat) {
|
||||
Heartbeat heartbeat = (Heartbeat) message;
|
||||
|
||||
//Check that the heartbeat number is greater than the previous value, and then set the last heartbeat time.
|
||||
if (heartbeat.getSequenceNumber() > this.lastHeartbeatSequenceNum) {
|
||||
lastHeartbeatTime = System.currentTimeMillis();
|
||||
lastHeartbeatSequenceNum = heartbeat.getSequenceNumber();
|
||||
//System.out.println("HeartBeat Message! " + lastHeartbeatSequenceNum);
|
||||
}
|
||||
}
|
||||
//RaceStatus.
|
||||
else if (message instanceof RaceStatus) {
|
||||
RaceStatus raceStatus = (RaceStatus) message;
|
||||
|
||||
//System.out.println("Race Status Message");
|
||||
this.raceStatus = raceStatus;
|
||||
for (BoatStatus boatStatus: this.raceStatus.getBoatStatuses()) {
|
||||
this.boatStatusMap.put(boatStatus.getSourceID(), boatStatus);
|
||||
}
|
||||
setCourseWindDirection(raceStatus.getScaledWindDirection());
|
||||
}
|
||||
//DisplayTextMessage.
|
||||
/*else if (message instanceof DisplayTextMessage) {
|
||||
//System.out.println("Display Text Message");
|
||||
//No decoder for this.
|
||||
}*/
|
||||
//XMLMessage.
|
||||
else if (message instanceof XMLMessage) {
|
||||
XMLMessage xmlMessage = (XMLMessage) message;
|
||||
|
||||
//System.out.println("XML Message!");
|
||||
|
||||
Platform.runLater(()-> {
|
||||
if (xmlMessage.getXmlMsgSubType() == XMLMessage.XMLTypeRegatta) {
|
||||
//System.out.println("Setting Regatta");
|
||||
try {
|
||||
course.setRegattaXMLReader(new RegattaXMLReader(xmlMessage.getXmlMessage()));
|
||||
|
||||
}
|
||||
//TODO REFACTOR should put all of these exceptions behind a RegattaXMLReaderException.
|
||||
catch (IOException | SAXException | ParserConfigurationException e) {
|
||||
System.err.println("Error creating RegattaXMLReader: " + e.getMessage());
|
||||
//Continue to the next loop iteration/message.
|
||||
}
|
||||
|
||||
} else if (xmlMessage.getXmlMsgSubType() == XMLMessage.XMLTypeRace) {
|
||||
//System.out.println("Setting Course");
|
||||
try {
|
||||
course.setStreamedCourseXMLReader(new StreamedCourseXMLReader(xmlMessage.getXmlMessage()));
|
||||
}
|
||||
//TODO REFACTOR should put all of these exceptions behind a StreamedCourseXMLReaderException.
|
||||
catch (IOException | SAXException | ParserConfigurationException | StreamedCourseXMLException e) {
|
||||
System.err.println("Error creating StreamedCourseXMLReader: " + e.getMessage());
|
||||
//Continue to the next loop iteration/message.
|
||||
}
|
||||
|
||||
} else if (xmlMessage.getXmlMsgSubType() == XMLMessage.XMLTypeBoat) {
|
||||
//System.out.println("Setting Boats");
|
||||
try {
|
||||
course.setBoatXMLReader(new BoatXMLReader(xmlMessage.getXmlMessage()));
|
||||
}
|
||||
//TODO REFACTOR should put all of these exceptions behind a BoatXMLReaderException.
|
||||
catch (IOException | SAXException | ParserConfigurationException e) {
|
||||
System.err.println("Error creating BoatXMLReader: " + e.getMessage());
|
||||
//Continue to the next loop iteration/message.
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
//RaceStartStatus.
|
||||
else if (message instanceof RaceStartStatus) {
|
||||
|
||||
//System.out.println("Race Start Status Message");
|
||||
}
|
||||
//YachtEventCode.
|
||||
/*else if (message instanceof YachtEventCode) {
|
||||
YachtEventCode yachtEventCode = (YachtEventCode) message;
|
||||
|
||||
//System.out.println("Yacht Event Code!");
|
||||
//No decoder for this.
|
||||
|
||||
}*/
|
||||
//YachtActionCode.
|
||||
/*else if (message instanceof YachtActionCode) {
|
||||
YachtActionCode yachtActionCode = (YachtActionCode) message;
|
||||
|
||||
//System.out.println("Yacht Action Code!");
|
||||
//No decoder for this.
|
||||
|
||||
}*/
|
||||
//ChatterText.
|
||||
/*else if (message instanceof ChatterText) {
|
||||
ChatterText chatterText = (ChatterText) message;
|
||||
|
||||
//System.out.println("Chatter Text Message!");
|
||||
//No decoder for this.
|
||||
|
||||
}*/
|
||||
//BoatLocation.
|
||||
else if (message instanceof BoatLocation) {
|
||||
BoatLocation boatLocation = (BoatLocation) message;
|
||||
|
||||
//System.out.println("Boat Location!");
|
||||
if (this.boatLocationMap.containsKey(boatLocation.getSourceID())) {
|
||||
//If our boatlocation map already contains a boat location message for this boat, check that the new message is actually for a later timestamp (i.e., newer).
|
||||
if (boatLocation.getTime() > this.boatLocationMap.get(boatLocation.getSourceID()).getTime()){
|
||||
//If it is, replace the old message.
|
||||
this.boatLocationMap.put(boatLocation.getSourceID(), boatLocation);
|
||||
}
|
||||
}else{
|
||||
//If the map _doesn't_ already contain a message for this boat, insert the message.
|
||||
this.boatLocationMap.put(boatLocation.getSourceID(), boatLocation);
|
||||
}
|
||||
}
|
||||
//MarkRounding.
|
||||
else if (message instanceof MarkRounding) {
|
||||
MarkRounding markRounding = (MarkRounding) message;
|
||||
|
||||
//System.out.println("Mark Rounding Message!");
|
||||
|
||||
if (this.markRoundingMap.containsKey(markRounding.getSourceID())) {
|
||||
//If our markRoundingMap already contains a mark rounding message for this boat, check that the new message is actually for a later timestamp (i.e., newer).
|
||||
if (markRounding.getTime() > this.markRoundingMap.get(markRounding.getSourceID()).getTime()){
|
||||
//If it is, replace the old message.
|
||||
this.markRoundingMap.put(markRounding.getSourceID(), markRounding);
|
||||
}
|
||||
}else{
|
||||
//If the map _doesn't_ already contain a message for this boat, insert the message.
|
||||
this.markRoundingMap.put(markRounding.getSourceID(), markRounding);
|
||||
}
|
||||
|
||||
}
|
||||
//CourseWinds.
|
||||
else if (message instanceof CourseWinds) {
|
||||
|
||||
//System.out.println("Course Wind Message!");
|
||||
this.courseWinds = (CourseWinds) message;
|
||||
|
||||
}
|
||||
//AverageWind.
|
||||
else if (message instanceof AverageWind) {
|
||||
|
||||
//System.out.println("Average Wind Message!");
|
||||
this.averageWind = (AverageWind) message;
|
||||
|
||||
}
|
||||
//Unrecognised message.
|
||||
else {
|
||||
System.out.println("Broken Message!");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,156 @@
|
||||
package visualiser.dataInput;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.xml.sax.SAXException;
|
||||
import seng302.Model.Boat;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* XML Reader that reads in a file and initializes
|
||||
* {@link seng302.Mock.StreamedBoat StreamedBoat}s that will be participating
|
||||
* in a race.
|
||||
*/
|
||||
public class BoatXMLReader extends XMLReader {
|
||||
private final Map<Integer, StreamedBoat> streamedBoatMap = new HashMap<>();
|
||||
private Map<Integer, StreamedBoat> participants = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Constructor for Boat XML Reader
|
||||
* @param filePath path of the file
|
||||
* @throws IOException error
|
||||
* @throws SAXException error
|
||||
* @throws ParserConfigurationException error
|
||||
*/
|
||||
public BoatXMLReader(String filePath) throws IOException, SAXException, ParserConfigurationException {
|
||||
this(filePath, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for Boat XML Reader
|
||||
* @param filePath file path to read
|
||||
* @param read whether or not to read and store the files straight away.
|
||||
* @throws IOException error
|
||||
* @throws SAXException error
|
||||
* @throws ParserConfigurationException error
|
||||
*/
|
||||
public BoatXMLReader(String filePath, boolean read) throws IOException, SAXException, ParserConfigurationException {
|
||||
super(filePath);
|
||||
if (read) {
|
||||
read();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for Boat XML Reader
|
||||
* @param xmlString sting to read
|
||||
* @throws IOException error
|
||||
* @throws SAXException error
|
||||
* @throws ParserConfigurationException error
|
||||
*/
|
||||
public BoatXMLReader(InputStream xmlString) throws IOException, SAXException, ParserConfigurationException {
|
||||
super(xmlString);
|
||||
read();
|
||||
}
|
||||
|
||||
public void read() {
|
||||
readSettings();
|
||||
readShapes();
|
||||
readBoats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads boats settings.
|
||||
* INFORMATION FROM HERE IS IGNORED FOR NOW
|
||||
*/
|
||||
private void readSettings() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads different kinds of boat.
|
||||
* INFORMATION FROM HERE IS IGNORED FOR NOW
|
||||
*/
|
||||
private void readShapes() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the boats in the race
|
||||
*/
|
||||
private void readBoats() {
|
||||
Element nBoats = (Element) doc.getElementsByTagName("Boats").item(0);
|
||||
for (int i = 0; i < nBoats.getChildNodes().getLength(); i++) {
|
||||
Node boat = nBoats.getChildNodes().item(i);
|
||||
if (boat.getNodeName().equals("Boat") && boat.getAttributes().getNamedItem("Type").getTextContent().equals("Yacht")) {
|
||||
readSingleBoat(boat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the information about one boat
|
||||
* Ignored values: ShapeID, StoweName, HullNum, Skipper, Type
|
||||
* @param boat The node to read boat data from.
|
||||
*/
|
||||
private void readSingleBoat(Node boat) {
|
||||
StreamedBoat streamedBoat;
|
||||
String country = null;
|
||||
int sourceID = Integer.parseInt(boat.getAttributes().getNamedItem("SourceID").getTextContent());
|
||||
String boatName = boat.getAttributes().getNamedItem("BoatName").getTextContent();
|
||||
String shortName = boat.getAttributes().getNamedItem("ShortName").getTextContent();
|
||||
if (exists(boat, "Country")) country = boat.getAttributes().getNamedItem("Country").getTextContent();
|
||||
|
||||
// Ignore all non participating boats
|
||||
if (participants.containsKey(sourceID)) {
|
||||
|
||||
if (!streamedBoatMap.containsKey(sourceID)) {
|
||||
if (country != null) {
|
||||
streamedBoat = new StreamedBoat(sourceID, boatName, country);
|
||||
} else {
|
||||
streamedBoat = new StreamedBoat(sourceID, boatName, shortName);
|
||||
}
|
||||
streamedBoatMap.put(sourceID, streamedBoat);
|
||||
// Override boat with new boat
|
||||
participants.put(sourceID, streamedBoat);
|
||||
}
|
||||
|
||||
for (int i = 0; i < boat.getChildNodes().getLength(); i++) {
|
||||
Node GPSposition = boat.getChildNodes().item(i);
|
||||
if (GPSposition.getNodeName().equals("GPSposition"))
|
||||
readBoatPositionInformation(sourceID, GPSposition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads the positional information about a boat
|
||||
* Ignored values: FlagPosition, MastTop, Z value of GPSPosition
|
||||
* @param sourceID The source ID of the boat.
|
||||
* @param GPSposition The relative GPS position of the boat.
|
||||
*/
|
||||
private void readBoatPositionInformation(int sourceID, Node GPSposition) {
|
||||
// TODO Get relative point before implementing. (GPSposition is based
|
||||
// off a relative point).
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the participants
|
||||
* @param participants boats participating the race mapped by their source ID's
|
||||
*/
|
||||
public void setParticipants(Map<Integer, StreamedBoat> participants) {
|
||||
this.participants = participants;
|
||||
}
|
||||
|
||||
public List<Boat> getBoats() {
|
||||
return new ArrayList<>(streamedBoatMap.values());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package visualiser.dataInput;
|
||||
|
||||
import seng302.Model.Boat;
|
||||
import seng302.Model.Leg;
|
||||
import seng302.Model.Marker;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An object that holds relevant data for a race. <br>
|
||||
* Information includes: {@link seng302.Model.Boat Boat}s,
|
||||
* {@link seng302.Model.Leg Leg}s, {@link seng302.Model.Marker Marker}s and
|
||||
* the {@link seng302.GPSCoordinate GPSCoordinate}s to create a
|
||||
* {@link seng302.Model.ResizableRaceMap ResizableRaceMap}.
|
||||
*/
|
||||
public interface RaceDataSource {
|
||||
List<Boat> getBoats();
|
||||
List<Leg> getLegs();
|
||||
List<Marker> getMarkers();
|
||||
List<GPSCoordinate> getBoundary();
|
||||
|
||||
ZonedDateTime getZonedDateTime();
|
||||
GPSCoordinate getMapTopLeft();
|
||||
GPSCoordinate getMapBottomRight();
|
||||
}
|
||||
@ -0,0 +1,165 @@
|
||||
package visualiser.dataInput;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
import seng302.GPSCoordinate;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Created by jjg64 on 19/04/17.
|
||||
*/
|
||||
public class RegattaXMLReader extends XMLReader {
|
||||
private int regattaID;
|
||||
private String regattaName;
|
||||
private int raceID = 0;
|
||||
private String courseName;
|
||||
private double centralLatitude;
|
||||
private double centralLongitude;
|
||||
private double centralAltitude;
|
||||
private float utcOffset;
|
||||
private float magneticVariation;
|
||||
|
||||
/**
|
||||
* Constructor for Regatta XML
|
||||
*
|
||||
* @param filePath path of the file
|
||||
* @throws IOException error
|
||||
* @throws SAXException error
|
||||
* @throws ParserConfigurationException error
|
||||
*/
|
||||
public RegattaXMLReader(String filePath) throws IOException, SAXException, ParserConfigurationException {
|
||||
this(filePath, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for Regatta XML
|
||||
*
|
||||
* @param filePath file path to read
|
||||
* @param read whether or not to read and store the files straight away.
|
||||
* @throws IOException error
|
||||
* @throws SAXException error
|
||||
* @throws ParserConfigurationException error
|
||||
*/
|
||||
private RegattaXMLReader(String filePath, boolean read) throws IOException, SAXException, ParserConfigurationException {
|
||||
super(filePath);
|
||||
if (read) {
|
||||
read();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternate Constructor that takes in an inputstream instead
|
||||
* @param xmlString Input stream of the XML
|
||||
* @throws IOException Error with input
|
||||
* @throws SAXException Error with XML Format
|
||||
* @throws ParserConfigurationException Error with XMl contents
|
||||
*/
|
||||
public RegattaXMLReader(InputStream xmlString) throws IOException, SAXException, ParserConfigurationException {
|
||||
super(xmlString);
|
||||
read();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the XML
|
||||
*/
|
||||
private void read() {
|
||||
NodeList attributeConfig = doc.getElementsByTagName("RegattaConfig");
|
||||
Element attributes = (Element) attributeConfig.item(0);
|
||||
makeRegatta(attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the information from the attributes
|
||||
* @param attributes attributes to extract information form.
|
||||
*/
|
||||
private void makeRegatta(Element attributes) {
|
||||
this.regattaID = Integer.parseInt(getTextValueOfNode(attributes, "RegattaID"));
|
||||
this.regattaName = getTextValueOfNode(attributes, "RegattaName");
|
||||
this.courseName = getTextValueOfNode(attributes, "CourseName");
|
||||
this.centralLatitude = Double.parseDouble(getTextValueOfNode(attributes, "CentralLatitude"));
|
||||
this.centralLongitude = Double.parseDouble(getTextValueOfNode(attributes, "CentralLongitude"));
|
||||
this.centralAltitude = Double.parseDouble(getTextValueOfNode(attributes, "CentralAltitude"));
|
||||
this.utcOffset = Float.parseFloat(getTextValueOfNode(attributes, "UtcOffset"));
|
||||
this.magneticVariation = Float.parseFloat(getTextValueOfNode(attributes, "MagneticVariation"));
|
||||
}
|
||||
|
||||
public int getRegattaID() {
|
||||
return regattaID;
|
||||
}
|
||||
|
||||
public void setRegattaID(int ID) {
|
||||
this.regattaID = ID;
|
||||
}
|
||||
|
||||
public String getRegattaName() {
|
||||
return regattaName;
|
||||
}
|
||||
|
||||
public void setRegattaName(String regattaName) {
|
||||
this.regattaName = regattaName;
|
||||
}
|
||||
|
||||
public int getRaceID() {
|
||||
return raceID;
|
||||
}
|
||||
|
||||
public void setRaceID(int raceID) {
|
||||
this.raceID = raceID;
|
||||
}
|
||||
|
||||
public String getCourseName() {
|
||||
return courseName;
|
||||
}
|
||||
|
||||
public void setCourseName(String courseName) {
|
||||
this.courseName = courseName;
|
||||
}
|
||||
|
||||
public double getCentralLatitude() {
|
||||
return centralLatitude;
|
||||
}
|
||||
|
||||
public void setCentralLatitude(double centralLatitude) {
|
||||
this.centralLatitude = centralLatitude;
|
||||
}
|
||||
|
||||
public double getCentralLongitude() {
|
||||
return centralLongitude;
|
||||
}
|
||||
|
||||
public void setCentralLongitude(double centralLongitude) {
|
||||
this.centralLongitude = centralLongitude;
|
||||
}
|
||||
|
||||
public double getCentralAltitude() {
|
||||
return centralAltitude;
|
||||
}
|
||||
|
||||
public void setCentralAltitude(double centralAltitude) {
|
||||
this.centralAltitude = centralAltitude;
|
||||
}
|
||||
|
||||
public float getUtcOffset() {
|
||||
return utcOffset;
|
||||
}
|
||||
|
||||
public void setUtcOffset(float utcOffset) {
|
||||
this.utcOffset = utcOffset;
|
||||
}
|
||||
|
||||
public float getMagneticVariation() {
|
||||
return magneticVariation;
|
||||
}
|
||||
|
||||
public void setMagneticVariation(float magneticVariation) {
|
||||
this.magneticVariation = magneticVariation;
|
||||
}
|
||||
|
||||
public GPSCoordinate getGPSCoordinate() {
|
||||
return new GPSCoordinate(centralLatitude, centralLongitude);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,290 @@
|
||||
package visualiser.dataInput;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
import seng302.GPSCoordinate;
|
||||
import seng302.Model.Leg;
|
||||
import seng302.Model.Marker;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* XML Read for the Course that is being received
|
||||
*/
|
||||
public class StreamedCourseXMLReader extends XMLReader {
|
||||
private static final double COORDINATEPADDING = 0.000;
|
||||
private GPSCoordinate mapTopLeft, mapBottomRight;
|
||||
private final List<GPSCoordinate> boundary = new ArrayList<>();
|
||||
private final Map<Integer,Element> compoundMarks = new HashMap<>();
|
||||
private final Map<Integer, StreamedBoat> participants = new HashMap<>();
|
||||
private final List<Leg> legs = new ArrayList<>();
|
||||
private final List<Marker> markers = new ArrayList<>();
|
||||
private ZonedDateTime creationTimeDate;
|
||||
private ZonedDateTime raceStartTime;
|
||||
private int raceID;
|
||||
private String raceType;
|
||||
private boolean postpone;
|
||||
|
||||
/**
|
||||
* Constructor for Streamed Race XML
|
||||
* @param filePath file path to read
|
||||
* @param read whether or not to read and store the files straight away.
|
||||
* @throws IOException error
|
||||
* @throws SAXException error
|
||||
* @throws ParserConfigurationException error
|
||||
* @throws StreamedCourseXMLException error
|
||||
*/
|
||||
public StreamedCourseXMLReader(String filePath, boolean read) throws IOException, SAXException, ParserConfigurationException, StreamedCourseXMLException {
|
||||
super(filePath);
|
||||
if (read) {
|
||||
read();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for Streamed Race XML
|
||||
* @param xmlString string to read
|
||||
* @throws IOException error
|
||||
* @throws SAXException error
|
||||
* @throws ParserConfigurationException error
|
||||
* @throws StreamedCourseXMLException error
|
||||
*/
|
||||
public StreamedCourseXMLReader(InputStream xmlString) throws IOException, SAXException, ParserConfigurationException, StreamedCourseXMLException {
|
||||
super(xmlString);
|
||||
read();
|
||||
}
|
||||
|
||||
/**
|
||||
* reads
|
||||
* @throws StreamedCourseXMLException error
|
||||
*/
|
||||
private void read() throws StreamedCourseXMLException {
|
||||
readRace();
|
||||
readParticipants();
|
||||
readCourse();
|
||||
}
|
||||
|
||||
/**
|
||||
* reads a race
|
||||
*/
|
||||
private void readRace() {
|
||||
DateTimeFormatter dateFormat = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
||||
Element settings = (Element) doc.getElementsByTagName("Race").item(0);
|
||||
NamedNodeMap raceTimeTag = doc.getElementsByTagName("RaceStartTime").item(0).getAttributes();
|
||||
|
||||
if (raceTimeTag.getNamedItem("Time") != null) dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
|
||||
|
||||
|
||||
raceID = Integer.parseInt(getTextValueOfNode(settings, "RaceID"));
|
||||
raceType = getTextValueOfNode(settings, "RaceType");
|
||||
|
||||
creationTimeDate = ZonedDateTime.parse(getTextValueOfNode(settings, "CreationTimeDate"), dateFormat);
|
||||
|
||||
if (raceTimeTag.getNamedItem("Time") != null) raceStartTime = ZonedDateTime.parse(raceTimeTag.getNamedItem("Time").getTextContent(), dateFormat);
|
||||
else raceStartTime = ZonedDateTime.parse(raceTimeTag.getNamedItem("Start").getTextContent(), dateFormat);
|
||||
|
||||
postpone = Boolean.parseBoolean(raceTimeTag.getNamedItem("Postpone").getTextContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the participants of the race.
|
||||
*/
|
||||
private void readParticipants() {
|
||||
Element nParticipants = (Element) doc.getElementsByTagName("Participants").item(0);
|
||||
nParticipants.getChildNodes().getLength();
|
||||
for (int i = 0; i < nParticipants.getChildNodes().getLength(); i++) {
|
||||
int sourceID;
|
||||
Node yacht = nParticipants.getChildNodes().item(i);
|
||||
if (yacht.getNodeName().equals("Yacht")) {
|
||||
if (exists(yacht, "SourceID")) {
|
||||
sourceID = Integer.parseInt(yacht.getAttributes().getNamedItem("SourceID").getTextContent());
|
||||
participants.put(sourceID, new StreamedBoat(sourceID));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* reads a course
|
||||
* @throws StreamedCourseXMLException error
|
||||
*/
|
||||
private void readCourse() throws StreamedCourseXMLException {
|
||||
readCompoundMarks();
|
||||
readCompoundMarkSequence();
|
||||
readCourseLimit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indexes CompoundMark elements by their ID for use in generating the course, and populates list of Markers.
|
||||
* @throws StreamedCourseXMLException if a CompoundMark element contains an unhandled number of compoundMarks.
|
||||
* @see seng302.Model.Marker
|
||||
*/
|
||||
private void readCompoundMarks() throws StreamedCourseXMLException {
|
||||
Element nCourse = (Element) doc.getElementsByTagName("Course").item(0);
|
||||
for(int i = 0; i < nCourse.getChildNodes().getLength(); i++) {
|
||||
Node compoundMark = nCourse.getChildNodes().item(i);
|
||||
if(compoundMark.getNodeName().equals("CompoundMark")) {
|
||||
int compoundMarkID = getCompoundMarkID((Element) compoundMark);
|
||||
compoundMarks.put(compoundMarkID, (Element)compoundMark);
|
||||
markers.add(getMarker(compoundMarkID));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a Marker from the CompoundMark element with given ID.
|
||||
* @param compoundMarkID index of required CompoundMark element
|
||||
* @return generated Marker
|
||||
* @throws StreamedCourseXMLException if CompoundMark element contains unhandled number of compoundMarks
|
||||
* @see seng302.Model.Marker
|
||||
*/
|
||||
private Marker getMarker(int compoundMarkID) throws StreamedCourseXMLException {
|
||||
Element compoundMark = compoundMarks.get(compoundMarkID);
|
||||
NodeList nMarks = compoundMark.getElementsByTagName("Mark");
|
||||
Marker marker;
|
||||
|
||||
switch(nMarks.getLength()) {
|
||||
case 1: marker = new Marker(getCoordinate((Element)nMarks.item(0)),getSourceId((Element)nMarks.item(0))); break;
|
||||
case 2: marker = new Marker(getCoordinate((Element)nMarks.item(0)), getCoordinate((Element)nMarks.item(1)),
|
||||
getSourceId((Element)nMarks.item(0)), getSourceId((Element)nMarks.item(1))); break;
|
||||
default: throw new StreamedCourseXMLException();
|
||||
}
|
||||
|
||||
return marker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the GPS Coordinates from a XMl Element
|
||||
* @param mark Element to Extract from
|
||||
* @return the GPS coordinate that it reflects
|
||||
*/
|
||||
private GPSCoordinate getCoordinate(Element mark) {
|
||||
double lat = Double.parseDouble(mark.getAttribute("TargetLat"));
|
||||
double lon = Double.parseDouble(mark.getAttribute("TargetLng"));
|
||||
return new GPSCoordinate(lat,lon);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the SourceID from a XML ELement
|
||||
* @param mark Element to Extract from
|
||||
* @return the Source ID of the Extracted Element.
|
||||
*/
|
||||
private int getSourceId(Element mark) {
|
||||
String sourceId = mark.getAttribute("SourceID");
|
||||
if (sourceId.isEmpty()){
|
||||
return 0;
|
||||
}
|
||||
return Integer.parseInt(sourceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads "compoundMarkID" attribute of CompoundMark or Corner element
|
||||
* @param element with "compoundMarkID" attribute
|
||||
* @return value of "compoundMarkID" attribute
|
||||
*/
|
||||
private int getCompoundMarkID(Element element) {
|
||||
return Integer.parseInt(element.getAttribute("CompoundMarkID"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads "name" attribute of CompoundMark element with corresponding CompoundMarkID
|
||||
* @param compoundMarkID unique ID for CompoundMark element
|
||||
* @return value of "name" attribute
|
||||
*/
|
||||
private String getCompoundMarkName(int compoundMarkID) {
|
||||
return compoundMarks.get(compoundMarkID).getAttribute("Name");
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates list of legs given CompoundMarkSequence element and referenced CompoundMark elements.
|
||||
* @throws StreamedCourseXMLException if markers cannot be resolved from CompoundMark
|
||||
*/
|
||||
private void readCompoundMarkSequence() throws StreamedCourseXMLException {
|
||||
Element nCompoundMarkSequence = (Element) doc.getElementsByTagName("CompoundMarkSequence").item(0);
|
||||
NodeList nCorners = nCompoundMarkSequence.getElementsByTagName("Corner");
|
||||
Element markXML = (Element)nCorners.item(0);
|
||||
Marker lastMarker = getMarker(getCompoundMarkID(markXML));
|
||||
String legName = getCompoundMarkName(getCompoundMarkID(markXML));
|
||||
for(int i = 1; i < nCorners.getLength(); i++) {
|
||||
markXML = (Element)nCorners.item(i);
|
||||
Marker currentMarker = getMarker(getCompoundMarkID(markXML));
|
||||
legs.add(new Leg(legName, lastMarker, currentMarker, i-1));
|
||||
lastMarker = currentMarker;
|
||||
legName = getCompoundMarkName(getCompoundMarkID(markXML));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the course boundary limitations of the course recieved.
|
||||
*/
|
||||
private void readCourseLimit() {
|
||||
Element nCourseLimit = (Element) doc.getElementsByTagName("CourseLimit").item(0);
|
||||
for(int i = 0; i < nCourseLimit.getChildNodes().getLength(); i++) {
|
||||
Node limit = nCourseLimit.getChildNodes().item(i);
|
||||
if (limit.getNodeName().equals("Limit")) {
|
||||
double lat = Double.parseDouble(limit.getAttributes().getNamedItem("Lat").getTextContent());
|
||||
double lon = Double.parseDouble(limit.getAttributes().getNamedItem("Lon").getTextContent());
|
||||
boundary.add(new GPSCoordinate(lat, lon));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
double maxLatitude = boundary.stream().max(Comparator.comparingDouble(GPSCoordinate::getLatitude)).get().getLatitude() + COORDINATEPADDING;
|
||||
double maxLongitude = boundary.stream().max(Comparator.comparingDouble(GPSCoordinate::getLongitude)).get().getLongitude() + COORDINATEPADDING;
|
||||
double minLatitude = boundary.stream().min(Comparator.comparingDouble(GPSCoordinate::getLatitude)).get().getLatitude() + COORDINATEPADDING;
|
||||
double minLongitude = boundary.stream().min(Comparator.comparingDouble(GPSCoordinate::getLongitude)).get().getLongitude() + COORDINATEPADDING;
|
||||
|
||||
mapTopLeft = new GPSCoordinate(minLatitude, minLongitude);
|
||||
mapBottomRight = new GPSCoordinate(maxLatitude, maxLongitude);
|
||||
}
|
||||
|
||||
public List<GPSCoordinate> getBoundary() {
|
||||
return boundary;
|
||||
}
|
||||
|
||||
public GPSCoordinate getMapTopLeft() {
|
||||
return mapTopLeft;
|
||||
}
|
||||
|
||||
public GPSCoordinate getMapBottomRight() {
|
||||
return mapBottomRight;
|
||||
}
|
||||
|
||||
public List<Leg> getLegs() {
|
||||
return legs;
|
||||
}
|
||||
|
||||
public List<Marker> getMarkers() { return markers; }
|
||||
|
||||
public Double getPadding() {
|
||||
return COORDINATEPADDING;
|
||||
}
|
||||
|
||||
public ZonedDateTime getRaceStartTime() {
|
||||
return raceStartTime;
|
||||
}
|
||||
|
||||
public int getRaceID() {
|
||||
return raceID;
|
||||
}
|
||||
|
||||
public String getRaceType() {
|
||||
return raceType;
|
||||
}
|
||||
|
||||
public boolean isPostpone() {
|
||||
return postpone;
|
||||
}
|
||||
|
||||
public Map<Integer, StreamedBoat> getParticipants() {
|
||||
return participants;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package visualiser.dataInput;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* The abstract class for reading in XML race data.
|
||||
*/
|
||||
public abstract class XMLReader {
|
||||
|
||||
protected Document doc;
|
||||
|
||||
protected XMLReader(String filePath) throws ParserConfigurationException, IOException, SAXException {
|
||||
InputStream fXmlFile = getClass().getClassLoader().getResourceAsStream(filePath);
|
||||
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
|
||||
doc = dBuilder.parse(fXmlFile);
|
||||
doc.getDocumentElement().normalize();
|
||||
}
|
||||
|
||||
protected XMLReader(InputStream xmlInput) throws ParserConfigurationException, IOException, SAXException {
|
||||
|
||||
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
|
||||
doc = dBuilder.parse(xmlInput);
|
||||
}
|
||||
|
||||
public Document getDocument() {
|
||||
return doc;
|
||||
}
|
||||
|
||||
protected String getTextValueOfNode(Element n, String tagName) {
|
||||
return n.getElementsByTagName(tagName).item(0).getTextContent();
|
||||
}
|
||||
|
||||
protected boolean exists(Node node, String attribute) {
|
||||
return node.getAttributes().getNamedItem(attribute) != null;
|
||||
}
|
||||
|
||||
public String getAttribute(Element n, String attr) {
|
||||
return n.getAttribute(attr);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package visualiser.exceptions;
|
||||
|
||||
/**
|
||||
* Created by cbt24 on 25/04/17.
|
||||
*/
|
||||
public class StreamedCourseXMLException extends Throwable {
|
||||
}
|
||||
@ -0,0 +1,284 @@
|
||||
package visualiser.model;
|
||||
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Class that processes user selected annotation visibility options to
|
||||
* display the requested information on the
|
||||
* {@link seng302.Model.ResizableRaceMap ResizbleRaceMap}. These are displayed
|
||||
* via the {@link seng302.Controllers.RaceController RaceController}. <br>
|
||||
* Annotation options for a {@link seng302.Model.Boat Boat} include: its name,
|
||||
* abbreviation, speed, the time since it passed the last
|
||||
* {@link seng302.Model.Marker Marker}, estimated time to the next marker,
|
||||
* and a path it has travelled made up of
|
||||
* {@link seng302.Model.TrackPoint TrackPoint}s.
|
||||
*/
|
||||
public class Annotations {
|
||||
private ResizableRaceCanvas raceMap;
|
||||
|
||||
// checkable objects in the anchor pane
|
||||
private Map<String, CheckBox> checkBoxes = new HashMap<>();
|
||||
private Map<String, Toggle> annoToggles = new HashMap<>();
|
||||
|
||||
// maps of selected and saved annotations
|
||||
private Map<String, Boolean> importantAnno = new HashMap<>();
|
||||
private Map<String, Boolean> annoShownBeforeHide = new HashMap<>();
|
||||
|
||||
// string values match the fx:id value of check boxes
|
||||
private static String nameCheckAnno = "showName";
|
||||
private static String abbrevCheckAnno = "showAbbrev";
|
||||
private static String speedCheckAnno = "showSpeed";
|
||||
private static String pathCheckAnno = "showBoatPath";
|
||||
private static String timeCheckAnno = "showTime";
|
||||
private static String estTimeCheckAnno = "showEstTime";
|
||||
|
||||
// string values match the fx:id value of radio buttons
|
||||
private static String noBtn = "noBtn";
|
||||
private static String hideBtn = "hideAnnoRBtn";
|
||||
private static String showBtn = "showAnnoRBtn";
|
||||
private static String partialBtn = "partialAnnoRBtn";
|
||||
private static String importantBtn = "importantAnnoRBtn";
|
||||
|
||||
private Boolean selectShow = false;
|
||||
private String buttonChecked;
|
||||
private String prevBtnChecked;
|
||||
@FXML Button saveAnnoBtn;
|
||||
|
||||
/**
|
||||
* Constructor to set up and display initial annotations
|
||||
* @param annotationPane javaFX pane containing annotation options
|
||||
* @param raceMap the canvas to update annotation displays
|
||||
*/
|
||||
public Annotations(AnchorPane annotationPane, ResizableRaceCanvas raceMap){
|
||||
this.raceMap = raceMap;
|
||||
|
||||
for (Node child : annotationPane.getChildren()) {
|
||||
// collect all check boxes into a map
|
||||
if (child.getClass()==CheckBox.class){
|
||||
checkBoxes.put(child.getId(), (CheckBox)child);
|
||||
}
|
||||
// collect annotation toggle radio buttons into a map
|
||||
else if (child.getClass()== RadioButton.class){
|
||||
//annotationGroup.getToggles().add((RadioButton)child);
|
||||
annoToggles.put(child.getId(), (RadioButton)child);
|
||||
}
|
||||
else if (child.getClass() == Button.class){
|
||||
saveAnnoBtn = (Button)child;
|
||||
}
|
||||
}
|
||||
initializeAnnotations();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set up initial boat annotations and shows all data.
|
||||
* Defines partial annotations.
|
||||
* Creates listeners for when the user selects a different annotation
|
||||
* visibility.
|
||||
*/
|
||||
public void initializeAnnotations() {
|
||||
for (Map.Entry<String, CheckBox> checkBox : checkBoxes.entrySet())
|
||||
{ annoShownBeforeHide.put(checkBox.getKey(), true); }
|
||||
|
||||
addCheckBoxListeners();
|
||||
addSaveAnnoListener();
|
||||
addAnnoToggleListeners();
|
||||
|
||||
annoToggles.get(showBtn).setSelected(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates listeners for each checkbox so the annotation display is
|
||||
* updated when a user selects a different level of annotation visibility.
|
||||
*/
|
||||
private void addCheckBoxListeners(){
|
||||
//listener for show name in annotation
|
||||
checkBoxes.get(nameCheckAnno).selectedProperty()
|
||||
.addListener((ov, old_val, new_val) -> {
|
||||
if (old_val != new_val) {
|
||||
raceMap.toggleAnnoName();
|
||||
storeCurrentAnnotationState(nameCheckAnno, new_val);
|
||||
raceMap.update();
|
||||
}
|
||||
});
|
||||
|
||||
//listener for show abbreviation for annotation
|
||||
checkBoxes.get(abbrevCheckAnno).selectedProperty()
|
||||
.addListener((ov, old_val, new_val) -> {
|
||||
if (old_val != new_val) {
|
||||
raceMap.toggleAnnoAbbrev();
|
||||
storeCurrentAnnotationState(abbrevCheckAnno, new_val);
|
||||
raceMap.update();
|
||||
}
|
||||
});
|
||||
|
||||
//listener for show boat path for annotation
|
||||
checkBoxes.get(pathCheckAnno).selectedProperty()
|
||||
.addListener((ov, old_val, new_val) -> {
|
||||
if (old_val != new_val) {
|
||||
raceMap.toggleBoatPath();
|
||||
storeCurrentAnnotationState(pathCheckAnno, new_val);
|
||||
raceMap.update();
|
||||
}
|
||||
});
|
||||
|
||||
//listener to show speed for annotation
|
||||
checkBoxes.get(speedCheckAnno).selectedProperty()
|
||||
.addListener((ov, old_val, new_val) -> {
|
||||
if (old_val != new_val) {
|
||||
raceMap.toggleAnnoSpeed();
|
||||
storeCurrentAnnotationState(speedCheckAnno, new_val);
|
||||
raceMap.update();
|
||||
}
|
||||
});
|
||||
|
||||
//listener to show time for annotation
|
||||
checkBoxes.get(timeCheckAnno).selectedProperty()
|
||||
.addListener((ov, old_val, new_val) -> {
|
||||
if (old_val != new_val) {
|
||||
raceMap.toggleAnnoTime();
|
||||
storeCurrentAnnotationState(timeCheckAnno, new_val);
|
||||
raceMap.update();
|
||||
}
|
||||
});
|
||||
|
||||
//listener to show estimated time for annotation
|
||||
checkBoxes.get(estTimeCheckAnno).selectedProperty()
|
||||
.addListener((ov, old_val, new_val) -> {
|
||||
if (old_val != new_val) {
|
||||
raceMap.toggleAnnoEstTime();
|
||||
storeCurrentAnnotationState(estTimeCheckAnno, new_val);
|
||||
raceMap.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a listener so the system knows when to save a users currently
|
||||
* selected annotation options as important for future use.
|
||||
*/
|
||||
private void addSaveAnnoListener(){
|
||||
//listener to save currently selected annotations as important
|
||||
saveAnnoBtn.setOnAction(event -> {
|
||||
importantAnno.clear();
|
||||
for (Map.Entry<String, CheckBox> checkBox : checkBoxes.entrySet()){
|
||||
importantAnno.put(checkBox.getKey(),
|
||||
checkBox.getValue().isSelected());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates listeners for each visibility option so that the annotation
|
||||
* display is updated when a user selects a different level of annotation
|
||||
* visibility.
|
||||
*/
|
||||
private void addAnnoToggleListeners(){
|
||||
//listener for hiding all annotations
|
||||
RadioButton hideAnnoRBtn = (RadioButton)annoToggles.get(hideBtn);
|
||||
hideAnnoRBtn.setOnAction((e)->{
|
||||
buttonChecked = hideBtn;
|
||||
selectShow = false;
|
||||
for (Map.Entry<String, CheckBox> checkBox : checkBoxes.entrySet()){
|
||||
checkBox.getValue().setSelected(false);
|
||||
}
|
||||
raceMap.update();
|
||||
buttonChecked = noBtn;
|
||||
prevBtnChecked = hideBtn;
|
||||
selectShow = true;
|
||||
});
|
||||
|
||||
//listener for showing previously visible annotations
|
||||
RadioButton showAnnoRBTN = (RadioButton)annoToggles.get(showBtn);
|
||||
showAnnoRBTN.setOnAction((e)->{
|
||||
if (selectShow) {
|
||||
buttonChecked = showBtn;
|
||||
for (Map.Entry<String, CheckBox> checkBox : checkBoxes.entrySet()){
|
||||
checkBox.getValue().setSelected(
|
||||
annoShownBeforeHide.get(checkBox.getKey()));
|
||||
}
|
||||
raceMap.update();
|
||||
buttonChecked = noBtn;
|
||||
prevBtnChecked = showBtn;
|
||||
}
|
||||
selectShow = true;
|
||||
});
|
||||
|
||||
//listener for showing a predetermined subset of annotations
|
||||
RadioButton partialAnnoRBTN = (RadioButton)annoToggles.get(partialBtn);
|
||||
partialAnnoRBTN.setOnAction((e)->{
|
||||
selectShow = false;
|
||||
buttonChecked = partialBtn;
|
||||
for (Map.Entry<String, CheckBox> checkBox : checkBoxes.entrySet()){
|
||||
// the checkbox defaults for partial annotations
|
||||
if (checkBox.getKey().equals(abbrevCheckAnno)
|
||||
|| checkBox.getKey().equals(speedCheckAnno)){
|
||||
checkBox.getValue().setSelected(true);
|
||||
}
|
||||
else { checkBox.getValue().setSelected(false); }
|
||||
}
|
||||
raceMap.update();
|
||||
buttonChecked = noBtn;
|
||||
prevBtnChecked = partialBtn;
|
||||
selectShow = true;
|
||||
});
|
||||
|
||||
//listener for showing all saved important annotations
|
||||
RadioButton importantAnnoRBTN = (RadioButton)annoToggles.get(importantBtn);
|
||||
importantAnnoRBTN.setOnAction((e) ->{
|
||||
selectShow = false;
|
||||
buttonChecked = importantBtn;
|
||||
if (importantAnno.size()>0){
|
||||
for (Map.Entry<String, CheckBox> checkBox : checkBoxes.entrySet()){
|
||||
checkBox.getValue().setSelected
|
||||
(importantAnno.get(checkBox.getKey()));
|
||||
}
|
||||
}
|
||||
buttonChecked = noBtn;
|
||||
prevBtnChecked = importantBtn;
|
||||
selectShow = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current state of an annotation so that when a user
|
||||
* deselects the hidden visibility option, the previous annotations will
|
||||
* be displayed again.
|
||||
* @param dictionaryAnnotationKey annotation checkbox
|
||||
* @param selected boolean value representing the state of the checkbox
|
||||
*/
|
||||
private void storeCurrentAnnotationState(String dictionaryAnnotationKey, boolean selected){
|
||||
if (buttonChecked != hideBtn) {
|
||||
//if we are checking the box straight out of hide instead of using the radio buttons
|
||||
annoShownBeforeHide.put(dictionaryAnnotationKey, selected);
|
||||
if (prevBtnChecked == hideBtn && buttonChecked == noBtn){
|
||||
storeCurrentAnnotationDictionary();
|
||||
}
|
||||
if (buttonChecked == noBtn) {
|
||||
selectShow = false;
|
||||
annoToggles.get(showBtn).setSelected(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores all current annotation states so that when a user
|
||||
* deselects the hidden visibility option, the previous annotations will
|
||||
* be displayed again.
|
||||
*/
|
||||
private void storeCurrentAnnotationDictionary(){
|
||||
for (Map.Entry<String, CheckBox> checkBox : checkBoxes.entrySet()){
|
||||
annoShownBeforeHide.put(checkBox.getKey(),
|
||||
checkBox.getValue().isSelected());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package visualiser.model;
|
||||
|
||||
/**
|
||||
* It is a coordinate representing a location on the
|
||||
* {@link seng302.Model.ResizableRaceMap ResizableRaceMap}.
|
||||
* It has been converted from a {@link seng302.GPSCoordinate GPSCoordinate}
|
||||
* to display objects in their relative positions.
|
||||
*/
|
||||
public class GraphCoordinate {
|
||||
private final int x;
|
||||
private final int y;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*
|
||||
* @param x X coordinate.
|
||||
* @param y Y coordinate.
|
||||
*/
|
||||
public GraphCoordinate(int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the X coordinate.
|
||||
*
|
||||
* @return x axis Coordinate.
|
||||
*/
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Y coordinate.
|
||||
*
|
||||
* @return y axis Coordinate.
|
||||
*/
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,133 @@
|
||||
package visualiser.model;
|
||||
|
||||
import com.github.bfsmith.geotimezone.TimeZoneLookup;
|
||||
import com.github.bfsmith.geotimezone.TimeZoneResult;
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import seng302.GPSCoordinate;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* This class is used to implement a clock which keeps track of and
|
||||
* displays times relevant to a race. This is displayed on the
|
||||
* {@link seng302.Model.ResizableRaceCanvas ResizableRaceCanvas} via the
|
||||
* {@link seng302.Controllers.RaceController RaceController} and the
|
||||
* {@link seng302.Controllers.StartController StartController}.
|
||||
*/
|
||||
public class RaceClock implements Runnable {
|
||||
private long lastTime;
|
||||
private final ZoneId zoneId;
|
||||
private ZonedDateTime time;
|
||||
private ZonedDateTime startingTime;
|
||||
private final StringProperty timeString;
|
||||
private StringProperty duration;
|
||||
|
||||
public RaceClock(ZonedDateTime zonedDateTime) {
|
||||
this.zoneId = zonedDateTime.getZone();
|
||||
this.timeString = new SimpleStringProperty();
|
||||
this.duration = new SimpleStringProperty();
|
||||
this.time = zonedDateTime;
|
||||
setTime(time);
|
||||
}
|
||||
|
||||
public static ZonedDateTime getCurrentZonedDateTime(GPSCoordinate gpsCoordinate) {
|
||||
TimeZoneLookup timeZoneLookup = new TimeZoneLookup();
|
||||
TimeZoneResult timeZoneResult = timeZoneLookup.getTimeZone(gpsCoordinate.getLatitude(), gpsCoordinate.getLongitude());
|
||||
ZoneId zone = ZoneId.of(timeZoneResult.getResult());
|
||||
return LocalDateTime.now(zone).atZone(zone);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
new AnimationTimer() {
|
||||
@Override
|
||||
public void handle(long now) {
|
||||
updateTime();
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets time to arbitrary zoned time.
|
||||
*
|
||||
* @param time arbitrary time with timezone.
|
||||
*/
|
||||
private void setTime(ZonedDateTime time) {
|
||||
this.time = time;
|
||||
this.timeString.set(DateTimeFormatter.ofPattern("HH:mm:ss dd/MM/YYYY Z").format(time));
|
||||
this.lastTime = System.currentTimeMillis();
|
||||
|
||||
|
||||
if(startingTime != null) {
|
||||
long seconds = Duration.between(startingTime.toLocalDateTime(), time.toLocalDateTime()).getSeconds();
|
||||
if(seconds < 0)
|
||||
duration.set(String.format("Starting in: %02d:%02d:%02d", -seconds/3600, -(seconds%3600)/60, -seconds%60));
|
||||
else
|
||||
duration.set(String.format("Time: %02d:%02d:%02d", seconds/3600, (seconds%3600)/60, seconds%60));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets time to given UTC time in seconds from Unix epoch, preserving timezone.
|
||||
* @param time UTC time
|
||||
*/
|
||||
public void setUTCTime(long time) {
|
||||
Date utcTime = new Date(time);
|
||||
setTime(utcTime.toInstant().atZone(this.zoneId));
|
||||
}
|
||||
|
||||
public ZonedDateTime getStartingTime() {
|
||||
return startingTime;
|
||||
}
|
||||
|
||||
public void setStartingTime(ZonedDateTime startingTime) {
|
||||
this.startingTime = startingTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ZonedDateTime corresponding to local time zone and given UTC time.
|
||||
* @param time time in mills
|
||||
* @return local date time
|
||||
*/
|
||||
public ZonedDateTime getLocalTime(long time) {
|
||||
Date utcTime = new Date(time);
|
||||
return utcTime.toInstant().atZone(this.zoneId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates time by duration elapsed since last update.
|
||||
*/
|
||||
private void updateTime() {
|
||||
this.time = this.time.plus(Duration.of(System.currentTimeMillis() - this.lastTime, ChronoUnit.MILLIS));
|
||||
this.lastTime = System.currentTimeMillis();
|
||||
setTime(time);
|
||||
}
|
||||
|
||||
public String getDuration() {
|
||||
return duration.get();
|
||||
}
|
||||
|
||||
public StringProperty durationProperty() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public String getTimeZone() {
|
||||
return zoneId.toString();
|
||||
}
|
||||
|
||||
public ZonedDateTime getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public StringProperty timeStringProperty() {
|
||||
return timeString;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
package visualiser.model;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
|
||||
/**
|
||||
* Created by cbt24 on 3/05/17.
|
||||
*/
|
||||
public class RaceConnection {
|
||||
private final StringProperty hostname;
|
||||
private final int port;
|
||||
private final StringProperty status;
|
||||
|
||||
public RaceConnection(String hostname, int port) {
|
||||
this.hostname = new SimpleStringProperty(hostname);
|
||||
this.port = port;
|
||||
this.status = new SimpleStringProperty("");
|
||||
check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to create a socket to hostname and port, indicates status after test.
|
||||
* @return true if socket can connect
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public boolean check() {
|
||||
InetSocketAddress i = new InetSocketAddress(hostname.get(), port);
|
||||
try (Socket s = new Socket()){
|
||||
s.connect(i, 5000);
|
||||
status.set("Ready");
|
||||
return true;
|
||||
} catch (IOException e) {}
|
||||
|
||||
status.set("Offline");
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getHostname() {
|
||||
return hostname.get();
|
||||
}
|
||||
|
||||
public StringProperty hostnameProperty() {
|
||||
return hostname;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public StringProperty statusProperty() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
package visualiser.model;
|
||||
|
||||
/**
|
||||
* The base size of the map to be used for the
|
||||
* {@link seng302.Model.ResizableRaceMap ResizableRaceMap} and
|
||||
* {@link seng302.Model.ResizableRaceCanvas ResizableRaceCanvas}. It is used
|
||||
* to convert {@link seng302.GPSCoordinate GPSCoordinate}s to relative
|
||||
* {@link seng302.GraphCoordinate GraphCoordinate}s.
|
||||
*/
|
||||
public class RaceMap {
|
||||
private final double x1;
|
||||
private final double x2;
|
||||
private final double y1;
|
||||
private final double y2;
|
||||
private int width, height;
|
||||
|
||||
/**
|
||||
* Constructor Method.
|
||||
*
|
||||
* @param x1 Longitude of the top left point.
|
||||
* @param y1 Latitude of the top left point.
|
||||
* @param x2 Longitude of the top right point.
|
||||
* @param y2 Latitude of the top right point.
|
||||
* @param width width that the Canvas the race is to be drawn on is.
|
||||
* @param height height that the Canvas the race is to be drawn on is.
|
||||
*/
|
||||
public RaceMap(double y1, double x1, double y2, double x2, int height, int width) {
|
||||
this.x1 = x1;
|
||||
this.x2 = x2;
|
||||
this.y1 = y1;
|
||||
this.y2 = y2;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts GPS coordinates to coordinates for container
|
||||
*
|
||||
* @param lat GPS latitude
|
||||
* @param lon GPS longitude
|
||||
* @return GraphCoordinate (pair of doubles)
|
||||
* @see GraphCoordinate
|
||||
*/
|
||||
private GraphCoordinate convertGPS(double lat, double lon) {
|
||||
int difference = Math.abs(width - height);
|
||||
int size = width;
|
||||
if (width > height) {
|
||||
size = height;
|
||||
return new GraphCoordinate((int) ((size * (lon - x1) / (x2 - x1)) + difference / 2), (int) (size - (size * (lat - y1) / (y2 - y1))));
|
||||
} else {
|
||||
return new GraphCoordinate((int) (size * (lon - x1) / (x2 - x1)), (int) ((size - (size * (lat - y1) / (y2 - y1))) + difference / 2));
|
||||
}
|
||||
|
||||
//return new GraphCoordinate((int) (width * (lon - x1) / (x2 - x1)), (int) (height - (height * (lat - y1) / (y2 - y1))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the GPS Coordinate to GraphCoordinates
|
||||
*
|
||||
* @param coordinate GPSCoordinate representation of Latitude and Longitude.
|
||||
* @return GraphCoordinate that the GPS is coordinates are to be displayed on the map.
|
||||
* @see GraphCoordinate
|
||||
* @see GPSCoordinate
|
||||
*/
|
||||
public GraphCoordinate convertGPS(GPSCoordinate coordinate) {
|
||||
return convertGPS(coordinate.getLatitude(), coordinate.getLongitude());
|
||||
}
|
||||
|
||||
public void setWidth(int width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public void setHeight(int height) {
|
||||
this.height = height;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package visualiser.model;
|
||||
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
|
||||
/**
|
||||
* The abstract class for the resizable race canvases.
|
||||
*/
|
||||
public abstract class ResizableCanvas extends Canvas {
|
||||
protected final GraphicsContext gc;
|
||||
|
||||
public ResizableCanvas(){
|
||||
this.gc = this.getGraphicsContext2D();
|
||||
// Redraw canvas when size changes.
|
||||
widthProperty().addListener(evt -> draw());
|
||||
heightProperty().addListener(evt -> draw());
|
||||
|
||||
}
|
||||
|
||||
abstract void draw();
|
||||
|
||||
|
||||
/**
|
||||
* Set the Canvas to resizable.
|
||||
*
|
||||
* @return That the Canvas is resizable.
|
||||
*/
|
||||
@Override
|
||||
public boolean isResizable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the preferred width of the Canvas
|
||||
*
|
||||
* @param width of canvas
|
||||
* @return Returns the width of the Canvas
|
||||
*/
|
||||
@Override
|
||||
public double prefWidth(double width) {
|
||||
return getWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the preferred height of the Canvas
|
||||
*
|
||||
* @param height of canvas
|
||||
* @return Returns the height of the Canvas
|
||||
*/
|
||||
@Override
|
||||
public double prefHeight(double height) {
|
||||
return getHeight();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,392 @@
|
||||
package visualiser.model;
|
||||
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.transform.Rotate;
|
||||
import seng302.Mock.StreamedCourse;
|
||||
import seng302.RaceDataSource;
|
||||
import seng302.RaceMap;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* This JavaFX Canvas is used to update and display details for a
|
||||
* {@link seng302.RaceMap RaceMap} via the
|
||||
* {@link seng302.Controllers.RaceController RaceController}.<br>
|
||||
* It fills it's parent and cannot be downsized. <br>
|
||||
* Details displayed include:
|
||||
* {@link seng302.Model.Boat Boats} (and their
|
||||
* {@link seng302.Model.TrackPoint TrackPoint}s),
|
||||
* {@link seng302.Model.Marker Markers}, a
|
||||
* {@link seng302.Model.RaceClock RaceClock}, a wind direction arrow and
|
||||
* various user selected {@link seng302.Model.Annotations Annotations}.
|
||||
*/
|
||||
public class ResizableRaceCanvas extends ResizableCanvas {
|
||||
private RaceMap map;
|
||||
private List<Boat> boats;
|
||||
private List<Marker> boatMarkers;
|
||||
private boolean annoName = true;
|
||||
private boolean annoAbbrev = true;
|
||||
private boolean annoSpeed = true;
|
||||
private boolean annoPath = true;
|
||||
private boolean annoEstTime = true;
|
||||
private boolean annoTimeSinceLastMark = true;
|
||||
private List<Color> colours;
|
||||
private final List<Marker> markers;
|
||||
private final RaceDataSource raceData;
|
||||
private Map<Integer, Color> boatColours = new HashMap<>();
|
||||
private Node arrow;
|
||||
|
||||
private RaceClock raceClock;
|
||||
|
||||
public ResizableRaceCanvas(RaceDataSource raceData) {
|
||||
super();
|
||||
|
||||
double lat1 = raceData.getMapTopLeft().getLatitude();
|
||||
double long1 = raceData.getMapTopLeft().getLongitude();
|
||||
double lat2 = raceData.getMapBottomRight().getLatitude();
|
||||
double long2 = raceData.getMapBottomRight().getLongitude();
|
||||
|
||||
setMap(new RaceMap(lat1, long1, lat2, long2, (int) getWidth(), (int) getHeight()));
|
||||
|
||||
this.markers = raceData.getMarkers();
|
||||
makeColours();
|
||||
this.raceData = raceData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the boats that are to be displayed in this race.
|
||||
*
|
||||
* @param boats in race
|
||||
*/
|
||||
public void setBoats(List<Boat> boats) {
|
||||
this.boats = boats;
|
||||
mapBoatColours();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the boat markers that are to be displayed in this race.
|
||||
*
|
||||
* @param boatMarkers in race
|
||||
*/
|
||||
public void setBoatMarkers(ObservableList<Marker> boatMarkers) {
|
||||
this.boatMarkers = boatMarkers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the RaceMap that the RaceCanvas is to be displaying for.
|
||||
*
|
||||
* @param map race map
|
||||
*/
|
||||
private void setMap(RaceMap map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
private void displayBoat(Boat boat, double angle, Color colour) {
|
||||
if (boat.getCurrentPosition() != null) {
|
||||
GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition());
|
||||
|
||||
double[] x = {pos.getX() - 6, pos.getX(), pos.getX() + 6};
|
||||
double[] y = {pos.getY() + 12, pos.getY() - 12, pos.getY() + 12};
|
||||
gc.setFill(colour);
|
||||
|
||||
gc.save();
|
||||
rotate(angle, pos.getX(), pos.getY());
|
||||
gc.fillPolygon(x, y, 3);
|
||||
gc.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a line on the map with rectangles on the starting and ending point of the line.
|
||||
*
|
||||
* @param graphCoordinateA Starting Point of the line in GraphCoordinate.
|
||||
* @param graphCoordinateB End Point of the line in GraphCoordinate.
|
||||
* @param paint Colour the line is to coloured.
|
||||
* @see GraphCoordinate
|
||||
* @see Color
|
||||
* @see Paint
|
||||
*/
|
||||
private void displayLine(GraphCoordinate graphCoordinateA, GraphCoordinate graphCoordinateB, Paint paint) {
|
||||
gc.setStroke(paint);
|
||||
gc.setFill(paint);
|
||||
gc.fillOval(graphCoordinateA.getX() - 3, graphCoordinateA.getY() - 3, 6, 6);
|
||||
gc.fillOval(graphCoordinateB.getX() - 3, graphCoordinateB.getY() - 3, 6, 6);
|
||||
gc.strokeLine(graphCoordinateA.getX(), graphCoordinateA.getY(), graphCoordinateB.getX(), graphCoordinateB.getY());
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a point on the Canvas
|
||||
*
|
||||
* @param graphCoordinate Coordinate that the point is to be displayed at.
|
||||
* @param paint Colour that the boat is to be coloured.
|
||||
* @see GraphCoordinate
|
||||
* @see Paint
|
||||
* @see Color
|
||||
*/
|
||||
private void displayPoint(GraphCoordinate graphCoordinate, Paint paint) {
|
||||
gc.setFill(paint);
|
||||
gc.fillOval(graphCoordinate.getX(), graphCoordinate.getY(), 10, 10);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Displays an arrow representing wind direction on the Canvas
|
||||
*
|
||||
* @param angle Angle that the arrow is to be facing in degrees 0 degrees = North (Up).
|
||||
* @see GraphCoordinate
|
||||
*/
|
||||
private void displayWindArrow(double angle) {
|
||||
angle = angle % 360;
|
||||
|
||||
// show direction wind is coming from
|
||||
if (angle<180){angle = angle + 180;}
|
||||
else {angle = angle - 180;}
|
||||
|
||||
if (arrow != null && arrow.getRotate() != angle) {
|
||||
arrow.setRotate(angle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates things on the canvas Note: this must be called in between gc.save() and gc.restore() else they will rotate everything
|
||||
*
|
||||
* @param angle Bearing angle to rotate at in degrees
|
||||
* @param px Pivot point x of rotation.
|
||||
* @param py Pivot point y of rotation.
|
||||
*/
|
||||
private void rotate(double angle, double px, double py) {
|
||||
Rotate r = new Rotate(angle, px, py);
|
||||
gc.setTransform(r.getMxx(), r.getMyx(), r.getMxy(), r.getMyy(), r.getTx(), r.getTy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Display given name and speed of boat at a graph coordinate
|
||||
*
|
||||
* @param name name of the boat
|
||||
* @param abbrev abbreviation of the boat name
|
||||
* @param speed speed of the boat
|
||||
* @param coordinate coordinate the text appears
|
||||
* @param timeSinceLastMark time since the last mark was passed
|
||||
*/
|
||||
private void displayText(String name, String abbrev, double speed, GraphCoordinate coordinate, String estTime, ZonedDateTime timeSinceLastMark) {
|
||||
String text = "";
|
||||
//Check name toggle value
|
||||
if (annoName){
|
||||
text += String.format("%s ", name);
|
||||
}
|
||||
//Check abbreviation toggle value
|
||||
if (annoAbbrev){
|
||||
text += String.format("%s ", abbrev);
|
||||
}
|
||||
//Check speed toggle value
|
||||
if (annoSpeed){
|
||||
text += String.format("%.2fkn ", speed);
|
||||
}
|
||||
if (annoEstTime) {
|
||||
text += estTime;
|
||||
}
|
||||
//Check time since last mark toggle value
|
||||
if(annoTimeSinceLastMark){
|
||||
Duration timeSince = Duration.between(timeSinceLastMark, raceClock.getTime());
|
||||
text += String.format(" %ds ", timeSince.getSeconds());
|
||||
}
|
||||
//String text = String.format("%s, %2$.2fkn", name, speed);
|
||||
long xCoord = coordinate.getX() + 20;
|
||||
long yCoord = coordinate.getY();
|
||||
if (xCoord + (text.length() * 7) >= getWidth()) {
|
||||
xCoord -= text.length() * 7;
|
||||
}
|
||||
if (yCoord - (text.length() * 2) <= 0) {
|
||||
yCoord += 30;
|
||||
}
|
||||
gc.fillText(text, xCoord, yCoord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws race map with up to date data.
|
||||
*/
|
||||
public void update() {
|
||||
this.draw();
|
||||
this.updateBoats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw race markers
|
||||
*/
|
||||
private void drawMarkers() {
|
||||
for(Marker marker: markers) {
|
||||
GraphCoordinate mark1 = this.map.convertGPS(marker.getMark1());
|
||||
// removed drawing of lines between the marks as only
|
||||
// the start and finish line should have a line drawn
|
||||
if(marker.isCompoundMark()) {
|
||||
GraphCoordinate mark2 = this.map.convertGPS(marker.getMark2());
|
||||
displayPoint(mark1, Color.LIMEGREEN);
|
||||
displayPoint(mark2, Color.LIMEGREEN);
|
||||
} else {
|
||||
displayPoint(mark1, Color.GREEN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the Race Map
|
||||
*/
|
||||
public void draw() {
|
||||
|
||||
double width = getWidth();
|
||||
double height = getHeight();
|
||||
|
||||
gc.clearRect(0, 0, width, height);
|
||||
|
||||
if (map == null) {
|
||||
return;//TODO this should return a exception in the future
|
||||
}
|
||||
this.map.setHeight((int) height);
|
||||
this.map.setWidth((int) width);
|
||||
|
||||
gc.setLineWidth(2);
|
||||
updateBoats();
|
||||
drawMarkers();
|
||||
|
||||
//display wind direction arrow - specify origin point and angle - angle now set to random angle
|
||||
if (raceData instanceof StreamedCourse) {
|
||||
displayWindArrow(((StreamedCourse) raceData).getWindDirection());
|
||||
} else {
|
||||
displayWindArrow(150);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Toggle name display in annotation
|
||||
*/
|
||||
public void toggleAnnoName() {
|
||||
annoName = !annoName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle boat path display in annotation
|
||||
*/
|
||||
public void toggleBoatPath() {
|
||||
annoPath = !annoPath;
|
||||
}
|
||||
|
||||
public void toggleAnnoEstTime() {
|
||||
annoEstTime = !annoEstTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle boat time display in annotation
|
||||
*/
|
||||
public void toggleAnnoTime() { annoTimeSinceLastMark = !annoTimeSinceLastMark;}
|
||||
|
||||
/**
|
||||
* Toggle abbreviation display in annotation
|
||||
*/
|
||||
public void toggleAnnoAbbrev() {
|
||||
annoAbbrev = !annoAbbrev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle speed display in annotation
|
||||
*/
|
||||
public void toggleAnnoSpeed() {
|
||||
annoSpeed = !annoSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws boats while race in progress, when leg heading is set.
|
||||
*/
|
||||
private void updateBoats() {
|
||||
if (boats != null) {
|
||||
if (boatColours.size() < boats.size()) mapBoatColours();
|
||||
for (Boat boat : boats) {
|
||||
boolean finished = boat.getCurrentLeg().getName().equals("Finish") || boat.getCurrentLeg().getName().equals("DNF");
|
||||
boolean isStart = boat.isStarted();
|
||||
int sourceID = boat.getSourceID();
|
||||
if (!finished && isStart) {
|
||||
displayBoat(boat, boat.getHeading(), boatColours.get(sourceID));
|
||||
GraphCoordinate wakeFrom = this.map.convertGPS(boat.getCurrentPosition());
|
||||
GraphCoordinate wakeTo = this.map.convertGPS(boat.getWake());
|
||||
displayLine(wakeFrom, wakeTo, boatColours.get(sourceID));
|
||||
} else if (!isStart) {
|
||||
displayBoat(boat, boat.getHeading(), boatColours.get(sourceID));
|
||||
} else {
|
||||
displayBoat(boat, 0, boatColours.get(sourceID));
|
||||
}
|
||||
|
||||
if (Duration.between(boat.getTimeSinceLastMark(), raceClock.getTime()).getSeconds() < 0) {
|
||||
boat.setTimeSinceLastMark(raceClock.getTime());
|
||||
}
|
||||
|
||||
//If the race hasn't started, we set the time since last mark to the current time, to ensure we don't start counting until the race actually starts.
|
||||
if (boat.isStarted() == false) {
|
||||
boat.setTimeSinceLastMark(raceClock.getTime());
|
||||
}
|
||||
|
||||
displayText(boat.toString(), boat.getAbbrev(), boat.getVelocity(), this.map.convertGPS(boat.getCurrentPosition()), boat.getFormattedEstTime(), boat.getTimeSinceLastMark());
|
||||
//TODO this needs to be fixed.
|
||||
drawTrack(boat, boatColours.get(sourceID));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws all track points for a given boat. Colour is set by boat, opacity by track point.
|
||||
* @param boat whose track is displayed
|
||||
* @param colour The color to use for the track.
|
||||
* @see seng302.Model.TrackPoint
|
||||
*/
|
||||
private void drawTrack(Boat boat, Color colour) {
|
||||
if (annoPath) {
|
||||
for (TrackPoint point : boat.getTrack()) {
|
||||
GraphCoordinate scaledCoordinate = this.map.convertGPS(point.getCoordinate());
|
||||
gc.setFill(new Color(colour.getRed(), colour.getGreen(), colour.getBlue(), point.getAlpha()));
|
||||
gc.fillOval(scaledCoordinate.getX(), scaledCoordinate.getY(), 5, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* makes colours
|
||||
*/
|
||||
private void makeColours() {
|
||||
colours = new ArrayList<>(Arrays.asList(
|
||||
Color.BLUEVIOLET,
|
||||
Color.BLACK,
|
||||
Color.RED,
|
||||
Color.ORANGE,
|
||||
Color.DARKOLIVEGREEN,
|
||||
Color.LIMEGREEN,
|
||||
Color.PURPLE,
|
||||
Color.DARKGRAY,
|
||||
Color.YELLOW
|
||||
));
|
||||
}
|
||||
|
||||
public void setArrow(Node arrow) {
|
||||
this.arrow = arrow;
|
||||
}
|
||||
|
||||
public void setRaceClock(RaceClock raceClock) {
|
||||
this.raceClock = raceClock;
|
||||
}
|
||||
|
||||
private void mapBoatColours() {
|
||||
int currentColour = 0;
|
||||
for (Boat boat : boats) {
|
||||
if (!boatColours.containsKey(boat.getSourceID())) {
|
||||
boatColours.put(boat.getSourceID(), colours.get(currentColour));
|
||||
}
|
||||
currentColour = (currentColour + 1) % colours.size();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
package visualiser.model;
|
||||
|
||||
import javafx.scene.paint.Color;
|
||||
import seng302.GPSCoordinate;
|
||||
import seng302.RaceDataSource;
|
||||
import seng302.RaceMap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This JavaFX Canvas is used to generate the size of a
|
||||
* {@link seng302.RaceMap RaceMap} using co-ordinates from a
|
||||
* {@link seng302.RaceDataSource RaceDataSource}. This is done via the
|
||||
* {@link seng302.Controllers.RaceController RaceController}.
|
||||
*/
|
||||
public class ResizableRaceMap extends ResizableCanvas {
|
||||
private RaceMap map;
|
||||
private final List<GPSCoordinate> raceBoundaries;
|
||||
private double[] xpoints = {};
|
||||
private double[] ypoints = {};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param raceData Race which it is taking its information to be displayed from
|
||||
*/
|
||||
public ResizableRaceMap(RaceDataSource raceData){
|
||||
super();
|
||||
raceBoundaries = raceData.getBoundary();
|
||||
|
||||
double lat1 = raceData.getMapTopLeft().getLatitude();
|
||||
double long1 = raceData.getMapTopLeft().getLongitude();
|
||||
double lat2 = raceData.getMapBottomRight().getLatitude();
|
||||
double long2 = raceData.getMapBottomRight().getLongitude();
|
||||
setMap(new RaceMap(lat1, long1, lat2, long2, (int) getWidth(), (int) getHeight()));
|
||||
//draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the map race that it is supposed to be viewing.
|
||||
* @param map the map to be set
|
||||
*/
|
||||
private void setMap(RaceMap map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw boundary of the race.
|
||||
*/
|
||||
private void drawBoundaries() {
|
||||
if (this.raceBoundaries == null) {
|
||||
return;
|
||||
}
|
||||
gc.setFill(Color.AQUA);
|
||||
setRaceBoundCoordinates();
|
||||
|
||||
gc.setLineWidth(1);
|
||||
gc.fillPolygon(xpoints, ypoints, xpoints.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the coordinately of the race boundaries
|
||||
*/
|
||||
private void setRaceBoundCoordinates() {
|
||||
xpoints = new double[this.raceBoundaries.size()];
|
||||
ypoints = new double[this.raceBoundaries.size()];
|
||||
for (int i = 0; i < raceBoundaries.size(); i++) {
|
||||
GraphCoordinate coord = map.convertGPS(raceBoundaries.get(i));
|
||||
xpoints[i] = coord.getX();
|
||||
ypoints[i] = coord.getY();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw update for the canvas
|
||||
*/
|
||||
public void draw(){
|
||||
|
||||
double width = getWidth();
|
||||
double height = getHeight();
|
||||
|
||||
gc.clearRect(0, 0, width, height);
|
||||
|
||||
if (map == null) {
|
||||
return;//TODO this should return a exception in the future
|
||||
}
|
||||
this.map.setHeight((int) height);
|
||||
this.map.setWidth((int) width);
|
||||
|
||||
gc.setLineWidth(2);
|
||||
drawBoundaries();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,180 @@
|
||||
package visualiser.model;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.chart.LineChart;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import javafx.scene.chart.XYChart;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Class to process and modify a sparkline display. This display keeps visual
|
||||
* track of {@link seng302.Model.Boat Boats}s in a race and their current
|
||||
* placing position as they complete each {@link seng302.Model.Leg Leg} by
|
||||
* passing a course {@link seng302.Model.Marker Marker}. <br>
|
||||
* This sparkline is displayed using the
|
||||
* {@link seng302.Controllers.RaceController RaceController}.
|
||||
*/
|
||||
public class Sparkline {
|
||||
private ArrayList<String> colours;
|
||||
private ArrayList<Boat> startBoats = new ArrayList<>();
|
||||
private Map<Integer, String> boatColours = new HashMap<>();
|
||||
private Integer legNum;
|
||||
private Integer sparkLineNumber = 0;
|
||||
@FXML LineChart<Number, Number> sparklineChart;
|
||||
@FXML NumberAxis xAxis;
|
||||
@FXML NumberAxis yAxis;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor to set up initial sparkline (LineChart) object
|
||||
* @param boats boats to display on the sparkline
|
||||
* @param legNum total number of legs in the race
|
||||
* @param sparklineChart javaFX LineChart for the sparkline
|
||||
*/
|
||||
public Sparkline(ObservableList<Boat> boats, Integer legNum,
|
||||
LineChart<Number,Number> sparklineChart) {
|
||||
this.sparklineChart = sparklineChart;
|
||||
this.legNum = legNum;
|
||||
this.yAxis = (NumberAxis)sparklineChart.getYAxis();
|
||||
this.xAxis = (NumberAxis)sparklineChart.getXAxis();
|
||||
startBoats.addAll(boats);
|
||||
|
||||
makeColours();
|
||||
mapBoatColours();
|
||||
createSparkline();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates and sets initial display for Sparkline for race positions.
|
||||
* A data series for each boat in the race is added.
|
||||
* Position numbers are displayed.
|
||||
*/
|
||||
public void createSparkline(){
|
||||
// NOTE: Y axis is in negatives to display correct positions
|
||||
|
||||
// all boats start in 'last' place
|
||||
for (int i=0; i<startBoats.size(); i++){
|
||||
XYChart.Series<Number, Number> series = new XYChart.Series();
|
||||
series.getData().add(new XYChart.Data(0, -startBoats.size()));
|
||||
series.getData().add(new XYChart.Data(0, -startBoats.size()));
|
||||
sparklineChart.getData().add(series);
|
||||
sparklineChart.getData().get(i).getNode().setStyle("-fx-stroke: " +
|
||||
""+boatColours.get(startBoats.get(i).getSourceID())+";");
|
||||
}
|
||||
|
||||
sparklineChart.setCreateSymbols(false);
|
||||
|
||||
// set x axis details
|
||||
xAxis.setAutoRanging(false);
|
||||
xAxis.setTickMarkVisible(false);
|
||||
xAxis.setTickLabelsVisible(false);
|
||||
xAxis.setMinorTickVisible(false);
|
||||
xAxis.setUpperBound((startBoats.size()+1)*legNum);
|
||||
xAxis.setTickUnit((startBoats.size()+1)*legNum);
|
||||
|
||||
// set y axis details
|
||||
yAxis.setLowerBound(-(startBoats.size()+1));
|
||||
yAxis.setUpperBound(0);
|
||||
yAxis.setAutoRanging(false);
|
||||
yAxis.setLabel("Position in Race");
|
||||
yAxis.setTickUnit(1);
|
||||
yAxis.setTickMarkVisible(false);
|
||||
yAxis.setMinorTickVisible(false);
|
||||
|
||||
// hide minus number from displaying on axis
|
||||
yAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(yAxis) {
|
||||
@Override
|
||||
public String toString(Number value) {
|
||||
if ((Double)value == 0.0
|
||||
|| (Double)value < -startBoats.size()){
|
||||
return "";
|
||||
}
|
||||
else {
|
||||
return String.format("%7.0f", -value.doubleValue());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the sparkline to display current boat positions.
|
||||
* New points are plotted to represent each boat when required.
|
||||
* @param boatsInRace current position of the boats in race
|
||||
*/
|
||||
public void updateSparkline(ObservableList<Boat> boatsInRace){
|
||||
int placingVal = boatsInRace.size();
|
||||
sparkLineNumber++;
|
||||
|
||||
for (int i = boatsInRace.size() - 1; i >= 0; i--){
|
||||
for (int j = startBoats.size() - 1; j >= 0; j--){
|
||||
if (boatsInRace.get(i)==startBoats.get(j)){
|
||||
|
||||
// when a boat is on its first leg
|
||||
if (boatsInRace.get(i).getCurrentLeg().getLegNumber()==0){
|
||||
// adjust boats latest point on X axis
|
||||
sparklineChart.getData().get(j).getData().get(1)
|
||||
.setXValue(sparkLineNumber);
|
||||
}
|
||||
|
||||
// when a boat first enters its second leg
|
||||
else if (boatsInRace.get(i).getCurrentLeg().getLegNumber
|
||||
()==1 && sparklineChart.getData().get(j).getData
|
||||
().size()==2){
|
||||
// adjust boats position from start mark
|
||||
sparklineChart.getData().get(j).getData().get(1)
|
||||
.setYValue(-placingVal);
|
||||
sparklineChart.getData().get(j).getData().get(1)
|
||||
.setXValue(sparkLineNumber);
|
||||
sparklineChart.getData().get(j).getData().add(new XYChart.Data<>
|
||||
(sparkLineNumber, -placingVal));
|
||||
}
|
||||
|
||||
// plot new point for boats current position
|
||||
else {
|
||||
sparklineChart.getData().get(j).getData().add
|
||||
(new XYChart.Data<>(sparkLineNumber, -placingVal));
|
||||
}
|
||||
placingVal-=1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void makeColours() {
|
||||
colours = new ArrayList<>(Arrays.asList(
|
||||
colourToHex(Color.BLUEVIOLET),
|
||||
colourToHex(Color.BLACK),
|
||||
colourToHex(Color.RED),
|
||||
colourToHex(Color.ORANGE),
|
||||
colourToHex(Color.DARKOLIVEGREEN),
|
||||
colourToHex(Color.LIMEGREEN),
|
||||
colourToHex(Color.PURPLE),
|
||||
colourToHex(Color.DARKGRAY),
|
||||
colourToHex(Color.YELLOW)
|
||||
));
|
||||
}
|
||||
|
||||
private String colourToHex(Color color) {
|
||||
return String.format( "#%02X%02X%02X",
|
||||
(int)( color.getRed() * 255 ),
|
||||
(int)( color.getGreen() * 255 ),
|
||||
(int)( color.getBlue() * 255 ) );
|
||||
}
|
||||
|
||||
private void mapBoatColours() {
|
||||
int currentColour = 0;
|
||||
for (Boat boat : startBoats) {
|
||||
if (!boatColours.containsKey(boat.getSourceID())) {
|
||||
boatColours.put(boat.getSourceID(), colours.get(currentColour));
|
||||
}
|
||||
currentColour = (currentColour + 1) % colours.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,106 @@
|
||||
package visualiser.model;
|
||||
|
||||
import seng302.GPSCoordinate;
|
||||
import seng302.Model.Boat;
|
||||
import seng302.Model.Leg;
|
||||
import seng302.Model.Marker;
|
||||
import seng302.RaceDataSource;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Observable;
|
||||
|
||||
/**
|
||||
* COurse that the is being received.
|
||||
*/
|
||||
public class StreamedCourse extends Observable implements RaceDataSource {
|
||||
private StreamedCourseXMLReader streamedCourseXMLReader = null;
|
||||
private BoatXMLReader boatXMLReader = null;
|
||||
private RegattaXMLReader regattaXMLReader = null;
|
||||
private double windDirection = 0;
|
||||
|
||||
public StreamedCourse() {}
|
||||
|
||||
/**
|
||||
* Read and set the new XML that has been received.
|
||||
* @param boatXMLReader new XMl of the boats.
|
||||
*/
|
||||
public void setBoatXMLReader(BoatXMLReader boatXMLReader) {
|
||||
this.boatXMLReader = boatXMLReader;
|
||||
if (streamedCourseXMLReader != null && boatXMLReader != null) {
|
||||
this.boatXMLReader.setParticipants(streamedCourseXMLReader.getParticipants());
|
||||
|
||||
boatXMLReader.read();
|
||||
}
|
||||
setChanged();
|
||||
notifyObservers();
|
||||
}
|
||||
|
||||
public StreamedCourseXMLReader getStreamedCourseXMLReader() {
|
||||
return streamedCourseXMLReader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and sets the new Course that has been received
|
||||
* @param streamedCourseXMLReader COurse XML that has been received
|
||||
*/
|
||||
public void setStreamedCourseXMLReader(StreamedCourseXMLReader streamedCourseXMLReader) {
|
||||
this.streamedCourseXMLReader = streamedCourseXMLReader;
|
||||
if (streamedCourseXMLReader != null && boatXMLReader != null) {
|
||||
boatXMLReader.setParticipants(streamedCourseXMLReader.getParticipants());
|
||||
boatXMLReader.read();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and sets the new Regatta that has been received
|
||||
* @param regattaXMLReader Regatta XMl that has been received.
|
||||
*/
|
||||
public void setRegattaXMLReader(RegattaXMLReader regattaXMLReader) {
|
||||
this.regattaXMLReader = regattaXMLReader;
|
||||
setChanged();
|
||||
notifyObservers();
|
||||
}
|
||||
|
||||
public void setWindDirection(double windDirection) {
|
||||
this.windDirection = windDirection;
|
||||
}
|
||||
|
||||
public double getWindDirection() {
|
||||
return windDirection;
|
||||
}
|
||||
|
||||
public boolean hasReadRegatta() { return regattaXMLReader != null; }
|
||||
|
||||
public boolean hasReadBoats() { return boatXMLReader != null; }
|
||||
|
||||
public boolean hasReadCourse() { return streamedCourseXMLReader != null; }
|
||||
|
||||
public String getRegattaName() { return regattaXMLReader.getRegattaName(); }
|
||||
|
||||
public List<Boat> getBoats() {
|
||||
return boatXMLReader.getBoats();
|
||||
}
|
||||
|
||||
public List<Leg> getLegs() {
|
||||
return streamedCourseXMLReader.getLegs();
|
||||
}
|
||||
|
||||
public List<Marker> getMarkers() { return streamedCourseXMLReader.getMarkers(); }
|
||||
|
||||
public List<GPSCoordinate> getBoundary() {
|
||||
return streamedCourseXMLReader.getBoundary();
|
||||
}
|
||||
|
||||
public ZonedDateTime getZonedDateTime() {
|
||||
return streamedCourseXMLReader.getRaceStartTime();
|
||||
}
|
||||
|
||||
public GPSCoordinate getMapTopLeft() {
|
||||
return streamedCourseXMLReader.getMapTopLeft();
|
||||
}
|
||||
|
||||
public GPSCoordinate getMapBottomRight() {
|
||||
return streamedCourseXMLReader.getMapBottomRight();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,285 @@
|
||||
package visualiser.model;
|
||||
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import seng302.Controllers.FinishController;
|
||||
import seng302.Controllers.RaceController;
|
||||
import seng302.GPSCoordinate;
|
||||
import seng302.Model.Boat;
|
||||
import seng302.Model.Leg;
|
||||
import seng302.Model.Marker;
|
||||
import seng302.Networking.Messages.BoatLocation;
|
||||
import seng302.Networking.Messages.BoatStatus;
|
||||
import seng302.Networking.Messages.Enums.BoatStatusEnum;
|
||||
import seng302.VisualiserInput;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The Class used to view the race streamed.
|
||||
*/
|
||||
public class StreamedRace implements Runnable {
|
||||
private final VisualiserInput visualiserInput;
|
||||
private final ObservableList<Boat> startingBoats;
|
||||
private final ObservableList<Marker> boatMarkers;
|
||||
private final List<Leg> legs;
|
||||
private RaceController controller;
|
||||
protected FinishController finishController;
|
||||
private int boatsFinished = 0;
|
||||
private long totalTimeElapsed;
|
||||
|
||||
private int lastFPS = 20;
|
||||
|
||||
public StreamedRace(VisualiserInput visualiserInput, RaceController controller) {
|
||||
StreamedCourse course = visualiserInput.getCourse();
|
||||
|
||||
this.startingBoats = FXCollections.observableArrayList(course.getBoats());
|
||||
this.boatMarkers = FXCollections.observableArrayList(course.getMarkers());
|
||||
this.legs = course.getLegs();
|
||||
this.legs.add(new Leg("Finish", this.legs.size()));
|
||||
this.controller = controller;
|
||||
if (startingBoats != null && startingBoats.size() > 0) {
|
||||
initialiseBoats();
|
||||
}
|
||||
this.visualiserInput = visualiserInput;
|
||||
}
|
||||
|
||||
private void initialiseBoats() {
|
||||
Leg officialStart = legs.get(0);
|
||||
String name = officialStart.getName();
|
||||
Marker endCompoundMark = officialStart.getEndMarker();
|
||||
|
||||
for (Boat boat : startingBoats) {
|
||||
if (boat != null) {
|
||||
Leg startLeg = new Leg(name, 0);
|
||||
startLeg.setEndMarker(endCompoundMark);
|
||||
boat.setCurrentLeg(startLeg, controller.getRaceClock());
|
||||
boat.setTimeSinceLastMark(controller.getRaceClock().getTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the boat cannot finish the race
|
||||
* @return True if boat cannot finish the race
|
||||
*/
|
||||
protected boolean doNotFinish() {
|
||||
// DNF is no longer random and is now determined by a dnf packet
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the position of the boat.
|
||||
*
|
||||
* @param boat Boat that the position is to be updated for.
|
||||
* @param timeElapsed Time that has elapse since the start of the the race.
|
||||
*/
|
||||
private void checkPosition(Boat boat, long timeElapsed) {
|
||||
boolean legChanged = false;
|
||||
StreamedCourse raceData = visualiserInput.getCourse();
|
||||
BoatStatus boatStatusMessage = visualiserInput.getBoatStatusMap().get(boat.getSourceID());
|
||||
if (boatStatusMessage != null) {
|
||||
BoatStatusEnum boatStatusEnum = BoatStatusEnum.fromByte(boatStatusMessage.getBoatStatus());
|
||||
|
||||
int legNumber = boatStatusMessage.getLegNumber();
|
||||
|
||||
|
||||
if (legNumber >= 1 && legNumber < legs.size()) {
|
||||
if (boat.getCurrentLeg() != legs.get(legNumber)){
|
||||
boat.setCurrentLeg(legs.get(legNumber), controller.getRaceClock());
|
||||
legChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (boatStatusEnum == BoatStatusEnum.RACING) {
|
||||
boat.addTrackPoint(boat.getCurrentPosition());
|
||||
} else if (boatStatusEnum == BoatStatusEnum.DNF) {
|
||||
boat.setDnf(true);
|
||||
} else if (boatStatusEnum == BoatStatusEnum.FINISHED || legNumber == raceData.getLegs().size()) {
|
||||
boatsFinished++;
|
||||
boat.setTimeFinished(timeElapsed);
|
||||
boat.setFinished(true);
|
||||
}
|
||||
}
|
||||
if (legChanged) {
|
||||
//Update the boat display table in the GUI to reflect the leg change
|
||||
updatePositions();
|
||||
controller.updateSparkline(startingBoats);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the boat's gps coordinates
|
||||
*
|
||||
* @param boat to be updated
|
||||
*/
|
||||
private void updatePosition(Boat boat) {
|
||||
int sourceID = boat.getSourceID();
|
||||
BoatLocation boatLocation = visualiserInput.getBoatLocationMessage(sourceID);
|
||||
BoatStatus boatStatus = visualiserInput.getBoatStatusMessage(sourceID);
|
||||
if(boatLocation != null) {
|
||||
double lat = boatLocation.getLatitudeDouble();
|
||||
double lon = boatLocation.getLongitudeDouble();
|
||||
boat.setCurrentPosition(new GPSCoordinate(lat, lon));
|
||||
boat.setHeading(boatLocation.getHeadingDegrees());
|
||||
boat.setEstTime(convertEstTime(boatStatus.getEstTimeAtNextMark(), boatLocation.getTime()));
|
||||
double MMPS_TO_KN = 0.001944;
|
||||
boat.setVelocity(boatLocation.getBoatSOG() * MMPS_TO_KN);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the boat's gps coordinates
|
||||
*
|
||||
* @param mark to be updated
|
||||
*/
|
||||
private void updateMarker(Marker mark) {
|
||||
int sourceID = mark.getSourceId1();
|
||||
BoatLocation boatLocation1 = visualiserInput.getBoatLocationMessage(sourceID);
|
||||
if(boatLocation1 != null) {
|
||||
double lat = boatLocation1.getLatitudeDouble();
|
||||
double lon = boatLocation1.getLongitudeDouble();
|
||||
mark.setCurrentPosition1(new GPSCoordinate(lat, lon));
|
||||
}
|
||||
int sourceID2 = mark.getSourceId2();
|
||||
BoatLocation boatLocation2 = visualiserInput.getBoatLocationMessage(sourceID2);
|
||||
if(boatLocation2 != null) {
|
||||
double lat = boatLocation2.getLatitudeDouble();
|
||||
double lon = boatLocation2.getLongitudeDouble();
|
||||
mark.setCurrentPosition2(new GPSCoordinate(lat, lon));
|
||||
}
|
||||
}
|
||||
|
||||
public void setController(RaceController controller) {
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Runnable for the thread.
|
||||
*/
|
||||
public void run() {
|
||||
setControllerListeners();
|
||||
Platform.runLater(() -> controller.createSparkLine(startingBoats));
|
||||
initialiseBoats();
|
||||
startRaceStream();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the calculated fps to the fps label
|
||||
*
|
||||
* @param fps The new calculated fps value
|
||||
*/
|
||||
private void updateFPS(int fps) {
|
||||
Platform.runLater(() -> controller.setFrames("FPS: " + fps));
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the Race Simulation, playing the race start to finish with the timescale.
|
||||
* This prints the boats participating, the order that the events occur in time order, and the respective information of the events.
|
||||
*/
|
||||
private void startRaceStream() {
|
||||
|
||||
System.setProperty("javafx.animation.fullspeed", "true");
|
||||
|
||||
|
||||
|
||||
new AnimationTimer() {
|
||||
|
||||
final long timeRaceStarted = System.currentTimeMillis(); //start time of loop
|
||||
int fps = 0; //init fps value
|
||||
long timeCurrent = System.currentTimeMillis(); //current time
|
||||
|
||||
@Override
|
||||
public void handle(long arg0) {
|
||||
totalTimeElapsed = System.currentTimeMillis() - timeRaceStarted;
|
||||
|
||||
|
||||
//Check if the race has actually started.
|
||||
if (visualiserInput.getRaceStatus().isStarted()) {
|
||||
//Set all boats to started.
|
||||
for (Boat boat : startingBoats) {
|
||||
boat.setStarted(true);
|
||||
}
|
||||
}
|
||||
|
||||
for (Boat boat : startingBoats) {
|
||||
if (boat != null && !boat.isFinished()) {
|
||||
updatePosition(boat);
|
||||
checkPosition(boat, totalTimeElapsed);
|
||||
}
|
||||
|
||||
}
|
||||
for (Marker mark: boatMarkers){
|
||||
if (mark != null){
|
||||
updateMarker(mark);
|
||||
}
|
||||
}
|
||||
|
||||
if (visualiserInput.getRaceStatus().isFinished()) {
|
||||
controller.finishRace(startingBoats);
|
||||
stop();
|
||||
}
|
||||
controller.updateMap(startingBoats, boatMarkers);
|
||||
fps++;
|
||||
if ((System.currentTimeMillis() - timeCurrent) > 1000) {
|
||||
updateFPS(fps);
|
||||
lastFPS = fps;
|
||||
fps = 0;
|
||||
timeCurrent = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update position of boats in race, no position if on starting leg or DNF.
|
||||
*/
|
||||
private void updatePositions() {
|
||||
FXCollections.sort(startingBoats, (a, b) -> b.getCurrentLeg().getLegNumber() - a.getCurrentLeg().getLegNumber());
|
||||
for(Boat boat: startingBoats) {
|
||||
if(boat != null) {
|
||||
boat.setPosition(Integer.toString(startingBoats.indexOf(boat) + 1));
|
||||
if (boat.isDnf() || !boat.isStarted() || boat.getCurrentLeg().getLegNumber() < 0)
|
||||
boat.setPosition("-");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update call for the controller.
|
||||
*/
|
||||
private void setControllerListeners() {
|
||||
if (controller != null) controller.setInfoTable(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the boats that have started the race.
|
||||
*
|
||||
* @return ObservableList of Boat class that participated in the race.
|
||||
* @see ObservableList
|
||||
* @see Boat
|
||||
*/
|
||||
public ObservableList<Boat> getStartingBoats() {
|
||||
return startingBoats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an estimated time an event will occur, and converts it to the
|
||||
* number of seconds before the event will occur.
|
||||
*
|
||||
* @param estTimeMillis estimated time in milliseconds
|
||||
* @return int difference between time the race started and the estimated time
|
||||
*/
|
||||
private int convertEstTime(long estTimeMillis, long currentTime) {
|
||||
|
||||
long estElapsedMillis = estTimeMillis - currentTime;
|
||||
int estElapsedSecs = Math.round(estElapsedMillis/1000);
|
||||
return estElapsedSecs;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in new issue