mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-08 21:30:57 +00:00
1222 lines
58 KiB
Java
Executable File
1222 lines
58 KiB
Java
Executable File
package com.sap.sailing.server.impl;
|
|
|
|
import java.io.IOException;
|
|
import java.io.ObjectInputStream;
|
|
import java.io.ObjectOutputStream;
|
|
import java.io.Serializable;
|
|
import java.net.MalformedURLException;
|
|
import java.net.SocketException;
|
|
import java.net.URI;
|
|
import java.net.URISyntaxException;
|
|
import java.net.URL;
|
|
import java.net.UnknownHostException;
|
|
import java.text.ParseException;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.ScheduledExecutorService;
|
|
import java.util.concurrent.ScheduledFuture;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
|
|
import com.sap.sailing.domain.base.Buoy;
|
|
import com.sap.sailing.domain.base.Competitor;
|
|
import com.sap.sailing.domain.base.ControlPoint;
|
|
import com.sap.sailing.domain.base.Fleet;
|
|
import com.sap.sailing.domain.base.RaceColumn;
|
|
import com.sap.sailing.domain.base.RaceDefinition;
|
|
import com.sap.sailing.domain.base.Regatta;
|
|
import com.sap.sailing.domain.base.RegattaListener;
|
|
import com.sap.sailing.domain.base.Waypoint;
|
|
import com.sap.sailing.domain.base.impl.RegattaImpl;
|
|
import com.sap.sailing.domain.common.DefaultLeaderboardName;
|
|
import com.sap.sailing.domain.common.RaceIdentifier;
|
|
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
|
|
import com.sap.sailing.domain.common.RegattaIdentifier;
|
|
import com.sap.sailing.domain.common.RegattaName;
|
|
import com.sap.sailing.domain.common.TimePoint;
|
|
import com.sap.sailing.domain.common.WindSource;
|
|
import com.sap.sailing.domain.common.impl.Util;
|
|
import com.sap.sailing.domain.common.impl.Util.Pair;
|
|
import com.sap.sailing.domain.common.impl.Util.Triple;
|
|
import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard;
|
|
import com.sap.sailing.domain.leaderboard.Leaderboard;
|
|
import com.sap.sailing.domain.leaderboard.LeaderboardGroup;
|
|
import com.sap.sailing.domain.leaderboard.impl.FlexibleLeaderboardImpl;
|
|
import com.sap.sailing.domain.leaderboard.impl.LeaderboardGroupImpl;
|
|
import com.sap.sailing.domain.leaderboard.impl.LowerScoreIsBetter;
|
|
import com.sap.sailing.domain.leaderboard.impl.ResultDiscardingRuleImpl;
|
|
import com.sap.sailing.domain.leaderboard.impl.ScoreCorrectionImpl;
|
|
import com.sap.sailing.domain.persistence.DomainObjectFactory;
|
|
import com.sap.sailing.domain.persistence.MongoFactory;
|
|
import com.sap.sailing.domain.persistence.MongoObjectFactory;
|
|
import com.sap.sailing.domain.swisstimingadapter.Race;
|
|
import com.sap.sailing.domain.swisstimingadapter.SailMasterConnector;
|
|
import com.sap.sailing.domain.swisstimingadapter.SailMasterMessage;
|
|
import com.sap.sailing.domain.swisstimingadapter.SwissTimingFactory;
|
|
import com.sap.sailing.domain.swisstimingadapter.persistence.SwissTimingAdapterPersistence;
|
|
import com.sap.sailing.domain.tracking.DynamicTrackedRegatta;
|
|
import com.sap.sailing.domain.tracking.GPSFix;
|
|
import com.sap.sailing.domain.tracking.GPSFixMoving;
|
|
import com.sap.sailing.domain.tracking.MarkPassing;
|
|
import com.sap.sailing.domain.tracking.RaceChangeListener;
|
|
import com.sap.sailing.domain.tracking.RaceListener;
|
|
import com.sap.sailing.domain.tracking.RaceTracker;
|
|
import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters;
|
|
import com.sap.sailing.domain.tracking.RacesHandle;
|
|
import com.sap.sailing.domain.tracking.TrackedRace;
|
|
import com.sap.sailing.domain.tracking.TrackedRegatta;
|
|
import com.sap.sailing.domain.tracking.Wind;
|
|
import com.sap.sailing.domain.tracking.WindStore;
|
|
import com.sap.sailing.domain.tracking.WindTracker;
|
|
import com.sap.sailing.domain.tracking.impl.DynamicTrackedRegattaImpl;
|
|
import com.sap.sailing.domain.tractracadapter.DomainFactory;
|
|
import com.sap.sailing.domain.tractracadapter.JSONService;
|
|
import com.sap.sailing.domain.tractracadapter.RaceRecord;
|
|
import com.sap.sailing.domain.tractracadapter.Receiver;
|
|
import com.sap.sailing.expeditionconnector.ExpeditionListener;
|
|
import com.sap.sailing.expeditionconnector.ExpeditionWindTrackerFactory;
|
|
import com.sap.sailing.expeditionconnector.UDPExpeditionReceiver;
|
|
import com.sap.sailing.mongodb.MongoDBService;
|
|
import com.sap.sailing.operationaltransformation.Operation;
|
|
import com.sap.sailing.server.OperationExecutionListener;
|
|
import com.sap.sailing.server.RacingEventService;
|
|
import com.sap.sailing.server.RacingEventServiceOperation;
|
|
import com.sap.sailing.server.operationaltransformation.AddRaceDefinition;
|
|
import com.sap.sailing.server.operationaltransformation.AddRegatta;
|
|
import com.sap.sailing.server.operationaltransformation.ConnectTrackedRaceToLeaderboardColumn;
|
|
import com.sap.sailing.server.operationaltransformation.CreateTrackedRace;
|
|
import com.sap.sailing.server.operationaltransformation.RecordBuoyGPSFix;
|
|
import com.sap.sailing.server.operationaltransformation.RecordCompetitorGPSFix;
|
|
import com.sap.sailing.server.operationaltransformation.RecordWindFix;
|
|
import com.sap.sailing.server.operationaltransformation.RemoveWindFix;
|
|
import com.sap.sailing.server.operationaltransformation.TrackRegatta;
|
|
import com.sap.sailing.server.operationaltransformation.UpdateMarkPassings;
|
|
import com.sap.sailing.server.operationaltransformation.UpdateRaceDelayToLive;
|
|
import com.sap.sailing.server.operationaltransformation.UpdateRaceTimes;
|
|
import com.sap.sailing.server.operationaltransformation.UpdateWindAveragingTime;
|
|
import com.sap.sailing.server.operationaltransformation.UpdateWindSourcesToExclude;
|
|
|
|
public class RacingEventServiceImpl implements RacingEventService, RegattaListener {
|
|
private static final Logger logger = Logger.getLogger(RacingEventServiceImpl.class.getName());
|
|
|
|
/**
|
|
* A scheduler for the periodic checks of the paramURL documents for the advent of {@link ControlPoint}s
|
|
* with static position information otherwise not available through <code>MarkPassingReceiver</code>'s events.
|
|
*/
|
|
private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
|
|
|
private final DomainFactory tractracDomainFactory;
|
|
|
|
private final com.sap.sailing.domain.swisstimingadapter.DomainFactory swissTimingDomainFactory;
|
|
|
|
private final ExpeditionWindTrackerFactory windTrackerFactory;
|
|
|
|
/**
|
|
* Holds the {@link Regatta} objects for those races registered with this service. Note that there may be {@link Regatta}
|
|
* objects that exist outside this service for races not (yet) registered here.
|
|
*/
|
|
protected final Map<String, Regatta> regattasByName;
|
|
|
|
private final Map<RaceDefinition, CourseChangeReplicator> courseListeners;
|
|
|
|
protected final Map<Regatta, Set<RaceTracker>> raceTrackersByRegatta;
|
|
|
|
/**
|
|
* Remembers the trackers by paramURL/liveURI/storedURI to avoid duplication
|
|
*/
|
|
protected final Map<Object, RaceTracker> raceTrackersByID;
|
|
|
|
/**
|
|
* Leaderboards managed by this racing event service
|
|
*/
|
|
private final Map<String, Leaderboard> leaderboardsByName;
|
|
|
|
private final Map<String, LeaderboardGroup> leaderboardGroupsByName;
|
|
|
|
private Set<DynamicTrackedRegatta> regattasObservedForDefaultLeaderboard = new HashSet<DynamicTrackedRegatta>();
|
|
|
|
private final MongoObjectFactory mongoObjectFactory;
|
|
|
|
private final DomainObjectFactory domainObjectFactory;
|
|
|
|
private final SwissTimingFactory swissTimingFactory;
|
|
|
|
private final SwissTimingAdapterPersistence swissTimingAdapterPersistence;
|
|
|
|
private final Map<Regatta, DynamicTrackedRegatta> regattaTrackingCache;
|
|
|
|
private final Set<OperationExecutionListener> operationExecutionListeners;
|
|
|
|
/**
|
|
* The globally used configuration of the time delay (in milliseconds) to the 'live' timepoint used for each new tracked race.
|
|
*/
|
|
private long delayToLiveInMillis;
|
|
|
|
public RacingEventServiceImpl() {
|
|
this(MongoFactory.INSTANCE.getDefaultDomainObjectFactory(), MongoFactory.INSTANCE.getDefaultMongoObjectFactory());
|
|
}
|
|
|
|
private RacingEventServiceImpl(DomainObjectFactory domainObjectFactory, MongoObjectFactory mongoObjectFactory) {
|
|
tractracDomainFactory = DomainFactory.INSTANCE;
|
|
this.domainObjectFactory = domainObjectFactory;
|
|
this.mongoObjectFactory = mongoObjectFactory;
|
|
swissTimingFactory = SwissTimingFactory.INSTANCE;
|
|
swissTimingDomainFactory = com.sap.sailing.domain.swisstimingadapter.DomainFactory.INSTANCE;
|
|
swissTimingAdapterPersistence = SwissTimingAdapterPersistence.INSTANCE;
|
|
windTrackerFactory = ExpeditionWindTrackerFactory.getInstance();
|
|
regattasByName = new HashMap<String, Regatta>();
|
|
regattaTrackingCache = new HashMap<Regatta, DynamicTrackedRegatta>();
|
|
raceTrackersByRegatta = new HashMap<Regatta, Set<RaceTracker>>();
|
|
raceTrackersByID = new HashMap<Object, RaceTracker>();
|
|
leaderboardGroupsByName = new HashMap<String, LeaderboardGroup>();
|
|
leaderboardsByName = new HashMap<String, Leaderboard>();
|
|
operationExecutionListeners = new HashSet<OperationExecutionListener>();
|
|
courseListeners = new HashMap<RaceDefinition, CourseChangeReplicator>();
|
|
delayToLiveInMillis = TrackedRace.DEFAULT_LIVE_DELAY_IN_MILLISECONDS;
|
|
// Add one default leaderboard that aggregates all races currently tracked by this service.
|
|
// This is more for debugging purposes than for anything else.
|
|
addFlexibleLeaderboard(DefaultLeaderboardName.DEFAULT_LEADERBOARD_NAME, new int[] { 5, 8 });
|
|
loadStoredLeaderboardsAndGroups();
|
|
}
|
|
|
|
public RacingEventServiceImpl(MongoDBService mongoDBService) {
|
|
this(MongoFactory.INSTANCE.getDomainObjectFactory(mongoDBService), MongoFactory.INSTANCE.getMongoObjectFactory(mongoDBService));
|
|
}
|
|
|
|
private void loadStoredLeaderboardsAndGroups() {
|
|
// Loading all leaderboard groups and putting the contained leaderboards
|
|
for (LeaderboardGroup leaderboardGroup : domainObjectFactory.getAllLeaderboardGroups()) {
|
|
leaderboardGroupsByName.put(leaderboardGroup.getName(), leaderboardGroup);
|
|
for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) {
|
|
leaderboardsByName.put(leaderboard.getName(), leaderboard);
|
|
}
|
|
}
|
|
// Loading the remaining leaderboards
|
|
for (Leaderboard leaderboard : domainObjectFactory.getLeaderboardsNotInGroup()) {
|
|
leaderboardsByName.put(leaderboard.getName(), leaderboard);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Leaderboard addFlexibleLeaderboard(String name, int[] discardThresholds) {
|
|
Leaderboard result = new FlexibleLeaderboardImpl(name, new ScoreCorrectionImpl(), new ResultDiscardingRuleImpl(
|
|
discardThresholds), new LowerScoreIsBetter());
|
|
synchronized (leaderboardsByName) {
|
|
if (leaderboardsByName.containsKey(name)) {
|
|
throw new IllegalArgumentException("Leaderboard with name "+name+" already exists");
|
|
}
|
|
leaderboardsByName.put(name, result);
|
|
}
|
|
mongoObjectFactory.storeLeaderboard(result);
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public RaceColumn addColumnToLeaderboard(String columnName, String leaderboardName, boolean medalRace) {
|
|
Leaderboard leaderboard = getLeaderboardByName(leaderboardName);
|
|
if (leaderboard != null && leaderboard instanceof FlexibleLeaderboard) {
|
|
// uses the default fleet as the single fleet for the new column
|
|
RaceColumn result = ((FlexibleLeaderboard) leaderboard).addRaceColumn(columnName, medalRace, leaderboard.getFleet(null));
|
|
updateStoredLeaderboard(leaderboard);
|
|
return result;
|
|
} else {
|
|
throw new IllegalArgumentException("Leaderboard named " + leaderboardName + " not found");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void moveLeaderboardColumnUp(String leaderboardName, String columnName) {
|
|
Leaderboard leaderboard = getLeaderboardByName(leaderboardName);
|
|
if (leaderboard != null && leaderboard instanceof FlexibleLeaderboard) {
|
|
((FlexibleLeaderboard) leaderboard).moveRaceColumnUp(columnName);
|
|
updateStoredLeaderboard(leaderboard);
|
|
} else {
|
|
throw new IllegalArgumentException("Leaderboard named " + leaderboardName + " not found");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void moveLeaderboardColumnDown(String leaderboardName, String columnName) {
|
|
Leaderboard leaderboard = getLeaderboardByName(leaderboardName);
|
|
if (leaderboard != null && leaderboard instanceof FlexibleLeaderboard) {
|
|
((FlexibleLeaderboard) leaderboard).moveRaceColumnDown(columnName);
|
|
updateStoredLeaderboard(leaderboard);
|
|
} else {
|
|
throw new IllegalArgumentException("Leaderboard named " + leaderboardName + " not found");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void removeLeaderboardColumn(String leaderboardName, String columnName) {
|
|
Leaderboard leaderboard = getLeaderboardByName(leaderboardName);
|
|
if (leaderboard != null && leaderboard instanceof FlexibleLeaderboard) {
|
|
((FlexibleLeaderboard) leaderboard).removeRaceColumn(columnName);
|
|
updateStoredLeaderboard(leaderboard);
|
|
} else {
|
|
throw new IllegalArgumentException("Leaderboard named "+leaderboardName+" not found");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void renameLeaderboardColumn(String leaderboardName, String oldColumnName, String newColumnName) {
|
|
Leaderboard leaderboard = getLeaderboardByName(leaderboardName);
|
|
if (leaderboard != null) {
|
|
leaderboard.getRaceColumnByName(oldColumnName).setName(newColumnName);
|
|
updateStoredLeaderboard(leaderboard);
|
|
} else {
|
|
throw new IllegalArgumentException("Leaderboard named "+leaderboardName+" not found");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void renameLeaderboard(String oldName, String newName) {
|
|
synchronized (leaderboardsByName) {
|
|
if (!leaderboardsByName.containsKey(oldName)) {
|
|
throw new IllegalArgumentException("No leaderboard with name "+oldName+" found");
|
|
}
|
|
if (leaderboardsByName.containsKey(newName)) {
|
|
throw new IllegalArgumentException("Leaderboard with name "+newName+" already exists");
|
|
}
|
|
Leaderboard toRename = leaderboardsByName.remove(oldName);
|
|
if (toRename instanceof FlexibleLeaderboard) {
|
|
((FlexibleLeaderboard) toRename).setName(newName);
|
|
leaderboardsByName.put(newName, toRename);
|
|
mongoObjectFactory.renameLeaderboard(oldName, newName);
|
|
} else {
|
|
throw new IllegalArgumentException("Leaderboard with name "+newName+" is not a FlexibleLeaderboard and therefore cannot be renamed");
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void updateStoredLeaderboard(Leaderboard leaderboard) {
|
|
mongoObjectFactory.storeLeaderboard(leaderboard);
|
|
syncGroupsAfterLeaderboardChange(leaderboard, true);
|
|
}
|
|
|
|
/**
|
|
* Checks all groups, if they contain a leaderboard with the name of the <code>updatedLeaderboard</code> and
|
|
* replaces the one in the group with the updated one.<br />
|
|
* This synchronizes things like the RaceIdentifier in the leaderboard columns.
|
|
*
|
|
* @param updatedLeaderboard
|
|
*/
|
|
private void syncGroupsAfterLeaderboardChange(Leaderboard updatedLeaderboard, boolean doDatabaseUpdate) {
|
|
boolean groupNeedsUpdate = false;
|
|
synchronized (leaderboardGroupsByName) {
|
|
for (LeaderboardGroup leaderboardGroup : leaderboardGroupsByName.values()) {
|
|
for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) {
|
|
if (leaderboard.getName().equals(updatedLeaderboard.getName())) {
|
|
int index = leaderboardGroup.getIndexOf(leaderboard);
|
|
leaderboardGroup.removeLeaderboard(leaderboard);
|
|
leaderboardGroup.addLeaderboardAt(updatedLeaderboard, index);
|
|
groupNeedsUpdate = true;
|
|
// TODO we assume that the leaderboard names are unique, so we can break the inner loop here
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (doDatabaseUpdate && groupNeedsUpdate) {
|
|
mongoObjectFactory.storeLeaderboardGroup(leaderboardGroup);
|
|
}
|
|
groupNeedsUpdate = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void removeLeaderboard(String leaderboardName) {
|
|
synchronized (leaderboardsByName) {
|
|
leaderboardsByName.remove(leaderboardName);
|
|
}
|
|
mongoObjectFactory.removeLeaderboard(leaderboardName);
|
|
|
|
syncGroupsAfterLeaderboardRemove(leaderboardName, true);
|
|
}
|
|
|
|
/**
|
|
* Checks all groups, if they contain a leaderboard with the <code>removedLeaderboardName</code> and removes it from the group.
|
|
* @param removedLeaderboardName
|
|
*/
|
|
private void syncGroupsAfterLeaderboardRemove(String removedLeaderboardName, boolean doDatabaseUpdate) {
|
|
boolean groupNeedsUpdate = false;
|
|
synchronized (leaderboardGroupsByName) {
|
|
for (LeaderboardGroup leaderboardGroup : leaderboardGroupsByName.values()) {
|
|
for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) {
|
|
if (leaderboard.getName().equals(removedLeaderboardName)) {
|
|
leaderboardGroup.removeLeaderboard(leaderboard);
|
|
groupNeedsUpdate = true;
|
|
// TODO we assume that the leaderboard names are unique, so we can break the inner loop here
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (doDatabaseUpdate && groupNeedsUpdate) {
|
|
mongoObjectFactory.storeLeaderboardGroup(leaderboardGroup);
|
|
}
|
|
groupNeedsUpdate = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Leaderboard getLeaderboardByName(String name) {
|
|
synchronized (leaderboardsByName) {
|
|
return leaderboardsByName.get(name);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Map<String, Leaderboard> getLeaderboards() {
|
|
synchronized (leaderboardsByName) {
|
|
return Collections.unmodifiableMap(new HashMap<String, Leaderboard>(leaderboardsByName));
|
|
}
|
|
}
|
|
|
|
private DomainFactory getDomainFactory() {
|
|
return tractracDomainFactory;
|
|
}
|
|
|
|
@Override
|
|
public SwissTimingFactory getSwissTimingFactory() {
|
|
return swissTimingFactory;
|
|
}
|
|
|
|
@Override
|
|
public synchronized Iterable<Regatta> getAllRegattas() {
|
|
return Collections.unmodifiableCollection(new ArrayList<Regatta>(regattasByName.values()));
|
|
}
|
|
|
|
@Override
|
|
public boolean isRaceBeingTracked(RaceDefinition r) {
|
|
for (Set<RaceTracker> trackers : raceTrackersByRegatta.values()) {
|
|
for (RaceTracker tracker : trackers) {
|
|
if (tracker.getRaces() != null && tracker.getRaces().contains(r)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public Regatta getRegattaByName(String name) {
|
|
return regattasByName.get(name);
|
|
}
|
|
|
|
@Override
|
|
public synchronized Regatta addRegatta(URL jsonURL, URI liveURI, URI storedURI, WindStore windStore, long timeoutInMilliseconds) throws Exception {
|
|
JSONService jsonService = getDomainFactory().parseJSONURL(jsonURL);
|
|
Regatta regatta = null;
|
|
for (RaceRecord rr : jsonService.getRaceRecords()) {
|
|
URL paramURL = rr.getParamURL();
|
|
regatta = addTracTracRace(paramURL, liveURI, storedURI, windStore, timeoutInMilliseconds).getRegatta();
|
|
}
|
|
return regatta;
|
|
}
|
|
|
|
@Override
|
|
public Regatta getOrCreateRegatta(String baseEventName, String boatClassName, boolean boatClassTypicallyStartsUpwind) {
|
|
Regatta regatta = new RegattaImpl(baseEventName, com.sap.sailing.domain.base.DomainFactory.INSTANCE.getOrCreateBoatClass(
|
|
boatClassName, boatClassTypicallyStartsUpwind));
|
|
Regatta result = regattasByName.get(regatta.getName());
|
|
if (result == null) {
|
|
result = regatta;
|
|
logger.info("Created regatta "+result.getName()+" ("+hashCode()+")");
|
|
cacheAndReplicateRegatta(result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public Pair<String, List<RaceRecord>> getTracTracRaceRecords(URL jsonURL) throws IOException, ParseException, org.json.simple.parser.ParseException, URISyntaxException {
|
|
JSONService jsonService = getDomainFactory().parseJSONURL(jsonURL);
|
|
return new Pair<String, List<RaceRecord>>(jsonService.getEventName(), jsonService.getRaceRecords());
|
|
}
|
|
|
|
@Override
|
|
public List<com.sap.sailing.domain.swisstimingadapter.RaceRecord> getSwissTimingRaceRecords(String hostname,
|
|
int port, boolean canSendRequests) throws InterruptedException, UnknownHostException, IOException, ParseException {
|
|
List<com.sap.sailing.domain.swisstimingadapter.RaceRecord> result = new ArrayList<com.sap.sailing.domain.swisstimingadapter.RaceRecord>();
|
|
// SailMasterConnector swissTimingConnector = swissTimingFactory.getOrCreateSailMasterConnector(hostname, port, swissTimingAdapterPersistence,
|
|
// canSendRequests);
|
|
//TODO remove after testing and enable the upper code instead
|
|
SailMasterConnector swissTimingConnector = swissTimingFactory.getOrCreateSailMasterLiveSimulatorConnector(hostname, port, swissTimingAdapterPersistence,
|
|
canSendRequests);
|
|
//
|
|
for (Race race : swissTimingConnector.getRaces()) {
|
|
TimePoint startTime = swissTimingConnector.getStartTime(race.getRaceID());
|
|
result.add(new com.sap.sailing.domain.swisstimingadapter.RaceRecord(race.getRaceID(), race.getDescription(),
|
|
startTime==null?null:startTime.asDate()));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public synchronized RacesHandle addSwissTimingRace(RegattaIdentifier regattaToAddTo, String raceID, String hostname,
|
|
int port, boolean canSendRequests, WindStore windStore, long timeoutInMilliseconds) throws Exception {
|
|
return addRace(
|
|
regattaToAddTo,
|
|
swissTimingDomainFactory.createTrackingConnectivityParameters(hostname, port, raceID, canSendRequests, delayToLiveInMillis,
|
|
swissTimingFactory, swissTimingDomainFactory, windStore, swissTimingAdapterPersistence), windStore, timeoutInMilliseconds);
|
|
}
|
|
|
|
@Override
|
|
public synchronized RacesHandle addTracTracRace(URL paramURL, URI liveURI, URI storedURI, WindStore windStore,
|
|
long timeoutInMilliseconds) throws Exception {
|
|
return addRace(
|
|
/* regattaToAddTo */ null,
|
|
getDomainFactory().createTrackingConnectivityParameters(paramURL, liveURI, storedURI,
|
|
/* startOfTracking */ null,
|
|
/* endOfTracking */null, delayToLiveInMillis, windStore), windStore, timeoutInMilliseconds);
|
|
}
|
|
|
|
@Override
|
|
public void addRace(RegattaIdentifier addToRegatta, RaceDefinition raceDefinition) {
|
|
Regatta regatta = getRegatta(addToRegatta);
|
|
regatta.addRace(raceDefinition); // will trigger the raceAdded operation because this service is listening on all its regattass
|
|
}
|
|
|
|
@Override
|
|
public void raceAdded(Regatta regatta, RaceDefinition raceDefinition) {
|
|
final CourseChangeReplicator listener = new CourseChangeReplicator(this, regatta, raceDefinition);
|
|
courseListeners.put(raceDefinition, listener);
|
|
raceDefinition.getCourse().addCourseListener(listener);
|
|
replicate(new AddRaceDefinition(regatta.getRegattaIdentifier(), raceDefinition));
|
|
}
|
|
|
|
@Override
|
|
public void raceRemoved(Regatta regatta, RaceDefinition raceDefinition) {
|
|
raceDefinition.getCourse().removeCourseListener(courseListeners.remove(raceDefinition));
|
|
}
|
|
|
|
@Override
|
|
public synchronized RacesHandle addRace(RegattaIdentifier regattaToAddTo, RaceTrackingConnectivityParameters params,
|
|
WindStore windStore, long timeoutInMilliseconds) throws Exception {
|
|
RaceTracker tracker = raceTrackersByID.get(params.getTrackerID());
|
|
if (tracker == null) {
|
|
Regatta regatta = regattaToAddTo == null ? null : getRegatta(regattaToAddTo);
|
|
if (regatta == null) {
|
|
// create tracker and use an existing or create a default regatta
|
|
tracker = params.createRaceTracker(this);
|
|
} else {
|
|
// use the regatta selected by the RaceIdentifier regattaToAddTo
|
|
tracker = params.createRaceTracker(regatta, this);
|
|
assert tracker.getRegatta() == regatta;
|
|
}
|
|
raceTrackersByID.put(params.getTrackerID(), tracker);
|
|
Set<RaceTracker> trackers = raceTrackersByRegatta.get(tracker.getRegatta());
|
|
if (trackers == null) {
|
|
trackers = new HashSet<RaceTracker>();
|
|
raceTrackersByRegatta.put(tracker.getRegatta(), trackers);
|
|
}
|
|
trackers.add(tracker);
|
|
// TODO we assume here that the event name is unique which necessitates adding the boat class name to it in EventImpl constructor
|
|
String regattaName = tracker.getRegatta().getName();
|
|
Regatta regattaWithName = regattasByName.get(regattaName);
|
|
// TODO we assume here that the event name is unique which necessitates adding the boat class name to it in EventImpl constructor
|
|
if (regattaWithName != null) {
|
|
if (regattaWithName != tracker.getRegatta()) {
|
|
if (Util.isEmpty(regattaWithName.getAllRaces())) {
|
|
// probably, tracker removed the last races from the old regatta and created a new one
|
|
cacheAndReplicateRegatta(tracker.getRegatta());
|
|
} else {
|
|
throw new RuntimeException("Internal error. Two Event objects with equal name "+regattaName);
|
|
}
|
|
}
|
|
} else {
|
|
cacheAndReplicateRegatta(tracker.getRegatta());
|
|
}
|
|
} else {
|
|
WindStore existingTrackersWindStore = tracker.getWindStore();
|
|
if (!existingTrackersWindStore.equals(windStore)) {
|
|
logger.warning("Wind store mismatch. Requested wind store: "+windStore+
|
|
". Wind store in use by existing tracker: "+existingTrackersWindStore);
|
|
}
|
|
}
|
|
if (timeoutInMilliseconds != -1) {
|
|
scheduleAbortTrackerAfterInitialTimeout(tracker, timeoutInMilliseconds);
|
|
}
|
|
return tracker.getRacesHandle();
|
|
}
|
|
|
|
/**
|
|
* If <code>regatta</code> is not yet in {@link #regattasByName}, it is added, this service is
|
|
* {@link Regatta#addRegattaListener(RegattaListener) added} as regatta listener, and the regatta and all its contained
|
|
* {@link Regatta#getAllRaces() races} are replicated to all replica.
|
|
*/
|
|
private void cacheAndReplicateRegatta(Regatta regatta) {
|
|
if (!regattasByName.containsKey(regatta.getName())) {
|
|
regattasByName.put(regatta.getName(), regatta);
|
|
regatta.addRegattaListener(this);
|
|
replicate(new AddRegatta(regatta.getBaseName(), regatta.getBoatClass() == null ? null : regatta.getBoatClass().getName(),
|
|
regatta.getBoatClass() == null ? false : regatta.getBoatClass().typicallyStartsUpwind()));
|
|
RegattaIdentifier regattaIdentifier = regatta.getRegattaIdentifier();
|
|
for (RaceDefinition race : regatta.getAllRaces()) {
|
|
replicate(new AddRaceDefinition(regattaIdentifier, race));
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public synchronized TrackedRace createTrackedRace(RegattaAndRaceIdentifier raceIdentifier, WindStore windStore,
|
|
long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed) {
|
|
DynamicTrackedRegatta trackedRegatta = getTrackedRegatta(getRegatta(raceIdentifier));
|
|
RaceDefinition race = getRace(raceIdentifier);
|
|
return trackedRegatta.createTrackedRace(race, windStore, delayToLiveInMillis, millisecondsOverWhichToAverageWind, millisecondsOverWhichToAverageSpeed,
|
|
/* raceDefinitionSetToUpdate */ null);
|
|
}
|
|
|
|
@Override
|
|
public synchronized RacesHandle addTracTracRace(RegattaIdentifier regattaToAddTo, URL paramURL, URI liveURI,
|
|
URI storedURI, TimePoint startOfTracking, TimePoint endOfTracking,
|
|
WindStore windStore, long timeoutInMilliseconds) throws Exception {
|
|
return addRace(regattaToAddTo, getDomainFactory().createTrackingConnectivityParameters(paramURL, liveURI, storedURI, startOfTracking,
|
|
endOfTracking, delayToLiveInMillis, windStore), windStore, timeoutInMilliseconds);
|
|
}
|
|
|
|
private void ensureRegattaIsObservedForDefaultLeaderboardAndAutoLeaderboardLinking(DynamicTrackedRegatta trackedRegatta) {
|
|
synchronized (regattasObservedForDefaultLeaderboard) {
|
|
if (!regattasObservedForDefaultLeaderboard.contains(trackedRegatta)) {
|
|
trackedRegatta.addRaceListener(new RaceAdditionListener());
|
|
regattasObservedForDefaultLeaderboard.add(trackedRegatta);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A listener class used to ensure that when a tracked race is added to any {@link TrackedRegatta} managed by this
|
|
* service, the service adds the tracked race to the default leaderboard and links it to the leaderboard columns
|
|
* that were previously connected to it. Additionally, a {@link RaceChangeListener} is added to the {@link TrackedRace}
|
|
* which is responsible for triggering the replication of all relevant changes to the tracked race. When a tracked
|
|
* race is removed, the {@link TrackedRaceReplicator} that was added as listener to that tracked race is removed again.
|
|
*
|
|
* @author Axel Uhl (d043530)
|
|
*
|
|
*/
|
|
private class RaceAdditionListener implements RaceListener, Serializable {
|
|
private static final long serialVersionUID = 1036955460477000265L;
|
|
|
|
private final Map<TrackedRace, TrackedRaceReplicator> trackedRaceReplicators;
|
|
|
|
public RaceAdditionListener() {
|
|
this.trackedRaceReplicators = new HashMap<TrackedRace, TrackedRaceReplicator>();
|
|
}
|
|
|
|
@Override
|
|
public void raceRemoved(TrackedRace trackedRace) {
|
|
TrackedRaceReplicator trackedRaceReplicator = trackedRaceReplicators.remove(trackedRace);
|
|
if (trackedRaceReplicator != null) {
|
|
trackedRace.removeListener(trackedRaceReplicator);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void raceAdded(TrackedRace trackedRace) {
|
|
// replicate the addition of the tracked race:
|
|
CreateTrackedRace op = new CreateTrackedRace(trackedRace.getRaceIdentifier(), trackedRace.getWindStore(),
|
|
trackedRace.getDelayToLiveInMillis(),
|
|
trackedRace.getMillisecondsOverWhichToAverageWind(), trackedRace.getMillisecondsOverWhichToAverageSpeed());
|
|
replicate(op);
|
|
linkRaceToConfiguredLeaderboardColumns(trackedRace);
|
|
final FlexibleLeaderboard defaultLeaderboard = (FlexibleLeaderboard) leaderboardsByName.get(DefaultLeaderboardName.DEFAULT_LEADERBOARD_NAME);
|
|
defaultLeaderboard.addRace(trackedRace,
|
|
trackedRace.getRace().getName(), /* medalRace */false, defaultLeaderboard.getFleet(null));
|
|
TrackedRaceReplicator trackedRaceReplicator = new TrackedRaceReplicator(trackedRace);
|
|
trackedRaceReplicators.put(trackedRace, trackedRaceReplicator);
|
|
trackedRace.addListener(trackedRaceReplicator);
|
|
}
|
|
}
|
|
|
|
private class TrackedRaceReplicator implements RaceChangeListener {
|
|
private final TrackedRace trackedRace;
|
|
|
|
public TrackedRaceReplicator(TrackedRace trackedRace) {
|
|
this.trackedRace = trackedRace;
|
|
}
|
|
|
|
@Override
|
|
public void windSourcesToExcludeChanged(Iterable<? extends WindSource> windSourcesToExclude) {
|
|
replicate(new UpdateWindSourcesToExclude(getRaceIdentifier(), windSourcesToExclude));
|
|
}
|
|
|
|
@Override
|
|
public void raceTimesChanged(TimePoint startOfTracking, TimePoint endOfTracking, TimePoint startTimeReceived) {
|
|
replicate(new UpdateRaceTimes(getRaceIdentifier(), startOfTracking, endOfTracking, startTimeReceived));
|
|
}
|
|
|
|
@Override
|
|
public void delayToLiveChanged(long delayToLiveInMillis) {
|
|
replicate(new UpdateRaceDelayToLive(getRaceIdentifier(), delayToLiveInMillis));
|
|
}
|
|
|
|
@Override
|
|
public void windDataReceived(Wind wind, WindSource windSource) {
|
|
replicate(new RecordWindFix(getRaceIdentifier(), windSource, wind));
|
|
}
|
|
|
|
@Override
|
|
public void windDataRemoved(Wind wind, WindSource windSource) {
|
|
replicate(new RemoveWindFix(getRaceIdentifier(), windSource, wind));
|
|
}
|
|
|
|
@Override
|
|
public void windAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) {
|
|
replicate(new UpdateWindAveragingTime(getRaceIdentifier(), newMillisecondsOverWhichToAverage));
|
|
}
|
|
|
|
@Override
|
|
public void competitorPositionChanged(GPSFixMoving fix, Competitor competitor) {
|
|
replicate(new RecordCompetitorGPSFix(getRaceIdentifier(), competitor, fix));
|
|
}
|
|
|
|
@Override
|
|
public void buoyPositionChanged(GPSFix fix, Buoy buoy) {
|
|
replicate(new RecordBuoyGPSFix(getRaceIdentifier(), buoy, fix));
|
|
}
|
|
|
|
@Override
|
|
public void markPassingReceived(Map<Waypoint, MarkPassing> oldMarkPassings, Iterable<MarkPassing> markPassings) {
|
|
replicate(new UpdateMarkPassings(getRaceIdentifier(), markPassings.iterator().next().getCompetitor(),
|
|
markPassings));
|
|
}
|
|
|
|
@Override
|
|
public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) {
|
|
replicate(new UpdateWindAveragingTime(getRaceIdentifier(), newMillisecondsOverWhichToAverage));
|
|
}
|
|
|
|
private RegattaAndRaceIdentifier getRaceIdentifier() {
|
|
return trackedRace.getRaceIdentifier();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Based on the <code>trackedRace</code>'s {@link TrackedRace#getRaceIdentifier() race identifier}, the tracked race
|
|
* is (re-)associated to all {@link RaceColumn race columns} that currently have no
|
|
* {@link RaceColumn#getTrackedRace(Fleet) tracked race assigned} and whose
|
|
* {@link RaceColumn#getRaceIdentifier(Fleet) race identifier} equals that of <code>trackedRace</code>.
|
|
*/
|
|
private void linkRaceToConfiguredLeaderboardColumns(TrackedRace trackedRace) {
|
|
boolean leaderboardHasChanged = false;
|
|
RaceIdentifier trackedRaceIdentifier = trackedRace.getRaceIdentifier();
|
|
for (Leaderboard leaderboard : getLeaderboards().values()) {
|
|
for (RaceColumn column : leaderboard.getRaceColumns()) {
|
|
for (Fleet fleet : column.getFleets()) {
|
|
if (trackedRaceIdentifier.equals(column.getRaceIdentifier(fleet)) && column.getTrackedRace(fleet) == null) {
|
|
column.setTrackedRace(fleet, trackedRace);
|
|
leaderboardHasChanged = true;
|
|
replicate(new ConnectTrackedRaceToLeaderboardColumn(leaderboard.getName(), column.getName(),
|
|
fleet.getName(), trackedRaceIdentifier));
|
|
}
|
|
}
|
|
}
|
|
if (leaderboardHasChanged) {
|
|
//Update the corresponding groups, to keep them in sync
|
|
syncGroupsAfterLeaderboardChange(leaderboard, /*doDatabaseUpdate*/ false);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public synchronized void stopTracking(Regatta regatta) throws MalformedURLException, IOException, InterruptedException {
|
|
if (raceTrackersByRegatta.containsKey(regatta)) {
|
|
for (RaceTracker raceTracker : raceTrackersByRegatta.get(regatta)) {
|
|
for (RaceDefinition race : raceTracker.getRaces()) {
|
|
stopTrackingWind(regatta, race);
|
|
}
|
|
raceTracker.stop(); // this also removes the TrackedRace from trackedRegatta
|
|
raceTrackersByID.remove(raceTracker.getID());
|
|
}
|
|
raceTrackersByRegatta.remove(regatta);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public synchronized void stopTrackingAndRemove(Regatta regatta) throws MalformedURLException, IOException, InterruptedException {
|
|
stopTracking(regatta);
|
|
if (regatta != null) {
|
|
if (regatta.getName() != null) {
|
|
logger.info("Removing regatta "+regatta.getName()+" ("+regatta.hashCode()+") from RacingEventServiceImpl");
|
|
regattasByName.remove(regatta.getName());
|
|
regattaTrackingCache.remove(regatta);
|
|
regatta.removeRegattaListener(this);
|
|
}
|
|
for (RaceDefinition race : regatta.getAllRaces()) {
|
|
stopTrackingWind(regatta, race);
|
|
// remove from default leaderboard
|
|
FlexibleLeaderboard defaultLeaderboard = (FlexibleLeaderboard) getLeaderboardByName(DefaultLeaderboardName.DEFAULT_LEADERBOARD_NAME);
|
|
defaultLeaderboard.removeRaceColumn(race.getName());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The tracker will initially try to connect to the TracTrac infrastructure to obtain basic race master data. If
|
|
* this fails after some timeout, to avoid garbage and lingering threads, the task scheduled by this method will
|
|
* check after the timeout expires if race master data was successfully received. If so, the tracker continues
|
|
* normally. Otherwise, the tracker is shut down orderly by {@link Receiver#stopPreemptively() stopping} all
|
|
* receivers and {@link DataController#stop(boolean) stopping} the TracTrac controller for this tracker.
|
|
*
|
|
* @return the scheduled task, in case the caller wants to {@link ScheduledFuture#cancel(boolean) cancel} it, e.g.,
|
|
* when the tracker is stopped or has successfully received the race
|
|
*/
|
|
private ScheduledFuture<?> scheduleAbortTrackerAfterInitialTimeout(final RaceTracker tracker, final long timeoutInMilliseconds) {
|
|
ScheduledFuture<?> task = getScheduler().schedule(new Runnable() {
|
|
@Override public void run() {
|
|
if (tracker.getRaces() == null || tracker.getRaces().isEmpty()) {
|
|
try {
|
|
Regatta regatta = tracker.getRegatta();
|
|
logger.log(Level.SEVERE, "RaceDefinition for a race in regatta "+regatta.getName()+" not obtained within "+
|
|
timeoutInMilliseconds+"ms. Aborting tracker for this race.");
|
|
Set<RaceTracker> trackersForRegatta = raceTrackersByRegatta.get(regatta);
|
|
if (trackersForRegatta != null) {
|
|
trackersForRegatta.remove(tracker);
|
|
}
|
|
tracker.stop();
|
|
raceTrackersByID.remove(tracker.getID());
|
|
if (trackersForRegatta == null || trackersForRegatta.isEmpty()) {
|
|
stopTracking(regatta);
|
|
}
|
|
} catch (Exception e) {
|
|
logger.throwing(RacingEventServiceImpl.class.getName(), "scheduleAbortTrackerAfterInitialTimeout", e);
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
}
|
|
}, /* delay */ timeoutInMilliseconds, /* unit */ TimeUnit.MILLISECONDS);
|
|
return task;
|
|
}
|
|
|
|
@Override
|
|
public synchronized void stopTracking(Regatta regatta, RaceDefinition race) throws MalformedURLException, IOException, InterruptedException {
|
|
logger.info("Stopping tracking for "+race+"...");
|
|
if (raceTrackersByRegatta.containsKey(regatta)) {
|
|
Iterator<RaceTracker> trackerIter = raceTrackersByRegatta.get(regatta).iterator();
|
|
while (trackerIter.hasNext()) {
|
|
RaceTracker raceTracker = trackerIter.next();
|
|
if (raceTracker.getRaces() != null && raceTracker.getRaces().contains(race)) {
|
|
logger.info("Found tracker to stop for races "+raceTracker.getRaces());
|
|
raceTracker.stop(); // this also removes the TrackedRace from trackedRegatta
|
|
// do not remove the tracker from raceTrackersByRegatta, because it should still exist there, but with the state "non-tracked"
|
|
trackerIter.remove();
|
|
raceTrackersByID.remove(raceTracker.getID());
|
|
}
|
|
}
|
|
} else {
|
|
logger.warning("Didn't find any trackers for regatta "+regatta);
|
|
}
|
|
stopTrackingWind(regatta, race);
|
|
// if the last tracked race was removed, remove the entire regatta
|
|
if (raceTrackersByRegatta.get(regatta).isEmpty()) {
|
|
stopTracking(regatta);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public synchronized void removeRegatta(Regatta regatta) throws MalformedURLException, IOException, InterruptedException {
|
|
for (RaceDefinition race : regatta.getAllRaces()) {
|
|
removeRace(regatta, race);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public synchronized void removeRace(Regatta regatta, RaceDefinition race) throws MalformedURLException,
|
|
IOException, InterruptedException {
|
|
logger.info("Removing the race " + race + "...");
|
|
stopAllTrackersForWhichRaceIsLastReachable(regatta, race);
|
|
stopTrackingWind(regatta, race);
|
|
TrackedRace trackedRace = getExistingTrackedRace(regatta, race);
|
|
if (trackedRace != null) {
|
|
TrackedRegatta trackedRegatta = getTrackedRegatta(regatta);
|
|
if (trackedRegatta != null) {
|
|
trackedRegatta.removeTrackedRace(trackedRace);
|
|
}
|
|
if (Util.isEmpty(trackedRegatta.getTrackedRaces())) {
|
|
removeTrackedRegatta(regatta);
|
|
}
|
|
for (Leaderboard leaderboard : getLeaderboards().values()) {
|
|
boolean changed = false;
|
|
for (RaceColumn raceColumn : leaderboard.getRaceColumns()) {
|
|
for (Fleet fleet : raceColumn.getFleets()) {
|
|
if (raceColumn.getTrackedRace(fleet) == trackedRace) {
|
|
raceColumn.releaseTrackedRace(fleet); // but leave the RaceIdentifier on the race column
|
|
changed = true; // untouched, e.g., for later re-load
|
|
}
|
|
}
|
|
}
|
|
if (changed) {
|
|
updateStoredLeaderboard(leaderboard);
|
|
}
|
|
}
|
|
}
|
|
// remove the race from the regatta
|
|
regatta.removeRace(race);
|
|
if (Util.isEmpty(regatta.getAllRaces())) {
|
|
logger.info("Removing regatta "+regatta.getName()+" from service "+this);
|
|
regattasByName.remove(regatta.getName());
|
|
regatta.removeRegattaListener(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Doesn't stop any wind trackers
|
|
*/
|
|
private void stopAllTrackersForWhichRaceIsLastReachable(Regatta regatta, RaceDefinition race)
|
|
throws MalformedURLException, IOException, InterruptedException {
|
|
if (raceTrackersByRegatta.containsKey(regatta)) {
|
|
Iterator<RaceTracker> trackerIter = raceTrackersByRegatta.get(regatta).iterator();
|
|
while (trackerIter.hasNext()) {
|
|
RaceTracker raceTracker = trackerIter.next();
|
|
if (raceTracker.getRaces() != null && raceTracker.getRaces().contains(race)) {
|
|
boolean foundReachableRace = false;
|
|
for (RaceDefinition raceTrackedByTracker : raceTracker.getRaces()) {
|
|
if (raceTrackedByTracker != race && isReachable(regatta, raceTrackedByTracker)) {
|
|
foundReachableRace = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!foundReachableRace) {
|
|
// firstly stop the tracker
|
|
raceTracker.stop();
|
|
// remove it from the raceTrackers by Regatta
|
|
trackerIter.remove();
|
|
raceTrackersByID.remove(raceTracker.getID());
|
|
// if the last tracked race was removed, remove the entire regatta
|
|
if (raceTrackersByRegatta.get(regatta).isEmpty()) {
|
|
stopTracking(regatta);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean isReachable(Regatta regatta, RaceDefinition race) {
|
|
return Util.contains(regatta.getAllRaces(), race);
|
|
}
|
|
|
|
@Override
|
|
public void startTrackingWind(Regatta regatta, RaceDefinition race,
|
|
boolean correctByDeclination) throws SocketException {
|
|
windTrackerFactory.createWindTracker(getOrCreateTrackedRegatta(regatta), race, correctByDeclination);
|
|
}
|
|
|
|
@Override
|
|
public synchronized void stopTrackingWind(Regatta regatta, RaceDefinition race) throws SocketException, IOException {
|
|
WindTracker windTracker = windTrackerFactory.getExistingWindTracker(race);
|
|
if (windTracker != null) {
|
|
windTracker.stop();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public synchronized Iterable<Triple<Regatta, RaceDefinition, String>> getWindTrackedRaces() {
|
|
List<Triple<Regatta, RaceDefinition, String>> result = new ArrayList<Triple<Regatta, RaceDefinition, String>>();
|
|
for (Regatta regatta : getAllRegattas()) {
|
|
for (RaceDefinition race : regatta.getAllRaces()) {
|
|
WindTracker windTracker = windTrackerFactory.getExistingWindTracker(race);
|
|
if (windTracker != null) {
|
|
result.add(new Triple<Regatta, RaceDefinition, String>(regatta, race, windTracker.toString()));
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public TrackedRace getTrackedRace(Regatta regatta, RaceDefinition race) {
|
|
return getOrCreateTrackedRegatta(regatta).getTrackedRace(race);
|
|
}
|
|
|
|
private TrackedRace getExistingTrackedRace(Regatta regatta, RaceDefinition race) {
|
|
return getOrCreateTrackedRegatta(regatta).getExistingTrackedRace(race);
|
|
}
|
|
|
|
@Override
|
|
public DynamicTrackedRegatta getOrCreateTrackedRegatta(Regatta regatta) {
|
|
cacheAndReplicateRegatta(regatta);
|
|
synchronized (regattaTrackingCache) {
|
|
DynamicTrackedRegatta result = regattaTrackingCache.get(regatta);
|
|
if (result == null) {
|
|
logger.info("Creating DynamicTrackedRegattaImpl for regatta "+regatta.getName()+
|
|
" with hashCode "+regatta.hashCode());
|
|
result = new DynamicTrackedRegattaImpl(regatta);
|
|
replicate(new TrackRegatta(regatta.getRegattaIdentifier()));
|
|
regattaTrackingCache.put(regatta, result);
|
|
ensureRegattaIsObservedForDefaultLeaderboardAndAutoLeaderboardLinking(result);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public DynamicTrackedRegatta getTrackedRegatta(com.sap.sailing.domain.base.Regatta regatta) {
|
|
return regattaTrackingCache.get(regatta);
|
|
}
|
|
|
|
@Override
|
|
public void removeTrackedRegatta(Regatta regatta) {
|
|
logger.info("Removing regatta "+regatta.getName()+" from regattaTrackingCache");
|
|
regattaTrackingCache.remove(regatta);
|
|
}
|
|
|
|
@Override
|
|
public void storeSwissTimingDummyRace(String racMessage, String stlMessage, String ccgMessage){
|
|
SailMasterMessage racSMMessage = swissTimingFactory.createMessage(racMessage, null);
|
|
SailMasterMessage stlSMMessage = swissTimingFactory.createMessage(stlMessage, null);
|
|
SailMasterMessage ccgSMMessage = swissTimingFactory.createMessage(ccgMessage, null);
|
|
if (swissTimingAdapterPersistence.getRace(stlSMMessage.getRaceID()) != null) {
|
|
throw new IllegalArgumentException("Race with raceID \"" + stlSMMessage.getRaceID() + "\" already exists.");
|
|
}
|
|
else {
|
|
swissTimingAdapterPersistence.storeSailMasterMessage(racSMMessage);
|
|
swissTimingAdapterPersistence.storeSailMasterMessage(stlSMMessage);
|
|
swissTimingAdapterPersistence.storeSailMasterMessage(ccgSMMessage);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Regatta getRegatta(RegattaName regattaName) {
|
|
return (Regatta) regattasByName.get(regattaName.getRegattaName());
|
|
}
|
|
|
|
@Override
|
|
public Regatta getRegatta(RegattaIdentifier regattaIdentifier) {
|
|
return (Regatta) regattaIdentifier.getRegatta(this);
|
|
}
|
|
|
|
@Override
|
|
public TrackedRace getTrackedRace(RegattaAndRaceIdentifier raceIdentifier) {
|
|
TrackedRace result = null;
|
|
Regatta regatta = regattasByName.get(raceIdentifier.getRegattaName());
|
|
if (regatta != null) {
|
|
DynamicTrackedRegatta trackedRegatta = regattaTrackingCache.get(regatta);
|
|
if (trackedRegatta != null) {
|
|
RaceDefinition race = getRace(raceIdentifier);
|
|
if (race != null) {
|
|
result = trackedRegatta.getTrackedRace(race);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public TrackedRace getExistingTrackedRace(RaceIdentifier raceIdentifier) {
|
|
Regatta regatta = getRegattaByName(raceIdentifier.getRegattaName());
|
|
TrackedRace trackedRace = null;
|
|
if (regatta != null) {
|
|
RaceDefinition race = regatta.getRaceByName(raceIdentifier.getRaceName());
|
|
trackedRace = getOrCreateTrackedRegatta(regatta).getExistingTrackedRace(race);
|
|
}
|
|
return trackedRace;
|
|
}
|
|
|
|
@Override
|
|
public RaceDefinition getRace(RegattaAndRaceIdentifier regattaNameAndRaceName) {
|
|
RaceDefinition result = null;
|
|
Regatta regatta = getRegatta(regattaNameAndRaceName);
|
|
if (regatta != null) {
|
|
result = regatta.getRaceByName(regattaNameAndRaceName.getRaceName());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public Map<String, LeaderboardGroup> getLeaderboardGroups() {
|
|
synchronized (leaderboardGroupsByName) {
|
|
return Collections.unmodifiableMap(new HashMap<String, LeaderboardGroup>(leaderboardGroupsByName));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public LeaderboardGroup getLeaderboardGroupByName(String groupName) {
|
|
synchronized (leaderboardGroupsByName) {
|
|
return leaderboardGroupsByName.get(groupName);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public LeaderboardGroup addLeaderboardGroup(String groupName, String description, List<String> leaderboardNames) {
|
|
ArrayList<Leaderboard> leaderboards = new ArrayList<>();
|
|
synchronized (leaderboardsByName) {
|
|
for (String leaderboardName : leaderboardNames) {
|
|
Leaderboard leaderboard = leaderboardsByName.get(leaderboardName);
|
|
if (leaderboard == null) {
|
|
throw new IllegalArgumentException("No leaderboard with name " + leaderboardName + " found");
|
|
} else {
|
|
leaderboards.add(leaderboard);
|
|
}
|
|
}
|
|
}
|
|
LeaderboardGroup result = new LeaderboardGroupImpl(groupName, description, leaderboards);
|
|
synchronized (leaderboardGroupsByName) {
|
|
if (leaderboardGroupsByName.containsKey(groupName)) {
|
|
throw new IllegalArgumentException("Leaderboard group with name " + groupName + " already exists");
|
|
}
|
|
leaderboardGroupsByName.put(groupName, result);
|
|
}
|
|
mongoObjectFactory.storeLeaderboardGroup(result);
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public void removeLeaderboardGroup(String groupName) {
|
|
synchronized (leaderboardGroupsByName) {
|
|
leaderboardGroupsByName.remove(groupName);
|
|
}
|
|
mongoObjectFactory.removeLeaderboardGroup(groupName);
|
|
}
|
|
|
|
@Override
|
|
public void renameLeaderboardGroup(String oldName, String newName) {
|
|
synchronized (leaderboardGroupsByName) {
|
|
if (!leaderboardGroupsByName.containsKey(oldName)) {
|
|
throw new IllegalArgumentException("No leaderboard group with name " + oldName + " found");
|
|
}
|
|
if (leaderboardGroupsByName.containsKey(newName)) {
|
|
throw new IllegalArgumentException("Leaderboard group with name " + newName + " already exists");
|
|
}
|
|
LeaderboardGroup toRename = leaderboardGroupsByName.remove(oldName);
|
|
toRename.setName(newName);
|
|
leaderboardGroupsByName.put(newName, toRename);
|
|
mongoObjectFactory.renameLeaderboardGroup(oldName, newName);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void updateLeaderboardGroup(String oldName, String newName, String description, List<String> leaderboardNames) {
|
|
if (!oldName.equals(newName)) {
|
|
renameLeaderboardGroup(oldName, newName);
|
|
}
|
|
LeaderboardGroup group = getLeaderboardGroupByName(newName);
|
|
if (!description.equals(group.getDescription())) {
|
|
group.setDescriptiom(description);
|
|
}
|
|
group.clearLeaderboards();
|
|
for (String leaderboardName : leaderboardNames) {
|
|
Leaderboard leaderboard = getLeaderboardByName(leaderboardName);
|
|
if (leaderboard != null) {
|
|
group.addLeaderboard(leaderboard);
|
|
}
|
|
}
|
|
mongoObjectFactory.storeLeaderboardGroup(group);
|
|
}
|
|
|
|
@Override
|
|
public void updateStoredLeaderboardGroup(LeaderboardGroup leaderboardGroup) {
|
|
mongoObjectFactory.storeLeaderboardGroup(leaderboardGroup);
|
|
}
|
|
|
|
@Override
|
|
public void addExpeditionListener(ExpeditionListener listener, boolean validMessagesOnly) throws SocketException {
|
|
UDPExpeditionReceiver receiver = windTrackerFactory.getOrCreateWindReceiverOnDefaultPort();
|
|
receiver.addListener(listener, validMessagesOnly);
|
|
}
|
|
|
|
@Override
|
|
public void removeExpeditionListener(ExpeditionListener listener) {
|
|
UDPExpeditionReceiver receiver;
|
|
try {
|
|
receiver = windTrackerFactory.getOrCreateWindReceiverOnDefaultPort();
|
|
receiver.removeListener(listener);
|
|
} catch (SocketException e) {
|
|
logger.info("Failed to remove expedition listener "+listener+
|
|
"; exception while trying to retrieve wind receiver: "+e.getMessage());
|
|
}
|
|
}
|
|
|
|
private ScheduledExecutorService getScheduler() {
|
|
return scheduler;
|
|
}
|
|
|
|
/**
|
|
* Currently, the operation is executed by immediately {@link Operation#internalApplyTo(Object) applying} it to this
|
|
* service object.<p>
|
|
*
|
|
* Future implementations of this method will need to also replicate the effects of the operation to all replica
|
|
* of this service known.
|
|
*/
|
|
@Override
|
|
public <T> T apply(RacingEventServiceOperation<T> operation) {
|
|
try {
|
|
T result = operation.internalApplyTo(this);
|
|
replicate(operation);
|
|
return result;
|
|
} catch (Exception e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
<T> void replicate(RacingEventServiceOperation<T> operation) {
|
|
for (OperationExecutionListener listener : operationExecutionListeners) {
|
|
listener.executed(operation); // TODO consider exception handling
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void addOperationExecutionListener(OperationExecutionListener listener) {
|
|
operationExecutionListeners.add(listener);
|
|
}
|
|
|
|
@Override
|
|
public void removeOperationExecutionListener(OperationExecutionListener listener) {
|
|
operationExecutionListeners.remove(listener);
|
|
}
|
|
|
|
@Override
|
|
public void serializeForInitialReplication(ObjectOutputStream oos) throws IOException {
|
|
oos.writeObject(regattasByName);
|
|
oos.writeObject(regattasObservedForDefaultLeaderboard);
|
|
oos.writeObject(regattaTrackingCache);
|
|
oos.writeObject(leaderboardGroupsByName);
|
|
oos.writeObject(leaderboardsByName);
|
|
}
|
|
|
|
@SuppressWarnings("unchecked") // the type-parameters in the casts of the de-serialized collection objects can't be checked
|
|
@Override
|
|
public synchronized void initiallyFillFrom(ObjectInputStream ois) throws IOException, ClassNotFoundException {
|
|
ClassLoader oldContextClassloader = Thread.currentThread().getContextClassLoader();
|
|
try {
|
|
// Use this object's class's class loader as the context class loader which will then be used for
|
|
// de-serialization; this will cause all classes to be visible that this bundle
|
|
// (com.sap.sailing.server) can see
|
|
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
|
|
regattasByName.clear();
|
|
regattasByName.putAll((Map<String, Regatta>) ois.readObject());
|
|
regattasObservedForDefaultLeaderboard.clear();
|
|
for (DynamicTrackedRegatta trackedRegattaToObserve : (Set<DynamicTrackedRegatta>) ois.readObject()) {
|
|
ensureRegattaIsObservedForDefaultLeaderboardAndAutoLeaderboardLinking(trackedRegattaToObserve);
|
|
}
|
|
regattaTrackingCache.clear();
|
|
regattaTrackingCache.putAll((Map<Regatta, DynamicTrackedRegatta>) ois.readObject());
|
|
leaderboardGroupsByName.clear();
|
|
leaderboardGroupsByName.putAll((Map<String, LeaderboardGroup>) ois.readObject());
|
|
leaderboardsByName.clear();
|
|
leaderboardsByName.putAll((Map<String, Leaderboard>) ois.readObject());
|
|
} finally {
|
|
Thread.currentThread().setContextClassLoader(oldContextClassloader);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public long getDelayToLiveInMillis() {
|
|
return delayToLiveInMillis;
|
|
}
|
|
|
|
@Override
|
|
public void setDelayToLiveInMillis(long delayToLiveInMillis) {
|
|
this.delayToLiveInMillis = delayToLiveInMillis;
|
|
}
|
|
|
|
}
|