Merge remote-tracking branch 'external/master' into jens--media-synch

umn from SettableScoreCorrection to ScoreCorrection
This commit is contained in:
Jens Rommel committed 2013-04-10 10:08:08 +02:00
commit 6eea85c5a3
360 files changed
+37324 -1235

No files matched your search

+7
View File
@@ -274,6 +274,13 @@ if [[ "$@" == "install" ]] || [[ "$@" == "all" ]]; then
mkdir $ACDIR/configuration
fi
# seems that initial scripts not there
if [ ! -f "$ACDIR/start" ]; then
cp -v $PROJECT_HOME/java/target/start $ACDIR
cp -v $PROJECT_HOME/java/target/http2udpmirror $ACDIR
cp -v $PROJECT_HOME/java/target/configuration/logging.properties $ACDIR/configuration
fi
cd $ACDIR
rm -rf $ACDIR/plugins/*.*
+1 -2
View File
@@ -1,7 +1,6 @@
port = 27017
master = true
dbpath = /opt/mongodb/data//mongodb-data
logpath = /opt/mongodb/data/logs/mongodb-data/mongodb.log
dbpath = /opt/mongodb/data/mongodb-data
logappend = true
directoryperdb = true
rest = true
File diff suppressed because it is too large. Load diff
Binary file not shown.
+7 -1
View File
@@ -8,13 +8,19 @@ Like businesses, sailors need the latest information to make strategic decisions
### Table of Contents
* [[How to work with this wiki|wiki/howto]]
* [[Information about this Wiki and HowTo|wiki/howto]]
* [[General Project Information|wiki/general-information]]
* [[OnBoarding Information|wiki/onboarding]]
* [[Architecture and Infrastructure|wiki/architecture-and-infrastructure]]
* [[Production Environment|wiki/production-environment]]
* [[Development Environment|wiki/development-environment]]
* [[Server Replication|wiki/server-replication]]
* [[Configure Races on Server|wiki/configure-races-on-server]]
* [[Cook Book|wiki/cook-book]]
* [[Planning|wiki/planning]]
* Event Specifica
* [[General Planning|wiki/general-event-planning]]
* [[Extreme Sailing Series|wiki/extreme-sailing-series]]
### Internal services (not related to wiki but useful)
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/com.sap.sailing.declination/src/com/sap/sailing/declination/impl/DeclinationStore.java"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="1"/>
</listAttribute>
<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.sap.sailing.declination.impl.DeclinationStore"/>
<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="2013 2013 1"/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="com.sap.sailing.declination"/>
</launchConfiguration>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/com.sap.sailing.declination/src/com/sap/sailing/declination/impl/DeclinationStore.java"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="1"/>
</listAttribute>
<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.sap.sailing.declination.impl.DeclinationStore"/>
<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="2012 2012 1"/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="com.sap.sailing.declination"/>
<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Dhttp.proxyHost=proxy.wdf.sap.corp -Dhttp.proxyPort=8080"/>
</launchConfiguration>
@@ -4,4 +4,8 @@ public interface LeaderboardNameConstants {
static final String DEFAULT_LEADERBOARD_NAME = "Default Leaderboard";
static final String OVERALL = "Overall";
static final String DEFAULT_FLEET_NAME = "Default";
static final String DEFAULT_SERIES_NAME = "Default";
}
@@ -14,7 +14,7 @@ import com.sap.sailing.domain.common.impl.Util.Pair;
*/
public interface ScoreCorrectionProvider extends Named {
/**
* Returns a valid map whose keys are event names such as "Kieler Woche 2011" or "IDM Travemünde 2011" and whose
* Returns a valid map whose keys are event names such as "Kieler Woche 2011" or "IDM Travemuende 2011" and whose
* values are sets of pairs of boat class names and the times for which it has score corrections. If there are multiple
* score corrections taken at different times. Later score corrections are expected to be cumulative, meaning they
* also contain all previous corrections.
@@ -1,5 +1,6 @@
package com.sap.sailing.domain.common;
public enum ScoringSchemeType {
LOW_POINT, HIGH_POINT, HIGH_POINT_ESS_OVERALL, HIGH_POINT_LAST_BREAKS_TIE, HIGH_POINT_FIRST_GETS_TEN
LOW_POINT, HIGH_POINT, HIGH_POINT_ESS_OVERALL, HIGH_POINT_LAST_BREAKS_TIE,
HIGH_POINT_FIRST_GETS_TEN, LOW_POINT_WINNER_GETS_ZERO
}
@@ -0,0 +1,8 @@
package com.sap.sailing.domain.common;
public interface TimingConstants {
/**
* The time after the end of a race during which it is still considered and displayed as "live"
*/
final long IS_LIVE_GRACE_PERIOD_IN_MILLIS = 3 * 60 * 1000; // three minutes
}
@@ -64,4 +64,9 @@ public class CountryCodeImpl implements CountryCode {
public String getITUCallPrefix() {
return ituCallPrefix;
}
@Override
public String toString() {
return getThreeLetterIOCCode();
}
}
@@ -161,7 +161,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
} catch (Exception e) {
// something went wrong during DB access; report, then use empty new wind track
logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboard "+name+".");
logger.throwing(DomainObjectFactoryImpl.class.getName(), "loadLeaderboard", e);
logger.log(Level.SEVERE, "loadLeaderboard", e);
}
return result;
}
@@ -180,7 +180,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
} catch (Exception e) {
// something went wrong during DB access; report, then use empty new wind track
logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboards.");
logger.throwing(DomainObjectFactoryImpl.class.getName(), "getAllLeaderboards", e);
logger.log(Level.SEVERE, "getAllLeaderboards", e);
}
return result;
}
@@ -341,58 +341,61 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
private FlexibleLeaderboard loadFlexibleLeaderboard(DBObject dbLeaderboard,
SettableScoreCorrection scoreCorrection, ThresholdBasedResultDiscardingRule resultDiscardingRule) {
final ScoringScheme scoringScheme = loadScoringScheme(dbLeaderboard);
RaceLogStore raceLogStore = MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(
new MongoObjectFactoryImpl(database),
this);
CourseArea courseArea = loadCourseAreaFromEvents(dbLeaderboard);
FlexibleLeaderboardImpl result = new FlexibleLeaderboardImpl(
raceLogStore,
(String) dbLeaderboard.get(FieldNames.LEADERBOARD_NAME.name()),
scoreCorrection,
resultDiscardingRule,
scoringScheme,
courseArea);
final FlexibleLeaderboardImpl result;
BasicDBList dbRaceColumns = (BasicDBList) dbLeaderboard.get(FieldNames.LEADERBOARD_COLUMNS.name());
// For a FlexibleLeaderboard, fleets are owned by the leaderboard's RaceColumn objects. We need to manage them here:
Map<String, Fleet> fleetsByName = new HashMap<String, Fleet>();
for (Object dbRaceColumnAsObject : dbRaceColumns) {
BasicDBObject dbRaceColumn = (BasicDBObject) dbRaceColumnAsObject;
Map<String, RaceIdentifier> raceIdentifiers = loadRaceIdentifiers(dbRaceColumn);
RaceIdentifier defaultFleetRaceIdentifier = raceIdentifiers.get(null);
if (defaultFleetRaceIdentifier != null) {
Fleet defaultFleet = result.getFleet(null);
if (defaultFleet != null) {
raceIdentifiers.put(defaultFleet.getName(), defaultFleetRaceIdentifier);
} else {
// leaderboard has no default fleet; don't know what to do with default RaceIdentifier
logger.warning("Discarding RaceIdentifier "+defaultFleetRaceIdentifier+" for default fleet for leaderboard "+result.getName()+
" because no default fleet was found in leaderboard");
if (dbRaceColumns == null) {
// this was probably an orphaned overall leaderboard
logger.warning("Probably found orphan overall leaderboard named "
+ dbLeaderboard.get(FieldNames.LEADERBOARD_NAME.name())+". Ignoring.");
result = null;
} else {
final ScoringScheme scoringScheme = loadScoringScheme(dbLeaderboard);
RaceLogStore raceLogStore = MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(
new MongoObjectFactoryImpl(database), this);
CourseArea courseArea = loadCourseAreaFromEvents(dbLeaderboard);
result = new FlexibleLeaderboardImpl(raceLogStore, (String) dbLeaderboard.get(FieldNames.LEADERBOARD_NAME
.name()), scoreCorrection, resultDiscardingRule, scoringScheme, courseArea);
// For a FlexibleLeaderboard, fleets are owned by the leaderboard's RaceColumn objects. We need to manage
// them here:
Map<String, Fleet> fleetsByName = new HashMap<String, Fleet>();
for (Object dbRaceColumnAsObject : dbRaceColumns) {
BasicDBObject dbRaceColumn = (BasicDBObject) dbRaceColumnAsObject;
Map<String, RaceIdentifier> raceIdentifiers = loadRaceIdentifiers(dbRaceColumn);
RaceIdentifier defaultFleetRaceIdentifier = raceIdentifiers.get(null);
if (defaultFleetRaceIdentifier != null) {
Fleet defaultFleet = result.getFleet(null);
if (defaultFleet != null) {
raceIdentifiers.put(defaultFleet.getName(), defaultFleetRaceIdentifier);
} else {
// leaderboard has no default fleet; don't know what to do with default RaceIdentifier
logger.warning("Discarding RaceIdentifier " + defaultFleetRaceIdentifier
+ " for default fleet for leaderboard " + result.getName()
+ " because no default fleet was found in leaderboard");
}
raceIdentifiers.remove(null);
}
raceIdentifiers.remove(null);
}
List<Fleet> fleets = new ArrayList<Fleet>();
for (String fleetName : raceIdentifiers.keySet()) {
Fleet fleet = fleetsByName.get(fleetName);
if (fleet == null) {
fleet = new FleetImpl(fleetName);
fleetsByName.put(fleetName, fleet);
List<Fleet> fleets = new ArrayList<Fleet>();
for (String fleetName : raceIdentifiers.keySet()) {
Fleet fleet = fleetsByName.get(fleetName);
if (fleet == null) {
fleet = new FleetImpl(fleetName);
fleetsByName.put(fleetName, fleet);
}
fleets.add(fleet);
}
fleets.add(fleet);
}
if (fleets.isEmpty()) {
fleets.add(result.getFleet(null));
}
String columnName = (String) dbRaceColumn.get(FieldNames.LEADERBOARD_COLUMN_NAME.name());
if (fleets.isEmpty()) {
fleets.add(result.getFleet(null));
}
String columnName = (String) dbRaceColumn.get(FieldNames.LEADERBOARD_COLUMN_NAME.name());
RaceColumn raceColumn = result.addRaceColumn(columnName,
(Boolean) dbRaceColumn.get(FieldNames.LEADERBOARD_IS_MEDAL_RACE_COLUMN.name()),
fleets.toArray(new Fleet[0]));
for (Map.Entry<String, RaceIdentifier> e : raceIdentifiers.entrySet()) {
raceColumn.setRaceIdentifier(fleetsByName.get(e.getKey()), e.getValue());
}
RaceColumn raceColumn = result.addRaceColumn(columnName,
(Boolean) dbRaceColumn.get(FieldNames.LEADERBOARD_IS_MEDAL_RACE_COLUMN.name()),
fleets.toArray(new Fleet[0]));
for (Map.Entry<String, RaceIdentifier> e : raceIdentifiers.entrySet()) {
raceColumn.setRaceIdentifier(fleetsByName.get(e.getKey()), e.getValue());
}
}
}
return result;
}
@@ -539,7 +542,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
leaderboardGroup = loadLeaderboardGroup(leaderboardGroupCollection.findOne(query), regattaRegistry, leaderboardRegistry);
} catch (Exception e) {
logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboard group "+name+".");
logger.throwing(DomainObjectFactoryImpl.class.getName(), "loadLeaderboardGroup", e);
logger.log(Level.SEVERE, "loadLeaderboardGroup", e);
}
return leaderboardGroup;
@@ -555,7 +558,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboard groups.");
logger.throwing(DomainObjectFactoryImpl.class.getName(), "loadLeaderboardGroup", e);
logger.log(Level.SEVERE, "loadLeaderboardGroup", e);
}
return leaderboardGroups;
@@ -575,10 +578,14 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
for (Object object : dbLeaderboardIds) {
ObjectId dbLeaderboardId = (ObjectId) object;
DBObject dbLeaderboard = leaderboardCollection.findOne(dbLeaderboardId);
final Leaderboard loadedLeaderboard = loadLeaderboard(dbLeaderboard, regattaRegistry,
leaderboardRegistry, /* groupForMetaLeaderboard */ null);
if (loadedLeaderboard != null) {
leaderboards.add(loadedLeaderboard);
if (dbLeaderboard != null) {
final Leaderboard loadedLeaderboard = loadLeaderboard(dbLeaderboard, regattaRegistry,
leaderboardRegistry, /* groupForMetaLeaderboard */null);
if (loadedLeaderboard != null) {
leaderboards.add(loadedLeaderboard);
}
} else {
logger.warning("couldn't find leaderboard with ID "+dbLeaderboardId+" referenced by leaderboard group "+name);
}
}
logger.info("loaded leaderboard group "+name);
@@ -605,7 +612,9 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name());
Set<Leaderboard> result = new HashSet<Leaderboard>();
try {
// Don't change the query object, unless you know what you're doing
// Don't change the query object, unless you know what you're doing.
// It queries all leaderboards not referenced to be part of a leaderboard group
// and in particular not being an overall leaderboard of a leaderboard group.
BasicDBObject query = new BasicDBObject("$where", "function() { return db." + CollectionNames.LEADERBOARD_GROUPS.name() + ".find({ "
+ FieldNames.LEADERBOARD_GROUP_LEADERBOARDS.name() + ": this._id }).count() == 0 && "
+ "db."+CollectionNames.LEADERBOARD_GROUPS.name()+".find({ "+FieldNames.LEADERBOARD_GROUP_OVERALL_LEADERBOARD.name() + ": this._id }).count() == 0 && "
@@ -619,7 +628,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboards.");
logger.throwing(DomainObjectFactoryImpl.class.getName(), "getAllLeaderboards", e);
logger.log(Level.SEVERE, "getAllLeaderboards", e);
}
return result;
}
@@ -690,7 +699,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
} catch (Exception e) {
// something went wrong during DB access; report, then use empty new wind track
logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load recorded wind data. Check MongoDB settings.");
logger.throwing(DomainObjectFactoryImpl.class.getName(), "loadWindTrack", e);
logger.log(Level.SEVERE, "loadWindTrack", e);
}
return result;
}
@@ -741,7 +750,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load events.");
logger.throwing(DomainObjectFactoryImpl.class.getName(), "loadEvents", e);
logger.log(Level.SEVERE, "loadEvents", e);
}
return result;
@@ -951,7 +960,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
} catch (Throwable t) {
// something went wrong during DB access; report, then use empty new race log
logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load recorded race log data. Check MongoDB settings.");
logger.throwing(DomainObjectFactoryImpl.class.getName(), "loadRaceLog", t);
logger.log(Level.SEVERE, "loadRaceLog", t);
}
return result;
}
@@ -2,6 +2,7 @@ package com.sap.sailing.domain.persistence.impl;
import java.io.Serializable;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bson.types.ObjectId;
@@ -100,7 +101,7 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory {
public DBCollection getWindTrackCollection() {
DBCollection result = database.getCollection(CollectionNames.WIND_TRACKS.name());
result.ensureIndex(new BasicDBObject(FieldNames.EVENT_NAME.name(), null));
result.ensureIndex(new BasicDBObject(FieldNames.REGATTA_NAME.name(), null));
return result;
}
@@ -148,7 +149,7 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory {
leaderboardCollection.ensureIndex(FieldNames.LEADERBOARD_NAME.name());
} catch (NullPointerException npe) {
// sometimes, for reasons yet to be clarified, ensuring an index on the name field causes an NPE
logger.throwing(MongoObjectFactoryImpl.class.getName(), "storeLeaderboard", npe);
logger.log(Level.SEVERE, "storeLeaderboard", npe);
}
BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), leaderboard.getName());
BasicDBObject dbLeaderboard = new BasicDBObject();
@@ -313,7 +314,7 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory {
leaderboardGroupCollection.ensureIndex(FieldNames.LEADERBOARD_GROUP_NAME.name());
} catch (NullPointerException npe) {
// sometimes, for reasons yet to be clarified, ensuring an index on the name field causes an NPE
logger.throwing(MongoObjectFactoryImpl.class.getName(), "storeLeaderboardGroup", npe);
logger.log(Level.SEVERE, "storeLeaderboardGroup", npe);
}
BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), leaderboardGroup.getName());
BasicDBObject dbLeaderboardGroup = new BasicDBObject();
@@ -8,7 +8,7 @@ import com.sap.sailing.domain.common.Named;
* Base interface for courses consisting of all static information, which might be shared
* by the server and an Android application.
*/
public interface CourseData extends Named {
public interface CourseBase extends Named {
/**
* Clients can safely iterate over the resulting list because it's a copy which therefore won't reflect
* waypoint additions and removals.
@@ -8,7 +8,7 @@ import com.sap.sailing.domain.common.WithID;
* Base interface for an Event consisting of all static information, which might be shared
* by the server and an Android application.
*/
public interface EventData extends Named, Renamable, WithID {
public interface EventBase extends Named, Renamable, WithID {
/**
* @return a non-<code>null</code> venue for this event
@@ -5,7 +5,7 @@ import com.sap.sailing.domain.common.Named;
/**
* Base interface for series consisting of static information describing the Series.
*/
public interface SeriesData extends Named {
public interface SeriesBase extends Named {
/**
* Returns the fleets of this series, in ascending order, better fleets first.
@@ -2,10 +2,10 @@ package com.sap.sailing.domain.base.impl;
import java.io.Serializable;
import com.sap.sailing.domain.base.EventData;
import com.sap.sailing.domain.base.EventBase;
import com.sap.sailing.domain.base.Venue;
public class EventDataImpl implements EventData {
public class EventBaseImpl implements EventBase {
private static final long serialVersionUID = -5749964088848611074L;
private String name;
@@ -14,14 +14,14 @@ public class EventDataImpl implements EventData {
private boolean isPublic;
private final Serializable id;
public EventDataImpl(String name, String venueName, String publicationUrl, boolean isPublic, Serializable id) {
public EventBaseImpl(String name, String venueName, String publicationUrl, boolean isPublic, Serializable id) {
this(name, new VenueImpl(venueName), publicationUrl, isPublic, id);
}
/**
* @param venue must not be <code>null</code>
*/
public EventDataImpl(String name, Venue venue, String publicationUrl, boolean isPublic, Serializable id) {
public EventBaseImpl(String name, Venue venue, String publicationUrl, boolean isPublic, Serializable id) {
assert venue != null;
this.id = id;
this.name = name;
@@ -1,11 +1,11 @@
package com.sap.sailing.domain.base.racegroup;
import com.sap.sailing.domain.base.SeriesData;
import com.sap.sailing.domain.base.SeriesBase;
/**
* The interface represents a series and holds its races. The interface abstracts from the server internal differences
* between Leaderboards and Regattas for any app since Leaderboards do not have any series, but Regattas.
*/
public interface SeriesWithRows extends SeriesData {
public interface SeriesWithRows extends SeriesBase {
public Iterable<RaceRow> getRaceRows();
}
@@ -1,7 +1,9 @@
package com.sap.sailing.util.impl;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.logging.Logger;
@@ -14,14 +16,13 @@ public class LockUtil {
private static final int NUMBER_OF_SECONDS_TO_WAIT_FOR_LOCK = 5;
private static final Logger logger = Logger.getLogger(Util.class.getName());
private static final Map<NamedReentrantReadWriteLock, TimePoint> lastTimeWriteLockWasObtained = new WeakHashMap<NamedReentrantReadWriteLock, TimePoint>();
private static final Map<Thread, Map<Lock, Integer>> lockCounts = new ConcurrentHashMap<Thread, Map<Lock,Integer>>();
/**
* Bug <a href="http://bugs.sun.com/view_bug.do?bug_id=6822370">http://bugs.sun.com/view_bug.do?bug_id=6822370</a> seems
* dangerous, particularly if it happens in a <code>LiveLeaderboardUpdater</code> thread. Even though the bug is reported to
* have been fixed in JDK 7(b79) we should be careful. This method tries to acquire a lock, allowing for five seconds to pass.
* After five seconds and not having retrieved the lock, tries again until the lock has been acquired.
* @param lockParent TODO
* @throws InterruptedException
*/
private static void lock(Lock lock, String lockDescriptionForTimeoutLogMessage, NamedReentrantReadWriteLock lockParent) {
boolean locked = false;
@@ -35,7 +36,7 @@ public class LockUtil {
message.append(lockDescriptionForTimeoutLogMessage);
message.append(" in ");
message.append(NUMBER_OF_SECONDS_TO_WAIT_FOR_LOCK);
message.append("s at ");
message.append("s in thread "+Thread.currentThread().getName()+" at ");
message.append(getCurrentStackTrace());
Thread writer = lockParent.getWriter();
if (writer != null) {
@@ -69,37 +70,97 @@ public class LockUtil {
}
public static void lockForRead(NamedReentrantReadWriteLock lock) {
lock(lock.readLock(), lock.getReadLockName(), lock);
if (isInCurrentThreadsLockSet(lock.readLock())) {
incrementLockCountForCurrentThread(lock.readLock());
} else {
lock(lock.readLock(), lock.getReadLockName(), lock);
addToCurrentThreadsLockSet(lock.readLock());
}
}
private static Map<Lock, Integer> getCurrentThreadsLockCounts() {
final Thread currentThread = Thread.currentThread();
return getLockCounts(currentThread);
}
private static Map<Lock, Integer> getLockCounts(final Thread thread) {
Map<Lock, Integer> result = lockCounts.get(thread);
if (result == null) {
result = new HashMap<Lock, Integer>();
lockCounts.put(thread, result);
}
return result;
}
private static void incrementLockCountForCurrentThread(Lock lock) {
Map<Lock, Integer> map = getCurrentThreadsLockCounts();
assert map.containsKey(lock);
map.put(lock, map.get(lock) + 1);
}
private static void decrementLockCountForCurrentThread(Lock lock) {
Map<Lock, Integer> map = getCurrentThreadsLockCounts();
assert map.containsKey(lock);
map.put(lock, map.get(lock)-1);
}
private static boolean isInCurrentThreadsLockSet(Lock lock) {
return getCurrentThreadsLockCounts().containsKey(lock);
}
private static void addToCurrentThreadsLockSet(Lock lock) {
getCurrentThreadsLockCounts().put(lock, 1);
}
private static void removeFromCurrentThreadsLockSet(Lock lock) {
getCurrentThreadsLockCounts().remove(lock);
}
public static void unlockAfterRead(NamedReentrantReadWriteLock lock) {
lock.readLock().unlock();
assert isInCurrentThreadsLockSet(lock.readLock());
if (getCurrentThreadsLockCounts().get(lock.readLock()) == 1) {
lock.readLock().unlock();
removeFromCurrentThreadsLockSet(lock.readLock());
} else {
decrementLockCountForCurrentThread(lock.readLock());
}
}
public static void lockForWrite(NamedReentrantReadWriteLock lock) {
lock(lock.writeLock(), lock.getWriteLockName(), lock);
synchronized (lastTimeWriteLockWasObtained) {
lastTimeWriteLockWasObtained.put(lock, MillisecondsTimePoint.now());
if (isInCurrentThreadsLockSet(lock.writeLock())) {
incrementLockCountForCurrentThread(lock.writeLock());
} else {
lock(lock.writeLock(), lock.getWriteLockName(), lock);
addToCurrentThreadsLockSet(lock.writeLock());
synchronized (lastTimeWriteLockWasObtained) {
lastTimeWriteLockWasObtained.put(lock, MillisecondsTimePoint.now());
}
}
}
public static void unlockAfterWrite(NamedReentrantReadWriteLock lock) {
lock.writeLock().unlock();
final TimePoint timePointWriteLockWasObtained;
synchronized (lastTimeWriteLockWasObtained) {
timePointWriteLockWasObtained = lastTimeWriteLockWasObtained.get(lock);
}
if (timePointWriteLockWasObtained == null) {
logger.info("Internal error: write lock "+lock.getName()+" to be unlocked but no time recorded for when it was last obtained.\n"+
"This is where the lock interaction happened:\n"+getCurrentStackTrace());
} else {
TimePoint now = MillisecondsTimePoint.now();
if (now.asMillis()-timePointWriteLockWasObtained.asMillis() > 10000l) {
String stackTrace = getCurrentStackTrace();
logger.info("write lock "+lock.getName()+" was held for more than 10s. It got unlocked here: "+
stackTrace);
assert isInCurrentThreadsLockSet(lock.writeLock());
if (getCurrentThreadsLockCounts().get(lock.writeLock()) == 1) {
lock.writeLock().unlock();
removeFromCurrentThreadsLockSet(lock.writeLock());
final TimePoint timePointWriteLockWasObtained;
synchronized (lastTimeWriteLockWasObtained) {
timePointWriteLockWasObtained = lastTimeWriteLockWasObtained.get(lock);
}
if (timePointWriteLockWasObtained == null) {
logger.info("Internal error: write lock "+lock.getName()+" to be unlocked but no time recorded for when it was last obtained.\n"+
"This is where the lock interaction happened:\n"+getCurrentStackTrace());
} else {
TimePoint now = MillisecondsTimePoint.now();
if (now.asMillis()-timePointWriteLockWasObtained.asMillis() > 10000l) {
String stackTrace = getCurrentStackTrace();
logger.info("write lock "+lock.getName()+" was held for more than 10s. It got unlocked here: "+
stackTrace);
}
}
} else {
decrementLockCountForCurrentThread(lock.writeLock());
}
}
private static String getStackTrace(Thread thread) {
@@ -114,4 +175,48 @@ public class LockUtil {
private static String getCurrentStackTrace() {
return getStackTrace(Thread.currentThread());
}
/**
* ATTENTION: Calling this method makes a very strong assertion! It asserts that the calling thread
* will call {@link #unpropagateLockSetFrom(Thread)} for the same <code>thread</code> passed to this
* call before <code>thread</code> releases any of the locks it currently holds. The effect of making
* this call is that the calling thread, when trying to acquire a lock already held by <code>thread</code>,
* will not actually acquire that lock again. This, in particular, has the effect that a read lock
* already held by <code>thread</code> will not have to be acquired again, which in turn avoids a
* read-read deadlock on a fair lock in case another thread is attempting to acquire the corresponding
* write lock before the current thread tries to re-acquire the read lock.<p>
*
* Always use this in a <code>try/finally</code> combination where in the <code>finally</code> you call
* {@link #unpropagateLockSetFrom(Thread)}.
*/
public static void propagateLockSetFrom(Thread thread) {
Map<Lock, Integer> otherMap = lockCounts.get(thread);
if (otherMap != null) {
Map<Lock, Integer> currentMap = getCurrentThreadsLockCounts();
for (Map.Entry<Lock, Integer> otherEntry : otherMap.entrySet()) {
if (currentMap.containsKey(otherEntry.getKey())) {
currentMap.put(otherEntry.getKey(), currentMap.get(otherEntry.getKey()) + otherEntry.getValue());
} else {
currentMap.put(otherEntry.getKey(), otherEntry.getValue());
}
}
}
}
/**
* A thread that previously propagated the lock set from another thread by using {@link #propagateLockSetFrom(Thread)}
* ends the propagation with this call. After this call, <code>thread</code> is free to release any locks it held
* at the time {@link #propagateLockSetFrom(Thread)} was called by the current thread with <code>thread</code> as
* the argument.
*/
public static void unpropagateLockSetFrom(Thread thread) {
Map<Lock, Integer> otherMap = lockCounts.get(thread);
if (otherMap != null) {
Map<Lock, Integer> currentMap = getCurrentThreadsLockCounts();
for (Map.Entry<Lock, Integer> otherEntry : otherMap.entrySet()) {
assert currentMap.containsKey(otherEntry.getKey());
currentMap.put(otherEntry.getKey(), currentMap.get(otherEntry.getKey()) - otherEntry.getValue());
}
}
}
}
@@ -7,7 +7,11 @@ import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.mongodb.BasicDBObject;
@@ -63,7 +67,7 @@ public class StoreAndForward implements Runnable {
private final Thread storeAndForwardThread;
private Socket socket;
private final Set<ReceivingThread> receivingThreads;
/**
* Use of this server socket is optional and happens if and only if this object is operated in
@@ -85,13 +89,14 @@ public class StoreAndForward implements Runnable {
*/
public StoreAndForward(String sailMasterHostname, int sailMasterPort, int portForClients, SwissTimingFactory swissTimingFactory,
SwissTimingAdapterPersistence swissTimingAdapterPersistence, MongoDBService mongoDBService) throws InterruptedException, IOException {
this.receivingThreads = new HashSet<>();
this.db = mongoDBService.getDB();
this.listenPort = -1;
this.transceiver = swissTimingFactory.createSailMasterTransceiver();
this.portForClients = portForClients;
this.sailMasterHostname = sailMasterHostname;
this.sailMasterPort = sailMasterPort;
this.streamsToForwardTo = new ArrayList<OutputStream>();
this.streamsToForwardTo = Collections.synchronizedList(new ArrayList<OutputStream>());
this.socketsToForwardTo = new ArrayList<Socket>();
this.swissTimingAdapterPersistence = swissTimingAdapterPersistence;
this.swissTimingFactory = swissTimingFactory;
@@ -125,11 +130,12 @@ public class StoreAndForward implements Runnable {
*/
public StoreAndForward(final int listenPort, final int portForClients, SwissTimingFactory swissTimingFactory,
SwissTimingAdapterPersistence swissTimingAdapterPersistence, MongoDBService mongoDBService) throws InterruptedException, IOException {
this.receivingThreads = new HashSet<>();
this.db = mongoDBService.getDB();
this.listenPort = listenPort;
this.transceiver = swissTimingFactory.createSailMasterTransceiver();
this.portForClients = portForClients;
this.streamsToForwardTo = new ArrayList<OutputStream>();
this.streamsToForwardTo = Collections.synchronizedList(new ArrayList<OutputStream>());
this.socketsToForwardTo = new ArrayList<Socket>();
this.swissTimingAdapterPersistence = swissTimingAdapterPersistence;
this.swissTimingFactory = swissTimingFactory;
@@ -201,20 +207,29 @@ public class StoreAndForward implements Runnable {
}
/**
* Depending on the mode of operation, accepts an inbound connect from a SailMaster server / bridge or
* actively initiates a TCP connection to the SailMaster address/port configured. When this method
* returns, the {@link #socket} holds an open and connected socket.
* Depending on the mode of operation, accepts an inbound connect from a SailMaster server / bridge or actively
* initiates a TCP connection to the SailMaster address/port configured. When a connection has been established, a
* new {@link ReceivingThread} is spawned for the new socket. If in listening mode, the
* {@link ServerSocket#accept()} call is repeated until the instance is {@link #stop()}ed.
*
* @throws IOException
*/
private void establishConnection() throws IOException {
private ReceivingThread establishConnection() throws IOException {
Socket socket;
ReceivingThread receivingThread = null;
if (isInSailMasterListeningMode()) {
synchronized (this) {
receivingFromSailMaster = true;
logger.info("StoreAndForward waiting for inbound SailMaster connections on port "+listenPort);
notifyAll();
while (!stopped) {
synchronized (this) {
receivingFromSailMaster = true;
logger.info("StoreAndForward waiting for inbound SailMaster connections on port " + listenPort);
notifyAll();
}
socket = serverSocketListeningForSailMasterBridge.accept();
logger.info("StoreAndForward received SailMaster connect on port " + listenPort);
receivingThread = new ReceivingThread("SwissTiming SailMaster ReceivingThread for "+socket, socket);
receivingThreads.add(receivingThread);
receivingThread.start();
}
socket = serverSocketListeningForSailMasterBridge.accept();
logger.info("StoreAndForward received SailMaster connect on port "+listenPort);
} else {
synchronized (this) {
receivingFromSailMaster = true;
@@ -223,6 +238,119 @@ public class StoreAndForward implements Runnable {
}
socket = new Socket(sailMasterHostname, sailMasterPort);
logger.info("StoreAndForward connections to SailMaster "+sailMasterHostname+":"+sailMasterPort+" established");
receivingThread = new ReceivingThread("SwissTiming SailMaster ReceivingThread for "+socket, socket);
receivingThreads.add(receivingThread);
receivingThread.start();
}
return receivingThread;
}
/**
* Does the actual receiving. Is initialized with the socket on which to receive.
*
* @author Axel Uhl (D043530)
*
*/
private class ReceivingThread extends Thread {
private final Socket socket;
private boolean stopped;
public ReceivingThread(String threadName, Socket socket) {
super(threadName);
this.socket = socket;
}
@Override
public void run() {
logger.entering(getClass().getName(), "run");
while (!stopped) {
try {
InputStream is = socket.getInputStream();
Pair<String, Long> messageAndOptionalSequenceNumber = transceiver.receiveMessage(is);
if (messageAndOptionalSequenceNumber == null) {
// found EOF; stopping
stopped = true;
} else {
// ignore any sequence number contained in the message; we'll create our own
DBObject emptyQuery = new BasicDBObject();
DBObject incrementLastMessageCountQuery = new BasicDBObject().append("$inc",
new BasicDBObject().append(FieldNames.LAST_MESSAGE_COUNT.name(), 1));
while (!stopped && messageAndOptionalSequenceNumber != null) {
logger.fine("Thread " + this + " received message: "
+ messageAndOptionalSequenceNumber.getA());
DBObject newCountRecord = lastMessageCountCollection.findAndModify(emptyQuery,
incrementLastMessageCountQuery);
lastMessageCount = ((newCountRecord == null) ? 0l : ((Number) newCountRecord
.get(FieldNames.LAST_MESSAGE_COUNT.name())).longValue());
SailMasterMessage message = swissTimingFactory.createMessage(
messageAndOptionalSequenceNumber.getA(), lastMessageCount);
swissTimingAdapterPersistence.storeSailMasterMessage(message);
synchronized (StoreAndForward.this) {
for (OutputStream os : new ArrayList<OutputStream>(streamsToForwardTo)) {
// write the sequence number of the message into the stream before actually writing
// the
// SwissTiming message
try {
transceiver.sendMessage(message, os);
} catch (Exception e) {
logger.log(Level.SEVERE, "Error sending message to " + os, e);
int i = streamsToForwardTo.indexOf(os);
try {
os.close();
} catch (Exception exc) {
logger.log(Level.SEVERE, "Exception closing socket output stream " + os
+ " after being unable to forward message " + message, exc);
}
streamsToForwardTo.remove(os);
Socket s = socketsToForwardTo.remove(i);
logger.info("Unable to send to socket " + s
+ ". Trying to close. Removing from sockets to forward to.");
try {
s.close();
} catch (Exception exc) {
logger.log(Level.WARNING, "Exception trying to close socket " + s
+ " after being unable to forward message " + message, exc);
}
}
}
}
if (!stopped) {
messageAndOptionalSequenceNumber = transceiver.receiveMessage(is);
if (messageAndOptionalSequenceNumber == null) {
// received EOF; stopping received
stopped = true;
}
}
}
}
// note that we're not changing anything with the sockets to which we forward messages; that will only happen
// if forwarding to any of those sockets fails
} catch (Exception e) {
e.printStackTrace();
if (!stopped) {
logger.log(Level.INFO, "Error during forwarding message. Continuing...", e);
try {
Thread.sleep(1000l); // wait a little bit before trying to re-establish a connection
} catch (InterruptedException e1) {
logger.log(Level.INFO, "Can't find any sleep...", e1);
}
} else {
logger.info("StoreAndForward socket was closed.");
}
}
}
logger.info("Terminating StoreAndForward ReceivingThread for socket "+socket);
receivingThreads.remove(this);
}
/**
* Stops execution after having received the next message
*/
public void stopReceiver() throws UnknownHostException, IOException, InterruptedException {
logger.entering(getClass().getName(), "stop");
stopped = true;
logger.info("closing socket");
socket.close(); // will let a read terminate abnormally
}
}
@@ -236,9 +364,16 @@ public class StoreAndForward implements Runnable {
closer.close();
logger.info("joining clientListener thread "+clientListener);
clientListener.join();
socket.close(); // will let a read terminate abnormally
logger.info("joining storeAndForwardThread "+storeAndForwardThread);
storeAndForwardThread.join();
for (ReceivingThread receivingThread : receivingThreads) {
receivingThread.stopReceiver();
logger.info("joining storeAndForwardThread "+storeAndForwardThread);
receivingThread.join();
}
logger.info("StoreAndForward is closing receiving server socket");
if (serverSocketListeningForSailMasterBridge != null && !serverSocketListeningForSailMasterBridge.isClosed()) {
serverSocketListeningForSailMasterBridge.close();
}
logger.info("Stopping StoreAndForward server.");
}
public static void main(String[] args) throws InterruptedException, IOException {
@@ -266,66 +401,12 @@ public class StoreAndForward implements Runnable {
try {
while (!stopped) {
try {
establishConnection();
InputStream is = socket.getInputStream();
Pair<String, Long> messageAndOptionalSequenceNumber = transceiver.receiveMessage(is);
// ignore any sequence number contained in the message; we'll create our own
DBObject emptyQuery = new BasicDBObject();
DBObject incrementLastMessageCountQuery = new BasicDBObject().
append("$inc", new BasicDBObject().append(FieldNames.LAST_MESSAGE_COUNT.name(), 1));
while (!stopped && messageAndOptionalSequenceNumber != null) {
logger.fine("Received message: "+messageAndOptionalSequenceNumber.getA());
DBObject newCountRecord = lastMessageCountCollection.findAndModify(emptyQuery, incrementLastMessageCountQuery);
lastMessageCount = ((newCountRecord == null) ? 0l :
((Number) newCountRecord.get(FieldNames.LAST_MESSAGE_COUNT.name())).longValue());
SailMasterMessage message = swissTimingFactory.createMessage(messageAndOptionalSequenceNumber.getA(), lastMessageCount);
swissTimingAdapterPersistence.storeSailMasterMessage(message);
synchronized (this) {
for (OutputStream os : new ArrayList<OutputStream>(streamsToForwardTo)) {
// write the sequence number of the message into the stream before actually writing the
// SwissTiming message
try {
// TODO if forwarding to os doesn't work, e.g., because the socket was closed or the client died, remove os from streamsToForwardTo and the socket from socketsToForwardTo
transceiver.sendMessage(message, os);
} catch (Exception e) {
int i=streamsToForwardTo.indexOf(os);
try {
os.close();
} catch (Exception exc) {
logger.throwing(StoreAndForward.class.getName(), "run", exc);
}
streamsToForwardTo.remove(os);
Socket s = socketsToForwardTo.remove(i);
try {
s.close();
} catch (Exception exc) {
logger.throwing(StoreAndForward.class.getName(), "run", exc);
}
}
}
}
if (!stopped) {
messageAndOptionalSequenceNumber = transceiver.receiveMessage(is);
}
}
for (OutputStream os : streamsToForwardTo) {
os.close();
}
for (Socket socketToForwardTo : socketsToForwardTo) {
socketToForwardTo.close();
ReceivingThread receivingThread = establishConnection();
if (!isInSailMasterListeningMode()) {
receivingThread.join(); // we're in active connecting mode; wait for thread to die, then trie again
}
} catch (Exception e) {
e.printStackTrace();
if (!stopped) {
logger.throwing(StoreAndForward.class.getName(), "Error during forwarding message. Continuing...", e);
try {
Thread.sleep(1000l); // wait a little bit before trying to re-establish a connection
} catch (InterruptedException e1) {
logger.throwing(StoreAndForward.class.getName(), "Can't find any sleep...", e1);
}
} else {
logger.info("StoreAndForward socket was closed.");
}
logger.log(Level.SEVERE, "Exception in StoreAndForward", e);
}
}
logger.info("StoreAndForward is closing receiving server socket");
@@ -141,7 +141,7 @@ public class SwissTimingAdapterPersistenceImpl implements SwissTimingAdapterPers
@Override
public List<SailMasterMessage> loadRaceMessages(String raceID) {
DBCollection racesMessagesCollection = database.getCollection(CollectionNames.RACES_MESSAGES.name());
racesMessagesCollection.ensureIndex(new BasicDBObject(FieldNames.MESSAGE_SEQUENCE_NUMBER.name(), null)); // no sort without index
racesMessagesCollection.ensureIndex(new BasicDBObject(FieldNames.MESSAGE_SEQUENCE_NUMBER.name(), 1)); // no sort without index
BasicDBObject query = new BasicDBObject();
query.append(FieldNames.RACE_ID.name(), raceID);
DBCursor results = racesMessagesCollection.find(query).sort(
@@ -0,0 +1,6 @@
RAC|1|SAM005923;Men 470 Race 23
STL|SAM005923|34|AUS-7;AUS;AUS-7 |AUT-11;AUT;AUT-11 |AUT-477;AUT;AUT-477 |BEL-16;BEL;BEL-16 |ESP-1;ESP;ESP-1 |ESP-11;ESP;ESP-11 |ESP-13;ESP;ESP-13 |ESP-16;ESP;ESP-16 |ESP-17;ESP;ESP-17 |ESP-18;ESP;ESP-18 |ESP-1868;ESP;ESP-1868 |ESP-22;ESP;ESP-22 |FIN-111;FIN;FIN-111 |FRA-15;FRA;FRA-15 |FRA-20;FRA;FRA-20 |FRA-76;FRA;FRA-76 |GBR-828;GBR;GBR-828 |GBR-843;GBR;GBR-843 |GER-13;GER;GER-13 |GER-15;GER;GER-15 |GER-48;GER;GER-48 |GER-5025;GER;GER-5025 |GER-84;GER;GER-84 |ITA-22;ITA;ITA-22 |ITA-25;ITA;ITA-25 |ITA-28;ITA;ITA-28 |ITA-300;ITA;ITA-300 |ITA-32;ITA;ITA-32 |ITA-99;ITA;ITA-99 |POR-2;POR;POR-2 |RSA-8;RSA;RSA-8 |RUS-100;RUS;RUS-100 |UKR-11;UKR;UKR-11 |UKR-23;UKR;UKR-23
CCG|SAM005923|7|0;Start;1;SB;SP|1;Mark 1;2;M1;|2;Mark 2;1;M4p;M4s|3;Mark 3;2;M1;|4;Mark 4;2;M2;|5;Mark 5;2;M3p;|6;Finish;1;FB;FP
TMD|SAM005923|GBR-828|1|0;;00:00:00
RPD|SAM005923|0|2013-04-04T10:35:24+02:00|||||55|FB;4;2;39.514533;2.721949;0.8;;;256;;;;;|FP;4;2;39.512227;2.724561;0.4;;;146;;;;;|J1;3;-1;39.499894;2.750856;0.2;;;172;;;;;|J2;3;-1;39.499831;2.750965;0.0;;;32;;;;;|M1;5;2;39.526214;2.706992;0.4;;;138;;;;;|M2;5;2;39.518901;2.707612;11.9;;;316;;;;;|M3p;5;2;39.512236;2.724599;0.6;;;140;;;;;|M3s;5;378;39.513290;2.723822;1.2;;;240;;;;;|M4p;5;2;39.518983;2.720770;0.4;;;54;;;;;|M4s;5;2;39.518967;2.720791;0.4;;;60;;;;;|R1;4;2;39.518919;2.707606;10.9;;;322;;;;;|R2;4;2;39.526186;2.706922;0.6;;;124;;;;;|R3;4;0;39.499793;2.750926;0.0;;;334;;;;;|S1;5;2;39.526193;2.706945;0.4;;;140;;;;;|S2;5;2;39.518899;2.707619;11.1;;;320;;;;;|S3p;5;;;;;;;;;;;;|S3s;5;;;;;;;;;;;;|S4p;5;;;;;;;;;;;;|S4s;5;;;;;;;;;;;;|SB;2;0;39.519168;2.721933;0.4;;;280;;;;;|SP;4;0;39.517652;2.719715;0.8;;;116;;;;;|AUS-7;1;;;;;;;;;;;;|AUT-11;1;;;;;;;;;;;;|AUT-477;1;;;;;;;;;;;;|BEL-16;1;;;;;;;;;;;;|ESP-1;1;;;;;;;;;;;;|ESP-11;1;;;;;;;;;;;;|ESP-13;1;;;;;;;;;;;;|ESP-16;1;;;;;;;;;;;;|ESP-17;1;;;;;;;;;;;;|ESP-18;1;;;;;;;;;;;;|ESP-1868;1;;;;;;;;;;;;|ESP-22;1;;;;;;;;;;;;|FIN-111;1;;;;;;;;;;;;|FRA-15;1;;;;;;;;;;;;|FRA-20;1;;;;;;;;;;;;|FRA-76;1;;;;;;;;;;;;|GBR-828;1;;;;;;;;;;;;|GBR-843;1;;;;;;;;;;;;|GER-13;1;;;;;;;;;;;;|GER-15;1;;;;;;;;;;;;|GER-48;1;;;;;;;;;;;;|GER-5025;1;;;;;;;;;;;;|GER-84;1;;;;;;;;;;;;|ITA-22;1;;;;;;;;;;;;|ITA-25;1;;;;;;;;;;;;|ITA-28;1;;;;;;;;;;;;|ITA-300;1;;;;;;;;;;;;|ITA-32;1;;;;;;;;;;;;|ITA-99;1;;;;;;;;;;;;|POR-2;1;;;;;;;;;;;;|RSA-8;1;;;;;;;;;;;;|RUS-100;1;;;;;;;;;;;;|UKR-11;1;;;;;;;;;;;;|UKR-23;1;;;;;;;;;;;;
RPD|SAM005923|3|2013-04-04T14:53:29+02:00|13:45:00|01:08:29|||55|FB;4;1;39.514595;2.726782;0.6;;;352;;;;;|FP;4;1;39.514796;2.727207;0.6;;;52;;;;;|J1;3;0;39.515297;2.721503;2.1;;;332;;;;;|J2;3;0;39.516209;2.721166;4.1;;;334;;;;;|M1;5;1;39.507557;2.700071;0.2;;;22;;;;;|M2;5;1;39.503624;2.704863;0.4;;;274;;;;;|M3p;5;1;39.511114;2.726937;0.6;;;268;;;;;|M3s;5;1;39.510720;2.727125;1.6;;;54;;;;;|M4p;5;1;39.514059;2.718241;1.4;;;110;;;;;|M4s;5;1;39.514376;2.718104;0.4;;;278;;;;;|R1;4;1;39.502029;2.704981;5.4;;;310;;;;;|R2;4;1;39.503993;2.703844;1.0;;;148;;;;;|R3;4;1;39.516238;2.721159;4.5;;;344;;;;;|S1;5;1;39.503955;2.703858;1.6;;;138;;;;;|S2;5;13;39.501492;2.706392;0.2;;;102;;;;;|S3p;5;;;;;;;;;;;;|S3s;5;;;;;;;;;;;;|S4p;5;;;;;;;;;;;;|S4s;5;;;;;;;;;;;;|SB;2;-1;39.515923;2.720501;0.2;;;160;;;;;|SP;4;1;39.512957;2.721845;0.4;;;22;;;;;|AUS-7;1;1;39.515843;2.721024;6.8;;;0;;6;0;;|AUT-11;1;0;39.516144;2.720703;0.8;;;208;;21;0;;|AUT-477;1;1;39.515635;2.720475;4.7;;;312;;29;0;;|BEL-16;1;0;39.515838;2.720738;2.3;;;224;;10;0;;|ESP-1;1;3;39.515593;2.720689;3.5;;;198;;18;0;;|ESP-11;1;0;39.516054;2.720826;5.1;;;308;;9;0;;|ESP-13;1;;;;;;;;;;;;|ESP-16;1;1;39.515497;2.721003;4.9;;;124;;27;0;;|ESP-17;1;0;39.516150;2.720747;2.5;;;204;;24;0;;|ESP-18;1;1;39.514838;2.721314;4.7;;;170;;31;0;;|ESP-1868;1;1;39.500431;2.749792;0.0;;;258;;23;0;;|ESP-22;1;1;39.516085;2.720959;4.7;;;328;;14;0;;|FIN-111;1;0;39.515651;2.721038;2.1;;;290;;13;0;;|FRA-15;1;1;39.515935;2.720659;1.6;;;178;;11;0;;|FRA-20;1;3;39.516128;2.720589;7.0;;;136;;15;0;;|FRA-76;1;1;39.516355;2.720682;6.4;;;162;;3;0;;|GBR-828;1;1;39.514721;2.721544;6.0;;;334;;26;0;;|GBR-843;1;1;39.514117;2.721600;7.2;;;330;;1;0;;|GER-13;1;-1;39.515559;2.721045;2.7;;;276;;20;0;;|GER-15;1;1;39.514951;2.721463;5.6;;;336;;2;0;;|GER-48;1;1;39.514667;2.721653;2.1;;;182;;25;0;;|GER-5025;1;1;39.515795;2.720919;2.9;;;270;;7;0;;|GER-84;1;1;39.516074;2.720768;1.7;;;280;;5;0;;|ITA-22;1;0;39.516090;2.720580;1.9;;;120;;17;0;;|ITA-25;1;1;39.515364;2.720907;1.6;;;188;;12;0;;|ITA-28;1;1;39.516405;2.720717;6.8;;;318;;19;0;;|ITA-300;1;0;39.515867;2.720828;4.1;;;182;;16;0;;|ITA-32;1;1;39.515687;2.720687;3.1;;;330;;22;0;;|ITA-99;1;-1;39.515737;2.720794;0.8;;;286;;4;0;;|POR-2;1;1;39.515852;2.720619;0.2;;;162;;8;0;;|RSA-8;1;1;39.515505;2.720828;6.6;;;20;;28;0;;|RUS-100;1;0;39.515896;2.720777;3.7;;;178;;30;0;;|UKR-11;1;1;39.516422;2.720378;3.1;;;174;;32;0;;|UKR-23;1;1;39.501535;2.750179;0.0;0.0;1.1;168;1;33;10221;3713;BFD
@@ -0,0 +1,507 @@
RAC|1|SAM005923;Men 470 Race 23
STL|SAM005923|34|AUS-7;AUS;AUS-7 |AUT-11;AUT;AUT-11 |AUT-477;AUT;AUT-477 |BEL-16;BEL;BEL-16 |ESP-1;ESP;ESP-1 |ESP-11;ESP;ESP-11 |ESP-13;ESP;ESP-13 |ESP-16;ESP;ESP-16 |ESP-17;ESP;ESP-17 |ESP-18;ESP;ESP-18 |ESP-1868;ESP;ESP-1868 |ESP-22;ESP;ESP-22 |FIN-111;FIN;FIN-111 |FRA-15;FRA;FRA-15 |FRA-20;FRA;FRA-20 |FRA-76;FRA;FRA-76 |GBR-828;GBR;GBR-828 |GBR-843;GBR;GBR-843 |GER-13;GER;GER-13 |GER-15;GER;GER-15 |GER-48;GER;GER-48 |GER-5025;GER;GER-5025 |GER-84;GER;GER-84 |ITA-22;ITA;ITA-22 |ITA-25;ITA;ITA-25 |ITA-28;ITA;ITA-28 |ITA-300;ITA;ITA-300 |ITA-32;ITA;ITA-32 |ITA-99;ITA;ITA-99 |POR-2;POR;POR-2 |RSA-8;RSA;RSA-8 |RUS-100;RUS;RUS-100 |UKR-11;UKR;UKR-11 |UKR-23;UKR;UKR-23
CCG|SAM005923|7|0;Start;1;SB;SP|1;Mark 1;2;M1;|2;Mark 2;1;M4p;M4s|3;Mark 3;2;M1;|4;Mark 4;2;M2;|5;Mark 5;2;M3p;|6;Finish;1;FB;FP
TMD|SAM005923|GBR-828|1|0;;00:00:00
TMD|SAM005923|GBR-843|1|0;;00:00:00
TMD|SAM005923|GER-13|1|0;;00:00:00
TMD|SAM005923|GER-15|1|0;;00:00:00
TMD|SAM005923|GER-48|1|0;;00:00:00
TMD|SAM005923|GER-5025|1|0;;00:00:00
TMD|SAM005923|GER-84|1|0;;00:00:00
TMD|SAM005923|ITA-22|1|0;;00:00:00
TMD|SAM005923|AUS-7|1|0;;00:00:00
TMD|SAM005923|ESP-11|1|0;;00:00:00
TMD|SAM005923|ESP-13|1|0;;00:00:00
TMD|SAM005923|ESP-16|1|0;;00:00:00
TMD|SAM005923|ESP-17|1|0;;00:00:00
TMD|SAM005923|ESP-18|1|0;;00:00:00
TMD|SAM005923|ESP-1868|1|0;;00:00:00
TMD|SAM005923|ESP-22|1|0;;00:00:00
TMD|SAM005923|FIN-111|1|0;;00:00:00
TMD|SAM005923|FRA-15|1|0;;00:00:00
TMD|SAM005923|FRA-20|1|0;;00:00:00
TMD|SAM005923|FRA-76|1|0;;00:00:00
TMD|SAM005923|ITA-25|1|0;;00:00:00
TMD|SAM005923|ITA-28|1|0;;00:00:00
TMD|SAM005923|ITA-300|1|0;;00:00:00
TMD|SAM005923|ITA-32|1|0;;00:00:00
TMD|SAM005923|ITA-99|1|0;;00:00:00
TMD|SAM005923|POR-2|1|0;;00:00:00
TMD|SAM005923|RSA-8|1|0;;00:00:00
TMD|SAM005923|RUS-100|1|0;;00:00:00
TMD|SAM005923|UKR-11|1|0;;00:00:00
TMD|SAM005923|UKR-23|1|0;;00:00:00
TMD|SAM005923|AUS-7|1|0;;00:00:00
TMD|SAM005923|AUT-11|1|0;;00:00:00
TMD|SAM005923|AUT-477|1|0;;00:00:00
TMD|SAM005923|BEL-16|1|0;;00:00:00
TMD|SAM005923|ESP-1|1|0;;00:00:00
TMD|SAM005923|ESP-11|1|0;;00:00:00
TMD|SAM005923|ESP-13|1|0;;00:00:00
TMD|SAM005923|ESP-16|1|0;;00:00:00
TMD|SAM005923|ESP-17|1|0;;00:00:00
TMD|SAM005923|ESP-18|1|0;;00:00:00
TMD|SAM005923|ESP-1868|1|0;;00:00:00
TMD|SAM005923|ESP-22|1|0;;00:00:00
TMD|SAM005923|FIN-111|1|0;;00:00:00
TMD|SAM005923|FRA-15|1|0;;00:00:00
TMD|SAM005923|FRA-20|1|0;;00:00:00
TMD|SAM005923|FRA-76|1|0;;00:00:00
TMD|SAM005923|GBR-828|1|0;;00:00:00
TMD|SAM005923|GBR-843|1|0;;00:00:00
TMD|SAM005923|GER-13|1|0;;00:00:00
TMD|SAM005923|GER-15|1|0;;00:00:00
TMD|SAM005923|GER-48|1|0;;00:00:00
TMD|SAM005923|GER-5025|1|0;;00:00:00
TMD|SAM005923|GER-84|1|0;;00:00:00
TMD|SAM005923|ITA-22|1|0;;00:00:00
TMD|SAM005923|ITA-25|1|0;;00:00:00
TMD|SAM005923|ITA-28|1|0;;00:00:00
TMD|SAM005923|ITA-300|1|0;;00:00:00
TMD|SAM005923|ITA-32|1|0;;00:00:00
TMD|SAM005923|ITA-99|1|0;;00:00:00
TMD|SAM005923|POR-2|1|0;;00:00:00
TMD|SAM005923|RSA-8|1|0;;00:00:00
TMD|SAM005923|RUS-100|1|0;;00:00:00
TMD|SAM005923|UKR-11|1|0;;00:00:00
TMD|SAM005923|UKR-23|1|0;;00:00:00
TMD|SAM005923|AUS-7|1|0;;00:00:00
TMD|SAM005923|BEL-16|1|0;;00:00:00
TMD|SAM005923|ESP-1|1|0;;00:00:00
TMD|SAM005923|ESP-11|1|0;;00:00:00
TMD|SAM005923|ESP-13|1|0;;00:00:00
TMD|SAM005923|ESP-16|1|0;;00:00:00
TMD|SAM005923|ESP-17|1|0;;00:00:00
TMD|SAM005923|ESP-18|1|0;;00:00:00
TMD|SAM005923|ESP-1868|1|0;;00:00:00
TMD|SAM005923|ESP-22|1|0;;00:00:00
TMD|SAM005923|FIN-111|1|0;;00:00:00
TMD|SAM005923|FRA-15|1|0;;00:00:00
TMD|SAM005923|FRA-20|1|0;;00:00:00
TMD|SAM005923|FRA-76|1|0;;00:00:00
TMD|SAM005923|GBR-828|1|0;;00:00:00
TMD|SAM005923|GBR-843|1|0;;00:00:00
TMD|SAM005923|GER-13|1|0;;00:00:00
TMD|SAM005923|GER-15|1|0;;00:00:00
TMD|SAM005923|GER-48|1|0;;00:00:00
TMD|SAM005923|GER-5025|1|0;;00:00:00
TMD|SAM005923|GER-84|1|0;;00:00:00
TMD|SAM005923|ITA-22|1|0;;00:00:00
TMD|SAM005923|ITA-25|1|0;;00:00:00
TMD|SAM005923|ITA-28|1|0;;00:00:00
TMD|SAM005923|ITA-300|1|0;;00:00:00
TMD|SAM005923|ITA-32|1|0;;00:00:00
TMD|SAM005923|ITA-99|1|0;;00:00:00
TMD|SAM005923|POR-2|1|0;;00:00:00
TMD|SAM005923|RSA-8|1|0;;00:00:00
TMD|SAM005923|RUS-100|1|0;;00:00:00
TMD|SAM005923|UKR-11|1|0;;00:00:00
TMD|SAM005923|UKR-23|1|0;;00:00:00
TMD|SAM005923|GBR-843|1|1;;00:13:14
TMD|SAM005923|GBR-843|1|1;1;00:13:14
TMD|SAM005923|FIN-111|1|1;;00:13:40
TMD|SAM005923|FIN-111|1|1;2;00:13:40
TMD|SAM005923|GER-5025|1|1;;00:13:42
TMD|SAM005923|GER-5025|1|1;3;00:13:42
TMD|SAM005923|GER-15|1|1;;00:13:48
TMD|SAM005923|GER-15|1|1;4;00:13:48
TMD|SAM005923|FRA-76|1|1;;00:13:52
TMD|SAM005923|FRA-76|1|1;5;00:13:52
TMD|SAM005923|ESP-11|1|1;;00:13:59
TMD|SAM005923|ESP-11|1|1;6;00:13:59
TMD|SAM005923|GBR-828|1|1;;00:13:59
TMD|SAM005923|GBR-828|1|1;7;00:13:59
TMD|SAM005923|GER-84|1|1;;00:14:08
TMD|SAM005923|GER-84|1|1;8;00:14:08
TMD|SAM005923|ESP-1|1|1;;00:14:09
TMD|SAM005923|ESP-1|1|1;9;00:14:09
TMD|SAM005923|ITA-22|1|1;;00:14:11
TMD|SAM005923|ITA-22|1|1;10;00:14:11
TMD|SAM005923|FRA-20|1|1;;00:14:11
TMD|SAM005923|FRA-20|1|1;11;00:14:11
TMD|SAM005923|ITA-99|1|1;;00:14:15
TMD|SAM005923|ITA-99|1|1;12;00:14:15
TMD|SAM005923|POR-2|1|1;;00:14:16
TMD|SAM005923|POR-2|1|1;13;00:14:16
TMD|SAM005923|ESP-16|1|1;;00:14:19
TMD|SAM005923|ESP-16|1|1;14;00:14:19
TMD|SAM005923|AUT-11|1|1;;00:14:24
TMD|SAM005923|AUT-11|1|1;15;00:14:24
TMD|SAM005923|FRA-15|1|1;;00:14:27
TMD|SAM005923|FRA-15|1|1;16;00:14:27
TMD|SAM005923|ITA-25|1|1;;00:14:31
TMD|SAM005923|ITA-25|1|1;17;00:14:31
TMD|SAM005923|ESP-17|1|1;;00:14:34
TMD|SAM005923|ESP-17|1|1;18;00:14:34
TMD|SAM005923|ESP-1868|1|1;;00:14:32
TMD|SAM005923|ESP-1868|1|1;18;00:14:32
TMD|SAM005923|ESP-17|1|1;19;00:14:34
TMD|SAM005923|BEL-16|1|1;;00:14:35
TMD|SAM005923|BEL-16|1|1;20;00:14:35
TMD|SAM005923|ESP-22|1|1;;00:14:37
TMD|SAM005923|ESP-22|1|1;21;00:14:37
TMD|SAM005923|ITA-300|1|1;;00:14:37
TMD|SAM005923|ITA-300|1|1;21;00:14:37
TMD|SAM005923|ESP-22|1|1;22;00:14:37
TMD|SAM005923|AUS-7|1|1;;00:14:39
TMD|SAM005923|AUS-7|1|1;23;00:14:39
TMD|SAM005923|ITA-28|1|1;;00:14:40
TMD|SAM005923|ITA-28|1|1;24;00:14:40
TMD|SAM005923|GER-48|1|1;;00:14:42
TMD|SAM005923|GER-48|1|1;25;00:14:42
TMD|SAM005923|ITA-32|1|1;;00:15:01
TMD|SAM005923|ITA-32|1|1;26;00:15:01
TMD|SAM005923|RSA-8|1|1;;00:15:00
TMD|SAM005923|RSA-8|1|1;26;00:15:00
TMD|SAM005923|ITA-32|1|1;27;00:15:01
TMD|SAM005923|GER-13|1|1;;00:15:02
TMD|SAM005923|GER-13|1|1;28;00:15:02
TMD|SAM005923|UKR-11|1|1;;00:15:25
TMD|SAM005923|UKR-11|1|1;29;00:15:25
TMD|SAM005923|AUT-477|1|1;;00:15:37
TMD|SAM005923|AUT-477|1|1;30;00:15:37
TMD|SAM005923|RUS-100|1|1;;00:15:52
TMD|SAM005923|RUS-100|1|1;31;00:15:52
TMD|SAM005923|ESP-18|1|1;;00:16:07
TMD|SAM005923|ESP-18|1|1;32;00:16:07
TMD|SAM005923|GBR-843|1|2;;00:20:33
TMD|SAM005923|GBR-843|1|2;1;00:20:33
TMD|SAM005923|GER-15|1|2;;00:20:51
TMD|SAM005923|GER-15|1|2;2;00:20:51
TMD|SAM005923|FRA-76|1|2;;00:21:17
TMD|SAM005923|FRA-76|1|2;3;00:21:17
TMD|SAM005923|ITA-99|1|2;;00:21:27
TMD|SAM005923|ITA-99|1|2;4;00:21:27
TMD|SAM005923|GER-5025|1|2;;00:21:28
TMD|SAM005923|GER-5025|1|2;5;00:21:28
TMD|SAM005923|ESP-11|1|2;;00:21:29
TMD|SAM005923|ESP-11|1|2;6;00:21:29
TMD|SAM005923|ESP-1|1|2;;00:21:30
TMD|SAM005923|ESP-1|1|2;7;00:21:30
TMD|SAM005923|GER-84|1|2;;00:21:32
TMD|SAM005923|GER-84|1|2;8;00:21:32
TMD|SAM005923|FIN-111|1|2;;00:21:34
TMD|SAM005923|FIN-111|1|2;9;00:21:34
TMD|SAM005923|AUS-7|1|2;;00:21:47
TMD|SAM005923|AUS-7|1|2;10;00:21:47
TMD|SAM005923|ITA-22|1|2;;00:21:50
TMD|SAM005923|ITA-22|1|2;11;00:21:50
TMD|SAM005923|POR-2|1|2;;00:21:50
TMD|SAM005923|POR-2|1|2;12;00:21:50
TMD|SAM005923|BEL-16|1|2;;00:21:53
TMD|SAM005923|BEL-16|1|2;13;00:21:53
TMD|SAM005923|FRA-20|1|2;;00:21:46
TMD|SAM005923|FRA-20|1|2;10;00:21:46
TMD|SAM005923|AUS-7|1|2;11;00:21:47
TMD|SAM005923|ITA-22|1|2;12;00:21:50
TMD|SAM005923|POR-2|1|2;13;00:21:50
TMD|SAM005923|BEL-16|1|2;14;00:21:53
TMD|SAM005923|ESP-22|1|2;;00:21:57
TMD|SAM005923|ESP-22|1|2;15;00:21:57
TMD|SAM005923|FRA-15|1|2;;00:22:00
TMD|SAM005923|FRA-15|1|2;16;00:22:00
TMD|SAM005923|ITA-300|1|2;;00:22:06
TMD|SAM005923|ITA-300|1|2;17;00:22:06
TMD|SAM005923|ESP-17|1|2;;00:22:09
TMD|SAM005923|ESP-17|1|2;18;00:22:09
TMD|SAM005923|GER-48|1|2;;00:22:09
TMD|SAM005923|GER-48|1|2;19;00:22:09
TMD|SAM005923|ITA-25|1|2;;00:22:11
TMD|SAM005923|ITA-25|1|2;20;00:22:11
TMD|SAM005923|ITA-28|1|2;;00:22:13
TMD|SAM005923|ITA-28|1|2;21;00:22:13
TMD|SAM005923|ESP-16|1|2;;00:22:13
TMD|SAM005923|ESP-16|1|2;21;00:22:13
TMD|SAM005923|ITA-28|1|2;22;00:22:13
TMD|SAM005923|AUT-11|1|2;;00:22:16
TMD|SAM005923|AUT-11|1|2;23;00:22:16
TMD|SAM005923|ESP-1868|1|2;;00:22:15
TMD|SAM005923|ESP-1868|1|2;23;00:22:15
TMD|SAM005923|AUT-11|1|2;24;00:22:16
TMD|SAM005923|GBR-828|1|2;;00:22:16
TMD|SAM005923|GBR-828|1|2;24;00:22:16
TMD|SAM005923|AUT-11|1|2;25;00:22:16
TMD|SAM005923|GER-13|1|2;;00:22:19
TMD|SAM005923|GER-13|1|2;26;00:22:19
TMD|SAM005923|ITA-32|1|2;;00:22:36
TMD|SAM005923|ITA-32|1|2;27;00:22:36
TMD|SAM005923|RSA-8|1|2;;00:23:04
TMD|SAM005923|RSA-8|1|2;28;00:23:04
TMD|SAM005923|AUT-477|1|2;;00:23:05
TMD|SAM005923|AUT-477|1|2;29;00:23:05
TMD|SAM005923|UKR-11|1|2;;00:23:09
TMD|SAM005923|UKR-11|1|2;30;00:23:09
TMD|SAM005923|RUS-100|1|2;;00:23:39
TMD|SAM005923|RUS-100|1|2;31;00:23:39
TMD|SAM005923|ESP-18|1|2;;00:23:43
TMD|SAM005923|ESP-18|1|2;32;00:23:43
TMD|SAM005923|GBR-843|1|3;;00:32:27
TMD|SAM005923|GBR-843|1|3;1;00:32:27
TMD|SAM005923|GER-15|1|3;;00:32:58
TMD|SAM005923|GER-15|1|3;2;00:32:58
TMD|SAM005923|FRA-76|1|3;;00:33:18
TMD|SAM005923|FRA-76|1|3;3;00:33:18
TMD|SAM005923|GER-84|1|3;;00:33:48
TMD|SAM005923|GER-84|1|3;4;00:33:48
TMD|SAM005923|GER-5025|1|3;;00:33:51
TMD|SAM005923|GER-5025|1|3;5;00:33:51
TMD|SAM005923|ESP-11|1|3;;00:34:00
TMD|SAM005923|ESP-11|1|3;6;00:34:00
TMD|SAM005923|ITA-99|1|3;;00:34:00
TMD|SAM005923|ITA-99|1|3;7;00:34:00
TMD|SAM005923|GBR-843|1|4;;00:34:12
TMD|SAM005923|GBR-843|1|4;1;00:34:12
TMD|SAM005923|AUS-7|1|3;;00:34:12
TMD|SAM005923|AUS-7|1|3;8;00:34:12
TMD|SAM005923|FIN-111|1|3;;00:34:18
TMD|SAM005923|FIN-111|1|3;9;00:34:18
TMD|SAM005923|FRA-20|1|3;;00:34:20
TMD|SAM005923|FRA-20|1|3;10;00:34:20
TMD|SAM005923|POR-2|1|3;;00:34:23
TMD|SAM005923|POR-2|1|3;11;00:34:23
TMD|SAM005923|FRA-15|1|3;;00:34:23
TMD|SAM005923|FRA-15|1|3;11;00:34:23
TMD|SAM005923|POR-2|1|3;12;00:34:23
TMD|SAM005923|ESP-1|1|3;;00:34:29
TMD|SAM005923|ESP-1|1|3;13;00:34:29
TMD|SAM005923|ITA-22|1|3;;00:34:32
TMD|SAM005923|ITA-22|1|3;14;00:34:32
TMD|SAM005923|ESP-22|1|3;;00:34:30
TMD|SAM005923|ESP-22|1|3;14;00:34:30
TMD|SAM005923|ITA-22|1|3;15;00:34:32
TMD|SAM005923|GER-15|1|4;;00:34:42
TMD|SAM005923|GER-15|1|4;2;00:34:42
TMD|SAM005923|ITA-300|1|3;;00:34:46
TMD|SAM005923|ITA-300|1|3;16;00:34:46
TMD|SAM005923|BEL-16|1|3;;00:34:47
TMD|SAM005923|BEL-16|1|3;17;00:34:47
TMD|SAM005923|ITA-28|1|3;;00:34:50
TMD|SAM005923|ITA-28|1|3;18;00:34:50
TMD|SAM005923|FRA-76|1|4;;00:35:04
TMD|SAM005923|FRA-76|1|4;3;00:35:04
TMD|SAM005923|ITA-25|1|3;;00:35:04
TMD|SAM005923|ITA-25|1|3;19;00:35:04
TMD|SAM005923|AUT-11|1|3;;00:35:14
TMD|SAM005923|AUT-11|1|3;20;00:35:14
TMD|SAM005923|GER-48|1|3;;00:35:13
TMD|SAM005923|GER-48|1|3;20;00:35:13
TMD|SAM005923|AUT-11|1|3;21;00:35:14
TMD|SAM005923|GER-13|1|3;;00:35:27
TMD|SAM005923|GER-13|1|3;22;00:35:27
TMD|SAM005923|GER-84|1|4;;00:35:31
TMD|SAM005923|GER-84|1|4;4;00:35:31
TMD|SAM005923|ITA-32|1|3;;00:35:32
TMD|SAM005923|ITA-32|1|3;23;00:35:32
TMD|SAM005923|ESP-17|1|3;;00:35:37
TMD|SAM005923|ESP-17|1|3;24;00:35:37
TMD|SAM005923|GER-5025|1|4;;00:35:37
TMD|SAM005923|GER-5025|1|4;5;00:35:37
TMD|SAM005923|ESP-1868|1|3;;00:35:42
TMD|SAM005923|ESP-1868|1|3;25;00:35:42
TMD|SAM005923|ITA-99|1|4;;00:35:51
TMD|SAM005923|ITA-99|1|4;6;00:35:51
TMD|SAM005923|ESP-11|1|4;;00:35:58
TMD|SAM005923|ESP-11|1|4;7;00:35:58
TMD|SAM005923|ESP-16|1|3;;00:35:59
TMD|SAM005923|ESP-16|1|3;26;00:35:59
TMD|SAM005923|GBR-828|1|3;;00:36:02
TMD|SAM005923|GBR-828|1|3;27;00:36:02
TMD|SAM005923|AUS-7|1|4;;00:36:05
TMD|SAM005923|AUS-7|1|4;8;00:36:05
TMD|SAM005923|FIN-111|1|4;;00:36:19
TMD|SAM005923|FIN-111|1|4;9;00:36:19
TMD|SAM005923|RSA-8|1|3;;00:36:20
TMD|SAM005923|RSA-8|1|3;28;00:36:20
TMD|SAM005923|FRA-20|1|4;;00:36:23
TMD|SAM005923|FRA-20|1|4;10;00:36:23
TMD|SAM005923|RUS-100|1|3;;00:36:23
TMD|SAM005923|RUS-100|1|3;29;00:36:23
TMD|SAM005923|FRA-15|1|4;;00:36:25
TMD|SAM005923|FRA-15|1|4;11;00:36:25
TMD|SAM005923|ESP-22|1|4;;00:36:29
TMD|SAM005923|ESP-22|1|4;12;00:36:29
TMD|SAM005923|AUT-477|1|3;;00:36:29
TMD|SAM005923|AUT-477|1|3;30;00:36:29
TMD|SAM005923|POR-2|1|4;;00:36:30
TMD|SAM005923|POR-2|1|4;13;00:36:30
TMD|SAM005923|ESP-1|1|4;;00:36:32
TMD|SAM005923|ESP-1|1|4;14;00:36:32
TMD|SAM005923|ITA-22|1|4;;00:36:35
TMD|SAM005923|ITA-22|1|4;15;00:36:35
TMD|SAM005923|BEL-16|1|4;;00:36:42
TMD|SAM005923|BEL-16|1|4;16;00:36:42
TMD|SAM005923|UKR-11|1|3;;00:36:40
TMD|SAM005923|UKR-11|1|3;31;00:36:40
TMD|SAM005923|ITA-300|1|4;;00:36:55
TMD|SAM005923|ITA-300|1|4;17;00:36:55
TMD|SAM005923|ITA-28|1|4;;00:36:55
TMD|SAM005923|ITA-28|1|4;17;00:36:55
TMD|SAM005923|ITA-300|1|4;18;00:36:55
TMD|SAM005923|ITA-25|1|4;;00:36:55
TMD|SAM005923|ITA-25|1|4;18;00:36:55
TMD|SAM005923|ITA-300|1|4;19;00:36:55
TMD|SAM005923|ESP-18|1|3;;00:37:15
TMD|SAM005923|ESP-18|1|3;32;00:37:15
TMD|SAM005923|AUT-11|1|4;;00:37:20
TMD|SAM005923|AUT-11|1|4;20;00:37:20
TMD|SAM005923|GER-48|1|4;;00:37:26
TMD|SAM005923|GER-48|1|4;21;00:37:26
TMD|SAM005923|GER-13|1|4;;00:37:26
TMD|SAM005923|GER-13|1|4;22;00:37:26
TMD|SAM005923|ITA-32|1|4;;00:37:37
TMD|SAM005923|ITA-32|1|4;23;00:37:37
TMD|SAM005923|ESP-17|1|4;;00:37:46
TMD|SAM005923|ESP-17|1|4;24;00:37:46
TMD|SAM005923|ESP-1868|1|4;;00:37:54
TMD|SAM005923|ESP-1868|1|4;25;00:37:54
TMD|SAM005923|ESP-16|1|4;;00:38:10
TMD|SAM005923|ESP-16|1|4;26;00:38:10
TMD|SAM005923|GBR-828|1|4;;00:38:17
TMD|SAM005923|GBR-828|1|4;27;00:38:17
TMD|SAM005923|RSA-8|1|4;;00:38:22
TMD|SAM005923|RSA-8|1|4;28;00:38:22
TMD|SAM005923|RUS-100|1|4;;00:38:36
TMD|SAM005923|RUS-100|1|4;29;00:38:36
TMD|SAM005923|AUT-477|1|4;;00:38:41
TMD|SAM005923|AUT-477|1|4;30;00:38:41
TMD|SAM005923|UKR-11|1|4;;00:39:22
TMD|SAM005923|UKR-11|1|4;31;00:39:22
TMD|SAM005923|ESP-18|1|4;;00:39:46
TMD|SAM005923|ESP-18|1|4;32;00:39:46
TMD|SAM005923|GBR-843|1|5;;00:43:36
TMD|SAM005923|GBR-843|1|5;1;00:43:36
TMD|SAM005923|GER-15|1|5;;00:44:02
TMD|SAM005923|GER-15|1|5;2;00:44:02
TMD|SAM005923|FRA-76|1|5;;00:44:19
TMD|SAM005923|FRA-76|1|5;3;00:44:19
TMD|SAM005923|GBR-843|1|6;;00:44:49
TMD|SAM005923|GBR-843|1|6;1;00:44:49
TMD|SAM005923|GER-84|1|5;;00:44:55
TMD|SAM005923|GER-84|1|5;4;00:44:55
TMD|SAM005923|ITA-99|1|5;;00:44:54
TMD|SAM005923|ITA-99|1|5;4;00:44:54
TMD|SAM005923|GER-84|1|5;5;00:44:55
TMD|SAM005923|AUS-7|1|5;;00:44:59
TMD|SAM005923|AUS-7|1|5;6;00:44:59
TMD|SAM005923|GER-5025|1|5;;00:45:03
TMD|SAM005923|GER-5025|1|5;7;00:45:03
TMD|SAM005923|GER-15|1|6;;00:45:07
TMD|SAM005923|GER-15|1|6;2;00:45:07
TMD|SAM005923|POR-2|1|5;;00:45:17
TMD|SAM005923|POR-2|1|5;8;00:45:17
TMD|SAM005923|ESP-11|1|5;;00:45:24
TMD|SAM005923|ESP-11|1|5;9;00:45:24
TMD|SAM005923|FRA-76|1|6;;00:45:26
TMD|SAM005923|FRA-76|1|6;3;00:45:26
TMD|SAM005923|FRA-15|1|5;;00:45:33
TMD|SAM005923|FRA-15|1|5;10;00:45:33
TMD|SAM005923|BEL-16|1|5;;00:45:41
TMD|SAM005923|BEL-16|1|5;11;00:45:41
TMD|SAM005923|ITA-25|1|5;;00:45:48
TMD|SAM005923|ITA-25|1|5;12;00:45:48
TMD|SAM005923|FIN-111|1|5;;00:45:51
TMD|SAM005923|FIN-111|1|5;13;00:45:51
TMD|SAM005923|ESP-22|1|5;;00:45:55
TMD|SAM005923|ESP-22|1|5;14;00:45:55
TMD|SAM005923|FRA-20|1|5;;00:45:57
TMD|SAM005923|FRA-20|1|5;15;00:45:57
TMD|SAM005923|ITA-22|1|5;;00:46:00
TMD|SAM005923|ITA-22|1|5;16;00:46:00
TMD|SAM005923|ITA-99|1|6;;00:46:03
TMD|SAM005923|ITA-99|1|6;4;00:46:03
TMD|SAM005923|ITA-300|1|5;;00:46:05
TMD|SAM005923|ITA-300|1|5;17;00:46:05
TMD|SAM005923|ESP-1|1|5;;00:46:08
TMD|SAM005923|ESP-1|1|5;18;00:46:08
TMD|SAM005923|GER-84|1|6;;00:46:08
TMD|SAM005923|GER-84|1|6;5;00:46:08
TMD|SAM005923|AUS-7|1|6;;00:46:11
TMD|SAM005923|AUS-7|1|6;6;00:46:11
TMD|SAM005923|GER-5025|1|6;;00:46:14
TMD|SAM005923|GER-5025|1|6;7;00:46:14
TMD|SAM005923|POR-2|1|6;;00:46:26
TMD|SAM005923|POR-2|1|6;8;00:46:26
TMD|SAM005923|ITA-28|1|5;;00:46:26
TMD|SAM005923|ITA-28|1|5;19;00:46:26
TMD|SAM005923|GER-13|1|5;;00:46:29
TMD|SAM005923|GER-13|1|5;20;00:46:29
TMD|SAM005923|ESP-11|1|6;;00:46:37
TMD|SAM005923|ESP-11|1|6;9;00:46:37
TMD|SAM005923|AUT-11|1|5;;00:46:50
TMD|SAM005923|AUT-11|1|5;21;00:46:50
TMD|SAM005923|ITA-32|1|5;;00:46:51
TMD|SAM005923|ITA-32|1|5;22;00:46:51
TMD|SAM005923|BEL-16|1|6;;00:46:52
TMD|SAM005923|BEL-16|1|6;10;00:46:52
TMD|SAM005923|FRA-15|1|6;;00:46:58
TMD|SAM005923|FRA-15|1|6;11;00:46:58
TMD|SAM005923|ITA-25|1|6;;00:46:59
TMD|SAM005923|ITA-25|1|6;12;00:46:59
TMD|SAM005923|ESP-1868|1|5;;00:46:59
TMD|SAM005923|ESP-1868|1|5;23;00:46:59
TMD|SAM005923|ESP-17|1|5;;00:47:04
TMD|SAM005923|ESP-17|1|5;24;00:47:04
TMD|SAM005923|FIN-111|1|6;;00:47:03
TMD|SAM005923|FIN-111|1|6;13;00:47:03
TMD|SAM005923|ESP-22|1|6;;00:47:07
TMD|SAM005923|ESP-22|1|6;14;00:47:07
TMD|SAM005923|FRA-20|1|6;;00:47:14
TMD|SAM005923|FRA-20|1|6;15;00:47:14
TMD|SAM005923|GER-48|1|5;;00:47:17
TMD|SAM005923|GER-48|1|5;25;00:47:17
TMD|SAM005923|ITA-300|1|6;;00:47:17
TMD|SAM005923|ITA-300|1|6;16;00:47:17
TMD|SAM005923|ITA-22|1|6;;00:47:20
TMD|SAM005923|ITA-22|1|6;17;00:47:20
TMD|SAM005923|ESP-1|1|6;;00:47:23
TMD|SAM005923|ESP-1|1|6;18;00:47:23
TMD|SAM005923|GBR-828|1|5;;00:47:26
TMD|SAM005923|GBR-828|1|5;26;00:47:26
TMD|SAM005923|ESP-16|1|5;;00:47:34
TMD|SAM005923|ESP-16|1|5;27;00:47:34
TMD|SAM005923|ITA-28|1|6;;00:47:39
TMD|SAM005923|ITA-28|1|6;19;00:47:39
TMD|SAM005923|RSA-8|1|5;;00:47:39
TMD|SAM005923|RSA-8|1|5;28;00:47:39
TMD|SAM005923|GER-13|1|6;;00:47:49
TMD|SAM005923|GER-13|1|6;20;00:47:49
TMD|SAM005923|AUT-11|1|6;;00:48:01
TMD|SAM005923|AUT-11|1|6;21;00:48:01
TMD|SAM005923|AUT-477|1|5;;00:48:03
TMD|SAM005923|AUT-477|1|5;29;00:48:03
TMD|SAM005923|ITA-32|1|6;;00:48:06
TMD|SAM005923|ITA-32|1|6;22;00:48:06
TMD|SAM005923|ESP-1868|1|6;;00:48:13
TMD|SAM005923|ESP-1868|1|6;23;00:48:13
TMD|SAM005923|RUS-100|1|5;;00:48:17
TMD|SAM005923|RUS-100|1|5;30;00:48:17
TMD|SAM005923|ESP-17|1|6;;00:48:19
TMD|SAM005923|ESP-17|1|6;24;00:48:19
TMD|SAM005923|GER-48|1|6;;00:48:28
TMD|SAM005923|GER-48|1|6;25;00:48:28
TMD|SAM005923|ESP-18|1|5;;00:48:42
TMD|SAM005923|ESP-18|1|5;31;00:48:42
TMD|SAM005923|GBR-828|1|6;;00:48:44
TMD|SAM005923|GBR-828|1|6;26;00:48:44
TMD|SAM005923|UKR-11|1|5;;00:48:44
TMD|SAM005923|UKR-11|1|5;32;00:48:44
TMD|SAM005923|ESP-16|1|6;;00:48:49
TMD|SAM005923|ESP-16|1|6;27;00:48:49
TMD|SAM005923|RSA-8|1|6;;00:48:50
TMD|SAM005923|RSA-8|1|6;28;00:48:50
TMD|SAM005923|AUT-477|1|6;;00:49:23
TMD|SAM005923|AUT-477|1|6;29;00:49:23
TMD|SAM005923|RUS-100|1|6;;00:49:34
TMD|SAM005923|RUS-100|1|6;30;00:49:34
TMD|SAM005923|ESP-18|1|6;;00:50:01
TMD|SAM005923|ESP-18|1|6;31;00:50:01
TMD|SAM005923|UKR-11|1|6;;00:50:06
TMD|SAM005923|UKR-11|1|6;32;00:50:06
TMD|SAM005923|AUT-11|1|0;;00:00:00
TMD|SAM005923|AUT-477|1|0;;00:00:00
TMD|SAM005923|BEL-16|1|0;;00:00:00
TMD|SAM005923|ESP-1|1|0;;00:00:00
TMD|SAM005923|AUT-11|1|0;;00:00:00
TMD|SAM005923|AUT-477|1|0;;00:00:00
RPD|SAM005923|0|2013-04-04T10:35:24+02:00|||||55|FB;4;2;39.514533;2.721949;0.8;;;256;;;;;|FP;4;2;39.512227;2.724561;0.4;;;146;;;;;|J1;3;-1;39.499894;2.750856;0.2;;;172;;;;;|J2;3;-1;39.499831;2.750965;0.0;;;32;;;;;|M1;5;2;39.526214;2.706992;0.4;;;138;;;;;|M2;5;2;39.518901;2.707612;11.9;;;316;;;;;|M3p;5;2;39.512236;2.724599;0.6;;;140;;;;;|M3s;5;378;39.513290;2.723822;1.2;;;240;;;;;|M4p;5;2;39.518983;2.720770;0.4;;;54;;;;;|M4s;5;2;39.518967;2.720791;0.4;;;60;;;;;|R1;4;2;39.518919;2.707606;10.9;;;322;;;;;|R2;4;2;39.526186;2.706922;0.6;;;124;;;;;|R3;4;0;39.499793;2.750926;0.0;;;334;;;;;|S1;5;2;39.526193;2.706945;0.4;;;140;;;;;|S2;5;2;39.518899;2.707619;11.1;;;320;;;;;|S3p;5;;;;;;;;;;;;|S3s;5;;;;;;;;;;;;|S4p;5;;;;;;;;;;;;|S4s;5;;;;;;;;;;;;|SB;2;0;39.519168;2.721933;0.4;;;280;;;;;|SP;4;0;39.517652;2.719715;0.8;;;116;;;;;|AUS-7;1;;;;;;;;;;;;|AUT-11;1;;;;;;;;;;;;|AUT-477;1;;;;;;;;;;;;|BEL-16;1;;;;;;;;;;;;|ESP-1;1;;;;;;;;;;;;|ESP-11;1;;;;;;;;;;;;|ESP-13;1;;;;;;;;;;;;|ESP-16;1;;;;;;;;;;;;|ESP-17;1;;;;;;;;;;;;|ESP-18;1;;;;;;;;;;;;|ESP-1868;1;;;;;;;;;;;;|ESP-22;1;;;;;;;;;;;;|FIN-111;1;;;;;;;;;;;;|FRA-15;1;;;;;;;;;;;;|FRA-20;1;;;;;;;;;;;;|FRA-76;1;;;;;;;;;;;;|GBR-828;1;;;;;;;;;;;;|GBR-843;1;;;;;;;;;;;;|GER-13;1;;;;;;;;;;;;|GER-15;1;;;;;;;;;;;;|GER-48;1;;;;;;;;;;;;|GER-5025;1;;;;;;;;;;;;|GER-84;1;;;;;;;;;;;;|ITA-22;1;;;;;;;;;;;;|ITA-25;1;;;;;;;;;;;;|ITA-28;1;;;;;;;;;;;;|ITA-300;1;;;;;;;;;;;;|ITA-32;1;;;;;;;;;;;;|ITA-99;1;;;;;;;;;;;;|POR-2;1;;;;;;;;;;;;|RSA-8;1;;;;;;;;;;;;|RUS-100;1;;;;;;;;;;;;|UKR-11;1;;;;;;;;;;;;|UKR-23;1;;;;;;;;;;;;
RPD|SAM005923|3|2013-04-04T14:53:29+02:00|13:45:00|01:08:29|||55|FB;4;1;39.514595;2.726782;0.6;;;352;;;;;|FP;4;1;39.514796;2.727207;0.6;;;52;;;;;|J1;3;0;39.515297;2.721503;2.1;;;332;;;;;|J2;3;0;39.516209;2.721166;4.1;;;334;;;;;|M1;5;1;39.507557;2.700071;0.2;;;22;;;;;|M2;5;1;39.503624;2.704863;0.4;;;274;;;;;|M3p;5;1;39.511114;2.726937;0.6;;;268;;;;;|M3s;5;1;39.510720;2.727125;1.6;;;54;;;;;|M4p;5;1;39.514059;2.718241;1.4;;;110;;;;;|M4s;5;1;39.514376;2.718104;0.4;;;278;;;;;|R1;4;1;39.502029;2.704981;5.4;;;310;;;;;|R2;4;1;39.503993;2.703844;1.0;;;148;;;;;|R3;4;1;39.516238;2.721159;4.5;;;344;;;;;|S1;5;1;39.503955;2.703858;1.6;;;138;;;;;|S2;5;13;39.501492;2.706392;0.2;;;102;;;;;|S3p;5;;;;;;;;;;;;|S3s;5;;;;;;;;;;;;|S4p;5;;;;;;;;;;;;|S4s;5;;;;;;;;;;;;|SB;2;-1;39.515923;2.720501;0.2;;;160;;;;;|SP;4;1;39.512957;2.721845;0.4;;;22;;;;;|AUS-7;1;1;39.515843;2.721024;6.8;;;0;;6;0;;|AUT-11;1;0;39.516144;2.720703;0.8;;;208;;21;0;;|AUT-477;1;1;39.515635;2.720475;4.7;;;312;;29;0;;|BEL-16;1;0;39.515838;2.720738;2.3;;;224;;10;0;;|ESP-1;1;3;39.515593;2.720689;3.5;;;198;;18;0;;|ESP-11;1;0;39.516054;2.720826;5.1;;;308;;9;0;;|ESP-13;1;;;;;;;;;;;;|ESP-16;1;1;39.515497;2.721003;4.9;;;124;;27;0;;|ESP-17;1;0;39.516150;2.720747;2.5;;;204;;24;0;;|ESP-18;1;1;39.514838;2.721314;4.7;;;170;;31;0;;|ESP-1868;1;1;39.500431;2.749792;0.0;;;258;;23;0;;|ESP-22;1;1;39.516085;2.720959;4.7;;;328;;14;0;;|FIN-111;1;0;39.515651;2.721038;2.1;;;290;;13;0;;|FRA-15;1;1;39.515935;2.720659;1.6;;;178;;11;0;;|FRA-20;1;3;39.516128;2.720589;7.0;;;136;;15;0;;|FRA-76;1;1;39.516355;2.720682;6.4;;;162;;3;0;;|GBR-828;1;1;39.514721;2.721544;6.0;;;334;;26;0;;|GBR-843;1;1;39.514117;2.721600;7.2;;;330;;1;0;;|GER-13;1;-1;39.515559;2.721045;2.7;;;276;;20;0;;|GER-15;1;1;39.514951;2.721463;5.6;;;336;;2;0;;|GER-48;1;1;39.514667;2.721653;2.1;;;182;;25;0;;|GER-5025;1;1;39.515795;2.720919;2.9;;;270;;7;0;;|GER-84;1;1;39.516074;2.720768;1.7;;;280;;5;0;;|ITA-22;1;0;39.516090;2.720580;1.9;;;120;;17;0;;|ITA-25;1;1;39.515364;2.720907;1.6;;;188;;12;0;;|ITA-28;1;1;39.516405;2.720717;6.8;;;318;;19;0;;|ITA-300;1;0;39.515867;2.720828;4.1;;;182;;16;0;;|ITA-32;1;1;39.515687;2.720687;3.1;;;330;;22;0;;|ITA-99;1;-1;39.515737;2.720794;0.8;;;286;;4;0;;|POR-2;1;1;39.515852;2.720619;0.2;;;162;;8;0;;|RSA-8;1;1;39.515505;2.720828;6.6;;;20;;28;0;;|RUS-100;1;0;39.515896;2.720777;3.7;;;178;;30;0;;|UKR-11;1;1;39.516422;2.720378;3.1;;;174;;32;0;;|UKR-23;1;1;39.501535;2.750179;0.0;0.0;1.1;168;1;33;10221;3713;BFD
@@ -9,9 +9,12 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.NavigableSet;
import java.util.Set;
import java.util.logging.Logger;
@@ -43,6 +46,7 @@ import com.sap.sailing.domain.tracking.DynamicTrackedRegatta;
import com.sap.sailing.domain.tracking.GPSFix;
import com.sap.sailing.domain.tracking.GPSFixMoving;
import com.sap.sailing.domain.tracking.GPSFixTrack;
import com.sap.sailing.domain.tracking.MarkPassing;
import com.sap.sailing.domain.tracking.RacesHandle;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.impl.EmptyWindStore;
@@ -301,6 +305,61 @@ public class EndToEndListeningStoreAndFowardTest {
assertEquals(7, Util.size(waypoints));
}
@Test
public void testTMDMessageBeforeReceivingStartTime() throws Exception {
final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
String[] racesToTrack = new String[] { "SAM005923" };
setUpUsingScript(racesToTrack, "/TMDBeforeStartTimeExample.txt");
Set<TrackedRace> allTrackedRaces = new HashSet<TrackedRace>();
Iterable<Regatta> allEvents = racingEventService.getAllRegattas();
for (Regatta event : allEvents) {
DynamicTrackedRegatta trackedRegatta = racingEventService.getTrackedRegatta(event);
Iterable<TrackedRace> trackedRaces = trackedRegatta.getTrackedRaces();
for (TrackedRace trackedRace : trackedRaces) {
allTrackedRaces.add(trackedRace);
}
}
assertEquals(1, allTrackedRaces.size());
TrackedRace trackedRace = allTrackedRaces.iterator().next();
assertEquals("SAM005923", trackedRace.getRace().getName());
assertEquals(dateFormat.parse("2013-04-04T13:45:00+0200"), trackedRace.getStartOfRace().asDate());
for (Competitor competitor : trackedRace.getRace().getCompetitors()) {
if (competitor.getBoat().getSailID().equals("GBR-828")) {
NavigableSet<MarkPassing> markPassings = trackedRace.getMarkPassings(competitor);
assertEquals(1, markPassings.size());
assertNotNull(markPassings.iterator().next().getTimePoint());
}
}
}
@Test
public void testTMDMessageBeforeReceivingStartTimeWithManyTMDs() throws Exception {
final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
String[] racesToTrack = new String[] { "SAM005923" };
setUpUsingScript(racesToTrack, "/TMDBeforeStartTimeExample_ManyTMDs.txt");
Set<TrackedRace> allTrackedRaces = new HashSet<TrackedRace>();
Iterable<Regatta> allEvents = racingEventService.getAllRegattas();
for (Regatta event : allEvents) {
DynamicTrackedRegatta trackedRegatta = racingEventService.getTrackedRegatta(event);
Iterable<TrackedRace> trackedRaces = trackedRegatta.getTrackedRaces();
for (TrackedRace trackedRace : trackedRaces) {
allTrackedRaces.add(trackedRace);
}
}
assertEquals(1, allTrackedRaces.size());
TrackedRace trackedRace = allTrackedRaces.iterator().next();
assertEquals("SAM005923", trackedRace.getRace().getName());
assertEquals(dateFormat.parse("2013-04-04T13:45:00+0200"), trackedRace.getStartOfRace().asDate());
for (Competitor competitor : trackedRace.getRace().getCompetitors()) {
NavigableSet<MarkPassing> markPassings = trackedRace.getMarkPassings(competitor);
if (competitor.getBoat().getSailID().equals("GBR-828")) {
assertEquals(7, markPassings.size());
}
assertNotNull(markPassings.iterator().next().getTimePoint());
assertEquals(trackedRace.getStartOfRace(), markPassings.iterator().next().getTimePoint());
}
}
@Test
public void testRongRaceLogRACZero() throws Exception{
String[] racesToTrack = new String[] { "W4702" };
@@ -121,8 +121,8 @@ public class DomainFactoryImpl implements DomainFactory {
return baseDomainFactory.getExistingCompetitorById(getCompetitorID(boatID, boatClass));
}
private Pair<String, BoatClass> getCompetitorID(String boatID, BoatClass boatClass) {
return new Util.Pair<String, BoatClass>(boatID, boatClass);
private String getCompetitorID(String boatID, BoatClass boatClass) {
return boatID + "/" + boatClass.getName();
}
@Override
@@ -22,6 +22,7 @@ import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sap.sailing.domain.base.SpeedWithBearing;
@@ -55,7 +56,7 @@ import com.sap.sailing.domain.swisstimingadapter.TrackerType;
/**
* Implements the connector to the SwissTiming Sail Master system. It uses a host name and port number to establish the
* connecting via TCP. The connector offers a number of explicit service request methods. Additionally, the connector
* connection via TCP. The connector offers a number of explicit service request methods. Additionally, the connector
* can receive "spontaneous" events sent by the sail master system. Clients can register for those spontaneous events
* (see {@link #addSailMasterListener}).
* <p>
@@ -182,6 +183,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
if (message.getSequenceNumber() != null) {
maxSequenceNumber = message.getSequenceNumber();
}
i++;
notifyListenersStoredDataProgress(raceID, ((double) i)/(double) messages.size());
}
// now process the buffered messages one by one:
@@ -222,53 +224,65 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
public void run() {
try {
while (!stopped) {
ensureSocketIsOpen();
try {
ensureSocketIsOpen();
Pair<String, Long> receivedMessageAndOptionalSequenceNumber = receiveMessage(socket.getInputStream());
SailMasterMessage message = new SailMasterMessageImpl(
receivedMessageAndOptionalSequenceNumber.getA(),
receivedMessageAndOptionalSequenceNumber.getB());
// drop race-specific messages for non-tracked races
if (!message.getType().isRaceSpecific() || idsOfTrackedRaces.contains(message.getRaceID())) {
boolean messageProcessed = false;
synchronized (this) {
if (message.getType().isRaceSpecific() && isCurrentlyBuffering(message.getRaceID())) {
buffer(message);
messageProcessed = true;
}
if (receivedMessageAndOptionalSequenceNumber == null) {
// reached EOF; this means the socket is or can be closed
if (socket != null && !socket.isClosed()) {
socket.close();
}
if (!messageProcessed) {
if (message.isResponse()) {
// this is a response for an explicit request
rendevouz(message);
} else if (message.isEvent()) {
// only notify if it hasn't been loaded from a store yet
if (!message.getType().isRaceSpecific() || message.getSequenceNumber() == null ||
(sequenceNumberOfLastMessageForRaceID.containsKey(message.getRaceID()) &&
message.getSequenceNumber() > sequenceNumberOfLastMessageForRaceID.get(message.getRaceID()))) {
// a spontaneous event
logger.fine("notifying message " + message);
notifyListeners(message);
} else {
logger.info("discarding already notified message " + message);
socket = null;
} else {
SailMasterMessage message = new SailMasterMessageImpl(
receivedMessageAndOptionalSequenceNumber.getA(),
receivedMessageAndOptionalSequenceNumber.getB());
// drop race-specific messages for non-tracked races
if (!message.getType().isRaceSpecific() || idsOfTrackedRaces.contains(message.getRaceID())) {
boolean messageProcessed = false;
synchronized (this) {
if (message.getType().isRaceSpecific() && isCurrentlyBuffering(message.getRaceID())) {
buffer(message);
messageProcessed = true;
}
}
if (!messageProcessed) {
if (message.isResponse()) {
// this is a response for an explicit request
rendevouz(message);
} else if (message.isEvent()) {
// only notify if it hasn't been loaded from a store yet
if (!message.getType().isRaceSpecific()
|| message.getSequenceNumber() == null
|| (sequenceNumberOfLastMessageForRaceID.containsKey(message.getRaceID()) && message
.getSequenceNumber() > sequenceNumberOfLastMessageForRaceID
.get(message.getRaceID()))) {
// a spontaneous event
logger.fine("notifying message " + message);
notifyListeners(message);
} else {
logger.info("discarding already notified message " + message);
}
}
}
}
}
if (message.getType() == MessageType._STOPSERVER) {
logger.info("SailMasterConnector received "+MessageType._STOPSERVER.name());
stop();
if (message.getType() == MessageType._STOPSERVER) {
logger.info("SailMasterConnector received " + MessageType._STOPSERVER.name());
stop();
}
}
} catch (SocketException se) {
// This occurs if the socket was closed which may mean the connector was stopped. Check in while
logger.info("Caught exception "+se+" during socket operation; setting socket to null");
socket = null;
Thread.sleep(1000); // try again in 1s
}
}
} catch (Exception e) {
logger.throwing(SailMasterConnectorImpl.class.getName(), "run", e);
logger.log(Level.SEVERE, "Exception in sail master connector "+SailMasterConnectorImpl.class.getName()+".run", e);
}
logger.info("Stopping Sail Master connector thread");
stopped = true;
}
private synchronized void buffer(SailMasterMessage message) {
@@ -304,28 +318,33 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
blockingQueue.offer(message);
}
protected void notifyListeners(SailMasterMessage message) throws ParseException {
switch (message.getType()) {
case RPD:
notifyListenersRPD(message);
break;
case RAC:
notifyListenersRAC(message);
break;
case CCG:
notifyListenersCCG(message);
break;
case STL:
notifyListenersSTL(message);
break;
case CAM:
notifyListenersCAM(message);
break;
case TMD:
notifyListenersTMD(message);
break;
default:
// ignore all other messages because there are no notification patterns for those
protected void notifyListeners(SailMasterMessage message) {
try {
switch (message.getType()) {
case RPD:
notifyListenersRPD(message);
break;
case RAC:
notifyListenersRAC(message);
break;
case CCG:
notifyListenersCCG(message);
break;
case STL:
notifyListenersSTL(message);
break;
case CAM:
notifyListenersCAM(message);
break;
case TMD:
notifyListenersTMD(message);
break;
default:
// ignore all other messages because there are no notification patterns for those
}
} catch (Exception e) {
// broken messages are ignored
logger.warning("Exception caught during parsing of message '" + message.getMessage() + "' : " + e.getMessage());
}
}
@@ -42,7 +42,7 @@ public class SailMasterLiveSimulatorConnectorImpl extends SailMasterConnectorImp
}
@Override
protected void notifyListeners(SailMasterMessage message) throws ParseException {
protected void notifyListeners(SailMasterMessage message) {
// we cache all messages here and deliver them with a different speed
bufferedMessageList.add(message);
}
@@ -5,6 +5,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.logging.Logger;
import com.sap.sailing.domain.common.impl.Util.Pair;
import com.sap.sailing.domain.swisstimingadapter.SailMasterMessage;
@@ -17,6 +18,7 @@ import com.sap.sailing.domain.swisstimingadapter.SailMasterTransceiver;
*
*/
public class SailMasterTransceiverImpl implements SailMasterTransceiver {
private static final Logger logger = Logger.getLogger(SailMasterTransceiverImpl.class.getName());
private static final byte STX = 0x02;
private static final byte ETX = 0x03;
@@ -35,6 +37,7 @@ public class SailMasterTransceiverImpl implements SailMasterTransceiver {
public synchronized void sendMessage(SailMasterMessage message, OutputStream os) throws IOException {
os.write(("" + message.getSequenceNumber()).getBytes());
sendMessage(message.getMessage(), os);
os.flush();
}
/**
@@ -59,9 +62,13 @@ public class SailMasterTransceiverImpl implements SailMasterTransceiver {
bufferForOptionalSequenceNumber.write(read);
read = inputStream.read();
}
// now either we've read an STX byte or reached EOF
String message = null;
if (read == STX) {
message = readMessage(inputStream);
} else {
// EOF; indicate by returning null
logger.info("Received EOF in SailMasterTransceiver. Returning null as message.");
}
if (bufferForOptionalSequenceNumber.size() > 0) {
sequenceNumber = getSequenceNumber(bufferForOptionalSequenceNumber);
@@ -10,9 +10,9 @@ import java.util.List;
import java.util.NavigableSet;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.Mark;
@@ -47,6 +47,7 @@ import com.sap.sailing.domain.tracking.MarkPassing;
import com.sap.sailing.domain.tracking.RaceTracker;
import com.sap.sailing.domain.tracking.RacesHandle;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.TrackedRaceStatus;
import com.sap.sailing.domain.tracking.TrackedRegattaRegistry;
import com.sap.sailing.domain.tracking.WindStore;
import com.sap.sailing.domain.tracking.WindTrack;
@@ -65,7 +66,12 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
private final Regatta regatta;
private final WindStore windStore;
/**
* Starts out as <code>null</code> and is set when the race definition has been created. When this happens, this object is
* {@link Object#notifyAll() notified}.
*/
private RaceDefinition race;
private Course course;
private StartList startList;
private DynamicTrackedRace trackedRace;
@@ -74,6 +80,13 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
private boolean loggedIgnore;
private final long delayToLiveInMillis;
/**
* For TMD messages received when there was no start time set, this message queue stores those TMD messages. When a start
* time is received, it will be sent to the queue which in turn will re-apply all not yet applied TMD messages again to this
* tracker by calling {@link #receivedTimingData(String, String, List)}.
*/
private final TMDMessageQueue tmdMessageQueue;
protected SwissTimingRaceTrackerImpl(String raceID, String hostname, int port, RaceLogStore raceLogStore, WindStore windStore,
DomainFactory domainFactory, SwissTimingFactory factory, RaceSpecificMessageLoader messageLoader,
TrackedRegattaRegistry trackedRegattaRegistry, boolean canSendRequests, long delayToLiveInMillis) throws InterruptedException,
@@ -87,6 +100,7 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
TrackedRegattaRegistry trackedRegattaRegistry, boolean canSendRequests, long delayToLiveInMillis) throws InterruptedException,
UnknownHostException, IOException, ParseException {
super();
this.tmdMessageQueue = new TMDMessageQueue(this);
this.regatta = regatta;
this.connector = factory.getOrCreateSailMasterConnector(hostname, port, messageLoader, canSendRequests);
this.domainFactory = domainFactory;
@@ -111,6 +125,10 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
@Override
public void stop() throws MalformedURLException, IOException, InterruptedException {
if (isTrackedRaceStillReachable()) {
TrackedRaceStatus newStatus = new TrackedRaceStatusImpl(TrackedRaceStatusEnum.FINISHED, 1.0);
trackedRace.setStatus(newStatus);
}
connector.removeSailMasterListener(raceID, this);
}
@@ -130,12 +148,37 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
@Override
public Set<RaceDefinition> getRaces() {
synchronized (this) {
while (race == null) {
try {
this.wait();
} catch (InterruptedException e) {
logger.log(Level.SEVERE, "Interrupted wait", e);
}
}
}
return Collections.singleton(race);
}
@Override
public Set<RaceDefinition> getRaces(long timeoutInMilliseconds) {
return Collections.singleton(race);
long start = System.currentTimeMillis();
synchronized (this) {
RaceDefinition result = race;
boolean interrupted = false;
while ((System.currentTimeMillis()-start < timeoutInMilliseconds) && !interrupted && result == null) {
try {
long timeToWait = timeoutInMilliseconds - (System.currentTimeMillis() - start);
if (timeToWait > 0) {
this.wait(timeToWait);
}
result = race;
} catch (InterruptedException e) {
interrupted = true;
}
}
return result == null ? null : Collections.singleton(result);
}
}
@Override
@@ -174,6 +217,7 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
if (this.raceID.equals(raceID)) {
if (startTime != null) {
trackedRace.setStartTimeReceived(startTime);
tmdMessageQueue.validStartTimeReceived();
}
for (Fix fix : fixes) {
GPSFixMoving gpsFix = domainFactory.createGPSFix(timePoint, fix);
@@ -236,11 +280,23 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
for (Triple<Integer, Integer, Long> markIndexRankAndTimeSinceStartInMilliseconds : markIndicesRanksAndTimesSinceStartInMilliseconds) {
Waypoint waypoint = Util.get(trackedRace.getRace().getCourse().getWaypoints(),
markIndexRankAndTimeSinceStartInMilliseconds.getA());
MillisecondsTimePoint timePoint = trackedRace.getStartOfRace() == null ? null : new MillisecondsTimePoint(
trackedRace.getStartOfRace().asMillis() + markIndexRankAndTimeSinceStartInMilliseconds.getC());
MarkPassing markPassing = domainFactory.createMarkPassing(timePoint, waypoint,
domainFactory.getCompetitorByBoatIDAndBoatClass(boatID, boatClass));
markPassingsByMarkIndex.put(markIndexRankAndTimeSinceStartInMilliseconds.getA(), markPassing);
// update mark passing only if we have a start time; guessed start times don't make sense and
// for the start line would lead subsequent calls to getStartOfRace() return that guessed start time
// which then cannot be identified as "guessed" anymore...
if (trackedRace.getStartOfRace() != null) {
final TimePoint startTime = trackedRace.getStartOfRace();
MillisecondsTimePoint timePoint = new MillisecondsTimePoint(
startTime.asMillis() + markIndexRankAndTimeSinceStartInMilliseconds.getC());
MarkPassing markPassing = domainFactory.createMarkPassing(timePoint, waypoint,
domainFactory.getCompetitorByBoatIDAndBoatClass(boatID, boatClass));
markPassingsByMarkIndex.put(markIndexRankAndTimeSinceStartInMilliseconds.getA(), markPassing);
} else {
//
logger.warning("Received mark passing with time relative to start of race "+trackedRace.getRace().getName()+
" before having received a race start time."
+ " Queueing message for re-application when a start time has been received.");
tmdMessageQueue.enqueue(raceID, boatID, markIndicesRanksAndTimesSinceStartInMilliseconds);
}
}
trackedRace.updateMarkPassings(competitor, markPassingsByMarkIndex.values());
} else {
@@ -282,11 +338,11 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
StartList oldStartList = this.startList;
this.startList = startList;
if (oldStartList == null && course != null) {
createRaceDefinition(raceID);
createRaceDefinition(raceID, course);
}
}
private void createRaceDefinition(String raceID) {
private void createRaceDefinition(String raceID, Course course) {
assert this.raceID.equals(raceID);
assert startList != null;
assert course != null;
@@ -320,15 +376,16 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
@Override
public void receivedCourseConfiguration(String raceID, Course course) {
Course oldCourse = this.course;
this.course = course;
if (trackedRace == null) {
if (oldCourse == null && startList != null) {
createRaceDefinition(raceID);
createRaceDefinition(raceID, course);
this.course = course;
}
} else {
if (isTrackedRaceStillReachable()) {
try {
domainFactory.updateCourseWaypoints(trackedRace.getRace().getCourse(), course.getMarks());
this.course = course;
} catch (PatchFailedException e) {
logger.info("Internal error trying to update course: " + e.getMessage());
logger.throwing(SwissTimingRaceTrackerImpl.class.getName(), "receivedCourseConfiguration", e);
@@ -348,4 +405,12 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
public void receivedAvailableRaces(Iterable<Race> races) {
// don't care
}
protected DynamicTrackedRace getTrackedRace() {
return trackedRace;
}
public DomainFactory getDomainFactory() {
return domainFactory;
}
}
@@ -0,0 +1,152 @@
package com.sap.sailing.domain.swisstimingadapter.impl;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Logger;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.Course;
import com.sap.sailing.domain.common.impl.Util.Triple;
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
import com.sap.sailing.domain.tracking.MarkPassing;
import com.sap.sailing.domain.tracking.TrackedRace;
/**
* When TMD messages about mark passings arrive with their race start time-relative time stamp, and no race start time
* has been received yet, the TMD message contents need to be queued and re-applied once a start time for the race was
* received. The queuing is managed by this class.
*
* @author Axel Uhl (D043530)
*
*/
public class TMDMessageQueue {
private static final Logger logger = Logger.getLogger(TMDMessageQueue.class.getName());
private static class TMDMessageContents {
private final String raceID;
private final String boatID;
private final List<Triple<Integer, Integer, Long>> markIndicesRanksAndTimesSinceStartInMilliseconds;
public TMDMessageContents(String raceID, String boatID,
List<Triple<Integer, Integer, Long>> markIndicesRanksAndTimesSinceStartInMilliseconds) {
super();
this.raceID = raceID;
this.boatID = boatID;
this.markIndicesRanksAndTimesSinceStartInMilliseconds = markIndicesRanksAndTimesSinceStartInMilliseconds;
}
public String getRaceID() {
return raceID;
}
public String getBoatID() {
return boatID;
}
public List<Triple<Integer, Integer, Long>> getMarkIndicesRanksAndTimesSinceStartInMilliseconds() {
return markIndicesRanksAndTimesSinceStartInMilliseconds;
}
@Override
public String toString() {
StringBuilder result = new StringBuilder("TMD|"+getRaceID()+"|"+getBoatID()+"|");
result.append(getMarkIndicesRanksAndTimesSinceStartInMilliseconds().size());
result.append('|');
for (Triple<Integer, Integer, Long> markIndexRankAndTimeSinceStartInMilliseconds : getMarkIndicesRanksAndTimesSinceStartInMilliseconds()) {
result.append(markIndexRankAndTimeSinceStartInMilliseconds.getA());
result.append(';');
if (markIndexRankAndTimeSinceStartInMilliseconds.getB() != null) {
result.append(markIndexRankAndTimeSinceStartInMilliseconds.getB());
}
result.append(";<");
result.append(markIndexRankAndTimeSinceStartInMilliseconds.getC());
result.append("ms>");
}
return result.toString();
}
}
private final SwissTimingRaceTrackerImpl raceTracker;
private final Set<TMDMessageContents> queuedMessages;
public TMDMessageQueue(SwissTimingRaceTrackerImpl raceTracker) {
super();
this.raceTracker = raceTracker;
queuedMessages = new HashSet<>();
}
public synchronized void enqueue(String raceID, String boatID, List<Triple<Integer, Integer, Long>> markIndicesRanksAndTimesSinceStartInMilliseconds) {
final TMDMessageContents message = new TMDMessageContents(raceID, boatID, markIndicesRanksAndTimesSinceStartInMilliseconds);
queuedMessages.add(message);
logger.info("Queued TMD message "+message+" for replay when start time has been received");
}
/**
* The tracker shall call this when a start time has been received. In case there are any messages in this queue, they will
* be re-sent to the tracker where they now can be evaluated properly. Precondition to calling this method is that the start
* time was set on the {@link TrackedRace} managed by the {@link SwissTimingRaceTrackerImpl}.
*/
public synchronized void validStartTimeReceived() {
DynamicTrackedRace trackedRace = raceTracker.getTrackedRace();
Iterator<TMDMessageContents> i = queuedMessages.iterator();
Map<Competitor, TreeMap<Integer, MarkPassing>> cleansedMarkPassings = new HashMap<>();
// first remove all mark passings that will then be updated to make sure no "guessed time" remains;
// otherwise, this could lead to the "guessed time" to
while (i.hasNext()) {
TMDMessageContents messageContents = i.next();
Competitor competitor = raceTracker.getDomainFactory().getCompetitorByBoatIDAndBoatClass(messageContents.getBoatID(),
trackedRace.getRace().getBoatClass());
NavigableSet<MarkPassing> oldMarkPassings = trackedRace.getMarkPassings(competitor);
TreeMap<Integer, MarkPassing> cleansedMarkPassingsForCompetitor = cleansedMarkPassings.get(competitor);
if (cleansedMarkPassingsForCompetitor == null) {
cleansedMarkPassingsForCompetitor = new TreeMap<>();
cleansedMarkPassings.put(competitor, cleansedMarkPassingsForCompetitor);
Course course = trackedRace.getRace().getCourse();
course.lockForRead();
try {
trackedRace.lockForRead(oldMarkPassings);
try {
for (MarkPassing oldMarkPassing : oldMarkPassings) {
int waypointIndex = course.getIndexOfWaypoint(oldMarkPassing.getWaypoint());
cleansedMarkPassingsForCompetitor.put(waypointIndex, oldMarkPassing);
}
} finally {
trackedRace.unlockAfterRead(oldMarkPassings);
}
} finally {
course.unlockAfterRead();
}
}
// remove those mark passings for which the TMD message has mark passing times; their time points would just have been guessed.
// This will avoid that the start time inference
// rules consider them and let them take precedence over the start time received
for (Triple<Integer, Integer, Long> markIndexRankAndTimeSinceStartInMilliseconds : messageContents.getMarkIndicesRanksAndTimesSinceStartInMilliseconds()) {
if (cleansedMarkPassingsForCompetitor.containsKey(markIndexRankAndTimeSinceStartInMilliseconds.getA())) {
logger.info("Removing mark passing for mark #"+markIndexRankAndTimeSinceStartInMilliseconds.getA()+
" for competitor "+competitor.getName()+" because its time point "+
cleansedMarkPassingsForCompetitor.get(markIndexRankAndTimeSinceStartInMilliseconds.getA()).getTimePoint()+
" was guessed; will replace momentarily...");
cleansedMarkPassingsForCompetitor.remove(markIndexRankAndTimeSinceStartInMilliseconds.getA());
}
}
}
for (Map.Entry<Competitor, TreeMap<Integer, MarkPassing>> e : cleansedMarkPassings.entrySet()) {
trackedRace.updateMarkPassings(e.getKey(), e.getValue().values());
}
i = queuedMessages.iterator();
while (i.hasNext()) {
TMDMessageContents messageContents = i.next();
logger.info("Re-Playing TMD message "+messageContents+" with new race start time "+trackedRace.getStartOfRace());
raceTracker.receivedTimingData(messageContents.getRaceID(), messageContents.getBoatID(),
messageContents.getMarkIndicesRanksAndTimesSinceStartInMilliseconds());
i.remove();
}
}
}
@@ -151,8 +151,8 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest {
/**
* Regarding bug 961, test scoring in a leaderboard that has a qualification series with two unordered groups where for one
* column only one group has raced (expressed by a mocked TrackedRace attached to the column). Ensure that the column doesn't
* count for neither the total points sum nor the competitor ordering.
* column only one group has raced (expressed by a mocked TrackedRace attached to the column). Ensure that the column does
* count for the total points sum but that the competitor ordering is controlled by the number of races.
*/
@Test
public void testUnorderedGroupsWithOneGroupNotHavingRacedInAColumn() throws NoWindException {
@@ -180,18 +180,21 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest {
assertEquals((double) (i+1), leaderboard.getTotalPoints(competitors.get(i), later), 0.000000001);
assertEquals((double) (i+1), leaderboard.getTotalPoints(competitors.get(i+5), later), 0.0000000001);
}
// now add one race for yellow fleet and test that it doesn't count because blue fleet is still missing its race for Q2
// now add one race for yellow fleet and test that it counts for the score but not the ordering
// because blue fleet is still missing its race for Q2
TrackedRace q2Yellow = new MockedTrackedRaceWithStartTimeAndRanks(now, competitors.subList(3, 8));
RaceColumn q2Column = qualificationSeries.getRaceColumnByName("Q2");
q2Column.setTrackedRace(q2Column.getFleetByName("Yellow"), q2Yellow);
List<Competitor> rankedCompetitorsWithOneRaceMissingInQ2 = leaderboard.getCompetitorsFromBestToWorst(later);
for (int i=0; i<5; i++) {
assertTrue(competitors.get(i) == rankedCompetitorsWithOneRaceMissingInQ2.get(2*i) ||
competitors.get(i) == rankedCompetitorsWithOneRaceMissingInQ2.get(2*i+1));
assertTrue(competitors.get(i+5) == rankedCompetitorsWithOneRaceMissingInQ2.get(2*i) ||
competitors.get(i+5) == rankedCompetitorsWithOneRaceMissingInQ2.get(2*i+1));
assertEquals((double) (i+1), leaderboard.getTotalPoints(competitors.get(i), later), 0.000000001);
assertEquals((double) (i+1), leaderboard.getTotalPoints(competitors.get(i+5), later), 0.0000000001);
// scores: C1=1, C2=2, C3=3, C4=5, C5=7, C6=4, C7=6, C8=8, C9=4, C10=5
// ordered by scores: C1, C2, C3, C6/C9, C4/C10, C7, C5, C8
// ordered primarily by number of races: C6, C4, C7, C5, C8, C1, C2, C3, C9, C10
assertEquals(Arrays.asList(new Competitor[] { competitors.get(5), competitors.get(3), competitors.get(6),
competitors.get(4), competitors.get(7), competitors.get(0), competitors.get(1), competitors.get(2),
competitors.get(8), competitors.get(9) }), rankedCompetitorsWithOneRaceMissingInQ2);
double[] points = new double[] { 1, 2, 3, 5, 7, 4, 6, 8, 4, 5 };
for (int i=0; i<9; i++) {
assertEquals(points[i], leaderboard.getTotalPoints(competitors.get(i), later), 0.000000001);
}
// now add a tracked race for the blue fleet for Q2 and assert that the Q2 scores count for the total points sum
TrackedRace q2Blue = new MockedTrackedRaceWithStartTimeAndRanks(now,
@@ -216,10 +219,13 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest {
}
/**
* Regarding bug 961, test scoring in a leaderboard that has a qualification series with two unordered groups where for one
* column only one group has raced (expressed by a mocked TrackedRace attached to the column). Then add the second tracked
* race for the second column but such that it hasn't started yet at the time point of the query. Then add a score correction,
* simulating the use of a proxy race (that doesn't ever start) and a manual score entry.
* Regarding bug 961 and 1023, test scoring in a leaderboard that has a qualification series with two unordered
* groups where for one column only one group has raced (expressed by a mocked TrackedRace attached to the column).
* Those competitors are expected to rank better than those who haven't raced because they have a greater number of
* races. Then add the second tracked race for the second column but such that it hasn't started yet at the time
* point of the query. Still, the competitors of the fleet that did race already shall be scored better. Then add a
* score correction, simulating the use of a proxy race (that doesn't ever start) and a manual score entry. Now, the
* second fleet shall be ranked according to the score comparison with the competitors from the first fleet.
*/
@Test
public void testUnorderedGroupsWithOneGroupNotHavingStartedRacedInAcolumnAndThenCorrectingScore() throws NoWindException {
@@ -251,15 +257,19 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest {
competitors.get(8), competitors.get(9) }));
q2Column.setTrackedRace(q2Column.getFleetByName("Blue"), q2Blue);
List<Competitor> rankedCompetitorsWithOneRaceMissingInQ2 = leaderboard.getCompetitorsFromBestToWorst(later);
for (int i=0; i<5; i++) {
assertTrue(competitors.get(i) == rankedCompetitorsWithOneRaceMissingInQ2.get(2*i) ||
competitors.get(i) == rankedCompetitorsWithOneRaceMissingInQ2.get(2*i+1));
assertTrue(competitors.get(i+5) == rankedCompetitorsWithOneRaceMissingInQ2.get(2*i) ||
competitors.get(i+5) == rankedCompetitorsWithOneRaceMissingInQ2.get(2*i+1));
assertEquals((double) (i+1), leaderboard.getTotalPoints(competitors.get(i), later), 0.000000001);
assertEquals((double) (i+1), leaderboard.getTotalPoints(competitors.get(i+5), later), 0.0000000001);
// scores: C1=1, C2=2, C3=3, C4=5, C5=7, C6=4, C7=6, C8=8, C9=4, C10=5
// ordered by scores: C1, C2, C3, C6/C9, C4/C10, C7, C5, C8
// ordered primarily by number of races: C6, C4, C7, C5, C8, C1, C2, C3, C9, C10
assertEquals(Arrays.asList(new Competitor[] { competitors.get(5), competitors.get(3), competitors.get(6),
competitors.get(4), competitors.get(7), competitors.get(0), competitors.get(1), competitors.get(2),
competitors.get(8), competitors.get(9) }), rankedCompetitorsWithOneRaceMissingInQ2);
double[] points = new double[] { 1, 2, 3, 5, 7, 4, 6, 8, 4, 5 };
for (int i=0; i<9; i++) {
assertEquals(points[i], leaderboard.getTotalPoints(competitors.get(i), later), 0.000000001);
}
assertFalse(leaderboard.getScoringScheme().isValidInTotalScore(leaderboard, q2Column, later));
// expect all results to be valid because a fleet will have its score counted even if not all fleets have raced;
// see discussion for bug 961, but more importantly later on bug 1023.
assertTrue(leaderboard.getScoringScheme().isValidInTotalScore(leaderboard, q2Column, later));
// now add a score correction for Q2/Blue to make it count:
leaderboard.getScoreCorrection().correctScore(competitors.get(9), q2Column, 42.);
assertTrue(leaderboard.getScoringScheme().isValidInTotalScore(leaderboard, q2Column, later));
@@ -271,8 +281,8 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest {
/**
* Regarding bug 961, test scoring in a leaderboard that has a qualification series with two unordered groups where for one
* column only one group has raced (expressed by a mocked TrackedRace attached to the column). Ensure that the column doesn't
* count for computing the number of discards.
* column only one group has raced (expressed by a mocked TrackedRace attached to the column). Those who already raced their
* second race will have one discard, the others won't. Test this.
*/
@Test
public void testDiscardsForUnorderedGroupsWithOneGroupNotHavingRacedInAColumn() throws NoWindException {
@@ -297,9 +307,14 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest {
TrackedRace q2Yellow = new MockedTrackedRaceWithStartTimeAndRanks(now, competitors.subList(3, 8));
RaceColumn q2Column = qualificationSeries.getRaceColumnByName("Q2");
q2Column.setTrackedRace(q2Column.getFleetByName("Yellow"), q2Yellow);
for (Competitor competitor : competitors) {
assertFalse("Competitor "+competitor+" has a discard in Q1 but shouldn't", leaderboard.isDiscarded(competitor, q1Column, later));
assertFalse("Competitor "+competitor+" has a discard in Q2 but shouldn't", leaderboard.isDiscarded(competitor, q2Column, later));
for (Competitor competitor : competitors.subList(3, 8)) {
assertTrue("Competitor "+competitor+" has no discard but should have one",
leaderboard.isDiscarded(competitor, q1Column, later) || leaderboard.isDiscarded(competitor, q2Column, later));
}
for (Competitor competitor : Arrays.asList(new Competitor[] { competitors.get(0), competitors.get(1), competitors.get(2),
competitors.get(8), competitors.get(9) })) {
assertFalse("Competitor "+competitor+" has a discard but should'nt have one",
leaderboard.isDiscarded(competitor, q1Column, later) || leaderboard.isDiscarded(competitor, q2Column, later));
}
// now add a tracked race for the blue fleet for Q2 and assert that all competitors have one discard
TrackedRace q2Blue = new MockedTrackedRaceWithStartTimeAndRanks(now,
@@ -0,0 +1,100 @@
package com.sap.sailing.domain.test;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.junit.Test;
import com.sap.sailing.util.impl.LockUtil;
import com.sap.sailing.util.impl.NamedReentrantReadWriteLock;
/**
* {@link ReentrantReadWriteLock} can produce a lock between readers and other readers if the lock
* is <em>fair</em> and a writer applies between the first read lock acquisition and the second read
* lock acquisition attempt. We try to enhance {@link LockUtil} such that it is possible to pass
* a "lock set" from one thread to the next, such that when a thread tries to acquire a read lock
* that is already in the thread's lock set, it doesn't really acquire the lock but only increments
* a counter. This will then avoid the read-read deadlock.
*
* @author Axel Uhl (D043530)
*
*/
public class ReadReadDeadlockTest {
@Test
public void testReadReadDeadlock() throws InterruptedException {
final Object notifier = new Object();
final NamedReentrantReadWriteLock lock = new NamedReentrantReadWriteLock("testReadReadDeadlock", /* fair */ true);
final boolean[] t1ObtainedReadLock = new boolean[1];
final Thread t1 = new Thread("t1") {
public void run() {
LockUtil.lockForRead(lock);
try {
synchronized (notifier) {
t1ObtainedReadLock[0] = true;
notifier.notifyAll();
try {
notifier.wait(); // wait for t2 to have announced acquiring the write lock
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
Thread.sleep(100); // wait until t2 really has requested the write lock
final Thread myT1 = Thread.currentThread();
// Now spawn a new thread and wait for its termination synchronously.
// This guarantees that t1 will hold on to the read lock until t3's run method
// returns. Therefore, t3 doesn't necessarily need to apply for the read lock again.
final Thread t3 = new Thread("t3") {
public void run() {
LockUtil.propagateLockSetFrom(myT1);
// without lock set propagation, this will cause a deadlock
LockUtil.lockForRead(lock);
try {
System.out.println("Got read lock in t3");
} finally {
LockUtil.unlockAfterRead(lock);
}
LockUtil.unpropagateLockSetFrom(myT1);
}
};
t3.start();
try {
t3.join(); // joins only if lock set propagation works
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} catch (InterruptedException e1) {
throw new RuntimeException(e1);
} finally {
LockUtil.unlockAfterRead(lock); // this will let t2 continue and terminate
}
}
};
final Thread t2 = new Thread("t2") {
public void run() {
// wait until t1 has obtained the read lock
synchronized (notifier) {
while (!t1ObtainedReadLock[0]) {
try {
notifier.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
try {
synchronized (notifier) {
notifier.notifyAll();
}
// safe to lock after notifying because waiters can only continue once
// this method has left the synchronized(notifier) block
LockUtil.lockForWrite(lock); // will wait for t1 which owns the read lock
} finally {
LockUtil.unlockAfterWrite(lock);
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
}
}
@@ -5,6 +5,12 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
import org.junit.Ignore;
import org.junit.Test;
import com.sap.sailing.util.impl.SmartFutureCache;
@@ -36,10 +42,20 @@ public class SmartFutureCacheTest {
final boolean[] updateWasCalled = new boolean[1];
final boolean[] cacheWasCalled = new boolean[1];
final boolean[] mayProceed = new boolean[1];
SmartFutureCache<String, String, SmartFutureCache.EmptyUpdateInterval> sfc = new SmartFutureCache<String, String, SmartFutureCache.EmptyUpdateInterval>(
new SmartFutureCache.AbstractCacheUpdater<String, String, SmartFutureCache.EmptyUpdateInterval>() {
@Override
public String computeCacheUpdate(String key, EmptyUpdateInterval updateInterval) throws Exception {
synchronized (mayProceed) {
while (!mayProceed[0]) {
try {
mayProceed.wait();
} catch (InterruptedException e) {
e.printStackTrace();
} // wait until test driver has checked the un-updated cache
}
}
updateWasCalled[0] = true;
return "Humba";
}
@@ -47,22 +63,13 @@ public class SmartFutureCacheTest {
@Override
protected void cache(String key, String value) {
super.cache(key, value);
synchronized (mayProceed) {
while (!mayProceed[0]) {
try {
cacheWasCalled.wait();
} catch (InterruptedException e) {
e.printStackTrace();
} // wait until test driver has checked the un-updated cache
}
}
synchronized (cacheWasCalled) {
cacheWasCalled[0] = true;
cacheWasCalled.notifyAll();
}
}
};
sfc.triggerUpdate("Trala", /* updateInterval */ null);
sfc.triggerUpdate("Trala", /* updateInterval */ null); // will be held up in computeCacheUpdate
assertNull(sfc.get("Trala", /* waitForLatest */ false));
synchronized (mayProceed) {
mayProceed[0] = true;
@@ -100,6 +107,11 @@ public class SmartFutureCacheTest {
public int getB() {
return b;
}
@Override
public String toString() {
return "["+getA()+", "+getB()+"]";
}
}
@Test
@@ -158,4 +170,62 @@ public class SmartFutureCacheTest {
assertTrue(updateWasCalled[0]);
assertEquals(new Integer(91), sfc.get("Trala", /* waitForLatest */ true));
}
@Ignore // used only to solve bug 1314; it's hard to test re-cycling of tasks reliably without depending on timing issues
@Test
public void testOverloadingCacheWithUpdateRequests() throws InterruptedException {
final Set<String> updateWasCalled = new ConcurrentSkipListSet<String>();
final boolean[] cacheWasCalled = new boolean[1];
final int[] computeCacheUpdateCount = new int[1];
SmartFutureCache<String, Integer, FromAToBUpdateInterval> sfc = new SmartFutureCache<String, Integer, FromAToBUpdateInterval>(
new SmartFutureCache.AbstractCacheUpdater<String, Integer, FromAToBUpdateInterval>() {
@Override
public Integer computeCacheUpdate(String key, FromAToBUpdateInterval updateInterval)
throws Exception {
updateWasCalled.add(key);
computeCacheUpdateCount[0]++;
Thread.sleep(10); // pretending a long calculation
return updateInterval.getA() + updateInterval.getB();
}
}, "SmartFutureCacheTest.testJoiningOfUpdateIntervalsWhenBeingResumed") {
@Override
protected void cache(String key, Integer value) {
super.cache(key, value);
synchronized (cacheWasCalled) {
cacheWasCalled[0] = true;
cacheWasCalled.notifyAll();
}
}
};
String[] strings = new String[100];
for (int i=0; i<strings.length; i++) {
strings[i] = Integer.toString(i);
}
Random random = new Random();
// Trigger very many updates shortly after each other, causing many re-calculations to be scheduled.
final int numberOfUpdateTriggers = 1000000;
Set<Integer> updatesTriggeredFor = new HashSet<Integer>();
for (int i=0; i<numberOfUpdateTriggers; i++) {
final int nextInt = random.nextInt(strings.length);
sfc.triggerUpdate(strings[nextInt], new FromAToBUpdateInterval(i, nextInt));
updatesTriggeredFor.add(nextInt);
}
System.out.println(updatesTriggeredFor.size());
System.out.println(updatesTriggeredFor);
System.out.println(updateWasCalled.size());
System.out.println(updateWasCalled);
System.out.println(computeCacheUpdateCount[0]);
System.out.println("Tasks re-used: "+sfc.getSmartFutureCacheTaskReuseCounter());
Thread.sleep(500);
System.out.println(updateWasCalled.size());
System.out.println(updateWasCalled);
System.out.println(computeCacheUpdateCount[0]);
System.out.println("Tasks re-used: "+sfc.getSmartFutureCacheTaskReuseCounter());
Thread.sleep(500);
System.out.println(updateWasCalled.size());
System.out.println(updateWasCalled);
System.out.println(computeCacheUpdateCount[0]);
System.out.println("Tasks re-used: "+sfc.getSmartFutureCacheTaskReuseCounter());
assertEquals(updatesTriggeredFor.size(), updateWasCalled.size());
}
}
@@ -36,7 +36,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
} catch (Exception e) {
// something went wrong during DB access; report, then use empty new wind track
logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load recorded TracTrac configurations. Check MongoDB settings.");
logger.throwing(DomainObjectFactoryImpl.class.getName(), "getTracTracConfigurations", e);
logger.log(Level.SEVERE, "getTracTracConfigurations", e);
}
return result;
}
@@ -14,6 +14,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -366,7 +367,7 @@ public class ClientParamsPHP {
return getTimePoint("RaceTrackingStartTime");
} catch (ParseException e) {
logger.info("Exception trying to parse property RaceTrackingStartTime with value "+properties.get("RaceTrackingStartTime"));
logger.throwing(ClientParamsPHP.class.getName(), "getRaceTrackingStartTime", e);
logger.log(Level.SEVERE, "getRaceTrackingStartTime", e);
return null;
}
}
@@ -376,7 +377,7 @@ public class ClientParamsPHP {
return getTimePoint("RaceTrackingEndTime");
} catch (ParseException e) {
logger.info("Exception trying to parse property RaceTrackingEndTime with value "+properties.get("RaceTrackingEndTime"));
logger.throwing(ClientParamsPHP.class.getName(), "RaceTrackingEndTime", e);
logger.log(Level.SEVERE, "RaceTrackingEndTime", e);
return null;
}
}
@@ -114,7 +114,7 @@ public class RaceCourseReceiver extends AbstractReceiverWithQueue<Route, RouteDa
}
} catch (PatchFailedException e) {
logger.log(Level.SEVERE, "Internal error updating race course "+course+": "+e.getMessage());
logger.throwing(RaceCourseReceiver.class.getName(), "handleEvent", e);
logger.log(Level.SEVERE, "handleEvent", e);
}
} else {
logger.log(Level.INFO, "Received course for non-existing race "+event.getC().getName()+". Creating RaceDefinition.");
@@ -21,6 +21,7 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -261,18 +262,21 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements
private void pollAndParseClientParamsPHP(final URL paramURL, final Simulator simulator) {
Set<RaceDefinition> raceDefinitions = getRaces();
if (raceDefinitions != null && !raceDefinitions.isEmpty()) {
logger.info("fetching paramURL "+paramURL+" to check for updates for race(s) "+getRaces());
logger.fine("Fetching paramURL "+paramURL+" to check for updates for race(s) "+getRaces());
final ClientParamsPHP clientParams;
try {
clientParams = new ClientParamsPHP(new InputStreamReader(paramURL.openStream()));
List<Pair<com.sap.sailing.domain.base.ControlPoint, NauticalSide>> newCourseControlPoints = new ArrayList<>();
List<com.sap.sailing.domain.base.ControlPoint> newCourseControlPoints = new ArrayList<>();
List<Pair<com.sap.sailing.domain.base.ControlPoint, NauticalSide>> newCourseControlPointsWithPassingSide = new ArrayList<>();
final List<? extends TracTracControlPoint> newTracTracControlPoints = clientParams.getRaceDefaultRoute().getControlPoints();
Map<Integer, NauticalSide> passingSideData = parsePassingSideData(clientParams.getRaceDefaultRoute(), newTracTracControlPoints);
int i = 1;
for (TracTracControlPoint newTracTracControlPoint : newTracTracControlPoints) {
NauticalSide nauticalSide = passingSideData.containsKey(i) ? passingSideData.get(i) : null;
newCourseControlPoints.add(new Pair<com.sap.sailing.domain.base.ControlPoint,
NauticalSide>(domainFactory.getOrCreateControlPoint(newTracTracControlPoint), nauticalSide));
final com.sap.sailing.domain.base.ControlPoint newControlPoint = domainFactory.getOrCreateControlPoint(newTracTracControlPoint);
newCourseControlPoints.add(newControlPoint);
newCourseControlPointsWithPassingSide.add(new Pair<com.sap.sailing.domain.base.ControlPoint,
NauticalSide>(newControlPoint, nauticalSide));
i++;
}
List<com.sap.sailing.domain.base.ControlPoint> currentCourseControlPoints = new ArrayList<>();
@@ -283,10 +287,10 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements
if (!newCourseControlPoints.equals(currentCourseControlPoints)) {
logger.info("Detected course change based on clientparams.php contents for races "+getRaces());
try {
course.update(newCourseControlPoints, domainFactory.getBaseDomainFactory());
course.update(newCourseControlPointsWithPassingSide, domainFactory.getBaseDomainFactory());
} catch (PatchFailedException pfe) {
logger.severe("Failed to apply course update "+newTracTracControlPoints+" to course "+course);
logger.throwing(TracTracRaceTrackerImpl.class.getName(), "scheduleClientParamsPHPPoller.run", pfe);
logger.log(Level.SEVERE, "scheduleClientParamsPHPPoller.run", pfe);
}
}
updateStartStopTimesAndLiveDelay(clientParams, simulator);
@@ -311,7 +315,7 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements
}
} catch (IOException e) {
logger.info("Exception "+e.getMessage()+" while trying to read clientparams.php for races "+getRaces());
logger.throwing(TracTracRaceTracker.class.getName(), "scheduleClientParamsPHPPoller.run", e);
logger.log(Level.SEVERE, "scheduleClientParamsPHPPoller.run", e);
}
}
}
@@ -27,7 +27,7 @@ import difflib.PatchFailedException;
* @author Axel Uhl (d043530)
*
*/
public interface Course extends CourseData {
public interface Course extends CourseBase {
void lockForRead();
void unlockAfterRead();
@@ -12,7 +12,7 @@ import com.sap.sailing.domain.common.WithID;
* @author Axel Uhl (D043530)
*
*/
public interface Event extends Named, Renamable, WithID, EventData {
public interface Event extends Named, Renamable, WithID, EventBase {
Iterable<Regatta> getRegattas();
void addRegatta(Regatta regatta);
@@ -18,7 +18,7 @@ import com.sap.sailing.domain.tracking.TrackedRegattaRegistry;
* @author Axel Uhl (D043530)
*
*/
public interface Series extends SeriesData {
public interface Series extends SeriesBase {
/**
* A series consists of one or more "race columns." Some people would just say "race," but we use the term "race" for
@@ -181,7 +181,7 @@ public class CourseImpl extends NamedImpl implements Course {
} catch (Exception e) {
logger.log(Level.SEVERE, "Exception while notifying listener about waypoint " + waypointToRemove
+ " that got removed from course " + this + ": " + e.getMessage());
logger.throwing(CourseImpl.class.getName(), "notifyListenersWaypointRemoved", e);
logger.log(Level.SEVERE, "notifyListenersWaypointRemoved", e);
}
}
}
@@ -193,7 +193,7 @@ public class CourseImpl extends NamedImpl implements Course {
} catch (Exception e) {
logger.log(Level.SEVERE, "Exception while notifying listener about waypoint " + waypointToAdd
+ " that got added to course " + this + ": " + e.getMessage());
logger.throwing(CourseImpl.class.getName(), "notifyListenersWaypointAdded", e);
logger.log(Level.SEVERE, "notifyListenersWaypointAdded", e);
}
}
}
@@ -32,9 +32,10 @@ import com.sap.sailing.domain.common.WithID;
import com.sap.sailing.domain.leaderboard.ScoringScheme;
import com.sap.sailing.domain.leaderboard.impl.HighPoint;
import com.sap.sailing.domain.leaderboard.impl.HighPointExtremeSailingSeriesOverall;
import com.sap.sailing.domain.leaderboard.impl.HighPointFirstGets10;
import com.sap.sailing.domain.leaderboard.impl.HighPointFirstGets10LastBreaksTie;
import com.sap.sailing.domain.leaderboard.impl.HighPointLastBreaksTie;
import com.sap.sailing.domain.leaderboard.impl.LowPoint;
import com.sap.sailing.domain.leaderboard.impl.LowPointWinnerGetsZero;
import com.sap.sailing.domain.tracking.MarkPassing;
import com.sap.sailing.domain.tracking.impl.MarkPassingImpl;
@@ -287,9 +288,10 @@ public class DomainFactoryImpl implements DomainFactory {
case HIGH_POINT_LAST_BREAKS_TIE:
return new HighPointLastBreaksTie();
case HIGH_POINT_FIRST_GETS_TEN:
return new HighPointFirstGets10();
default:
throw new RuntimeException("Unknown scoring scheme type "+scoringSchemeType.name());
return new HighPointFirstGets10LastBreaksTie();
case LOW_POINT_WINNER_GETS_ZERO:
return new LowPointWinnerGetsZero();
}
throw new RuntimeException("Unknown scoring scheme type "+scoringSchemeType.name());
}
}
@@ -9,7 +9,7 @@ import com.sap.sailing.domain.base.Event;
import com.sap.sailing.domain.base.Regatta;
import com.sap.sailing.domain.base.Venue;
public class EventImpl extends EventDataImpl implements Event {
public class EventImpl extends EventBaseImpl implements Event {
private static final long serialVersionUID = 855135446595485715L;
private final Set<Regatta> regattas;
@@ -20,6 +20,7 @@ import com.sap.sailing.domain.base.RaceDefinition;
import com.sap.sailing.domain.base.Regatta;
import com.sap.sailing.domain.base.RegattaListener;
import com.sap.sailing.domain.base.Series;
import com.sap.sailing.domain.common.LeaderboardNameConstants;
import com.sap.sailing.domain.common.RegattaIdentifier;
import com.sap.sailing.domain.common.RegattaName;
import com.sap.sailing.domain.common.impl.NamedImpl;
@@ -84,8 +85,9 @@ public class RegattaImpl extends NamedImpl implements Regatta, RaceColumnListene
* carried out.
*/
public RegattaImpl(RaceLogStore raceLogStore, String baseName, BoatClass boatClass, TrackedRegattaRegistry trackedRegattaRegistry, ScoringScheme scoringScheme, Serializable id, CourseArea courseArea) {
this(raceLogStore, baseName, boatClass, Collections.singletonList(new SeriesImpl("Default", /* isMedal */false, Collections
.singletonList(new FleetImpl("Default")), /* race column names */new ArrayList<String>(),
this(raceLogStore, baseName, boatClass, Collections.singletonList(new SeriesImpl(LeaderboardNameConstants.DEFAULT_SERIES_NAME,
/* isMedal */false, Collections
.singletonList(new FleetImpl(LeaderboardNameConstants.DEFAULT_FLEET_NAME)), /* race column names */new ArrayList<String>(),
trackedRegattaRegistry)), /* persistent */false, scoringScheme, id, courseArea);
}
@@ -95,7 +97,7 @@ public class RegattaImpl extends NamedImpl implements Regatta, RaceColumnListene
* this new regatta.
*/
public RegattaImpl(RaceLogStore raceLogStore, String baseName, BoatClass boatClass, Iterable<? extends Series> series, boolean persistent, ScoringScheme scoringScheme, Serializable id, CourseArea courseArea) {
super(getFullName(baseName, boatClass==null?null:boatClass.getName()));
super(getDefaultName(baseName, boatClass==null?null:boatClass.getName()));
this.id = id;
this.raceLogStore = raceLogStore;
races = new HashSet<RaceDefinition>();
@@ -126,7 +128,7 @@ public class RegattaImpl extends NamedImpl implements Regatta, RaceColumnListene
new RaceLogOnRegattaIdentifier(this, raceColumn.getName())));
}
public static String getFullName(String baseName, String boatClassName) {
public static String getDefaultName(String baseName, String boatClassName) {
return baseName+(boatClassName==null?"":" ("+boatClassName+")");
}
@@ -90,7 +90,6 @@ public interface ScoringScheme extends Serializable {
* for the leaderboard's total scores. This may, e.g., be the case if a column is split into more than one fleet and
* those fleets are unordered. In that case, scores need to be available for all fleets before the column counts
* for the total scores.
* @param at TODO
*/
boolean isValidInTotalScore(Leaderboard leaderboard, RaceColumn raceColumn, TimePoint at);
}
@@ -70,6 +70,13 @@ public abstract class AbstractScoringSchemeImpl implements ScoringScheme {
return new ScoreComparator(nullScoresAreBetter);
}
/**
* This default implementation handles the somewhat tricky case where a score is considered valid for the total score only
* if for multiple unordered fleets in the race all fleets have raced or are currently racing. There have been controversial
* discussions whether this is desired. Usually, subclasses will override such that a result is generally valid for the
* total scores, and the {@link #compareByNumberOfRacesScored(int, int)} method has to just rank those better who ran
* more races.
*/
@Override
public boolean isValidInTotalScore(Leaderboard leaderboard, RaceColumn raceColumn, TimePoint at) {
boolean result;
@@ -124,22 +131,23 @@ public abstract class AbstractScoringSchemeImpl implements ScoringScheme {
public int compareByBetterScore(List<Pair<RaceColumn, Double>> o1Scores, List<Pair<RaceColumn, Double>> o2Scores, boolean nullScoresAreBetter) {
assert o1Scores.size() == o2Scores.size();
final Comparator<Double> pureScoreComparator = getScoreComparator(nullScoresAreBetter);
Comparator<Pair<RaceColumn, Double>> scoreComparator = new Comparator<Pair<RaceColumn, Double>>() {
@Override
public int compare(Pair<RaceColumn, Double> o1, Pair<RaceColumn, Double> o2) {
return pureScoreComparator.compare(o1.getB(), o2.getB());
}
};
List<Pair<RaceColumn, Double>> o1ScoresCopy = new ArrayList<Pair<RaceColumn, Double>>(o1Scores);
List<Pair<RaceColumn, Double>> o2ScoresCopy = new ArrayList<Pair<RaceColumn, Double>>(o2Scores);
Collections.sort(o1ScoresCopy, scoreComparator);
Collections.sort(o2ScoresCopy, scoreComparator);
// needs to compare net points; therefore, divide the total points by the column factor for comparison:
List<Double> o1NetScores = new ArrayList<>();
for (Pair<RaceColumn, Double> o1ColumnAndScore : o1Scores) {
o1NetScores.add(o1ColumnAndScore.getB()/o1ColumnAndScore.getA().getFactor());
}
List<Double> o2NetScores = new ArrayList<>();
for (Pair<RaceColumn, Double> o2ColumnAndScore : o2Scores) {
o2NetScores.add(o2ColumnAndScore.getB()/o2ColumnAndScore.getA().getFactor());
}
Collections.sort(o1NetScores, pureScoreComparator);
Collections.sort(o2NetScores, pureScoreComparator);
// now both lists are sorted from best to worst score
Iterator<Pair<RaceColumn, Double>> o1Iter = o1ScoresCopy.iterator();
Iterator<Pair<RaceColumn, Double>> o2Iter = o2ScoresCopy.iterator();
Iterator<Double> o1Iter = o1NetScores.iterator();
Iterator<Double> o2Iter = o2NetScores.iterator();
int result = 0;
while (result == 0 && o1Iter.hasNext() && o2Iter.hasNext()) {
result = scoreComparator.compare(o1Iter.next(), o2Iter.next());
result = pureScoreComparator.compare(o1Iter.next(), o2Iter.next());
}
return result;
}
@@ -280,7 +280,7 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race
/**
* All competitors with non-<code>null</code> net points are added to the result which is then sorted by net points in ascending
* order. The fleet is the primary ordering criterion, followed by the net points.
* order. The fleet, if ordered, is the primary ordering criterion, followed by the net points.
*/
@Override
public List<Competitor> getCompetitorsFromBestToWorst(final RaceColumn raceColumn, TimePoint timePoint) throws NoWindException {
@@ -14,6 +14,7 @@ import com.sap.sailing.domain.base.Fleet;
import com.sap.sailing.domain.base.RaceColumn;
import com.sap.sailing.domain.base.RaceColumnListener;
import com.sap.sailing.domain.base.impl.FleetImpl;
import com.sap.sailing.domain.common.LeaderboardNameConstants;
import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard;
import com.sap.sailing.domain.leaderboard.FlexibleRaceColumn;
import com.sap.sailing.domain.leaderboard.ScoringScheme;
@@ -38,7 +39,7 @@ import com.sap.sailing.domain.tracking.TrackedRace;
public class FlexibleLeaderboardImpl extends AbstractLeaderboardImpl implements FlexibleLeaderboard {
private static Logger logger = Logger.getLogger(FlexibleLeaderboardImpl.class.getName());
protected static final Fleet defaultFleet = new FleetImpl("Default");
protected static final Fleet defaultFleet = new FleetImpl(LeaderboardNameConstants.DEFAULT_FLEET_NAME);
private static final long serialVersionUID = -5708971849158747846L;
private final List<FlexibleRaceColumn> races;
private final ScoringScheme scoringScheme;
@@ -4,6 +4,8 @@ import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.RaceColumn;
import com.sap.sailing.domain.common.MaxPointsReason;
import com.sap.sailing.domain.common.ScoringSchemeType;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.leaderboard.Leaderboard;
/**
@@ -44,4 +46,10 @@ public class HighPoint extends AbstractScoringSchemeImpl {
public ScoringSchemeType getType() {
return ScoringSchemeType.HIGH_POINT;
}
@Override
public boolean isValidInTotalScore(Leaderboard leaderboard, RaceColumn raceColumn, TimePoint at) {
return true;
}
}
@@ -1,16 +0,0 @@
package com.sap.sailing.domain.leaderboard.impl;
import com.sap.sailing.domain.common.ScoringSchemeType;
public class HighPointFirstGets10 extends HighPointFirstGetsFixedScore {
private static final long serialVersionUID = 1L;
public HighPointFirstGets10() {
super(10.0);
}
@Override
public ScoringSchemeType getType() {
return ScoringSchemeType.HIGH_POINT_FIRST_GETS_TEN;
}
}
@@ -0,0 +1,25 @@
package com.sap.sailing.domain.leaderboard.impl;
import java.util.List;
import com.sap.sailing.domain.base.RaceColumn;
import com.sap.sailing.domain.common.ScoringSchemeType;
import com.sap.sailing.domain.common.impl.Util.Pair;
public class HighPointFirstGets10LastBreaksTie extends HighPointFirstGetsFixedScore {
private static final long serialVersionUID = 1L;
public HighPointFirstGets10LastBreaksTie() {
super(10.0);
}
@Override
public ScoringSchemeType getType() {
return ScoringSchemeType.HIGH_POINT_FIRST_GETS_TEN;
}
@Override
public int compareByBetterScore(List<Pair<RaceColumn, Double>> o1Scores, List<Pair<RaceColumn, Double>> o2Scores, boolean nullScoresAreBetter) {
return 0;
}
}
@@ -2,7 +2,6 @@ package com.sap.sailing.domain.leaderboard.impl;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.RaceColumn;
import com.sap.sailing.domain.common.MaxPointsReason;
/**
@@ -36,9 +35,4 @@ public abstract class HighPointFirstGetsFixedScore extends HighPoint {
}
return result;
}
@Override
public Double getPenaltyScore(RaceColumn raceColumn, Competitor competitor, MaxPointsReason maxPointsReason, Integer numberOfCompetitorsInRace, int numberOfCompetitorsInLeaderboard) {
return 0.0;
}
}
@@ -261,6 +261,6 @@ public class LeaderboardTotalRankComparator implements Comparator<Competitor> {
* Compares a competitor better (lesser) if it has the greater number of races scored
*/
protected int compareByNumberOfRacesScored(int o1NumberOfRacesScored, int o2NumberOfRacesScored) {
return o2NumberOfRacesScored - o1NumberOfRacesScored;
return scoringScheme.compareByNumberOfRacesScored(o1NumberOfRacesScored, o2NumberOfRacesScored);
}
}
@@ -4,6 +4,8 @@ import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.RaceColumn;
import com.sap.sailing.domain.common.MaxPointsReason;
import com.sap.sailing.domain.common.ScoringSchemeType;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.leaderboard.Leaderboard;
/**
@@ -42,4 +44,9 @@ public class LowPoint extends AbstractScoringSchemeImpl {
public ScoringSchemeType getType() {
return ScoringSchemeType.LOW_POINT;
}
@Override
public boolean isValidInTotalScore(Leaderboard leaderboard, RaceColumn raceColumn, TimePoint at) {
return true;
}
}
@@ -0,0 +1,41 @@
package com.sap.sailing.domain.leaderboard.impl;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.RaceColumn;
import com.sap.sailing.domain.common.ScoringSchemeType;
/**
* As compared to the regular low-point scheme, the 2013 ISAF World Cup will score the winner of a race with
* 0 points, all others with their ranks. Ties are broken like this:
* <ul>
* <li>25.9 RRS B8 is deleted. RRS A8 is changed as follows:
* <li> 25.9.1 For boats competing in a medal stage, ties in the regatta score are broken by the medal stage score.
* <li> 25.9.2 Ties in a medal stage with a single race are broken by applying RRS A8 to the opening series scores.
* <li> 25.9.3 All other series ties shall be broken in accordance with A8.2.
* </ul>
*
* Discards are to be constrained to each series, with no discards in the medal series. However, since there is
* a new rule for carrying the qualification results to the final series, this won't be a regular regatta
* leaderboard anyhow. Instead, the final series starts out with an artificial race that has its scores based
* on the competitor rankings after the qualification series, where each fleet's scores again start with 0, then 2, ...
* So the best boat in the silver fleet will get 0 points in the carry race, the next boat 2, and so on.
* The carry race is discardable.
*
* @author Axel Uhl (D043530)
*
*/
public class LowPointWinnerGetsZero extends LowPoint {
private static final long serialVersionUID = 3405249102284162690L;
@Override
public Double getScoreForRank(RaceColumn raceColumn, Competitor competitor, int rank, Integer numberOfCompetitorsInRace) {
return rank == 0 ? null : rank == 1 ? 0.0 : (double) rank;
}
// TODO handle tie-breaking rules properly
@Override
public ScoringSchemeType getType() {
return ScoringSchemeType.LOW_POINT_WINNER_GETS_ZERO;
}
}
@@ -11,10 +11,13 @@ public abstract class AbstractRaceTrackerImpl implements RaceTracker {
@Override
public Set<RegattaAndRaceIdentifier> getRaceIdentifiers() {
Set<RegattaAndRaceIdentifier> result = new HashSet<RegattaAndRaceIdentifier>();
for (RaceDefinition race : getRaces()) {
TrackedRace trackedRace = getTrackedRegatta().getTrackedRace(race);
if (trackedRace != null) {
result.add(trackedRace.getRaceIdentifier());
final Set<RaceDefinition> races = getRaces();
if (races != null) {
for (RaceDefinition race : races) {
TrackedRace trackedRace = getTrackedRegatta().getTrackedRace(race);
if (trackedRace != null) {
result.add(trackedRace.getRaceIdentifier());
}
}
}
return result;
@@ -47,11 +47,11 @@ public interface RaceTracker {
com.sap.sailing.domain.base.Regatta getRegatta();
/**
* Returns the races being tracked by this tracker. Non-blocking call that returns <code>null</code> if
* the {@link RaceDefinition} for a TracTrac Event hasn't been created yet, e.g., because the course definition
* hasn't been received yet or the listener for receiving course information hasn't been registered (yet).
* Also returns races that have been removed from containing structures which may lead this tracker to no
* longer update their {@link TrackedRace} with new data.
* Returns the races being tracked by this tracker. Non-blocking call that returns <code>null</code> or an empty set
* if the {@link RaceDefinition} for a TracTrac Event hasn't been created yet, e.g., because the course definition
* hasn't been received yet or the listener for receiving course information hasn't been registered (yet). Also
* returns races that have been removed from containing structures which may lead this tracker to no longer update
* their {@link TrackedRace} with new data.
*/
Set<RaceDefinition> getRaces();
@@ -1,14 +1,17 @@
package com.sap.sailing.domain.tracking.impl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import com.sap.sailing.domain.base.Mark;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.Leg;
import com.sap.sailing.domain.base.Mark;
import com.sap.sailing.domain.base.Timed;
import com.sap.sailing.domain.base.Waypoint;
import com.sap.sailing.domain.base.impl.MillisecondsTimePoint;
@@ -334,7 +337,9 @@ public class CrossTrackErrorCache extends AbstractRaceChangeListener {
@Override
public void markPositionChanged(GPSFix fix, Mark mark) {
TimePoint from = owner.getOrCreateTrack(mark).getEstimatedPositionTimePeriodAffectedBy(fix).getA();
for (Competitor competitor : cachePerCompetitor.keySet()) {
final List<Competitor> shuffledCompetitors = new ArrayList<>(cachePerCompetitor.keySet());
Collections.shuffle(shuffledCompetitors);
for (Competitor competitor : shuffledCompetitors) {
invalidate(competitor, from);
}
}
@@ -21,6 +21,7 @@ import com.sap.sailing.domain.base.RaceDefinition;
import com.sap.sailing.domain.base.Waypoint;
import com.sap.sailing.domain.base.impl.MillisecondsTimePoint;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.TimingConstants;
import com.sap.sailing.domain.common.WindSource;
import com.sap.sailing.domain.common.WindSourceType;
import com.sap.sailing.domain.common.impl.Util;
@@ -238,7 +239,7 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
listener.windSourcesToExcludeChanged(windSourcesToExclude);
} catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage());
logger.throwing(DynamicTrackedRaceImpl.class.getName(), "notifyListenersWindSourcesToExcludeChanged(Iterable<WindSource>)", e);
logger.log(Level.SEVERE, "notifyListenersWindSourcesToExcludeChanged(Iterable<WindSource>)", e);
}
}
}
@@ -254,7 +255,7 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
listener.raceTimesChanged(startOfTracking, endOfTracking, startTimeReceived);
} catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage());
logger.throwing(DynamicTrackedRaceImpl.class.getName(), "notifyListenersRaceTimesChanged(TimePoint, TimePoint, TimePoint)", e);
logger.log(Level.SEVERE, "notifyListenersRaceTimesChanged(TimePoint, TimePoint, TimePoint)", e);
}
}
}
@@ -269,7 +270,7 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
listener.markPositionChanged(fix, mark);
} catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage());
logger.throwing(DynamicTrackedRaceImpl.class.getName(), "notifyListeners(GPSFix, Competitor)", e);
logger.log(Level.SEVERE, "notifyListeners(GPSFix, Competitor)", e);
}
}
}
@@ -284,7 +285,7 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
listener.competitorPositionChanged(fix, competitor);
} catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage());
logger.throwing(DynamicTrackedRaceImpl.class.getName(), "notifyListeners(GPSFix, Competitor)", e);
logger.log(Level.SEVERE, "notifyListeners(GPSFix, Competitor)", e);
}
}
}
@@ -299,7 +300,7 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
listener.statusChanged(status);
} catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage());
logger.throwing(DynamicTrackedRaceImpl.class.getName(), "notifyListeners(TrackedRaceStatus)", e);
logger.log(Level.SEVERE, "notifyListeners(TrackedRaceStatus)", e);
}
}
}
@@ -314,7 +315,7 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
listener.windDataReceived(wind, windSource);
} catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage());
logger.throwing(DynamicTrackedRaceImpl.class.getName(), "notifyListeners(Wind)", e);
logger.log(Level.SEVERE, "notifyListeners(Wind)", e);
}
}
}
@@ -330,7 +331,7 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
newMillisecondsOverWhichToAverageSpeed);
} catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage());
logger.throwing(DynamicTrackedRaceImpl.class.getName(),
logger.log(Level.SEVERE,
"notifyListenersSpeedAveragingChanged(long, long)", e);
}
}
@@ -347,7 +348,7 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
newMillisecondsOverWhichToAverageWind);
} catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage());
logger.throwing(DynamicTrackedRaceImpl.class.getName(),
logger.log(Level.SEVERE,
"notifyListenersWindAveragingChanged(long, long)", e);
}
}
@@ -363,7 +364,7 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
listener.delayToLiveChanged(delayToLiveInMillis);
} catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage());
logger.throwing(DynamicTrackedRaceImpl.class.getName(),
logger.log(Level.SEVERE,
"notifyListenersDelayToLiveChanged(long)", e);
}
}
@@ -379,7 +380,7 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
listener.windDataRemoved(wind, windSource);
} catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage());
logger.throwing(DynamicTrackedRaceImpl.class.getName(), "notifyListenersWindRemoved(Wind)", e);
logger.log(Level.SEVERE, "notifyListenersWindRemoved(Wind)", e);
}
}
}
@@ -394,7 +395,7 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
listener.markPassingReceived(competitor, oldMarkPassings, markPassings);
} catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage());
logger.throwing(DynamicTrackedRaceImpl.class.getName(), "notifyListeners(MarkPassing)", e);
logger.log(Level.SEVERE, "notifyListeners(MarkPassing)", e);
}
}
}
@@ -567,10 +568,22 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
@Override
public void recordWind(Wind wind, WindSource windSource) {
getOrCreateWindTrack(windSource).add(wind);
updated(/* time point */null); // wind events shouldn't advance race time
triggerManeuverCacheRecalculationForAllCompetitors();
notifyListeners(wind, windSource);
// TODO check what a good filter is; remember that start/end of tracking may change over time; what if we have discarded valuable wind fixes?
TimePoint startOfRace = getStartOfRace();
TimePoint startOfTracking = getStartOfTracking();
TimePoint endOfRace = getEndOfRace();
TimePoint endOfTracking = getEndOfTracking();
// record wind fix only if it's still within reasonable time after the race has ended or the race hasn't ended yet
if ((startOfTracking == null || !startOfTracking.after(wind.getTimePoint()) ||
(startOfRace != null && !startOfRace.after(wind.getTimePoint())))
&&
(endOfTracking == null || endOfTracking.plus(TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS).after(wind.getTimePoint()) ||
(endOfRace != null && endOfRace.plus(TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS).after(wind.getTimePoint())))) {
getOrCreateWindTrack(windSource).add(wind);
updated(/* time point */null); // wind events shouldn't advance race time
triggerManeuverCacheRecalculationForAllCompetitors();
notifyListeners(wind, windSource);
}
}
@Override
@@ -1604,7 +1604,15 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
getRace().getCourse().lockForRead(); // ensure the course doesn't change, particularly lose the leg we're interested in, while we're running
try {
for (Competitor competitor : getRace().getCompetitors()) {
TrackedLegOfCompetitor leg = getTrackedLeg(competitor, timePoint);
TrackedLegOfCompetitor leg;
try {
leg = getTrackedLeg(competitor, timePoint);
} catch (IllegalArgumentException iae) {
logger.warning("Caught "+iae+" during wind estimation; ignoring seemingly broken leg");
logger.log(Level.SEVERE, "clusterBearingsByLegType", iae);
// supposedly, we got a "Waypoint X isn't start of any leg in Y" exception; leg not found
leg = null;
}
// if bearings was set to null this indicates there was an exception; no need for further calculations, return null
if (bearings != null && leg != null) {
TrackedLeg trackedLeg = getTrackedLeg(leg.getLeg());
@@ -1756,7 +1764,12 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
}
protected void triggerManeuverCacheRecalculationForAllCompetitors() {
for (Competitor competitor : getRace().getCompetitors()) {
final List<Competitor> shuffledCompetitors = new ArrayList<>();
for (Competitor competitor : (getRace().getCompetitors())) {
shuffledCompetitors.add(competitor);
}
Collections.shuffle(shuffledCompetitors);
for (Competitor competitor : shuffledCompetitors) {
triggerManeuverCacheRecalculation(competitor);
}
}
@@ -1768,6 +1781,8 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
}
private Triple<TimePoint, TimePoint, List<Maneuver>> computeManeuvers(Competitor competitor) throws NoWindException {
logger.fine("computeManeuvers("+competitor.getName()+") called in tracked race "+this);
long startedAt = System.currentTimeMillis();
// compute the maneuvers for competitor
Triple<TimePoint, TimePoint, List<Maneuver>> result = null;
NavigableSet<MarkPassing> markPassings = getMarkPassings(competitor);
@@ -1806,6 +1821,8 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
result = new Triple<TimePoint, TimePoint, List<Maneuver>>(extendedFrom, extendedTo, extendedResultForCache);
} // else competitor has no fixes to consider; remove any maneuver cache entry
} // else competitor hasn't started yet; remove any maneuver cache entry
logger.fine("computeManeuvers("+competitor.getName()+") called in tracked race "+this+
" took "+(System.currentTimeMillis()-startedAt)+"ms");
return result;
}
@@ -115,7 +115,7 @@ public class WindTrackImpl extends TrackImpl<Wind> implements WindTrack {
listener.windDataReceived(wind);
} catch (Exception e) {
logger.log(Level.SEVERE, "WindListener " + listener + " threw exception " + e.getMessage());
logger.throwing(WindTrackImpl.class.getName(), "notifyListenersAboutReceive(Wind)", e);
logger.log(Level.SEVERE, "notifyListenersAboutReceive(Wind)", e);
}
}
}
@@ -128,7 +128,7 @@ public class WindTrackImpl extends TrackImpl<Wind> implements WindTrack {
listener.windAveragingChanged(oldMillisecondsOverWhichToAverage, newMillisecondsOverWhichToAverage);
} catch (Exception e) {
logger.log(Level.SEVERE, "WindListener " + listener + " threw exception " + e.getMessage());
logger.throwing(WindTrackImpl.class.getName(), "notifyListenersAboutAveragingChange(long, long)", e);
logger.log(Level.SEVERE, "notifyListenersAboutAveragingChange(long, long)", e);
}
}
}
@@ -141,7 +141,7 @@ public class WindTrackImpl extends TrackImpl<Wind> implements WindTrack {
listener.windDataRemoved(wind);
} catch (Exception e) {
logger.log(Level.SEVERE, "WindListener " + listener + " threw exception " + e.getMessage());
logger.throwing(WindTrackImpl.class.getName(), "notifyListenersAboutRemoval(Wind)", e);
logger.log(Level.SEVERE, "notifyListenersAboutRemoval(Wind)", e);
}
}
}
@@ -14,6 +14,8 @@ import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sap.sailing.util.impl.SmartFutureCache.UpdateInterval;
@@ -74,11 +76,20 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
* {@link #ongoingManeuverCacheRecalculations}. This will let {@link Future#cancel(boolean)} return <code>false</code>
* should it be called on that Future.
*/
private final Map<K, FutureTaskWithCancelBlocking<V, U>> ongoingRecalculations;
private final Map<K, FutureTaskWithCancelBlocking> ongoingRecalculations;
private final Map<K, V> cache;
private final Executor recalculator;
/**
* Note that this needs to have more than one thread because there may be calculations used for cache updates that
* need to wait for other cache updates to finish. If those were all to be handled by a single thread, deadlocks
* would occur. Remember that there may still be single-core machines, so the factor with which
* <code>availableProcessors</code> is multiplied needs to be greater than one at least.
*/
private final static Executor recalculator = new ThreadPoolExecutor(/* corePoolSize */ 0,
/* maximumPoolSize */ 3*Runtime.getRuntime().availableProcessors(),
/* keepAliveTime */ 60, TimeUnit.SECONDS,
/* workQueue */ new LinkedBlockingQueue<Runnable>());
private final CacheUpdater<K, V, U> cacheUpdateComputer;
@@ -93,6 +104,8 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
private final Map<K, U> triggeredWhileSuspended;
private int smartFutureCacheTaskReuseCounter;
/**
* An immutable "interval" description for a cache update
*
@@ -162,6 +175,20 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
}
}
private static class SettableCallable<V> implements Callable<V> {
private Callable<V> callable;
public void setCallable(Callable<V> callable) {
this.callable = callable;
}
@Override
public V call() throws Exception {
return callable.call();
}
}
/**
* Once a client has fetched such a Future from {@link TrackedRaceImpl##ongoingManeuverCacheRecalculations} while
* holding the object monitor of {@link TrackedRaceImpl##ongoingManeuverCacheRecalculations}, the client knows that
@@ -173,14 +200,88 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
* @author Axel Uhl (D043530)
*
*/
private static class FutureTaskWithCancelBlocking<V, U extends UpdateInterval<U>> extends FutureTask<V> {
private class FutureTaskWithCancelBlocking extends FutureTask<V> implements Callable<V> {
private boolean dontCancel;
private final U updateInterval;
private boolean runningAndReadUpdateInterval;
public FutureTaskWithCancelBlocking(Callable<V> callable, U updateInterval) {
private final K key;
private U updateInterval;
private final boolean callerWaitsSynchronouslyForResult;
private final Thread callerThread;
public FutureTaskWithCancelBlocking(final K key, final U updateInterval,
final boolean callerWaitsSynchronouslyForResult, final Thread callerThread) {
this(new SettableCallable<V>(), key, updateInterval, callerWaitsSynchronouslyForResult, callerThread);
}
public FutureTaskWithCancelBlocking(SettableCallable<V> callable, final K key, final U updateInterval,
final boolean callerWaitsSynchronouslyForResult, final Thread callerThread) {
super(callable);
callable.setCallable(this);
this.key = key;
this.updateInterval = updateInterval;
this.callerWaitsSynchronouslyForResult = callerWaitsSynchronouslyForResult;
this.callerThread = callerThread;
}
/**
* Tries to update this task's update interval so as to re-used an already scheduled task for a given key.
* The update can only be performed if the task hasn't read the update interval after it has been started.
*
* @return <code>true</code> if the update was applied successfully before the task read the update interval; this means
* that if the task is not cancelled, it will use the new update interval. If <code>false</code> is returned, the task has already
* read the update interval, and a change to the update interval for this task is no longer possible. The caller will probably
* have to schedule a new task with the new update interval.
*/
public synchronized boolean tryToUpdateUpdateInterval(U newUpdateInterval) {
final boolean result;
if (runningAndReadUpdateInterval) {
result = false;
} else {
result = true;
updateInterval = newUpdateInterval;
}
return result;
}
@Override
public V call() {
try {
final U updateInterval;
synchronized (this) {
updateInterval = getUpdateInterval();
runningAndReadUpdateInterval = true;
}
if (callerWaitsSynchronouslyForResult) {
LockUtil.propagateLockSetFrom(callerThread);
}
try {
V preResult = cacheUpdateComputer.computeCacheUpdate(key, updateInterval);
final NamedReentrantReadWriteLock lock = getOrCreateLockForKey(key);
LockUtil.lockForWrite(lock);
try {
V result = cacheUpdateComputer.provideNewCacheValue(key, cache.get(key), preResult,
updateInterval);
cache(key, result);
return result;
} finally {
LockUtil.unlockAfterWrite(lock);
ongoingRecalculations.remove(key);
}
} finally {
if (callerWaitsSynchronouslyForResult) {
LockUtil.unpropagateLockSetFrom(callerThread);
}
}
} catch (Exception e) {
// cache won't be updated
logger.log(Level.SEVERE, "triggerUpdate", e);
throw new RuntimeException(e);
}
}
public synchronized void dontCancel() {
@@ -196,17 +297,18 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
}
}
public boolean isCallerWaitsSynchronouslyForResult() {
return callerWaitsSynchronouslyForResult;
}
public U getUpdateInterval() {
return updateInterval;
}
}
public SmartFutureCache(CacheUpdater<K, V, U> cacheUpdateComputer, String nameForLocks) {
this.ongoingRecalculations = new ConcurrentHashMap<K, FutureTaskWithCancelBlocking<V, U>>();
this.ongoingRecalculations = new ConcurrentHashMap<K, FutureTaskWithCancelBlocking>();
this.cache = new ConcurrentHashMap<K, V>();
this.recalculator = new ThreadPoolExecutor(/* corePoolSize */ 0,
/* maximumPoolSize */ 1, /* keepAliveTime */ 60, TimeUnit.SECONDS,
/* workQueue */ new LinkedBlockingQueue<Runnable>());
this.cacheUpdateComputer = cacheUpdateComputer;
this.locksForKeys = new ConcurrentHashMap<K, NamedReentrantReadWriteLock>();
this.nameForLocks = nameForLocks;
@@ -241,19 +343,28 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
}
}
/**
* Triggers a cache update for <code>key</code> for the <code>updateInterval</code> specified. If a re-calculation
* for this key is already scheduled, this method will try to update its update interval by
* {@link UpdateInterval#join(UpdateInterval) joining} the new with the existing one. If that doesn't work because
* the task has already been started and has already read its update interval, this method will schedule a new task
* with the extended update interval.
* <p>
*
* If the running task has a different setting for the caller's waiting for the task, the task will be canceled
* (which may or may not work), and a new task with the joined update interval is scheduled.
*/
public void triggerUpdate(final K key, U updateInterval) {
// establish and maintain the following invariant: after lock on ongoingManeuverCacheRecalculations is released,
// establish and maintain the following invariant: after lock on ongoingRecalculations is released,
// no Future contained in it is in cancelled state
synchronized (ongoingRecalculations) {
final U oldUpdateInterval;
final U joinedUpdateInterval;
FutureTaskWithCancelBlocking oldFuture = null;
if (suspended) {
oldUpdateInterval = triggeredWhileSuspended.get(key);
} else {
FutureTaskWithCancelBlocking<V, U> oldFuture = ongoingRecalculations.get(key);
if (oldFuture != null) {
oldFuture.cancel(/* mayInterruptIfRunning */false);
}
oldFuture = ongoingRecalculations.get(key);
oldUpdateInterval = oldFuture == null ? null : oldFuture.getUpdateInterval();
}
if (oldUpdateInterval == null) {
@@ -268,42 +379,41 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
if (suspended) {
triggeredWhileSuspended.put(key, joinedUpdateInterval);
} else {
createAndExecuteRecalculation(key, joinedUpdateInterval);
boolean reuseExistingFuture = false;
if (oldFuture != null) {
if (!oldFuture.isCallerWaitsSynchronouslyForResult()) {
reuseExistingFuture = oldFuture.tryToUpdateUpdateInterval(joinedUpdateInterval);
}
if (!reuseExistingFuture) {
oldFuture.cancel(/* mayInterruptIfRunning */ false);
} else {
smartFutureCacheTaskReuseCounter++;
}
}
if (!reuseExistingFuture) {
// the following call adds the resulting new Future to ongoingRecalculations
createAndExecuteRecalculation(key, joinedUpdateInterval, /* callerWaitsSynchronouslyForResult */ false);
}
}
}
}
/**
* Creates a {@link FutureTask} for the (re-)calculation of the cache entry for <code>key</code> across update interval
* <code>joinedUpdateInterval</code>, enters it into {@link #ongoingRecalculations} and schedules its execution with
* {@link #recalculator}. The method synchronizes on {@link #ongoingRecalculations}.
* Creates a {@link FutureTask} for the (re-)calculation of the cache entry for <code>key</code> across update
* interval <code>joinedUpdateInterval</code>, enters it into {@link #ongoingRecalculations} and schedules its
* execution with {@link #recalculator}. The method synchronizes on {@link #ongoingRecalculations}.
*
* @param callerWaitsSynchronouslyForResult
* if <code>true</code>, this allows the future to assume the caller's locks. See also
* {@link LockUtil#propagateLockSetFrom(Thread)}. This can be helpful to avoid read-read deadlocks
* in conjunction with fair {@link ReentrantReadWriteLock}s.
*/
private void createAndExecuteRecalculation(final K key, final U joinedUpdateInterval) {
private void createAndExecuteRecalculation(final K key, final U joinedUpdateInterval,
final boolean callerWaitsSynchronouslyForResult) {
final Thread callerThread = Thread.currentThread();
synchronized (ongoingRecalculations) {
final FutureTaskWithCancelBlocking<V, U> future;
future = new FutureTaskWithCancelBlocking<V, U>(new Callable<V>() {
@Override
public V call() {
try {
V preResult = cacheUpdateComputer.computeCacheUpdate(key, joinedUpdateInterval);
final NamedReentrantReadWriteLock lock = getOrCreateLockForKey(key);
LockUtil.lockForWrite(lock);
try {
V result = cacheUpdateComputer.provideNewCacheValue(key, cache.get(key), preResult,
joinedUpdateInterval);
cache(key, result);
return result;
} finally {
LockUtil.unlockAfterWrite(lock);
ongoingRecalculations.remove(key);
}
} catch (Exception e) {
// cache won't be updated
logger.throwing(SmartFutureCache.class.getName(), "triggerUpdate", e);
throw new RuntimeException(e);
}
}
}, joinedUpdateInterval);
final FutureTaskWithCancelBlocking future = new FutureTaskWithCancelBlocking(key,
joinedUpdateInterval, callerWaitsSynchronouslyForResult, callerThread);
ongoingRecalculations.put(key, future);
recalculator.execute(future);
}
@@ -318,16 +428,22 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
public V get(K key, boolean waitForLatest) {
V value = null;
if (waitForLatest) {
FutureTaskWithCancelBlocking<V, U> future;
FutureTaskWithCancelBlocking future;
synchronized (ongoingRecalculations) {
if (suspended && waitForLatest) {
final boolean wasTriggeredWhileSuspended = triggeredWhileSuspended.containsKey(key); // update interval may have deliberately been null
if (wasTriggeredWhileSuspended) {
// If suspended and the caller wants to get the latest results, and during suspend recalculations were
// triggered, and execute them now.
// If suspended and the caller wants to get the latest results, and while suspended recalculations were
// triggered, execute them now.
// This will enter the new future into ongoingRecalculations, and therefore this method
// will wait for its completion below.
createAndExecuteRecalculation(key, triggeredWhileSuspended.remove(key));
// It is OK to assert that this method will synchronously wait for the results before
// releasing any locks held by the current thread because
// createAndExecuteRecalculation adds key to ongoingRecalculations, and
// further down this method, the future will be fetched from ongoingRecalculations
// with that same key, and a get will be performed on that future.
createAndExecuteRecalculation(key, triggeredWhileSuspended.remove(key),
/* callerWaitsSynchronouslyForResult */ true);
}
}
// as long as we hold the lock on ongoingManeuverCacheRecalculations, the Futures contained in it are not cancelled
@@ -342,7 +458,7 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
try {
value = future.get();
} catch (InterruptedException | ExecutionException e) {
logger.throwing(SmartFutureCache.class.getName(), "get", e);
logger.log(Level.SEVERE, "get", e);
throw new RuntimeException(e);
}
} // else no calculation currently going on; value has been fetched from latest cache entry
@@ -364,8 +480,18 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
protected void cache(final K key, V value) {
if (value == null) {
cache.remove(key);
locksForKeys.remove(key);
} else {
cache.put(key, value);
}
}
/**
* For debugging and testing; tells how many times a task got re-cycled with a new update interval instead of canceling it
* and scheduling a new one. See also bug 1314.
*/
public int getSmartFutureCacheTaskReuseCounter() {
return smartFutureCacheTaskReuseCounter;
}
}
@@ -79,7 +79,8 @@ public class UDPExpeditionReceiverTest {
"#0,9,320.0*05",
"#0,9,320.0*05",
"#0,1,7.700,2,-36.0,3,25.10,4,-49.5,5,19.41,6,271.5,9,321.0,12,1.07,50,327.3,146,40348.390058*10",
"#0,9,321.0*04"
"#0,9,321.0*04",
"#5,2,-163.2,3,0.00,13,305.8,39,2.0,48,39.500717,49,2.747750*X19"
};
someValidWithFourInvalidLines = new String[] {
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/com.sap.sailing.expeditionconnector/src/com/sap/sailing/expeditionconnector/FromHttpToUdpMirror.java"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="1"/>
</listAttribute>
<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.sap.sailing.expeditionconnector.FromHttpToUdpMirror"/>
<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-v http://test.sapsailing.com/sailingserver/expedition 2012"/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="com.sap.sailing.expeditionconnector"/>
</launchConfiguration>
@@ -13,7 +13,7 @@ import java.net.InetAddress;
* @author Axel Uhl (d043530)
*
*/
public class UDPMirror implements Runnable {
public class UDPMirror {
/**
* @param args 0: the port to listen to; 2*i-1, 2*i for i>0: host/port to which to forward
@@ -55,13 +55,6 @@ public class UDPMirror implements Runnable {
}
}
/**
* Listens for an inbound TCP connection
*/
public void run() {
}
private static void usage() {
System.out.println("Usage: java "+UDPMirror.class.getName()+" [-v] <listeningport> hostname1 port1 [hostname2 port2]*");
System.out.println(" -v\tPrint packets received to stdout");
@@ -3,6 +3,7 @@ package com.sap.sailing.expeditionconnector.impl;
import java.net.DatagramPacket;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -13,6 +14,7 @@ import com.sap.sailing.expeditionconnector.UDPExpeditionReceiver;
import com.sap.sailing.udpconnector.UDPMessageParser;
public class ExpeditionMessageParser implements UDPMessageParser<ExpeditionMessage> {
private static final Logger logger = Logger.getLogger(ExpeditionMessageParser.class.getName());
private UDPExpeditionReceiver receiver;
@@ -25,7 +27,7 @@ public class ExpeditionMessageParser implements UDPMessageParser<ExpeditionMessa
String packetAsString = new String(p.getData(), p.getOffset(), p.getLength()).trim();
if (packetAsString.length() > 0) {
Pattern completeLinePattern = Pattern
.compile("#([0-9]*)(( *,([0-9][0-9]*) *, *(-?[0-9]*(\\.[0-9]*)?))*)\\*([0-9a-fA-F][0-9a-fA-F]*)");
.compile("#([0-9]*)(( *,([0-9][0-9]*) *, *(-?[0-9]*(\\.[0-9]*)?))*)\\*X?([0-9a-fA-F][0-9a-fA-F]*)");
Matcher m = completeLinePattern.matcher(packetAsString);
boolean valid = m.matches();
if (valid) {
@@ -62,7 +64,7 @@ public class ExpeditionMessageParser implements UDPMessageParser<ExpeditionMessa
}
return result;
} else {
System.err.println("Unparsable expedition message: " + packetAsString);
logger.warning("Unparsable expedition message: " + packetAsString);
return null; // couldn't even parse
}
} else {
@@ -72,7 +74,12 @@ public class ExpeditionMessageParser implements UDPMessageParser<ExpeditionMessa
private boolean checksumOk(int checksum, String packetAsString) {
int b = 0;
for (byte stringByte : packetAsString.substring(0, packetAsString.lastIndexOf('*')).getBytes()) {
int posOfLastCharToAddToChecksum = packetAsString.lastIndexOf('*');
if (packetAsString.length() > posOfLastCharToAddToChecksum && packetAsString.charAt(posOfLastCharToAddToChecksum+1) == 'X') {
posOfLastCharToAddToChecksum++;
}
String checksumString = packetAsString.substring(0, posOfLastCharToAddToChecksum);
for (byte stringByte : checksumString.getBytes()) {
b ^= stringByte;
}
return b == checksum;
@@ -46,6 +46,7 @@
<plugin id="com.sap.sailing.www" autoStart="true" startLevel="4" />
<plugin id="com.sap.sailing.www.events" autoStart="true" startLevel="4" />
<plugin id="com.sap.sailing.xcelsiusadapter" autoStart="true" startLevel="4" />
<plugin id="com.sap.sailing.xrr.resultimport" autoStart="true" startLevel="3" />
<plugin id="org.eclipse.equinox.common" autoStart="true" startLevel="2" />
<plugin id="org.eclipse.equinox.simpleconfigurator" autoStart="true" startLevel="2" />
<plugin id="org.eclipse.jetty.continuation" autoStart="true" startLevel="4" />
+7
View File
@@ -230,4 +230,11 @@ Computes leaderboard information for sailing races and offers RESTful APIs to al
version="0.0.0"
unpack="false"/>
<plugin
id="com.sap.sailing.xrr.resultimport"
download-size="0"
install-size="0"
version="0.0.0"
unpack="false"/>
</feature>
@@ -24,7 +24,8 @@ public class TestColumnSwapping {
private LeaderboardDTO lb = null;
private static final String LEADERBOARDNAME = "test";
private static final String DEFAULT_FLEET_NAME = "Default";
private static final FleetDTO DEFAULT_FLEET = new FleetDTO(DEFAULT_FLEET_NAME, /* ordering */ 0, /* color */ null);
private static final String DEFAULT_SERIES_NAME = "Default";
private static final FleetDTO DEFAULT_FLEET = new FleetDTO(DEFAULT_FLEET_NAME, DEFAULT_SERIES_NAME, /* ordering */ 0, /* color */ null);
private SailingServiceImpl service;
private LeaderboardDTO leaderboardOriginalDTO;
private LeaderboardDTO leaderboardDTO;
@@ -131,3 +131,7 @@ body, table td, select, button {
.gwt-DialogBox {
z-index: 20;
}
.bold {
font-family: "OpenSansCondensedBold", sans-serif;
}
+1 -2
View File
@@ -176,8 +176,7 @@ input.magnifier {
margin: 0 0 25px 0;
}
.leaderboardGroupPanel-LeaderboardsTableLabel {
margin: 0 0 10px;
.leaderboardGroupPanel-LeaderboardsLabel {
font-size: 17px;
font-weight: 500;
}
@@ -1,39 +0,0 @@
package com.sap.sailing.gwt.ui.actions;
import java.util.Date;
import java.util.List;
import java.util.Map;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
import com.sap.sailing.gwt.ui.client.SailingServiceAsync;
import com.sap.sailing.gwt.ui.shared.CompetitorDTO;
import com.sap.sailing.gwt.ui.shared.GPSFixDTO;
public class GetBoatPositionsAction extends DefaultAsyncAction<Map<CompetitorDTO, List<GPSFixDTO>>> {
private final SailingServiceAsync sailingService;
private final RegattaAndRaceIdentifier raceIdentifier;
private final Map<CompetitorDTO, Date> from;
private final Map<CompetitorDTO, Date> to;
private final boolean extrapolate;
public GetBoatPositionsAction(SailingServiceAsync sailingService, RegattaAndRaceIdentifier raceIdentifier,
Map<CompetitorDTO, Date> from, Map<CompetitorDTO, Date> to, boolean extrapolate,
AsyncCallback<Map<CompetitorDTO, List<GPSFixDTO>>> callback) {
super(callback);
this.sailingService = sailingService;
this.raceIdentifier = raceIdentifier;
this.from = from;
this.to = to;
this.extrapolate = extrapolate;
}
@Override
public void execute(AsyncActionsExecutor asyncActionsExecutor) {
sailingService.getBoatPositions(raceIdentifier, from, to, extrapolate,
(AsyncCallback<Map<CompetitorDTO, List<GPSFixDTO>>>) getWrapperCallback(asyncActionsExecutor));
}
}
@@ -1,5 +1,7 @@
package com.sap.sailing.gwt.ui.adminconsole;
import java.util.Date;
import com.google.gwt.user.client.ui.DoubleBox;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.Label;
@@ -13,7 +15,7 @@ import com.sap.sailing.gwt.ui.shared.PositionDTO;
import com.sap.sailing.gwt.ui.shared.RaceDTO;
import com.sap.sailing.gwt.ui.shared.WindDTO;
public class WindSettingDialog extends DataEntryDialog<WindDTO> {
public class AddWindFixDialog extends DataEntryDialog<WindDTO> {
private final StringMessages stringMessages;
private final DoubleBox speedInKnotsBox;
@@ -23,7 +25,7 @@ public class WindSettingDialog extends DataEntryDialog<WindDTO> {
private final DateBox timeBox;
protected static class WindDataValidator implements Validator<WindDTO> {
protected final StringMessages stringMessages;
private final StringMessages stringMessages;
public WindDataValidator(StringMessages stringMessages){
super();
@@ -42,6 +44,8 @@ public class WindSettingDialog extends DataEntryDialog<WindDTO> {
errorMessage = stringMessages.pleaseEnterAValue();
} else if(windDTO.trueWindFromDeg != null && (windDTO.trueWindFromDeg < 0.0 || windDTO.trueWindFromDeg > 360.0)){
errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.fromDeg(), "0", "360");
} else if(windDTO.measureTimepoint == null) {
errorMessage = stringMessages.pleaseEnterAValue();
} else if(windDTO.position != null) {
if(windDTO.position.latDeg < -90.0 || windDTO.position.latDeg > 90.0){
errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.latitude(), "-90", "90");
@@ -54,12 +58,12 @@ public class WindSettingDialog extends DataEntryDialog<WindDTO> {
}
}
public WindSettingDialog(RaceDTO race, CoursePositionsDTO courseDTO, StringMessages stringMessages, DialogCallback<WindDTO> callback) {
public AddWindFixDialog(RaceDTO race, CoursePositionsDTO courseDTO, StringMessages stringMessages, DialogCallback<WindDTO> callback) {
super(stringMessages.actionAddWindData(), null, stringMessages.ok(), stringMessages.cancel(), new WindDataValidator(stringMessages), callback);
this.stringMessages = stringMessages;
speedInKnotsBox = createDoubleBox(5);
fromInDegBox = createDoubleBox(5);
if(courseDTO != null && courseDTO.waypointPositions.get(0) != null) {
if(courseDTO != null && courseDTO.waypointPositions != null && courseDTO.waypointPositions.get(0) != null) {
PositionDTO positionDTO = courseDTO.waypointPositions.get(0);
latDegBox = createDoubleBox(positionDTO.latDeg, 10);
lngDegBox = createDoubleBox(positionDTO.lngDeg, 10);
@@ -67,10 +71,11 @@ public class WindSettingDialog extends DataEntryDialog<WindDTO> {
latDegBox = createDoubleBox(10);
lngDegBox = createDoubleBox(10);
}
if(race.startOfRace != null) {
timeBox = createDateBox(race.startOfRace.getTime(), 20);
if(race.trackedRace != null && race.trackedRace.startOfTracking != null) {
timeBox = createDateBox(race.trackedRace.startOfTracking.getTime(), 20);
} else {
timeBox = createDateBox(20);
Date now = new Date();
timeBox = createDateBox(now.getTime(), 20);
}
}
@@ -84,12 +89,12 @@ public class WindSettingDialog extends DataEntryDialog<WindDTO> {
grid.setWidget(0, 1, speedInKnotsBox);
grid.setWidget(1, 0, new Label(stringMessages.fromDeg() + ":"));
grid.setWidget(1, 1, fromInDegBox);
grid.setWidget(2, 0, new Label(stringMessages.latitude() + " (" + stringMessages.optional() + "):"));
grid.setWidget(2, 1, latDegBox);
grid.setWidget(3, 0, new Label(stringMessages.longitude() + " (" + stringMessages.optional() + "):"));
grid.setWidget(3, 1, lngDegBox);
grid.setWidget(4, 0, new Label(stringMessages.time() + " (" + stringMessages.optional() + "):"));
grid.setWidget(4, 1, timeBox);
grid.setWidget(2, 0, new Label(stringMessages.time() + ":"));
grid.setWidget(2, 1, timeBox);
grid.setWidget(3, 0, new Label(stringMessages.latitude() + " (" + stringMessages.optional() + "):"));
grid.setWidget(3, 1, latDegBox);
grid.setWidget(4, 0, new Label(stringMessages.longitude() + " (" + stringMessages.optional() + "):"));
grid.setWidget(4, 1, lngDegBox);
return mainPanel;
}
@@ -100,7 +105,7 @@ public class WindSettingDialog extends DataEntryDialog<WindDTO> {
result.trueWindSpeedInKnots = speedInKnotsBox.getValue();
result.trueWindFromDeg = fromInDegBox.getValue();
result.measureTimepoint = timeBox.getValue() != null ? timeBox.getValue().getTime() : System.currentTimeMillis();
result.measureTimepoint = timeBox.getValue() != null ? timeBox.getValue().getTime() : null;
if (latDegBox.getValue() != null && lngDegBox.getValue() != null) {
result.position = new PositionDTO(latDegBox.getValue(), lngDegBox.getValue());
}
@@ -63,7 +63,7 @@ public class AdminConsoleEntryPoint extends AbstractEntryPoint implements Regatt
tabPanel.add(swisstimingEventManagementPanel, stringMessages.swissTimingEvents(), false);
regattaDisplayers.add(swisstimingEventManagementPanel);
CreateSwissTimingRacePanel createSwissTimingRacePanel = new CreateSwissTimingRacePanel(sailingService,this,stringMessages);
CreateSwissTimingRacePanel createSwissTimingRacePanel = new CreateSwissTimingRacePanel(sailingService, this, stringMessages);
//createSwissTimingRacePanel.ensureDebugId("CreateSwissTimingRace");
createSwissTimingRacePanel.setSize("90%", "90%");
tabPanel.add(createSwissTimingRacePanel,"Create SwissTiming race",false);
@@ -96,7 +96,7 @@ public class AdminConsoleEntryPoint extends AbstractEntryPoint implements Regatt
final LeaderboardConfigPanel leaderboardConfigPanel = new LeaderboardConfigPanel(sailingService, this, this, stringMessages,
/* showRaceDetails */ true);
//leaderboardConfigPanel.ensureDebugId("LeaderboardConfiguration");
leaderboardConfigPanel.ensureDebugId("LeaderboardConfiguration");
leaderboardConfigPanel.setSize("90%", "90%");
tabPanel.add(leaderboardConfigPanel, stringMessages.leaderboardConfiguration(), /* asHTML */ false);
regattaDisplayers.add(leaderboardConfigPanel);
@@ -13,6 +13,7 @@ public class FlexibleLeaderboardCreateDialog extends FlexibleLeaderboardDialog {
Collection<EventDTO> existingEvents, ErrorReporter errorReporter, DialogCallback<LeaderboardDescriptor> callback) {
super(stringMessages.createFlexibleLeaderboard(), new LeaderboardDescriptor(), stringMessages, existingEvents, errorReporter, new FlexibleLeaderboardDialog.LeaderboardParameterValidator(stringMessages, existingLeaderboards), callback);
nameTextBox = createTextBox(null);
nameTextBox.ensureDebugId("LeaderboardNameField");
displayNameTextBox = createTextBox(null);
nameTextBox.setVisibleLength(50);
displayNameTextBox.setVisibleLength(50);
@@ -52,6 +52,7 @@ import com.sap.sailing.gwt.ui.adminconsole.DisablableCheckboxCell.IsEnabled;
import com.sap.sailing.gwt.ui.adminconsole.RaceColumnInLeaderboardDialog.RaceColumnDescriptor;
import com.sap.sailing.gwt.ui.client.DataEntryDialog.DialogCallback;
import com.sap.sailing.gwt.ui.client.ErrorReporter;
import com.sap.sailing.gwt.ui.client.MarkedAsyncCallback;
import com.sap.sailing.gwt.ui.client.ParallelExecutionCallback;
import com.sap.sailing.gwt.ui.client.ParallelExecutionHolder;
import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener;
@@ -311,6 +312,7 @@ public class LeaderboardConfigPanel extends FormPanel implements SelectedLeaderb
leaderboardButtonPanel.setSpacing(5);
leaderboardsPanel.add(leaderboardButtonPanel);
Button createFlexibleLeaderboardBtn = new Button(stringMessages.createFlexibleLeaderboard() + "...");
createFlexibleLeaderboardBtn.ensureDebugId("CreateFlexibleLeaderboardButton");
leaderboardButtonPanel.add(createFlexibleLeaderboardBtn);
createFlexibleLeaderboardBtn.addClickHandler(new ClickHandler() {
@Override
@@ -320,6 +322,7 @@ public class LeaderboardConfigPanel extends FormPanel implements SelectedLeaderb
});
Button createRegattaLeaderboardBtn = new Button(stringMessages.createRegattaLeaderboard() + "...");
createRegattaLeaderboardBtn.ensureDebugId("CreateRegattaLeaderboardButton");
leaderboardButtonPanel.add(createRegattaLeaderboardBtn);
createRegattaLeaderboardBtn.addClickHandler(new ClickHandler() {
@Override
@@ -929,15 +932,15 @@ public class LeaderboardConfigPanel extends FormPanel implements SelectedLeaderb
}
private void createFlexibleLeaderboard() {
sailingService.getEvents(new AsyncCallback<List<EventDTO>>() {
sailingService.getEvents(new MarkedAsyncCallback<List<EventDTO>>() {
@Override
public void onSuccess(List<EventDTO> result) {
public void handleSuccess(List<EventDTO> result) {
createFlexibleLeaderboard(result);
}
@Override
public void onFailure(Throwable caught) {
public void handleFailure(Throwable caught) {
createFlexibleLeaderboard(new ArrayList<EventDTO>());
}
});
@@ -955,20 +958,21 @@ public class LeaderboardConfigPanel extends FormPanel implements SelectedLeaderb
public void ok(final LeaderboardDescriptor newLeaderboard) {
sailingService.createFlexibleLeaderboard(newLeaderboard.getName(), newLeaderboard.getDisplayName(), newLeaderboard.getDiscardThresholds(),
newLeaderboard.getScoringScheme(), newLeaderboard.getCourseAreaIdAsString(),
new AsyncCallback<StrippedLeaderboardDTO>() {
new MarkedAsyncCallback<StrippedLeaderboardDTO>() {
@Override
public void onFailure(Throwable t) {
public void handleFailure(Throwable t) {
errorReporter.reportError("Error trying to create the new flexible leaderboard " + newLeaderboard.getName()
+ ": " + t.getMessage());
}
@Override
public void onSuccess(StrippedLeaderboardDTO result) {
public void handleSuccess(StrippedLeaderboardDTO result) {
addLeaderboard(result);
}
});
}
});
dialog.ensureDebugId("CreateFlexibleLeaderboardDialog");
dialog.show();
}
@@ -138,7 +138,7 @@ public class RaceColumnInLeaderboardDialog extends DataEntryDialog<RaceColumnInL
super.show();
if (isRegattaLeaderboard) {
explicitFactorBox.setFocus(true);
explicitFactorBox.setSelectionRange(0, explicitFactorBox.getText().length());
explicitFactorBox.selectAll();
} else {
raceNameBox.setFocus(true);
}
@@ -34,11 +34,14 @@ import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.view.client.ListDataProvider;
import com.google.gwt.view.client.MultiSelectionModel;
import com.sap.sailing.domain.common.RegattaIdentifier;
import com.sap.sailing.domain.common.RegattaName;
import com.sap.sailing.gwt.ui.client.ErrorReporter;
import com.sap.sailing.gwt.ui.client.RaceSelectionModel;
import com.sap.sailing.gwt.ui.client.RegattaRefresher;
import com.sap.sailing.gwt.ui.client.SailingServiceAsync;
import com.sap.sailing.gwt.ui.client.StringMessages;
import com.sap.sailing.gwt.ui.shared.RegattaDTO;
import com.sap.sailing.gwt.ui.shared.SwissTimingConfigurationDTO;
import com.sap.sailing.gwt.ui.shared.SwissTimingRaceRecordDTO;
@@ -138,7 +141,6 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane
}
});
TextColumn<SwissTimingRaceRecordDTO> raceNameColumn = new TextColumn<SwissTimingRaceRecordDTO>() {
@Override
public String getValue(SwissTimingRaceRecordDTO object) {
@@ -146,6 +148,24 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane
}
};
TextColumn<SwissTimingRaceRecordDTO> raceDescriptionColumn = new TextColumn<SwissTimingRaceRecordDTO>() {
@Override
public String getValue(SwissTimingRaceRecordDTO object) {
return object.description;
}
};
TextColumn<SwissTimingRaceRecordDTO> boatClassColumn = new TextColumn<SwissTimingRaceRecordDTO>() {
@Override
public String getValue(SwissTimingRaceRecordDTO object) {
String result = object.boatClass != null ? object.boatClass : "";
if(object.discipline != null) {
result += " (" + object.discipline + ")";
}
return result;
}
};
TextColumn<SwissTimingRaceRecordDTO> raceStartTrackingColumn = new TextColumn<SwissTimingRaceRecordDTO>() {
@Override
public String getValue(SwissTimingRaceRecordDTO object) {
@@ -153,7 +173,6 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane
}
};
HorizontalPanel racesSplitPanel = new HorizontalPanel();
mainPanel.add(racesSplitPanel);
@@ -179,6 +198,15 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane
filterPanel.setSpacing(5);
racesPanel.add(filterPanel);
// Regatta
HorizontalPanel regattaPanel = new HorizontalPanel();
regattaPanel.setSpacing(5);
Label regattaForTrackingLabel = new Label(stringMessages.regattaUsedForTheTrackedRace());
regattaForTrackingLabel.setWordWrap(false);
regattaPanel.add(regattaForTrackingLabel);
regattaPanel.add(getAvailableRegattasListBox());
racesPanel.add(regattaPanel);
Label lblFilterEvents = new Label(stringConstants.filterRacesByName() + ":");
filterPanel.add(lblFilterEvents);
filterPanel.setCellVerticalAlignment(lblFilterEvents, HasVerticalAlignment.ALIGN_MIDDLE);
@@ -205,6 +233,8 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane
AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class);
raceTable = new CellTable<SwissTimingRaceRecordDTO>(/* pageSize */ 10000, tableRes);
raceTable.addColumn(raceNameColumn, stringConstants.name());
raceTable.addColumn(raceDescriptionColumn, stringConstants.description());
raceTable.addColumn(boatClassColumn, stringConstants.boatClass());
raceTable.addColumn(raceStartTrackingColumn, stringConstants.raceStartTimeColumn());
raceTable.setWidth("300px");
raceTable.setSelectionModel(new MultiSelectionModel<SwissTimingRaceRecordDTO>() {});
@@ -345,8 +375,13 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane
selectedRaces.add(race);
}
}
RegattaDTO selectedRegatta = getSelectedRegatta();
RegattaIdentifier regattaIdentifier = null;
if (selectedRegatta != null) {
regattaIdentifier = new RegattaName(selectedRegatta.name);
}
sailingService.trackWithSwissTiming(
/* regattaToAddTo */null, // TODO allow user to select a pre-defined regatta
/* regattaToAddTo */ regattaIdentifier,
selectedRaces, hostname, port, /* canSendRequests */false, trackWind, correctWindByDeclination,
new AsyncCallback<Void>() {
@Override
@@ -186,9 +186,8 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel {
layoutTable.setWidget(2, 0, hostnameLabel);
layoutTable.setWidget(2, 1, this.hostnameTextBox);
// Regatta name
// Event name
Label eventNameLabel = new Label(this.stringMessages.eventName() + ":");
this.eventNameTextBox = new TextBox();
this.eventNameTextBox.ensureDebugId("RegattaName");
this.eventNameTextBox.setText("event_2011...");
@@ -198,7 +197,6 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel {
synchURIs();
}
});
layoutTable.setWidget(3, 0, eventNameLabel);
layoutTable.setWidget(3, 1, this.eventNameTextBox);
@@ -338,9 +336,8 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel {
//columnFormatter.setWidth(1, "80%");
// Regatta
Label regattaForTrackingLabel = new Label("Regatta used for the tracked race:");
Label regattaForTrackingLabel = new Label(stringMessages.regattaUsedForTheTrackedRace());
regattaForTrackingLabel.setWordWrap(false);
layoutTable.setWidget(0, 0, regattaForTrackingLabel);
layoutTable.setWidget(0, 1, getAvailableRegattasListBox());
@@ -619,11 +616,9 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel {
String storedURI = this.storedURITextBox.getValue();
RegattaDTO selectedRegatta = getSelectedRegatta();
RegattaIdentifier regattaIdentifier = null;
if (selectedRegatta != null) {
regattaIdentifier = new RegattaName(selectedRegatta.name);
}
// Check if the assigned regatta makes sense
List<TracTracRaceRecordDTO> allRaces = this.raceList.getList();
List<TracTracRaceRecordDTO> racesWithNotMatchingBoatClasses = new ArrayList<TracTracRaceRecordDTO>();
@@ -631,33 +626,27 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel {
for (TracTracRaceRecordDTO race : allRaces) {
if (selectionModel.isSelected(race)) {
if(!checkBoatClassMatch(race, selectedRegatta))
if (!checkBoatClassMatch(race, selectedRegatta))
racesWithNotMatchingBoatClasses.add(race);
}
}
if(racesWithNotMatchingBoatClasses.size() > 0) {
if (racesWithNotMatchingBoatClasses.size() > 0) {
StringBuilder builder = new StringBuilder(100 + racesWithNotMatchingBoatClasses.size() * 30);
builder.append("WARNING\n");
if(selectedRegatta != null) {
if (selectedRegatta != null) {
builder.append(this.stringMessages.boatClassDoesNotMatchSelectedRegatta(selectedRegatta.boatClass.name,
selectedRegatta.name));
} else {
builder.append(this.stringMessages.regattaExistForSelectedBoatClass());
}
builder.append("\n\n");
builder.append(this.stringMessages.races());
builder.append("\n");
for(TracTracRaceRecordDTO record: racesWithNotMatchingBoatClasses) {
for (TracTracRaceRecordDTO record: racesWithNotMatchingBoatClasses) {
builder.append(record.name);
builder.append("\n");
}
if(!Window.confirm(builder.toString())) {
if (!Window.confirm(builder.toString())) {
return;
}
}
@@ -109,7 +109,7 @@ public class WindPanel extends FormPanel implements RegattaDisplayer, WindShower
RegattaAndRaceIdentifier selectedRace = getSelectedRace();
if(selectedRace != null) {
final RaceDTO race = trackedRacesListComposite.getRaceByIdentifier(selectedRace);
sailingService.getCoursePositions(selectedRace, race.startOfRace, new AsyncCallback<CoursePositionsDTO>() {
sailingService.getCoursePositions(selectedRace, race.trackedRace.startOfTracking, new AsyncCallback<CoursePositionsDTO>() {
@Override
public void onFailure(Throwable caught) {
showWindSettingDialog(race, null);
@@ -215,7 +215,7 @@ public class WindPanel extends FormPanel implements RegattaDisplayer, WindShower
}
private void showWindSettingDialog(RaceDTO race, CoursePositionsDTO course) {
WindSettingDialog windSettingDialog = new WindSettingDialog(race, course, stringMessages,
AddWindFixDialog windSettingDialog = new AddWindFixDialog(race, course, stringMessages,
new DialogCallback<WindDTO>() {
@Override
public void cancel() {
@@ -88,9 +88,11 @@ public abstract class DataEntryDialog<T> {
this.validator = validator;
okButton = new Button(okButtonName);
okButton.getElement().getStyle().setMargin(3, Unit.PX);
okButton.ensureDebugId("OkButton");
FlowPanel dialogFPanel = new FlowPanel();
dialogFPanel.setWidth("100%");
statusLabel = new Label();
statusLabel.ensureDebugId("StatusLabel");
dialogFPanel.add(statusLabel);
if (message != null) {
Label messageLabel = new Label(message);
@@ -107,6 +109,7 @@ public abstract class DataEntryDialog<T> {
buttonPanel.add(okButton);
cancelButton = new Button(cancelButtonName);
cancelButton.getElement().getStyle().setMargin(3, Unit.PX);
cancelButton.ensureDebugId("CancelButton");
buttonPanel.add(cancelButton);
cancelButton.addClickHandler(new ClickHandler() {
@Override
@@ -238,19 +241,20 @@ public abstract class DataEntryDialog<T> {
}
private DateBox createDateBoxInternal(Date initialDate, int visibleLength) {
DateBox textBox = new DateBox();
textBox.getTextBox().setVisibleLength(visibleLength);
textBox.setValue(initialDate);
AbstractEntryPoint.addFocusUponKeyUpToggler(textBox.getTextBox());
textBox.getTextBox().addChangeHandler(new ChangeHandler() {
DateBox dateBox = new DateBox();
dateBox.getTextBox().setVisibleLength(visibleLength);
dateBox.setFireNullValues(true);
dateBox.setValue(initialDate);
AbstractEntryPoint.addFocusUponKeyUpToggler(dateBox.getTextBox());
dateBox.addValueChangeHandler(new ValueChangeHandler<Date>() {
@Override
public void onChange(ChangeEvent event) {
public void onValueChange(ValueChangeEvent<Date> event) {
validate();
}
});
AbstractEntryPoint.linkEnterToButton(getOkButton(), textBox.getTextBox());
AbstractEntryPoint.linkEscapeToButton(getCancelButton(), textBox.getTextBox());
return textBox;
AbstractEntryPoint.linkEnterToButton(getOkButton(), dateBox.getTextBox());
AbstractEntryPoint.linkEscapeToButton(getCancelButton(), dateBox.getTextBox());
return dateBox;
}
/**
@@ -405,4 +409,7 @@ public abstract class DataEntryDialog<T> {
dateEntryDialog.center();
}
public void ensureDebugId(String debugId) {
dateEntryDialog.ensureDebugId(debugId);
}
}
@@ -50,11 +50,8 @@ public class RaceTimePanel extends TimePanel<RaceTimePanelSettings> implements R
// in case the race is not tracked anymore we reset the time slider
resetTimeSlider();
} else {
// check if the live delay is already been set
if (!isUserExplicitlyChangedLivePlayDelay() && timer.getLivePlayDelayInMillis() != raceTimesInfo.delayToLiveInMs) {
timer.setLivePlayDelayInMillis(raceTimesInfo.delayToLiveInMs);
}
// timer will only accept this update if the delay hasn't been updated explicitly
timer.setLivePlayDelayInMillis(raceTimesInfo.delayToLiveInMs);
if ((raceTimesInfo.startOfTracking != null || raceTimesInfo.startOfRace != null) &&
(raceTimesInfo.newestTrackingEvent != null || raceTimesInfo.endOfRace != null)) {
// we set here the min and max of the time slider, the start and end of the race as well as the known
@@ -109,13 +106,14 @@ public class RaceTimePanel extends TimePanel<RaceTimePanelSettings> implements R
protected boolean isLiveModeToBeMadePossible() {
long livePlayDelayInMillis = timer.getLivePlayDelayInMillis();
long eventTimeoutTolerance = 60 * 1000; // 60s
long timeBeforeRaceStartTolerance = 3 * 60 * 1000; // 3min
long liveTimePointInMillis = System.currentTimeMillis() - livePlayDelayInMillis;
RaceTimesInfoDTO lastRaceTimesInfo = raceTimesInfoProvider != null ? raceTimesInfoProvider.getRaceTimesInfo(selectedRace) : null;
return lastRaceTimesInfo != null &&
lastRaceTimesInfo.newestTrackingEvent != null &&
liveTimePointInMillis < lastRaceTimesInfo.newestTrackingEvent.getTime() + eventTimeoutTolerance &&
lastRaceTimesInfo.startOfTracking != null &&
liveTimePointInMillis > lastRaceTimesInfo.startOfTracking.getTime();
((lastRaceTimesInfo.startOfTracking != null && liveTimePointInMillis > lastRaceTimesInfo.startOfTracking.getTime())||
(lastRaceTimesInfo.startOfRace != null && liveTimePointInMillis > lastRaceTimesInfo.startOfRace.getTime() - timeBeforeRaceStartTolerance));
}
@Override
@@ -367,6 +367,7 @@ public interface StringMessages extends Messages {
String insertWaypointAfterSelected();
String scoringSchemeLowPointSystem();
String scoringSchemeHighPointSystem();
String scoringSchemeLowPointWinnerGetsZero();
String scoringSystem();
String createFlexibleLeaderboard();
String createRegattaLeaderboard();
@@ -520,4 +521,7 @@ public interface StringMessages extends Messages {
String correctScoreFor(String competitorName, String raceColumnName);
String penaltyOrRedress();
String netScore();
String timePointMustBeAfterStartOfTracking();
String timePointMustBeBeforeEndOfTracking();
String regattaUsedForTheTrackedRace();
}
@@ -395,6 +395,7 @@ scoringSchemeLowPointSystem=Low Point System
scoringSchemeHighPointSystem=High Point System
scoringSchemeHighPointEssOverall=High Point System ESS Overall
scoringSchemeHighPointLastBreaksTie=High Point System, Last Race Breaks Tie
scoringSchemeLowPointWinnerGetsZero=Low Point System, Winner Gets Zero
scoringSystem=Scoring system
createFlexibleLeaderboard=Create flexible leaderboard
createRegattaLeaderboard=Create regatta leaderboard
@@ -522,3 +523,6 @@ correctScore=Correct Score
correctScoreFor=Correct score for competitor {0} in race {1}
penaltyOrRedress=Penalty/Redress
netScore=Net points
timePointMustBeAfterStartOfTracking=Time point must be after start of tracking.
timePointMustBeBeforeEndOfTracking=Time point must be before end of tracking.
regattaUsedForTheTrackedRace=Regatta used for the tracked race
@@ -396,6 +396,7 @@ scoringSchemeLowPointSystem=Low Point System
scoringSchemeHighPointSystem=High Point System
scoringSchemeHighPointEssOverall=High Point System ESS Gesamtwertung
scoringSchemeHighPointLastBreaksTie=High Point System, letztes Rennen entscheidet bei Gleichstand
scoringSchemeLowPointWinnerGetsZero=Low Point System, Gewinner erhält 0 Punkte
scoringSystem=Bepunktungssystem
createFlexibleLeaderboard=Flexible Rangliste anlegen
createRegattaLeaderboard=Rangliste für Regatta anlegen
@@ -522,3 +523,6 @@ correctScore=Ergebnis bearbeiten
correctScoreFor=Ergebnis für Teilnehmer {0} in Rennen {1} bearbeiten
penaltyOrRedress=Strafe/Gutschrift
netScore=Nettoergebnis
timePointMustBeAfterStartOfTracking=Der Zeitpunkt muß nach dem Trackingbeginn sein.
timePointMustBeBeforeEndOfTracking=Der Zeitpunkt muß vor dem Trackingende sein.
regattaUsedForTheTrackedRace=Regatta, zu der das Rennen rehört
@@ -55,13 +55,6 @@ public class TimePanel<T extends TimePanelSettings> extends FormPanel implements
private final Button slowDownButton;
private final Button speedUpButton;
/**
* The live delay may be adjusted automatically if the server decides so. However, if the user explicitly sets a live delay,
* server-side updates to the delay should be suppressed. This flag records whether the user has performed a manual delay
* override.
*/
private boolean userExplicitlyChangedLivePlayDelay;
/**
* the minimum time the slider extends it's time when the end of the slider is reached
*/
@@ -89,7 +82,6 @@ public class TimePanel<T extends TimePanelSettings> extends FormPanel implements
timer.addTimeListener(this);
timer.addPlayStateListener(this);
timeRangeProvider.addTimeRangeChangeListener(this);
userExplicitlyChangedLivePlayDelay = false;
FlowPanel timePanelInnerWrapper = new FlowPanel();
timePanelInnerWrapper.setStyleName("timePanelInnerWrapper");
timePanelInnerWrapper.setSize("100%", "100%");
@@ -460,16 +452,12 @@ public class TimePanel<T extends TimePanelSettings> extends FormPanel implements
return new TimePanelSettingsDialogComponent<T>(getSettings(), stringMessages);
}
protected boolean isUserExplicitlyChangedLivePlayDelay() {
return userExplicitlyChangedLivePlayDelay;
}
@Override
public void updateSettings(T newSettings) {
boolean delayChanged = newSettings.getDelayToLivePlayInSeconds() != getSettings().getDelayToLivePlayInSeconds();
if (delayChanged) {
userExplicitlyChangedLivePlayDelay = true;
timer.setLivePlayDelayInMillis(1000l * newSettings.getDelayToLivePlayInSeconds());
// explicit change always goes through and disables further automatic delay updates in timer
timer.setLivePlayDelayInMillisExplicitly(1000l * newSettings.getDelayToLivePlayInSeconds());
if (timer.getPlayMode() == PlayModes.Live) {
timeDelayLabel.setText(String.valueOf(newSettings.getDelayToLivePlayInSeconds()) + " s");
}
@@ -79,6 +79,13 @@ public class Timer {
*/
private boolean autoAdvance;
/**
* If {@link #setLivePlayDelayInMillisExplicitly(long)} was called, this flag is set to <code>true</code> which will
* cause calls to {@link #setLivePlayDelayInMillis(long)} to be ignored. Then, the only way to update the live delay is
* to call {@link #setLivePlayDelayInMillisExplicitly(long)}.
*/
private boolean delaySetExplicity;
/**
* The timer can run in two different modes: Live and Replay
* 'Live' means the timer is used for a real time event
@@ -261,6 +268,21 @@ public class Timer {
}
public void setLivePlayDelayInMillis(long delayInMilliseconds) {
if (!delaySetExplicity) {
basicSetLivePlayDelayInMillis(delayInMilliseconds);
}
}
/**
* Always updates the delay if it is different from the current delay setting. The fact that an explicit delay update was
* performed is recorded which will block further automatic delay updates from this point onwards.
*/
public void setLivePlayDelayInMillisExplicitly(long delayInMilliseconds) {
basicSetLivePlayDelayInMillis(delayInMilliseconds);
delaySetExplicity = true;
}
private void basicSetLivePlayDelayInMillis(long delayInMilliseconds) {
if (this.livePlayDelayInMillis != delayInMilliseconds) {
this.livePlayDelayInMillis = delayInMilliseconds;
if (getPlayMode() == PlayModes.Live) {
@@ -24,10 +24,13 @@ public abstract class AbstractRaceColumnSelection implements RaceColumnSelection
@Override
public Iterable<RaceColumnDTO> getSelectedRaceColumnsOrderedAsInLeaderboard(LeaderboardDTO leaderboard) {
Iterable<RaceColumnDTO> selectedColumns = getSelectedRaceColumns();
List<String> namesOfSelectedRaceColumns = new ArrayList<String>();
for (RaceColumnDTO selectedColumn : getSelectedRaceColumns()) {
namesOfSelectedRaceColumns.add(selectedColumn.name);
}
List<RaceColumnDTO> result = new ArrayList<RaceColumnDTO>();
for (RaceColumnDTO column : leaderboard.getRaceList()) {
if (Util.contains(selectedColumns, column)) {
if (Util.contains(namesOfSelectedRaceColumns, column.name)) {
result.add(column);
}
}
@@ -4,7 +4,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@@ -15,15 +14,19 @@ import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider;
import com.sap.sailing.gwt.ui.client.RaceTimesInfoProviderListener;
import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettings.RaceColumnSelectionStrategies;
import com.sap.sailing.gwt.ui.shared.CompetitorDTO;
import com.sap.sailing.gwt.ui.shared.FleetDTO;
import com.sap.sailing.gwt.ui.shared.LeaderboardDTO;
import com.sap.sailing.gwt.ui.shared.LeaderboardEntryDTO;
import com.sap.sailing.gwt.ui.shared.LeaderboardRowDTO;
import com.sap.sailing.gwt.ui.shared.RaceColumnDTO;
import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO;
/**
* Doesn't respect requests to explicitly turn on or off certain columns. Instead, up to the last <code>n</code> (with
* <code>n</code> being an integer number to configure through the constructor) race columns will be selected. "Last" is
* decided based on the race start times which are obtained through a {@link RaceTimesInfoProvider}.
* decided based on the column ordering in the leaderboard, considering races with a valid start time which are obtained
* through a {@link RaceTimesInfoProvider} or a score correction.
*
* @author Axel Uhl (D043530)
*
@@ -87,33 +90,35 @@ public class LastNRacesColumnSelection extends AbstractRaceColumnSelection imple
this.raceTimesInfo = raceTimesInfo;
}
private Iterable<RaceColumnDTO> getRaceColumnsFromNewestToOldest(LeaderboardDTO leaderboard,
private Iterable<RaceColumnDTO> getRaceColumnsFromNewestToOldest(final LeaderboardDTO leaderboard,
Map<RegattaAndRaceIdentifier, RaceTimesInfoDTO> raceTimesInfo) {
final Map<RaceColumnDTO, Date> latestStarts = new HashMap<RaceColumnDTO, Date>();
List<RaceColumnDTO> columns = new ArrayList<RaceColumnDTO>(leaderboard.getRaceList());
for (Iterator<RaceColumnDTO> i=columns.iterator(); i.hasNext(); ) {
RaceColumnDTO column = i.next();
Date latestStart = getLatestStart(column);
if (latestStart == null) {
if (getLatestStart(column) == null && !hasScoreCorrections(leaderboard, column)) {
i.remove();
} else {
latestStarts.put(column, latestStart);
}
}
Comparator<RaceColumnDTO> comparator = new Comparator<RaceColumnDTO>() {
@Override
public int compare(RaceColumnDTO o1, RaceColumnDTO o2) {
int result;
Date latestStartO1 = latestStarts.get(o1);
Date latestStartO2 = latestStarts.get(o2);
result = latestStartO2.compareTo(latestStartO1);
return result;
return leaderboard.getRaceList().indexOf(o2) - leaderboard.getRaceList().indexOf(o1);
}
};
Collections.sort(columns, comparator);
return columns;
}
private boolean hasScoreCorrections(LeaderboardDTO leaderboard, RaceColumnDTO column) {
for (Map.Entry<CompetitorDTO, LeaderboardRowDTO> e : leaderboard.rows.entrySet()) {
LeaderboardEntryDTO entry = e.getValue().fieldsByRaceColumnName.get(column.name);
if (entry != null && entry.hasScoreCorrection()) {
return true;
}
}
return false;
}
private Date getLatestStart(RaceColumnDTO column) {
Date latestStart = null;
for (FleetDTO fleet : column.getFleets()) {
@@ -47,6 +47,14 @@ public class LeaderboardEntryPoint extends AbstractEntryPoint {
private static final String PARAM_REFRESH_INTERVAL_MILLIS = "refreshIntervalMillis";
private static final String PARAM_SHOW_OVERALL_LEADERBOARDS_ON_SAME_PAGE = "showOverallLeaderboardsOnSamePage";
private static final String PARAM_DELAY_TO_LIVE_MILLIS = "delayToLiveMillis";
/**
* Parameter to support scaling the complete page by a given factor. This works by either using the
* CSS3 zoom property or by applying scale operation to the body element. This comes in handy
* when having to deal with screens that have high resolutions and that can't be controlled manually.
* It is also a very simple method of adapting the viewport to a tv resolution. This parameter works
* with value from 0.0 to 10.0 where 1.0 denotes the unchanged level (100%).
*/
private static final String PARAM_ZOOM_TO = "zoomTo";
/**
@@ -80,7 +88,7 @@ public class LeaderboardEntryPoint extends AbstractEntryPoint {
final String zoomTo = Window.Location.getParameter(PARAM_ZOOM_TO);
if (zoomTo != null) {
RootPanel.getBodyElement().setAttribute("style", "zoom:"+zoomTo);
RootPanel.getBodyElement().setAttribute("style", "zoom: "+zoomTo+";-moz-transform: scale("+zoomTo+");-moz-transform-origin: 0 0;-o-transform: scale("+zoomTo+");-o-transform-origin: 0 0;-webkit-transform: scale("+zoomTo+");-webkit-transform-origin: 0 0;");
}
}
@@ -242,7 +242,7 @@ public class LeaderboardPanel extends FormPanel implements TimeListener, PlaySta
private final Label liveRaceLabel;
private final DateTimeFormat dateFormatter = DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.DATE_LONG);
private final DateTimeFormat timeFormatter = DateTimeFormat.getFormat("HH:mm:ss");
private final DateTimeFormat timeFormatter = DateTimeFormat.getFormat("HH:mm:ss zzz");
private boolean isEmbedded = false;
@@ -430,7 +430,7 @@ public class LeaderboardPanel extends FormPanel implements TimeListener, PlaySta
timer.setRefreshInterval(newSettings.getDelayBetweenAutoAdvancesInMilliseconds());
}
if (newSettings.getDelayInMilliseconds() != null) {
setDelayInMilliseconds(newSettings.getDelayInMilliseconds());
timer.setLivePlayDelayInMillisExplicitly(newSettings.getDelayInMilliseconds());
}
for (ExpandableSortableColumn<?> expandableSortableColumn : columnsToExpandAgain) {
expandableSortableColumn.toggleExpansion();
@@ -1840,11 +1840,11 @@ public class LeaderboardPanel extends FormPanel implements TimeListener, PlaySta
/**
* Based on the {@link LeaderboardDTO#getDelayToLiveInMillisForLatestRace()} for the race that has the latest
* {@link RaceColumnDTO#getStartDate(FleetDTO) start time}, if no {@link #settingsUpdatedExplicitly explicit setting
* changes} were performed, automatically adjusts the delay accordingly.
* {@link RaceColumnDTO#getStartDate(FleetDTO) start time}, automatically adjusts the delay accordingly unless it
* was manually explicitly updated.
*/
private void adjustDelayToLive() {
if (!settingsUpdatedExplicitly && leaderboard.getDelayToLiveInMillisForLatestRace() != null) {
if (leaderboard.getDelayToLiveInMillisForLatestRace() != null) {
setDelayInMilliseconds(leaderboard.getDelayToLiveInMillisForLatestRace());
}
}
@@ -4,18 +4,20 @@ import com.sap.sailing.domain.common.ScoringSchemeType;
import com.sap.sailing.gwt.ui.client.StringMessages;
public class ScoringSchemeTypeFormatter {
public static String format(ScoringSchemeType scoringSchemeType, StringMessages stringConstants) {
public static String format(ScoringSchemeType scoringSchemeType, StringMessages stringMessages) {
switch (scoringSchemeType) {
case LOW_POINT:
return stringConstants.scoringSchemeLowPointSystem();
return stringMessages.scoringSchemeLowPointSystem();
case HIGH_POINT:
return stringConstants.scoringSchemeHighPointSystem();
return stringMessages.scoringSchemeHighPointSystem();
case HIGH_POINT_ESS_OVERALL:
return stringConstants.scoringSchemeHighPointEssOverall();
return stringMessages.scoringSchemeHighPointEssOverall();
case HIGH_POINT_LAST_BREAKS_TIE:
return stringConstants.scoringSchemeHighPointLastBreaksTie();
return stringMessages.scoringSchemeHighPointLastBreaksTie();
case HIGH_POINT_FIRST_GETS_TEN:
return stringConstants.scoringSchemeHighPointFirstGetsTen();
return stringMessages.scoringSchemeHighPointFirstGetsTen();
case LOW_POINT_WINNER_GETS_ZERO:
return stringMessages.scoringSchemeLowPointWinnerGetsZero();
}
return null;
}
@@ -60,4 +60,11 @@ public class EditScoreDialog extends DataEntryDialog<Pair<MaxPointsReason, Doubl
grid.setWidget(1, 1, netPointsBox);
return grid;
}
@Override
public void show() {
super.show();
netPointsBox.setFocus(true);
netPointsBox.selectAll();
}
}
@@ -390,7 +390,7 @@ public class EditableLeaderboardPanel extends LeaderboardPanel {
String prefix;
String suffix;
if (netPointsCorrected) {
prefix = "<b>";
prefix = "<b class='bold'>";
suffix = "</b>";
} else {
prefix = "";
@@ -251,12 +251,16 @@ public class MatchAndApplyScoreCorrectionsDialog extends DataEntryDialog<BulkSco
if (officialSailID != null && raceNameOrNumber != null) {
ScoreCorrectionEntryDTO officialCorrectionEntry = regattaScoreCorrection
.getScoreCorrectionsByRaceNameOrNumber().get(raceNameOrNumber).get(officialSailID);
result.addMaxPointsReasonUpdate(competitor, raceColumn,
officialCorrectionEntry.getMaxPointsReason());
if (officialCorrectionEntry.getScore() != null) {
double officialTotalPoints = officialCorrectionEntry.getScore().doubleValue();
double officialNetPoints = officialTotalPoints / raceColumn.getEffectiveFactor();
result.addScoreUpdate(competitor, raceColumn, officialNetPoints);
// the competitor may not have an official result for this race, for example, if in case of split fleets
// the competitor raced in a different fleet
if (officialCorrectionEntry != null) {
result.addMaxPointsReasonUpdate(competitor, raceColumn,
officialCorrectionEntry.getMaxPointsReason());
if (officialCorrectionEntry.getScore() != null) {
double officialTotalPoints = officialCorrectionEntry.getScore().doubleValue();
double officialNetPoints = officialTotalPoints / raceColumn.getEffectiveFactor();
result.addScoreUpdate(competitor, raceColumn, officialNetPoints);
}
}
}
}
@@ -308,36 +312,42 @@ public class MatchAndApplyScoreCorrectionsDialog extends DataEntryDialog<BulkSco
ScoreCorrectionEntryDTO officialCorrectionEntry =
regattaScoreCorrection.getScoreCorrectionsByRaceNameOrNumber()
.get(raceNameOrNumber).get(officialSailID);
final Double officialTotalPoints = officialCorrectionEntry.isDiscarded() ? new Double(0) : officialCorrectionEntry.getScore();
final Double officialNetPoints = officialCorrectionEntry.getScore() == null ? null :
final Double officialTotalPoints = officialCorrectionEntry == null ? null :
officialCorrectionEntry.isDiscarded() ? new Double(0) : officialCorrectionEntry.getScore();
final Double officialNetPoints = officialCorrectionEntry == null ? null :
officialCorrectionEntry.getScore() == null ? null :
officialCorrectionEntry.getScore() / raceColumn.getEffectiveFactor();
final MaxPointsReason officialMaxPointsReason = officialCorrectionEntry == null ? null :
officialCorrectionEntry.getMaxPointsReason();
SafeHtmlBuilder sb = new SafeHtmlBuilder();
boolean entriesDiffer =
((officialNetPoints == null && entry.netPoints != null) || (officialNetPoints != null && entry.netPoints != null && !new Double(entry.netPoints).equals(officialNetPoints))) ||
((officialTotalPoints == null && entry.totalPoints != null) || (officialTotalPoints != null && entry.totalPoints != null && !new Double(entry.totalPoints).equals(officialTotalPoints))) ||
((officialCorrectionEntry.getMaxPointsReason() == null && entry.reasonForMaxPoints != MaxPointsReason.NONE) ||
officialCorrectionEntry.getMaxPointsReason() != null && officialCorrectionEntry.getMaxPointsReason() != entry.reasonForMaxPoints);
// entries not considered "different' in case we haven't got an entry for the competitor/race at all;
// those non-existing "entries" will be ignored by getResults anyhow
boolean entriesDiffer = officialCorrectionEntry != null &&
(((officialNetPoints == null && entry.netPoints != null) || (officialNetPoints != null && (entry.netPoints == null || !new Double(entry.netPoints).equals(officialNetPoints)))) ||
((officialTotalPoints == null && entry.totalPoints != null) || (officialTotalPoints != null && (entry.totalPoints == null || !new Double(entry.totalPoints).equals(officialTotalPoints)))) ||
((officialMaxPointsReason == null && entry.reasonForMaxPoints != MaxPointsReason.NONE) ||
officialMaxPointsReason != null && officialMaxPointsReason != entry.reasonForMaxPoints));
if (entriesDiffer) {
sb.appendHtmlConstant("<span style=\"color: #0000FF;\"><b>");
}
if (officialNetPoints == null) {
sb.append(0);
sb.appendEscaped("null");
} else {
sb.append(officialNetPoints);
}
sb.appendEscaped("/");
if (officialTotalPoints == null) {
sb.append(0);
sb.appendEscaped("null");
} else {
sb.append(officialTotalPoints);
}
sb.appendEscaped("/");
if (officialCorrectionEntry.getMaxPointsReason() != null) {
sb.appendEscaped(officialCorrectionEntry.getMaxPointsReason().name());
if (officialMaxPointsReason != null) {
sb.appendEscaped(officialMaxPointsReason.name());
} else {
sb.appendEscaped(MaxPointsReason.NONE.name());
}
if (officialCorrectionEntry.isDiscarded()) {
if (officialCorrectionEntry != null && officialCorrectionEntry.isDiscarded()) {
sb.appendEscaped("/discarded");
}
if (entriesDiffer) {
@@ -88,11 +88,11 @@ public class MediaSelector implements /*RaceTimesInfoProviderListener,*/ PlaySta
Collection<MediaTrack> reachableVideoTracks = new ArrayList<MediaTrack>();
Collection<MediaTrack> reachableAudioTracks = new ArrayList<MediaTrack>();
for (MediaTrack mediaTrack : mediaTracks) {
if (MediaTrack.Status.REACHABLE.equals(mediaTrack.status)) {
if (isPotentiallyPlayable(mediaTrack)) {
switch (mediaTrack.mimeType.mediaType) {
case video:
reachableVideoTracks.add(mediaTrack);
case audio: //intentionall fall through
case audio: //intentional fall through
reachableAudioTracks.add(mediaTrack);
}
}
@@ -129,20 +129,19 @@ public class MediaSelector implements /*RaceTimesInfoProviderListener,*/ PlaySta
}
private boolean isPotentiallyPlayable(MediaTrack mediaTrack) {
return MediaTrack.Status.REACHABLE.equals(mediaTrack.status) || MediaTrack.Status.UNDEFINED.equals(mediaTrack.status);
}
private void setStatus(final MediaTrack mediaTrack) {
if (!mediaTrack.isYoutube()) {
Audio audio = Audio.createIfSupported();
if (audio != null) {
AudioElement mediaReachableTester = audio.getAudioElement();
String canPlay = mediaReachableTester.canPlayType(mediaTrack.typeToString());
if (MediaElement.CAN_PLAY_PROBABLY.equals(canPlay) || MediaElement.CAN_PLAY_MAYBE.equals(canPlay)) {
addLoadMetadataHandler(mediaReachableTester, mediaTrack);
mediaReachableTester.setPreload(MediaElement.PRELOAD_METADATA);
mediaReachableTester.setSrc(mediaTrack.url);
mediaReachableTester.load();
} else {
mediaTrack.status = Status.CANNOT_PLAY;
}
addLoadMetadataHandler(mediaReachableTester, mediaTrack);
mediaReachableTester.setPreload(MediaElement.PRELOAD_METADATA);
mediaReachableTester.setSrc(mediaTrack.url);
mediaReachableTester.load();
} else {
mediaTrack.status = Status.CANNOT_PLAY;
}
@@ -156,12 +155,19 @@ public class MediaSelector implements /*RaceTimesInfoProviderListener,*/ PlaySta
mediaElement.addEventListener('loadedmetadata', function() {
that.@com.sap.sailing.gwt.ui.raceboard.MediaSelector::loadedmetadata(Lcom/sap/sailing/gwt/ui/shared/media/MediaTrack;)(mediaTrack);
});
mediaElement.addEventListener('error', function() {
that.@com.sap.sailing.gwt.ui.raceboard.MediaSelector::mediaError(Lcom/sap/sailing/gwt/ui/shared/media/MediaTrack;)(mediaTrack);
});
}-*/;
public void loadedmetadata(MediaTrack mediaTrack) {
mediaTrack.status = Status.REACHABLE;
}
public void mediaError(MediaTrack mediaTrack) {
mediaTrack.status = Status.NOT_REACHABLE;
}
private void playDefault() {
MediaTrack defaultVideo = getDefaultVideo();
if (defaultVideo != null) {
@@ -176,7 +182,7 @@ public class MediaSelector implements /*RaceTimesInfoProviderListener,*/ PlaySta
private MediaTrack getDefaultAudio() {
//TODO: implement a better heuristic than just taking the first to come
for (MediaTrack mediaTrack : mediaTracks) {
if (MediaType.audio.equals(mediaTrack.mimeType.mediaType)) {
if (MediaType.audio.equals(mediaTrack.mimeType.mediaType) && isPotentiallyPlayable(mediaTrack)) {
return mediaTrack;
}
}
@@ -185,7 +191,7 @@ public class MediaSelector implements /*RaceTimesInfoProviderListener,*/ PlaySta
private MediaTrack getDefaultVideo() {
for (MediaTrack mediaTrack : mediaTracks) {
if (MediaType.video.equals(mediaTrack.mimeType.mediaType)) {
if (MediaType.video.equals(mediaTrack.mimeType.mediaType) && isPotentiallyPlayable(mediaTrack)) {
return mediaTrack;
}
}
@@ -44,6 +44,7 @@ import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.TrackedRaceStatus;
import com.sap.sailing.domain.tracking.Wind;
import com.sap.sailing.gwt.ui.shared.LeaderboardDTO;
import com.sap.sailing.util.impl.LockUtil;
/**
* Caches the expensive to compute {@link LeaderboardDTO} results of a
@@ -402,12 +403,21 @@ public class LeaderboardDTOCache {
future = map.get(key);
}
if (future == null) {
final Thread callerThread = Thread.currentThread();
future = new FutureTask<LeaderboardDTO>(new Callable<LeaderboardDTO>() {
@Override
public LeaderboardDTO call() throws Exception {
LeaderboardDTO result = sailingService.computeLeaderboardByName(leaderboard, adjustedTimePoint,
namesOfRaceColumnsForWhichToLoadLegDetails, waitForLatestAnalyses);
return result;
// The outer getLeaderboardByName(...) method will always wait for this future's completion.
// Therefore, it's safe to propagate the calling thread's locks to this one:
LockUtil.propagateLockSetFrom(callerThread);
try {
LeaderboardDTO result = sailingService.computeLeaderboardByName(leaderboard,
adjustedTimePoint, namesOfRaceColumnsForWhichToLoadLegDetails,
waitForLatestAnalyses);
return result;
} finally {
LockUtil.unpropagateLockSetFrom(callerThread);
}
}
});
computeLeadearboardByNameExecutor.execute(future);
@@ -72,6 +72,7 @@ import com.sap.sailing.domain.common.Color;
import com.sap.sailing.domain.common.CountryCode;
import com.sap.sailing.domain.common.DetailType;
import com.sap.sailing.domain.common.Distance;
import com.sap.sailing.domain.common.LeaderboardNameConstants;
import com.sap.sailing.domain.common.LegType;
import com.sap.sailing.domain.common.ManeuverType;
import com.sap.sailing.domain.common.MaxPointsReason;
@@ -347,7 +348,11 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
/* workQueue */ new LinkedBlockingQueue<Runnable>());
raceDetailsExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
leaderboardByNameLiveUpdaters = new HashMap<String, LiveLeaderboardUpdater>();
leaderboardDTOCacheWaitingForLatestAnalysis = new LeaderboardDTOCache(this, /* waitForLatestAnalyses */ true);
// The leaderboard cache is invalidated upon all competitor and mark position changes; some analyses
// are pretty expensive, such as the maneuver re-calculation. Waiting for the latest analysis after only a
// single fix was updated is too expensive if users use the replay feature while a race is still running.
// Therefore, using waitForLatestAnalyses==false seems appropriate here.
leaderboardDTOCacheWaitingForLatestAnalysis = new LeaderboardDTOCache(this, /* waitForLatestAnalyses */ false);
}
private void writeObject(ObjectOutputStream oos) throws IOException {
@@ -509,7 +514,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
RegattaAndRaceIdentifier raceIdentifier = null;
RaceDTO race = null;
TrackedRace trackedRace = raceColumn.getTrackedRace(fleet);
final FleetDTO fleetDTO = convertToFleetDTO(fleet);
final FleetDTO fleetDTO = convertToFleetDTO(raceColumn, fleet);
if (trackedRace != null) {
raceIdentifier = new RegattaNameAndRaceName(trackedRace.getTrackedRegatta().getRegatta()
.getName(), trackedRace.getRace().getName());
@@ -577,7 +583,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
public LeaderboardEntryDTO call() {
try {
Entry entry = leaderboard.getEntry(competitor, raceColumn, timePoint);
return getLeaderboardEntryDTO(entry, raceColumn.getTrackedRace(competitor), competitor, timePoint,
return getLeaderboardEntryDTO(entry, raceColumn, competitor, timePoint,
namesOfRaceColumnsForWhichToLoadLegDetails != null
&& namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn.getName()),
waitForLatestAnalyses, legRanksCache);
@@ -608,7 +614,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
}
}
}
logger.fine("computeLeaderboardByName("+leaderboard.getName()+", "+timePoint+", "+namesOfRaceColumnsForWhichToLoadLegDetails+") took "+
logger.info("computeLeaderboardByName("+leaderboard.getName()+", "+timePoint+", "+namesOfRaceColumnsForWhichToLoadLegDetails+") took "+
(System.currentTimeMillis()-startOfRequestHandling)+"ms");
return result;
}
@@ -646,10 +652,11 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
* may not reflect all data already received; otherwise, the method will always block for the latest
* cache updates to have happened before returning.
*/
private LeaderboardEntryDTO getLeaderboardEntryDTO(Entry entry, TrackedRace trackedRace, Competitor competitor,
private LeaderboardEntryDTO getLeaderboardEntryDTO(Entry entry, RaceColumn raceColumn, Competitor competitor,
TimePoint timePoint, boolean addLegDetails, boolean waitForLatestAnalyses,
Map<Leg, LinkedHashMap<Competitor, Integer>> legRanksCache) throws NoWindException {
LeaderboardEntryDTO entryDTO = new LeaderboardEntryDTO();
TrackedRace trackedRace = raceColumn.getTrackedRace(competitor);
entryDTO.race = trackedRace == null ? null : trackedRace.getRaceIdentifier();
entryDTO.netPoints = entry.getNetPoints();
entryDTO.netPointsCorrected = entry.isNetPointsCorrected();
@@ -671,7 +678,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
}
}
final Fleet fleet = entry.getFleet();
entryDTO.fleet = fleet == null ? null : convertToFleetDTO(fleet);
entryDTO.fleet = fleet == null ? null : convertToFleetDTO(raceColumn, fleet);
return entryDTO;
}
@@ -1068,7 +1075,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
private SeriesDTO convertToSeriesDTO(Series series) {
List<FleetDTO> fleets = new ArrayList<FleetDTO>();
for (Fleet fleet : series.getFleets()) {
fleets.add(convertToFleetDTO(fleet));
fleets.add(convertToFleetDTO(series, fleet));
}
List<RaceColumnDTO> raceColumns = new ArrayList<RaceColumnDTO>();
for (RaceColumnInSeries raceColumn : series.getRaceColumns()) {
@@ -1082,8 +1089,16 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
return result;
}
private FleetDTO convertToFleetDTO(Fleet fleet) {
return new FleetDTO(fleet.getName(), fleet.getOrdering(), fleet.getColor());
private FleetDTO convertToFleetDTO(Series series, Fleet fleet) {
return new FleetDTO(fleet.getName(), series.getName(), fleet.getOrdering(), fleet.getColor());
}
private FleetDTO convertToFleetDTO(RaceColumn raceColumn, Fleet fleet) {
String seriesNameOfFleet = null;
if (raceColumn instanceof RaceColumnInSeries) {
seriesNameOfFleet = ((RaceColumnInSeries) raceColumn).getSeries().getName();
}
return new FleetDTO(fleet.getName(), seriesNameOfFleet, fleet.getOrdering(), fleet.getColor());
}
private List<RaceWithCompetitorsDTO> convertToRaceDTOs(Regatta regatta) {
@@ -2132,7 +2147,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive = liveTimePointForTrackedRace;
}
}
final FleetDTO fleetDTO = convertToFleetDTO(fleet);
final FleetDTO fleetDTO = convertToFleetDTO(raceColumn, fleet);
RaceColumnDTO raceColumnDTO = leaderboardDTO.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), raceColumn.getFactor(),
fleetDTO, raceColumn.isMedalRace(), raceIdentifier, raceDTO);
if (latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive != null) {
@@ -2407,7 +2422,11 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
throws UnknownHostException, IOException, InterruptedException, ParseException {
List<SwissTimingRaceRecordDTO> result = new ArrayList<SwissTimingRaceRecordDTO>();
for (com.sap.sailing.domain.swisstimingadapter.RaceRecord rr : getService().getSwissTimingRaceRecords(hostname, port, canSendRequests)) {
result.add(new SwissTimingRaceRecordDTO(rr.getRaceID(), rr.getDescription(), rr.getStartTime()));
SwissTimingRaceRecordDTO swissTimingRaceRecordDTO = new SwissTimingRaceRecordDTO(rr.getRaceID(), rr.getDescription(), rr.getStartTime());
BoatClass boatClass = com.sap.sailing.domain.swisstimingadapter.DomainFactory.INSTANCE.getOrCreateBoatClassFromRaceID(rr.getRaceID());
swissTimingRaceRecordDTO.boatClass = boatClass != null ? boatClass.getName() : null;
swissTimingRaceRecordDTO.discipline = rr.getRaceID().length() >= 3 ? rr.getRaceID().substring(2, 3) : null;
result.add(swissTimingRaceRecordDTO);
}
return result;
}
@@ -2421,7 +2440,6 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
public void trackWithSwissTiming(RegattaIdentifier regattaToAddTo, Iterable<SwissTimingRaceRecordDTO> rrs, String hostname, int port,
boolean canSendRequests, boolean trackWind, final boolean correctWindByDeclination) throws Exception {
for (SwissTimingRaceRecordDTO rr : rrs) {
final RacesHandle raceHandle = getService().addSwissTimingRace(regattaToAddTo, rr.ID, hostname, port,
canSendRequests,
MongoWindStoreFactory.INSTANCE.getMongoWindStore(mongoObjectFactory, domainObjectFactory),
@@ -2476,11 +2494,11 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
regatta = getService().createRegatta(
replayRaceDTO.rsc,
boatClass.trim(),
RegattaImpl.getFullName(replayRaceDTO.rsc, replayRaceDTO.boat_class),
RegattaImpl.getDefaultName(replayRaceDTO.rsc, replayRaceDTO.boat_class),
Collections.singletonList(new SeriesImpl(
"Default",
LeaderboardNameConstants.DEFAULT_SERIES_NAME,
/* isMedal */false,
Collections.singletonList(new FleetImpl("Default")),
Collections.singletonList(new FleetImpl(LeaderboardNameConstants.DEFAULT_FLEET_NAME)),
/* race column names */ new ArrayList<String>(), getService())),
false,
baseDomainFactory.createScoringScheme(ScoringSchemeType.LOW_POINT), null);
@@ -7,10 +7,14 @@ public class FleetDTO extends NamedDTO implements IsSerializable {
private Color color;
private int orderNo;
/** the name of the series the fleet belongs to */
private String seriesName;
public FleetDTO() {}
public FleetDTO(String fleetName, int orderNo, Color color) {
public FleetDTO(String fleetName, String seriesName, int orderNo, Color color) {
super(fleetName);
this.seriesName = seriesName;
this.orderNo = orderNo;
this.color = color;
}
@@ -31,12 +35,17 @@ public class FleetDTO extends NamedDTO implements IsSerializable {
this.orderNo = orderNo;
}
public String getSeriesName() {
return seriesName;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((color == null) ? 0 : color.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + orderNo;
result = prime * result + ((seriesName == null) ? 0 : seriesName.hashCode());
return result;
}
@@ -49,13 +58,19 @@ public class FleetDTO extends NamedDTO implements IsSerializable {
if (getClass() != obj.getClass())
return false;
FleetDTO other = (FleetDTO) obj;
if (color == null) {
if (other.color != null)
if (name == null) {
if (other.name != null)
return false;
} else if (!color.equals(other.color))
} else if (!name.equals(other.name))
return false;
if (orderNo != other.orderNo)
return false;
if (seriesName == null) {
if (other.seriesName != null)
return false;
} else if (!seriesName.equals(other.seriesName))
return false;
return true;
}
}
Loaded 100 of 360 files, more files were not shown because too many files have changed in this diff. Show more