package mock.xml; import org.xml.sax.SAXException; import shared.dataInput.RaceXMLReader; import shared.enums.XMLFileType; import shared.exceptions.InvalidRaceDataException; import shared.exceptions.XMLReaderException; import shared.model.CompoundMark; import shared.model.Constants; import shared.model.GPSCoordinate; import shared.xml.Race.XMLCompoundMark; import shared.xml.Race.XMLLimit; import shared.xml.Race.XMLMark; import shared.xml.Race.XMLRace; import shared.xml.XMLUtilities; import javax.xml.bind.JAXBException; import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; import java.io.InputStream; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; /** * Helper Class for creating a Race XML */ public class RaceXMLCreator { /** * get the windward gate in a race * @param reader reads in the mark * @return the windward gate. */ public static CompoundMark getWindwardGate(RaceXMLReader reader){ for (CompoundMark mark: reader.getCompoundMarks()){ if (mark.getName().equals("Windward Gate")) return mark; } return null; } /** * get the leeward gate in a race * @param reader reads in the mark * @return the leeward gate. */ public static CompoundMark getLeewardGate(RaceXMLReader reader){ for (CompoundMark mark: reader.getCompoundMarks()){ if (mark.getName().equals("Leeward Gate")) return mark; } return null; } /** * Rotates the race in a specified direction. * @param s xml file name or contents. * @param fileType Whether s is a file name or contents. * @param degrees degrees to rotate * @param tutorial Whether we wish to run the tutorial - this changes the race start time. * @return the new xml file as a string * @throws XMLReaderException if the xml is not readable * @throws InvalidRaceDataException if the race is invalid */ public static String alterRaceToWind(String s, XMLFileType fileType, double degrees, boolean tutorial) throws XMLReaderException, InvalidRaceDataException { RaceXMLReader reader = new RaceXMLReader(s, fileType); try { XMLRace race = XMLUtilities.xmlToClass( s, RaceXMLCreator.class.getClassLoader().getResource("mock/mockXML/schema/raceSchema.xsd"), XMLRace.class); if(tutorial){ setRaceXMLAtCurrentTimeToNow(race, 1000l, 5000l); } else { setRaceXMLAtCurrentTimeToNow(race); } CompoundMark leewardGate = getLeewardGate(reader); CompoundMark windwardGate = getWindwardGate(reader); double raceOriginalBearing = 0; /*if (leewardGate != null && windwardGate != null) { raceOriginalBearing = getLineAngle( leewardGate.getMark1Position(), windwardGate.getMark1Position() ); }*/ double degreesToRotate = degrees - raceOriginalBearing; alterRaceRotation(race, degreesToRotate); return XMLUtilities.classToXML(race); } catch (ParserConfigurationException | IOException | SAXException | JAXBException e) { throw new InvalidRaceDataException("Could not parse or marshall race data file.", e); } } /** * Rotate the features in a race such as the boundary, and the marks. * @param race the race to alter * @param degrees the degrees to rotate by. */ public static void alterRaceRotation(XMLRace race, double degrees){ GPSCoordinate center = getCenter(race); for(XMLLimit limit: race.getCourseLimit().getLimit()){ GPSCoordinate rotatedLim = rotate(center, limitToGPSCoordinate(limit), degrees); limit.setLat(rotatedLim.getLatitude()); limit.setLon(rotatedLim.getLongitude()); } for(XMLCompoundMark compoundMark: race.getCourse().getCompoundMark()){ for (XMLMark mark: compoundMark.getMark()){ GPSCoordinate rotatedMark = rotate(center, markToGPSCoordinate(mark), degrees); mark.setTargetLat(rotatedMark.getLatitude()); mark.setTargetLng(rotatedMark.getLongitude()); } } } /** * Converts a Race.CourseLimit.Limit to a GPS coordinate * @param limit limit to convert * @return gps coordinate corresponding to the limit */ public static GPSCoordinate limitToGPSCoordinate(XMLLimit limit){ return new GPSCoordinate(limit.getLat(), limit.getLon()); } /** * get new gps coordinate after rotating * @param pivot center point to rotating from. * @param point point to rotate * @param degrees number of degress to rotate by * @return the new GPSCoordinate of the transformed point. */ public static GPSCoordinate rotate(GPSCoordinate pivot, GPSCoordinate point, double degrees){ double radDeg = Math.toRadians(degrees); double deltaLat = (point.getLatitude() - pivot.getLatitude()); double deltaLon = (point.getLongitude() - pivot.getLongitude()); //map to (0,1) vector and use vector maths to rotate. double resLat = deltaLat * Math.cos(radDeg) - deltaLon * Math.sin(radDeg) + pivot.getLatitude(); double resLon = deltaLat * Math.sin(radDeg) + deltaLon * Math.cos(radDeg) + pivot.getLongitude(); return new GPSCoordinate(resLat, resLon); } /** * obtains the GPSCoordinates of a mark * @param mark mark to obtain the GPSCoordinates of * @return the GPSCOordinatess of a mark */ public static GPSCoordinate markToGPSCoordinate(XMLMark mark){ return new GPSCoordinate(mark.getTargetLat(), mark.getTargetLng()); } /** * get the center of a race * @param race race to get the center of * @return GPSCoordinates of the center */ public static GPSCoordinate getCenter(XMLRace race){ double avgLat = 0; double avgLng = 0; for (XMLLimit limit: race.getCourseLimit().getLimit()){ avgLat += limit.getLat(); avgLng += limit.getLon(); } avgLat = avgLat/race.getCourseLimit().getLimit().size(); avgLng = avgLng/race.getCourseLimit().getLimit().size(); return new GPSCoordinate(avgLat, avgLng); } /** * gets the angle of a line * @param coord1 point a of the line * @param coord2 point b of the line * @return the angle in degrees that the bearing of the line is [-180, 180] */ public static double getLineAngle(GPSCoordinate coord1, GPSCoordinate coord2){ double dx = coord1.getLongitude() - coord2.getLongitude(); double dy = coord1.getLatitude() - coord2.getLatitude(); return Math.atan2(dy, dx)/Math.PI * 180; } public static void setRaceXMLAtCurrentTimeToNow(XMLRace raceXML, long racePrestartTime, long racePreparatoryTime){ //The start time is current time + 4 minutes. prestart is 3 minutes, and we add another minute. long millisecondsToAdd = racePrestartTime + racePreparatoryTime; long secondsToAdd = millisecondsToAdd / 1000; DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"); ZonedDateTime creationTime = ZonedDateTime.now(); raceXML.setCreationTimeDate(dateFormat.format(creationTime)); raceXML.getRaceStartTime().setTime(dateFormat.format(creationTime.plusSeconds(secondsToAdd))); } /** * Sets the xml description of the race to show the race was created now, and starts in 4 minutes * @param raceXML The race.xml contents. */ public static void setRaceXMLAtCurrentTimeToNow(XMLRace raceXML) { setRaceXMLAtCurrentTimeToNow(raceXML, Constants.RacePreStartTime, Constants.RacePreparatoryTime); } }