Merge remote-tracking branch 'origin/984_Xml_Generation' into Broadcasting_GameState

# Conflicts:
#	src/main/java/seng302/controllers/FinishScreenViewController.java
#	src/main/java/seng302/controllers/RaceViewController.java
#	src/main/java/seng302/gameServer/GameServerThread.java
#	src/main/java/seng302/gameServer/GameState.java
#	src/main/java/seng302/models/Yacht.java
This commit is contained in:
Kusal Ekanayake
2017-07-21 12:31:35 +12:00
11 changed files with 797 additions and 3 deletions
+7
View File
@@ -36,6 +36,13 @@
<version>2.7.13</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.freemarker/freemarker -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.26-incubating</version>
</dependency>
</dependencies>
<build>
@@ -321,7 +321,7 @@ public class CanvasController {
}
for (Yacht boat : boats.values()) {
if (participantIDs.contains(boat.getSourceID())) {
if (participantIDs.contains(boat.getSourceId())) {
boat.setColour(Colors.getColor());
BoatGroup boatGroup = new BoatGroup(boat, boat.getColour());
boatGroups.add(boatGroup);
@@ -316,7 +316,7 @@ public class BoatGroup extends Group {
* @return An array containing all ID's associated with this RaceObject.
*/
public long getRaceId() {
return boat.getSourceID();
return boat.getSourceId();
}
public Group getWake () {
@@ -0,0 +1,368 @@
package seng302.gameServer;
import seng302.models.Player;
import seng302.models.Yacht;
import seng302.server.messages.*;
import seng302.server.simulator.Boat;
import seng302.server.simulator.Simulator;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.SocketOption;
import java.net.SocketOptions;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.*;
public class GameServerThread implements Runnable, Observer, ClientConnectionDelegate{
private static final Integer MAX_NUM_PLAYERS = 10;
public static final int PORT_NUMBER = 4950;
private Boolean hosting = true;
private ServerSocketChannel server;
private long startTime;
private short seqNum;
private final int RACE_STATUS_PERIOD = 1000/2;
private final int RACE_START_STATUS_PERIOD = 1000;
private final int BOAT_LOCATION_PERIOD = 1000/5;
private final int TIME_TILL_RACE_START = 20*1000;
private static final int LOG_LEVEL = 1;
public GameServerThread(String threadName){
Thread runner = new Thread(this, threadName);
runner.setDaemon(true);
seqNum = 0;
runner.start();
}
public static void serverLog(String message, int logLevel){
if(logLevel <= LOG_LEVEL){
System.out.println("[SERVER] " + message);
}
}
/**
* Creates and returns an XML Message from the file specified
* @param fileName The source XML file
* @param type The XML Message type
* @return The XML Message
*/
private Message getXmlMessage(String fileName, XMLMessageSubType type){
String fileContents = null;
try {
InputStream thisStream = this.getClass().getResourceAsStream(fileName);
fileContents = new String(org.apache.commons.io.IOUtils.toByteArray(thisStream));
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException e){
return null;
}
if (fileContents != null){
return new XMLMessage(fileContents, type, seqNum);
}
return null;
}
/**
* @return Get a race status message for the current race
*/
private Message getRaceStatusMessage(){
List<BoatSubMessage> boatSubMessages = new ArrayList<>();
BoatStatus boatStatus;
RaceStatus raceStatus;
boolean thereAreBoatsNotFinished = false;
for (Player player : GameState.getPlayers()){
Yacht y = player.getYacht();
if (GameState.getCurrentStage() == GameStages.PRE_RACE){
boatStatus = BoatStatus.PRESTART;
thereAreBoatsNotFinished = true;
}
else if(false){ //@TODO if boat has finished
boatStatus = BoatStatus.FINISHED;
}
else{
boatStatus = BoatStatus.PRESTART;
thereAreBoatsNotFinished = true;
}
BoatSubMessage m = new BoatSubMessage(y.getSourceId(), boatStatus, y.getLastMarkRounded().getId(), 0, 0, 1234l, 1234l);
boatSubMessages.add(m);
}
if (thereAreBoatsNotFinished){
if (GameState.getCurrentStage() == GameStages.RACING){
raceStatus = RaceStatus.STARTED;
}
else{
long currentTime = System.currentTimeMillis();
long timeDifference = startTime - currentTime;
if (timeDifference > 60*3){
raceStatus = RaceStatus.PRESTART;
}
else if (timeDifference > 60){
raceStatus = RaceStatus.WARNING;
}
else{
raceStatus = RaceStatus.PREPARATORY;
}
}
}
else{
raceStatus = RaceStatus.TERMINATED;
}
return new RaceStatusMessage(1, raceStatus, startTime, WindDirection.SOUTH,
100, GameState.getPlayers().size(), RaceType.MATCH_RACE, 1, boatSubMessages);
}
/**
* Start sending race start status messages until race starts
*/
private void startSendingRaceStartStatusMessages(){
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
Message raceStartStatusMessage = new RaceStartStatusMessage(seqNum, startTime , 1,
RaceStartNotificationType.SET_RACE_START_TIME);
try {
if (startTime < System.currentTimeMillis() && GameState.getCurrentStage() != GameStages.RACING){
}
else{
broadcast(raceStartStatusMessage);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}, 0, RACE_START_STATUS_PERIOD);
}
/**
* Start sending race start status messages until race starts
*/
private void startSendingRaceStatusMessages(){
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
Message raceStatusMessage = getRaceStatusMessage();
try {
broadcast(raceStatusMessage);
} catch (IOException e) {
e.printStackTrace();
}
}
}, 0, RACE_STATUS_PERIOD);
}
/**
* Sends the race, boat, and regatta XML files to the client
*/
private void sendXml(){
try{
Message raceData = getXmlMessage("/server_config/race.xml", XMLMessageSubType.RACE);
Message boatData = getXmlMessage("/server_config/boats.xml", XMLMessageSubType.BOAT);
Message regatta = getXmlMessage("/server_config/regatta.xml", XMLMessageSubType.REGATTA);
if (raceData != null){
broadcast(raceData);
}
if (boatData != null){
broadcast(boatData);
}
if (regatta != null){
broadcast(regatta);
}
} catch (IOException e) {
serverLog("Couldn't send an XML Message: " + e.getMessage(), 0);
}
}
/**
* Send the post-start race course information
*/
private void sendPostStartCourseXml(){
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
try {
Message raceData = getXmlMessage("/server_config/courseLimits.xml", XMLMessageSubType.RACE);
if (raceData != null) {
broadcast(raceData);
}
}catch (IOException e) {
serverLog("Couldn't send an XML Message: " + e.getMessage(), 0);
}
}
},1000);
//Delays the new course xml data for 25 seconds so the boats are able to pass the starting line
}
public void run() {
ServerListenThread serverListenThread;
HeartbeatThread heartbeatThread;
Boolean serverIsSendingMessages = false;
try{
server = ServerSocketChannel.open();
server.socket().bind(new InetSocketAddress("localhost", PORT_NUMBER));
serverListenThread = new ServerListenThread(server, this);
heartbeatThread = new HeartbeatThread(this);
heartbeatThread.start();
serverListenThread.start();
}
catch (IOException e){
serverLog("Failed to bind socket: " + e.getMessage(), 0);
}
while (hosting) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (GameState.getCurrentStage() == GameStages.RACING && !serverIsSendingMessages) {
serverLog("Race Started", 0);
sendXml();
startSendingRaceStartStatusMessages();
//startSendingRaceStatusMessages();
sendPostStartCourseXml();
serverIsSendingMessages = true;
}
else if (GameState.getCurrentStage() == GameStages.FINISHED) {
serverLog("Race Finished", 0);
}
startTime = System.currentTimeMillis() + TIME_TILL_RACE_START;
}
}
// /**
// * Start sending static boat position updates when race has finished
// */
// private void startSendingRaceFinishedBoatPositions(){
// Timer t = new Timer();
// t.schedule(new TimerTask() {
// @Override
// public void run() {
// try {
// for (Boat b : raceSimulator.getBoats()){
// Message m = new BoatLocationMessage(b.getSourceID(), seqNum, b.getLat(),
// b.getLng(), b.getLastPassedCorner().getBearingToNextCorner(),
// ((long) 0));
//
// server.send(m);
// }
//
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
// }, 0, BOAT_LOCATION_PERIOD);
// }
/**
* A client has tried to connect to the server
* @param player The player that connected
*/
@Override
public void clientConnected(Player player) {
if (GameState.getPlayers().size() < MAX_NUM_PLAYERS && GameState.getCurrentStage() == GameStages.LOBBYING) {
serverLog("Player Connected", 0);
GameState.addPlayer(player);
sendXml();
}
}
/**
* A player has left the game, remove the player from the GameState
* @param player The player that left
*/
@Override
public void clientDisconnected(Player player) {
serverLog("Player disconnected", 0);
GameState.removePlayer(player);
sendXml();
}
void broadcast(Message message) throws IOException{
for(Player player : GameState.getPlayers()) {
//heh
player.getSocketChannel().socket().getOutputStream().write(message.getBuffer());
}
seqNum++;
}
/**
* Send a boat location message when they are updated by the simulator
* @param o .
* @param arg .
*/
@Override
@SuppressWarnings("unchecked")
public void update(Observable o, Object arg) {
/* Only send if server started
// TODO: I don't understand why i need to check server is null or not ... confused - haoming 2/5/17
if(server == null || !server.isStarted()){
return;
}
int numOfBoatsFinished = 0;
for (Boat boat : (List<Boat>) arg){
try {
if (boat.isFinished()) {
numOfBoatsFinished ++;
if (!boatsFinished.get(boat.getSourceID())) {
boatsFinished.put(boat.getSourceID(), true);
}
}
Message m = new BoatLocationMessage(boat.getSourceID(), 1, boat.getLat(),
boat.getLng(), boat.getLastPassedCorner().getBearingToNextCorner(),
((long) boat.getSpeed()));
broadcast(m);
} catch (IOException e) {
serverLog("Couldn't send a boat status message", 3);
return;
}
catch (NullPointerException e){
e.printStackTrace();
}*/
}
// if (numOfBoatsFinished == ((List<Boat>) arg).size()) {
// startSendingRaceFinishedBoatPositions();
// }
//}
public void terminateGame() {
try {
//TODO: for now, I just close the socket, but i think we should terminate the whole thread instead. -hyi25 13 July
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@@ -558,7 +558,7 @@ public class XMLParser {
getNodeAttributeString(currentBoat, "Country"));
this.boats.add(boat);
if (boat.getBoatType().equals("Yacht")) {
competingBoats.put(boat.getSourceID(), boat);
competingBoats.put(boat.getSourceId(), boat);
}
}
}
@@ -0,0 +1,53 @@
package seng302.models.xml;
import seng302.models.Yacht;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A Race object that can be parsed into XML
*/
public class Race {
private List<Yacht> yachts;
private LocalDateTime startTime;
public Race(){
yachts = new ArrayList<>();
startTime = LocalDateTime.now();
}
/**
* Add a boat to the race
* @param yacht The boat to add
*/
public void addBoat(Yacht yacht){
yachts.add(yacht);
}
/**
* Get a list of boats in the race
* @return A List of boats
*/
public List<Yacht> getBoats(){
return Collections.unmodifiableList(yachts);
}
/**
* Set the time until the race starts
* @param seconds The time in seconds until the race starts
*/
public void setRaceStartDelay(Integer seconds){
startTime = startTime.plusMinutes(seconds);
}
/**
* Get the time the race starts
* @return The time the race starts
*/
public String getRaceStartTime(){
return startTime.toString();
}
}
@@ -0,0 +1,77 @@
package seng302.models.xml;
/**
* A Race regatta that can be parsed into XML
*/
public class Regatta {
private final Double DEFAULT_ALTITUDE = 0d;
private final Integer DEFAULT_REGATTA_ID = 0;
private Integer id;
private String name;
private String courseName;
private Double latitude;
private Double longitude;
private Double altitude;
private Integer utcOffset;
private Double magneticVariation;
public Regatta(String name, Double latitude, Double longitude) {
this.name = name;
this.id = DEFAULT_REGATTA_ID;
this.courseName = name;
this.latitude = latitude;
this.longitude = longitude;
this.altitude = DEFAULT_ALTITUDE;
this.utcOffset = 0;
this.magneticVariation = 0d;
}
public void setMagneticVariation(Double magneticVariation){
this.magneticVariation = magneticVariation;
}
public void setUtcOffset(Integer offset){
this.utcOffset = offset;
}
/*
NOTE!! The following getters must follow the JavaBean standard (getPropertyName()), and must be public.
*/
public String getName(){
return name;
}
public String getCourseName(){
return courseName;
}
public Integer getRegattaId(){
return id;
}
public Double getLatitude() {
return latitude;
}
public Double getLongitude() {
return longitude;
}
public Double getAltitude() {
return altitude;
}
public Integer getUtcOffset(){
return utcOffset;
}
public Double getMagneticVariation(){
return magneticVariation;
}
}
@@ -0,0 +1,164 @@
package seng302.models.xml;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import seng302.server.messages.XMLMessageSubType;
import java.io.*;
import java.net.URISyntaxException;
/**
* An XML generator to generate the Race, Boat, and Regatta XML dynamically
*/
public class XMLGenerator {
private static final String XML_TEMPLATE_DIR = "/server_config/xml_templates";
private static final String REGATTA_TEMPLATE_NAME = "regatta.ftlh";
private static final String BOATS_TEMPLATE_NAME = "boats.ftlh";
private static final String RACE_TEMPLATE_NAME = "race.ftlh";
private Configuration configuration;
private Regatta regatta;
private Race race;
/**
* Set up a configuration instance for Apache Freemake
*/
private void setupConfiguration() {
configuration = new Configuration(Configuration.VERSION_2_3_26);
try {
configuration.setDirectoryForTemplateLoading(new File(getClass().getResource(XML_TEMPLATE_DIR).toURI()));
} catch (IOException e){
System.out.println("[FATAL] Server could not read XML templates");
} catch (URISyntaxException e) {
System.out.println("[FATAL] Xml template directory URI is invalid");
} catch (NullPointerException e){
System.out.println("[FATAL] Server could not load XML Template directory, ensure this directory isn't empty");
}
}
/**
* Create an instance of the XML Generator
*/
public XMLGenerator(){
setupConfiguration();
}
/**
* Set the race regatta to send to players
* Note: This must be set before a regatta message can be generated
* @param regatta The race regatta
*/
public void setRegatta(Regatta regatta){
this.regatta = regatta;
}
/**
* Set the race to send to players
* Note: This must be set before a boat or race message can be generated
* @param race The race
*/
public void setRace(Race race){
this.race = race;
}
/**
* Parse an XML template and generate the output as a string
* @param templateName The templates file name
* @param type The XML message sub type
*/
private String parseToXmlString(String templateName, XMLMessageSubType type) throws IOException, TemplateException {
Template template;
ByteArrayOutputStream os = new ByteArrayOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(os);
template = configuration.getTemplate(templateName);
switch (type) {
case REGATTA:
template.process(regatta, writer);
break;
case BOAT:
template.process(race, writer);
break;
case RACE:
template.process(race, writer);
break;
default:
throw new UnsupportedOperationException();
}
try {
return os.toString("UTF-8");
} catch (UnsupportedEncodingException e) {
System.out.println("[FATAL] UTF-8 Not supported");
return null;
}
}
/**
* Get the race regatta as a string
* Note: Regatta must be set before calling this
* @return String containing the regatta XML, null if there was an error
*/
public String getRegattaAsXml(){
String result = null;
if (regatta == null) return null;
try {
result = parseToXmlString(REGATTA_TEMPLATE_NAME, XMLMessageSubType.REGATTA);
} catch (TemplateException e) {
System.out.println("[FATAL] Error parsing regatta");
} catch (IOException e) {
System.out.println("[FATAL] Error reading regatta");
}
return result;
}
/**
* Get the boats XML as a string
* Note: Race must be set before calling this
* @return String containing the boats XML, null if there was an error
*/
public String getBoatsAsXml() {
String result = null;
if (race == null) return null;
try {
result = parseToXmlString(BOATS_TEMPLATE_NAME, XMLMessageSubType.BOAT);
} catch (TemplateException e) {
System.out.println("[FATAL] Error parsing boats");
} catch (IOException e) {
System.out.println("[FATAL] Error reading boats");
}
return result;
}
/**
* Get the race XML as a string
* Note: Race must be set before calling this
* @return String containing the race XML, null if there was an error
*/
public String getRaceAsXml() {
String result = null;
if (race == null) return null;
try {
result = parseToXmlString(RACE_TEMPLATE_NAME, XMLMessageSubType.RACE);
} catch (TemplateException e) {
System.out.println("[FATAL] Error parsing race");
} catch (IOException e) {
System.out.println("[FATAL] Error reading race");
}
return result;
}
}
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<BoatConfig>
<Modified>2012-05-17T07:49:40+0200</Modified>
<Version>12</Version>
<Settings>
<RaceBoatType Type="AC45" />
<BoatDimension BoatLength="14.019" HullLength="13.449" />
<ZoneSize MarkZoneSize="40.347" CourseZoneSize="40.347" />
<ZoneLimits Limit1="200" Limit2="100" Limit3="40.347" Limit4="0" Limit5="-100" />
</Settings>
<BoatShapes>
<#-- Not used -->
</BoatShapes>
<Boats>
<#list boats as boat>
<Boat Type="Yacht" SourceID="${boat.sourceId}" ShapeID="4" HullNum="${boat.hullId}" StoweName="${boat.shortName}" ShortName="${boat.shortName}"
BoatName="${boat.boatName}" Country="${boat.country}">
<GPSposition Z="0" Y="${boat.lat}" X="${boat.lon}" />
<MastTop Z="0" Y="${boat.lat}" X="${boat.lon}" />
</Boat>
</#list>
</Boats>
</BoatConfig>
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<Race>
<CreationTimeDate>${raceStartTime}</CreationTimeDate>
<RaceStartTime Start="${raceStartTime}" Postpone="False" />
<RaceID>15082901</RaceID>
<RaceType>Fleet</RaceType>
<Participants>
<#list boats as boat>
<Yacht SourceID="${boat.sourceId}"/>
</#list>
</Participants>
<Course>
<CompoundMark CompoundMarkID="1" Name="Mark0">
<Mark SeqID="1" Name="Start Line 1" TargetLat="57.6703330" TargetLng="11.8278330" SourceID="122" />
<Mark SeqID="2" Name="Start Line 2" TargetLat="57.6703330" TargetLng="11.8278330" SourceID="123" />
</CompoundMark>
<CompoundMark CompoundMarkID="2" Name="Mark1">
<Mark SeqID="1" Name="Mark1" TargetLat="57.6675700" TargetLng="11.8359880" SourceID="131" />
</CompoundMark>
<CompoundMark CompoundMarkID="3" Name="Mark2">
<Mark SeqID="1" Name="Lee Gate 1" TargetLat="57.6708220" TargetLng="11.8433900" SourceID="124" />
<Mark SeqID="2" Name="Lee Gate 2" TargetLat="57.6708220" TargetLng="11.8433900" SourceID="125" />
</CompoundMark>
<CompoundMark CompoundMarkID="4" Name="Mark3">
<Mark SeqID="1" Name="Wind Gate 1" TargetLat="57.6650170" TargetLng="11.8279170" SourceID="126" />
<Mark SeqID="2" Name="Wind Gate 2" TargetLat="57.6650170" TargetLng="11.8279170" SourceID="127" />
</CompoundMark>
<CompoundMark CompoundMarkID="5" Name="Mark2">
<Mark SeqID="1" Name="Lee Gate 1" TargetLat="57.6708220" TargetLng="11.8433900" SourceID="124" />
<Mark SeqID="2" Name="Lee Gate 2" TargetLat="57.6708220" TargetLng="11.8433900" SourceID="125" />
</CompoundMark>
<CompoundMark CompoundMarkID="6" Name="Mark3">
<Mark SeqID="1" Name="Wind Gate 1" TargetLat="57.6650170" TargetLng="11.8279170" SourceID="126" />
<Mark SeqID="2" Name="Wind Gate 2" TargetLat="57.6650170" TargetLng="11.8279170" SourceID="127" />
</CompoundMark>
<CompoundMark CompoundMarkID="7" Name="Mark2">
<Mark SeqID="1" Name="Lee Gate 1" TargetLat="57.6708220" TargetLng="11.8433900" SourceID="124" />
<Mark SeqID="2" Name="Lee Gate 2" TargetLat="57.6708220" TargetLng="11.8433900" SourceID="125" />
</CompoundMark>
<CompoundMark CompoundMarkID="8" Name="Mark3">
<Mark SeqID="1" Name="Wind Gate 1" TargetLat="57.6650170" TargetLng="11.8279170" SourceID="126" />
<Mark SeqID="2" Name="Wind Gate 2" TargetLat="57.6650170" TargetLng="11.8279170" SourceID="127" />
</CompoundMark>
<CompoundMark CompoundMarkID="9" Name="Mark2">
<Mark SeqID="1" Name="Lee Gate 1" TargetLat="57.6708220" TargetLng="11.8433900" SourceID="124" />
<Mark SeqID="2" Name="Lee Gate 2" TargetLat="57.6708220" TargetLng="11.8433900" SourceID="125" />
</CompoundMark>
<CompoundMark CompoundMarkID="10" Name="Mark3">
<Mark SeqID="1" Name="Wind Gate 1" TargetLat="57.6650170" TargetLng="11.8279170" SourceID="126" />
<Mark SeqID="2" Name="Wind Gate 2" TargetLat="57.6650170" TargetLng="11.8279170" SourceID="127" />
</CompoundMark>
<CompoundMark CompoundMarkID="11" Name="Mark4">
<Mark SeqID="1" Name="Finish Line 1" TargetLat="57.6715240" TargetLng="11.8444950" SourceID="128" />
<Mark SeqID="2" Name="Finish Line 2" TargetLat="57.6715240" TargetLng="11.8444950" SourceID="129" />
</CompoundMark>
</Course>
<CompoundMarkSequence>
<Corner SeqID="1" CompoundMarkID="1" Rounding="PS" ZoneSize="3" />
<Corner SeqID="2" CompoundMarkID="2" Rounding="Port" ZoneSize="3" />
<Corner SeqID="3" CompoundMarkID="3" Rounding="SP" ZoneSize="3" />
<Corner SeqID="4" CompoundMarkID="4" Rounding="PS" ZoneSize="3" />
<Corner SeqID="5" CompoundMarkID="5" Rounding="SP" ZoneSize="3" />
<Corner SeqID="6" CompoundMarkID="6" Rounding="PS" ZoneSize="3" />
<Corner SeqID="7" CompoundMarkID="7" Rounding="SP" ZoneSize="3" />
<Corner SeqID="8" CompoundMarkID="8" Rounding="PS" ZoneSize="3" />
<Corner SeqID="9" CompoundMarkID="9" Rounding="SP" ZoneSize="3" />
<Corner SeqID="10" CompoundMarkID="10" Rounding="PS" ZoneSize="3" />
<Corner SeqID="11" CompoundMarkID="11" Rounding="PS" ZoneSize="3" />
</CompoundMarkSequence>
<CourseLimit>
<Limit SeqID="1" Lat="57.6739450" Lon="11.8417100" />
<Limit SeqID="2" Lat="57.6709520" Lon="11.8485010" />
<Limit SeqID="3" Lat="57.6690260" Lon="11.8472790" />
<Limit SeqID="4" Lat="57.6693140" Lon="11.8457610" />
<Limit SeqID="5" Lat="57.6665370" Lon="11.8432910" />
<Limit SeqID="6" Lat="57.6641400" Lon="11.8385840" />
<Limit SeqID="7" Lat="57.6629430" Lon="11.8332030" />
<Limit SeqID="8" Lat="57.6629480" Lon="11.8249660" />
<Limit SeqID="9" Lat="57.6686890" Lon="11.8250920" />
<Limit SeqID="10" Lat="57.6692230" Lon="11.8231430" />
<Limit SeqID="11" Lat="57.6725370" Lon="11.8272480" />
<Limit SeqID="12" Lat="57.6708220" Lon="11.8321340" />
</CourseLimit>
</Race>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<RegattaConfig>
<RegattaID>${regattaId}</RegattaID>
<RegattaName>${name}</RegattaName>
<CourseName>${courseName}</CourseName>
<CentralLatitude>${latitude}</CentralLatitude>
<CentralLongitude>${longitude}</CentralLongitude>
<CentralAltitude>${altitude}</CentralAltitude>
<UtcOffset>${utcOffset}</UtcOffset>
<MagneticVariation>${magneticVariation}</MagneticVariation>
</RegattaConfig>