Merge branch 'master' into racecommittee_app

This commit is contained in:
Axel Uhl committed 2013-12-06 14:49:01 +01:00
commit 1efebeef57
84 files changed
+2098 -449

No files matched your search

+1 -1
View File
@@ -137,7 +137,7 @@ do
case $option in
g) gwtcompile=0;;
t) testing=0;;
b) onegwtpermutationonly=1;;
b) onegwtpermutationonly=1;;
o) offline=1;;
c) clean="";;
p) proxy=1;;
@@ -4,6 +4,7 @@ import java.io.IOException;
import java.text.ParseException;
import com.sap.sailing.declination.impl.DeclinationServiceImpl;
import com.sap.sailing.declination.impl.NOAAImporter;
import com.sap.sailing.domain.common.Distance;
import com.sap.sailing.domain.common.Mile;
import com.sap.sailing.domain.common.Position;
@@ -20,11 +21,15 @@ public interface DeclinationService {
/**
* Obtains declination information with the default precision of this declination service in time and space
* dimensions (e.g., one degree N/S and E/W and one year on the time axis). Particularly the time resolution may
* benefit from an extrapolation using the {@link Declination#getAnnualChange() annual change} specification.
* Note that the result is <em>not</em> corrected automatically by the {@link Declination#getAnnualChange()
* annual change} of the result found for the position. The caller may do that using
* benefit from an extrapolation using the {@link Declination#getAnnualChange() annual change} specification. Note
* that the result is <em>not</em> corrected automatically by the {@link Declination#getAnnualChange() annual
* change} of the result found for the position. The caller may do that using
* {@link Declination#getBearingCorrectedTo(TimePoint)}.
* @param timeoutForOnlineFetchInMilliseconds TODO
*
* @param timeoutForOnlineFetchInMilliseconds
* see {@link NOAAImporter#getDeclination(Position, TimePoint, long)}; if 0, waits forever; only needed
* if the pre-stored declinations don't fulfill the request up to the tolerance distance configured when
* constructing this service
*/
Declination getDeclination(TimePoint timePoint, Position position, long timeoutForOnlineFetchInMilliseconds) throws IOException,
ClassNotFoundException, ParseException;
@@ -36,9 +41,13 @@ public interface DeclinationService {
* <p>
*
* The time point precision is the default precision of this service. Note that the result is <em>not</em> corrected
* automatically by the {@link Declination#getAnnualChange() annual change} of the result found for the
* position. The caller may do that using {@link Declination#getBearingCorrectedTo(TimePoint)}.
* @param timeoutForOnlineFetchInMilliseconds TODO
* automatically by the {@link Declination#getAnnualChange() annual change} of the result found for the position.
* The caller may do that using {@link Declination#getBearingCorrectedTo(TimePoint)}.
*
* @param timeoutForOnlineFetchInMilliseconds
* see {@link NOAAImporter#getDeclination(Position, TimePoint, long)}; if 0, waits forever; only needed
* if the pre-stored declinations don't fulfill the request up to the tolerance distance provided by the
* <code>maxDistance</code> parameter
*/
Declination getDeclination(TimePoint timePoint, Position position, Distance maxDistance, long timeoutForOnlineFetchInMilliseconds) throws IOException,
ClassNotFoundException, ParseException;
@@ -13,6 +13,10 @@ public interface Bearing extends Serializable {
*/
Bearing reverse();
/**
* Adds the bearing <code>diff</code> to this bearing and returns the new result. The result will be between 0 and 360 (inclusive) if
* this bearing and <code>diff</code> are also within 0 and 360.
*/
Bearing add(Bearing diff);
/**
@@ -29,4 +29,8 @@ public interface SpeedWithBearing extends Speed {
*/
Speed projectTo(Position position, Bearing bearing);
/**
* Adds two directed speeds onto each other using vector addition.
*/
SpeedWithBearing add(SpeedWithBearing other);
}
@@ -33,6 +33,11 @@ public abstract class AbstractSpeedWithAbstractBearingImpl extends AbstractSpeed
&& getBearing().equals(((SpeedWithBearing) object).getBearing());
}
@Override
public SpeedWithBearing add(SpeedWithBearing other) {
return AbstractSpeedWithAbstractBearingImpl.add(this, other);
}
@Override
public CourseChange getCourseChangeRequiredToReach(SpeedWithBearing targetSpeedWithBearing) {
return AbstractSpeedWithBearingImpl.getCourseChangeRequiredToReach(this, targetSpeedWithBearing);
@@ -80,4 +85,12 @@ public abstract class AbstractSpeedWithAbstractBearingImpl extends AbstractSpeed
public Speed projectTo(Position position, Bearing projectTo) {
return projectTo(this, position, projectTo);
}
public static SpeedWithBearing add(SpeedWithBearing first, SpeedWithBearing other) {
double x = first.getMetersPerSecond()*Math.cos(first.getBearing().getRadians()) + other.getMetersPerSecond()*Math.cos(other.getBearing().getRadians());
double y = first.getMetersPerSecond()*Math.sin(first.getBearing().getRadians()) + other.getMetersPerSecond()*Math.sin(other.getBearing().getRadians());
double metersPerSecond = Math.sqrt(x*x+y*y);
double directionRad = (360.+Math.atan2(y, x))%360.;
return new MeterPerSecondSpeedWithDegreeBearingImpl(metersPerSecond, new RadianBearingImpl(directionRad));
}
}
@@ -15,5 +15,4 @@ public abstract class AbstractSpeedWithBearingImpl extends AbstractSpeedWithAbst
public Bearing getBearing() {
return bearing;
}
}
@@ -11,7 +11,6 @@ import org.json.simple.parser.ParseException;
import org.junit.Test;
import com.sap.sailing.domain.common.impl.Util;
import com.sap.sailing.domain.igtimiadapter.IgtimiConnectionFactory;
import com.sap.sailing.domain.igtimiadapter.persistence.PersistenceFactory;
import com.sap.sailing.mongodb.MongoDBConfiguration;
import com.sap.sailing.mongodb.MongoDBService;
@@ -23,7 +22,7 @@ public class WebSocketServerListTest {
"aa569cf4909bdc7b0e04b11873f3c4ea20687421e010fcc25b771cca9e6f3f9a", "http://127.0.0.1:8888/igtimi/oauth/v1/authorizationcallback");
MongoDBConfiguration mongoTestConfig = MongoDBConfiguration.getDefaultTestConfiguration();
MongoDBService mongoTestService = mongoTestConfig.getService();
final IgtimiConnectionFactory igtimiConnectionFactory = new IgtimiConnectionFactoryImpl(testAppClient, PersistenceFactory.INSTANCE.getDomainObjectFactory(mongoTestService),
final IgtimiConnectionFactoryImpl igtimiConnectionFactory = new IgtimiConnectionFactoryImpl(testAppClient, PersistenceFactory.INSTANCE.getDomainObjectFactory(mongoTestService),
PersistenceFactory.INSTANCE.getMongoObjectFactory(mongoTestService));
Iterable<URI> serverUris = igtimiConnectionFactory.getWebsocketServers();
assertFalse(Util.isEmpty(serverUris));
@@ -0,0 +1,69 @@
package com.sap.sailing.domain.igtimiadapter.impl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import com.sap.sailing.domain.common.SpeedWithBearing;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sailing.domain.common.impl.MillisecondsTimePoint;
import com.sap.sailing.domain.igtimiadapter.datatypes.AWA;
import com.sap.sailing.domain.igtimiadapter.datatypes.AWS;
import com.sap.sailing.domain.igtimiadapter.datatypes.COG;
import com.sap.sailing.domain.igtimiadapter.datatypes.GpsLatLong;
import com.sap.sailing.domain.igtimiadapter.datatypes.HDG;
import com.sap.sailing.domain.igtimiadapter.datatypes.SOG;
import com.sap.sailing.domain.tracking.Wind;
import com.sap.sailing.domain.tracking.WindListener;
public class WindReceiverTest {
@Test
public void simpleWindReceiverTest() {
final List<Wind> windReceived = new ArrayList<>();
final String deviceSerialNumber = "Non-Existing Test Device";
IgtimiWindReceiver receiver = new IgtimiWindReceiver(deviceSerialNumber);
receiver.addListener(new WindListener() {
@Override
public void windDataReceived(Wind wind) {
windReceived.add(wind);
}
@Override
public void windDataRemoved(Wind wind) {}
@Override
public void windAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) {}
});
TimePoint timePoint = MillisecondsTimePoint.now();
Map<Integer, Object> awaMap = new HashMap<>(); awaMap.put(1, 123. /* degrees from */);
Map<Integer, Object> awsMap = new HashMap<>(); awsMap.put(1, 12. /* knots */);
Map<Integer, Object> hdgMap = new HashMap<>(); hdgMap.put(1, 20. /* true degrees */);
Map<Integer, Object> gpsLatLongMap = new HashMap<>(); gpsLatLongMap.put(2, 49.8 /* lat */); gpsLatLongMap.put(1, 8.93 /* lng */);
Map<Integer, Object> sogMap = new HashMap<>(); sogMap.put(1, 10. /* knots */);
Map<Integer, Object> cogMap = new HashMap<>(); cogMap.put(1, 22. /* degrees; 2 degrees drift */);
final SensorImpl sensor = new SensorImpl(deviceSerialNumber, 0);
SpeedWithBearing boatSogCog = new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(22));
SpeedWithBearing apparentWind = new KnotSpeedWithBearingImpl(12, new DegreeBearingImpl(20).add(new DegreeBearingImpl(123).reverse()));
SpeedWithBearing expectedTrueWind = apparentWind.add(boatSogCog);
receiver.received(Arrays.asList(new AWA(timePoint, sensor, awaMap),
new AWS(timePoint, sensor, awsMap),
new HDG(timePoint, sensor, hdgMap),
new GpsLatLong(timePoint, sensor, gpsLatLongMap),
new SOG(timePoint, sensor, sogMap),
new COG(timePoint, sensor, cogMap)));
assertFalse(windReceived.isEmpty());
assertEquals(1, windReceived.size());
Wind wind = windReceived.get(0);
assertEquals(49.8, wind.getPosition().getLatDeg(), 0.00000001);
assertEquals(8.93, wind.getPosition().getLngDeg(), 0.00000001);
assertEquals(expectedTrueWind.getKnots(), wind.getKnots(), 0.00000001);
assertEquals(expectedTrueWind.getBearing().getDegrees(), wind.getBearing().getDegrees(), 0.00000001);
}
}
@@ -36,7 +36,7 @@ public class SignInTest {
"aa569cf4909bdc7b0e04b11873f3c4ea20687421e010fcc25b771cca9e6f3f9a", "http://127.0.0.1:8888/igtimi/oauth/v1/authorizationcallback");
MongoDBConfiguration mongoTestConfig = MongoDBConfiguration.getDefaultTestConfiguration();
MongoDBService mongoTestService = mongoTestConfig.getService();
final IgtimiConnectionFactory igtimiConnectionFactory = new IgtimiConnectionFactoryImpl(testAppClient, PersistenceFactory.INSTANCE.getDomainObjectFactory(mongoTestService),
final IgtimiConnectionFactoryImpl igtimiConnectionFactory = new IgtimiConnectionFactoryImpl(testAppClient, PersistenceFactory.INSTANCE.getDomainObjectFactory(mongoTestService),
PersistenceFactory.INSTANCE.getMongoObjectFactory(mongoTestService));
final String code = igtimiConnectionFactory.authorizeAndReturnAuthorizedCode("axel.uhl@gmx.de", "123456");
logger.info("Igtimi OAuth code is "+code);
@@ -54,20 +54,35 @@ public class SignInTest {
"4d66022d1ec3e2991f8053514495b61cc076ff02d664f0dc8f3df9150c3864ef", "http://1.2.3.4");
MongoDBConfiguration mongoTestConfig = MongoDBConfiguration.getDefaultTestConfiguration();
MongoDBService mongoTestService = mongoTestConfig.getService();
final IgtimiConnectionFactory igtimiConnectionFactory = new IgtimiConnectionFactoryImpl(testAppClient, PersistenceFactory.INSTANCE.getDomainObjectFactory(mongoTestService),
final IgtimiConnectionFactoryImpl igtimiConnectionFactory = new IgtimiConnectionFactoryImpl(testAppClient, PersistenceFactory.INSTANCE.getDomainObjectFactory(mongoTestService),
PersistenceFactory.INSTANCE.getMongoObjectFactory(mongoTestService));
final String code = igtimiConnectionFactory.authorizeAndReturnAuthorizedCode("axel.uhl@gmx.de", "123456");
logger.info("Igtimi OAuth code is "+code);
assertNotNull(code);
}
@Test
public void testAuthorize() throws ClientProtocolException, IOException,
IllegalStateException, ParserConfigurationException, SAXException, ClassNotFoundException,
InstantiationException, IllegalAccessException, ClassCastException, ParseException {
// use the credentials of "Another Test App"
final Client testAppClient = new ClientImpl("a4cecd8593e12d43a03433a6db0eea243a411749f93c278dce6a26d4804eebd2",
"4d66022d1ec3e2991f8053514495b61cc076ff02d664f0dc8f3df9150c3864ef", "http://1.2.3.4");
MongoDBConfiguration mongoTestConfig = MongoDBConfiguration.getDefaultTestConfiguration();
MongoDBService mongoTestService = mongoTestConfig.getService();
final IgtimiConnectionFactory igtimiConnectionFactory = new IgtimiConnectionFactoryImpl(testAppClient, PersistenceFactory.INSTANCE.getDomainObjectFactory(mongoTestService),
PersistenceFactory.INSTANCE.getMongoObjectFactory(mongoTestService));
final Account account = igtimiConnectionFactory.createAccountToAccessUserData("axel.uhl@gmx.de", "123456");
assertNotNull(account);
logger.info("Igtimi account is "+account);
}
@Test
public void testAddToken() throws ClientProtocolException, IllegalStateException, IOException, ParseException {
final IgtimiConnectionFactory connectionFactory = Activator.getInstance().getConnectionFactory();
Account account = connectionFactory.registerAccountForWhichClientIsAuthorized("3b6cbd0522423bb1ac274ddb9e7e579c4b3be6667622271086c4fdbf30634ba9");
assertEquals("axel.uhl@gmx.de", account.getUser().getEmail());
assertSame(account, connectionFactory.getAccountByEmail("axel.uhl@gmx.de"));
assertSame(account, connectionFactory.getExistingAccountByEmail("axel.uhl@gmx.de"));
}
}
@@ -28,9 +28,14 @@ import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.junit.Test;
import com.sap.sailing.domain.common.impl.Util;
import com.sap.sailing.domain.igtimiadapter.Account;
import com.sap.sailing.domain.igtimiadapter.BulkFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Client;
import com.sap.sailing.domain.igtimiadapter.IgtimiConnection;
import com.sap.sailing.domain.igtimiadapter.IgtimiConnectionFactory;
import com.sap.sailing.domain.igtimiadapter.LiveDataConnection;
import com.sap.sailing.domain.igtimiadapter.datatypes.Fix;
import com.sap.sailing.domain.igtimiadapter.impl.ClientImpl;
import com.sap.sailing.domain.igtimiadapter.impl.IgtimiConnectionFactoryImpl;
import com.sap.sailing.domain.igtimiadapter.persistence.PersistenceFactory;
@@ -142,6 +147,7 @@ public class WebSocketTest {
@Test
public void testWebSocketConnect() throws Exception {
final List<Fix> allFixesReceived = new ArrayList<>();
final Client client = new ClientImpl("7fcdd217e0aa16090edb4ad55b09ec43b2021090e209541fc9b7003c2a2b70c6",
"aa569cf4909bdc7b0e04b11873f3c4ea20687421e010fcc25b771cca9e6f3f9a", "http://127.0.0.1:8888/igtimi/oauth/v1/authorizationcallback");
MongoDBConfiguration mongoTestConfig = MongoDBConfiguration.getDefaultTestConfiguration();
@@ -149,9 +155,16 @@ public class WebSocketTest {
final IgtimiConnectionFactory igtimiConnectionFactory = new IgtimiConnectionFactoryImpl(client, PersistenceFactory.INSTANCE.getDomainObjectFactory(mongoTestService),
PersistenceFactory.INSTANCE.getMongoObjectFactory(mongoTestService));
Account account = igtimiConnectionFactory.registerAccountForWhichClientIsAuthorized("3b6cbd0522423bb1ac274ddb9e7e579c4b3be6667622271086c4fdbf30634ba9");
WebSocketConnectionManager manager = new WebSocketConnectionManager(igtimiConnectionFactory, Collections.singleton("GA-EN-AAEJ"), account);
assertNotNull(manager);
assertTrue("Connection handshake not successful within 5s", manager.waitForConnection(5000));
manager.disconnect();
IgtimiConnection conn = igtimiConnectionFactory.connect(account);
LiveDataConnection liveDataConnection = conn.createLiveConnection(Collections.singleton("GA-EN-AAEJ"));
liveDataConnection.addListener(new BulkFixReceiver() {
@Override
public void received(Iterable<Fix> fixes) {
Util.addAll(fixes, allFixesReceived);
}
});
assertNotNull(liveDataConnection);
assertTrue("Connection handshake not successful within 5s", liveDataConnection.waitForConnection(5000l));
liveDataConnection.disconnect();
}
}
@@ -14,7 +14,8 @@ Require-Bundle: org.json.simple;bundle-version="1.1.0",
com.sap.sailing.domain.common,
com.sap.sailing.domain.igtimiadapter.persistence,
com.sap.sailing.domain.shared.android,
com.sap.sailing.domain
com.sap.sailing.domain,
com.sap.sailing.declination
Import-Package: javax.ws.rs;version="1.1.1",
javax.ws.rs.core;version="1.1.1",
javax.ws.rs.ext;version="1.1.1",
@@ -29,3 +30,5 @@ Import-Package: javax.ws.rs;version="1.1.1",
org.osgi.framework;version="1.7.0"
Web-ContextPath: /igtimi
Bundle-Activator: com.sap.sailing.domain.igtimiadapter.impl.Activator
Export-Package: com.sap.sailing.domain.igtimiadapter,
com.sap.sailing.domain.igtimiadapter.datatypes
@@ -0,0 +1,15 @@
package com.sap.sailing.domain.igtimiadapter;
import com.sap.sailing.domain.igtimiadapter.datatypes.Fix;
/**
* Allows listeners to receive a batch of fixes that the connector received in one transaction. This will usually
* improve chances that sensor readings belonging together (such as an AWA and an AWS fix) will be contained in the
* same batch.
*
* @author Axel Uhl (d043530)
*
*/
public interface BulkFixReceiver {
void received(Iterable<Fix> fixes);
}
@@ -0,0 +1,25 @@
package com.sap.sailing.domain.igtimiadapter;
import com.sap.sailing.domain.igtimiadapter.datatypes.Fix;
/**
* Notifies a {@link IgtimiFixReceiver receiver} about each fix received.
*
* @author Axel Uhl (d043530)
*
*/
public class FixVisitor implements BulkFixReceiver {
private final IgtimiFixReceiver receiver;
public FixVisitor(IgtimiFixReceiver receiver) {
this.receiver = receiver;
}
@Override
public void received(Iterable<Fix> fixes) {
for (Fix fix : fixes) {
fix.notify(receiver);
}
}
}
@@ -63,6 +63,39 @@ public interface IgtimiConnection {
Type... types) throws IllegalStateException, ClientProtocolException, IOException,
ParseException;
/**
* Shorthand for {@link #getResourceData(TimePoint, TimePoint, Iterable, Map)} where no compression is requested for
* any type. The fixes received are forwarded to the {@link BulkFixReceiver} <code>bulkFixReceiver</code> in one call.
*/
Iterable<Fix> getAndNotifyResourceData(TimePoint startTime, TimePoint endTime,
Iterable<String> deviceSerialNumbers, BulkFixReceiver bulkFixReceiver, Type... types)
throws IllegalStateException, ClientProtocolException, IOException, ParseException;
/**
* Same as {@link #getResourceData(TimePoint, TimePoint, Iterable, Type...)}, but the resulting {@link Fix}es are
* grouped into {@link Track}s per fix type and per device.
*
* @return a map whose keys are the devices' serial numbers and whose values are the fixes produced by the device
* identified by the key, grouped in a map with the fix {@link Type} as its key. Note that if a device
* didn't produce any fixes at all under the requested parameters, its serial number may not appear as a key
* in the map. Note further that should a device not have produced fixes of a given type, that type won't
* appear as a key in the map for that device.
*/
Map<String, Map<Type, DynamicTrack<Fix>>> getResourceDataAsTracks(TimePoint startTime, TimePoint endTime,
Iterable<String> deviceSerialNumbers, Type... types) throws IllegalStateException, ClientProtocolException,
IOException, ParseException;
/**
* For the devices specified by <code>deviceSerialNumbers</code>, creates a live data connection. The <code>account</code>
* needs to be authorized to access the devices' data for the current time window. Fixes received through this connection
* are forwarded in the batches in which they are received to the listeners that can be added to the live connection
* using {@link LiveDataConnection#addListener(BulkFixReceiver)}.
*
* @return a connection that the caller can use to stop the live feed by calling {@link LiveDataConnection#disconnect()}.
*/
LiveDataConnection createLiveConnection(Iterable<String> deviceSerialNumbers) throws Exception;
/**
* @param sessionIds
* the optional IDs of the sessions for which to obtain the metadata; if <code>null</code>, all available
@@ -107,19 +140,5 @@ public interface IgtimiConnection {
Iterable<Group> getGroups() throws IllegalStateException, ClientProtocolException, IOException, ParseException;
/**
* Same as {@link #getResourceData(TimePoint, TimePoint, Iterable, Type...)}, but the resulting {@link Fix}es are
* grouped into {@link Track}s per fix type and per device.
*
* @return a map whose keys are the devices' serial numbers and whose values are the fixes produced by the device
* identified by the key, grouped in a map with the fix {@link Type} as its key. Note that if a device
* didn't produce any fixes at all under the requested parameters, its serial number may not appear as a key
* in the map. Note further that should a device not have produced fixes of a given type, that type won't
* appear as a key in the map for that device.
*/
Map<String, Map<Type, DynamicTrack<Fix>>> getResourceDataAsTracks(TimePoint startTime, TimePoint endTime,
Iterable<String> deviceSerialNumbers, Type... types) throws IllegalStateException, ClientProtocolException,
IOException, ParseException;
Account getAccount();
}
@@ -1,21 +1,13 @@
package com.sap.sailing.domain.igtimiadapter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.json.simple.JSONObject;
import org.json.simple.parser.ParseException;
import org.xml.sax.SAXException;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.igtimiadapter.datatypes.Type;
import com.sap.sailing.domain.igtimiadapter.oauth.AuthorizationCallback;
/**
@@ -26,6 +18,10 @@ import com.sap.sailing.domain.igtimiadapter.oauth.AuthorizationCallback;
* @author Axel Uhl (d043530)
*/
public interface IgtimiConnectionFactory {
Iterable<Account> getAllAccounts();
IgtimiConnection connect(Account account);
/**
* Obtains a URL that a user agent (e.g., a web browser) can be sent to in order to allow that user to authenticate
* and then authorize this factory's {@link Client} for accessing the user's Igtimi data. The URL is chosen such that
@@ -47,73 +43,17 @@ public interface IgtimiConnectionFactory {
* Matches <code>eMail</code> with the e-mail information retrieved from the "account" service earlier when an access
* token was registered.
*/
Account getAccountByEmail(String eMail);
IgtimiConnection connect(Account account);
Account getExistingAccountByEmail(String eMail);
/**
* Uses the /oauth/token service to obtain and {@link #registerAccountForWhichClientIsAuthorized(String) register}
* an access token for an authorization code which encodes the authorization given by a user to this factory's
* client.
* Tries to authorize our client on behalf of a user identified by e-mail and password. If successful, the
* account data and the relevant OAuth access token will be stored persistently.
*
* @return the account encoding the application that is authorized for a user's account
* @throws RuntimeException in case there was an error while retrieving the token
* @return the account with which a caller can then {@link #connect obtain a connection} for the data that the user
* identified by <code>userEmail</code> and <code>userPassword</code> shares with our client.
*/
Account obtainAccessTokenFromAuthorizationCode(String code) throws UnsupportedEncodingException,
ClientProtocolException, IOException, IllegalStateException, ParseException;
String getAccountUrl(Account account);
String getUsersUrl(Account account);
String getUserUrl(long id, Account account);
String getGroupsUrl(Account account);
String getGroupUrl(long id, Account account);
String getResourcesUrl(Permission permission, TimePoint startTime, TimePoint endTime,
Iterable<String> serialNumbers, Iterable<String> streamIds, Account account);
String getResourceDataUrl(TimePoint startTime, TimePoint endTime, Iterable<String> serialNumbers,
Map<Type, Double> typeAndCompression, Account account);
String getSessionsUrl(Iterable<Long> sessionIds, Boolean isPublic, Integer limit, Boolean includeIncomplete,
Account account);
String getSessionUrl(long id, Account account);
String getOwnedDevicesUrl(Account account);
String getDataAccessWindowsUrl(Permission permission, TimePoint startTime, TimePoint endTime,
Iterable<String> deviceSerialNumbers, Account account);
Iterable<URI> getWebsocketServers() throws IllegalStateException, ClientProtocolException, IOException, ParseException, URISyntaxException;
HttpClient getHttpClient();
/**
* Retrieves the JSON object to send in its string-serialized form to a web socket connection in order to receive
* live data from the units whose IDs are specified by <code>unitIds</code>. The sending units are expected to
* belong to the user account to which this factory's application client has been granted permission.
*
* @param account
* represents this factory's client's permissions to access a user account's data
* @param deviceIds
* IDs of the transmitting units expected to be visible to the account's {@link Account#getUser() user's}
*/
JSONObject getWebSocketConfigurationMessage(Account account, Iterable<String> deviceIds);
Iterable<Account> getAllAccounts();
/**
* Tries to authorize our client on behalf of a user identified by e-mail and password.
*
* @return the authorization code which can then be used to obtain a permanent access token to be used by our client
* to access data owned by the user identified by e-mail and password.
*/
String authorizeAndReturnAuthorizedCode(String userEmail, String userPassword) throws ClientProtocolException,
Account createAccountToAccessUserData(String userEmail, String userPassword) throws ClientProtocolException,
IOException, IllegalStateException, ParserConfigurationException, SAXException, ClassNotFoundException,
InstantiationException, IllegalAccessException, ClassCastException;
InstantiationException, IllegalAccessException, ClassCastException, ParseException;
}
@@ -0,0 +1,43 @@
package com.sap.sailing.domain.igtimiadapter;
import com.sap.sailing.domain.igtimiadapter.datatypes.AWA;
import com.sap.sailing.domain.igtimiadapter.datatypes.AWS;
import com.sap.sailing.domain.igtimiadapter.datatypes.AntCbst;
import com.sap.sailing.domain.igtimiadapter.datatypes.AntHrm;
import com.sap.sailing.domain.igtimiadapter.datatypes.BatteryLevel;
import com.sap.sailing.domain.igtimiadapter.datatypes.COG;
import com.sap.sailing.domain.igtimiadapter.datatypes.File;
import com.sap.sailing.domain.igtimiadapter.datatypes.GpsAltitude;
import com.sap.sailing.domain.igtimiadapter.datatypes.GpsLatLong;
import com.sap.sailing.domain.igtimiadapter.datatypes.GpsQualityHdop;
import com.sap.sailing.domain.igtimiadapter.datatypes.GpsQualityIndicator;
import com.sap.sailing.domain.igtimiadapter.datatypes.GpsQualitySatCount;
import com.sap.sailing.domain.igtimiadapter.datatypes.HDG;
import com.sap.sailing.domain.igtimiadapter.datatypes.HDGM;
import com.sap.sailing.domain.igtimiadapter.datatypes.SOG;
import com.sap.sailing.domain.igtimiadapter.datatypes.STW;
/**
* Callback interface for Igtimi fix receivers.
*
* @author Axel Uhl (d043530)
*
*/
public interface IgtimiFixReceiver {
void received(AntCbst fix);
void received(AntHrm fix);
void received(AWA fix);
void received(AWS fix);
void received(BatteryLevel fix);
void received(COG fix);
void received(File fix);
void received(GpsAltitude fix);
void received(GpsLatLong fix);
void received(GpsQualityHdop fix);
void received(GpsQualityIndicator fix);
void received(GpsQualitySatCount fix);
void received(HDG fix);
void received(HDGM fix);
void received(SOG fix);
void received(STW fix);
}
@@ -0,0 +1,85 @@
package com.sap.sailing.domain.igtimiadapter;
import com.sap.sailing.domain.igtimiadapter.datatypes.AWA;
import com.sap.sailing.domain.igtimiadapter.datatypes.AWS;
import com.sap.sailing.domain.igtimiadapter.datatypes.AntCbst;
import com.sap.sailing.domain.igtimiadapter.datatypes.AntHrm;
import com.sap.sailing.domain.igtimiadapter.datatypes.BatteryLevel;
import com.sap.sailing.domain.igtimiadapter.datatypes.COG;
import com.sap.sailing.domain.igtimiadapter.datatypes.File;
import com.sap.sailing.domain.igtimiadapter.datatypes.GpsAltitude;
import com.sap.sailing.domain.igtimiadapter.datatypes.GpsLatLong;
import com.sap.sailing.domain.igtimiadapter.datatypes.GpsQualityHdop;
import com.sap.sailing.domain.igtimiadapter.datatypes.GpsQualityIndicator;
import com.sap.sailing.domain.igtimiadapter.datatypes.GpsQualitySatCount;
import com.sap.sailing.domain.igtimiadapter.datatypes.HDG;
import com.sap.sailing.domain.igtimiadapter.datatypes.HDGM;
import com.sap.sailing.domain.igtimiadapter.datatypes.SOG;
import com.sap.sailing.domain.igtimiadapter.datatypes.STW;
public class IgtimiFixReceiverAdapter implements IgtimiFixReceiver {
@Override
public void received(AntCbst fix) {
}
@Override
public void received(AntHrm fix) {
}
@Override
public void received(AWA fix) {
}
@Override
public void received(AWS fix) {
}
@Override
public void received(BatteryLevel fix) {
}
@Override
public void received(COG fix) {
}
@Override
public void received(File fix) {
}
@Override
public void received(GpsAltitude fix) {
}
@Override
public void received(GpsLatLong fix) {
}
@Override
public void received(GpsQualityHdop fix) {
}
@Override
public void received(GpsQualityIndicator fix) {
}
@Override
public void received(GpsQualitySatCount fix) {
}
@Override
public void received(HDG fix) {
}
@Override
public void received(HDGM fix) {
}
@Override
public void received(SOG fix) {
}
@Override
public void received(STW fix) {
}
}
@@ -0,0 +1,19 @@
package com.sap.sailing.domain.igtimiadapter;
public interface LiveDataConnection {
/**
* Disconnects this connection. Afterwards, the connection will no longer feed live data to its listener(s).
*/
void disconnect() throws Exception;
/**
* Waits for a successful connection for <code>timeoutInMillis</code> milliseconds. If the connection is successful
* within this time, <code>true</code> is returned; <code>false</code> otherwise.
*
* @param timeoutInMillis
* use 0 to wait indefinitely
*/
boolean waitForConnection(long timeoutInMillis) throws InterruptedException;
void addListener(BulkFixReceiver listener);
}
@@ -1,7 +0,0 @@
package com.sap.sailing.domain.igtimiadapter;
import com.sap.sailing.domain.igtimiadapter.datatypes.Fix;
public interface LiveDataListener {
void fixesReceived(Iterable<Fix> fixes);
}
@@ -5,6 +5,7 @@ import java.util.Map;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Sensor;
/**
@@ -31,4 +32,9 @@ public class AWA extends Fix {
protected String localToString() {
return "AWA: "+getApparentWindAngle();
}
@Override
public void notify(IgtimiFixReceiver receiver) {
receiver.received(this);
}
}
@@ -5,6 +5,7 @@ import java.util.Map;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.impl.KnotSpeedImpl;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Sensor;
/**
@@ -30,4 +31,9 @@ public class AWS extends Fix {
protected String localToString() {
return "AWS: "+getApparentWindSpeed();
}
@Override
public void notify(IgtimiFixReceiver receiver) {
receiver.received(this);
}
}
@@ -3,6 +3,7 @@ package com.sap.sailing.domain.igtimiadapter.datatypes;
import java.util.Map;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Sensor;
/**
@@ -28,4 +29,9 @@ public class AntCbst extends Fix {
public String localToString() {
return "Cadence "+getCadence();
}
@Override
public void notify(IgtimiFixReceiver receiver) {
receiver.received(this);
}
}
@@ -3,6 +3,7 @@ package com.sap.sailing.domain.igtimiadapter.datatypes;
import java.util.Map;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Sensor;
/**
@@ -40,4 +41,9 @@ public class AntHrm extends Fix {
protected String localToString() {
return "heart rate "+getHeartRate()+"/min";
}
@Override
public void notify(IgtimiFixReceiver receiver) {
receiver.received(this);
}
}
@@ -3,6 +3,7 @@ package com.sap.sailing.domain.igtimiadapter.datatypes;
import java.util.Map;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Sensor;
public class BatteryLevel extends Fix {
@@ -22,4 +23,9 @@ public class BatteryLevel extends Fix {
protected String localToString() {
return "Battery: "+getPercentage()+"%";
}
@Override
public void notify(IgtimiFixReceiver receiver) {
receiver.received(this);
}
}
@@ -5,6 +5,7 @@ import java.util.Map;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Sensor;
/**
@@ -30,4 +31,9 @@ public class COG extends Fix {
protected String localToString() {
return "COG: "+getCourseOverGround();
}
@Override
public void notify(IgtimiFixReceiver receiver) {
receiver.received(this);
}
}
@@ -3,6 +3,7 @@ package com.sap.sailing.domain.igtimiadapter.datatypes;
import java.util.Map;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Sensor;
public class File extends Fix {
@@ -52,4 +53,9 @@ public class File extends Fix {
protected String localToString() {
return "File: "+getFileName()+", MD5: "+getMd5()+" content type "+getContentType()+", "+getSize()+" bytes";
}
@Override
public void notify(IgtimiFixReceiver receiver) {
receiver.received(this);
}
}
@@ -2,6 +2,7 @@ package com.sap.sailing.domain.igtimiadapter.datatypes;
import com.sap.sailing.domain.base.Timed;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Sensor;
public abstract class Fix implements Timed {
@@ -30,6 +31,8 @@ public abstract class Fix implements Timed {
abstract protected String localToString();
abstract public void notify(IgtimiFixReceiver receiver);
@Override
public String toString() {
return localToString() + " at "+getTimePoint()+" from "+getSensor();
@@ -5,6 +5,7 @@ import java.util.Map;
import com.sap.sailing.domain.common.Distance;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.impl.MeterDistance;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Sensor;
public class GpsAltitude extends Fix {
@@ -24,4 +25,9 @@ public class GpsAltitude extends Fix {
protected String localToString() {
return "Altitude "+getAltitude();
}
@Override
public void notify(IgtimiFixReceiver receiver) {
receiver.received(this);
}
}
@@ -5,6 +5,7 @@ import java.util.Map;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.impl.DegreePosition;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Sensor;
public class GpsLatLong extends Fix {
@@ -24,4 +25,9 @@ public class GpsLatLong extends Fix {
protected String localToString() {
return position.toString();
}
@Override
public void notify(IgtimiFixReceiver receiver) {
receiver.received(this);
}
}
@@ -5,6 +5,7 @@ import java.util.Map;
import com.sap.sailing.domain.common.Distance;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.impl.MeterDistance;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Sensor;
public class GpsQualityHdop extends Fix {
@@ -24,4 +25,9 @@ public class GpsQualityHdop extends Fix {
protected String localToString() {
return "HDOP: "+getHdop();
}
@Override
public void notify(IgtimiFixReceiver receiver) {
receiver.received(this);
}
}
@@ -3,6 +3,7 @@ package com.sap.sailing.domain.igtimiadapter.datatypes;
import java.util.Map;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Sensor;
public class GpsQualityIndicator extends Fix {
@@ -22,4 +23,9 @@ public class GpsQualityIndicator extends Fix {
protected String localToString() {
return "GPS Quality: "+getQuality();
}
@Override
public void notify(IgtimiFixReceiver receiver) {
receiver.received(this);
}
}
@@ -3,6 +3,7 @@ package com.sap.sailing.domain.igtimiadapter.datatypes;
import java.util.Map;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Sensor;
/**
@@ -43,4 +44,9 @@ public class GpsQualitySatCount extends Fix {
protected String localToString() {
return ""+getSatCount()+" satellites";
}
@Override
public void notify(IgtimiFixReceiver receiver) {
receiver.received(this);
}
}
@@ -5,6 +5,7 @@ import java.util.Map;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Sensor;
/**
@@ -30,4 +31,9 @@ public class HDG extends Fix {
protected String localToString() {
return "HDG: "+getTrueHeading();
}
@Override
public void notify(IgtimiFixReceiver receiver) {
receiver.received(this);
}
}
@@ -5,6 +5,7 @@ import java.util.Map;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Sensor;
/**
@@ -30,4 +31,9 @@ public class HDGM extends Fix {
protected String localToString() {
return "HDGM: "+getMagnetigHeading();
}
@Override
public void notify(IgtimiFixReceiver receiver) {
receiver.received(this);
}
}
@@ -5,6 +5,7 @@ import java.util.Map;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.impl.KnotSpeedImpl;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Sensor;
/**
@@ -30,4 +31,9 @@ public class SOG extends Fix {
protected String localToString() {
return "SOG: "+getSpeedOverGround();
}
@Override
public void notify(IgtimiFixReceiver receiver) {
receiver.received(this);
}
}
@@ -5,6 +5,7 @@ import java.util.Map;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.impl.KnotSpeedImpl;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiver;
import com.sap.sailing.domain.igtimiadapter.Sensor;
/**
@@ -30,4 +31,9 @@ public class STW extends Fix {
protected String localToString() {
return "STW: "+getSpeedThroughWater();
}
@Override
public void notify(IgtimiFixReceiver receiver) {
receiver.received(this);
}
}
@@ -33,7 +33,7 @@ public class Activator implements BundleActivator {
private static final String CLIENT_ID_PROPERTY_NAME = "igtimi.client.id";
private static final String CLIENT_SECRET_PROPERTY_NAME = "igtimi.client.secret";
private static final String CLIENT_REDIRECT_URI_PROPERTY_NAME = "igtimi.client.redirecturi";
private IgtimiConnectionFactory connectionFactory;
private IgtimiConnectionFactoryImpl connectionFactory;
public Activator() throws ClientProtocolException, IllegalStateException, IOException, ParseException {
final String clientId = System.getProperty(CLIENT_ID_PROPERTY_NAME, DEFAULT_CLIENT_ID);
@@ -58,7 +58,7 @@ public class Activator implements BundleActivator {
return INSTANCE;
}
public IgtimiConnectionFactory getConnectionFactory() {
public IgtimiConnectionFactoryImpl getConnectionFactory() {
return connectionFactory;
}
@@ -120,7 +120,7 @@ public class IgtimiConnectionFactoryImpl implements IgtimiConnectionFactory {
}
@Override
public Account getAccountByEmail(String eMail) {
public Account getExistingAccountByEmail(String eMail) {
return accountsByEmail.get(eMail);
}
@@ -138,12 +138,20 @@ public class IgtimiConnectionFactoryImpl implements IgtimiConnectionFactory {
return accessTokensByAccount.get(account);
}
@Override
public String getAccountUrl(Account account) {
return getApiV1BaseUrl()+"account?"+getAccessTokenUrlParameter(account);
}
@Override
/**
* Retrieves the JSON object to send in its string-serialized form to a web socket connection in order to receive
* live data from the units whose IDs are specified by <code>unitIds</code>. The sending units are expected to
* belong to the user account to which this factory's application client has been granted permission.
*
* @param account
* represents this factory's client's permissions to access a user account's data
* @param deviceIds
* IDs of the transmitting units expected to be visible to the account's {@link Account#getUser() user's}
*/
public JSONObject getWebSocketConfigurationMessage(Account account, Iterable<String> deviceIds) {
JSONObject result = new JSONObject();
result.put("access_token", getAccessTokenForAccount(account));
@@ -155,37 +163,30 @@ public class IgtimiConnectionFactoryImpl implements IgtimiConnectionFactory {
return result;
}
@Override
public String getUsersUrl(Account account) {
return getApiV1BaseUrl()+"users?"+getAccessTokenUrlParameter(account);
}
@Override
public String getUserUrl(long id, Account account) {
return getApiV1BaseUrl()+"users/"+id+"?"+getAccessTokenUrlParameter(account);
}
@Override
public String getGroupsUrl(Account account) {
return getApiV1BaseUrl()+"groups?"+getAccessTokenUrlParameter(account);
}
@Override
public String getGroupUrl(long id, Account account) {
return getApiV1BaseUrl()+"groups/"+id+"?"+getAccessTokenUrlParameter(account);
}
@Override
public String getSessionUrl(long id, Account account) {
return getApiV1BaseUrl()+"sessions/"+id+"?"+getAccessTokenUrlParameter(account);
}
@Override
public String getOwnedDevicesUrl(Account account) {
return getApiV1BaseUrl()+"devices/?"+getAccessTokenUrlParameter(account);
}
@Override
public String getDataAccessWindowsUrl(Permission permission, TimePoint startTime, TimePoint endTime,
Iterable<String> deviceSerialNumbers, Account account) {
StringBuilder url = new StringBuilder(getApiV1BaseUrl());
@@ -210,7 +211,6 @@ public class IgtimiConnectionFactoryImpl implements IgtimiConnectionFactory {
return url.toString();
}
@Override
public String getResourceDataUrl(TimePoint startTime, TimePoint endTime, Iterable<String> serialNumbers,
Map<Type, Double> typeAndCompression, Account account) {
StringBuilder url = new StringBuilder(getApiV1BaseUrl());
@@ -230,7 +230,6 @@ public class IgtimiConnectionFactoryImpl implements IgtimiConnectionFactory {
return url.toString();
}
@Override
public String getResourcesUrl(Permission permission, TimePoint startTime, TimePoint endTime,
Iterable<String> serialNumbers, Iterable<String> streamIds, Account account) {
StringBuilder url = new StringBuilder(getApiV1BaseUrl());
@@ -263,7 +262,6 @@ public class IgtimiConnectionFactoryImpl implements IgtimiConnectionFactory {
}
@Override
public String getSessionsUrl(Iterable<Long> sessionIds, Boolean isPublic, Integer limit, Boolean includeIncomplete, Account account) {
StringBuilder url = new StringBuilder(getApiV1BaseUrl());
url.append("sessions?");
@@ -302,7 +300,14 @@ public class IgtimiConnectionFactoryImpl implements IgtimiConnectionFactory {
return client;
}
@Override
/**
* Uses the /oauth/token service to obtain and {@link #registerAccountForWhichClientIsAuthorized(String) register}
* an access token for an authorization code which encodes the authorization given by a user to this factory's
* client.
*
* @return the account encoding the application that is authorized for a user's account
* @throws RuntimeException in case there was an error while retrieving the token
*/
public Account obtainAccessTokenFromAuthorizationCode(String code) throws ClientProtocolException, IOException, IllegalStateException, ParseException {
HttpClient client = new SystemDefaultHttpClient();
HttpPost post = new HttpPost(getOauthTokenUrl());
@@ -325,7 +330,12 @@ public class IgtimiConnectionFactoryImpl implements IgtimiConnectionFactory {
return result;
}
@Override
/**
* Tries to authorize our client on behalf of a user identified by e-mail and password.
*
* @return the authorization code which can then be used to obtain a permanent access token to be used by our client
* to access data owned by the user identified by e-mail and password.
*/
public String authorizeAndReturnAuthorizedCode(String userEmail, String userPassword)
throws ClientProtocolException, IOException, IllegalStateException, ParserConfigurationException,
SAXException, ClassNotFoundException, InstantiationException, IllegalAccessException, ClassCastException {
@@ -338,6 +348,20 @@ public class IgtimiConnectionFactoryImpl implements IgtimiConnectionFactory {
HttpResponse responseForAuthorize = client.execute(get);
return signInAndReturnAuthorizationForm(client, responseForAuthorize, userEmail, userPassword);
}
@Override
public Account createAccountToAccessUserData(String userEmail, String userPassword) throws ClientProtocolException,
IOException, IllegalStateException, ParserConfigurationException, SAXException, ClassNotFoundException,
InstantiationException, IllegalAccessException, ClassCastException, ParseException {
final Account result;
String code = authorizeAndReturnAuthorizedCode(userEmail, userPassword);
if (code != null) {
result = obtainAccessTokenFromAuthorizationCode(code);
} else {
result = null;
}
return result;
}
private String signInAndReturnAuthorizationForm(DefaultHttpClient client, HttpResponse response,
final String userEmail, final String userPassword) throws ParserConfigurationException, SAXException,
@@ -391,10 +415,6 @@ public class IgtimiConnectionFactoryImpl implements IgtimiConnectionFactory {
* Parses the form in the <code>autorizationForm</code> response and posts it by submitting the form that contains
* the commit button with the value "Authorize". If a redirect strategy is set on the <code>client</code> it will
* see the redirect URL in the <code>Location</code> header. The redirection target is closed immediately.
* @throws ClassCastException
* @throws IllegalAccessException
* @throws InstantiationException
* @throws ClassNotFoundException
*/
private void authorizeAndGetCode(HttpResponse authorizationForm, DefaultHttpClient client)
throws IllegalStateException, SAXException, IOException, ParserConfigurationException,
@@ -441,13 +461,11 @@ public class IgtimiConnectionFactoryImpl implements IgtimiConnectionFactory {
authorizationResponse.getEntity().getContent().close();
}
@Override
public HttpClient getHttpClient() {
HttpClient client = new SystemDefaultHttpClient();
return client;
}
@Override
public Iterable<URI> getWebsocketServers() throws IllegalStateException, ClientProtocolException, IOException, ParseException, URISyntaxException {
HttpClient client = getHttpClient();
HttpGet getWebsocketServers = new HttpGet("https://www.igtimi.com/server_listers/web_sockets");
@@ -15,25 +15,27 @@ import org.json.simple.parser.ParseException;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.igtimiadapter.Account;
import com.sap.sailing.domain.igtimiadapter.BulkFixReceiver;
import com.sap.sailing.domain.igtimiadapter.DataAccessWindow;
import com.sap.sailing.domain.igtimiadapter.Device;
import com.sap.sailing.domain.igtimiadapter.Group;
import com.sap.sailing.domain.igtimiadapter.IgtimiConnection;
import com.sap.sailing.domain.igtimiadapter.IgtimiConnectionFactory;
import com.sap.sailing.domain.igtimiadapter.LiveDataConnection;
import com.sap.sailing.domain.igtimiadapter.Permission;
import com.sap.sailing.domain.igtimiadapter.Resource;
import com.sap.sailing.domain.igtimiadapter.Session;
import com.sap.sailing.domain.igtimiadapter.User;
import com.sap.sailing.domain.igtimiadapter.datatypes.Fix;
import com.sap.sailing.domain.igtimiadapter.datatypes.Type;
import com.sap.sailing.domain.igtimiadapter.websocket.WebSocketConnectionManager;
import com.sap.sailing.domain.tracking.DynamicTrack;
import com.sap.sailing.domain.tracking.impl.DynamicTrackImpl;
public class IgtimiConnectionImpl implements IgtimiConnection {
private final Account account;
private final IgtimiConnectionFactory connectionFactory;
private final IgtimiConnectionFactoryImpl connectionFactory;
public IgtimiConnectionImpl(IgtimiConnectionFactory connectionFactory, Account account) {
public IgtimiConnectionImpl(IgtimiConnectionFactoryImpl connectionFactory, Account account) {
this.connectionFactory = connectionFactory;
this.account = account;
}
@@ -136,6 +138,15 @@ public class IgtimiConnectionImpl implements IgtimiConnection {
return getResourceData(startTime, endTime, deviceSerialNumbers, typeAndCompression);
}
@Override
public Iterable<Fix> getAndNotifyResourceData(TimePoint startTime, TimePoint endTime,
Iterable<String> deviceSerialNumbers, BulkFixReceiver bulkFixReceiver, Type... types)
throws IllegalStateException, ClientProtocolException, IOException, ParseException {
Iterable<Fix> result = getResourceData(startTime, endTime, deviceSerialNumbers, types);
bulkFixReceiver.received(result);
return result;
}
@Override
public Map<String, Map<Type, DynamicTrack<Fix>>> getResourceDataAsTracks(TimePoint startTime, TimePoint endTime, Iterable<String> deviceSerialNumbers,
Type... types) throws IllegalStateException, ClientProtocolException, IOException, ParseException {
@@ -150,6 +161,12 @@ public class IgtimiConnectionImpl implements IgtimiConnection {
return result;
}
@Override
public LiveDataConnection createLiveConnection(Iterable<String> deviceSerialNumbers) throws Exception {
return new WebSocketConnectionManager(connectionFactory, deviceSerialNumbers, getAccount());
}
private DynamicTrack<Fix> getOrCreateTrack(Map<String, Map<Type, DynamicTrack<Fix>>> result,
String deviceSerialNumber, Type type) {
Map<Type, DynamicTrack<Fix>> mapForDevice = result.get(deviceSerialNumber);
@@ -0,0 +1,402 @@
package com.sap.sailing.domain.igtimiadapter.impl;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sap.sailing.declination.Declination;
import com.sap.sailing.declination.DeclinationService;
import com.sap.sailing.domain.base.Timed;
import com.sap.sailing.domain.base.impl.ScalableBearing;
import com.sap.sailing.domain.base.impl.ScalablePosition;
import com.sap.sailing.domain.base.impl.ScalableSpeed;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.SpeedWithBearing;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sailing.domain.common.impl.Util.Pair;
import com.sap.sailing.domain.confidence.ScalableValue;
import com.sap.sailing.domain.igtimiadapter.BulkFixReceiver;
import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiverAdapter;
import com.sap.sailing.domain.igtimiadapter.datatypes.AWA;
import com.sap.sailing.domain.igtimiadapter.datatypes.AWS;
import com.sap.sailing.domain.igtimiadapter.datatypes.COG;
import com.sap.sailing.domain.igtimiadapter.datatypes.Fix;
import com.sap.sailing.domain.igtimiadapter.datatypes.GpsLatLong;
import com.sap.sailing.domain.igtimiadapter.datatypes.HDG;
import com.sap.sailing.domain.igtimiadapter.datatypes.HDGM;
import com.sap.sailing.domain.igtimiadapter.datatypes.SOG;
import com.sap.sailing.domain.tracking.DynamicTrack;
import com.sap.sailing.domain.tracking.Track;
import com.sap.sailing.domain.tracking.Wind;
import com.sap.sailing.domain.tracking.WindListener;
import com.sap.sailing.domain.tracking.impl.DynamicTrackImpl;
import com.sap.sailing.domain.tracking.impl.WindImpl;
/**
* Receives Igtimi {@link Fix}es and tries to generate a {@link Wind} object from each {@link AWS} fix. For this to
* work, the time-wise adjacent {@link AWA}, {@link HDG}/{@link HDGM} and {@link GpsLatLong} fixes are used. If only
* a magnetic heading ({@link HDGM}) is available, the {@link DeclinationService} is used to map that to a true heading.
* The true wind direction is determined by adding the boat speed vector onto the apparent wind vector.
*
* @author Axel Uhl (d043530)
*
*/
public class IgtimiWindReceiver implements BulkFixReceiver {
private static final Logger logger = Logger.getLogger(IgtimiWindReceiver.class.getName());
private final DynamicTrack<AWA> awaTrack;
private final DynamicTrack<AWS> awsTrack;
private final DynamicTrack<GpsLatLong> gpsTrack;
private final DynamicTrack<COG> cogTrack;
private final DynamicTrack<SOG> sogTrack;
private final DynamicTrack<HDG> hdgTrack;
private final DynamicTrack<HDGM> hdgmTrack;
private final FixReceiver receiver;
private final DeclinationService declinationService;
private final ConcurrentHashMap<WindListener, WindListener> listeners;
private class FixReceiver extends IgtimiFixReceiverAdapter {
@Override
public void received(AWA fix) {
getAwaTrack().add(fix);
}
@Override
public void received(AWS fix) {
getAwsTrack().add(fix);
}
@Override
public void received(GpsLatLong fix) {
getGpsTrack().add(fix);
}
@Override
public void received(COG fix) {
getCogTrack().add(fix);
}
@Override
public void received(SOG fix) {
getSogTrack().add(fix);
}
@Override
public void received(HDG fix) {
getHdgTrack().add(fix);
}
@Override
public void received(HDGM fix) {
getHdgmTrack().add(fix);
}
}
public IgtimiWindReceiver(String deviceSerialNumber) {
receiver = new FixReceiver();
declinationService = DeclinationService.INSTANCE;
listeners = new ConcurrentHashMap<>();
awaTrack = new DynamicTrackImpl<>("AWA Track for Igtimi wind track for device "+deviceSerialNumber);
awsTrack = new DynamicTrackImpl<>("AWS Track for Igtimi wind track for device "+deviceSerialNumber);
gpsTrack = new DynamicTrackImpl<>("GPS Track for Igtimi wind track for device "+deviceSerialNumber);
cogTrack = new DynamicTrackImpl<>("COG Track for Igtimi wind track for device "+deviceSerialNumber);
sogTrack = new DynamicTrackImpl<>("SOG Track for Igtimi wind track for device "+deviceSerialNumber);
hdgTrack = new DynamicTrackImpl<>("HDG Track for Igtimi wind track for device "+deviceSerialNumber);
hdgmTrack = new DynamicTrackImpl<>("HDGM Track for Igtimi wind track for device "+deviceSerialNumber);
}
/**
* Called when a batch of fixes was received, e.g., after loading from stored data or from a live web socket receiver.
* The fixes are pumped into the tracks by the superclass implementation. Then, for all new {@link AWS} fixes, a
* call to {@link IgtimiWindReceiver#getWind} is performed.
*/
@Override
public void received(Iterable<Fix> fixes) {
final List<AWS> awsFixes = new ArrayList<>();
for (Fix fix : fixes) {
fix.notify(receiver);
fix.notify(new IgtimiFixReceiverAdapter() {
@Override
public void received(AWS fix) {
awsFixes.add(fix);
}
});
}
for (AWS aws : awsFixes) {
try {
final Wind wind = getWind(aws.getTimePoint());
if (wind != null) {
notifyListeners(wind);
}
} catch (ClassNotFoundException | IOException | ParseException e) {
logger.log(Level.INFO, "Exception while trying to construct Wind fix from Igtimi fix " + aws, e);
}
}
}
public void addListener(WindListener listener) {
listeners.put(listener, listener);
}
public void notifyListeners(Wind wind) {
for (WindListener listener : listeners.keySet()) {
listener.windDataReceived(wind);
}
}
private Wind getWind(TimePoint timePoint) throws ClassNotFoundException, IOException, ParseException {
final Wind result;
Pair<AWA, AWA> awaPair = getSurroundingFixes(getAwaTrack(), timePoint);
Bearing awa = getAWA(timePoint, awaPair);
Pair<AWS, AWS> awsPair = getSurroundingFixes(getAwsTrack(), timePoint);
Speed aws = getAWS(timePoint, awsPair);
Pair<GpsLatLong, GpsLatLong> gpsPair = getSurroundingFixes(getGpsTrack(), timePoint);
Position pos = getPosition(timePoint, gpsPair);
Pair<HDG, HDG> hdgPair = getSurroundingFixes(getHdgTrack(), timePoint);
Pair<HDGM, HDGM> hdgmPair = getSurroundingFixes(getHdgmTrack(), timePoint);
Bearing heading = getHeading(timePoint, hdgPair, hdgmPair, pos);
if (awa != null && aws != null && pos != null && heading != null) {
Bearing apparentWindDirection = heading.add(awa);
SpeedWithBearing apparentWindSpeedWithDirection = new KnotSpeedWithBearingImpl(aws.getKnots(), apparentWindDirection);
/*
* Hint from Brent Russell from Igtimi, at 2013-12-05 on the question whether to use GpsLatLong to improve
* precision of boat speed / coarse over SOG/COG measurements:
*
* "Personally I would use COG/SOG exclusively, and if unhappy with the result add a small amount of
* smoothing and consider dropping samples as outliers if they cause a SOG discontinuity. The latter might
* happen as a satellite is dropped/acquired - and I'd expect to see a time correlated position jump as
* well. Probably not though a direction/speed correlation :)
*
* All our GPS systems are using Doppler to calculate COG/SOG and this should be the most accurate measure.
* I don't believe that delta position really adds any more "truth" to the measurement of physical reality,
* if that makes sense. I'd trust d-p even less at low speeds, where you'll see the most disagreement. Also
* there is a significant quantisation noise error in the d-p calculations from the GPS resolution too, so
* you'd have to smooth it before averaging - possibly in a speed dependent way.
*
* Remember that Doppler COG/SOG is using the same raw satellite measurements that are being used to
* calculate position, just the algorithm is different. I suspect that merging the two might be, in
* practice, just a slightly indirect way of averaging. If you like the central limit theorem in action over
* the set of algorithms!
*
* So again, my personal preference would be to work with the data that should be the most accurate
* (COG/SOG) and consider algorithms that handle smoothing of that data best."
*/
Pair<SOG, SOG> sogPair = getSurroundingFixes(getSogTrack(), timePoint);
Speed sog = getSOG(timePoint, sogPair);
Pair<COG, COG> cogPair = getSurroundingFixes(getCogTrack(), timePoint);
Bearing cog = getCOG(timePoint, cogPair);
SpeedWithBearing sogCog = new KnotSpeedWithBearingImpl(sog.getKnots(), cog);
SpeedWithBearing trueWindSpeedAndDirection = apparentWindSpeedWithDirection.add(sogCog);
result = new WindImpl(pos, timePoint, trueWindSpeedAndDirection);
} else {
result = null;
}
return result;
}
private Bearing getAWA(TimePoint timePoint, Pair<AWA, AWA> awaPair) {
final Bearing awaFrom;
if (awaPair.getA() == null) {
if (awaPair.getB() == null) {
awaFrom = null;
} else {
awaFrom = awaPair.getB().getApparentWindAngle();
}
} else {
if (awaPair.getB() == null) {
awaFrom = awaPair.getA().getApparentWindAngle();
} else {
awaFrom = timeBasedAgerage(timePoint,
new ScalableBearing(awaPair.getA().getApparentWindAngle()), awaPair.getA().getTimePoint(),
new ScalableBearing(awaPair.getB().getApparentWindAngle()), awaPair.getB().getTimePoint());
}
}
return awaFrom == null ? null : awaFrom.reverse();
}
private Speed getAWS(TimePoint timePoint, Pair<AWS, AWS> awsPair) {
final Speed aws;
if (awsPair.getA() == null) {
if (awsPair.getB() == null) {
aws = null;
} else {
aws = awsPair.getB().getApparentWindSpeed();
}
} else {
if (awsPair.getB() == null) {
aws = awsPair.getA().getApparentWindSpeed();
} else {
aws = timeBasedAgerage(timePoint,
new ScalableSpeed(awsPair.getA().getApparentWindSpeed()), awsPair.getA().getTimePoint(),
new ScalableSpeed(awsPair.getB().getApparentWindSpeed()), awsPair.getB().getTimePoint());
}
}
return aws;
}
private Speed getSOG(TimePoint timePoint, Pair<SOG, SOG> sogPair) {
final Speed sog;
if (sogPair.getA() == null) {
if (sogPair.getB() == null) {
sog = null;
} else {
sog = sogPair.getB().getSpeedOverGround();
}
} else {
if (sogPair.getB() == null) {
sog = sogPair.getA().getSpeedOverGround();
} else {
sog = timeBasedAgerage(timePoint,
new ScalableSpeed(sogPair.getA().getSpeedOverGround()), sogPair.getA().getTimePoint(),
new ScalableSpeed(sogPair.getB().getSpeedOverGround()), sogPair.getB().getTimePoint());
}
}
return sog;
}
private Bearing getCOG(TimePoint timePoint, Pair<COG, COG> cogPair) {
final Bearing sog;
if (cogPair.getA() == null) {
if (cogPair.getB() == null) {
sog = null;
} else {
sog = cogPair.getB().getCourseOverGround();
}
} else {
if (cogPair.getB() == null) {
sog = cogPair.getA().getCourseOverGround();
} else {
sog = timeBasedAgerage(timePoint,
new ScalableBearing(cogPair.getA().getCourseOverGround()), cogPair.getA().getTimePoint(),
new ScalableBearing(cogPair.getB().getCourseOverGround()), cogPair.getB().getTimePoint());
}
}
return sog;
}
private Position getPosition(TimePoint timePoint, Pair<GpsLatLong, GpsLatLong> gpsPair) {
final Position pos;
if (gpsPair.getA() == null) {
if (gpsPair.getB() == null) {
pos = null;
} else {
pos = gpsPair.getB().getPosition();
}
} else {
if (gpsPair.getB() == null) {
pos = gpsPair.getA().getPosition();
} else {
pos = timeBasedAgerage(timePoint, new ScalablePosition(gpsPair.getA().getPosition()), gpsPair.getA()
.getTimePoint(), new ScalablePosition(gpsPair.getB().getPosition()), gpsPair.getB()
.getTimePoint());
}
}
return pos;
}
private Bearing getHDG(TimePoint timePoint, Pair<HDG, HDG> hdgPair) {
final Bearing hdg;
if (hdgPair.getA() == null) {
if (hdgPair.getB() == null) {
hdg = null;
} else {
hdg = hdgPair.getB().getTrueHeading();
}
} else {
if (hdgPair.getB() == null) {
hdg = hdgPair.getA().getTrueHeading();
} else {
hdg = timeBasedAgerage(timePoint,
new ScalableBearing(hdgPair.getA().getTrueHeading()), hdgPair.getA().getTimePoint(),
new ScalableBearing(hdgPair.getB().getTrueHeading()), hdgPair.getB().getTimePoint());
}
}
return hdg;
}
private Bearing getHDGM(TimePoint timePoint, Pair<HDGM, HDGM> hdgmPair) {
final Bearing hdgm;
if (hdgmPair.getA() == null) {
if (hdgmPair.getB() == null) {
hdgm = null;
} else {
hdgm = hdgmPair.getB().getMagnetigHeading();
}
} else {
if (hdgmPair.getB() == null) {
hdgm = hdgmPair.getA().getMagnetigHeading();
} else {
hdgm = timeBasedAgerage(timePoint,
new ScalableBearing(hdgmPair.getA().getMagnetigHeading()), hdgmPair.getA().getTimePoint(),
new ScalableBearing(hdgmPair.getB().getMagnetigHeading()), hdgmPair.getB().getTimePoint());
}
}
return hdgm;
}
private Bearing getHeading(TimePoint timePoint, Pair<HDG, HDG> hdgPair, Pair<HDGM, HDGM> hdgmPair, Position position) throws ClassNotFoundException, IOException, ParseException {
final Bearing trueHeading;
Bearing hdg = getHDG(timePoint, hdgPair);
if (hdg != null) {
trueHeading = hdg;
} else {
Bearing hdgm = getHDGM(timePoint, hdgmPair);
Declination declination = declinationService.getDeclination(timePoint, position, /* timeoutForOnlineFetchInMilliseconds 5s */ 5000);
trueHeading = hdgm.add(declination.getBearingCorrectedTo(timePoint));
}
return trueHeading;
}
private <V, T> T timeBasedAgerage(TimePoint timePoint, ScalableValue<V, T> value1, TimePoint timePoint1, ScalableValue<V, T> value2, TimePoint timePoint2) {
final T acc;
if (timePoint1.equals(timePoint2)) {
acc = value1.add(value2).divide(2);
} else {
long timeDiff1 = Math.abs(timePoint1.asMillis() - timePoint.asMillis());
long timeDiff2 = Math.abs(timePoint2.asMillis() - timePoint.asMillis());
acc = value1.multiply(timeDiff2).add(value2.multiply(timeDiff1)).divide(timeDiff1 + timeDiff2);
}
return acc;
}
private <T extends Timed> Pair<T, T> getSurroundingFixes(Track<T> track, TimePoint timePoint) {
T left = track.getLastFixAtOrBefore(timePoint);
T right = track.getFirstFixAtOrAfter(timePoint);
Pair<T, T> result = new Pair<>(left, right);
return result;
}
private DynamicTrack<AWA> getAwaTrack() {
return awaTrack;
}
private DynamicTrack<AWS> getAwsTrack() {
return awsTrack;
}
private DynamicTrack<GpsLatLong> getGpsTrack() {
return gpsTrack;
}
private DynamicTrack<COG> getCogTrack() {
return cogTrack;
}
private DynamicTrack<SOG> getSogTrack() {
return sogTrack;
}
private DynamicTrack<HDG> getHdgTrack() {
return hdgTrack;
}
private DynamicTrack<HDGM> getHdgmTrack() {
return hdgmTrack;
}
}
@@ -6,9 +6,9 @@ public class SensorImpl implements Sensor {
private final String deviceSerialNumber;
private final long subDeviceId;
protected SensorImpl(String eviceSerialNumber, long subDeviceId) {
protected SensorImpl(String deviceSerialNumber, long subDeviceId) {
super();
this.deviceSerialNumber = eviceSerialNumber;
this.deviceSerialNumber = deviceSerialNumber;
this.subDeviceId = subDeviceId;
}
@@ -17,8 +17,8 @@ import javax.ws.rs.core.UriInfo;
import org.apache.http.client.ClientProtocolException;
import org.json.simple.parser.ParseException;
import com.sap.sailing.domain.igtimiadapter.IgtimiConnectionFactory;
import com.sap.sailing.domain.igtimiadapter.impl.Activator;
import com.sap.sailing.domain.igtimiadapter.impl.IgtimiConnectionFactoryImpl;
@Path(AuthorizationCallback.V1)
public class AuthorizationCallback {
@@ -27,7 +27,7 @@ public class AuthorizationCallback {
private static final String AUTHORIZATIONCALLBACK = "/authorizationcallback";
static final String V1 = "/v1";
static final String V1_AUTHORIZATIONCALLBACK = V1 + AUTHORIZATIONCALLBACK;
private final IgtimiConnectionFactory connectionFactory;
private final IgtimiConnectionFactoryImpl connectionFactory;
public AuthorizationCallback() throws ClientProtocolException, IllegalStateException, IOException, ParseException {
connectionFactory = Activator.getInstance().getConnectionFactory();
@@ -5,10 +5,9 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -26,14 +25,15 @@ import com.sap.sailing.domain.common.impl.MillisecondsTimePoint;
import com.sap.sailing.domain.common.impl.Util;
import com.sap.sailing.domain.common.impl.Util.Pair;
import com.sap.sailing.domain.igtimiadapter.Account;
import com.sap.sailing.domain.igtimiadapter.IgtimiConnectionFactory;
import com.sap.sailing.domain.igtimiadapter.LiveDataListener;
import com.sap.sailing.domain.igtimiadapter.BulkFixReceiver;
import com.sap.sailing.domain.igtimiadapter.LiveDataConnection;
import com.sap.sailing.domain.igtimiadapter.datatypes.Fix;
import com.sap.sailing.domain.igtimiadapter.impl.FixFactory;
import com.sap.sailing.domain.igtimiadapter.impl.IgtimiConnectionFactoryImpl;
public class WebSocketConnectionManager extends WebSocketAdapter {
public class WebSocketConnectionManager extends WebSocketAdapter implements LiveDataConnection {
private static final Logger logger = Logger.getLogger(WebSocketConnectionManager.class.getName());
private final IgtimiConnectionFactory connectionFactory;
private final IgtimiConnectionFactoryImpl connectionFactory;
private static enum TargetState { OPEN, CLOSED };
private TargetState targetState;
private final WebSocketClient client;
@@ -44,19 +44,19 @@ public class WebSocketConnectionManager extends WebSocketAdapter {
private final Account account;
private final FixFactory fixFactory;
private boolean receivedServerHeartbeatInInterval;
private final Set<LiveDataListener> liveDataListeners;
private final ConcurrentHashMap<BulkFixReceiver, BulkFixReceiver> listeners;
private TimePoint igtimiServerTimepoint;
private TimePoint localTimepointWhenServerTimepointWasReceived;
public WebSocketConnectionManager(IgtimiConnectionFactory connectionFactory, Iterable<String> deviceIds, Account account) throws Exception {
this.timer = new Timer("Timer for WebSocketConnectionManager for units "+deviceIds+" and account "+account);
this.deviceIds = deviceIds;
public WebSocketConnectionManager(IgtimiConnectionFactoryImpl connectionFactory, Iterable<String> deviceSerialNumbers, Account account) throws Exception {
this.timer = new Timer("Timer for WebSocketConnectionManager for units "+deviceSerialNumbers+" and account "+account);
this.deviceIds = deviceSerialNumbers;
this.account = account;
this.fixFactory = new FixFactory();
this.connectionFactory = connectionFactory;
this.liveDataListeners = new ConcurrentSkipListSet<>();
this.listeners = new ConcurrentHashMap<>();
client = new WebSocketClient();
configurationMessage = connectionFactory.getWebSocketConfigurationMessage(account, deviceIds);
configurationMessage = connectionFactory.getWebSocketConfigurationMessage(account, deviceSerialNumbers);
request = new ClientUpgradeRequest();
client.start();
reconnect();
@@ -71,6 +71,7 @@ public class WebSocketConnectionManager extends WebSocketAdapter {
* use 0 to wait indefinitely
* @return <code>true</code> if the connection is established before the timeout occurred
*/
@Override
public boolean waitForConnection(long timeoutInMillis) throws InterruptedException {
long startedToWait = System.currentTimeMillis();
synchronized (this) {
@@ -81,10 +82,6 @@ public class WebSocketConnectionManager extends WebSocketAdapter {
}
}
public void addLiveDataListener(LiveDataListener listener) {
liveDataListeners.add(listener);
}
public void disconnect() throws Exception {
targetState = TargetState.CLOSED;
client.stop();
@@ -141,9 +138,14 @@ public class WebSocketConnectionManager extends WebSocketAdapter {
return new Pair<TimePoint, TimePoint>(igtimiServerTimepoint, localTimepointWhenServerTimepointWasReceived);
}
@Override
public void addListener(BulkFixReceiver listener) {
listeners.put(listener, listener);
}
private void notifyListeners(List<Fix> fixes) {
for (LiveDataListener listener : liveDataListeners) {
listener.fixesReceived(fixes);
for (BulkFixReceiver listener : listeners.keySet()) {
listener.received(fixes);
}
}
@@ -198,7 +200,7 @@ public class WebSocketConnectionManager extends WebSocketAdapter {
}, /* delay */ 0, /* send ping message every other minute; has to be at least every five minutes, so this should be safe */ 120000l);
}
public void reconnect() throws IOException, IllegalStateException, ParseException, URISyntaxException {
private void reconnect() throws IOException, IllegalStateException, ParseException, URISyntaxException {
IOException lastException = null;
for (URI uri : connectionFactory.getWebsocketServers()) {
try {
@@ -104,4 +104,24 @@ public interface Track<FixType extends Timed> extends Serializable {
*/
Iterator<FixType> getRawFixesIterator(TimePoint startingAt, boolean inclusive);
/**
* Returns a descending iterator starting at the first fix before <code>startingAt</code> (or "at or before" in case
* <code>inclusive</code> is <code>true</code>). The fixes returned by the iterator are the smoothened fixes (see
* also {@link #getFixes()}, without any smoothening or dampening applied.
*
* Callers must have called {@link #lockForRead()} before calling this method. This will be checked, and an exception
* will be thrown in case the caller has failed to do so.
*/
Iterator<FixType> getFixesDescendingIterator(TimePoint startingAt, boolean inclusive);
/**
* Returns a descending iterator starting at the first raw fix before <code>startingAt</code> (or "at or before" in case
* <code>inclusive</code> is <code>true</code>). The fixes returned by the iterator are the raw fixes (see also
* {@link #getRawFixes()}, without any smoothening or dampening applied.
*
* Callers must have called {@link #lockForRead()} before calling this method. This will be checked, and an exception
* will be thrown in case the caller has failed to do so.
*/
Iterator<FixType> getRawFixesDescendingIterator(TimePoint startingAt, boolean inclusive);
}
@@ -253,6 +253,14 @@ public class TrackImpl<FixType extends Timed> implements Track<FixType> {
return result;
}
@Override
public Iterator<FixType> getFixesDescendingIterator(TimePoint startingAt, boolean inclusive) {
assertReadLock();
Iterator<FixType> result = (Iterator<FixType>) getInternalFixes().headSet(
getDummyFix(startingAt), inclusive).descendingIterator();
return result;
}
/**
* Creates a dummy fix that conforms to <code>FixType</code>. This in particular means that subclasses
* instantiating <code>FixType</code> with a specific class need to redefine this method so as to return
@@ -274,6 +282,14 @@ public class TrackImpl<FixType extends Timed> implements Track<FixType> {
return result;
}
@Override
public Iterator<FixType> getRawFixesDescendingIterator(TimePoint startingAt, boolean inclusive) {
assertReadLock();
Iterator<FixType> result = (Iterator<FixType>) getInternalRawFixes().headSet(
getDummyFix(startingAt), inclusive).descendingIterator();
return result;
}
protected boolean add(FixType fix) {
lockForWrite();
try {
@@ -5,6 +5,9 @@ import static org.junit.Assert.assertEquals;
import org.junit.Test;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.SpeedWithBearing;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sailing.domain.common.impl.MeterPerSecondSpeedImpl;
public class SpeedUnitTest {
@@ -13,4 +16,30 @@ public class SpeedUnitTest {
Speed s = new MeterPerSecondSpeedImpl(1852./3600.);
assertEquals(1, s.getKnots(), 0.0000001);
}
@Test
public void testSpeedWithBearingAddInverse() {
SpeedWithBearing s1 = new KnotSpeedWithBearingImpl(12., new DegreeBearingImpl(0));
SpeedWithBearing s2 = new KnotSpeedWithBearingImpl(12., new DegreeBearingImpl(180));
SpeedWithBearing sum = s1.add(s2);
assertEquals(0.0, sum.getKnots(), 0.0000001);
}
@Test
public void testSpeedWithBearingAddCollinear() {
SpeedWithBearing s1 = new KnotSpeedWithBearingImpl(12., new DegreeBearingImpl(123));
SpeedWithBearing s2 = new KnotSpeedWithBearingImpl(15., new DegreeBearingImpl(123));
SpeedWithBearing sum = s1.add(s2);
assertEquals(27.0, sum.getKnots(), 0.01);
assertEquals(123, sum.getBearing().getDegrees(), 0.0000001);
}
@Test
public void testSpeedWithBearingAddOrthogonal() {
SpeedWithBearing s1 = new KnotSpeedWithBearingImpl(12., new DegreeBearingImpl(20));
SpeedWithBearing s2 = new KnotSpeedWithBearingImpl(12., new DegreeBearingImpl(110));
SpeedWithBearing sum = s1.add(s2);
assertEquals(Math.sqrt(12*12+12*12), sum.getKnots(), 0.01);
assertEquals(65., sum.getBearing().getDegrees(), 0.0000001);
}
}
@@ -1,7 +1,13 @@
package com.sap.sailing.domain.tractracadapter;
import com.sap.sailing.domain.base.DomainFactory;
import com.sap.sailing.domain.tractracadapter.impl.TracTracAdapterFactoryImpl;
public interface TracTracAdapterFactory {
/**
* A default instance to be used only for testing!
*/
TracTracAdapterFactory INSTANCE = new TracTracAdapterFactoryImpl();
TracTracAdapter getOrCreateTracTracAdapter(DomainFactory baseDomainFactory);
}
@@ -3,7 +3,6 @@ package com.sap.sailing.domain.base.impl;
import com.sap.sailing.domain.base.BearingWithConfidence;
import com.sap.sailing.domain.base.BearingWithConfidence.DoublePair;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.impl.RadianBearingImpl;
import com.sap.sailing.domain.confidence.IsScalable;
import com.sap.sailing.domain.confidence.ScalableValue;
@@ -19,58 +18,4 @@ implements BearingWithConfidence<RelativeTo>, IsScalable<DoublePair, Bearing> {
public ScalableValue<DoublePair, Bearing> getScalableValue() {
return new ScalableBearing(getObject());
}
private static class ScalableBearing implements ScalableValue<DoublePair, Bearing> {
private final double sin;
private final double cos;
public ScalableBearing(Bearing bearing) {
this.sin = Math.sin(bearing.getRadians());
this.cos = Math.cos(bearing.getRadians());
}
private ScalableBearing(double sin, double cos) {
this.sin = sin;
this.cos = cos;
}
@Override
public ScalableValue<DoublePair, Bearing> multiply(double factor) {
DoublePair pair = getValue();
return new ScalableBearing(factor*pair.getA(), factor*pair.getB());
}
@Override
public ScalableValue<DoublePair, Bearing> add(ScalableValue<DoublePair, Bearing> t) {
DoublePair value = getValue();
DoublePair tValue = t.getValue();
return new ScalableBearing(value.getA()+tValue.getA(), value.getB()+tValue.getB());
}
/**
* If the combined confidence was 0.0, no {@link Bearing} object can reasonably be computed; hence, <code>null</code>
* is returned in such cases.
*/
@Override
public Bearing divide(double divisor) {
Bearing result;
if (sin == 0 && cos == 0) {
result = null;
} else {
double angle;
if (cos == 0) {
angle = sin >= 0 ? Math.PI / 2 : -Math.PI / 2;
} else {
angle = Math.atan2(sin, cos);
}
result = new RadianBearingImpl(angle < 0 ? angle + 2 * Math.PI : angle);
}
return result;
}
@Override
public DoublePair getValue() {
return new DoublePair(sin, cos);
}
}
}
@@ -29,6 +29,11 @@ public class KilometersPerHourSpeedWithBearingImpl extends KilometersPerHourSpee
return pos.translateGreatCircle(getBearing(), this.travel(from, to));
}
@Override
public SpeedWithBearing add(SpeedWithBearing other) {
return AbstractSpeedWithBearingImpl.add(this, other);
}
@Override
public SpeedWithBearing applyCourseChange(CourseChange courseChange) {
return AbstractSpeedWithBearingImpl.applyCourseChange(this, courseChange);
@@ -0,0 +1,60 @@
package com.sap.sailing.domain.base.impl;
import com.sap.sailing.domain.base.BearingWithConfidence.DoublePair;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.impl.RadianBearingImpl;
import com.sap.sailing.domain.confidence.ScalableValue;
public class ScalableBearing implements ScalableValue<DoublePair, Bearing> {
private final double sin;
private final double cos;
public ScalableBearing(Bearing bearing) {
this.sin = Math.sin(bearing.getRadians());
this.cos = Math.cos(bearing.getRadians());
}
private ScalableBearing(double sin, double cos) {
this.sin = sin;
this.cos = cos;
}
@Override
public ScalableValue<DoublePair, Bearing> multiply(double factor) {
DoublePair pair = getValue();
return new ScalableBearing(factor*pair.getA(), factor*pair.getB());
}
@Override
public ScalableValue<DoublePair, Bearing> add(ScalableValue<DoublePair, Bearing> t) {
DoublePair value = getValue();
DoublePair tValue = t.getValue();
return new ScalableBearing(value.getA()+tValue.getA(), value.getB()+tValue.getB());
}
/**
* If the combined confidence was 0.0, no {@link Bearing} object can reasonably be computed; hence, <code>null</code>
* is returned in such cases.
*/
@Override
public Bearing divide(double divisor) {
Bearing result;
if (sin == 0 && cos == 0) {
result = null;
} else {
double angle;
if (cos == 0) {
angle = sin >= 0 ? Math.PI / 2 : -Math.PI / 2;
} else {
angle = Math.atan2(sin, cos);
}
result = new RadianBearingImpl(angle < 0 ? angle + 2 * Math.PI : angle);
}
return result;
}
@Override
public DoublePair getValue() {
return new DoublePair(sin, cos);
}
}
@@ -0,0 +1,37 @@
package com.sap.sailing.domain.base.impl;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.impl.KnotSpeedImpl;
import com.sap.sailing.domain.confidence.ScalableValue;
public class ScalableSpeed implements ScalableValue<Double, Speed> {
private final double knots;
public ScalableSpeed(Speed speed) {
this.knots = speed.getKnots();
}
private ScalableSpeed(double knots) {
this.knots = knots;
}
@Override
public ScalableValue<Double, Speed> multiply(double factor) {
return new ScalableSpeed(factor*knots);
}
@Override
public ScalableValue<Double, Speed> add(ScalableValue<Double, Speed> t) {
return new ScalableSpeed(knots+t.getValue());
}
@Override
public Speed divide(double divisor) {
return new KnotSpeedImpl(knots / divisor);
}
@Override
public Double getValue() {
return knots;
}
}
@@ -2,7 +2,6 @@ package com.sap.sailing.domain.base.impl;
import com.sap.sailing.domain.base.SpeedWithConfidence;
import com.sap.sailing.domain.common.Speed;
import com.sap.sailing.domain.common.impl.KnotSpeedImpl;
import com.sap.sailing.domain.confidence.ScalableValue;
public class SpeedWithConfidenceImpl<RelativeTo> extends HasConfidenceImpl<Double, Speed, RelativeTo> implements SpeedWithConfidence<RelativeTo> {
@@ -16,36 +15,4 @@ public class SpeedWithConfidenceImpl<RelativeTo> extends HasConfidenceImpl<Doubl
public ScalableValue<Double, Speed> getScalableValue() {
return new ScalableSpeed(getObject());
}
private static class ScalableSpeed implements ScalableValue<Double, Speed> {
private final double knots;
public ScalableSpeed(Speed speed) {
this.knots = speed.getKnots();
}
private ScalableSpeed(double knots) {
this.knots = knots;
}
@Override
public ScalableValue<Double, Speed> multiply(double factor) {
return new ScalableSpeed(factor*knots);
}
@Override
public ScalableValue<Double, Speed> add(ScalableValue<Double, Speed> t) {
return new ScalableSpeed(knots+t.getValue());
}
@Override
public Speed divide(double divisor) {
return new KnotSpeedImpl(knots / divisor);
}
@Override
public Double getValue() {
return knots;
}
}
}
@@ -54,6 +54,11 @@ public class CompactGPSFixMovingImpl extends CompactGPSFixImpl implements GPSFix
return AbstractSpeedWithBearingImpl.getCourseChangeRequiredToReach(getSpeed(), targetSpeedWithBearing);
}
@Override
public SpeedWithBearing add(SpeedWithBearing other) {
return AbstractSpeedWithAbstractBearingImpl.add(this, other);
}
@Override
public String toString() {
return super.toString()+" to "+getBearing().getDegrees()+"°";
@@ -41,7 +41,7 @@ public class WindTrackImpl extends TrackImpl<Wind> implements WindTrack {
private final static Logger logger = Logger.getLogger(WindTrackImpl.class.getName());
private final static double DEFAULT_BASE_CONFIDENCE = 0.9;
protected final static double DEFAULT_BASE_CONFIDENCE = 0.9;
private final double baseConfidence;
@@ -341,6 +341,10 @@ public class WindTrackImpl extends TrackImpl<Wind> implements WindTrack {
return null;
}
@Override
public SpeedWithBearing add(SpeedWithBearing other) {
return null;
}
@Override
public int compareTo(Speed o) {
return 0;
}
@@ -1,3 +1,4 @@
bin
war
gwt-unitCache
www-test
@@ -1,23 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="com.google.gwt.eclipse.core.launch.gwtJUnit">
<stringAttribute key="com.google.gwt.eclipse.core.LOG_LEVEL" value="INFO"/>
<booleanAttribute key="com.google.gwt.eclipse.core.NOT_HEADLESS" value="false"/>
<stringAttribute key="com.google.gwt.eclipse.core.OUT_DIR" value="www-test"/>
<booleanAttribute key="com.google.gwt.eclipse.core.STANDARDS_MODE" value="false"/>
<stringAttribute key="com.google.gwt.eclipse.core.STYLE" value="OBFUSCATED"/>
<stringAttribute key="com.google.gwt.eclipse.core.WEB_MODE" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/GwtTestCaseColumnToggling.java"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="1"/>
</listAttribute>
<stringAttribute key="org.eclipse.jdt.junit.CONTAINER" value=""/>
<booleanAttribute key="org.eclipse.jdt.junit.KEEPRUNNING_ATTR" value="false"/>
<stringAttribute key="org.eclipse.jdt.junit.TESTNAME" value=""/>
<stringAttribute key="org.eclipse.jdt.junit.TEST_KIND" value="org.eclipse.jdt.junit.loader.junit4"/>
<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="com.google.gwt.eclipse.core.moduleClasspathProvider"/>
<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.sap.sailing.gwt.ui.test.GwtTestCaseColumnToggling"/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="com.sap.sailing.gwt.ui.test"/>
<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Dhttp.proxyHost=proxy -Dhttp.proxyPort=8080 -XX:+UseParallelGC -Dtractrac.tunnel=true"/>
</launchConfiguration>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="com.google.gwt.eclipse.core.launch.gwtJUnit">
<stringAttribute key="com.google.gwt.eclipse.core.LOG_LEVEL" value="INFO"/>
<booleanAttribute key="com.google.gwt.eclipse.core.NOT_HEADLESS" value="false"/>
<stringAttribute key="com.google.gwt.eclipse.core.OUT_DIR" value="www-test"/>
<booleanAttribute key="com.google.gwt.eclipse.core.STANDARDS_MODE" value="false"/>
<stringAttribute key="com.google.gwt.eclipse.core.STYLE" value="OBFUSCATED"/>
<stringAttribute key="com.google.gwt.eclipse.core.WEB_MODE" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/com.sap.sailing.gwt.ui.test"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="4"/>
</listAttribute>
<stringAttribute key="org.eclipse.jdt.junit.CONTAINER" value="=com.sap.sailing.gwt.ui.test"/>
<booleanAttribute key="org.eclipse.jdt.junit.KEEPRUNNING_ATTR" value="false"/>
<stringAttribute key="org.eclipse.jdt.junit.TESTNAME" value=""/>
<stringAttribute key="org.eclipse.jdt.junit.TEST_KIND" value="org.eclipse.jdt.junit.loader.junit4"/>
<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="com.google.gwt.eclipse.core.moduleClasspathProvider"/>
<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7"/>
<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value=""/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="com.sap.sailing.gwt.ui.test"/>
<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-ea -XX:+UseParallelGC"/>
</launchConfiguration>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="com.google.gwt.eclipse.core.launch.gwtJUnit">
<stringAttribute key="com.google.gwt.eclipse.core.LOG_LEVEL" value="INFO"/>
<booleanAttribute key="com.google.gwt.eclipse.core.NOT_HEADLESS" value="false"/>
<stringAttribute key="com.google.gwt.eclipse.core.OUT_DIR" value="www-test"/>
<booleanAttribute key="com.google.gwt.eclipse.core.STANDARDS_MODE" value="false"/>
<stringAttribute key="com.google.gwt.eclipse.core.STYLE" value="OBFUSCATED"/>
<stringAttribute key="com.google.gwt.eclipse.core.WEB_MODE" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/com.sap.sailing.gwt.ui.test"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="4"/>
</listAttribute>
<stringAttribute key="org.eclipse.jdt.junit.CONTAINER" value="=com.sap.sailing.gwt.ui.test"/>
<booleanAttribute key="org.eclipse.jdt.junit.KEEPRUNNING_ATTR" value="false"/>
<stringAttribute key="org.eclipse.jdt.junit.TESTNAME" value=""/>
<stringAttribute key="org.eclipse.jdt.junit.TEST_KIND" value="org.eclipse.jdt.junit.loader.junit4"/>
<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="com.google.gwt.eclipse.core.moduleClasspathProvider"/>
<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7"/>
<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value=""/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="com.sap.sailing.gwt.ui.test"/>
<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-ea -Dhttp.proxyHost=proxy -Dhttp.proxyPort=8080 -XX:+UseParallelGC -Dtractrac.tunnel=true -Dtractrac.tunnel.host=10.18.22.50"/>
</launchConfiguration>
@@ -8,7 +8,9 @@ Bundle-RequiredExecutionEnvironment: JavaSE-1.6
Fragment-Host: com.sap.sailing.gwt.ui
Require-Bundle: com.sap.sailing.server,
org.hamcrest.core;bundle-version="1.1.0",
com.sap.sailing.expeditionconnector
com.sap.sailing.expeditionconnector,
org.mockito.mockito-core;bundle-version="1.9.5",
org.objenesis;bundle-version="1.3.0"
Bundle-ClassPath: .
Import-Package: javax.jms;version="1.1.0",
javax.servlet;version="2.6.0",
@@ -1,9 +1,12 @@
package com.sap.sailing.gwt.ui.test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.Test;
import com.google.gwt.core.client.GWT;
import com.google.gwt.junit.client.GWTTestCase;
@@ -27,15 +30,15 @@ public class GwtTestCaseColumnToggling extends GWTTestCase {
//These objects should be created by calling GWT.create(Class c);
private LeaderboardPanelMock leaderboardPanel;
private MyTestSailingServiceAsync service;
private StringMessages sc;
private StringMessages stringMessages;
//Test data.
private final String LEADERBOARD_NAME = "test";
private static final String DEFAULT_FLEET_NAME = "Default";
private final String COLUMN1_NAME = "r1";
private final String EVENT_NAME = "Sailing Team Germany (STG)";
protected static final boolean tractracTunnel = true; // Boolean.valueOf(System.getProperty("tractrac.tunnel", "false"));
protected static final String tractracTunnelHost = "10.18.206.73"; // System.getProperty("tractrac.tunnel.host", "localhost");
protected static final boolean tractracTunnel = Boolean.valueOf(System.getProperty("tractrac.tunnel", "false"));
protected static final String tractracTunnelHost = System.getProperty("tractrac.tunnel.host", "localhost");
private final String JSON_URL= "http://" + TracTracConnectionConstants.HOST_NAME + "/events/event_20110505_SailingTea/jsonservice.php";
private final String TRACKED_RACE = "schwerttest";
private final String COURSE_DESIGN_UPDATE_URI = "http://tracms.traclive.dk/update_course";
@@ -52,18 +55,17 @@ public class GwtTestCaseColumnToggling extends GWTTestCase {
protected void gwtSetUp() throws Exception {
super.gwtSetUp();
service = GWT.create(MyTestSailingService.class);
sc = GWT.create(StringMessages.class);
stringMessages = GWT.create(StringMessages.class);
}
@Test
public void testColumnDeleting() {
delayTestFinish(1000000);
listRacesInEvent();
}
private void listRacesInEvent(){
service.listTracTracRacesInEvent(JSON_URL, false, new AsyncCallback<Pair<String,List<TracTracRaceRecordDTO>>>() {
service.listTracTracRacesInEvent(JSON_URL, /* hidden */ true, new AsyncCallback<Pair<String,List<TracTracRaceRecordDTO>>>() {
@Override
public void onFailure(Throwable caught) {
@@ -78,7 +80,6 @@ public class GwtTestCaseColumnToggling extends GWTTestCase {
rrDao = rr;
}
}
//assertNotNull("rrDao != null",rrDao);
assertNotNull(rrDao);
trackRace();
}
@@ -86,7 +87,9 @@ public class GwtTestCaseColumnToggling extends GWTTestCase {
}
private void trackRace(){
service.trackWithTracTrac(/* regattaToAddTo */null, Collections.singleton(rrDao), tractracTunnel ? "tcp://" + tractracTunnelHost + ":"
final Set<TracTracRaceRecordDTO> rrDAOs = new HashSet<TracTracRaceRecordDTO>();
rrDAOs.add(rrDao);
service.trackWithTracTrac(/* regattaToAddTo */null, rrDAOs, tractracTunnel ? "tcp://" + tractracTunnelHost + ":"
+ TracTracConnectionConstants.PORT_TUNNEL_LIVE : "tcp://" + TracTracConnectionConstants.HOST_NAME + ":"
+ TracTracConnectionConstants.PORT_LIVE, tractracTunnel ? "tcp://" + tractracTunnelHost + ":"
+ TracTracConnectionConstants.PORT_TUNNEL_STORED : "tcp://" + TracTracConnectionConstants.HOST_NAME
@@ -108,30 +111,39 @@ public class GwtTestCaseColumnToggling extends GWTTestCase {
}
private void createLeaderboard(){
service.createFlexibleLeaderboard(LEADERBOARD_NAME, null, new int[] { 1, 2 }, ScoringSchemeType.LOW_POINT, null,
new AsyncCallback<StrippedLeaderboardDTO>() {
@Override
public void onSuccess(StrippedLeaderboardDTO result) {
System.out.println("Created Leaderboard "+result.name);
addColumnToLeaderboard();
}
service.removeLeaderboard(LEADERBOARD_NAME, new AsyncCallback<Void>() {
@Override
public void onFailure(Throwable caught) {
fail("Failed to remove Leaderboard "+LEADERBOARD_NAME+": " + caught.getLocalizedMessage());
finishTest();
}
@Override
public void onFailure(Throwable caught) {
fail("Failed to create Leaderboard." + caught.getLocalizedMessage());
finishTest();
}
});
@Override
public void onSuccess(Void result) {
service.createFlexibleLeaderboard(LEADERBOARD_NAME, null, new int[] { 1, 2 }, ScoringSchemeType.LOW_POINT, null,
new AsyncCallback<StrippedLeaderboardDTO>() {
@Override
public void onSuccess(StrippedLeaderboardDTO result) {
System.out.println("Created Leaderboard "+result.name);
addColumnToLeaderboard();
}
@Override
public void onFailure(Throwable caught) {
fail("Failed to create Leaderboard." + caught.getLocalizedMessage());
}
});
}
});
}
private void addColumnToLeaderboard() {
leaderboardPanel = new LeaderboardPanelMock(service, LEADERBOARD_NAME, null, sc);
leaderboardPanel = new LeaderboardPanelMock(service, LEADERBOARD_NAME, null, stringMessages);
service.addColumnToLeaderboard(COLUMN1_NAME, LEADERBOARD_NAME, false,
new AsyncCallback<Void>() {
@Override
public void onFailure(Throwable caught) {
fail("Failed to add column to leaderboard.");
finishTest();
}
@Override
@@ -158,12 +170,12 @@ public class GwtTestCaseColumnToggling extends GWTTestCase {
@Override
public void onSuccess(Boolean result) {
System.out.println("Success of linking race to column: "+result);
getLeaderboard();
getLeaderboardByNameAndDeleteColumn();
}
});
}
private void getLeaderboard(){
private void getLeaderboardByNameAndDeleteColumn(){
ArrayList<String> al = new ArrayList<String>();
al.add(COLUMN1_NAME);
service.getLeaderboardByName(LEADERBOARD_NAME, new Date(), al, /* previousLeaderboardId */ null, new AsyncCallback<IncrementalOrFullLeaderboardDTO>() {
@@ -176,7 +188,6 @@ public class GwtTestCaseColumnToggling extends GWTTestCase {
@Override
public void onSuccess(IncrementalOrFullLeaderboardDTO result) {
System.out.println("Got leaderboard.");
leaderboard = result.getLeaderboardDTO(/* previousVersion */ null);
leaderboardPanel.updateLeaderboard(leaderboard);
for (int i = 0; i < leaderboardPanel.getLeaderboardTable()
@@ -197,7 +208,6 @@ public class GwtTestCaseColumnToggling extends GWTTestCase {
private void removeColumnAndAssert(){
service.removeLeaderboardColumn(LEADERBOARD_NAME, COLUMN1_NAME,
new AsyncCallback<Void>() {
@Override
public void onFailure(Throwable caught) {
fail("Failed to remoce column.");
@@ -3,10 +3,12 @@ package com.sap.sailing.gwt.ui.test;
import com.sap.sailing.domain.common.dto.LeaderboardDTO;
import com.sap.sailing.domain.common.dto.LeaderboardRowDTO;
import com.sap.sailing.domain.common.dto.RaceColumnDTO;
import com.sap.sailing.gwt.ui.actions.AsyncActionsExecutor;
import com.sap.sailing.gwt.ui.client.CompetitorSelectionModel;
import com.sap.sailing.gwt.ui.client.ErrorReporter;
import com.sap.sailing.gwt.ui.client.SailingServiceAsync;
import com.sap.sailing.gwt.ui.client.StringMessages;
import com.sap.sailing.gwt.ui.client.UserAgentDetails;
import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel;
import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettingsFactory;
import com.sap.sailing.gwt.ui.leaderboard.SortableColumn;
@@ -16,10 +18,10 @@ public class LeaderboardPanelMock extends LeaderboardPanel {
public LeaderboardPanelMock(SailingServiceAsync sailingService,
String leaderboardName, ErrorReporter errorReporter,
StringMessages stringConstants) {
super(sailingService, null, LeaderboardSettingsFactory.getInstance().createNewDefaultSettings(
StringMessages stringMessages) {
super(sailingService, new AsyncActionsExecutor(), LeaderboardSettingsFactory.getInstance().createNewDefaultSettings(
/* racesToShow */null, /* namesOfRacesToShow */ null, null, /* autoExpandFirstRace */ false), new CompetitorSelectionModel(
/* hasMultiSelection */true), leaderboardName, errorReporter, stringConstants, null, /* showRaceDetails */ true);
/* hasMultiSelection */true), leaderboardName, errorReporter, stringMessages, new UserAgentDetails("gecko1_8"), /* showRaceDetails */ true);
}
@Override
@@ -1,32 +1,10 @@
package com.sap.sailing.gwt.ui.test;
import org.osgi.framework.BundleContext;
import org.osgi.util.tracker.ServiceTracker;
import com.sap.sailing.gwt.ui.server.SailingServiceImpl;
import com.sap.sailing.server.RacingEventService;
import com.sap.sailing.server.impl.RacingEventServiceImpl;
public class MyTestSailingServiceImpl extends SailingServiceImpl implements MyTestSailingService {
public class MyTestSailingServiceImpl extends SailingServiceImplMock implements MyTestSailingService {
private static final long serialVersionUID = 3647254510616623048L;
private RacingEventService service;
public MyTestSailingServiceImpl(){
super();
service = new RacingEventServiceImpl();
}
public RacingEventService getService(){
return service;
}
@Override
protected ServiceTracker<RacingEventService, RacingEventService> createAndOpenRacingEventServiceTracker(
BundleContext context) {
// TODO Auto-generated method stub
return null;
}
}
@@ -9,8 +9,6 @@ import java.util.concurrent.ConcurrentSkipListSet;
import junit.framework.Assert;
import org.junit.Test;
import org.osgi.framework.BundleContext;
import org.osgi.util.tracker.ServiceTracker;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.base.Competitor;
@@ -29,7 +27,6 @@ import com.sap.sailing.domain.common.PolarSheetsData;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
import com.sap.sailing.domain.common.RegattaNameAndRaceName;
import com.sap.sailing.domain.common.ScoreCorrectionProvider;
import com.sap.sailing.domain.common.SpeedWithBearing;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.WindSource;
@@ -43,8 +40,6 @@ import com.sap.sailing.domain.common.impl.Util;
import com.sap.sailing.domain.common.impl.Util.Pair;
import com.sap.sailing.domain.common.impl.WindSourceImpl;
import com.sap.sailing.domain.common.impl.WindSteppingWithMaxDistance;
import com.sap.sailing.domain.swisstimingadapter.SwissTimingAdapterFactory;
import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayServiceFactory;
import com.sap.sailing.domain.tracking.DynamicGPSFixTrack;
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
import com.sap.sailing.domain.tracking.GPSFixMoving;
@@ -57,12 +52,9 @@ import com.sap.sailing.domain.tracking.impl.MarkPassingByTimeComparator;
import com.sap.sailing.domain.tracking.impl.MarkPassingImpl;
import com.sap.sailing.domain.tracking.impl.WindImpl;
import com.sap.sailing.domain.tracking.impl.WindWithConfidenceImpl;
import com.sap.sailing.domain.tractracadapter.TracTracAdapterFactory;
import com.sap.sailing.gwt.ui.client.SailingService;
import com.sap.sailing.gwt.ui.server.SailingServiceImpl;
import com.sap.sailing.server.RacingEventService;
import com.sap.sailing.server.impl.RacingEventServiceImpl;
import com.sap.sailing.server.replication.ReplicationService;
public class PolarSheetGenerationServiceTest {
@@ -100,50 +92,12 @@ public class PolarSheetGenerationServiceTest {
@SuppressWarnings("serial")
private class MockSailingServiceForPolarSheetGeneration extends SailingServiceImpl {
private class MockSailingServiceForPolarSheetGeneration extends SailingServiceImplMock {
@Override
protected RacingEventService getService() {
RacingEventService service = new MockRacingEventServiceForPolarSheetGeneration();
return service;
}
@Override
protected ServiceTracker<RacingEventService, RacingEventService> createAndOpenRacingEventServiceTracker(
BundleContext context) {
return null;
}
@Override
protected ServiceTracker<ReplicationService, ReplicationService> createAndOpenReplicationServiceTracker(
BundleContext context) {
return null;
}
@Override
protected ServiceTracker<ScoreCorrectionProvider, ScoreCorrectionProvider> createAndOpenScoreCorrectionProviderServiceTracker(
BundleContext bundleContext) {;
return null;
}
@Override
protected ServiceTracker<TracTracAdapterFactory, TracTracAdapterFactory> createAndOpenTracTracAdapterTracker(
BundleContext context) {
return null;
}
@Override
protected ServiceTracker<SwissTimingAdapterFactory, SwissTimingAdapterFactory> createAndOpenSwissTimingAdapterTracker(
BundleContext context) {
return null;
}
@Override
protected ServiceTracker<SwissTimingReplayServiceFactory, SwissTimingReplayServiceFactory> createAndOpenSwissTimingReplayServiceTracker(
BundleContext context) {
return null;
}
}
private class MockRacingEventServiceForPolarSheetGeneration extends RacingEventServiceImpl {
@@ -1,15 +1,40 @@
package com.sap.sailing.gwt.ui.test;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.util.List;
import org.osgi.framework.BundleContext;
import org.osgi.util.tracker.ServiceTracker;
import com.sap.sailing.domain.base.DomainFactory;
import com.sap.sailing.domain.common.RegattaIdentifier;
import com.sap.sailing.domain.common.ScoreCorrectionProvider;
import com.sap.sailing.domain.racelog.RaceLogStore;
import com.sap.sailing.domain.swisstimingadapter.RaceSpecificMessageLoader;
import com.sap.sailing.domain.swisstimingadapter.SwissTimingAdapter;
import com.sap.sailing.domain.swisstimingadapter.SwissTimingAdapterFactory;
import com.sap.sailing.domain.swisstimingadapter.SwissTimingFactory;
import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayService;
import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayServiceFactory;
import com.sap.sailing.domain.tracking.RacesHandle;
import com.sap.sailing.domain.tracking.TrackerManager;
import com.sap.sailing.domain.tractracadapter.TracTracAdapterFactory;
import com.sap.sailing.gwt.ui.server.SailingServiceImpl;
import com.sap.sailing.server.RacingEventService;
import com.sap.sailing.server.impl.RacingEventServiceImpl;
import com.sap.sailing.server.replication.ReplicationService;
import com.sap.sailing.server.replication.impl.ReplicationInstancesManager;
import com.sap.sailing.server.replication.impl.ReplicationServiceImpl;
public class SailingServiceImplMock extends SailingServiceImpl {
private static final long serialVersionUID = 8564037671550730455L;
private final RacingEventService service;
private RacingEventService service;
public SailingServiceImplMock() {
super();
@@ -19,11 +44,123 @@ public class SailingServiceImplMock extends SailingServiceImpl {
@Override
protected ServiceTracker<RacingEventService, RacingEventService> createAndOpenRacingEventServiceTracker(
BundleContext context) {
@SuppressWarnings("unchecked")
ServiceTracker<RacingEventService, RacingEventService> result = mock(ServiceTracker.class);
when(result.getService()).thenReturn(new RacingEventServiceImpl());
return result;
}
@Override
protected ServiceTracker<ReplicationService, ReplicationService> createAndOpenReplicationServiceTracker(
BundleContext context) {
@SuppressWarnings("unchecked")
ServiceTracker<ReplicationService, ReplicationService> result = mock(ServiceTracker.class);
try {
final ReplicationServiceImpl replicationService = new ReplicationServiceImpl("test exchange", "localhost", new ReplicationInstancesManager()) {
@Override
protected ServiceTracker<RacingEventService, RacingEventService> getRacingEventServiceTracker() {
@SuppressWarnings("unchecked")
ServiceTracker<RacingEventService, RacingEventService> result = (ServiceTracker<RacingEventService, RacingEventService>) mock(ServiceTracker.class);
doReturn("Humba Humba").when(result).toString();
when(result.getService()).thenReturn(new RacingEventServiceImpl());
return result;
}
};
when(result.getService()).thenReturn(replicationService);
} catch (IOException e) {
throw new RuntimeException();
}
return result;
}
@Override
protected ServiceTracker<ScoreCorrectionProvider, ScoreCorrectionProvider> createAndOpenScoreCorrectionProviderServiceTracker(
BundleContext bundleContext) {
return null;
}
@Override
protected ServiceTracker<TracTracAdapterFactory, TracTracAdapterFactory> createAndOpenTracTracAdapterTracker(
BundleContext context) {
@SuppressWarnings("unchecked")
ServiceTracker<TracTracAdapterFactory, TracTracAdapterFactory> result = mock(ServiceTracker.class);
when(result.getService()).thenReturn(TracTracAdapterFactory.INSTANCE);
return result;
}
@Override
protected ServiceTracker<SwissTimingAdapterFactory, SwissTimingAdapterFactory> createAndOpenSwissTimingAdapterTracker(
BundleContext context) {
@SuppressWarnings("unchecked")
ServiceTracker<SwissTimingAdapterFactory, SwissTimingAdapterFactory> result = mock(ServiceTracker.class);
when(result.getService()).thenReturn(new SwissTimingAdapterFactory() {
@Override
public SwissTimingAdapter getOrCreateSwissTimingAdapter(DomainFactory baseDomainFactory,
RaceSpecificMessageLoader persistence) {
return new SwissTimingAdapter() {
@Override
public List<com.sap.sailing.domain.swisstimingadapter.RaceRecord> getSwissTimingRaceRecords(
String hostname, int port, boolean canSendRequests) throws InterruptedException,
UnknownHostException, IOException, ParseException {
// TODO Auto-generated method stub
return null;
}
@Override
public RacesHandle addSwissTimingRace(TrackerManager trackerManager,
RegattaIdentifier regattaToAddTo, String raceID, String hostname, int port,
boolean canSendRequests, RaceLogStore logStore, long timeoutInMilliseconds)
throws InterruptedException, UnknownHostException, IOException, ParseException,
Exception {
// TODO Auto-generated method stub
return null;
}
@Override
public SwissTimingFactory getSwissTimingFactory() {
// TODO Auto-generated method stub
return null;
}
@Override
public void storeSwissTimingDummyRace(String racMessage, String stlMesssage, String ccgMessage)
throws IllegalArgumentException {
// TODO Auto-generated method stub
}
@Override
public com.sap.sailing.domain.swisstimingadapter.DomainFactory getSwissTimingDomainFactory() {
return com.sap.sailing.domain.swisstimingadapter.DomainFactory.INSTANCE;
}
};
}
});
return result;
}
@Override
protected ServiceTracker<SwissTimingReplayServiceFactory, SwissTimingReplayServiceFactory> createAndOpenSwissTimingReplayServiceTracker(
BundleContext context) {
@SuppressWarnings("unchecked")
ServiceTracker<SwissTimingReplayServiceFactory, SwissTimingReplayServiceFactory> result = mock(ServiceTracker.class);
final SwissTimingReplayServiceFactory swissTimingReplayService = new SwissTimingReplayServiceFactory() {
@Override
public SwissTimingReplayService createSwissTimingReplayService(
com.sap.sailing.domain.swisstimingadapter.DomainFactory domainFactory) {
// TODO Auto-generated method stub
return null;
}
};
when(result.getService()).thenReturn(swissTimingReplayService);
return result;
}
@Override
protected RacingEventService getService() {
if (service == null) {
service = new RacingEventServiceImpl();
}
return service;
}
}
@@ -1,6 +1,7 @@
package com.sap.sailing.gwt.ui.test;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -42,6 +43,7 @@ public class TestColumnSwapping {
public void prepareColumnSwapping() {
service = new SailingServiceImplMock();
int[] disc = { 5, 8, 9, 0, 7, 5, 43 };
service.removeLeaderboard(LEADERBOARDNAME);
service.createFlexibleLeaderboard(LEADERBOARDNAME, null, disc, ScoringSchemeType.LOW_POINT, null);
service.addColumnToLeaderboard("Race1", LEADERBOARDNAME, true);
service.addColumnToLeaderboard("Race2", LEADERBOARDNAME, true);
@@ -59,9 +61,9 @@ public class TestColumnSwapping {
return UUID.randomUUID().toString();
}
});
leaderboardOriginalDTO.addRace("Race1", /* explicitFactor */ null, 1., DEFAULT_FLEET, true, null, null);
leaderboardOriginalDTO.addRace("Race3", /* explicitFactor */ null, 1., DEFAULT_FLEET, true, null, null);
leaderboardOriginalDTO.addRace("Race2", /* explicitFactor */ null, 1., DEFAULT_FLEET, true, null, null);
leaderboardOriginalDTO.addRace("Race1", /* explicitFactor */ null, 2., DEFAULT_FLEET, true, null, null);
leaderboardOriginalDTO.addRace("Race3", /* explicitFactor */ null, 2., DEFAULT_FLEET, true, null, null);
leaderboardOriginalDTO.addRace("Race2", /* explicitFactor */ null, 2., DEFAULT_FLEET, true, null, null);
} catch (Exception e) {
e.printStackTrace();
}
@@ -79,6 +81,7 @@ public class TestColumnSwapping {
sailingService = new SailingServiceImplMock();
assertNotNull("Sailingservice != NULL", sailingService);
int td[] = { 5, 8 };
sailingService.removeLeaderboard(TEST_LEADERBOARD_NAME);
sailingService.createFlexibleLeaderboard(TEST_LEADERBOARD_NAME, null, td, ScoringSchemeType.LOW_POINT, null);
for (int i = 0; i < races.length; i++)
sailingService.addColumnToLeaderboard(races[i], TEST_LEADERBOARD_NAME, isMedalRace[i]);
@@ -107,7 +110,7 @@ public class TestColumnSwapping {
boolean[] medalRace = { true, false, false };
for (int i = 0; i < lb.getRaceList().size(); i++) {
assertTrue("Race[" + i + "] == " + raceNames[i], lb.getRaceList().get(i).equals(raceNames[i]));
assertEquals("Race[" + i + "] == " + raceNames[i], raceNames[i], lb.getRaceList().get(i).getName());
assertTrue("Race[" + i + "] is " + (medalRace[i] ? "" : "no ") + "medalrace.",
lb.raceIsMedalRace(races[i]) == medalRace[i]);
}
@@ -127,8 +130,9 @@ public class TestColumnSwapping {
lb.addRace("3", /* explicitFactor */ null, 1., DEFAULT_FLEET, true, null, null);
lb.moveRaceDown("1");
String[] s = new String[] { "2", "1", "3" };
for (int i = 0; i < lb.getRaceList().size(); i++)
assertTrue("Race[" + i + "] = " + s[i], lb.getRaceList().get(i).equals(s[i]));
for (int i = 0; i < lb.getRaceList().size(); i++) {
assertEquals("Race[" + i + "] = " + s[i], s[i], lb.getRaceList().get(i).getName());
}
}
@Test
@@ -367,10 +367,11 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
baseDomainFactory = getService().getBaseDomainFactory();
mongoObjectFactory = getService().getMongoObjectFactory();
domainObjectFactory = getService().getDomainObjectFactory();
tracTracAdapter = createAndOpenTracTracAdapterTracker(context).getService().getOrCreateTracTracAdapter(baseDomainFactory);
tracTracAdapter = getTracTracAdapter(context);
// TODO what about passing on the mongo/domain object factory to obtain an according SwissTimingAdapterPersistence instance similar to how the tractracDomainObjectFactory etc. are created below?
swissTimingAdapterPersistence = SwissTimingAdapterPersistence.INSTANCE;
swissTimingAdapter = createAndOpenSwissTimingAdapterTracker(context).getService().getOrCreateSwissTimingAdapter(baseDomainFactory, swissTimingAdapterPersistence);
swissTimingReplayService = createAndOpenSwissTimingReplayServiceTracker(context).getService().createSwissTimingReplayService(swissTimingAdapter.getSwissTimingDomainFactory());
swissTimingAdapter = getSwissTimingAdapter(context);
swissTimingReplayService = getSwissTimingReplayService(context);
scoreCorrectionProviderServiceTracker = createAndOpenScoreCorrectionProviderServiceTracker(context);
tractracDomainObjectFactory = com.sap.sailing.domain.tractracadapter.persistence.PersistenceFactory.INSTANCE.createDomainObjectFactory(mongoObjectFactory.getDatabase(), tracTracAdapter.getTracTracDomainFactory());
tractracMongoObjectFactory = com.sap.sailing.domain.tractracadapter.persistence.MongoObjectFactory.INSTANCE;
@@ -400,6 +401,18 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
/* workQueue */ new LinkedBlockingQueue<Runnable>());
}
protected SwissTimingReplayService getSwissTimingReplayService(BundleContext context) {
return createAndOpenSwissTimingReplayServiceTracker(context).getService().createSwissTimingReplayService(swissTimingAdapter.getSwissTimingDomainFactory());
}
protected SwissTimingAdapter getSwissTimingAdapter(BundleContext context) {
return createAndOpenSwissTimingAdapterTracker(context).getService().getOrCreateSwissTimingAdapter(baseDomainFactory, swissTimingAdapterPersistence);
}
protected TracTracAdapter getTracTracAdapter(BundleContext context) {
return createAndOpenTracTracAdapterTracker(context).getService().getOrCreateTracTracAdapter(baseDomainFactory);
}
protected ServiceTracker<TracTracAdapterFactory, TracTracAdapterFactory> createAndOpenTracTracAdapterTracker(BundleContext context) {
ServiceTracker<TracTracAdapterFactory, TracTracAdapterFactory> result = new ServiceTracker<TracTracAdapterFactory, TracTracAdapterFactory>(
context, TracTracAdapterFactory.class.getName(), null);
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.jdt.junit.launchconfig">
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/com.sap.sailing.selenium.test"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="4"/>
</listAttribute>
<stringAttribute key="org.eclipse.jdt.junit.CONTAINER" value="=com.sap.sailing.selenium.test"/>
<booleanAttribute key="org.eclipse.jdt.junit.KEEPRUNNING_ATTR" value="false"/>
<stringAttribute key="org.eclipse.jdt.junit.TESTNAME" value=""/>
<stringAttribute key="org.eclipse.jdt.junit.TEST_KIND" value="org.eclipse.jdt.junit.loader.junit4"/>
<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value=""/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="com.sap.sailing.selenium.test"/>
<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-ea -Dselenium.test.environment.configuration=local-test-environment.xml -Dgwt.codesvr=gwt.codesvr=127.0.0.1:9997"/>
</launchConfiguration>
@@ -12,5 +12,5 @@
<stringAttribute key="org.eclipse.jdt.junit.TEST_KIND" value="org.eclipse.jdt.junit.loader.junit4"/>
<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value=""/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="com.sap.sailing.selenium.test"/>
<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-ea -Dselenium.test.environment.configuration=local-test-environment.xml -Dhttp.proxyHost=proxy -Dhttp.proxyPort=8080"/>
<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-ea -Dselenium.test.environment.configuration=local-test-environment.xml -Dhttp.proxyHost=proxy -Dhttp.proxyPort=8080 -Dwebdriver.firefox.profile=Selenium"/>
</launchConfiguration>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.jdt.junit.launchconfig">
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/com.sap.sailing.selenium.test"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="4"/>
</listAttribute>
<stringAttribute key="org.eclipse.jdt.junit.CONTAINER" value="=com.sap.sailing.selenium.test"/>
<booleanAttribute key="org.eclipse.jdt.junit.KEEPRUNNING_ATTR" value="false"/>
<stringAttribute key="org.eclipse.jdt.junit.TESTNAME" value=""/>
<stringAttribute key="org.eclipse.jdt.junit.TEST_KIND" value="org.eclipse.jdt.junit.loader.junit4"/>
<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value=""/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="com.sap.sailing.selenium.test"/>
<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-ea -Dselenium.test.environment.configuration=local-test-environment.xml -Dhttp.proxyHost=proxy -Dhttp.proxyPort=8080 -Dgwt.codesvr=gwt.codesvr=127.0.0.1:9997 -Dwebdriver.firefox.profile=Selenium"/>
</launchConfiguration>
@@ -54,9 +54,9 @@
The capabilities section can be used for fine tuning of the driver (e.g. version). For more informations see the
pages mentioned above.
-->
<!--
<!-- -->
<driver-definition class="org.openqa.selenium.firefox.FirefoxDriver">
-->
<!-- -->
<!--
<capabilities>
<capability>
@@ -65,11 +65,11 @@
</capability>
</capabilities>
-->
<!--
<!-- -->
</driver-definition>
-->
<!-- -->
<!-- -->
<!--
<driver-definition class="org.openqa.selenium.chrome.ChromeDriver" />
<!-- -->
-->
</test-environment>
@@ -85,8 +85,7 @@ public class ReplicationServiceImpl implements ReplicationService, OperationExec
public ReplicationServiceImpl(String exchangeName, String exchangeHost, final ReplicationInstancesManager replicationInstancesManager) throws IOException {
this.replicationInstancesManager = replicationInstancesManager;
replicaUUIDs = new HashMap<ReplicationMasterDescriptor, String>();
racingEventServiceTracker = new ServiceTracker<RacingEventService, RacingEventService>(
Activator.getDefaultContext(), RacingEventService.class.getName(), null);
racingEventServiceTracker = getRacingEventServiceTracker();
racingEventServiceTracker.open();
localService = null;
this.exchangeName = exchangeName;
@@ -95,6 +94,11 @@ public class ReplicationServiceImpl implements ReplicationService, OperationExec
serverUUID = UUID.randomUUID();
logger.info("Setting " + serverUUID.toString() + " as unique replication identifier.");
}
protected ServiceTracker<RacingEventService, RacingEventService> getRacingEventServiceTracker() {
return new ServiceTracker<RacingEventService, RacingEventService>(
Activator.getDefaultContext(), RacingEventService.class.getName(), null);
}
/**
* Like {@link #ReplicationServiceImpl(String, ReplicationInstancesManager)}, only that instead of using
@@ -163,7 +163,7 @@ public class RacingEventServiceImpl implements RacingEventService, RegattaListen
private final com.sap.sailing.domain.base.DomainFactory baseDomainFactory;
private final ExpeditionWindTrackerFactory windTrackerFactory;
private final ExpeditionWindTrackerFactory expeditionWindTrackerFactory;
/**
* Holds the {@link Event} objects for those event registered with this service. Note that there may be
@@ -274,7 +274,7 @@ public class RacingEventServiceImpl implements RacingEventService, RegattaListen
this.baseDomainFactory = baseDomainFactory;
this.domainObjectFactory = domainObjectFactory;
this.mongoObjectFactory = mongoObjectFactory;
windTrackerFactory = ExpeditionWindTrackerFactory.getInstance();
expeditionWindTrackerFactory = ExpeditionWindTrackerFactory.getInstance();
regattasByName = new ConcurrentHashMap<String, Regatta>();
eventsById = new ConcurrentHashMap<Serializable, Event>();
regattaTrackingCache = new ConcurrentHashMap<Regatta, DynamicTrackedRegatta>();
@@ -1334,12 +1334,12 @@ public class RacingEventServiceImpl implements RacingEventService, RegattaListen
@Override
public void startTrackingWind(Regatta regatta, RaceDefinition race, boolean correctByDeclination)
throws SocketException {
windTrackerFactory.createWindTracker(getOrCreateTrackedRegatta(regatta), race, correctByDeclination);
expeditionWindTrackerFactory.createWindTracker(getOrCreateTrackedRegatta(regatta), race, correctByDeclination);
}
@Override
public void stopTrackingWind(Regatta regatta, RaceDefinition race) throws SocketException, IOException {
WindTracker windTracker = windTrackerFactory.getExistingWindTracker(race);
WindTracker windTracker = expeditionWindTrackerFactory.getExistingWindTracker(race);
if (windTracker != null) {
windTracker.stop();
}
@@ -1350,7 +1350,7 @@ public class RacingEventServiceImpl implements RacingEventService, RegattaListen
List<Triple<Regatta, RaceDefinition, String>> result = new ArrayList<Triple<Regatta, RaceDefinition, String>>();
for (Regatta regatta : getAllRegattas()) {
for (RaceDefinition race : regatta.getAllRaces()) {
WindTracker windTracker = windTrackerFactory.getExistingWindTracker(race);
WindTracker windTracker = expeditionWindTrackerFactory.getExistingWindTracker(race);
if (windTracker != null) {
result.add(new Triple<Regatta, RaceDefinition, String>(regatta, race, windTracker.toString()));
}
@@ -1572,7 +1572,7 @@ public class RacingEventServiceImpl implements RacingEventService, RegattaListen
@Override
public void addExpeditionListener(ExpeditionListener listener, boolean validMessagesOnly) throws SocketException {
UDPExpeditionReceiver receiver = windTrackerFactory.getOrCreateWindReceiverOnDefaultPort();
UDPExpeditionReceiver receiver = expeditionWindTrackerFactory.getOrCreateWindReceiverOnDefaultPort();
receiver.addListener(listener, validMessagesOnly);
}
@@ -1580,7 +1580,7 @@ public class RacingEventServiceImpl implements RacingEventService, RegattaListen
public void removeExpeditionListener(ExpeditionListener listener) {
UDPExpeditionReceiver receiver;
try {
receiver = windTrackerFactory.getOrCreateWindReceiverOnDefaultPort();
receiver = expeditionWindTrackerFactory.getOrCreateWindReceiverOnDefaultPort();
receiver.removeListener(listener);
} catch (SocketException e) {
logger.info("Failed to remove expedition listener " + listener
+1 -1
View File
@@ -2,7 +2,7 @@
During our workshop of 2013-09-30, three major areas of development were identified as candidates for focus topics for the next few months. These are:
* [[Data quality improvement|wiki/planning/businesscases/DataQualityImprovement]]
* [[Data Consistency|wiki/planning/businesscases/DataConsistency]]
* [[UI-Renovation|wiki/planning/businesscases/UIRenovation]]
@@ -0,0 +1,102 @@
# Data Consistency
Author: Christopher Ronnewinkel
### Rationale
The SAP sailing solutions are a set of software applications that process data - live and recorded - about sailing regattas, in order to visualize sailing races, various linked measures, as well as, regatta results. In order to capture sailing races, information about the race course, the competitors, the boat positions, the weather conditions (especially wind and water current), mark passing times and race times are transmitted using various kinds of wireless communication and collected on servers as raw data. In further processing steps, this raw data is linked by data structures to allow for visualization, live ranking estimation, scoring calculation and calculation of aggregate measures like boat speed, velocity-made-good, distance-to-leader, and more.
Linkage of raw data for example includes mapping of GPS-tracker-IDs to mark-IDs and competitor-IDs, joining time-dependent data along UTC time line and handling differing sampling rates, as well as, transmission delays for different kinds of raw data.
In an ideal world, this linkage would always work correctly and result in an all-time consistent representation of the race in terms of visualization and derived measures. However, in the real world of public events, a complex mixture of causes typically leads to intermittent faulty, misaligned or even missing raw data which in turn influences derived measures negatively with regards to accuracy, consistency, or even their well-definedness.
The experience from previous sailing events has shown that the SAP sailing solutions are currently quite sensitive to such intermittent faults in raw data. This results for example in misleading live ranking estimation, wrong visualization of the advantage line, non-availability of live play or replay of races or obvious out-of-range values of derived aggregate measures.
The status quo to tackle these problems is to work and improve on the data measurement units (e.g. GPS-trackers, wind measurement units), improve the data transmission infrastructure (e.g. wide-range Wifi vs. mobile network GPRS), increase the stability of required WAN network connections to remote servers (e.g. reserve DSL-lines for sailing IT) and extend education & discipline of involved people (e.g. sailors should not accidentally power-off GPS-trackers, race committee officers should not rearrange GPS-trackers across race course marks). But even on events were all these requirements were seemingly fulfilled, intermittent faults in raw data and hence faults of SAP sailing solutions have occurred in the past.
### Purpose
In the "Data Consistency" project, we take the new approach to gain higher stability of SAP sailing solutions by introducing robustness against raw data faults right into the heart of data processing: data structures should be able to represent raw data faults and aggregate calculations should react moderately ensuring well-definedness and consistency of visualization and all data displayed.
### Features
* Start Phase
* Check correctness of start time based on several data sources: tracker, race committee, start detection
* Stabilize start detection algorithms
* Completeness Checks
* Considered levels: leg, race, regatta, regatta series
* Add completeness state "complete/incomplete" on all levels
* Refined calculation-behavior of measures dependent on completeness on all levels
* Refined display-behavior in leaderboards and comparison charts dependent on completeness on all levels
* Visualize completeness state
* Connected Checks
* Considered entities: GPS-track, mark-track, wind measurement
* Add connected state "connected/disconnected" on all entities
* Log state changes
* Visualize connected state
* Data Properties
* Visualize (or make accessible) data properties
* Sampling frequency of GPS-tracker (per competitor, on average)
* Number of active GPS-satellites
* Accuracies of measurements, especially GPS-positions and wind bearing & speed
* Outlier detection
* Considered entities: GPS-track, mark-track, wind measurement
* Add outlier state "in/out" on all entities
* Detect faulty jumps in GPS-tracks and mark-tracks by checking physical limits
* Detect faulty noise in wind measurements
* Correction of faulty raw data
* Focus: GPS-tracks, mark-tracks, wind measurements
* Apply interpolation and extrapolation where possible
* Robustness of Aggregate Calculations
* Remove outliers from calculation
* Remove incomplete entities from calculation (if possible, otherwise return "n/a")
* Adapt to missing data or changed sampling rate
### Description
The features listed above each address problems that have occurred frequently in the past while using the SAP sailing software during public events.
#### Start Phase
During the start phase of a race, it is a common problem to determine the actual start time of a race accurately. This is partly caused by time shifts of the various involved clocks, e.g. SAP server time, GPS time, tracking provider time, race committee time. Moreover, the manual start time input via the race committee app is subject to inaccuracies due to the user's reaction and software handling time. An automated start time detection based on GPS-tracks that takes into account other start time values shall lead to higher stability to avoid recording a race with a wrong start time.
#### Completeness Checks
There are various reasons why in terms of raw data a competitor does not finish a race, e.g. tracker lost power, tracker lost network, tracker lost satellites, SAP server lost connection to tracking provider server, competitor capsized, competitor decided to finish the race earlier. From the raw data most these cases cannot be distinguished. But based on comparisons of GPS-tracks, mark-tracks and times it can be determined whether the current leg has been finished. Based on the finished legs it can be decided whether the race has been regularly finished. And in the same manner, it can be derived whether regatta and regatta series have been regularly finished by a competitor.
The completeness state of legs, races, regattas and regatta series has to be taken into account for calculated scores and measures. Moreover, the completeness state has to be taken into account and visualized in leaderboards and comparison charts so that the user is able to easily understand that scores and measures may have been influenced by "incomplete" state.
#### Connected Checks
Since raw data is transmitted using various network technologies that are vulnerable to connection dropouts, it is of interest for data processing and also the user of SAP sailing software to know when a connection is or was lost. The connection to GPS-trackers can unfortunately not be tested with alive-pings but needs to be monitored based on average transmission rate and time-out threshold. The connected state of GPS-trackers and wind measurement units shall be logged and visualized in the application so that equipment failures as a reason for non-availability or changes of data and/or measures becomes apparent to the eye of the user.
#### Data Properties
The base properties of data like GPS sampling rate & number of active satellites are interesting to expert users and shall be accessible via the UI. Furthermore, accuracies of measurement shall be optionally visualized (or at least accessible via the UI) to avoid misinterpretations of the visualized races and belonging measures, e.g. the general accuracy of GPS positions is up to 20 meters which should be at least optionally be visualized especially when trying to clarify race protests of sailors.
#### Outlier detection
GPS-tracks sometimes contain intermittent jumps (caused by changes in the GPS satellite reception of the tracker and shadowing from other boats) that should be detected in order to ignore them because such jumps can drastically change the aggregate measures and lead to severely wrong data display. Similarly, wind measurements can suffer from very strong noise that should be detected to be able to display plausible wind information and construct a meaningful advantage line.
#### Correction of faulty raw data
For time series data, like GPS-tracks and wind measurements, detected faulty raw data may be corrected using interpolation or extrapolation. The essential ingredient is assuming a numerical model for boat movement or wind pattern which is calibrated by live raw data (or recorded raw data) and used to bridge detected intermittent raw data faults. The duration of time that may be bridged shall be limited by a time threshold above which equipment failures should be visualized, as described in paragraph "Connected Checks".
#### Robustness of Aggregate Calculations
Aggregate calculations depend on a set of raw data and are influenced by intermittent faulty raw data as determined by outlier state, as well as, incomplete raw data as determined by the completeness state. In order to do automatically identify constellations in which parts of the raw data due to faultiness or incompleteness should be ignored, all aggregate calculations have to incorporate outlier state and completeness state to compute plausible results also in special raw data constellations.
### Synergies
The data consistency features belong closely to the calculation procedures implemented in the sailing analytics server.
### Risks
Since this project targets at increasing the robustness of SAP sailing solutions, the overall risk of failures should be reduced as an outcome.
### Prototype
n/a
### Further Information
* [[Meeting Minutes, December 2, 2013|wiki/planning/businesscases/dataconsistency20131202]]
@@ -0,0 +1,68 @@
# Meeting Minutes, December 2, 2013
## Initial Brainstorming Meeting
### Live-Delay Analysis
* GPS per competitor
* wind for advantage line
### Outlier detection
* GPS tracks
* Wind
### Dropout interpolation
* GPS tracks
* Wind
### Connection loss
* GPS Track:
* introduce states "no data" / "no connection"
* has to be based on transmission rates
* no ping available
* Wind
### Data consistency measures
* Start Phase
* correctness of start time on start boat
* stabilize start detection algorithms / heuristic
* Completeness of Leg
* mark tracks & passings
* gps fixes
* wind measurements
* leg type detection
* Completeness of Race
* competitor tracks: gps, mark passings
* mark tracks: validation (compare marks & competitor tracks); recalculation of mark passings
* all competitor or topN competitors
* all marks or future (remaining) marks
* take into account #{common legs} of competitors in aggregate calculation
* Completeness of Regatta
* (all races complete) only in rare cases achievable
* cleansing of races, if possible
* include race states, e.g. DNF, into aggregate calculations, e.g. average speed
* take into account #{common races} of competitors in aggregate calculation
* Robustness of Aggregate Calculations
* on all levels: leg, race, regatta, regatta series
* aggregate calculation can be corrupted by filtered outliers, lost connection
* Quality based on Outliers and/or Dropouts
* GPS
* Wind
* Network Connectivity
* all required channels
### Master Data
* Competitors: names
* Sail numbers
* Boat classes: names
* Scoring schemes: score corrections
### Data properties to be visualized or accessible
* Sampling frequency for GPS
* Number of active GPS satellites
* Reflect accuracies of measurements somehow on the UI, e.g. tails along GPS tracks
### Extra-/Interpolation of GPS tracks
* bridge phase without connection to network or connection to GPS satellites
### Extra-/Interpolation for wind
* bridge phases without estimation or measurement
@@ -1,3 +0,0 @@
# Data quality improvement
## topic 1
+3
View File
@@ -56,6 +56,9 @@ The Smartphone Tracking would also benefit from the project as many small regatt
One risk is the lack of understanding the real world problems of handicap regattas. So we can't prove that the listed features address the right problem spaces. To minimize that risk we should attend some handicap regattas.
Another risk is the that we don't know how big the investment must be to reach a point where we can really change the way handicap regattas are presented.
## Estimation
It's not possible to do any estimation for the development right now as we don't know the set of features we need yet. We should rather investigate the world of ORC/IRC sailing by attending some events and work together with ORC representatives to come up with a first proposal.
## Information Sources
ORC: http://orc.org/<br/>
IRC: http://www.ircrating.org/
@@ -0,0 +1,68 @@
# Minutes, December 2, 2013
## Initial Brainstorming Meeting
### Live-Delay Analysis
* GPS per competitor
* wind for advantage line
### Outlier detection
* GPS tracks
* Wind
### Dropout interpolation
* GPS tracks
* Wind
### Connection loss
* GPS Track:
* introduce states "no data" / "no connection"
* has to be based on transmission rates
* no ping available
* Wind
### Data consistency measures
* Start Phase
* correctness of start time on start boat
* stabilize start detection algorithms / heuristic
* Completeness of Leg
* mark tracks & passings
* gps fixes
* wind measurements
* leg type detection
* Completeness of Race
* competitor tracks: gps, mark passings
* mark tracks: validation (compare marks & competitor tracks); recalculation of mark passings
* all competitor or topN competitors
* all marks or future (remaining) marks
* take into account #{common legs} of competitors in aggregate calculation
* Completeness of Regatta
* (all races complete) only in rare cases achievable
* cleansing of races, if possible
* include race states, e.g. DNF, into aggregate calculations, e.g. average speed
* take into account #{common races} of competitors in aggregate calculation
* Robustness of Aggregate Calculations
* on all levels: leg, race, regatta, regatta series
* aggregate calculation can be corrupted by filtered outliers, lost connection
* Quality based on Outliers and/or Dropouts
* GPS
* Wind
* Network Connectivity
* all required channels
### Master Data
* Competitors: names
* Sail numbers
* Boat classes: names
* Scoring schemes: score corrections
### Data properties to be visualized or accessible
* Sampling frequency for GPS
* Number of active GPS satellites
* Reflect accuracies of measurements somehow on the UI, e.g. tails along GPS tracks
### Extra-/Interpolation of GPS tracks
* bridge phase without connection to network or connection to GPS satellites
### Extra-/Interpolation for wind
* bridge phases without estimation or measurement
@@ -9,9 +9,9 @@ With this information the public can closely stay in touch with events and the r
## Purpose
Provide the race committee with a native mobile implementation, that not only accepts input that is then fed into the analytics server but also helps users doing their job better by providing useful information about course layout, wind, competitors and time. This app should work for a professional race committee but also for a sailor executing a regatta on a sunday afternoon.
Provide the race committee with a native mobile implementation, that not only accepts input that is then fed into the analytics server but also helps users doing their job better by providing useful information about course layout, wind, competitors and time. This app should work for a professional race committee but also for a sailor executing a regatta on a Sunday afternoon.
The app should exist in two version:
The app should exist in two versions:
- The first one is dedicated to professionals that run big to mid-sized regattas. This version would heavily rely on tracking and wind measurement devices and the Sailing Analytics server. It would require professional support.
@@ -51,11 +51,11 @@ For each race provide the user with **tools to edit**
## Description
As there currently exist an implementation that covers some of the features, the aim of this project is to re-design and re-implement some of the code of the prototype. This especially holds for the user interface that currently needs some revamp to be also accessible to the average user.
As there currently exists an implementation that covers some of the features, the aim of this project is to re-design and re-implement some of the code of the prototype. This especially holds for the user interface that currently needs some revamp to be also accessible to the average user.
This project can be divided into three major stages:
- The first one is to design a user interface that one the one hand please the eye and is tailored to a mobile touch device. This design should take into account all of the desired features and provide a mock-up like interface for both versions. It has been agreed that this step needs to be taken out by an external agency that has a mobile background, the skills and the experience. This step relies on a document that is yet to be built that describes the desired features and the clustering of these features with regards to the two versions. In addition to delivering this document, one or two members of our team need to plan for at least 4-6 one day workshops with the agency to make sure that they understand the domain.
- The first one is to design a user interface that on the one hand pleases the eye and is tailored to a mobile touch device. This design should take into account all of the desired features and provide a mock-up like interface for both versions. It has been agreed that this step needs to be taken out by an external agency that has a mobile background, the skills and the experience. This step relies on a document that is yet to be built that describes the desired features and the clustering of these features with regards to the two versions. In addition to delivering this document, one or two members of our team need to plan for at least 4-6 one day workshops with the agency to make sure that they understand the domain.
- The second step is about implementing and integrating the design with the Sailing Analytics server. This requires a major effort from the tech team and also team work between the external agency and our developers. Some work from the current prototype can be reused but a major part needs to be rewritten.
@@ -69,13 +69,13 @@ Providing the public with such an app could quickly raise synergies with other s
It is currently unclear how much interest can be expected by the sailor community for the public version of the app. In contrast to that the efforts and risks that need to be taken into account developing such an app are big. Not only that we can't judge how much work such a complex application would require from the external agency, but also that the risks deploying and supporting an app that is downloadable by everyone can't just be numbered.
In addition to that it is unknown if the feature set a first public version would have then, really would meet the expectations of the target group. In the worst case the community just won't use the app and that could damage the brand.
In addition to that it is unknown if the feature set that a first public version would have really would meet the expectations of the target group. In the worst case the community just won't use the app and that could damage the brand. Similarly, if the app is downloaded by hundreds or thousands of people, if it doesn't work properly or if we fail to support and maintain it adequately, harm to the brand may be caused.
The development of the professional version has the same risk factors as the public version regarding the development of the interface. As the feature set is known the risks of failure are much lower than for the public version.
## Prototype
A working prototype has been developed and has also been used quite successful in regattas all over the world.
A working prototype has been developed and has also been used quite successfully in regattas all over the world.
## Estimation
@@ -55,4 +55,21 @@ If the approach is generally successful, we'll end up in some substantial suppor
## Prototype
Some prototypical work has already been launched to investigate the technical challenges closer. See [[here|wiki/smartphone-tracking]] for the status of this work.
Some prototypical work has already been launched to investigate the technical challenges closer. See [[here|wiki/smartphone-tracking]] for the status of this work.
## Estimation
Scope:
* Features as listed above
* Android platform only
We assume that a professional smartphone tracking app should be built by an external agency according to our specifications and in conjunction with the [race committee app](http://wiki.sapsailing.com/wiki/planning/businesscases/racecommitteeapp). Efforts therefore are to be divided into those for the agency and those for our team. We assume that parts of the prototype can be used, particularly back-end components such as the replicating race log that is shared with the race committee app.
* Agency and Team: Problem description and architecture and domain knowledge transfer, three weeks
* Agency: UI design, two weeks
* Team: UI design sign-off and iterations, one week
* Agency: App implementation including an integration with the RCApp where necessary, eight weeks
* Team: back-end architecture support, three weeks
Summary: Agency 13 weeks, Team 7 weeks
+66 -9
View File
@@ -1,27 +1,84 @@
# UI-Renovation
After 2 years of functional growth of our SAP sailing analytics we get more and more feedback that our user interfaces are not meeting the user expectations anymore. This applies not only to external users of our solutions but also to colleagues presenting our solutions at sailing events.
# Rationale
After 2 years of functional growth of our SAP sailing analytics we get more and more feedback that our user interfaces are not meeting the user expectations anymore. This applies not only to external users of our solutions but also to colleagues presenting our solutions at sailing events. Furthermore we lack mobile support for our solutions seeing a growing usage of solutions from mobile devices at the same time.
## www.sapsailing.com
# Purpose
We want to improve the user experience of our sailing solutions by providing more user role focused user interfaces for the common devices used today.
# Topics
## Homepage (www.sapsailing.com)
### Description
We have developed a growing number of technical solutions for the sailing world, but we lack a common description of these solutions. Furthermore we get more and more external requests from interested sailing professionals to use our solutions and we would like to provide them a good entry point into our solution world. Therefore a renovation of our homepage describing our solutions is needed.
## Mobile support
### Features
- Renovate the www.sapsailing.com entry page
- Add descriptions for event and team solutions
- Add common social media elements
- Advertise live events or other interesting news
## Mobile support
### Description
Over the last year we have seen a huge increase in the mobile usage of our solutions. During the Kiel week 2013 around 50% of the website access came from mobile devices. As our solutions has been designed for the usage on laptops and PC's we urgently need to improve the user experience for tablet and smartphone devices.
### Features
- Add UI support for common smartphones (iPhone, Android phones, windows phones)
- Add UI support for common tablets (iPad, Android tablets, windows tablets?)
## Sailing Analytics
We started 2011 with a solution for sailing moderators. From there by extended the supported user roles step by step to sailing event visitors, sailors, trainers, race officers, VIP lounge stuff and so on. So far we tried to serve all user roles with the same UI, but as a result the UI is overloaded with too many functions. As we understand the needs of the different user roles much better now, we think that we should provide more focused UI's for specific user roles.
### Raceboard
## Raceboard
### Description
The raceboard is our 2D viewer to display tracked races on a map enriched with other sensor data (e.g. wind) and a lot of derived data (ranks, analytical data, etc.). Additionally we can also integrate media streams (video, audio). This feature rich viewer is recognized as complex and overloaded as it tries to be two viewers in one: one viewer for visitors following live races and another one for deep analysis of races for sailors and trainers.
By providing different viewers for the different scenarios we can focus more on the really required feature sets.
#### 1. Race viewer for specators -> to view
#### 2. Race viewer for professionals (sailors, trainers) -> to analyze
### Features of race viewer for spectators -> to watch
- Support full screen map with the leaderboard and other useful data shown as an overlay
- Simplification of all settings
- Full integration of audio and video streams
- Implementation of many small map improvements (see http://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=1546)
### Leaderboard
### Features of race viewer for professionals (sailors, trainers) -> to analyze
- Specialized views for predefined analysis scenarios (e.g. start analysis)
- Customizable viewer to integrate more data sources and data views on demand (portlet like)
## Leaderboard
### Description
A leaderboard is actually an overall ranking for the competitors of a regatta. We advanced the leaderboard by adding analytical data on different levels (leg, race, regatta) for each competitor to a rather generic competitor data table. This data table is very useful as the basis for further competitor analysis but lacks any help if you are looking for a specific problems (e.g. let's do a maneuver performance analysis).
### Features
- Simplification of the leaderboard for spectators
- Add predefined screens for common analytical use cases for professionals (see also 'raceboard')
### Event/Regatta structure navigation
tbd.
## Event/Regatta structure navigation
### Description
Sailing events can be quite complex in terms of their structure. An event can have multiple regattas, each regatta can consist of multiple rounds (e.g. qualification, finals) and even racing in such rounds can be splitted into separate fleets. Additionally events can be grouped to event series with a overall ranking like the Extreme Sailing Series. To build a good navigation structure which makes it easy for users to find their races of interest is still a challenge.
### Features
- Support event series as well as single events
- Support recurring events (e.g. Kieler Woche 2012, Kieler Woche 2013, etc.)
- Support events with multiple regattas
- Support regattas with rounds (e.g. qualification, finals, etc.)
- Find events and event series (search by name, boat class, location, etc.)
# Synergies
no idea...
# Risks
The hardest decision we have to take is whether we should go for native apps for mobile devices or if we can survive with a HTML5 based UI. Development of native apps is cost intensive as if have to support several platforms in parallel. Furthermore we lack the right knowledge for this task in our team. One way we could go is to try first how far we can go with HTML5 and only move to native apps when we can't achieve the expected user experience in any other way.<br/>
All UI-Renovation projects will have similar phases like "user interaction design", "prototype", "visual design", "implementation", "test" and so on. As we don't have all skills for all phases in our team we will need additional external resources.
# Prototype
We already started (together with the SAP AppHouse Heidelberg) to develop 3 clickable prototypes for different device types: Smartphone, Tablet and Desktop.<br/>
So far you can access the prototype for smartphone via:<br/>
- on your smartphone via: http://static.sapsailing.com/prototype/Smartphone/Home.html
- on the desktop via: http://static.sapsailing.com/prototype/Smartphone/start.html
# Estimation
This estimation is very rough as we have no experience with mobile design and implementation.
- Homepage (1 week design, 1 week implementation)
- Mobile Support (4-6 weeks design, 12 weeks implementation)
- Sailing Analytics
- Raceboard: Simple race viewer for spectators (UID concept to be defined by Anna's master thesis)
- Raceboard: Analytical race viewer for professionals (UID concept to be defined by Anna's master thesis)
- Event/Regatta structure navigation (2 weeks design, 3 weeks implementation)
+66 -14
View File
@@ -1,5 +1,9 @@
# RaceCommittee App (Administrator's User Guide)
[[_TOC_]]
## Introduction
This user guide will help you to configure all your RaceCommittee App devices. The guide is targeted at a multi-device, multi-course-area, multi-race-officer setting. So feel free to omit steps whenever you feel that you don't have to use every feature for your current regatta. It is reasonable to stick to the presented order of steps.
## Preparation of devices
@@ -8,17 +12,21 @@ The RaceCommittee App supports remote configuration via the AdminConsole interfa
<img src="/wiki/images/rcapp/admin_config_step_1.png" />
First you need to enter a device identifier. It is a good idea to label the device with the this identifier, to enable the identification of the device by an untrained user.
First you need to enter a device identifier. It is a good idea to label the device with this identifier, to enable the identification of the device by an untrained user.
<img src="/wiki/images/rcapp/admin_config_step_2.png" />
You need to assign this identifier to the target device. Click on **QR-Sync**, recheck the current identifier and ensure that the Server URL is the correct server the device should be talking to. When done with your modifications hit **Generate**.
You need to assign this identifier to the target device. Click on **QR-Sync**, recheck the current identifier and ensure that the Server URL is the correct server the device should be talking to.
<img src="/wiki/images/rcapp/admin_config_step_3.png" />
Fire up the RaceCommittee app on the target device. Go to the **settings view** by clicking on the options overflow button (top right corner) or your device's menu button. Select the preference category **General** and click on **Synchronize connection settings**. Scan the barcode shown in the AdminConsole (if you don't have a barcode app installed the RaceCommittee App should redirect you to the Google Play Store offering an app). The **Device Identifier** and **Webservice URL** preferences should reflect the changes made.
After the server URL has been updated the app will check the server whether it should auto-update itself or not. To ensure that your app is compatible with the server you should choose to install the APK provided by the server.
_Hint: if you don't have the app installed yet on the target device, use the QR-Code as an URL to download the app for the first time. This should work with any barcode scanner app._
<img src="/wiki/images/rcapp/admin_config_step_3a.png" /> <img src="/wiki/images/rcapp/admin_config_step_3b.png" />
After the server URL has been updated the app will check the server whether it should auto-update itself or not. To ensure that your app is compatible with the server you should choose to install the APK provided by the server. Follow the instructions on the screen and reopen the newly updated app. After the update you must re-synchronize your device with the server.
<img src="/wiki/images/rcapp/admin_config_step_4.png" />
@@ -30,7 +38,7 @@ Repeat until you've configured all your devices.
There are several preferences you can configure to control the overall functionality of the RaceCommittee App.
All these options can be configured either via remote configuration (see above) or on the device itself. The remote configuration is fetched on selecting an event in the event selection screen. If there is a configuration stored on the server for the target device, each configured option will overwrite the corresponding option in the device's local configuration. If you leave an option unconfigured in the AdminConsole it will not overwrite the corresponding option, the device will preserves its value. E.g. by leaving the 'Results recipient' option unchecked in the AdminConsole will preserve the recipient configuration currently stored on the device.
All these options can be configured either via remote configuration (see above) or on the device itself. The remote configuration is fetched on selecting an event in the event selection screen. If there is a configuration stored on the server for the target device, each configured option will override the corresponding option in the device's local configuration. If you leave an option unconfigured in the AdminConsole, the device will preserve its value. E.g. by leaving the 'Results recipient' option unchecked in the AdminConsole will preserve the recipient configuration currently stored on the device.
In the following we will have a look at some of these settings. Most of them are enabled for remote configuration.
@@ -41,22 +49,66 @@ In the following we will have a look at some of these settings. Most of them are
* **Managed course areas:** The user is able to logon to the configured course areas.
* **Results recipient:** When active, the race's results (photos) will be sent to this mail.
### Racing Procedure
* **Overwrite Racing Procedure**: Selecting this option will let the app preselect your overwrite racing procedure regardless of the regatta's configuration and the history of the race's procedures.
* Configuration of procedure: Each racing procedure has some options to be configured. Keep in mind that changing these options will have an effect on _new_ racing procedures (ongoing race with no change in their racing procedure will keep the old settings!)
### Course Designer
* **Overwrite Course Designer**: See overwrite of racing procedure.
* Configuration of designers: Each designer has some options to be configured.
### Regatta Configuration (default)
* **Default racing procedure:** the app will preselect the chosen racing procedure for all races, whose specific regatta configuration (see below) doesn't override this.
* **Default course designer:** the app will preselect the chosen course designer for all races, whose specific regatta configuration (see below) doesn't override this.
* Configuration of procedure: Each racing procedure has some options to be configured. An option value applies to all races, whose specific regatta configuration doesn't have this option set.
<img src="/wiki/images/rcapp/admin_config_regatta_configuration.png" />
As you can see you are able to select for which racing procedure type you wish to create a configuration. If you don't activate a racing procedure's configuration the device's local configuration values will be preserved.
Keep in mind that changing these options in the app will only have an effect on _new_ racing procedures (ongoing race with no change in their racing procedure will keep the old settings).
## Configuration of regattas
For races attached to a regatta (i.e. RegattaLeaderboard) you are able to configure certain options _per-regatta_. The regatta's configuration is fetched by the app together with the race information on logon.
In the **Regatta** tab of the AdminConsole select click on the **Edit **action of the regatta to be configured. You are able to configure the following options:
In the **Regatta** tab of the AdminConsole click the **Edit** action of the regatta to be configured. If you check **Set regatta configuration** this configuration will override any device configuration for races of this regatta.
* **Regatta's default racing procedure:** the app will preselect the chosen racing procedure for all races of this regatta. _This option will have no effect when **Overwrite Racing Procedure** is set in the device's configuration_.
* **Regatta's default course designer:** the app will preselect the chosen course designer for all races of this regatta. This option will have no effect when **Overwrite Course Designer** is set in the device's configuration.
* **Set racing procedure configuration:** When checked enables you to configure the default racing procedure of this regatta. _This option will have no effect when **Overwrite racing procedure configuration** is set for the device's **remote** configuration_.
* **Regatta's default racing procedure:** the app will preselect the chosen racing procedure for all races of this regatta. _This option will override the device's configuration_.
* **Regatta's default course designer:** the app will preselect the chosen course designer for all races of this regatta. This option will override the device's configuration.
* **Set racing procedure configuration:** When checked enables you to configure the default racing procedure of this regatta. Setting regatta-specific preferences here will override device-global preferences.
<img src="/wiki/images/rcapp/admin_config_edit_regatta.png" />
## On-Device Configuration
As stated above it is possible to (re-)configure these preferences in the app. Changes on the device will not be synchronized with the server.
## Troubleshooting
Let's have a look at the most common problems.
### Connectivity
In the top right corner three dots indicate the connectivity status. If there is a problem with sending or receiving data to/from the server the dots will turn red. The device will re-try to send data every minute.
![Connectivity ok](images/rcapp/app_conn_ok.jpg) **versus** ![Connectivity bad](images/rcapp/app_conn_bad.jpg)
If your device users experience persistent connectivity issues, advise them to click on the three red dots. This will display further information including the last successful sent time. Recheck that the device has an overall network connectivity and check if your server is not accepting the data sent by the device. This might be the case when the RaceCommittee App is trying to send data for already deleted/renamed regattas.
![Expert view](images/rcapp/app_expert.jpg)
Selecting the "Expert information" option you'll have access to the expert information screen. This screen gives you the chance to have a look at all the data which could not be sent. Additionally you stop the device from trying to send this data by clicking on "Clear Events". Be sure to re-logon onto the course area or 'Reload' all data!
### Failure to load data
When trying to logon onto a course area the RaceCommittee App opens an error dialog showing an error similar to "Failure to load data: 'null'" or something more expressive. Most of the times this happens when the current version of the app is not compatible with the server. Redo the synchronization step or even force an update via the app's preference screen.
If this doesn't help have a log in the runtime-log (see below).
### Runtime- and Crash-Logs
For further fault localization the app maintains runtime-logs. They are stored on the (emulated) SD-Storage partition under
/sd/racecommittee/DATE.log
In the case of a crash due to a unchecked exception to app writes to a crash log located in
/sd/racecommittee/DATE_crash.log
+183 -1
View File
@@ -1,3 +1,185 @@
# RaceCommittee App (User Guide)
Start the app by clicking on the icon.
[[_TOC_]]
## Introduction
This user guide is intended for race officers and other users of the RaceCommittee App. The document assumes that you have been handed a fully configured device (see [[guide for device administrators|wiki/racecommittee-app-administrator]]).
## Logon
Each race officer is in charge of managing the races of a course area (Alpha, Bravo, Off-Shore,...). After selecting the active event on the left pane, click on your course area.
![Logon onto course area](/wiki/images/rcapp/app_login.jpg)
In the dialog screen choose _"Login as race officer (start vessel)"_. Press **Login**. From now on you will see in the lower right corner the RaceCommittee App icon signaling that you are managing this course area. Using this notification icon you are always able to return back to your race management screen.
![The notification icon](/wiki/images/rcapp/app_notification_icon.jpg)
## Race management
On the race management screen you see all races grouped by their regatta and fleet on the left side. By default the race list is filtered showing only active races. Use the top action bar to switch to "show all races" if you need to see all.
![Race management screen](/wiki/images/rcapp/app_running.jpg)
Clicking on a race in the race list opens up the race information screen on the right pane. This screen allows you to umpire a race.
Clicking the back button on your device or touching the home icon (top left corner) logs you out of the active course area.
### Scheduling a race
One of the most important task in the life of a race officer is scheduling her races. Whenever you select a **unschedulded race** the race information screen asks you to select a racing procedure and a start time. By default a start time in the near future is selected.
![Scheduling a race](/wiki/images/rcapp/app_schedule.jpg)
When you are satisfied with the start time selected (hint: click on 'Today' to select a different date) and racing procedure, click **'Start'**. As you will see below you are always able to reschedule a race. Therefore the best race officers schedule multiple races at once. After scheduling the first race of the day, just select the next race in the race list and set an estimated start time.
On the same race information screen you are able to **postpone** an unschedulded race. Click on 'Postpone' and select the postponment flags.
#### Scheduling a race in the past
It is perfectly fine to schedule a race into the past. The RaceCommittee App will simulate the race and its startphase up until the present time. When scheduling a race in the past it might happen that the app asks you for additional information regarding the startphase of the selected racing procedure (e.g. setting a startmode flag).
### Managing the startphase
Selecting a scheduled race will take you to the startphase race information screen. On this screen you are able to manage the startphase of the race's racing procedure. While the available actions vary between the racing procedures you always see the following:
* Countdown to start
* Countdown to next flag change
* Currently displayed and removed flags
![Startphase](/wiki/images/rcapp/app_startphase.jpg)
Regardless of the racing procedure you're able to **abort** (AP flag) and **reschedule** the race.
### Managing an on-going race
While a race is on-going the race information screen shows the running race information screen.
You are still able to **abort** the race (AP or November flag) and signal a **general recall**. Both actions will unschedule the race, taking you back to the schedule race screen.
![A running race](/wiki/images/rcapp/app_running.jpg)
If active for your configuration/regatta you are able to signal an **individual recall** (X-Ray flag). Currently you're not able to enter the competitors violating the starting line. The X-Ray flag will be removed after 4 minutes automatically. You can always remove it before the timer terminates by clicking on "individual recall" again.
When the race is about to finish (finishing vessel displays blue flag, first finisher crosses the line, last leg,...) click the blue flag to **start the finishing phase**.
### Finishing a race
While a race is finishing the race information screen will show you the finishing phase.
On this screen you're able to **abort** (November flag) the race or **end** the race. To end the race click on the blue flag.
### Further management activities
Besides the normal race management workflow described above there are additional management activities which can be executed on a race at any time. See also [Signaling a course change](#coursechange).
#### Protest time
If active for your configuration/regatta you can click the **bravo flag** on a fleet's header in the race list to signal the start of the protest time for this fleet.
![Setting the protest time](/wiki/images/rcapp/app_protesttime.jpg)
In the dialog window select all races of the fleet whose protest time should be set. By default all already finished races are selected.
#### Wind
In the top bar of each race information screen you are able to enter wind data. Click on the yellow link text 'set wind' to open the wind input dialog
![Wind input dialog](/wiki/images/rcapp/app_wind.jpg)
Using the GPS receiver of your device the dialog will determine your position. Enter the wind direction and speed and click 'store'.
## Racing Procedures
<a name="procedures"></a>
Every time you start a race (e.g. schedule a race for the first time, restarting a race after general recall, ...) you are able to choose the **racing procedure**. The racing procedure defines how the startphase is set up. This may include signaling flags that are triggered at a fixed time and signals you have to choose from.
### RRS26
The RRS26 racing procedure should be used for races using the _RRS26 starting sequence_. This sequence is defined as the following:
1. **5 minutes** before start the warning signal is set. Class flag (as configured) is displayed.
2. **4 minutes** before start the preparatory signal is set. By default you are able to choose between the following flags: Papa, India, Zulu, Black to be displayed.
3. **1 minute** before start the one minute warning is set. The preparatory flag is removed.
4. **At start** the class flag is removed.
Using the default configuration for the RRS26 racing procedure you are able to signal individual recalls (expect when you've chosen Black as the preparatory signal).
### Gate Start
The Gate Start racing procedure should be used for races using a gate (or rabbit or gate launch) start. We've seen some variations of the implementation of a gate start, therefore you should double check with the administration team that the Gate Start racing procedure is correctly configured fitting your regatta's sailing instructions.
1. At **any time** you are able to configure the following:
* **Pathfinder:** If enabled for your regatta, you are able to set the pathfinder. You have to enter the pathfinder's sailing identifier.
* **Gate launch stop time:** Before the race starts, you have to set the gate launch stop time. This time determines how many minutes after the start the gate launch will stop.
* **Golf down time:** If enabled for your regatta, you have to set the additional golf down time. This time determines how many minutes _after the gate launch has stopped_, the starting line will be closed. If your regatta is configured to not use an additional golf down time, the starting line will close after the gate launch has stopped.
2. **8 minutes** before start the warning signal is set. Class flag (as configured) over Golf is displayed.
3. **4 minutes** before start the preparatory signal is set. Papa flag is displayed.
4. **1 minute** before start the one minute warning is set. Papa flag is removed.
5. **At start** Class flag is removed.
6. After the **gate launch stop** time has expired the golf flag is hoisted to half-mast. The gate launch stops. This step is not yet visualized in the app.
7. After the additional **golf down** time has expired the golf is removed. The starting line is closed. If you're regatta is configured to not use the additional golf down time, the starting line will close after the gate launch stop time is expired.
By default you are not able to signal individual recalls for the Gate Start racing procedure.
### ESS Start
The ESS racing procedure models the starting sequence used at the Extreme Sailing Series.
1. **4 minutes** before start AP flag is removed.
2. **3 minutes** before start a flag indicating a 3 is displayed.
3. **2 minutes** before start a flag indicating a 2 is displayed. The previous flag is removed.
3. **1 minutes** before start a flag indicating a 1 is displayed. The previous flag is removed.
4. **At start** the one minute flag is removed.
By default you are able to signal individual recalls for the ESS racing procedure.
Additionally you are able to enter the **finishing list** on the race finishing information screen. Just drag and drop the available sailors from the right to their correct position on the left. You are able to reorder them as you need. **Be sure to confirm your finishing list after the race is finished**. Click on the 'Confirm Competitors* buttons after you have finished the race.
### Basic Countdown Start
Use the Basic Countdown racing procedure whenever all the other procedures do not match your needs. There are no flags used in the starting sequence, it is just a countdown to the scheduled starting time.
## Signaling a course change
<a name="coursechange"></a>
There are different ways to signal a course change. Your device has been configured by the [[administration team|wiki/racecommittee-app-administrator]] to select the best method for you.
### By-Name
Simple course name selection screen. The available course names are pre-configured by the administration team.
### By-Marks
Design the course by defining marks and gates from existing assets provided by the server. You can choose to reuse the last published course design when no course change has occurred.
### By-Map
Lorem ipsum.
## Troubleshooting
The following sections will help you with some common problems.
### Race reset
Whenever a managed race is in a wrong state or you want to redo the race, click 'Reset race' in the race information's top bar. Please read the warning displayed.
### Connectivity problems
In the top right corner three dots indicate the connectivity status. If there is a problem with sending or receiving data to/from the server the dots will turn red. Don't panic: since you probably aren't sitting in the range of a Starbuck's WiFi this will happen **often**. Just wait some time and put the device somewhere you expect celluar network coverage. The device will re-try to send data every minute.
![Connectivity ok](/wiki/images/rcapp/app_conn_ok.jpg) **versus * ![Connectivity bad](/wiki/images/rcapp/app_conn_bad.jpg)
If the dots stay red contact the administration team or see the [[administration wiki|wiki/racecommittee-app-administrator]].
### Reload all data
If the available course areas/regattas/races on your device aren't up to date anymore, e.g. you are missing a regatta, you can try to reload all data from the server. Click your devices menu button or select the options overflow icon (top right corner) and click 'Reload'. This will log you out of your course area.
### Crashes
If the app crashes it will try to restart. You'll have to re-logon onto your course area. The previous state of your managed race will be restored, including unsent changes and data.
If the app crashes over and over again contact the [[administration team|wiki/racecommittee-app-administrator]].
+38 -8
View File
@@ -4,18 +4,12 @@
## Introduction
The RaceCommittee App is an Android 3.2 Tablet application.
The RaceCommittee App is an Android 3.2 Tablet application. This document serves as the main information hub about the App and how to develop for it.
* See the [[OnBoarding Information|wiki/onboarding]] on how to setup your local environment for building the app
* See [[Mobile Development|wiki/mobile-development]] for general tipps on mobile development.
* See [[Server Environment|wiki/racecommittee-app-environment]] on how the server's build environment is configured for building the RaceCommittee App.
## Features
A Feature List collected in September 2013 at the Testevent in Santander can be seen here:
[Feature List RaceCommittee App](/doc/RaceCommittee Feature List.xlsx) (RaceCommittee Feature List, Sep. 2013, Julian Gimbel)
The Feedback a User gave to the Application and the Feedback the user gave to the Applikation and Hardware of SwissTiming can be seen here:
[Feature List RaceCommittee App](/doc/Swiss Timing Race Committee App.docx) (Swiss Timing App and Feedback for SAP RaceCommittee App, Sep. 2013, Julian Gimbel)
## User Guide
Have a look at the following user guides to get an idea how to work with the RaceCommittee App.
@@ -23,13 +17,49 @@ Have a look at the following user guides to get an idea how to work with the Rac
* [[RaceCommittee App as an administrator|wiki/racecommittee-app-administrator]]
* [[RaceCommittee App as a race officer|wiki/racecommittee-app-user]]
## Features
A Feature List collected in September 2013 at the Testevent in Santander can be seen here:
[Feature List RaceCommittee App](/doc/RaceCommittee Feature List.xlsx) (RaceCommittee Feature List, Sep. 2013, Julian Gimbel)
The Feedback a User gave to the Application and the Feedback the user gave to the Applikation and Hardware of SwissTiming can be seen here:
[Feature List RaceCommittee App](/doc/Swiss Timing Race Committee App.docx) (Swiss Timing App and Feedback for SAP RaceCommittee App, Sep. 2013, Julian Gimbel)
## Course Updates
Kuruh, Kuruh,...
## RaceState
intro-text zu racestate, was ist das, wo kann ich es nutzen
### Adding a new UI
text
### Adding a new racing procedure
configuration!
## Configuration
intro-text verweis zu admin-guide
### Adding a new configuration option
DeviceConfiguration vs RegattaConfiguration
merge
PreferenceScreen and AppPreferences
## RaceLog priorities and authors
Bruuuum.
## Versioning
Baruh
## Build and Auto-Update
On Maven builds the resulting APK of the RaceCommittee App will be made available as a static content on the server's web page.
On Maven builds the resulting APK of the RaceCommittee App will be made available as static content on the server's web page.
The RaceCommittee App is set up as an optional dependency of the bundle **com.sap.sailing.www**. This way the app will be build before the www-bundle. After the install phase the RaceCommittee App bundle will copy its artifact APK into _com.sap.sailing.www/apps_. The contents of this folder are packaged into the **com.sap.sailing.www** plugin, which will be deployed as the server's web page. When build with _buildAndUpdateProduct.sh_ an additional version information file is stored alongside the APK. Version information is taken from the AndroidManifest.xml (**android:versionCode**).
+16 -4
View File
@@ -4,13 +4,25 @@
## Quick Start: How to run tests locally in Eclipse
There are two ways to run the Selenium tests locally on your computer. Either, you compile the GWT UI using our build script and run the tests based on the compiled UI, or you run the tests using the GWT hosted mode.
### Firefox Prerequisites
You have to ensure that your Firefox browser has a profile called "Selenium" and that in this profile the latest version of the GWT plugin is installed. To ensure this, launch the server by choosing the "Sailing Server (Proxy)" or "Sailing Server (No Proxy)" launch config. Then, run the "SailingGWT" launch to start the GWT UI in hosted / development mode. Afterwards you can launch Firefox from the command line with the -p option. On Windows machines, you can do this by pressing the Windows key, then typing "firefox.exe -p". In the profile manager create a profile called Selenium and start Firefow with that profile. Hit the entry page of the AdminConsole by entering `http://127.0.0.1:8888/gwt/AdminConsole.html?gwt.codesvr=127.0.0.1:9997` into the address bar. This will ask you to install the GWT plugin into your Selenium profile. When done, exit the browser. You may use the profile manager again to set your default profile to your original profile.
### Running the tests with GWT hosted mode
Launch the server by choosing the "Sailing Server (Proxy)" or "Sailing Server (No Proxy)" launch config. Then, run the "SailingGWT" launch to start the GWT UI in hosted / development mode.
When the GWT development mode has finished its initialization as indicated by the "Development Mode" view showing the entry point URLs, launch the "com.sap.sailing.senelium.test (Proxy, GWT Codesvr)" or "com.sap.sailing.senelium.test (No Proxy, GWT Codesvr)" launch. This will then pop up Firefox windows using the "Selenium" profile and run the tests.
### Running the tests after a successful local GWT compile
Stop any SailingServers currently running on your local machine (if you fail to do so, the Maven build will be unable to write some of the build artifacts in the _com.sap.sailing.gwt.ui_ project). Build using the git-managed build script from _configuration/buildAndUpdateProduct.sh_. To lower round-trip times, we introduced the _-b_ option that only builds one GWT permuation (Chrome, English). You may use the _-t_ option to keep the Maven build from running the tests. If inside the SAP VPN, additionally use the _-p_ option to ensure the HTTP proxy is being used. This makes for a command line like this:
`./configuration/buildAndUpdateProduct.sh -p -b -t build`
After the build has completed (hopefully successfully), refresh the `com.sap.sailing.gwt.ui` project in your Eclipse workspace. Make sure you have the ChromeSeleniumDriver installed. You can download it for your platform from [here](http://chromedriver.storage.googleapis.com/index.html). On Windows, you need to install it to `c:\apps\ChromeSeleniumDriver\chromedriver.exe`.
Start the SailingServer launch configuration (with / without proxy, depending on your VPN state). Then, launch the JUnit launch configuration _com.sap.sailing.selenium.test_ (with / without proxy). It will run the Selenium tests against your local server, running on port 8888.
After the build has completed (hopefully successfully), refresh the `com.sap.sailing.gwt.ui` project in your Eclipse workspace. Start the SailingServer launch configuration (with / without proxy, depending on your VPN state). Then, launch the JUnit launch configuration _com.sap.sailing.selenium.test_ (with / without proxy). It will run the Selenium tests against your local server, running on port 8888. Make sure you have a "Selenium" firefox profile installed, see above.
## General
@@ -91,4 +103,4 @@ After the build, Tycho will start a server instance and runs all tests against t
Since a full Maven build needs some time, you can also execute the tests in the Eclipse IDE. Here you have to start a server manually via an appropriated launch configuration (e.g. _SailingServer (Proxy, Jetty on 8889)_). From there you can either run all UI-Tests with the JUnit launch configuration _com.sap.sailing.selenium.test_ (with/without proxy) which expects the configuration file under the name _local-test-environment.xml_ or you can run a single test by selecting _Run As -> JUnit Test_ for your test class and specifying the configuration file in the _VM Arguments_ section of the run configuration.
For a more practical example of how to write page objects and test you should take a look at the [[tutorial|wiki/ui-tests-tutorial]].
For a more practical example of how to write page objects and test you should take a look at the [[tutorial|wiki/ui-tests-tutorial]].