+ *
+ * When the {@code hostname} is {@code null}, this is assumed to request a URL-based connection, and {@code raceDataUrl} has
+ * to provide a valid URL leading to a downloadable log file.
*/
- SailMasterConnector getOrCreateSailMasterConnector(String hostname, int port, String raceId, String raceName,
- String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker)
+ SailMasterConnector getOrCreateSailMasterConnector(String hostname, int port, String raceId, URL raceDataUrl,
+ String raceName, String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker)
throws InterruptedException, ParseException;
SailMasterConnector getOrCreateSailMasterLiveSimulatorConnector(String host, int port, String raceId, String raceName,
diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterConnectorImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/AbstractSailMasterConnector.java
similarity index 92%
rename from java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterConnectorImpl.java
rename to java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/AbstractSailMasterConnector.java
index b6cfaa0ba84..09a8eb908a5 100755
--- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterConnectorImpl.java
+++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/AbstractSailMasterConnector.java
@@ -3,7 +3,6 @@ package com.sap.sailing.domain.swisstimingadapter.impl;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.text.DateFormat;
@@ -30,6 +29,7 @@ import java.util.stream.Collectors;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.SpeedWithBearing;
+import com.sap.sailing.domain.common.TrackedRaceStatusEnum;
import com.sap.sailing.domain.common.impl.DegreePosition;
import com.sap.sailing.domain.common.impl.KilometersPerHourSpeedImpl;
import com.sap.sailing.domain.common.impl.KnotSpeedImpl;
@@ -83,12 +83,9 @@ import com.sap.sse.util.impl.UUIDHelper;
* @author Axel Uhl (d043530)
*
*/
-public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implements SailMasterConnector, Runnable {
- private static final Logger logger = Logger.getLogger(SailMasterConnectorImpl.class.getName());
+public abstract class AbstractSailMasterConnector extends SailMasterTransceiverImpl implements SailMasterConnector, Runnable {
+ private static final Logger logger = Logger.getLogger(AbstractSailMasterConnector.class.getName());
- private final String host;
- private final int port;
- private Socket socket;
private final DateFormat dateFormat;
private final Set listeners;
private final Thread receiverThread;
@@ -131,7 +128,11 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
private Long numberOfStoredMessages;
- public SailMasterConnectorImpl(String host, int port, String raceId, String raceName, String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker) throws InterruptedException, ParseException {
+ /**
+ * Subclasses must invoke {@link #startReceiverThread()} in their constructor before the constructor completes, but
+ * usually after finishing the initialization of all their fields.
+ */
+ protected AbstractSailMasterConnector(String raceId, String raceName, String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker) throws InterruptedException, ParseException {
super();
maxSequenceNumber = -1l;
this.raceId = raceId; // from this time on, the connector interprets messages for raceID
@@ -139,12 +140,13 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
this.raceDescription = raceDescription;
this.boatClass = boatClass;
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
- this.host = host;
- this.port = port;
- this.listeners = new HashSet();
- this.unprocessedMessagesByType = new HashMap>();
+ this.listeners = new HashSet<>();
+ this.unprocessedMessagesByType = new HashMap<>();
this.addSailMasterListener(swissTimingRaceTracker);
receiverThread = new Thread(this, "SwissTiming SailMaster Receiver");
+ }
+
+ protected void startReceiverThread() {
receiverThread.start();
}
@@ -152,15 +154,15 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
try {
while (!stopped) {
try {
- ensureSocketIsOpen(); // the result of this may be that the connector w\s stopped in between, so another check is required
- if (!stopped && socket != null) {
- String receivedMessage = receiveMessage(socket.getInputStream());
+ ensureConnected(); // the result of this may be that the connector w\s stopped in between, so another check is required
+ if (!stopped && isConnected()) {
+ String receivedMessage = receiveMessage(getInputStream());
if (receivedMessage == null) {
+ logger.info("Reached EOF for "+this+"; disconnecting");
// reached EOF; this means the socket is or can be closed
- if (socket != null && !socket.isClosed()) {
- socket.close();
+ if (isConnected()) {
+ disconnect();
}
- socket = null;
} else {
SailMasterMessage message = new SailMasterMessageImpl(receivedMessage);
// drop race-specific messages for non-tracked races
@@ -188,12 +190,12 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
} catch (SocketException se) {
// This occurs if the socket was closed which may mean the connector was stopped. Check in while
logger.info("Caught exception "+se+" during socket operation; setting socket to null");
- socket = null;
+ disconnect();
Thread.sleep(1000); // try again in 1s
}
}
} catch (Exception e) {
- logger.log(Level.SEVERE, "Exception in sail master connector "+SailMasterConnectorImpl.class.getName()+".run for "+this, e);
+ logger.log(Level.SEVERE, "Exception in sail master connector "+AbstractSailMasterConnector.class.getName()+".run for "+this, e);
}
logger.info("Stopping Sail Master connector thread for "+this);
stopped = true;
@@ -261,13 +263,15 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
private void notifyListenersStoredDataProgress(String raceID, double progress) {
for (SailMasterListener listener : getListeners()) {
try {
- listener.storedDataProgress(raceID, progress);
+ listener.storedDataProgress(raceID, progress, getStatusAfterLoadingIsComplete());
} catch (Exception e) {
logger.info("Exception occurred trying to notify listener "+listener+" about progress "+progress);
- logger.throwing(SailMasterConnectorImpl.class.getName(), "notifyStoredDataProgress", e);
+ logger.throwing(AbstractSailMasterConnector.class.getName(), "notifyStoredDataProgress", e);
}
}
}
+
+ protected abstract TrackedRaceStatusEnum getStatusAfterLoadingIsComplete();
private void notifyListenersWND(SailMasterMessage message) {
// example message: WND|W4702|1|320|5.4
@@ -280,7 +284,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
listener.receivedWindData(raceID, zeroBasedMarkIndex, windDirectionTrueDegrees, windSpeedInKnots);
} catch (Exception e) {
logger.info("Exception occurred trying to notify listener "+listener+" about "+message+": "+e.getMessage());
- logger.throwing(SailMasterConnectorImpl.class.getName(), "notifyListenersWND", e);
+ logger.throwing(AbstractSailMasterConnector.class.getName(), "notifyListenersWND", e);
}
}
}
@@ -304,7 +308,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
listener.receivedTimingData(raceID, competitorIdAsString, markIndicesRanksAndTimesSinceStartInMilliseconds);
} catch (Exception e) {
logger.info("Exception occurred trying to notify listener "+listener+" about "+message+": "+e.getMessage());
- logger.throwing(SailMasterConnectorImpl.class.getName(), "notifyListenersTMD", e);
+ logger.throwing(AbstractSailMasterConnector.class.getName(), "notifyListenersTMD", e);
}
}
}
@@ -316,7 +320,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
listener.receivedClockAtMark(message.getSections()[1], clockAtMarkResults);
} catch (Exception e) {
logger.info("Exception occurred trying to notify listener "+listener+" about "+message+": "+e.getMessage());
- logger.throwing(SailMasterConnectorImpl.class.getName(), "notifyListenersCAM", e);
+ logger.throwing(AbstractSailMasterConnector.class.getName(), "notifyListenersCAM", e);
}
}
}
@@ -328,7 +332,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
listener.receivedStartList(message.getSections()[1], startListMessage);
} catch (Exception e) {
logger.info("Exception occurred trying to notify listener "+listener+" about "+message+": "+e.getMessage());
- logger.throwing(SailMasterConnectorImpl.class.getName(), "notifyListenersSTL", e);
+ logger.throwing(AbstractSailMasterConnector.class.getName(), "notifyListenersSTL", e);
}
}
}
@@ -340,7 +344,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
listener.receivedCourseConfiguration(message.getSections()[1], course);
} catch (Exception e) {
logger.info("Exception occurred trying to notify listener "+listener+" about "+message+": "+e.getMessage());
- logger.throwing(SailMasterConnectorImpl.class.getName(), "notifyListenersCCG", e);
+ logger.throwing(AbstractSailMasterConnector.class.getName(), "notifyListenersCCG", e);
}
}
}
@@ -352,7 +356,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
listener.receivedAvailableRaces(races);
} catch (Exception e) {
logger.info("Exception occurred trying to notify listener "+listener+" about "+message+": "+e.getMessage());
- logger.throwing(SailMasterConnectorImpl.class.getName(), "notifyListenersRAC", e);
+ logger.throwing(AbstractSailMasterConnector.class.getName(), "notifyListenersRAC", e);
}
}
// not race specific; no need to notify any listener from raceSpecificListeners
@@ -447,7 +451,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
millisecondsSinceRaceStart, nextMarkIndexForLeader, distanceToNextMarkForLeader, fixes);
} catch (Exception e) {
logger.info("Exception occurred trying to notify listener "+listener+" about "+message+": "+e.getMessage());
- logger.throwing(SailMasterConnectorImpl.class.getName(), "notifyListenersRPD", e);
+ logger.throwing(AbstractSailMasterConnector.class.getName(), "notifyListenersRPD", e);
}
}
}
@@ -458,19 +462,11 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
}
}
- @Override
- public String toString() {
- return "SailMasterConnector listening on port "+port+" with socket "+socket+" for race "+raceName+" with ID "+raceId+" ("+raceDescription+")";
- }
-
@Override
public void stop() throws IOException {
- logger.info("Stopping "+this);
stopped = true;
- if (socket != null) {
- socket.close();
- socket = null;
- }
+ logger.info("Stopping "+this);
+ disconnect();
synchronized (this) {
notifyAll();
}
@@ -499,13 +495,13 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
}
public SailMasterMessage sendRequestAndGetResponse(MessageType messageType, String... args) throws UnknownHostException, IOException, InterruptedException {
- ensureSocketIsOpen();
+ ensureConnected();
return sendRequestAndGetResponseAssumingSocketIsOpen(messageType, args);
}
private SailMasterMessage sendRequestAndGetResponseAssumingSocketIsOpen(MessageType messageType, String... args)
throws IOException, InterruptedException {
- OutputStream os = socket.getOutputStream();
+ OutputStream os = getOutputStream();
final SailMasterMessage sailMasterMessage = createSailMasterMessage(messageType, args);
sendMessage(sailMasterMessage, os);
return receiveMessage(messageType);
@@ -525,17 +521,18 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
final SailMasterMessage sailMasterMessage = new SailMasterMessageImpl(requestMessage.toString());
return sailMasterMessage;
}
+
+ protected abstract OutputStream getOutputStream() throws IOException;
+ protected abstract InputStream getInputStream() throws IOException;
private final Object ensureSocketIsOpenSemaphor = new Object();
- private void ensureSocketIsOpen() throws InterruptedException {
+ private void ensureConnected() throws InterruptedException, NumberFormatException, IOException {
synchronized (ensureSocketIsOpenSemaphor) {
- while (!stopped && socket == null) {
+ while (!stopped && !isConnected()) {
try {
- logger.info("Opening socket to " + host + ":" + port + " and sending " + MessageType.OPN.name()
- + " message...");
- socket = new Socket(host, port);
- final OutputStream os = socket.getOutputStream();
- final InputStream is = socket.getInputStream();
+ connect();
+ final OutputStream os = getOutputStream();
+ final InputStream is = getInputStream();
final SailMasterMessage opnRequest = createSailMasterMessage(MessageType.OPN, raceId);
sendMessage(opnRequest, os);
SailMasterMessage opnResponse = new SailMasterMessageImpl(receiveMessage(is));
@@ -543,7 +540,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
logger.info("Recevied non-OK response " + opnResponse + " in "+this+" for our request " + opnRequest
+ ". Closing socket and stopping because we have no hope for recovery");
stopped = true;
- closeSocket();
+ disconnect();
} else {
logger.info("Received " + opnResponse + " in "+this+" which seems OK. Continuing with "
+ MessageType.LSN.name() + " request...");
@@ -565,7 +562,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
if (lsnResponse.getType() != MessageType.LSN || !"OK".equals(lsnResponse.getSections()[1])) {
logger.info("Received non-OK response " + lsnResponse + " for our request " + lsnRequest
+ " in "+this+". Closing socket and trying again in 1s...");
- closeAndNullSocketAndWaitABit();
+ disconnectAndWaitABit();
} else {
logger.info("Received " + lsnResponse
+ " which seems to be OK. I think we're connected in "+this+"!");
@@ -573,28 +570,23 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
}
} catch (IOException e) {
logger.log(Level.INFO, "Exception trying to establish connection in "+this+". Trying again in 1s.", e);
- closeAndNullSocketAndWaitABit();
+ disconnectAndWaitABit();
}
}
}
}
- private void closeAndNullSocketAndWaitABit() throws InterruptedException {
- closeSocket();
+ protected abstract void connect() throws IOException;
+
+ protected abstract boolean isConnected() throws IOException;
+
+ protected abstract void disconnect() throws IOException;
+
+ private void disconnectAndWaitABit() throws InterruptedException, IOException {
+ disconnect();
Thread.sleep(1000);
}
-
- private void closeSocket() {
- if (socket != null) {
- try {
- socket.close();
- } catch (IOException e) {
- logger.log(Level.INFO, "Exception trying to close socket. Maybe already closed. Continuing", e);
- }
- socket = null;
- }
- }
-
+
@Override
public Race getRace() {
return new RaceImpl(raceId, raceName, raceDescription, boatClass);
@@ -857,5 +849,5 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
public void disableRacePositionData() throws UnknownHostException, IOException, InterruptedException {
sendRequestAndGetResponse(MessageType.RPD, "0");
}
-
+
}
diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterConnectorForSocket.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterConnectorForSocket.java
new file mode 100755
index 00000000000..3c3bd519e51
--- /dev/null
+++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterConnectorForSocket.java
@@ -0,0 +1,96 @@
+package com.sap.sailing.domain.swisstimingadapter.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.text.ParseException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.sap.sailing.domain.base.BoatClass;
+import com.sap.sailing.domain.common.TrackedRaceStatusEnum;
+import com.sap.sailing.domain.swisstimingadapter.MessageType;
+
+/**
+ * Implements the connector to the SwissTiming Sail Master system. It uses a host name and port number to establish the
+ * connection via TCP. The connector offers a number of explicit service request methods. Additionally, the connector
+ * can receive "spontaneous" events sent by the sail master system. Clients can register for those spontaneous events
+ * (see {@link #addSailMasterListener}).
+ *
+ *
+ * When the connector is used with SailMaster instances hidden behind a "bridge" / firewall, no explicit requests are
+ * possible, and the connector has to rely solely on the events it receives. It may, though, load recorded race-specific
+ * messages through a {@link RaceSpecificMessageLoader} object. If a non-null {@link RaceSpecificMessageLoader}
+ * is provided to the constructor, the connector will fetch the {@link #getRace() race} from that loader.
+ * Additionally, the connector will use the loader upon each {@link #trackRace(String)} to load all messages recorded
+ * by the loader for the race requested so far.
+ *
+ *
+ * Generally, the connector needs to be instructed for which races it shall handle events using calls to the
+ * {@link #trackRace} and {@link #stopTrackingRace} operations. {@link MessageType#isRaceSpecific() Race-specific
+ * messages} for other races are ignored and not forwarded to any listener.
+ *
+ * Clients that want to wait until the connector changes to {@link #isStopped()} can {@link Object#wait()} on this
+ * object because it notifies all waiters when changing from !{@link #isStopped()} to {@link #isStopped()}.
+ *
+ * @author Axel Uhl (d043530)
+ *
+ */
+public class SailMasterConnectorForSocket extends AbstractSailMasterConnector {
+ private static final Logger logger = Logger.getLogger(SailMasterConnectorForSocket.class.getName());
+
+ private final String host;
+ private final int port;
+
+ /**
+ * Will have a socket connecting to {@link #host}:{@link #port}
+ */
+ private Socket socket;
+
+ public SailMasterConnectorForSocket(String host, int port, String raceId, String raceName, String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker) throws InterruptedException, ParseException {
+ super(raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker);
+ this.host = host;
+ this.port = port;
+ startReceiverThread();
+ }
+
+ @Override
+ protected void disconnect() {
+ if (socket != null) {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ logger.log(Level.INFO, "Exception trying to close socket. Maybe already closed. Continuing", e);
+ }
+ socket = null;
+ }
+ }
+
+ @Override
+ protected OutputStream getOutputStream() throws IOException {
+ return socket.getOutputStream();
+ }
+
+ @Override
+ protected InputStream getInputStream() throws IOException {
+ return socket.getInputStream();
+ }
+
+ @Override
+ protected void connect() throws IOException {
+ logger.info("Opening socket to " + host + ":" + port + " and sending " + MessageType.OPN.name()
+ + " message...");
+ socket = new Socket(host, port);
+ }
+
+ @Override
+ protected boolean isConnected() {
+ return socket != null && socket.isConnected();
+ }
+
+ @Override
+ protected TrackedRaceStatusEnum getStatusAfterLoadingIsComplete() {
+ return TrackedRaceStatusEnum.TRACKING; // more stuff can come through the socket
+ }
+}
diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterConnectorForUrlDownload.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterConnectorForUrlDownload.java
new file mode 100755
index 00000000000..e9e439b9090
--- /dev/null
+++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterConnectorForUrlDownload.java
@@ -0,0 +1,95 @@
+package com.sap.sailing.domain.swisstimingadapter.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.text.ParseException;
+import java.util.logging.Logger;
+
+import com.sap.sailing.domain.base.BoatClass;
+import com.sap.sailing.domain.common.TrackedRaceStatusEnum;
+import com.sap.sailing.domain.swisstimingadapter.MessageType;
+import com.sap.sse.util.HttpUrlConnectionHelper;
+
+/**
+ * Implements the connector to the SwissTiming Sail Master system. It uses a host name and port number to establish the
+ * connection via TCP. The connector offers a number of explicit service request methods. Additionally, the connector
+ * can receive "spontaneous" events sent by the sail master system. Clients can register for those spontaneous events
+ * (see {@link #addSailMasterListener}).
+ *
+ *
+ * When the connector is used with SailMaster instances hidden behind a "bridge" / firewall, no explicit requests are
+ * possible, and the connector has to rely solely on the events it receives. It may, though, load recorded race-specific
+ * messages through a {@link RaceSpecificMessageLoader} object. If a non-null {@link RaceSpecificMessageLoader}
+ * is provided to the constructor, the connector will fetch the {@link #getRace() race} from that loader.
+ * Additionally, the connector will use the loader upon each {@link #trackRace(String)} to load all messages recorded
+ * by the loader for the race requested so far.
+ *
+ *
+ * Generally, the connector needs to be instructed for which races it shall handle events using calls to the
+ * {@link #trackRace} and {@link #stopTrackingRace} operations. {@link MessageType#isRaceSpecific() Race-specific
+ * messages} for other races are ignored and not forwarded to any listener.
+ *
+ * Clients that want to wait until the connector changes to {@link #isStopped()} can {@link Object#wait()} on this
+ * object because it notifies all waiters when changing from !{@link #isStopped()} to {@link #isStopped()}.
+ *
+ * @author Axel Uhl (d043530)
+ *
+ */
+public class SailMasterConnectorForUrlDownload extends AbstractSailMasterConnector {
+ private static final Logger logger = Logger.getLogger(SailMasterConnectorForUrlDownload.class.getName());
+
+ private final URL raceDataUrl;
+
+ /**
+ * If the {@link #socket} is not being used, a {@link HttpURLConnection} is expected to provide the
+ * {@link InputStream} from which this connector reads.
+ */
+ private URLConnection urlConnection;
+
+ public SailMasterConnectorForUrlDownload(String raceId, URL raceDataUrl, String raceName, String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker) throws InterruptedException, ParseException {
+ super(raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker);
+ this.raceDataUrl = raceDataUrl;
+ startReceiverThread();
+ }
+
+ @Override
+ protected OutputStream getOutputStream() throws IOException {
+ return null; // we don't write to the URL connection's output stream
+ }
+
+ @Override
+ protected InputStream getInputStream() throws IOException {
+ return urlConnection.getInputStream();
+ }
+
+ @Override
+ protected void connect() throws IOException {
+ logger.info("Connecting to "+raceDataUrl);
+ urlConnection = HttpUrlConnectionHelper.redirectConnection(raceDataUrl);
+ }
+
+ @Override
+ protected boolean isConnected() throws IOException {
+ return urlConnection != null && getInputStream() != null;
+ }
+
+ @Override
+ protected void disconnect() throws IOException {
+ if (urlConnection != null) {
+ if (urlConnection.getInputStream() != null) {
+ urlConnection.getInputStream().close();
+ }
+ urlConnection = null;
+ stop(); // disconnecting from the URL also means to stop the connector
+ }
+ }
+
+ @Override
+ protected TrackedRaceStatusEnum getStatusAfterLoadingIsComplete() {
+ return TrackedRaceStatusEnum.FINISHED; // nothing more to come
+ }
+}
diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterLiveSimulatorConnectorImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterLiveSimulatorConnectorImpl.java
index 3ee62dbcced..69fd38d80d3 100644
--- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterLiveSimulatorConnectorImpl.java
+++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterLiveSimulatorConnectorImpl.java
@@ -1,33 +1,41 @@
package com.sap.sailing.domain.swisstimingadapter.impl;
import java.text.ParseException;
-import java.util.ArrayList;
import java.util.Collections;
+import java.util.LinkedList;
import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.swisstimingadapter.SailMasterMessage;
-public class SailMasterLiveSimulatorConnectorImpl extends SailMasterConnectorImpl {
+/**
+ * Delivers SwissTiming messages with a constant delay (default 250ms) between them. This produces a simulated "live feed"
+ * that may be used for testing purposes.
+ */
+public class SailMasterLiveSimulatorConnectorImpl extends SailMasterConnectorForSocket {
+ private final static Logger logger = Logger.getLogger(SailMasterLiveSimulatorConnectorImpl.class.getName());
+
private final List bufferedMessageList;
private long messageDeliveryIntervalInMs = Long.valueOf(System.getProperty("simulateLiveMode.delayInMillis", "250"));
public SailMasterLiveSimulatorConnectorImpl(String host, int port, String raceId, String raceName, String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker)
throws InterruptedException, ParseException {
- super(host, port, raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker);
- bufferedMessageList = Collections.synchronizedList(new ArrayList());
+ super(host, port, raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker); // causes original delivery to this.notifyListeners(...)
+ bufferedMessageList = Collections.synchronizedList(new LinkedList<>());
Thread messageDeliveryThread = new Thread("SailMasterLiveSimulatorConnector") {
public void run() {
- while(true) {
+ while (true) {
try {
- if(!bufferedMessageList.isEmpty()) {
- SailMasterMessage message = bufferedMessageList.get(0);
+ if (!bufferedMessageList.isEmpty()) {
+ SailMasterMessage message = bufferedMessageList.remove(0);
notifyParentListeners(message);
- bufferedMessageList.remove(0);
- }
+ }
Thread.sleep(messageDeliveryIntervalInMs);
} catch (Exception e) {
+ logger.log(Level.SEVERE, "Error trying to simulate SwissTiming messages", e);
}
}
}
diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterTransceiverImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterTransceiverImpl.java
index 6663131b088..0a6a215f225 100755
--- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterTransceiverImpl.java
+++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterTransceiverImpl.java
@@ -27,23 +27,27 @@ public class SailMasterTransceiverImpl implements SailMasterTransceiver {
*/
@Override
public synchronized void sendMessage(String message, OutputStream os) throws IOException {
- os.write(STX);
- os.write(message.getBytes(Charset.forName("cp1252")));
- os.write(ETX);
+ if (os != null) {
+ os.write(STX);
+ os.write(message.getBytes(Charset.forName("cp1252")));
+ os.write(ETX);
+ }
}
@Override
public synchronized void sendMessage(SailMasterMessage message, OutputStream os) throws IOException {
- if (message.getSequenceNumber() == null) {
- sendMessage(message.getMessage(), os);
- } else {
- StringBuilder sb = new StringBuilder();
- sb.append(message.getSequenceNumber());
- sb.append('|');
- sb.append(message.getMessage());
- sendMessage(sb.toString(), os);
+ if (os != null) {
+ if (message.getSequenceNumber() == null) {
+ sendMessage(message.getMessage(), os);
+ } else {
+ StringBuilder sb = new StringBuilder();
+ sb.append(message.getSequenceNumber());
+ sb.append('|');
+ sb.append(message.getMessage());
+ sendMessage(sb.toString(), os);
+ }
+ os.flush();
}
- os.flush();
}
/**
diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingAdapterImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingAdapterImpl.java
index 9ad31a1ba29..55fbfc44b36 100755
--- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingAdapterImpl.java
+++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingAdapterImpl.java
@@ -34,6 +34,7 @@ import com.sap.sailing.xrr.schema.Person;
import com.sap.sailing.xrr.schema.RaceResult;
import com.sap.sailing.xrr.schema.RegattaResults;
import com.sap.sailing.xrr.schema.Team;
+import com.sap.sse.util.HttpUrlConnectionHelper;
public class SwissTimingAdapterImpl implements SwissTimingAdapter {
private final SwissTimingFactory swissTimingFactory;
@@ -145,7 +146,7 @@ public class SwissTimingAdapterImpl implements SwissTimingAdapter {
public RegattaResults readRegattaEntryListFromXrrUrl(String xrrEntryListUrl) throws IOException, JAXBException {
// try to read the entry list from manage2sail
URL regattaEntryListUrl = new URL(xrrEntryListUrl);
- URLConnection regattaEntryListConn = regattaEntryListUrl.openConnection();
+ URLConnection regattaEntryListConn = HttpUrlConnectionHelper.redirectConnection(regattaEntryListUrl);
RegattaResults regattaResults = ParserFactory.INSTANCE
.createParser((InputStream) regattaEntryListConn.getContent(), xrrEntryListUrl).parse();
return regattaResults;
diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java
index 3552de93e27..fa265d5e7a1 100755
--- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java
+++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java
@@ -2,6 +2,7 @@ package com.sap.sailing.domain.swisstimingadapter.impl;
import java.io.IOException;
import java.net.URISyntaxException;
+import java.net.URL;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.util.HashMap;
@@ -44,12 +45,12 @@ public class SwissTimingFactoryImpl implements SwissTimingFactory {
}
@Override
- public SailMasterConnector getOrCreateSailMasterConnector(String host, int port, String raceId, String raceName,
- String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker) throws InterruptedException, ParseException {
+ public SailMasterConnector getOrCreateSailMasterConnector(String host, int port, String raceId, URL raceDataUrl,
+ String raceName, String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker) throws InterruptedException, ParseException {
if (Boolean.valueOf(System.getProperty("simulateLiveMode", "false"))) {
return getOrCreateSailMasterLiveSimulatorConnector(host, port, raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker);
} else {
- Util.Triple key = new Util.Triple(host, port, raceId);
+ Util.Triple key = new Util.Triple<>(host, port, raceId);
SailMasterConnector result = connectors.get(key);
if (result == null || result.isStopped()) {
if (result == null) {
@@ -57,7 +58,14 @@ public class SwissTimingFactoryImpl implements SwissTimingFactory {
} else {
logger.info("Creating a new connector for "+key+" because the old one was stopped");
}
- result = new SailMasterConnectorImpl(host, port, raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker);
+ if (host == null) {
+ if (raceDataUrl == null) {
+ throw new IllegalArgumentException("raceDataUrl must be provided if no host/port is provided");
+ }
+ result = new SailMasterConnectorForUrlDownload(raceId, raceDataUrl, raceName, raceDescription, boatClass, swissTimingRaceTracker);
+ } else {
+ result = new SailMasterConnectorForSocket(host, port, raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker);
+ }
connectors.put(key, result);
// TODO how do connectors get stopped, terminated and removed from the connectors map again?
} else {
@@ -72,13 +80,10 @@ public class SwissTimingFactoryImpl implements SwissTimingFactory {
String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker) throws InterruptedException, ParseException {
Util.Triple key = new Util.Triple<>(host, port, raceId);
SailMasterConnector result = connectors.get(key);
- if (result == null) {
+ if (result == null || result.isStopped()) {
result = new SailMasterLiveSimulatorConnectorImpl(host, port, raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker);
connectors.put(key, result);
// TODO how do connectors get stopped, terminated and removed from the connectors map again?
- } else if (result.isStopped()) {
- result = new SailMasterLiveSimulatorConnectorImpl(host, port, raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker);
- connectors.put(key, result);
}
return result;
}
diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java
index 6d61ce4a071..621d89a566b 100644
--- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java
+++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java
@@ -4,6 +4,7 @@ import java.io.IOException;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
+import java.net.URL;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.util.Collection;
@@ -36,7 +37,6 @@ import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sailing.domain.common.impl.WindImpl;
import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID;
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
-import com.sap.sailing.domain.common.tracking.TrackingConnectorType;
import com.sap.sailing.domain.racelog.RaceLogAndTrackedRaceResolver;
import com.sap.sailing.domain.racelog.RaceLogStore;
import com.sap.sailing.domain.regattalog.RegattaLogStore;
@@ -49,6 +49,7 @@ import com.sap.sailing.domain.swisstimingadapter.RacingStatus;
import com.sap.sailing.domain.swisstimingadapter.SailMasterConnector;
import com.sap.sailing.domain.swisstimingadapter.SailMasterListener;
import com.sap.sailing.domain.swisstimingadapter.StartList;
+import com.sap.sailing.domain.swisstimingadapter.SwissTimingAdapter;
import com.sap.sailing.domain.swisstimingadapter.SwissTimingFactory;
import com.sap.sailing.domain.swisstimingadapter.SwissTimingRaceTracker;
import com.sap.sailing.domain.tracking.AbstractRaceTrackerImpl;
@@ -60,13 +61,13 @@ import com.sap.sailing.domain.tracking.MarkPassing;
import com.sap.sailing.domain.tracking.RaceHandle;
import com.sap.sailing.domain.tracking.RaceTracker;
import com.sap.sailing.domain.tracking.RaceTrackingHandler;
+import com.sap.sailing.domain.tracking.RaceTrackingHandler.DefaultRaceTrackingHandler;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.TrackedRaceStatus;
import com.sap.sailing.domain.tracking.TrackedRegattaRegistry;
import com.sap.sailing.domain.tracking.TrackingDataLoader;
import com.sap.sailing.domain.tracking.WindStore;
import com.sap.sailing.domain.tracking.WindTrack;
-import com.sap.sailing.domain.tracking.RaceTrackingHandler.DefaultRaceTrackingHandler;
import com.sap.sailing.domain.tracking.impl.TrackedRaceStatusImpl;
import com.sap.sailing.domain.tracking.impl.TrackingConnectorInfoImpl;
import com.sap.sailing.domain.tracking.impl.UpdateHandler;
@@ -187,8 +188,32 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl
}
// Ensure this is called last, otherwise the connector starts running without having a complete racetracker
this.connector = factory.getOrCreateSailMasterConnector(connectivityParams.getHostname(),
- connectivityParams.getPort(), connectivityParams.getRaceID(), connectivityParams.getRaceName(),
- connectivityParams.getRaceDescription(), connectivityParams.getBoatClass(), this);
+ connectivityParams.getPort(), connectivityParams.getRaceID(), getRaceDataUrl(connectivityParams),
+ connectivityParams.getRaceName(), connectivityParams.getRaceDescription(), connectivityParams.getBoatClass(), this);
+ }
+
+ /**
+ * If a valid hostname/port combination is set in the connectivity parameters, {@code null} is returned. Otherwise,
+ * a {@link URL} is constructed from the {@link SwissTimingTrackingConnectivityParameters#getManage2SailEventUrl()}
+ * by stripping all URL parameters and replacing the last path element which is expected to be a file name for the
+ * {@code .json} file by the {race-id}.log combination, expecting the race data to reside in the same folder as
+ * the JSON file describing the Manage2Sail event.
+ */
+ private URL getRaceDataUrl(SwissTimingTrackingConnectivityParameters connectivityParams) throws MalformedURLException {
+ final URL result;
+ if (connectivityParams.getHostname() == null) {
+ final java.net.URL manage2SailEventURL = new URL(connectivityParams.getManage2SailEventUrl());
+ final String path = manage2SailEventURL.getPath();
+ final String logFile = path.substring(0, path.lastIndexOf('/')+1) + connectivityParams.getRaceID()+".log";
+ if (manage2SailEventURL.getPort() != -1) {
+ result = new java.net.URL(manage2SailEventURL.getProtocol(), manage2SailEventURL.getHost(), manage2SailEventURL.getPort(), logFile);
+ } else {
+ result = new java.net.URL(manage2SailEventURL.getProtocol(), manage2SailEventURL.getHost(), logFile);
+ }
+ } else {
+ result = null;
+ }
+ return result;
}
@Override
@@ -415,18 +440,25 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl
}
@Override
- public void storedDataProgress(String raceID, double progress) {
+ public void storedDataProgress(String raceID, double progress, TrackedRaceStatusEnum statusAfterLoadingComplete) {
assert this.raceID.equals(raceID);
if (isTrackedRaceStillReachable()) {
final TrackedRaceStatusImpl newStatus;
if (progress == 0.0) {
newStatus = new TrackedRaceStatusImpl(TrackedRaceStatusEnum.PREPARED, 0.0);
} else if (progress == 1.0) {
- newStatus = new TrackedRaceStatusImpl(TrackedRaceStatusEnum.TRACKING, progress);
+ newStatus = new TrackedRaceStatusImpl(statusAfterLoadingComplete, progress);
} else {
newStatus = new TrackedRaceStatusImpl(TrackedRaceStatusEnum.LOADING, progress);
}
trackedRace.onStatusChanged(this, newStatus);
+ if (newStatus.getStatus() == TrackedRaceStatusEnum.FINISHED) {
+ try {
+ trackedRegattaRegistry.stopTracker(getRegatta(), this);
+ } catch (IOException | InterruptedException e) {
+ logger.log(Level.SEVERE, "Error trying to stop the tracker "+this, e);
+ }
+ }
}
}
@@ -508,7 +540,7 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl
}
}, useInternalMarkPassingAlgorithm, raceLogResolver,
/* Not needed because the RaceTracker is not active on a replica */ Optional.empty(),
- new TrackingConnectorInfoImpl(TrackingConnectorType.SwissTiming, /*no api connection to query the webUrl*/ null));
+ new TrackingConnectorInfoImpl(SwissTimingAdapter.NAME, SwissTimingAdapter.DEFAULT_URL,/*no api connection to query the webUrl*/ null));
addUpdateHandlers();
notifyRaceCreationListeners();
logger.info("Created SwissTiming RaceDefinition and TrackedRace for "+race.getName());
diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/SwissTimingReplayService.java b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/SwissTimingReplayService.java
index ab9c45b1a98..dbfa4fb84f1 100755
--- a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/SwissTimingReplayService.java
+++ b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/SwissTimingReplayService.java
@@ -17,7 +17,6 @@ import com.sap.sailing.domain.tracking.TrackedRegattaRegistry;
import com.sap.sailing.domain.tracking.TrackerManager;
public interface SwissTimingReplayService {
-
List parseJSONObject(InputStream inputStream, String swissTimingUrlText) throws IOException,
ParseException, org.json.simple.parser.ParseException;
diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayToDomainAdapter.java b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayToDomainAdapter.java
index fec9e9f6755..55681df8b58 100755
--- a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayToDomainAdapter.java
+++ b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayToDomainAdapter.java
@@ -36,12 +36,12 @@ import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sailing.domain.common.impl.WindImpl;
import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID;
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
-import com.sap.sailing.domain.common.tracking.TrackingConnectorType;
import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl;
import com.sap.sailing.domain.racelog.RaceLogAndTrackedRaceResolver;
import com.sap.sailing.domain.racelog.RaceLogStore;
import com.sap.sailing.domain.regattalog.RegattaLogStore;
import com.sap.sailing.domain.swisstimingadapter.DomainFactory;
+import com.sap.sailing.domain.swisstimingadapter.SwissTimingAdapter;
import com.sap.sailing.domain.swisstimingreplayadapter.CompetitorStatus;
import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayListener;
import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayParser;
@@ -417,7 +417,7 @@ public class SwissTimingReplayToDomainAdapter extends SwissTimingReplayAdapter i
/* time over which to average speed: */ race.getBoatClass().getApproximateManeuverDurationInMilliseconds(),
/* raceDefinitionSetToUpdate */ null, useInternalMarkPassingAlgorithm, raceLogResolver,
/* Not needed because the RaceTracker is not active on a replica */ Optional.empty(),
- new TrackingConnectorInfoImpl(TrackingConnectorType.SwissTiming, /*no api connection to query the webUrl*/ null));
+ new TrackingConnectorInfoImpl(SwissTimingAdapter.NAME, SwissTimingAdapter.DEFAULT_URL,/*no api connection to query the webUrl*/ null));
trackedRace.onStatusChanged(this, new TrackedRaceStatusImpl(TrackedRaceStatusEnum.LOADING, 0));
TimePoint bestStartTimeKnownSoFar = bestStartTimePerRaceID.get(currentRaceID);
if (bestStartTimeKnownSoFar != null) {
diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/TracTracAdapter.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/TracTracAdapter.java
index 249e99480fd..9394f380580 100755
--- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/TracTracAdapter.java
+++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/TracTracAdapter.java
@@ -24,6 +24,9 @@ import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
public interface TracTracAdapter {
+ static final String NAME = "TracTrac";
+ static final String DEFAULT_URL = "https://www.tractrac.com/";
+
DomainFactory getTracTracDomainFactory();
/**
@@ -105,5 +108,5 @@ public interface TracTracAdapter {
TracTracConfiguration createTracTracConfiguration(String creatorName, String name, String jsonURL,
String liveDataURI,
String storedDataURI, String courseDesignUpdateURI, String tracTracUsername, String tracTracPassword);
-
+
}
diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java
index b499de8dcce..0c0b43e5683 100755
--- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java
+++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java
@@ -56,7 +56,6 @@ import com.sap.sailing.domain.common.RankingMetrics;
import com.sap.sailing.domain.common.ScoringSchemeType;
import com.sap.sailing.domain.common.impl.DegreePosition;
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
-import com.sap.sailing.domain.common.tracking.TrackingConnectorType;
import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl;
import com.sap.sailing.domain.leaderboard.Leaderboard;
import com.sap.sailing.domain.leaderboard.LeaderboardGroup;
@@ -90,6 +89,7 @@ import com.sap.sailing.domain.tractracadapter.MetadataParser.BoatMetaData;
import com.sap.sailing.domain.tractracadapter.MetadataParser.ControlPointMetaData;
import com.sap.sailing.domain.tractracadapter.Receiver;
import com.sap.sailing.domain.tractracadapter.ReceiverType;
+import com.sap.sailing.domain.tractracadapter.TracTracAdapter;
import com.sap.sailing.domain.tractracadapter.TracTracConfiguration;
import com.sap.sailing.domain.tractracadapter.TracTracControlPoint;
import com.sap.sailing.domain.tractracadapter.TracTracRaceTracker;
@@ -633,11 +633,13 @@ public class DomainFactoryImpl implements DomainFactory {
@Override
public DynamicTrackedRace getOrCreateRaceDefinitionAndTrackedRace(DynamicTrackedRegatta trackedRegatta, UUID raceId,
- String raceName, BoatClass boatClass, Map competitorsAndBoats, Course course, Iterable sidelines, WindStore windStore,
- long delayToLiveInMillis, long millisecondsOverWhichToAverageWind,
- DynamicRaceDefinitionSet raceDefinitionSetToUpdate, URI tracTracUpdateURI, UUID tracTracEventUuid,
- String tracTracUsername, String tracTracPassword, boolean ignoreTracTracMarkPassings, RaceLogAndTrackedRaceResolver raceLogResolver,
- Consumer runBeforeExposingRace, IRace tractracRace, RaceTrackingHandler raceTrackingHandler) {
+ String raceName, BoatClass boatClass, Map competitorsAndBoats, Course course,
+ Iterable sidelines, WindStore windStore, long delayToLiveInMillis,
+ long millisecondsOverWhichToAverageWind, DynamicRaceDefinitionSet raceDefinitionSetToUpdate,
+ URI tracTracUpdateURI, UUID tracTracEventUuid, String tracTracUsername, String tracTracPassword,
+ boolean ignoreTracTracMarkPassings, RaceLogAndTrackedRaceResolver raceLogResolver,
+ Consumer runBeforeExposingRace, IRace tractracRace,
+ RaceTrackingHandler raceTrackingHandler) {
synchronized (raceCache) {
RaceDefinition raceDefinition = raceCache.get(raceId);
if (raceDefinition == null) {
@@ -661,7 +663,10 @@ public class DomainFactoryImpl implements DomainFactory {
trackedRegatta.getRegatta().addRace(raceDefinition);
TrackingConnectorInfo trackingConnectorInfo = null;
if (tractracRace != null) {
- trackingConnectorInfo = new TrackingConnectorInfoImpl(TrackingConnectorType.TracTrac, tractracRace.getEvent().getWebURL());
+ final URL webUrl = tractracRace.getEvent().getWebURL();
+ final String webUrlString = webUrl == null ? null : webUrl.toString();
+ trackingConnectorInfo = new TrackingConnectorInfoImpl(TracTracAdapter.NAME,
+ TracTracAdapter.DEFAULT_URL, webUrlString);
}
trackedRace = createTrackedRace(trackedRegatta, raceDefinition, sidelines, windStore,
delayToLiveInMillis, millisecondsOverWhichToAverageWind, raceDefinitionSetToUpdate, ignoreTracTracMarkPassings,
diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceCourseReceiver.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceCourseReceiver.java
index 366cb2236bd..e7efca55991 100755
--- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceCourseReceiver.java
+++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceCourseReceiver.java
@@ -1,6 +1,7 @@
package com.sap.sailing.domain.tractracadapter.impl;
import java.net.URI;
+import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@@ -20,7 +21,6 @@ import com.sap.sailing.domain.base.RaceDefinition;
import com.sap.sailing.domain.base.Regatta;
import com.sap.sailing.domain.base.Sideline;
import com.sap.sailing.domain.common.PassingInstruction;
-import com.sap.sailing.domain.common.tracking.TrackingConnectorType;
import com.sap.sailing.domain.leaderboard.LeaderboardGroupResolver;
import com.sap.sailing.domain.racelog.RaceLogAndTrackedRaceResolver;
import com.sap.sailing.domain.tracking.DynamicRaceDefinitionSet;
@@ -31,6 +31,7 @@ import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.WindStore;
import com.sap.sailing.domain.tracking.impl.TrackingConnectorInfoImpl;
import com.sap.sailing.domain.tractracadapter.DomainFactory;
+import com.sap.sailing.domain.tractracadapter.TracTracAdapter;
import com.sap.sailing.domain.tractracadapter.TracTracControlPoint;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util.Pair;
@@ -269,14 +270,17 @@ public class RaceCourseReceiver extends AbstractReceiverWithQueue sidelines,
Consumer runAfterCreatingTrackedRace) {
- DynamicTrackedRace trackedRace = raceTrackingHandler.createTrackedRace(getTrackedRegatta(), race, sidelines, windStore,
- delayToLiveInMillis, millisecondsOverWhichToAverageWind,
+ final URL webUrl = tractracRace.getEvent().getWebURL();
+ final String webUrlString = webUrl == null ? null : webUrl.toString();
+ DynamicTrackedRace trackedRace = raceTrackingHandler.createTrackedRace(getTrackedRegatta(), race, sidelines,
+ windStore, delayToLiveInMillis, millisecondsOverWhichToAverageWind,
/* time over which to average speed: */ race.getBoatClass()
.getApproximateManeuverDurationInMilliseconds(),
raceDefinitionSetToUpdate, useInternalMarkPassingAlgorithm, raceLogResolver,
/* ThreadLocalTransporter not needed because the RaceTracker is not active on a replica */ Optional
.empty(),
- new TrackingConnectorInfoImpl(TrackingConnectorType.TracTrac, tractracRace.getEvent().getWebURL()));
+ new TrackingConnectorInfoImpl(TracTracAdapter.NAME, TracTracAdapter.DEFAULT_URL,
+ webUrlString));
if (runAfterCreatingTrackedRace != null) {
runAfterCreatingTrackedRace.accept(trackedRace);
}
diff --git a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderPollerTest.java b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderPollerTest.java
index 500d7970fd1..396271b9fdf 100644
--- a/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderPollerTest.java
+++ b/java/com.sap.sailing.domain.windfinderadapter.test/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderPollerTest.java
@@ -18,6 +18,7 @@ import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import com.sap.sailing.domain.base.RaceDefinition;
+import com.sap.sailing.domain.base.Regatta;
import com.sap.sailing.domain.common.Wind;
import com.sap.sailing.domain.common.WindSource;
import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID;
@@ -34,8 +35,11 @@ public class WindFinderPollerTest {
factory.addReviewedSpotCollection(new ReviewedSpotsCollectionImpl("schilksee"));
final List> wind = Collections.synchronizedList(new ArrayList<>());
final RaceDefinition mockedRaceDefinition = mock(RaceDefinition.class);
+ final Regatta mockedRegatta = mock(Regatta.class);
final DynamicTrackedRegatta mockedTrackedRegatta = mock(DynamicTrackedRegatta.class);
final DynamicTrackedRace mockedTrackedRace = mock(DynamicTrackedRace.class);
+ when(mockedTrackedRace.getTrackedRegatta()).thenReturn(mockedTrackedRegatta);
+ when(mockedTrackedRegatta.getRegatta()).thenReturn(mockedRegatta);
when(mockedTrackedRegatta.getExistingTrackedRace(mockedRaceDefinition)).thenReturn(mockedTrackedRace);
when(mockedTrackedRegatta.getTrackedRace(mockedRaceDefinition)).thenReturn(mockedTrackedRace);
when(mockedTrackedRace.recordWind(any(Wind.class), any(WindSourceWithAdditionalID.class))).thenAnswer(new Answer() {
diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java
index d93ab6ddf45..63fce69766b 100644
--- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java
+++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/ReviewedSpotsCollectionImpl.java
@@ -104,10 +104,15 @@ public class ReviewedSpotsCollectionImpl implements ReviewedSpotsCollection {
private Iterable loadSpots() throws IOException, ParseException, MalformedURLException {
final Iterable result;
- JSONArray spotsAsJson = (JSONArray) new JSONParser().parse(new InputStreamReader(
- (InputStream) new URL(Activator.BASE_URL_FOR_JSON_DOCUMENTS+"/"+getId()+SPOT_LIST_DOCUMENT_SUFFIX).getContent()));
- result = parser.parseSpots(spotsAsJson, this);
- return result;
+ final InputStreamReader in = new InputStreamReader(
+ (InputStream) new URL(Activator.BASE_URL_FOR_JSON_DOCUMENTS+"/"+getId()+SPOT_LIST_DOCUMENT_SUFFIX).getContent());
+ try {
+ JSONArray spotsAsJson = (JSONArray) new JSONParser().parse(in);
+ result = parser.parseSpots(spotsAsJson, this);
+ return result;
+ } finally {
+ in.close();
+ }
}
@Override
diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java
index 5cbf86a8081..6a26d719caf 100644
--- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java
+++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/SpotImpl.java
@@ -7,8 +7,13 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import org.json.simple.JSONArray;
import org.json.simple.parser.JSONParser;
@@ -18,34 +23,83 @@ import com.sap.sailing.domain.common.Wind;
import com.sap.sailing.domain.common.windfinder.SpotDTO;
import com.sap.sailing.domain.windfinder.ReviewedSpotsCollection;
import com.sap.sailing.domain.windfinder.Spot;
+import com.sap.sailing.domain.windfinder.WindFinderSpotListener;
+import com.sap.sse.common.Duration;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
+import com.sap.sse.util.ThreadPoolUtil;
public class SpotImpl extends SpotDTO implements Spot {
private static final long serialVersionUID = -7793334775486020313L;
+
+ private static final Logger logger = Logger.getLogger(SpotImpl.class.getName());
private final WindFinderReportParser parser;
private final ReviewedSpotsCollection collection;
+ private final ConcurrentMap listeners;
+ private TimePoint latestMeasurement;
+ private static final Duration POLL_EVERY = Duration.ONE_MINUTE;
+ private ScheduledFuture> poller;
public SpotImpl(String name, String id, String keyword, String englishCountryName, Position position, WindFinderReportParser parser, ReviewedSpotsCollection collection) {
super(name, id, keyword, englishCountryName, position);
this.parser = parser;
this.collection = collection;
+ listeners = new ConcurrentHashMap<>();
}
@Override
- public Wind getLatestMeasurement() throws NumberFormatException, ParseException, org.json.simple.parser.ParseException, MalformedURLException, IOException {
- final Iterable measurements = getAllMeasurements();
- final Wind result;
- if (measurements != null && !Util.isEmpty(measurements)) {
- final List measurementsSortedByTimepoint = new ArrayList<>();
- Util.addAll(measurements, measurementsSortedByTimepoint);
- Collections.sort(measurementsSortedByTimepoint, (w1, w2)->w1.getTimePoint().compareTo(w2.getTimePoint()));
- result = Util.last(measurementsSortedByTimepoint);
- } else {
- result = null;
+ public void addListener(WindFinderSpotListener listener) {
+ if (listeners.isEmpty()) {
+ ensurePollerRunning();
+ }
+ listeners.put(listener, true);
+ }
+
+ private synchronized void ensurePollerRunning() {
+ if (poller == null) {
+ logger.info("Starting to poll from WindFinder spot "+getId()+" as the first listener started listening.");
+ poller = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor().scheduleAtFixedRate(this::poll,
+ /* initialDelay */ 0, /* period */ POLL_EVERY.asMillis(), TimeUnit.MILLISECONDS);
+ }
+ }
+
+ /**
+ * Fetches wind measurements from this spot and notifies new ones to the {@link #listeners}.
+ */
+ private void poll() {
+ try {
+ final Iterable windFixes = getAllMeasurementsAfter(latestMeasurement);
+ logger.fine("Received "+Util.size(windFixes)+" wind fixes from WindFinder spot "+getId()+". Notifying "+listeners.size()+" listeners.");
+ for (final Wind wind : windFixes) {
+ if (latestMeasurement == null || wind.getTimePoint().after(latestMeasurement)) {
+ latestMeasurement = wind.getTimePoint();
+ }
+ }
+ notifyListeners(windFixes);
+ } catch (IOException | ParseException | org.json.simple.parser.ParseException e) {
+ logger.log(Level.SEVERE, "Error trying to obtain WindFinder measurements from spot "+getId(), e);
+ }
+ }
+
+ private void notifyListeners(Iterable windFixes) {
+ for (final WindFinderSpotListener listener : listeners.keySet()) {
+ listener.windDataReceived(windFixes, this);
+ }
+ }
+
+ private synchronized void stopPoller() {
+ logger.info("Stopping polling from WindFinder spot "+getId()+" as last listener stopped listening.");
+ poller.cancel(/* mayInterruptIfRunning */ false);
+ poller = null;
+ }
+
+ @Override
+ public void removeListener(WindFinderSpotListener listener) {
+ listeners.remove(listener);
+ if (listeners.isEmpty()) {
+ stopPoller();
}
- return result;
}
@Override
diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactoryImpl.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactoryImpl.java
index 35420c335c2..cae65c2f2e6 100644
--- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactoryImpl.java
+++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderTrackerFactoryImpl.java
@@ -20,6 +20,7 @@ import org.json.simple.parser.ParseException;
import org.osgi.util.tracker.ServiceTracker;
import com.sap.sailing.domain.base.RaceDefinition;
+import com.sap.sailing.domain.common.RegattaIdentifier;
import com.sap.sailing.domain.common.WindFinderReviewedSpotsCollectionIdProvider;
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
import com.sap.sailing.domain.tracking.DynamicTrackedRegatta;
@@ -47,7 +48,7 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory {
/**
* Can hold reviewed spots collections. Starts out valid but empty. Useful if we're running outside of an OSGi
* environment such as a test set-up. The collections provided here will be added to the set returned by
- * {@link #getReviewedSpotsCollections(boolean)}.
+ * {@link #getAllReviewedSpotsCollections(boolean)}.
*/
private Set reviewedSpotsCollections;
private final ServiceTracker reviewedSpotsCollectionIdProvider;
@@ -63,9 +64,8 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory {
reviewedSpotsCollectionsByIdCache = executor.schedule(
()->{
final ConcurrentMap result = new ConcurrentHashMap<>();
- reviewedSpotsCollectionIdProvider.getService();
try {
- for (final ReviewedSpotsCollection c : loadSpotCollectionsFromProvider(/* lookupInCache */ false)) {
+ for (final ReviewedSpotsCollection c : loadAllSpotCollectionsFromProvider(/* lookupInCache */ false)) {
result.put(c.getId(), c);
}
} catch (Exception e) {
@@ -119,13 +119,26 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory {
}
@Override
- public Iterable getReviewedSpotsCollections(boolean cached) throws InterruptedException, ExecutionException {
+ public Iterable getReviewedSpotsCollections(RegattaIdentifier regattaIdentifier) throws InterruptedException, ExecutionException {
+ final Set result = new HashSet<>();
+ result.addAll(reviewedSpotsCollections);
+ final WindFinderReviewedSpotsCollectionIdProvider provider;
+ if (reviewedSpotsCollectionIdProvider != null && (provider = reviewedSpotsCollectionIdProvider.getService()) != null) {
+ for (String id : provider.getWindFinderReviewedSpotsCollectionIdsByRegatta(regattaIdentifier)) {
+ result.add(getReviewedSpotsCollectionById(id, /* lookupInCache, re-use existing spot collection objects */ true));
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public Iterable getAllReviewedSpotsCollections(boolean cached) throws InterruptedException, ExecutionException {
final Set result = new HashSet<>();
if (cached) {
result.addAll(reviewedSpotsCollectionsByIdCache.get().values());
} else {
result.addAll(reviewedSpotsCollections);
- Util.addAll(loadSpotCollectionsFromProvider(/* lookupInCache */ true), result);
+ Util.addAll(loadAllSpotCollectionsFromProvider(/* lookupInCache */ true), result);
for (final ReviewedSpotsCollection c : result) {
reviewedSpotsCollectionsByIdCache.get().putIfAbsent(c.getId(), c);
}
@@ -133,11 +146,11 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory {
return result;
}
- private Iterable loadSpotCollectionsFromProvider(boolean lookupInCache) throws InterruptedException, ExecutionException {
+ private Iterable loadAllSpotCollectionsFromProvider(boolean lookupInCache) throws InterruptedException, ExecutionException {
final Set result = new HashSet<>();
final WindFinderReviewedSpotsCollectionIdProvider provider;
if (reviewedSpotsCollectionIdProvider != null && (provider = reviewedSpotsCollectionIdProvider.getService()) != null) {
- for (String id : provider.getWindFinderReviewedSpotsCollectionIds()) {
+ for (String id : provider.getAllWindFinderReviewedSpotsCollectionIds()) {
result.add(getReviewedSpotsCollectionById(id, lookupInCache));
}
}
@@ -158,7 +171,7 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory {
@Override
public Spot getSpotById(String spotId, boolean cached) throws MalformedURLException, IOException, ParseException, InterruptedException, ExecutionException {
Spot result = null;
- for (final ReviewedSpotsCollection coll : getReviewedSpotsCollections(/* cached */ cached)) {
+ for (final ReviewedSpotsCollection coll : getAllReviewedSpotsCollections(/* cached */ cached)) {
for (final Spot spot : coll.getSpots(/* cached */ cached)) {
if (Util.equalsWithNull(spot.getId(), spotId)) {
result = spot;
@@ -178,4 +191,5 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory {
reviewedSpotsCollections.remove(collection);
reviewedSpotsCollectionsByIdCache.get().remove(collection.getId());
}
+
}
diff --git a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java
index 07cad18abb5..574047393e8 100644
--- a/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java
+++ b/java/com.sap.sailing.domain.windfinderadapter/src/com/sap/sailing/domain/windfinderadapter/impl/WindFinderWindTracker.java
@@ -6,10 +6,6 @@ import java.util.HashSet;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-import java.util.logging.Level;
-import java.util.logging.Logger;
import org.json.simple.parser.ParseException;
@@ -21,10 +17,8 @@ import com.sap.sailing.domain.tracking.WindTrack;
import com.sap.sailing.domain.tracking.WindTracker;
import com.sap.sailing.domain.windfinder.ReviewedSpotsCollection;
import com.sap.sailing.domain.windfinder.Spot;
-import com.sap.sse.common.Duration;
+import com.sap.sailing.domain.windfinder.WindFinderSpotListener;
import com.sap.sse.common.TimePoint;
-import com.sap.sse.common.Util;
-import com.sap.sse.util.ThreadPoolUtil;
/**
* When an object of this tracker type is created and there are WindFinder stations available, it schedules regular
@@ -42,74 +36,53 @@ import com.sap.sse.util.ThreadPoolUtil;
* @author Axel Uhl (D043530)
*
*/
-public class WindFinderWindTracker implements WindTracker, Runnable {
- private static final Logger logger = Logger.getLogger(WindFinderWindTracker.class.getName());
- private static final Duration POLL_EVERY = Duration.ONE_MINUTE;
-
+public class WindFinderWindTracker implements WindTracker, WindFinderSpotListener {
private final DynamicTrackedRace trackedRace;
private final WindFinderTrackerFactoryImpl factory;
-
- private final ScheduledFuture> poller;
-
private WeakHashMap timePointOfLastMeasurement;
+ private final Iterable spots;
- /**
- * The set of all {@link ReviewedSpotsCollection}s delivered by the {@link #factory} when this tracker
- * was created. This is the basis for {@link #getUsefulSpots()} when evaluating, e.g., based on the
- * {@link #trackedRace race's} location, which of the spots is actually useful for this tracker.
- */
- private final Iterable allSpotCollections;
-
- public WindFinderWindTracker(DynamicTrackedRace trackedRace, WindFinderTrackerFactoryImpl factory) throws InterruptedException, ExecutionException {
+ public WindFinderWindTracker(DynamicTrackedRace trackedRace, WindFinderTrackerFactoryImpl factory)
+ throws InterruptedException, ExecutionException, MalformedURLException, IOException, ParseException {
this.trackedRace = trackedRace;
this.factory = factory;
- this.allSpotCollections = factory.getReviewedSpotsCollections(/* cached */ false); // obtain fresh copy of all spots, updating cache
this.timePointOfLastMeasurement = new WeakHashMap<>();
- this.poller = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor().scheduleAtFixedRate(this,
- /* initialDelay */ 0, /* period */ POLL_EVERY.asMillis(), TimeUnit.MILLISECONDS);
+ spots = subscribeToUsefulSpots();
}
- /**
- * This method is executed in order to poll the WindFinder spots for new measurements
- */
@Override
- public void run() {
- try {
- final Iterable usefulSpots = getUsefulSpots();
- for (final Spot usefulSpot : usefulSpots) {
- final Iterable windFixes = usefulSpot.getAllMeasurementsAfter(timePointOfLastMeasurement.get(usefulSpot));
- if (!Util.isEmpty(windFixes)) {
- final WindSourceWithAdditionalID windSource = new WindSourceWithAdditionalID(WindSourceType.WINDFINDER, usefulSpot.getId());
- final WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource);
- for (final Wind wind : windFixes) {
- final Wind existingFix = windTrack==null?null/*test scenario*/:windTrack.getFirstRawFixAtOrAfter(wind.getTimePoint());
- if (existingFix == null || !existingFix.getTimePoint().equals(wind.getTimePoint())) {
- // avoid duplicates by adding the same fix again
- trackedRace.recordWind(wind, windSource);
- if (!timePointOfLastMeasurement.containsKey(usefulSpot) || wind.getTimePoint().after(timePointOfLastMeasurement.get(usefulSpot))) {
- timePointOfLastMeasurement.put(usefulSpot, wind.getTimePoint());
- }
- }
- }
+ public void windDataReceived(Iterable windFixes, Spot spot) {
+ final WindSourceWithAdditionalID windSource = new WindSourceWithAdditionalID(WindSourceType.WINDFINDER, spot.getId());
+ final WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource);
+ for (final Wind wind : windFixes) {
+ final Wind existingFix = windTrack==null?null/*test scenario*/:windTrack.getFirstRawFixAtOrAfter(wind.getTimePoint());
+ if (existingFix == null || !existingFix.getTimePoint().equals(wind.getTimePoint())) {
+ // avoid duplicates by adding the same fix again
+ trackedRace.recordWind(wind, windSource);
+ if (!timePointOfLastMeasurement.containsKey(spot) || wind.getTimePoint().after(timePointOfLastMeasurement.get(spot))) {
+ timePointOfLastMeasurement.put(spot, wind.getTimePoint());
}
}
- } catch (Exception e) {
- logger.log(Level.WARNING, "Exception trying to obtain WindFinder data for race "+trackedRace.getRace().getName(), e);
}
}
-
- private Iterable getUsefulSpots() throws MalformedURLException, IOException, ParseException, InterruptedException, ExecutionException {
- final Set spots = new HashSet<>();
- for (final ReviewedSpotsCollection collection : allSpotCollections) {
- // TODO bug1301 judge each spot's usefulness given the location of trackedRace
- Util.addAll(collection.getSpots(/* cached */ false), spots);
+
+ public Iterable subscribeToUsefulSpots()
+ throws MalformedURLException, IOException, ParseException, InterruptedException, ExecutionException {
+ final Set result = new HashSet<>();
+ for (final ReviewedSpotsCollection spotCollection : factory.getReviewedSpotsCollections(trackedRace.getTrackedRegatta().getRegatta().getRegattaIdentifier())) {
+ for (final Spot spot : spotCollection.getSpots(/* cached */ true)) {
+ result.add(spot);
+ spot.addListener(this);
+ }
}
- return spots;
+ return result;
}
@Override
public void stop() {
- poller.cancel(/* mayInterruptIfRunning */ false);
+ for (final Spot spot : spots) {
+ spot.removeListener(this);
+ }
factory.trackerStopped(trackedRace.getRace());
}
diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateChooserImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateChooserImpl.java
index 46bd2013682..e063568b46d 100755
--- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateChooserImpl.java
+++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateChooserImpl.java
@@ -481,43 +481,49 @@ public class CandidateChooserImpl implements CandidateChooser {
@Override
public void setFixedPassing(Competitor c, Integer zeroBasedIndexOfWaypoint, TimePoint t) {
- LockUtil.lockForWrite(perCompetitorLocks.get(c));
- try {
- Candidate fixedCan = new CandidateForFixedMarkPassingImpl(zeroBasedIndexOfWaypoint + 1, t, 1, Util.get(race.getRace().getCourse().getWaypoints(), zeroBasedIndexOfWaypoint));
- NavigableSet fixed = fixedPassings.get(c);
- if (fixed != null) { // can only set the mark passing if the competitor is still part of this race
- if (!fixed.add(fixedCan)) {
- Candidate old = fixed.ceiling(fixedCan);
- fixed.remove(old);
- removeCandidates(c, Collections.singleton(old));
- fixed.add(fixedCan);
+ final NamedReentrantReadWriteLock lock = perCompetitorLocks.get(c);
+ if (lock != null) { // otherwise this is a phony competitor and we aren't interested in fixed mark passings for it
+ LockUtil.lockForWrite(lock);
+ try {
+ Candidate fixedCan = new CandidateForFixedMarkPassingImpl(zeroBasedIndexOfWaypoint + 1, t, 1, Util.get(race.getRace().getCourse().getWaypoints(), zeroBasedIndexOfWaypoint));
+ NavigableSet fixed = fixedPassings.get(c);
+ if (fixed != null) { // can only set the mark passing if the competitor is still part of this race
+ if (!fixed.add(fixedCan)) {
+ Candidate old = fixed.ceiling(fixedCan);
+ fixed.remove(old);
+ removeCandidates(c, Collections.singleton(old));
+ fixed.add(fixedCan);
+ }
+ addCandidates(c, Collections.singleton(fixedCan));
+ findShortestPath(c);
}
- addCandidates(c, Collections.singleton(fixedCan));
- findShortestPath(c);
+ } finally {
+ LockUtil.unlockAfterWrite(lock);
}
- } finally {
- LockUtil.unlockAfterWrite(perCompetitorLocks.get(c));
}
}
@Override
public void removeFixedPassing(Competitor c, Integer zeroBasedIndexOfWaypoint) {
- LockUtil.lockForWrite(perCompetitorLocks.get(c));
- try {
- Candidate toRemove = null;
- for (Candidate can : fixedPassings.get(c)) {
- if (can.getOneBasedIndexOfWaypoint() - 1 == zeroBasedIndexOfWaypoint) {
- toRemove = can;
- break;
+ final NamedReentrantReadWriteLock lock = perCompetitorLocks.get(c);
+ if (lock != null) { // otherwise this is a phony competitor and we aren't interested in fixed mark passings for it
+ LockUtil.lockForWrite(lock);
+ try {
+ Candidate toRemove = null;
+ for (Candidate can : fixedPassings.get(c)) {
+ if (can.getOneBasedIndexOfWaypoint() - 1 == zeroBasedIndexOfWaypoint) {
+ toRemove = can;
+ break;
+ }
}
+ if (toRemove != null) {
+ fixedPassings.get(c).remove(toRemove);
+ removeCandidates(c, Arrays.asList(toRemove));
+ findShortestPath(c);
+ }
+ } finally {
+ LockUtil.unlockAfterWrite(lock);
}
- if (toRemove != null) {
- fixedPassings.get(c).remove(toRemove);
- removeCandidates(c, Arrays.asList(toRemove));
- findShortestPath(c);
- }
- } finally {
- LockUtil.unlockAfterWrite(perCompetitorLocks.get(c));
}
}
diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/TimeRangeWithNullStartMeaningEmpty.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/TimeRangeWithNullStartMeaningEmpty.java
index 48d9af3d562..b22b4c84e88 100644
--- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/TimeRangeWithNullStartMeaningEmpty.java
+++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/TimeRangeWithNullStartMeaningEmpty.java
@@ -20,7 +20,10 @@ public class TimeRangeWithNullStartMeaningEmpty {
private final boolean toIsInclusive;
public TimeRangeWithNullStartMeaningEmpty(TimePoint from, TimePoint to, boolean toIsInclusive) {
- timeRangeOrNull = from == null ? null : new TimeRangeImpl(from, to, toIsInclusive);
+ // the time range will be adjusted to an empty time range starting at "to" in case "from" is after
+ // "to"; this to avoid an IllegalArgumentException should "from" be after "to"; see bug 5362
+ timeRangeOrNull = from == null ? null : new TimeRangeImpl(
+ to==null || from.before(to) ? from : to, to, toIsInclusive);
this.from = from;
this.to = to;
this.toIsInclusive = toIsInclusive;
@@ -54,6 +57,11 @@ public class TimeRangeWithNullStartMeaningEmpty {
return getWithNewTo(toExclusive, /* toIsInclusive */ false);
}
+ /**
+ * @return {@code null} if {@link #from} is {@code null}, representing a time range that is open at its start;
+ * should {@link #from} have been set to a time point later than {@link #to}, the time range returned is
+ * {@link TimeRange#isEmpty() empty} and positioned at {@link #to}.
+ */
public TimeRange getTimeRangeOrNull() {
return timeRangeOrNull;
}
diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/Spot.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/Spot.java
index 5cdf0d3ce53..2200e4e384f 100644
--- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/Spot.java
+++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/Spot.java
@@ -16,8 +16,10 @@ import com.sap.sse.common.TimePoint;
*
*/
public interface Spot extends SpotBase {
- Wind getLatestMeasurement() throws NumberFormatException, ParseException, org.json.simple.parser.ParseException, MalformedURLException, IOException;
-
+ void addListener(WindFinderSpotListener listener);
+
+ void removeListener(WindFinderSpotListener listener);
+
Iterable getAllMeasurements() throws IOException, MalformedURLException, ParseException, org.json.simple.parser.ParseException;
/**
diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/WindFinderSpotListener.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/WindFinderSpotListener.java
new file mode 100755
index 00000000000..8b40be0d214
--- /dev/null
+++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/WindFinderSpotListener.java
@@ -0,0 +1,7 @@
+package com.sap.sailing.domain.windfinder;
+
+import com.sap.sailing.domain.common.Wind;
+
+public interface WindFinderSpotListener {
+ void windDataReceived(Iterable windFixes, Spot spot);
+}
diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/WindFinderTrackerFactory.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/WindFinderTrackerFactory.java
index e48885bb85c..c82bb73bc18 100644
--- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/WindFinderTrackerFactory.java
+++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/windfinder/WindFinderTrackerFactory.java
@@ -6,14 +6,15 @@ import java.util.concurrent.ExecutionException;
import org.json.simple.parser.ParseException;
+import com.sap.sailing.domain.common.RegattaIdentifier;
import com.sap.sailing.domain.tracking.WindTrackerFactory;
public interface WindFinderTrackerFactory extends WindTrackerFactory {
/**
* Obtains the reviewed spot collections that this factory knows about. This set is constructed from the collections
- * provided explicitly using {@link #addReviewedSpotCollection(ReviewedSpotCollection)} and
- * {@link #removeReviewedSpotCollection(ReviewedSpotsCollection)}, and is extended by the collections obtained
+ * provided explicitly using {@code addReviewedSpotCollection(ReviewedSpotCollection)} and
+ * {@code removeReviewedSpotCollection(ReviewedSpotsCollection)}, and is extended by the collections obtained
* through {@link #reviewedSpotsCollectionIdProvider} if a corresponding service can be resolved.
*
* @param cached
@@ -23,7 +24,13 @@ public interface WindFinderTrackerFactory extends WindTrackerFactory {
*
* @return a non-live set of spots collections known by this factory at this point in time
*/
- Iterable getReviewedSpotsCollections(boolean cached) throws InterruptedException, ExecutionException;
+ Iterable getAllReviewedSpotsCollections(boolean cached) throws InterruptedException, ExecutionException;
+
+ /**
+ * Like {@link #getAllReviewedSpotsCollections(boolean)}, but never using caching, always asking the underlying provider for
+ * a fresh copy of spot collections, qualified by the regatta to which they shall apply.
+ */
+ Iterable getReviewedSpotsCollections(RegattaIdentifier regattaIdentifier) throws InterruptedException, ExecutionException;
ReviewedSpotsCollection getReviewedSpotsCollectionById(String spotsCollectionId, boolean lookupInCache) throws InterruptedException, ExecutionException;
diff --git a/java/com.sap.sailing.gwt.ui/AdminConsole.html b/java/com.sap.sailing.gwt.ui/AdminConsole.html
index db0e90a4034..1eca6e0137e 100644
--- a/java/com.sap.sailing.gwt.ui/AdminConsole.html
+++ b/java/com.sap.sailing.gwt.ui/AdminConsole.html
@@ -8,7 +8,11 @@
-
+
+
+
+
diff --git a/java/com.sap.sailing.gwt.ui/AutoPlay.html b/java/com.sap.sailing.gwt.ui/AutoPlay.html
index 622fe347c41..8e8cdbe0456 100644
--- a/java/com.sap.sailing.gwt.ui/AutoPlay.html
+++ b/java/com.sap.sailing.gwt.ui/AutoPlay.html
@@ -9,6 +9,11 @@
+
+
+
+
diff --git a/java/com.sap.sailing.gwt.ui/DataMining.css b/java/com.sap.sailing.gwt.ui/DataMining.css
index 1818050740f..be6f4c49aee 100644
--- a/java/com.sap.sailing.gwt.ui/DataMining.css
+++ b/java/com.sap.sailing.gwt.ui/DataMining.css
@@ -317,11 +317,11 @@ input.magnifier {
.dataMiningListBox {
min-width: 120px;
+ margin-right: 5px;
}
.queryDefinitionViewerContent {
background-color: white;
- padding: 5px;
}
.filterDimensionsTitleLabel {
diff --git a/java/com.sap.sailing.gwt.ui/DataMining.html b/java/com.sap.sailing.gwt.ui/DataMining.html
index cea6a7dfbbd..5b8c0fe86d4 100644
--- a/java/com.sap.sailing.gwt.ui/DataMining.html
+++ b/java/com.sap.sailing.gwt.ui/DataMining.html
@@ -7,6 +7,11 @@
+
+
+
+
diff --git a/java/com.sap.sailing.gwt.ui/Home.html b/java/com.sap.sailing.gwt.ui/Home.html
index 3014189d5ef..11e7626d524 100644
--- a/java/com.sap.sailing.gwt.ui/Home.html
+++ b/java/com.sap.sailing.gwt.ui/Home.html
@@ -3,7 +3,11 @@
-
+
+
+
+
${SAP}Sailing - Homepage
@@ -14,8 +18,7 @@
href="/sailing-fontface-1.0.cache.css">
-
-
+
@@ -24,7 +27,7 @@