diff --git a/java/com.googlecode.java-diff-utils.test/src/diffutils/Example.java b/java/com.googlecode.java-diff-utils.test/src/diffutils/Example.java index 1e148e790bd..25727867563 100755 --- a/java/com.googlecode.java-diff-utils.test/src/diffutils/Example.java +++ b/java/com.googlecode.java-diff-utils.test/src/diffutils/Example.java @@ -15,6 +15,7 @@ public class Example { while ((line = in.readLine()) != null) { lines.add(line); } + in.close(); } catch (IOException e) { e.printStackTrace(); } diff --git a/java/com.googlecode.java-diff-utils.test/src/diffutils/GenerateUnifiedDiffTest.java b/java/com.googlecode.java-diff-utils.test/src/diffutils/GenerateUnifiedDiffTest.java index f924d69a2e4..c651bef4b65 100755 --- a/java/com.googlecode.java-diff-utils.test/src/diffutils/GenerateUnifiedDiffTest.java +++ b/java/com.googlecode.java-diff-utils.test/src/diffutils/GenerateUnifiedDiffTest.java @@ -26,6 +26,7 @@ public class GenerateUnifiedDiffTest extends TestCase { while ((line = in.readLine()) != null) { lines.add(line); } + in.close(); } catch (IOException e) { e.printStackTrace(); fail(e.getMessage()); diff --git a/java/com.sap.sailing.declination.test/src/com/sap/sailing/declination/test/DeclinationStoreTest.java b/java/com.sap.sailing.declination.test/src/com/sap/sailing/declination/test/DeclinationStoreTest.java index c11ab591163..4d9253902b0 100755 --- a/java/com.sap.sailing.declination.test/src/com/sap/sailing/declination/test/DeclinationStoreTest.java +++ b/java/com.sap.sailing.declination.test/src/com/sap/sailing/declination/test/DeclinationStoreTest.java @@ -313,6 +313,9 @@ public class DeclinationStoreTest extends AbstractDeclinationTest { // this was to be expected } ois.close(); + + oos1.close(); + oos2.close(); } finally { tmpFile.delete(); } diff --git a/java/com.sap.sailing.domain.swisstimingadapter.persistence/src/com/sap/sailing/domain/swisstimingadapter/persistence/StoreAndForward.java b/java/com.sap.sailing.domain.swisstimingadapter.persistence/src/com/sap/sailing/domain/swisstimingadapter/persistence/StoreAndForward.java index c042e09f409..01bffb539b7 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter.persistence/src/com/sap/sailing/domain/swisstimingadapter/persistence/StoreAndForward.java +++ b/java/com.sap.sailing.domain.swisstimingadapter.persistence/src/com/sap/sailing/domain/swisstimingadapter/persistence/StoreAndForward.java @@ -232,7 +232,8 @@ public class StoreAndForward implements Runnable { public void stop() throws UnknownHostException, IOException, InterruptedException { logger.entering(getClass().getName(), "stop"); stopped = true; - new Socket("localhost", portForClients); // this is to stop the client listener thread + Socket closer = new Socket("localhost", portForClients); // this is to stop the client listener thread + closer.close(); logger.info("joining clientListener thread "+clientListener); clientListener.join(); socket.close(); // will let a read terminate abnormally diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterConnectorImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterConnectorImpl.java index d335ebce782..df03e0967d5 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterConnectorImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterConnectorImpl.java @@ -320,6 +320,8 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement case TMD: notifyListenersTMD(message); break; + default: + // ignore all other messages because there are no notification patterns for those } } diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayServiceImpl.java b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayServiceImpl.java index 316080ec6a0..a4bd9a926c5 100644 --- a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayServiceImpl.java +++ b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayServiceImpl.java @@ -132,6 +132,8 @@ public class SwissTimingReplayServiceImpl implements SwissTimingReplayService { bos.write(buf, 0, read); } new SwissTimingReplayParserImpl().readData(new ByteArrayInputStream(bos.getBuffer(), 0, bos.size()), replayListener); + + bos.close(); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayToDomainAdapter.java b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayToDomainAdapter.java old mode 100755 new mode 100644 index c346dc409c8..4730b0c0f84 --- a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayToDomainAdapter.java +++ b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayToDomainAdapter.java @@ -134,7 +134,7 @@ public class SwissTimingReplayToDomainAdapter extends SwissTimingReplayAdapter { * use / creation of a default regatta per race */ public SwissTimingReplayToDomainAdapter(Regatta regatta, DomainFactory domainFactory, TrackedRegattaRegistry trackedRegattaRegistry) { - this.regatta = null; + this.regatta = regatta; this.trackedRegattaRegistry = trackedRegattaRegistry; racePerRaceID = new HashMap<>(); trackedRacePerRaceID = new HashMap<>(); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CompactWindImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CompactWindImpl.java index a80380e5cc7..d1849dc8c8d 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CompactWindImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CompactWindImpl.java @@ -1,132 +1,132 @@ -package com.sap.sailing.domain.tracking.impl; - -import com.sap.sailing.domain.base.impl.AbstractSpeedWithAbstractBearingImpl; -import com.sap.sailing.domain.base.impl.AbstractTimePoint; -import com.sap.sailing.domain.common.AbstractBearing; -import com.sap.sailing.domain.common.AbstractPosition; -import com.sap.sailing.domain.common.Bearing; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.TimePoint; -import com.sap.sailing.domain.tracking.Wind; - -public class CompactWindImpl extends AbstractSpeedWithAbstractBearingImpl implements Wind { - private static final long serialVersionUID = -5059956032663387929L; - private final double latDeg; - private final double lngDeg; - private final boolean positionIsNull; - private final boolean bearingIsNull; - private final boolean timePointIsNull; - private final double speedInKnots; - private final double degBearing; - private final long timePointAsMillis; - - private class CompactPosition extends AbstractPosition { - private static final long serialVersionUID = 5621506820766614178L; - - @Override - public double getLatDeg() { - return latDeg; - } - - @Override - public double getLngDeg() { - return lngDeg; - } - } - - private class CompactBearing extends AbstractBearing { - private static final long serialVersionUID = -6474909210513108635L; - - @Override - public double getDegrees() { - return degBearing; - } - - @Override - public double getRadians() { - return degBearing / 180. * Math.PI; - } - } - - private class CompactTimePoint extends AbstractTimePoint implements TimePoint { - private static final long serialVersionUID = -2470922642359937437L; - - @Override - public long asMillis() { - return timePointAsMillis; - } - } - - public CompactWindImpl(Wind wind) { - if (wind.getBearing() == null) { - bearingIsNull = true; - degBearing = 0; - } else { - this.degBearing = wind.getBearing().getDegrees(); - bearingIsNull = false; - } - if (wind.getPosition() == null) { - positionIsNull = true; - this.latDeg = 0; - this.lngDeg = 0; - } else { - this.latDeg = wind.getPosition().getLatDeg(); - this.lngDeg = wind.getPosition().getLngDeg(); - positionIsNull = false; - } - this.speedInKnots = wind.getKnots(); - if (wind.getTimePoint() == null) { - timePointIsNull = true; - this.timePointAsMillis = 0; - } else { - timePointIsNull = false; - this.timePointAsMillis = wind.getTimePoint().asMillis(); - } - } - - @Override - public Position getPosition() { - if (positionIsNull) { - return null; - } else { - return new CompactPosition(); - } - } - - @Override - public TimePoint getTimePoint() { - if (timePointIsNull) { - return null; - } else { - return new CompactTimePoint(); - } - } - - @Override - public Bearing getBearing() { - if (bearingIsNull) { - return null; - } else { - return new CompactBearing(); - } - } - - @Override - public double getKnots() { - return speedInKnots; - } - - @Override - public Bearing getFrom() { - if (getBearing() == null) { - return null; - } else { - return getBearing().reverse(); - } - } - - @Override - public String toString() { - return ""+getTimePoint()+"@"+getPosition()+": "+getKnots()+"kn from "+getFrom(); - } -} +package com.sap.sailing.domain.tracking.impl; + +import com.sap.sailing.domain.base.impl.AbstractSpeedWithAbstractBearingImpl; +import com.sap.sailing.domain.base.impl.AbstractTimePoint; +import com.sap.sailing.domain.common.AbstractBearing; +import com.sap.sailing.domain.common.AbstractPosition; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.tracking.Wind; + +public class CompactWindImpl extends AbstractSpeedWithAbstractBearingImpl implements Wind { + private static final long serialVersionUID = -5059956032663387929L; + private final double latDeg; + private final double lngDeg; + private final boolean positionIsNull; + private final boolean bearingIsNull; + private final boolean timePointIsNull; + private final double speedInKnots; + private final double degBearing; + private final long timePointAsMillis; + + private class CompactPosition extends AbstractPosition { + private static final long serialVersionUID = 5621506820766614178L; + + @Override + public double getLatDeg() { + return latDeg; + } + + @Override + public double getLngDeg() { + return lngDeg; + } + } + + private class CompactBearing extends AbstractBearing { + private static final long serialVersionUID = -6474909210513108635L; + + @Override + public double getDegrees() { + return degBearing; + } + + @Override + public double getRadians() { + return degBearing / 180. * Math.PI; + } + } + + private class CompactTimePoint extends AbstractTimePoint implements TimePoint { + private static final long serialVersionUID = -2470922642359937437L; + + @Override + public long asMillis() { + return timePointAsMillis; + } + } + + public CompactWindImpl(Wind wind) { + if (wind.getBearing() == null) { + bearingIsNull = true; + degBearing = 0; + } else { + this.degBearing = wind.getBearing().getDegrees(); + bearingIsNull = false; + } + if (wind.getPosition() == null) { + positionIsNull = true; + this.latDeg = 0; + this.lngDeg = 0; + } else { + this.latDeg = wind.getPosition().getLatDeg(); + this.lngDeg = wind.getPosition().getLngDeg(); + positionIsNull = false; + } + this.speedInKnots = wind.getKnots(); + if (wind.getTimePoint() == null) { + timePointIsNull = true; + this.timePointAsMillis = 0; + } else { + timePointIsNull = false; + this.timePointAsMillis = wind.getTimePoint().asMillis(); + } + } + + @Override + public Position getPosition() { + if (positionIsNull) { + return null; + } else { + return new CompactPosition(); + } + } + + @Override + public TimePoint getTimePoint() { + if (timePointIsNull) { + return null; + } else { + return new CompactTimePoint(); + } + } + + @Override + public Bearing getBearing() { + if (bearingIsNull) { + return null; + } else { + return new CompactBearing(); + } + } + + @Override + public double getKnots() { + return speedInKnots; + } + + @Override + public Bearing getFrom() { + if (getBearing() == null) { + return null; + } else { + return getBearing().reverse(); + } + } + + @Override + public String toString() { + return ""+getTimePoint()+"@"+getPosition()+": "+getKnots()+"kn from "+getFrom(); + } +} diff --git a/java/com.sap.sailing.expeditionconnector.test/src/com/sap/sailing/expeditionconnector/test/UDPMirrorTest.java b/java/com.sap.sailing.expeditionconnector.test/src/com/sap/sailing/expeditionconnector/test/UDPMirrorTest.java index 7c545a936a0..117a45f49fb 100755 --- a/java/com.sap.sailing.expeditionconnector.test/src/com/sap/sailing/expeditionconnector/test/UDPMirrorTest.java +++ b/java/com.sap.sailing.expeditionconnector.test/src/com/sap/sailing/expeditionconnector/test/UDPMirrorTest.java @@ -53,6 +53,8 @@ public class UDPMirrorTest { didReceive[0] = true; didReceive.notifyAll(); } + + udpSocket.close(); } catch (Exception e) { throw new RuntimeException(e); } @@ -67,6 +69,8 @@ public class UDPMirrorTest { didReceive.wait(); } } + sendingSocket.close(); + assertTrue(Arrays.equals(buf1, buf2)); } diff --git a/java/com.sap.sailing.expeditionconnector/src/com/sap/sailing/expeditionconnector/UDPMirror.java b/java/com.sap.sailing.expeditionconnector/src/com/sap/sailing/expeditionconnector/UDPMirror.java index 25639e11f08..1cc938390b1 100755 --- a/java/com.sap.sailing.expeditionconnector/src/com/sap/sailing/expeditionconnector/UDPMirror.java +++ b/java/com.sap.sailing.expeditionconnector/src/com/sap/sailing/expeditionconnector/UDPMirror.java @@ -30,7 +30,8 @@ public class UDPMirror implements Runnable { } int listeningOnPort = Integer.valueOf(args[c++]); byte[] buf = new byte[65536]; - DatagramSocket udpSocket = new DatagramSocket(listeningOnPort); + @SuppressWarnings("resource") //Can't close resource due to the infinite loop. Will be closed, when the thread is terminated. + DatagramSocket udpSocket = new DatagramSocket(listeningOnPort); DatagramPacket received = new DatagramPacket(buf, buf.length); DatagramSocket[] sendingSockets = new DatagramSocket[(args.length - 1) / 2]; DatagramPacket[] mirroredPackets = new DatagramPacket[(args.length - 1) / 2]; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index de111213f0e..afe766680ee 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -1,3004 +1,3019 @@ -package com.sap.sailing.gwt.ui.server; - -import java.io.IOException; -import java.io.ObjectOutputStream; -import java.io.Serializable; -import java.net.MalformedURLException; -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.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.WeakHashMap; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; -import java.util.concurrent.RunnableFuture; -import java.util.logging.Logger; - -import javax.servlet.ServletContext; - -import org.osgi.framework.BundleContext; -import org.osgi.util.tracker.ServiceTracker; - -import com.sap.sailing.domain.base.BoatClass; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.ControlPoint; -import com.sap.sailing.domain.base.Course; -import com.sap.sailing.domain.base.Event; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.Gate; -import com.sap.sailing.domain.base.Leg; -import com.sap.sailing.domain.base.Mark; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceColumnInSeries; -import com.sap.sailing.domain.base.RaceDefinition; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.Series; -import com.sap.sailing.domain.base.SpeedWithBearing; -import com.sap.sailing.domain.base.Waypoint; -import com.sap.sailing.domain.base.impl.FleetImpl; -import com.sap.sailing.domain.base.impl.KnotSpeedImpl; -import com.sap.sailing.domain.base.impl.KnotSpeedWithBearingImpl; -import com.sap.sailing.domain.base.impl.MillisecondsTimePoint; -import com.sap.sailing.domain.base.impl.RegattaImpl; -import com.sap.sailing.domain.base.impl.SeriesImpl; -import com.sap.sailing.domain.common.Bearing; -import com.sap.sailing.domain.common.Color; -import com.sap.sailing.domain.common.CountryCode; -import com.sap.sailing.domain.common.DetailType; -import com.sap.sailing.domain.common.Distance; -import com.sap.sailing.domain.common.LegType; -import com.sap.sailing.domain.common.ManeuverType; -import com.sap.sailing.domain.common.MaxPointsReason; -import com.sap.sailing.domain.common.NoWindError; -import com.sap.sailing.domain.common.NoWindException; -import com.sap.sailing.domain.common.Placemark; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.RaceFetcher; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.RegattaFetcher; -import com.sap.sailing.domain.common.RegattaIdentifier; -import com.sap.sailing.domain.common.RegattaName; -import com.sap.sailing.domain.common.RegattaNameAndRaceName; -import com.sap.sailing.domain.common.RegattaScoreCorrections; -import com.sap.sailing.domain.common.RegattaScoreCorrections.ScoreCorrectionForCompetitorInRace; -import com.sap.sailing.domain.common.RegattaScoreCorrections.ScoreCorrectionsForRace; -import com.sap.sailing.domain.common.ScoreCorrectionProvider; -import com.sap.sailing.domain.common.ScoringSchemeType; -import com.sap.sailing.domain.common.Speed; -import com.sap.sailing.domain.common.Tack; -import com.sap.sailing.domain.common.TimePoint; -import com.sap.sailing.domain.common.WindSource; -import com.sap.sailing.domain.common.WindSourceType; -import com.sap.sailing.domain.common.impl.DegreeBearingImpl; -import com.sap.sailing.domain.common.impl.DegreePosition; -import com.sap.sailing.domain.common.impl.KilometersPerHourSpeedImpl; -import com.sap.sailing.domain.common.impl.MeterDistance; -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.common.impl.WindSourceImpl; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.leaderboard.Leaderboard.Entry; -import com.sap.sailing.domain.leaderboard.LeaderboardGroup; -import com.sap.sailing.domain.leaderboard.MetaLeaderboard; -import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; -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.persistence.MongoWindStoreFactory; -import com.sap.sailing.domain.swisstimingadapter.SwissTimingArchiveConfiguration; -import com.sap.sailing.domain.swisstimingadapter.SwissTimingConfiguration; -import com.sap.sailing.domain.swisstimingadapter.SwissTimingFactory; -import com.sap.sailing.domain.swisstimingadapter.persistence.SwissTimingAdapterPersistence; -import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayRace; -import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayService; -import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayServiceFactory; -import com.sap.sailing.domain.tracking.DynamicTrackedRace; -import com.sap.sailing.domain.tracking.GPSFix; -import com.sap.sailing.domain.tracking.GPSFixMoving; -import com.sap.sailing.domain.tracking.GPSFixTrack; -import com.sap.sailing.domain.tracking.Maneuver; -import com.sap.sailing.domain.tracking.MarkPassing; -import com.sap.sailing.domain.tracking.MarkPassingManeuver; -import com.sap.sailing.domain.tracking.RaceChangeListener; -import com.sap.sailing.domain.tracking.RaceTracker; -import com.sap.sailing.domain.tracking.RacesHandle; -import com.sap.sailing.domain.tracking.TrackedLeg; -import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor; -import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sailing.domain.tracking.Wind; -import com.sap.sailing.domain.tracking.WindTrack; -import com.sap.sailing.domain.tracking.WindWithConfidence; -import com.sap.sailing.domain.tracking.impl.GPSFixMovingImpl; -import com.sap.sailing.domain.tracking.impl.TrackedRaceImpl; -import com.sap.sailing.domain.tracking.impl.WindImpl; -import com.sap.sailing.domain.tractracadapter.DomainFactory; -import com.sap.sailing.domain.tractracadapter.RaceRecord; -import com.sap.sailing.domain.tractracadapter.TracTracConfiguration; -import com.sap.sailing.freg.resultimport.FregResultProvider; -import com.sap.sailing.geocoding.ReverseGeocoder; -import com.sap.sailing.gwt.ui.client.SailingService; -import com.sap.sailing.gwt.ui.shared.BoatClassDTO; -import com.sap.sailing.gwt.ui.shared.BulkScoreCorrectionDTO; -import com.sap.sailing.gwt.ui.shared.CompetitorDTO; -import com.sap.sailing.gwt.ui.shared.CompetitorRaceDataDTO; -import com.sap.sailing.gwt.ui.shared.CompetitorsRaceDataDTO; -import com.sap.sailing.gwt.ui.shared.ControlPointDTO; -import com.sap.sailing.gwt.ui.shared.CourseDTO; -import com.sap.sailing.gwt.ui.shared.EventDTO; -import com.sap.sailing.gwt.ui.shared.FleetDTO; -import com.sap.sailing.gwt.ui.shared.GPSFixDTO; -import com.sap.sailing.gwt.ui.shared.GateDTO; -import com.sap.sailing.gwt.ui.shared.LeaderboardDTO; -import com.sap.sailing.gwt.ui.shared.LeaderboardEntryDTO; -import com.sap.sailing.gwt.ui.shared.LeaderboardGroupDTO; -import com.sap.sailing.gwt.ui.shared.LeaderboardRowDTO; -import com.sap.sailing.gwt.ui.shared.LegEntryDTO; -import com.sap.sailing.gwt.ui.shared.LegInfoDTO; -import com.sap.sailing.gwt.ui.shared.ManeuverDTO; -import com.sap.sailing.gwt.ui.shared.MarkDTO; -import com.sap.sailing.gwt.ui.shared.MarkPassingTimesDTO; -import com.sap.sailing.gwt.ui.shared.MarkpassingManeuverDTO; -import com.sap.sailing.gwt.ui.shared.PlacemarkDTO; -import com.sap.sailing.gwt.ui.shared.PlacemarkOrderDTO; -import com.sap.sailing.gwt.ui.shared.PositionDTO; -import com.sap.sailing.gwt.ui.shared.QuickRankDTO; -import com.sap.sailing.gwt.ui.shared.RaceColumnDTO; -import com.sap.sailing.gwt.ui.shared.RaceColumnInSeriesDTO; -import com.sap.sailing.gwt.ui.shared.RaceCourseMarksDTO; -import com.sap.sailing.gwt.ui.shared.RaceDTO; -import com.sap.sailing.gwt.ui.shared.RaceMapDataDTO; -import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; -import com.sap.sailing.gwt.ui.shared.RaceWithCompetitorsDTO; -import com.sap.sailing.gwt.ui.shared.RegattaDTO; -import com.sap.sailing.gwt.ui.shared.RegattaScoreCorrectionDTO; -import com.sap.sailing.gwt.ui.shared.RegattaScoreCorrectionDTO.ScoreCorrectionEntryDTO; -import com.sap.sailing.gwt.ui.shared.ReplicaDTO; -import com.sap.sailing.gwt.ui.shared.ReplicationMasterDTO; -import com.sap.sailing.gwt.ui.shared.ReplicationStateDTO; -import com.sap.sailing.gwt.ui.shared.ScoreCorrectionProviderDTO; -import com.sap.sailing.gwt.ui.shared.SeriesDTO; -import com.sap.sailing.gwt.ui.shared.SpeedWithBearingDTO; -import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; -import com.sap.sailing.gwt.ui.shared.SwissTimingArchiveConfigurationDTO; -import com.sap.sailing.gwt.ui.shared.SwissTimingConfigurationDTO; -import com.sap.sailing.gwt.ui.shared.SwissTimingRaceRecordDTO; -import com.sap.sailing.gwt.ui.shared.SwissTimingReplayRaceDTO; -import com.sap.sailing.gwt.ui.shared.TracTracConfigurationDTO; -import com.sap.sailing.gwt.ui.shared.TracTracRaceRecordDTO; -import com.sap.sailing.gwt.ui.shared.TrackedRaceDTO; -import com.sap.sailing.gwt.ui.shared.VenueDTO; -import com.sap.sailing.gwt.ui.shared.WindDTO; -import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; -import com.sap.sailing.gwt.ui.shared.WindTrackInfoDTO; -import com.sap.sailing.server.RacingEventService; -import com.sap.sailing.server.RacingEventServiceOperation; -import com.sap.sailing.server.operationaltransformation.AddColumnToLeaderboard; -import com.sap.sailing.server.operationaltransformation.AddColumnToSeries; -import com.sap.sailing.server.operationaltransformation.AddSpecificRegatta; -import com.sap.sailing.server.operationaltransformation.ConnectTrackedRaceToLeaderboardColumn; -import com.sap.sailing.server.operationaltransformation.CreateEvent; -import com.sap.sailing.server.operationaltransformation.CreateFlexibleLeaderboard; -import com.sap.sailing.server.operationaltransformation.CreateLeaderboardGroup; -import com.sap.sailing.server.operationaltransformation.CreateRegattaLeaderboard; -import com.sap.sailing.server.operationaltransformation.DisconnectLeaderboardColumnFromTrackedRace; -import com.sap.sailing.server.operationaltransformation.MoveColumnInSeriesDown; -import com.sap.sailing.server.operationaltransformation.MoveColumnInSeriesUp; -import com.sap.sailing.server.operationaltransformation.MoveLeaderboardColumnDown; -import com.sap.sailing.server.operationaltransformation.MoveLeaderboardColumnUp; -import com.sap.sailing.server.operationaltransformation.RemoveAndUntrackRace; -import com.sap.sailing.server.operationaltransformation.RemoveColumnFromSeries; -import com.sap.sailing.server.operationaltransformation.RemoveEvent; -import com.sap.sailing.server.operationaltransformation.RemoveLeaderboard; -import com.sap.sailing.server.operationaltransformation.RemoveLeaderboardColumn; -import com.sap.sailing.server.operationaltransformation.RemoveLeaderboardGroup; -import com.sap.sailing.server.operationaltransformation.RemoveRegatta; -import com.sap.sailing.server.operationaltransformation.RenameEvent; -import com.sap.sailing.server.operationaltransformation.RenameLeaderboard; -import com.sap.sailing.server.operationaltransformation.RenameLeaderboardColumn; -import com.sap.sailing.server.operationaltransformation.RenameLeaderboardGroup; -import com.sap.sailing.server.operationaltransformation.SetRaceIsKnownToStartUpwind; -import com.sap.sailing.server.operationaltransformation.SetSuppressedFlagForCompetitorInLeaderboard; -import com.sap.sailing.server.operationaltransformation.SetWindSourcesToExclude; -import com.sap.sailing.server.operationaltransformation.StopTrackingRace; -import com.sap.sailing.server.operationaltransformation.StopTrackingRegatta; -import com.sap.sailing.server.operationaltransformation.UpdateCompetitorDisplayNameInLeaderboard; -import com.sap.sailing.server.operationaltransformation.UpdateEvent; -import com.sap.sailing.server.operationaltransformation.UpdateIsMedalRace; -import com.sap.sailing.server.operationaltransformation.UpdateLeaderboard; -import com.sap.sailing.server.operationaltransformation.UpdateLeaderboardCarryValue; -import com.sap.sailing.server.operationaltransformation.UpdateLeaderboardColumnFactor; -import com.sap.sailing.server.operationaltransformation.UpdateLeaderboardGroup; -import com.sap.sailing.server.operationaltransformation.UpdateLeaderboardMaxPointsReason; -import com.sap.sailing.server.operationaltransformation.UpdateLeaderboardScoreCorrection; -import com.sap.sailing.server.operationaltransformation.UpdateLeaderboardScoreCorrectionMetadata; -import com.sap.sailing.server.operationaltransformation.UpdateRaceDelayToLive; -import com.sap.sailing.server.replication.ReplicaDescriptor; -import com.sap.sailing.server.replication.ReplicationFactory; -import com.sap.sailing.server.replication.ReplicationMasterDescriptor; -import com.sap.sailing.server.replication.ReplicationService; - -/** - * The server side implementation of the RPC service. - */ -public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements SailingService, RaceFetcher, RegattaFetcher { - private static final Logger logger = Logger.getLogger(SailingServiceImpl.class.getName()); - - private static final long serialVersionUID = 9031688830194537489L; - - private final ServiceTracker racingEventServiceTracker; - - private final ServiceTracker replicationServiceTracker; - - private final ServiceTracker scoreCorrectionProviderServiceTracker; - - private final MongoObjectFactory mongoObjectFactory; - - private final SwissTimingAdapterPersistence swissTimingAdapterPersistence; - - private final com.sap.sailing.domain.tractracadapter.persistence.MongoObjectFactory tractracMongoObjectFactory; - - private final DomainObjectFactory domainObjectFactory; - - private final SwissTimingFactory swissTimingFactory; - - private final com.sap.sailing.domain.tractracadapter.persistence.DomainObjectFactory tractracDomainObjectFactory; - - private final com.sap.sailing.domain.common.CountryCodeFactory countryCodeFactory; - - private final Executor executor; - - /** - * {@link #getLeaderboardByName(String, Date, Collection)} is the application's bottleneck. When two clients ask the - * same data for the same leaderboard with their waitForLatestAnalyses parameters set to - * false, expansion state and (quantized) time stamp, no two computations should be spawned for the two - * clients. Instead, if the computation is still running, all clients asking the same wait for the single result. - * Results are cached in this LRU-based evicting cache. - */ - private final Map leaderboardByNameLiveUpdaters; - - /** - * This executor needs to be a different one than {@link #executor} because the tasks run by {@link #executor} - * can depend on the results of the tasks run by {@link #raceDetailsExecutor}, and an {@link Executor} doesn't - * move a task that is blocked by waiting for another {@link FutureTask} to the side but blocks permanently, - * ending in a deadlock (one that cannot easily be detected by the Eclipse debugger either). - */ - private final Executor raceDetailsExecutor; - - private final WeakHashMap weakCompetitorDTOCache; - - private final Map, RunnableFuture> raceDetailsAtEndOfTrackingCache; - - /** - * Used to remove all these listeners from their tracked races when this servlet is {@link #destroy() destroyed}. - */ - private final Set cacheInvalidationListeners; - - private final LeaderboardDTOCache leaderboardDTOCacheWaitingForLatestAnalysis; - - private final DomainFactory tractracDomainFactory; - - private final com.sap.sailing.domain.base.DomainFactory baseDomainFactory; - - public SailingServiceImpl() { - BundleContext context = Activator.getDefault(); - tractracDomainFactory = DomainFactory.INSTANCE; - baseDomainFactory = tractracDomainFactory.getBaseDomainFactory(); - raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); - cacheInvalidationListeners = new HashSet(); - weakCompetitorDTOCache = new WeakHashMap(); - racingEventServiceTracker = createAndOpenRacingEventServiceTracker(context); - replicationServiceTracker = createAndOpenReplicationServiceTracker(context); - scoreCorrectionProviderServiceTracker = createAndOpenScoreCorrectionProviderServiceTracker(context); - mongoObjectFactory = MongoFactory.INSTANCE.getDefaultMongoObjectFactory(); - domainObjectFactory = MongoFactory.INSTANCE.getDefaultDomainObjectFactory(); - swissTimingAdapterPersistence = SwissTimingAdapterPersistence.INSTANCE; - tractracDomainObjectFactory = com.sap.sailing.domain.tractracadapter.persistence.DomainObjectFactory.INSTANCE; - tractracMongoObjectFactory = com.sap.sailing.domain.tractracadapter.persistence.MongoObjectFactory.INSTANCE; - swissTimingFactory = SwissTimingFactory.INSTANCE; - countryCodeFactory = com.sap.sailing.domain.common.CountryCodeFactory.INSTANCE; - executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); - raceDetailsExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); - leaderboardByNameLiveUpdaters = new HashMap(); - leaderboardDTOCacheWaitingForLatestAnalysis = new LeaderboardDTOCache(this, /* waitForLatestAnalyses */ true); - } - - private void writeObject(ObjectOutputStream oos) throws IOException { - oos.defaultWriteObject(); - } - - public void destroy() { - super.destroy(); - for (CacheInvalidationListener cacheInvalidationListener : cacheInvalidationListeners) { - cacheInvalidationListener.removeFromTrackedRace(); - } - } - - protected ServiceTracker createAndOpenRacingEventServiceTracker( - BundleContext context) { - ServiceTracker result = new ServiceTracker( - context, RacingEventService.class.getName(), null); - result.open(); - return result; - } - - /** - * Asks the OSGi system for registered score correction provider services - */ - private ServiceTracker createAndOpenScoreCorrectionProviderServiceTracker( - BundleContext bundleContext) { - ServiceTracker tracker = new ServiceTracker(bundleContext, - ScoreCorrectionProvider.class.getName(), - /* customizer */null); - tracker.open(); - return tracker; - } - - @Override - public Iterable getScoreCorrectionProviderDTOs() throws Exception { - List result = new ArrayList(); - final Iterable services = getScoreCorrectionProviders(); - for (ScoreCorrectionProvider scoreCorrectionProvider : services) { - result.add(createScoreCorrectionProviderDTO((ScoreCorrectionProvider) scoreCorrectionProvider)); - } - return result; - } - - private Iterable getScoreCorrectionProviders() { - final Object[] services = scoreCorrectionProviderServiceTracker.getServices(); - List result = new ArrayList(); - if (services != null) { - for (Object service : services) { - result.add((ScoreCorrectionProvider) service); - } - } - return result; - } - - private ScoreCorrectionProviderDTO createScoreCorrectionProviderDTO(ScoreCorrectionProvider scoreCorrectionProvider) - throws Exception { - Map>> hasResultsForBoatClassFromDateByEventName = new HashMap>>(); - for (Map.Entry>> e : scoreCorrectionProvider - .getHasResultsForBoatClassFromDateByEventName().entrySet()) { - Set> set = new HashSet>(); - for (Pair p : e.getValue()) { - set.add(new Pair(p.getA(), p.getB().asDate())); - } - hasResultsForBoatClassFromDateByEventName.put(e.getKey(), set); - } - return new ScoreCorrectionProviderDTO(scoreCorrectionProvider.getName(), hasResultsForBoatClassFromDateByEventName); - } - - protected ServiceTracker createAndOpenReplicationServiceTracker( - BundleContext context) { - ServiceTracker result = new ServiceTracker( - context, ReplicationService.class.getName(), null); - result.open(); - return result; - } - - /** - * If date is null, the {@link LiveLeaderboardUpdater} for the - * leaderboardName requested is obtained or created if it doesn't exist yet. The request is then passed - * on to the live leaderboard updater which will respond with its live {@link LeaderboardDTO} if it has at least the - * columns requested as per namesOfRaceColumnsForWhichToLoadLegDetails. Otherwise, the updater will add - * the missing columns to its profile and start a synchronous computation for the requesting client, the result of - * which will be used as live leaderboard cache update. - *

- * - * Otherwise, the leaderboard is {@link #computeLeaderboardByName(String, Date, Collection, boolean) computed} - * synchronously on the fly. - */ - @Override - public LeaderboardDTO getLeaderboardByName(final String leaderboardName, final Date date, - final Collection namesOfRaceColumnsForWhichToLoadLegDetails) - throws NoWindException, InterruptedException, ExecutionException { - long startOfRequestHandling = System.currentTimeMillis(); - LeaderboardDTO result = null; - TimePoint timePoint; - LiveLeaderboardUpdater liveLeaderboardUpdater; - final Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName); - if (leaderboard != null) { - if (date == null) { - // date==null means live mode; however, if we're after the end of all races and after all score - // corrections, don't use the live leaderboard updater which would keep re-calculating over and over again, but map - // this to a usual non-live call which uses the regular LeaderboardDTOCache which is invalidated properly - // when the tracked race associations or score corrections or tracked race contents changes: - // TODO see bug 929: use lock instead of synchronized to increase concurrency - synchronized (leaderboardByNameLiveUpdaters) { - liveLeaderboardUpdater = leaderboardByNameLiveUpdaters.get(leaderboardName); - if (liveLeaderboardUpdater == null) { - liveLeaderboardUpdater = new LiveLeaderboardUpdater(leaderboard, this); - leaderboardByNameLiveUpdaters.put(leaderboardName, liveLeaderboardUpdater); - } - } - final TimePoint nowMinusDelay = leaderboard.getNowMinusDelay(); - final TimePoint timePointOfLatestModification = leaderboard.getTimePointOfLatestModification(); - if (timePointOfLatestModification != null && !nowMinusDelay.before(timePointOfLatestModification)) { - timePoint = timePointOfLatestModification; - } else { - timePoint = null; - result = liveLeaderboardUpdater.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, - nowMinusDelay); - } - } else { - timePoint = new MillisecondsTimePoint(date); - } - if (timePoint != null) { - // in replay we'd like up-to-date results; they are still cached - // which is OK because the cache is invalidated whenever any of the tracked races attached to the - // leaderboard changes. - result = leaderboardDTOCacheWaitingForLatestAnalysis.getLeaderboardByName(leaderboard, timePoint, - namesOfRaceColumnsForWhichToLoadLegDetails); - } - logger.fine("getLeaderboardByName(" + leaderboardName + ", " + date + ", " - + namesOfRaceColumnsForWhichToLoadLegDetails + ") took " - + (System.currentTimeMillis() - startOfRequestHandling) + "ms"); - } - return result; - } - - protected LeaderboardDTO computeLeaderboardByName(final Leaderboard leaderboard, final TimePoint timePoint, - final Collection namesOfRaceColumnsForWhichToLoadLegDetails, final boolean waitForLatestAnalyses) - throws NoWindException { - long startOfRequestHandling = System.currentTimeMillis(); - LeaderboardDTO result = null; - if (leaderboard != null) { - result = new LeaderboardDTO(leaderboard.getScoreCorrection().getTimePointOfLastCorrectionsValidity()==null ? - null : leaderboard.getScoreCorrection().getTimePointOfLastCorrectionsValidity().asDate(), - leaderboard.getScoreCorrection()==null?null:leaderboard.getScoreCorrection().getComment(), - leaderboard.getScoringScheme().isHigherBetter()); - result.competitors = new ArrayList(); - result.name = leaderboard.getName(); - result.competitorDisplayNames = new HashMap(); - for (Competitor suppressedCompetitor : leaderboard.getSuppressedCompetitors()) { - result.setSuppressed(convertToCompetitorDTO(suppressedCompetitor), true); - } - for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - RaceColumnDTO raceColumnDTO = result.createEmptyRaceColumn(raceColumn.getName(), raceColumn.isMedalRace(), - leaderboard.getScoringScheme().isValidInTotalScore(leaderboard, raceColumn, timePoint)); - for (Fleet fleet : raceColumn.getFleets()) { - TimePoint latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive = null; - RegattaAndRaceIdentifier raceIdentifier = null; - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - final FleetDTO fleetDTO = convertToFleetDTO(fleet); - if (trackedRace != null) { - raceIdentifier = new RegattaNameAndRaceName(trackedRace.getTrackedRegatta().getRegatta() - .getName(), trackedRace.getRace().getName()); - if (trackedRace.hasStarted(timePoint) && trackedRace.hasGPSData() && trackedRace.hasWindData()) { - TimePoint liveTimePointForTrackedRace = timePoint; - final TimePoint endOfRace = trackedRace.getEndOfRace(); - if (endOfRace != null) { - liveTimePointForTrackedRace = endOfRace; - } - latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive = liveTimePointForTrackedRace; - } - } - // Note: the RaceColumnDTO won't be created by the following addRace call because it has been created - // above by the result.createEmptyRaceColumn call - result.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), fleetDTO, - raceColumn.isMedalRace(), raceIdentifier, /* StrippedRaceDTO */ null); - if (latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive != null) { - raceColumnDTO.setWhenLastTrackedRaceWasLive(fleetDTO, latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive.asDate()); - } - } - result.setCompetitorsFromBestToWorst(raceColumnDTO, getCompetitorDTOList(leaderboard.getCompetitorsFromBestToWorst(raceColumn, timePoint))); - } - result.setDelayToLiveInMillisForLatestRace(leaderboard.getDelayToLiveInMillis()); - result.rows = new HashMap(); - result.hasCarriedPoints = leaderboard.hasCarriedPoints(); - result.discardThresholds = leaderboard.getResultDiscardingRule().getDiscardIndexResultsStartingWithHowManyRaces(); - // Computing the competitor leg ranks is expensive, especially in live mode, in case new events keep invalidating - // the ranks cache in TrackedLegImpl. Then problem then is that the sorting based on wind data is repeated for each - // competitor, leading to square effort. We therefore need to compute the leg ranks for those race where leg details - // are requested only once and pass them into getLeaderboardEntryDTO - final Map> legRanksCache = new HashMap>(); - for (final RaceColumn raceColumn : leaderboard.getRaceColumns()) { - // if details for the column are requested, cache the leg's ranks - if (namesOfRaceColumnsForWhichToLoadLegDetails != null - && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn.getName())) { - for (Fleet fleet : raceColumn.getFleets()) { - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - if (trackedRace != null) { - trackedRace.getRace().getCourse().lockForRead(); - try { - for (TrackedLeg trackedLeg : trackedRace.getTrackedLegs()) { - legRanksCache.put(trackedLeg.getLeg(), trackedLeg.getRanks(timePoint)); - } - } finally { - trackedRace.getRace().getCourse().unlockAfterRead(); - } - } - } - } - } - for (final Competitor competitor : leaderboard.getCompetitorsFromBestToWorst(timePoint)) { - CompetitorDTO competitorDTO = convertToCompetitorDTO(competitor); - LeaderboardRowDTO row = new LeaderboardRowDTO(); - row.competitor = competitorDTO; - row.fieldsByRaceColumnName = new HashMap(); - row.carriedPoints = leaderboard.hasCarriedPoints(competitor) ? leaderboard.getCarriedPoints(competitor) : null; - addOverallDetailsToRow(leaderboard, timePoint, competitor, row); - result.competitors.add(competitorDTO); - Map> futuresForColumnName = new HashMap>(); - for (final RaceColumn raceColumn : leaderboard.getRaceColumns()) { - RunnableFuture future = new FutureTask(new Callable() { - @Override - public LeaderboardEntryDTO call() { - try { - Entry entry = leaderboard.getEntry(competitor, raceColumn, timePoint); - return getLeaderboardEntryDTO(entry, raceColumn.getTrackedRace(competitor), competitor, timePoint, - namesOfRaceColumnsForWhichToLoadLegDetails != null - && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn.getName()), - waitForLatestAnalyses, legRanksCache); - } catch (NoWindException e) { - throw new NoWindError(e); - } - } - }); - executor.execute(future); - futuresForColumnName.put(raceColumn.getName(), future); - } - for (Map.Entry> raceColumnNameAndFuture : futuresForColumnName.entrySet()) { - try { - row.fieldsByRaceColumnName.put(raceColumnNameAndFuture.getKey(), raceColumnNameAndFuture.getValue().get()); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - throw new RuntimeException(e); - } - } - result.rows.put(competitorDTO, row); - String displayName = leaderboard.getDisplayName(competitor); - if (displayName != null) { - result.competitorDisplayNames.put(competitorDTO, displayName); - } - } - } - logger.fine("computeLeaderboardByName("+leaderboard.getName()+", "+timePoint+", "+namesOfRaceColumnsForWhichToLoadLegDetails+") took "+ - (System.currentTimeMillis()-startOfRequestHandling)+"ms"); - return result; - } - - private void addOverallDetailsToRow(final Leaderboard leaderboard, final TimePoint timePoint, - final Competitor competitor, LeaderboardRowDTO row) throws NoWindException { - final Pair maximumSpeedOverGround = leaderboard.getMaximumSpeedOverGround(competitor, timePoint); - if (maximumSpeedOverGround != null && maximumSpeedOverGround.getB() != null) { - row.maximumSpeedOverGroundInKnots = maximumSpeedOverGround.getB().getKnots(); - row.whenMaximumSpeedOverGroundWasAchieved = maximumSpeedOverGround.getA().getTimePoint().asDate(); - } - final Long totalTimeSailedDownwindInMilliseconds = leaderboard.getTotalTimeSailedInLegTypeInMilliseconds(competitor, LegType.DOWNWIND, timePoint); - row.totalTimeSailedDownwindInSeconds = totalTimeSailedDownwindInMilliseconds==null?null:1./1000.*totalTimeSailedDownwindInMilliseconds; - final Long totalTimeSailedUpwindInMilliseconds = leaderboard.getTotalTimeSailedInLegTypeInMilliseconds(competitor, LegType.UPWIND, timePoint); - row.totalTimeSailedUpwindInSeconds = totalTimeSailedUpwindInMilliseconds==null?null:1./1000.*totalTimeSailedUpwindInMilliseconds; - final Long totalTimeSailedReachingInMilliseconds = leaderboard.getTotalTimeSailedInLegTypeInMilliseconds(competitor, LegType.REACHING, timePoint); - row.totalTimeSailedReachingInSeconds = totalTimeSailedReachingInMilliseconds==null?null:1./1000.*totalTimeSailedReachingInMilliseconds; - final Long totalTimeSailedInMilliseconds = leaderboard.getTotalTimeSailedInMilliseconds(competitor, timePoint); - row.totalTimeSailedInSeconds = totalTimeSailedInMilliseconds==null?null:1./1000.*totalTimeSailedInMilliseconds; - } - - private List getCompetitorDTOList(List competitors) { - List result = new ArrayList(); - for (Competitor competitor : competitors) { - result.add(convertToCompetitorDTO(competitor)); - } - return result; - } - - /** - * @param waitForLatestAnalyses - * if false, this method is allowed to read the maneuver analysis results from a cache that - * may not reflect all data already received; otherwise, the method will always block for the latest - * cache updates to have happened before returning. - */ - private LeaderboardEntryDTO getLeaderboardEntryDTO(Entry entry, TrackedRace trackedRace, Competitor competitor, - TimePoint timePoint, boolean addLegDetails, boolean waitForLatestAnalyses, - Map> legRanksCache) throws NoWindException { - LeaderboardEntryDTO entryDTO = new LeaderboardEntryDTO(); - entryDTO.race = trackedRace == null ? null : trackedRace.getRaceIdentifier(); - entryDTO.netPoints = entry.getNetPoints(); - entryDTO.netPointsCorrected = entry.isNetPointsCorrected(); - entryDTO.totalPoints = entry.getTotalPoints(); - entryDTO.reasonForMaxPoints = entry.getMaxPointsReason(); - entryDTO.discarded = entry.isDiscarded(); - if (addLegDetails && trackedRace != null) { - try { - RaceDetails raceDetails = getRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, legRanksCache); - entryDTO.legDetails = raceDetails.getLegDetails(); - entryDTO.windwardDistanceToOverallLeaderInMeters = raceDetails.getWindwardDistanceToOverallLeader() == null ? null - : raceDetails.getWindwardDistanceToOverallLeader().getMeters(); - entryDTO.averageCrossTrackErrorInMeters = raceDetails.getAverageCrossTrackError() == null ? null - : raceDetails.getAverageCrossTrackError().getMeters(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - throw new RuntimeException(e); // the future used to calculate the leg details was interrupted; escalate as runtime exception - } - } - final Fleet fleet = entry.getFleet(); - entryDTO.fleet = fleet == null ? null : convertToFleetDTO(fleet); - return entryDTO; - } - - private static class RaceDetails { - private final List legDetails; - private final Distance windwardDistanceToOverallLeader; - private final Distance averageCrossTrackError; - public RaceDetails(List legDetails, Distance windwardDistanceToOverallLeader, - Distance averageCrossTrackError) { - super(); - this.legDetails = legDetails; - this.windwardDistanceToOverallLeader = windwardDistanceToOverallLeader; - this.averageCrossTrackError = averageCrossTrackError; - } - public List getLegDetails() { - return legDetails; - } - public Distance getWindwardDistanceToOverallLeader() { - return windwardDistanceToOverallLeader; - } - public Distance getAverageCrossTrackError() { - return averageCrossTrackError; - } - } - - /** - * If timePoint is after the end of the race's tracking the query will be adjusted to obtain the values - * at the end of the {@link TrackedRace#getEndOfTracking() race's tracking time}. If the time point adjusted this - * way equals the end of the tracking time, the query results will be looked up in a cache first and if not found, - * they will be stored to the cache after calculating them. A cache invalidation {@link RaceChangeListener listener} - * will be registered with the race which will be triggered for any event received by the race. - * @param waitForLatestAnalyses - * if false, this method is allowed to read the maneuver analysis results from a cache that - * may not reflect all data already received; otherwise, the method will always block for the latest - * cache updates to have happened before returning. - */ - private RaceDetails getRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, - boolean waitForLatestAnalyses, Map> legRanksCache) throws NoWindException, InterruptedException, ExecutionException { - RaceDetails raceDetails; - if (trackedRace.getEndOfTracking() != null && trackedRace.getEndOfTracking().compareTo(timePoint) < 0) { - raceDetails = getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(trackedRace, competitor, legRanksCache); - } else { - raceDetails = calculateRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, legRanksCache); - } - return raceDetails; - } - - private RaceDetails getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(final TrackedRace trackedRace, - final Competitor competitor, final Map> legRanksCache) - throws NoWindException, InterruptedException, ExecutionException { - final Pair key = new Pair(trackedRace, competitor); - RunnableFuture raceDetails; - synchronized (raceDetailsAtEndOfTrackingCache) { - raceDetails = raceDetailsAtEndOfTrackingCache.get(key); - if (raceDetails == null) { - raceDetails = new FutureTask(new Callable() { - @Override - public RaceDetails call() throws Exception { - return calculateRaceDetails(trackedRace, competitor, trackedRace.getEndOfTracking(), - /* waitForLatestManeuverAnalysis */ true /* because this is done only once after end of tracking */, legRanksCache); - } - }); - raceDetailsExecutor.execute(raceDetails); - raceDetailsAtEndOfTrackingCache.put(key, raceDetails); - final CacheInvalidationListener cacheInvalidationListener = new CacheInvalidationListener(trackedRace, - competitor); - trackedRace.addListener(cacheInvalidationListener); - cacheInvalidationListeners.add(cacheInvalidationListener); - } - } - return raceDetails.get(); - } - - /** - * Handles the invalidation of the {@link SailingServiceImpl#raceDetailsAtEndOfTrackingCache} entries if the tracked race - * changes in any way. - * - * @author Axel Uhl (D043530) - * - */ - private class CacheInvalidationListener implements RaceChangeListener { - private final TrackedRace trackedRace; - private final Competitor competitor; - - public CacheInvalidationListener(TrackedRace trackedRace, Competitor competitor) { - this.trackedRace = trackedRace; - this.competitor = competitor; - } - - public void removeFromTrackedRace() { - trackedRace.removeListener(this); - } - - private void invalidateCacheAndRemoveThisListenerFromTrackedRace() { - synchronized (raceDetailsAtEndOfTrackingCache) { - raceDetailsAtEndOfTrackingCache.remove(new Pair(trackedRace, competitor)); - removeFromTrackedRace(); - } - } - - @Override - public void competitorPositionChanged(GPSFixMoving fix, Competitor competitor) { - invalidateCacheAndRemoveThisListenerFromTrackedRace(); - } - - @Override - public void markPositionChanged(GPSFix fix, Mark mark) { - invalidateCacheAndRemoveThisListenerFromTrackedRace(); - } - - @Override - public void markPassingReceived(Competitor competitor, Map oldMarkPassings, - Iterable markPassings) { - invalidateCacheAndRemoveThisListenerFromTrackedRace(); - } - - @Override - public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) { - invalidateCacheAndRemoveThisListenerFromTrackedRace(); - } - - @Override - public void windDataReceived(Wind wind, WindSource windSource) { - invalidateCacheAndRemoveThisListenerFromTrackedRace(); - } - - @Override - public void windDataRemoved(Wind wind, WindSource windSource) { - invalidateCacheAndRemoveThisListenerFromTrackedRace(); - } - - @Override - public void windAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) { - invalidateCacheAndRemoveThisListenerFromTrackedRace(); - } - - @Override - public void raceTimesChanged(TimePoint startOfTracking, TimePoint endOfTracking, TimePoint startTimeReceived) { - invalidateCacheAndRemoveThisListenerFromTrackedRace(); - } - - @Override - public void delayToLiveChanged(long delayToLiveInMillis) { - invalidateCacheAndRemoveThisListenerFromTrackedRace(); - } - - @Override - public void windSourcesToExcludeChanged(Iterable windSourcesToExclude) { - invalidateCacheAndRemoveThisListenerFromTrackedRace(); - } - } - - private RaceDetails calculateRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, - boolean waitForLatestAnalyses, Map> legRanksCache) throws NoWindException { - List legDetails; - legDetails = new ArrayList(); - final Course course = trackedRace.getRace().getCourse(); - course.lockForRead(); // hold back any course re-configurations while looping over the legs - try { - for (Leg leg : course.getLegs()) { - LegEntryDTO legEntry; - // We loop over a copy of the course's legs; during a course change, legs may become "stale," even with - // regard to the leg/trackedLeg structures inside the tracked race which is updated by the course change - // immediately. Make sure we're tolerant against disappearing legs! See bug 794. - TrackedLegOfCompetitor trackedLeg = trackedRace.getTrackedLeg(competitor, leg); - if (trackedLeg != null && trackedLeg.hasStartedLeg(timePoint)) { - legEntry = createLegEntry(trackedLeg, timePoint, waitForLatestAnalyses, legRanksCache); - } else { - legEntry = null; - } - legDetails.add(legEntry); - } - final Distance windwardDistanceToOverallLeader = trackedRace == null ? null : trackedRace - .getWindwardDistanceToOverallLeader(competitor, timePoint); - final Distance averageCrossTrackError = trackedRace == null ? null : trackedRace.getAverageCrossTrackError( - competitor, timePoint, waitForLatestAnalyses); - return new RaceDetails(legDetails, windwardDistanceToOverallLeader, averageCrossTrackError); - } finally { - course.unlockAfterRead(); - } - } - - private LegEntryDTO createLegEntry(TrackedLegOfCompetitor trackedLeg, TimePoint timePoint, - boolean waitForLatestAnalyses, Map> legRanksCache) throws NoWindException { - LegEntryDTO result; - if (trackedLeg == null) { - result = null; - } else { - result = new LegEntryDTO(); - final Speed averageSpeedOverGround = trackedLeg.getAverageSpeedOverGround(timePoint); - result.averageSpeedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); - final Distance averageCrossTrackError = trackedLeg.getAverageCrossTrackError(timePoint, waitForLatestAnalyses); - result.averageCrossTrackErrorInMeters = averageCrossTrackError == null ? null : averageCrossTrackError.getMeters(); - Double speedOverGroundInKnots; - if (trackedLeg.hasFinishedLeg(timePoint)) { - speedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); - } else { - final SpeedWithBearing speedOverGround = trackedLeg.getSpeedOverGround(timePoint); - speedOverGroundInKnots = speedOverGround == null ? null : speedOverGround.getKnots(); - } - result.currentSpeedOverGroundInKnots = speedOverGroundInKnots == null ? null : speedOverGroundInKnots; - Distance distanceTraveled = trackedLeg.getDistanceTraveled(timePoint); - result.distanceTraveledInMeters = distanceTraveled == null ? null : distanceTraveled.getMeters(); - result.estimatedTimeToNextWaypointInSeconds = trackedLeg.getEstimatedTimeToNextMarkInSeconds(timePoint); - result.timeInMilliseconds = trackedLeg.getTimeInMilliSeconds(timePoint); - result.finished = trackedLeg.hasFinishedLeg(timePoint); - result.gapToLeaderInSeconds = trackedLeg.getGapToLeaderInSeconds(timePoint, - legRanksCache.get(trackedLeg.getLeg()).entrySet().iterator().next().getKey()); - if (result.gapToLeaderInSeconds != null) { - // FIXME problem: asking just after the beginning of the leg yields very different values from asking for the end of the previous leg. - // This is because for the previous leg it's decided based on the mark passings; for the next (current) leg it's decided based on - // windward distance and VMG - Double gapAtEndOfPreviousLegInSeconds = getGapAtEndOfPreviousLegInSeconds(trackedLeg); - if (gapAtEndOfPreviousLegInSeconds != null) { - result.gapChangeSinceLegStartInSeconds = result.gapToLeaderInSeconds - gapAtEndOfPreviousLegInSeconds; - } - } - LinkedHashMap legRanks = legRanksCache.get(trackedLeg.getLeg()); - if (legRanks != null) { - result.rank = legRanks.get(trackedLeg.getCompetitor()); - } else { - result.rank = trackedLeg.getRank(timePoint); - } - result.started = trackedLeg.hasStartedLeg(timePoint); - Speed velocityMadeGood; - if (trackedLeg.hasFinishedLeg(timePoint)) { - velocityMadeGood = trackedLeg.getAverageVelocityMadeGood(timePoint); - } else { - velocityMadeGood = trackedLeg.getVelocityMadeGood(timePoint); - } - result.velocityMadeGoodInKnots = velocityMadeGood == null ? null : velocityMadeGood.getKnots(); - Distance windwardDistanceToGo = trackedLeg.getWindwardDistanceToGo(timePoint); - result.windwardDistanceToGoInMeters = windwardDistanceToGo == null ? null : windwardDistanceToGo - .getMeters(); - final TimePoint startOfRace = trackedLeg.getTrackedLeg().getTrackedRace().getStartOfRace(); - if (startOfRace != null && trackedLeg.hasStartedLeg(timePoint)) { - // not using trackedLeg.getManeuvers(...) because it may not catch the mark passing maneuver starting this leg - // because that may have been detected as slightly before the mark passing time, hence associated with the previous leg - List maneuvers = trackedLeg.getTrackedLeg().getTrackedRace() - .getManeuvers(trackedLeg.getCompetitor(), startOfRace, timePoint, waitForLatestAnalyses); - if (maneuvers != null) { - result.numberOfManeuvers = new HashMap(); - result.numberOfManeuvers.put(ManeuverType.TACK, 0); - result.numberOfManeuvers.put(ManeuverType.JIBE, 0); - result.numberOfManeuvers.put(ManeuverType.PENALTY_CIRCLE, 0); - Map totalManeuverLossInMeters = new HashMap(); - totalManeuverLossInMeters.put(ManeuverType.TACK, 0.0); - totalManeuverLossInMeters.put(ManeuverType.JIBE, 0.0); - totalManeuverLossInMeters.put(ManeuverType.PENALTY_CIRCLE, 0.0); - TimePoint startOfLeg = trackedLeg.getStartTime(); - for (Maneuver maneuver : maneuvers) { - // don't count maneuvers that were in previous legs - switch (maneuver.getType()) { - case TACK: - case JIBE: - case PENALTY_CIRCLE: - if (!maneuver.getTimePoint().before(startOfLeg) && (!trackedLeg.hasFinishedLeg(timePoint) || - maneuver.getTimePoint().before(trackedLeg.getFinishTime()))) { - if (maneuver.getManeuverLoss() != null) { - result.numberOfManeuvers.put(maneuver.getType(), - result.numberOfManeuvers.get(maneuver.getType()) + 1); - totalManeuverLossInMeters.put(maneuver.getType(), - totalManeuverLossInMeters.get(maneuver.getType()) - + maneuver.getManeuverLoss().getMeters()); - } - } - break; - case MARK_PASSING: - // analyze all mark passings, not only those after this leg's start, to catch the mark passing - // maneuver starting this leg, even if its time point is slightly before the mark passing starting this leg - MarkPassingManeuver mpm = (MarkPassingManeuver) maneuver; - if (mpm.getWaypointPassed() == trackedLeg.getLeg().getFrom()) { - result.sideToWhichMarkAtLegStartWasRounded = mpm.getSide(); - } - break; - } - } - result.averageManeuverLossInMeters = new HashMap(); - for (ManeuverType maneuverType : new ManeuverType[] { ManeuverType.TACK, ManeuverType.JIBE, - ManeuverType.PENALTY_CIRCLE }) { - if (result.numberOfManeuvers.get(maneuverType) != 0) { - result.averageManeuverLossInMeters.put( - maneuverType, - totalManeuverLossInMeters.get(maneuverType) - / result.numberOfManeuvers.get(maneuverType)); - } - } - } - } - } - return result; - } - - private Double getGapAtEndOfPreviousLegInSeconds(TrackedLegOfCompetitor trackedLeg) throws NoWindException { - final Double result; - final Course course = trackedLeg.getTrackedLeg().getTrackedRace().getRace().getCourse(); - course.lockForRead(); - try { - int indexOfStartWaypoint = course.getIndexOfWaypoint(trackedLeg.getLeg().getFrom()); - if (indexOfStartWaypoint == 0) { - // trackedLeg was the first leg; gap is determined by gap of start line passing time points - Iterable markPassingsForLegStart = trackedLeg.getTrackedLeg().getTrackedRace().getMarkPassingsInOrder(trackedLeg.getLeg().getFrom()); - trackedLeg.getTrackedLeg().getTrackedRace().lockForRead(markPassingsForLegStart); - try { - final Iterator markPassingsIter = markPassingsForLegStart.iterator(); - if (markPassingsIter.hasNext()) { - TimePoint firstStart = markPassingsIter.next().getTimePoint(); - final MarkPassing markPassingForFrom = trackedLeg.getTrackedLeg().getTrackedRace() - .getMarkPassing(trackedLeg.getCompetitor(), trackedLeg.getLeg().getFrom()); - if (markPassingForFrom != null) { - TimePoint competitorStart = markPassingForFrom.getTimePoint(); - result = (double) (competitorStart.asMillis() - firstStart.asMillis()) / 1000.; - } else { - result = null; - } - } else { - result = null; - } - } finally { - trackedLeg.getTrackedLeg().getTrackedRace().unlockAfterRead(markPassingsForLegStart); - } - } else { - TrackedLeg previousTrackedLeg = trackedLeg.getTrackedLeg().getTrackedRace().getTrackedLeg(course.getLegs().get(indexOfStartWaypoint-1)); - TrackedLegOfCompetitor previousTrackedLegOfCompetitor = previousTrackedLeg.getTrackedLeg(trackedLeg.getCompetitor()); - result = previousTrackedLegOfCompetitor.getGapToLeaderInSeconds(previousTrackedLegOfCompetitor.getFinishTime()); - } - return result; - } finally { - course.unlockAfterRead(); - } - } - - @Override - public List getRegattas() throws IllegalArgumentException { - List result = new ArrayList(); - for (Regatta regatta : getService().getAllRegattas()) { - result.add(convertToRegattaDTO(regatta)); - } - return result; - } - - private MarkDTO convertToMarkDTO(Mark mark, Position position) { - MarkDTO markDTO = new MarkDTO(mark.getName(), position.getLatDeg(), position.getLngDeg()); - markDTO.color = mark.getColor(); - markDTO.shape = mark.getShape(); - markDTO.pattern = mark.getPattern(); - markDTO.type = mark.getType(); - return markDTO; - } - - private RegattaDTO convertToRegattaDTO(Regatta regatta) { - RegattaDTO regattaDTO = new RegattaDTO(regatta.getName(), regatta.getScoringScheme().getType()); - regattaDTO.races = convertToRaceDTOs(regatta); - regattaDTO.series = convertToSeriesDTOs(regatta); - BoatClass boatClass = regatta.getBoatClass(); - if (boatClass != null) { - regattaDTO.boatClass = new BoatClassDTO(boatClass.getName(), boatClass.getHullLength().getMeters()); - } - return regattaDTO; - } - - private List convertToSeriesDTOs(Regatta regatta) { - List result = new ArrayList(); - for (Series series : regatta.getSeries()) { - SeriesDTO seriesDTO = convertToSeriesDTO(series); - result.add(seriesDTO); - } - return result; - } - - private SeriesDTO convertToSeriesDTO(Series series) { - List fleets = new ArrayList(); - for (Fleet fleet : series.getFleets()) { - fleets.add(convertToFleetDTO(fleet)); - } - List raceColumns = new ArrayList(); - for (RaceColumnInSeries raceColumn : series.getRaceColumns()) { - RaceColumnDTO raceColumnDTO = new RaceColumnDTO(/* isValidInTotalScore not relevant here because no scores conveyed */ null); - raceColumnDTO.name = raceColumn.getName(); - raceColumnDTO.setMedalRace(raceColumn.isMedalRace()); - raceColumnDTO.setExplicitFactor(raceColumn.getExplicitFactor()); - raceColumns.add(raceColumnDTO); - } - SeriesDTO result = new SeriesDTO(series.getName(), fleets, raceColumns, series.isMedal()); - return result; - } - - private FleetDTO convertToFleetDTO(Fleet fleet) { - return new FleetDTO(fleet.getName(), fleet.getOrdering(), fleet.getColor()); - } - - private List convertToRaceDTOs(Regatta regatta) { - List result = new ArrayList(); - for (RaceDefinition r : regatta.getAllRaces()) { - RegattaAndRaceIdentifier raceIdentifier = new RegattaNameAndRaceName(regatta.getName(), r.getName()); - TrackedRace trackedRace = getService().getExistingTrackedRace(raceIdentifier); - TrackedRaceDTO trackedRaceDTO = null; - if (trackedRace != null) { - trackedRaceDTO = new TrackedRaceDTO(); - trackedRaceDTO.hasWindData = trackedRace.hasWindData(); - trackedRaceDTO.hasGPSData = trackedRace.hasGPSData(); - trackedRaceDTO.startOfTracking = trackedRace.getStartOfTracking() == null ? null : trackedRace.getStartOfTracking().asDate(); - trackedRaceDTO.endOfTracking = trackedRace.getEndOfTracking() == null ? null : trackedRace.getEndOfTracking().asDate(); - trackedRaceDTO.timePointOfNewestEvent = trackedRace.getTimePointOfNewestEvent() == null ? null : trackedRace.getTimePointOfNewestEvent().asDate(); - trackedRaceDTO.delayToLiveInMs = trackedRace.getDelayToLiveInMillis(); - } - RaceWithCompetitorsDTO raceDTO = new RaceWithCompetitorsDTO(raceIdentifier, convertToCompetitorDTOs(r.getCompetitors()), - trackedRaceDTO, getService().isRaceBeingTracked(r)); - if (trackedRace != null) { - raceDTO.startOfRace = trackedRace.getStartOfRace() == null ? null : trackedRace.getStartOfRace().asDate(); - raceDTO.endOfRace = trackedRace.getEndOfRace() == null ? null : trackedRace.getEndOfRace().asDate(); - } - raceDTO.boatClass = regatta.getBoatClass() == null ? null : regatta.getBoatClass().getName(); - result.add(raceDTO); - } - return result; - } - - private List convertToCompetitorDTOs(Iterable competitors) { - List result = new ArrayList(); - for (Competitor c : competitors) { - CompetitorDTO competitorDTO = convertToCompetitorDTO(c); - result.add(competitorDTO); - } - return result; - } - - private CompetitorDTO convertToCompetitorDTO(Competitor c) { - CompetitorDTO competitorDTO = weakCompetitorDTOCache.get(c); - if (competitorDTO == null) { - CountryCode countryCode = c.getTeam().getNationality().getCountryCode(); - competitorDTO = new CompetitorDTO(c.getName(), countryCode == null ? "" - : countryCode.getTwoLetterISOCode(), - countryCode == null ? "" : countryCode.getThreeLetterIOCCode(), countryCode == null ? "" - : countryCode.getName(), c.getBoat().getSailID(), c.getId().toString(), - new BoatClassDTO(c.getBoat().getBoatClass().getName(), c.getBoat().getBoatClass().getHullLength() - .getMeters())); - weakCompetitorDTOCache.put(c, competitorDTO); - } - return competitorDTO; - } - - @Override - public Pair> listTracTracRacesInEvent(String eventJsonURL) throws MalformedURLException, IOException, - ParseException, org.json.simple.parser.ParseException, URISyntaxException { - com.sap.sailing.domain.common.impl.Util.Pair> raceRecords; - raceRecords = getService().getTracTracRaceRecords(new URL(eventJsonURL)); - List result = new ArrayList(); - for (RaceRecord raceRecord : raceRecords.getB()) { - result.add(new TracTracRaceRecordDTO(raceRecord.getID(), raceRecord.getEventName(), raceRecord.getName(), - raceRecord.getParamURL().toString(), raceRecord.getReplayURL(), raceRecord.getLiveURI().toString(), - raceRecord.getStoredURI().toString(), raceRecord.getTrackingStartTime().asDate(), raceRecord - .getTrackingEndTime().asDate(), raceRecord.getRaceStartTime().asDate(), raceRecord.getBoatClassNames())); - } - return new Pair>(raceRecords.getA(), result); - } - - @Override - public void trackWithTracTrac(RegattaIdentifier regattaToAddTo, TracTracRaceRecordDTO rr, String liveURI, String storedURI, - boolean trackWind, final boolean correctWindByDeclination, final boolean simulateWithStartTimeNow) throws Exception { - if (liveURI == null || liveURI.trim().length() == 0) { - liveURI = rr.liveURI; - } - if (storedURI == null || storedURI.trim().length() == 0) { - storedURI = rr.storedURI; - } - final RacesHandle raceHandle = getService().addTracTracRace(regattaToAddTo, new URL(rr.paramURL), - new URI(liveURI), new URI(storedURI), new MillisecondsTimePoint(rr.trackingStartTime), - new MillisecondsTimePoint(rr.trackingEndTime), - MongoWindStoreFactory.INSTANCE.getMongoWindStore(mongoObjectFactory, domainObjectFactory), - RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS, simulateWithStartTimeNow); - if (trackWind) { - new Thread("Wind tracking starter for race "+rr.regattaName+"/"+rr.name) { - public void run() { - try { - startTrackingWind(raceHandle, correctWindByDeclination, RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }.start(); - } - } - - @Override - public List getPreviousTracTracConfigurations() throws Exception { - Iterable configs = tractracDomainObjectFactory.getTracTracConfigurations(); - List result = new ArrayList(); - for (TracTracConfiguration ttConfig : configs) { - result.add(new TracTracConfigurationDTO(ttConfig.getName(), ttConfig.getJSONURL().toString(), - ttConfig.getLiveDataURI().toString(), ttConfig.getStoredDataURI().toString())); - } - return result; - } - - @Override - public void storeTracTracConfiguration(String name, String jsonURL, String liveDataURI, String storedDataURI) throws Exception { - tractracMongoObjectFactory.storeTracTracConfiguration(tractracDomainFactory.createTracTracConfiguration(name, jsonURL, liveDataURI, storedDataURI)); - } - - @Override - public void stopTrackingEvent(RegattaIdentifier regattaIdentifier) throws Exception { - getService().apply(new StopTrackingRegatta(regattaIdentifier)); - } - - private RaceDefinition getRaceByName(Regatta regatta, String raceName) { - if (regatta != null) { - return regatta.getRaceByName(raceName); - } else { - return null; - } - } - - @Override - public void stopTrackingRace(RegattaAndRaceIdentifier regattaAndRaceIdentifier) throws Exception { - getService().apply(new StopTrackingRace(regattaAndRaceIdentifier)); - } - - @Override - public void removeAndUntrackRace(RegattaAndRaceIdentifier regattatAndRaceidentifier) { - getService().apply(new RemoveAndUntrackRace(regattatAndRaceidentifier)); - } - - /** - * @param timeoutInMilliseconds eventually passed to {@link RacesHandle#getRaces(long)}. If the race definition - * can be obtained within this timeout, wind for the race will be tracked; otherwise, the method returns without - * taking any effect. - */ - private void startTrackingWind(RacesHandle raceHandle, boolean correctByDeclination, long timeoutInMilliseconds) throws Exception { - Regatta regatta = raceHandle.getRegatta(); - if (regatta != null) { - for (RaceDefinition race : raceHandle.getRaces(timeoutInMilliseconds)) { - if (race != null) { - getService().startTrackingWind(regatta, race, correctByDeclination); - } else { - log("RaceDefinition wasn't received within " + timeoutInMilliseconds + "ms for a race in regatta " - + regatta.getName() + ". Aborting wait; no wind tracking for this race."); - } - } - } - } - - @Override - public WindInfoForRaceDTO getRawWindFixes(RegattaAndRaceIdentifier raceIdentifier, Collection windSources) { - WindInfoForRaceDTO result = null; - TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); - if (trackedRace != null) { - result = new WindInfoForRaceDTO(); - result.raceIsKnownToStartUpwind = trackedRace.raceIsKnownToStartUpwind(); - Map windTrackInfoDTOs = new HashMap(); - result.windTrackInfoByWindSource = windTrackInfoDTOs; - - List windSourcesToDeliver = new ArrayList(); - if (windSources != null) { - windSourcesToDeliver.addAll(windSources); - } else { - windSourcesToDeliver.add(new WindSourceImpl(WindSourceType.EXPEDITION)); - windSourcesToDeliver.add(new WindSourceImpl(WindSourceType.WEB)); - } - for (WindSource windSource : windSourcesToDeliver) { - if(windSource.getType() == WindSourceType.WEB) { - WindTrackInfoDTO windTrackInfoDTO = new WindTrackInfoDTO(); - windTrackInfoDTO.windFixes = new ArrayList(); - WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource); - - windTrack.lockForRead(); - try { - Iterator windIter = windTrack.getRawFixes().iterator(); - while (windIter.hasNext()) { - Wind wind = windIter.next(); - if(wind != null) { - WindDTO windDTO = createWindDTO(wind, windTrack); - windTrackInfoDTO.windFixes.add(windDTO); - } - } - } finally { - windTrack.unlockAfterRead(); - } - - windTrackInfoDTOs.put(windSource, windTrackInfoDTO); - } - } - } - return result; - } - - protected WindDTO createWindDTO(Wind wind, WindTrack windTrack) { - WindDTO windDTO = new WindDTO(); - windDTO.trueWindBearingDeg = wind.getBearing().getDegrees(); - windDTO.trueWindFromDeg = wind.getBearing().reverse().getDegrees(); - windDTO.trueWindSpeedInKnots = wind.getKnots(); - windDTO.trueWindSpeedInMetersPerSecond = wind.getMetersPerSecond(); - if (wind.getPosition() != null) { - windDTO.position = new PositionDTO(wind.getPosition().getLatDeg(), wind.getPosition() - .getLngDeg()); - } - if (wind.getTimePoint() != null) { - windDTO.measureTimepoint = wind.getTimePoint().asMillis(); - Wind estimatedWind = windTrack - .getAveragedWind(wind.getPosition(), wind.getTimePoint()); - if (estimatedWind != null) { - windDTO.dampenedTrueWindBearingDeg = estimatedWind.getBearing().getDegrees(); - windDTO.dampenedTrueWindFromDeg = estimatedWind.getBearing().reverse().getDegrees(); - windDTO.dampenedTrueWindSpeedInKnots = estimatedWind.getKnots(); - windDTO.dampenedTrueWindSpeedInMetersPerSecond = estimatedWind.getMetersPerSecond(); - } - } - return windDTO; - } - - /** - * Uses wind for both, the non-dampened and dampened fields of the {@link WindDTO} object returned - */ - protected WindDTO createWindDTOFromAlreadyAveraged(Wind wind, WindTrack windTrack, TimePoint requestTimepoint) { - WindDTO windDTO = new WindDTO(); - windDTO.requestTimepoint = requestTimepoint.asMillis(); - windDTO.trueWindBearingDeg = wind.getBearing().getDegrees(); - windDTO.trueWindFromDeg = wind.getBearing().reverse().getDegrees(); - windDTO.trueWindSpeedInKnots = wind.getKnots(); - windDTO.trueWindSpeedInMetersPerSecond = wind.getMetersPerSecond(); - windDTO.dampenedTrueWindBearingDeg = wind.getBearing().getDegrees(); - windDTO.dampenedTrueWindFromDeg = wind.getBearing().reverse().getDegrees(); - windDTO.dampenedTrueWindSpeedInKnots = wind.getKnots(); - windDTO.dampenedTrueWindSpeedInMetersPerSecond = wind.getMetersPerSecond(); - if (wind.getPosition() != null) { - windDTO.position = new PositionDTO(wind.getPosition().getLatDeg(), wind.getPosition() - .getLngDeg()); - } - if (wind.getTimePoint() != null) { - windDTO.measureTimepoint = wind.getTimePoint().asMillis(); - } - return windDTO; - } - - /** - * Fetches the {@link WindTrack#getAveragedWind(Position, TimePoint) average wind} from all wind tracks or those identified - * by windSourceTypeNames - */ - //@Override - public WindInfoForRaceDTO getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, long millisecondsStepWidth, - int numberOfFixes, double latDeg, double lngDeg, Collection windSourceTypeNames) - throws NoWindException { - Position position = new DegreePosition(latDeg, lngDeg); - WindInfoForRaceDTO result = null; - TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); - if (trackedRace != null) { - result = new WindInfoForRaceDTO(); - result.raceIsKnownToStartUpwind = trackedRace.raceIsKnownToStartUpwind(); - List windSourcesToExclude = new ArrayList(); - for (WindSource windSourceToExclude : trackedRace.getWindSourcesToExclude()) { - windSourcesToExclude.add(windSourceToExclude); - } - result.windSourcesToExclude = windSourcesToExclude; - Map windTrackInfoDTOs = new HashMap(); - result.windTrackInfoByWindSource = windTrackInfoDTOs; - List windSourcesToDeliver = new ArrayList(); - Util.addAll(trackedRace.getWindSources(), windSourcesToDeliver); - // TODO bug #375: add the combined wind; currently, CombinedWindTrackImpl just takes too long to return results... - windSourcesToDeliver.add(new WindSourceImpl(WindSourceType.COMBINED)); - for (WindSource windSource : windSourcesToDeliver) { - if (windSourceTypeNames == null || windSourceTypeNames.contains(windSource.getType().name())) { - TimePoint fromTimePoint = new MillisecondsTimePoint(from); - WindTrackInfoDTO windTrackInfoDTO = new WindTrackInfoDTO(); - windTrackInfoDTO.windFixes = new ArrayList(); - WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource); - windTrackInfoDTOs.put(windSource, windTrackInfoDTO); - windTrackInfoDTO.dampeningIntervalInMilliseconds = windTrack - .getMillisecondsOverWhichToAverageWind(); - TimePoint timePoint = fromTimePoint; - Double minWindConfidence = 2.0; - Double maxWindConfidence = -1.0; - for (int i = 0; i < numberOfFixes; i++) { - WindWithConfidence> averagedWindWithConfidence = windTrack.getAveragedWindWithConfidence(position, timePoint); - - if (averagedWindWithConfidence != null) { - WindDTO windDTO = createWindDTOFromAlreadyAveraged(averagedWindWithConfidence.getObject(), windTrack, timePoint); - double confidence = averagedWindWithConfidence.getConfidence(); - windDTO.confidence = confidence; - windTrackInfoDTO.windFixes.add(windDTO); - if(confidence < minWindConfidence) { - minWindConfidence = confidence; - } - if(confidence > maxWindConfidence) { - maxWindConfidence = confidence; - } - } - timePoint = new MillisecondsTimePoint(timePoint.asMillis() + millisecondsStepWidth); - } - windTrackInfoDTO.minWindConfidence = minWindConfidence; - windTrackInfoDTO.maxWindConfidence = maxWindConfidence; - } - } - } - return result; - } - - @Override - public WindInfoForRaceDTO getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, long millisecondsStepWidth, - int numberOfFixes, Collection windSourceTypeNames) - throws NoWindException { - assert from != null; - TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); - - WindInfoForRaceDTO result = getAveragedWindInfo(new MillisecondsTimePoint(from), millisecondsStepWidth, numberOfFixes, - windSourceTypeNames, trackedRace); - return result; - } - - private WindInfoForRaceDTO getAveragedWindInfo(TimePoint from, long millisecondsStepWidth, int numberOfFixes, - Collection windSourceTypeNames, TrackedRace trackedRace) { - WindInfoForRaceDTO result = null; - if (trackedRace != null) { - TimePoint newestEvent = trackedRace.getTimePointOfNewestEvent(); - result = new WindInfoForRaceDTO(); - result.raceIsKnownToStartUpwind = trackedRace.raceIsKnownToStartUpwind(); - List windSourcesToExclude = new ArrayList(); - for (WindSource windSourceToExclude : trackedRace.getWindSourcesToExclude()) { - windSourcesToExclude.add(windSourceToExclude); - } - result.windSourcesToExclude = windSourcesToExclude; - Map windTrackInfoDTOs = new HashMap(); - result.windTrackInfoByWindSource = windTrackInfoDTOs; - List windSourcesToDeliver = new ArrayList(); - Util.addAll(trackedRace.getWindSources(), windSourcesToDeliver); - windSourcesToDeliver.add(new WindSourceImpl(WindSourceType.COMBINED)); - for (WindSource windSource : windSourcesToDeliver) { - // TODO consider parallelizing - if (windSourceTypeNames == null || windSourceTypeNames.contains(windSource.getType().name())) { - WindTrackInfoDTO windTrackInfoDTO = new WindTrackInfoDTO(); - windTrackInfoDTOs.put(windSource, windTrackInfoDTO); - windTrackInfoDTO.windFixes = new ArrayList(); - WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource); - windTrackInfoDTO.dampeningIntervalInMilliseconds = windTrack - .getMillisecondsOverWhichToAverageWind(); - TimePoint timePoint = from; - Double minWindConfidence = 2.0; - Double maxWindConfidence = -1.0; - for (int i = 0; i < numberOfFixes && newestEvent != null && timePoint.compareTo(newestEvent) < 0; i++) { - WindWithConfidence> averagedWindWithConfidence = windTrack.getAveragedWindWithConfidence(null, timePoint); - if (averagedWindWithConfidence != null) { - double confidence = averagedWindWithConfidence.getConfidence(); - WindDTO windDTO = createWindDTOFromAlreadyAveraged(averagedWindWithConfidence.getObject(), windTrack, timePoint); - windDTO.confidence = confidence; - windTrackInfoDTO.windFixes.add(windDTO); - if(confidence < minWindConfidence) { - minWindConfidence = confidence; - } - if(confidence > maxWindConfidence) { - maxWindConfidence = confidence; - } - } - timePoint = new MillisecondsTimePoint(timePoint.asMillis() + millisecondsStepWidth); - } - windTrackInfoDTO.minWindConfidence = minWindConfidence; - windTrackInfoDTO.maxWindConfidence = maxWindConfidence; - } - } - } - return result; - } - - @Override - public WindInfoForRaceDTO getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, Date to, - long resolutionInMilliseconds, Collection windSourceTypeNames) { - TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); - WindInfoForRaceDTO result = null; - if (trackedRace != null) { - TimePoint fromTimePoint = from == null ? trackedRace.getStartOfTracking() : new MillisecondsTimePoint(from); - TimePoint toTimePoint = to == null ? trackedRace.getEndOfRace() : new MillisecondsTimePoint(to); - if(fromTimePoint != null && toTimePoint != null) { - int numberOfFixes = (int) ((toTimePoint.asMillis() - fromTimePoint.asMillis())/resolutionInMilliseconds); - result = getAveragedWindInfo(fromTimePoint, resolutionInMilliseconds, numberOfFixes, windSourceTypeNames, trackedRace); - } - } - return result; - } - - @Override - public void setWind(RegattaAndRaceIdentifier raceIdentifier, WindDTO windDTO) { - DynamicTrackedRace trackedRace = (DynamicTrackedRace) getExistingTrackedRace(raceIdentifier); - if (trackedRace != null) { - Position p = null; - if (windDTO.position != null) { - p = new DegreePosition(windDTO.position.latDeg, windDTO.position.lngDeg); - } - TimePoint at = null; - if (windDTO.measureTimepoint != null) { - at = new MillisecondsTimePoint(windDTO.measureTimepoint); - } - SpeedWithBearing speedWithBearing = null; - Speed speed = null; - if (windDTO.trueWindSpeedInKnots != null) { - speed = new KnotSpeedImpl(windDTO.trueWindSpeedInKnots); - } else if (windDTO.trueWindSpeedInMetersPerSecond != null) { - speed = new KilometersPerHourSpeedImpl(windDTO.trueWindSpeedInMetersPerSecond * 3600. / 1000.); - } else if (windDTO.dampenedTrueWindSpeedInKnots != null) { - speed = new KnotSpeedImpl(windDTO.dampenedTrueWindSpeedInKnots); - } else if (windDTO.dampenedTrueWindSpeedInMetersPerSecond != null) { - speed = new KilometersPerHourSpeedImpl(windDTO.dampenedTrueWindSpeedInMetersPerSecond * 3600. / 1000.); - } - if (speed != null) { - if (windDTO.trueWindBearingDeg != null) { - speedWithBearing = new KnotSpeedWithBearingImpl(speed.getKnots(), new DegreeBearingImpl( - windDTO.trueWindBearingDeg)); - } else if (windDTO.trueWindFromDeg != null) { - speedWithBearing = new KnotSpeedWithBearingImpl(speed.getKnots(), new DegreeBearingImpl( - windDTO.trueWindFromDeg).reverse()); - } - } - Wind wind = new WindImpl(p, at, speedWithBearing); - Iterable webWindSources = trackedRace.getWindSources(WindSourceType.WEB); - if(Util.size(webWindSources) == 0) { - // create a new WEB wind source if not available - trackedRace.recordWind(wind, new WindSourceImpl(WindSourceType.WEB)); - } else { - trackedRace.recordWind(wind, webWindSources.iterator().next()); - } - } - } - - @Override - public RaceMapDataDTO getRaceMapData(RegattaAndRaceIdentifier raceIdentifier, Date date, - Map from, Map to, - boolean extrapolate) throws NoWindException { - RaceMapDataDTO raceMapDataDTO = new RaceMapDataDTO(); - - raceMapDataDTO.boatPositions = getBoatPositions(raceIdentifier, from, to, extrapolate); - raceMapDataDTO.coursePositions = getCoursePositions(raceIdentifier, date); - raceMapDataDTO.quickRanks = getQuickRanks(raceIdentifier, date); - - return raceMapDataDTO; - } - - @Override - public Map> getBoatPositions(RegattaAndRaceIdentifier raceIdentifier, - Map from, Map to, - boolean extrapolate) throws NoWindException { - Map> result = new HashMap>(); - TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); - if (trackedRace != null) { - for (Competitor competitor : trackedRace.getRace().getCompetitors()) { - CompetitorDTO competitorDTO = convertToCompetitorDTO(competitor); - if (from.containsKey(competitorDTO)) { - List fixesForCompetitor = new ArrayList(); - result.put(competitorDTO, fixesForCompetitor); - GPSFixTrack track = trackedRace.getTrack(competitor); - TimePoint fromTimePoint = new MillisecondsTimePoint(from.get(competitorDTO)); - TimePoint toTimePointExcluding = new MillisecondsTimePoint(to.get(competitorDTO)); - // copy the fixes into a list while holding the monitor; then release the monitor to avoid deadlocks - // during wind estimations required for tack determination - List fixes = new ArrayList(); - track.lockForRead(); - try { - Iterator fixIter = track.getFixesIterator(fromTimePoint, /* inclusive */true); - while (fixIter.hasNext()) { - GPSFixMoving fix = fixIter.next(); - if (fix.getTimePoint().compareTo(toTimePointExcluding) < 0) { - fixes.add(fix); - } else { - break; - } - } - } finally { - track.unlockAfterRead(); - } - if (fixes.isEmpty()) { - // then there was no (smoothened) fix between fromTimePoint and toTimePointExcluding; estimate... - TimePoint middle = new MillisecondsTimePoint((toTimePointExcluding.asMillis()+fromTimePoint.asMillis())/2); - Position estimatedPosition = track.getEstimatedPosition(middle, extrapolate); - SpeedWithBearing estimatedSpeed = track.getEstimatedSpeed(middle); - if(estimatedPosition != null && estimatedSpeed != null) { - fixes.add(new GPSFixMovingImpl(estimatedPosition, middle, estimatedSpeed)); - } - } - Iterator fixIter = fixes.iterator(); - if (fixIter.hasNext()) { - final WindSource windSource = new WindSourceImpl(WindSourceType.COMBINED); - GPSFixMoving fix = fixIter.next(); - while (fix != null && (fix.getTimePoint().compareTo(toTimePointExcluding) < 0 || - (fix.getTimePoint().equals(toTimePointExcluding) && toTimePointExcluding.equals(fromTimePoint)))) { - Tack tack = trackedRace.getTack(competitor, fix.getTimePoint()); - TrackedLegOfCompetitor trackedLegOfCompetitor = trackedRace.getTrackedLeg(competitor, - fix.getTimePoint()); - LegType legType = trackedLegOfCompetitor == null ? null : trackedRace.getTrackedLeg( - trackedLegOfCompetitor.getLeg()).getLegType(fix.getTimePoint()); - Wind wind = trackedRace.getWind(fix.getPosition(),toTimePointExcluding); - WindDTO windDTO = createWindDTOFromAlreadyAveraged(wind, trackedRace.getOrCreateWindTrack(windSource), toTimePointExcluding); - GPSFixDTO fixDTO = createGPSFixDTO(fix, fix.getSpeed(), windDTO, tack, legType, /* extrapolate */ - false); - fixesForCompetitor.add(fixDTO); - if (fixIter.hasNext()) { - fix = fixIter.next(); - } else { - // check if fix was at date and if extrapolation is requested - if (!fix.getTimePoint().equals(toTimePointExcluding) && extrapolate) { - Position position = track.getEstimatedPosition(toTimePointExcluding, extrapolate); - Tack tack2 = trackedRace.getTack(competitor, toTimePointExcluding); - LegType legType2 = trackedLegOfCompetitor == null ? null : trackedRace - .getTrackedLeg(trackedLegOfCompetitor.getLeg()).getLegType( - fix.getTimePoint()); - SpeedWithBearing speedWithBearing = track.getEstimatedSpeed(toTimePointExcluding); - Wind wind2 = trackedRace.getWind(position, toTimePointExcluding); - WindDTO windDTO2 = createWindDTOFromAlreadyAveraged(wind2, trackedRace.getOrCreateWindTrack(windSource), toTimePointExcluding); - GPSFixDTO extrapolated = new GPSFixDTO( - to.get(competitorDTO), - position==null?null:new PositionDTO(position.getLatDeg(), position.getLngDeg()), - speedWithBearing==null?null:createSpeedWithBearingDTO(speedWithBearing), windDTO2, - tack2, legType2, /* extrapolated */ true); - fixesForCompetitor.add(extrapolated); - } - fix = null; - } - } - } - } - } - } - return result; - } - - private SpeedWithBearingDTO createSpeedWithBearingDTO(SpeedWithBearing speedWithBearing) { - return new SpeedWithBearingDTO(speedWithBearing.getKnots(), speedWithBearing - .getBearing().getDegrees()); - } - - private GPSFixDTO createGPSFixDTO(GPSFix fix, SpeedWithBearing speedWithBearing, WindDTO windDTO, Tack tack, LegType legType, boolean extrapolated) { - return new GPSFixDTO(fix.getTimePoint().asDate(), fix.getPosition()==null?null:new PositionDTO(fix - .getPosition().getLatDeg(), fix.getPosition().getLngDeg()), - speedWithBearing==null?null:createSpeedWithBearingDTO(speedWithBearing), windDTO, tack, legType, extrapolated); - } - - @Override - public RaceTimesInfoDTO getRaceTimesInfo(RegattaAndRaceIdentifier raceIdentifier) { - RaceTimesInfoDTO raceTimesInfo = null; - TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); - - if (trackedRace != null) { - raceTimesInfo = new RaceTimesInfoDTO(raceIdentifier); - List legInfos = new ArrayList(); - raceTimesInfo.setLegInfos(legInfos); - List markPassingTimesDTOs = new ArrayList(); - raceTimesInfo.setMarkPassingTimes(markPassingTimesDTOs); - - raceTimesInfo.startOfRace = trackedRace.getStartOfRace() == null ? null : trackedRace.getStartOfRace().asDate(); - raceTimesInfo.startOfTracking = trackedRace.getStartOfTracking() == null ? null : trackedRace.getStartOfTracking().asDate(); - raceTimesInfo.newestTrackingEvent = trackedRace.getTimePointOfNewestEvent() == null ? null : trackedRace.getTimePointOfNewestEvent().asDate(); - raceTimesInfo.endOfTracking = trackedRace.getEndOfTracking() == null ? null : trackedRace.getEndOfTracking().asDate(); - raceTimesInfo.endOfRace = trackedRace.getEndOfRace() == null ? null : trackedRace.getEndOfRace().asDate(); - raceTimesInfo.delayToLiveInMs = trackedRace.getDelayToLiveInMillis(); - - Iterable>> markPassingsTimes = trackedRace.getMarkPassingsTimes(); - synchronized (markPassingsTimes) { - int numberOfWaypoints = Util.size(markPassingsTimes); - int wayPointNumber = 1; - for (Pair> markPassingTimes : markPassingsTimes) { - MarkPassingTimesDTO markPassingTimesDTO = new MarkPassingTimesDTO(); - String name = "M" + (wayPointNumber - 1); - if (wayPointNumber == 1) { - name = "S"; - } else if (wayPointNumber == numberOfWaypoints) { - name = "F"; - } - markPassingTimesDTO.name = name; - Pair timesPair = markPassingTimes.getB(); - TimePoint firstPassingTime = timesPair.getA(); - TimePoint lastPassingTime = timesPair.getB(); - markPassingTimesDTO.firstPassingDate = firstPassingTime == null ? null : firstPassingTime.asDate(); - markPassingTimesDTO.lastPassingDate = lastPassingTime == null ? null : lastPassingTime.asDate(); - markPassingTimesDTOs.add(markPassingTimesDTO); - wayPointNumber++; - } - } - trackedRace.getRace().getCourse().lockForRead(); - try { - Iterable trackedLegs = trackedRace.getTrackedLegs(); - int legNumber = 1; - for (TrackedLeg trackedLeg : trackedLegs) { - LegInfoDTO legInfoDTO = new LegInfoDTO(legNumber); - legInfoDTO.name = "L" + legNumber; - try { - MarkPassingTimesDTO markPassingTimesDTO = markPassingTimesDTOs.get(legNumber - 1); - if (markPassingTimesDTO.firstPassingDate != null) { - TimePoint p = new MillisecondsTimePoint(markPassingTimesDTO.firstPassingDate); - legInfoDTO.legType = trackedLeg.getLegType(p); - legInfoDTO.legBearingInDegrees = trackedLeg.getLegBearing(p).getDegrees(); - } - } catch (NoWindException e) { - // do nothing - } - legInfos.add(legInfoDTO); - legNumber++; - } - } finally { - trackedRace.getRace().getCourse().unlockAfterRead(); - } - } - return raceTimesInfo; - } - - @Override - public List getRaceTimesInfos(Collection raceIdentifiers) { - List raceTimesInfos = new ArrayList(); - for (RegattaAndRaceIdentifier raceIdentifier : raceIdentifiers) { - RaceTimesInfoDTO raceTimesInfo = getRaceTimesInfo(raceIdentifier); - if (raceTimesInfo != null) { - raceTimesInfos.add(raceTimesInfo); - } - } - return raceTimesInfos; - } - - @Override - public CourseDTO getCoursePositions(RegattaAndRaceIdentifier raceIdentifier, Date date) { - CourseDTO result = new CourseDTO(); - if (date != null) { - TimePoint dateAsTimePoint = new MillisecondsTimePoint(date); - TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); - if (trackedRace != null) { - result.marks = new HashSet(); - result.waypointPositions = new ArrayList(); - Set marks = new HashSet(); - Course course = trackedRace.getRace().getCourse(); - for (Waypoint waypoint : course.getWaypoints()) { - Position waypointPosition = trackedRace.getApproximatePosition(waypoint, dateAsTimePoint); - if (waypointPosition != null) { - result.waypointPositions.add(new PositionDTO(waypointPosition.getLatDeg(), waypointPosition.getLngDeg())); - } - for (Mark b : waypoint.getMarks()) { - marks.add(b); - } - } - for (Mark mark : marks) { - GPSFixTrack track = trackedRace.getOrCreateTrack(mark); - Position positionAtDate = track.getEstimatedPosition(dateAsTimePoint, /* extrapolate */false); - if (positionAtDate != null) { - result.marks.add(convertToMarkDTO(mark, positionAtDate)); - } - } - - // set the positions of start and finish - Waypoint firstWaypoint = course.getFirstWaypoint(); - if (firstWaypoint != null) { - result.startMarkPositions = getMarkPositionDTOs(dateAsTimePoint, trackedRace, firstWaypoint); - } - Waypoint lastWaypoint = course.getLastWaypoint(); - if (lastWaypoint != null) { - result.finishMarkPositions = getMarkPositionDTOs(dateAsTimePoint, trackedRace, lastWaypoint); - } - } - } - return result; - } - - @Override - public RaceCourseMarksDTO getRaceCourseMarks(RegattaAndRaceIdentifier raceIdentifier, Date date) { - RaceCourseMarksDTO raceMarksDTO = new RaceCourseMarksDTO(); - TimePoint dateAsTimePoint = new MillisecondsTimePoint(date); - TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); - if (trackedRace != null) { - raceMarksDTO.marks = new HashSet(); - Iterable marks = trackedRace.getMarks(); - for (Mark mark : marks) { - GPSFixTrack track = trackedRace.getOrCreateTrack(mark); - Position positionAtDate = track.getEstimatedPosition(dateAsTimePoint, /* extrapolate */false); - if (positionAtDate != null) { - raceMarksDTO.marks.add(convertToMarkDTO(mark, positionAtDate)); - } - } - } - return raceMarksDTO; - } - - @Override - public List getRaceCourse(RegattaAndRaceIdentifier raceIdentifier, Date date) { - List result = new ArrayList(); - if (date != null) { - TimePoint dateAsTimePoint = new MillisecondsTimePoint(date); - TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); - if (trackedRace != null) { - Course course = trackedRace.getRace().getCourse(); - for (Waypoint waypoint : course.getWaypoints()) { - result.add(createControlPointDTO(waypoint.getControlPoint(), trackedRace, dateAsTimePoint)); - } - } - } - return result; - } - - private ControlPointDTO createControlPointDTO(ControlPoint controlPoint, TrackedRace trackedRace, TimePoint timePoint) { - ControlPointDTO result; - if (controlPoint instanceof Gate) { - final Mark left = ((Gate) controlPoint).getLeft(); - final Position leftPos = trackedRace.getOrCreateTrack(left).getEstimatedPosition(timePoint, /* extrapolate */ false); - final Mark right = ((Gate) controlPoint).getRight(); - final Position rightPos = trackedRace.getOrCreateTrack(right).getEstimatedPosition(timePoint, /* extrapolate */ false); - result = new GateDTO(controlPoint.getName(), convertToMarkDTO(left, leftPos), convertToMarkDTO(right, rightPos)); - } else { - final Position posOfFirst = trackedRace.getOrCreateTrack(controlPoint.getMarks().iterator().next()). - getEstimatedPosition(timePoint, /* extrapolate */ false); - result = new MarkDTO(controlPoint.getName(), posOfFirst.getLatDeg(), posOfFirst.getLngDeg()); - } - return result; - } - - /** - * For each {@link ControlPointDTO} in controlPoints tries to find the best-matching waypoint - * from the course that belongs to the race identified by raceIdentifier. If such a waypoint is - * found, its control point is added to the control point list for the new course. Otherwise, a new control - * point is created using the default {@link com.sap.sailing.domain.base.DomainFactory} instance. The resulting - * list of control points is then passed to {@link Course#update(List, com.sap.sailing.domain.base.DomainFactory)} for - * the course of the race identified by raceIdentifier. - */ - @Override - public void updateRaceCourse(RegattaAndRaceIdentifier raceIdentifier, List controlPoints) { - TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); - if (trackedRace != null) { - Course course = trackedRace.getRace().getCourse(); - Iterable waypoints = course.getWaypoints(); - List newControlPoints = new ArrayList(); - int lastMatchPosition = -1; - for (ControlPointDTO controlPointDTO : controlPoints) { - ControlPoint matchFromOldCourse = null; - for (int i=lastMatchPosition+1; matchFromOldCourse == null && i marks, Iterable marksDTOs) { - Iterator marksIter = marks.iterator(); - Iterator markDTOsIter = marksDTOs.iterator(); - while (marksIter.hasNext() && markDTOsIter.hasNext()) { - Mark nextMark = marksIter.next(); - MarkDTO nextMarkDTO = markDTOsIter.next(); - if (!nextMark.getName().equals(nextMarkDTO.name)) { - return false; - } - } - return marksIter.hasNext() == markDTOsIter.hasNext(); - } - - private List getMarkPositionDTOs(TimePoint timePoint, TrackedRace trackedRace, Waypoint waypoint) { - List startMarkPositions = new ArrayList(); - for (Mark startMark : waypoint.getMarks()) { - final Position estimatedMarkPosition = trackedRace.getOrCreateTrack(startMark) - .getEstimatedPosition(timePoint, /* extrapolate */false); - if (estimatedMarkPosition != null) { - startMarkPositions.add(new PositionDTO(estimatedMarkPosition.getLatDeg(), estimatedMarkPosition.getLngDeg())); - } - } - return startMarkPositions; - } - - @Override - public List getQuickRanks(RegattaAndRaceIdentifier raceIdentifier, Date date) throws NoWindException { - List result = new ArrayList(); - if (date != null) { - TimePoint dateAsTimePoint = new MillisecondsTimePoint(date); - TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); - if (trackedRace != null) { - RaceDefinition race = trackedRace.getRace(); - for (Competitor competitor : race.getCompetitors()) { - int rank = trackedRace.getRank(competitor, dateAsTimePoint); - TrackedLegOfCompetitor trackedLeg = trackedRace.getTrackedLeg(competitor, dateAsTimePoint); - if (trackedLeg != null) { - int legNumber = race.getCourse().getLegs().indexOf(trackedLeg.getLeg()) + 1; - QuickRankDTO quickRankDTO = new QuickRankDTO(convertToCompetitorDTO(competitor), rank, legNumber); - result.add(quickRankDTO); - } - } - Collections.sort(result, new Comparator() { - @Override - public int compare(QuickRankDTO o1, QuickRankDTO o2) { - return o1.rank - o2.rank; - } - }); - } - } - return result; - } - - @Override - public void setRaceIsKnownToStartUpwind(RegattaAndRaceIdentifier raceIdentifier, boolean raceIsKnownToStartUpwind) { - getService().apply(new SetRaceIsKnownToStartUpwind(raceIdentifier, raceIsKnownToStartUpwind)); - } - - @Override - public void setWindSourcesToExclude(RegattaAndRaceIdentifier raceIdentifier, Iterable windSourcesToExclude) { - getService().apply(new SetWindSourcesToExclude(raceIdentifier, windSourcesToExclude)); - } - - @Override - public WindInfoForRaceDTO getWindSourcesInfo(RegattaAndRaceIdentifier raceIdentifier) { - WindInfoForRaceDTO result = null; - TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); - if (trackedRace != null) { - result = new WindInfoForRaceDTO(); - result.raceIsKnownToStartUpwind = trackedRace.raceIsKnownToStartUpwind(); - List windSourcesToExclude = new ArrayList(); - for (WindSource windSourceToExclude : trackedRace.getWindSourcesToExclude()) { - windSourcesToExclude.add(windSourceToExclude); - } - result.windSourcesToExclude = windSourcesToExclude; - Map windTrackInfoDTOs = new HashMap(); - result.windTrackInfoByWindSource = windTrackInfoDTOs; - - for(WindSource windSource: trackedRace.getWindSources()) { - windTrackInfoDTOs.put(windSource, new WindTrackInfoDTO()); - } - windTrackInfoDTOs.put(new WindSourceImpl(WindSourceType.COMBINED), new WindTrackInfoDTO()); - } - return result; - } - - @Override - public void removeWind(RegattaAndRaceIdentifier raceIdentifier, WindDTO windDTO) { - DynamicTrackedRace trackedRace = (DynamicTrackedRace) getExistingTrackedRace(raceIdentifier); - if (trackedRace != null) { - Position p = null; - if (windDTO.position != null) { - p = new DegreePosition(windDTO.position.latDeg, windDTO.position.lngDeg); - } - TimePoint at = null; - if (windDTO.measureTimepoint != null) { - at = new MillisecondsTimePoint(windDTO.measureTimepoint); - } - SpeedWithBearing speedWithBearing = null; - Speed speed = null; - if (windDTO.trueWindSpeedInKnots != null) { - speed = new KnotSpeedImpl(windDTO.trueWindSpeedInKnots); - } else if (windDTO.trueWindSpeedInMetersPerSecond != null) { - speed = new KilometersPerHourSpeedImpl(windDTO.trueWindSpeedInMetersPerSecond * 3600. / 1000.); - } else if (windDTO.dampenedTrueWindSpeedInKnots != null) { - speed = new KnotSpeedImpl(windDTO.dampenedTrueWindSpeedInKnots); - } else if (windDTO.dampenedTrueWindSpeedInMetersPerSecond != null) { - speed = new KilometersPerHourSpeedImpl(windDTO.dampenedTrueWindSpeedInMetersPerSecond * 3600. / 1000.); - } - if (speed != null) { - if (windDTO.trueWindBearingDeg != null) { - speedWithBearing = new KnotSpeedWithBearingImpl(speed.getKnots(), new DegreeBearingImpl( - windDTO.trueWindBearingDeg)); - } else if (windDTO.trueWindFromDeg != null) { - speedWithBearing = new KnotSpeedWithBearingImpl(speed.getKnots(), new DegreeBearingImpl( - windDTO.trueWindFromDeg).reverse()); - } - } - Wind wind = new WindImpl(p, at, speedWithBearing); - trackedRace.removeWind(wind, trackedRace.getWindSources(WindSourceType.WEB).iterator().next()); - } - } - - protected RacingEventService getService() { - return racingEventServiceTracker.getService(); // grab the service - } - - private ReplicationService getReplicationService() { - return replicationServiceTracker.getService(); - } - - @Override - public List getLeaderboardNames() throws Exception { - return new ArrayList(getService().getLeaderboards().keySet()); - } - - @Override - public StrippedLeaderboardDTO createFlexibleLeaderboard(String leaderboardName, int[] discardThresholds, ScoringSchemeType scoringSchemeType) { - return createStrippedLeaderboardDTO(getService().apply(new CreateFlexibleLeaderboard(leaderboardName, discardThresholds, - baseDomainFactory.createScoringScheme(scoringSchemeType))), false); - } - - @Override - public StrippedLeaderboardDTO createRegattaLeaderboard(RegattaIdentifier regattaIdentifier, int[] discardThresholds) { - return createStrippedLeaderboardDTO(getService().apply(new CreateRegattaLeaderboard(regattaIdentifier, discardThresholds)), false); - } - - @Override - public List getLeaderboards() { - Map leaderboards = getService().getLeaderboards(); - List results = new ArrayList(); - for(Leaderboard leaderboard: leaderboards.values()) { - StrippedLeaderboardDTO dao = createStrippedLeaderboardDTO(leaderboard, false); - results.add(dao); - } - return results; - } - - @Override - public StrippedLeaderboardDTO getLeaderboard(String leaderboardName) { - Map leaderboards = getService().getLeaderboards(); - StrippedLeaderboardDTO result = null; - Leaderboard leaderboard = leaderboards.get(leaderboardName); - if (leaderboard != null) { - result = createStrippedLeaderboardDTO(leaderboard, false); - } - return result; - } - - @Override - public List getLeaderboardsByEvent(RegattaDTO regatta) { - List results = new ArrayList(); - for (RaceDTO race : regatta.races) { - List leaderboard = getLeaderboardsByRace(race); - if (leaderboard != null && !leaderboard.isEmpty()) { - results.addAll(leaderboard); - } - } - // Removing duplicates - HashSet set = new HashSet(results); - results.clear(); - results.addAll(set); - return results; - } - - @Override - public List getLeaderboardsByRace(RaceDTO race) { - List results = new ArrayList(); - Map leaderboards = getService().getLeaderboards(); - for (Leaderboard leaderboard : leaderboards.values()) { - Iterable races = leaderboard.getRaceColumns(); - for (RaceColumn raceInLeaderboard : races) { - for (Fleet fleet : raceInLeaderboard.getFleets()) { - TrackedRace trackedRace = raceInLeaderboard.getTrackedRace(fleet); - if (trackedRace != null) { - RaceDefinition trackedRaceDef = trackedRace.getRace(); - if (trackedRaceDef.getName().equals(race.name)) { - results.add(createStrippedLeaderboardDTO(leaderboard, false)); - break; - } - } - } - } - } - return results; - } - - /** - * Creates a {@link LeaderboardDTO} for leaderboard and fills in the name, race master data - * in the form of {@link RaceColumnDTO}s, whether or not there are {@link LeaderboardDTO#hasCarriedPoints carried points} - * and the {@link LeaderboardDTO#discardThresholds discarding thresholds} for the leaderboard. No data about the points - * is filled into the result object. No data about the competitor display names is filled in; instead, an empty map - * is used for {@link LeaderboardDTO#competitorDisplayNames}.
- * If withGeoLocationData is true the geographical location of all races will be determined. - */ - private StrippedLeaderboardDTO createStrippedLeaderboardDTO(Leaderboard leaderboard, boolean withGeoLocationData) { - StrippedLeaderboardDTO leaderboardDTO = new StrippedLeaderboardDTO(); - TimePoint startOfLatestRace = null; - TimePoint now = MillisecondsTimePoint.now(); - Long delayToLiveInMillisForLatestRace = null; - leaderboardDTO.name = leaderboard.getName(); - leaderboardDTO.competitorDisplayNames = new HashMap(); - leaderboardDTO.isMetaLeaderboard = leaderboard instanceof MetaLeaderboard ? true : false; - if (leaderboard instanceof RegattaLeaderboard) { - RegattaLeaderboard regattaLeaderboard = (RegattaLeaderboard) leaderboard; - Regatta regatta = regattaLeaderboard.getRegatta(); - leaderboardDTO.regattaName = regatta.getName(); - leaderboardDTO.isRegattaLeaderboard = true; - leaderboardDTO.scoringScheme = regatta.getScoringScheme().getType(); - } else { - leaderboardDTO.isRegattaLeaderboard = false; - leaderboardDTO.scoringScheme = leaderboard.getScoringScheme().getType(); - } - leaderboardDTO.setDelayToLiveInMillisForLatestRace(delayToLiveInMillisForLatestRace); - leaderboardDTO.hasCarriedPoints = leaderboard.hasCarriedPoints(); - leaderboardDTO.discardThresholds = leaderboard.getResultDiscardingRule().getDiscardIndexResultsStartingWithHowManyRaces(); - for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - for (Fleet fleet : raceColumn.getFleets()) { - TimePoint latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive = null; - RaceDTO raceDTO = null; - RegattaAndRaceIdentifier raceIdentifier = null; - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - if (trackedRace != null) { - if (startOfLatestRace == null || (trackedRace.getStartOfRace() != null && trackedRace.getStartOfRace().compareTo(startOfLatestRace) > 0)) { - delayToLiveInMillisForLatestRace = trackedRace.getDelayToLiveInMillis(); - } - raceIdentifier = new RegattaNameAndRaceName(trackedRace.getTrackedRegatta().getRegatta().getName(), trackedRace.getRace().getName()); - // Optional: Getting the places of the race - PlacemarkOrderDTO racePlaces = withGeoLocationData ? getRacePlaces(trackedRace) : null; - - TrackedRaceDTO trackedRaceDTO = new TrackedRaceDTO(); - trackedRaceDTO.startOfTracking = trackedRace.getStartOfTracking() == null ? null : trackedRace.getStartOfTracking().asDate(); - trackedRaceDTO.hasWindData = trackedRace.hasWindData(); - trackedRaceDTO.hasGPSData = trackedRace.hasGPSData(); - raceDTO = new RaceDTO(raceIdentifier, trackedRaceDTO, getService().isRaceBeingTracked(trackedRace.getRace())); - raceDTO.places = racePlaces; - raceDTO.startOfRace = trackedRace.getStartOfRace() == null ? null : trackedRace.getStartOfRace().asDate(); - raceDTO.endOfRace = trackedRace.getEndOfRace() == null ? null : trackedRace.getEndOfRace().asDate(); - if (trackedRace.hasStarted(now) && trackedRace.hasGPSData() && trackedRace.hasWindData()) { - TimePoint liveTimePointForTrackedRace = now; - final TimePoint endOfRace = trackedRace.getEndOfRace(); - if (endOfRace != null) { - liveTimePointForTrackedRace = endOfRace; - } - latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive = liveTimePointForTrackedRace; - } - } - final FleetDTO fleetDTO = convertToFleetDTO(fleet); - RaceColumnDTO raceColumnDTO = leaderboardDTO.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), fleetDTO, - raceColumn.isMedalRace(), raceIdentifier, raceDTO); - if (latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive != null) { - raceColumnDTO.setWhenLastTrackedRaceWasLive(fleetDTO, latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive.asDate()); - } - } - } - return leaderboardDTO; - } - - private PlacemarkOrderDTO getRacePlaces(TrackedRace trackedRace) { - Pair startAndFinish = getStartFinishPlacemarksForTrackedRace(trackedRace); - PlacemarkOrderDTO racePlaces = new PlacemarkOrderDTO(); - if (startAndFinish.getA() != null) { - racePlaces.getPlacemarks().add(convertToPlacemarkDTO(startAndFinish.getA())); - } - if (startAndFinish.getB() != null) { - racePlaces.getPlacemarks().add(convertToPlacemarkDTO(startAndFinish.getB())); - } - if (racePlaces.isEmpty()) { - racePlaces = null; - } - return racePlaces; - } - - private Pair getStartFinishPlacemarksForTrackedRace(TrackedRace race) { - double radiusCalculationFactor = 10.0; - Placemark startBest = null; - Placemark finishBest = null; - - // Get start postition - Iterator startMarks = race.getRace().getCourse().getFirstWaypoint().getMarks().iterator(); - GPSFix startMarkFix = startMarks.hasNext() ? race.getOrCreateTrack(startMarks.next()).getLastRawFix() : null; - Position startPosition = startMarkFix != null ? startMarkFix.getPosition() : null; - if (startPosition != null) { - try { - // Get distance to nearest placemark and calculate the search radius - Placemark startNearest = ReverseGeocoder.INSTANCE.getPlacemarkNearest(startPosition); - if (startNearest != null) { - Distance startNearestDistance = startNearest.distanceFrom(startPosition); - double startRadius = startNearestDistance.getKilometers() * radiusCalculationFactor; - - // Get the estimated best start place - startBest = ReverseGeocoder.INSTANCE.getPlacemarkLast(startPosition, startRadius, - new Placemark.ByPopulationDistanceRatio(startPosition)); - } - } catch (IOException e) { - logger.throwing(TrackedRaceImpl.class.getName(), "getPlaceOrder()", e); - } catch (org.json.simple.parser.ParseException e) { - logger.throwing(TrackedRaceImpl.class.getName(), "getPlaceOrder()", e); - } - } - - // Get finish position - Iterator finishMarks = race.getRace().getCourse().getFirstWaypoint().getMarks().iterator(); - GPSFix finishMarkFix = finishMarks.hasNext() ? race.getOrCreateTrack(finishMarks.next()).getLastRawFix() : null; - Position finishPosition = finishMarkFix != null ? finishMarkFix.getPosition() : null; - if (startPosition != null && finishPosition != null) { - if (startPosition.getDistance(finishPosition).getKilometers() <= ReverseGeocoder.POSITION_CACHE_DISTANCE_LIMIT_IN_KM) { - finishBest = startBest; - } else { - try { - // Get distance to nearest placemark and calculate the search radius - Placemark finishNearest = ReverseGeocoder.INSTANCE.getPlacemarkNearest(finishPosition); - Distance finishNearestDistance = finishNearest.distanceFrom(finishPosition); - double finishRadius = finishNearestDistance.getKilometers() * radiusCalculationFactor; - - // Get the estimated best finish place - finishBest = ReverseGeocoder.INSTANCE.getPlacemarkLast(finishPosition, finishRadius, - new Placemark.ByPopulationDistanceRatio(finishPosition)); - } catch (IOException e) { - logger.throwing(TrackedRaceImpl.class.getName(), "getPlaceOrder()", e); - } catch (org.json.simple.parser.ParseException e) { - logger.throwing(TrackedRaceImpl.class.getName(), "getPlaceOrder()", e); - } - } - } - Pair placemarks = new Pair(startBest, finishBest); - return placemarks; - } - - @Override - public void updateLeaderboard(String leaderboardName, String newLeaderboardName, int[] newDiscardingThresholds) { - getService().apply(new UpdateLeaderboard(leaderboardName, newLeaderboardName, newDiscardingThresholds)); - } - - @Override - public void removeLeaderboard(String leaderboardName) { - getService().apply(new RemoveLeaderboard(leaderboardName)); - } - - @Override - public void renameLeaderboard(String leaderboardName, String newLeaderboardName) { - getService().apply(new RenameLeaderboard(leaderboardName, newLeaderboardName)); - } - - @Override - public void addColumnToLeaderboard(String columnName, String leaderboardName, boolean medalRace) { - getService().apply(new AddColumnToLeaderboard(columnName, leaderboardName, medalRace)); - } - - @Override - public void addColumnsToLeaderboard(String leaderboardName, List> columnsToAdd) { - for(Pair columnToAdd: columnsToAdd) { - getService().apply(new AddColumnToLeaderboard(columnToAdd.getA(), leaderboardName, columnToAdd.getB())); - } - } - - @Override - public void removeLeaderboardColumns(String leaderboardName, List columnsToRemove) { - for (String columnToRemove : columnsToRemove) { - getService().apply(new RemoveLeaderboardColumn(columnToRemove, leaderboardName)); - } - } - - @Override - public void removeLeaderboardColumn(String leaderboardName, String columnName) { - getService().apply(new RemoveLeaderboardColumn(columnName, leaderboardName)); - } - - @Override - public void renameLeaderboardColumn(String leaderboardName, String oldColumnName, String newColumnName) { - getService().apply(new RenameLeaderboardColumn(leaderboardName, oldColumnName, newColumnName)); - } - - @Override - public void updateLeaderboardColumnFactor(String leaderboardName, String columnName, Double newFactor) { - getService().apply(new UpdateLeaderboardColumnFactor(leaderboardName, columnName, newFactor)); - } - - @Override - public void suppressCompetitorInLeaderboard(String leaderboardName, String competitorIdAsString, boolean suppressed) { - getService().apply(new SetSuppressedFlagForCompetitorInLeaderboard(leaderboardName, competitorIdAsString, suppressed)); - } - - @Override - public boolean connectTrackedRaceToLeaderboardColumn(String leaderboardName, String raceColumnName, - String fleetName, RegattaAndRaceIdentifier raceIdentifier) { - return getService().apply(new ConnectTrackedRaceToLeaderboardColumn(leaderboardName, raceColumnName, fleetName, raceIdentifier)); - } - - @Override - public Map getRegattaAndRaceNameOfTrackedRaceConnectedToLeaderboardColumn(String leaderboardName, String raceColumnName) { - Map result = new HashMap(); - Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName); - if (leaderboard != null) { - RaceColumn raceColumn = leaderboard.getRaceColumnByName(raceColumnName); - if (raceColumn != null) { - for (Fleet fleet : raceColumn.getFleets()) { - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - if (trackedRace != null) { - result.put(fleet.getName(), trackedRace.getRaceIdentifier()); - } else { - result.put(fleet.getName(), null); - } - } - } - } - return result; - } - - @Override - public void disconnectLeaderboardColumnFromTrackedRace(String leaderboardName, String raceColumnName, String fleetName) { - getService().apply(new DisconnectLeaderboardColumnFromTrackedRace(leaderboardName, raceColumnName, fleetName)); - } - - @Override - public void updateLeaderboardCarryValue(String leaderboardName, String competitorIdAsString, Double carriedPoints) { - getService().apply(new UpdateLeaderboardCarryValue(leaderboardName, competitorIdAsString, carriedPoints)); - } - - @Override - public Triple updateLeaderboardMaxPointsReason(String leaderboardName, String competitorIdAsString, String raceColumnName, - MaxPointsReason maxPointsReason, Date date) throws NoWindException { - return getService().apply( - new UpdateLeaderboardMaxPointsReason(leaderboardName, raceColumnName, competitorIdAsString, - maxPointsReason, new MillisecondsTimePoint(date))); - } - - @Override - public Triple updateLeaderboardScoreCorrection(String leaderboardName, - String competitorIdAsString, String columnName, Double correctedScore, Date date) throws NoWindException { - return getService().apply( - new UpdateLeaderboardScoreCorrection(leaderboardName, columnName, competitorIdAsString, correctedScore, - new MillisecondsTimePoint(date))); - } - - @Override - public Void updateLeaderboardScoreCorrectionMetadata(String leaderboardName, Date timePointOfLastCorrectionValidity, String comment) { - return getService().apply( - new UpdateLeaderboardScoreCorrectionMetadata(leaderboardName, - timePointOfLastCorrectionValidity == null ? null : new MillisecondsTimePoint(timePointOfLastCorrectionValidity), - comment)); - } - - @Override - public void updateLeaderboardScoreCorrectionsAndMaxPointsReasons(BulkScoreCorrectionDTO updates) throws NoWindException { - Date dateForResults = new Date(); // we don't care about the result date/time here; use current date as default - for (Map.Entry> e : updates.getScoreUpdatesForRaceColumnByCompetitorIdAsString().entrySet()) { - for (Map.Entry raceColumnNameAndCorrectedScore : e.getValue().entrySet()) { - updateLeaderboardScoreCorrection(updates.getLeaderboardName(), e.getKey(), - raceColumnNameAndCorrectedScore.getKey(), raceColumnNameAndCorrectedScore.getValue(), dateForResults); - } - } - for (Map.Entry> e : updates.getMaxPointsUpdatesForRaceColumnByCompetitorIdAsString().entrySet()) { - for (Map.Entry raceColumnNameAndNewMaxPointsReason : e.getValue().entrySet()) { - updateLeaderboardMaxPointsReason(updates.getLeaderboardName(), e.getKey(), - raceColumnNameAndNewMaxPointsReason.getKey(), raceColumnNameAndNewMaxPointsReason.getValue(), dateForResults); - } - } - } - - @Override - public void updateCompetitorDisplayNameInLeaderboard(String leaderboardName, String competitorIdAsString, String displayName) { - getService().apply(new UpdateCompetitorDisplayNameInLeaderboard(leaderboardName, competitorIdAsString, displayName)); - } - - @Override - public void moveLeaderboardColumnUp(String leaderboardName, String columnName) { - getService().apply(new MoveLeaderboardColumnUp(leaderboardName, columnName)); - } - - @Override - public void moveLeaderboardColumnDown(String leaderboardName, String columnName) { - getService().apply(new MoveLeaderboardColumnDown(leaderboardName, columnName)); - } - - @Override - public void updateIsMedalRace(String leaderboardName, String columnName, boolean isMedalRace) { - getService().apply(new UpdateIsMedalRace(leaderboardName, columnName, isMedalRace)); - } - - @Override - public void updateRaceDelayToLive(RegattaAndRaceIdentifier regattaAndRaceIdentifier, long delayToLiveInMs) { - getService().apply(new UpdateRaceDelayToLive(regattaAndRaceIdentifier, delayToLiveInMs)); - } - - @Override - public void updateRacesDelayToLive(List regattaAndRaceIdentifiers, long delayToLiveInMs) { - for (RegattaAndRaceIdentifier regattaAndRaceIdentifier : regattaAndRaceIdentifiers) { - getService().apply(new UpdateRaceDelayToLive(regattaAndRaceIdentifier, delayToLiveInMs)); - } - } - - @Override - public List getPreviousSwissTimingConfigurations() { - Iterable configs = swissTimingAdapterPersistence.getSwissTimingConfigurations(); - List result = new ArrayList(); - for (SwissTimingConfiguration stConfig : configs) { - result.add(new SwissTimingConfigurationDTO(stConfig.getName(), stConfig.getHostname(), stConfig.getPort(), stConfig.canSendRequests())); - } - return result; - } - - @Override - public List listSwissTimingRaces(String hostname, int port, boolean canSendRequests) - throws UnknownHostException, IOException, InterruptedException, ParseException { - List result = new ArrayList(); - for (com.sap.sailing.domain.swisstimingadapter.RaceRecord rr : getService().getSwissTimingRaceRecords(hostname, port, canSendRequests)) { - result.add(new SwissTimingRaceRecordDTO(rr.getRaceID(), rr.getDescription(), rr.getStartTime())); - } - return result; - } - - @Override - public void storeSwissTimingConfiguration(String configName, String hostname, int port, boolean canSendRequests) { - swissTimingAdapterPersistence.storeSwissTimingConfiguration(swissTimingFactory.createSwissTimingConfiguration(configName, hostname, port, canSendRequests)); - } - - @Override - public void trackWithSwissTiming(RegattaIdentifier regattaToAddTo, SwissTimingRaceRecordDTO rr, String hostname, int port, - boolean canSendRequests, boolean trackWind, final boolean correctWindByDeclination) throws Exception { - final RacesHandle raceHandle = getService().addSwissTimingRace(regattaToAddTo, rr.ID, hostname, port, - canSendRequests, - MongoWindStoreFactory.INSTANCE.getMongoWindStore(mongoObjectFactory, domainObjectFactory), - RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS); - if (trackWind) { - new Thread("Wind tracking starter for race "+rr.ID+"/"+rr.description) { - public void run() { - try { - startTrackingWind(raceHandle, correctWindByDeclination, RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }.start(); - } - } - - @Override - public void sendSwissTimingDummyRace(String racMessage, String stlMesssage, String ccgMessage) { - getService().storeSwissTimingDummyRace(racMessage,stlMesssage,ccgMessage); - } - - @Override - public List listSwissTiminigReplayRaces(String swissTimingUrl) { - List replayRaces = SwissTimingReplayServiceFactory.INSTANCE - .createSwissTimingReplayService().listReplayRaces(swissTimingUrl); - List result = new ArrayList(replayRaces.size()); - for (SwissTimingReplayRace replayRace : replayRaces) { - result.add(new SwissTimingReplayRaceDTO(replayRace.getFlightNumber(), replayRace.getRaceId(), replayRace.getRsc(), replayRace.getName(), replayRace.getBoatClass(), replayRace.getStartTime(), replayRace.getLink())); - } - return result; - } - - @Override - public void replaySwissTimingRace(RegattaIdentifier regattaIdentifier, SwissTimingReplayRaceDTO replayRaceDTO, - boolean trackWind, boolean correctWindByDeclination, boolean simulateWithStartTimeNow) { - Regatta regatta; - if (regattaIdentifier == null) { - regatta = getService().createRegatta(replayRaceDTO.rsc, replayRaceDTO.boat_class, - RegattaImpl.getFullName(replayRaceDTO.rsc, replayRaceDTO.boat_class), - Collections.singletonList(new SeriesImpl("Default", /* isMedal */false, Collections - .singletonList(new FleetImpl("Default")), /* race column names */new ArrayList(), - getService())), - false, baseDomainFactory.createScoringScheme(ScoringSchemeType.LOW_POINT)); - } else { - regatta = getService().getRegatta(regattaIdentifier); - } - SwissTimingReplayService replayService = SwissTimingReplayServiceFactory.INSTANCE.createSwissTimingReplayService(); - replayService.loadRaceData(replayRaceDTO.link, getService().getSwissTimingDomainFactory(), regatta, getService()); - } - - @Override - public String[] getCountryCodes() { - List countryCodes = new ArrayList(); - for (CountryCode cc : countryCodeFactory.getAll()) { - if (cc.getThreeLetterIOCCode() != null && !cc.getThreeLetterIOCCode().equals("")) { - countryCodes.add(cc.getThreeLetterIOCCode()); - } - } - Collections.sort(countryCodes); - return countryCodes.toArray(new String[0]); - } - - /** - * Finds a competitor in a sequence of competitors that has an {@link Competitor#getId()} equal to id. - */ - private Competitor getCompetitorById(Iterable competitors, String id) { - for (Competitor c : competitors) { - if (c.getId().toString().equals(id)) { - return c; - } - } - return null; - } - - private Double getCompetitorRaceDataEntry(DetailType dataType, TrackedRace trackedRace, Competitor competitor, - TimePoint timePoint) throws NoWindException { - Double result = null; - Course course = trackedRace.getRace().getCourse(); - course.lockForRead(); // make sure the tracked leg survives this call even if a course update is pending - try { - TrackedLegOfCompetitor trackedLeg = trackedRace.getTrackedLeg(competitor, timePoint); - if (trackedLeg != null) { - switch (dataType) { - case CURRENT_SPEED_OVER_GROUND_IN_KNOTS: - SpeedWithBearing speedOverGround = trackedLeg.getSpeedOverGround(timePoint); - result = (speedOverGround == null) ? null : speedOverGround.getKnots(); - break; - case VELOCITY_MADE_GOOD_IN_KNOTS: - Speed velocityMadeGood = trackedLeg.getVelocityMadeGood(timePoint); - result = (velocityMadeGood == null) ? null : velocityMadeGood.getKnots(); - break; - case DISTANCE_TRAVELED: - Distance distanceTraveled = trackedRace.getDistanceTraveled(competitor, timePoint); - result = distanceTraveled == null ? null : distanceTraveled.getMeters(); - break; - case GAP_TO_LEADER_IN_SECONDS: - result = trackedLeg.getGapToLeaderInSeconds(timePoint); - break; - case WINDWARD_DISTANCE_TO_OVERALL_LEADER: - Distance distanceToLeader = trackedLeg.getWindwardDistanceToOverallLeader(timePoint); - result = (distanceToLeader == null) ? null : distanceToLeader.getMeters(); - break; - case RACE_RANK: - result = (double) trackedLeg.getRank(timePoint); - break; - } - } - return result; - } finally { - course.unlockAfterRead(); - } - } - - @Override - public CompetitorsRaceDataDTO getCompetitorsRaceData(RegattaAndRaceIdentifier race, List competitors, Date from, Date to, - long stepSizeInMs, DetailType detailType) throws NoWindException { - CompetitorsRaceDataDTO result = null; - TrackedRace trackedRace = getExistingTrackedRace(race); - if (trackedRace != null) { - TimePoint newestEvent = trackedRace.getTimePointOfNewestEvent(); - TimePoint startTime = from == null ? trackedRace.getStartOfTracking() : new MillisecondsTimePoint(from); - TimePoint endTime = (to == null || to.after(newestEvent.asDate())) ? newestEvent : new MillisecondsTimePoint(to); - result = new CompetitorsRaceDataDTO(detailType, startTime.asDate(), endTime.asDate()); - - for (CompetitorDTO competitorDTO : competitors) { - Competitor competitor = getCompetitorById(trackedRace.getRace().getCompetitors(), competitorDTO.id); - ArrayList> markPassingsData = new ArrayList>(); - ArrayList> raceData = new ArrayList>(); - // Filling the mark passings - Set competitorMarkPassings = trackedRace.getMarkPassings(competitor); - if (competitorMarkPassings != null) { - trackedRace.lockForRead(competitorMarkPassings); - try { - for (MarkPassing markPassing : competitorMarkPassings) { - MillisecondsTimePoint time = new MillisecondsTimePoint(markPassing.getTimePoint().asMillis()); - markPassingsData.add(new Triple(markPassing.getWaypoint().getName(), - time.asDate(), - getCompetitorRaceDataEntry(detailType, trackedRace, competitor, time))); - } - } finally { - trackedRace.unlockAfterRead(competitorMarkPassings); - } - } - if (startTime != null && endTime != null) { - for (long i = startTime.asMillis(); i <= endTime.asMillis(); i += stepSizeInMs) { - MillisecondsTimePoint time = new MillisecondsTimePoint(i); - raceData.add(new Pair(time.asDate(), getCompetitorRaceDataEntry(detailType, trackedRace, - competitor, time))); - } - } - result.setCompetitorData(competitorDTO, new CompetitorRaceDataDTO(competitorDTO, detailType, markPassingsData, raceData)); - } - } - return result; - } - - @Override - public Map> getDouglasPoints(RegattaAndRaceIdentifier raceIdentifier, - Map from, Map to, - double meters) throws NoWindException { - Map> result = new HashMap>(); - TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); - if (trackedRace != null) { - final WindSource windSource = new WindSourceImpl(WindSourceType.COMBINED); - MeterDistance maxDistance = new MeterDistance(meters); - for (Competitor competitor : trackedRace.getRace().getCompetitors()) { - CompetitorDTO competitorDTO = convertToCompetitorDTO(competitor); - if (from.containsKey(competitorDTO)) { - // get Track of competitor - GPSFixTrack gpsFixTrack = trackedRace.getTrack(competitor); - // Distance for DouglasPeucker - TimePoint timePointFrom = new MillisecondsTimePoint(from.get(competitorDTO)); - TimePoint timePointTo = new MillisecondsTimePoint(to.get(competitorDTO)); - List gpsFixApproximation = trackedRace.approximate(competitor, maxDistance, - timePointFrom, timePointTo); - List gpsFixDouglasList = new ArrayList(); - for (int i = 0; i < gpsFixApproximation.size(); i++) { - GPSFix fix = gpsFixApproximation.get(i); - SpeedWithBearing speedWithBearing; - if (i < gpsFixApproximation.size() - 1) { - GPSFix next = gpsFixApproximation.get(i + 1); - Bearing bearing = fix.getPosition().getBearingGreatCircle(next.getPosition()); - Speed speed = fix.getPosition().getDistance(next.getPosition()) - .inTime(next.getTimePoint().asMillis() - fix.getTimePoint().asMillis()); - speedWithBearing = new KnotSpeedWithBearingImpl(speed.getKnots(), bearing); - } else { - speedWithBearing = gpsFixTrack.getEstimatedSpeed(fix.getTimePoint()); - } - Tack tack = trackedRace.getTack(competitor, fix.getTimePoint()); - TrackedLegOfCompetitor trackedLegOfCompetitor = trackedRace.getTrackedLeg(competitor, fix.getTimePoint()); - LegType legType = trackedLegOfCompetitor == null ? null : trackedRace.getTrackedLeg( - trackedLegOfCompetitor.getLeg()).getLegType(fix.getTimePoint()); - Wind wind = trackedRace.getWind(fix.getPosition(), fix.getTimePoint()); - WindDTO windDTO = createWindDTOFromAlreadyAveraged(wind, trackedRace.getOrCreateWindTrack(windSource), fix.getTimePoint()); - GPSFixDTO fixDTO = createGPSFixDTO(fix, speedWithBearing, windDTO, tack, legType, /* extrapolated */false); - gpsFixDouglasList.add(fixDTO); - } - result.put(competitorDTO, gpsFixDouglasList); - } - } - } - return result; - } - - @Override - public Map> getManeuvers(RegattaAndRaceIdentifier raceIdentifier, - Map from, Map to) throws NoWindException { - Map> result = new HashMap>(); - final TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); - if (trackedRace != null) { - Map>> futures = new HashMap>>(); - for (final Competitor competitor : trackedRace.getRace().getCompetitors()) { - CompetitorDTO competitorDTO = convertToCompetitorDTO(competitor); - if (from.containsKey(competitorDTO)) { - final TimePoint timePointFrom = new MillisecondsTimePoint(from.get(competitorDTO)); - final TimePoint timePointTo = new MillisecondsTimePoint(to.get(competitorDTO)); - RunnableFuture> future = new FutureTask>( - new Callable>() { - @Override - public List call() { - List maneuversForCompetitor; - try { - maneuversForCompetitor = trackedRace.getManeuvers(competitor, timePointFrom, - timePointTo, /* waitForLatest */ true); - } catch (NoWindException e) { - throw new NoWindError(e); - } - return createManeuverDTOsForCompetitor(maneuversForCompetitor, trackedRace, - competitor); - } - }); - executor.execute(future); - futures.put(competitorDTO, future); - } - } - for (Map.Entry>> competitorAndFuture : futures.entrySet()) { - try { - result.put(competitorAndFuture.getKey(), competitorAndFuture.getValue().get()); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - throw new RuntimeException(e); - } - } - } - return result; - } - - private List createManeuverDTOsForCompetitor(List maneuvers, TrackedRace trackedRace, Competitor competitor) { - List result = new ArrayList(); - for (Maneuver maneuver : maneuvers) { - final ManeuverDTO maneuverDTO; - if (maneuver.getType() == ManeuverType.MARK_PASSING) { - maneuverDTO = new MarkpassingManeuverDTO(maneuver.getType(), maneuver.getNewTack(), - new PositionDTO(maneuver.getPosition().getLatDeg(), maneuver.getPosition().getLngDeg()), - maneuver.getTimePoint().asDate(), - createSpeedWithBearingDTO(maneuver.getSpeedWithBearingBefore()), - createSpeedWithBearingDTO(maneuver.getSpeedWithBearingAfter()), - maneuver.getDirectionChangeInDegrees(), maneuver.getManeuverLoss()==null?null:maneuver.getManeuverLoss().getMeters(), - ((MarkPassingManeuver) maneuver).getSide()); - } else { - maneuverDTO = new ManeuverDTO(maneuver.getType(), maneuver.getNewTack(), - new PositionDTO(maneuver.getPosition().getLatDeg(), maneuver.getPosition().getLngDeg()), - maneuver.getTimePoint().asDate(), - createSpeedWithBearingDTO(maneuver.getSpeedWithBearingBefore()), - createSpeedWithBearingDTO(maneuver.getSpeedWithBearingAfter()), - maneuver.getDirectionChangeInDegrees(), maneuver.getManeuverLoss()==null?null:maneuver.getManeuverLoss().getMeters()); - } - result.add(maneuverDTO); - } - return result; - } - - @Override - public RaceDefinition getRace(RegattaAndRaceIdentifier raceIdentifier) { - Regatta regatta = getService().getRegattaByName(raceIdentifier.getRegattaName()); - RaceDefinition race = getRaceByName(regatta, raceIdentifier.getRaceName()); - return race; - } - - @Override - public TrackedRace getTrackedRace(RegattaAndRaceIdentifier regattaNameAndRaceName) { - Regatta regatta = getService().getRegattaByName(regattaNameAndRaceName.getRegattaName()); - RaceDefinition race = getRaceByName(regatta, regattaNameAndRaceName.getRaceName()); - TrackedRace trackedRace = getService().getOrCreateTrackedRegatta(regatta).getTrackedRace(race); - return trackedRace; - } - - @Override - public TrackedRace getExistingTrackedRace(RegattaAndRaceIdentifier regattaNameAndRaceName) { - return getService().getExistingTrackedRace(regattaNameAndRaceName); - } - - @Override - public Regatta getRegatta(RegattaName regattaIdentifier) { - return getService().getRegattaByName(regattaIdentifier.getRegattaName()); - } - - /** - * Returns a servlet context that, when asked for a resource, first tries the original servlet context's implementation. If that - * fails, it prepends "war/" to the request because the war/ folder contains all the resources exposed externally - * through the HTTP server. - */ - @Override - public ServletContext getServletContext() { - return new DelegatingServletContext(super.getServletContext()); - } - - @Override - /** - * Override of function to prevent exception "Blocked request without GWT permutation header (XSRF attack?)" when testing the GWT sites - */ - protected void checkPermutationStrongName() throws SecurityException { - //Override to prevent exception "Blocked request without GWT permutation header (XSRF attack?)" when testing the GWT sites - return; - } - - @Override - public List getLeaderboardGroups(boolean withGeoLocationData) { - ArrayList leaderboardGroupDTOs = new ArrayList(); - Map leaderboardGroups = getService().getLeaderboardGroups(); - - for (LeaderboardGroup leaderboardGroup : leaderboardGroups.values()) { - leaderboardGroupDTOs.add(convertToLeaderboardGroupDTO(leaderboardGroup, withGeoLocationData)); - } - - return leaderboardGroupDTOs; - } - - @Override - public LeaderboardGroupDTO getLeaderboardGroupByName(String groupName, boolean withGeoLocationData) { - return convertToLeaderboardGroupDTO(getService().getLeaderboardGroupByName(groupName), withGeoLocationData); - } - - private LeaderboardGroupDTO convertToLeaderboardGroupDTO(LeaderboardGroup leaderboardGroup, boolean withGeoLocationData) { - LeaderboardGroupDTO groupDTO = new LeaderboardGroupDTO(); - groupDTO.name = leaderboardGroup.getName(); - groupDTO.description = leaderboardGroup.getDescription(); - groupDTO.displayLeaderboardsInReverseOrder = leaderboardGroup.isDisplayGroupsInReverseOrder(); - for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { - groupDTO.leaderboards.add(createStrippedLeaderboardDTO(leaderboard, withGeoLocationData)); - } - Leaderboard overallLeaderboard = leaderboardGroup.getOverallLeaderboard(); - if (overallLeaderboard != null) { - groupDTO.setOverallLeaderboardDiscardThresholds(overallLeaderboard.getResultDiscardingRule().getDiscardIndexResultsStartingWithHowManyRaces()); - groupDTO.setOverallLeaderboardScoringSchemeType(overallLeaderboard.getScoringScheme().getType()); - } - return groupDTO; - } - - - private PlacemarkDTO convertToPlacemarkDTO(Placemark placemark) { - Position position = placemark.getPosition(); - return new PlacemarkDTO(placemark.getName(), placemark.getCountryCode(), new PositionDTO(position.getLatDeg(), - position.getLngDeg()), placemark.getPopulation()); - } - - @Override - public void renameLeaderboardGroup(String oldName, String newName) { - getService().apply(new RenameLeaderboardGroup(oldName, newName)); - } - - @Override - public void removeLeaderboardGroup(String groupName) { - getService().apply(new RemoveLeaderboardGroup(groupName)); - } - - @Override - public LeaderboardGroupDTO createLeaderboardGroup(String groupName, String description, boolean displayGroupsInReverseOrder, - int[] overallLeaderboardDiscardThresholds, ScoringSchemeType overallLeaderboardScoringSchemeType) { - CreateLeaderboardGroup createLeaderboardGroupOp = new CreateLeaderboardGroup(groupName, description, displayGroupsInReverseOrder, - new ArrayList(), overallLeaderboardDiscardThresholds, overallLeaderboardScoringSchemeType); - return convertToLeaderboardGroupDTO(getService().apply(createLeaderboardGroupOp), false); - } - - @Override - public void updateLeaderboardGroup(String oldName, String newName, String description, List leaderboardNames, - int[] overallLeaderboardDiscardThresholds, ScoringSchemeType overallLeaderboardScoringSchemeType) { - getService().apply( - new UpdateLeaderboardGroup(oldName, newName, description, leaderboardNames, - overallLeaderboardDiscardThresholds, overallLeaderboardScoringSchemeType)); - } - - @Override - public ReplicationStateDTO getReplicaInfo() { - ReplicationService service = getReplicationService(); - Set replicaDTOs = new HashSet(); - for (ReplicaDescriptor replicaDescriptor : service.getReplicaInfo()) { - final Map>, Integer> statistics = service.getStatistics(replicaDescriptor); - Map replicationCountByOperationClassName = new HashMap(); - for (Map.Entry>, Integer> e : statistics.entrySet()) { - replicationCountByOperationClassName.put(e.getKey().getName(), e.getValue()); - } - replicaDTOs.add(new ReplicaDTO(replicaDescriptor.getIpAddress().getHostName(), replicaDescriptor.getRegistrationTime().asDate(), - replicationCountByOperationClassName)); - } - ReplicationMasterDTO master; - ReplicationMasterDescriptor replicatingFromMaster = service.isReplicatingFromMaster(); - if (replicatingFromMaster == null) { - master = null; - } else { - master = new ReplicationMasterDTO(replicatingFromMaster.getHostname(), replicatingFromMaster.getMessagingPort(), - replicatingFromMaster.getServletPort()); - } - return new ReplicationStateDTO(master, replicaDTOs); - } - - @Override - public void startReplicatingFromMaster(String masterName, String exchangeName, int servletPort, int messagingPort) throws IOException, ClassNotFoundException { - getReplicationService().startToReplicateFrom( - ReplicationFactory.INSTANCE.createReplicationMasterDescriptor(masterName, exchangeName, servletPort, messagingPort)); - } - - @Override - public List getEvents() { - List result = new ArrayList(); - for (Event event : getService().getAllEvents()) { - EventDTO eventDTO = convertToEventDTO(event); - result.add(eventDTO); - } - return result; - } - - @Override - public void updateEvent(String eventName, Serializable id, VenueDTO venue, String publicationUrl, boolean isPublic, List regattaNames) { - getService().apply(new UpdateEvent(id, eventName, venue.name, publicationUrl, isPublic, regattaNames)); - } - - @Override - public EventDTO createEvent(String eventName, String venue, String publicationUrl, boolean isPublic) { - return convertToEventDTO(getService().apply(new CreateEvent(eventName, venue, publicationUrl, isPublic, new ArrayList()))); - } - - @Override - public void removeEvent(String eventName) { - getService().apply(new RemoveEvent(eventName)); - } - - @Override - public void renameEvent(String oldName, String newName) { - getService().apply(new RenameEvent(oldName, newName)); - } - - @Override - public EventDTO getEventByName(String eventName) { - EventDTO result = null; - for (Event event : getService().getAllEvents()) { - if(event.getName().equals(eventName)) { - result = convertToEventDTO(event); - break; - } - } - return result; - } - - private EventDTO convertToEventDTO(Event event) { - EventDTO eventDTO = new EventDTO(event.getName()); - eventDTO.venue = new VenueDTO(); - eventDTO.venue.name = event.getVenue() != null ? event.getVenue().getName() : null; - eventDTO.publicationUrl = event.getPublicationUrl(); - eventDTO.isPublic = event.isPublic(); - eventDTO.id = event.getId(); - eventDTO.regattas = new ArrayList(); - for (Regatta regatta: event.getRegattas()) { - RegattaDTO regattaDTO = new RegattaDTO(); - regattaDTO.name = regatta.getName(); - eventDTO.regattas.add(regattaDTO); - } - return eventDTO; - } - - @Override - public void removeRegatta(RegattaIdentifier regattaIdentifier) { - getService().apply(new RemoveRegatta(regattaIdentifier)); - } - - private RaceColumnInSeriesDTO convertToRaceColumnInSeriesDTO(RaceColumnInSeries raceColumnInSeries) { - RaceColumnInSeriesDTO raceColumnInSeriesDTO = new RaceColumnInSeriesDTO(raceColumnInSeries.getSeries().getName(), - raceColumnInSeries.getRegatta().getName()); - raceColumnInSeriesDTO.name = raceColumnInSeries.getName(); - raceColumnInSeriesDTO.setMedalRace(raceColumnInSeries.isMedalRace()); - return raceColumnInSeriesDTO; - } - - @Override - public List addRaceColumnsToSeries(RegattaIdentifier regattaIdentifier, String seriesName, List columnNames) { - List result = new ArrayList(); - for(String columnName: columnNames) { - RaceColumnInSeries raceColumnInSeries = getService().apply(new AddColumnToSeries(regattaIdentifier, seriesName, columnName)); - if(raceColumnInSeries != null) { - result.add(convertToRaceColumnInSeriesDTO(raceColumnInSeries)); - } - } - return result; - } - - @Override - public RaceColumnInSeriesDTO addRaceColumnToSeries(RegattaIdentifier regattaIdentifier, String seriesName, String columnName) { - RaceColumnInSeriesDTO result = null; - RaceColumnInSeries raceColumnInSeries = getService().apply(new AddColumnToSeries(regattaIdentifier, seriesName, columnName)); - if(raceColumnInSeries != null) { - result = convertToRaceColumnInSeriesDTO(raceColumnInSeries); - } - return result; - } - - @Override - public void removeRaceColumnsFromSeries(RegattaIdentifier regattaIdentifier, String seriesName, List columnNames) { - for(String columnName: columnNames) { - getService().apply(new RemoveColumnFromSeries(regattaIdentifier, seriesName, columnName)); - } - } - - @Override - public void removeRaceColumnFromSeries(RegattaIdentifier regattaIdentifier, String seriesName, String columnName) { - getService().apply(new RemoveColumnFromSeries(regattaIdentifier, seriesName, columnName)); - } - - @Override - public void moveRaceColumnInSeriesUp(RegattaIdentifier regattaIdentifier, String seriesName, String columnName) { - getService().apply(new MoveColumnInSeriesUp(regattaIdentifier, seriesName, columnName)); - } - - @Override - public void moveRaceColumnInSeriesDown(RegattaIdentifier regattaIdentifier, String seriesName, String columnName) { - getService().apply(new MoveColumnInSeriesDown(regattaIdentifier, seriesName, columnName)); - } - - @Override - public RegattaDTO createRegatta(String regattaName, String boatClassName, - LinkedHashMap>, Boolean>> seriesNamesWithFleetNamesAndFleetOrderingAndMedal, - boolean persistent, ScoringSchemeType scoringSchemeType) { - Regatta regatta = getService().apply( - new AddSpecificRegatta(regattaName, boatClassName, UUID.randomUUID(), - seriesNamesWithFleetNamesAndFleetOrderingAndMedal, - persistent, baseDomainFactory.createScoringScheme(scoringSchemeType))); - return convertToRegattaDTO(regatta); - } - - @Override - public RegattaScoreCorrectionDTO getScoreCorrections(String scoreCorrectionProviderName, String eventName, - String boatClassName, Date timePointWhenResultPublished) throws Exception { - RegattaScoreCorrectionDTO result = null; - for (ScoreCorrectionProvider scp : getScoreCorrectionProviders()) { - if (scp.getName().equals(scoreCorrectionProviderName)) { - result = createScoreCorrection(scp.getScoreCorrections(eventName, boatClassName, - new MillisecondsTimePoint(timePointWhenResultPublished))); - break; - } - } - return result; - } - - private RegattaScoreCorrectionDTO createScoreCorrection(RegattaScoreCorrections scoreCorrections) { - // Key is the race name or number as String; values are maps whose key is the sailID. - LinkedHashMap> map = new LinkedHashMap>(); - for (ScoreCorrectionsForRace sc4r : scoreCorrections.getScoreCorrectionsForRaces()) { - Map entryMap = new HashMap(); - for (String sailID : sc4r.getSailIDs()) { - entryMap.put(sailID, createScoreCorrectionEntryDTO(sc4r.getScoreCorrectionForCompetitor(sailID))); - } - map.put(sc4r.getRaceNameOrNumber(), entryMap); - } - return new RegattaScoreCorrectionDTO(scoreCorrections.getProvider().getName(), map); - } - - private ScoreCorrectionEntryDTO createScoreCorrectionEntryDTO( - ScoreCorrectionForCompetitorInRace scoreCorrectionForCompetitor) { - return new ScoreCorrectionEntryDTO(scoreCorrectionForCompetitor.getPoints(), - scoreCorrectionForCompetitor.isDiscarded(), scoreCorrectionForCompetitor.getMaxPointsReason()); - } - - private FregResultProvider getFregService() { - FregResultProvider result = null; - for (ScoreCorrectionProvider scp : getScoreCorrectionProviders()) { - if (scp instanceof FregResultProvider) { - result = (FregResultProvider) scp; - break; - } - } - return result; - } - - @Override - public List getFregResultUrls() { - List result = new ArrayList(); - final FregResultProvider fregService = getFregService(); - if (fregService != null) { - Iterable allUrls = fregService.getAllUrls(); - for (URL url : allUrls) { - result.add(url.toString()); - } - } - return result; - } - - @Override - public void removeFregURLs(Set toRemove) throws Exception { - FregResultProvider fregService = getFregService(); - if (fregService != null) { - for (String urlToRemove : toRemove) { - fregService.removeResultUrl(new URL(urlToRemove)); - } - } - } - - @Override - public void addFragUrl(String result) throws Exception { - FregResultProvider fregService = getFregService(); - if (fregService != null) { - fregService.registerResultUrl(new URL(result)); - } - } - - @Override - public List>> getRankedCompetitorsFromBestToWorstAfterEachRaceColumn(String leaderboardName, Date date) throws NoWindException { - Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName); - final TimePoint timePoint; - if (date == null) { - final TimePoint nowMinusDelay = leaderboard.getNowMinusDelay(); - final TimePoint timePointOfLatestModification = leaderboard.getTimePointOfLatestModification(); - if (timePointOfLatestModification != null && !nowMinusDelay.before(timePointOfLatestModification)) { - timePoint = timePointOfLatestModification; - } else { - timePoint = nowMinusDelay; - } - } else { - timePoint = new MillisecondsTimePoint(date); - } - Map> preResult = leaderboard - .getRankedCompetitorsFromBestToWorstAfterEachRaceColumn(timePoint); - List>> result = new ArrayList>>(); - for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - List competitorList = getCompetitorDTOList(preResult.get(raceColumn)); - result.add(new Pair>(raceColumn.getName(), competitorList)); - } - return result; - } - - @Override - public List getOverallLeaderboardNamesContaining(String leaderboardName) { - Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName); - if (leaderboard == null) { - throw new IllegalArgumentException("Couldn't find leaderboard named "+leaderboardName); - } - List result = new ArrayList(); - for (Map.Entry leaderboardEntry : getService().getLeaderboards().entrySet()) { - if (leaderboardEntry.getValue() instanceof MetaLeaderboard) { - MetaLeaderboard metaLeaderboard = (MetaLeaderboard) leaderboardEntry.getValue(); - if (Util.contains(metaLeaderboard.getLeaderboards(), leaderboard)) { - result.add(leaderboardEntry.getKey()); - } - } - } - return result; - } - - @Override - public List getPreviousSwissTimingArchiveConfigurations() { - Iterable configs = swissTimingAdapterPersistence.getSwissTimingArchiveConfigurations(); - List result = new ArrayList(); - for (SwissTimingArchiveConfiguration stArchiveConfig : configs) { - result.add(new SwissTimingArchiveConfigurationDTO(stArchiveConfig.getJsonUrl())); - } - return result; - } - - @Override - public void storeSwissTimingArchiveConfiguration(String swissTimingJsonUrl) { - swissTimingAdapterPersistence.storeSwissTimingArchiveConfiguration(swissTimingFactory.createSwissTimingArchiveConfiguration( - swissTimingJsonUrl)); - } +package com.sap.sailing.gwt.ui.server; + +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.net.MalformedURLException; +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.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.WeakHashMap; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.RunnableFuture; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.ServletContext; + +import org.osgi.framework.BundleContext; +import org.osgi.util.tracker.ServiceTracker; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.ControlPoint; +import com.sap.sailing.domain.base.Course; +import com.sap.sailing.domain.base.Event; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.Gate; +import com.sap.sailing.domain.base.Leg; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnInSeries; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.SpeedWithBearing; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.base.impl.KnotSpeedImpl; +import com.sap.sailing.domain.base.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.base.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.base.impl.RegattaImpl; +import com.sap.sailing.domain.base.impl.SeriesImpl; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.Color; +import com.sap.sailing.domain.common.CountryCode; +import com.sap.sailing.domain.common.DetailType; +import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.LegType; +import com.sap.sailing.domain.common.ManeuverType; +import com.sap.sailing.domain.common.MaxPointsReason; +import com.sap.sailing.domain.common.NoWindError; +import com.sap.sailing.domain.common.NoWindException; +import com.sap.sailing.domain.common.Placemark; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.RaceFetcher; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.RegattaFetcher; +import com.sap.sailing.domain.common.RegattaIdentifier; +import com.sap.sailing.domain.common.RegattaName; +import com.sap.sailing.domain.common.RegattaNameAndRaceName; +import com.sap.sailing.domain.common.RegattaScoreCorrections; +import com.sap.sailing.domain.common.RegattaScoreCorrections.ScoreCorrectionForCompetitorInRace; +import com.sap.sailing.domain.common.RegattaScoreCorrections.ScoreCorrectionsForRace; +import com.sap.sailing.domain.common.ScoreCorrectionProvider; +import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.Tack; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.WindSourceType; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sailing.domain.common.impl.KilometersPerHourSpeedImpl; +import com.sap.sailing.domain.common.impl.MeterDistance; +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.common.impl.WindSourceImpl; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.Leaderboard.Entry; +import com.sap.sailing.domain.leaderboard.LeaderboardGroup; +import com.sap.sailing.domain.leaderboard.MetaLeaderboard; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +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.persistence.MongoWindStoreFactory; +import com.sap.sailing.domain.swisstimingadapter.SwissTimingArchiveConfiguration; +import com.sap.sailing.domain.swisstimingadapter.SwissTimingConfiguration; +import com.sap.sailing.domain.swisstimingadapter.SwissTimingFactory; +import com.sap.sailing.domain.swisstimingadapter.persistence.SwissTimingAdapterPersistence; +import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayRace; +import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayService; +import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayServiceFactory; +import com.sap.sailing.domain.tracking.DynamicTrackedRace; +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.GPSFixTrack; +import com.sap.sailing.domain.tracking.Maneuver; +import com.sap.sailing.domain.tracking.MarkPassing; +import com.sap.sailing.domain.tracking.MarkPassingManeuver; +import com.sap.sailing.domain.tracking.RaceChangeListener; +import com.sap.sailing.domain.tracking.RaceTracker; +import com.sap.sailing.domain.tracking.RacesHandle; +import com.sap.sailing.domain.tracking.TrackedLeg; +import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.domain.tracking.Wind; +import com.sap.sailing.domain.tracking.WindTrack; +import com.sap.sailing.domain.tracking.WindWithConfidence; +import com.sap.sailing.domain.tracking.impl.GPSFixMovingImpl; +import com.sap.sailing.domain.tracking.impl.TrackedRaceImpl; +import com.sap.sailing.domain.tracking.impl.WindImpl; +import com.sap.sailing.domain.tractracadapter.DomainFactory; +import com.sap.sailing.domain.tractracadapter.RaceRecord; +import com.sap.sailing.domain.tractracadapter.TracTracConfiguration; +import com.sap.sailing.freg.resultimport.FregResultProvider; +import com.sap.sailing.geocoding.ReverseGeocoder; +import com.sap.sailing.gwt.ui.client.SailingService; +import com.sap.sailing.gwt.ui.shared.BoatClassDTO; +import com.sap.sailing.gwt.ui.shared.BulkScoreCorrectionDTO; +import com.sap.sailing.gwt.ui.shared.CompetitorDTO; +import com.sap.sailing.gwt.ui.shared.CompetitorRaceDataDTO; +import com.sap.sailing.gwt.ui.shared.CompetitorsRaceDataDTO; +import com.sap.sailing.gwt.ui.shared.ControlPointDTO; +import com.sap.sailing.gwt.ui.shared.CourseDTO; +import com.sap.sailing.gwt.ui.shared.EventDTO; +import com.sap.sailing.gwt.ui.shared.FleetDTO; +import com.sap.sailing.gwt.ui.shared.GPSFixDTO; +import com.sap.sailing.gwt.ui.shared.GateDTO; +import com.sap.sailing.gwt.ui.shared.LeaderboardDTO; +import com.sap.sailing.gwt.ui.shared.LeaderboardEntryDTO; +import com.sap.sailing.gwt.ui.shared.LeaderboardGroupDTO; +import com.sap.sailing.gwt.ui.shared.LeaderboardRowDTO; +import com.sap.sailing.gwt.ui.shared.LegEntryDTO; +import com.sap.sailing.gwt.ui.shared.LegInfoDTO; +import com.sap.sailing.gwt.ui.shared.ManeuverDTO; +import com.sap.sailing.gwt.ui.shared.MarkDTO; +import com.sap.sailing.gwt.ui.shared.MarkPassingTimesDTO; +import com.sap.sailing.gwt.ui.shared.MarkpassingManeuverDTO; +import com.sap.sailing.gwt.ui.shared.PlacemarkDTO; +import com.sap.sailing.gwt.ui.shared.PlacemarkOrderDTO; +import com.sap.sailing.gwt.ui.shared.PositionDTO; +import com.sap.sailing.gwt.ui.shared.QuickRankDTO; +import com.sap.sailing.gwt.ui.shared.RaceColumnDTO; +import com.sap.sailing.gwt.ui.shared.RaceColumnInSeriesDTO; +import com.sap.sailing.gwt.ui.shared.RaceCourseMarksDTO; +import com.sap.sailing.gwt.ui.shared.RaceDTO; +import com.sap.sailing.gwt.ui.shared.RaceMapDataDTO; +import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; +import com.sap.sailing.gwt.ui.shared.RaceWithCompetitorsDTO; +import com.sap.sailing.gwt.ui.shared.RegattaDTO; +import com.sap.sailing.gwt.ui.shared.RegattaScoreCorrectionDTO; +import com.sap.sailing.gwt.ui.shared.RegattaScoreCorrectionDTO.ScoreCorrectionEntryDTO; +import com.sap.sailing.gwt.ui.shared.ReplicaDTO; +import com.sap.sailing.gwt.ui.shared.ReplicationMasterDTO; +import com.sap.sailing.gwt.ui.shared.ReplicationStateDTO; +import com.sap.sailing.gwt.ui.shared.ScoreCorrectionProviderDTO; +import com.sap.sailing.gwt.ui.shared.SeriesDTO; +import com.sap.sailing.gwt.ui.shared.SpeedWithBearingDTO; +import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; +import com.sap.sailing.gwt.ui.shared.SwissTimingArchiveConfigurationDTO; +import com.sap.sailing.gwt.ui.shared.SwissTimingConfigurationDTO; +import com.sap.sailing.gwt.ui.shared.SwissTimingRaceRecordDTO; +import com.sap.sailing.gwt.ui.shared.SwissTimingReplayRaceDTO; +import com.sap.sailing.gwt.ui.shared.TracTracConfigurationDTO; +import com.sap.sailing.gwt.ui.shared.TracTracRaceRecordDTO; +import com.sap.sailing.gwt.ui.shared.TrackedRaceDTO; +import com.sap.sailing.gwt.ui.shared.VenueDTO; +import com.sap.sailing.gwt.ui.shared.WindDTO; +import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; +import com.sap.sailing.gwt.ui.shared.WindTrackInfoDTO; +import com.sap.sailing.server.RacingEventService; +import com.sap.sailing.server.RacingEventServiceOperation; +import com.sap.sailing.server.operationaltransformation.AddColumnToLeaderboard; +import com.sap.sailing.server.operationaltransformation.AddColumnToSeries; +import com.sap.sailing.server.operationaltransformation.AddSpecificRegatta; +import com.sap.sailing.server.operationaltransformation.ConnectTrackedRaceToLeaderboardColumn; +import com.sap.sailing.server.operationaltransformation.CreateEvent; +import com.sap.sailing.server.operationaltransformation.CreateFlexibleLeaderboard; +import com.sap.sailing.server.operationaltransformation.CreateLeaderboardGroup; +import com.sap.sailing.server.operationaltransformation.CreateRegattaLeaderboard; +import com.sap.sailing.server.operationaltransformation.DisconnectLeaderboardColumnFromTrackedRace; +import com.sap.sailing.server.operationaltransformation.MoveColumnInSeriesDown; +import com.sap.sailing.server.operationaltransformation.MoveColumnInSeriesUp; +import com.sap.sailing.server.operationaltransformation.MoveLeaderboardColumnDown; +import com.sap.sailing.server.operationaltransformation.MoveLeaderboardColumnUp; +import com.sap.sailing.server.operationaltransformation.RemoveAndUntrackRace; +import com.sap.sailing.server.operationaltransformation.RemoveColumnFromSeries; +import com.sap.sailing.server.operationaltransformation.RemoveEvent; +import com.sap.sailing.server.operationaltransformation.RemoveLeaderboard; +import com.sap.sailing.server.operationaltransformation.RemoveLeaderboardColumn; +import com.sap.sailing.server.operationaltransformation.RemoveLeaderboardGroup; +import com.sap.sailing.server.operationaltransformation.RemoveRegatta; +import com.sap.sailing.server.operationaltransformation.RenameEvent; +import com.sap.sailing.server.operationaltransformation.RenameLeaderboard; +import com.sap.sailing.server.operationaltransformation.RenameLeaderboardColumn; +import com.sap.sailing.server.operationaltransformation.RenameLeaderboardGroup; +import com.sap.sailing.server.operationaltransformation.SetRaceIsKnownToStartUpwind; +import com.sap.sailing.server.operationaltransformation.SetSuppressedFlagForCompetitorInLeaderboard; +import com.sap.sailing.server.operationaltransformation.SetWindSourcesToExclude; +import com.sap.sailing.server.operationaltransformation.StopTrackingRace; +import com.sap.sailing.server.operationaltransformation.StopTrackingRegatta; +import com.sap.sailing.server.operationaltransformation.UpdateCompetitorDisplayNameInLeaderboard; +import com.sap.sailing.server.operationaltransformation.UpdateEvent; +import com.sap.sailing.server.operationaltransformation.UpdateIsMedalRace; +import com.sap.sailing.server.operationaltransformation.UpdateLeaderboard; +import com.sap.sailing.server.operationaltransformation.UpdateLeaderboardCarryValue; +import com.sap.sailing.server.operationaltransformation.UpdateLeaderboardColumnFactor; +import com.sap.sailing.server.operationaltransformation.UpdateLeaderboardGroup; +import com.sap.sailing.server.operationaltransformation.UpdateLeaderboardMaxPointsReason; +import com.sap.sailing.server.operationaltransformation.UpdateLeaderboardScoreCorrection; +import com.sap.sailing.server.operationaltransformation.UpdateLeaderboardScoreCorrectionMetadata; +import com.sap.sailing.server.operationaltransformation.UpdateRaceDelayToLive; +import com.sap.sailing.server.replication.ReplicaDescriptor; +import com.sap.sailing.server.replication.ReplicationFactory; +import com.sap.sailing.server.replication.ReplicationMasterDescriptor; +import com.sap.sailing.server.replication.ReplicationService; + +/** + * The server side implementation of the RPC service. + */ +public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements SailingService, RaceFetcher, RegattaFetcher { + private static final Logger logger = Logger.getLogger(SailingServiceImpl.class.getName()); + + private static final long serialVersionUID = 9031688830194537489L; + + private final ServiceTracker racingEventServiceTracker; + + private final ServiceTracker replicationServiceTracker; + + private final ServiceTracker scoreCorrectionProviderServiceTracker; + + private final MongoObjectFactory mongoObjectFactory; + + private final SwissTimingAdapterPersistence swissTimingAdapterPersistence; + + private final com.sap.sailing.domain.tractracadapter.persistence.MongoObjectFactory tractracMongoObjectFactory; + + private final DomainObjectFactory domainObjectFactory; + + private final SwissTimingFactory swissTimingFactory; + + private final com.sap.sailing.domain.tractracadapter.persistence.DomainObjectFactory tractracDomainObjectFactory; + + private final com.sap.sailing.domain.common.CountryCodeFactory countryCodeFactory; + + private final Executor executor; + + /** + * {@link #getLeaderboardByName(String, Date, Collection)} is the application's bottleneck. When two clients ask the + * same data for the same leaderboard with their waitForLatestAnalyses parameters set to + * false, expansion state and (quantized) time stamp, no two computations should be spawned for the two + * clients. Instead, if the computation is still running, all clients asking the same wait for the single result. + * Results are cached in this LRU-based evicting cache. + */ + private final Map leaderboardByNameLiveUpdaters; + + /** + * This executor needs to be a different one than {@link #executor} because the tasks run by {@link #executor} + * can depend on the results of the tasks run by {@link #raceDetailsExecutor}, and an {@link Executor} doesn't + * move a task that is blocked by waiting for another {@link FutureTask} to the side but blocks permanently, + * ending in a deadlock (one that cannot easily be detected by the Eclipse debugger either). + */ + private final Executor raceDetailsExecutor; + + private final WeakHashMap weakCompetitorDTOCache; + + private final Map, RunnableFuture> raceDetailsAtEndOfTrackingCache; + + /** + * Used to remove all these listeners from their tracked races when this servlet is {@link #destroy() destroyed}. + */ + private final Set cacheInvalidationListeners; + + private final LeaderboardDTOCache leaderboardDTOCacheWaitingForLatestAnalysis; + + private final DomainFactory tractracDomainFactory; + + private final com.sap.sailing.domain.base.DomainFactory baseDomainFactory; + + public SailingServiceImpl() { + BundleContext context = Activator.getDefault(); + tractracDomainFactory = DomainFactory.INSTANCE; + baseDomainFactory = tractracDomainFactory.getBaseDomainFactory(); + raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); + cacheInvalidationListeners = new HashSet(); + weakCompetitorDTOCache = new WeakHashMap(); + racingEventServiceTracker = createAndOpenRacingEventServiceTracker(context); + replicationServiceTracker = createAndOpenReplicationServiceTracker(context); + scoreCorrectionProviderServiceTracker = createAndOpenScoreCorrectionProviderServiceTracker(context); + mongoObjectFactory = MongoFactory.INSTANCE.getDefaultMongoObjectFactory(); + domainObjectFactory = MongoFactory.INSTANCE.getDefaultDomainObjectFactory(); + swissTimingAdapterPersistence = SwissTimingAdapterPersistence.INSTANCE; + tractracDomainObjectFactory = com.sap.sailing.domain.tractracadapter.persistence.DomainObjectFactory.INSTANCE; + tractracMongoObjectFactory = com.sap.sailing.domain.tractracadapter.persistence.MongoObjectFactory.INSTANCE; + swissTimingFactory = SwissTimingFactory.INSTANCE; + countryCodeFactory = com.sap.sailing.domain.common.CountryCodeFactory.INSTANCE; + executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + raceDetailsExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + leaderboardByNameLiveUpdaters = new HashMap(); + leaderboardDTOCacheWaitingForLatestAnalysis = new LeaderboardDTOCache(this, /* waitForLatestAnalyses */ true); + } + + private void writeObject(ObjectOutputStream oos) throws IOException { + oos.defaultWriteObject(); + } + + public void destroy() { + super.destroy(); + for (CacheInvalidationListener cacheInvalidationListener : cacheInvalidationListeners) { + cacheInvalidationListener.removeFromTrackedRace(); + } + } + + protected ServiceTracker createAndOpenRacingEventServiceTracker( + BundleContext context) { + ServiceTracker result = new ServiceTracker( + context, RacingEventService.class.getName(), null); + result.open(); + return result; + } + + /** + * Asks the OSGi system for registered score correction provider services + */ + private ServiceTracker createAndOpenScoreCorrectionProviderServiceTracker( + BundleContext bundleContext) { + ServiceTracker tracker = new ServiceTracker(bundleContext, + ScoreCorrectionProvider.class.getName(), + /* customizer */null); + tracker.open(); + return tracker; + } + + @Override + public Iterable getScoreCorrectionProviderDTOs() throws Exception { + List result = new ArrayList(); + final Iterable services = getScoreCorrectionProviders(); + for (ScoreCorrectionProvider scoreCorrectionProvider : services) { + result.add(createScoreCorrectionProviderDTO((ScoreCorrectionProvider) scoreCorrectionProvider)); + } + return result; + } + + private Iterable getScoreCorrectionProviders() { + final Object[] services = scoreCorrectionProviderServiceTracker.getServices(); + List result = new ArrayList(); + if (services != null) { + for (Object service : services) { + result.add((ScoreCorrectionProvider) service); + } + } + return result; + } + + private ScoreCorrectionProviderDTO createScoreCorrectionProviderDTO(ScoreCorrectionProvider scoreCorrectionProvider) + throws Exception { + Map>> hasResultsForBoatClassFromDateByEventName = new HashMap>>(); + for (Map.Entry>> e : scoreCorrectionProvider + .getHasResultsForBoatClassFromDateByEventName().entrySet()) { + Set> set = new HashSet>(); + for (Pair p : e.getValue()) { + set.add(new Pair(p.getA(), p.getB().asDate())); + } + hasResultsForBoatClassFromDateByEventName.put(e.getKey(), set); + } + return new ScoreCorrectionProviderDTO(scoreCorrectionProvider.getName(), hasResultsForBoatClassFromDateByEventName); + } + + protected ServiceTracker createAndOpenReplicationServiceTracker( + BundleContext context) { + ServiceTracker result = new ServiceTracker( + context, ReplicationService.class.getName(), null); + result.open(); + return result; + } + + /** + * If date is null, the {@link LiveLeaderboardUpdater} for the + * leaderboardName requested is obtained or created if it doesn't exist yet. The request is then passed + * on to the live leaderboard updater which will respond with its live {@link LeaderboardDTO} if it has at least the + * columns requested as per namesOfRaceColumnsForWhichToLoadLegDetails. Otherwise, the updater will add + * the missing columns to its profile and start a synchronous computation for the requesting client, the result of + * which will be used as live leaderboard cache update. + *

+ * + * Otherwise, the leaderboard is {@link #computeLeaderboardByName(String, Date, Collection, boolean) computed} + * synchronously on the fly. + */ + @Override + public LeaderboardDTO getLeaderboardByName(final String leaderboardName, final Date date, + final Collection namesOfRaceColumnsForWhichToLoadLegDetails) + throws NoWindException, InterruptedException, ExecutionException { + long startOfRequestHandling = System.currentTimeMillis(); + LeaderboardDTO result = null; + TimePoint timePoint; + LiveLeaderboardUpdater liveLeaderboardUpdater; + final Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName); + if (leaderboard != null) { + if (date == null) { + // date==null means live mode; however, if we're after the end of all races and after all score + // corrections, don't use the live leaderboard updater which would keep re-calculating over and over again, but map + // this to a usual non-live call which uses the regular LeaderboardDTOCache which is invalidated properly + // when the tracked race associations or score corrections or tracked race contents changes: + // TODO see bug 929: use lock instead of synchronized to increase concurrency + synchronized (leaderboardByNameLiveUpdaters) { + liveLeaderboardUpdater = leaderboardByNameLiveUpdaters.get(leaderboardName); + if (liveLeaderboardUpdater == null) { + liveLeaderboardUpdater = new LiveLeaderboardUpdater(leaderboard, this); + leaderboardByNameLiveUpdaters.put(leaderboardName, liveLeaderboardUpdater); + } + } + final TimePoint nowMinusDelay = leaderboard.getNowMinusDelay(); + final TimePoint timePointOfLatestModification = leaderboard.getTimePointOfLatestModification(); + if (timePointOfLatestModification != null && !nowMinusDelay.before(timePointOfLatestModification)) { + timePoint = timePointOfLatestModification; + } else { + timePoint = null; + result = liveLeaderboardUpdater.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, + nowMinusDelay); + } + } else { + timePoint = new MillisecondsTimePoint(date); + } + if (timePoint != null) { + // in replay we'd like up-to-date results; they are still cached + // which is OK because the cache is invalidated whenever any of the tracked races attached to the + // leaderboard changes. + result = leaderboardDTOCacheWaitingForLatestAnalysis.getLeaderboardByName(leaderboard, timePoint, + namesOfRaceColumnsForWhichToLoadLegDetails); + } + logger.fine("getLeaderboardByName(" + leaderboardName + ", " + date + ", " + + namesOfRaceColumnsForWhichToLoadLegDetails + ") took " + + (System.currentTimeMillis() - startOfRequestHandling) + "ms"); + } + return result; + } + + protected LeaderboardDTO computeLeaderboardByName(final Leaderboard leaderboard, final TimePoint timePoint, + final Collection namesOfRaceColumnsForWhichToLoadLegDetails, final boolean waitForLatestAnalyses) + throws NoWindException { + long startOfRequestHandling = System.currentTimeMillis(); + LeaderboardDTO result = null; + if (leaderboard != null) { + result = new LeaderboardDTO(leaderboard.getScoreCorrection().getTimePointOfLastCorrectionsValidity()==null ? + null : leaderboard.getScoreCorrection().getTimePointOfLastCorrectionsValidity().asDate(), + leaderboard.getScoreCorrection()==null?null:leaderboard.getScoreCorrection().getComment(), + leaderboard.getScoringScheme().isHigherBetter()); + result.competitors = new ArrayList(); + result.name = leaderboard.getName(); + result.competitorDisplayNames = new HashMap(); + for (Competitor suppressedCompetitor : leaderboard.getSuppressedCompetitors()) { + result.setSuppressed(convertToCompetitorDTO(suppressedCompetitor), true); + } + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + RaceColumnDTO raceColumnDTO = result.createEmptyRaceColumn(raceColumn.getName(), raceColumn.isMedalRace(), + leaderboard.getScoringScheme().isValidInTotalScore(leaderboard, raceColumn, timePoint)); + for (Fleet fleet : raceColumn.getFleets()) { + TimePoint latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive = null; + RegattaAndRaceIdentifier raceIdentifier = null; + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + final FleetDTO fleetDTO = convertToFleetDTO(fleet); + if (trackedRace != null) { + raceIdentifier = new RegattaNameAndRaceName(trackedRace.getTrackedRegatta().getRegatta() + .getName(), trackedRace.getRace().getName()); + if (trackedRace.hasStarted(timePoint) && trackedRace.hasGPSData() && trackedRace.hasWindData()) { + TimePoint liveTimePointForTrackedRace = timePoint; + final TimePoint endOfRace = trackedRace.getEndOfRace(); + if (endOfRace != null) { + liveTimePointForTrackedRace = endOfRace; + } + latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive = liveTimePointForTrackedRace; + } + } + // Note: the RaceColumnDTO won't be created by the following addRace call because it has been created + // above by the result.createEmptyRaceColumn call + result.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), fleetDTO, + raceColumn.isMedalRace(), raceIdentifier, /* StrippedRaceDTO */ null); + if (latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive != null) { + raceColumnDTO.setWhenLastTrackedRaceWasLive(fleetDTO, latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive.asDate()); + } + } + result.setCompetitorsFromBestToWorst(raceColumnDTO, getCompetitorDTOList(leaderboard.getCompetitorsFromBestToWorst(raceColumn, timePoint))); + } + result.setDelayToLiveInMillisForLatestRace(leaderboard.getDelayToLiveInMillis()); + result.rows = new HashMap(); + result.hasCarriedPoints = leaderboard.hasCarriedPoints(); + result.discardThresholds = leaderboard.getResultDiscardingRule().getDiscardIndexResultsStartingWithHowManyRaces(); + // Computing the competitor leg ranks is expensive, especially in live mode, in case new events keep invalidating + // the ranks cache in TrackedLegImpl. Then problem then is that the sorting based on wind data is repeated for each + // competitor, leading to square effort. We therefore need to compute the leg ranks for those race where leg details + // are requested only once and pass them into getLeaderboardEntryDTO + final Map> legRanksCache = new HashMap>(); + for (final RaceColumn raceColumn : leaderboard.getRaceColumns()) { + // if details for the column are requested, cache the leg's ranks + if (namesOfRaceColumnsForWhichToLoadLegDetails != null + && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn.getName())) { + for (Fleet fleet : raceColumn.getFleets()) { + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + trackedRace.getRace().getCourse().lockForRead(); + try { + for (TrackedLeg trackedLeg : trackedRace.getTrackedLegs()) { + legRanksCache.put(trackedLeg.getLeg(), trackedLeg.getRanks(timePoint)); + } + } finally { + trackedRace.getRace().getCourse().unlockAfterRead(); + } + } + } + } + } + for (final Competitor competitor : leaderboard.getCompetitorsFromBestToWorst(timePoint)) { + CompetitorDTO competitorDTO = convertToCompetitorDTO(competitor); + LeaderboardRowDTO row = new LeaderboardRowDTO(); + row.competitor = competitorDTO; + row.fieldsByRaceColumnName = new HashMap(); + row.carriedPoints = leaderboard.hasCarriedPoints(competitor) ? leaderboard.getCarriedPoints(competitor) : null; + addOverallDetailsToRow(leaderboard, timePoint, competitor, row); + result.competitors.add(competitorDTO); + Map> futuresForColumnName = new HashMap>(); + for (final RaceColumn raceColumn : leaderboard.getRaceColumns()) { + RunnableFuture future = new FutureTask(new Callable() { + @Override + public LeaderboardEntryDTO call() { + try { + Entry entry = leaderboard.getEntry(competitor, raceColumn, timePoint); + return getLeaderboardEntryDTO(entry, raceColumn.getTrackedRace(competitor), competitor, timePoint, + namesOfRaceColumnsForWhichToLoadLegDetails != null + && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn.getName()), + waitForLatestAnalyses, legRanksCache); + } catch (NoWindException e) { + throw new NoWindError(e); + } + } + }); + executor.execute(future); + futuresForColumnName.put(raceColumn.getName(), future); + } + for (Map.Entry> raceColumnNameAndFuture : futuresForColumnName.entrySet()) { + try { + row.fieldsByRaceColumnName.put(raceColumnNameAndFuture.getKey(), raceColumnNameAndFuture.getValue().get()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + result.rows.put(competitorDTO, row); + String displayName = leaderboard.getDisplayName(competitor); + if (displayName != null) { + result.competitorDisplayNames.put(competitorDTO, displayName); + } + } + } + logger.fine("computeLeaderboardByName("+leaderboard.getName()+", "+timePoint+", "+namesOfRaceColumnsForWhichToLoadLegDetails+") took "+ + (System.currentTimeMillis()-startOfRequestHandling)+"ms"); + return result; + } + + private void addOverallDetailsToRow(final Leaderboard leaderboard, final TimePoint timePoint, + final Competitor competitor, LeaderboardRowDTO row) throws NoWindException { + final Pair maximumSpeedOverGround = leaderboard.getMaximumSpeedOverGround(competitor, timePoint); + if (maximumSpeedOverGround != null && maximumSpeedOverGround.getB() != null) { + row.maximumSpeedOverGroundInKnots = maximumSpeedOverGround.getB().getKnots(); + row.whenMaximumSpeedOverGroundWasAchieved = maximumSpeedOverGround.getA().getTimePoint().asDate(); + } + final Long totalTimeSailedDownwindInMilliseconds = leaderboard.getTotalTimeSailedInLegTypeInMilliseconds(competitor, LegType.DOWNWIND, timePoint); + row.totalTimeSailedDownwindInSeconds = totalTimeSailedDownwindInMilliseconds==null?null:1./1000.*totalTimeSailedDownwindInMilliseconds; + final Long totalTimeSailedUpwindInMilliseconds = leaderboard.getTotalTimeSailedInLegTypeInMilliseconds(competitor, LegType.UPWIND, timePoint); + row.totalTimeSailedUpwindInSeconds = totalTimeSailedUpwindInMilliseconds==null?null:1./1000.*totalTimeSailedUpwindInMilliseconds; + final Long totalTimeSailedReachingInMilliseconds = leaderboard.getTotalTimeSailedInLegTypeInMilliseconds(competitor, LegType.REACHING, timePoint); + row.totalTimeSailedReachingInSeconds = totalTimeSailedReachingInMilliseconds==null?null:1./1000.*totalTimeSailedReachingInMilliseconds; + final Long totalTimeSailedInMilliseconds = leaderboard.getTotalTimeSailedInMilliseconds(competitor, timePoint); + row.totalTimeSailedInSeconds = totalTimeSailedInMilliseconds==null?null:1./1000.*totalTimeSailedInMilliseconds; + } + + private List getCompetitorDTOList(List competitors) { + List result = new ArrayList(); + for (Competitor competitor : competitors) { + result.add(convertToCompetitorDTO(competitor)); + } + return result; + } + + /** + * @param waitForLatestAnalyses + * if false, this method is allowed to read the maneuver analysis results from a cache that + * may not reflect all data already received; otherwise, the method will always block for the latest + * cache updates to have happened before returning. + */ + private LeaderboardEntryDTO getLeaderboardEntryDTO(Entry entry, TrackedRace trackedRace, Competitor competitor, + TimePoint timePoint, boolean addLegDetails, boolean waitForLatestAnalyses, + Map> legRanksCache) throws NoWindException { + LeaderboardEntryDTO entryDTO = new LeaderboardEntryDTO(); + entryDTO.race = trackedRace == null ? null : trackedRace.getRaceIdentifier(); + entryDTO.netPoints = entry.getNetPoints(); + entryDTO.netPointsCorrected = entry.isNetPointsCorrected(); + entryDTO.totalPoints = entry.getTotalPoints(); + entryDTO.reasonForMaxPoints = entry.getMaxPointsReason(); + entryDTO.discarded = entry.isDiscarded(); + if (addLegDetails && trackedRace != null) { + try { + RaceDetails raceDetails = getRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, legRanksCache); + entryDTO.legDetails = raceDetails.getLegDetails(); + entryDTO.windwardDistanceToOverallLeaderInMeters = raceDetails.getWindwardDistanceToOverallLeader() == null ? null + : raceDetails.getWindwardDistanceToOverallLeader().getMeters(); + entryDTO.averageCrossTrackErrorInMeters = raceDetails.getAverageCrossTrackError() == null ? null + : raceDetails.getAverageCrossTrackError().getMeters(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e); // the future used to calculate the leg details was interrupted; escalate as runtime exception + } + } + final Fleet fleet = entry.getFleet(); + entryDTO.fleet = fleet == null ? null : convertToFleetDTO(fleet); + return entryDTO; + } + + private static class RaceDetails { + private final List legDetails; + private final Distance windwardDistanceToOverallLeader; + private final Distance averageCrossTrackError; + public RaceDetails(List legDetails, Distance windwardDistanceToOverallLeader, + Distance averageCrossTrackError) { + super(); + this.legDetails = legDetails; + this.windwardDistanceToOverallLeader = windwardDistanceToOverallLeader; + this.averageCrossTrackError = averageCrossTrackError; + } + public List getLegDetails() { + return legDetails; + } + public Distance getWindwardDistanceToOverallLeader() { + return windwardDistanceToOverallLeader; + } + public Distance getAverageCrossTrackError() { + return averageCrossTrackError; + } + } + + /** + * If timePoint is after the end of the race's tracking the query will be adjusted to obtain the values + * at the end of the {@link TrackedRace#getEndOfTracking() race's tracking time}. If the time point adjusted this + * way equals the end of the tracking time, the query results will be looked up in a cache first and if not found, + * they will be stored to the cache after calculating them. A cache invalidation {@link RaceChangeListener listener} + * will be registered with the race which will be triggered for any event received by the race. + * @param waitForLatestAnalyses + * if false, this method is allowed to read the maneuver analysis results from a cache that + * may not reflect all data already received; otherwise, the method will always block for the latest + * cache updates to have happened before returning. + */ + private RaceDetails getRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, + boolean waitForLatestAnalyses, Map> legRanksCache) throws NoWindException, InterruptedException, ExecutionException { + RaceDetails raceDetails; + if (trackedRace.getEndOfTracking() != null && trackedRace.getEndOfTracking().compareTo(timePoint) < 0) { + raceDetails = getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(trackedRace, competitor, legRanksCache); + } else { + raceDetails = calculateRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, legRanksCache); + } + return raceDetails; + } + + private RaceDetails getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(final TrackedRace trackedRace, + final Competitor competitor, final Map> legRanksCache) + throws NoWindException, InterruptedException, ExecutionException { + final Pair key = new Pair(trackedRace, competitor); + RunnableFuture raceDetails; + synchronized (raceDetailsAtEndOfTrackingCache) { + raceDetails = raceDetailsAtEndOfTrackingCache.get(key); + if (raceDetails == null) { + raceDetails = new FutureTask(new Callable() { + @Override + public RaceDetails call() throws Exception { + return calculateRaceDetails(trackedRace, competitor, trackedRace.getEndOfTracking(), + /* waitForLatestManeuverAnalysis */ true /* because this is done only once after end of tracking */, legRanksCache); + } + }); + raceDetailsExecutor.execute(raceDetails); + raceDetailsAtEndOfTrackingCache.put(key, raceDetails); + final CacheInvalidationListener cacheInvalidationListener = new CacheInvalidationListener(trackedRace, + competitor); + trackedRace.addListener(cacheInvalidationListener); + cacheInvalidationListeners.add(cacheInvalidationListener); + } + } + return raceDetails.get(); + } + + /** + * Handles the invalidation of the {@link SailingServiceImpl#raceDetailsAtEndOfTrackingCache} entries if the tracked race + * changes in any way. + * + * @author Axel Uhl (D043530) + * + */ + private class CacheInvalidationListener implements RaceChangeListener { + private final TrackedRace trackedRace; + private final Competitor competitor; + + public CacheInvalidationListener(TrackedRace trackedRace, Competitor competitor) { + this.trackedRace = trackedRace; + this.competitor = competitor; + } + + public void removeFromTrackedRace() { + trackedRace.removeListener(this); + } + + private void invalidateCacheAndRemoveThisListenerFromTrackedRace() { + synchronized (raceDetailsAtEndOfTrackingCache) { + raceDetailsAtEndOfTrackingCache.remove(new Pair(trackedRace, competitor)); + removeFromTrackedRace(); + } + } + + @Override + public void competitorPositionChanged(GPSFixMoving fix, Competitor competitor) { + invalidateCacheAndRemoveThisListenerFromTrackedRace(); + } + + @Override + public void markPositionChanged(GPSFix fix, Mark mark) { + invalidateCacheAndRemoveThisListenerFromTrackedRace(); + } + + @Override + public void markPassingReceived(Competitor competitor, Map oldMarkPassings, + Iterable markPassings) { + invalidateCacheAndRemoveThisListenerFromTrackedRace(); + } + + @Override + public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) { + invalidateCacheAndRemoveThisListenerFromTrackedRace(); + } + + @Override + public void windDataReceived(Wind wind, WindSource windSource) { + invalidateCacheAndRemoveThisListenerFromTrackedRace(); + } + + @Override + public void windDataRemoved(Wind wind, WindSource windSource) { + invalidateCacheAndRemoveThisListenerFromTrackedRace(); + } + + @Override + public void windAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) { + invalidateCacheAndRemoveThisListenerFromTrackedRace(); + } + + @Override + public void raceTimesChanged(TimePoint startOfTracking, TimePoint endOfTracking, TimePoint startTimeReceived) { + invalidateCacheAndRemoveThisListenerFromTrackedRace(); + } + + @Override + public void delayToLiveChanged(long delayToLiveInMillis) { + invalidateCacheAndRemoveThisListenerFromTrackedRace(); + } + + @Override + public void windSourcesToExcludeChanged(Iterable windSourcesToExclude) { + invalidateCacheAndRemoveThisListenerFromTrackedRace(); + } + } + + private RaceDetails calculateRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, + boolean waitForLatestAnalyses, Map> legRanksCache) throws NoWindException { + List legDetails; + legDetails = new ArrayList(); + final Course course = trackedRace.getRace().getCourse(); + course.lockForRead(); // hold back any course re-configurations while looping over the legs + try { + for (Leg leg : course.getLegs()) { + LegEntryDTO legEntry; + // We loop over a copy of the course's legs; during a course change, legs may become "stale," even with + // regard to the leg/trackedLeg structures inside the tracked race which is updated by the course change + // immediately. Make sure we're tolerant against disappearing legs! See bug 794. + TrackedLegOfCompetitor trackedLeg = trackedRace.getTrackedLeg(competitor, leg); + if (trackedLeg != null && trackedLeg.hasStartedLeg(timePoint)) { + legEntry = createLegEntry(trackedLeg, timePoint, waitForLatestAnalyses, legRanksCache); + } else { + legEntry = null; + } + legDetails.add(legEntry); + } + final Distance windwardDistanceToOverallLeader = trackedRace == null ? null : trackedRace + .getWindwardDistanceToOverallLeader(competitor, timePoint); + final Distance averageCrossTrackError = trackedRace == null ? null : trackedRace.getAverageCrossTrackError( + competitor, timePoint, waitForLatestAnalyses); + return new RaceDetails(legDetails, windwardDistanceToOverallLeader, averageCrossTrackError); + } finally { + course.unlockAfterRead(); + } + } + + private LegEntryDTO createLegEntry(TrackedLegOfCompetitor trackedLeg, TimePoint timePoint, + boolean waitForLatestAnalyses, Map> legRanksCache) throws NoWindException { + LegEntryDTO result; + if (trackedLeg == null) { + result = null; + } else { + result = new LegEntryDTO(); + final Speed averageSpeedOverGround = trackedLeg.getAverageSpeedOverGround(timePoint); + result.averageSpeedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); + final Distance averageCrossTrackError = trackedLeg.getAverageCrossTrackError(timePoint, waitForLatestAnalyses); + result.averageCrossTrackErrorInMeters = averageCrossTrackError == null ? null : averageCrossTrackError.getMeters(); + Double speedOverGroundInKnots; + if (trackedLeg.hasFinishedLeg(timePoint)) { + speedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); + } else { + final SpeedWithBearing speedOverGround = trackedLeg.getSpeedOverGround(timePoint); + speedOverGroundInKnots = speedOverGround == null ? null : speedOverGround.getKnots(); + } + result.currentSpeedOverGroundInKnots = speedOverGroundInKnots == null ? null : speedOverGroundInKnots; + Distance distanceTraveled = trackedLeg.getDistanceTraveled(timePoint); + result.distanceTraveledInMeters = distanceTraveled == null ? null : distanceTraveled.getMeters(); + result.estimatedTimeToNextWaypointInSeconds = trackedLeg.getEstimatedTimeToNextMarkInSeconds(timePoint); + result.timeInMilliseconds = trackedLeg.getTimeInMilliSeconds(timePoint); + result.finished = trackedLeg.hasFinishedLeg(timePoint); + result.gapToLeaderInSeconds = trackedLeg.getGapToLeaderInSeconds(timePoint, + legRanksCache.get(trackedLeg.getLeg()).entrySet().iterator().next().getKey()); + if (result.gapToLeaderInSeconds != null) { + // FIXME problem: asking just after the beginning of the leg yields very different values from asking for the end of the previous leg. + // This is because for the previous leg it's decided based on the mark passings; for the next (current) leg it's decided based on + // windward distance and VMG + Double gapAtEndOfPreviousLegInSeconds = getGapAtEndOfPreviousLegInSeconds(trackedLeg); + if (gapAtEndOfPreviousLegInSeconds != null) { + result.gapChangeSinceLegStartInSeconds = result.gapToLeaderInSeconds - gapAtEndOfPreviousLegInSeconds; + } + } + LinkedHashMap legRanks = legRanksCache.get(trackedLeg.getLeg()); + if (legRanks != null) { + result.rank = legRanks.get(trackedLeg.getCompetitor()); + } else { + result.rank = trackedLeg.getRank(timePoint); + } + result.started = trackedLeg.hasStartedLeg(timePoint); + Speed velocityMadeGood; + if (trackedLeg.hasFinishedLeg(timePoint)) { + velocityMadeGood = trackedLeg.getAverageVelocityMadeGood(timePoint); + } else { + velocityMadeGood = trackedLeg.getVelocityMadeGood(timePoint); + } + result.velocityMadeGoodInKnots = velocityMadeGood == null ? null : velocityMadeGood.getKnots(); + Distance windwardDistanceToGo = trackedLeg.getWindwardDistanceToGo(timePoint); + result.windwardDistanceToGoInMeters = windwardDistanceToGo == null ? null : windwardDistanceToGo + .getMeters(); + final TimePoint startOfRace = trackedLeg.getTrackedLeg().getTrackedRace().getStartOfRace(); + if (startOfRace != null && trackedLeg.hasStartedLeg(timePoint)) { + // not using trackedLeg.getManeuvers(...) because it may not catch the mark passing maneuver starting this leg + // because that may have been detected as slightly before the mark passing time, hence associated with the previous leg + List maneuvers = trackedLeg.getTrackedLeg().getTrackedRace() + .getManeuvers(trackedLeg.getCompetitor(), startOfRace, timePoint, waitForLatestAnalyses); + if (maneuvers != null) { + result.numberOfManeuvers = new HashMap(); + result.numberOfManeuvers.put(ManeuverType.TACK, 0); + result.numberOfManeuvers.put(ManeuverType.JIBE, 0); + result.numberOfManeuvers.put(ManeuverType.PENALTY_CIRCLE, 0); + Map totalManeuverLossInMeters = new HashMap(); + totalManeuverLossInMeters.put(ManeuverType.TACK, 0.0); + totalManeuverLossInMeters.put(ManeuverType.JIBE, 0.0); + totalManeuverLossInMeters.put(ManeuverType.PENALTY_CIRCLE, 0.0); + TimePoint startOfLeg = trackedLeg.getStartTime(); + for (Maneuver maneuver : maneuvers) { + // don't count maneuvers that were in previous legs + switch (maneuver.getType()) { + case TACK: + case JIBE: + case PENALTY_CIRCLE: + if (!maneuver.getTimePoint().before(startOfLeg) && (!trackedLeg.hasFinishedLeg(timePoint) || + maneuver.getTimePoint().before(trackedLeg.getFinishTime()))) { + if (maneuver.getManeuverLoss() != null) { + result.numberOfManeuvers.put(maneuver.getType(), + result.numberOfManeuvers.get(maneuver.getType()) + 1); + totalManeuverLossInMeters.put(maneuver.getType(), + totalManeuverLossInMeters.get(maneuver.getType()) + + maneuver.getManeuverLoss().getMeters()); + } + } + break; + case MARK_PASSING: + // analyze all mark passings, not only those after this leg's start, to catch the mark passing + // maneuver starting this leg, even if its time point is slightly before the mark passing starting this leg + MarkPassingManeuver mpm = (MarkPassingManeuver) maneuver; + if (mpm.getWaypointPassed() == trackedLeg.getLeg().getFrom()) { + result.sideToWhichMarkAtLegStartWasRounded = mpm.getSide(); + } + break; + default: + throw new UnsupportedOperationException("Theres currently no support for the enum value '" + maneuver.getType() + "' in this method."); + } + } + result.averageManeuverLossInMeters = new HashMap(); + for (ManeuverType maneuverType : new ManeuverType[] { ManeuverType.TACK, ManeuverType.JIBE, + ManeuverType.PENALTY_CIRCLE }) { + if (result.numberOfManeuvers.get(maneuverType) != 0) { + result.averageManeuverLossInMeters.put( + maneuverType, + totalManeuverLossInMeters.get(maneuverType) + / result.numberOfManeuvers.get(maneuverType)); + } + } + } + } + } + return result; + } + + private Double getGapAtEndOfPreviousLegInSeconds(TrackedLegOfCompetitor trackedLeg) throws NoWindException { + final Double result; + final Course course = trackedLeg.getTrackedLeg().getTrackedRace().getRace().getCourse(); + course.lockForRead(); + try { + int indexOfStartWaypoint = course.getIndexOfWaypoint(trackedLeg.getLeg().getFrom()); + if (indexOfStartWaypoint == 0) { + // trackedLeg was the first leg; gap is determined by gap of start line passing time points + Iterable markPassingsForLegStart = trackedLeg.getTrackedLeg().getTrackedRace().getMarkPassingsInOrder(trackedLeg.getLeg().getFrom()); + trackedLeg.getTrackedLeg().getTrackedRace().lockForRead(markPassingsForLegStart); + try { + final Iterator markPassingsIter = markPassingsForLegStart.iterator(); + if (markPassingsIter.hasNext()) { + TimePoint firstStart = markPassingsIter.next().getTimePoint(); + final MarkPassing markPassingForFrom = trackedLeg.getTrackedLeg().getTrackedRace() + .getMarkPassing(trackedLeg.getCompetitor(), trackedLeg.getLeg().getFrom()); + if (markPassingForFrom != null) { + TimePoint competitorStart = markPassingForFrom.getTimePoint(); + result = (double) (competitorStart.asMillis() - firstStart.asMillis()) / 1000.; + } else { + result = null; + } + } else { + result = null; + } + } finally { + trackedLeg.getTrackedLeg().getTrackedRace().unlockAfterRead(markPassingsForLegStart); + } + } else { + TrackedLeg previousTrackedLeg = trackedLeg.getTrackedLeg().getTrackedRace().getTrackedLeg(course.getLegs().get(indexOfStartWaypoint-1)); + TrackedLegOfCompetitor previousTrackedLegOfCompetitor = previousTrackedLeg.getTrackedLeg(trackedLeg.getCompetitor()); + result = previousTrackedLegOfCompetitor.getGapToLeaderInSeconds(previousTrackedLegOfCompetitor.getFinishTime()); + } + return result; + } finally { + course.unlockAfterRead(); + } + } + + @Override + public List getRegattas() throws IllegalArgumentException { + List result = new ArrayList(); + for (Regatta regatta : getService().getAllRegattas()) { + result.add(convertToRegattaDTO(regatta)); + } + return result; + } + + private MarkDTO convertToMarkDTO(Mark mark, Position position) { + MarkDTO markDTO = new MarkDTO(mark.getName(), position.getLatDeg(), position.getLngDeg()); + markDTO.color = mark.getColor(); + markDTO.shape = mark.getShape(); + markDTO.pattern = mark.getPattern(); + markDTO.type = mark.getType(); + return markDTO; + } + + private RegattaDTO convertToRegattaDTO(Regatta regatta) { + RegattaDTO regattaDTO = new RegattaDTO(regatta.getName(), regatta.getScoringScheme().getType()); + regattaDTO.races = convertToRaceDTOs(regatta); + regattaDTO.series = convertToSeriesDTOs(regatta); + BoatClass boatClass = regatta.getBoatClass(); + if (boatClass != null) { + regattaDTO.boatClass = new BoatClassDTO(boatClass.getName(), boatClass.getHullLength().getMeters()); + } + return regattaDTO; + } + + private List convertToSeriesDTOs(Regatta regatta) { + List result = new ArrayList(); + for (Series series : regatta.getSeries()) { + SeriesDTO seriesDTO = convertToSeriesDTO(series); + result.add(seriesDTO); + } + return result; + } + + private SeriesDTO convertToSeriesDTO(Series series) { + List fleets = new ArrayList(); + for (Fleet fleet : series.getFleets()) { + fleets.add(convertToFleetDTO(fleet)); + } + List raceColumns = new ArrayList(); + for (RaceColumnInSeries raceColumn : series.getRaceColumns()) { + RaceColumnDTO raceColumnDTO = new RaceColumnDTO(/* isValidInTotalScore not relevant here because no scores conveyed */ null); + raceColumnDTO.name = raceColumn.getName(); + raceColumnDTO.setMedalRace(raceColumn.isMedalRace()); + raceColumnDTO.setExplicitFactor(raceColumn.getExplicitFactor()); + raceColumns.add(raceColumnDTO); + } + SeriesDTO result = new SeriesDTO(series.getName(), fleets, raceColumns, series.isMedal()); + return result; + } + + private FleetDTO convertToFleetDTO(Fleet fleet) { + return new FleetDTO(fleet.getName(), fleet.getOrdering(), fleet.getColor()); + } + + private List convertToRaceDTOs(Regatta regatta) { + List result = new ArrayList(); + for (RaceDefinition r : regatta.getAllRaces()) { + RegattaAndRaceIdentifier raceIdentifier = new RegattaNameAndRaceName(regatta.getName(), r.getName()); + TrackedRace trackedRace = getService().getExistingTrackedRace(raceIdentifier); + TrackedRaceDTO trackedRaceDTO = null; + if (trackedRace != null) { + trackedRaceDTO = new TrackedRaceDTO(); + trackedRaceDTO.hasWindData = trackedRace.hasWindData(); + trackedRaceDTO.hasGPSData = trackedRace.hasGPSData(); + trackedRaceDTO.startOfTracking = trackedRace.getStartOfTracking() == null ? null : trackedRace.getStartOfTracking().asDate(); + trackedRaceDTO.endOfTracking = trackedRace.getEndOfTracking() == null ? null : trackedRace.getEndOfTracking().asDate(); + trackedRaceDTO.timePointOfNewestEvent = trackedRace.getTimePointOfNewestEvent() == null ? null : trackedRace.getTimePointOfNewestEvent().asDate(); + trackedRaceDTO.delayToLiveInMs = trackedRace.getDelayToLiveInMillis(); + } + RaceWithCompetitorsDTO raceDTO = new RaceWithCompetitorsDTO(raceIdentifier, convertToCompetitorDTOs(r.getCompetitors()), + trackedRaceDTO, getService().isRaceBeingTracked(r)); + if (trackedRace != null) { + raceDTO.startOfRace = trackedRace.getStartOfRace() == null ? null : trackedRace.getStartOfRace().asDate(); + raceDTO.endOfRace = trackedRace.getEndOfRace() == null ? null : trackedRace.getEndOfRace().asDate(); + } + raceDTO.boatClass = regatta.getBoatClass() == null ? null : regatta.getBoatClass().getName(); + result.add(raceDTO); + } + return result; + } + + private List convertToCompetitorDTOs(Iterable competitors) { + List result = new ArrayList(); + for (Competitor c : competitors) { + CompetitorDTO competitorDTO = convertToCompetitorDTO(c); + result.add(competitorDTO); + } + return result; + } + + private CompetitorDTO convertToCompetitorDTO(Competitor c) { + CompetitorDTO competitorDTO = weakCompetitorDTOCache.get(c); + if (competitorDTO == null) { + CountryCode countryCode = c.getTeam().getNationality().getCountryCode(); + competitorDTO = new CompetitorDTO(c.getName(), countryCode == null ? "" + : countryCode.getTwoLetterISOCode(), + countryCode == null ? "" : countryCode.getThreeLetterIOCCode(), countryCode == null ? "" + : countryCode.getName(), c.getBoat().getSailID(), c.getId().toString(), + new BoatClassDTO(c.getBoat().getBoatClass().getName(), c.getBoat().getBoatClass().getHullLength() + .getMeters())); + weakCompetitorDTOCache.put(c, competitorDTO); + } + return competitorDTO; + } + + @Override + public Pair> listTracTracRacesInEvent(String eventJsonURL) throws MalformedURLException, IOException, + ParseException, org.json.simple.parser.ParseException, URISyntaxException { + com.sap.sailing.domain.common.impl.Util.Pair> raceRecords; + raceRecords = getService().getTracTracRaceRecords(new URL(eventJsonURL)); + List result = new ArrayList(); + for (RaceRecord raceRecord : raceRecords.getB()) { + result.add(new TracTracRaceRecordDTO(raceRecord.getID(), raceRecord.getEventName(), raceRecord.getName(), + raceRecord.getParamURL().toString(), raceRecord.getReplayURL(), raceRecord.getLiveURI().toString(), + raceRecord.getStoredURI().toString(), raceRecord.getTrackingStartTime().asDate(), raceRecord + .getTrackingEndTime().asDate(), raceRecord.getRaceStartTime().asDate(), raceRecord.getBoatClassNames())); + } + return new Pair>(raceRecords.getA(), result); + } + + @Override + public void trackWithTracTrac(RegattaIdentifier regattaToAddTo, TracTracRaceRecordDTO rr, String liveURI, String storedURI, + boolean trackWind, final boolean correctWindByDeclination, final boolean simulateWithStartTimeNow) throws Exception { + if (liveURI == null || liveURI.trim().length() == 0) { + liveURI = rr.liveURI; + } + if (storedURI == null || storedURI.trim().length() == 0) { + storedURI = rr.storedURI; + } + final RacesHandle raceHandle = getService().addTracTracRace(regattaToAddTo, new URL(rr.paramURL), + new URI(liveURI), new URI(storedURI), new MillisecondsTimePoint(rr.trackingStartTime), + new MillisecondsTimePoint(rr.trackingEndTime), + MongoWindStoreFactory.INSTANCE.getMongoWindStore(mongoObjectFactory, domainObjectFactory), + RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS, simulateWithStartTimeNow); + if (trackWind) { + new Thread("Wind tracking starter for race "+rr.regattaName+"/"+rr.name) { + public void run() { + try { + startTrackingWind(raceHandle, correctWindByDeclination, RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }.start(); + } + } + + @Override + public List getPreviousTracTracConfigurations() throws Exception { + Iterable configs = tractracDomainObjectFactory.getTracTracConfigurations(); + List result = new ArrayList(); + for (TracTracConfiguration ttConfig : configs) { + result.add(new TracTracConfigurationDTO(ttConfig.getName(), ttConfig.getJSONURL().toString(), + ttConfig.getLiveDataURI().toString(), ttConfig.getStoredDataURI().toString())); + } + return result; + } + + @Override + public void storeTracTracConfiguration(String name, String jsonURL, String liveDataURI, String storedDataURI) throws Exception { + tractracMongoObjectFactory.storeTracTracConfiguration(tractracDomainFactory.createTracTracConfiguration(name, jsonURL, liveDataURI, storedDataURI)); + } + + @Override + public void stopTrackingEvent(RegattaIdentifier regattaIdentifier) throws Exception { + getService().apply(new StopTrackingRegatta(regattaIdentifier)); + } + + private RaceDefinition getRaceByName(Regatta regatta, String raceName) { + if (regatta != null) { + return regatta.getRaceByName(raceName); + } else { + return null; + } + } + + @Override + public void stopTrackingRace(RegattaAndRaceIdentifier regattaAndRaceIdentifier) throws Exception { + getService().apply(new StopTrackingRace(regattaAndRaceIdentifier)); + } + + @Override + public void removeAndUntrackRace(RegattaAndRaceIdentifier regattatAndRaceidentifier) { + getService().apply(new RemoveAndUntrackRace(regattatAndRaceidentifier)); + } + + /** + * @param timeoutInMilliseconds eventually passed to {@link RacesHandle#getRaces(long)}. If the race definition + * can be obtained within this timeout, wind for the race will be tracked; otherwise, the method returns without + * taking any effect. + */ + private void startTrackingWind(RacesHandle raceHandle, boolean correctByDeclination, long timeoutInMilliseconds) throws Exception { + Regatta regatta = raceHandle.getRegatta(); + if (regatta != null) { + for (RaceDefinition race : raceHandle.getRaces(timeoutInMilliseconds)) { + if (race != null) { + getService().startTrackingWind(regatta, race, correctByDeclination); + } else { + log("RaceDefinition wasn't received within " + timeoutInMilliseconds + "ms for a race in regatta " + + regatta.getName() + ". Aborting wait; no wind tracking for this race."); + } + } + } + } + + @Override + public WindInfoForRaceDTO getRawWindFixes(RegattaAndRaceIdentifier raceIdentifier, Collection windSources) { + WindInfoForRaceDTO result = null; + TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); + if (trackedRace != null) { + result = new WindInfoForRaceDTO(); + result.raceIsKnownToStartUpwind = trackedRace.raceIsKnownToStartUpwind(); + Map windTrackInfoDTOs = new HashMap(); + result.windTrackInfoByWindSource = windTrackInfoDTOs; + + List windSourcesToDeliver = new ArrayList(); + if (windSources != null) { + windSourcesToDeliver.addAll(windSources); + } else { + windSourcesToDeliver.add(new WindSourceImpl(WindSourceType.EXPEDITION)); + windSourcesToDeliver.add(new WindSourceImpl(WindSourceType.WEB)); + } + for (WindSource windSource : windSourcesToDeliver) { + if(windSource.getType() == WindSourceType.WEB) { + WindTrackInfoDTO windTrackInfoDTO = new WindTrackInfoDTO(); + windTrackInfoDTO.windFixes = new ArrayList(); + WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource); + + windTrack.lockForRead(); + try { + Iterator windIter = windTrack.getRawFixes().iterator(); + while (windIter.hasNext()) { + Wind wind = windIter.next(); + if(wind != null) { + WindDTO windDTO = createWindDTO(wind, windTrack); + windTrackInfoDTO.windFixes.add(windDTO); + } + } + } finally { + windTrack.unlockAfterRead(); + } + + windTrackInfoDTOs.put(windSource, windTrackInfoDTO); + } + } + } + return result; + } + + protected WindDTO createWindDTO(Wind wind, WindTrack windTrack) { + WindDTO windDTO = new WindDTO(); + windDTO.trueWindBearingDeg = wind.getBearing().getDegrees(); + windDTO.trueWindFromDeg = wind.getBearing().reverse().getDegrees(); + windDTO.trueWindSpeedInKnots = wind.getKnots(); + windDTO.trueWindSpeedInMetersPerSecond = wind.getMetersPerSecond(); + if (wind.getPosition() != null) { + windDTO.position = new PositionDTO(wind.getPosition().getLatDeg(), wind.getPosition() + .getLngDeg()); + } + if (wind.getTimePoint() != null) { + windDTO.measureTimepoint = wind.getTimePoint().asMillis(); + Wind estimatedWind = windTrack + .getAveragedWind(wind.getPosition(), wind.getTimePoint()); + if (estimatedWind != null) { + windDTO.dampenedTrueWindBearingDeg = estimatedWind.getBearing().getDegrees(); + windDTO.dampenedTrueWindFromDeg = estimatedWind.getBearing().reverse().getDegrees(); + windDTO.dampenedTrueWindSpeedInKnots = estimatedWind.getKnots(); + windDTO.dampenedTrueWindSpeedInMetersPerSecond = estimatedWind.getMetersPerSecond(); + } + } + return windDTO; + } + + /** + * Uses wind for both, the non-dampened and dampened fields of the {@link WindDTO} object returned + */ + protected WindDTO createWindDTOFromAlreadyAveraged(Wind wind, WindTrack windTrack, TimePoint requestTimepoint) { + WindDTO windDTO = new WindDTO(); + windDTO.requestTimepoint = requestTimepoint.asMillis(); + windDTO.trueWindBearingDeg = wind.getBearing().getDegrees(); + windDTO.trueWindFromDeg = wind.getBearing().reverse().getDegrees(); + windDTO.trueWindSpeedInKnots = wind.getKnots(); + windDTO.trueWindSpeedInMetersPerSecond = wind.getMetersPerSecond(); + windDTO.dampenedTrueWindBearingDeg = wind.getBearing().getDegrees(); + windDTO.dampenedTrueWindFromDeg = wind.getBearing().reverse().getDegrees(); + windDTO.dampenedTrueWindSpeedInKnots = wind.getKnots(); + windDTO.dampenedTrueWindSpeedInMetersPerSecond = wind.getMetersPerSecond(); + if (wind.getPosition() != null) { + windDTO.position = new PositionDTO(wind.getPosition().getLatDeg(), wind.getPosition() + .getLngDeg()); + } + if (wind.getTimePoint() != null) { + windDTO.measureTimepoint = wind.getTimePoint().asMillis(); + } + return windDTO; + } + + /** + * Fetches the {@link WindTrack#getAveragedWind(Position, TimePoint) average wind} from all wind tracks or those identified + * by windSourceTypeNames + */ + //@Override + public WindInfoForRaceDTO getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, long millisecondsStepWidth, + int numberOfFixes, double latDeg, double lngDeg, Collection windSourceTypeNames) + throws NoWindException { + Position position = new DegreePosition(latDeg, lngDeg); + WindInfoForRaceDTO result = null; + TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); + if (trackedRace != null) { + result = new WindInfoForRaceDTO(); + result.raceIsKnownToStartUpwind = trackedRace.raceIsKnownToStartUpwind(); + List windSourcesToExclude = new ArrayList(); + for (WindSource windSourceToExclude : trackedRace.getWindSourcesToExclude()) { + windSourcesToExclude.add(windSourceToExclude); + } + result.windSourcesToExclude = windSourcesToExclude; + Map windTrackInfoDTOs = new HashMap(); + result.windTrackInfoByWindSource = windTrackInfoDTOs; + List windSourcesToDeliver = new ArrayList(); + Util.addAll(trackedRace.getWindSources(), windSourcesToDeliver); + // TODO bug #375: add the combined wind; currently, CombinedWindTrackImpl just takes too long to return results... + windSourcesToDeliver.add(new WindSourceImpl(WindSourceType.COMBINED)); + for (WindSource windSource : windSourcesToDeliver) { + if (windSourceTypeNames == null || windSourceTypeNames.contains(windSource.getType().name())) { + TimePoint fromTimePoint = new MillisecondsTimePoint(from); + WindTrackInfoDTO windTrackInfoDTO = new WindTrackInfoDTO(); + windTrackInfoDTO.windFixes = new ArrayList(); + WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource); + windTrackInfoDTOs.put(windSource, windTrackInfoDTO); + windTrackInfoDTO.dampeningIntervalInMilliseconds = windTrack + .getMillisecondsOverWhichToAverageWind(); + TimePoint timePoint = fromTimePoint; + Double minWindConfidence = 2.0; + Double maxWindConfidence = -1.0; + for (int i = 0; i < numberOfFixes; i++) { + WindWithConfidence> averagedWindWithConfidence = windTrack.getAveragedWindWithConfidence(position, timePoint); + + if (averagedWindWithConfidence != null) { + WindDTO windDTO = createWindDTOFromAlreadyAveraged(averagedWindWithConfidence.getObject(), windTrack, timePoint); + double confidence = averagedWindWithConfidence.getConfidence(); + windDTO.confidence = confidence; + windTrackInfoDTO.windFixes.add(windDTO); + if(confidence < minWindConfidence) { + minWindConfidence = confidence; + } + if(confidence > maxWindConfidence) { + maxWindConfidence = confidence; + } + } + timePoint = new MillisecondsTimePoint(timePoint.asMillis() + millisecondsStepWidth); + } + windTrackInfoDTO.minWindConfidence = minWindConfidence; + windTrackInfoDTO.maxWindConfidence = maxWindConfidence; + } + } + } + return result; + } + + @Override + public WindInfoForRaceDTO getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, long millisecondsStepWidth, + int numberOfFixes, Collection windSourceTypeNames) + throws NoWindException { + assert from != null; + TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); + + WindInfoForRaceDTO result = getAveragedWindInfo(new MillisecondsTimePoint(from), millisecondsStepWidth, numberOfFixes, + windSourceTypeNames, trackedRace); + return result; + } + + private WindInfoForRaceDTO getAveragedWindInfo(TimePoint from, long millisecondsStepWidth, int numberOfFixes, + Collection windSourceTypeNames, TrackedRace trackedRace) { + WindInfoForRaceDTO result = null; + if (trackedRace != null) { + TimePoint newestEvent = trackedRace.getTimePointOfNewestEvent(); + result = new WindInfoForRaceDTO(); + result.raceIsKnownToStartUpwind = trackedRace.raceIsKnownToStartUpwind(); + List windSourcesToExclude = new ArrayList(); + for (WindSource windSourceToExclude : trackedRace.getWindSourcesToExclude()) { + windSourcesToExclude.add(windSourceToExclude); + } + result.windSourcesToExclude = windSourcesToExclude; + Map windTrackInfoDTOs = new HashMap(); + result.windTrackInfoByWindSource = windTrackInfoDTOs; + List windSourcesToDeliver = new ArrayList(); + Util.addAll(trackedRace.getWindSources(), windSourcesToDeliver); + windSourcesToDeliver.add(new WindSourceImpl(WindSourceType.COMBINED)); + for (WindSource windSource : windSourcesToDeliver) { + // TODO consider parallelizing + if (windSourceTypeNames == null || windSourceTypeNames.contains(windSource.getType().name())) { + WindTrackInfoDTO windTrackInfoDTO = new WindTrackInfoDTO(); + windTrackInfoDTOs.put(windSource, windTrackInfoDTO); + windTrackInfoDTO.windFixes = new ArrayList(); + WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource); + windTrackInfoDTO.dampeningIntervalInMilliseconds = windTrack + .getMillisecondsOverWhichToAverageWind(); + TimePoint timePoint = from; + Double minWindConfidence = 2.0; + Double maxWindConfidence = -1.0; + for (int i = 0; i < numberOfFixes && newestEvent != null && timePoint.compareTo(newestEvent) < 0; i++) { + WindWithConfidence> averagedWindWithConfidence = windTrack.getAveragedWindWithConfidence(null, timePoint); + if (averagedWindWithConfidence != null) { + double confidence = averagedWindWithConfidence.getConfidence(); + WindDTO windDTO = createWindDTOFromAlreadyAveraged(averagedWindWithConfidence.getObject(), windTrack, timePoint); + windDTO.confidence = confidence; + windTrackInfoDTO.windFixes.add(windDTO); + if(confidence < minWindConfidence) { + minWindConfidence = confidence; + } + if(confidence > maxWindConfidence) { + maxWindConfidence = confidence; + } + } + timePoint = new MillisecondsTimePoint(timePoint.asMillis() + millisecondsStepWidth); + } + windTrackInfoDTO.minWindConfidence = minWindConfidence; + windTrackInfoDTO.maxWindConfidence = maxWindConfidence; + } + } + } + return result; + } + + @Override + public WindInfoForRaceDTO getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, Date to, + long resolutionInMilliseconds, Collection windSourceTypeNames) { + TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); + WindInfoForRaceDTO result = null; + if (trackedRace != null) { + TimePoint fromTimePoint = from == null ? trackedRace.getStartOfTracking() : new MillisecondsTimePoint(from); + TimePoint toTimePoint = to == null ? trackedRace.getEndOfRace() : new MillisecondsTimePoint(to); + if(fromTimePoint != null && toTimePoint != null) { + int numberOfFixes = (int) ((toTimePoint.asMillis() - fromTimePoint.asMillis())/resolutionInMilliseconds); + result = getAveragedWindInfo(fromTimePoint, resolutionInMilliseconds, numberOfFixes, windSourceTypeNames, trackedRace); + } + } + return result; + } + + @Override + public void setWind(RegattaAndRaceIdentifier raceIdentifier, WindDTO windDTO) { + DynamicTrackedRace trackedRace = (DynamicTrackedRace) getExistingTrackedRace(raceIdentifier); + if (trackedRace != null) { + Position p = null; + if (windDTO.position != null) { + p = new DegreePosition(windDTO.position.latDeg, windDTO.position.lngDeg); + } + TimePoint at = null; + if (windDTO.measureTimepoint != null) { + at = new MillisecondsTimePoint(windDTO.measureTimepoint); + } + SpeedWithBearing speedWithBearing = null; + Speed speed = null; + if (windDTO.trueWindSpeedInKnots != null) { + speed = new KnotSpeedImpl(windDTO.trueWindSpeedInKnots); + } else if (windDTO.trueWindSpeedInMetersPerSecond != null) { + speed = new KilometersPerHourSpeedImpl(windDTO.trueWindSpeedInMetersPerSecond * 3600. / 1000.); + } else if (windDTO.dampenedTrueWindSpeedInKnots != null) { + speed = new KnotSpeedImpl(windDTO.dampenedTrueWindSpeedInKnots); + } else if (windDTO.dampenedTrueWindSpeedInMetersPerSecond != null) { + speed = new KilometersPerHourSpeedImpl(windDTO.dampenedTrueWindSpeedInMetersPerSecond * 3600. / 1000.); + } + if (speed != null) { + if (windDTO.trueWindBearingDeg != null) { + speedWithBearing = new KnotSpeedWithBearingImpl(speed.getKnots(), new DegreeBearingImpl( + windDTO.trueWindBearingDeg)); + } else if (windDTO.trueWindFromDeg != null) { + speedWithBearing = new KnotSpeedWithBearingImpl(speed.getKnots(), new DegreeBearingImpl( + windDTO.trueWindFromDeg).reverse()); + } + } + Wind wind = new WindImpl(p, at, speedWithBearing); + Iterable webWindSources = trackedRace.getWindSources(WindSourceType.WEB); + if(Util.size(webWindSources) == 0) { + // create a new WEB wind source if not available + trackedRace.recordWind(wind, new WindSourceImpl(WindSourceType.WEB)); + } else { + trackedRace.recordWind(wind, webWindSources.iterator().next()); + } + } + } + + @Override + public RaceMapDataDTO getRaceMapData(RegattaAndRaceIdentifier raceIdentifier, Date date, + Map from, Map to, + boolean extrapolate) throws NoWindException { + RaceMapDataDTO raceMapDataDTO = new RaceMapDataDTO(); + + raceMapDataDTO.boatPositions = getBoatPositions(raceIdentifier, from, to, extrapolate); + raceMapDataDTO.coursePositions = getCoursePositions(raceIdentifier, date); + raceMapDataDTO.quickRanks = getQuickRanks(raceIdentifier, date); + + return raceMapDataDTO; + } + + @Override + public Map> getBoatPositions(RegattaAndRaceIdentifier raceIdentifier, + Map from, Map to, + boolean extrapolate) throws NoWindException { + Map> result = new HashMap>(); + TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); + if (trackedRace != null) { + for (Competitor competitor : trackedRace.getRace().getCompetitors()) { + CompetitorDTO competitorDTO = convertToCompetitorDTO(competitor); + if (from.containsKey(competitorDTO)) { + List fixesForCompetitor = new ArrayList(); + result.put(competitorDTO, fixesForCompetitor); + GPSFixTrack track = trackedRace.getTrack(competitor); + TimePoint fromTimePoint = new MillisecondsTimePoint(from.get(competitorDTO)); + TimePoint toTimePointExcluding = new MillisecondsTimePoint(to.get(competitorDTO)); + // copy the fixes into a list while holding the monitor; then release the monitor to avoid deadlocks + // during wind estimations required for tack determination + List fixes = new ArrayList(); + track.lockForRead(); + try { + Iterator fixIter = track.getFixesIterator(fromTimePoint, /* inclusive */true); + while (fixIter.hasNext()) { + GPSFixMoving fix = fixIter.next(); + if (fix.getTimePoint().compareTo(toTimePointExcluding) < 0) { + fixes.add(fix); + } else { + break; + } + } + } finally { + track.unlockAfterRead(); + } + if (fixes.isEmpty()) { + // then there was no (smoothened) fix between fromTimePoint and toTimePointExcluding; estimate... + TimePoint middle = new MillisecondsTimePoint((toTimePointExcluding.asMillis()+fromTimePoint.asMillis())/2); + Position estimatedPosition = track.getEstimatedPosition(middle, extrapolate); + SpeedWithBearing estimatedSpeed = track.getEstimatedSpeed(middle); + if(estimatedPosition != null && estimatedSpeed != null) { + fixes.add(new GPSFixMovingImpl(estimatedPosition, middle, estimatedSpeed)); + } + } + Iterator fixIter = fixes.iterator(); + if (fixIter.hasNext()) { + final WindSource windSource = new WindSourceImpl(WindSourceType.COMBINED); + GPSFixMoving fix = fixIter.next(); + while (fix != null && (fix.getTimePoint().compareTo(toTimePointExcluding) < 0 || + (fix.getTimePoint().equals(toTimePointExcluding) && toTimePointExcluding.equals(fromTimePoint)))) { + Tack tack = trackedRace.getTack(competitor, fix.getTimePoint()); + TrackedLegOfCompetitor trackedLegOfCompetitor = trackedRace.getTrackedLeg(competitor, + fix.getTimePoint()); + LegType legType = trackedLegOfCompetitor == null ? null : trackedRace.getTrackedLeg( + trackedLegOfCompetitor.getLeg()).getLegType(fix.getTimePoint()); + Wind wind = trackedRace.getWind(fix.getPosition(),toTimePointExcluding); + WindDTO windDTO = createWindDTOFromAlreadyAveraged(wind, trackedRace.getOrCreateWindTrack(windSource), toTimePointExcluding); + GPSFixDTO fixDTO = createGPSFixDTO(fix, fix.getSpeed(), windDTO, tack, legType, /* extrapolate */ + false); + fixesForCompetitor.add(fixDTO); + if (fixIter.hasNext()) { + fix = fixIter.next(); + } else { + // check if fix was at date and if extrapolation is requested + if (!fix.getTimePoint().equals(toTimePointExcluding) && extrapolate) { + Position position = track.getEstimatedPosition(toTimePointExcluding, extrapolate); + Tack tack2 = trackedRace.getTack(competitor, toTimePointExcluding); + LegType legType2 = trackedLegOfCompetitor == null ? null : trackedRace + .getTrackedLeg(trackedLegOfCompetitor.getLeg()).getLegType( + fix.getTimePoint()); + SpeedWithBearing speedWithBearing = track.getEstimatedSpeed(toTimePointExcluding); + Wind wind2 = trackedRace.getWind(position, toTimePointExcluding); + WindDTO windDTO2 = createWindDTOFromAlreadyAveraged(wind2, trackedRace.getOrCreateWindTrack(windSource), toTimePointExcluding); + GPSFixDTO extrapolated = new GPSFixDTO( + to.get(competitorDTO), + position==null?null:new PositionDTO(position.getLatDeg(), position.getLngDeg()), + speedWithBearing==null?null:createSpeedWithBearingDTO(speedWithBearing), windDTO2, + tack2, legType2, /* extrapolated */ true); + fixesForCompetitor.add(extrapolated); + } + fix = null; + } + } + } + } + } + } + return result; + } + + private SpeedWithBearingDTO createSpeedWithBearingDTO(SpeedWithBearing speedWithBearing) { + return new SpeedWithBearingDTO(speedWithBearing.getKnots(), speedWithBearing + .getBearing().getDegrees()); + } + + private GPSFixDTO createGPSFixDTO(GPSFix fix, SpeedWithBearing speedWithBearing, WindDTO windDTO, Tack tack, LegType legType, boolean extrapolated) { + return new GPSFixDTO(fix.getTimePoint().asDate(), fix.getPosition()==null?null:new PositionDTO(fix + .getPosition().getLatDeg(), fix.getPosition().getLngDeg()), + speedWithBearing==null?null:createSpeedWithBearingDTO(speedWithBearing), windDTO, tack, legType, extrapolated); + } + + @Override + public RaceTimesInfoDTO getRaceTimesInfo(RegattaAndRaceIdentifier raceIdentifier) { + RaceTimesInfoDTO raceTimesInfo = null; + TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); + + if (trackedRace != null) { + raceTimesInfo = new RaceTimesInfoDTO(raceIdentifier); + List legInfos = new ArrayList(); + raceTimesInfo.setLegInfos(legInfos); + List markPassingTimesDTOs = new ArrayList(); + raceTimesInfo.setMarkPassingTimes(markPassingTimesDTOs); + + raceTimesInfo.startOfRace = trackedRace.getStartOfRace() == null ? null : trackedRace.getStartOfRace().asDate(); + raceTimesInfo.startOfTracking = trackedRace.getStartOfTracking() == null ? null : trackedRace.getStartOfTracking().asDate(); + raceTimesInfo.newestTrackingEvent = trackedRace.getTimePointOfNewestEvent() == null ? null : trackedRace.getTimePointOfNewestEvent().asDate(); + raceTimesInfo.endOfTracking = trackedRace.getEndOfTracking() == null ? null : trackedRace.getEndOfTracking().asDate(); + raceTimesInfo.endOfRace = trackedRace.getEndOfRace() == null ? null : trackedRace.getEndOfRace().asDate(); + raceTimesInfo.delayToLiveInMs = trackedRace.getDelayToLiveInMillis(); + + Iterable>> markPassingsTimes = trackedRace.getMarkPassingsTimes(); + synchronized (markPassingsTimes) { + int numberOfWaypoints = Util.size(markPassingsTimes); + int wayPointNumber = 1; + for (Pair> markPassingTimes : markPassingsTimes) { + MarkPassingTimesDTO markPassingTimesDTO = new MarkPassingTimesDTO(); + String name = "M" + (wayPointNumber - 1); + if (wayPointNumber == 1) { + name = "S"; + } else if (wayPointNumber == numberOfWaypoints) { + name = "F"; + } + markPassingTimesDTO.name = name; + Pair timesPair = markPassingTimes.getB(); + TimePoint firstPassingTime = timesPair.getA(); + TimePoint lastPassingTime = timesPair.getB(); + markPassingTimesDTO.firstPassingDate = firstPassingTime == null ? null : firstPassingTime.asDate(); + markPassingTimesDTO.lastPassingDate = lastPassingTime == null ? null : lastPassingTime.asDate(); + markPassingTimesDTOs.add(markPassingTimesDTO); + wayPointNumber++; + } + } + trackedRace.getRace().getCourse().lockForRead(); + try { + Iterable trackedLegs = trackedRace.getTrackedLegs(); + int legNumber = 1; + for (TrackedLeg trackedLeg : trackedLegs) { + LegInfoDTO legInfoDTO = new LegInfoDTO(legNumber); + legInfoDTO.name = "L" + legNumber; + try { + MarkPassingTimesDTO markPassingTimesDTO = markPassingTimesDTOs.get(legNumber - 1); + if (markPassingTimesDTO.firstPassingDate != null) { + TimePoint p = new MillisecondsTimePoint(markPassingTimesDTO.firstPassingDate); + legInfoDTO.legType = trackedLeg.getLegType(p); + legInfoDTO.legBearingInDegrees = trackedLeg.getLegBearing(p).getDegrees(); + } + } catch (NoWindException e) { + // do nothing + } + legInfos.add(legInfoDTO); + legNumber++; + } + } finally { + trackedRace.getRace().getCourse().unlockAfterRead(); + } + } + return raceTimesInfo; + } + + @Override + public List getRaceTimesInfos(Collection raceIdentifiers) { + List raceTimesInfos = new ArrayList(); + for (RegattaAndRaceIdentifier raceIdentifier : raceIdentifiers) { + RaceTimesInfoDTO raceTimesInfo = getRaceTimesInfo(raceIdentifier); + if (raceTimesInfo != null) { + raceTimesInfos.add(raceTimesInfo); + } + } + return raceTimesInfos; + } + + @Override + public CourseDTO getCoursePositions(RegattaAndRaceIdentifier raceIdentifier, Date date) { + CourseDTO result = new CourseDTO(); + if (date != null) { + TimePoint dateAsTimePoint = new MillisecondsTimePoint(date); + TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); + if (trackedRace != null) { + result.marks = new HashSet(); + result.waypointPositions = new ArrayList(); + Set marks = new HashSet(); + Course course = trackedRace.getRace().getCourse(); + for (Waypoint waypoint : course.getWaypoints()) { + Position waypointPosition = trackedRace.getApproximatePosition(waypoint, dateAsTimePoint); + if (waypointPosition != null) { + result.waypointPositions.add(new PositionDTO(waypointPosition.getLatDeg(), waypointPosition.getLngDeg())); + } + for (Mark b : waypoint.getMarks()) { + marks.add(b); + } + } + for (Mark mark : marks) { + GPSFixTrack track = trackedRace.getOrCreateTrack(mark); + Position positionAtDate = track.getEstimatedPosition(dateAsTimePoint, /* extrapolate */false); + if (positionAtDate != null) { + result.marks.add(convertToMarkDTO(mark, positionAtDate)); + } + } + + // set the positions of start and finish + Waypoint firstWaypoint = course.getFirstWaypoint(); + if (firstWaypoint != null) { + result.startMarkPositions = getMarkPositionDTOs(dateAsTimePoint, trackedRace, firstWaypoint); + } + Waypoint lastWaypoint = course.getLastWaypoint(); + if (lastWaypoint != null) { + result.finishMarkPositions = getMarkPositionDTOs(dateAsTimePoint, trackedRace, lastWaypoint); + } + } + } + return result; + } + + @Override + public RaceCourseMarksDTO getRaceCourseMarks(RegattaAndRaceIdentifier raceIdentifier, Date date) { + RaceCourseMarksDTO raceMarksDTO = new RaceCourseMarksDTO(); + TimePoint dateAsTimePoint = new MillisecondsTimePoint(date); + TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); + if (trackedRace != null) { + raceMarksDTO.marks = new HashSet(); + Iterable marks = trackedRace.getMarks(); + for (Mark mark : marks) { + GPSFixTrack track = trackedRace.getOrCreateTrack(mark); + Position positionAtDate = track.getEstimatedPosition(dateAsTimePoint, /* extrapolate */false); + if (positionAtDate != null) { + raceMarksDTO.marks.add(convertToMarkDTO(mark, positionAtDate)); + } + } + } + return raceMarksDTO; + } + + @Override + public List getRaceCourse(RegattaAndRaceIdentifier raceIdentifier, Date date) { + List result = new ArrayList(); + if (date != null) { + TimePoint dateAsTimePoint = new MillisecondsTimePoint(date); + TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); + if (trackedRace != null) { + Course course = trackedRace.getRace().getCourse(); + for (Waypoint waypoint : course.getWaypoints()) { + result.add(createControlPointDTO(waypoint.getControlPoint(), trackedRace, dateAsTimePoint)); + } + } + } + return result; + } + + private ControlPointDTO createControlPointDTO(ControlPoint controlPoint, TrackedRace trackedRace, TimePoint timePoint) { + ControlPointDTO result; + if (controlPoint instanceof Gate) { + final Mark left = ((Gate) controlPoint).getLeft(); + final Position leftPos = trackedRace.getOrCreateTrack(left).getEstimatedPosition(timePoint, /* extrapolate */ false); + final Mark right = ((Gate) controlPoint).getRight(); + final Position rightPos = trackedRace.getOrCreateTrack(right).getEstimatedPosition(timePoint, /* extrapolate */ false); + result = new GateDTO(controlPoint.getName(), convertToMarkDTO(left, leftPos), convertToMarkDTO(right, rightPos)); + } else { + final Position posOfFirst = trackedRace.getOrCreateTrack(controlPoint.getMarks().iterator().next()). + getEstimatedPosition(timePoint, /* extrapolate */ false); + result = new MarkDTO(controlPoint.getName(), posOfFirst.getLatDeg(), posOfFirst.getLngDeg()); + } + return result; + } + + /** + * For each {@link ControlPointDTO} in controlPoints tries to find the best-matching waypoint + * from the course that belongs to the race identified by raceIdentifier. If such a waypoint is + * found, its control point is added to the control point list for the new course. Otherwise, a new control + * point is created using the default {@link com.sap.sailing.domain.base.DomainFactory} instance. The resulting + * list of control points is then passed to {@link Course#update(List, com.sap.sailing.domain.base.DomainFactory)} for + * the course of the race identified by raceIdentifier. + */ + @Override + public void updateRaceCourse(RegattaAndRaceIdentifier raceIdentifier, List controlPoints) { + TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); + if (trackedRace != null) { + Course course = trackedRace.getRace().getCourse(); + Iterable waypoints = course.getWaypoints(); + List newControlPoints = new ArrayList(); + int lastMatchPosition = -1; + for (ControlPointDTO controlPointDTO : controlPoints) { + ControlPoint matchFromOldCourse = null; + for (int i=lastMatchPosition+1; matchFromOldCourse == null && i marks, Iterable marksDTOs) { + Iterator marksIter = marks.iterator(); + Iterator markDTOsIter = marksDTOs.iterator(); + while (marksIter.hasNext() && markDTOsIter.hasNext()) { + Mark nextMark = marksIter.next(); + MarkDTO nextMarkDTO = markDTOsIter.next(); + if (!nextMark.getName().equals(nextMarkDTO.name)) { + return false; + } + } + return marksIter.hasNext() == markDTOsIter.hasNext(); + } + + private List getMarkPositionDTOs(TimePoint timePoint, TrackedRace trackedRace, Waypoint waypoint) { + List startMarkPositions = new ArrayList(); + for (Mark startMark : waypoint.getMarks()) { + final Position estimatedMarkPosition = trackedRace.getOrCreateTrack(startMark) + .getEstimatedPosition(timePoint, /* extrapolate */false); + if (estimatedMarkPosition != null) { + startMarkPositions.add(new PositionDTO(estimatedMarkPosition.getLatDeg(), estimatedMarkPosition.getLngDeg())); + } + } + return startMarkPositions; + } + + @Override + public List getQuickRanks(RegattaAndRaceIdentifier raceIdentifier, Date date) throws NoWindException { + List result = new ArrayList(); + if (date != null) { + TimePoint dateAsTimePoint = new MillisecondsTimePoint(date); + TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); + if (trackedRace != null) { + RaceDefinition race = trackedRace.getRace(); + for (Competitor competitor : race.getCompetitors()) { + int rank = trackedRace.getRank(competitor, dateAsTimePoint); + TrackedLegOfCompetitor trackedLeg = trackedRace.getTrackedLeg(competitor, dateAsTimePoint); + if (trackedLeg != null) { + int legNumber = race.getCourse().getLegs().indexOf(trackedLeg.getLeg()) + 1; + QuickRankDTO quickRankDTO = new QuickRankDTO(convertToCompetitorDTO(competitor), rank, legNumber); + result.add(quickRankDTO); + } + } + Collections.sort(result, new Comparator() { + @Override + public int compare(QuickRankDTO o1, QuickRankDTO o2) { + return o1.rank - o2.rank; + } + }); + } + } + return result; + } + + @Override + public void setRaceIsKnownToStartUpwind(RegattaAndRaceIdentifier raceIdentifier, boolean raceIsKnownToStartUpwind) { + getService().apply(new SetRaceIsKnownToStartUpwind(raceIdentifier, raceIsKnownToStartUpwind)); + } + + @Override + public void setWindSourcesToExclude(RegattaAndRaceIdentifier raceIdentifier, Iterable windSourcesToExclude) { + getService().apply(new SetWindSourcesToExclude(raceIdentifier, windSourcesToExclude)); + } + + @Override + public WindInfoForRaceDTO getWindSourcesInfo(RegattaAndRaceIdentifier raceIdentifier) { + WindInfoForRaceDTO result = null; + TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); + if (trackedRace != null) { + result = new WindInfoForRaceDTO(); + result.raceIsKnownToStartUpwind = trackedRace.raceIsKnownToStartUpwind(); + List windSourcesToExclude = new ArrayList(); + for (WindSource windSourceToExclude : trackedRace.getWindSourcesToExclude()) { + windSourcesToExclude.add(windSourceToExclude); + } + result.windSourcesToExclude = windSourcesToExclude; + Map windTrackInfoDTOs = new HashMap(); + result.windTrackInfoByWindSource = windTrackInfoDTOs; + + for(WindSource windSource: trackedRace.getWindSources()) { + windTrackInfoDTOs.put(windSource, new WindTrackInfoDTO()); + } + windTrackInfoDTOs.put(new WindSourceImpl(WindSourceType.COMBINED), new WindTrackInfoDTO()); + } + return result; + } + + @Override + public void removeWind(RegattaAndRaceIdentifier raceIdentifier, WindDTO windDTO) { + DynamicTrackedRace trackedRace = (DynamicTrackedRace) getExistingTrackedRace(raceIdentifier); + if (trackedRace != null) { + Position p = null; + if (windDTO.position != null) { + p = new DegreePosition(windDTO.position.latDeg, windDTO.position.lngDeg); + } + TimePoint at = null; + if (windDTO.measureTimepoint != null) { + at = new MillisecondsTimePoint(windDTO.measureTimepoint); + } + SpeedWithBearing speedWithBearing = null; + Speed speed = null; + if (windDTO.trueWindSpeedInKnots != null) { + speed = new KnotSpeedImpl(windDTO.trueWindSpeedInKnots); + } else if (windDTO.trueWindSpeedInMetersPerSecond != null) { + speed = new KilometersPerHourSpeedImpl(windDTO.trueWindSpeedInMetersPerSecond * 3600. / 1000.); + } else if (windDTO.dampenedTrueWindSpeedInKnots != null) { + speed = new KnotSpeedImpl(windDTO.dampenedTrueWindSpeedInKnots); + } else if (windDTO.dampenedTrueWindSpeedInMetersPerSecond != null) { + speed = new KilometersPerHourSpeedImpl(windDTO.dampenedTrueWindSpeedInMetersPerSecond * 3600. / 1000.); + } + if (speed != null) { + if (windDTO.trueWindBearingDeg != null) { + speedWithBearing = new KnotSpeedWithBearingImpl(speed.getKnots(), new DegreeBearingImpl( + windDTO.trueWindBearingDeg)); + } else if (windDTO.trueWindFromDeg != null) { + speedWithBearing = new KnotSpeedWithBearingImpl(speed.getKnots(), new DegreeBearingImpl( + windDTO.trueWindFromDeg).reverse()); + } + } + Wind wind = new WindImpl(p, at, speedWithBearing); + trackedRace.removeWind(wind, trackedRace.getWindSources(WindSourceType.WEB).iterator().next()); + } + } + + protected RacingEventService getService() { + return racingEventServiceTracker.getService(); // grab the service + } + + private ReplicationService getReplicationService() { + return replicationServiceTracker.getService(); + } + + @Override + public List getLeaderboardNames() throws Exception { + return new ArrayList(getService().getLeaderboards().keySet()); + } + + @Override + public StrippedLeaderboardDTO createFlexibleLeaderboard(String leaderboardName, int[] discardThresholds, ScoringSchemeType scoringSchemeType) { + return createStrippedLeaderboardDTO(getService().apply(new CreateFlexibleLeaderboard(leaderboardName, discardThresholds, + baseDomainFactory.createScoringScheme(scoringSchemeType))), false); + } + + @Override + public StrippedLeaderboardDTO createRegattaLeaderboard(RegattaIdentifier regattaIdentifier, int[] discardThresholds) { + return createStrippedLeaderboardDTO(getService().apply(new CreateRegattaLeaderboard(regattaIdentifier, discardThresholds)), false); + } + + @Override + public List getLeaderboards() { + Map leaderboards = getService().getLeaderboards(); + List results = new ArrayList(); + for(Leaderboard leaderboard: leaderboards.values()) { + StrippedLeaderboardDTO dao = createStrippedLeaderboardDTO(leaderboard, false); + results.add(dao); + } + return results; + } + + @Override + public StrippedLeaderboardDTO getLeaderboard(String leaderboardName) { + Map leaderboards = getService().getLeaderboards(); + StrippedLeaderboardDTO result = null; + Leaderboard leaderboard = leaderboards.get(leaderboardName); + if (leaderboard != null) { + result = createStrippedLeaderboardDTO(leaderboard, false); + } + return result; + } + + @Override + public List getLeaderboardsByEvent(RegattaDTO regatta) { + List results = new ArrayList(); + for (RaceDTO race : regatta.races) { + List leaderboard = getLeaderboardsByRace(race); + if (leaderboard != null && !leaderboard.isEmpty()) { + results.addAll(leaderboard); + } + } + // Removing duplicates + HashSet set = new HashSet(results); + results.clear(); + results.addAll(set); + return results; + } + + @Override + public List getLeaderboardsByRace(RaceDTO race) { + List results = new ArrayList(); + Map leaderboards = getService().getLeaderboards(); + for (Leaderboard leaderboard : leaderboards.values()) { + Iterable races = leaderboard.getRaceColumns(); + for (RaceColumn raceInLeaderboard : races) { + for (Fleet fleet : raceInLeaderboard.getFleets()) { + TrackedRace trackedRace = raceInLeaderboard.getTrackedRace(fleet); + if (trackedRace != null) { + RaceDefinition trackedRaceDef = trackedRace.getRace(); + if (trackedRaceDef.getName().equals(race.name)) { + results.add(createStrippedLeaderboardDTO(leaderboard, false)); + break; + } + } + } + } + } + return results; + } + + /** + * Creates a {@link LeaderboardDTO} for leaderboard and fills in the name, race master data + * in the form of {@link RaceColumnDTO}s, whether or not there are {@link LeaderboardDTO#hasCarriedPoints carried points} + * and the {@link LeaderboardDTO#discardThresholds discarding thresholds} for the leaderboard. No data about the points + * is filled into the result object. No data about the competitor display names is filled in; instead, an empty map + * is used for {@link LeaderboardDTO#competitorDisplayNames}.
+ * If withGeoLocationData is true the geographical location of all races will be determined. + */ + private StrippedLeaderboardDTO createStrippedLeaderboardDTO(Leaderboard leaderboard, boolean withGeoLocationData) { + StrippedLeaderboardDTO leaderboardDTO = new StrippedLeaderboardDTO(); + TimePoint startOfLatestRace = null; + TimePoint now = MillisecondsTimePoint.now(); + Long delayToLiveInMillisForLatestRace = null; + leaderboardDTO.name = leaderboard.getName(); + leaderboardDTO.competitorDisplayNames = new HashMap(); + leaderboardDTO.isMetaLeaderboard = leaderboard instanceof MetaLeaderboard ? true : false; + if (leaderboard instanceof RegattaLeaderboard) { + RegattaLeaderboard regattaLeaderboard = (RegattaLeaderboard) leaderboard; + Regatta regatta = regattaLeaderboard.getRegatta(); + leaderboardDTO.regattaName = regatta.getName(); + leaderboardDTO.isRegattaLeaderboard = true; + leaderboardDTO.scoringScheme = regatta.getScoringScheme().getType(); + } else { + leaderboardDTO.isRegattaLeaderboard = false; + leaderboardDTO.scoringScheme = leaderboard.getScoringScheme().getType(); + } + leaderboardDTO.setDelayToLiveInMillisForLatestRace(delayToLiveInMillisForLatestRace); + leaderboardDTO.hasCarriedPoints = leaderboard.hasCarriedPoints(); + leaderboardDTO.discardThresholds = leaderboard.getResultDiscardingRule().getDiscardIndexResultsStartingWithHowManyRaces(); + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + for (Fleet fleet : raceColumn.getFleets()) { + TimePoint latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive = null; + RaceDTO raceDTO = null; + RegattaAndRaceIdentifier raceIdentifier = null; + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + if (startOfLatestRace == null || (trackedRace.getStartOfRace() != null && trackedRace.getStartOfRace().compareTo(startOfLatestRace) > 0)) { + delayToLiveInMillisForLatestRace = trackedRace.getDelayToLiveInMillis(); + } + raceIdentifier = new RegattaNameAndRaceName(trackedRace.getTrackedRegatta().getRegatta().getName(), trackedRace.getRace().getName()); + // Optional: Getting the places of the race + PlacemarkOrderDTO racePlaces = withGeoLocationData ? getRacePlaces(trackedRace) : null; + + TrackedRaceDTO trackedRaceDTO = new TrackedRaceDTO(); + trackedRaceDTO.startOfTracking = trackedRace.getStartOfTracking() == null ? null : trackedRace.getStartOfTracking().asDate(); + trackedRaceDTO.hasWindData = trackedRace.hasWindData(); + trackedRaceDTO.hasGPSData = trackedRace.hasGPSData(); + raceDTO = new RaceDTO(raceIdentifier, trackedRaceDTO, getService().isRaceBeingTracked(trackedRace.getRace())); + raceDTO.places = racePlaces; + raceDTO.startOfRace = trackedRace.getStartOfRace() == null ? null : trackedRace.getStartOfRace().asDate(); + raceDTO.endOfRace = trackedRace.getEndOfRace() == null ? null : trackedRace.getEndOfRace().asDate(); + if (trackedRace.hasStarted(now) && trackedRace.hasGPSData() && trackedRace.hasWindData()) { + TimePoint liveTimePointForTrackedRace = now; + final TimePoint endOfRace = trackedRace.getEndOfRace(); + if (endOfRace != null) { + liveTimePointForTrackedRace = endOfRace; + } + latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive = liveTimePointForTrackedRace; + } + } + final FleetDTO fleetDTO = convertToFleetDTO(fleet); + RaceColumnDTO raceColumnDTO = leaderboardDTO.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), fleetDTO, + raceColumn.isMedalRace(), raceIdentifier, raceDTO); + if (latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive != null) { + raceColumnDTO.setWhenLastTrackedRaceWasLive(fleetDTO, latestTimePointAfterQueryTimePointWhenATrackedRaceWasLive.asDate()); + } + } + } + return leaderboardDTO; + } + + private PlacemarkOrderDTO getRacePlaces(TrackedRace trackedRace) { + Pair startAndFinish = getStartFinishPlacemarksForTrackedRace(trackedRace); + PlacemarkOrderDTO racePlaces = new PlacemarkOrderDTO(); + if (startAndFinish.getA() != null) { + racePlaces.getPlacemarks().add(convertToPlacemarkDTO(startAndFinish.getA())); + } + if (startAndFinish.getB() != null) { + racePlaces.getPlacemarks().add(convertToPlacemarkDTO(startAndFinish.getB())); + } + if (racePlaces.isEmpty()) { + racePlaces = null; + } + return racePlaces; + } + + private Pair getStartFinishPlacemarksForTrackedRace(TrackedRace race) { + double radiusCalculationFactor = 10.0; + Placemark startBest = null; + Placemark finishBest = null; + + // Get start postition + Iterator startMarks = race.getRace().getCourse().getFirstWaypoint().getMarks().iterator(); + GPSFix startMarkFix = startMarks.hasNext() ? race.getOrCreateTrack(startMarks.next()).getLastRawFix() : null; + Position startPosition = startMarkFix != null ? startMarkFix.getPosition() : null; + if (startPosition != null) { + try { + // Get distance to nearest placemark and calculate the search radius + Placemark startNearest = ReverseGeocoder.INSTANCE.getPlacemarkNearest(startPosition); + if (startNearest != null) { + Distance startNearestDistance = startNearest.distanceFrom(startPosition); + double startRadius = startNearestDistance.getKilometers() * radiusCalculationFactor; + + // Get the estimated best start place + startBest = ReverseGeocoder.INSTANCE.getPlacemarkLast(startPosition, startRadius, + new Placemark.ByPopulationDistanceRatio(startPosition)); + } + } catch (IOException e) { + logger.throwing(TrackedRaceImpl.class.getName(), "getPlaceOrder()", e); + } catch (org.json.simple.parser.ParseException e) { + logger.throwing(TrackedRaceImpl.class.getName(), "getPlaceOrder()", e); + } + } + + // Get finish position + Iterator finishMarks = race.getRace().getCourse().getFirstWaypoint().getMarks().iterator(); + GPSFix finishMarkFix = finishMarks.hasNext() ? race.getOrCreateTrack(finishMarks.next()).getLastRawFix() : null; + Position finishPosition = finishMarkFix != null ? finishMarkFix.getPosition() : null; + if (startPosition != null && finishPosition != null) { + if (startPosition.getDistance(finishPosition).getKilometers() <= ReverseGeocoder.POSITION_CACHE_DISTANCE_LIMIT_IN_KM) { + finishBest = startBest; + } else { + try { + // Get distance to nearest placemark and calculate the search radius + Placemark finishNearest = ReverseGeocoder.INSTANCE.getPlacemarkNearest(finishPosition); + Distance finishNearestDistance = finishNearest.distanceFrom(finishPosition); + double finishRadius = finishNearestDistance.getKilometers() * radiusCalculationFactor; + + // Get the estimated best finish place + finishBest = ReverseGeocoder.INSTANCE.getPlacemarkLast(finishPosition, finishRadius, + new Placemark.ByPopulationDistanceRatio(finishPosition)); + } catch (IOException e) { + logger.throwing(TrackedRaceImpl.class.getName(), "getPlaceOrder()", e); + } catch (org.json.simple.parser.ParseException e) { + logger.throwing(TrackedRaceImpl.class.getName(), "getPlaceOrder()", e); + } + } + } + Pair placemarks = new Pair(startBest, finishBest); + return placemarks; + } + + @Override + public void updateLeaderboard(String leaderboardName, String newLeaderboardName, int[] newDiscardingThresholds) { + getService().apply(new UpdateLeaderboard(leaderboardName, newLeaderboardName, newDiscardingThresholds)); + } + + @Override + public void removeLeaderboard(String leaderboardName) { + getService().apply(new RemoveLeaderboard(leaderboardName)); + } + + @Override + public void renameLeaderboard(String leaderboardName, String newLeaderboardName) { + getService().apply(new RenameLeaderboard(leaderboardName, newLeaderboardName)); + } + + @Override + public void addColumnToLeaderboard(String columnName, String leaderboardName, boolean medalRace) { + getService().apply(new AddColumnToLeaderboard(columnName, leaderboardName, medalRace)); + } + + @Override + public void addColumnsToLeaderboard(String leaderboardName, List> columnsToAdd) { + for(Pair columnToAdd: columnsToAdd) { + getService().apply(new AddColumnToLeaderboard(columnToAdd.getA(), leaderboardName, columnToAdd.getB())); + } + } + + @Override + public void removeLeaderboardColumns(String leaderboardName, List columnsToRemove) { + for (String columnToRemove : columnsToRemove) { + getService().apply(new RemoveLeaderboardColumn(columnToRemove, leaderboardName)); + } + } + + @Override + public void removeLeaderboardColumn(String leaderboardName, String columnName) { + getService().apply(new RemoveLeaderboardColumn(columnName, leaderboardName)); + } + + @Override + public void renameLeaderboardColumn(String leaderboardName, String oldColumnName, String newColumnName) { + getService().apply(new RenameLeaderboardColumn(leaderboardName, oldColumnName, newColumnName)); + } + + @Override + public void updateLeaderboardColumnFactor(String leaderboardName, String columnName, Double newFactor) { + getService().apply(new UpdateLeaderboardColumnFactor(leaderboardName, columnName, newFactor)); + } + + @Override + public void suppressCompetitorInLeaderboard(String leaderboardName, String competitorIdAsString, boolean suppressed) { + getService().apply(new SetSuppressedFlagForCompetitorInLeaderboard(leaderboardName, competitorIdAsString, suppressed)); + } + + @Override + public boolean connectTrackedRaceToLeaderboardColumn(String leaderboardName, String raceColumnName, + String fleetName, RegattaAndRaceIdentifier raceIdentifier) { + return getService().apply(new ConnectTrackedRaceToLeaderboardColumn(leaderboardName, raceColumnName, fleetName, raceIdentifier)); + } + + @Override + public Map getRegattaAndRaceNameOfTrackedRaceConnectedToLeaderboardColumn(String leaderboardName, String raceColumnName) { + Map result = new HashMap(); + Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName); + if (leaderboard != null) { + RaceColumn raceColumn = leaderboard.getRaceColumnByName(raceColumnName); + if (raceColumn != null) { + for (Fleet fleet : raceColumn.getFleets()) { + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + result.put(fleet.getName(), trackedRace.getRaceIdentifier()); + } else { + result.put(fleet.getName(), null); + } + } + } + } + return result; + } + + @Override + public void disconnectLeaderboardColumnFromTrackedRace(String leaderboardName, String raceColumnName, String fleetName) { + getService().apply(new DisconnectLeaderboardColumnFromTrackedRace(leaderboardName, raceColumnName, fleetName)); + } + + @Override + public void updateLeaderboardCarryValue(String leaderboardName, String competitorIdAsString, Double carriedPoints) { + getService().apply(new UpdateLeaderboardCarryValue(leaderboardName, competitorIdAsString, carriedPoints)); + } + + @Override + public Triple updateLeaderboardMaxPointsReason(String leaderboardName, String competitorIdAsString, String raceColumnName, + MaxPointsReason maxPointsReason, Date date) throws NoWindException { + return getService().apply( + new UpdateLeaderboardMaxPointsReason(leaderboardName, raceColumnName, competitorIdAsString, + maxPointsReason, new MillisecondsTimePoint(date))); + } + + @Override + public Triple updateLeaderboardScoreCorrection(String leaderboardName, + String competitorIdAsString, String columnName, Double correctedScore, Date date) throws NoWindException { + return getService().apply( + new UpdateLeaderboardScoreCorrection(leaderboardName, columnName, competitorIdAsString, correctedScore, + new MillisecondsTimePoint(date))); + } + + @Override + public Void updateLeaderboardScoreCorrectionMetadata(String leaderboardName, Date timePointOfLastCorrectionValidity, String comment) { + return getService().apply( + new UpdateLeaderboardScoreCorrectionMetadata(leaderboardName, + timePointOfLastCorrectionValidity == null ? null : new MillisecondsTimePoint(timePointOfLastCorrectionValidity), + comment)); + } + + @Override + public void updateLeaderboardScoreCorrectionsAndMaxPointsReasons(BulkScoreCorrectionDTO updates) throws NoWindException { + Date dateForResults = new Date(); // we don't care about the result date/time here; use current date as default + for (Map.Entry> e : updates.getScoreUpdatesForRaceColumnByCompetitorIdAsString().entrySet()) { + for (Map.Entry raceColumnNameAndCorrectedScore : e.getValue().entrySet()) { + updateLeaderboardScoreCorrection(updates.getLeaderboardName(), e.getKey(), + raceColumnNameAndCorrectedScore.getKey(), raceColumnNameAndCorrectedScore.getValue(), dateForResults); + } + } + for (Map.Entry> e : updates.getMaxPointsUpdatesForRaceColumnByCompetitorIdAsString().entrySet()) { + for (Map.Entry raceColumnNameAndNewMaxPointsReason : e.getValue().entrySet()) { + updateLeaderboardMaxPointsReason(updates.getLeaderboardName(), e.getKey(), + raceColumnNameAndNewMaxPointsReason.getKey(), raceColumnNameAndNewMaxPointsReason.getValue(), dateForResults); + } + } + } + + @Override + public void updateCompetitorDisplayNameInLeaderboard(String leaderboardName, String competitorIdAsString, String displayName) { + getService().apply(new UpdateCompetitorDisplayNameInLeaderboard(leaderboardName, competitorIdAsString, displayName)); + } + + @Override + public void moveLeaderboardColumnUp(String leaderboardName, String columnName) { + getService().apply(new MoveLeaderboardColumnUp(leaderboardName, columnName)); + } + + @Override + public void moveLeaderboardColumnDown(String leaderboardName, String columnName) { + getService().apply(new MoveLeaderboardColumnDown(leaderboardName, columnName)); + } + + @Override + public void updateIsMedalRace(String leaderboardName, String columnName, boolean isMedalRace) { + getService().apply(new UpdateIsMedalRace(leaderboardName, columnName, isMedalRace)); + } + + @Override + public void updateRaceDelayToLive(RegattaAndRaceIdentifier regattaAndRaceIdentifier, long delayToLiveInMs) { + getService().apply(new UpdateRaceDelayToLive(regattaAndRaceIdentifier, delayToLiveInMs)); + } + + @Override + public void updateRacesDelayToLive(List regattaAndRaceIdentifiers, long delayToLiveInMs) { + for (RegattaAndRaceIdentifier regattaAndRaceIdentifier : regattaAndRaceIdentifiers) { + getService().apply(new UpdateRaceDelayToLive(regattaAndRaceIdentifier, delayToLiveInMs)); + } + } + + @Override + public List getPreviousSwissTimingConfigurations() { + Iterable configs = swissTimingAdapterPersistence.getSwissTimingConfigurations(); + List result = new ArrayList(); + for (SwissTimingConfiguration stConfig : configs) { + result.add(new SwissTimingConfigurationDTO(stConfig.getName(), stConfig.getHostname(), stConfig.getPort(), stConfig.canSendRequests())); + } + return result; + } + + @Override + public List listSwissTimingRaces(String hostname, int port, boolean canSendRequests) + throws UnknownHostException, IOException, InterruptedException, ParseException { + List result = new ArrayList(); + for (com.sap.sailing.domain.swisstimingadapter.RaceRecord rr : getService().getSwissTimingRaceRecords(hostname, port, canSendRequests)) { + result.add(new SwissTimingRaceRecordDTO(rr.getRaceID(), rr.getDescription(), rr.getStartTime())); + } + return result; + } + + @Override + public void storeSwissTimingConfiguration(String configName, String hostname, int port, boolean canSendRequests) { + swissTimingAdapterPersistence.storeSwissTimingConfiguration(swissTimingFactory.createSwissTimingConfiguration(configName, hostname, port, canSendRequests)); + } + + @Override + public void trackWithSwissTiming(RegattaIdentifier regattaToAddTo, SwissTimingRaceRecordDTO rr, String hostname, int port, + boolean canSendRequests, boolean trackWind, final boolean correctWindByDeclination) throws Exception { + final RacesHandle raceHandle = getService().addSwissTimingRace(regattaToAddTo, rr.ID, hostname, port, + canSendRequests, + MongoWindStoreFactory.INSTANCE.getMongoWindStore(mongoObjectFactory, domainObjectFactory), + RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS); + if (trackWind) { + new Thread("Wind tracking starter for race "+rr.ID+"/"+rr.description) { + public void run() { + try { + startTrackingWind(raceHandle, correctWindByDeclination, RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }.start(); + } + } + + @Override + public void sendSwissTimingDummyRace(String racMessage, String stlMesssage, String ccgMessage) { + getService().storeSwissTimingDummyRace(racMessage,stlMesssage,ccgMessage); + } + + @Override + public List listSwissTiminigReplayRaces(String swissTimingUrl) { + List replayRaces = SwissTimingReplayServiceFactory.INSTANCE + .createSwissTimingReplayService().listReplayRaces(swissTimingUrl); + List result = new ArrayList(replayRaces.size()); + for (SwissTimingReplayRace replayRace : replayRaces) { + result.add(new SwissTimingReplayRaceDTO(replayRace.getFlightNumber(), replayRace.getRaceId(), replayRace.getRsc(), replayRace.getName(), replayRace.getBoatClass(), replayRace.getStartTime(), replayRace.getLink())); + } + return result; + } + + @Override + public void replaySwissTimingRace(RegattaIdentifier regattaIdentifier, SwissTimingReplayRaceDTO replayRaceDTO, + boolean trackWind, boolean correctWindByDeclination, boolean simulateWithStartTimeNow) { + Regatta regatta; + if (regattaIdentifier == null) { + String boatClass = replayRaceDTO.boat_class; + for (String genderIndicator : new String[] { "Man", "Woman", "Men", "Women", "M", "W" }) { + Pattern p = Pattern.compile("(( - )|-| )"+genderIndicator+"$"); + Matcher m = p.matcher(boatClass.trim()); + if (m.find()) { + boatClass = boatClass.trim().substring(0, m.start(1)); + break; + } + } + regatta = getService().createRegatta(replayRaceDTO.rsc, boatClass.trim(), + RegattaImpl.getFullName(replayRaceDTO.rsc, replayRaceDTO.boat_class), + Collections.singletonList(new SeriesImpl("Default", /* isMedal */false, Collections + .singletonList(new FleetImpl("Default")), /* race column names */new ArrayList(), + getService())), + false, baseDomainFactory.createScoringScheme(ScoringSchemeType.LOW_POINT)); + } else { + regatta = getService().getRegatta(regattaIdentifier); + } + SwissTimingReplayService replayService = SwissTimingReplayServiceFactory.INSTANCE.createSwissTimingReplayService(); + replayService.loadRaceData(replayRaceDTO.link, getService().getSwissTimingDomainFactory(), regatta, getService()); + } + + @Override + public String[] getCountryCodes() { + List countryCodes = new ArrayList(); + for (CountryCode cc : countryCodeFactory.getAll()) { + if (cc.getThreeLetterIOCCode() != null && !cc.getThreeLetterIOCCode().equals("")) { + countryCodes.add(cc.getThreeLetterIOCCode()); + } + } + Collections.sort(countryCodes); + return countryCodes.toArray(new String[0]); + } + + /** + * Finds a competitor in a sequence of competitors that has an {@link Competitor#getId()} equal to id. + */ + private Competitor getCompetitorById(Iterable competitors, String id) { + for (Competitor c : competitors) { + if (c.getId().toString().equals(id)) { + return c; + } + } + return null; + } + + private Double getCompetitorRaceDataEntry(DetailType dataType, TrackedRace trackedRace, Competitor competitor, + TimePoint timePoint) throws NoWindException { + Double result = null; + Course course = trackedRace.getRace().getCourse(); + course.lockForRead(); // make sure the tracked leg survives this call even if a course update is pending + try { + TrackedLegOfCompetitor trackedLeg = trackedRace.getTrackedLeg(competitor, timePoint); + if (trackedLeg != null) { + switch (dataType) { + case CURRENT_SPEED_OVER_GROUND_IN_KNOTS: + SpeedWithBearing speedOverGround = trackedLeg.getSpeedOverGround(timePoint); + result = (speedOverGround == null) ? null : speedOverGround.getKnots(); + break; + case VELOCITY_MADE_GOOD_IN_KNOTS: + Speed velocityMadeGood = trackedLeg.getVelocityMadeGood(timePoint); + result = (velocityMadeGood == null) ? null : velocityMadeGood.getKnots(); + break; + case DISTANCE_TRAVELED: + Distance distanceTraveled = trackedRace.getDistanceTraveled(competitor, timePoint); + result = distanceTraveled == null ? null : distanceTraveled.getMeters(); + break; + case GAP_TO_LEADER_IN_SECONDS: + result = trackedLeg.getGapToLeaderInSeconds(timePoint); + break; + case WINDWARD_DISTANCE_TO_OVERALL_LEADER: + Distance distanceToLeader = trackedLeg.getWindwardDistanceToOverallLeader(timePoint); + result = (distanceToLeader == null) ? null : distanceToLeader.getMeters(); + break; + case RACE_RANK: + result = (double) trackedLeg.getRank(timePoint); + break; + default: + throw new UnsupportedOperationException("Theres currently no support for the enum value '" + dataType + "' in this method."); + } + } + return result; + } finally { + course.unlockAfterRead(); + } + } + + @Override + public CompetitorsRaceDataDTO getCompetitorsRaceData(RegattaAndRaceIdentifier race, List competitors, Date from, Date to, + long stepSizeInMs, DetailType detailType) throws NoWindException { + CompetitorsRaceDataDTO result = null; + TrackedRace trackedRace = getExistingTrackedRace(race); + if (trackedRace != null) { + TimePoint newestEvent = trackedRace.getTimePointOfNewestEvent(); + TimePoint startTime = from == null ? trackedRace.getStartOfTracking() : new MillisecondsTimePoint(from); + TimePoint endTime = (to == null || to.after(newestEvent.asDate())) ? newestEvent : new MillisecondsTimePoint(to); + result = new CompetitorsRaceDataDTO(detailType, startTime.asDate(), endTime.asDate()); + + for (CompetitorDTO competitorDTO : competitors) { + Competitor competitor = getCompetitorById(trackedRace.getRace().getCompetitors(), competitorDTO.id); + ArrayList> markPassingsData = new ArrayList>(); + ArrayList> raceData = new ArrayList>(); + // Filling the mark passings + Set competitorMarkPassings = trackedRace.getMarkPassings(competitor); + if (competitorMarkPassings != null) { + trackedRace.lockForRead(competitorMarkPassings); + try { + for (MarkPassing markPassing : competitorMarkPassings) { + MillisecondsTimePoint time = new MillisecondsTimePoint(markPassing.getTimePoint().asMillis()); + markPassingsData.add(new Triple(markPassing.getWaypoint().getName(), + time.asDate(), + getCompetitorRaceDataEntry(detailType, trackedRace, competitor, time))); + } + } finally { + trackedRace.unlockAfterRead(competitorMarkPassings); + } + } + if (startTime != null && endTime != null) { + for (long i = startTime.asMillis(); i <= endTime.asMillis(); i += stepSizeInMs) { + MillisecondsTimePoint time = new MillisecondsTimePoint(i); + raceData.add(new Pair(time.asDate(), getCompetitorRaceDataEntry(detailType, trackedRace, + competitor, time))); + } + } + result.setCompetitorData(competitorDTO, new CompetitorRaceDataDTO(competitorDTO, detailType, markPassingsData, raceData)); + } + } + return result; + } + + @Override + public Map> getDouglasPoints(RegattaAndRaceIdentifier raceIdentifier, + Map from, Map to, + double meters) throws NoWindException { + Map> result = new HashMap>(); + TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); + if (trackedRace != null) { + final WindSource windSource = new WindSourceImpl(WindSourceType.COMBINED); + MeterDistance maxDistance = new MeterDistance(meters); + for (Competitor competitor : trackedRace.getRace().getCompetitors()) { + CompetitorDTO competitorDTO = convertToCompetitorDTO(competitor); + if (from.containsKey(competitorDTO)) { + // get Track of competitor + GPSFixTrack gpsFixTrack = trackedRace.getTrack(competitor); + // Distance for DouglasPeucker + TimePoint timePointFrom = new MillisecondsTimePoint(from.get(competitorDTO)); + TimePoint timePointTo = new MillisecondsTimePoint(to.get(competitorDTO)); + List gpsFixApproximation = trackedRace.approximate(competitor, maxDistance, + timePointFrom, timePointTo); + List gpsFixDouglasList = new ArrayList(); + for (int i = 0; i < gpsFixApproximation.size(); i++) { + GPSFix fix = gpsFixApproximation.get(i); + SpeedWithBearing speedWithBearing; + if (i < gpsFixApproximation.size() - 1) { + GPSFix next = gpsFixApproximation.get(i + 1); + Bearing bearing = fix.getPosition().getBearingGreatCircle(next.getPosition()); + Speed speed = fix.getPosition().getDistance(next.getPosition()) + .inTime(next.getTimePoint().asMillis() - fix.getTimePoint().asMillis()); + speedWithBearing = new KnotSpeedWithBearingImpl(speed.getKnots(), bearing); + } else { + speedWithBearing = gpsFixTrack.getEstimatedSpeed(fix.getTimePoint()); + } + Tack tack = trackedRace.getTack(competitor, fix.getTimePoint()); + TrackedLegOfCompetitor trackedLegOfCompetitor = trackedRace.getTrackedLeg(competitor, fix.getTimePoint()); + LegType legType = trackedLegOfCompetitor == null ? null : trackedRace.getTrackedLeg( + trackedLegOfCompetitor.getLeg()).getLegType(fix.getTimePoint()); + Wind wind = trackedRace.getWind(fix.getPosition(), fix.getTimePoint()); + WindDTO windDTO = createWindDTOFromAlreadyAveraged(wind, trackedRace.getOrCreateWindTrack(windSource), fix.getTimePoint()); + GPSFixDTO fixDTO = createGPSFixDTO(fix, speedWithBearing, windDTO, tack, legType, /* extrapolated */false); + gpsFixDouglasList.add(fixDTO); + } + result.put(competitorDTO, gpsFixDouglasList); + } + } + } + return result; + } + + @Override + public Map> getManeuvers(RegattaAndRaceIdentifier raceIdentifier, + Map from, Map to) throws NoWindException { + Map> result = new HashMap>(); + final TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); + if (trackedRace != null) { + Map>> futures = new HashMap>>(); + for (final Competitor competitor : trackedRace.getRace().getCompetitors()) { + CompetitorDTO competitorDTO = convertToCompetitorDTO(competitor); + if (from.containsKey(competitorDTO)) { + final TimePoint timePointFrom = new MillisecondsTimePoint(from.get(competitorDTO)); + final TimePoint timePointTo = new MillisecondsTimePoint(to.get(competitorDTO)); + RunnableFuture> future = new FutureTask>( + new Callable>() { + @Override + public List call() { + List maneuversForCompetitor; + try { + maneuversForCompetitor = trackedRace.getManeuvers(competitor, timePointFrom, + timePointTo, /* waitForLatest */ true); + } catch (NoWindException e) { + throw new NoWindError(e); + } + return createManeuverDTOsForCompetitor(maneuversForCompetitor, trackedRace, + competitor); + } + }); + executor.execute(future); + futures.put(competitorDTO, future); + } + } + for (Map.Entry>> competitorAndFuture : futures.entrySet()) { + try { + result.put(competitorAndFuture.getKey(), competitorAndFuture.getValue().get()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + } + return result; + } + + private List createManeuverDTOsForCompetitor(List maneuvers, TrackedRace trackedRace, Competitor competitor) { + List result = new ArrayList(); + for (Maneuver maneuver : maneuvers) { + final ManeuverDTO maneuverDTO; + if (maneuver.getType() == ManeuverType.MARK_PASSING) { + maneuverDTO = new MarkpassingManeuverDTO(maneuver.getType(), maneuver.getNewTack(), + new PositionDTO(maneuver.getPosition().getLatDeg(), maneuver.getPosition().getLngDeg()), + maneuver.getTimePoint().asDate(), + createSpeedWithBearingDTO(maneuver.getSpeedWithBearingBefore()), + createSpeedWithBearingDTO(maneuver.getSpeedWithBearingAfter()), + maneuver.getDirectionChangeInDegrees(), maneuver.getManeuverLoss()==null?null:maneuver.getManeuverLoss().getMeters(), + ((MarkPassingManeuver) maneuver).getSide()); + } else { + maneuverDTO = new ManeuverDTO(maneuver.getType(), maneuver.getNewTack(), + new PositionDTO(maneuver.getPosition().getLatDeg(), maneuver.getPosition().getLngDeg()), + maneuver.getTimePoint().asDate(), + createSpeedWithBearingDTO(maneuver.getSpeedWithBearingBefore()), + createSpeedWithBearingDTO(maneuver.getSpeedWithBearingAfter()), + maneuver.getDirectionChangeInDegrees(), maneuver.getManeuverLoss()==null?null:maneuver.getManeuverLoss().getMeters()); + } + result.add(maneuverDTO); + } + return result; + } + + @Override + public RaceDefinition getRace(RegattaAndRaceIdentifier raceIdentifier) { + Regatta regatta = getService().getRegattaByName(raceIdentifier.getRegattaName()); + RaceDefinition race = getRaceByName(regatta, raceIdentifier.getRaceName()); + return race; + } + + @Override + public TrackedRace getTrackedRace(RegattaAndRaceIdentifier regattaNameAndRaceName) { + Regatta regatta = getService().getRegattaByName(regattaNameAndRaceName.getRegattaName()); + RaceDefinition race = getRaceByName(regatta, regattaNameAndRaceName.getRaceName()); + TrackedRace trackedRace = getService().getOrCreateTrackedRegatta(regatta).getTrackedRace(race); + return trackedRace; + } + + @Override + public TrackedRace getExistingTrackedRace(RegattaAndRaceIdentifier regattaNameAndRaceName) { + return getService().getExistingTrackedRace(regattaNameAndRaceName); + } + + @Override + public Regatta getRegatta(RegattaName regattaIdentifier) { + return getService().getRegattaByName(regattaIdentifier.getRegattaName()); + } + + /** + * Returns a servlet context that, when asked for a resource, first tries the original servlet context's implementation. If that + * fails, it prepends "war/" to the request because the war/ folder contains all the resources exposed externally + * through the HTTP server. + */ + @Override + public ServletContext getServletContext() { + return new DelegatingServletContext(super.getServletContext()); + } + + @Override + /** + * Override of function to prevent exception "Blocked request without GWT permutation header (XSRF attack?)" when testing the GWT sites + */ + protected void checkPermutationStrongName() throws SecurityException { + //Override to prevent exception "Blocked request without GWT permutation header (XSRF attack?)" when testing the GWT sites + return; + } + + @Override + public List getLeaderboardGroups(boolean withGeoLocationData) { + ArrayList leaderboardGroupDTOs = new ArrayList(); + Map leaderboardGroups = getService().getLeaderboardGroups(); + + for (LeaderboardGroup leaderboardGroup : leaderboardGroups.values()) { + leaderboardGroupDTOs.add(convertToLeaderboardGroupDTO(leaderboardGroup, withGeoLocationData)); + } + + return leaderboardGroupDTOs; + } + + @Override + public LeaderboardGroupDTO getLeaderboardGroupByName(String groupName, boolean withGeoLocationData) { + return convertToLeaderboardGroupDTO(getService().getLeaderboardGroupByName(groupName), withGeoLocationData); + } + + private LeaderboardGroupDTO convertToLeaderboardGroupDTO(LeaderboardGroup leaderboardGroup, boolean withGeoLocationData) { + LeaderboardGroupDTO groupDTO = new LeaderboardGroupDTO(); + groupDTO.name = leaderboardGroup.getName(); + groupDTO.description = leaderboardGroup.getDescription(); + groupDTO.displayLeaderboardsInReverseOrder = leaderboardGroup.isDisplayGroupsInReverseOrder(); + for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { + groupDTO.leaderboards.add(createStrippedLeaderboardDTO(leaderboard, withGeoLocationData)); + } + Leaderboard overallLeaderboard = leaderboardGroup.getOverallLeaderboard(); + if (overallLeaderboard != null) { + groupDTO.setOverallLeaderboardDiscardThresholds(overallLeaderboard.getResultDiscardingRule().getDiscardIndexResultsStartingWithHowManyRaces()); + groupDTO.setOverallLeaderboardScoringSchemeType(overallLeaderboard.getScoringScheme().getType()); + } + return groupDTO; + } + + + private PlacemarkDTO convertToPlacemarkDTO(Placemark placemark) { + Position position = placemark.getPosition(); + return new PlacemarkDTO(placemark.getName(), placemark.getCountryCode(), new PositionDTO(position.getLatDeg(), + position.getLngDeg()), placemark.getPopulation()); + } + + @Override + public void renameLeaderboardGroup(String oldName, String newName) { + getService().apply(new RenameLeaderboardGroup(oldName, newName)); + } + + @Override + public void removeLeaderboardGroup(String groupName) { + getService().apply(new RemoveLeaderboardGroup(groupName)); + } + + @Override + public LeaderboardGroupDTO createLeaderboardGroup(String groupName, String description, boolean displayGroupsInReverseOrder, + int[] overallLeaderboardDiscardThresholds, ScoringSchemeType overallLeaderboardScoringSchemeType) { + CreateLeaderboardGroup createLeaderboardGroupOp = new CreateLeaderboardGroup(groupName, description, displayGroupsInReverseOrder, + new ArrayList(), overallLeaderboardDiscardThresholds, overallLeaderboardScoringSchemeType); + return convertToLeaderboardGroupDTO(getService().apply(createLeaderboardGroupOp), false); + } + + @Override + public void updateLeaderboardGroup(String oldName, String newName, String description, List leaderboardNames, + int[] overallLeaderboardDiscardThresholds, ScoringSchemeType overallLeaderboardScoringSchemeType) { + getService().apply( + new UpdateLeaderboardGroup(oldName, newName, description, leaderboardNames, + overallLeaderboardDiscardThresholds, overallLeaderboardScoringSchemeType)); + } + + @Override + public ReplicationStateDTO getReplicaInfo() { + ReplicationService service = getReplicationService(); + Set replicaDTOs = new HashSet(); + for (ReplicaDescriptor replicaDescriptor : service.getReplicaInfo()) { + final Map>, Integer> statistics = service.getStatistics(replicaDescriptor); + Map replicationCountByOperationClassName = new HashMap(); + for (Map.Entry>, Integer> e : statistics.entrySet()) { + replicationCountByOperationClassName.put(e.getKey().getName(), e.getValue()); + } + replicaDTOs.add(new ReplicaDTO(replicaDescriptor.getIpAddress().getHostName(), replicaDescriptor.getRegistrationTime().asDate(), + replicationCountByOperationClassName)); + } + ReplicationMasterDTO master; + ReplicationMasterDescriptor replicatingFromMaster = service.isReplicatingFromMaster(); + if (replicatingFromMaster == null) { + master = null; + } else { + master = new ReplicationMasterDTO(replicatingFromMaster.getHostname(), replicatingFromMaster.getMessagingPort(), + replicatingFromMaster.getServletPort()); + } + return new ReplicationStateDTO(master, replicaDTOs); + } + + @Override + public void startReplicatingFromMaster(String masterName, String exchangeName, int servletPort, int messagingPort) throws IOException, ClassNotFoundException { + getReplicationService().startToReplicateFrom( + ReplicationFactory.INSTANCE.createReplicationMasterDescriptor(masterName, exchangeName, servletPort, messagingPort)); + } + + @Override + public List getEvents() { + List result = new ArrayList(); + for (Event event : getService().getAllEvents()) { + EventDTO eventDTO = convertToEventDTO(event); + result.add(eventDTO); + } + return result; + } + + @Override + public void updateEvent(String eventName, Serializable id, VenueDTO venue, String publicationUrl, boolean isPublic, List regattaNames) { + getService().apply(new UpdateEvent(id, eventName, venue.name, publicationUrl, isPublic, regattaNames)); + } + + @Override + public EventDTO createEvent(String eventName, String venue, String publicationUrl, boolean isPublic) { + return convertToEventDTO(getService().apply(new CreateEvent(eventName, venue, publicationUrl, isPublic, new ArrayList()))); + } + + @Override + public void removeEvent(String eventName) { + getService().apply(new RemoveEvent(eventName)); + } + + @Override + public void renameEvent(String oldName, String newName) { + getService().apply(new RenameEvent(oldName, newName)); + } + + @Override + public EventDTO getEventByName(String eventName) { + EventDTO result = null; + for (Event event : getService().getAllEvents()) { + if(event.getName().equals(eventName)) { + result = convertToEventDTO(event); + break; + } + } + return result; + } + + private EventDTO convertToEventDTO(Event event) { + EventDTO eventDTO = new EventDTO(event.getName()); + eventDTO.venue = new VenueDTO(); + eventDTO.venue.name = event.getVenue() != null ? event.getVenue().getName() : null; + eventDTO.publicationUrl = event.getPublicationUrl(); + eventDTO.isPublic = event.isPublic(); + eventDTO.id = event.getId(); + eventDTO.regattas = new ArrayList(); + for (Regatta regatta: event.getRegattas()) { + RegattaDTO regattaDTO = new RegattaDTO(); + regattaDTO.name = regatta.getName(); + eventDTO.regattas.add(regattaDTO); + } + return eventDTO; + } + + @Override + public void removeRegatta(RegattaIdentifier regattaIdentifier) { + getService().apply(new RemoveRegatta(regattaIdentifier)); + } + + private RaceColumnInSeriesDTO convertToRaceColumnInSeriesDTO(RaceColumnInSeries raceColumnInSeries) { + RaceColumnInSeriesDTO raceColumnInSeriesDTO = new RaceColumnInSeriesDTO(raceColumnInSeries.getSeries().getName(), + raceColumnInSeries.getRegatta().getName()); + raceColumnInSeriesDTO.name = raceColumnInSeries.getName(); + raceColumnInSeriesDTO.setMedalRace(raceColumnInSeries.isMedalRace()); + return raceColumnInSeriesDTO; + } + + @Override + public List addRaceColumnsToSeries(RegattaIdentifier regattaIdentifier, String seriesName, List columnNames) { + List result = new ArrayList(); + for(String columnName: columnNames) { + RaceColumnInSeries raceColumnInSeries = getService().apply(new AddColumnToSeries(regattaIdentifier, seriesName, columnName)); + if(raceColumnInSeries != null) { + result.add(convertToRaceColumnInSeriesDTO(raceColumnInSeries)); + } + } + return result; + } + + @Override + public RaceColumnInSeriesDTO addRaceColumnToSeries(RegattaIdentifier regattaIdentifier, String seriesName, String columnName) { + RaceColumnInSeriesDTO result = null; + RaceColumnInSeries raceColumnInSeries = getService().apply(new AddColumnToSeries(regattaIdentifier, seriesName, columnName)); + if(raceColumnInSeries != null) { + result = convertToRaceColumnInSeriesDTO(raceColumnInSeries); + } + return result; + } + + @Override + public void removeRaceColumnsFromSeries(RegattaIdentifier regattaIdentifier, String seriesName, List columnNames) { + for(String columnName: columnNames) { + getService().apply(new RemoveColumnFromSeries(regattaIdentifier, seriesName, columnName)); + } + } + + @Override + public void removeRaceColumnFromSeries(RegattaIdentifier regattaIdentifier, String seriesName, String columnName) { + getService().apply(new RemoveColumnFromSeries(regattaIdentifier, seriesName, columnName)); + } + + @Override + public void moveRaceColumnInSeriesUp(RegattaIdentifier regattaIdentifier, String seriesName, String columnName) { + getService().apply(new MoveColumnInSeriesUp(regattaIdentifier, seriesName, columnName)); + } + + @Override + public void moveRaceColumnInSeriesDown(RegattaIdentifier regattaIdentifier, String seriesName, String columnName) { + getService().apply(new MoveColumnInSeriesDown(regattaIdentifier, seriesName, columnName)); + } + + @Override + public RegattaDTO createRegatta(String regattaName, String boatClassName, + LinkedHashMap>, Boolean>> seriesNamesWithFleetNamesAndFleetOrderingAndMedal, + boolean persistent, ScoringSchemeType scoringSchemeType) { + Regatta regatta = getService().apply( + new AddSpecificRegatta(regattaName, boatClassName, UUID.randomUUID(), + seriesNamesWithFleetNamesAndFleetOrderingAndMedal, + persistent, baseDomainFactory.createScoringScheme(scoringSchemeType))); + return convertToRegattaDTO(regatta); + } + + @Override + public RegattaScoreCorrectionDTO getScoreCorrections(String scoreCorrectionProviderName, String eventName, + String boatClassName, Date timePointWhenResultPublished) throws Exception { + RegattaScoreCorrectionDTO result = null; + for (ScoreCorrectionProvider scp : getScoreCorrectionProviders()) { + if (scp.getName().equals(scoreCorrectionProviderName)) { + result = createScoreCorrection(scp.getScoreCorrections(eventName, boatClassName, + new MillisecondsTimePoint(timePointWhenResultPublished))); + break; + } + } + return result; + } + + private RegattaScoreCorrectionDTO createScoreCorrection(RegattaScoreCorrections scoreCorrections) { + // Key is the race name or number as String; values are maps whose key is the sailID. + LinkedHashMap> map = new LinkedHashMap>(); + for (ScoreCorrectionsForRace sc4r : scoreCorrections.getScoreCorrectionsForRaces()) { + Map entryMap = new HashMap(); + for (String sailID : sc4r.getSailIDs()) { + entryMap.put(sailID, createScoreCorrectionEntryDTO(sc4r.getScoreCorrectionForCompetitor(sailID))); + } + map.put(sc4r.getRaceNameOrNumber(), entryMap); + } + return new RegattaScoreCorrectionDTO(scoreCorrections.getProvider().getName(), map); + } + + private ScoreCorrectionEntryDTO createScoreCorrectionEntryDTO( + ScoreCorrectionForCompetitorInRace scoreCorrectionForCompetitor) { + return new ScoreCorrectionEntryDTO(scoreCorrectionForCompetitor.getPoints(), + scoreCorrectionForCompetitor.isDiscarded(), scoreCorrectionForCompetitor.getMaxPointsReason()); + } + + private FregResultProvider getFregService() { + FregResultProvider result = null; + for (ScoreCorrectionProvider scp : getScoreCorrectionProviders()) { + if (scp instanceof FregResultProvider) { + result = (FregResultProvider) scp; + break; + } + } + return result; + } + + @Override + public List getFregResultUrls() { + List result = new ArrayList(); + final FregResultProvider fregService = getFregService(); + if (fregService != null) { + Iterable allUrls = fregService.getAllUrls(); + for (URL url : allUrls) { + result.add(url.toString()); + } + } + return result; + } + + @Override + public void removeFregURLs(Set toRemove) throws Exception { + FregResultProvider fregService = getFregService(); + if (fregService != null) { + for (String urlToRemove : toRemove) { + fregService.removeResultUrl(new URL(urlToRemove)); + } + } + } + + @Override + public void addFragUrl(String result) throws Exception { + FregResultProvider fregService = getFregService(); + if (fregService != null) { + fregService.registerResultUrl(new URL(result)); + } + } + + @Override + public List>> getRankedCompetitorsFromBestToWorstAfterEachRaceColumn(String leaderboardName, Date date) throws NoWindException { + Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName); + final TimePoint timePoint; + if (date == null) { + final TimePoint nowMinusDelay = leaderboard.getNowMinusDelay(); + final TimePoint timePointOfLatestModification = leaderboard.getTimePointOfLatestModification(); + if (timePointOfLatestModification != null && !nowMinusDelay.before(timePointOfLatestModification)) { + timePoint = timePointOfLatestModification; + } else { + timePoint = nowMinusDelay; + } + } else { + timePoint = new MillisecondsTimePoint(date); + } + Map> preResult = leaderboard + .getRankedCompetitorsFromBestToWorstAfterEachRaceColumn(timePoint); + List>> result = new ArrayList>>(); + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + List competitorList = getCompetitorDTOList(preResult.get(raceColumn)); + result.add(new Pair>(raceColumn.getName(), competitorList)); + } + return result; + } + + @Override + public List getOverallLeaderboardNamesContaining(String leaderboardName) { + Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName); + if (leaderboard == null) { + throw new IllegalArgumentException("Couldn't find leaderboard named "+leaderboardName); + } + List result = new ArrayList(); + for (Map.Entry leaderboardEntry : getService().getLeaderboards().entrySet()) { + if (leaderboardEntry.getValue() instanceof MetaLeaderboard) { + MetaLeaderboard metaLeaderboard = (MetaLeaderboard) leaderboardEntry.getValue(); + if (Util.contains(metaLeaderboard.getLeaderboards(), leaderboard)) { + result.add(leaderboardEntry.getKey()); + } + } + } + return result; + } + + @Override + public List getPreviousSwissTimingArchiveConfigurations() { + Iterable configs = swissTimingAdapterPersistence.getSwissTimingArchiveConfigurations(); + List result = new ArrayList(); + for (SwissTimingArchiveConfiguration stArchiveConfig : configs) { + result.add(new SwissTimingArchiveConfigurationDTO(stArchiveConfig.getJsonUrl())); + } + return result; + } + + @Override + public void storeSwissTimingArchiveConfiguration(String swissTimingJsonUrl) { + swissTimingAdapterPersistence.storeSwissTimingArchiveConfiguration(swissTimingFactory.createSwissTimingArchiveConfiguration( + swissTimingJsonUrl)); + } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/AbstractChartPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/AbstractChartPanel.java index 8387dfed970..6624b4bb8ec 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/AbstractChartPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/charts/AbstractChartPanel.java @@ -1,628 +1,630 @@ -package com.sap.sailing.gwt.ui.shared.charts; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.moxieapps.gwt.highcharts.client.Axis; -import org.moxieapps.gwt.highcharts.client.Chart; -import org.moxieapps.gwt.highcharts.client.ChartSubtitle; -import org.moxieapps.gwt.highcharts.client.ChartTitle; -import org.moxieapps.gwt.highcharts.client.Color; -import org.moxieapps.gwt.highcharts.client.Credits; -import org.moxieapps.gwt.highcharts.client.Point; -import org.moxieapps.gwt.highcharts.client.Series; -import org.moxieapps.gwt.highcharts.client.ToolTip; -import org.moxieapps.gwt.highcharts.client.ToolTipData; -import org.moxieapps.gwt.highcharts.client.ToolTipFormatter; -import org.moxieapps.gwt.highcharts.client.events.ChartClickEvent; -import org.moxieapps.gwt.highcharts.client.events.ChartClickEventHandler; -import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; -import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEventHandler; -import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsData; -import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsFormatter; -import org.moxieapps.gwt.highcharts.client.labels.XAxisLabels; -import org.moxieapps.gwt.highcharts.client.plotOptions.LinePlotOptions; -import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; -import org.moxieapps.gwt.highcharts.client.plotOptions.ScatterPlotOptions; - -import com.google.gwt.i18n.client.NumberFormat; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.RequiresResize; -import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.domain.common.DetailType; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -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.gwt.ui.actions.AsyncActionsExecutor; -import com.sap.sailing.gwt.ui.actions.GetCompetitorsRaceDataAction; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; -import com.sap.sailing.gwt.ui.client.DetailTypeFormatter; -import com.sap.sailing.gwt.ui.client.ErrorReporter; -import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.TimeRangeWithZoomProvider; -import com.sap.sailing.gwt.ui.client.Timer; -import com.sap.sailing.gwt.ui.client.Timer.PlayModes; -import com.sap.sailing.gwt.ui.shared.CompetitorDTO; -import com.sap.sailing.gwt.ui.shared.CompetitorRaceDataDTO; -import com.sap.sailing.gwt.ui.shared.CompetitorsRaceDataDTO; -import com.sap.sailing.gwt.ui.shared.components.Component; - -/** - * ChartPanel is a GWT panel that can show one sort of competitor data (e.g. current speed over ground, windward - * distance to leader) for different races in a chart. - * - * When calling the constructor a chart is created that creates a final amount of series (so the maximum number of - * competitors cannot be changed in one chart) which are connected to competitors, when the sailing service returns the - * data. So {@code seriesID, competitorID and markSeriesID} are linked with the index. So if u know for example the - * seriesID-index, you can get the competitor by calling competitorID.get(index). - * - * @author Benjamin Ebling (D056866), Axel Uhl (d043530) - * - */ -public abstract class AbstractChartPanel extends RaceChart implements - CompetitorSelectionChangeListener, RequiresResize { - private static final int LINE_WIDTH = 1; - private static final int MAX_SERIES_POINTS = 10000; - private boolean compactChart; - private final Label noCompetitorsSelectedLabel; - private final Map dataSeriesByCompetitor; - private final Map markPassingSeriesByCompetitor; - private final boolean allowTimeAdjust; - private long stepSize = 5000; - private DetailType selectedDetailType; - private final CompetitorSelectionProvider competitorSelectionProvider; - - private Long timeOfEarliestRequestInMillis; - private Long timeOfLatestRequestInMillis; - - public AbstractChartPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, - CompetitorSelectionProvider competitorSelectionProvider, RaceSelectionProvider raceSelectionProvider, - Timer timer, TimeRangeWithZoomProvider timeRangeWithZoomProvider, final StringMessages stringMessages, - ErrorReporter errorReporter, DetailType detailType, boolean compactChart, boolean allowTimeAdjust) { - super(sailingService, timer, timeRangeWithZoomProvider, stringMessages, asyncActionsExecutor, errorReporter); - this.competitorSelectionProvider = competitorSelectionProvider; - this.compactChart = compactChart; - this.allowTimeAdjust = allowTimeAdjust; - dataSeriesByCompetitor = new HashMap(); - markPassingSeriesByCompetitor = new HashMap(); - setSize("100%", "100%"); - noCompetitorsSelectedLabel = new Label(stringMessages.selectAtLeastOneCompetitor() + "."); - noCompetitorsSelectedLabel.setStyleName("abstractChartPanel-importantMessageOfChart"); - createChart(); - setSelectedDetailType(detailType); - competitorSelectionProvider.addCompetitorSelectionChangeListener(this); - raceSelectionProvider.addRaceSelectionChangeListener(this); - } - - /** - * Creates a new chart for the given {@link DetailType} dataToShow and also clears the - * {@link #chartData}, the {@link #dataSeriesByCompetitor} and the {@link #markPassingSeriesByCompetitor}. - * - * @param detailType - * The detail type for the new chart. - * @return A chart for the given detail Type - */ - private Chart createChart() { - Chart chart = new Chart().setZoomType(Chart.ZoomType.X) - .setPersistent(true) - .setWidth100() - .setHeight100() - .setMarginLeft(65) - .setMarginRight(65) - .setBorderColor(new Color("#CACACA")) - .setBackgroundColor(new Color("#EBEBEB")) - .setBorderWidth(0) - .setBorderRadius(0) - .setPlotBorderWidth(0) - .setCredits(new Credits().setEnabled(false)) - .setChartSubtitle(new ChartSubtitle().setText(stringMessages.clickAndDragToZoomIn())) - .setLinePlotOptions( - new LinePlotOptions() - .setLineWidth(LINE_WIDTH) - .setMarker( - new Marker().setEnabled(false).setHoverState( - new Marker().setEnabled(true).setRadius(4))).setShadow(false) - .setHoverStateLineWidth(LINE_WIDTH)); - ChartUtil.useCheckboxesToShowAndHide(chart); - - if (allowTimeAdjust) { - chart.setClickEventHandler(new ChartClickEventHandler() { - @Override - public boolean onClick(ChartClickEvent chartClickEvent) { - return AbstractChartPanel.this.onClick(chartClickEvent); - } - }); - chart.setSelectionEventHandler(new ChartSelectionEventHandler() { - @Override - public boolean onSelection(ChartSelectionEvent chartSelectionEvent) { - return AbstractChartPanel.this.onXAxisSelectionChange(chartSelectionEvent); - } - }); - } - - chart.getXAxis().setType(Axis.Type.DATE_TIME).setMaxZoom(60 * 1000); // 1 minute - chart.getYAxis().setStartOnTick(false).setShowFirstLabel(false); - chart.getXAxis().setLabels(new XAxisLabels().setFormatter(new AxisLabelsFormatter() { - @Override - public String format(AxisLabelsData axisLabelsData) { - return dateFormatHoursMinutes.format(new Date(axisLabelsData.getValueAsLong())); - } - })); - - if (compactChart) { - chart.setSpacingBottom(10).setSpacingLeft(10).setSpacingRight(10).setSpacingTop(2) - .setOption("legend/margin", 2).setOption("title/margin", 5).setChartSubtitle(null).getXAxis() - .setAxisTitleText(null); - } - - return chart; - } - - protected abstract Component getComponent(); - - /** - * Loads the needed data (data which isn't in the {@link #chartData cache}) for the - * {@link #getSelectedCompetitors() visible competitors} via - * {@link SailingServiceAsync#getCompetitorsRaceData(RaceIdentifier, List, long, DetailType, AsyncCallback)}. After - * loading is the method {@link #drawChartData()} called.
- * If no data needs to be {@link #needsDataLoading() loaded}, the no competitors selected label is displayed. - */ - private void updateChart(Date from, Date to, boolean append) { - if (hasSelectedCompetitors()) { - setWidget(chart); - - ArrayList competitorsToLoad = new ArrayList(); - for (CompetitorDTO competitorDTO : getSelectedCompetitors()) { - competitorsToLoad.add(competitorDTO); - } - - loadData(from, to, competitorsToLoad, append); - } else { - setWidget(noCompetitorsSelectedLabel); - } - } - - private void loadData(final Date from, final Date to, final List competitors, final boolean append) { - showLoading(stringMessages.loadingCompetitorData()); - ArrayList competitorsToLoad = new ArrayList(); - for (CompetitorDTO competitorDTO : competitors) { - competitorsToLoad.add(competitorDTO); - } - - GetCompetitorsRaceDataAction getCompetitorsRaceDataAction = new GetCompetitorsRaceDataAction(sailingService, - selectedRaceIdentifier, competitorsToLoad, from, to, getStepSize(), - getSelectedDetailType(), new AsyncCallback() { - @Override - public void onSuccess(final CompetitorsRaceDataDTO result) { - hideLoading(); - - if (result != null) { - updateChartSeries(result, append); - } else { - if (!append) { - clearChart(); - } - } - } - - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError( - getStringMessages().failedToLoadRaceData() + ": " + caught.toString(), - timer.getPlayMode() == PlayModes.Live); - hideLoading(); - } - }); - asyncActionsExecutor.execute(getCompetitorsRaceDataAction); - - } - - @Override - public void addedToSelection(CompetitorDTO competitor) { - if (isVisible()) { - ArrayList competitorsToLoad = new ArrayList(); - competitorsToLoad.add(competitor); - Date fromDate = timeOfEarliestRequestInMillis == null ? null : new Date(timeOfEarliestRequestInMillis); - Date toDate = timeOfLatestRequestInMillis == null ? null : new Date(timeOfLatestRequestInMillis); - loadData(fromDate, toDate, competitorsToLoad, false); - } - } - - @Override - public void removedFromSelection(CompetitorDTO competitor) { - Series competitorSeries = dataSeriesByCompetitor.get(competitor); - if (competitorSeries != null) { - chart.removeSeries(competitorSeries, false); - } - Series competitorMarkPassingSeries = markPassingSeriesByCompetitor.get(competitor); - if (competitorMarkPassingSeries != null) { - chart.removeSeries(competitorMarkPassingSeries, false); - } - - if (isVisible()) { - if(hasSelectedCompetitors()) { - chart.redraw(); - } else { - setWidget(noCompetitorsSelectedLabel); - } - } - } - - /** - * Creates the series for all selected competitors if these aren't created yet.
- * Fills the series for the selected competitors with the data in {@link AbstractChartPanel#chartData}.
- */ - private synchronized void updateChartSeries(CompetitorsRaceDataDTO chartData, boolean append) { - // Make sure the busy indicator is removed at this point, or plotting the data results in an exception - setWidget(chart); - - List chartSeries = Arrays.asList(chart.getSeries()); - for (CompetitorDTO competitor : chartData.getCompetitors()) { - Series competitorDataSeries = getOrCreateCompetitorDataSeries(competitor); - Series markPassingSeries = getOrCreateCompetitorMarkPassingSeries(competitor); - CompetitorRaceDataDTO competitorData = chartData.getCompetitorData(competitor); - if (competitorData != null) { - Date toDate = new Date(System.currentTimeMillis() - timer.getLivePlayDelayInMillis()); - - List> markPassingsData = competitorData.getMarkPassingsData(); - List markPassingPoints = new ArrayList(); - for (Triple markPassingData : markPassingsData) { - if (markPassingData.getB() != null && markPassingData.getC() != null) { - if (markPassingData.getB().before(toDate)) { - Point markPassingPoint = new Point(markPassingData.getB().getTime(), - markPassingData.getC()); - markPassingPoint.setName(markPassingData.getA()); - markPassingPoints.add(markPassingPoint); - } - } - } - markPassingSeries.setPoints(markPassingPoints.toArray(new Point[0]), false); - - Point[] oldRaceDataPoints = competitorDataSeries.getPoints(); - List> raceData = competitorData.getRaceData(); - - Point[] raceDataPointsToAdd = new Point[raceData.size()]; - int currentPointIndex = 0; - for (Pair raceDataPoint : raceData) { - Double dataPointValue = raceDataPoint.getB(); - if(dataPointValue != null) { - long dataPointTimeAsMillis = raceDataPoint.getA().getTime(); - if(append == false || (timeOfEarliestRequestInMillis == null || dataPointTimeAsMillis < timeOfEarliestRequestInMillis) || - timeOfLatestRequestInMillis == null || dataPointTimeAsMillis > timeOfLatestRequestInMillis) { - raceDataPointsToAdd[currentPointIndex++] = new Point(dataPointTimeAsMillis, dataPointValue); - } - } - } - - Point[] newRaceDataPoints; - if (append) { - newRaceDataPoints = new Point[oldRaceDataPoints.length + currentPointIndex]; - System.arraycopy(oldRaceDataPoints, 0, newRaceDataPoints, 0, oldRaceDataPoints.length); - System.arraycopy(raceDataPointsToAdd, 0, newRaceDataPoints, oldRaceDataPoints.length, currentPointIndex); - } else { - newRaceDataPoints = new Point[currentPointIndex]; - System.arraycopy(raceDataPointsToAdd, 0, newRaceDataPoints, 0, currentPointIndex); - } - setSeriesPoints(competitorDataSeries, newRaceDataPoints); - // Adding the series if chart doesn't contain it - if (!chartSeries.contains(competitorDataSeries)) { - chart.addSeries(competitorDataSeries); - chart.addSeries(markPassingSeries); - } - } - } - if (timeOfEarliestRequestInMillis == null || timeOfEarliestRequestInMillis > chartData.getRequestedFromTime().getTime()) { - timeOfEarliestRequestInMillis = chartData.getRequestedFromTime().getTime(); - } - if (timeOfLatestRequestInMillis == null || timeOfLatestRequestInMillis < chartData.getRequestedToTime().getTime()) { - timeOfLatestRequestInMillis = chartData.getRequestedToTime().getTime(); - } - chart.redraw(); - } - - private Iterable getSelectedCompetitors() { - return competitorSelectionProvider.getSelectedCompetitors(); - } - - private boolean hasSelectedCompetitors() { - return Util.size(getSelectedCompetitors()) > 0; - } - - /** - * - * @param competitor - * @return A series in the chart, that can be used to show the data of a specific competitor. - */ - private Series getOrCreateCompetitorDataSeries(final CompetitorDTO competitor) { - Series result = dataSeriesByCompetitor.get(competitor); - if (result == null) { - result = chart.createSeries().setType(Series.Type.LINE).setName(competitor.name); - result.setPlotOptions(new LinePlotOptions() - .setLineWidth(LINE_WIDTH) - .setMarker(new Marker().setEnabled(false).setHoverState(new Marker().setEnabled(true).setRadius(4))) - .setShadow(false).setHoverStateLineWidth(LINE_WIDTH) - .setColor(competitorSelectionProvider.getColor(competitor)).setSelected(true)); - result.setOption("turboThreshold", MAX_SERIES_POINTS); - dataSeriesByCompetitor.put(competitor, result); - } - return result; - } - - private String getDetailTypeUnit() { - String detailTypeUnit = ""; - switch (getSelectedDetailType()) { - case CURRENT_SPEED_OVER_GROUND_IN_KNOTS: - detailTypeUnit = getStringMessages().currentSpeedOverGroundInKnotsUnit(); - break; - case DISTANCE_TRAVELED: - detailTypeUnit = getStringMessages().distanceInMetersUnit(); - break; - case GAP_TO_LEADER_IN_SECONDS: - detailTypeUnit = getStringMessages().gapToLeaderInSecondsUnit(); - break; - case VELOCITY_MADE_GOOD_IN_KNOTS: - detailTypeUnit = getStringMessages().velocityMadeGoodInKnotsUnit(); - break; - case WINDWARD_DISTANCE_TO_OVERALL_LEADER: - detailTypeUnit = getStringMessages().windwardDistanceToGoInMetersUnit(); - } - return detailTypeUnit; - } - - /** - * - * @param competitor - * @return A series in the chart, that can be used to show the mark passings. - */ - private Series getOrCreateCompetitorMarkPassingSeries(CompetitorDTO competitor) { - Series result = markPassingSeriesByCompetitor.get(competitor); - if (result == null) { - result = chart.createSeries().setType(Series.Type.SCATTER) - .setName(stringMessages.markPassing() + " " + competitor.name); - result.setPlotOptions(new ScatterPlotOptions().setColor(competitorSelectionProvider.getColor(competitor)) - .setSelected(true)); - markPassingSeriesByCompetitor.put(competitor, result); - } - return result; - } - - @Override - public void setVisible(boolean visible) { - super.setVisible(visible); - if(visible) { - // Workaround for a highcharts bug: - // Set a chart title, overwrite the title, switch chart to invisible and visible again -> the old title appears - chart.setTitle(new ChartTitle().setText(DetailTypeFormatter.format(selectedDetailType, stringMessages)), - null); - } - } - - /** - * Clears the whole chart and empties cached data. - */ - protected void clearChart() { - timeOfEarliestRequestInMillis = null; - timeOfLatestRequestInMillis = null; - dataSeriesByCompetitor.clear(); - markPassingSeriesByCompetitor.clear(); - chart.removeAllSeries(); - } - - public String getLocalizedShortName() { - return DetailTypeFormatter.format(getSelectedDetailType(), getStringMessages()); - } - - public Widget getEntryWidget() { - return this; - } - - public boolean hasSettings() { - return true; - } - - public ChartSettings getAbstractSettings() { - return new ChartSettings(getStepSize()); - } - - /** - * Updates the settings known to be contained in {@link ChartSettings}. Subclasses have to update settings provided - * by subclasses thereof. Subclasses also need to call {@link #clearChart()} and {@link #updateChart(boolean)}, if - * this method returns true; - * - * @return true if the settings had been changed and a clearing and loading is needed. - */ - protected boolean updateSettingsOnly(ChartSettings newSettings) { - boolean settingsChanged = false; - if (getStepSize() != newSettings.getStepSize()) { - setStepSize(newSettings.getStepSize()); - settingsChanged = true; - } - return settingsChanged; - } - - protected StringMessages getStringMessages() { - return stringMessages; - } - - protected long getStepSize() { - return stepSize; - } - - protected void setStepSize(long stepSize) { - this.stepSize = stepSize; - } - - protected DetailType getSelectedDetailType() { - return this.selectedDetailType; - } - - private boolean hasReversedYAxis(DetailType detailType) { - return detailType == DetailType.WINDWARD_DISTANCE_TO_OVERALL_LEADER || detailType == DetailType.GAP_TO_LEADER_IN_SECONDS; - } - - /** - * Updates the {@link #selectedDetailType} field, clears the chart for the new selectedDetailType and - * clears the {@link #chartData}.
- * Doesn't {@link #updateChart(boolean) load the data}. - * - * @return true if the detail type changed - */ - protected boolean setSelectedDetailType(DetailType newSelectedDetailType) { - boolean hasDetailTypeChanged = newSelectedDetailType != this.selectedDetailType; - if (hasDetailTypeChanged) { - boolean oldReversedYAxis = hasReversedYAxis(this.selectedDetailType); - this.selectedDetailType = newSelectedDetailType; - // TODO There is a bug in the highcharts library which prevents to change the reverse property of the YAxis - // Because we need this functionality we need to recreate the chart each time the YAxis changes - if (oldReversedYAxis != hasReversedYAxis(selectedDetailType)) { - chart = createChart(); - if (isZoomed) { - Pair zoomRange = timeRangeWithZoomProvider.getTimeZoom(); - onTimeZoomChanged(zoomRange.getA(), zoomRange.getB()); - } else { - resetMinMaxAndExtremesInterval(/* redraw */ true); - } - } - chart.setTitle(new ChartTitle().setText(DetailTypeFormatter.format(selectedDetailType, stringMessages)), null); - final String unit = getDetailTypeUnit(); - if (!compactChart) { - chart.getYAxis().setAxisTitleText( - DetailTypeFormatter.format(selectedDetailType, stringMessages) + " [" + unit + "]"); - } else { - chart.getYAxis().setAxisTitleText("[" + unit + "]"); - } - chart.getYAxis().setReversed( - selectedDetailType == DetailType.WINDWARD_DISTANCE_TO_OVERALL_LEADER || selectedDetailType == DetailType.GAP_TO_LEADER_IN_SECONDS); - final NumberFormat numberFormat = DetailTypeFormatter.getNumberFormat(selectedDetailType); - chart.setToolTip(new ToolTip().setEnabled(true).setFormatter(new ToolTipFormatter() { - @Override - public String format(ToolTipData toolTipData) { - String seriesName = toolTipData.getSeriesName(); - - if (seriesName.equals(stringMessages.time())) { - return "" + seriesName + ": " + dateFormat.format(new Date(toolTipData.getXAsLong())) - + "
(" + stringMessages.clickChartToSetTime() + ")"; - } else { - return "" + seriesName - + (toolTipData.getPointName() != null ? " " + toolTipData.getPointName() : "") - + "
" + dateFormat.format(new Date(toolTipData.getXAsLong())) + ": " - + numberFormat.format(toolTipData.getYAsDouble()) + " " + unit; - } - } - })); - } - return hasDetailTypeChanged; - } - - @Override - public void onRaceSelectionChange(List selectedRaces) { - if (selectedRaces != null && !selectedRaces.isEmpty()) { - selectedRaceIdentifier = selectedRaces.iterator().next(); - } else { - selectedRaceIdentifier = null; - } - - clearChart(); - if (selectedRaceIdentifier != null) { - timeChanged(timer.getTime()); - } - } - - /** - * Checks the relation of the mark passings to the selection range. - * - * @param markPassingInRange - * A Boolean matrix filled by - * {@link AbstractChartPanel#fillPotentialXValues(double, double, ArrayList, ArrayList, ArrayList) - * fillPotentialXValues(...)} - * @return A pair of Booleans. Value A contains false if a passing is not in the selection (error), so that the - * selection range needs to be refactored. Value B returns true if two passings are in range before the - * error happened or false, if the error happens before two passings were in the selection. B can be - * null. - */ - public Pair checkPassingRelationToSelection(ArrayList> markPassingInRange) { - boolean everyPassingInRange = true; - Boolean twoPassingsInRangeBeforeError = null; - ArrayList competitorPassings = markPassingInRange.get(0); - for (int i = 0; i < competitorPassings.size(); i++) { - Boolean passingInRange = competitorPassings.get(i); - for (int j = 1; j < markPassingInRange.size(); j++) { - Boolean passingToCompare = markPassingInRange.get(j).get(i); - if (passingInRange != null) { - if (passingToCompare != null && everyPassingInRange) { - everyPassingInRange = passingInRange.equals(passingToCompare); - if (passingInRange && passingToCompare) { - twoPassingsInRangeBeforeError = true; - } - } else if (passingToCompare != null) { - if (passingInRange && passingToCompare) { - twoPassingsInRangeBeforeError = false; - } - } - } else { - passingInRange = passingToCompare; - } - } - } - - return new Pair(everyPassingInRange, twoPassingsInRangeBeforeError); - } - - @Override - public void timeChanged(Date date) { - if (!isVisible()) { - return; - } - - switch (timer.getPlayMode()) { - case Live: { - // is date before first cache entry or is cache empty? - if (timeOfEarliestRequestInMillis == null || date.getTime() < timeOfEarliestRequestInMillis) { - updateChart(timeRangeWithZoomProvider.getFromTime(), date, /* append */true); - } else if (date.getTime() > timeOfLatestRequestInMillis) { - updateChart(new Date(timeOfLatestRequestInMillis), timeRangeWithZoomProvider.getToTime(), /* append */true); - } - // otherwise the cache spans across date and so we don't need to load anything - break; - } - case Replay: { - if (timeOfLatestRequestInMillis == null) { - // pure replay mode - updateChart(timeRangeWithZoomProvider.getFromTime(), timeRangeWithZoomProvider.getToTime(), /* append */false); - } else { - // replay mode during live play - if (timeOfEarliestRequestInMillis == null || date.getTime() < timeOfEarliestRequestInMillis) { - updateChart(timeRangeWithZoomProvider.getFromTime(), date, /* append */true); - } else if (date.getTime() > timeOfLatestRequestInMillis) { - updateChart(new Date(timeOfLatestRequestInMillis), timeRangeWithZoomProvider.getToTime(), /* append */true); - } - } - break; - } - } - } - - @Override - public void onResize() { - chart.setSizeToMatchContainer(); - // it's important here to recall the redraw method, otherwise the bug fix for wrong checkbox positions - // (nativeAdjustCheckboxPosition) - // in the BaseChart class would not be called - chart.redraw(); - } - - @Override - public void competitorsListChanged(Iterable competitors) { - timeChanged(timer.getTime()); - } -} +package com.sap.sailing.gwt.ui.shared.charts; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.moxieapps.gwt.highcharts.client.Axis; +import org.moxieapps.gwt.highcharts.client.Chart; +import org.moxieapps.gwt.highcharts.client.ChartSubtitle; +import org.moxieapps.gwt.highcharts.client.ChartTitle; +import org.moxieapps.gwt.highcharts.client.Color; +import org.moxieapps.gwt.highcharts.client.Credits; +import org.moxieapps.gwt.highcharts.client.Point; +import org.moxieapps.gwt.highcharts.client.Series; +import org.moxieapps.gwt.highcharts.client.ToolTip; +import org.moxieapps.gwt.highcharts.client.ToolTipData; +import org.moxieapps.gwt.highcharts.client.ToolTipFormatter; +import org.moxieapps.gwt.highcharts.client.events.ChartClickEvent; +import org.moxieapps.gwt.highcharts.client.events.ChartClickEventHandler; +import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; +import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEventHandler; +import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsData; +import org.moxieapps.gwt.highcharts.client.labels.AxisLabelsFormatter; +import org.moxieapps.gwt.highcharts.client.labels.XAxisLabels; +import org.moxieapps.gwt.highcharts.client.plotOptions.LinePlotOptions; +import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; +import org.moxieapps.gwt.highcharts.client.plotOptions.ScatterPlotOptions; + +import com.google.gwt.i18n.client.NumberFormat; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.RequiresResize; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.DetailType; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +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.gwt.ui.actions.AsyncActionsExecutor; +import com.sap.sailing.gwt.ui.actions.GetCompetitorsRaceDataAction; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionChangeListener; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; +import com.sap.sailing.gwt.ui.client.DetailTypeFormatter; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.TimeRangeWithZoomProvider; +import com.sap.sailing.gwt.ui.client.Timer; +import com.sap.sailing.gwt.ui.client.Timer.PlayModes; +import com.sap.sailing.gwt.ui.shared.CompetitorDTO; +import com.sap.sailing.gwt.ui.shared.CompetitorRaceDataDTO; +import com.sap.sailing.gwt.ui.shared.CompetitorsRaceDataDTO; +import com.sap.sailing.gwt.ui.shared.components.Component; + +/** + * ChartPanel is a GWT panel that can show one sort of competitor data (e.g. current speed over ground, windward + * distance to leader) for different races in a chart. + * + * When calling the constructor a chart is created that creates a final amount of series (so the maximum number of + * competitors cannot be changed in one chart) which are connected to competitors, when the sailing service returns the + * data. So {@code seriesID, competitorID and markSeriesID} are linked with the index. So if u know for example the + * seriesID-index, you can get the competitor by calling competitorID.get(index). + * + * @author Benjamin Ebling (D056866), Axel Uhl (d043530) + * + */ +public abstract class AbstractChartPanel extends RaceChart implements + CompetitorSelectionChangeListener, RequiresResize { + private static final int LINE_WIDTH = 1; + private static final int MAX_SERIES_POINTS = 10000; + private boolean compactChart; + private final Label noCompetitorsSelectedLabel; + private final Map dataSeriesByCompetitor; + private final Map markPassingSeriesByCompetitor; + private final boolean allowTimeAdjust; + private long stepSize = 5000; + private DetailType selectedDetailType; + private final CompetitorSelectionProvider competitorSelectionProvider; + + private Long timeOfEarliestRequestInMillis; + private Long timeOfLatestRequestInMillis; + + public AbstractChartPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + CompetitorSelectionProvider competitorSelectionProvider, RaceSelectionProvider raceSelectionProvider, + Timer timer, TimeRangeWithZoomProvider timeRangeWithZoomProvider, final StringMessages stringMessages, + ErrorReporter errorReporter, DetailType detailType, boolean compactChart, boolean allowTimeAdjust) { + super(sailingService, timer, timeRangeWithZoomProvider, stringMessages, asyncActionsExecutor, errorReporter); + this.competitorSelectionProvider = competitorSelectionProvider; + this.compactChart = compactChart; + this.allowTimeAdjust = allowTimeAdjust; + dataSeriesByCompetitor = new HashMap(); + markPassingSeriesByCompetitor = new HashMap(); + setSize("100%", "100%"); + noCompetitorsSelectedLabel = new Label(stringMessages.selectAtLeastOneCompetitor() + "."); + noCompetitorsSelectedLabel.setStyleName("abstractChartPanel-importantMessageOfChart"); + createChart(); + setSelectedDetailType(detailType); + competitorSelectionProvider.addCompetitorSelectionChangeListener(this); + raceSelectionProvider.addRaceSelectionChangeListener(this); + } + + /** + * Creates a new chart for the given {@link DetailType} dataToShow and also clears the + * {@link #chartData}, the {@link #dataSeriesByCompetitor} and the {@link #markPassingSeriesByCompetitor}. + * + * @param detailType + * The detail type for the new chart. + * @return A chart for the given detail Type + */ + private Chart createChart() { + Chart chart = new Chart().setZoomType(Chart.ZoomType.X) + .setPersistent(true) + .setWidth100() + .setHeight100() + .setMarginLeft(65) + .setMarginRight(65) + .setBorderColor(new Color("#CACACA")) + .setBackgroundColor(new Color("#EBEBEB")) + .setBorderWidth(0) + .setBorderRadius(0) + .setPlotBorderWidth(0) + .setCredits(new Credits().setEnabled(false)) + .setChartSubtitle(new ChartSubtitle().setText(stringMessages.clickAndDragToZoomIn())) + .setLinePlotOptions( + new LinePlotOptions() + .setLineWidth(LINE_WIDTH) + .setMarker( + new Marker().setEnabled(false).setHoverState( + new Marker().setEnabled(true).setRadius(4))).setShadow(false) + .setHoverStateLineWidth(LINE_WIDTH)); + ChartUtil.useCheckboxesToShowAndHide(chart); + + if (allowTimeAdjust) { + chart.setClickEventHandler(new ChartClickEventHandler() { + @Override + public boolean onClick(ChartClickEvent chartClickEvent) { + return AbstractChartPanel.this.onClick(chartClickEvent); + } + }); + chart.setSelectionEventHandler(new ChartSelectionEventHandler() { + @Override + public boolean onSelection(ChartSelectionEvent chartSelectionEvent) { + return AbstractChartPanel.this.onXAxisSelectionChange(chartSelectionEvent); + } + }); + } + + chart.getXAxis().setType(Axis.Type.DATE_TIME).setMaxZoom(60 * 1000); // 1 minute + chart.getYAxis().setStartOnTick(false).setShowFirstLabel(false); + chart.getXAxis().setLabels(new XAxisLabels().setFormatter(new AxisLabelsFormatter() { + @Override + public String format(AxisLabelsData axisLabelsData) { + return dateFormatHoursMinutes.format(new Date(axisLabelsData.getValueAsLong())); + } + })); + + if (compactChart) { + chart.setSpacingBottom(10).setSpacingLeft(10).setSpacingRight(10).setSpacingTop(2) + .setOption("legend/margin", 2).setOption("title/margin", 5).setChartSubtitle(null).getXAxis() + .setAxisTitleText(null); + } + + return chart; + } + + protected abstract Component getComponent(); + + /** + * Loads the needed data (data which isn't in the {@link #chartData cache}) for the + * {@link #getSelectedCompetitors() visible competitors} via + * {@link SailingServiceAsync#getCompetitorsRaceData(RaceIdentifier, List, long, DetailType, AsyncCallback)}. After + * loading is the method {@link #drawChartData()} called.
+ * If no data needs to be {@link #needsDataLoading() loaded}, the no competitors selected label is displayed. + */ + private void updateChart(Date from, Date to, boolean append) { + if (hasSelectedCompetitors()) { + setWidget(chart); + + ArrayList competitorsToLoad = new ArrayList(); + for (CompetitorDTO competitorDTO : getSelectedCompetitors()) { + competitorsToLoad.add(competitorDTO); + } + + loadData(from, to, competitorsToLoad, append); + } else { + setWidget(noCompetitorsSelectedLabel); + } + } + + private void loadData(final Date from, final Date to, final List competitors, final boolean append) { + showLoading(stringMessages.loadingCompetitorData()); + ArrayList competitorsToLoad = new ArrayList(); + for (CompetitorDTO competitorDTO : competitors) { + competitorsToLoad.add(competitorDTO); + } + + GetCompetitorsRaceDataAction getCompetitorsRaceDataAction = new GetCompetitorsRaceDataAction(sailingService, + selectedRaceIdentifier, competitorsToLoad, from, to, getStepSize(), + getSelectedDetailType(), new AsyncCallback() { + @Override + public void onSuccess(final CompetitorsRaceDataDTO result) { + hideLoading(); + + if (result != null) { + updateChartSeries(result, append); + } else { + if (!append) { + clearChart(); + } + } + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError( + getStringMessages().failedToLoadRaceData() + ": " + caught.toString(), + timer.getPlayMode() == PlayModes.Live); + hideLoading(); + } + }); + asyncActionsExecutor.execute(getCompetitorsRaceDataAction); + + } + + @Override + public void addedToSelection(CompetitorDTO competitor) { + if (isVisible()) { + ArrayList competitorsToLoad = new ArrayList(); + competitorsToLoad.add(competitor); + Date fromDate = timeOfEarliestRequestInMillis == null ? null : new Date(timeOfEarliestRequestInMillis); + Date toDate = timeOfLatestRequestInMillis == null ? null : new Date(timeOfLatestRequestInMillis); + loadData(fromDate, toDate, competitorsToLoad, false); + } + } + + @Override + public void removedFromSelection(CompetitorDTO competitor) { + Series competitorSeries = dataSeriesByCompetitor.get(competitor); + if (competitorSeries != null) { + chart.removeSeries(competitorSeries, false); + } + Series competitorMarkPassingSeries = markPassingSeriesByCompetitor.get(competitor); + if (competitorMarkPassingSeries != null) { + chart.removeSeries(competitorMarkPassingSeries, false); + } + + if (isVisible()) { + if(hasSelectedCompetitors()) { + chart.redraw(); + } else { + setWidget(noCompetitorsSelectedLabel); + } + } + } + + /** + * Creates the series for all selected competitors if these aren't created yet.
+ * Fills the series for the selected competitors with the data in {@link AbstractChartPanel#chartData}.
+ */ + private synchronized void updateChartSeries(CompetitorsRaceDataDTO chartData, boolean append) { + // Make sure the busy indicator is removed at this point, or plotting the data results in an exception + setWidget(chart); + + List chartSeries = Arrays.asList(chart.getSeries()); + for (CompetitorDTO competitor : chartData.getCompetitors()) { + Series competitorDataSeries = getOrCreateCompetitorDataSeries(competitor); + Series markPassingSeries = getOrCreateCompetitorMarkPassingSeries(competitor); + CompetitorRaceDataDTO competitorData = chartData.getCompetitorData(competitor); + if (competitorData != null) { + Date toDate = new Date(System.currentTimeMillis() - timer.getLivePlayDelayInMillis()); + + List> markPassingsData = competitorData.getMarkPassingsData(); + List markPassingPoints = new ArrayList(); + for (Triple markPassingData : markPassingsData) { + if (markPassingData.getB() != null && markPassingData.getC() != null) { + if (markPassingData.getB().before(toDate)) { + Point markPassingPoint = new Point(markPassingData.getB().getTime(), + markPassingData.getC()); + markPassingPoint.setName(markPassingData.getA()); + markPassingPoints.add(markPassingPoint); + } + } + } + markPassingSeries.setPoints(markPassingPoints.toArray(new Point[0]), false); + + Point[] oldRaceDataPoints = competitorDataSeries.getPoints(); + List> raceData = competitorData.getRaceData(); + + Point[] raceDataPointsToAdd = new Point[raceData.size()]; + int currentPointIndex = 0; + for (Pair raceDataPoint : raceData) { + Double dataPointValue = raceDataPoint.getB(); + if(dataPointValue != null) { + long dataPointTimeAsMillis = raceDataPoint.getA().getTime(); + if(append == false || (timeOfEarliestRequestInMillis == null || dataPointTimeAsMillis < timeOfEarliestRequestInMillis) || + timeOfLatestRequestInMillis == null || dataPointTimeAsMillis > timeOfLatestRequestInMillis) { + raceDataPointsToAdd[currentPointIndex++] = new Point(dataPointTimeAsMillis, dataPointValue); + } + } + } + + Point[] newRaceDataPoints; + if (append) { + newRaceDataPoints = new Point[oldRaceDataPoints.length + currentPointIndex]; + System.arraycopy(oldRaceDataPoints, 0, newRaceDataPoints, 0, oldRaceDataPoints.length); + System.arraycopy(raceDataPointsToAdd, 0, newRaceDataPoints, oldRaceDataPoints.length, currentPointIndex); + } else { + newRaceDataPoints = new Point[currentPointIndex]; + System.arraycopy(raceDataPointsToAdd, 0, newRaceDataPoints, 0, currentPointIndex); + } + setSeriesPoints(competitorDataSeries, newRaceDataPoints); + // Adding the series if chart doesn't contain it + if (!chartSeries.contains(competitorDataSeries)) { + chart.addSeries(competitorDataSeries); + chart.addSeries(markPassingSeries); + } + } + } + if (timeOfEarliestRequestInMillis == null || timeOfEarliestRequestInMillis > chartData.getRequestedFromTime().getTime()) { + timeOfEarliestRequestInMillis = chartData.getRequestedFromTime().getTime(); + } + if (timeOfLatestRequestInMillis == null || timeOfLatestRequestInMillis < chartData.getRequestedToTime().getTime()) { + timeOfLatestRequestInMillis = chartData.getRequestedToTime().getTime(); + } + chart.redraw(); + } + + private Iterable getSelectedCompetitors() { + return competitorSelectionProvider.getSelectedCompetitors(); + } + + private boolean hasSelectedCompetitors() { + return Util.size(getSelectedCompetitors()) > 0; + } + + /** + * + * @param competitor + * @return A series in the chart, that can be used to show the data of a specific competitor. + */ + private Series getOrCreateCompetitorDataSeries(final CompetitorDTO competitor) { + Series result = dataSeriesByCompetitor.get(competitor); + if (result == null) { + result = chart.createSeries().setType(Series.Type.LINE).setName(competitor.name); + result.setPlotOptions(new LinePlotOptions() + .setLineWidth(LINE_WIDTH) + .setMarker(new Marker().setEnabled(false).setHoverState(new Marker().setEnabled(true).setRadius(4))) + .setShadow(false).setHoverStateLineWidth(LINE_WIDTH) + .setColor(competitorSelectionProvider.getColor(competitor)).setSelected(true)); + result.setOption("turboThreshold", MAX_SERIES_POINTS); + dataSeriesByCompetitor.put(competitor, result); + } + return result; + } + + private String getDetailTypeUnit() { + String detailTypeUnit = ""; + switch (getSelectedDetailType()) { + case CURRENT_SPEED_OVER_GROUND_IN_KNOTS: + detailTypeUnit = getStringMessages().currentSpeedOverGroundInKnotsUnit(); + break; + case DISTANCE_TRAVELED: + detailTypeUnit = getStringMessages().distanceInMetersUnit(); + break; + case GAP_TO_LEADER_IN_SECONDS: + detailTypeUnit = getStringMessages().gapToLeaderInSecondsUnit(); + break; + case VELOCITY_MADE_GOOD_IN_KNOTS: + detailTypeUnit = getStringMessages().velocityMadeGoodInKnotsUnit(); + break; + case WINDWARD_DISTANCE_TO_OVERALL_LEADER: + detailTypeUnit = getStringMessages().windwardDistanceToGoInMetersUnit(); + default: + throw new UnsupportedOperationException("Theres currently no support for the enum value '" + getSelectedDetailType() + "' in this method."); + } + return detailTypeUnit; + } + + /** + * + * @param competitor + * @return A series in the chart, that can be used to show the mark passings. + */ + private Series getOrCreateCompetitorMarkPassingSeries(CompetitorDTO competitor) { + Series result = markPassingSeriesByCompetitor.get(competitor); + if (result == null) { + result = chart.createSeries().setType(Series.Type.SCATTER) + .setName(stringMessages.markPassing() + " " + competitor.name); + result.setPlotOptions(new ScatterPlotOptions().setColor(competitorSelectionProvider.getColor(competitor)) + .setSelected(true)); + markPassingSeriesByCompetitor.put(competitor, result); + } + return result; + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if(visible) { + // Workaround for a highcharts bug: + // Set a chart title, overwrite the title, switch chart to invisible and visible again -> the old title appears + chart.setTitle(new ChartTitle().setText(DetailTypeFormatter.format(selectedDetailType, stringMessages)), + null); + } + } + + /** + * Clears the whole chart and empties cached data. + */ + protected void clearChart() { + timeOfEarliestRequestInMillis = null; + timeOfLatestRequestInMillis = null; + dataSeriesByCompetitor.clear(); + markPassingSeriesByCompetitor.clear(); + chart.removeAllSeries(); + } + + public String getLocalizedShortName() { + return DetailTypeFormatter.format(getSelectedDetailType(), getStringMessages()); + } + + public Widget getEntryWidget() { + return this; + } + + public boolean hasSettings() { + return true; + } + + public ChartSettings getAbstractSettings() { + return new ChartSettings(getStepSize()); + } + + /** + * Updates the settings known to be contained in {@link ChartSettings}. Subclasses have to update settings provided + * by subclasses thereof. Subclasses also need to call {@link #clearChart()} and {@link #updateChart(boolean)}, if + * this method returns true; + * + * @return true if the settings had been changed and a clearing and loading is needed. + */ + protected boolean updateSettingsOnly(ChartSettings newSettings) { + boolean settingsChanged = false; + if (getStepSize() != newSettings.getStepSize()) { + setStepSize(newSettings.getStepSize()); + settingsChanged = true; + } + return settingsChanged; + } + + protected StringMessages getStringMessages() { + return stringMessages; + } + + protected long getStepSize() { + return stepSize; + } + + protected void setStepSize(long stepSize) { + this.stepSize = stepSize; + } + + protected DetailType getSelectedDetailType() { + return this.selectedDetailType; + } + + private boolean hasReversedYAxis(DetailType detailType) { + return detailType == DetailType.WINDWARD_DISTANCE_TO_OVERALL_LEADER || detailType == DetailType.GAP_TO_LEADER_IN_SECONDS; + } + + /** + * Updates the {@link #selectedDetailType} field, clears the chart for the new selectedDetailType and + * clears the {@link #chartData}.
+ * Doesn't {@link #updateChart(boolean) load the data}. + * + * @return true if the detail type changed + */ + protected boolean setSelectedDetailType(DetailType newSelectedDetailType) { + boolean hasDetailTypeChanged = newSelectedDetailType != this.selectedDetailType; + if (hasDetailTypeChanged) { + boolean oldReversedYAxis = hasReversedYAxis(this.selectedDetailType); + this.selectedDetailType = newSelectedDetailType; + // TODO There is a bug in the highcharts library which prevents to change the reverse property of the YAxis + // Because we need this functionality we need to recreate the chart each time the YAxis changes + if (oldReversedYAxis != hasReversedYAxis(selectedDetailType)) { + chart = createChart(); + if (isZoomed) { + Pair zoomRange = timeRangeWithZoomProvider.getTimeZoom(); + onTimeZoomChanged(zoomRange.getA(), zoomRange.getB()); + } else { + resetMinMaxAndExtremesInterval(/* redraw */ true); + } + } + chart.setTitle(new ChartTitle().setText(DetailTypeFormatter.format(selectedDetailType, stringMessages)), null); + final String unit = getDetailTypeUnit(); + if (!compactChart) { + chart.getYAxis().setAxisTitleText( + DetailTypeFormatter.format(selectedDetailType, stringMessages) + " [" + unit + "]"); + } else { + chart.getYAxis().setAxisTitleText("[" + unit + "]"); + } + chart.getYAxis().setReversed( + selectedDetailType == DetailType.WINDWARD_DISTANCE_TO_OVERALL_LEADER || selectedDetailType == DetailType.GAP_TO_LEADER_IN_SECONDS); + final NumberFormat numberFormat = DetailTypeFormatter.getNumberFormat(selectedDetailType); + chart.setToolTip(new ToolTip().setEnabled(true).setFormatter(new ToolTipFormatter() { + @Override + public String format(ToolTipData toolTipData) { + String seriesName = toolTipData.getSeriesName(); + + if (seriesName.equals(stringMessages.time())) { + return "" + seriesName + ": " + dateFormat.format(new Date(toolTipData.getXAsLong())) + + "
(" + stringMessages.clickChartToSetTime() + ")"; + } else { + return "" + seriesName + + (toolTipData.getPointName() != null ? " " + toolTipData.getPointName() : "") + + "
" + dateFormat.format(new Date(toolTipData.getXAsLong())) + ": " + + numberFormat.format(toolTipData.getYAsDouble()) + " " + unit; + } + } + })); + } + return hasDetailTypeChanged; + } + + @Override + public void onRaceSelectionChange(List selectedRaces) { + if (selectedRaces != null && !selectedRaces.isEmpty()) { + selectedRaceIdentifier = selectedRaces.iterator().next(); + } else { + selectedRaceIdentifier = null; + } + + clearChart(); + if (selectedRaceIdentifier != null) { + timeChanged(timer.getTime()); + } + } + + /** + * Checks the relation of the mark passings to the selection range. + * + * @param markPassingInRange + * A Boolean matrix filled by + * {@link AbstractChartPanel#fillPotentialXValues(double, double, ArrayList, ArrayList, ArrayList) + * fillPotentialXValues(...)} + * @return A pair of Booleans. Value A contains false if a passing is not in the selection (error), so that the + * selection range needs to be refactored. Value B returns true if two passings are in range before the + * error happened or false, if the error happens before two passings were in the selection. B can be + * null. + */ + public Pair checkPassingRelationToSelection(ArrayList> markPassingInRange) { + boolean everyPassingInRange = true; + Boolean twoPassingsInRangeBeforeError = null; + ArrayList competitorPassings = markPassingInRange.get(0); + for (int i = 0; i < competitorPassings.size(); i++) { + Boolean passingInRange = competitorPassings.get(i); + for (int j = 1; j < markPassingInRange.size(); j++) { + Boolean passingToCompare = markPassingInRange.get(j).get(i); + if (passingInRange != null) { + if (passingToCompare != null && everyPassingInRange) { + everyPassingInRange = passingInRange.equals(passingToCompare); + if (passingInRange && passingToCompare) { + twoPassingsInRangeBeforeError = true; + } + } else if (passingToCompare != null) { + if (passingInRange && passingToCompare) { + twoPassingsInRangeBeforeError = false; + } + } + } else { + passingInRange = passingToCompare; + } + } + } + + return new Pair(everyPassingInRange, twoPassingsInRangeBeforeError); + } + + @Override + public void timeChanged(Date date) { + if (!isVisible()) { + return; + } + + switch (timer.getPlayMode()) { + case Live: { + // is date before first cache entry or is cache empty? + if (timeOfEarliestRequestInMillis == null || date.getTime() < timeOfEarliestRequestInMillis) { + updateChart(timeRangeWithZoomProvider.getFromTime(), date, /* append */true); + } else if (date.getTime() > timeOfLatestRequestInMillis) { + updateChart(new Date(timeOfLatestRequestInMillis), timeRangeWithZoomProvider.getToTime(), /* append */true); + } + // otherwise the cache spans across date and so we don't need to load anything + break; + } + case Replay: { + if (timeOfLatestRequestInMillis == null) { + // pure replay mode + updateChart(timeRangeWithZoomProvider.getFromTime(), timeRangeWithZoomProvider.getToTime(), /* append */false); + } else { + // replay mode during live play + if (timeOfEarliestRequestInMillis == null || date.getTime() < timeOfEarliestRequestInMillis) { + updateChart(timeRangeWithZoomProvider.getFromTime(), date, /* append */true); + } else if (date.getTime() > timeOfLatestRequestInMillis) { + updateChart(new Date(timeOfLatestRequestInMillis), timeRangeWithZoomProvider.getToTime(), /* append */true); + } + } + break; + } + } + } + + @Override + public void onResize() { + chart.setSizeToMatchContainer(); + // it's important here to recall the redraw method, otherwise the bug fix for wrong checkbox positions + // (nativeAdjustCheckboxPosition) + // in the BaseChart class would not be called + chart.redraw(); + } + + @Override + public void competitorsListChanged(Iterable competitors) { + timeChanged(timer.getTime()); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/RaceMap.java index ed81169156e..6e386b8f22d 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/RaceMap.java @@ -438,6 +438,8 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe lastCombinedWindTrackInfoDTO = windTrackInfoDTO; } break; + default: + throw new UnsupportedOperationException("Theres currently no support for the enum value '" + windSource.getType() + "' in this method."); } } } diff --git a/java/com.sap.sailing.udpconnector/src/com/sap/sailing/udpconnector/UDPReceiver.java b/java/com.sap.sailing.udpconnector/src/com/sap/sailing/udpconnector/UDPReceiver.java index 8f309dd6e49..3bcfcb0b809 100755 --- a/java/com.sap.sailing.udpconnector/src/com/sap/sailing/udpconnector/UDPReceiver.java +++ b/java/com.sap.sailing.udpconnector/src/com/sap/sailing/udpconnector/UDPReceiver.java @@ -86,7 +86,11 @@ public abstract class UDPReceiver