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
+39846 -3757

No files matched your search

+7
View File
@@ -274,6 +274,13 @@ if [[ "$@" == "install" ]] || [[ "$@" == "all" ]]; then
mkdir $ACDIR/configuration mkdir $ACDIR/configuration
fi 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 cd $ACDIR
rm -rf $ACDIR/plugins/*.* rm -rf $ACDIR/plugins/*.*
+1 -2
View File
@@ -1,7 +1,6 @@
port = 27017 port = 27017
master = true master = true
dbpath = /opt/mongodb/data//mongodb-data dbpath = /opt/mongodb/data/mongodb-data
logpath = /opt/mongodb/data/logs/mongodb-data/mongodb.log
logappend = true logappend = true
directoryperdb = true directoryperdb = true
rest = 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 ### 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]] * [[General Project Information|wiki/general-information]]
* [[OnBoarding Information|wiki/onboarding]] * [[OnBoarding Information|wiki/onboarding]]
* [[Architecture and Infrastructure|wiki/architecture-and-infrastructure]] * [[Architecture and Infrastructure|wiki/architecture-and-infrastructure]]
* [[Production Environment|wiki/production-environment]]
* [[Development Environment|wiki/development-environment]]
* [[Server Replication|wiki/server-replication]] * [[Server Replication|wiki/server-replication]]
* [[Configure Races on Server|wiki/configure-races-on-server]] * [[Configure Races on Server|wiki/configure-races-on-server]]
* [[Cook Book|wiki/cook-book]]
* [[Planning|wiki/planning]] * [[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) ### 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 DEFAULT_LEADERBOARD_NAME = "Default Leaderboard";
static final String OVERALL = "Overall"; 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 { 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 * 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 * score corrections taken at different times. Later score corrections are expected to be cumulative, meaning they
* also contain all previous corrections. * also contain all previous corrections.
@@ -1,5 +1,6 @@
package com.sap.sailing.domain.common; package com.sap.sailing.domain.common;
public enum ScoringSchemeType { 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() { public String getITUCallPrefix() {
return ituCallPrefix; return ituCallPrefix;
} }
@Override
public String toString() {
return getThreeLetterIOCCode();
}
} }
@@ -161,7 +161,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
} catch (Exception e) { } catch (Exception e) {
// something went wrong during DB access; report, then use empty new wind track // 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.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; return result;
} }
@@ -180,7 +180,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
} catch (Exception e) { } catch (Exception e) {
// something went wrong during DB access; report, then use empty new wind track // 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.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; return result;
} }
@@ -341,58 +341,61 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
private FlexibleLeaderboard loadFlexibleLeaderboard(DBObject dbLeaderboard, private FlexibleLeaderboard loadFlexibleLeaderboard(DBObject dbLeaderboard,
SettableScoreCorrection scoreCorrection, ThresholdBasedResultDiscardingRule resultDiscardingRule) { SettableScoreCorrection scoreCorrection, ThresholdBasedResultDiscardingRule resultDiscardingRule) {
final ScoringScheme scoringScheme = loadScoringScheme(dbLeaderboard); final FlexibleLeaderboardImpl result;
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);
BasicDBList dbRaceColumns = (BasicDBList) dbLeaderboard.get(FieldNames.LEADERBOARD_COLUMNS.name()); 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: if (dbRaceColumns == null) {
Map<String, Fleet> fleetsByName = new HashMap<String, Fleet>(); // this was probably an orphaned overall leaderboard
for (Object dbRaceColumnAsObject : dbRaceColumns) { logger.warning("Probably found orphan overall leaderboard named "
BasicDBObject dbRaceColumn = (BasicDBObject) dbRaceColumnAsObject; + dbLeaderboard.get(FieldNames.LEADERBOARD_NAME.name())+". Ignoring.");
Map<String, RaceIdentifier> raceIdentifiers = loadRaceIdentifiers(dbRaceColumn); result = null;
RaceIdentifier defaultFleetRaceIdentifier = raceIdentifiers.get(null); } else {
if (defaultFleetRaceIdentifier != null) { final ScoringScheme scoringScheme = loadScoringScheme(dbLeaderboard);
Fleet defaultFleet = result.getFleet(null); RaceLogStore raceLogStore = MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(
if (defaultFleet != null) { new MongoObjectFactoryImpl(database), this);
raceIdentifiers.put(defaultFleet.getName(), defaultFleetRaceIdentifier); CourseArea courseArea = loadCourseAreaFromEvents(dbLeaderboard);
} else { result = new FlexibleLeaderboardImpl(raceLogStore, (String) dbLeaderboard.get(FieldNames.LEADERBOARD_NAME
// leaderboard has no default fleet; don't know what to do with default RaceIdentifier .name()), scoreCorrection, resultDiscardingRule, scoringScheme, courseArea);
logger.warning("Discarding RaceIdentifier "+defaultFleetRaceIdentifier+" for default fleet for leaderboard "+result.getName()+ // For a FlexibleLeaderboard, fleets are owned by the leaderboard's RaceColumn objects. We need to manage
" because no default fleet was found in leaderboard"); // 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()) {
List<Fleet> fleets = new ArrayList<Fleet>(); Fleet fleet = fleetsByName.get(fleetName);
for (String fleetName : raceIdentifiers.keySet()) { if (fleet == null) {
Fleet fleet = fleetsByName.get(fleetName); fleet = new FleetImpl(fleetName);
if (fleet == null) { fleetsByName.put(fleetName, fleet);
fleet = new FleetImpl(fleetName); }
fleetsByName.put(fleetName, fleet); fleets.add(fleet);
} }
fleets.add(fleet); if (fleets.isEmpty()) {
} fleets.add(result.getFleet(null));
if (fleets.isEmpty()) { }
fleets.add(result.getFleet(null)); String columnName = (String) dbRaceColumn.get(FieldNames.LEADERBOARD_COLUMN_NAME.name());
}
String columnName = (String) dbRaceColumn.get(FieldNames.LEADERBOARD_COLUMN_NAME.name());
RaceColumn raceColumn = result.addRaceColumn(columnName, RaceColumn raceColumn = result.addRaceColumn(columnName,
(Boolean) dbRaceColumn.get(FieldNames.LEADERBOARD_IS_MEDAL_RACE_COLUMN.name()), (Boolean) dbRaceColumn.get(FieldNames.LEADERBOARD_IS_MEDAL_RACE_COLUMN.name()),
fleets.toArray(new Fleet[0])); fleets.toArray(new Fleet[0]));
for (Map.Entry<String, RaceIdentifier> e : raceIdentifiers.entrySet()) { for (Map.Entry<String, RaceIdentifier> e : raceIdentifiers.entrySet()) {
raceColumn.setRaceIdentifier(fleetsByName.get(e.getKey()), e.getValue()); raceColumn.setRaceIdentifier(fleetsByName.get(e.getKey()), e.getValue());
} }
}
} }
return result; return result;
} }
@@ -539,7 +542,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
leaderboardGroup = loadLeaderboardGroup(leaderboardGroupCollection.findOne(query), regattaRegistry, leaderboardRegistry); leaderboardGroup = loadLeaderboardGroup(leaderboardGroupCollection.findOne(query), regattaRegistry, leaderboardRegistry);
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboard group "+name+"."); 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; return leaderboardGroup;
@@ -555,7 +558,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
} }
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboard groups."); 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; return leaderboardGroups;
@@ -575,10 +578,14 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
for (Object object : dbLeaderboardIds) { for (Object object : dbLeaderboardIds) {
ObjectId dbLeaderboardId = (ObjectId) object; ObjectId dbLeaderboardId = (ObjectId) object;
DBObject dbLeaderboard = leaderboardCollection.findOne(dbLeaderboardId); DBObject dbLeaderboard = leaderboardCollection.findOne(dbLeaderboardId);
final Leaderboard loadedLeaderboard = loadLeaderboard(dbLeaderboard, regattaRegistry, if (dbLeaderboard != null) {
leaderboardRegistry, /* groupForMetaLeaderboard */ null); final Leaderboard loadedLeaderboard = loadLeaderboard(dbLeaderboard, regattaRegistry,
if (loadedLeaderboard != null) { leaderboardRegistry, /* groupForMetaLeaderboard */null);
leaderboards.add(loadedLeaderboard); 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); logger.info("loaded leaderboard group "+name);
@@ -605,7 +612,9 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name());
Set<Leaderboard> result = new HashSet<Leaderboard>(); Set<Leaderboard> result = new HashSet<Leaderboard>();
try { 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({ " BasicDBObject query = new BasicDBObject("$where", "function() { return db." + CollectionNames.LEADERBOARD_GROUPS.name() + ".find({ "
+ FieldNames.LEADERBOARD_GROUP_LEADERBOARDS.name() + ": this._id }).count() == 0 && " + 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 && " + "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) { } catch (Exception e) {
logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboards."); 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; return result;
} }
@@ -690,7 +699,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
} catch (Exception e) { } catch (Exception e) {
// something went wrong during DB access; report, then use empty new wind track // 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.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; return result;
} }
@@ -741,7 +750,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
} }
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load events."); 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; return result;
@@ -951,7 +960,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
} catch (Throwable t) { } catch (Throwable t) {
// something went wrong during DB access; report, then use empty new race log // 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.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; return result;
} }
@@ -2,6 +2,7 @@ package com.sap.sailing.domain.persistence.impl;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
@@ -100,7 +101,7 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory {
public DBCollection getWindTrackCollection() { public DBCollection getWindTrackCollection() {
DBCollection result = database.getCollection(CollectionNames.WIND_TRACKS.name()); 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; return result;
} }
@@ -148,7 +149,7 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory {
leaderboardCollection.ensureIndex(FieldNames.LEADERBOARD_NAME.name()); leaderboardCollection.ensureIndex(FieldNames.LEADERBOARD_NAME.name());
} catch (NullPointerException npe) { } catch (NullPointerException npe) {
// sometimes, for reasons yet to be clarified, ensuring an index on the name field causes an 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 query = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), leaderboard.getName());
BasicDBObject dbLeaderboard = new BasicDBObject(); BasicDBObject dbLeaderboard = new BasicDBObject();
@@ -313,7 +314,7 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory {
leaderboardGroupCollection.ensureIndex(FieldNames.LEADERBOARD_GROUP_NAME.name()); leaderboardGroupCollection.ensureIndex(FieldNames.LEADERBOARD_GROUP_NAME.name());
} catch (NullPointerException npe) { } catch (NullPointerException npe) {
// sometimes, for reasons yet to be clarified, ensuring an index on the name field causes an 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 query = new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), leaderboardGroup.getName());
BasicDBObject dbLeaderboardGroup = new BasicDBObject(); 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 * Base interface for courses consisting of all static information, which might be shared
* by the server and an Android application. * 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 * Clients can safely iterate over the resulting list because it's a copy which therefore won't reflect
* waypoint additions and removals. * 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 * Base interface for an Event consisting of all static information, which might be shared
* by the server and an Android application. * 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 * @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. * 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. * 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 java.io.Serializable;
import com.sap.sailing.domain.base.EventData; import com.sap.sailing.domain.base.EventBase;
import com.sap.sailing.domain.base.Venue; import com.sap.sailing.domain.base.Venue;
public class EventDataImpl implements EventData { public class EventBaseImpl implements EventBase {
private static final long serialVersionUID = -5749964088848611074L; private static final long serialVersionUID = -5749964088848611074L;
private String name; private String name;
@@ -14,14 +14,14 @@ public class EventDataImpl implements EventData {
private boolean isPublic; private boolean isPublic;
private final Serializable id; 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); this(name, new VenueImpl(venueName), publicationUrl, isPublic, id);
} }
/** /**
* @param venue must not be <code>null</code> * @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; assert venue != null;
this.id = id; this.id = id;
this.name = name; this.name = name;
@@ -1,11 +1,11 @@
package com.sap.sailing.domain.base.racegroup; 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 * 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. * 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(); public Iterable<RaceRow> getRaceRows();
} }
@@ -1,7 +1,9 @@
package com.sap.sailing.util.impl; package com.sap.sailing.util.impl;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.logging.Logger; 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 int NUMBER_OF_SECONDS_TO_WAIT_FOR_LOCK = 5;
private static final Logger logger = Logger.getLogger(Util.class.getName()); 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<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 * 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 * 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. * 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. * 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) { private static void lock(Lock lock, String lockDescriptionForTimeoutLogMessage, NamedReentrantReadWriteLock lockParent) {
boolean locked = false; boolean locked = false;
@@ -35,7 +36,7 @@ public class LockUtil {
message.append(lockDescriptionForTimeoutLogMessage); message.append(lockDescriptionForTimeoutLogMessage);
message.append(" in "); message.append(" in ");
message.append(NUMBER_OF_SECONDS_TO_WAIT_FOR_LOCK); 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()); message.append(getCurrentStackTrace());
Thread writer = lockParent.getWriter(); Thread writer = lockParent.getWriter();
if (writer != null) { if (writer != null) {
@@ -69,37 +70,97 @@ public class LockUtil {
} }
public static void lockForRead(NamedReentrantReadWriteLock lock) { 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) { 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) { public static void lockForWrite(NamedReentrantReadWriteLock lock) {
lock(lock.writeLock(), lock.getWriteLockName(), lock); if (isInCurrentThreadsLockSet(lock.writeLock())) {
synchronized (lastTimeWriteLockWasObtained) { incrementLockCountForCurrentThread(lock.writeLock());
lastTimeWriteLockWasObtained.put(lock, MillisecondsTimePoint.now()); } else {
lock(lock.writeLock(), lock.getWriteLockName(), lock);
addToCurrentThreadsLockSet(lock.writeLock());
synchronized (lastTimeWriteLockWasObtained) {
lastTimeWriteLockWasObtained.put(lock, MillisecondsTimePoint.now());
}
} }
} }
public static void unlockAfterWrite(NamedReentrantReadWriteLock lock) { public static void unlockAfterWrite(NamedReentrantReadWriteLock lock) {
lock.writeLock().unlock(); assert isInCurrentThreadsLockSet(lock.writeLock());
final TimePoint timePointWriteLockWasObtained; if (getCurrentThreadsLockCounts().get(lock.writeLock()) == 1) {
synchronized (lastTimeWriteLockWasObtained) { lock.writeLock().unlock();
timePointWriteLockWasObtained = lastTimeWriteLockWasObtained.get(lock); removeFromCurrentThreadsLockSet(lock.writeLock());
} final TimePoint timePointWriteLockWasObtained;
if (timePointWriteLockWasObtained == null) { synchronized (lastTimeWriteLockWasObtained) {
logger.info("Internal error: write lock "+lock.getName()+" to be unlocked but no time recorded for when it was last obtained.\n"+ timePointWriteLockWasObtained = lastTimeWriteLockWasObtained.get(lock);
"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);
} }
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) { private static String getStackTrace(Thread thread) {
@@ -114,4 +175,48 @@ public class LockUtil {
private static String getCurrentStackTrace() { private static String getCurrentStackTrace() {
return getStackTrace(Thread.currentThread()); 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.Socket;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import com.mongodb.BasicDBObject; import com.mongodb.BasicDBObject;
@@ -63,7 +67,7 @@ public class StoreAndForward implements Runnable {
private final Thread storeAndForwardThread; 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 * 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, public StoreAndForward(String sailMasterHostname, int sailMasterPort, int portForClients, SwissTimingFactory swissTimingFactory,
SwissTimingAdapterPersistence swissTimingAdapterPersistence, MongoDBService mongoDBService) throws InterruptedException, IOException { SwissTimingAdapterPersistence swissTimingAdapterPersistence, MongoDBService mongoDBService) throws InterruptedException, IOException {
this.receivingThreads = new HashSet<>();
this.db = mongoDBService.getDB(); this.db = mongoDBService.getDB();
this.listenPort = -1; this.listenPort = -1;
this.transceiver = swissTimingFactory.createSailMasterTransceiver(); this.transceiver = swissTimingFactory.createSailMasterTransceiver();
this.portForClients = portForClients; this.portForClients = portForClients;
this.sailMasterHostname = sailMasterHostname; this.sailMasterHostname = sailMasterHostname;
this.sailMasterPort = sailMasterPort; this.sailMasterPort = sailMasterPort;
this.streamsToForwardTo = new ArrayList<OutputStream>(); this.streamsToForwardTo = Collections.synchronizedList(new ArrayList<OutputStream>());
this.socketsToForwardTo = new ArrayList<Socket>(); this.socketsToForwardTo = new ArrayList<Socket>();
this.swissTimingAdapterPersistence = swissTimingAdapterPersistence; this.swissTimingAdapterPersistence = swissTimingAdapterPersistence;
this.swissTimingFactory = swissTimingFactory; this.swissTimingFactory = swissTimingFactory;
@@ -125,11 +130,12 @@ public class StoreAndForward implements Runnable {
*/ */
public StoreAndForward(final int listenPort, final int portForClients, SwissTimingFactory swissTimingFactory, public StoreAndForward(final int listenPort, final int portForClients, SwissTimingFactory swissTimingFactory,
SwissTimingAdapterPersistence swissTimingAdapterPersistence, MongoDBService mongoDBService) throws InterruptedException, IOException { SwissTimingAdapterPersistence swissTimingAdapterPersistence, MongoDBService mongoDBService) throws InterruptedException, IOException {
this.receivingThreads = new HashSet<>();
this.db = mongoDBService.getDB(); this.db = mongoDBService.getDB();
this.listenPort = listenPort; this.listenPort = listenPort;
this.transceiver = swissTimingFactory.createSailMasterTransceiver(); this.transceiver = swissTimingFactory.createSailMasterTransceiver();
this.portForClients = portForClients; this.portForClients = portForClients;
this.streamsToForwardTo = new ArrayList<OutputStream>(); this.streamsToForwardTo = Collections.synchronizedList(new ArrayList<OutputStream>());
this.socketsToForwardTo = new ArrayList<Socket>(); this.socketsToForwardTo = new ArrayList<Socket>();
this.swissTimingAdapterPersistence = swissTimingAdapterPersistence; this.swissTimingAdapterPersistence = swissTimingAdapterPersistence;
this.swissTimingFactory = swissTimingFactory; 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 * Depending on the mode of operation, accepts an inbound connect from a SailMaster server / bridge or actively
* actively initiates a TCP connection to the SailMaster address/port configured. When this method * initiates a TCP connection to the SailMaster address/port configured. When a connection has been established, a
* returns, the {@link #socket} holds an open and connected socket. * new {@link ReceivingThread} is spawned for the new socket. If in listening mode, the
* @throws IOException * {@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()) { if (isInSailMasterListeningMode()) {
synchronized (this) { while (!stopped) {
receivingFromSailMaster = true; synchronized (this) {
logger.info("StoreAndForward waiting for inbound SailMaster connections on port "+listenPort); receivingFromSailMaster = true;
notifyAll(); 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 { } else {
synchronized (this) { synchronized (this) {
receivingFromSailMaster = true; receivingFromSailMaster = true;
@@ -223,6 +238,119 @@ public class StoreAndForward implements Runnable {
} }
socket = new Socket(sailMasterHostname, sailMasterPort); socket = new Socket(sailMasterHostname, sailMasterPort);
logger.info("StoreAndForward connections to SailMaster "+sailMasterHostname+":"+sailMasterPort+" established"); 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(); closer.close();
logger.info("joining clientListener thread "+clientListener); logger.info("joining clientListener thread "+clientListener);
clientListener.join(); clientListener.join();
socket.close(); // will let a read terminate abnormally for (ReceivingThread receivingThread : receivingThreads) {
logger.info("joining storeAndForwardThread "+storeAndForwardThread); receivingThread.stopReceiver();
storeAndForwardThread.join(); 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 { public static void main(String[] args) throws InterruptedException, IOException {
@@ -266,66 +401,12 @@ public class StoreAndForward implements Runnable {
try { try {
while (!stopped) { while (!stopped) {
try { try {
establishConnection(); ReceivingThread receivingThread = establishConnection();
InputStream is = socket.getInputStream(); if (!isInSailMasterListeningMode()) {
Pair<String, Long> messageAndOptionalSequenceNumber = transceiver.receiveMessage(is); receivingThread.join(); // we're in active connecting mode; wait for thread to die, then trie again
// 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();
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); logger.log(Level.SEVERE, "Exception in StoreAndForward", e);
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.info("StoreAndForward is closing receiving server socket"); logger.info("StoreAndForward is closing receiving server socket");
@@ -141,7 +141,7 @@ public class SwissTimingAdapterPersistenceImpl implements SwissTimingAdapterPers
@Override @Override
public List<SailMasterMessage> loadRaceMessages(String raceID) { public List<SailMasterMessage> loadRaceMessages(String raceID) {
DBCollection racesMessagesCollection = database.getCollection(CollectionNames.RACES_MESSAGES.name()); 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(); BasicDBObject query = new BasicDBObject();
query.append(FieldNames.RACE_ID.name(), raceID); query.append(FieldNames.RACE_ID.name(), raceID);
DBCursor results = racesMessagesCollection.find(query).sort( 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.io.OutputStream;
import java.net.Socket; import java.net.Socket;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.NavigableSet;
import java.util.Set; import java.util.Set;
import java.util.logging.Logger; 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.GPSFix;
import com.sap.sailing.domain.tracking.GPSFixMoving; import com.sap.sailing.domain.tracking.GPSFixMoving;
import com.sap.sailing.domain.tracking.GPSFixTrack; 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.RacesHandle;
import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.impl.EmptyWindStore; import com.sap.sailing.domain.tracking.impl.EmptyWindStore;
@@ -301,6 +305,61 @@ public class EndToEndListeningStoreAndFowardTest {
assertEquals(7, Util.size(waypoints)); 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 @Test
public void testRongRaceLogRACZero() throws Exception{ public void testRongRaceLogRACZero() throws Exception{
String[] racesToTrack = new String[] { "W4702" }; String[] racesToTrack = new String[] { "W4702" };
@@ -121,8 +121,8 @@ public class DomainFactoryImpl implements DomainFactory {
return baseDomainFactory.getExistingCompetitorById(getCompetitorID(boatID, boatClass)); return baseDomainFactory.getExistingCompetitorById(getCompetitorID(boatID, boatClass));
} }
private Pair<String, BoatClass> getCompetitorID(String boatID, BoatClass boatClass) { private String getCompetitorID(String boatID, BoatClass boatClass) {
return new Util.Pair<String, BoatClass>(boatID, boatClass); return boatID + "/" + boatClass.getName();
} }
@Override @Override
@@ -22,6 +22,7 @@ import java.util.Set;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import com.sap.sailing.domain.base.SpeedWithBearing; 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 * 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 * can receive "spontaneous" events sent by the sail master system. Clients can register for those spontaneous events
* (see {@link #addSailMasterListener}). * (see {@link #addSailMasterListener}).
* <p> * <p>
@@ -182,6 +183,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
if (message.getSequenceNumber() != null) { if (message.getSequenceNumber() != null) {
maxSequenceNumber = message.getSequenceNumber(); maxSequenceNumber = message.getSequenceNumber();
} }
i++;
notifyListenersStoredDataProgress(raceID, ((double) i)/(double) messages.size()); notifyListenersStoredDataProgress(raceID, ((double) i)/(double) messages.size());
} }
// now process the buffered messages one by one: // now process the buffered messages one by one:
@@ -222,53 +224,65 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
public void run() { public void run() {
try { try {
while (!stopped) { while (!stopped) {
ensureSocketIsOpen();
try { try {
ensureSocketIsOpen();
Pair<String, Long> receivedMessageAndOptionalSequenceNumber = receiveMessage(socket.getInputStream()); Pair<String, Long> receivedMessageAndOptionalSequenceNumber = receiveMessage(socket.getInputStream());
SailMasterMessage message = new SailMasterMessageImpl( if (receivedMessageAndOptionalSequenceNumber == null) {
receivedMessageAndOptionalSequenceNumber.getA(), // reached EOF; this means the socket is or can be closed
receivedMessageAndOptionalSequenceNumber.getB()); if (socket != null && !socket.isClosed()) {
// drop race-specific messages for non-tracked races socket.close();
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) { socket = null;
if (message.isResponse()) { } else {
// this is a response for an explicit request SailMasterMessage message = new SailMasterMessageImpl(
rendevouz(message); receivedMessageAndOptionalSequenceNumber.getA(),
} else if (message.isEvent()) { receivedMessageAndOptionalSequenceNumber.getB());
// only notify if it hasn't been loaded from a store yet // drop race-specific messages for non-tracked races
if (!message.getType().isRaceSpecific() || message.getSequenceNumber() == null || if (!message.getType().isRaceSpecific() || idsOfTrackedRaces.contains(message.getRaceID())) {
(sequenceNumberOfLastMessageForRaceID.containsKey(message.getRaceID()) && boolean messageProcessed = false;
message.getSequenceNumber() > sequenceNumberOfLastMessageForRaceID.get(message.getRaceID()))) { synchronized (this) {
// a spontaneous event if (message.getType().isRaceSpecific() && isCurrentlyBuffering(message.getRaceID())) {
logger.fine("notifying message " + message); buffer(message);
notifyListeners(message); messageProcessed = true;
} else { }
logger.info("discarding already notified message " + message); }
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) {
if (message.getType() == MessageType._STOPSERVER) { logger.info("SailMasterConnector received " + MessageType._STOPSERVER.name());
logger.info("SailMasterConnector received "+MessageType._STOPSERVER.name()); stop();
stop(); }
} }
} catch (SocketException se) { } catch (SocketException se) {
// This occurs if the socket was closed which may mean the connector was stopped. Check in while // 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"); logger.info("Caught exception "+se+" during socket operation; setting socket to null");
socket = null; socket = null;
Thread.sleep(1000); // try again in 1s
} }
} }
} catch (Exception e) { } 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"); logger.info("Stopping Sail Master connector thread");
stopped = true;
} }
private synchronized void buffer(SailMasterMessage message) { private synchronized void buffer(SailMasterMessage message) {
@@ -304,28 +318,33 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
blockingQueue.offer(message); blockingQueue.offer(message);
} }
protected void notifyListeners(SailMasterMessage message) throws ParseException { protected void notifyListeners(SailMasterMessage message) {
switch (message.getType()) { try {
case RPD: switch (message.getType()) {
notifyListenersRPD(message); case RPD:
break; notifyListenersRPD(message);
case RAC: break;
notifyListenersRAC(message); case RAC:
break; notifyListenersRAC(message);
case CCG: break;
notifyListenersCCG(message); case CCG:
break; notifyListenersCCG(message);
case STL: break;
notifyListenersSTL(message); case STL:
break; notifyListenersSTL(message);
case CAM: break;
notifyListenersCAM(message); case CAM:
break; notifyListenersCAM(message);
case TMD: break;
notifyListenersTMD(message); case TMD:
break; notifyListenersTMD(message);
default: break;
// ignore all other messages because there are no notification patterns for those 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 @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 // we cache all messages here and deliver them with a different speed
bufferedMessageList.add(message); bufferedMessageList.add(message);
} }
@@ -5,6 +5,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.logging.Logger;
import com.sap.sailing.domain.common.impl.Util.Pair; import com.sap.sailing.domain.common.impl.Util.Pair;
import com.sap.sailing.domain.swisstimingadapter.SailMasterMessage; import com.sap.sailing.domain.swisstimingadapter.SailMasterMessage;
@@ -17,6 +18,7 @@ import com.sap.sailing.domain.swisstimingadapter.SailMasterTransceiver;
* *
*/ */
public class SailMasterTransceiverImpl implements 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 STX = 0x02;
private static final byte ETX = 0x03; 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 { public synchronized void sendMessage(SailMasterMessage message, OutputStream os) throws IOException {
os.write(("" + message.getSequenceNumber()).getBytes()); os.write(("" + message.getSequenceNumber()).getBytes());
sendMessage(message.getMessage(), os); sendMessage(message.getMessage(), os);
os.flush();
} }
/** /**
@@ -59,9 +62,13 @@ public class SailMasterTransceiverImpl implements SailMasterTransceiver {
bufferForOptionalSequenceNumber.write(read); bufferForOptionalSequenceNumber.write(read);
read = inputStream.read(); read = inputStream.read();
} }
// now either we've read an STX byte or reached EOF
String message = null; String message = null;
if (read == STX) { if (read == STX) {
message = readMessage(inputStream); message = readMessage(inputStream);
} else {
// EOF; indicate by returning null
logger.info("Received EOF in SailMasterTransceiver. Returning null as message.");
} }
if (bufferForOptionalSequenceNumber.size() > 0) { if (bufferForOptionalSequenceNumber.size() > 0) {
sequenceNumber = getSequenceNumber(bufferForOptionalSequenceNumber); sequenceNumber = getSequenceNumber(bufferForOptionalSequenceNumber);
@@ -10,9 +10,9 @@ import java.util.List;
import java.util.NavigableSet; import java.util.NavigableSet;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.Mark; 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.RaceTracker;
import com.sap.sailing.domain.tracking.RacesHandle; import com.sap.sailing.domain.tracking.RacesHandle;
import com.sap.sailing.domain.tracking.TrackedRace; 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.TrackedRegattaRegistry;
import com.sap.sailing.domain.tracking.WindStore; import com.sap.sailing.domain.tracking.WindStore;
import com.sap.sailing.domain.tracking.WindTrack; import com.sap.sailing.domain.tracking.WindTrack;
@@ -65,7 +66,12 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
private final Regatta regatta; private final Regatta regatta;
private final WindStore windStore; 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 RaceDefinition race;
private Course course; private Course course;
private StartList startList; private StartList startList;
private DynamicTrackedRace trackedRace; private DynamicTrackedRace trackedRace;
@@ -74,6 +80,13 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
private boolean loggedIgnore; private boolean loggedIgnore;
private final long delayToLiveInMillis; 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, protected SwissTimingRaceTrackerImpl(String raceID, String hostname, int port, RaceLogStore raceLogStore, WindStore windStore,
DomainFactory domainFactory, SwissTimingFactory factory, RaceSpecificMessageLoader messageLoader, DomainFactory domainFactory, SwissTimingFactory factory, RaceSpecificMessageLoader messageLoader,
TrackedRegattaRegistry trackedRegattaRegistry, boolean canSendRequests, long delayToLiveInMillis) throws InterruptedException, 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, TrackedRegattaRegistry trackedRegattaRegistry, boolean canSendRequests, long delayToLiveInMillis) throws InterruptedException,
UnknownHostException, IOException, ParseException { UnknownHostException, IOException, ParseException {
super(); super();
this.tmdMessageQueue = new TMDMessageQueue(this);
this.regatta = regatta; this.regatta = regatta;
this.connector = factory.getOrCreateSailMasterConnector(hostname, port, messageLoader, canSendRequests); this.connector = factory.getOrCreateSailMasterConnector(hostname, port, messageLoader, canSendRequests);
this.domainFactory = domainFactory; this.domainFactory = domainFactory;
@@ -111,6 +125,10 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
@Override @Override
public void stop() throws MalformedURLException, IOException, InterruptedException { public void stop() throws MalformedURLException, IOException, InterruptedException {
if (isTrackedRaceStillReachable()) {
TrackedRaceStatus newStatus = new TrackedRaceStatusImpl(TrackedRaceStatusEnum.FINISHED, 1.0);
trackedRace.setStatus(newStatus);
}
connector.removeSailMasterListener(raceID, this); connector.removeSailMasterListener(raceID, this);
} }
@@ -130,12 +148,37 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
@Override @Override
public Set<RaceDefinition> getRaces() { 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); return Collections.singleton(race);
} }
@Override @Override
public Set<RaceDefinition> getRaces(long timeoutInMilliseconds) { 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 @Override
@@ -174,6 +217,7 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
if (this.raceID.equals(raceID)) { if (this.raceID.equals(raceID)) {
if (startTime != null) { if (startTime != null) {
trackedRace.setStartTimeReceived(startTime); trackedRace.setStartTimeReceived(startTime);
tmdMessageQueue.validStartTimeReceived();
} }
for (Fix fix : fixes) { for (Fix fix : fixes) {
GPSFixMoving gpsFix = domainFactory.createGPSFix(timePoint, fix); GPSFixMoving gpsFix = domainFactory.createGPSFix(timePoint, fix);
@@ -236,11 +280,23 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
for (Triple<Integer, Integer, Long> markIndexRankAndTimeSinceStartInMilliseconds : markIndicesRanksAndTimesSinceStartInMilliseconds) { for (Triple<Integer, Integer, Long> markIndexRankAndTimeSinceStartInMilliseconds : markIndicesRanksAndTimesSinceStartInMilliseconds) {
Waypoint waypoint = Util.get(trackedRace.getRace().getCourse().getWaypoints(), Waypoint waypoint = Util.get(trackedRace.getRace().getCourse().getWaypoints(),
markIndexRankAndTimeSinceStartInMilliseconds.getA()); markIndexRankAndTimeSinceStartInMilliseconds.getA());
MillisecondsTimePoint timePoint = trackedRace.getStartOfRace() == null ? null : new MillisecondsTimePoint( // update mark passing only if we have a start time; guessed start times don't make sense and
trackedRace.getStartOfRace().asMillis() + markIndexRankAndTimeSinceStartInMilliseconds.getC()); // for the start line would lead subsequent calls to getStartOfRace() return that guessed start time
MarkPassing markPassing = domainFactory.createMarkPassing(timePoint, waypoint, // which then cannot be identified as "guessed" anymore...
domainFactory.getCompetitorByBoatIDAndBoatClass(boatID, boatClass)); if (trackedRace.getStartOfRace() != null) {
markPassingsByMarkIndex.put(markIndexRankAndTimeSinceStartInMilliseconds.getA(), markPassing); 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()); trackedRace.updateMarkPassings(competitor, markPassingsByMarkIndex.values());
} else { } else {
@@ -282,11 +338,11 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
StartList oldStartList = this.startList; StartList oldStartList = this.startList;
this.startList = startList; this.startList = startList;
if (oldStartList == null && course != null) { 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 this.raceID.equals(raceID);
assert startList != null; assert startList != null;
assert course != null; assert course != null;
@@ -320,15 +376,16 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
@Override @Override
public void receivedCourseConfiguration(String raceID, Course course) { public void receivedCourseConfiguration(String raceID, Course course) {
Course oldCourse = this.course; Course oldCourse = this.course;
this.course = course;
if (trackedRace == null) { if (trackedRace == null) {
if (oldCourse == null && startList != null) { if (oldCourse == null && startList != null) {
createRaceDefinition(raceID); createRaceDefinition(raceID, course);
this.course = course;
} }
} else { } else {
if (isTrackedRaceStillReachable()) { if (isTrackedRaceStillReachable()) {
try { try {
domainFactory.updateCourseWaypoints(trackedRace.getRace().getCourse(), course.getMarks()); domainFactory.updateCourseWaypoints(trackedRace.getRace().getCourse(), course.getMarks());
this.course = course;
} catch (PatchFailedException e) { } catch (PatchFailedException e) {
logger.info("Internal error trying to update course: " + e.getMessage()); logger.info("Internal error trying to update course: " + e.getMessage());
logger.throwing(SwissTimingRaceTrackerImpl.class.getName(), "receivedCourseConfiguration", e); logger.throwing(SwissTimingRaceTrackerImpl.class.getName(), "receivedCourseConfiguration", e);
@@ -348,4 +405,12 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen
public void receivedAvailableRaces(Iterable<Race> races) { public void receivedAvailableRaces(Iterable<Race> races) {
// don't care // 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 * 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 * column only one group has raced (expressed by a mocked TrackedRace attached to the column). Ensure that the column does
* count for neither the total points sum nor the competitor ordering. * count for the total points sum but that the competitor ordering is controlled by the number of races.
*/ */
@Test @Test
public void testUnorderedGroupsWithOneGroupNotHavingRacedInAColumn() throws NoWindException { 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), later), 0.000000001);
assertEquals((double) (i+1), leaderboard.getTotalPoints(competitors.get(i+5), later), 0.0000000001); 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)); TrackedRace q2Yellow = new MockedTrackedRaceWithStartTimeAndRanks(now, competitors.subList(3, 8));
RaceColumn q2Column = qualificationSeries.getRaceColumnByName("Q2"); RaceColumn q2Column = qualificationSeries.getRaceColumnByName("Q2");
q2Column.setTrackedRace(q2Column.getFleetByName("Yellow"), q2Yellow); q2Column.setTrackedRace(q2Column.getFleetByName("Yellow"), q2Yellow);
List<Competitor> rankedCompetitorsWithOneRaceMissingInQ2 = leaderboard.getCompetitorsFromBestToWorst(later); List<Competitor> rankedCompetitorsWithOneRaceMissingInQ2 = leaderboard.getCompetitorsFromBestToWorst(later);
for (int i=0; i<5; i++) { // scores: C1=1, C2=2, C3=3, C4=5, C5=7, C6=4, C7=6, C8=8, C9=4, C10=5
assertTrue(competitors.get(i) == rankedCompetitorsWithOneRaceMissingInQ2.get(2*i) || // ordered by scores: C1, C2, C3, C6/C9, C4/C10, C7, C5, C8
competitors.get(i) == rankedCompetitorsWithOneRaceMissingInQ2.get(2*i+1)); // ordered primarily by number of races: C6, C4, C7, C5, C8, C1, C2, C3, C9, C10
assertTrue(competitors.get(i+5) == rankedCompetitorsWithOneRaceMissingInQ2.get(2*i) || assertEquals(Arrays.asList(new Competitor[] { competitors.get(5), competitors.get(3), competitors.get(6),
competitors.get(i+5) == rankedCompetitorsWithOneRaceMissingInQ2.get(2*i+1)); competitors.get(4), competitors.get(7), competitors.get(0), competitors.get(1), competitors.get(2),
assertEquals((double) (i+1), leaderboard.getTotalPoints(competitors.get(i), later), 0.000000001); competitors.get(8), competitors.get(9) }), rankedCompetitorsWithOneRaceMissingInQ2);
assertEquals((double) (i+1), leaderboard.getTotalPoints(competitors.get(i+5), later), 0.0000000001); 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 // 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, 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 * Regarding bug 961 and 1023, test scoring in a leaderboard that has a qualification series with two unordered
* column only one group has raced (expressed by a mocked TrackedRace attached to the column). Then add the second tracked * groups where for one column only one group has raced (expressed by a mocked TrackedRace attached to the column).
* 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, * Those competitors are expected to rank better than those who haven't raced because they have a greater number of
* simulating the use of a proxy race (that doesn't ever start) and a manual score entry. * 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 @Test
public void testUnorderedGroupsWithOneGroupNotHavingStartedRacedInAcolumnAndThenCorrectingScore() throws NoWindException { public void testUnorderedGroupsWithOneGroupNotHavingStartedRacedInAcolumnAndThenCorrectingScore() throws NoWindException {
@@ -251,15 +257,19 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest {
competitors.get(8), competitors.get(9) })); competitors.get(8), competitors.get(9) }));
q2Column.setTrackedRace(q2Column.getFleetByName("Blue"), q2Blue); q2Column.setTrackedRace(q2Column.getFleetByName("Blue"), q2Blue);
List<Competitor> rankedCompetitorsWithOneRaceMissingInQ2 = leaderboard.getCompetitorsFromBestToWorst(later); List<Competitor> rankedCompetitorsWithOneRaceMissingInQ2 = leaderboard.getCompetitorsFromBestToWorst(later);
for (int i=0; i<5; i++) { // scores: C1=1, C2=2, C3=3, C4=5, C5=7, C6=4, C7=6, C8=8, C9=4, C10=5
assertTrue(competitors.get(i) == rankedCompetitorsWithOneRaceMissingInQ2.get(2*i) || // ordered by scores: C1, C2, C3, C6/C9, C4/C10, C7, C5, C8
competitors.get(i) == rankedCompetitorsWithOneRaceMissingInQ2.get(2*i+1)); // ordered primarily by number of races: C6, C4, C7, C5, C8, C1, C2, C3, C9, C10
assertTrue(competitors.get(i+5) == rankedCompetitorsWithOneRaceMissingInQ2.get(2*i) || assertEquals(Arrays.asList(new Competitor[] { competitors.get(5), competitors.get(3), competitors.get(6),
competitors.get(i+5) == rankedCompetitorsWithOneRaceMissingInQ2.get(2*i+1)); competitors.get(4), competitors.get(7), competitors.get(0), competitors.get(1), competitors.get(2),
assertEquals((double) (i+1), leaderboard.getTotalPoints(competitors.get(i), later), 0.000000001); competitors.get(8), competitors.get(9) }), rankedCompetitorsWithOneRaceMissingInQ2);
assertEquals((double) (i+1), leaderboard.getTotalPoints(competitors.get(i+5), later), 0.0000000001); 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: // now add a score correction for Q2/Blue to make it count:
leaderboard.getScoreCorrection().correctScore(competitors.get(9), q2Column, 42.); leaderboard.getScoreCorrection().correctScore(competitors.get(9), q2Column, 42.);
assertTrue(leaderboard.getScoringScheme().isValidInTotalScore(leaderboard, q2Column, later)); 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 * 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 * column only one group has raced (expressed by a mocked TrackedRace attached to the column). Those who already raced their
* count for computing the number of discards. * second race will have one discard, the others won't. Test this.
*/ */
@Test @Test
public void testDiscardsForUnorderedGroupsWithOneGroupNotHavingRacedInAColumn() throws NoWindException { public void testDiscardsForUnorderedGroupsWithOneGroupNotHavingRacedInAColumn() throws NoWindException {
@@ -297,9 +307,14 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest {
TrackedRace q2Yellow = new MockedTrackedRaceWithStartTimeAndRanks(now, competitors.subList(3, 8)); TrackedRace q2Yellow = new MockedTrackedRaceWithStartTimeAndRanks(now, competitors.subList(3, 8));
RaceColumn q2Column = qualificationSeries.getRaceColumnByName("Q2"); RaceColumn q2Column = qualificationSeries.getRaceColumnByName("Q2");
q2Column.setTrackedRace(q2Column.getFleetByName("Yellow"), q2Yellow); q2Column.setTrackedRace(q2Column.getFleetByName("Yellow"), q2Yellow);
for (Competitor competitor : competitors) { for (Competitor competitor : competitors.subList(3, 8)) {
assertFalse("Competitor "+competitor+" has a discard in Q1 but shouldn't", leaderboard.isDiscarded(competitor, q1Column, later)); assertTrue("Competitor "+competitor+" has no discard but should have one",
assertFalse("Competitor "+competitor+" has a discard in Q2 but shouldn't", leaderboard.isDiscarded(competitor, q2Column, later)); 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 // now add a tracked race for the blue fleet for Q2 and assert that all competitors have one discard
TrackedRace q2Blue = new MockedTrackedRaceWithStartTimeAndRanks(now, 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.assertNull;
import static org.junit.Assert.assertTrue; 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 org.junit.Test;
import com.sap.sailing.util.impl.SmartFutureCache; import com.sap.sailing.util.impl.SmartFutureCache;
@@ -36,10 +42,20 @@ public class SmartFutureCacheTest {
final boolean[] updateWasCalled = new boolean[1]; final boolean[] updateWasCalled = new boolean[1];
final boolean[] cacheWasCalled = new boolean[1]; final boolean[] cacheWasCalled = new boolean[1];
final boolean[] mayProceed = new boolean[1]; final boolean[] mayProceed = new boolean[1];
SmartFutureCache<String, String, SmartFutureCache.EmptyUpdateInterval> sfc = new SmartFutureCache<String, String, SmartFutureCache.EmptyUpdateInterval>( SmartFutureCache<String, String, SmartFutureCache.EmptyUpdateInterval> sfc = new SmartFutureCache<String, String, SmartFutureCache.EmptyUpdateInterval>(
new SmartFutureCache.AbstractCacheUpdater<String, String, SmartFutureCache.EmptyUpdateInterval>() { new SmartFutureCache.AbstractCacheUpdater<String, String, SmartFutureCache.EmptyUpdateInterval>() {
@Override @Override
public String computeCacheUpdate(String key, EmptyUpdateInterval updateInterval) throws Exception { 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; updateWasCalled[0] = true;
return "Humba"; return "Humba";
} }
@@ -47,22 +63,13 @@ public class SmartFutureCacheTest {
@Override @Override
protected void cache(String key, String value) { protected void cache(String key, String value) {
super.cache(key, 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) { synchronized (cacheWasCalled) {
cacheWasCalled[0] = true; cacheWasCalled[0] = true;
cacheWasCalled.notifyAll(); cacheWasCalled.notifyAll();
} }
} }
}; };
sfc.triggerUpdate("Trala", /* updateInterval */ null); sfc.triggerUpdate("Trala", /* updateInterval */ null); // will be held up in computeCacheUpdate
assertNull(sfc.get("Trala", /* waitForLatest */ false)); assertNull(sfc.get("Trala", /* waitForLatest */ false));
synchronized (mayProceed) { synchronized (mayProceed) {
mayProceed[0] = true; mayProceed[0] = true;
@@ -100,6 +107,11 @@ public class SmartFutureCacheTest {
public int getB() { public int getB() {
return b; return b;
} }
@Override
public String toString() {
return "["+getA()+", "+getB()+"]";
}
} }
@Test @Test
@@ -158,4 +170,62 @@ public class SmartFutureCacheTest {
assertTrue(updateWasCalled[0]); assertTrue(updateWasCalled[0]);
assertEquals(new Integer(91), sfc.get("Trala", /* waitForLatest */ true)); 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) { } catch (Exception e) {
// something went wrong during DB access; report, then use empty new wind track // 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.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; return result;
} }
@@ -14,6 +14,7 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -366,7 +367,7 @@ public class ClientParamsPHP {
return getTimePoint("RaceTrackingStartTime"); return getTimePoint("RaceTrackingStartTime");
} catch (ParseException e) { } catch (ParseException e) {
logger.info("Exception trying to parse property RaceTrackingStartTime with value "+properties.get("RaceTrackingStartTime")); 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; return null;
} }
} }
@@ -376,7 +377,7 @@ public class ClientParamsPHP {
return getTimePoint("RaceTrackingEndTime"); return getTimePoint("RaceTrackingEndTime");
} catch (ParseException e) { } catch (ParseException e) {
logger.info("Exception trying to parse property RaceTrackingEndTime with value "+properties.get("RaceTrackingEndTime")); 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; return null;
} }
} }
@@ -114,7 +114,7 @@ public class RaceCourseReceiver extends AbstractReceiverWithQueue<Route, RouteDa
} }
} catch (PatchFailedException e) { } catch (PatchFailedException e) {
logger.log(Level.SEVERE, "Internal error updating race course "+course+": "+e.getMessage()); 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 { } else {
logger.log(Level.INFO, "Received course for non-existing race "+event.getC().getName()+". Creating RaceDefinition."); 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.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -261,18 +262,21 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements
private void pollAndParseClientParamsPHP(final URL paramURL, final Simulator simulator) { private void pollAndParseClientParamsPHP(final URL paramURL, final Simulator simulator) {
Set<RaceDefinition> raceDefinitions = getRaces(); Set<RaceDefinition> raceDefinitions = getRaces();
if (raceDefinitions != null && !raceDefinitions.isEmpty()) { 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; final ClientParamsPHP clientParams;
try { try {
clientParams = new ClientParamsPHP(new InputStreamReader(paramURL.openStream())); 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(); final List<? extends TracTracControlPoint> newTracTracControlPoints = clientParams.getRaceDefaultRoute().getControlPoints();
Map<Integer, NauticalSide> passingSideData = parsePassingSideData(clientParams.getRaceDefaultRoute(), newTracTracControlPoints); Map<Integer, NauticalSide> passingSideData = parsePassingSideData(clientParams.getRaceDefaultRoute(), newTracTracControlPoints);
int i = 1; int i = 1;
for (TracTracControlPoint newTracTracControlPoint : newTracTracControlPoints) { for (TracTracControlPoint newTracTracControlPoint : newTracTracControlPoints) {
NauticalSide nauticalSide = passingSideData.containsKey(i) ? passingSideData.get(i) : null; NauticalSide nauticalSide = passingSideData.containsKey(i) ? passingSideData.get(i) : null;
newCourseControlPoints.add(new Pair<com.sap.sailing.domain.base.ControlPoint, final com.sap.sailing.domain.base.ControlPoint newControlPoint = domainFactory.getOrCreateControlPoint(newTracTracControlPoint);
NauticalSide>(domainFactory.getOrCreateControlPoint(newTracTracControlPoint), nauticalSide)); newCourseControlPoints.add(newControlPoint);
newCourseControlPointsWithPassingSide.add(new Pair<com.sap.sailing.domain.base.ControlPoint,
NauticalSide>(newControlPoint, nauticalSide));
i++; i++;
} }
List<com.sap.sailing.domain.base.ControlPoint> currentCourseControlPoints = new ArrayList<>(); List<com.sap.sailing.domain.base.ControlPoint> currentCourseControlPoints = new ArrayList<>();
@@ -283,10 +287,10 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements
if (!newCourseControlPoints.equals(currentCourseControlPoints)) { if (!newCourseControlPoints.equals(currentCourseControlPoints)) {
logger.info("Detected course change based on clientparams.php contents for races "+getRaces()); logger.info("Detected course change based on clientparams.php contents for races "+getRaces());
try { try {
course.update(newCourseControlPoints, domainFactory.getBaseDomainFactory()); course.update(newCourseControlPointsWithPassingSide, domainFactory.getBaseDomainFactory());
} catch (PatchFailedException pfe) { } catch (PatchFailedException pfe) {
logger.severe("Failed to apply course update "+newTracTracControlPoints+" to course "+course); 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); updateStartStopTimesAndLiveDelay(clientParams, simulator);
@@ -311,7 +315,7 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements
} }
} catch (IOException e) { } catch (IOException e) {
logger.info("Exception "+e.getMessage()+" while trying to read clientparams.php for races "+getRaces()); 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);
} }
} }
} }
@@ -1,46 +1,46 @@
package com.sap.sailing.domain.base; package com.sap.sailing.domain.base;
import java.util.List; import java.util.List;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import com.sap.sailing.domain.common.NauticalSide; import com.sap.sailing.domain.common.NauticalSide;
import com.sap.sailing.domain.common.impl.Util.Pair; import com.sap.sailing.domain.common.impl.Util.Pair;
import difflib.PatchFailedException; import difflib.PatchFailedException;
/** /**
* A course consists of a sequence of {@link Waypoint}s. The {@link Leg}s extend between the adjacent waypoints. * A course consists of a sequence of {@link Waypoint}s. The {@link Leg}s extend between the adjacent waypoints.
* Therefore, there is one waypoint more than there are legs. * Therefore, there is one waypoint more than there are legs.
* <p> * <p>
* *
* A course maintains a re-entrant read/write lock (see {@link ReentrantReadWriteLock}. Individual reading methods * A course maintains a re-entrant read/write lock (see {@link ReentrantReadWriteLock}. Individual reading methods
* internally use the read lock to protect against concurrent modifications through {@link #removeWaypoint(int)} or * internally use the read lock to protect against concurrent modifications through {@link #removeWaypoint(int)} or
* {@link #addWaypoint(int, Waypoint)} which each obtain the write lock. However, if callers expect the waypoint or leg * {@link #addWaypoint(int, Waypoint)} which each obtain the write lock. However, if callers expect the waypoint or leg
* obtained from a reading call to remain valid, holding the read lock is required. Otherwise, particularly a * obtained from a reading call to remain valid, holding the read lock is required. Otherwise, particularly a
* {@link #removeWaypoint(int)} call may turn a leg and of course the waypoint invalid and therefore violate the * {@link #removeWaypoint(int)} call may turn a leg and of course the waypoint invalid and therefore violate the
* reader's assumptions. The write lock is still held while propagating the updates to all {@link CourseListener}s * reader's assumptions. The write lock is still held while propagating the updates to all {@link CourseListener}s
* registered. It is of course necessary to always use {@link #lockForRead()} and {@link #unlockAfterRead()} * registered. It is of course necessary to always use {@link #lockForRead()} and {@link #unlockAfterRead()}
* symmetrically, where the {@link #unlockAfterRead()} call should happen in a <code>finally</code> clause to guarantee * symmetrically, where the {@link #unlockAfterRead()} call should happen in a <code>finally</code> clause to guarantee
* execution. * execution.
* *
* @author Axel Uhl (d043530) * @author Axel Uhl (d043530)
* *
*/ */
public interface Course extends CourseData { public interface Course extends CourseBase {
void lockForRead(); void lockForRead();
void unlockAfterRead(); void unlockAfterRead();
void addCourseListener(CourseListener listener); void addCourseListener(CourseListener listener);
void removeCourseListener(CourseListener listener); void removeCourseListener(CourseListener listener);
/** /**
* Carefully merges the new list of control points into this course by constructing a minimal difference between the * Carefully merges the new list of control points into this course by constructing a minimal difference between the
* control point list and the control points referenced by this course's waypoints. Change events are propagated * control point list and the control points referenced by this course's waypoints. Change events are propagated
* to the registered {@link CourseListener}s as if {@link #addWaypoint(int, Waypoint)} and {@link #removeWaypoint(int)} * to the registered {@link CourseListener}s as if {@link #addWaypoint(int, Waypoint)} and {@link #removeWaypoint(int)}
* had been used. * had been used.
*/ */
void update(List<Pair<ControlPoint, NauticalSide>> newControlPoints, DomainFactory baseDomainFactory) throws PatchFailedException; void update(List<Pair<ControlPoint, NauticalSide>> newControlPoints, DomainFactory baseDomainFactory) throws PatchFailedException;
} }
@@ -1,21 +1,21 @@
package com.sap.sailing.domain.base; package com.sap.sailing.domain.base;
import com.sap.sailing.domain.common.Named; import com.sap.sailing.domain.common.Named;
import com.sap.sailing.domain.common.Renamable; import com.sap.sailing.domain.common.Renamable;
import com.sap.sailing.domain.common.WithID; import com.sap.sailing.domain.common.WithID;
/** /**
* An event is a group of {@link Regatta regattas} carried out at a common venue within a common time frame. For * An event is a group of {@link Regatta regattas} carried out at a common venue within a common time frame. For
* example, Kiel Week 2011 is an event, and the International German Championship 2011 held, e.g., in Travem�nde, is an event, * example, Kiel Week 2011 is an event, and the International German Championship 2011 held, e.g., in Travem�nde, is an event,
* too. * too.
* *
* @author Axel Uhl (D043530) * @author Axel Uhl (D043530)
* *
*/ */
public interface Event extends Named, Renamable, WithID, EventData { public interface Event extends Named, Renamable, WithID, EventBase {
Iterable<Regatta> getRegattas(); Iterable<Regatta> getRegattas();
void addRegatta(Regatta regatta); void addRegatta(Regatta regatta);
void removeRegatta(Regatta regatta); void removeRegatta(Regatta regatta);
} }
@@ -18,7 +18,7 @@ import com.sap.sailing.domain.tracking.TrackedRegattaRegistry;
* @author Axel Uhl (D043530) * @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 * 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) { } catch (Exception e) {
logger.log(Level.SEVERE, "Exception while notifying listener about waypoint " + waypointToRemove logger.log(Level.SEVERE, "Exception while notifying listener about waypoint " + waypointToRemove
+ " that got removed from course " + this + ": " + e.getMessage()); + " 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) { } catch (Exception e) {
logger.log(Level.SEVERE, "Exception while notifying listener about waypoint " + waypointToAdd logger.log(Level.SEVERE, "Exception while notifying listener about waypoint " + waypointToAdd
+ " that got added to course " + this + ": " + e.getMessage()); + " 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.ScoringScheme;
import com.sap.sailing.domain.leaderboard.impl.HighPoint; import com.sap.sailing.domain.leaderboard.impl.HighPoint;
import com.sap.sailing.domain.leaderboard.impl.HighPointExtremeSailingSeriesOverall; 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.HighPointLastBreaksTie;
import com.sap.sailing.domain.leaderboard.impl.LowPoint; 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.MarkPassing;
import com.sap.sailing.domain.tracking.impl.MarkPassingImpl; import com.sap.sailing.domain.tracking.impl.MarkPassingImpl;
@@ -287,9 +288,10 @@ public class DomainFactoryImpl implements DomainFactory {
case HIGH_POINT_LAST_BREAKS_TIE: case HIGH_POINT_LAST_BREAKS_TIE:
return new HighPointLastBreaksTie(); return new HighPointLastBreaksTie();
case HIGH_POINT_FIRST_GETS_TEN: case HIGH_POINT_FIRST_GETS_TEN:
return new HighPointFirstGets10(); return new HighPointFirstGets10LastBreaksTie();
default: case LOW_POINT_WINNER_GETS_ZERO:
throw new RuntimeException("Unknown scoring scheme type "+scoringSchemeType.name()); return new LowPointWinnerGetsZero();
} }
throw new RuntimeException("Unknown scoring scheme type "+scoringSchemeType.name());
} }
} }
@@ -1,43 +1,43 @@
package com.sap.sailing.domain.base.impl; package com.sap.sailing.domain.base.impl;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import com.sap.sailing.domain.base.Event; import com.sap.sailing.domain.base.Event;
import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.base.Regatta;
import com.sap.sailing.domain.base.Venue; 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 static final long serialVersionUID = 855135446595485715L;
private final Set<Regatta> regattas; private final Set<Regatta> regattas;
public EventImpl(String name, String venueName, String publicationUrl, boolean isPublic, Serializable id) { public EventImpl(String name, String venueName, String publicationUrl, boolean isPublic, Serializable id) {
this(name, new VenueImpl(venueName), publicationUrl, isPublic, id); this(name, new VenueImpl(venueName), publicationUrl, isPublic, id);
} }
/** /**
* @param venue must not be <code>null</code> * @param venue must not be <code>null</code>
*/ */
public EventImpl(String name, Venue venue, String publicationUrl, boolean isPublic, Serializable id) { public EventImpl(String name, Venue venue, String publicationUrl, boolean isPublic, Serializable id) {
super(name, venue, publicationUrl, isPublic, id); super(name, venue, publicationUrl, isPublic, id);
this.regattas = new HashSet<Regatta>(); this.regattas = new HashSet<Regatta>();
} }
@Override @Override
public Iterable<Regatta> getRegattas() { public Iterable<Regatta> getRegattas() {
return Collections.unmodifiableSet(regattas); return Collections.unmodifiableSet(regattas);
} }
@Override @Override
public void addRegatta(Regatta regatta) { public void addRegatta(Regatta regatta) {
regattas.add(regatta); regattas.add(regatta);
} }
@Override @Override
public void removeRegatta(Regatta regatta) { public void removeRegatta(Regatta regatta) {
regattas.remove(regatta); regattas.remove(regatta);
} }
} }
@@ -20,6 +20,7 @@ import com.sap.sailing.domain.base.RaceDefinition;
import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.base.Regatta;
import com.sap.sailing.domain.base.RegattaListener; import com.sap.sailing.domain.base.RegattaListener;
import com.sap.sailing.domain.base.Series; 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.RegattaIdentifier;
import com.sap.sailing.domain.common.RegattaName; import com.sap.sailing.domain.common.RegattaName;
import com.sap.sailing.domain.common.impl.NamedImpl; import com.sap.sailing.domain.common.impl.NamedImpl;
@@ -84,8 +85,9 @@ public class RegattaImpl extends NamedImpl implements Regatta, RaceColumnListene
* carried out. * carried out.
*/ */
public RegattaImpl(RaceLogStore raceLogStore, String baseName, BoatClass boatClass, TrackedRegattaRegistry trackedRegattaRegistry, ScoringScheme scoringScheme, Serializable id, CourseArea courseArea) { 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 this(raceLogStore, baseName, boatClass, Collections.singletonList(new SeriesImpl(LeaderboardNameConstants.DEFAULT_SERIES_NAME,
.singletonList(new FleetImpl("Default")), /* race column names */new ArrayList<String>(), /* isMedal */false, Collections
.singletonList(new FleetImpl(LeaderboardNameConstants.DEFAULT_FLEET_NAME)), /* race column names */new ArrayList<String>(),
trackedRegattaRegistry)), /* persistent */false, scoringScheme, id, courseArea); trackedRegattaRegistry)), /* persistent */false, scoringScheme, id, courseArea);
} }
@@ -95,7 +97,7 @@ public class RegattaImpl extends NamedImpl implements Regatta, RaceColumnListene
* this new regatta. * this new regatta.
*/ */
public RegattaImpl(RaceLogStore raceLogStore, String baseName, BoatClass boatClass, Iterable<? extends Series> series, boolean persistent, ScoringScheme scoringScheme, Serializable id, CourseArea courseArea) { 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.id = id;
this.raceLogStore = raceLogStore; this.raceLogStore = raceLogStore;
races = new HashSet<RaceDefinition>(); races = new HashSet<RaceDefinition>();
@@ -126,7 +128,7 @@ public class RegattaImpl extends NamedImpl implements Regatta, RaceColumnListene
new RaceLogOnRegattaIdentifier(this, raceColumn.getName()))); 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+")"); 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 * 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 * those fleets are unordered. In that case, scores need to be available for all fleets before the column counts
* for the total scores. * for the total scores.
* @param at TODO
*/ */
boolean isValidInTotalScore(Leaderboard leaderboard, RaceColumn raceColumn, TimePoint at); boolean isValidInTotalScore(Leaderboard leaderboard, RaceColumn raceColumn, TimePoint at);
} }
@@ -70,6 +70,13 @@ public abstract class AbstractScoringSchemeImpl implements ScoringScheme {
return new ScoreComparator(nullScoresAreBetter); 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 @Override
public boolean isValidInTotalScore(Leaderboard leaderboard, RaceColumn raceColumn, TimePoint at) { public boolean isValidInTotalScore(Leaderboard leaderboard, RaceColumn raceColumn, TimePoint at) {
boolean result; 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) { public int compareByBetterScore(List<Pair<RaceColumn, Double>> o1Scores, List<Pair<RaceColumn, Double>> o2Scores, boolean nullScoresAreBetter) {
assert o1Scores.size() == o2Scores.size(); assert o1Scores.size() == o2Scores.size();
final Comparator<Double> pureScoreComparator = getScoreComparator(nullScoresAreBetter); final Comparator<Double> pureScoreComparator = getScoreComparator(nullScoresAreBetter);
Comparator<Pair<RaceColumn, Double>> scoreComparator = new Comparator<Pair<RaceColumn, Double>>() { // needs to compare net points; therefore, divide the total points by the column factor for comparison:
@Override List<Double> o1NetScores = new ArrayList<>();
public int compare(Pair<RaceColumn, Double> o1, Pair<RaceColumn, Double> o2) { for (Pair<RaceColumn, Double> o1ColumnAndScore : o1Scores) {
return pureScoreComparator.compare(o1.getB(), o2.getB()); o1NetScores.add(o1ColumnAndScore.getB()/o1ColumnAndScore.getA().getFactor());
} }
}; List<Double> o2NetScores = new ArrayList<>();
List<Pair<RaceColumn, Double>> o1ScoresCopy = new ArrayList<Pair<RaceColumn, Double>>(o1Scores); for (Pair<RaceColumn, Double> o2ColumnAndScore : o2Scores) {
List<Pair<RaceColumn, Double>> o2ScoresCopy = new ArrayList<Pair<RaceColumn, Double>>(o2Scores); o2NetScores.add(o2ColumnAndScore.getB()/o2ColumnAndScore.getA().getFactor());
Collections.sort(o1ScoresCopy, scoreComparator); }
Collections.sort(o2ScoresCopy, scoreComparator); Collections.sort(o1NetScores, pureScoreComparator);
Collections.sort(o2NetScores, pureScoreComparator);
// now both lists are sorted from best to worst score // now both lists are sorted from best to worst score
Iterator<Pair<RaceColumn, Double>> o1Iter = o1ScoresCopy.iterator(); Iterator<Double> o1Iter = o1NetScores.iterator();
Iterator<Pair<RaceColumn, Double>> o2Iter = o2ScoresCopy.iterator(); Iterator<Double> o2Iter = o2NetScores.iterator();
int result = 0; int result = 0;
while (result == 0 && o1Iter.hasNext() && o2Iter.hasNext()) { while (result == 0 && o1Iter.hasNext() && o2Iter.hasNext()) {
result = scoreComparator.compare(o1Iter.next(), o2Iter.next()); result = pureScoreComparator.compare(o1Iter.next(), o2Iter.next());
} }
return result; 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 * 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 @Override
public List<Competitor> getCompetitorsFromBestToWorst(final RaceColumn raceColumn, TimePoint timePoint) throws NoWindException { 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.RaceColumn;
import com.sap.sailing.domain.base.RaceColumnListener; import com.sap.sailing.domain.base.RaceColumnListener;
import com.sap.sailing.domain.base.impl.FleetImpl; 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.FlexibleLeaderboard;
import com.sap.sailing.domain.leaderboard.FlexibleRaceColumn; import com.sap.sailing.domain.leaderboard.FlexibleRaceColumn;
import com.sap.sailing.domain.leaderboard.ScoringScheme; 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 { public class FlexibleLeaderboardImpl extends AbstractLeaderboardImpl implements FlexibleLeaderboard {
private static Logger logger = Logger.getLogger(FlexibleLeaderboardImpl.class.getName()); 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 static final long serialVersionUID = -5708971849158747846L;
private final List<FlexibleRaceColumn> races; private final List<FlexibleRaceColumn> races;
private final ScoringScheme scoringScheme; 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.base.RaceColumn;
import com.sap.sailing.domain.common.MaxPointsReason; import com.sap.sailing.domain.common.MaxPointsReason;
import com.sap.sailing.domain.common.ScoringSchemeType; 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() { public ScoringSchemeType getType() {
return ScoringSchemeType.HIGH_POINT; 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.Competitor;
import com.sap.sailing.domain.base.RaceColumn; 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; 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 * Compares a competitor better (lesser) if it has the greater number of races scored
*/ */
protected int compareByNumberOfRacesScored(int o1NumberOfRacesScored, int o2NumberOfRacesScored) { 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.base.RaceColumn;
import com.sap.sailing.domain.common.MaxPointsReason; import com.sap.sailing.domain.common.MaxPointsReason;
import com.sap.sailing.domain.common.ScoringSchemeType; 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() { public ScoringSchemeType getType() {
return ScoringSchemeType.LOW_POINT; 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 @Override
public Set<RegattaAndRaceIdentifier> getRaceIdentifiers() { public Set<RegattaAndRaceIdentifier> getRaceIdentifiers() {
Set<RegattaAndRaceIdentifier> result = new HashSet<RegattaAndRaceIdentifier>(); Set<RegattaAndRaceIdentifier> result = new HashSet<RegattaAndRaceIdentifier>();
for (RaceDefinition race : getRaces()) { final Set<RaceDefinition> races = getRaces();
TrackedRace trackedRace = getTrackedRegatta().getTrackedRace(race); if (races != null) {
if (trackedRace != null) { for (RaceDefinition race : races) {
result.add(trackedRace.getRaceIdentifier()); TrackedRace trackedRace = getTrackedRegatta().getTrackedRace(race);
if (trackedRace != null) {
result.add(trackedRace.getRaceIdentifier());
}
} }
} }
return result; return result;
@@ -47,11 +47,11 @@ public interface RaceTracker {
com.sap.sailing.domain.base.Regatta getRegatta(); com.sap.sailing.domain.base.Regatta getRegatta();
/** /**
* Returns the races being tracked by this tracker. Non-blocking call that returns <code>null</code> if * Returns the races being tracked by this tracker. Non-blocking call that returns <code>null</code> or an empty set
* the {@link RaceDefinition} for a TracTrac Event hasn't been created yet, e.g., because the course definition * 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). * hasn't been received yet or the listener for receiving course information hasn't been registered (yet). Also
* Also returns races that have been removed from containing structures which may lead this tracker to no * returns races that have been removed from containing structures which may lead this tracker to no longer update
* longer update their {@link TrackedRace} with new data. * their {@link TrackedRace} with new data.
*/ */
Set<RaceDefinition> getRaces(); Set<RaceDefinition> getRaces();
@@ -1,14 +1,17 @@
package com.sap.sailing.domain.tracking.impl; package com.sap.sailing.domain.tracking.impl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.logging.Logger; 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.Competitor;
import com.sap.sailing.domain.base.Leg; 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.Timed;
import com.sap.sailing.domain.base.Waypoint; import com.sap.sailing.domain.base.Waypoint;
import com.sap.sailing.domain.base.impl.MillisecondsTimePoint; import com.sap.sailing.domain.base.impl.MillisecondsTimePoint;
@@ -334,7 +337,9 @@ public class CrossTrackErrorCache extends AbstractRaceChangeListener {
@Override @Override
public void markPositionChanged(GPSFix fix, Mark mark) { public void markPositionChanged(GPSFix fix, Mark mark) {
TimePoint from = owner.getOrCreateTrack(mark).getEstimatedPositionTimePeriodAffectedBy(fix).getA(); 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); 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.Waypoint;
import com.sap.sailing.domain.base.impl.MillisecondsTimePoint; import com.sap.sailing.domain.base.impl.MillisecondsTimePoint;
import com.sap.sailing.domain.common.TimePoint; 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.WindSource;
import com.sap.sailing.domain.common.WindSourceType; import com.sap.sailing.domain.common.WindSourceType;
import com.sap.sailing.domain.common.impl.Util; import com.sap.sailing.domain.common.impl.Util;
@@ -238,7 +239,7 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
listener.windSourcesToExcludeChanged(windSourcesToExclude); listener.windSourcesToExcludeChanged(windSourcesToExclude);
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); 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); listener.raceTimesChanged(startOfTracking, endOfTracking, startTimeReceived);
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); 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); listener.markPositionChanged(fix, mark);
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); 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); listener.competitorPositionChanged(fix, competitor);
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); 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); listener.statusChanged(status);
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); 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); listener.windDataReceived(wind, windSource);
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); 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); newMillisecondsOverWhichToAverageSpeed);
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage());
logger.throwing(DynamicTrackedRaceImpl.class.getName(), logger.log(Level.SEVERE,
"notifyListenersSpeedAveragingChanged(long, long)", e); "notifyListenersSpeedAveragingChanged(long, long)", e);
} }
} }
@@ -347,7 +348,7 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
newMillisecondsOverWhichToAverageWind); newMillisecondsOverWhichToAverageWind);
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage());
logger.throwing(DynamicTrackedRaceImpl.class.getName(), logger.log(Level.SEVERE,
"notifyListenersWindAveragingChanged(long, long)", e); "notifyListenersWindAveragingChanged(long, long)", e);
} }
} }
@@ -363,7 +364,7 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
listener.delayToLiveChanged(delayToLiveInMillis); listener.delayToLiveChanged(delayToLiveInMillis);
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage());
logger.throwing(DynamicTrackedRaceImpl.class.getName(), logger.log(Level.SEVERE,
"notifyListenersDelayToLiveChanged(long)", e); "notifyListenersDelayToLiveChanged(long)", e);
} }
} }
@@ -379,7 +380,7 @@ DynamicTrackedRace, GPSTrackListener<Competitor, GPSFixMoving> {
listener.windDataRemoved(wind, windSource); listener.windDataRemoved(wind, windSource);
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); 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); listener.markPassingReceived(competitor, oldMarkPassings, markPassings);
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); 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 @Override
public void recordWind(Wind wind, WindSource windSource) { public void recordWind(Wind wind, WindSource windSource) {
getOrCreateWindTrack(windSource).add(wind); // 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?
updated(/* time point */null); // wind events shouldn't advance race time TimePoint startOfRace = getStartOfRace();
triggerManeuverCacheRecalculationForAllCompetitors(); TimePoint startOfTracking = getStartOfTracking();
notifyListeners(wind, windSource); 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 @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 getRace().getCourse().lockForRead(); // ensure the course doesn't change, particularly lose the leg we're interested in, while we're running
try { try {
for (Competitor competitor : getRace().getCompetitors()) { 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 was set to null this indicates there was an exception; no need for further calculations, return null
if (bearings != null && leg != null) { if (bearings != null && leg != null) {
TrackedLeg trackedLeg = getTrackedLeg(leg.getLeg()); TrackedLeg trackedLeg = getTrackedLeg(leg.getLeg());
@@ -1756,7 +1764,12 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
} }
protected void triggerManeuverCacheRecalculationForAllCompetitors() { 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); triggerManeuverCacheRecalculation(competitor);
} }
} }
@@ -1768,6 +1781,8 @@ public abstract class TrackedRaceImpl implements TrackedRace, CourseListener {
} }
private Triple<TimePoint, TimePoint, List<Maneuver>> computeManeuvers(Competitor competitor) throws NoWindException { 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 // compute the maneuvers for competitor
Triple<TimePoint, TimePoint, List<Maneuver>> result = null; Triple<TimePoint, TimePoint, List<Maneuver>> result = null;
NavigableSet<MarkPassing> markPassings = getMarkPassings(competitor); 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); result = new Triple<TimePoint, TimePoint, List<Maneuver>>(extendedFrom, extendedTo, extendedResultForCache);
} // else competitor has no fixes to consider; remove any maneuver cache entry } // else competitor has no fixes to consider; remove any maneuver cache entry
} // else competitor hasn't started yet; 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; return result;
} }
@@ -115,7 +115,7 @@ public class WindTrackImpl extends TrackImpl<Wind> implements WindTrack {
listener.windDataReceived(wind); listener.windDataReceived(wind);
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "WindListener " + listener + " threw exception " + e.getMessage()); 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); listener.windAveragingChanged(oldMillisecondsOverWhichToAverage, newMillisecondsOverWhichToAverage);
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "WindListener " + listener + " threw exception " + e.getMessage()); 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); listener.windDataRemoved(wind);
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "WindListener " + listener + " threw exception " + e.getMessage()); 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.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import com.sap.sailing.util.impl.SmartFutureCache.UpdateInterval; 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> * {@link #ongoingManeuverCacheRecalculations}. This will let {@link Future#cancel(boolean)} return <code>false</code>
* should it be called on that Future. * 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 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; private final CacheUpdater<K, V, U> cacheUpdateComputer;
@@ -92,6 +103,8 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
private boolean suspended; private boolean suspended;
private final Map<K, U> triggeredWhileSuspended; private final Map<K, U> triggeredWhileSuspended;
private int smartFutureCacheTaskReuseCounter;
/** /**
* An immutable "interval" description for a cache update * 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 * 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 * 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) * @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 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); super(callable);
callable.setCallable(this);
this.key = key;
this.updateInterval = updateInterval; 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() { 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() { public U getUpdateInterval() {
return updateInterval; return updateInterval;
} }
} }
public SmartFutureCache(CacheUpdater<K, V, U> cacheUpdateComputer, String nameForLocks) { 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.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.cacheUpdateComputer = cacheUpdateComputer;
this.locksForKeys = new ConcurrentHashMap<K, NamedReentrantReadWriteLock>(); this.locksForKeys = new ConcurrentHashMap<K, NamedReentrantReadWriteLock>();
this.nameForLocks = nameForLocks; 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) { 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 // no Future contained in it is in cancelled state
synchronized (ongoingRecalculations) { synchronized (ongoingRecalculations) {
final U oldUpdateInterval; final U oldUpdateInterval;
final U joinedUpdateInterval; final U joinedUpdateInterval;
FutureTaskWithCancelBlocking oldFuture = null;
if (suspended) { if (suspended) {
oldUpdateInterval = triggeredWhileSuspended.get(key); oldUpdateInterval = triggeredWhileSuspended.get(key);
} else { } else {
FutureTaskWithCancelBlocking<V, U> oldFuture = ongoingRecalculations.get(key); oldFuture = ongoingRecalculations.get(key);
if (oldFuture != null) {
oldFuture.cancel(/* mayInterruptIfRunning */false);
}
oldUpdateInterval = oldFuture == null ? null : oldFuture.getUpdateInterval(); oldUpdateInterval = oldFuture == null ? null : oldFuture.getUpdateInterval();
} }
if (oldUpdateInterval == null) { if (oldUpdateInterval == null) {
@@ -268,42 +379,41 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
if (suspended) { if (suspended) {
triggeredWhileSuspended.put(key, joinedUpdateInterval); triggeredWhileSuspended.put(key, joinedUpdateInterval);
} else { } 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 * Creates a {@link FutureTask} for the (re-)calculation of the cache entry for <code>key</code> across update
* <code>joinedUpdateInterval</code>, enters it into {@link #ongoingRecalculations} and schedules its execution with * interval <code>joinedUpdateInterval</code>, enters it into {@link #ongoingRecalculations} and schedules its
* {@link #recalculator}. The method synchronizes on {@link #ongoingRecalculations}. * 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) { synchronized (ongoingRecalculations) {
final FutureTaskWithCancelBlocking<V, U> future; final FutureTaskWithCancelBlocking future = new FutureTaskWithCancelBlocking(key,
future = new FutureTaskWithCancelBlocking<V, U>(new Callable<V>() { joinedUpdateInterval, callerWaitsSynchronouslyForResult, callerThread);
@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);
ongoingRecalculations.put(key, future); ongoingRecalculations.put(key, future);
recalculator.execute(future); recalculator.execute(future);
} }
@@ -318,16 +428,22 @@ public class SmartFutureCache<K, V, U extends UpdateInterval<U>> {
public V get(K key, boolean waitForLatest) { public V get(K key, boolean waitForLatest) {
V value = null; V value = null;
if (waitForLatest) { if (waitForLatest) {
FutureTaskWithCancelBlocking<V, U> future; FutureTaskWithCancelBlocking future;
synchronized (ongoingRecalculations) { synchronized (ongoingRecalculations) {
if (suspended && waitForLatest) { if (suspended && waitForLatest) {
final boolean wasTriggeredWhileSuspended = triggeredWhileSuspended.containsKey(key); // update interval may have deliberately been null final boolean wasTriggeredWhileSuspended = triggeredWhileSuspended.containsKey(key); // update interval may have deliberately been null
if (wasTriggeredWhileSuspended) { if (wasTriggeredWhileSuspended) {
// If suspended and the caller wants to get the latest results, and during suspend recalculations were // If suspended and the caller wants to get the latest results, and while suspended recalculations were
// triggered, and execute them now. // triggered, execute them now.
// This will enter the new future into ongoingRecalculations, and therefore this method // This will enter the new future into ongoingRecalculations, and therefore this method
// will wait for its completion below. // 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 // 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 { try {
value = future.get(); value = future.get();
} catch (InterruptedException | ExecutionException e) { } catch (InterruptedException | ExecutionException e) {
logger.throwing(SmartFutureCache.class.getName(), "get", e); logger.log(Level.SEVERE, "get", e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} // else no calculation currently going on; value has been fetched from latest cache entry } // 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) { protected void cache(final K key, V value) {
if (value == null) { if (value == null) {
cache.remove(key); cache.remove(key);
locksForKeys.remove(key);
} else { } else {
cache.put(key, value); 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,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,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[] { 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) * @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 * @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() { private static void usage() {
System.out.println("Usage: java "+UDPMirror.class.getName()+" [-v] <listeningport> hostname1 port1 [hostname2 port2]*"); System.out.println("Usage: java "+UDPMirror.class.getName()+" [-v] <listeningport> hostname1 port1 [hostname2 port2]*");
System.out.println(" -v\tPrint packets received to stdout"); 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.net.DatagramPacket;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.logging.Logger;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -13,6 +14,7 @@ import com.sap.sailing.expeditionconnector.UDPExpeditionReceiver;
import com.sap.sailing.udpconnector.UDPMessageParser; import com.sap.sailing.udpconnector.UDPMessageParser;
public class ExpeditionMessageParser implements UDPMessageParser<ExpeditionMessage> { public class ExpeditionMessageParser implements UDPMessageParser<ExpeditionMessage> {
private static final Logger logger = Logger.getLogger(ExpeditionMessageParser.class.getName());
private UDPExpeditionReceiver receiver; private UDPExpeditionReceiver receiver;
@@ -25,7 +27,7 @@ public class ExpeditionMessageParser implements UDPMessageParser<ExpeditionMessa
String packetAsString = new String(p.getData(), p.getOffset(), p.getLength()).trim(); String packetAsString = new String(p.getData(), p.getOffset(), p.getLength()).trim();
if (packetAsString.length() > 0) { if (packetAsString.length() > 0) {
Pattern completeLinePattern = Pattern 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); Matcher m = completeLinePattern.matcher(packetAsString);
boolean valid = m.matches(); boolean valid = m.matches();
if (valid) { if (valid) {
@@ -62,7 +64,7 @@ public class ExpeditionMessageParser implements UDPMessageParser<ExpeditionMessa
} }
return result; return result;
} else { } else {
System.err.println("Unparsable expedition message: " + packetAsString); logger.warning("Unparsable expedition message: " + packetAsString);
return null; // couldn't even parse return null; // couldn't even parse
} }
} else { } else {
@@ -72,7 +74,12 @@ public class ExpeditionMessageParser implements UDPMessageParser<ExpeditionMessa
private boolean checksumOk(int checksum, String packetAsString) { private boolean checksumOk(int checksum, String packetAsString) {
int b = 0; 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; b ^= stringByte;
} }
return b == checksum; return b == checksum;
@@ -1,72 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?pde version="3.5"?> <?pde version="3.5"?>
<product name="SAP Race Analysis" uid="raceanalysis.product.id" id="raceanalysis.product" application="raceanalysis.application" version="1.0.0" useFeatures="true" includeLaunchers="false"> <product name="SAP Race Analysis" uid="raceanalysis.product.id" id="raceanalysis.product" application="raceanalysis.application" version="1.0.0" useFeatures="true" includeLaunchers="false">
<configIni use="default"> <configIni use="default">
</configIni> </configIni>
<launcherArgs> <launcherArgs>
<programArgs>-os ${target.os} -ws ${target.ws} -arch ${target.arch} -nl ${target.nl} -consoleLog -console -clean</programArgs> <programArgs>-os ${target.os} -ws ${target.ws} -arch ${target.arch} -nl ${target.nl} -consoleLog -console -clean</programArgs>
<vmArgs>-Declipse.ignoreApp=true -Dosgi.noShutdown=true <vmArgs>-Declipse.ignoreApp=true -Dosgi.noShutdown=true
-Xmx1024m -Djetty.home=configuration/jetty -Xmx1024m -Djetty.home=configuration/jetty
-XX:+UseMembar</vmArgs> -XX:+UseMembar</vmArgs>
</launcherArgs> </launcherArgs>
<windowImages/> <windowImages/>
<launcher> <launcher>
<solaris/> <solaris/>
<win useIco="false"> <win useIco="false">
<bmp/> <bmp/>
</win> </win>
</launcher> </launcher>
<vm> <vm>
</vm> </vm>
<plugins> <plugins>
</plugins> </plugins>
<features> <features>
<feature id="com.sap.sailing.feature" version="1.0.0.qualifier"/> <feature id="com.sap.sailing.feature" version="1.0.0.qualifier"/>
<feature id="com.sap.sailing.feature.runtime" version="1.0.0.qualifier"/> <feature id="com.sap.sailing.feature.runtime" version="1.0.0.qualifier"/>
</features> </features>
<configurations> <configurations>
<plugin id="com.sap.sailing.ess40.resultimport" autoStart="true" startLevel="3" /> <plugin id="com.sap.sailing.ess40.resultimport" autoStart="true" startLevel="3" />
<plugin id="com.sap.sailing.freg.resultimport" autoStart="true" startLevel="3" /> <plugin id="com.sap.sailing.freg.resultimport" autoStart="true" startLevel="3" />
<plugin id="com.sap.sailing.gwt.ui" autoStart="true" startLevel="4" /> <plugin id="com.sap.sailing.gwt.ui" autoStart="true" startLevel="4" />
<plugin id="com.sap.sailing.kiworesultimport" autoStart="true" startLevel="3" /> <plugin id="com.sap.sailing.kiworesultimport" autoStart="true" startLevel="3" />
<plugin id="com.sap.sailing.monitoring" autoStart="true" startLevel="5" /> <plugin id="com.sap.sailing.monitoring" autoStart="true" startLevel="5" />
<plugin id="com.sap.sailing.server" autoStart="true" startLevel="4" /> <plugin id="com.sap.sailing.server" autoStart="true" startLevel="4" />
<plugin id="com.sap.sailing.server.gateway" autoStart="true" startLevel="4" /> <plugin id="com.sap.sailing.server.gateway" autoStart="true" startLevel="4" />
<plugin id="com.sap.sailing.server.gateway.ess40" autoStart="true" startLevel="4" /> <plugin id="com.sap.sailing.server.gateway.ess40" autoStart="true" startLevel="4" />
<plugin id="com.sap.sailing.server.replication" autoStart="true" startLevel="4" /> <plugin id="com.sap.sailing.server.replication" autoStart="true" startLevel="4" />
<plugin id="com.sap.sailing.www" autoStart="true" startLevel="4" /> <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.www.events" autoStart="true" startLevel="4" />
<plugin id="com.sap.sailing.xcelsiusadapter" autoStart="true" startLevel="4" /> <plugin id="com.sap.sailing.xcelsiusadapter" autoStart="true" startLevel="4" />
<plugin id="org.eclipse.equinox.common" autoStart="true" startLevel="2" /> <plugin id="com.sap.sailing.xrr.resultimport" autoStart="true" startLevel="3" />
<plugin id="org.eclipse.equinox.simpleconfigurator" autoStart="true" startLevel="2" /> <plugin id="org.eclipse.equinox.common" autoStart="true" startLevel="2" />
<plugin id="org.eclipse.jetty.continuation" autoStart="true" startLevel="4" /> <plugin id="org.eclipse.equinox.simpleconfigurator" autoStart="true" startLevel="2" />
<plugin id="org.eclipse.jetty.deploy" autoStart="true" startLevel="4" /> <plugin id="org.eclipse.jetty.continuation" autoStart="true" startLevel="4" />
<plugin id="org.eclipse.jetty.http" autoStart="true" startLevel="4" /> <plugin id="org.eclipse.jetty.deploy" autoStart="true" startLevel="4" />
<plugin id="org.eclipse.jetty.io" autoStart="true" startLevel="4" /> <plugin id="org.eclipse.jetty.http" autoStart="true" startLevel="4" />
<plugin id="org.eclipse.jetty.jmx" autoStart="true" startLevel="4" /> <plugin id="org.eclipse.jetty.io" autoStart="true" startLevel="4" />
<plugin id="org.eclipse.jetty.nested" autoStart="true" startLevel="4" /> <plugin id="org.eclipse.jetty.jmx" autoStart="true" startLevel="4" />
<plugin id="org.eclipse.jetty.osgi.boot" autoStart="true" startLevel="3" /> <plugin id="org.eclipse.jetty.nested" autoStart="true" startLevel="4" />
<plugin id="org.eclipse.jetty.security" autoStart="true" startLevel="4" /> <plugin id="org.eclipse.jetty.osgi.boot" autoStart="true" startLevel="3" />
<plugin id="org.eclipse.jetty.server" autoStart="true" startLevel="4" /> <plugin id="org.eclipse.jetty.security" autoStart="true" startLevel="4" />
<plugin id="org.eclipse.jetty.servlet" autoStart="true" startLevel="4" /> <plugin id="org.eclipse.jetty.server" autoStart="true" startLevel="4" />
<plugin id="org.eclipse.jetty.util" autoStart="true" startLevel="4" /> <plugin id="org.eclipse.jetty.servlet" autoStart="true" startLevel="4" />
<plugin id="org.eclipse.jetty.webapp" autoStart="true" startLevel="4" /> <plugin id="org.eclipse.jetty.util" autoStart="true" startLevel="4" />
<plugin id="org.eclipse.jetty.xml" autoStart="true" startLevel="4" /> <plugin id="org.eclipse.jetty.webapp" autoStart="true" startLevel="4" />
<plugin id="org.mortbay.jetty.util" autoStart="true" startLevel="4" /> <plugin id="org.eclipse.jetty.xml" autoStart="true" startLevel="4" />
<property name="eclipse.ignoreApp" value="true" /> <plugin id="org.mortbay.jetty.util" autoStart="true" startLevel="4" />
<property name="expedition.udp.port" value="2010" /> <property name="eclipse.ignoreApp" value="true" />
<property name="mongo.port" value="10200" /> <property name="expedition.udp.port" value="2010" />
<property name="osgi.noShutdown" value="true" /> <property name="mongo.port" value="10200" />
<property name="replication.exchangeName" value="sapsailinganalytics-dev" /> <property name="osgi.noShutdown" value="true" />
</configurations> <property name="replication.exchangeName" value="sapsailinganalytics-dev" />
</configurations>
</product>
</product>
+237 -230
View File
@@ -1,233 +1,240 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<feature <feature
id="com.sap.sailing.feature" id="com.sap.sailing.feature"
label="SAP Sailing Server" label="SAP Sailing Server"
version="1.0.0.qualifier" version="1.0.0.qualifier"
provider-name="SAP"> provider-name="SAP">
<description url="http://www.sap.com"> <description url="http://www.sap.com">
The SAP Sailing Server. The SAP Sailing Server.
Provides connectivity to a TracTrac live GPS feed as well as a live feed from an Expedition system. Provides connectivity to a TracTrac live GPS feed as well as a live feed from an Expedition system.
Computes leaderboard information for sailing races and offers RESTful APIs to all of this. Computes leaderboard information for sailing races and offers RESTful APIs to all of this.
</description> </description>
<copyright url="http://www.example.com/copyright"> <copyright url="http://www.example.com/copyright">
[Enter Copyright Description here.] [Enter Copyright Description here.]
</copyright> </copyright>
<license url="http://www.example.com/license"> <license url="http://www.example.com/license">
[Enter License Description here.] [Enter License Description here.]
</license> </license>
<requires> <requires>
<import plugin="com.googlecode.java-diff-utils" version="1.3.0" match="greaterOrEqual"/> <import plugin="com.googlecode.java-diff-utils" version="1.3.0" match="greaterOrEqual"/>
<import plugin="org.eclipse.osgi"/> <import plugin="org.eclipse.osgi"/>
<import plugin="javax.servlet" version="2.5.0" match="greaterOrEqual"/> <import plugin="javax.servlet" version="2.5.0" match="greaterOrEqual"/>
<import plugin="com.mongodb.driver" version="2.6.2" match="greaterOrEqual"/> <import plugin="com.mongodb.driver" version="2.6.2" match="greaterOrEqual"/>
<import plugin="com.google.gwt.osgi" version="2.4.0" match="greaterOrEqual"/> <import plugin="com.google.gwt.osgi" version="2.4.0" match="greaterOrEqual"/>
<import plugin="com.sap.sailing.domain.common"/> <import plugin="com.sap.sailing.domain.common"/>
<import plugin="org.json.simple" version="1.1.0" match="greaterOrEqual"/> <import plugin="org.json.simple" version="1.1.0" match="greaterOrEqual"/>
<import plugin="com.sap.sailing.operationaltransformation"/> <import plugin="com.sap.sailing.operationaltransformation"/>
<import plugin="org.moxieapps.gwt.highcharts" version="1.1.4" match="greaterOrEqual"/> <import plugin="org.moxieapps.gwt.highcharts" version="1.1.4" match="greaterOrEqual"/>
<import plugin="com.sap.sailing.geocoding"/> <import plugin="com.sap.sailing.geocoding"/>
<import plugin="org.apache.geronimo.specs.geronimo-jms_1.1_spec"/> <import plugin="org.apache.geronimo.specs.geronimo-jms_1.1_spec"/>
<import plugin="com.rabbitmq.client"/> <import plugin="com.rabbitmq.client"/>
<import plugin="org.apache.geronimo.specs.geronimo-j2ee-management_1.1_spec"/> <import plugin="org.apache.geronimo.specs.geronimo-j2ee-management_1.1_spec"/>
</requires> </requires>
<plugin <plugin
id="com.sap.sailing.domain" id="com.sap.sailing.domain"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.domain.tractracadapter" id="com.sap.sailing.domain.tractracadapter"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.expeditionconnector" id="com.sap.sailing.expeditionconnector"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.server" id="com.sap.sailing.server"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.server.gateway" id="com.sap.sailing.server.gateway"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.server.gateway.ess40" id="com.sap.sailing.server.gateway.ess40"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.tractrac.clientmodule" id="com.tractrac.clientmodule"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="org.json.simple" id="org.json.simple"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.declination" id="com.sap.sailing.declination"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.mongodb.driver" id="com.mongodb.driver"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.mongodb" id="com.sap.sailing.mongodb"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.domain.persistence" id="com.sap.sailing.domain.persistence"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.domain.swisstimingadapter" id="com.sap.sailing.domain.swisstimingadapter"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.domain.swisstimingadapter.persistence" id="com.sap.sailing.domain.swisstimingadapter.persistence"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.domain.swisstimingreplayadapter" id="com.sap.sailing.domain.swisstimingreplayadapter"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.domain.tractracadapter.persistence" id="com.sap.sailing.domain.tractracadapter.persistence"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.gwt.ui" id="com.sap.sailing.gwt.ui"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.udpconnector" id="com.sap.sailing.udpconnector"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.xcelsiusadapter" id="com.sap.sailing.xcelsiusadapter"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.google.gwt.osgi" id="com.google.gwt.osgi"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.googlecode.java-diff-utils" id="com.googlecode.java-diff-utils"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.www" id="com.sap.sailing.www"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0"/> version="0.0.0"/>
<plugin <plugin
id="com.sap.sailing.www.events" id="com.sap.sailing.www.events"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0"/> version="0.0.0"/>
<plugin <plugin
id="com.sap.sailing.server.replication" id="com.sap.sailing.server.replication"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.kiworesultimport" id="com.sap.sailing.kiworesultimport"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.ess40.resultimport" id="com.sap.sailing.ess40.resultimport"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.freg.resultimport" id="com.sap.sailing.freg.resultimport"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
<plugin <plugin
id="com.sap.sailing.monitoring" id="com.sap.sailing.monitoring"
download-size="0" download-size="0"
install-size="0" install-size="0"
version="0.0.0" version="0.0.0"
unpack="false"/> unpack="false"/>
</feature> <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 LeaderboardDTO lb = null;
private static final String LEADERBOARDNAME = "test"; private static final String LEADERBOARDNAME = "test";
private static final String DEFAULT_FLEET_NAME = "Default"; 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 SailingServiceImpl service;
private LeaderboardDTO leaderboardOriginalDTO; private LeaderboardDTO leaderboardOriginalDTO;
private LeaderboardDTO leaderboardDTO; private LeaderboardDTO leaderboardDTO;
@@ -131,3 +131,7 @@ body, table td, select, button {
.gwt-DialogBox { .gwt-DialogBox {
z-index: 20; z-index: 20;
} }
.bold {
font-family: "OpenSansCondensedBold", sans-serif;
}
+1 -2
View File
@@ -176,8 +176,7 @@ input.magnifier {
margin: 0 0 25px 0; margin: 0 0 25px 0;
} }
.leaderboardGroupPanel-LeaderboardsTableLabel { .leaderboardGroupPanel-LeaderboardsLabel {
margin: 0 0 10px;
font-size: 17px; font-size: 17px;
font-weight: 500; 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; 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.DoubleBox;
import com.google.gwt.user.client.ui.Grid; import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.Label; 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.RaceDTO;
import com.sap.sailing.gwt.ui.shared.WindDTO; 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 StringMessages stringMessages;
private final DoubleBox speedInKnotsBox; private final DoubleBox speedInKnotsBox;
@@ -23,7 +25,7 @@ public class WindSettingDialog extends DataEntryDialog<WindDTO> {
private final DateBox timeBox; private final DateBox timeBox;
protected static class WindDataValidator implements Validator<WindDTO> { protected static class WindDataValidator implements Validator<WindDTO> {
protected final StringMessages stringMessages; private final StringMessages stringMessages;
public WindDataValidator(StringMessages stringMessages){ public WindDataValidator(StringMessages stringMessages){
super(); super();
@@ -42,24 +44,26 @@ public class WindSettingDialog extends DataEntryDialog<WindDTO> {
errorMessage = stringMessages.pleaseEnterAValue(); errorMessage = stringMessages.pleaseEnterAValue();
} else if(windDTO.trueWindFromDeg != null && (windDTO.trueWindFromDeg < 0.0 || windDTO.trueWindFromDeg > 360.0)){ } else if(windDTO.trueWindFromDeg != null && (windDTO.trueWindFromDeg < 0.0 || windDTO.trueWindFromDeg > 360.0)){
errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.fromDeg(), "0", "360"); errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.fromDeg(), "0", "360");
} else if(windDTO.measureTimepoint == null) {
errorMessage = stringMessages.pleaseEnterAValue();
} else if(windDTO.position != null) { } else if(windDTO.position != null) {
if(windDTO.position.latDeg < -90.0 || windDTO.position.latDeg > 90.0){ if(windDTO.position.latDeg < -90.0 || windDTO.position.latDeg > 90.0){
errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.latitude(), "-90", "90"); errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.latitude(), "-90", "90");
} else if(windDTO.position.lngDeg < -180.0 || windDTO.position.lngDeg > 180.0){ } else if(windDTO.position.lngDeg < -180.0 || windDTO.position.lngDeg > 180.0){
errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.longitude(), "-180", "180"); errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.longitude(), "-180", "180");
} }
} }
return errorMessage; return errorMessage;
} }
} }
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); super(stringMessages.actionAddWindData(), null, stringMessages.ok(), stringMessages.cancel(), new WindDataValidator(stringMessages), callback);
this.stringMessages = stringMessages; this.stringMessages = stringMessages;
speedInKnotsBox = createDoubleBox(5); speedInKnotsBox = createDoubleBox(5);
fromInDegBox = 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); PositionDTO positionDTO = courseDTO.waypointPositions.get(0);
latDegBox = createDoubleBox(positionDTO.latDeg, 10); latDegBox = createDoubleBox(positionDTO.latDeg, 10);
lngDegBox = createDoubleBox(positionDTO.lngDeg, 10); lngDegBox = createDoubleBox(positionDTO.lngDeg, 10);
@@ -67,10 +71,11 @@ public class WindSettingDialog extends DataEntryDialog<WindDTO> {
latDegBox = createDoubleBox(10); latDegBox = createDoubleBox(10);
lngDegBox = createDoubleBox(10); lngDegBox = createDoubleBox(10);
} }
if(race.startOfRace != null) { if(race.trackedRace != null && race.trackedRace.startOfTracking != null) {
timeBox = createDateBox(race.startOfRace.getTime(), 20); timeBox = createDateBox(race.trackedRace.startOfTracking.getTime(), 20);
} else { } 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(0, 1, speedInKnotsBox);
grid.setWidget(1, 0, new Label(stringMessages.fromDeg() + ":")); grid.setWidget(1, 0, new Label(stringMessages.fromDeg() + ":"));
grid.setWidget(1, 1, fromInDegBox); grid.setWidget(1, 1, fromInDegBox);
grid.setWidget(2, 0, new Label(stringMessages.latitude() + " (" + stringMessages.optional() + "):")); grid.setWidget(2, 0, new Label(stringMessages.time() + ":"));
grid.setWidget(2, 1, latDegBox); grid.setWidget(2, 1, timeBox);
grid.setWidget(3, 0, new Label(stringMessages.longitude() + " (" + stringMessages.optional() + "):")); grid.setWidget(3, 0, new Label(stringMessages.latitude() + " (" + stringMessages.optional() + "):"));
grid.setWidget(3, 1, lngDegBox); grid.setWidget(3, 1, latDegBox);
grid.setWidget(4, 0, new Label(stringMessages.time() + " (" + stringMessages.optional() + "):")); grid.setWidget(4, 0, new Label(stringMessages.longitude() + " (" + stringMessages.optional() + "):"));
grid.setWidget(4, 1, timeBox); grid.setWidget(4, 1, lngDegBox);
return mainPanel; return mainPanel;
} }
@@ -100,7 +105,7 @@ public class WindSettingDialog extends DataEntryDialog<WindDTO> {
result.trueWindSpeedInKnots = speedInKnotsBox.getValue(); result.trueWindSpeedInKnots = speedInKnotsBox.getValue();
result.trueWindFromDeg = fromInDegBox.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) { if (latDegBox.getValue() != null && lngDegBox.getValue() != null) {
result.position = new PositionDTO(latDegBox.getValue(), lngDegBox.getValue()); 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); tabPanel.add(swisstimingEventManagementPanel, stringMessages.swissTimingEvents(), false);
regattaDisplayers.add(swisstimingEventManagementPanel); regattaDisplayers.add(swisstimingEventManagementPanel);
CreateSwissTimingRacePanel createSwissTimingRacePanel = new CreateSwissTimingRacePanel(sailingService,this,stringMessages); CreateSwissTimingRacePanel createSwissTimingRacePanel = new CreateSwissTimingRacePanel(sailingService, this, stringMessages);
//createSwissTimingRacePanel.ensureDebugId("CreateSwissTimingRace"); //createSwissTimingRacePanel.ensureDebugId("CreateSwissTimingRace");
createSwissTimingRacePanel.setSize("90%", "90%"); createSwissTimingRacePanel.setSize("90%", "90%");
tabPanel.add(createSwissTimingRacePanel,"Create SwissTiming race",false); 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, final LeaderboardConfigPanel leaderboardConfigPanel = new LeaderboardConfigPanel(sailingService, this, this, stringMessages,
/* showRaceDetails */ true); /* showRaceDetails */ true);
//leaderboardConfigPanel.ensureDebugId("LeaderboardConfiguration"); leaderboardConfigPanel.ensureDebugId("LeaderboardConfiguration");
leaderboardConfigPanel.setSize("90%", "90%"); leaderboardConfigPanel.setSize("90%", "90%");
tabPanel.add(leaderboardConfigPanel, stringMessages.leaderboardConfiguration(), /* asHTML */ false); tabPanel.add(leaderboardConfigPanel, stringMessages.leaderboardConfiguration(), /* asHTML */ false);
regattaDisplayers.add(leaderboardConfigPanel); regattaDisplayers.add(leaderboardConfigPanel);
@@ -1,24 +1,25 @@
package com.sap.sailing.gwt.ui.adminconsole; package com.sap.sailing.gwt.ui.adminconsole;
import java.util.Collection; import java.util.Collection;
import com.sap.sailing.gwt.ui.client.ErrorReporter; import com.sap.sailing.gwt.ui.client.ErrorReporter;
import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.StringMessages;
import com.sap.sailing.gwt.ui.shared.EventDTO; import com.sap.sailing.gwt.ui.shared.EventDTO;
import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO;
public class FlexibleLeaderboardCreateDialog extends FlexibleLeaderboardDialog { public class FlexibleLeaderboardCreateDialog extends FlexibleLeaderboardDialog {
public FlexibleLeaderboardCreateDialog(Collection<StrippedLeaderboardDTO> existingLeaderboards, StringMessages stringMessages, public FlexibleLeaderboardCreateDialog(Collection<StrippedLeaderboardDTO> existingLeaderboards, StringMessages stringMessages,
Collection<EventDTO> existingEvents, ErrorReporter errorReporter, DialogCallback<LeaderboardDescriptor> callback) { Collection<EventDTO> existingEvents, ErrorReporter errorReporter, DialogCallback<LeaderboardDescriptor> callback) {
super(stringMessages.createFlexibleLeaderboard(), new LeaderboardDescriptor(), stringMessages, existingEvents, errorReporter, new FlexibleLeaderboardDialog.LeaderboardParameterValidator(stringMessages, existingLeaderboards), callback); super(stringMessages.createFlexibleLeaderboard(), new LeaderboardDescriptor(), stringMessages, existingEvents, errorReporter, new FlexibleLeaderboardDialog.LeaderboardParameterValidator(stringMessages, existingLeaderboards), callback);
nameTextBox = createTextBox(null); nameTextBox = createTextBox(null);
displayNameTextBox = createTextBox(null); nameTextBox.ensureDebugId("LeaderboardNameField");
nameTextBox.setVisibleLength(50); displayNameTextBox = createTextBox(null);
displayNameTextBox.setVisibleLength(50); nameTextBox.setVisibleLength(50);
displayNameTextBox.setVisibleLength(50);
scoringSchemeListBox = createScoringSchemeListBox(this, stringMessages);
sailingEventsListBox = createSailingEventListBox(); scoringSchemeListBox = createScoringSchemeListBox(this, stringMessages);
discardThresholdBoxes = initEmptyDiscardThresholdBoxes(this); sailingEventsListBox = createSailingEventListBox();
} discardThresholdBoxes = initEmptyDiscardThresholdBoxes(this);
} }
}
@@ -138,7 +138,7 @@ public class RaceColumnInLeaderboardDialog extends DataEntryDialog<RaceColumnInL
super.show(); super.show();
if (isRegattaLeaderboard) { if (isRegattaLeaderboard) {
explicitFactorBox.setFocus(true); explicitFactorBox.setFocus(true);
explicitFactorBox.setSelectionRange(0, explicitFactorBox.getText().length()); explicitFactorBox.selectAll();
} else { } else {
raceNameBox.setFocus(true); 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.user.client.ui.VerticalPanel;
import com.google.gwt.view.client.ListDataProvider; import com.google.gwt.view.client.ListDataProvider;
import com.google.gwt.view.client.MultiSelectionModel; 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.ErrorReporter;
import com.sap.sailing.gwt.ui.client.RaceSelectionModel; import com.sap.sailing.gwt.ui.client.RaceSelectionModel;
import com.sap.sailing.gwt.ui.client.RegattaRefresher; import com.sap.sailing.gwt.ui.client.RegattaRefresher;
import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.SailingServiceAsync;
import com.sap.sailing.gwt.ui.client.StringMessages; 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.SwissTimingConfigurationDTO;
import com.sap.sailing.gwt.ui.shared.SwissTimingRaceRecordDTO; import com.sap.sailing.gwt.ui.shared.SwissTimingRaceRecordDTO;
@@ -138,14 +141,31 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane
} }
}); });
TextColumn<SwissTimingRaceRecordDTO> raceNameColumn = new TextColumn<SwissTimingRaceRecordDTO>() { TextColumn<SwissTimingRaceRecordDTO> raceNameColumn = new TextColumn<SwissTimingRaceRecordDTO>() {
@Override @Override
public String getValue(SwissTimingRaceRecordDTO object) { public String getValue(SwissTimingRaceRecordDTO object) {
return object.ID; return object.ID;
} }
}; };
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>() { TextColumn<SwissTimingRaceRecordDTO> raceStartTrackingColumn = new TextColumn<SwissTimingRaceRecordDTO>() {
@Override @Override
public String getValue(SwissTimingRaceRecordDTO object) { public String getValue(SwissTimingRaceRecordDTO object) {
@@ -153,7 +173,6 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane
} }
}; };
HorizontalPanel racesSplitPanel = new HorizontalPanel(); HorizontalPanel racesSplitPanel = new HorizontalPanel();
mainPanel.add(racesSplitPanel); mainPanel.add(racesSplitPanel);
@@ -179,6 +198,15 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane
filterPanel.setSpacing(5); filterPanel.setSpacing(5);
racesPanel.add(filterPanel); 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() + ":"); Label lblFilterEvents = new Label(stringConstants.filterRacesByName() + ":");
filterPanel.add(lblFilterEvents); filterPanel.add(lblFilterEvents);
filterPanel.setCellVerticalAlignment(lblFilterEvents, HasVerticalAlignment.ALIGN_MIDDLE); filterPanel.setCellVerticalAlignment(lblFilterEvents, HasVerticalAlignment.ALIGN_MIDDLE);
@@ -205,6 +233,8 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane
AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class);
raceTable = new CellTable<SwissTimingRaceRecordDTO>(/* pageSize */ 10000, tableRes); raceTable = new CellTable<SwissTimingRaceRecordDTO>(/* pageSize */ 10000, tableRes);
raceTable.addColumn(raceNameColumn, stringConstants.name()); raceTable.addColumn(raceNameColumn, stringConstants.name());
raceTable.addColumn(raceDescriptionColumn, stringConstants.description());
raceTable.addColumn(boatClassColumn, stringConstants.boatClass());
raceTable.addColumn(raceStartTrackingColumn, stringConstants.raceStartTimeColumn()); raceTable.addColumn(raceStartTrackingColumn, stringConstants.raceStartTimeColumn());
raceTable.setWidth("300px"); raceTable.setWidth("300px");
raceTable.setSelectionModel(new MultiSelectionModel<SwissTimingRaceRecordDTO>() {}); raceTable.setSelectionModel(new MultiSelectionModel<SwissTimingRaceRecordDTO>() {});
@@ -345,8 +375,13 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane
selectedRaces.add(race); selectedRaces.add(race);
} }
} }
RegattaDTO selectedRegatta = getSelectedRegatta();
RegattaIdentifier regattaIdentifier = null;
if (selectedRegatta != null) {
regattaIdentifier = new RegattaName(selectedRegatta.name);
}
sailingService.trackWithSwissTiming( sailingService.trackWithSwissTiming(
/* regattaToAddTo */null, // TODO allow user to select a pre-defined regatta /* regattaToAddTo */ regattaIdentifier,
selectedRaces, hostname, port, /* canSendRequests */false, trackWind, correctWindByDeclination, selectedRaces, hostname, port, /* canSendRequests */false, trackWind, correctWindByDeclination,
new AsyncCallback<Void>() { new AsyncCallback<Void>() {
@Override @Override
@@ -186,9 +186,8 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel {
layoutTable.setWidget(2, 0, hostnameLabel); layoutTable.setWidget(2, 0, hostnameLabel);
layoutTable.setWidget(2, 1, this.hostnameTextBox); layoutTable.setWidget(2, 1, this.hostnameTextBox);
// Regatta name // Event name
Label eventNameLabel = new Label(this.stringMessages.eventName() + ":"); Label eventNameLabel = new Label(this.stringMessages.eventName() + ":");
this.eventNameTextBox = new TextBox(); this.eventNameTextBox = new TextBox();
this.eventNameTextBox.ensureDebugId("RegattaName"); this.eventNameTextBox.ensureDebugId("RegattaName");
this.eventNameTextBox.setText("event_2011..."); this.eventNameTextBox.setText("event_2011...");
@@ -198,7 +197,6 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel {
synchURIs(); synchURIs();
} }
}); });
layoutTable.setWidget(3, 0, eventNameLabel); layoutTable.setWidget(3, 0, eventNameLabel);
layoutTable.setWidget(3, 1, this.eventNameTextBox); layoutTable.setWidget(3, 1, this.eventNameTextBox);
@@ -338,9 +336,8 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel {
//columnFormatter.setWidth(1, "80%"); //columnFormatter.setWidth(1, "80%");
// Regatta // Regatta
Label regattaForTrackingLabel = new Label("Regatta used for the tracked race:"); Label regattaForTrackingLabel = new Label(stringMessages.regattaUsedForTheTrackedRace());
regattaForTrackingLabel.setWordWrap(false); regattaForTrackingLabel.setWordWrap(false);
layoutTable.setWidget(0, 0, regattaForTrackingLabel); layoutTable.setWidget(0, 0, regattaForTrackingLabel);
layoutTable.setWidget(0, 1, getAvailableRegattasListBox()); layoutTable.setWidget(0, 1, getAvailableRegattasListBox());
@@ -619,11 +616,9 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel {
String storedURI = this.storedURITextBox.getValue(); String storedURI = this.storedURITextBox.getValue();
RegattaDTO selectedRegatta = getSelectedRegatta(); RegattaDTO selectedRegatta = getSelectedRegatta();
RegattaIdentifier regattaIdentifier = null; RegattaIdentifier regattaIdentifier = null;
if (selectedRegatta != null) { if (selectedRegatta != null) {
regattaIdentifier = new RegattaName(selectedRegatta.name); regattaIdentifier = new RegattaName(selectedRegatta.name);
} }
// Check if the assigned regatta makes sense // Check if the assigned regatta makes sense
List<TracTracRaceRecordDTO> allRaces = this.raceList.getList(); List<TracTracRaceRecordDTO> allRaces = this.raceList.getList();
List<TracTracRaceRecordDTO> racesWithNotMatchingBoatClasses = new ArrayList<TracTracRaceRecordDTO>(); List<TracTracRaceRecordDTO> racesWithNotMatchingBoatClasses = new ArrayList<TracTracRaceRecordDTO>();
@@ -631,33 +626,27 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel {
for (TracTracRaceRecordDTO race : allRaces) { for (TracTracRaceRecordDTO race : allRaces) {
if (selectionModel.isSelected(race)) { if (selectionModel.isSelected(race)) {
if(!checkBoatClassMatch(race, selectedRegatta)) if (!checkBoatClassMatch(race, selectedRegatta))
racesWithNotMatchingBoatClasses.add(race); racesWithNotMatchingBoatClasses.add(race);
} }
} }
if (racesWithNotMatchingBoatClasses.size() > 0) {
if(racesWithNotMatchingBoatClasses.size() > 0) {
StringBuilder builder = new StringBuilder(100 + racesWithNotMatchingBoatClasses.size() * 30); StringBuilder builder = new StringBuilder(100 + racesWithNotMatchingBoatClasses.size() * 30);
builder.append("WARNING\n"); builder.append("WARNING\n");
if (selectedRegatta != null) {
if(selectedRegatta != null) {
builder.append(this.stringMessages.boatClassDoesNotMatchSelectedRegatta(selectedRegatta.boatClass.name, builder.append(this.stringMessages.boatClassDoesNotMatchSelectedRegatta(selectedRegatta.boatClass.name,
selectedRegatta.name)); selectedRegatta.name));
} else { } else {
builder.append(this.stringMessages.regattaExistForSelectedBoatClass()); builder.append(this.stringMessages.regattaExistForSelectedBoatClass());
} }
builder.append("\n\n"); builder.append("\n\n");
builder.append(this.stringMessages.races()); builder.append(this.stringMessages.races());
builder.append("\n"); builder.append("\n");
for (TracTracRaceRecordDTO record: racesWithNotMatchingBoatClasses) {
for(TracTracRaceRecordDTO record: racesWithNotMatchingBoatClasses) {
builder.append(record.name); builder.append(record.name);
builder.append("\n"); builder.append("\n");
} }
if (!Window.confirm(builder.toString())) {
if(!Window.confirm(builder.toString())) {
return; return;
} }
} }
@@ -109,7 +109,7 @@ public class WindPanel extends FormPanel implements RegattaDisplayer, WindShower
RegattaAndRaceIdentifier selectedRace = getSelectedRace(); RegattaAndRaceIdentifier selectedRace = getSelectedRace();
if(selectedRace != null) { if(selectedRace != null) {
final RaceDTO race = trackedRacesListComposite.getRaceByIdentifier(selectedRace); final RaceDTO race = trackedRacesListComposite.getRaceByIdentifier(selectedRace);
sailingService.getCoursePositions(selectedRace, race.startOfRace, new AsyncCallback<CoursePositionsDTO>() { sailingService.getCoursePositions(selectedRace, race.trackedRace.startOfTracking, new AsyncCallback<CoursePositionsDTO>() {
@Override @Override
public void onFailure(Throwable caught) { public void onFailure(Throwable caught) {
showWindSettingDialog(race, null); showWindSettingDialog(race, null);
@@ -215,7 +215,7 @@ public class WindPanel extends FormPanel implements RegattaDisplayer, WindShower
} }
private void showWindSettingDialog(RaceDTO race, CoursePositionsDTO course) { private void showWindSettingDialog(RaceDTO race, CoursePositionsDTO course) {
WindSettingDialog windSettingDialog = new WindSettingDialog(race, course, stringMessages, AddWindFixDialog windSettingDialog = new AddWindFixDialog(race, course, stringMessages,
new DialogCallback<WindDTO>() { new DialogCallback<WindDTO>() {
@Override @Override
public void cancel() { public void cancel() {
@@ -88,9 +88,11 @@ public abstract class DataEntryDialog<T> {
this.validator = validator; this.validator = validator;
okButton = new Button(okButtonName); okButton = new Button(okButtonName);
okButton.getElement().getStyle().setMargin(3, Unit.PX); okButton.getElement().getStyle().setMargin(3, Unit.PX);
okButton.ensureDebugId("OkButton");
FlowPanel dialogFPanel = new FlowPanel(); FlowPanel dialogFPanel = new FlowPanel();
dialogFPanel.setWidth("100%"); dialogFPanel.setWidth("100%");
statusLabel = new Label(); statusLabel = new Label();
statusLabel.ensureDebugId("StatusLabel");
dialogFPanel.add(statusLabel); dialogFPanel.add(statusLabel);
if (message != null) { if (message != null) {
Label messageLabel = new Label(message); Label messageLabel = new Label(message);
@@ -107,6 +109,7 @@ public abstract class DataEntryDialog<T> {
buttonPanel.add(okButton); buttonPanel.add(okButton);
cancelButton = new Button(cancelButtonName); cancelButton = new Button(cancelButtonName);
cancelButton.getElement().getStyle().setMargin(3, Unit.PX); cancelButton.getElement().getStyle().setMargin(3, Unit.PX);
cancelButton.ensureDebugId("CancelButton");
buttonPanel.add(cancelButton); buttonPanel.add(cancelButton);
cancelButton.addClickHandler(new ClickHandler() { cancelButton.addClickHandler(new ClickHandler() {
@Override @Override
@@ -238,19 +241,20 @@ public abstract class DataEntryDialog<T> {
} }
private DateBox createDateBoxInternal(Date initialDate, int visibleLength) { private DateBox createDateBoxInternal(Date initialDate, int visibleLength) {
DateBox textBox = new DateBox(); DateBox dateBox = new DateBox();
textBox.getTextBox().setVisibleLength(visibleLength); dateBox.getTextBox().setVisibleLength(visibleLength);
textBox.setValue(initialDate); dateBox.setFireNullValues(true);
AbstractEntryPoint.addFocusUponKeyUpToggler(textBox.getTextBox()); dateBox.setValue(initialDate);
textBox.getTextBox().addChangeHandler(new ChangeHandler() { AbstractEntryPoint.addFocusUponKeyUpToggler(dateBox.getTextBox());
dateBox.addValueChangeHandler(new ValueChangeHandler<Date>() {
@Override @Override
public void onChange(ChangeEvent event) { public void onValueChange(ValueChangeEvent<Date> event) {
validate(); validate();
} }
}); });
AbstractEntryPoint.linkEnterToButton(getOkButton(), textBox.getTextBox()); AbstractEntryPoint.linkEnterToButton(getOkButton(), dateBox.getTextBox());
AbstractEntryPoint.linkEscapeToButton(getCancelButton(), textBox.getTextBox()); AbstractEntryPoint.linkEscapeToButton(getCancelButton(), dateBox.getTextBox());
return textBox; return dateBox;
} }
/** /**
@@ -405,4 +409,7 @@ public abstract class DataEntryDialog<T> {
dateEntryDialog.center(); 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 // in case the race is not tracked anymore we reset the time slider
resetTimeSlider(); resetTimeSlider();
} else { } else {
// check if the live delay is already been set // timer will only accept this update if the delay hasn't been updated explicitly
if (!isUserExplicitlyChangedLivePlayDelay() && timer.getLivePlayDelayInMillis() != raceTimesInfo.delayToLiveInMs) { timer.setLivePlayDelayInMillis(raceTimesInfo.delayToLiveInMs);
timer.setLivePlayDelayInMillis(raceTimesInfo.delayToLiveInMs);
}
if ((raceTimesInfo.startOfTracking != null || raceTimesInfo.startOfRace != null) && if ((raceTimesInfo.startOfTracking != null || raceTimesInfo.startOfRace != null) &&
(raceTimesInfo.newestTrackingEvent != null || raceTimesInfo.endOfRace != 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 // we set here the min and max of the time slider, the start and end of the race as well as the known
@@ -108,14 +105,15 @@ public class RaceTimePanel extends TimePanel<RaceTimePanelSettings> implements R
@Override @Override
protected boolean isLiveModeToBeMadePossible() { protected boolean isLiveModeToBeMadePossible() {
long livePlayDelayInMillis = timer.getLivePlayDelayInMillis(); long livePlayDelayInMillis = timer.getLivePlayDelayInMillis();
long eventTimeoutTolerance = 60 * 1000; // 60s long eventTimeoutTolerance = 60 * 1000; // 60s
long timeBeforeRaceStartTolerance = 3 * 60 * 1000; // 3min
long liveTimePointInMillis = System.currentTimeMillis() - livePlayDelayInMillis; long liveTimePointInMillis = System.currentTimeMillis() - livePlayDelayInMillis;
RaceTimesInfoDTO lastRaceTimesInfo = raceTimesInfoProvider != null ? raceTimesInfoProvider.getRaceTimesInfo(selectedRace) : null; RaceTimesInfoDTO lastRaceTimesInfo = raceTimesInfoProvider != null ? raceTimesInfoProvider.getRaceTimesInfo(selectedRace) : null;
return lastRaceTimesInfo != null && return lastRaceTimesInfo != null &&
lastRaceTimesInfo.newestTrackingEvent != null && lastRaceTimesInfo.newestTrackingEvent != null &&
liveTimePointInMillis < lastRaceTimesInfo.newestTrackingEvent.getTime() + eventTimeoutTolerance && liveTimePointInMillis < lastRaceTimesInfo.newestTrackingEvent.getTime() + eventTimeoutTolerance &&
lastRaceTimesInfo.startOfTracking != null && ((lastRaceTimesInfo.startOfTracking != null && liveTimePointInMillis > lastRaceTimesInfo.startOfTracking.getTime())||
liveTimePointInMillis > lastRaceTimesInfo.startOfTracking.getTime(); (lastRaceTimesInfo.startOfRace != null && liveTimePointInMillis > lastRaceTimesInfo.startOfRace.getTime() - timeBeforeRaceStartTolerance));
} }
@Override @Override
@@ -367,6 +367,7 @@ public interface StringMessages extends Messages {
String insertWaypointAfterSelected(); String insertWaypointAfterSelected();
String scoringSchemeLowPointSystem(); String scoringSchemeLowPointSystem();
String scoringSchemeHighPointSystem(); String scoringSchemeHighPointSystem();
String scoringSchemeLowPointWinnerGetsZero();
String scoringSystem(); String scoringSystem();
String createFlexibleLeaderboard(); String createFlexibleLeaderboard();
String createRegattaLeaderboard(); String createRegattaLeaderboard();
@@ -520,4 +521,7 @@ public interface StringMessages extends Messages {
String correctScoreFor(String competitorName, String raceColumnName); String correctScoreFor(String competitorName, String raceColumnName);
String penaltyOrRedress(); String penaltyOrRedress();
String netScore(); String netScore();
String timePointMustBeAfterStartOfTracking();
String timePointMustBeBeforeEndOfTracking();
String regattaUsedForTheTrackedRace();
} }
@@ -395,6 +395,7 @@ scoringSchemeLowPointSystem=Low Point System
scoringSchemeHighPointSystem=High Point System scoringSchemeHighPointSystem=High Point System
scoringSchemeHighPointEssOverall=High Point System ESS Overall scoringSchemeHighPointEssOverall=High Point System ESS Overall
scoringSchemeHighPointLastBreaksTie=High Point System, Last Race Breaks Tie scoringSchemeHighPointLastBreaksTie=High Point System, Last Race Breaks Tie
scoringSchemeLowPointWinnerGetsZero=Low Point System, Winner Gets Zero
scoringSystem=Scoring system scoringSystem=Scoring system
createFlexibleLeaderboard=Create flexible leaderboard createFlexibleLeaderboard=Create flexible leaderboard
createRegattaLeaderboard=Create regatta leaderboard createRegattaLeaderboard=Create regatta leaderboard
@@ -521,4 +522,7 @@ cannotAddRacesToRegattaLeaderboardButOnlyToRegatta=This is a regatta leaderboard
correctScore=Correct Score correctScore=Correct Score
correctScoreFor=Correct score for competitor {0} in race {1} correctScoreFor=Correct score for competitor {0} in race {1}
penaltyOrRedress=Penalty/Redress penaltyOrRedress=Penalty/Redress
netScore=Net points 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 scoringSchemeHighPointSystem=High Point System
scoringSchemeHighPointEssOverall=High Point System ESS Gesamtwertung scoringSchemeHighPointEssOverall=High Point System ESS Gesamtwertung
scoringSchemeHighPointLastBreaksTie=High Point System, letztes Rennen entscheidet bei Gleichstand scoringSchemeHighPointLastBreaksTie=High Point System, letztes Rennen entscheidet bei Gleichstand
scoringSchemeLowPointWinnerGetsZero=Low Point System, Gewinner erhält 0 Punkte
scoringSystem=Bepunktungssystem scoringSystem=Bepunktungssystem
createFlexibleLeaderboard=Flexible Rangliste anlegen createFlexibleLeaderboard=Flexible Rangliste anlegen
createRegattaLeaderboard=Rangliste für Regatta anlegen createRegattaLeaderboard=Rangliste für Regatta anlegen
@@ -521,4 +522,7 @@ cannotAddRacesToRegattaLeaderboardButOnlyToRegatta=Dies ist eine Regatta-Ergebni
correctScore=Ergebnis bearbeiten correctScore=Ergebnis bearbeiten
correctScoreFor=Ergebnis für Teilnehmer {0} in Rennen {1} bearbeiten correctScoreFor=Ergebnis für Teilnehmer {0} in Rennen {1} bearbeiten
penaltyOrRedress=Strafe/Gutschrift penaltyOrRedress=Strafe/Gutschrift
netScore=Nettoergebnis 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 slowDownButton;
private final Button speedUpButton; 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 * 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.addTimeListener(this);
timer.addPlayStateListener(this); timer.addPlayStateListener(this);
timeRangeProvider.addTimeRangeChangeListener(this); timeRangeProvider.addTimeRangeChangeListener(this);
userExplicitlyChangedLivePlayDelay = false;
FlowPanel timePanelInnerWrapper = new FlowPanel(); FlowPanel timePanelInnerWrapper = new FlowPanel();
timePanelInnerWrapper.setStyleName("timePanelInnerWrapper"); timePanelInnerWrapper.setStyleName("timePanelInnerWrapper");
timePanelInnerWrapper.setSize("100%", "100%"); timePanelInnerWrapper.setSize("100%", "100%");
@@ -460,16 +452,12 @@ public class TimePanel<T extends TimePanelSettings> extends FormPanel implements
return new TimePanelSettingsDialogComponent<T>(getSettings(), stringMessages); return new TimePanelSettingsDialogComponent<T>(getSettings(), stringMessages);
} }
protected boolean isUserExplicitlyChangedLivePlayDelay() {
return userExplicitlyChangedLivePlayDelay;
}
@Override @Override
public void updateSettings(T newSettings) { public void updateSettings(T newSettings) {
boolean delayChanged = newSettings.getDelayToLivePlayInSeconds() != getSettings().getDelayToLivePlayInSeconds(); boolean delayChanged = newSettings.getDelayToLivePlayInSeconds() != getSettings().getDelayToLivePlayInSeconds();
if (delayChanged) { if (delayChanged) {
userExplicitlyChangedLivePlayDelay = true; // explicit change always goes through and disables further automatic delay updates in timer
timer.setLivePlayDelayInMillis(1000l * newSettings.getDelayToLivePlayInSeconds()); timer.setLivePlayDelayInMillisExplicitly(1000l * newSettings.getDelayToLivePlayInSeconds());
if (timer.getPlayMode() == PlayModes.Live) { if (timer.getPlayMode() == PlayModes.Live) {
timeDelayLabel.setText(String.valueOf(newSettings.getDelayToLivePlayInSeconds()) + " s"); timeDelayLabel.setText(String.valueOf(newSettings.getDelayToLivePlayInSeconds()) + " s");
} }
@@ -78,6 +78,13 @@ public class Timer {
* Set to <code>true</code> to enable the timer to advance the max time automatically if the current time gets greater than the max time * Set to <code>true</code> to enable the timer to advance the max time automatically if the current time gets greater than the max time
*/ */
private boolean autoAdvance; 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 * The timer can run in two different modes: Live and Replay
@@ -261,6 +268,21 @@ public class Timer {
} }
public void setLivePlayDelayInMillis(long delayInMilliseconds) { 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) { if (this.livePlayDelayInMillis != delayInMilliseconds) {
this.livePlayDelayInMillis = delayInMilliseconds; this.livePlayDelayInMillis = delayInMilliseconds;
if (getPlayMode() == PlayModes.Live) { if (getPlayMode() == PlayModes.Live) {
@@ -24,10 +24,13 @@ public abstract class AbstractRaceColumnSelection implements RaceColumnSelection
@Override @Override
public Iterable<RaceColumnDTO> getSelectedRaceColumnsOrderedAsInLeaderboard(LeaderboardDTO leaderboard) { 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>(); List<RaceColumnDTO> result = new ArrayList<RaceColumnDTO>();
for (RaceColumnDTO column : leaderboard.getRaceList()) { for (RaceColumnDTO column : leaderboard.getRaceList()) {
if (Util.contains(selectedColumns, column)) { if (Util.contains(namesOfSelectedRaceColumns, column.name)) {
result.add(column); result.add(column);
} }
} }
@@ -4,7 +4,6 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@@ -15,18 +14,22 @@ import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider;
import com.sap.sailing.gwt.ui.client.RaceTimesInfoProviderListener; import com.sap.sailing.gwt.ui.client.RaceTimesInfoProviderListener;
import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettings.RaceColumnSelectionStrategies; 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.FleetDTO;
import com.sap.sailing.gwt.ui.shared.LeaderboardDTO; 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.RaceColumnDTO;
import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; 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 * 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 * <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) * @author Axel Uhl (D043530)
* *
*/ */
public class LastNRacesColumnSelection extends AbstractRaceColumnSelection implements RaceTimesInfoProviderListener { public class LastNRacesColumnSelection extends AbstractRaceColumnSelection implements RaceTimesInfoProviderListener {
private final int numberOfLastRacesToShow; private final int numberOfLastRacesToShow;
@@ -87,33 +90,35 @@ public class LastNRacesColumnSelection extends AbstractRaceColumnSelection imple
this.raceTimesInfo = raceTimesInfo; this.raceTimesInfo = raceTimesInfo;
} }
private Iterable<RaceColumnDTO> getRaceColumnsFromNewestToOldest(LeaderboardDTO leaderboard, private Iterable<RaceColumnDTO> getRaceColumnsFromNewestToOldest(final LeaderboardDTO leaderboard,
Map<RegattaAndRaceIdentifier, RaceTimesInfoDTO> raceTimesInfo) { Map<RegattaAndRaceIdentifier, RaceTimesInfoDTO> raceTimesInfo) {
final Map<RaceColumnDTO, Date> latestStarts = new HashMap<RaceColumnDTO, Date>();
List<RaceColumnDTO> columns = new ArrayList<RaceColumnDTO>(leaderboard.getRaceList()); List<RaceColumnDTO> columns = new ArrayList<RaceColumnDTO>(leaderboard.getRaceList());
for (Iterator<RaceColumnDTO> i=columns.iterator(); i.hasNext(); ) { for (Iterator<RaceColumnDTO> i=columns.iterator(); i.hasNext(); ) {
RaceColumnDTO column = i.next(); RaceColumnDTO column = i.next();
Date latestStart = getLatestStart(column); if (getLatestStart(column) == null && !hasScoreCorrections(leaderboard, column)) {
if (latestStart == null) {
i.remove(); i.remove();
} else {
latestStarts.put(column, latestStart);
} }
} }
Comparator<RaceColumnDTO> comparator = new Comparator<RaceColumnDTO>() { Comparator<RaceColumnDTO> comparator = new Comparator<RaceColumnDTO>() {
@Override @Override
public int compare(RaceColumnDTO o1, RaceColumnDTO o2) { public int compare(RaceColumnDTO o1, RaceColumnDTO o2) {
int result; return leaderboard.getRaceList().indexOf(o2) - leaderboard.getRaceList().indexOf(o1);
Date latestStartO1 = latestStarts.get(o1);
Date latestStartO2 = latestStarts.get(o2);
result = latestStartO2.compareTo(latestStartO1);
return result;
} }
}; };
Collections.sort(columns, comparator); Collections.sort(columns, comparator);
return columns; 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) { private Date getLatestStart(RaceColumnDTO column) {
Date latestStart = null; Date latestStart = null;
for (FleetDTO fleet : column.getFleets()) { 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_REFRESH_INTERVAL_MILLIS = "refreshIntervalMillis";
private static final String PARAM_SHOW_OVERALL_LEADERBOARDS_ON_SAME_PAGE = "showOverallLeaderboardsOnSamePage"; private static final String PARAM_SHOW_OVERALL_LEADERBOARDS_ON_SAME_PAGE = "showOverallLeaderboardsOnSamePage";
private static final String PARAM_DELAY_TO_LIVE_MILLIS = "delayToLiveMillis"; 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"; 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); final String zoomTo = Window.Location.getParameter(PARAM_ZOOM_TO);
if (zoomTo != null) { 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 Label liveRaceLabel;
private final DateTimeFormat dateFormatter = DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.DATE_LONG); 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; private boolean isEmbedded = false;
@@ -430,7 +430,7 @@ public class LeaderboardPanel extends FormPanel implements TimeListener, PlaySta
timer.setRefreshInterval(newSettings.getDelayBetweenAutoAdvancesInMilliseconds()); timer.setRefreshInterval(newSettings.getDelayBetweenAutoAdvancesInMilliseconds());
} }
if (newSettings.getDelayInMilliseconds() != null) { if (newSettings.getDelayInMilliseconds() != null) {
setDelayInMilliseconds(newSettings.getDelayInMilliseconds()); timer.setLivePlayDelayInMillisExplicitly(newSettings.getDelayInMilliseconds());
} }
for (ExpandableSortableColumn<?> expandableSortableColumn : columnsToExpandAgain) { for (ExpandableSortableColumn<?> expandableSortableColumn : columnsToExpandAgain) {
expandableSortableColumn.toggleExpansion(); 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 * 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 * {@link RaceColumnDTO#getStartDate(FleetDTO) start time}, automatically adjusts the delay accordingly unless it
* changes} were performed, automatically adjusts the delay accordingly. * was manually explicitly updated.
*/ */
private void adjustDelayToLive() { private void adjustDelayToLive() {
if (!settingsUpdatedExplicitly && leaderboard.getDelayToLiveInMillisForLatestRace() != null) { if (leaderboard.getDelayToLiveInMillisForLatestRace() != null) {
setDelayInMilliseconds(leaderboard.getDelayToLiveInMillisForLatestRace()); setDelayInMilliseconds(leaderboard.getDelayToLiveInMillisForLatestRace());
} }
} }
@@ -4,18 +4,20 @@ import com.sap.sailing.domain.common.ScoringSchemeType;
import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.StringMessages;
public class ScoringSchemeTypeFormatter { public class ScoringSchemeTypeFormatter {
public static String format(ScoringSchemeType scoringSchemeType, StringMessages stringConstants) { public static String format(ScoringSchemeType scoringSchemeType, StringMessages stringMessages) {
switch (scoringSchemeType) { switch (scoringSchemeType) {
case LOW_POINT: case LOW_POINT:
return stringConstants.scoringSchemeLowPointSystem(); return stringMessages.scoringSchemeLowPointSystem();
case HIGH_POINT: case HIGH_POINT:
return stringConstants.scoringSchemeHighPointSystem(); return stringMessages.scoringSchemeHighPointSystem();
case HIGH_POINT_ESS_OVERALL: case HIGH_POINT_ESS_OVERALL:
return stringConstants.scoringSchemeHighPointEssOverall(); return stringMessages.scoringSchemeHighPointEssOverall();
case HIGH_POINT_LAST_BREAKS_TIE: case HIGH_POINT_LAST_BREAKS_TIE:
return stringConstants.scoringSchemeHighPointLastBreaksTie(); return stringMessages.scoringSchemeHighPointLastBreaksTie();
case HIGH_POINT_FIRST_GETS_TEN: case HIGH_POINT_FIRST_GETS_TEN:
return stringConstants.scoringSchemeHighPointFirstGetsTen(); return stringMessages.scoringSchemeHighPointFirstGetsTen();
case LOW_POINT_WINNER_GETS_ZERO:
return stringMessages.scoringSchemeLowPointWinnerGetsZero();
} }
return null; return null;
} }
@@ -60,4 +60,11 @@ public class EditScoreDialog extends DataEntryDialog<Pair<MaxPointsReason, Doubl
grid.setWidget(1, 1, netPointsBox); grid.setWidget(1, 1, netPointsBox);
return grid; 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 prefix;
String suffix; String suffix;
if (netPointsCorrected) { if (netPointsCorrected) {
prefix = "<b>"; prefix = "<b class='bold'>";
suffix = "</b>"; suffix = "</b>";
} else { } else {
prefix = ""; prefix = "";
@@ -251,12 +251,16 @@ public class MatchAndApplyScoreCorrectionsDialog extends DataEntryDialog<BulkSco
if (officialSailID != null && raceNameOrNumber != null) { if (officialSailID != null && raceNameOrNumber != null) {
ScoreCorrectionEntryDTO officialCorrectionEntry = regattaScoreCorrection ScoreCorrectionEntryDTO officialCorrectionEntry = regattaScoreCorrection
.getScoreCorrectionsByRaceNameOrNumber().get(raceNameOrNumber).get(officialSailID); .getScoreCorrectionsByRaceNameOrNumber().get(raceNameOrNumber).get(officialSailID);
result.addMaxPointsReasonUpdate(competitor, raceColumn, // the competitor may not have an official result for this race, for example, if in case of split fleets
officialCorrectionEntry.getMaxPointsReason()); // the competitor raced in a different fleet
if (officialCorrectionEntry.getScore() != null) { if (officialCorrectionEntry != null) {
double officialTotalPoints = officialCorrectionEntry.getScore().doubleValue(); result.addMaxPointsReasonUpdate(competitor, raceColumn,
double officialNetPoints = officialTotalPoints / raceColumn.getEffectiveFactor(); officialCorrectionEntry.getMaxPointsReason());
result.addScoreUpdate(competitor, raceColumn, officialNetPoints); 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 = ScoreCorrectionEntryDTO officialCorrectionEntry =
regattaScoreCorrection.getScoreCorrectionsByRaceNameOrNumber() regattaScoreCorrection.getScoreCorrectionsByRaceNameOrNumber()
.get(raceNameOrNumber).get(officialSailID); .get(raceNameOrNumber).get(officialSailID);
final Double officialTotalPoints = officialCorrectionEntry.isDiscarded() ? new Double(0) : officialCorrectionEntry.getScore(); final Double officialTotalPoints = officialCorrectionEntry == null ? null :
final Double officialNetPoints = officialCorrectionEntry.getScore() == null ? null : officialCorrectionEntry.isDiscarded() ? new Double(0) : officialCorrectionEntry.getScore();
final Double officialNetPoints = officialCorrectionEntry == null ? null :
officialCorrectionEntry.getScore() == null ? null :
officialCorrectionEntry.getScore() / raceColumn.getEffectiveFactor(); officialCorrectionEntry.getScore() / raceColumn.getEffectiveFactor();
final MaxPointsReason officialMaxPointsReason = officialCorrectionEntry == null ? null :
officialCorrectionEntry.getMaxPointsReason();
SafeHtmlBuilder sb = new SafeHtmlBuilder(); SafeHtmlBuilder sb = new SafeHtmlBuilder();
boolean entriesDiffer = // entries not considered "different' in case we haven't got an entry for the competitor/race at all;
((officialNetPoints == null && entry.netPoints != null) || (officialNetPoints != null && entry.netPoints != null && !new Double(entry.netPoints).equals(officialNetPoints))) || // those non-existing "entries" will be ignored by getResults anyhow
((officialTotalPoints == null && entry.totalPoints != null) || (officialTotalPoints != null && entry.totalPoints != null && !new Double(entry.totalPoints).equals(officialTotalPoints))) || boolean entriesDiffer = officialCorrectionEntry != null &&
((officialCorrectionEntry.getMaxPointsReason() == null && entry.reasonForMaxPoints != MaxPointsReason.NONE) || (((officialNetPoints == null && entry.netPoints != null) || (officialNetPoints != null && (entry.netPoints == null || !new Double(entry.netPoints).equals(officialNetPoints)))) ||
officialCorrectionEntry.getMaxPointsReason() != null && officialCorrectionEntry.getMaxPointsReason() != entry.reasonForMaxPoints); ((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) { if (entriesDiffer) {
sb.appendHtmlConstant("<span style=\"color: #0000FF;\"><b>"); sb.appendHtmlConstant("<span style=\"color: #0000FF;\"><b>");
} }
if (officialNetPoints == null) { if (officialNetPoints == null) {
sb.append(0); sb.appendEscaped("null");
} else { } else {
sb.append(officialNetPoints); sb.append(officialNetPoints);
} }
sb.appendEscaped("/"); sb.appendEscaped("/");
if (officialTotalPoints == null) { if (officialTotalPoints == null) {
sb.append(0); sb.appendEscaped("null");
} else { } else {
sb.append(officialTotalPoints); sb.append(officialTotalPoints);
} }
sb.appendEscaped("/"); sb.appendEscaped("/");
if (officialCorrectionEntry.getMaxPointsReason() != null) { if (officialMaxPointsReason != null) {
sb.appendEscaped(officialCorrectionEntry.getMaxPointsReason().name()); sb.appendEscaped(officialMaxPointsReason.name());
} else { } else {
sb.appendEscaped(MaxPointsReason.NONE.name()); sb.appendEscaped(MaxPointsReason.NONE.name());
} }
if (officialCorrectionEntry.isDiscarded()) { if (officialCorrectionEntry != null && officialCorrectionEntry.isDiscarded()) {
sb.appendEscaped("/discarded"); sb.appendEscaped("/discarded");
} }
if (entriesDiffer) { if (entriesDiffer) {
@@ -88,11 +88,11 @@ public class MediaSelector implements /*RaceTimesInfoProviderListener,*/ PlaySta
Collection<MediaTrack> reachableVideoTracks = new ArrayList<MediaTrack>(); Collection<MediaTrack> reachableVideoTracks = new ArrayList<MediaTrack>();
Collection<MediaTrack> reachableAudioTracks = new ArrayList<MediaTrack>(); Collection<MediaTrack> reachableAudioTracks = new ArrayList<MediaTrack>();
for (MediaTrack mediaTrack : mediaTracks) { for (MediaTrack mediaTrack : mediaTracks) {
if (MediaTrack.Status.REACHABLE.equals(mediaTrack.status)) { if (isPotentiallyPlayable(mediaTrack)) {
switch (mediaTrack.mimeType.mediaType) { switch (mediaTrack.mimeType.mediaType) {
case video: case video:
reachableVideoTracks.add(mediaTrack); reachableVideoTracks.add(mediaTrack);
case audio: //intentionall fall through case audio: //intentional fall through
reachableAudioTracks.add(mediaTrack); 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) { private void setStatus(final MediaTrack mediaTrack) {
if (!mediaTrack.isYoutube()) { if (!mediaTrack.isYoutube()) {
Audio audio = Audio.createIfSupported(); Audio audio = Audio.createIfSupported();
if (audio != null) { if (audio != null) {
AudioElement mediaReachableTester = audio.getAudioElement(); AudioElement mediaReachableTester = audio.getAudioElement();
String canPlay = mediaReachableTester.canPlayType(mediaTrack.typeToString()); addLoadMetadataHandler(mediaReachableTester, mediaTrack);
if (MediaElement.CAN_PLAY_PROBABLY.equals(canPlay) || MediaElement.CAN_PLAY_MAYBE.equals(canPlay)) { mediaReachableTester.setPreload(MediaElement.PRELOAD_METADATA);
addLoadMetadataHandler(mediaReachableTester, mediaTrack); mediaReachableTester.setSrc(mediaTrack.url);
mediaReachableTester.setPreload(MediaElement.PRELOAD_METADATA); mediaReachableTester.load();
mediaReachableTester.setSrc(mediaTrack.url);
mediaReachableTester.load();
} else {
mediaTrack.status = Status.CANNOT_PLAY;
}
} else { } else {
mediaTrack.status = Status.CANNOT_PLAY; mediaTrack.status = Status.CANNOT_PLAY;
} }
@@ -156,11 +155,18 @@ public class MediaSelector implements /*RaceTimesInfoProviderListener,*/ PlaySta
mediaElement.addEventListener('loadedmetadata', function() { mediaElement.addEventListener('loadedmetadata', function() {
that.@com.sap.sailing.gwt.ui.raceboard.MediaSelector::loadedmetadata(Lcom/sap/sailing/gwt/ui/shared/media/MediaTrack;)(mediaTrack); 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) { public void loadedmetadata(MediaTrack mediaTrack) {
mediaTrack.status = Status.REACHABLE; mediaTrack.status = Status.REACHABLE;
} }
public void mediaError(MediaTrack mediaTrack) {
mediaTrack.status = Status.NOT_REACHABLE;
}
private void playDefault() { private void playDefault() {
MediaTrack defaultVideo = getDefaultVideo(); MediaTrack defaultVideo = getDefaultVideo();
@@ -176,7 +182,7 @@ public class MediaSelector implements /*RaceTimesInfoProviderListener,*/ PlaySta
private MediaTrack getDefaultAudio() { private MediaTrack getDefaultAudio() {
//TODO: implement a better heuristic than just taking the first to come //TODO: implement a better heuristic than just taking the first to come
for (MediaTrack mediaTrack : mediaTracks) { for (MediaTrack mediaTrack : mediaTracks) {
if (MediaType.audio.equals(mediaTrack.mimeType.mediaType)) { if (MediaType.audio.equals(mediaTrack.mimeType.mediaType) && isPotentiallyPlayable(mediaTrack)) {
return mediaTrack; return mediaTrack;
} }
} }
@@ -185,7 +191,7 @@ public class MediaSelector implements /*RaceTimesInfoProviderListener,*/ PlaySta
private MediaTrack getDefaultVideo() { private MediaTrack getDefaultVideo() {
for (MediaTrack mediaTrack : mediaTracks) { for (MediaTrack mediaTrack : mediaTracks) {
if (MediaType.video.equals(mediaTrack.mimeType.mediaType)) { if (MediaType.video.equals(mediaTrack.mimeType.mediaType) && isPotentiallyPlayable(mediaTrack)) {
return 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.TrackedRaceStatus;
import com.sap.sailing.domain.tracking.Wind; import com.sap.sailing.domain.tracking.Wind;
import com.sap.sailing.gwt.ui.shared.LeaderboardDTO; 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 * Caches the expensive to compute {@link LeaderboardDTO} results of a
@@ -402,12 +403,21 @@ public class LeaderboardDTOCache {
future = map.get(key); future = map.get(key);
} }
if (future == null) { if (future == null) {
final Thread callerThread = Thread.currentThread();
future = new FutureTask<LeaderboardDTO>(new Callable<LeaderboardDTO>() { future = new FutureTask<LeaderboardDTO>(new Callable<LeaderboardDTO>() {
@Override @Override
public LeaderboardDTO call() throws Exception { public LeaderboardDTO call() throws Exception {
LeaderboardDTO result = sailingService.computeLeaderboardByName(leaderboard, adjustedTimePoint, // The outer getLeaderboardByName(...) method will always wait for this future's completion.
namesOfRaceColumnsForWhichToLoadLegDetails, waitForLatestAnalyses); // Therefore, it's safe to propagate the calling thread's locks to this one:
return result; LockUtil.propagateLockSetFrom(callerThread);
try {
LeaderboardDTO result = sailingService.computeLeaderboardByName(leaderboard,
adjustedTimePoint, namesOfRaceColumnsForWhichToLoadLegDetails,
waitForLatestAnalyses);
return result;
} finally {
LockUtil.unpropagateLockSetFrom(callerThread);
}
} }
}); });
computeLeadearboardByNameExecutor.execute(future); 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.CountryCode;
import com.sap.sailing.domain.common.DetailType; import com.sap.sailing.domain.common.DetailType;
import com.sap.sailing.domain.common.Distance; 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.LegType;
import com.sap.sailing.domain.common.ManeuverType; import com.sap.sailing.domain.common.ManeuverType;
import com.sap.sailing.domain.common.MaxPointsReason; import com.sap.sailing.domain.common.MaxPointsReason;
@@ -347,7 +348,11 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
/* workQueue */ new LinkedBlockingQueue<Runnable>()); /* workQueue */ new LinkedBlockingQueue<Runnable>());
raceDetailsExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); raceDetailsExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
leaderboardByNameLiveUpdaters = new HashMap<String, LiveLeaderboardUpdater>(); 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 { private void writeObject(ObjectOutputStream oos) throws IOException {
@@ -509,7 +514,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
RegattaAndRaceIdentifier raceIdentifier = null; RegattaAndRaceIdentifier raceIdentifier = null;
RaceDTO race = null; RaceDTO race = null;
TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); TrackedRace trackedRace = raceColumn.getTrackedRace(fleet);
final FleetDTO fleetDTO = convertToFleetDTO(fleet);
final FleetDTO fleetDTO = convertToFleetDTO(raceColumn, fleet);
if (trackedRace != null) { if (trackedRace != null) {
raceIdentifier = new RegattaNameAndRaceName(trackedRace.getTrackedRegatta().getRegatta() raceIdentifier = new RegattaNameAndRaceName(trackedRace.getTrackedRegatta().getRegatta()
.getName(), trackedRace.getRace().getName()); .getName(), trackedRace.getRace().getName());
@@ -577,7 +583,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
public LeaderboardEntryDTO call() { public LeaderboardEntryDTO call() {
try { try {
Entry entry = leaderboard.getEntry(competitor, raceColumn, timePoint); Entry entry = leaderboard.getEntry(competitor, raceColumn, timePoint);
return getLeaderboardEntryDTO(entry, raceColumn.getTrackedRace(competitor), competitor, timePoint, return getLeaderboardEntryDTO(entry, raceColumn, competitor, timePoint,
namesOfRaceColumnsForWhichToLoadLegDetails != null namesOfRaceColumnsForWhichToLoadLegDetails != null
&& namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn.getName()), && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn.getName()),
waitForLatestAnalyses, legRanksCache); 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"); (System.currentTimeMillis()-startOfRequestHandling)+"ms");
return result; 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 * may not reflect all data already received; otherwise, the method will always block for the latest
* cache updates to have happened before returning. * 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, TimePoint timePoint, boolean addLegDetails, boolean waitForLatestAnalyses,
Map<Leg, LinkedHashMap<Competitor, Integer>> legRanksCache) throws NoWindException { Map<Leg, LinkedHashMap<Competitor, Integer>> legRanksCache) throws NoWindException {
LeaderboardEntryDTO entryDTO = new LeaderboardEntryDTO(); LeaderboardEntryDTO entryDTO = new LeaderboardEntryDTO();
TrackedRace trackedRace = raceColumn.getTrackedRace(competitor);
entryDTO.race = trackedRace == null ? null : trackedRace.getRaceIdentifier(); entryDTO.race = trackedRace == null ? null : trackedRace.getRaceIdentifier();
entryDTO.netPoints = entry.getNetPoints(); entryDTO.netPoints = entry.getNetPoints();
entryDTO.netPointsCorrected = entry.isNetPointsCorrected(); entryDTO.netPointsCorrected = entry.isNetPointsCorrected();
@@ -671,7 +678,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
} }
} }
final Fleet fleet = entry.getFleet(); final Fleet fleet = entry.getFleet();
entryDTO.fleet = fleet == null ? null : convertToFleetDTO(fleet); entryDTO.fleet = fleet == null ? null : convertToFleetDTO(raceColumn, fleet);
return entryDTO; return entryDTO;
} }
@@ -1068,7 +1075,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
private SeriesDTO convertToSeriesDTO(Series series) { private SeriesDTO convertToSeriesDTO(Series series) {
List<FleetDTO> fleets = new ArrayList<FleetDTO>(); List<FleetDTO> fleets = new ArrayList<FleetDTO>();
for (Fleet fleet : series.getFleets()) { for (Fleet fleet : series.getFleets()) {
fleets.add(convertToFleetDTO(fleet)); fleets.add(convertToFleetDTO(series, fleet));
} }
List<RaceColumnDTO> raceColumns = new ArrayList<RaceColumnDTO>(); List<RaceColumnDTO> raceColumns = new ArrayList<RaceColumnDTO>();
for (RaceColumnInSeries raceColumn : series.getRaceColumns()) { for (RaceColumnInSeries raceColumn : series.getRaceColumns()) {
@@ -1082,8 +1089,16 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
return result; return result;
} }
private FleetDTO convertToFleetDTO(Fleet fleet) { private FleetDTO convertToFleetDTO(Series series, Fleet fleet) {
return new FleetDTO(fleet.getName(), fleet.getOrdering(), fleet.getColor()); 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) { private List<RaceWithCompetitorsDTO> convertToRaceDTOs(Regatta regatta) {
@@ -2132,7 +2147,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive = liveTimePointForTrackedRace; latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive = liveTimePointForTrackedRace;
} }
} }
final FleetDTO fleetDTO = convertToFleetDTO(fleet); final FleetDTO fleetDTO = convertToFleetDTO(raceColumn, fleet);
RaceColumnDTO raceColumnDTO = leaderboardDTO.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), raceColumn.getFactor(), RaceColumnDTO raceColumnDTO = leaderboardDTO.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), raceColumn.getFactor(),
fleetDTO, raceColumn.isMedalRace(), raceIdentifier, raceDTO); fleetDTO, raceColumn.isMedalRace(), raceIdentifier, raceDTO);
if (latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive != null) { if (latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive != null) {
@@ -2407,7 +2422,11 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
throws UnknownHostException, IOException, InterruptedException, ParseException { throws UnknownHostException, IOException, InterruptedException, ParseException {
List<SwissTimingRaceRecordDTO> result = new ArrayList<SwissTimingRaceRecordDTO>(); List<SwissTimingRaceRecordDTO> result = new ArrayList<SwissTimingRaceRecordDTO>();
for (com.sap.sailing.domain.swisstimingadapter.RaceRecord rr : getService().getSwissTimingRaceRecords(hostname, port, canSendRequests)) { 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; 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, public void trackWithSwissTiming(RegattaIdentifier regattaToAddTo, Iterable<SwissTimingRaceRecordDTO> rrs, String hostname, int port,
boolean canSendRequests, boolean trackWind, final boolean correctWindByDeclination) throws Exception { boolean canSendRequests, boolean trackWind, final boolean correctWindByDeclination) throws Exception {
for (SwissTimingRaceRecordDTO rr : rrs) { for (SwissTimingRaceRecordDTO rr : rrs) {
final RacesHandle raceHandle = getService().addSwissTimingRace(regattaToAddTo, rr.ID, hostname, port, final RacesHandle raceHandle = getService().addSwissTimingRace(regattaToAddTo, rr.ID, hostname, port,
canSendRequests, canSendRequests,
MongoWindStoreFactory.INSTANCE.getMongoWindStore(mongoObjectFactory, domainObjectFactory), MongoWindStoreFactory.INSTANCE.getMongoWindStore(mongoObjectFactory, domainObjectFactory),
@@ -2476,11 +2494,11 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
regatta = getService().createRegatta( regatta = getService().createRegatta(
replayRaceDTO.rsc, replayRaceDTO.rsc,
boatClass.trim(), boatClass.trim(),
RegattaImpl.getFullName(replayRaceDTO.rsc, replayRaceDTO.boat_class), RegattaImpl.getDefaultName(replayRaceDTO.rsc, replayRaceDTO.boat_class),
Collections.singletonList(new SeriesImpl( Collections.singletonList(new SeriesImpl(
"Default", LeaderboardNameConstants.DEFAULT_SERIES_NAME,
/* isMedal */false, /* isMedal */false,
Collections.singletonList(new FleetImpl("Default")), Collections.singletonList(new FleetImpl(LeaderboardNameConstants.DEFAULT_FLEET_NAME)),
/* race column names */ new ArrayList<String>(), getService())), /* race column names */ new ArrayList<String>(), getService())),
false, false,
baseDomainFactory.createScoringScheme(ScoringSchemeType.LOW_POINT), null); baseDomainFactory.createScoringScheme(ScoringSchemeType.LOW_POINT), null);
@@ -7,10 +7,14 @@ public class FleetDTO extends NamedDTO implements IsSerializable {
private Color color; private Color color;
private int orderNo; private int orderNo;
/** the name of the series the fleet belongs to */
private String seriesName;
public FleetDTO() {} public FleetDTO() {}
public FleetDTO(String fleetName, int orderNo, Color color) { public FleetDTO(String fleetName, String seriesName, int orderNo, Color color) {
super(fleetName); super(fleetName);
this.seriesName = seriesName;
this.orderNo = orderNo; this.orderNo = orderNo;
this.color = color; this.color = color;
} }
@@ -31,12 +35,17 @@ public class FleetDTO extends NamedDTO implements IsSerializable {
this.orderNo = orderNo; this.orderNo = orderNo;
} }
public String getSeriesName() {
return seriesName;
}
@Override @Override
public int hashCode() { public int hashCode() {
final int prime = 31; final int prime = 31;
int result = super.hashCode(); 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 + orderNo;
result = prime * result + ((seriesName == null) ? 0 : seriesName.hashCode());
return result; return result;
} }
@@ -49,13 +58,19 @@ public class FleetDTO extends NamedDTO implements IsSerializable {
if (getClass() != obj.getClass()) if (getClass() != obj.getClass())
return false; return false;
FleetDTO other = (FleetDTO) obj; FleetDTO other = (FleetDTO) obj;
if (color == null) { if (name == null) {
if (other.color != null) if (other.name != null)
return false; return false;
} else if (!color.equals(other.color)) } else if (!name.equals(other.name))
return false; return false;
if (orderNo != other.orderNo) if (orderNo != other.orderNo)
return false; return false;
if (seriesName == null) {
if (other.seriesName != null)
return false;
} else if (!seriesName.equals(other.seriesName))
return false;
return true; return true;
} }
} }
Loaded 100 of 360 files, more files were not shown because too many files have changed in this diff. Show more