package seng302.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 seng302.Model.*; 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 boundary = new ArrayList<>(); private final Map compoundMarkMap = new HashMap<>(); private final Map participants = new HashMap<>(); private final List legs = new ArrayList<>(); private final List compoundMarks = new ArrayList<>(); private ZonedDateTime creationTimeDate; private ZonedDateTime raceStartTime; private int raceID; private String raceType; private boolean postpone; private Map boats; private Map 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 getBoundary() { return boundary; } public GPSCoordinate getMapTopLeft() { return mapTopLeft; } public GPSCoordinate getMapBottomRight() { return mapBottomRight; } public List getLegs() { return legs; } public List 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 getBoats() { return new ArrayList<>(participants.values()); } }