diff --git a/configuration/buildAndUpdateProduct.sh b/configuration/buildAndUpdateProduct.sh index 3c3b628b7e2..528d6230e88 100755 --- a/configuration/buildAndUpdateProduct.sh +++ b/configuration/buildAndUpdateProduct.sh @@ -241,6 +241,15 @@ if [[ "$@" == "release" ]]; then if [[ $OSGI_BUNDLE_NAME != "" ]]; then SIMPLE_VERSION_INFO="$OSGI_BUNDLE_NAME-$HEAD_DATE" fi + + echo " + MONGODB_NAME=myspecificevent + REPLICATION_CHANNEL=myspecificevent + SERVER_STARTUP_NOTIFY=simon.marcel.pamies@sap.com + SERVER_NAME=LIVE + USE_ENVIRONMENT=live-server + INSTALL_FROM_RELEASE=$SIMPLE_VERSION_INFO + " >> $ACDIR/amazon-launch-config.txt mkdir $PROJECT_HOME/dist/$SIMPLE_VERSION_INFO `which tar` cvzf $PROJECT_HOME/dist/$SIMPLE_VERSION_INFO/$SIMPLE_VERSION_INFO.tar.gz * diff --git a/configuration/live-server-env.sh b/configuration/live-server-env.sh new file mode 100644 index 00000000000..c307832707a --- /dev/null +++ b/configuration/live-server-env.sh @@ -0,0 +1,16 @@ +DEPLOY_TO=server +SERVER_NAME=LIVE +MEMORY=4096m +REPLICATION_HOST=172.31.25.253 +REPLICATION_CHANNEL=sapsailinganalytics-live +TELNET_PORT=14888 +SERVER_PORT=8888 +MONGODB_HOST=172.31.25.253 +MONGODB_PORT=10201 +EXPEDITION_PORT=2010 +REPLICATE_ON_START=False +REPLICATE_MASTER_SERVLET_HOST= +REPLICATE_MASTER_SERVLET_PORT= +REPLICATE_MASTER_QUEUE_HOST= +REPLICATE_MASTER_QUEUE_PORT= +ADDITIONAL_JAVA_ARGS="-XX:+UseMembar -XX:+UseParallelGC -XX:+UseParallelOldGC -verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:logs/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M -Dorg.eclipse.jetty.LEVEL=OFF -Dorg.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog" diff --git a/configuration/sailing b/configuration/sailing new file mode 100755 index 00000000000..9a4da0b50dc --- /dev/null +++ b/configuration/sailing @@ -0,0 +1,60 @@ +#!/bin/bash +# +# sailing Starts sailing services +# +# chkconfig: 2345 95 10 +# description: Sailing contains all sailing services + +# Source function library. +. /etc/init.d/functions + +RETVAL=0 + +JAVA_START_INSTANCES="server" +GIT_REPOSITORY=/home/sailing/code + +start_tmux() { + su - sailing -c "/home/sailing/bin/tmuxConsole.sh unattended" + success +} + +start_servers() { + for conf in $JAVA_START_INSTANCES; do + echo "Checking for amazon update..." + su - sailing -c "cd /home/sailing/servers/$conf && $GIT_REPOSITORY/java/target/refreshInstance.sh auto-install" 2>/var/log/sailing.err >/var/log/sailing.out + echo "Starting Java server $conf" + su - sailing -c "cd /home/sailing/servers/$conf && ./start" 2>/var/log/sailing.err >/var/log/sailing.out + RETVAL=$? + [ $RETVAL -eq 0 ] && success || failure + done +} + +stop_servers() { + for conf in $JAVA_START_INSTANCES; do + echo "Stopping Java server $conf" + su - sailing -c "cd /home/sailing/servers/$conf && ./stop" + RETVAL=$? + [ $RETVAL -eq 0 ] && success || failure + done + +} + +# See how we were called. +case "$1" in + start) + start_servers + ;; + stop) + stop_servers + ;; + status) + status java + RETVAL=$? + ;; + *) + echo $"Usage: $0 {start|status|stop}" + RETVAL=3 +esac + +exit $RETVAL + diff --git a/home.md b/home.md index c84bd2926fa..6d1afff61f2 100644 --- a/home.md +++ b/home.md @@ -34,7 +34,7 @@ Like businesses, sailors need the latest information to make strategic decisions * [[Architecture and Infrastructure|wiki/architecture-and-infrastructure]] * [[Sailing Domain Algorithms|wiki/sailing-domain-algorithms]] * [[Inventory|wiki/inventar-liste]] - * [[Smartphone Tracking|wiki/smartphone-tracking]] + * [[RaceLog tracking|wiki/racelog-tracking]] * [[Racecommittee App|wiki/racecommittee-app]] * [[Web Services API|wiki/sailing-webservices]] * [[Theses (Bachelor, Master, ...)|wiki/theses]] diff --git a/java/com.sap.sailing.domain.common/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.common/META-INF/MANIFEST.MF index cf73d6c518b..a0f964ee4ef 100755 --- a/java/com.sap.sailing.domain.common/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.common/META-INF/MANIFEST.MF @@ -14,5 +14,8 @@ Export-Package: com.sap.sailing.domain.common, com.sap.sailing.domain.common.quadtree, com.sap.sailing.domain.common.quadtree.impl;x-friends:="com.sap.sailing.domain.test", com.sap.sailing.domain.common.racelog, + com.sap.sailing.domain.common.racelog.tracking, com.sap.sailing.domain.common.racelog.utils, + com.sap.sailing.domain.common.scalablevalue, + com.sap.sailing.domain.common.scalablevalue.impl, com.sap.sailing.domain.common.trackfiles diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/SailingDomain.gwt.xml b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/SailingDomain.gwt.xml index 67cbee6e8e0..366a8944444 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/SailingDomain.gwt.xml +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/SailingDomain.gwt.xml @@ -6,4 +6,7 @@ + + + diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/DetailType.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/DetailType.java index e9c6215c65a..3e2fdf8e5be 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/DetailType.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/DetailType.java @@ -17,9 +17,10 @@ public enum DetailType implements Serializable { ESTIMATED_TIME_TO_NEXT_WAYPOINT_IN_SECONDS(1, ASCENDING), VELOCITY_MADE_GOOD_IN_KNOTS(2, DESCENDING), GAP_TO_LEADER_IN_SECONDS(0, ASCENDING), GAP_CHANGE_SINCE_LEG_START_IN_SECONDS(0, ASCENDING), SIDE_TO_WHICH_MARK_AT_LEG_START_WAS_ROUNDED(0, ASCENDING), WINDWARD_DISTANCE_TO_GO_IN_METERS(0, ASCENDING), - AVERAGE_CROSS_TRACK_ERROR_IN_METERS(0, ASCENDING), RACE_DISTANCE_TRAVELED(0, ASCENDING), + AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS(0, ASCENDING), AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS(0, ASCENDING), RACE_DISTANCE_TRAVELED(0, ASCENDING), RACE_AVERAGE_SPEED_OVER_GROUND_IN_KNOTS(2, DESCENDING), RACE_GAP_TO_LEADER_IN_SECONDS(0, ASCENDING), - RACE_DISTANCE_TO_LEADER_IN_METERS(0, ASCENDING), RACE_AVERAGE_CROSS_TRACK_ERROR_IN_METERS(0, ASCENDING), + RACE_DISTANCE_TO_LEADER_IN_METERS(0, ASCENDING), RACE_AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS(0, ASCENDING), + RACE_AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS(0, ASCENDING), WINDWARD_DISTANCE_TO_OVERALL_LEADER(0, ASCENDING), START_TACK(0, ASCENDING), DISTANCE_TO_START_AT_RACE_START(1, ASCENDING), SPEED_OVER_GROUND_AT_RACE_START(1, DESCENDING), SPEED_OVER_GROUND_WHEN_PASSING_START(1, DESCENDING), DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_PASSING_START_IN_METERS(1, ASCENDING), diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/DoublePair.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/DoublePair.java new file mode 100755 index 00000000000..dff2cf90181 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/DoublePair.java @@ -0,0 +1,20 @@ +package com.sap.sailing.domain.common; + +public class DoublePair { + private final double a; + private final double b; + + public DoublePair(double a, double b) { + super(); + this.a = a; + this.b = b; + } + + public double getA() { + return a; + } + + public double getB() { + return b; + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Duration.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Duration.java index f5f6363792c..00ab5b896b0 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Duration.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Duration.java @@ -12,6 +12,7 @@ import com.sap.sailing.domain.common.impl.MillisecondsDurationImpl; */ public interface Duration extends Serializable, Comparable { + static final Duration NULL = new MillisecondsDurationImpl(0); static final Duration ONE_SECOND = new MillisecondsDurationImpl(1000); static final Duration ONE_MINUTE = ONE_SECOND.times(60); static final Duration ONE_HOUR = ONE_MINUTE.times(60); diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PassingInstruction.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PassingInstruction.java index 94f2cb097de..76433b896a4 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PassingInstruction.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PassingInstruction.java @@ -12,13 +12,13 @@ package com.sap.sailing.domain.common; */ public enum PassingInstruction { - None, - Port, - Starboard, - Gate, - Line, - Offset, - FixedBearing; + None(1, 2), + Port(1), + Starboard(1), + Gate(2), + Line(2), + Offset(2), + FixedBearing(1); public static PassingInstruction[] relevantValues() { PassingInstruction[] uiValues = new PassingInstruction[PassingInstruction.values().length - 1]; @@ -40,4 +40,15 @@ public enum PassingInstruction { return null; } + /** + * Denotes for which types of {@code ControlPoints} (defined by the number of marks they are made up of) + * this passing instruction is applicable.

+ * E.g. {@code [1,2]} denotes that the passing instruction is applicable for {@code ControlPoints} with either one + * or two marks. + */ + public final int[] applicability; + + private PassingInstruction(int... applicability) { + this.applicability = applicability; + } } \ No newline at end of file diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/ScoringSchemeType.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/ScoringSchemeType.java index b49b524d5cb..687511c2430 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/ScoringSchemeType.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/ScoringSchemeType.java @@ -1,6 +1,6 @@ package com.sap.sailing.domain.common; public enum ScoringSchemeType { - LOW_POINT, HIGH_POINT, HIGH_POINT_ESS_OVERALL, HIGH_POINT_LAST_BREAKS_TIE, - HIGH_POINT_FIRST_GETS_TEN, HIGH_POINT_FIRST_GETS_ONE, LOW_POINT_WINNER_GETS_ZERO + LOW_POINT, HIGH_POINT, HIGH_POINT_ESS_OVERALL, HIGH_POINT_LAST_BREAKS_TIE, HIGH_POINT_FIRST_GETS_TEN, + HIGH_POINT_FIRST_GETS_ONE, LOW_POINT_WINNER_GETS_ZERO, HIGH_POINT_WINNER_GETS_SIX } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/TimeRange.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/TimeRange.java new file mode 100644 index 00000000000..600474436a5 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/TimeRange.java @@ -0,0 +1,67 @@ +package com.sap.sailing.domain.common; + +import java.io.Serializable; + +public interface TimeRange extends Comparable, Serializable { + /** + * @return {@code null}, if the timepoint is at the beginning of time + */ + TimePoint from(); + + /** + * @return {@code null}, if the timepoint is at the beginning of time + */ + TimePoint to(); + + /** + * Also returns true if (either or both) the from or to timepoints are equal. + * E.g. 10-100 lies within 10-00, and 10-50 lies within 10-100. + * @param other + * @return + */ + boolean liesWithin(TimeRange other); + + /** + * Reverse operation of {@link #liesWithin(TimeRange)}. + * x.includes(y) = y.liesWithin(x) + * @param other + * @return + */ + boolean includes(TimeRange other); + + boolean includes(TimePoint timePoint); + + boolean intersects(TimeRange other); + + boolean startsBefore(TimeRange other); + + boolean endsAfter(TimeRange other); + + /** + * Is the time range open ended towards older timepoints? + */ + boolean openBeginning(); + + /** + * Is the time range open ended towards newer timepoints? + */ + boolean openEnd(); + + /** + * Get time-difference between the timepoint and this timerange. + * It will be 0 if the timepoint lies within the timerange, otherwise the time-difference + * to either the start or end of the timerange, depending on which is closer. The + * time difference is a duration that is guaranteed to be positive. + */ + Duration timeDifference(TimePoint timePoint); + + /** + * Merges the two ranges, only possible if {@code other} {@link #intersects()} this range. + */ + TimeRange union(TimeRange other); + + /** + * Intersection of the two ranges, only possible if {@code other} {@link #intersects()} this range. + */ + TimeRange intersection(TimeRange other); +} \ No newline at end of file diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java index d60d94ac02b..76050e1f12a 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java @@ -30,7 +30,7 @@ public abstract class AbstractLeaderboardDTO implements Serializable { public UUID defaultCourseAreaId; public String defaultCourseAreaName; public ScoringSchemeType scoringScheme; - public LeaderboardType type; + public LeaderboardType type; private Long delayToLiveInMillisForLatestRace; diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/CompetitorDTOImpl.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/CompetitorDTOImpl.java index 7061682af37..93bece9fbff 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/CompetitorDTOImpl.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/CompetitorDTOImpl.java @@ -23,7 +23,7 @@ public class CompetitorDTOImpl extends NamedDTO implements CompetitorDTO, Serial private String idAsString; private BoatClassDTO boatClass; - CompetitorDTOImpl() {} + public CompetitorDTOImpl() {} public CompetitorDTOImpl(String name, Color color, String twoLetterIsoCountryCode, String threeLetterIocCountryCode, String countryName, String sailID, String idAsString, BoatClassDTO boatClass) { diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardEntryDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardEntryDTO.java index b5531ace848..54710cd4c22 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardEntryDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardEntryDTO.java @@ -44,7 +44,9 @@ public class LeaderboardEntryDTO implements Serializable { public Double windwardDistanceToOverallLeaderInMeters; - public Double averageCrossTrackErrorInMeters; + public Double averageAbsoluteCrossTrackErrorInMeters; + + public Double averageSignedCrossTrackErrorInMeters; public Double distanceToStartLineFiveSecondsBeforeStartInMeters; @@ -99,7 +101,9 @@ public class LeaderboardEntryDTO implements Serializable { final int prime = 31; int result = 1; result = prime * result - + ((averageCrossTrackErrorInMeters == null) ? 0 : averageCrossTrackErrorInMeters.hashCode()); + + ((averageAbsoluteCrossTrackErrorInMeters == null) ? 0 : averageAbsoluteCrossTrackErrorInMeters.hashCode()); + result = prime * result + + ((averageSignedCrossTrackErrorInMeters == null) ? 0 : averageSignedCrossTrackErrorInMeters.hashCode()); result = prime * result + (discarded ? 1231 : 1237); result = prime * result @@ -141,10 +145,15 @@ public class LeaderboardEntryDTO implements Serializable { if (getClass() != obj.getClass()) return false; LeaderboardEntryDTO other = (LeaderboardEntryDTO) obj; - if (averageCrossTrackErrorInMeters == null) { - if (other.averageCrossTrackErrorInMeters != null) + if (averageAbsoluteCrossTrackErrorInMeters == null) { + if (other.averageAbsoluteCrossTrackErrorInMeters != null) return false; - } else if (!averageCrossTrackErrorInMeters.equals(other.averageCrossTrackErrorInMeters)) + } else if (!averageAbsoluteCrossTrackErrorInMeters.equals(other.averageAbsoluteCrossTrackErrorInMeters)) + return false; + if (averageSignedCrossTrackErrorInMeters == null) { + if (other.averageSignedCrossTrackErrorInMeters != null) + return false; + } else if (!averageSignedCrossTrackErrorInMeters.equals(other.averageSignedCrossTrackErrorInMeters)) return false; if (discarded != other.discarded) return false; diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LegEntryDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LegEntryDTO.java index e80fd7b9a0b..5e647835271 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LegEntryDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LegEntryDTO.java @@ -46,13 +46,16 @@ public class LegEntryDTO implements Serializable { public boolean finished; public Map numberOfManeuvers; public Map averageManeuverLossInMeters; - public Double averageCrossTrackErrorInMeters; + public Double averageAbsoluteCrossTrackErrorInMeters; + public Double averageSignedCrossTrackErrorInMeters; @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result - + ((averageCrossTrackErrorInMeters == null) ? 0 : averageCrossTrackErrorInMeters.hashCode()); + + ((averageAbsoluteCrossTrackErrorInMeters == null) ? 0 : averageAbsoluteCrossTrackErrorInMeters.hashCode()); + result = prime * result + + ((averageSignedCrossTrackErrorInMeters == null) ? 0 : averageSignedCrossTrackErrorInMeters.hashCode()); result = prime * result + ((averageManeuverLossInMeters == null) ? 0 : averageManeuverLossInMeters.hashCode()); result = prime * result + ((averageSpeedOverGroundInKnots == null) ? 0 : averageSpeedOverGroundInKnots.hashCode()); @@ -87,10 +90,15 @@ public class LegEntryDTO implements Serializable { if (getClass() != obj.getClass()) return false; LegEntryDTO other = (LegEntryDTO) obj; - if (averageCrossTrackErrorInMeters == null) { - if (other.averageCrossTrackErrorInMeters != null) + if (averageAbsoluteCrossTrackErrorInMeters == null) { + if (other.averageAbsoluteCrossTrackErrorInMeters != null) return false; - } else if (!averageCrossTrackErrorInMeters.equals(other.averageCrossTrackErrorInMeters)) + } else if (!averageAbsoluteCrossTrackErrorInMeters.equals(other.averageAbsoluteCrossTrackErrorInMeters)) + return false; + if (averageSignedCrossTrackErrorInMeters == null) { + if (other.averageSignedCrossTrackErrorInMeters != null) + return false; + } else if (!averageSignedCrossTrackErrorInMeters.equals(other.averageSignedCrossTrackErrorInMeters)) return false; if (averageManeuverLossInMeters == null) { if (other.averageManeuverLossInMeters != null) diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceColumnDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceColumnDTO.java index f1a6e33525f..c399d6dbdf6 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceColumnDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceColumnDTO.java @@ -22,6 +22,8 @@ public class RaceColumnDTO extends NamedDTO implements Serializable { private Boolean isValidInTotalScore; private Double explicitFactor; private double effectiveFactor; + + private Map raceLogTrackingInfos = new HashMap(); RaceColumnDTO() {} // for GWT serialization @@ -283,4 +285,12 @@ public class RaceColumnDTO extends NamedDTO implements Serializable { public double getEffectiveFactor() { return effectiveFactor; } + + public RaceLogTrackingInfoDTO getRaceLogTrackingInfo(FleetDTO fleet) { + return raceLogTrackingInfos.get(fleet); + } + + public void setRaceLogTrackingInfo(FleetDTO fleet, RaceLogTrackingInfoDTO info) { + raceLogTrackingInfos.put(fleet, info); + } } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceLogTrackingInfoDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceLogTrackingInfoDTO.java new file mode 100644 index 00000000000..5a424d1412b --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceLogTrackingInfoDTO.java @@ -0,0 +1,21 @@ +package com.sap.sailing.domain.common.dto; + +import java.io.Serializable; + +import com.sap.sailing.domain.common.racelog.tracking.RaceLogTrackingState; + +public class RaceLogTrackingInfoDTO implements Serializable { + private static final long serialVersionUID = -6861382173074620168L; + public boolean raceLogTrackerExists; + public boolean competitorRegistrationsExists; + public RaceLogTrackingState raceLogTrackingState; + + protected RaceLogTrackingInfoDTO() {} + + public RaceLogTrackingInfoDTO(boolean raceLogTrackerExists, boolean competitorRegistrationsExists, + RaceLogTrackingState raceLogTrackingState) { + this.raceLogTrackerExists = raceLogTrackerExists; + this.competitorRegistrationsExists = competitorRegistrationsExists; + this.raceLogTrackingState = raceLogTrackingState; + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/AbstractTimePoint.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/AbstractTimePoint.java index a2177caedcc..a2be3e6e4d0 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/AbstractTimePoint.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/AbstractTimePoint.java @@ -1,112 +1,113 @@ -package com.sap.sailing.domain.common.impl; - -import java.util.Comparator; -import java.util.Date; - -import com.sap.sailing.domain.common.Duration; -import com.sap.sailing.domain.common.TimePoint; - -public abstract class AbstractTimePoint implements TimePoint { - private static final long serialVersionUID = 8825508619301420378L; - - public static Comparator TIMEPOINT_COMPARATOR = new SerializableComparator() { - private static final long serialVersionUID = 7644726881387366025L; - - @Override - public int compare(TimePoint o1, TimePoint o2) { - long milliDiff = o1.asMillis() - o2.asMillis(); - return milliDiff<0 ? -1 : milliDiff == 0 ? 0 : 1; - } - }; - - @Override - public int compareTo(TimePoint o) { - return TIMEPOINT_COMPARATOR.compare(this, o); - } - - @Override - public Date asDate() { - Date result = getDateFromCache(); - if (result == null) { - result = new Date(asMillis()); - cacheDate(result); - } - return result; - } - - @Override - public TimePoint plus(Duration duration) { - final TimePoint result; - if (duration != null) { - result = plus(duration.asMillis()); - } else { - result = this; - } - return result; - } - - @Override - public TimePoint minus(Duration duration) { - final TimePoint result; - if (duration != null) { - result = minus(duration.asMillis()); - } else { - result = this; - } - return result; - } - - @Override - public Duration until(TimePoint later) { - return new MillisecondsDurationImpl(later.asMillis()-asMillis()); - } - - @Override - public TimePoint plus(long milliseconds) { - return new MillisecondsTimePoint(asMillis()+milliseconds); - } - - @Override - public TimePoint minus(long milliseconds) { - return plus(-milliseconds); - } - - @Override - public boolean after(TimePoint other) { - return this.compareTo(other) > 0; - } - - @Override - public boolean before(TimePoint other) { - return this.compareTo(other) < 0; - } - - @Override - public int hashCode() { - return (int) (asMillis() & Integer.MAX_VALUE); - } - - @Override - public boolean equals(Object obj) { - return compareTo((TimePoint) obj) == 0; - } - - @Override - public String toString() { - return asDate().toString(); - } - - /** - * A subclass may implement a cache for the {@link Date} representation by overriding this method as well - * as {@link #cacheDate(Date)}. - */ - protected Date getDateFromCache() { - return null; - } - - /** - * A subclass may implement a cache for the {@link Date} representation by overriding this method as well - * as {@link #getDate()}. - */ - protected void cacheDate(Date date) {} -} +package com.sap.sailing.domain.common.impl; + +import java.util.Comparator; +import java.util.Date; + +import com.sap.sailing.domain.common.Duration; +import com.sap.sailing.domain.common.TimePoint; + +public abstract class AbstractTimePoint implements TimePoint { + private static final long serialVersionUID = 8825508619301420378L; + + public static Comparator TIMEPOINT_COMPARATOR = new SerializableComparator() { + private static final long serialVersionUID = 7644726881387366025L; + + @Override + public int compare(TimePoint o1, TimePoint o2) { + long o1Millis = o1.asMillis(); + long o2Millis = o2.asMillis(); + return o1Millis 0; + } + + @Override + public boolean before(TimePoint other) { + return this.compareTo(other) < 0; + } + + @Override + public int hashCode() { + return (int) (asMillis() & Integer.MAX_VALUE); + } + + @Override + public boolean equals(Object obj) { + return compareTo((TimePoint) obj) == 0; + } + + @Override + public String toString() { + return asDate().toString(); + } + + /** + * A subclass may implement a cache for the {@link Date} representation by overriding this method as well + * as {@link #cacheDate(Date)}. + */ + protected Date getDateFromCache() { + return null; + } + + /** + * A subclass may implement a cache for the {@link Date} representation by overriding this method as well + * as {@link #getDate()}. + */ + protected void cacheDate(Date date) {} +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/TimeRangeImpl.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/TimeRangeImpl.java new file mode 100644 index 00000000000..88606696347 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/TimeRangeImpl.java @@ -0,0 +1,121 @@ +package com.sap.sailing.domain.common.impl; + +import com.sap.sailing.domain.common.Duration; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.TimeRange; +import com.sap.sailing.domain.common.impl.Util.Pair; + +public class TimeRangeImpl extends Pair implements TimeRange { + public static final TimePoint BeginningOfTime = new MillisecondsTimePoint(Long.MIN_VALUE); + public static final TimePoint EndOfTime = new MillisecondsTimePoint(Long.MAX_VALUE); + private static final long serialVersionUID = 8710198176227507300L; + + public static TimeRange create(long fromMillis, long toMillis) { + return new TimeRangeImpl(new MillisecondsTimePoint(fromMillis), new MillisecondsTimePoint(toMillis)); + } + + public TimeRangeImpl(TimePoint from, TimePoint to) { + super(from == null ? BeginningOfTime : from, to == null ? EndOfTime : to); + if (from().after(to())) throw new IllegalArgumentException(String.format("from (%s) must lie before to (%s) in a TimeRange", from(), to())); + } + + @Override + public int compareTo(TimeRange other) { + if (other.from().equals(from()) && other.to().equals(to())) return 0; + return startsBefore(other) ? -1 : 1; + } + + @Override + public TimePoint from() { + return getA(); + } + + @Override + public TimePoint to() { + return getB(); + } + + @Override + public boolean liesWithin(TimeRange other) { + return from().compareTo(other.from()) >= 0 && to().compareTo(other.to()) <= 0; + } + + @Override + public boolean includes(TimeRange other) { + return other.liesWithin(this); + } + + @Override + public boolean intersects(TimeRange other) { + return includes(other.from()) || includes(other.to()) || liesWithin(other); + } + + @Override + public boolean includes(TimePoint timePoint) { + return from().compareTo(timePoint) <= 0 && to().compareTo(timePoint) >= 0; + } + + @Override + public boolean startsBefore(TimeRange other) { + return from().before(other.from()); + } + + @Override + public boolean endsAfter(TimeRange other) { + return to().after(other.to()); + } + + @Override + public Duration timeDifference(TimePoint timePoint) { + final Duration result; + if (includes(timePoint)) { + result = Duration.NULL; + } else if (timePoint.before(from())) { + result = timePoint.until(from()); + } else { + result = to().until(timePoint); + } + return result; + } + + @Override + public TimeRange union(TimeRange other) { + if (! intersects(other)) return null; + TimePoint newFrom = startsBefore(other) ? from() : other.from(); + TimePoint newTo = endsAfter(other) ? to() : other.to(); + return new TimeRangeImpl(newFrom, newTo); + } + + @Override + public boolean openBeginning() { + return from().equals(BeginningOfTime); + } + + @Override + public boolean openEnd() { + return to().equals(EndOfTime); + } + + @Override + public TimeRange intersection(TimeRange other) { + if (! intersects(other)) return null; + TimePoint newFrom = startsBefore(other) ? other.from() : from(); + TimePoint newTo = endsAfter(other) ? other.to() : to(); + return new TimeRangeImpl(newFrom, newTo); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj instanceof TimeRange) { + TimeRange other = (TimeRange) obj; + return from().equals(other.from()) && to().equals(other.to()); + } + return super.equals(obj); + } + + @Override + public String toString() { + return from() + "-" + to(); + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/NoCorrespondingServiceRegisteredException.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/NoCorrespondingServiceRegisteredException.java new file mode 100644 index 00000000000..daad2f61a18 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/NoCorrespondingServiceRegisteredException.java @@ -0,0 +1,24 @@ +package com.sap.sailing.domain.common.racelog.tracking; + +public class NoCorrespondingServiceRegisteredException extends RuntimeException { + public String type; + public String serviceInterface; + private static final long serialVersionUID = -358955216089477585L; + + protected NoCorrespondingServiceRegisteredException() {} + + public NoCorrespondingServiceRegisteredException(String message, String type, String serviceInterface) { + super(message); + this.type = type; + this.serviceInterface = serviceInterface; + } + + public String getDeviceType() { + return type; + } + + @Override + public String toString() { + return super.toString() + " (Service: " + serviceInterface + ", Type: " + type + ")"; + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/NotDenotableForRaceLogTrackingException.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/NotDenotableForRaceLogTrackingException.java new file mode 100644 index 00000000000..296f4e29659 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/NotDenotableForRaceLogTrackingException.java @@ -0,0 +1,14 @@ +package com.sap.sailing.domain.common.racelog.tracking; + +public class NotDenotableForRaceLogTrackingException extends Exception { + + private static final long serialVersionUID = 6721038282862173471L; + + public NotDenotableForRaceLogTrackingException() { + super(); + } + + public NotDenotableForRaceLogTrackingException(String msg) { + super(msg); + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/NotDenotedForRaceLogTrackingException.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/NotDenotedForRaceLogTrackingException.java new file mode 100644 index 00000000000..badfccb5523 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/NotDenotedForRaceLogTrackingException.java @@ -0,0 +1,14 @@ +package com.sap.sailing.domain.common.racelog.tracking; + +public class NotDenotedForRaceLogTrackingException extends Exception { + + private static final long serialVersionUID = 6721038282862173471L; + + public NotDenotedForRaceLogTrackingException() { + super(); + } + + public NotDenotedForRaceLogTrackingException(String msg) { + super(msg); + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/NotRevokableException.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/NotRevokableException.java new file mode 100644 index 00000000000..f30eda4a13b --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/NotRevokableException.java @@ -0,0 +1,13 @@ +package com.sap.sailing.domain.common.racelog.tracking; + +public class NotRevokableException extends Exception { + private static final long serialVersionUID = 6825451915706047105L; + + public NotRevokableException() { + super(); + } + + public NotRevokableException(String msg) { + super(msg); + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/RaceLogRaceTrackerExistsException.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/RaceLogRaceTrackerExistsException.java new file mode 100644 index 00000000000..6f1bb16b2db --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/RaceLogRaceTrackerExistsException.java @@ -0,0 +1,14 @@ +package com.sap.sailing.domain.common.racelog.tracking; + +public class RaceLogRaceTrackerExistsException extends RuntimeException { + + private static final long serialVersionUID = 6721038282862173471L; + + public RaceLogRaceTrackerExistsException() { + super(); + } + + public RaceLogRaceTrackerExistsException(String raceLogName) { + super("Tracker for racelog " + raceLogName + " already exists"); + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/RaceLogTrackingState.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/RaceLogTrackingState.java new file mode 100644 index 00000000000..9f18093d622 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/RaceLogTrackingState.java @@ -0,0 +1,30 @@ +package com.sap.sailing.domain.common.racelog.tracking; + + +/** + * The state of the race tracked via the racelog. This can be determined by inspecting the corresponding events + * in the racelog with the {@link RaceLogTrackingStateAnalyzer}. + * @author Fredrik Teschke + * + */ +public enum RaceLogTrackingState { + //can't determine whether tracking has ended from race log, therefore omitting TRACKING_ENDED state for now + /** + * no {@link DenoteForTrackingEvent} is present + */ + NOT_A_RACELOG_TRACKED_RACE, + + /** + * a {@link DenoteForTrackingEvent} is present, but no {@link StartTrackingEvent} yet. + */ + AWAITING_RACE_DEFINITION, + + /** + * a {@link StartTrackingEvent} is present + */ + TRACKING; + + public boolean isForTracking() { + return this != NOT_A_RACELOG_TRACKED_RACE; + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/RaceNotCreatedException.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/RaceNotCreatedException.java new file mode 100644 index 00000000000..b3b7507efc4 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/RaceNotCreatedException.java @@ -0,0 +1,14 @@ +package com.sap.sailing.domain.common.racelog.tracking; + +public class RaceNotCreatedException extends RuntimeException { + + private static final long serialVersionUID = 6721038282862173471L; + + public RaceNotCreatedException() { + super(); + } + + public RaceNotCreatedException(String msg) { + super(msg); + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/SingleTypeBasedServiceFinderImpl.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/SingleTypeBasedServiceFinderImpl.java new file mode 100644 index 00000000000..1ea432216c9 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/SingleTypeBasedServiceFinderImpl.java @@ -0,0 +1,37 @@ +package com.sap.sailing.domain.common.racelog.tracking; + +/** + * Only can provide the service for a single {@code type}, e.g. on smartphones with no OSGi context. + * Optionally, a {@link #setFallbackService(Object) fallback} can be set. + * @author Fredrik Teschke + */ +public class SingleTypeBasedServiceFinderImpl implements +TypeBasedServiceFinder { + private final ServiceType service; + private final String type; + private ServiceType fallback; + + public SingleTypeBasedServiceFinderImpl(ServiceType service, ServiceType fallback, String type) { + this.service = service; + this.type = type; + this.fallback = fallback; + } + + public SingleTypeBasedServiceFinderImpl(ServiceType service, String type) { + this(service, null, type); + } + + @Override + public ServiceType findService(String type) + throws NoCorrespondingServiceRegisteredException { + if (type.equals(this.type)) return service; + if (fallback != null) return fallback; + throw new NoCorrespondingServiceRegisteredException("Only one service registered", type, service.getClass().getSimpleName()); + } + + @Override + public void setFallbackService(ServiceType fallback) { + this.fallback = fallback; + } + +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/TransformationException.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/TransformationException.java new file mode 100644 index 00000000000..0a63c6d8b5e --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/TransformationException.java @@ -0,0 +1,30 @@ +package com.sap.sailing.domain.common.racelog.tracking; + +import java.io.IOException; + +public class TransformationException extends IOException { + private static final long serialVersionUID = -7795777645371428971L; + + public TransformationException() { + } + + public TransformationException(Class from, Class to, String at, Throwable e) { + super("Couldn't transform "+from.getName()+" into "+to.getName()+" at "+at, e); + } + + public TransformationException(Class from, Class to, String at) { + this(from, to, at, null); + } + + public TransformationException(String message, Throwable e) { + super(message, e); + } + + public TransformationException(Throwable e) { + super(e); + } + + public TransformationException(String message) { + super(message); + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/TransformationHandler.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/TransformationHandler.java new file mode 100644 index 00000000000..ed5cb641218 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/TransformationHandler.java @@ -0,0 +1,13 @@ +package com.sap.sailing.domain.common.racelog.tracking; + +/** + * Handle transformation between two types. Most likely use-case is serialization, + * e.g. for JSON documents or database persistence. + * + * @author Fredrik Teschke + * + */ +public interface TransformationHandler { + T1 transformBack(T2 object) throws TransformationException; + T2 transformForth(T1 object) throws TransformationException; +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/TypeBasedServiceFinder.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/TypeBasedServiceFinder.java new file mode 100644 index 00000000000..33f8ac77c84 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/TypeBasedServiceFinder.java @@ -0,0 +1,29 @@ +package com.sap.sailing.domain.common.racelog.tracking; + +/** + * @author Fredrik Teschke + * + * @param Specifies desired the service interface + */ +public interface TypeBasedServiceFinder { + public static final String TYPE = "type"; + + /** + * Find a service that implements the interface for the specified {@code type} + * of object (e.g. type of a {@link GPSFix}). + * @param type A {@link String} that specifies for which type the service should be registered + * @throws NoCorrespondingServiceRegisteredException Thrown, if no service is registered for this type. + */ + ServiceType findService(String type) throws NoCorrespondingServiceRegisteredException; + + /** + * Sets a fallback service (optional), that is used if no service is registered for a {@code type}. + * This should be a generic service, that can deal with any {@code type}, and provides a + * "better-than-nothing" implementation, which will be returned instead of throwing a + * {@link NoCorrespondingServiceRegisteredException}. + *

+ * This method does not need to be implemented, but can remain a stub, if no fallback behaviour + * is desired. + */ + void setFallbackService(ServiceType fallback); +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/TypeBasedServiceFinderFactory.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/TypeBasedServiceFinderFactory.java new file mode 100644 index 00000000000..2b03570cf5a --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/TypeBasedServiceFinderFactory.java @@ -0,0 +1,18 @@ +package com.sap.sailing.domain.common.racelog.tracking; + + +/** + * A factory for creating {@link TypeBasedServiceFinder}s, for a specific type of service. + * The desired service type is specified by the {@link Class} object passed to the + * {@link #createServiceFinder(Class)} method. + * + * The created {@link TypeBasedServiceFinder} can then be used to find the desired + * service for a specific type. E.g., there may be a persistence service interface for + * {@link GPSFix}es, and there are several different service registered, depending on the type + * of {@link GPSFix}. The service finder then can find the appropriate service for each fix. + * @author Fredrik Teschke + * + */ +public interface TypeBasedServiceFinderFactory { + TypeBasedServiceFinder createServiceFinder(Class clazz); +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/AbstractScalarValue.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/AbstractScalarValue.java similarity index 83% rename from java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/AbstractScalarValue.java rename to java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/AbstractScalarValue.java index 8866576e8aa..c14cc01883c 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/AbstractScalarValue.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/AbstractScalarValue.java @@ -1,4 +1,5 @@ -package com.sap.sailing.domain.confidence; +package com.sap.sailing.domain.common.scalablevalue; + /** * A {@link ScalableValue} where the ValueType and the AveragesTo type are both the same because diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/IsScalable.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/IsScalable.java similarity index 73% rename from java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/IsScalable.java rename to java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/IsScalable.java index cf5d21aa3bd..4789e8a2a3d 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/IsScalable.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/IsScalable.java @@ -1,4 +1,4 @@ -package com.sap.sailing.domain.confidence; +package com.sap.sailing.domain.common.scalablevalue; import java.io.Serializable; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/ScalableValue.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/ScalableValue.java similarity index 81% rename from java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/ScalableValue.java rename to java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/ScalableValue.java index a65e5696786..61ae9bbf7ca 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/ScalableValue.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/ScalableValue.java @@ -1,4 +1,4 @@ -package com.sap.sailing.domain.confidence; +package com.sap.sailing.domain.common.scalablevalue; public interface ScalableValue { ScalableValue multiply(double factor); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/ScalableBearing.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalableBearing.java similarity index 91% rename from java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/ScalableBearing.java rename to java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalableBearing.java index 5fb345832fc..bc9985de895 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/ScalableBearing.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalableBearing.java @@ -1,9 +1,9 @@ -package com.sap.sailing.domain.base.impl; +package com.sap.sailing.domain.common.scalablevalue.impl; -import com.sap.sailing.domain.base.BearingWithConfidence.DoublePair; import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.DoublePair; import com.sap.sailing.domain.common.impl.RadianBearingImpl; -import com.sap.sailing.domain.confidence.ScalableValue; +import com.sap.sailing.domain.common.scalablevalue.ScalableValue; public class ScalableBearing implements ScalableValue { private final double sin; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/ScalablePosition.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalablePosition.java similarity index 91% rename from java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/ScalablePosition.java rename to java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalablePosition.java index 17d41414d81..fe37ec0f32e 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/ScalablePosition.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalablePosition.java @@ -1,8 +1,8 @@ -package com.sap.sailing.domain.base.impl; +package com.sap.sailing.domain.common.scalablevalue.impl; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.impl.RadianPosition; -import com.sap.sailing.domain.confidence.ScalableValue; +import com.sap.sailing.domain.common.scalablevalue.ScalableValue; public class ScalablePosition implements ScalableValue { private final double x, y, z; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/ScalableSpeed.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalableSpeed.java similarity index 87% rename from java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/ScalableSpeed.java rename to java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalableSpeed.java index 9093d34f8f8..6165e0acf56 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/ScalableSpeed.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalableSpeed.java @@ -1,8 +1,8 @@ -package com.sap.sailing.domain.base.impl; +package com.sap.sailing.domain.common.scalablevalue.impl; import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.impl.KnotSpeedImpl; -import com.sap.sailing.domain.confidence.ScalableValue; +import com.sap.sailing.domain.common.scalablevalue.ScalableValue; public class ScalableSpeed implements ScalableValue { private final double knots; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/ScalableSpeedWithBearing.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalableSpeedWithBearing.java similarity index 93% rename from java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/ScalableSpeedWithBearing.java rename to java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalableSpeedWithBearing.java index 03694b57c8b..a18a1294fb5 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/ScalableSpeedWithBearing.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/scalablevalue/impl/ScalableSpeedWithBearing.java @@ -1,4 +1,4 @@ -package com.sap.sailing.domain.base.impl; +package com.sap.sailing.domain.common.scalablevalue.impl; import com.sap.sailing.domain.common.Bearing; import com.sap.sailing.domain.common.Speed; @@ -7,7 +7,7 @@ import com.sap.sailing.domain.common.impl.KnotSpeedImpl; import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.common.impl.RadianBearingImpl; import com.sap.sailing.domain.common.impl.Util.Triple; -import com.sap.sailing.domain.confidence.ScalableValue; +import com.sap.sailing.domain.common.scalablevalue.ScalableValue; /** * Separately scales speed and bearing. Instead of considering speed and bearing a single vector that can be scaled, the diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/trackfiles/TrackFilesDataSource.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/trackfiles/TrackFilesDataSource.java index 44c5be215a6..173e168cb52 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/trackfiles/TrackFilesDataSource.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/trackfiles/TrackFilesDataSource.java @@ -1,5 +1,5 @@ package com.sap.sailing.domain.common.trackfiles; public enum TrackFilesDataSource { - METADATA, COMPETITORS, BUOYS, WIND, MANEUVERS + METADATA, COMPETITORS, BUOYS, WIND, MANEUVERS } diff --git a/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/shared/IgtimiWindReceiver.java b/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/shared/IgtimiWindReceiver.java index 0ef67dd0e93..2433265bb0f 100755 --- a/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/shared/IgtimiWindReceiver.java +++ b/java/com.sap.sailing.domain.igtimiadapter/src/com/sap/sailing/domain/igtimiadapter/shared/IgtimiWindReceiver.java @@ -13,9 +13,6 @@ import java.util.logging.Logger; import com.sap.sailing.declination.Declination; import com.sap.sailing.declination.DeclinationService; import com.sap.sailing.domain.base.Timed; -import com.sap.sailing.domain.base.impl.ScalableBearing; -import com.sap.sailing.domain.base.impl.ScalablePosition; -import com.sap.sailing.domain.base.impl.ScalableSpeed; import com.sap.sailing.domain.common.Bearing; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.Speed; @@ -24,7 +21,10 @@ import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.common.impl.Util; import com.sap.sailing.domain.common.impl.Util.Pair; -import com.sap.sailing.domain.confidence.ScalableValue; +import com.sap.sailing.domain.common.scalablevalue.ScalableValue; +import com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing; +import com.sap.sailing.domain.common.scalablevalue.impl.ScalablePosition; +import com.sap.sailing.domain.common.scalablevalue.impl.ScalableSpeed; import com.sap.sailing.domain.igtimiadapter.BulkFixReceiver; import com.sap.sailing.domain.igtimiadapter.IgtimiConnection; import com.sap.sailing.domain.igtimiadapter.IgtimiFixReceiverAdapter; diff --git a/java/com.sap.sailing.domain.persistence/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.persistence/META-INF/MANIFEST.MF index a6b892b6d84..815202fbdea 100755 --- a/java/com.sap.sailing.domain.persistence/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.persistence/META-INF/MANIFEST.MF @@ -13,8 +13,20 @@ Require-Bundle: com.sap.sailing.mongodb, com.sap.sailing.domain.shared.android, com.sap.sailing.server.gateway.serialization;bundle-version="1.0.0" Export-Package: com.sap.sailing.domain.persistence, - com.sap.sailing.domain.persistence.impl;x-friends:="com.sap.sailing.mongodb.test,com.sap.sailing.domain.swisstimingadapter.test,com.sap.sailing.domain.persistence.test", - com.sap.sailing.domain.persistence.media + com.sap.sailing.domain.persistence.impl; + x-friends:="com.sap.sailing.domain.persistence.test, + com.sap.sailing.domain.swisstimingadapter.test, + com.sap.sailing.mongodb.test, + com.sap.sailing.server.gateway.test, + com.sap.sailing.domain.racelogtrackingadapter.testsupport, + com.sap.sailing.domain.racelogtrackingadapter.test", + com.sap.sailing.domain.persistence.media, + com.sap.sailing.domain.persistence.racelog.tracking, + com.sap.sailing.domain.persistence.racelog.tracking.impl; + x-friends:="com.sap.sailing.domain.racelogtrackingadapter.testsupport, + com.sap.sailing.mongodb.test, + com.sap.sailing.domain.racelogtrackingadapter.test, + com.sap.sailing.server" Bundle-ActivationPolicy: lazy Import-Package: org.json.simple, org.json.simple.parser, diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/MongoObjectFactory.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/MongoObjectFactory.java index 556171816cd..181238ca3e0 100755 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/MongoObjectFactory.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/MongoObjectFactory.java @@ -1,115 +1,114 @@ -package com.sap.sailing.domain.persistence; - -import java.io.Serializable; - -import com.mongodb.DB; -import com.mongodb.DBObject; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.Event; -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.configuration.DeviceConfiguration; -import com.sap.sailing.domain.base.configuration.DeviceConfigurationMatcher; -import com.sap.sailing.domain.common.WindSource; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.leaderboard.LeaderboardGroup; -import com.sap.sailing.domain.racelog.RaceLogIdentifier; -import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sailing.domain.tracking.TrackedRegatta; - -/** - * Offers methods to construct {@link DBObject MongoDB objects} from domain objects. - * - * @author Axel Uhl (d043530) - * - */ -public interface MongoObjectFactory { - /** - * Registers for changes of the wind coming from windSource on the trackedRace. Each - * update received will be appended to the MongoDB and can later be retrieved. The key used to identify the race is - * the {@link RaceDefinition#getName() race name} and the {@link Regatta#getName() regatta name}. - */ - void addWindTrackDumper(TrackedRegatta trackedRegatta, TrackedRace trackedRace, WindSource windSource); - - /** - * Stores the configuration data of leaderboard in the Mongo DB associated with this - * factory. - */ - void storeLeaderboard(Leaderboard leaderboard); - - /** - * Removes the leaderboard named name from the database. - */ - void removeLeaderboard(String leaderboardName); - - void renameLeaderboard(String oldName, String newName); - - /** - * Stores the group, if it doesn't exist or updates it. Leaderboards in the group, which aren't stored in the - * database, will be stored. If the leaderboard group has an {@link LeaderboardGroup#getOverallLeaderboard() overall - * leaderboard}, it will be stored / updated as well. - */ - void storeLeaderboardGroup(LeaderboardGroup leaderboardGroup); - - /** - * Removes the group with the name groupName from the database. - */ - void removeLeaderboardGroup(String groupName); - - /** - * Renames the group with the name oldName. - */ - void renameLeaderboardGroup(String oldName, String newName); - - /** - * Stores the event with its name, venue and the venue's course areas. The regattas obtained by - * {@link Event#getRegattas()} are not stored by this call. They need to be stored separately - * by calls to {@link #storeRegatta} where a reference to their owning event is stored. - */ - void storeEvent(Event event); - - /** - * Renames the event with the name oldName. - */ - void renameEvent(Serializable id, String newName); - - /** - * Removes the event named eventName from the database. - */ - void removeEvent(Serializable id); - - /** - * Stores the regatta together with its name, {@link Series} definitions and an optional link to the - * {@link Event} to which the regatta belongs. - * @param oldSeriesNameNewName - */ - void storeRegatta(Regatta regatta); - - void removeRegatta(Regatta regatta); - - void storeRegattaForRaceID(String id, Regatta regatta); - - void removeRegattaForRaceID(String raceIDAsString, Regatta regatta); - - /** - * Stores a competitor, including the team and boat. This should not be done for competitors for which - * the master data is supplied by other systems, such as TracTrac, but rather for smartphone tracking, - * where this data is otherwise not recoverable. - * @param competitor the competitor to store/update in the database - */ - void storeCompetitor(Competitor competitor); - - void removeAllCompetitors(); - - void removeCompetitor(Competitor competitor); - - DB getDatabase(); - - void storeDeviceConfiguration(DeviceConfigurationMatcher matcher, DeviceConfiguration configuration); - - void removeDeviceConfiguration(DeviceConfigurationMatcher matcher); - - void removeRaceLog(RaceLogIdentifier identifier); - -} +package com.sap.sailing.domain.persistence; + +import java.io.Serializable; + +import com.mongodb.DB; +import com.mongodb.DBObject; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Event; +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.configuration.DeviceConfiguration; +import com.sap.sailing.domain.base.configuration.DeviceConfigurationMatcher; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.LeaderboardGroup; +import com.sap.sailing.domain.racelog.RaceLogIdentifier; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.domain.tracking.TrackedRegatta; + +/** + * Offers methods to construct {@link DBObject MongoDB objects} from domain objects. + * + * @author Axel Uhl (d043530) + * + */ +public interface MongoObjectFactory { + /** + * Registers for changes of the wind coming from windSource on the trackedRace. Each + * update received will be appended to the MongoDB and can later be retrieved. The key used to identify the race is + * the {@link RaceDefinition#getName() race name} and the {@link Regatta#getName() regatta name}. + */ + void addWindTrackDumper(TrackedRegatta trackedRegatta, TrackedRace trackedRace, WindSource windSource); + + /** + * Stores the configuration data of leaderboard in the Mongo DB associated with this + * factory. + */ + void storeLeaderboard(Leaderboard leaderboard); + + /** + * Removes the leaderboard named name from the database. + */ + void removeLeaderboard(String leaderboardName); + + void renameLeaderboard(String oldName, String newName); + + /** + * Stores the group, if it doesn't exist or updates it. Leaderboards in the group, which aren't stored in the + * database, will be stored. If the leaderboard group has an {@link LeaderboardGroup#getOverallLeaderboard() overall + * leaderboard}, it will be stored / updated as well. + */ + void storeLeaderboardGroup(LeaderboardGroup leaderboardGroup); + + /** + * Removes the group with the name groupName from the database. + */ + void removeLeaderboardGroup(String groupName); + + /** + * Renames the group with the name oldName. + */ + void renameLeaderboardGroup(String oldName, String newName); + + /** + * Stores the event with its name, venue and the venue's course areas. The regattas obtained by + * {@link Event#getRegattas()} are not stored by this call. They need to be stored separately + * by calls to {@link #storeRegatta} where a reference to their owning event is stored. + */ + void storeEvent(Event event); + + /** + * Renames the event with the name oldName. + */ + void renameEvent(Serializable id, String newName); + + /** + * Removes the event named eventName from the database. + */ + void removeEvent(Serializable id); + + /** + * Stores the regatta together with its name, {@link Series} definitions and an optional link to the + * {@link Event} to which the regatta belongs. + * @param oldSeriesNameNewName + */ + void storeRegatta(Regatta regatta); + + void removeRegatta(Regatta regatta); + + void storeRegattaForRaceID(String id, Regatta regatta); + + void removeRegattaForRaceID(String raceIDAsString, Regatta regatta); + + /** + * Stores a competitor, including the team and boat. This should not be done for competitors for which + * the master data is supplied by other systems, such as TracTrac, but rather for smartphone tracking, + * where this data is otherwise not recoverable. + * @param competitor the competitor to store/update in the database + */ + void storeCompetitor(Competitor competitor); + + void removeAllCompetitors(); + + void removeCompetitor(Competitor competitor); + + DB getDatabase(); + + void storeDeviceConfiguration(DeviceConfigurationMatcher matcher, DeviceConfiguration configuration); + + void removeDeviceConfiguration(DeviceConfigurationMatcher matcher); + void removeRaceLog(RaceLogIdentifier identifier); + +} diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/PersistenceFactory.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/PersistenceFactory.java index 9c07d6e31cd..c70f7cf4348 100755 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/PersistenceFactory.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/PersistenceFactory.java @@ -1,6 +1,7 @@ package com.sap.sailing.domain.persistence; import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinderFactory; import com.sap.sailing.domain.persistence.impl.PersistenceFactoryImpl; import com.sap.sailing.mongodb.MongoDBService; @@ -9,6 +10,9 @@ public interface PersistenceFactory { DomainObjectFactory getDefaultDomainObjectFactory(); DomainObjectFactory getDomainObjectFactory(MongoDBService mongoDBService, DomainFactory baseDomainFactory); + DomainObjectFactory getDomainObjectFactory(MongoDBService mongoDBService, DomainFactory baseDomainFactory, TypeBasedServiceFinderFactory serviceFinderFactory); MongoObjectFactory getDefaultMongoObjectFactory(); MongoObjectFactory getMongoObjectFactory(MongoDBService mongoDBService); + MongoObjectFactory getDefaultMongoObjectFactory(TypeBasedServiceFinderFactory serviceFinderFactory); + MongoObjectFactory getMongoObjectFactory(MongoDBService mongoDBService, TypeBasedServiceFinderFactory serviceFinderFactory); } diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/Activator.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/Activator.java index f0597d3226f..14452399bfa 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/Activator.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/Activator.java @@ -6,14 +6,13 @@ import org.osgi.framework.BundleContext; import com.sap.sailing.mongodb.MongoDBService; public class Activator implements BundleActivator { - @Override - public void start(BundleContext context) throws Exception { - for (CollectionNames name : CollectionNames.values()) - MongoDBService.INSTANCE.registerExclusively(CollectionNames.class, name.name()); - } - - @Override - public void stop(BundleContext context) throws Exception { - } + @Override + public void start(BundleContext context) throws Exception { + for (CollectionNames name : CollectionNames.values()) + MongoDBService.INSTANCE.registerExclusively(CollectionNames.class, name.name()); + } + @Override + public void stop(BundleContext context) throws Exception { + } } diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/CollectionNames.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/CollectionNames.java index 32e658d47db..2cfa54a0625 100755 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/CollectionNames.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/CollectionNames.java @@ -25,7 +25,7 @@ public enum CollectionNames { LEADERBOARD_GROUPS, /** - * Top-level event information about events such as Kiel Week 2011, or IDM Travemünde 2011, including name and + * Top-level event information about events such as Kiel Week 2011, or IDM Travem�nde 2011, including name and * course areas. */ EVENTS, @@ -62,5 +62,10 @@ public enum CollectionNames { /** * Stores configurations for mobile devices. */ - CONFIGURATIONS; + CONFIGURATIONS, + + /** + * Stores {@link GPSFix}es + */ + GPS_FIXES, } diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java index 510a433dc19..4c92f6a1cdf 100755 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java @@ -1,1487 +1,1629 @@ -package com.sap.sailing.domain.persistence.impl; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.UUID; -import java.util.logging.Level; -import java.util.logging.Logger; - -import org.bson.types.ObjectId; -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; -import org.json.simple.parser.ParseException; - -import com.mongodb.BasicDBList; -import com.mongodb.BasicDBObject; -import com.mongodb.BasicDBObjectBuilder; -import com.mongodb.DB; -import com.mongodb.DBCollection; -import com.mongodb.DBCursor; -import com.mongodb.DBObject; -import com.mongodb.MongoException; -import com.mongodb.util.JSON; -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.ControlPointWithTwoMarks; -import com.sap.sailing.domain.base.CourseArea; -import com.sap.sailing.domain.base.CourseBase; -import com.sap.sailing.domain.base.DomainFactory; -import com.sap.sailing.domain.base.Event; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.Mark; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceDefinition; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.RegattaRegistry; -import com.sap.sailing.domain.base.Series; -import com.sap.sailing.domain.base.Venue; -import com.sap.sailing.domain.base.Waypoint; -import com.sap.sailing.domain.base.configuration.DeviceConfiguration; -import com.sap.sailing.domain.base.configuration.DeviceConfigurationMatcher; -import com.sap.sailing.domain.base.configuration.RegattaConfiguration; -import com.sap.sailing.domain.base.configuration.impl.DeviceConfigurationImpl; -import com.sap.sailing.domain.base.configuration.impl.RegattaConfigurationImpl; -import com.sap.sailing.domain.base.impl.CourseDataImpl; -import com.sap.sailing.domain.base.impl.EventImpl; -import com.sap.sailing.domain.base.impl.FleetImpl; -import com.sap.sailing.domain.base.impl.RegattaImpl; -import com.sap.sailing.domain.base.impl.SeriesImpl; -import com.sap.sailing.domain.base.impl.VenueImpl; -import com.sap.sailing.domain.base.impl.WaypointImpl; -import com.sap.sailing.domain.common.Color; -import com.sap.sailing.domain.common.MarkType; -import com.sap.sailing.domain.common.MaxPointsReason; -import com.sap.sailing.domain.common.PassingInstruction; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.RegattaName; -import com.sap.sailing.domain.common.RegattaNameAndRaceName; -import com.sap.sailing.domain.common.ScoringSchemeType; -import com.sap.sailing.domain.common.SpeedWithBearing; -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.configuration.DeviceConfigurationMatcherType; -import com.sap.sailing.domain.common.impl.DegreeBearingImpl; -import com.sap.sailing.domain.common.impl.DegreePosition; -import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; -import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; -import com.sap.sailing.domain.common.impl.RGBColor; -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.common.impl.WindSourceWithAdditionalID; -import com.sap.sailing.domain.common.racelog.Flags; -import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; -import com.sap.sailing.domain.common.racelog.RacingProcedureType; -import com.sap.sailing.domain.leaderboard.DelayedLeaderboardCorrections; -import com.sap.sailing.domain.leaderboard.DelayedLeaderboardCorrections.LeaderboardCorrectionsResolvedListener; -import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.leaderboard.LeaderboardGroup; -import com.sap.sailing.domain.leaderboard.LeaderboardRegistry; -import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; -import com.sap.sailing.domain.leaderboard.ScoringScheme; -import com.sap.sailing.domain.leaderboard.SettableScoreCorrection; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sailing.domain.leaderboard.impl.DelayedLeaderboardCorrectionsImpl; -import com.sap.sailing.domain.leaderboard.impl.FlexibleLeaderboardImpl; -import com.sap.sailing.domain.leaderboard.impl.LeaderboardGroupImpl; -import com.sap.sailing.domain.leaderboard.impl.RegattaLeaderboardImpl; -import com.sap.sailing.domain.leaderboard.impl.ThresholdBasedResultDiscardingRuleImpl; -import com.sap.sailing.domain.leaderboard.meta.LeaderboardGroupMetaLeaderboard; -import com.sap.sailing.domain.persistence.DomainObjectFactory; -import com.sap.sailing.domain.persistence.MongoRaceLogStoreFactory; -import com.sap.sailing.domain.racelog.CompetitorResults; -import com.sap.sailing.domain.racelog.RaceLog; -import com.sap.sailing.domain.racelog.RaceLogCourseAreaChangedEvent; -import com.sap.sailing.domain.racelog.RaceLogCourseDesignChangedEvent; -import com.sap.sailing.domain.racelog.RaceLogEvent; -import com.sap.sailing.domain.racelog.RaceLogEventAuthor; -import com.sap.sailing.domain.racelog.RaceLogEventRestoreFactory; -import com.sap.sailing.domain.racelog.RaceLogFinishPositioningConfirmedEvent; -import com.sap.sailing.domain.racelog.RaceLogFinishPositioningListChangedEvent; -import com.sap.sailing.domain.racelog.RaceLogFlagEvent; -import com.sap.sailing.domain.racelog.RaceLogGateLineOpeningTimeEvent; -import com.sap.sailing.domain.racelog.RaceLogIdentifier; -import com.sap.sailing.domain.racelog.RaceLogPassChangeEvent; -import com.sap.sailing.domain.racelog.RaceLogPathfinderEvent; -import com.sap.sailing.domain.racelog.RaceLogProtestStartTimeEvent; -import com.sap.sailing.domain.racelog.RaceLogRaceStatusEvent; -import com.sap.sailing.domain.racelog.RaceLogStartProcedureChangedEvent; -import com.sap.sailing.domain.racelog.RaceLogStartTimeEvent; -import com.sap.sailing.domain.racelog.RaceLogStore; -import com.sap.sailing.domain.racelog.RaceLogWindFixEvent; -import com.sap.sailing.domain.racelog.impl.CompetitorResultsImpl; -import com.sap.sailing.domain.racelog.impl.RaceLogEventAuthorImpl; -import com.sap.sailing.domain.racelog.impl.RaceLogImpl; -import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; -import com.sap.sailing.domain.tracking.Wind; -import com.sap.sailing.domain.tracking.WindTrack; -import com.sap.sailing.domain.tracking.impl.WindImpl; -import com.sap.sailing.domain.tracking.impl.WindTrackImpl; -import com.sap.sailing.server.gateway.deserialization.JsonDeserializationException; -import com.sap.sailing.server.gateway.deserialization.JsonDeserializer; -import com.sap.sailing.server.gateway.deserialization.impl.CompetitorJsonDeserializer; -import com.sap.sailing.server.gateway.deserialization.impl.DeviceConfigurationJsonDeserializer; -import com.sap.sailing.server.gateway.deserialization.impl.Helpers; -import com.sap.sailing.server.gateway.deserialization.impl.RegattaConfigurationJsonDeserializer; - -public class DomainObjectFactoryImpl implements DomainObjectFactory { - private static final Logger logger = Logger.getLogger(DomainObjectFactoryImpl.class.getName()); - private final CompetitorJsonDeserializer competitorDeserializer; - - private final DB database; - - private RaceLogEventRestoreFactory raceLogEventFactory; - private final DomainFactory baseDomainFactory; - - public DomainObjectFactoryImpl(DB db, DomainFactory baseDomainFactory) { - super(); - this.baseDomainFactory = baseDomainFactory; - this.competitorDeserializer = CompetitorJsonDeserializer.create(baseDomainFactory); - this.database = db; - this.raceLogEventFactory = RaceLogEventRestoreFactory.INSTANCE; - } - - @Override - public DomainFactory getBaseDomainFactory() { - return baseDomainFactory; - } - - public Wind loadWind(DBObject object) { - return new WindImpl(loadPosition(object), loadTimePoint(object), loadSpeedWithBearing(object)); - } - - private Position loadPosition(DBObject object) { - Number latNumber = (Number) object.get(FieldNames.LAT_DEG.name()); - Double lat = latNumber == null ? null : latNumber.doubleValue(); - Number lngNumber = (Number) object.get(FieldNames.LNG_DEG.name()); - Double lng = lngNumber == null ? null : lngNumber.doubleValue(); - if (lat != null && lng != null) { - return new DegreePosition(lat, lng); - } else { - return null; - } - } - - private TimePoint loadTimePoint(DBObject object, FieldNames field) { - TimePoint result = null; - Number timePointAsNumber = (Number) object.get(field.name()); - if (timePointAsNumber != null) { - result = new MillisecondsTimePoint(timePointAsNumber.longValue()); - } - return result; - } - - /** - * Loads a {@link TimePoint} on the given object at {@link FieldNames#TIME_AS_MILLIS}. - */ - private TimePoint loadTimePoint(DBObject object) { - return loadTimePoint(object, FieldNames.TIME_AS_MILLIS); - } - - private SpeedWithBearing loadSpeedWithBearing(DBObject object) { - return new KnotSpeedWithBearingImpl(((Number) object.get(FieldNames.KNOT_SPEED.name())).doubleValue(), - new DegreeBearingImpl(((Number) object.get(FieldNames.DEGREE_BEARING.name())).doubleValue())); - } - - @Override - public RaceIdentifier loadRaceIdentifier(DBObject dbObject) { - RaceIdentifier result = null; - String regattaName = (String) dbObject.get(FieldNames.EVENT_NAME.name()); - String raceName = (String) dbObject.get(FieldNames.RACE_NAME.name()); - if (regattaName != null && raceName != null) { - result = new RegattaNameAndRaceName(regattaName, raceName); - } - return result; - } - - private void ensureIndicesOnWindTracks(DBCollection windTracks) { - windTracks.ensureIndex(FieldNames.RACE_ID.name()); // for new programmatic access - windTracks.ensureIndex(FieldNames.REGATTA_NAME.name()); // for export or human look-up - // for legacy access to not yet migrated fixes - windTracks.ensureIndex(new BasicDBObjectBuilder().add(FieldNames.EVENT_NAME.name(), 1) - .add(FieldNames.RACE_NAME.name(), 1).get()); - // Unique index - try { - windTracks.ensureIndex( - new BasicDBObjectBuilder().add(FieldNames.RACE_ID.name(), 1) - .add(FieldNames.WIND_SOURCE_NAME.name(), 1).add(FieldNames.WIND_SOURCE_ID.name(), 1) - .add(FieldNames.WIND.name() + "." + FieldNames.TIME_AS_MILLIS.name(), 1).get(), - new BasicDBObjectBuilder().add("unique", true).add("dropDups", true).get()); - } catch (MongoException exception) { - - if (exception.getCode() == 10092) { - logger.warning(String.format( - "Setting the unique index on the %s collection failed because you have too many duplicates. " - + "This leads to the mongo error code %s and the following message: %s \nTo fix this follow " - + "the steps provided on the wiki page: http://wiki.sapsailing.com/wiki/cook-book#Remove-" - + "duplicates-from-WIND_TRACK-collection", CollectionNames.WIND_TRACKS.name(), - exception.getCode(), exception.getMessage())); - } else { - logger.severe(String.format( - "Setting the unique index on the %s collection failed with error code %s and message: %s", - CollectionNames.WIND_TRACKS.name(), exception.getCode(), exception.getMessage())); - } - } - } - - @Override - public Leaderboard loadLeaderboard(String name, RegattaRegistry regattaRegistry) { - DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); - Leaderboard result = null; - try { - BasicDBObject query = new BasicDBObject(); - query.put(FieldNames.LEADERBOARD_NAME.name(), name); - for (DBObject o : leaderboardCollection.find(query)) { - result = loadLeaderboard(o, regattaRegistry, /* leaderboardRegistry */ null, /* groupForMetaLeaderboard */ null); - } - } catch (Exception e) { - // something went wrong during DB access; report, then use empty new wind track - logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboard "+name+"."); - logger.log(Level.SEVERE, "loadLeaderboard", e); - } - return result; - } - - @Override - public Iterable getAllLeaderboards(RegattaRegistry regattaRegistry, LeaderboardRegistry leaderboardRegistry) { - DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); - Set result = new HashSet(); - try { - for (DBObject o : leaderboardCollection.find()) { - final Leaderboard loadedLeaderboard = loadLeaderboard(o, regattaRegistry, leaderboardRegistry, /* groupForMetaLeaderboard */ null); - if (loadedLeaderboard != null) { - result.add(loadedLeaderboard); - } - } - } catch (Exception e) { - // something went wrong during DB access; report, then use empty new wind track - logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboards."); - logger.log(Level.SEVERE, "getAllLeaderboards", e); - } - return result; - } - - /** - * If the DBObject has a field {@link FieldNames#REGATTA_NAME} then the object represents a - * {@link RegattaLeaderboard}. Otherwise, a {@link FlexibleLeaderboard} will be loaded. - * - * @param leaderboardRegistry - * if not null, then before creating and loading the leaderboard it is looked up in this - * registry and only loaded if not found there. If leaderboardRegistry is null, - * the leaderboard is loaded in any case. If the leaderboard is loaded and - * leaderboardRegistry is not null, the leaderboard loaded is - * {@link LeaderboardRegistry#addLeaderboard(Leaderboard) added to the registry}. - * @param groupForMetaLeaderboard - * if not null, a {@link LeaderboardGroupMetaLeaderboard} instance is created and set as the - * group's {@link LeaderboardGroup#setOverallLeaderboard(Leaderboard) overall leaderboard} - * - * @return null in case the leaderboard couldn't be loaded, e.g., because the regatta referenced by a - * {@link RegattaLeaderboard} cannot be found; the leaderboard loaded or found in - * leaderboardRegistry, otherwise - */ - private Leaderboard loadLeaderboard(DBObject dbLeaderboard, RegattaRegistry regattaRegistry, LeaderboardRegistry leaderboardRegistry, - LeaderboardGroup groupForMetaLeaderboard) { - Leaderboard result = null; - String leaderboardName = (String) dbLeaderboard.get(FieldNames.LEADERBOARD_NAME.name()); - if (leaderboardRegistry != null) { - result = leaderboardRegistry.getLeaderboardByName(leaderboardName); - } - if (result == null) { - ThresholdBasedResultDiscardingRule resultDiscardingRule = loadResultDiscardingRule(dbLeaderboard, FieldNames.LEADERBOARD_DISCARDING_THRESHOLDS); - String regattaName = (String) dbLeaderboard.get(FieldNames.REGATTA_NAME.name()); - if (groupForMetaLeaderboard != null) { - result = new LeaderboardGroupMetaLeaderboard(groupForMetaLeaderboard, loadScoringScheme(dbLeaderboard), resultDiscardingRule); - groupForMetaLeaderboard.setOverallLeaderboard(result); - } else if (regattaName == null) { - result = loadFlexibleLeaderboard(dbLeaderboard, resultDiscardingRule); - } else { - result = loadRegattaLeaderboard(leaderboardName, regattaName, dbLeaderboard, resultDiscardingRule, regattaRegistry); - } - if (result != null) { - final Leaderboard finalResult = result; - finalResult.setDisplayName((String) dbLeaderboard.get(FieldNames.LEADERBOARD_DISPLAY_NAME.name())); - - DelayedLeaderboardCorrections loadedLeaderboardCorrections = new DelayedLeaderboardCorrectionsImpl(result, baseDomainFactory); - final boolean[] needsMigration = new boolean[1]; - loadedLeaderboardCorrections.addLeaderboardCorrectionsResolvedListener(new LeaderboardCorrectionsResolvedListener() { - @Override - public void correctionsResolved(DelayedLeaderboardCorrections delayedLeaderboardCorrections) { - if (needsMigration[0]) { - new MongoObjectFactoryImpl(database).storeLeaderboard(finalResult); - } - } - }); - needsMigration[0] = loadLeaderboardCorrections(dbLeaderboard, loadedLeaderboardCorrections, result.getScoreCorrection()) || needsMigration[0]; - needsMigration[0] = loadSuppressedCompetitors(dbLeaderboard, loadedLeaderboardCorrections) || needsMigration[0]; - loadColumnFactors(dbLeaderboard, result); - // add the leaderboard to the registry - if (leaderboardRegistry != null) { - leaderboardRegistry.addLeaderboard(result); - logger.info("loaded leaderboard "+result.getName()+" into "+leaderboardRegistry); - } - } - } - return result; - } - - private void loadColumnFactors(DBObject dbLeaderboard, Leaderboard result) { - DBObject dbColumnFactors = (DBObject) dbLeaderboard.get(FieldNames.LEADERBOARD_COLUMN_FACTORS.name()); - if (dbColumnFactors != null) { - for (String encodedRaceColumnName : dbColumnFactors.keySet()) { - double factor = ((Number) dbColumnFactors.get(encodedRaceColumnName)).doubleValue(); - String raceColumnName = MongoUtils.unescapeDollarAndDot(encodedRaceColumnName); - final RaceColumn raceColumn = result.getRaceColumnByName(raceColumnName); - if (raceColumn != null) { - raceColumn.setFactor(factor); - } else { - logger.warning("Expected to find race column named "+raceColumnName+" in leaderboard "+result.getName()+ - " to apply column factor "+factor+", but the race column wasn't found. Ignoring factor."); - } - } - } - } - - private boolean loadSuppressedCompetitors(DBObject dbLeaderboard, - DelayedLeaderboardCorrections loadedLeaderboardCorrections) { - final boolean needsMigration; - BasicDBList dbSuppressedCompetitorNames = (BasicDBList) dbLeaderboard.get(FieldNames.LEADERBOARD_SUPPRESSED_COMPETITORS.name()); - if (dbSuppressedCompetitorNames != null) { - needsMigration = true; - for (Object escapedCompetitorName : dbSuppressedCompetitorNames) { - loadedLeaderboardCorrections.suppressCompetitorByName(MongoUtils.unescapeDollarAndDot((String) escapedCompetitorName)); - } - } else { - needsMigration = false; - } - BasicDBList dbSuppressedCompetitorIDs = (BasicDBList) dbLeaderboard.get(FieldNames.LEADERBOARD_SUPPRESSED_COMPETITOR_IDS.name()); - if (dbSuppressedCompetitorIDs != null) { - for (Object competitorId : dbSuppressedCompetitorIDs) { - loadedLeaderboardCorrections.suppressCompetitorById((Serializable) competitorId); - } - } - return needsMigration; - } - - /** - * @param dbObject expects to find a field identified by field which holds a {@link BasicDBList} - */ - private ThresholdBasedResultDiscardingRule loadResultDiscardingRule(DBObject dbObject, FieldNames field) { - BasicDBList dbDiscardIndexResultsStartingWithHowManyRaces = (BasicDBList) dbObject.get(field.name()); - final ThresholdBasedResultDiscardingRule result; - if (dbDiscardIndexResultsStartingWithHowManyRaces == null) { - result = null; - } else { - int[] discardIndexResultsStartingWithHowManyRaces = new int[dbDiscardIndexResultsStartingWithHowManyRaces - .size()]; - int i = 0; - for (Object discardingThresholdAsObject : dbDiscardIndexResultsStartingWithHowManyRaces) { - discardIndexResultsStartingWithHowManyRaces[i++] = (Integer) discardingThresholdAsObject; - } - result = new ThresholdBasedResultDiscardingRuleImpl(discardIndexResultsStartingWithHowManyRaces); - } - return result; - } - - /** - * @return null if the regatta cannot be resolved; otherwise the leaderboard for the regatta specified - */ - private RegattaLeaderboard loadRegattaLeaderboard(String leaderboardName, String regattaName, DBObject dbLeaderboard, - ThresholdBasedResultDiscardingRule resultDiscardingRule, RegattaRegistry regattaRegistry) { - RegattaLeaderboard result = null; - Regatta regatta = regattaRegistry.getRegatta(new RegattaName(regattaName)); - if (regatta == null) { - logger.info("Couldn't find regatta "+regattaName+" for corresponding regatta leaderboard. Not loading regatta leaderboard."); - } else { - result = new RegattaLeaderboardImpl(regatta, resultDiscardingRule); - result.setName(leaderboardName); - } - return result; - } - - private FlexibleLeaderboard loadFlexibleLeaderboard(DBObject dbLeaderboard, - ThresholdBasedResultDiscardingRule resultDiscardingRule) { - final FlexibleLeaderboardImpl result; - BasicDBList dbRaceColumns = (BasicDBList) dbLeaderboard.get(FieldNames.LEADERBOARD_COLUMNS.name()); - if (dbRaceColumns == null) { - // this was probably an orphaned overall leaderboard - logger.warning("Probably found orphan overall leaderboard named " - + dbLeaderboard.get(FieldNames.LEADERBOARD_NAME.name())+". Ignoring."); - result = null; - } else { - final ScoringScheme scoringScheme = loadScoringScheme(dbLeaderboard); - RaceLogStore raceLogStore = MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore( - new MongoObjectFactoryImpl(database), this); - - Serializable courseAreaId = (Serializable) dbLeaderboard.get(FieldNames.COURSE_AREA_ID.name()); - CourseArea courseArea = null; - if (courseAreaId != null) { - UUID courseAreaUuid = UUID.fromString(courseAreaId.toString()); - courseArea = baseDomainFactory.getExistingCourseAreaById(courseAreaUuid); - } - - result = new FlexibleLeaderboardImpl(raceLogStore, (String) dbLeaderboard.get(FieldNames.LEADERBOARD_NAME - .name()), resultDiscardingRule, scoringScheme, courseArea); - // For a FlexibleLeaderboard, fleets are owned by the leaderboard's RaceColumn objects. We need to manage - // them here: - Map fleetsByName = new HashMap(); - for (Object dbRaceColumnAsObject : dbRaceColumns) { - BasicDBObject dbRaceColumn = (BasicDBObject) dbRaceColumnAsObject; - Map raceIdentifiers = loadRaceIdentifiers(dbRaceColumn); - RaceIdentifier defaultFleetRaceIdentifier = raceIdentifiers.get(null); - if (defaultFleetRaceIdentifier != null) { - Fleet defaultFleet = result.getFleet(null); - if (defaultFleet != null) { - raceIdentifiers.put(defaultFleet.getName(), defaultFleetRaceIdentifier); - } else { - // leaderboard has no default fleet; don't know what to do with default RaceIdentifier - logger.warning("Discarding RaceIdentifier " + defaultFleetRaceIdentifier - + " for default fleet for leaderboard " + result.getName() - + " because no default fleet was found in leaderboard"); - } - raceIdentifiers.remove(null); - } - List fleets = new ArrayList(); - for (String fleetName : raceIdentifiers.keySet()) { - Fleet fleet = fleetsByName.get(fleetName); - if (fleet == null) { - fleet = new FleetImpl(fleetName); - fleetsByName.put(fleetName, fleet); - } - fleets.add(fleet); - } - if (fleets.isEmpty()) { - fleets.add(result.getFleet(null)); - } - String columnName = (String) dbRaceColumn.get(FieldNames.LEADERBOARD_COLUMN_NAME.name()); - - RaceColumn raceColumn = result.addRaceColumn(columnName, - (Boolean) dbRaceColumn.get(FieldNames.LEADERBOARD_IS_MEDAL_RACE_COLUMN.name()), - fleets.toArray(new Fleet[0])); - for (Map.Entry e : raceIdentifiers.entrySet()) { - raceColumn.setRaceIdentifier(fleetsByName.get(e.getKey()), e.getValue()); - } - - } - } - return result; - } - - private ScoringScheme loadScoringScheme(DBObject dbLeaderboard) { - ScoringSchemeType scoringSchemeType = getScoringSchemeType(dbLeaderboard); - final ScoringScheme scoringScheme = baseDomainFactory.createScoringScheme(scoringSchemeType); - return scoringScheme; - } - - private boolean loadLeaderboardCorrections(DBObject dbLeaderboard, DelayedLeaderboardCorrections correctionsToUpdate, - SettableScoreCorrection scoreCorrectionToUpdate) { - boolean needsMigration = false; - DBObject carriedPoints = (DBObject) dbLeaderboard.get(FieldNames.LEADERBOARD_CARRIED_POINTS.name()); - if (carriedPoints != null) { - needsMigration = true; - for (String escapedCompetitorName : carriedPoints.keySet()) { - Double carriedPointsForCompetitor = ((Number) carriedPoints.get(escapedCompetitorName)).doubleValue(); - if (carriedPointsForCompetitor != null) { - correctionsToUpdate.setCarriedPointsByName(MongoUtils.unescapeDollarAndDot(escapedCompetitorName), carriedPointsForCompetitor); - } - } - } - BasicDBList carriedPointsById = (BasicDBList) dbLeaderboard.get(FieldNames.LEADERBOARD_CARRIED_POINTS_BY_ID.name()); - if (carriedPointsById != null) { - for (Object o : carriedPointsById) { - DBObject competitorIdAndCarriedPoints = (DBObject) o; - Serializable competitorId = (Serializable) competitorIdAndCarriedPoints.get(FieldNames.COMPETITOR_ID.name()); - Double carriedPointsForCompetitor = ((Number) competitorIdAndCarriedPoints - .get(FieldNames.LEADERBOARD_CARRIED_POINTS.name())).doubleValue(); - if (carriedPointsForCompetitor != null) { - correctionsToUpdate.setCarriedPointsByID(competitorId, carriedPointsForCompetitor); - } - } - } - DBObject dbScoreCorrection = (DBObject) dbLeaderboard.get(FieldNames.LEADERBOARD_SCORE_CORRECTIONS.name()); - if (dbScoreCorrection.containsField(FieldNames.LEADERBOARD_SCORE_CORRECTION_TIMESTAMP.name())) { - scoreCorrectionToUpdate.setTimePointOfLastCorrectionsValidity( - new MillisecondsTimePoint((Long) dbScoreCorrection.get(FieldNames.LEADERBOARD_SCORE_CORRECTION_TIMESTAMP.name()))); - dbScoreCorrection.removeField(FieldNames.LEADERBOARD_SCORE_CORRECTION_TIMESTAMP.name()); - } - if (dbScoreCorrection.containsField(FieldNames.LEADERBOARD_SCORE_CORRECTION_COMMENT.name())) { - scoreCorrectionToUpdate.setComment((String) dbScoreCorrection.get(FieldNames.LEADERBOARD_SCORE_CORRECTION_COMMENT.name())); - dbScoreCorrection.removeField(FieldNames.LEADERBOARD_SCORE_CORRECTION_COMMENT.name()); - } - for (String escapedRaceColumnName : dbScoreCorrection.keySet()) { - // deprecated style: a DBObject per race where the keys are the escaped competitor names - // new style: a BasicDBList per race where each entry is a DBObject with COMPETITOR_ID and - // LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON and LEADERBOARD_CORRECTED_SCORE fields each - DBObject dbScoreCorrectionForRace = (DBObject) dbScoreCorrection.get(escapedRaceColumnName); - final RaceColumn raceColumn = correctionsToUpdate.getLeaderboard().getRaceColumnByName(MongoUtils.unescapeDollarAndDot(escapedRaceColumnName)); - if (raceColumn != null) { - if (dbScoreCorrectionForRace instanceof BasicDBList) { - for (Object o : (BasicDBList) dbScoreCorrectionForRace) { - DBObject dbScoreCorrectionForCompetitorInRace = (DBObject) o; - Serializable competitorId = (Serializable) dbScoreCorrectionForCompetitorInRace - .get(FieldNames.COMPETITOR_ID.name()); - if (dbScoreCorrectionForCompetitorInRace - .containsField(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name())) { - correctionsToUpdate.setMaxPointsReasonByID(competitorId, raceColumn, MaxPointsReason - .valueOf((String) dbScoreCorrectionForCompetitorInRace - .get(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name()))); - } - if (dbScoreCorrectionForCompetitorInRace.containsField(FieldNames.LEADERBOARD_CORRECTED_SCORE - .name())) { - final Double leaderboardCorrectedScore = ((Number) dbScoreCorrectionForCompetitorInRace - .get(FieldNames.LEADERBOARD_CORRECTED_SCORE.name())).doubleValue(); - correctionsToUpdate.correctScoreByID(competitorId, raceColumn, - (Double) leaderboardCorrectedScore); - } - } - } else { - needsMigration = true; - for (String competitorName : dbScoreCorrectionForRace.keySet()) { - DBObject dbScoreCorrectionForCompetitorInRace = (DBObject) dbScoreCorrectionForRace - .get(competitorName); - if (dbScoreCorrectionForCompetitorInRace - .containsField(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name())) { - correctionsToUpdate.setMaxPointsReasonByName(MongoUtils - .unescapeDollarAndDot(competitorName), raceColumn, MaxPointsReason - .valueOf((String) dbScoreCorrectionForCompetitorInRace - .get(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name()))); - } - if (dbScoreCorrectionForCompetitorInRace.containsField(FieldNames.LEADERBOARD_CORRECTED_SCORE - .name())) { - final Double leaderboardCorrectedScore = ((Number) dbScoreCorrectionForCompetitorInRace - .get(FieldNames.LEADERBOARD_CORRECTED_SCORE.name())).doubleValue(); - correctionsToUpdate.correctScoreByName(MongoUtils.unescapeDollarAndDot(competitorName), - raceColumn, (Double) leaderboardCorrectedScore); - } - } - } - } else { - logger.warning("Couldn't find race column " + MongoUtils.unescapeDollarAndDot(escapedRaceColumnName) - + " in leaderboard " + correctionsToUpdate.getLeaderboard().getName()); - } - } - DBObject competitorDisplayNames = (DBObject) dbLeaderboard.get(FieldNames.LEADERBOARD_COMPETITOR_DISPLAY_NAMES.name()); - // deprecated style: a DBObject whose keys are the escaped competitor names - // new style: a BasicDBList whose entries are DBObjects with COMPETITOR_ID and COMPETITOR_DISPLAY_NAME fields - if (competitorDisplayNames != null) { - if (competitorDisplayNames instanceof BasicDBList) { - for (Object o : (BasicDBList) competitorDisplayNames) { - DBObject competitorDisplayName = (DBObject) o; - final Serializable competitorId = (Serializable) competitorDisplayName.get(FieldNames.COMPETITOR_ID.name()); - final String displayName = (String) competitorDisplayName.get(FieldNames.COMPETITOR_DISPLAY_NAME.name()); - correctionsToUpdate.setDisplayNameByID(competitorId, displayName); - } - } else { - needsMigration = true; - for (String escapedCompetitorName : competitorDisplayNames.keySet()) { - correctionsToUpdate.setDisplayNameByName(MongoUtils.unescapeDollarAndDot(escapedCompetitorName), - (String) competitorDisplayNames.get(escapedCompetitorName)); - } - } - } - return needsMigration; - } - - /** - * Expects a DBObject under the key {@link FieldNames#RACE_IDENTIFIERS} whose keys are the fleet names and whose - * values are the race identifiers as DBObjects (see {@link #loadRaceIdentifier(DBObject)}). If legacy DB instances - * have a {@link RaceIdentifier} that is not associated with a fleet name, it may be stored directly in the - * dbRaceColumn. In this case, it is returned with null as the fleet name key. - * - * @return a map with fleet names as key and the corresponding fleet's race identifier as value; the special - * null key is used to identify a "default fleet" for backward compatibility with stored - * leaderboards which don't know about fleets yet; this key should be mapped to the leaderboard's default - * fleet. - */ - private Map loadRaceIdentifiers(DBObject dbRaceColumn) { - Map result = new HashMap(); - // try to load a deprecated single race identifier to associate with the default fleet: - RaceIdentifier singleLegacyRaceIdentifier = loadRaceIdentifier(dbRaceColumn); - if (singleLegacyRaceIdentifier != null) { - result.put(null, singleLegacyRaceIdentifier); - } - DBObject raceIdentifiersPerFleet = (DBObject) dbRaceColumn.get(FieldNames.RACE_IDENTIFIERS.name()); - if (raceIdentifiersPerFleet != null) { - for (String escapedFleetName : raceIdentifiersPerFleet.keySet()) { - String fleetName = MongoUtils.unescapeDollarAndDot(escapedFleetName); - result.put(fleetName, loadRaceIdentifier((DBObject) raceIdentifiersPerFleet.get(fleetName))); - } - } - return result; - } - - @Override - public LeaderboardGroup loadLeaderboardGroup(String name, RegattaRegistry regattaRegistry, LeaderboardRegistry leaderboardRegistry) { - DBCollection leaderboardGroupCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()); - LeaderboardGroup leaderboardGroup = null; - try { - BasicDBObject query = new BasicDBObject(); - query.put(FieldNames.LEADERBOARD_GROUP_NAME.name(), name); - leaderboardGroup = loadLeaderboardGroup(leaderboardGroupCollection.findOne(query), regattaRegistry, leaderboardRegistry); - } catch (Exception e) { - logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboard group "+name+"."); - logger.log(Level.SEVERE, "loadLeaderboardGroup", e); - } - - return leaderboardGroup; - } - - @Override - public Iterable getAllLeaderboardGroups(RegattaRegistry regattaRegistry, LeaderboardRegistry leaderboardRegistry) { - DBCollection leaderboardGroupCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()); - Set leaderboardGroups = new HashSet(); - try { - for (DBObject o : leaderboardGroupCollection.find()) { - leaderboardGroups.add(loadLeaderboardGroup(o, regattaRegistry, leaderboardRegistry)); - } - } catch (Exception e) { - logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboard groups."); - logger.log(Level.SEVERE, "loadLeaderboardGroup", e); - } - - return leaderboardGroups; - } - - private LeaderboardGroup loadLeaderboardGroup(DBObject o, RegattaRegistry regattaRegistry, LeaderboardRegistry leaderboardRegistry) { - DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); - String name = (String) o.get(FieldNames.LEADERBOARD_GROUP_NAME.name()); - String description = (String) o.get(FieldNames.LEADERBOARD_GROUP_DESCRIPTION.name()); - boolean displayGroupsInReverseOrder = false; // default value - Object displayGroupsInReverseOrderObj = o.get(FieldNames.LEADERBOARD_GROUP_DISPLAY_IN_REVERSE_ORDER.name()); - if (displayGroupsInReverseOrderObj != null) { - displayGroupsInReverseOrder = (Boolean) displayGroupsInReverseOrderObj; - } - ArrayList leaderboards = new ArrayList(); - BasicDBList dbLeaderboardIds = (BasicDBList) o.get(FieldNames.LEADERBOARD_GROUP_LEADERBOARDS.name()); - for (Object object : dbLeaderboardIds) { - ObjectId dbLeaderboardId = (ObjectId) object; - DBObject dbLeaderboard = leaderboardCollection.findOne(dbLeaderboardId); - if (dbLeaderboard != null) { - final Leaderboard loadedLeaderboard = loadLeaderboard(dbLeaderboard, regattaRegistry, - leaderboardRegistry, /* groupForMetaLeaderboard */null); - if (loadedLeaderboard != null) { - leaderboards.add(loadedLeaderboard); - } - } else { - logger.warning("couldn't find leaderboard with ID "+dbLeaderboardId+" referenced by leaderboard group "+name); - } - } - logger.info("loaded leaderboard group "+name); - LeaderboardGroupImpl result = new LeaderboardGroupImpl(name, description, displayGroupsInReverseOrder, leaderboards); - Object overallLeaderboardIdOrName = o.get(FieldNames.LEADERBOARD_GROUP_OVERALL_LEADERBOARD.name()); - if (overallLeaderboardIdOrName != null) { - final DBObject dbOverallLeaderboard; - if (overallLeaderboardIdOrName instanceof ObjectId) { - dbOverallLeaderboard = leaderboardCollection.findOne(overallLeaderboardIdOrName); - } else { - dbOverallLeaderboard = (DBObject) overallLeaderboardIdOrName; - } - if (dbOverallLeaderboard != null) { - // the loadLeaderboard call adds the overall leaderboard to the leaderboard registry and sets it as the - // overall leaderboard of the leaderboard group - loadLeaderboard(dbOverallLeaderboard, regattaRegistry, leaderboardRegistry, /* groupForMetaLeaderboard */ result); - } - } - return result; - } - - @Override - public Iterable getLeaderboardsNotInGroup(RegattaRegistry regattaRegistry, LeaderboardRegistry leaderboardRegistry) { - DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); - Set result = new HashSet(); - try { - // For MongoDB 2.4 $where with refs to global objects no longer works - // http://docs.mongodb.org/manual/reference/operator/where/#op._S_where - // Also a single where leads to a table walk without using indexes. So avoid $where. - - // Don't change the query object, unless you know what you're doing. - // It queries all leaderboards not referenced to be part of a leaderboard group - // and in particular not being an overall leaderboard of a leaderboard group. - DBCursor allLeaderboards = leaderboardCollection.find(); - for (DBObject leaderboardFromDB : allLeaderboards) { - DBObject inLeaderboardGroupsQuery = new BasicDBObject(); - inLeaderboardGroupsQuery.put(FieldNames.LEADERBOARD_GROUP_LEADERBOARDS.name(), ((ObjectId)leaderboardFromDB.get("_id")).toString()); - boolean inLeaderboardGroups = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()).find(inLeaderboardGroupsQuery).size()>0; - - DBObject inLeaderboardGroupOverallQuery = new BasicDBObject(); - inLeaderboardGroupOverallQuery.put(FieldNames.LEADERBOARD_GROUP_OVERALL_LEADERBOARD.name(), ((ObjectId)leaderboardFromDB.get("_id")).toString()); - boolean inLeaderboardGroupOverall = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()).find(inLeaderboardGroupOverallQuery).size()>0; - - DBObject inLeaderboardGroupOverallQueryName = new BasicDBObject(); - inLeaderboardGroupOverallQueryName.put(FieldNames.LEADERBOARD_GROUP_OVERALL_LEADERBOARD.name(), leaderboardFromDB.get(FieldNames.LEADERBOARD_NAME.name())); - boolean inLeaderboardGroupOverallName = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()).find(inLeaderboardGroupOverallQueryName).size()>0; - - if (!inLeaderboardGroups && !inLeaderboardGroupOverall && !inLeaderboardGroupOverallName) { - final Leaderboard loadedLeaderboard = loadLeaderboard(leaderboardFromDB, regattaRegistry, leaderboardRegistry, /* groupForMetaLeaderboard */ null); - if (loadedLeaderboard != null) { - result.add(loadedLeaderboard); - } - } - } - } catch (Exception e) { - logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboards."); - logger.log(Level.SEVERE, "getAllLeaderboards", e); - } - return result; - } - - @Override - public WindTrack loadWindTrack(String regattaName, RaceDefinition race, WindSource windSource, long millisecondsOverWhichToAverage) { - final WindTrack result; - Map resultMap = loadWindTracks(regattaName, race, windSource, millisecondsOverWhichToAverage); - if (resultMap.containsKey(windSource)) { - result = resultMap.get(windSource); - } else { - // create an empty wind track as result if no fixes were found in store for the wind source requested - result = new WindTrackImpl(millisecondsOverWhichToAverage, windSource.getType().getBaseConfidence(), - windSource.getType().useSpeed(), - /* nameForReadWriteLock */ WindTrackImpl.class.getSimpleName()+" for source "+windSource.toString()); - } - return result; - } - - @Override - public Map loadWindTracks(String regattaName, RaceDefinition race, - long millisecondsOverWhichToAverageWind) { - Map result = loadWindTracks(regattaName, race, /* constrain wind source */ null, millisecondsOverWhichToAverageWind); - return result; - } - - /** - * @param constrainToWindSource - * if null, wind for all sources will be loaded; otherwise, only wind data for the wind - * source specified by this argument will be loaded - */ - private Map loadWindTracks(String regattaName, RaceDefinition race, - WindSource constrainToWindSource, long millisecondsOverWhichToAverageWind) { - Map result = new HashMap(); - try { - DBCollection windTracks = database.getCollection(CollectionNames.WIND_TRACKS.name()); - ensureIndicesOnWindTracks(windTracks); - BasicDBObject queryById = new BasicDBObject(); - queryById.put(FieldNames.RACE_ID.name(), race.getId()); - if (constrainToWindSource != null) { - queryById.put(FieldNames.WIND_SOURCE_NAME.name(), constrainToWindSource.name()); - } - for (DBObject dbWind : windTracks.find(queryById)) { - loadWindFix(result, dbWind, millisecondsOverWhichToAverageWind); - } - BasicDBObject queryByName = new BasicDBObject(); - queryByName.put(FieldNames.EVENT_NAME.name(), regattaName); - queryByName.put(FieldNames.RACE_NAME.name(), race.getName()); - if (constrainToWindSource != null) { - queryByName.put(FieldNames.WIND_SOURCE_NAME.name(), constrainToWindSource.name()); - } - final DBCursor windFixesFoundByName = windTracks.find(queryByName); - if (windFixesFoundByName.hasNext()) { - List windFixesToMigrate = new ArrayList(); - for (DBObject dbWind : windFixesFoundByName) { - Pair wind = loadWindFix(result, dbWind, millisecondsOverWhichToAverageWind); - // write the wind fix with the new ID-based key and remove the legacy wind fix from the DB - windFixesToMigrate.add(new MongoObjectFactoryImpl(database).storeWindTrackEntry(race, regattaName, - wind.getB(), wind.getA())); - } - logger.info("Migrating "+windFixesFoundByName.size()+" wind fixes of regatta "+regattaName+ - " and race "+race.getName()+" to ID-based keys"); - windTracks.insert(windFixesToMigrate.toArray(new DBObject[windFixesToMigrate.size()])); - logger.info("Removing "+windFixesFoundByName.size()+" wind fixes that were keyed by the names of regatta "+regattaName+ - " and race "+race.getName()); - windTracks.remove(queryByName); - } - } catch (Exception e) { - // something went wrong during DB access; report, then use empty new wind track - logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load recorded wind data. Check MongoDB settings."); - logger.log(Level.SEVERE, "loadWindTrack", e); - } - return result; - } - - private Pair loadWindFix(Map result, DBObject dbWind, long millisecondsOverWhichToAverageWind) { - Wind wind = loadWind((DBObject) dbWind.get(FieldNames.WIND.name())); - WindSourceType windSourceType = WindSourceType.valueOf((String) dbWind.get(FieldNames.WIND_SOURCE_NAME.name())); - WindSource windSource; - if (dbWind.containsField(FieldNames.WIND_SOURCE_ID.name())) { - windSource = new WindSourceWithAdditionalID(windSourceType, (String) dbWind.get(FieldNames.WIND_SOURCE_ID.name())); - } else { - windSource = new WindSourceImpl(windSourceType); - } - WindTrack track = result.get(windSource); - if (track == null) { - track = new WindTrackImpl(millisecondsOverWhichToAverageWind, windSource.getType().getBaseConfidence(), - windSource.getType().useSpeed(), - /* nameForReadWriteLock */ WindTrackImpl.class.getSimpleName()+" for source "+windSource.toString()); - result.put(windSource, track); - } - track.add(wind); - return new Pair(wind, windSource); - } - - @Override - public Event loadEvent(String name) { - Event result; - BasicDBObject query = new BasicDBObject(); - query.put(FieldNames.EVENT_NAME.name(), name); - DBCollection eventCollection = database.getCollection(CollectionNames.EVENTS.name()); - DBObject eventDBObject = eventCollection.findOne(query); - if (eventDBObject != null) { - result = loadEvent(eventDBObject); - } else { - result = null; - } - return result; - } - - @Override - public Iterable loadAllEvents() { - ArrayList result = new ArrayList(); - DBCollection eventCollection = database.getCollection(CollectionNames.EVENTS.name()); - - try { - for (DBObject o : eventCollection.find()) { - result.add(loadEvent(o)); - } - } catch (Exception e) { - logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load events."); - logger.log(Level.SEVERE, "loadEvents", e); - } - - return result; - } - - /** - * An event doesn't store its regattas; it's the regatta that stores a reference to its event; the regatta - * needs to add itself to the event when loaded or instantiated. - */ - private Event loadEvent(DBObject eventDBObject) { - String name = (String) eventDBObject.get(FieldNames.EVENT_NAME.name()); - UUID id = (UUID) eventDBObject.get(FieldNames.EVENT_ID.name()); - TimePoint startDate = loadTimePoint(eventDBObject, FieldNames.EVENT_START_DATE); - TimePoint endDate = loadTimePoint(eventDBObject, FieldNames.EVENT_END_DATE); - boolean isPublic = eventDBObject.get(FieldNames.EVENT_IS_PUBLIC.name()) != null ? (Boolean) eventDBObject.get(FieldNames.EVENT_IS_PUBLIC.name()) : false; - Venue venue = loadVenue((DBObject) eventDBObject.get(FieldNames.VENUE.name())); - Event result = new EventImpl(name, startDate, endDate, venue, isPublic, id); - return result; - } - - private Venue loadVenue(DBObject dbObject) { - String name = (String) dbObject.get(FieldNames.VENUE_NAME.name()); - BasicDBList dbCourseAreas = (BasicDBList) dbObject.get(FieldNames.COURSE_AREAS.name()); - Venue result = new VenueImpl(name); - for (Object courseAreaDBObject : dbCourseAreas) { - CourseArea courseArea = loadCourseArea((DBObject) courseAreaDBObject); - result.addCourseArea(courseArea); - } - return result; - } - - private CourseArea loadCourseArea(DBObject courseAreaDBObject) { - String name = (String) courseAreaDBObject.get(FieldNames.COURSE_AREA_NAME.name()); - UUID id = (UUID) courseAreaDBObject.get(FieldNames.COURSE_AREA_ID.name()); - return baseDomainFactory.getOrCreateCourseArea(id, name); - } - - @Override - public Iterable loadAllRegattas(TrackedRegattaRegistry trackedRegattaRegistry) { - List result = new ArrayList(); - DBCollection regattaCollection = database.getCollection(CollectionNames.REGATTAS.name()); - for (DBObject dbRegatta : regattaCollection.find()) { - result.add(loadRegatta(dbRegatta, trackedRegattaRegistry)); - } - return result; - } - - @Override - public Regatta loadRegatta(String name, TrackedRegattaRegistry trackedRegattaRegistry) { - DBObject query = new BasicDBObject(FieldNames.REGATTA_NAME.name(), name); - DBCollection regattaCollection = database.getCollection(CollectionNames.REGATTAS.name()); - DBObject dbRegatta = regattaCollection.findOne(query); - Regatta result = loadRegatta(dbRegatta, trackedRegattaRegistry); - assert result == null || result.getName().equals(name); - return result; - } - - private Regatta loadRegatta(DBObject dbRegatta, TrackedRegattaRegistry trackedRegattaRegistry) { - Regatta result = null; - if (dbRegatta != null) { - String baseName = (String) dbRegatta.get(FieldNames.REGATTA_BASE_NAME.name()); - String boatClassName = (String) dbRegatta.get(FieldNames.BOAT_CLASS_NAME.name()); - Serializable id = (Serializable) dbRegatta.get(FieldNames.REGATTA_ID.name()); - if (id == null) { - id = baseName + "("+boatClassName+")"; - } - BoatClass boatClass = null; - if (boatClassName != null) { - boolean typicallyStartsUpwind = (Boolean) dbRegatta.get(FieldNames.BOAT_CLASS_TYPICALLY_STARTS_UPWIND.name()); - boatClass = baseDomainFactory.getOrCreateBoatClass(boatClassName, typicallyStartsUpwind); - } - BasicDBList dbSeries = (BasicDBList) dbRegatta.get(FieldNames.REGATTA_SERIES.name()); - Iterable series = loadSeries(dbSeries, trackedRegattaRegistry); - RaceLogStore raceLogStore = MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore( - new MongoObjectFactoryImpl(database), - this); - - Serializable courseAreaId = (Serializable) dbRegatta.get(FieldNames.COURSE_AREA_ID.name()); - CourseArea courseArea = null; - if (courseAreaId != null) { - UUID courseAreaUuid = UUID.fromString(courseAreaId.toString()); - courseArea = baseDomainFactory.getExistingCourseAreaById(courseAreaUuid); - } - - RegattaConfiguration configuration = null; - if (dbRegatta.containsField(FieldNames.REGATTA_REGATTA_CONFIGURATION.name())) { - try { - JSONObject json = Helpers.toJSONObjectSafe(new JSONParser().parse(JSON.serialize(dbRegatta.get(FieldNames.REGATTA_REGATTA_CONFIGURATION.name())))); - configuration = RegattaConfigurationJsonDeserializer.create().deserialize(json); - } catch (JsonDeserializationException|ParseException e) { - logger.log(Level.WARNING, "Error loading racing procedure configration for regatta.", e); - } - } - - result = new RegattaImpl(raceLogStore, baseName, boatClass, series, /* persistent */ true, loadScoringScheme(dbRegatta), id, courseArea); - result.setRegattaConfiguration(configuration); - } - return result; - } - - private ScoringSchemeType getScoringSchemeType(DBObject dbObject) { - String scoringSchemeTypeName = (String) dbObject.get(FieldNames.SCORING_SCHEME_TYPE.name()); - ScoringSchemeType scoringSchemeType; - if (scoringSchemeTypeName == null) { - scoringSchemeType = ScoringSchemeType.LOW_POINT; // the default - } else { - scoringSchemeType = ScoringSchemeType.valueOf(scoringSchemeTypeName); - } - return scoringSchemeType; - } - - private Iterable loadSeries(BasicDBList dbSeries, TrackedRegattaRegistry trackedRegattaRegistry) { - List result = new ArrayList(); - for (Object o : dbSeries) { - DBObject oneDBSeries = (DBObject) o; - Series series = loadSeries(oneDBSeries, trackedRegattaRegistry); - result.add(series); - } - return result; - } - - private Series loadSeries(DBObject dbSeries, TrackedRegattaRegistry trackedRegattaRegistry) { - String name = (String) dbSeries.get(FieldNames.SERIES_NAME.name()); - boolean isMedal = (Boolean) dbSeries.get(FieldNames.SERIES_IS_MEDAL.name()); - Boolean startsWithZeroScore = (Boolean) dbSeries.get(FieldNames.SERIES_STARTS_WITH_ZERO_SCORE.name()); - Boolean hasSplitFleetContiguousScoring = (Boolean) dbSeries.get(FieldNames.SERIES_HAS_SPLIT_FLEET_CONTIGUOUS_SCORING.name()); - Boolean firstColumnIsNonDiscardableCarryForward = (Boolean) dbSeries.get(FieldNames.SERIES_STARTS_WITH_NON_DISCARDABLE_CARRY_FORWARD.name()); - final BasicDBList dbFleets = (BasicDBList) dbSeries.get(FieldNames.SERIES_FLEETS.name()); - Map fleetsByName = loadFleets(dbFleets); - BasicDBList dbRaceColumns = (BasicDBList) dbSeries.get(FieldNames.SERIES_RACE_COLUMNS.name()); - Iterable raceColumnNames = loadRaceColumnNames(dbRaceColumns, fleetsByName); - Series series = new SeriesImpl(name, isMedal, fleetsByName.values(), raceColumnNames, trackedRegattaRegistry); - if (dbSeries.get(FieldNames.SERIES_DISCARDING_THRESHOLDS.name()) != null) { - ThresholdBasedResultDiscardingRule resultDiscardingRule = loadResultDiscardingRule(dbSeries, FieldNames.SERIES_DISCARDING_THRESHOLDS); - series.setResultDiscardingRule(resultDiscardingRule); - } - if (startsWithZeroScore != null) { - series.setStartsWithZeroScore(startsWithZeroScore); - } - if (hasSplitFleetContiguousScoring != null) { - series.setSplitFleetContiguousScoring(hasSplitFleetContiguousScoring); - } - if (firstColumnIsNonDiscardableCarryForward != null) { - series.setFirstColumnIsNonDiscardableCarryForward(firstColumnIsNonDiscardableCarryForward); - } - loadRaceColumnRaceLinks(dbRaceColumns, series); - return series; - } - - /** - * @param fleetsByName used to ensure the {@link RaceColumn#getFleets()} points to the same {@link Fleet} objects also - * used in the {@link Series#getFleets()} collection. - */ - private Iterable loadRaceColumnNames(BasicDBList dbRaceColumns, Map fleetsByName) { - List result = new ArrayList(); - for (Object o : dbRaceColumns) { - DBObject dbRaceColumn = (DBObject) o; - result.add((String) dbRaceColumn.get(FieldNames.LEADERBOARD_COLUMN_NAME.name())); - } - return result; - } - - private void loadRaceColumnRaceLinks(BasicDBList dbRaceColumns, Series series) { - for (Object o : dbRaceColumns) { - DBObject dbRaceColumn = (DBObject) o; - String name = (String) dbRaceColumn.get(FieldNames.LEADERBOARD_COLUMN_NAME.name()); - Map raceIdentifiersPerFleetName = loadRaceIdentifiers(dbRaceColumn); - for (Map.Entry e : raceIdentifiersPerFleetName.entrySet()) { - // null key for "default" fleet is not acceptable here - if (e.getKey() == null) { - logger.warning("Ignoring null fleet name while loading RaceColumn " + name); - } else { - series.getRaceColumnByName(name).setRaceIdentifier(series.getFleetByName(e.getKey()), e.getValue()); - } - } - } - } - - private Map loadFleets(BasicDBList dbFleets) { - Map result = new HashMap(); - for (Object o : dbFleets) { - DBObject dbFleet = (DBObject) o; - Fleet fleet = loadFleet(dbFleet); - result.put(fleet.getName(), fleet); - } - return result; - } - - private Fleet loadFleet(DBObject dbFleet) { - Fleet result; - String name = (String) dbFleet.get(FieldNames.FLEET_NAME.name()); - Integer ordering = (Integer) dbFleet.get(FieldNames.FLEET_ORDERING.name()); - if (ordering == null) { - ordering = 0; - } - Integer colorAsInt = (Integer) dbFleet.get(FieldNames.FLEET_COLOR.name()); - Color color = null; - if (colorAsInt != null) { - int r = colorAsInt % 256; - int g = (colorAsInt / 256 ) % 256; - int b = (colorAsInt / 256 / 256) % 256; - color = new RGBColor(r, g, b); - } - result = new FleetImpl(name, ordering, color); - return result; - } - - @Override - public Map loadRaceIDToRegattaAssociations(RegattaRegistry regattaRegistry) { - DBCollection raceIDToRegattaCollection = database.getCollection(CollectionNames.REGATTA_FOR_RACE_ID.name()); - Map result = new HashMap(); - for (DBObject o : raceIDToRegattaCollection.find()) { - Regatta regatta = regattaRegistry.getRegattaByName((String) o.get(FieldNames.REGATTA_NAME.name())); - if (regatta != null) { - result.put((String) o.get(FieldNames.RACE_ID_AS_STRING.name()), regatta); - } else { - logger.warning("Couldn't find regatta " + o.get(FieldNames.REGATTA_NAME.name()) - + ". Cannot restore race associations with this regatta."); - } - } - return result; - } - - @Override - public RaceLog loadRaceLog(RaceLogIdentifier identifier) { - RaceLog result = new RaceLogImpl(RaceLogImpl.class.getSimpleName(), identifier.getIdentifier()); - try { - BasicDBObject query = new BasicDBObject(); - query.put(FieldNames.RACE_LOG_IDENTIFIER.name(), TripleSerializer.serialize(identifier.getIdentifier())); - loadRaceLogEvents(result, query); - - // query for events with deprecated identifier format... - query.put(FieldNames.RACE_LOG_IDENTIFIER.name(), MongoUtils.escapeDollarAndDot(identifier.getDeprecatedIdentifier())); - - List eventsToMigrate = loadRaceLogEvents(result, query); - if (!eventsToMigrate.isEmpty()) { - // ... migrate them... - MongoRaceLogStoreVisitor storeVisitor = new MongoRaceLogStoreVisitor(identifier, new MongoObjectFactoryImpl(database)); - for (RaceLogEvent event : eventsToMigrate) { - event.accept(storeVisitor); - } - // ... and delete the old ones... - database.getCollection(CollectionNames.RACE_LOGS.name()).remove(query); - } - } catch (Throwable t) { - // something went wrong during DB access; report, then use empty new race log - logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load recorded race log data. Check MongoDB settings."); - logger.log(Level.SEVERE, "loadRaceLog", t); - } - return result; - } - - private List loadRaceLogEvents(RaceLog targetRaceLog, BasicDBObject query) { - List result = new ArrayList<>(); - DBCollection raceLog = database.getCollection(CollectionNames.RACE_LOGS.name()); - for (DBObject o : raceLog.find(query)) { - try { - RaceLogEvent raceLogEvent = loadRaceLogEvent((DBObject) o.get(FieldNames.RACE_LOG_EVENT.name())); - if (raceLogEvent != null) { - targetRaceLog.load(raceLogEvent); - result.add(raceLogEvent); - } - } catch (IllegalStateException e) { - logger.log(Level.SEVERE, "Couldn't load race log event "+o+": "+e.getMessage(), e); - } - } - return result; - } - - public RaceLogEvent loadRaceLogEvent(DBObject dbObject) { - TimePoint logicalTimePoint = loadTimePoint(dbObject); - TimePoint createdAt = loadTimePoint(dbObject, FieldNames.RACE_LOG_EVENT_CREATED_AT); - Serializable id = (Serializable) dbObject.get(FieldNames.RACE_LOG_EVENT_ID.name()); - Integer passId = (Integer) dbObject.get(FieldNames.RACE_LOG_EVENT_PASS_ID.name()); - BasicDBList dbCompetitors = (BasicDBList) dbObject.get(FieldNames.RACE_LOG_EVENT_INVOLVED_BOATS.name()); - List competitors = loadCompetitorsForRaceLogEvent(dbCompetitors); - final RaceLogEventAuthor author; - String authorName = (String) dbObject.get(FieldNames.RACE_LOG_EVENT_AUTHOR_NAME.name()); - Number authorPriority = (Number) dbObject.get(FieldNames.RACE_LOG_EVENT_AUTHOR_PRIORITY.name()); - if (authorName != null && authorPriority != null) { - author = new RaceLogEventAuthorImpl(authorName, authorPriority.intValue()); - } else { - author = RaceLogEventAuthorImpl.createCompatibilityAuthor(); - } - - String eventClass = (String) dbObject.get(FieldNames.RACE_LOG_EVENT_CLASS.name()); - if (eventClass.equals(RaceLogStartTimeEvent.class.getSimpleName())) { - return loadRaceLogStartTimeEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); - } else if (eventClass.equals(RaceLogRaceStatusEvent.class.getSimpleName())) { - return loadRaceLogRaceStatusEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); - } else if (eventClass.equals(RaceLogFlagEvent.class.getSimpleName())) { - return loadRaceLogFlagEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); - } else if (eventClass.equals(RaceLogPassChangeEvent.class.getSimpleName())) { - return loadRaceLogPassChangeEvent(createdAt, author, logicalTimePoint, id, passId, competitors); - } else if (eventClass.equals(RaceLogCourseAreaChangedEvent.class.getSimpleName())) { - return loadRaceLogCourseAreaChangedEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); - } else if (eventClass.equals(RaceLogCourseDesignChangedEvent.class.getSimpleName())) { - return loadRaceLogCourseDesignChangedEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); - } else if (eventClass.equals(RaceLogFinishPositioningListChangedEvent.class.getSimpleName())) { - return loadRaceLogFinishPositioningListChangedEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); - } else if (eventClass.equals(RaceLogFinishPositioningConfirmedEvent.class.getSimpleName())) { - return loadRaceLogFinishPositioningConfirmedEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); - } else if (eventClass.equals(RaceLogPathfinderEvent.class.getSimpleName())) { - return loadRaceLogPathfinderEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); - } else if (eventClass.equals(RaceLogGateLineOpeningTimeEvent.class.getSimpleName())) { - return loadRaceLogGateLineOpeningTimeEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); - } else if (eventClass.equals(RaceLogStartProcedureChangedEvent.class.getSimpleName())) { - return loadRaceLogStartProcedureChangedEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); - } else if (eventClass.equals(RaceLogProtestStartTimeEvent.class.getSimpleName())) { - return loadRaceLogRaceLogProtestStartTimeEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); - } else if (eventClass.equals(RaceLogWindFixEvent.class.getSimpleName())) { - return loadRaceLogRaceLogWindFixEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); - } - - throw new IllegalStateException(String.format("Unknown RaceLogEvent type %s", eventClass)); - } - - private RaceLogEvent loadRaceLogRaceLogWindFixEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, - Serializable id, Integer passId, List competitors, DBObject dbObject) { - Wind wind = loadWind((DBObject) dbObject.get(FieldNames.WIND.name())); - return raceLogEventFactory.createWindFixEvent(createdAt, author, logicalTimePoint, id, competitors, passId, wind); - } - - private RaceLogEvent loadRaceLogRaceLogProtestStartTimeEvent(TimePoint createdAt, RaceLogEventAuthor author, - TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { - TimePoint protestStartTime = loadTimePoint(dbObject, FieldNames.RACE_LOG_PROTEST_START_TIME); - return raceLogEventFactory.createProtestStartTimeEvent(createdAt, author, logicalTimePoint, id, competitors, passId, protestStartTime); - } - - private RaceLogEvent loadRaceLogStartProcedureChangedEvent(TimePoint createdAt, RaceLogEventAuthor author, - TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { - RacingProcedureType type = RacingProcedureType.valueOf(dbObject.get(FieldNames.RACE_LOG_START_PROCEDURE_TYPE.name()).toString()); - return raceLogEventFactory.createStartProcedureChangedEvent(createdAt, author, logicalTimePoint, id, competitors, passId, type); - } - - private RaceLogEvent loadRaceLogGateLineOpeningTimeEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, - Serializable id, Integer passId, List competitors, DBObject dbObject) { - Number gateLaunchStopTime = (Number) dbObject.get(FieldNames.RACE_LOG_GATE_LINE_OPENING_TIME.name()); - Number golfDownTime = 0; - if (dbObject.containsField(FieldNames.RACE_LOG_GOLF_DOWN_TIME.name())) { - golfDownTime = (Number) dbObject.get(FieldNames.RACE_LOG_GOLF_DOWN_TIME.name()); - } - return raceLogEventFactory.createGateLineOpeningTimeEvent(createdAt, author, logicalTimePoint, id, competitors, - passId, gateLaunchStopTime == null ? null : gateLaunchStopTime.longValue(), golfDownTime.longValue()); - } - - private RaceLogEvent loadRaceLogPathfinderEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, - Serializable id, Integer passId, List competitors, DBObject dbObject) { - String pathfinderId = dbObject.get(FieldNames.RACE_LOG_PATHFINDER_ID.name()).toString(); - return raceLogEventFactory.createPathfinderEvent(createdAt, author, logicalTimePoint, id, competitors, passId, pathfinderId); - } - - private RaceLogEvent loadRaceLogFinishPositioningConfirmedEvent(TimePoint createdAt, RaceLogEventAuthor author, - TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { - BasicDBList dbPositionedCompetitorList = (BasicDBList) dbObject.get(FieldNames.RACE_LOG_POSITIONED_COMPETITORS.name()); - CompetitorResults positionedCompetitors = null; - //When a confirmation event is loaded that does not contain the positioned competitors (this is the case for the ESS events in - //Singapore and Quingdao) then null should be set for the positionedCompetitors, which is evaluated later on. - if (dbPositionedCompetitorList != null) { - positionedCompetitors = loadPositionedCompetitors(dbPositionedCompetitorList); - } - - return raceLogEventFactory.createFinishPositioningConfirmedEvent(createdAt, author, logicalTimePoint, id, competitors, passId, positionedCompetitors); - } - - private RaceLogEvent loadRaceLogFinishPositioningListChangedEvent(TimePoint createdAt, RaceLogEventAuthor author, - TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { - BasicDBList dbPositionedCompetitorList = (BasicDBList) dbObject.get(FieldNames.RACE_LOG_POSITIONED_COMPETITORS.name()); - CompetitorResults positionedCompetitors = loadPositionedCompetitors(dbPositionedCompetitorList); - - return raceLogEventFactory.createFinishPositioningListChangedEvent(createdAt, author, logicalTimePoint, id, competitors, passId, positionedCompetitors); - } - - private RaceLogEvent loadRaceLogPassChangeEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, - Serializable id, Integer passId, List competitors) { - return raceLogEventFactory.createPassChangeEvent(createdAt, author, logicalTimePoint, id, competitors, passId); - } - - private RaceLogCourseDesignChangedEvent loadRaceLogCourseDesignChangedEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { - String courseName = (String) dbObject.get(FieldNames.RACE_LOG_COURSE_DESIGN_NAME.name()); - CourseBase courseData = loadCourseData((BasicDBList) dbObject.get(FieldNames.RACE_LOG_COURSE_DESIGN.name()), courseName); - return raceLogEventFactory.createCourseDesignChangedEvent(createdAt, author, logicalTimePoint, id, competitors, passId, courseData); - } - - private RaceLogCourseAreaChangedEvent loadRaceLogCourseAreaChangedEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { - Serializable courseAreaId = (Serializable) dbObject.get(FieldNames.COURSE_AREA_ID.name()); - return raceLogEventFactory.createCourseAreaChangedEvent(createdAt, author, logicalTimePoint, id, competitors, passId, courseAreaId); - } - - private CompetitorResults loadPositionedCompetitors(BasicDBList dbPositionedCompetitorList) { - CompetitorResults positionedCompetitors = new CompetitorResultsImpl(); - for (Object object : dbPositionedCompetitorList) { - DBObject dbObject = (DBObject) object; - - Serializable competitorId = (Serializable) dbObject.get(FieldNames.COMPETITOR_ID.name()); - - String competitorName = (String) dbObject.get(FieldNames.COMPETITOR_DISPLAY_NAME.name()); - //The Competitor name is a new field in the list. Therefore the name might be null for existing events. In this case a standard name is set. - if (competitorName == null) { - competitorName = "loaded competitor"; - } - - //At this point we do not retrieve the competitor object since at any point in time, especially after a server restart, the DomainFactory and its competitor - //cache might be empty. But at this time the race log is loaded from database, so the competitor would be null. - //By not using the Competitor object retrieved from the DomainFactory we get completely independent from server restarts and the timepoint of loading - //competitors by tracking providers. - - MaxPointsReason maxPointsReason = MaxPointsReason.valueOf((String) dbObject.get(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name())); - - Triple positionedCompetitor = new Triple(competitorId, competitorName, maxPointsReason); - positionedCompetitors.add(positionedCompetitor); - } - return positionedCompetitors; - } - - private List loadCompetitorsForRaceLogEvent(BasicDBList dbCompetitorList) { - List competitors = new ArrayList(); - for (Object object : dbCompetitorList) { - Serializable competitorId = (Serializable) object; - Competitor competitor = baseDomainFactory.getCompetitorStore().getExistingCompetitorById(competitorId); - competitors.add(competitor); - } - return competitors; - } - - private RaceLogFlagEvent loadRaceLogFlagEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { - Flags upperFlag = Flags.valueOf((String) dbObject.get(FieldNames.RACE_LOG_EVENT_FLAG_UPPER.name())); - Flags lowerFlag = Flags.valueOf((String) dbObject.get(FieldNames.RACE_LOG_EVENT_FLAG_LOWER.name())); - Boolean displayed = Boolean.valueOf((String) dbObject.get(FieldNames.RACE_LOG_EVENT_FLAG_DISPLAYED.name())); - - if (upperFlag == null || lowerFlag == null || displayed == null) { - return null; - } - - return raceLogEventFactory.createFlagEvent(createdAt, author, logicalTimePoint, id, competitors, passId, upperFlag, lowerFlag, displayed); - } - - private RaceLogStartTimeEvent loadRaceLogStartTimeEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { - TimePoint startTime = loadTimePoint(dbObject, FieldNames.RACE_LOG_EVENT_START_TIME); - return raceLogEventFactory.createStartTimeEvent(createdAt, author, logicalTimePoint, id, competitors, passId, startTime); - } - - private RaceLogRaceStatusEvent loadRaceLogRaceStatusEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { - RaceLogRaceStatus nextStatus = RaceLogRaceStatus.valueOf((String) dbObject.get(FieldNames.RACE_LOG_EVENT_NEXT_STATUS.name())); - return raceLogEventFactory.createRaceStatusEvent(createdAt, author, logicalTimePoint, id, competitors, passId, nextStatus); - } - - /** - * The old field name WAYPOINT_PASSINGSIDE has been replaced by WAYPOINT_PASSINGINSTRUCTIONS. If a race with the old - * field is loaded, the value of PASSINGSIDE is used and then migrated to PASSINGINSTRUCTION. If the first or last - * Waypoint has the PassingInstructions Gate, it is transfered to Line. - * - */ - @SuppressWarnings("deprecation") // Used to migrate from PASSINGSIDE to the new PASSINGINSTRUCTIONS - private CourseBase loadCourseData(BasicDBList dbCourseList, String courseName) { - if (courseName == null) { - courseName = "Course"; - } - CourseBase courseData = new CourseDataImpl(courseName); - int i = 0; - for (Object object : dbCourseList) { - DBObject dbObject = (DBObject) object; - Waypoint waypoint = null; - PassingInstruction passingInstructions = null; - String waypointPassingInstruction = (String) dbObject.get(FieldNames.WAYPOINT_PASSINGINSTRUCTIONS.name()); - if (waypointPassingInstruction == null) { - waypointPassingInstruction = (String) dbObject.get(FieldNames.WAYPOINT_PASSINGSIDE.name()); - if(waypointPassingInstruction != null) { - logger.info("Migrating PassingInstruction "+waypointPassingInstruction+" to field name WAYPOINT_PASSINGINSTRUCTIONS"); - if((i==0||i==dbCourseList.size()-1)&&waypointPassingInstruction.toLowerCase().equals("gate")){ - logger.warning("Changing PassingInstructions of first or last Waypoint from Gate to Line."); - waypointPassingInstruction = "Line"; - } - dbObject.put(FieldNames.WAYPOINT_PASSINGINSTRUCTIONS.name(), waypointPassingInstruction); - dbObject.removeField(FieldNames.WAYPOINT_PASSINGSIDE.name()); - } - } - if (waypointPassingInstruction != null) { - passingInstructions = PassingInstruction.valueOfIgnoringCase(waypointPassingInstruction); - } - ControlPoint controlPoint = loadControlPoint((DBObject) dbObject.get(FieldNames.CONTROLPOINT.name())); - if (passingInstructions == null) { - waypoint = new WaypointImpl(controlPoint); - } else { - waypoint = new WaypointImpl(controlPoint, passingInstructions); - } - courseData.addWaypoint(i++, waypoint); - } - return courseData; - } - - private ControlPoint loadControlPoint(DBObject dbObject) { - String controlPointClass = (String) dbObject.get(FieldNames.CONTROLPOINT_CLASS.name()); - ControlPoint controlPoint = null; - if (controlPointClass != null) { - if (controlPointClass.equals(Mark.class.getSimpleName())) { - Mark mark = loadMark((DBObject) dbObject.get(FieldNames.CONTROLPOINT_VALUE.name())); - controlPoint = mark; - } else if(controlPointClass.equals("Gate")) { - ControlPointWithTwoMarks cpwtm = loadControlPointWithTwoMarks((DBObject) dbObject.get(FieldNames.CONTROLPOINT_VALUE.name())); - dbObject.put(FieldNames.CONTROLPOINT_CLASS.name(), ControlPointWithTwoMarks.class.getSimpleName()); - controlPoint = cpwtm; - } else if (controlPointClass.equals(ControlPointWithTwoMarks.class.getSimpleName())) { - ControlPointWithTwoMarks cpwtm = loadControlPointWithTwoMarks((DBObject) dbObject.get(FieldNames.CONTROLPOINT_VALUE.name())); - controlPoint = cpwtm; - } - } - return controlPoint; - } - - /** - * Checks for the old GATE fields and migrates them to the new CONTROLPOINTWITHTWOMARKS fields. - */ - @SuppressWarnings("deprecation") // Used for migrating old races - private ControlPointWithTwoMarks loadControlPointWithTwoMarks(DBObject dbObject) { - String controlPointName = (String) dbObject.get(FieldNames.CONTROLPOINTWITHTWOMARKS_NAME.name()); - if (controlPointName == null) { - controlPointName = (String) dbObject.get(FieldNames.GATE_NAME.name()); - logger.info("Migrating name of ControlPointWithTwoMarks " + controlPointName - + " from GATE_NAME to new field CONTROLPOINTWITHTWOMARKS_NAME."); - dbObject.put(FieldNames.CONTROLPOINTWITHTWOMARKS_NAME.name(), controlPointName); - dbObject.removeField(FieldNames.GATE_NAME.name()); - } - Serializable controlPointId = (Serializable) dbObject.get(FieldNames.CONTROLPOINTWITHTWOMARKS_ID.name()); - if (controlPointId == null) { - controlPointId = (Serializable) dbObject.get(FieldNames.GATE_ID.name()); - logger.info("Migrating id of ControlPointWithTwoMarks " + controlPointName - + " from old field GATE_ID to CONTROLPOINTWITHTWOMARKS_ID."); - dbObject.put(FieldNames.CONTROLPOINTWITHTWOMARKS_ID.name(), controlPointId); - dbObject.removeField(FieldNames.GATE_ID.name()); - } - DBObject dbLeft = (DBObject) dbObject.get(FieldNames.CONTROLPOINTWITHTWOMARKS_LEFT.name()); - if (dbLeft == null) { - dbLeft = (DBObject) dbObject.get(FieldNames.GATE_LEFT.name()); - logger.info("Migrating left Mark of ControlPointWithTwoMarks " + controlPointName + " from old field GATE_LEFT to CONTROLPOINTWITHTWOMARKS_LEFT"); - dbObject.put(FieldNames.CONTROLPOINTWITHTWOMARKS_LEFT.name(), dbLeft); - dbObject.removeField(FieldNames.GATE_LEFT.name()); - } - Mark leftMark = loadMark(dbLeft); - DBObject dbRight = (DBObject) dbObject.get(FieldNames.CONTROLPOINTWITHTWOMARKS_RIGHT.name()); - if (dbRight == null) { - dbRight = (DBObject) dbObject.get(FieldNames.GATE_RIGHT.name()); - logger.info("Migrating right Mark of ControlPointWithTwoMarks " + controlPointName + " from old field GATE_RIGHT to CONTROLPOINTWITHTWOMARKS_RIGHT"); - dbObject.put(FieldNames.CONTROLPOINTWITHTWOMARKS_RIGHT.name(), dbRight); - dbObject.removeField(FieldNames.GATE_RIGHT.name()); - } - Mark rightMark = loadMark(dbRight); - ControlPointWithTwoMarks gate = baseDomainFactory.createControlPointWithTwoMarks(controlPointId, leftMark, rightMark, controlPointName); - return gate; - } - - private Mark loadMark(DBObject dbObject) { - Serializable markId = (Serializable) dbObject.get(FieldNames.MARK_ID.name()); - String markColor = (String) dbObject.get(FieldNames.MARK_COLOR.name()); - String markName = (String) dbObject.get(FieldNames.MARK_NAME.name()); - String markPattern = (String) dbObject.get(FieldNames.MARK_PATTERN.name()); - String markShape = (String) dbObject.get(FieldNames.MARK_SHAPE.name()); - MarkType markType = MarkType.valueOf((String) dbObject.get(FieldNames.MARK_TYPE.name())); - - Mark mark = baseDomainFactory.getOrCreateMark(markId, markName, markType, markColor, markShape, markPattern); - return mark; - } - - @Override - public Collection loadAllCompetitors() { - ArrayList result = new ArrayList(); - DBCollection collection = database.getCollection(CollectionNames.COMPETITORS.name()); - try { - for (DBObject o : collection.find()) { - JSONObject json = Helpers.toJSONObjectSafe(new JSONParser().parse(JSON.serialize(o))); - Competitor c = competitorDeserializer.deserialize(json); - result.add(c); - } - } catch (Exception e) { - logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load competitors."); - logger.log(Level.SEVERE, "loadCompetitors", e); - } - return result; - } - - @Override - public Iterable> loadAllDeviceConfigurations() { - Map result = new HashMap<>(); - DBCollection configurationCollection = database.getCollection(CollectionNames.CONFIGURATIONS.name()); - - try { - for (DBObject dbObject : configurationCollection.find()) { - Pair entry = loadConfigurationEntry(dbObject); - result.put(entry.getA(), entry.getB()); - } - } catch (Exception e) { - logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load configurations."); - logger.log(Level.SEVERE, "loadAllDeviceConfigurations", e); - } - - return result.entrySet(); - } - - private Pair loadConfigurationEntry(DBObject dbObject) { - DBObject matcherObject = (DBObject) dbObject.get(FieldNames.CONFIGURATION_MATCHER.name()); - DBObject configObject = (DBObject) dbObject.get(FieldNames.CONFIGURATION_CONFIG.name()); - return new Pair(loadConfigurationMatcher(matcherObject), - loadConfiguration(configObject)); - } - - private DeviceConfigurationMatcher loadConfigurationMatcher(DBObject matcherObject) { - DeviceConfigurationMatcherType type = DeviceConfigurationMatcherType.valueOf( - matcherObject.get(FieldNames.CONFIGURATION_MATCHER_TYPE.name()).toString()); - List clientIdentifiers = new ArrayList(); - BasicDBList clientIdentifiersObject = (BasicDBList) matcherObject.get(FieldNames.CONFIGURATION_MATCHER_CLIENTS.name()); - if (clientIdentifiersObject != null) { - for (Object clientIdentifier : clientIdentifiersObject) { - clientIdentifiers.add(clientIdentifier.toString()); - } - } - return baseDomainFactory.getOrCreateDeviceConfigurationMatcher(type, clientIdentifiers); - } - - private DeviceConfiguration loadConfiguration(DBObject configObject) { - DeviceConfiguration configuration = null; - try { - JsonDeserializer deserializer = DeviceConfigurationJsonDeserializer.create(); - JSONObject json = Helpers.toJSONObjectSafe(new JSONParser().parse(JSON.serialize(configObject))); - configuration = deserializer.deserialize(json); - } catch (JsonDeserializationException | ParseException e) { - logger.log(Level.SEVERE, "Error parsing configuration object from MongoDB, falling back to empty configuration."); - logger.log(Level.SEVERE, "loadConfiguration", e); - configuration = new DeviceConfigurationImpl(new RegattaConfigurationImpl()); - } - return configuration; - } -} +package com.sap.sailing.domain.persistence.impl; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bson.types.ObjectId; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import com.mongodb.BasicDBList; +import com.mongodb.BasicDBObject; +import com.mongodb.BasicDBObjectBuilder; +import com.mongodb.DB; +import com.mongodb.DBCollection; +import com.mongodb.DBCursor; +import com.mongodb.DBObject; +import com.mongodb.MongoException; +import com.mongodb.util.JSON; +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.ControlPointWithTwoMarks; +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.base.CourseBase; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Event; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.RegattaRegistry; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.Venue; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.base.configuration.DeviceConfiguration; +import com.sap.sailing.domain.base.configuration.DeviceConfigurationMatcher; +import com.sap.sailing.domain.base.configuration.RegattaConfiguration; +import com.sap.sailing.domain.base.configuration.impl.DeviceConfigurationImpl; +import com.sap.sailing.domain.base.configuration.impl.RegattaConfigurationImpl; +import com.sap.sailing.domain.base.impl.CourseDataImpl; +import com.sap.sailing.domain.base.impl.EventImpl; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.base.impl.RegattaImpl; +import com.sap.sailing.domain.base.impl.SeriesImpl; +import com.sap.sailing.domain.base.impl.VenueImpl; +import com.sap.sailing.domain.base.impl.WaypointImpl; +import com.sap.sailing.domain.common.Color; +import com.sap.sailing.domain.common.MarkType; +import com.sap.sailing.domain.common.MaxPointsReason; +import com.sap.sailing.domain.common.PassingInstruction; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.RegattaName; +import com.sap.sailing.domain.common.RegattaNameAndRaceName; +import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sailing.domain.common.SpeedWithBearing; +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.configuration.DeviceConfigurationMatcherType; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.impl.RGBColor; +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.common.impl.WindSourceWithAdditionalID; +import com.sap.sailing.domain.common.racelog.Flags; +import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; +import com.sap.sailing.domain.common.racelog.RacingProcedureType; +import com.sap.sailing.domain.common.racelog.tracking.NoCorrespondingServiceRegisteredException; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinder; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinderFactory; +import com.sap.sailing.domain.leaderboard.DelayedLeaderboardCorrections; +import com.sap.sailing.domain.leaderboard.DelayedLeaderboardCorrections.LeaderboardCorrectionsResolvedListener; +import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.LeaderboardGroup; +import com.sap.sailing.domain.leaderboard.LeaderboardRegistry; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.SettableScoreCorrection; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sailing.domain.leaderboard.impl.DelayedLeaderboardCorrectionsImpl; +import com.sap.sailing.domain.leaderboard.impl.FlexibleLeaderboardImpl; +import com.sap.sailing.domain.leaderboard.impl.LeaderboardGroupImpl; +import com.sap.sailing.domain.leaderboard.impl.RegattaLeaderboardImpl; +import com.sap.sailing.domain.leaderboard.impl.ThresholdBasedResultDiscardingRuleImpl; +import com.sap.sailing.domain.leaderboard.meta.LeaderboardGroupMetaLeaderboard; +import com.sap.sailing.domain.persistence.DomainObjectFactory; +import com.sap.sailing.domain.persistence.MongoRaceLogStoreFactory; +import com.sap.sailing.domain.persistence.racelog.tracking.DeviceIdentifierMongoHandler; +import com.sap.sailing.domain.persistence.racelog.tracking.impl.PlaceHolderDeviceIdentifierMongoHandler; +import com.sap.sailing.domain.racelog.CompetitorResults; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogCourseAreaChangedEvent; +import com.sap.sailing.domain.racelog.RaceLogCourseDesignChangedEvent; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventRestoreFactory; +import com.sap.sailing.domain.racelog.RaceLogFinishPositioningConfirmedEvent; +import com.sap.sailing.domain.racelog.RaceLogFinishPositioningListChangedEvent; +import com.sap.sailing.domain.racelog.RaceLogFlagEvent; +import com.sap.sailing.domain.racelog.RaceLogGateLineOpeningTimeEvent; +import com.sap.sailing.domain.racelog.RaceLogIdentifier; +import com.sap.sailing.domain.racelog.RaceLogPassChangeEvent; +import com.sap.sailing.domain.racelog.RaceLogPathfinderEvent; +import com.sap.sailing.domain.racelog.RaceLogProtestStartTimeEvent; +import com.sap.sailing.domain.racelog.RaceLogRaceStatusEvent; +import com.sap.sailing.domain.racelog.RaceLogStartProcedureChangedEvent; +import com.sap.sailing.domain.racelog.RaceLogStartTimeEvent; +import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.racelog.RaceLogWindFixEvent; +import com.sap.sailing.domain.racelog.RevokeEvent; +import com.sap.sailing.domain.racelog.impl.CompetitorResultsImpl; +import com.sap.sailing.domain.racelog.impl.RaceLogEventAuthorImpl; +import com.sap.sailing.domain.racelog.impl.RaceLogImpl; +import com.sap.sailing.domain.racelog.tracking.CloseOpenEndedDeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DefineMarkEvent; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceCompetitorMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.DeviceMarkMappingEvent; +import com.sap.sailing.domain.racelog.tracking.RegisterCompetitorEvent; +import com.sap.sailing.domain.racelog.tracking.StartTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.impl.PlaceHolderDeviceIdentifierSerializationHandler; +import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; +import com.sap.sailing.domain.tracking.Wind; +import com.sap.sailing.domain.tracking.WindTrack; +import com.sap.sailing.domain.tracking.impl.WindImpl; +import com.sap.sailing.domain.tracking.impl.WindTrackImpl; +import com.sap.sailing.server.gateway.deserialization.JsonDeserializationException; +import com.sap.sailing.server.gateway.deserialization.JsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.CompetitorJsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.DeviceConfigurationJsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.Helpers; +import com.sap.sailing.server.gateway.deserialization.impl.RegattaConfigurationJsonDeserializer; + +public class DomainObjectFactoryImpl implements DomainObjectFactory { + private static final Logger logger = Logger.getLogger(DomainObjectFactoryImpl.class.getName()); + private final CompetitorJsonDeserializer competitorDeserializer; + + private final DB database; + + private RaceLogEventRestoreFactory raceLogEventFactory; + private final DomainFactory baseDomainFactory; + private final TypeBasedServiceFinder deviceIdentifierServiceFinder; + private final TypeBasedServiceFinderFactory serviceFinderFactory; + + /** + * Uses null as the {@link TypeBasedServiceFinder}, meaning that no {@link DeviceIdentifier}s can be loaded + * using this instance of a {@link DomainObjectFactory}. + */ + public DomainObjectFactoryImpl(DB db, DomainFactory baseDomainFactory) { + this(db, baseDomainFactory, /* deviceTypeServiceFinder */ null); + } + + public DomainObjectFactoryImpl(DB db, DomainFactory baseDomainFactory, TypeBasedServiceFinderFactory serviceFinderFactory) { + super(); + this.serviceFinderFactory = serviceFinderFactory; + if (serviceFinderFactory != null) { + this.deviceIdentifierServiceFinder = serviceFinderFactory.createServiceFinder(DeviceIdentifierMongoHandler.class); + this.deviceIdentifierServiceFinder.setFallbackService(new PlaceHolderDeviceIdentifierMongoHandler()); + } else { + this.deviceIdentifierServiceFinder = null; + } + this.baseDomainFactory = baseDomainFactory; + this.competitorDeserializer = CompetitorJsonDeserializer.create(baseDomainFactory); + this.database = db; + this.raceLogEventFactory = RaceLogEventRestoreFactory.INSTANCE; + } + + @Override + public DomainFactory getBaseDomainFactory() { + return baseDomainFactory; + } + + public Wind loadWind(DBObject object) { + return new WindImpl(loadPosition(object), loadTimePoint(object), loadSpeedWithBearing(object)); + } + + public Position loadPosition(DBObject object) { + Number latNumber = (Number) object.get(FieldNames.LAT_DEG.name()); + Double lat = latNumber == null ? null : latNumber.doubleValue(); + Number lngNumber = (Number) object.get(FieldNames.LNG_DEG.name()); + Double lng = lngNumber == null ? null : lngNumber.doubleValue(); + if (lat != null && lng != null) { + return new DegreePosition(lat, lng); + } else { + return null; + } + } + + public static TimePoint loadTimePoint(DBObject object, String fieldName) { + TimePoint result = null; + Number timePointAsNumber = (Number) object.get(fieldName); + if (timePointAsNumber != null) { + result = new MillisecondsTimePoint(timePointAsNumber.longValue()); + } + return result; + } + + public static TimePoint loadTimePoint(DBObject object, FieldNames field) { + return loadTimePoint(object, field.name()); + } + + /** + * Loads a {@link TimePoint} on the given object at {@link FieldNames#TIME_AS_MILLIS}. + */ + public TimePoint loadTimePoint(DBObject object) { + return loadTimePoint(object, FieldNames.TIME_AS_MILLIS); + } + + public SpeedWithBearing loadSpeedWithBearing(DBObject object) { + return new KnotSpeedWithBearingImpl(((Number) object.get(FieldNames.KNOT_SPEED.name())).doubleValue(), + new DegreeBearingImpl(((Number) object.get(FieldNames.DEGREE_BEARING.name())).doubleValue())); + } + + @Override + public RaceIdentifier loadRaceIdentifier(DBObject dbObject) { + RaceIdentifier result = null; + String regattaName = (String) dbObject.get(FieldNames.EVENT_NAME.name()); + String raceName = (String) dbObject.get(FieldNames.RACE_NAME.name()); + if (regattaName != null && raceName != null) { + result = new RegattaNameAndRaceName(regattaName, raceName); + } + return result; + } + + private void ensureIndicesOnWindTracks(DBCollection windTracks) { + windTracks.ensureIndex(FieldNames.RACE_ID.name()); // for new programmatic access + windTracks.ensureIndex(FieldNames.REGATTA_NAME.name()); // for export or human look-up + // for legacy access to not yet migrated fixes + windTracks.ensureIndex(new BasicDBObjectBuilder().add(FieldNames.EVENT_NAME.name(), 1) + .add(FieldNames.RACE_NAME.name(), 1).get()); + // Unique index + try { + windTracks.ensureIndex( + new BasicDBObjectBuilder().add(FieldNames.RACE_ID.name(), 1) + .add(FieldNames.WIND_SOURCE_NAME.name(), 1).add(FieldNames.WIND_SOURCE_ID.name(), 1) + .add(FieldNames.WIND.name() + "." + FieldNames.TIME_AS_MILLIS.name(), 1).get(), + new BasicDBObjectBuilder().add("unique", true).add("dropDups", true).get()); + } catch (MongoException exception) { + + if (exception.getCode() == 10092) { + logger.warning(String.format( + "Setting the unique index on the %s collection failed because you have too many duplicates. " + + "This leads to the mongo error code %s and the following message: %s \nTo fix this follow " + + "the steps provided on the wiki page: http://wiki.sapsailing.com/wiki/cook-book#Remove-" + + "duplicates-from-WIND_TRACK-collection", CollectionNames.WIND_TRACKS.name(), + exception.getCode(), exception.getMessage())); + } else { + logger.severe(String.format( + "Setting the unique index on the %s collection failed with error code %s and message: %s", + CollectionNames.WIND_TRACKS.name(), exception.getCode(), exception.getMessage())); + } + } + } + + @Override + public Leaderboard loadLeaderboard(String name, RegattaRegistry regattaRegistry) { + DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); + Leaderboard result = null; + try { + BasicDBObject query = new BasicDBObject(); + query.put(FieldNames.LEADERBOARD_NAME.name(), name); + for (DBObject o : leaderboardCollection.find(query)) { + result = loadLeaderboard(o, regattaRegistry, /* leaderboardRegistry */ null, /* groupForMetaLeaderboard */ null); + } + } catch (Exception e) { + // something went wrong during DB access; report, then use empty new wind track + logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboard "+name+"."); + logger.log(Level.SEVERE, "loadLeaderboard", e); + } + return result; + } + + @Override + public Iterable getAllLeaderboards(RegattaRegistry regattaRegistry, LeaderboardRegistry leaderboardRegistry) { + DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); + Set result = new HashSet(); + try { + for (DBObject o : leaderboardCollection.find()) { + final Leaderboard loadedLeaderboard = loadLeaderboard(o, regattaRegistry, leaderboardRegistry, /* groupForMetaLeaderboard */ null); + if (loadedLeaderboard != null) { + result.add(loadedLeaderboard); + } + } + } catch (Exception e) { + // something went wrong during DB access; report, then use empty new wind track + logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboards."); + logger.log(Level.SEVERE, "getAllLeaderboards", e); + } + return result; + } + + /** + * If the DBObject has a field {@link FieldNames#REGATTA_NAME} then the object represents a + * {@link RegattaLeaderboard}. Otherwise, a {@link FlexibleLeaderboard} will be loaded. + * + * @param leaderboardRegistry + * if not null, then before creating and loading the leaderboard it is looked up in this + * registry and only loaded if not found there. If leaderboardRegistry is null, + * the leaderboard is loaded in any case. If the leaderboard is loaded and + * leaderboardRegistry is not null, the leaderboard loaded is + * {@link LeaderboardRegistry#addLeaderboard(Leaderboard) added to the registry}. + * @param groupForMetaLeaderboard + * if not null, a {@link LeaderboardGroupMetaLeaderboard} instance is created and set as the + * group's {@link LeaderboardGroup#setOverallLeaderboard(Leaderboard) overall leaderboard} + * + * @return null in case the leaderboard couldn't be loaded, e.g., because the regatta referenced by a + * {@link RegattaLeaderboard} cannot be found; the leaderboard loaded or found in + * leaderboardRegistry, otherwise + */ + private Leaderboard loadLeaderboard(DBObject dbLeaderboard, RegattaRegistry regattaRegistry, LeaderboardRegistry leaderboardRegistry, + LeaderboardGroup groupForMetaLeaderboard) { + Leaderboard result = null; + String leaderboardName = (String) dbLeaderboard.get(FieldNames.LEADERBOARD_NAME.name()); + if (leaderboardRegistry != null) { + result = leaderboardRegistry.getLeaderboardByName(leaderboardName); + } + if (result == null) { + ThresholdBasedResultDiscardingRule resultDiscardingRule = loadResultDiscardingRule(dbLeaderboard, FieldNames.LEADERBOARD_DISCARDING_THRESHOLDS); + String regattaName = (String) dbLeaderboard.get(FieldNames.REGATTA_NAME.name()); + if (groupForMetaLeaderboard != null) { + result = new LeaderboardGroupMetaLeaderboard(groupForMetaLeaderboard, loadScoringScheme(dbLeaderboard), resultDiscardingRule); + groupForMetaLeaderboard.setOverallLeaderboard(result); + } else if (regattaName == null) { + result = loadFlexibleLeaderboard(dbLeaderboard, resultDiscardingRule); + } else { + result = loadRegattaLeaderboard(leaderboardName, regattaName, dbLeaderboard, resultDiscardingRule, regattaRegistry); + } + if (result != null) { + final Leaderboard finalResult = result; + finalResult.setDisplayName((String) dbLeaderboard.get(FieldNames.LEADERBOARD_DISPLAY_NAME.name())); + + DelayedLeaderboardCorrections loadedLeaderboardCorrections = new DelayedLeaderboardCorrectionsImpl(result, baseDomainFactory); + final boolean[] needsMigration = new boolean[1]; + loadedLeaderboardCorrections.addLeaderboardCorrectionsResolvedListener(new LeaderboardCorrectionsResolvedListener() { + @Override + public void correctionsResolved(DelayedLeaderboardCorrections delayedLeaderboardCorrections) { + if (needsMigration[0]) { + new MongoObjectFactoryImpl(database).storeLeaderboard(finalResult); + } + } + }); + needsMigration[0] = loadLeaderboardCorrections(dbLeaderboard, loadedLeaderboardCorrections, result.getScoreCorrection()) || needsMigration[0]; + needsMigration[0] = loadSuppressedCompetitors(dbLeaderboard, loadedLeaderboardCorrections) || needsMigration[0]; + loadColumnFactors(dbLeaderboard, result); + // add the leaderboard to the registry + if (leaderboardRegistry != null) { + leaderboardRegistry.addLeaderboard(result); + logger.info("loaded leaderboard "+result.getName()+" into "+leaderboardRegistry); + } + } + } + return result; + } + + private void loadColumnFactors(DBObject dbLeaderboard, Leaderboard result) { + DBObject dbColumnFactors = (DBObject) dbLeaderboard.get(FieldNames.LEADERBOARD_COLUMN_FACTORS.name()); + if (dbColumnFactors != null) { + for (String encodedRaceColumnName : dbColumnFactors.keySet()) { + double factor = ((Number) dbColumnFactors.get(encodedRaceColumnName)).doubleValue(); + String raceColumnName = MongoUtils.unescapeDollarAndDot(encodedRaceColumnName); + final RaceColumn raceColumn = result.getRaceColumnByName(raceColumnName); + if (raceColumn != null) { + raceColumn.setFactor(factor); + } else { + logger.warning("Expected to find race column named "+raceColumnName+" in leaderboard "+result.getName()+ + " to apply column factor "+factor+", but the race column wasn't found. Ignoring factor."); + } + } + } + } + + private boolean loadSuppressedCompetitors(DBObject dbLeaderboard, + DelayedLeaderboardCorrections loadedLeaderboardCorrections) { + final boolean needsMigration; + BasicDBList dbSuppressedCompetitorNames = (BasicDBList) dbLeaderboard.get(FieldNames.LEADERBOARD_SUPPRESSED_COMPETITORS.name()); + if (dbSuppressedCompetitorNames != null) { + needsMigration = true; + for (Object escapedCompetitorName : dbSuppressedCompetitorNames) { + loadedLeaderboardCorrections.suppressCompetitorByName(MongoUtils.unescapeDollarAndDot((String) escapedCompetitorName)); + } + } else { + needsMigration = false; + } + BasicDBList dbSuppressedCompetitorIDs = (BasicDBList) dbLeaderboard.get(FieldNames.LEADERBOARD_SUPPRESSED_COMPETITOR_IDS.name()); + if (dbSuppressedCompetitorIDs != null) { + for (Object competitorId : dbSuppressedCompetitorIDs) { + loadedLeaderboardCorrections.suppressCompetitorById((Serializable) competitorId); + } + } + return needsMigration; + } + + /** + * @param dbObject expects to find a field identified by field which holds a {@link BasicDBList} + */ + private ThresholdBasedResultDiscardingRule loadResultDiscardingRule(DBObject dbObject, FieldNames field) { + BasicDBList dbDiscardIndexResultsStartingWithHowManyRaces = (BasicDBList) dbObject.get(field.name()); + final ThresholdBasedResultDiscardingRule result; + if (dbDiscardIndexResultsStartingWithHowManyRaces == null) { + result = null; + } else { + int[] discardIndexResultsStartingWithHowManyRaces = new int[dbDiscardIndexResultsStartingWithHowManyRaces + .size()]; + int i = 0; + for (Object discardingThresholdAsObject : dbDiscardIndexResultsStartingWithHowManyRaces) { + discardIndexResultsStartingWithHowManyRaces[i++] = (Integer) discardingThresholdAsObject; + } + result = new ThresholdBasedResultDiscardingRuleImpl(discardIndexResultsStartingWithHowManyRaces); + } + return result; + } + + /** + * @return null if the regatta cannot be resolved; otherwise the leaderboard for the regatta specified + */ + private RegattaLeaderboard loadRegattaLeaderboard(String leaderboardName, String regattaName, DBObject dbLeaderboard, + ThresholdBasedResultDiscardingRule resultDiscardingRule, RegattaRegistry regattaRegistry) { + RegattaLeaderboard result = null; + Regatta regatta = regattaRegistry.getRegatta(new RegattaName(regattaName)); + if (regatta == null) { + logger.info("Couldn't find regatta "+regattaName+" for corresponding regatta leaderboard. Not loading regatta leaderboard."); + } else { + result = new RegattaLeaderboardImpl(regatta, resultDiscardingRule); + result.setName(leaderboardName); + } + return result; + } + + private FlexibleLeaderboard loadFlexibleLeaderboard(DBObject dbLeaderboard, + ThresholdBasedResultDiscardingRule resultDiscardingRule) { + final FlexibleLeaderboardImpl result; + BasicDBList dbRaceColumns = (BasicDBList) dbLeaderboard.get(FieldNames.LEADERBOARD_COLUMNS.name()); + if (dbRaceColumns == null) { + // this was probably an orphaned overall leaderboard + logger.warning("Probably found orphan overall leaderboard named " + + dbLeaderboard.get(FieldNames.LEADERBOARD_NAME.name())+". Ignoring."); + result = null; + } else { + final ScoringScheme scoringScheme = loadScoringScheme(dbLeaderboard); + RaceLogStore raceLogStore = MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore( + new MongoObjectFactoryImpl(database), this); + + Serializable courseAreaId = (Serializable) dbLeaderboard.get(FieldNames.COURSE_AREA_ID.name()); + CourseArea courseArea = null; + if (courseAreaId != null) { + UUID courseAreaUuid = UUID.fromString(courseAreaId.toString()); + courseArea = baseDomainFactory.getExistingCourseAreaById(courseAreaUuid); + } + + result = new FlexibleLeaderboardImpl(raceLogStore, (String) dbLeaderboard.get(FieldNames.LEADERBOARD_NAME + .name()), resultDiscardingRule, scoringScheme, courseArea); + // For a FlexibleLeaderboard, fleets are owned by the leaderboard's RaceColumn objects. We need to manage + // them here: + Map fleetsByName = new HashMap(); + for (Object dbRaceColumnAsObject : dbRaceColumns) { + BasicDBObject dbRaceColumn = (BasicDBObject) dbRaceColumnAsObject; + Map raceIdentifiers = loadRaceIdentifiers(dbRaceColumn); + RaceIdentifier defaultFleetRaceIdentifier = raceIdentifiers.get(null); + if (defaultFleetRaceIdentifier != null) { + Fleet defaultFleet = result.getFleet(null); + if (defaultFleet != null) { + raceIdentifiers.put(defaultFleet.getName(), defaultFleetRaceIdentifier); + } else { + // leaderboard has no default fleet; don't know what to do with default RaceIdentifier + logger.warning("Discarding RaceIdentifier " + defaultFleetRaceIdentifier + + " for default fleet for leaderboard " + result.getName() + + " because no default fleet was found in leaderboard"); + } + raceIdentifiers.remove(null); + } + List fleets = new ArrayList(); + for (String fleetName : raceIdentifiers.keySet()) { + Fleet fleet = fleetsByName.get(fleetName); + if (fleet == null) { + fleet = new FleetImpl(fleetName); + fleetsByName.put(fleetName, fleet); + } + fleets.add(fleet); + } + if (fleets.isEmpty()) { + fleets.add(result.getFleet(null)); + } + String columnName = (String) dbRaceColumn.get(FieldNames.LEADERBOARD_COLUMN_NAME.name()); + + RaceColumn raceColumn = result.addRaceColumn(columnName, + (Boolean) dbRaceColumn.get(FieldNames.LEADERBOARD_IS_MEDAL_RACE_COLUMN.name()), + fleets.toArray(new Fleet[0])); + for (Map.Entry e : raceIdentifiers.entrySet()) { + raceColumn.setRaceIdentifier(fleetsByName.get(e.getKey()), e.getValue()); + } + + } + } + return result; + } + + private ScoringScheme loadScoringScheme(DBObject dbLeaderboard) { + ScoringSchemeType scoringSchemeType = getScoringSchemeType(dbLeaderboard); + final ScoringScheme scoringScheme = baseDomainFactory.createScoringScheme(scoringSchemeType); + return scoringScheme; + } + + private boolean loadLeaderboardCorrections(DBObject dbLeaderboard, DelayedLeaderboardCorrections correctionsToUpdate, + SettableScoreCorrection scoreCorrectionToUpdate) { + boolean needsMigration = false; + DBObject carriedPoints = (DBObject) dbLeaderboard.get(FieldNames.LEADERBOARD_CARRIED_POINTS.name()); + if (carriedPoints != null) { + needsMigration = true; + for (String escapedCompetitorName : carriedPoints.keySet()) { + Double carriedPointsForCompetitor = ((Number) carriedPoints.get(escapedCompetitorName)).doubleValue(); + if (carriedPointsForCompetitor != null) { + correctionsToUpdate.setCarriedPointsByName(MongoUtils.unescapeDollarAndDot(escapedCompetitorName), carriedPointsForCompetitor); + } + } + } + BasicDBList carriedPointsById = (BasicDBList) dbLeaderboard.get(FieldNames.LEADERBOARD_CARRIED_POINTS_BY_ID.name()); + if (carriedPointsById != null) { + for (Object o : carriedPointsById) { + DBObject competitorIdAndCarriedPoints = (DBObject) o; + Serializable competitorId = (Serializable) competitorIdAndCarriedPoints.get(FieldNames.COMPETITOR_ID.name()); + Double carriedPointsForCompetitor = ((Number) competitorIdAndCarriedPoints + .get(FieldNames.LEADERBOARD_CARRIED_POINTS.name())).doubleValue(); + if (carriedPointsForCompetitor != null) { + correctionsToUpdate.setCarriedPointsByID(competitorId, carriedPointsForCompetitor); + } + } + } + DBObject dbScoreCorrection = (DBObject) dbLeaderboard.get(FieldNames.LEADERBOARD_SCORE_CORRECTIONS.name()); + if (dbScoreCorrection.containsField(FieldNames.LEADERBOARD_SCORE_CORRECTION_TIMESTAMP.name())) { + scoreCorrectionToUpdate.setTimePointOfLastCorrectionsValidity( + new MillisecondsTimePoint((Long) dbScoreCorrection.get(FieldNames.LEADERBOARD_SCORE_CORRECTION_TIMESTAMP.name()))); + dbScoreCorrection.removeField(FieldNames.LEADERBOARD_SCORE_CORRECTION_TIMESTAMP.name()); + } + if (dbScoreCorrection.containsField(FieldNames.LEADERBOARD_SCORE_CORRECTION_COMMENT.name())) { + scoreCorrectionToUpdate.setComment((String) dbScoreCorrection.get(FieldNames.LEADERBOARD_SCORE_CORRECTION_COMMENT.name())); + dbScoreCorrection.removeField(FieldNames.LEADERBOARD_SCORE_CORRECTION_COMMENT.name()); + } + for (String escapedRaceColumnName : dbScoreCorrection.keySet()) { + // deprecated style: a DBObject per race where the keys are the escaped competitor names + // new style: a BasicDBList per race where each entry is a DBObject with COMPETITOR_ID and + // LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON and LEADERBOARD_CORRECTED_SCORE fields each + DBObject dbScoreCorrectionForRace = (DBObject) dbScoreCorrection.get(escapedRaceColumnName); + final RaceColumn raceColumn = correctionsToUpdate.getLeaderboard().getRaceColumnByName(MongoUtils.unescapeDollarAndDot(escapedRaceColumnName)); + if (raceColumn != null) { + if (dbScoreCorrectionForRace instanceof BasicDBList) { + for (Object o : (BasicDBList) dbScoreCorrectionForRace) { + DBObject dbScoreCorrectionForCompetitorInRace = (DBObject) o; + Serializable competitorId = (Serializable) dbScoreCorrectionForCompetitorInRace + .get(FieldNames.COMPETITOR_ID.name()); + if (dbScoreCorrectionForCompetitorInRace + .containsField(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name())) { + correctionsToUpdate.setMaxPointsReasonByID(competitorId, raceColumn, MaxPointsReason + .valueOf((String) dbScoreCorrectionForCompetitorInRace + .get(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name()))); + } + if (dbScoreCorrectionForCompetitorInRace.containsField(FieldNames.LEADERBOARD_CORRECTED_SCORE + .name())) { + final Double leaderboardCorrectedScore = ((Number) dbScoreCorrectionForCompetitorInRace + .get(FieldNames.LEADERBOARD_CORRECTED_SCORE.name())).doubleValue(); + correctionsToUpdate.correctScoreByID(competitorId, raceColumn, + (Double) leaderboardCorrectedScore); + } + } + } else { + needsMigration = true; + for (String competitorName : dbScoreCorrectionForRace.keySet()) { + DBObject dbScoreCorrectionForCompetitorInRace = (DBObject) dbScoreCorrectionForRace + .get(competitorName); + if (dbScoreCorrectionForCompetitorInRace + .containsField(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name())) { + correctionsToUpdate.setMaxPointsReasonByName(MongoUtils + .unescapeDollarAndDot(competitorName), raceColumn, MaxPointsReason + .valueOf((String) dbScoreCorrectionForCompetitorInRace + .get(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name()))); + } + if (dbScoreCorrectionForCompetitorInRace.containsField(FieldNames.LEADERBOARD_CORRECTED_SCORE + .name())) { + final Double leaderboardCorrectedScore = ((Number) dbScoreCorrectionForCompetitorInRace + .get(FieldNames.LEADERBOARD_CORRECTED_SCORE.name())).doubleValue(); + correctionsToUpdate.correctScoreByName(MongoUtils.unescapeDollarAndDot(competitorName), + raceColumn, (Double) leaderboardCorrectedScore); + } + } + } + } else { + logger.warning("Couldn't find race column " + MongoUtils.unescapeDollarAndDot(escapedRaceColumnName) + + " in leaderboard " + correctionsToUpdate.getLeaderboard().getName()); + } + } + DBObject competitorDisplayNames = (DBObject) dbLeaderboard.get(FieldNames.LEADERBOARD_COMPETITOR_DISPLAY_NAMES.name()); + // deprecated style: a DBObject whose keys are the escaped competitor names + // new style: a BasicDBList whose entries are DBObjects with COMPETITOR_ID and COMPETITOR_DISPLAY_NAME fields + if (competitorDisplayNames != null) { + if (competitorDisplayNames instanceof BasicDBList) { + for (Object o : (BasicDBList) competitorDisplayNames) { + DBObject competitorDisplayName = (DBObject) o; + final Serializable competitorId = (Serializable) competitorDisplayName.get(FieldNames.COMPETITOR_ID.name()); + final String displayName = (String) competitorDisplayName.get(FieldNames.COMPETITOR_DISPLAY_NAME.name()); + correctionsToUpdate.setDisplayNameByID(competitorId, displayName); + } + } else { + needsMigration = true; + for (String escapedCompetitorName : competitorDisplayNames.keySet()) { + correctionsToUpdate.setDisplayNameByName(MongoUtils.unescapeDollarAndDot(escapedCompetitorName), + (String) competitorDisplayNames.get(escapedCompetitorName)); + } + } + } + return needsMigration; + } + + /** + * Expects a DBObject under the key {@link FieldNames#RACE_IDENTIFIERS} whose keys are the fleet names and whose + * values are the race identifiers as DBObjects (see {@link #loadRaceIdentifier(DBObject)}). If legacy DB instances + * have a {@link RaceIdentifier} that is not associated with a fleet name, it may be stored directly in the + * dbRaceColumn. In this case, it is returned with null as the fleet name key. + * + * @return a map with fleet names as key and the corresponding fleet's race identifier as value; the special + * null key is used to identify a "default fleet" for backward compatibility with stored + * leaderboards which don't know about fleets yet; this key should be mapped to the leaderboard's default + * fleet. + */ + private Map loadRaceIdentifiers(DBObject dbRaceColumn) { + Map result = new HashMap(); + // try to load a deprecated single race identifier to associate with the default fleet: + RaceIdentifier singleLegacyRaceIdentifier = loadRaceIdentifier(dbRaceColumn); + if (singleLegacyRaceIdentifier != null) { + result.put(null, singleLegacyRaceIdentifier); + } + DBObject raceIdentifiersPerFleet = (DBObject) dbRaceColumn.get(FieldNames.RACE_IDENTIFIERS.name()); + if (raceIdentifiersPerFleet != null) { + for (String escapedFleetName : raceIdentifiersPerFleet.keySet()) { + String fleetName = MongoUtils.unescapeDollarAndDot(escapedFleetName); + result.put(fleetName, loadRaceIdentifier((DBObject) raceIdentifiersPerFleet.get(fleetName))); + } + } + return result; + } + + @Override + public LeaderboardGroup loadLeaderboardGroup(String name, RegattaRegistry regattaRegistry, LeaderboardRegistry leaderboardRegistry) { + DBCollection leaderboardGroupCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()); + LeaderboardGroup leaderboardGroup = null; + try { + BasicDBObject query = new BasicDBObject(); + query.put(FieldNames.LEADERBOARD_GROUP_NAME.name(), name); + leaderboardGroup = loadLeaderboardGroup(leaderboardGroupCollection.findOne(query), regattaRegistry, leaderboardRegistry); + } catch (Exception e) { + logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboard group "+name+"."); + logger.log(Level.SEVERE, "loadLeaderboardGroup", e); + } + + return leaderboardGroup; + } + + @Override + public Iterable getAllLeaderboardGroups(RegattaRegistry regattaRegistry, LeaderboardRegistry leaderboardRegistry) { + DBCollection leaderboardGroupCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()); + Set leaderboardGroups = new HashSet(); + try { + for (DBObject o : leaderboardGroupCollection.find()) { + leaderboardGroups.add(loadLeaderboardGroup(o, regattaRegistry, leaderboardRegistry)); + } + } catch (Exception e) { + logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboard groups."); + logger.log(Level.SEVERE, "loadLeaderboardGroup", e); + } + + return leaderboardGroups; + } + + private LeaderboardGroup loadLeaderboardGroup(DBObject o, RegattaRegistry regattaRegistry, LeaderboardRegistry leaderboardRegistry) { + DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); + String name = (String) o.get(FieldNames.LEADERBOARD_GROUP_NAME.name()); + String description = (String) o.get(FieldNames.LEADERBOARD_GROUP_DESCRIPTION.name()); + boolean displayGroupsInReverseOrder = false; // default value + Object displayGroupsInReverseOrderObj = o.get(FieldNames.LEADERBOARD_GROUP_DISPLAY_IN_REVERSE_ORDER.name()); + if (displayGroupsInReverseOrderObj != null) { + displayGroupsInReverseOrder = (Boolean) displayGroupsInReverseOrderObj; + } + ArrayList leaderboards = new ArrayList(); + BasicDBList dbLeaderboardIds = (BasicDBList) o.get(FieldNames.LEADERBOARD_GROUP_LEADERBOARDS.name()); + for (Object object : dbLeaderboardIds) { + ObjectId dbLeaderboardId = (ObjectId) object; + DBObject dbLeaderboard = leaderboardCollection.findOne(dbLeaderboardId); + if (dbLeaderboard != null) { + final Leaderboard loadedLeaderboard = loadLeaderboard(dbLeaderboard, regattaRegistry, + leaderboardRegistry, /* groupForMetaLeaderboard */null); + if (loadedLeaderboard != null) { + leaderboards.add(loadedLeaderboard); + } + } else { + logger.warning("couldn't find leaderboard with ID "+dbLeaderboardId+" referenced by leaderboard group "+name); + } + } + logger.info("loaded leaderboard group "+name); + LeaderboardGroupImpl result = new LeaderboardGroupImpl(name, description, displayGroupsInReverseOrder, leaderboards); + Object overallLeaderboardIdOrName = o.get(FieldNames.LEADERBOARD_GROUP_OVERALL_LEADERBOARD.name()); + if (overallLeaderboardIdOrName != null) { + final DBObject dbOverallLeaderboard; + if (overallLeaderboardIdOrName instanceof ObjectId) { + dbOverallLeaderboard = leaderboardCollection.findOne(overallLeaderboardIdOrName); + } else { + dbOverallLeaderboard = (DBObject) overallLeaderboardIdOrName; + } + if (dbOverallLeaderboard != null) { + // the loadLeaderboard call adds the overall leaderboard to the leaderboard registry and sets it as the + // overall leaderboard of the leaderboard group + loadLeaderboard(dbOverallLeaderboard, regattaRegistry, leaderboardRegistry, /* groupForMetaLeaderboard */ result); + } + } + return result; + } + + @Override + public Iterable getLeaderboardsNotInGroup(RegattaRegistry regattaRegistry, LeaderboardRegistry leaderboardRegistry) { + DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); + Set result = new HashSet(); + try { + // For MongoDB 2.4 $where with refs to global objects no longer works + // http://docs.mongodb.org/manual/reference/operator/where/#op._S_where + // Also a single where leads to a table walk without using indexes. So avoid $where. + + // Don't change the query object, unless you know what you're doing. + // It queries all leaderboards not referenced to be part of a leaderboard group + // and in particular not being an overall leaderboard of a leaderboard group. + DBCursor allLeaderboards = leaderboardCollection.find(); + for (DBObject leaderboardFromDB : allLeaderboards) { + DBObject inLeaderboardGroupsQuery = new BasicDBObject(); + inLeaderboardGroupsQuery.put(FieldNames.LEADERBOARD_GROUP_LEADERBOARDS.name(), ((ObjectId)leaderboardFromDB.get("_id")).toString()); + boolean inLeaderboardGroups = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()).find(inLeaderboardGroupsQuery).size()>0; + + DBObject inLeaderboardGroupOverallQuery = new BasicDBObject(); + inLeaderboardGroupOverallQuery.put(FieldNames.LEADERBOARD_GROUP_OVERALL_LEADERBOARD.name(), ((ObjectId)leaderboardFromDB.get("_id")).toString()); + boolean inLeaderboardGroupOverall = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()).find(inLeaderboardGroupOverallQuery).size()>0; + + DBObject inLeaderboardGroupOverallQueryName = new BasicDBObject(); + inLeaderboardGroupOverallQueryName.put(FieldNames.LEADERBOARD_GROUP_OVERALL_LEADERBOARD.name(), leaderboardFromDB.get(FieldNames.LEADERBOARD_NAME.name())); + boolean inLeaderboardGroupOverallName = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()).find(inLeaderboardGroupOverallQueryName).size()>0; + + if (!inLeaderboardGroups && !inLeaderboardGroupOverall && !inLeaderboardGroupOverallName) { + final Leaderboard loadedLeaderboard = loadLeaderboard(leaderboardFromDB, regattaRegistry, leaderboardRegistry, /* groupForMetaLeaderboard */ null); + if (loadedLeaderboard != null) { + result.add(loadedLeaderboard); + } + } + } + } catch (Exception e) { + logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load leaderboards."); + logger.log(Level.SEVERE, "getAllLeaderboards", e); + } + return result; + } + + @Override + public WindTrack loadWindTrack(String regattaName, RaceDefinition race, WindSource windSource, long millisecondsOverWhichToAverage) { + final WindTrack result; + Map resultMap = loadWindTracks(regattaName, race, windSource, millisecondsOverWhichToAverage); + if (resultMap.containsKey(windSource)) { + result = resultMap.get(windSource); + } else { + // create an empty wind track as result if no fixes were found in store for the wind source requested + result = new WindTrackImpl(millisecondsOverWhichToAverage, windSource.getType().getBaseConfidence(), + windSource.getType().useSpeed(), + /* nameForReadWriteLock */ WindTrackImpl.class.getSimpleName()+" for source "+windSource.toString()); + } + return result; + } + + @Override + public Map loadWindTracks(String regattaName, RaceDefinition race, + long millisecondsOverWhichToAverageWind) { + Map result = loadWindTracks(regattaName, race, /* constrain wind source */ null, millisecondsOverWhichToAverageWind); + return result; + } + + /** + * @param constrainToWindSource + * if null, wind for all sources will be loaded; otherwise, only wind data for the wind + * source specified by this argument will be loaded + */ + private Map loadWindTracks(String regattaName, RaceDefinition race, + WindSource constrainToWindSource, long millisecondsOverWhichToAverageWind) { + Map result = new HashMap(); + try { + DBCollection windTracks = database.getCollection(CollectionNames.WIND_TRACKS.name()); + ensureIndicesOnWindTracks(windTracks); + BasicDBObject queryById = new BasicDBObject(); + queryById.put(FieldNames.RACE_ID.name(), race.getId()); + if (constrainToWindSource != null) { + queryById.put(FieldNames.WIND_SOURCE_NAME.name(), constrainToWindSource.name()); + } + for (DBObject dbWind : windTracks.find(queryById)) { + loadWindFix(result, dbWind, millisecondsOverWhichToAverageWind); + } + BasicDBObject queryByName = new BasicDBObject(); + queryByName.put(FieldNames.EVENT_NAME.name(), regattaName); + queryByName.put(FieldNames.RACE_NAME.name(), race.getName()); + if (constrainToWindSource != null) { + queryByName.put(FieldNames.WIND_SOURCE_NAME.name(), constrainToWindSource.name()); + } + final DBCursor windFixesFoundByName = windTracks.find(queryByName); + if (windFixesFoundByName.hasNext()) { + List windFixesToMigrate = new ArrayList(); + for (DBObject dbWind : windFixesFoundByName) { + Pair wind = loadWindFix(result, dbWind, millisecondsOverWhichToAverageWind); + // write the wind fix with the new ID-based key and remove the legacy wind fix from the DB + windFixesToMigrate.add(new MongoObjectFactoryImpl(database).storeWindTrackEntry(race, regattaName, + wind.getB(), wind.getA())); + } + logger.info("Migrating "+windFixesFoundByName.size()+" wind fixes of regatta "+regattaName+ + " and race "+race.getName()+" to ID-based keys"); + windTracks.insert(windFixesToMigrate.toArray(new DBObject[windFixesToMigrate.size()])); + logger.info("Removing "+windFixesFoundByName.size()+" wind fixes that were keyed by the names of regatta "+regattaName+ + " and race "+race.getName()); + windTracks.remove(queryByName); + } + } catch (Exception e) { + // something went wrong during DB access; report, then use empty new wind track + logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load recorded wind data. Check MongoDB settings."); + logger.log(Level.SEVERE, "loadWindTrack", e); + } + return result; + } + + private Pair loadWindFix(Map result, DBObject dbWind, long millisecondsOverWhichToAverageWind) { + Wind wind = loadWind((DBObject) dbWind.get(FieldNames.WIND.name())); + WindSourceType windSourceType = WindSourceType.valueOf((String) dbWind.get(FieldNames.WIND_SOURCE_NAME.name())); + WindSource windSource; + if (dbWind.containsField(FieldNames.WIND_SOURCE_ID.name())) { + windSource = new WindSourceWithAdditionalID(windSourceType, (String) dbWind.get(FieldNames.WIND_SOURCE_ID.name())); + } else { + windSource = new WindSourceImpl(windSourceType); + } + WindTrack track = result.get(windSource); + if (track == null) { + track = new WindTrackImpl(millisecondsOverWhichToAverageWind, windSource.getType().getBaseConfidence(), + windSource.getType().useSpeed(), + /* nameForReadWriteLock */ WindTrackImpl.class.getSimpleName()+" for source "+windSource.toString()); + result.put(windSource, track); + } + track.add(wind); + return new Pair(wind, windSource); + } + + @Override + public Event loadEvent(String name) { + Event result; + BasicDBObject query = new BasicDBObject(); + query.put(FieldNames.EVENT_NAME.name(), name); + DBCollection eventCollection = database.getCollection(CollectionNames.EVENTS.name()); + DBObject eventDBObject = eventCollection.findOne(query); + if (eventDBObject != null) { + result = loadEvent(eventDBObject); + } else { + result = null; + } + return result; + } + + @Override + public Iterable loadAllEvents() { + ArrayList result = new ArrayList(); + DBCollection eventCollection = database.getCollection(CollectionNames.EVENTS.name()); + + try { + for (DBObject o : eventCollection.find()) { + result.add(loadEvent(o)); + } + } catch (Exception e) { + logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load events."); + logger.log(Level.SEVERE, "loadEvents", e); + } + + return result; + } + + /** + * An event doesn't store its regattas; it's the regatta that stores a reference to its event; the regatta + * needs to add itself to the event when loaded or instantiated. + */ + private Event loadEvent(DBObject eventDBObject) { + String name = (String) eventDBObject.get(FieldNames.EVENT_NAME.name()); + UUID id = (UUID) eventDBObject.get(FieldNames.EVENT_ID.name()); + TimePoint startDate = loadTimePoint(eventDBObject, FieldNames.EVENT_START_DATE); + TimePoint endDate = loadTimePoint(eventDBObject, FieldNames.EVENT_END_DATE); + boolean isPublic = eventDBObject.get(FieldNames.EVENT_IS_PUBLIC.name()) != null ? (Boolean) eventDBObject.get(FieldNames.EVENT_IS_PUBLIC.name()) : false; + Venue venue = loadVenue((DBObject) eventDBObject.get(FieldNames.VENUE.name())); + Event result = new EventImpl(name, startDate, endDate, venue, isPublic, id); + return result; + } + + private Venue loadVenue(DBObject dbObject) { + String name = (String) dbObject.get(FieldNames.VENUE_NAME.name()); + BasicDBList dbCourseAreas = (BasicDBList) dbObject.get(FieldNames.COURSE_AREAS.name()); + Venue result = new VenueImpl(name); + for (Object courseAreaDBObject : dbCourseAreas) { + CourseArea courseArea = loadCourseArea((DBObject) courseAreaDBObject); + result.addCourseArea(courseArea); + } + return result; + } + + private CourseArea loadCourseArea(DBObject courseAreaDBObject) { + String name = (String) courseAreaDBObject.get(FieldNames.COURSE_AREA_NAME.name()); + UUID id = (UUID) courseAreaDBObject.get(FieldNames.COURSE_AREA_ID.name()); + return baseDomainFactory.getOrCreateCourseArea(id, name); + } + + @Override + public Iterable loadAllRegattas(TrackedRegattaRegistry trackedRegattaRegistry) { + List result = new ArrayList(); + DBCollection regattaCollection = database.getCollection(CollectionNames.REGATTAS.name()); + for (DBObject dbRegatta : regattaCollection.find()) { + result.add(loadRegatta(dbRegatta, trackedRegattaRegistry)); + } + return result; + } + + @Override + public Regatta loadRegatta(String name, TrackedRegattaRegistry trackedRegattaRegistry) { + DBObject query = new BasicDBObject(FieldNames.REGATTA_NAME.name(), name); + DBCollection regattaCollection = database.getCollection(CollectionNames.REGATTAS.name()); + DBObject dbRegatta = regattaCollection.findOne(query); + Regatta result = loadRegatta(dbRegatta, trackedRegattaRegistry); + assert result == null || result.getName().equals(name); + return result; + } + + private Regatta loadRegatta(DBObject dbRegatta, TrackedRegattaRegistry trackedRegattaRegistry) { + Regatta result = null; + if (dbRegatta != null) { + String baseName = (String) dbRegatta.get(FieldNames.REGATTA_BASE_NAME.name()); + String boatClassName = (String) dbRegatta.get(FieldNames.BOAT_CLASS_NAME.name()); + Serializable id = (Serializable) dbRegatta.get(FieldNames.REGATTA_ID.name()); + if (id == null) { + id = baseName + "("+boatClassName+")"; + } + BoatClass boatClass = null; + if (boatClassName != null) { + boolean typicallyStartsUpwind = (Boolean) dbRegatta.get(FieldNames.BOAT_CLASS_TYPICALLY_STARTS_UPWIND.name()); + boatClass = baseDomainFactory.getOrCreateBoatClass(boatClassName, typicallyStartsUpwind); + } + BasicDBList dbSeries = (BasicDBList) dbRegatta.get(FieldNames.REGATTA_SERIES.name()); + Iterable series = loadSeries(dbSeries, trackedRegattaRegistry); + RaceLogStore raceLogStore = MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore( + new MongoObjectFactoryImpl(database, serviceFinderFactory), + this); + + Serializable courseAreaId = (Serializable) dbRegatta.get(FieldNames.COURSE_AREA_ID.name()); + CourseArea courseArea = null; + if (courseAreaId != null) { + UUID courseAreaUuid = UUID.fromString(courseAreaId.toString()); + courseArea = baseDomainFactory.getExistingCourseAreaById(courseAreaUuid); + } + + RegattaConfiguration configuration = null; + if (dbRegatta.containsField(FieldNames.REGATTA_REGATTA_CONFIGURATION.name())) { + try { + JSONObject json = Helpers.toJSONObjectSafe(new JSONParser().parse(JSON.serialize(dbRegatta.get(FieldNames.REGATTA_REGATTA_CONFIGURATION.name())))); + configuration = RegattaConfigurationJsonDeserializer.create().deserialize(json); + } catch (JsonDeserializationException|ParseException e) { + logger.log(Level.WARNING, "Error loading racing procedure configration for regatta.", e); + } + } + + result = new RegattaImpl(raceLogStore, baseName, boatClass, series, /* persistent */ true, loadScoringScheme(dbRegatta), id, courseArea); + result.setRegattaConfiguration(configuration); + } + return result; + } + + private ScoringSchemeType getScoringSchemeType(DBObject dbObject) { + String scoringSchemeTypeName = (String) dbObject.get(FieldNames.SCORING_SCHEME_TYPE.name()); + ScoringSchemeType scoringSchemeType; + if (scoringSchemeTypeName == null) { + scoringSchemeType = ScoringSchemeType.LOW_POINT; // the default + } else { + scoringSchemeType = ScoringSchemeType.valueOf(scoringSchemeTypeName); + } + return scoringSchemeType; + } + + private Iterable loadSeries(BasicDBList dbSeries, TrackedRegattaRegistry trackedRegattaRegistry) { + List result = new ArrayList(); + for (Object o : dbSeries) { + DBObject oneDBSeries = (DBObject) o; + Series series = loadSeries(oneDBSeries, trackedRegattaRegistry); + result.add(series); + } + return result; + } + + private Series loadSeries(DBObject dbSeries, TrackedRegattaRegistry trackedRegattaRegistry) { + String name = (String) dbSeries.get(FieldNames.SERIES_NAME.name()); + boolean isMedal = (Boolean) dbSeries.get(FieldNames.SERIES_IS_MEDAL.name()); + Boolean startsWithZeroScore = (Boolean) dbSeries.get(FieldNames.SERIES_STARTS_WITH_ZERO_SCORE.name()); + Boolean hasSplitFleetContiguousScoring = (Boolean) dbSeries.get(FieldNames.SERIES_HAS_SPLIT_FLEET_CONTIGUOUS_SCORING.name()); + Boolean firstColumnIsNonDiscardableCarryForward = (Boolean) dbSeries.get(FieldNames.SERIES_STARTS_WITH_NON_DISCARDABLE_CARRY_FORWARD.name()); + final BasicDBList dbFleets = (BasicDBList) dbSeries.get(FieldNames.SERIES_FLEETS.name()); + Map fleetsByName = loadFleets(dbFleets); + BasicDBList dbRaceColumns = (BasicDBList) dbSeries.get(FieldNames.SERIES_RACE_COLUMNS.name()); + Iterable raceColumnNames = loadRaceColumnNames(dbRaceColumns, fleetsByName); + Series series = new SeriesImpl(name, isMedal, fleetsByName.values(), raceColumnNames, trackedRegattaRegistry); + if (dbSeries.get(FieldNames.SERIES_DISCARDING_THRESHOLDS.name()) != null) { + ThresholdBasedResultDiscardingRule resultDiscardingRule = loadResultDiscardingRule(dbSeries, FieldNames.SERIES_DISCARDING_THRESHOLDS); + series.setResultDiscardingRule(resultDiscardingRule); + } + if (startsWithZeroScore != null) { + series.setStartsWithZeroScore(startsWithZeroScore); + } + if (hasSplitFleetContiguousScoring != null) { + series.setSplitFleetContiguousScoring(hasSplitFleetContiguousScoring); + } + if (firstColumnIsNonDiscardableCarryForward != null) { + series.setFirstColumnIsNonDiscardableCarryForward(firstColumnIsNonDiscardableCarryForward); + } + loadRaceColumnRaceLinks(dbRaceColumns, series); + return series; + } + + /** + * @param fleetsByName used to ensure the {@link RaceColumn#getFleets()} points to the same {@link Fleet} objects also + * used in the {@link Series#getFleets()} collection. + */ + private Iterable loadRaceColumnNames(BasicDBList dbRaceColumns, Map fleetsByName) { + List result = new ArrayList(); + for (Object o : dbRaceColumns) { + DBObject dbRaceColumn = (DBObject) o; + result.add((String) dbRaceColumn.get(FieldNames.LEADERBOARD_COLUMN_NAME.name())); + } + return result; + } + + private void loadRaceColumnRaceLinks(BasicDBList dbRaceColumns, Series series) { + for (Object o : dbRaceColumns) { + DBObject dbRaceColumn = (DBObject) o; + String name = (String) dbRaceColumn.get(FieldNames.LEADERBOARD_COLUMN_NAME.name()); + Map raceIdentifiersPerFleetName = loadRaceIdentifiers(dbRaceColumn); + for (Map.Entry e : raceIdentifiersPerFleetName.entrySet()) { + // null key for "default" fleet is not acceptable here + if (e.getKey() == null) { + logger.warning("Ignoring null fleet name while loading RaceColumn " + name); + } else { + series.getRaceColumnByName(name).setRaceIdentifier(series.getFleetByName(e.getKey()), e.getValue()); + } + } + } + } + + private Map loadFleets(BasicDBList dbFleets) { + Map result = new HashMap(); + for (Object o : dbFleets) { + DBObject dbFleet = (DBObject) o; + Fleet fleet = loadFleet(dbFleet); + result.put(fleet.getName(), fleet); + } + return result; + } + + private Fleet loadFleet(DBObject dbFleet) { + Fleet result; + String name = (String) dbFleet.get(FieldNames.FLEET_NAME.name()); + Integer ordering = (Integer) dbFleet.get(FieldNames.FLEET_ORDERING.name()); + if (ordering == null) { + ordering = 0; + } + Integer colorAsInt = (Integer) dbFleet.get(FieldNames.FLEET_COLOR.name()); + Color color = null; + if (colorAsInt != null) { + int r = colorAsInt % 256; + int g = (colorAsInt / 256 ) % 256; + int b = (colorAsInt / 256 / 256) % 256; + color = new RGBColor(r, g, b); + } + result = new FleetImpl(name, ordering, color); + return result; + } + + @Override + public Map loadRaceIDToRegattaAssociations(RegattaRegistry regattaRegistry) { + DBCollection raceIDToRegattaCollection = database.getCollection(CollectionNames.REGATTA_FOR_RACE_ID.name()); + Map result = new HashMap(); + for (DBObject o : raceIDToRegattaCollection.find()) { + Regatta regatta = regattaRegistry.getRegattaByName((String) o.get(FieldNames.REGATTA_NAME.name())); + if (regatta != null) { + result.put((String) o.get(FieldNames.RACE_ID_AS_STRING.name()), regatta); + } else { + logger.warning("Couldn't find regatta " + o.get(FieldNames.REGATTA_NAME.name()) + + ". Cannot restore race associations with this regatta."); + } + } + return result; + } + + @Override + public RaceLog loadRaceLog(RaceLogIdentifier identifier) { + RaceLog result = new RaceLogImpl(RaceLogImpl.class.getSimpleName(), identifier.getIdentifier()); + try { + BasicDBObject query = new BasicDBObject(); + query.put(FieldNames.RACE_LOG_IDENTIFIER.name(), TripleSerializer.serialize(identifier.getIdentifier())); + loadRaceLogEvents(result, query); + + // query for events with deprecated identifier format... + query.put(FieldNames.RACE_LOG_IDENTIFIER.name(), MongoUtils.escapeDollarAndDot(identifier.getDeprecatedIdentifier())); + + List eventsToMigrate = loadRaceLogEvents(result, query); + if (!eventsToMigrate.isEmpty()) { + // ... migrate them... + MongoRaceLogStoreVisitor storeVisitor = new MongoRaceLogStoreVisitor(identifier, new MongoObjectFactoryImpl(database, serviceFinderFactory)); + for (RaceLogEvent event : eventsToMigrate) { + event.accept(storeVisitor); + } + // ... and delete the old ones... + database.getCollection(CollectionNames.RACE_LOGS.name()).remove(query); + } + } catch (Throwable t) { + // something went wrong during DB access; report, then use empty new race log + logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load recorded race log data. Check MongoDB settings."); + logger.log(Level.SEVERE, "loadRaceLog", t); + } + return result; + } + + private List loadRaceLogEvents(RaceLog targetRaceLog, BasicDBObject query) { + List result = new ArrayList<>(); + DBCollection raceLog = database.getCollection(CollectionNames.RACE_LOGS.name()); + for (DBObject o : raceLog.find(query)) { + try { + RaceLogEvent raceLogEvent = loadRaceLogEvent((DBObject) o.get(FieldNames.RACE_LOG_EVENT.name())); + if (raceLogEvent != null) { + targetRaceLog.load(raceLogEvent); + result.add(raceLogEvent); + } + } catch (IllegalStateException e) { + logger.log(Level.SEVERE, "Couldn't load race log event "+o+": "+e.getMessage(), e); + } + } + return result; + } + + public RaceLogEvent loadRaceLogEvent(DBObject dbObject) { + TimePoint logicalTimePoint = loadTimePoint(dbObject); + TimePoint createdAt = loadTimePoint(dbObject, FieldNames.RACE_LOG_EVENT_CREATED_AT); + Serializable id = (Serializable) dbObject.get(FieldNames.RACE_LOG_EVENT_ID.name()); + Integer passId = (Integer) dbObject.get(FieldNames.RACE_LOG_EVENT_PASS_ID.name()); + BasicDBList dbCompetitors = (BasicDBList) dbObject.get(FieldNames.RACE_LOG_EVENT_INVOLVED_BOATS.name()); + List competitors = loadCompetitorsForRaceLogEvent(dbCompetitors); + final RaceLogEventAuthor author; + String authorName = (String) dbObject.get(FieldNames.RACE_LOG_EVENT_AUTHOR_NAME.name()); + Number authorPriority = (Number) dbObject.get(FieldNames.RACE_LOG_EVENT_AUTHOR_PRIORITY.name()); + if (authorName != null && authorPriority != null) { + author = new RaceLogEventAuthorImpl(authorName, authorPriority.intValue()); + } else { + author = RaceLogEventAuthorImpl.createCompatibilityAuthor(); + } + + String eventClass = (String) dbObject.get(FieldNames.RACE_LOG_EVENT_CLASS.name()); + if (eventClass.equals(RaceLogStartTimeEvent.class.getSimpleName())) { + return loadRaceLogStartTimeEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(RaceLogRaceStatusEvent.class.getSimpleName())) { + return loadRaceLogRaceStatusEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(RaceLogFlagEvent.class.getSimpleName())) { + return loadRaceLogFlagEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(RaceLogPassChangeEvent.class.getSimpleName())) { + return loadRaceLogPassChangeEvent(createdAt, author, logicalTimePoint, id, passId, competitors); + } else if (eventClass.equals(RaceLogCourseAreaChangedEvent.class.getSimpleName())) { + return loadRaceLogCourseAreaChangedEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(RaceLogCourseDesignChangedEvent.class.getSimpleName())) { + return loadRaceLogCourseDesignChangedEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(RaceLogFinishPositioningListChangedEvent.class.getSimpleName())) { + return loadRaceLogFinishPositioningListChangedEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(RaceLogFinishPositioningConfirmedEvent.class.getSimpleName())) { + return loadRaceLogFinishPositioningConfirmedEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(RaceLogPathfinderEvent.class.getSimpleName())) { + return loadRaceLogPathfinderEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(RaceLogGateLineOpeningTimeEvent.class.getSimpleName())) { + return loadRaceLogGateLineOpeningTimeEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(RaceLogStartProcedureChangedEvent.class.getSimpleName())) { + return loadRaceLogStartProcedureChangedEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(RaceLogProtestStartTimeEvent.class.getSimpleName())) { + return loadRaceLogRaceLogProtestStartTimeEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(RaceLogWindFixEvent.class.getSimpleName())) { + return loadRaceLogRaceLogWindFixEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(DeviceCompetitorMappingEvent.class.getSimpleName())) { + return loadRaceLogDeviceCompetitorMappingEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(DeviceMarkMappingEvent.class.getSimpleName())) { + return loadRaceLogDeviceMarkMappingEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(DenoteForTrackingEvent.class.getSimpleName())) { + return loadRaceLogDenoteForTrackingEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(StartTrackingEvent.class.getSimpleName())) { + return loadRaceLogStartEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(RevokeEvent.class.getSimpleName())) { + return loadRaceLogRevokeEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(RegisterCompetitorEvent.class.getSimpleName())) { + return loadRaceLogRegisterCompetitorEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(DefineMarkEvent.class.getSimpleName())) { + return loadRaceLogDefineMarkEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } else if (eventClass.equals(CloseOpenEndedDeviceMappingEvent.class.getSimpleName())) { + return loadRaceLogCloseOpenEndedDeviceMappingEvent(createdAt, author, logicalTimePoint, id, passId, competitors, dbObject); + } + + throw new IllegalStateException(String.format("Unknown RaceLogEvent type %s", eventClass)); + } + + private RaceLogEvent loadRaceLogRaceLogWindFixEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable id, Integer passId, List competitors, DBObject dbObject) { + Wind wind = loadWind((DBObject) dbObject.get(FieldNames.WIND.name())); + return raceLogEventFactory.createWindFixEvent(createdAt, author, logicalTimePoint, id, competitors, passId, wind); + } + + private RaceLogEvent loadRaceLogDeviceCompetitorMappingEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable id, Integer passId, List competitors, DBObject dbObject) { + DeviceIdentifier device = null; + try { + device = loadDeviceId(deviceIdentifierServiceFinder, + (DBObject) dbObject.get(FieldNames.DEVICE_ID.name())); + } catch (Exception e) { + logger.log(Level.WARNING, "Could not load deviceId for RaceLogEvent", e); + e.printStackTrace(); + } + Competitor mappedTo = baseDomainFactory.getExistingCompetitorById( + (Serializable) dbObject.get(FieldNames.COMPETITOR_ID.name())); + TimePoint from = loadTimePoint(dbObject, FieldNames.RACE_LOG_FROM); + TimePoint to = loadTimePoint(dbObject, FieldNames.RACE_LOG_TO); + return raceLogEventFactory.createDeviceCompetitorMappingEvent(createdAt, author, logicalTimePoint, id, device, mappedTo, passId, from, to); + } + + private RaceLogEvent loadRaceLogDeviceMarkMappingEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable id, Integer passId, List competitors, DBObject dbObject) { + DeviceIdentifier device = null; + try { + device = loadDeviceId(deviceIdentifierServiceFinder, + (DBObject) dbObject.get(FieldNames.DEVICE_ID.name())); + } catch (Exception e) { + logger.log(Level.WARNING, "Could not load deviceId for RaceLogEvent", e); + e.printStackTrace(); + } + Mark mappedTo = baseDomainFactory.getOrCreateMark( + (Serializable) dbObject.get(FieldNames.MARK_ID.name()), null); + TimePoint from = loadTimePoint(dbObject, FieldNames.RACE_LOG_FROM); + TimePoint to = loadTimePoint(dbObject, FieldNames.RACE_LOG_TO); + return raceLogEventFactory.createDeviceMarkMappingEvent(createdAt, author, logicalTimePoint, id, device, mappedTo, passId, from, to); + } + + private RaceLogEvent loadRaceLogDenoteForTrackingEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable id, Integer passId, List competitors, DBObject dbObject) { + String raceName = (String) dbObject.get(FieldNames.RACE_NAME.name()); + BoatClass boatClass = baseDomainFactory.getOrCreateBoatClass((String) dbObject.get(FieldNames.BOAT_CLASS_NAME.name())); + Serializable raceId = (Serializable) dbObject.get(FieldNames.RACE_ID.name()); + return raceLogEventFactory.createDenoteForTrackingEvent(createdAt, author, logicalTimePoint, id, passId, raceName, boatClass, raceId); + } + + private RaceLogEvent loadRaceLogStartEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable id, Integer passId, List competitors, DBObject dbObject) { + return raceLogEventFactory.createStartTrackingEvent(createdAt, author, logicalTimePoint, id, passId); + } + + private RaceLogEvent loadRaceLogRevokeEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable id, Integer passId, List competitors, DBObject dbObject) { + Serializable revokedEventId = Helpers.tryUuidConversion( + (Serializable) dbObject.get(FieldNames.RACE_LOG_REVOKED_EVENT_ID.name())); + return raceLogEventFactory.createRevokeEvent(createdAt, author, logicalTimePoint, id, passId, revokedEventId); + } + + private RaceLogEvent loadRaceLogRegisterCompetitorEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable id, Integer passId, List competitors, DBObject dbObject) { + assert competitors.size() == 1 : "Should have exactly one competitor for RegisterCompetitorEvent"; + return raceLogEventFactory.createRegisterCompetitorEvent(createdAt, author, logicalTimePoint, id, passId, competitors.get(0)); + } + + private RaceLogEvent loadRaceLogDefineMarkEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable id, Integer passId, List competitors, DBObject dbObject) { + Mark mark = loadMark((DBObject) dbObject.get(FieldNames.RACE_LOG_MARK.name())); + return raceLogEventFactory.createDefineMarkEvent(createdAt, author, logicalTimePoint, id, passId, mark); + } + + private RaceLogEvent loadRaceLogCloseOpenEndedDeviceMappingEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable id, Integer passId, List competitors, DBObject dbObject) { + Serializable deviceMappingEventId = Helpers.tryUuidConversion((Serializable) dbObject.get(FieldNames.RACE_LOG_DEVICE_MAPPING_EVENT_ID.name())); + TimePoint closingTimePoint = loadTimePoint(dbObject, FieldNames.RACE_LOG_CLOSING_TIMEPOINT); + return raceLogEventFactory.createCloseOpenEndedDeviceMappingEvent(createdAt, author, logicalTimePoint, id, passId, + deviceMappingEventId, closingTimePoint); + } + + private RaceLogEvent loadRaceLogRaceLogProtestStartTimeEvent(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { + TimePoint protestStartTime = loadTimePoint(dbObject, FieldNames.RACE_LOG_PROTEST_START_TIME); + return raceLogEventFactory.createProtestStartTimeEvent(createdAt, author, logicalTimePoint, id, competitors, passId, protestStartTime); + } + + private RaceLogEvent loadRaceLogStartProcedureChangedEvent(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { + RacingProcedureType type = RacingProcedureType.valueOf(dbObject.get(FieldNames.RACE_LOG_START_PROCEDURE_TYPE.name()).toString()); + return raceLogEventFactory.createStartProcedureChangedEvent(createdAt, author, logicalTimePoint, id, competitors, passId, type); + } + + private RaceLogEvent loadRaceLogGateLineOpeningTimeEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable id, Integer passId, List competitors, DBObject dbObject) { + Number gateLaunchStopTime = (Number) dbObject.get(FieldNames.RACE_LOG_GATE_LINE_OPENING_TIME.name()); + Number golfDownTime = 0; + if (dbObject.containsField(FieldNames.RACE_LOG_GOLF_DOWN_TIME.name())) { + golfDownTime = (Number) dbObject.get(FieldNames.RACE_LOG_GOLF_DOWN_TIME.name()); + } + return raceLogEventFactory.createGateLineOpeningTimeEvent(createdAt, author, logicalTimePoint, id, competitors, + passId, gateLaunchStopTime == null ? null : gateLaunchStopTime.longValue(), golfDownTime.longValue()); + } + + private RaceLogEvent loadRaceLogPathfinderEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable id, Integer passId, List competitors, DBObject dbObject) { + String pathfinderId = dbObject.get(FieldNames.RACE_LOG_PATHFINDER_ID.name()).toString(); + return raceLogEventFactory.createPathfinderEvent(createdAt, author, logicalTimePoint, id, competitors, passId, pathfinderId); + } + + private RaceLogEvent loadRaceLogFinishPositioningConfirmedEvent(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { + BasicDBList dbPositionedCompetitorList = (BasicDBList) dbObject.get(FieldNames.RACE_LOG_POSITIONED_COMPETITORS.name()); + CompetitorResults positionedCompetitors = null; + //When a confirmation event is loaded that does not contain the positioned competitors (this is the case for the ESS events in + //Singapore and Quingdao) then null should be set for the positionedCompetitors, which is evaluated later on. + if (dbPositionedCompetitorList != null) { + positionedCompetitors = loadPositionedCompetitors(dbPositionedCompetitorList); + } + + return raceLogEventFactory.createFinishPositioningConfirmedEvent(createdAt, author, logicalTimePoint, id, competitors, passId, positionedCompetitors); + } + + private RaceLogEvent loadRaceLogFinishPositioningListChangedEvent(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { + BasicDBList dbPositionedCompetitorList = (BasicDBList) dbObject.get(FieldNames.RACE_LOG_POSITIONED_COMPETITORS.name()); + CompetitorResults positionedCompetitors = loadPositionedCompetitors(dbPositionedCompetitorList); + + return raceLogEventFactory.createFinishPositioningListChangedEvent(createdAt, author, logicalTimePoint, id, competitors, passId, positionedCompetitors); + } + + private RaceLogEvent loadRaceLogPassChangeEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable id, Integer passId, List competitors) { + return raceLogEventFactory.createPassChangeEvent(createdAt, author, logicalTimePoint, id, competitors, passId); + } + + private RaceLogCourseDesignChangedEvent loadRaceLogCourseDesignChangedEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { + String courseName = (String) dbObject.get(FieldNames.RACE_LOG_COURSE_DESIGN_NAME.name()); + CourseBase courseData = loadCourseData((BasicDBList) dbObject.get(FieldNames.RACE_LOG_COURSE_DESIGN.name()), courseName); + return raceLogEventFactory.createCourseDesignChangedEvent(createdAt, author, logicalTimePoint, id, competitors, passId, courseData); + } + + private RaceLogCourseAreaChangedEvent loadRaceLogCourseAreaChangedEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { + Serializable courseAreaId = (Serializable) dbObject.get(FieldNames.COURSE_AREA_ID.name()); + return raceLogEventFactory.createCourseAreaChangedEvent(createdAt, author, logicalTimePoint, id, competitors, passId, courseAreaId); + } + + private CompetitorResults loadPositionedCompetitors(BasicDBList dbPositionedCompetitorList) { + CompetitorResults positionedCompetitors = new CompetitorResultsImpl(); + for (Object object : dbPositionedCompetitorList) { + DBObject dbObject = (DBObject) object; + + Serializable competitorId = (Serializable) dbObject.get(FieldNames.COMPETITOR_ID.name()); + + String competitorName = (String) dbObject.get(FieldNames.COMPETITOR_DISPLAY_NAME.name()); + //The Competitor name is a new field in the list. Therefore the name might be null for existing events. In this case a standard name is set. + if (competitorName == null) { + competitorName = "loaded competitor"; + } + + //At this point we do not retrieve the competitor object since at any point in time, especially after a server restart, the DomainFactory and its competitor + //cache might be empty. But at this time the race log is loaded from database, so the competitor would be null. + //By not using the Competitor object retrieved from the DomainFactory we get completely independent from server restarts and the timepoint of loading + //competitors by tracking providers. + + MaxPointsReason maxPointsReason = MaxPointsReason.valueOf((String) dbObject.get(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name())); + + Triple positionedCompetitor = new Triple(competitorId, competitorName, maxPointsReason); + positionedCompetitors.add(positionedCompetitor); + } + return positionedCompetitors; + } + + private List loadCompetitorsForRaceLogEvent(BasicDBList dbCompetitorList) { + List competitors = new ArrayList(); + for (Object object : dbCompetitorList) { + Serializable competitorId = (Serializable) object; + Competitor competitor = baseDomainFactory.getCompetitorStore().getExistingCompetitorById(competitorId); + competitors.add(competitor); + } + return competitors; + } + + private RaceLogFlagEvent loadRaceLogFlagEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { + Flags upperFlag = Flags.valueOf((String) dbObject.get(FieldNames.RACE_LOG_EVENT_FLAG_UPPER.name())); + Flags lowerFlag = Flags.valueOf((String) dbObject.get(FieldNames.RACE_LOG_EVENT_FLAG_LOWER.name())); + Boolean displayed = Boolean.valueOf((String) dbObject.get(FieldNames.RACE_LOG_EVENT_FLAG_DISPLAYED.name())); + + if (upperFlag == null || lowerFlag == null || displayed == null) { + return null; + } + + return raceLogEventFactory.createFlagEvent(createdAt, author, logicalTimePoint, id, competitors, passId, upperFlag, lowerFlag, displayed); + } + + private RaceLogStartTimeEvent loadRaceLogStartTimeEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { + TimePoint startTime = loadTimePoint(dbObject, FieldNames.RACE_LOG_EVENT_START_TIME); + return raceLogEventFactory.createStartTimeEvent(createdAt, author, logicalTimePoint, id, competitors, passId, startTime); + } + + private RaceLogRaceStatusEvent loadRaceLogRaceStatusEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, Serializable id, Integer passId, List competitors, DBObject dbObject) { + RaceLogRaceStatus nextStatus = RaceLogRaceStatus.valueOf((String) dbObject.get(FieldNames.RACE_LOG_EVENT_NEXT_STATUS.name())); + return raceLogEventFactory.createRaceStatusEvent(createdAt, author, logicalTimePoint, id, competitors, passId, nextStatus); + } + + /** + * The old field name WAYPOINT_PASSINGSIDE has been replaced by WAYPOINT_PASSINGINSTRUCTIONS. If a race with the old + * field is loaded, the value of PASSINGSIDE is used and then migrated to PASSINGINSTRUCTION. If the first or last + * Waypoint has the PassingInstructions Gate, it is transfered to Line. + * + */ + @SuppressWarnings("deprecation") // Used to migrate from PASSINGSIDE to the new PASSINGINSTRUCTIONS + private CourseBase loadCourseData(BasicDBList dbCourseList, String courseName) { + if (courseName == null) { + courseName = "Course"; + } + CourseBase courseData = new CourseDataImpl(courseName); + int i = 0; + for (Object object : dbCourseList) { + DBObject dbObject = (DBObject) object; + Waypoint waypoint = null; + PassingInstruction passingInstructions = null; + String waypointPassingInstruction = (String) dbObject.get(FieldNames.WAYPOINT_PASSINGINSTRUCTIONS.name()); + if (waypointPassingInstruction == null) { + waypointPassingInstruction = (String) dbObject.get(FieldNames.WAYPOINT_PASSINGSIDE.name()); + if(waypointPassingInstruction != null) { + logger.info("Migrating PassingInstruction "+waypointPassingInstruction+" to field name WAYPOINT_PASSINGINSTRUCTIONS"); + if((i==0||i==dbCourseList.size()-1)&&waypointPassingInstruction.toLowerCase().equals("gate")){ + logger.warning("Changing PassingInstructions of first or last Waypoint from Gate to Line."); + waypointPassingInstruction = "Line"; + } + dbObject.put(FieldNames.WAYPOINT_PASSINGINSTRUCTIONS.name(), waypointPassingInstruction); + dbObject.removeField(FieldNames.WAYPOINT_PASSINGSIDE.name()); + } + } + if (waypointPassingInstruction != null) { + passingInstructions = PassingInstruction.valueOfIgnoringCase(waypointPassingInstruction); + } + ControlPoint controlPoint = loadControlPoint((DBObject) dbObject.get(FieldNames.CONTROLPOINT.name())); + if (passingInstructions == null) { + waypoint = new WaypointImpl(controlPoint); + } else { + waypoint = new WaypointImpl(controlPoint, passingInstructions); + } + courseData.addWaypoint(i++, waypoint); + } + return courseData; + } + + private ControlPoint loadControlPoint(DBObject dbObject) { + String controlPointClass = (String) dbObject.get(FieldNames.CONTROLPOINT_CLASS.name()); + ControlPoint controlPoint = null; + if (controlPointClass != null) { + if (controlPointClass.equals(Mark.class.getSimpleName())) { + Mark mark = loadMark((DBObject) dbObject.get(FieldNames.CONTROLPOINT_VALUE.name())); + controlPoint = mark; + } else if(controlPointClass.equals("Gate")) { + ControlPointWithTwoMarks cpwtm = loadControlPointWithTwoMarks((DBObject) dbObject.get(FieldNames.CONTROLPOINT_VALUE.name())); + dbObject.put(FieldNames.CONTROLPOINT_CLASS.name(), ControlPointWithTwoMarks.class.getSimpleName()); + controlPoint = cpwtm; + } else if (controlPointClass.equals(ControlPointWithTwoMarks.class.getSimpleName())) { + ControlPointWithTwoMarks cpwtm = loadControlPointWithTwoMarks((DBObject) dbObject.get(FieldNames.CONTROLPOINT_VALUE.name())); + controlPoint = cpwtm; + } + } + return controlPoint; + } + + /** + * Checks for the old GATE fields and migrates them to the new CONTROLPOINTWITHTWOMARKS fields. + */ + @SuppressWarnings("deprecation") // Used for migrating old races + private ControlPointWithTwoMarks loadControlPointWithTwoMarks(DBObject dbObject) { + String controlPointName = (String) dbObject.get(FieldNames.CONTROLPOINTWITHTWOMARKS_NAME.name()); + if (controlPointName == null) { + controlPointName = (String) dbObject.get(FieldNames.GATE_NAME.name()); + logger.info("Migrating name of ControlPointWithTwoMarks " + controlPointName + + " from GATE_NAME to new field CONTROLPOINTWITHTWOMARKS_NAME."); + dbObject.put(FieldNames.CONTROLPOINTWITHTWOMARKS_NAME.name(), controlPointName); + dbObject.removeField(FieldNames.GATE_NAME.name()); + } + Serializable controlPointId = (Serializable) dbObject.get(FieldNames.CONTROLPOINTWITHTWOMARKS_ID.name()); + if (controlPointId == null) { + controlPointId = (Serializable) dbObject.get(FieldNames.GATE_ID.name()); + logger.info("Migrating id of ControlPointWithTwoMarks " + controlPointName + + " from old field GATE_ID to CONTROLPOINTWITHTWOMARKS_ID."); + dbObject.put(FieldNames.CONTROLPOINTWITHTWOMARKS_ID.name(), controlPointId); + dbObject.removeField(FieldNames.GATE_ID.name()); + } + DBObject dbLeft = (DBObject) dbObject.get(FieldNames.CONTROLPOINTWITHTWOMARKS_LEFT.name()); + if (dbLeft == null) { + dbLeft = (DBObject) dbObject.get(FieldNames.GATE_LEFT.name()); + logger.info("Migrating left Mark of ControlPointWithTwoMarks " + controlPointName + " from old field GATE_LEFT to CONTROLPOINTWITHTWOMARKS_LEFT"); + dbObject.put(FieldNames.CONTROLPOINTWITHTWOMARKS_LEFT.name(), dbLeft); + dbObject.removeField(FieldNames.GATE_LEFT.name()); + } + Mark leftMark = loadMark(dbLeft); + DBObject dbRight = (DBObject) dbObject.get(FieldNames.CONTROLPOINTWITHTWOMARKS_RIGHT.name()); + if (dbRight == null) { + dbRight = (DBObject) dbObject.get(FieldNames.GATE_RIGHT.name()); + logger.info("Migrating right Mark of ControlPointWithTwoMarks " + controlPointName + " from old field GATE_RIGHT to CONTROLPOINTWITHTWOMARKS_RIGHT"); + dbObject.put(FieldNames.CONTROLPOINTWITHTWOMARKS_RIGHT.name(), dbRight); + dbObject.removeField(FieldNames.GATE_RIGHT.name()); + } + Mark rightMark = loadMark(dbRight); + ControlPointWithTwoMarks gate = baseDomainFactory.createControlPointWithTwoMarks(controlPointId, leftMark, rightMark, controlPointName); + return gate; + } + + private Mark loadMark(DBObject dbObject) { + Serializable markId = (Serializable) dbObject.get(FieldNames.MARK_ID.name()); + String markColor = (String) dbObject.get(FieldNames.MARK_COLOR.name()); + String markName = (String) dbObject.get(FieldNames.MARK_NAME.name()); + String markPattern = (String) dbObject.get(FieldNames.MARK_PATTERN.name()); + String markShape = (String) dbObject.get(FieldNames.MARK_SHAPE.name()); + MarkType markType = MarkType.valueOf((String) dbObject.get(FieldNames.MARK_TYPE.name())); + + Mark mark = baseDomainFactory.getOrCreateMark(markId, markName, markType, markColor, markShape, markPattern); + return mark; + } + + @Override + public Collection loadAllCompetitors() { + ArrayList result = new ArrayList(); + DBCollection collection = database.getCollection(CollectionNames.COMPETITORS.name()); + try { + for (DBObject o : collection.find()) { + JSONObject json = Helpers.toJSONObjectSafe(new JSONParser().parse(JSON.serialize(o))); + Competitor c = competitorDeserializer.deserialize(json); + result.add(c); + } + } catch (Exception e) { + logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load competitors."); + logger.log(Level.SEVERE, "loadCompetitors", e); + } + return result; + } + + @Override + public Iterable> loadAllDeviceConfigurations() { + Map result = new HashMap<>(); + DBCollection configurationCollection = database.getCollection(CollectionNames.CONFIGURATIONS.name()); + + try { + for (DBObject dbObject : configurationCollection.find()) { + Pair entry = loadConfigurationEntry(dbObject); + result.put(entry.getA(), entry.getB()); + } + } catch (Exception e) { + logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load configurations."); + logger.log(Level.SEVERE, "loadAllDeviceConfigurations", e); + } + + return result.entrySet(); + } + + private Pair loadConfigurationEntry(DBObject dbObject) { + DBObject matcherObject = (DBObject) dbObject.get(FieldNames.CONFIGURATION_MATCHER.name()); + DBObject configObject = (DBObject) dbObject.get(FieldNames.CONFIGURATION_CONFIG.name()); + return new Pair(loadConfigurationMatcher(matcherObject), + loadConfiguration(configObject)); + } + + private DeviceConfigurationMatcher loadConfigurationMatcher(DBObject matcherObject) { + DeviceConfigurationMatcherType type = DeviceConfigurationMatcherType.valueOf( + matcherObject.get(FieldNames.CONFIGURATION_MATCHER_TYPE.name()).toString()); + List clientIdentifiers = new ArrayList(); + BasicDBList clientIdentifiersObject = (BasicDBList) matcherObject.get(FieldNames.CONFIGURATION_MATCHER_CLIENTS.name()); + if (clientIdentifiersObject != null) { + for (Object clientIdentifier : clientIdentifiersObject) { + clientIdentifiers.add(clientIdentifier.toString()); + } + } + return baseDomainFactory.getOrCreateDeviceConfigurationMatcher(type, clientIdentifiers); + } + + private DeviceConfiguration loadConfiguration(DBObject configObject) { + DeviceConfiguration configuration = null; + try { + JsonDeserializer deserializer = DeviceConfigurationJsonDeserializer.create(); + JSONObject json = Helpers.toJSONObjectSafe(new JSONParser().parse(JSON.serialize(configObject))); + configuration = deserializer.deserialize(json); + } catch (JsonDeserializationException | ParseException e) { + logger.log(Level.SEVERE, "Error parsing configuration object from MongoDB, falling back to empty configuration."); + logger.log(Level.SEVERE, "loadConfiguration", e); + configuration = new DeviceConfigurationImpl(new RegattaConfigurationImpl()); + } + return configuration; + } + + private DeviceIdentifier loadDeviceId( + TypeBasedServiceFinder deviceIdentifierServiceFinder, DBObject deviceId) + throws TransformationException, NoCorrespondingServiceRegisteredException { + String deviceType = (String) deviceId.get(FieldNames.DEVICE_TYPE.name()); + Object deviceTypeId = deviceId.get(FieldNames.DEVICE_TYPE_SPECIFIC_ID.name()); + String stringRepresentation = (String) deviceId.get(FieldNames.DEVICE_STRING_REPRESENTATION.name()); + + try { + return deviceIdentifierServiceFinder.findService(deviceType).deserialize(deviceTypeId, deviceType, stringRepresentation); + } catch (TransformationException e) { + return new PlaceHolderDeviceIdentifierSerializationHandler().deserialize( + stringRepresentation, deviceType, stringRepresentation); + } + } +} diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java index 457a7c3bf07..6cb53df0346 100755 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/FieldNames.java @@ -77,7 +77,8 @@ public enum FieldNames { WAYPOINT_PASSINGINSTRUCTIONS, CONTROLPOINT, CONTROLPOINT_CLASS, MARK_ID, MARK_COLOR, MARK_NAME, MARK_PATTERN, MARK_SHAPE, MARK_TYPE, CONTROLPOINT_VALUE, CONTROLPOINTWITHTWOMARKS_ID, CONTROLPOINTWITHTWOMARKS_NAME, CONTROLPOINTWITHTWOMARKS_LEFT, CONTROLPOINTWITHTWOMARKS_RIGHT, RACE_LOG_POSITIONED_COMPETITORS, RACE_LOG_PATHFINDER_ID, RACE_LOG_GATE_LINE_OPENING_TIME, RACE_LOG_GOLF_DOWN_TIME, RACE_LOG_START_PROCEDURE_TYPE, - RACE_LOG_PROTEST_START_TIME, RACE_LOG_COURSE_DESIGN_NAME, + RACE_LOG_PROTEST_START_TIME, RACE_LOG_COURSE_DESIGN_NAME, RACE_LOG_REVOKED_EVENT_ID, RACE_LOG_FROM, RACE_LOG_TO, RACE_LOG_MARK, RACE_LOG_DEVICE_MAPPING_EVENT_ID, + RACE_LOG_CLOSING_TIMEPOINT, /** @deprecated Replaced by WAYPOINT_PASSINGINSTRUCTIONS, still used to migrate old races*/ @Deprecated WAYPOINT_PASSINGSIDE, /** @deprecated Replaced by CONTROLPOINTWITHTWOMARKS_ID, still used to migrate old races*/ @Deprecated GATE_ID, /** @deprecated Replaced by CONTROLPOINTWITHTWOMARKS_NAME, still used to migrate old races*/ @Deprecated GATE_NAME, @@ -88,5 +89,8 @@ public enum FieldNames { // configurations CONFIGURATION_MATCHER, CONFIGURATION_CONFIG, CONFIGURATION_MATCHER_ID, CONFIGURATION_MATCHER_TYPE, CONFIGURATION_MATCHER_CLIENTS, CONFIGURATION_CONFIG_ALLOWED_COURSE_AREA_NAMES, CONFIGURATION_CONFIG_MIN_ROUNDS, CONFIGURATION_CONFIG_MAX_ROUNDS, CONFIGURATION_CONFIG_RESULTS_MAIL, - CONFIGURATION_CONFIG_PROCEDURES; + CONFIGURATION_CONFIG_PROCEDURES, + + // GPSFixes + DEVICE_TYPE_SPECIFIC_ID, DEVICE_ID, DEVICE_TYPE, GPSFIX_TYPE, GPSFIX, DEVICE_STRING_REPRESENTATION, } diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java index c43f0a97082..3c17e1e54e8 100755 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java @@ -1,961 +1,1161 @@ -package com.sap.sailing.domain.persistence.impl; - -import java.io.Serializable; -import java.util.List; -import java.util.Map.Entry; -import java.util.logging.Level; -import java.util.logging.Logger; - -import org.bson.types.ObjectId; -import org.json.simple.JSONObject; - -import com.mongodb.BasicDBList; -import com.mongodb.BasicDBObject; -import com.mongodb.DB; -import com.mongodb.DBCollection; -import com.mongodb.DBObject; -import com.mongodb.WriteConcern; -import com.mongodb.util.JSON; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.ControlPoint; -import com.sap.sailing.domain.base.ControlPointWithTwoMarks; -import com.sap.sailing.domain.base.CourseArea; -import com.sap.sailing.domain.base.CourseBase; -import com.sap.sailing.domain.base.Event; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.Mark; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceDefinition; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.Series; -import com.sap.sailing.domain.base.Timed; -import com.sap.sailing.domain.base.Venue; -import com.sap.sailing.domain.base.Waypoint; -import com.sap.sailing.domain.base.configuration.DeviceConfiguration; -import com.sap.sailing.domain.base.configuration.DeviceConfigurationMatcher; -import com.sap.sailing.domain.base.configuration.RegattaConfiguration; -import com.sap.sailing.domain.base.configuration.impl.DeviceConfigurationMatcherMulti; -import com.sap.sailing.domain.base.configuration.impl.DeviceConfigurationMatcherSingle; -import com.sap.sailing.domain.base.impl.FleetImpl; -import com.sap.sailing.domain.common.Bearing; -import com.sap.sailing.domain.common.MaxPointsReason; -import com.sap.sailing.domain.common.PassingInstruction; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.Speed; -import com.sap.sailing.domain.common.SpeedWithBearing; -import com.sap.sailing.domain.common.TimePoint; -import com.sap.sailing.domain.common.WindSource; -import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; -import com.sap.sailing.domain.common.impl.Util; -import com.sap.sailing.domain.common.impl.Util.Triple; -import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.leaderboard.LeaderboardGroup; -import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; -import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; -import com.sap.sailing.domain.leaderboard.SettableScoreCorrection; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sailing.domain.persistence.MongoObjectFactory; -import com.sap.sailing.domain.racelog.RaceLogCourseAreaChangedEvent; -import com.sap.sailing.domain.racelog.RaceLogCourseDesignChangedEvent; -import com.sap.sailing.domain.racelog.RaceLogEvent; -import com.sap.sailing.domain.racelog.RaceLogEventAuthor; -import com.sap.sailing.domain.racelog.RaceLogFinishPositioningConfirmedEvent; -import com.sap.sailing.domain.racelog.RaceLogFinishPositioningListChangedEvent; -import com.sap.sailing.domain.racelog.RaceLogFlagEvent; -import com.sap.sailing.domain.racelog.RaceLogGateLineOpeningTimeEvent; -import com.sap.sailing.domain.racelog.RaceLogIdentifier; -import com.sap.sailing.domain.racelog.RaceLogPassChangeEvent; -import com.sap.sailing.domain.racelog.RaceLogPathfinderEvent; -import com.sap.sailing.domain.racelog.RaceLogProtestStartTimeEvent; -import com.sap.sailing.domain.racelog.RaceLogRaceStatusEvent; -import com.sap.sailing.domain.racelog.RaceLogStartProcedureChangedEvent; -import com.sap.sailing.domain.racelog.RaceLogStartTimeEvent; -import com.sap.sailing.domain.racelog.RaceLogWindFixEvent; -import com.sap.sailing.domain.tracking.Positioned; -import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sailing.domain.tracking.TrackedRegatta; -import com.sap.sailing.domain.tracking.Wind; -import com.sap.sailing.domain.tracking.WindTrack; -import com.sap.sailing.server.gateway.serialization.JsonSerializer; -import com.sap.sailing.server.gateway.serialization.impl.CompetitorJsonSerializer; -import com.sap.sailing.server.gateway.serialization.impl.DeviceConfigurationJsonSerializer; -import com.sap.sailing.server.gateway.serialization.impl.RegattaConfigurationJsonSerializer; - -public class MongoObjectFactoryImpl implements MongoObjectFactory { - private static Logger logger = Logger.getLogger(MongoObjectFactoryImpl.class.getName()); - private final DB database; - private final CompetitorJsonSerializer competitorSerializer = CompetitorJsonSerializer.create(); - - public MongoObjectFactoryImpl(DB database) { - super(); - this.database = database; - } - - @Override - public DB getDatabase() { - return database; - } - - public DBObject storeWind(Wind wind) { - DBObject result = new BasicDBObject(); - storePositioned(wind, result); - storeTimed(wind, result); - storeSpeedWithBearing(wind, result); - return result; - } - - private void storeTimePoint(TimePoint timePoint, DBObject result, FieldNames field) { - if (timePoint != null) { - result.put(field.name(), timePoint.asMillis()); - } - } - - private void storeTimed(Timed timed, DBObject result) { - if (timed.getTimePoint() != null) { - storeTimePoint(timed.getTimePoint(), result, FieldNames.TIME_AS_MILLIS); - } - } - - private void storeSpeedWithBearing(SpeedWithBearing speedWithBearing, DBObject result) { - storeSpeed(speedWithBearing, result); - storeBearing(speedWithBearing.getBearing(), result); - - } - - private void storeBearing(Bearing bearing, DBObject result) { - result.put(FieldNames.DEGREE_BEARING.name(), bearing.getDegrees()); - } - - private void storeSpeed(Speed speed, DBObject result) { - result.put(FieldNames.KNOT_SPEED.name(), speed.getKnots()); - } - - private void storePositioned(Positioned positioned, DBObject result) { - if (positioned.getPosition() != null) { - result.put(FieldNames.LAT_DEG.name(), positioned.getPosition().getLatDeg()); - result.put(FieldNames.LNG_DEG.name(), positioned.getPosition().getLngDeg()); - } - } - - @Override - public void addWindTrackDumper(TrackedRegatta trackedRegatta, TrackedRace trackedRace, WindSource windSource) { - WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource); - windTrack.addListener(new MongoWindListener(trackedRace, trackedRegatta.getRegatta().getName(), windSource, this, database)); - } - - public DBCollection getWindTrackCollection() { - DBCollection result = database.getCollection(CollectionNames.WIND_TRACKS.name()); - result.ensureIndex(new BasicDBObject(FieldNames.REGATTA_NAME.name(), null)); - return result; - } - - /** - * @param regattaName - * the regatta name is stored only for human readability purposes because a time stamp may be a bit unhandy for - * identifying where the wind fix was collected - */ - public DBObject storeWindTrackEntry(RaceDefinition race, String regattaName, WindSource windSource, Wind wind) { - BasicDBObject result = new BasicDBObject(); - result.put(FieldNames.RACE_ID.name(), race.getId()); - result.put(FieldNames.REGATTA_NAME.name(), regattaName); - result.put(FieldNames.WIND_SOURCE_NAME.name(), windSource.name()); - if (windSource.getId() != null) { - result.put(FieldNames.WIND_SOURCE_ID.name(), windSource.getId()); - } - result.put(FieldNames.WIND.name(), storeWind(wind)); - return result; - } - - private void storeRaceIdentifiers(RaceColumn raceColumn, DBObject dbObject) { - BasicDBObject raceIdentifiersPerFleet = new BasicDBObject(); - for (Fleet fleet : raceColumn.getFleets()) { - RaceIdentifier raceIdentifier = raceColumn.getRaceIdentifier(fleet); - if (raceIdentifier != null) { - DBObject raceIdentifierForFleet = new BasicDBObject(); - storeRaceIdentifier(raceIdentifierForFleet, raceIdentifier); - raceIdentifiersPerFleet.put(MongoUtils.escapeDollarAndDot(fleet.getName()), raceIdentifierForFleet); - } - } - dbObject.put(FieldNames.RACE_IDENTIFIERS.name(), raceIdentifiersPerFleet); - } - - private void storeRaceIdentifier(DBObject dbObject, RaceIdentifier raceIdentifier) { - if (raceIdentifier != null) { - dbObject.put(FieldNames.EVENT_NAME.name(), raceIdentifier.getRegattaName()); - dbObject.put(FieldNames.RACE_NAME.name(), raceIdentifier.getRaceName()); - } - } - - @Override - public void storeLeaderboard(Leaderboard leaderboard) { - DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); - try { - leaderboardCollection.ensureIndex(FieldNames.LEADERBOARD_NAME.name()); - } catch (NullPointerException npe) { - // sometimes, for reasons yet to be clarified, ensuring an index on the name field causes an NPE - logger.log(Level.SEVERE, "storeLeaderboard", npe); - } - BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), leaderboard.getName()); - BasicDBObject dbLeaderboard = new BasicDBObject(); - dbLeaderboard.put(FieldNames.LEADERBOARD_NAME.name(), leaderboard.getName()); - if (leaderboard.getDisplayName() != null) { - dbLeaderboard.put(FieldNames.LEADERBOARD_DISPLAY_NAME.name(), leaderboard.getDisplayName()); - } - BasicDBList dbSuppressedCompetitorIds = new BasicDBList(); - for (Competitor suppressedCompetitor : leaderboard.getSuppressedCompetitors()) { - dbSuppressedCompetitorIds.add(suppressedCompetitor.getId()); - } - dbLeaderboard.put(FieldNames.LEADERBOARD_SUPPRESSED_COMPETITOR_IDS.name(), dbSuppressedCompetitorIds); - if (leaderboard instanceof FlexibleLeaderboard) { - storeFlexibleLeaderboard((FlexibleLeaderboard) leaderboard, dbLeaderboard); - } else if (leaderboard instanceof RegattaLeaderboard) { - storeRegattaLeaderboard((RegattaLeaderboard) leaderboard, dbLeaderboard); - } else { - // at least store the scoring scheme - dbLeaderboard.put(FieldNames.SCORING_SCHEME_TYPE.name(), leaderboard.getScoringScheme().getType().name()); - } - if (leaderboard.getDefaultCourseArea() != null) { - dbLeaderboard.put(FieldNames.COURSE_AREA_ID.name(), leaderboard.getDefaultCourseArea().getId().toString()); - } else { - dbLeaderboard.put(FieldNames.COURSE_AREA_ID.name(), null); - } - storeColumnFactors(leaderboard, dbLeaderboard); - storeLeaderboardCorrectionsAndDiscards(leaderboard, dbLeaderboard); - leaderboardCollection.update(query, dbLeaderboard, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); - } - - private void storeColumnFactors(Leaderboard leaderboard, BasicDBObject dbLeaderboard) { - DBObject raceColumnFactors = new BasicDBObject(); - for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - Double explicitFactor = raceColumn.getExplicitFactor(); - if (explicitFactor != null) { - raceColumnFactors.put(MongoUtils.escapeDollarAndDot(raceColumn.getName()), explicitFactor); - } - } - dbLeaderboard.put(FieldNames.LEADERBOARD_COLUMN_FACTORS.name(), raceColumnFactors); - } - - private void storeRegattaLeaderboard(RegattaLeaderboard leaderboard, DBObject dbLeaderboard) { - dbLeaderboard.put(FieldNames.REGATTA_NAME.name(), leaderboard.getRegatta().getName()); - } - - private void storeFlexibleLeaderboard(FlexibleLeaderboard leaderboard, BasicDBObject dbLeaderboard) { - BasicDBList dbRaceColumns = new BasicDBList(); - dbLeaderboard.put(FieldNames.SCORING_SCHEME_TYPE.name(), leaderboard.getScoringScheme().getType().name()); - dbLeaderboard.put(FieldNames.LEADERBOARD_COLUMNS.name(), dbRaceColumns); - for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - BasicDBObject dbRaceColumn = storeRaceColumn(raceColumn); - dbRaceColumns.add(dbRaceColumn); - } - } - - private void storeLeaderboardCorrectionsAndDiscards(Leaderboard leaderboard, BasicDBObject dbLeaderboard) { - if (leaderboard.hasCarriedPoints()) { - BasicDBList dbCarriedPoints = new BasicDBList(); - dbLeaderboard.put(FieldNames.LEADERBOARD_CARRIED_POINTS_BY_ID.name(), dbCarriedPoints); - for (Entry competitorWithCarriedPoints : leaderboard - .getCompetitorsForWhichThereAreCarriedPoints().entrySet()) { - double carriedPoints = competitorWithCarriedPoints.getValue(); - Competitor competitor = competitorWithCarriedPoints.getKey(); - DBObject dbCarriedPointsForCompetitor = new BasicDBObject(); - dbCarriedPointsForCompetitor.put(FieldNames.COMPETITOR_ID.name(), competitor.getId()); - dbCarriedPointsForCompetitor.put(FieldNames.LEADERBOARD_CARRIED_POINTS.name(), carriedPoints); - dbCarriedPoints.add(dbCarriedPointsForCompetitor); - } - } - BasicDBObject dbScoreCorrections = new BasicDBObject(); - storeScoreCorrections(leaderboard, dbScoreCorrections); - dbLeaderboard.put(FieldNames.LEADERBOARD_SCORE_CORRECTIONS.name(), dbScoreCorrections); - final ResultDiscardingRule resultDiscardingRule = leaderboard.getResultDiscardingRule(); - storeResultDiscardingRule(dbLeaderboard, resultDiscardingRule, FieldNames.LEADERBOARD_DISCARDING_THRESHOLDS); - BasicDBList competitorDisplayNames = new BasicDBList(); - for (Competitor competitor : leaderboard.getCompetitors()) { - String displayNameForCompetitor = leaderboard.getDisplayName(competitor); - if (displayNameForCompetitor != null) { - DBObject dbDisplayName = new BasicDBObject(); - dbDisplayName.put(FieldNames.COMPETITOR_ID.name(), competitor.getId()); - dbDisplayName.put(FieldNames.COMPETITOR_DISPLAY_NAME.name(), displayNameForCompetitor); - competitorDisplayNames.add(dbDisplayName); - } - } - dbLeaderboard.put(FieldNames.LEADERBOARD_COMPETITOR_DISPLAY_NAMES.name(), competitorDisplayNames); - } - - /** - * Stores the result discarding rule to dbObject's field identified by field if the result discarding - * rule is not null and is of type {@link ThresholdBasedResultDiscardingRule}. Otherwise, it is assumed that the - * result discarding rule is otherwise implicitly obtained, e.g., from a definition of a regatta with its series, stored elsewhere. - */ - private void storeResultDiscardingRule(DBObject dbObject, - final ResultDiscardingRule resultDiscardingRule, FieldNames field) { - if (resultDiscardingRule != null && resultDiscardingRule instanceof ThresholdBasedResultDiscardingRule) { - BasicDBList dbResultDiscardingThresholds = new BasicDBList(); - for (int threshold : ((ThresholdBasedResultDiscardingRule) resultDiscardingRule).getDiscardIndexResultsStartingWithHowManyRaces()) { - dbResultDiscardingThresholds.add(threshold); - } - dbObject.put(field.name(), dbResultDiscardingThresholds); - } - } - - private BasicDBObject storeRaceColumn(RaceColumn raceColumn) { - BasicDBObject dbRaceColumn = new BasicDBObject(); - dbRaceColumn.put(FieldNames.LEADERBOARD_COLUMN_NAME.name(), raceColumn.getName()); - dbRaceColumn.put(FieldNames.LEADERBOARD_IS_MEDAL_RACE_COLUMN.name(), raceColumn.isMedalRace()); - storeRaceIdentifiers(raceColumn, dbRaceColumn); - return dbRaceColumn; - } - - private void storeScoreCorrections(Leaderboard leaderboard, BasicDBObject dbScoreCorrections) { - TimePoint now = MillisecondsTimePoint.now(); - SettableScoreCorrection scoreCorrection = leaderboard.getScoreCorrection(); - for (RaceColumn raceColumn : scoreCorrection.getRaceColumnsThatHaveCorrections()) { - BasicDBList dbCorrectionForRace = new BasicDBList(); - for (Competitor competitor : scoreCorrection.getCompetitorsThatHaveCorrectionsIn(raceColumn)) { - // TODO bug 655: make score corrections time dependent - if (scoreCorrection.isScoreCorrected(competitor, raceColumn, now)) { - BasicDBObject dbCorrectionForCompetitor = new BasicDBObject(); - dbCorrectionForCompetitor.put(FieldNames.COMPETITOR_ID.name(), competitor.getId()); - MaxPointsReason maxPointsReason = scoreCorrection.getMaxPointsReason(competitor, raceColumn, now); - if (maxPointsReason != MaxPointsReason.NONE) { - dbCorrectionForCompetitor.put(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name(), - maxPointsReason.name()); - } - Double explicitScoreCorrection = scoreCorrection - .getExplicitScoreCorrection(competitor, raceColumn); - if (explicitScoreCorrection != null) { - dbCorrectionForCompetitor.put(FieldNames.LEADERBOARD_CORRECTED_SCORE.name(), - explicitScoreCorrection); - } - dbCorrectionForRace.add(dbCorrectionForCompetitor); - } - } - if (!dbCorrectionForRace.isEmpty()) { - // using the column name as the key for the score corrections requires re-writing the score corrections - // of a meta-leaderboard if the name of one of its leaderboards changes - dbScoreCorrections.put(MongoUtils.escapeDollarAndDot(raceColumn.getName()), dbCorrectionForRace); - } - } - final TimePoint timePointOfLastCorrectionsValidity = scoreCorrection.getTimePointOfLastCorrectionsValidity(); - if (timePointOfLastCorrectionsValidity != null) { - dbScoreCorrections.put(FieldNames.LEADERBOARD_SCORE_CORRECTION_TIMESTAMP.name(), timePointOfLastCorrectionsValidity.asMillis()); - } - if (scoreCorrection.getComment() != null) { - dbScoreCorrections.put(FieldNames.LEADERBOARD_SCORE_CORRECTION_COMMENT.name(), scoreCorrection.getComment()); - } - } - - @Override - public void removeLeaderboard(String leaderboardName) { - DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); - BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), leaderboardName); - leaderboardCollection.remove(query); - } - - @Override - public void renameLeaderboard(String oldName, String newName) { - DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); - BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), oldName); - BasicDBObject renameUpdate = new BasicDBObject("$set", new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), newName)); - leaderboardCollection.update(query, renameUpdate, /* upsert */ true, /* multi */ false, WriteConcern.SAFE); - } - - @Override - public void storeLeaderboardGroup(LeaderboardGroup leaderboardGroup) { - DBCollection leaderboardGroupCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()); - DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); - - try { - leaderboardGroupCollection.ensureIndex(FieldNames.LEADERBOARD_GROUP_NAME.name()); - } catch (NullPointerException npe) { - // sometimes, for reasons yet to be clarified, ensuring an index on the name field causes an NPE - logger.log(Level.SEVERE, "storeLeaderboardGroup", npe); - } - BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), leaderboardGroup.getName()); - BasicDBObject dbLeaderboardGroup = new BasicDBObject(); - dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_NAME.name(), leaderboardGroup.getName()); - dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_DESCRIPTION.name(), leaderboardGroup.getDescription()); - dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_DISPLAY_IN_REVERSE_ORDER.name(), leaderboardGroup.isDisplayGroupsInReverseOrder()); - final Leaderboard overallLeaderboard = leaderboardGroup.getOverallLeaderboard(); - if (overallLeaderboard != null) { - BasicDBObject overallLeaderboardQuery = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), overallLeaderboard.getName()); - DBObject dbOverallLeaderboard = leaderboardCollection.findOne(overallLeaderboardQuery); - if (dbOverallLeaderboard == null) { - storeLeaderboard(overallLeaderboard); - dbOverallLeaderboard = leaderboardCollection.findOne(overallLeaderboardQuery); - } - ObjectId dbOverallLeaderboardId = (ObjectId) dbOverallLeaderboard.get("_id"); - dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_OVERALL_LEADERBOARD.name(), dbOverallLeaderboardId); - } - BasicDBList dbLeaderboardIds = new BasicDBList(); - for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { - BasicDBObject leaderboardQuery = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), leaderboard.getName()); - DBObject dbLeaderboard = leaderboardCollection.findOne(leaderboardQuery); - if (dbLeaderboard == null) { - storeLeaderboard(leaderboard); - dbLeaderboard = leaderboardCollection.findOne(leaderboardQuery); - } - ObjectId dbLeaderboardId = (ObjectId) dbLeaderboard.get("_id"); - dbLeaderboardIds.add(dbLeaderboardId); - } - dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_LEADERBOARDS.name(), dbLeaderboardIds); - leaderboardGroupCollection.update(query, dbLeaderboardGroup, true, false, WriteConcern.SAFE); - } - - @Override - public void removeLeaderboardGroup(String groupName) { - DBCollection leaderboardGroupCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()); - BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), groupName); - leaderboardGroupCollection.remove(query); - } - - @Override - public void renameLeaderboardGroup(String oldName, String newName) { - DBCollection leaderboardGroupCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()); - BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), oldName); - BasicDBObject update = new BasicDBObject("$set", new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), newName)); - leaderboardGroupCollection.update(query, update, /* upsert */ true, /* multi */ false, WriteConcern.SAFE); - } - - @Override - public void storeEvent(Event event) { - DBCollection eventCollection = database.getCollection(CollectionNames.EVENTS.name()); - eventCollection.ensureIndex(FieldNames.EVENT_ID.name()); - DBObject query = new BasicDBObject(); - query.put(FieldNames.EVENT_ID.name(), event.getId()); - DBObject eventDBObject = new BasicDBObject(); - eventDBObject.put(FieldNames.EVENT_NAME.name(), event.getName()); - eventDBObject.put(FieldNames.EVENT_ID.name(), event.getId()); - storeTimePoint(event.getStartDate(), eventDBObject, FieldNames.EVENT_START_DATE); - storeTimePoint(event.getEndDate(), eventDBObject, FieldNames.EVENT_END_DATE); - eventDBObject.put(FieldNames.EVENT_IS_PUBLIC.name(), event.isPublic()); - DBObject venueDBObject = getVenueAsDBObject(event.getVenue()); - eventDBObject.put(FieldNames.VENUE.name(), venueDBObject); - eventCollection.update(query, eventDBObject, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); - } - - @Override - public void renameEvent(Serializable id, String newName) { - DBCollection eventCollection = database.getCollection(CollectionNames.EVENTS.name()); - BasicDBObject query = new BasicDBObject(FieldNames.EVENT_ID.name(), id); - BasicDBObject renameUpdate = new BasicDBObject("$set", new BasicDBObject(FieldNames.EVENT_NAME.name(), newName)); - eventCollection.update(query, renameUpdate, /* upsert */ true, /* multi */ false, WriteConcern.SAFE); - } - - @Override - public void removeEvent(Serializable id) { - DBCollection eventsCollection = database.getCollection(CollectionNames.EVENTS.name()); - BasicDBObject query = new BasicDBObject(FieldNames.EVENT_ID.name(), id); - eventsCollection.remove(query); - } - - private DBObject getVenueAsDBObject(Venue venue) { - DBObject result = new BasicDBObject(); - result.put(FieldNames.VENUE_NAME.name(), venue.getName()); - BasicDBList courseAreaList = new BasicDBList(); - result.put(FieldNames.COURSE_AREAS.name(), courseAreaList); - for (CourseArea courseArea : venue.getCourseAreas()) { - DBObject dbCourseArea = new BasicDBObject(); - courseAreaList.add(dbCourseArea); - dbCourseArea.put(FieldNames.COURSE_AREA_NAME.name(), courseArea.getName()); - dbCourseArea.put(FieldNames.COURSE_AREA_ID.name(), courseArea.getId()); - } - return result; - } - - @Override - public void storeRegatta(Regatta regatta) { - DBCollection regattasCollection = database.getCollection(CollectionNames.REGATTAS.name()); - regattasCollection.ensureIndex(FieldNames.REGATTA_NAME.name()); - regattasCollection.ensureIndex(FieldNames.REGATTA_ID.name()); - DBObject dbRegatta = new BasicDBObject(); - DBObject query = new BasicDBObject(FieldNames.REGATTA_NAME.name(), regatta.getName()); - dbRegatta.put(FieldNames.REGATTA_NAME.name(), regatta.getName()); - dbRegatta.put(FieldNames.REGATTA_BASE_NAME.name(), regatta.getBaseName()); - dbRegatta.put(FieldNames.REGATTA_ID.name(), regatta.getId()); - dbRegatta.put(FieldNames.SCORING_SCHEME_TYPE.name(), regatta.getScoringScheme().getType().name()); - if (regatta.getBoatClass() != null) { - dbRegatta.put(FieldNames.BOAT_CLASS_NAME.name(), regatta.getBoatClass().getName()); - dbRegatta.put(FieldNames.BOAT_CLASS_TYPICALLY_STARTS_UPWIND.name(), regatta.getBoatClass().typicallyStartsUpwind()); - } - dbRegatta.put(FieldNames.REGATTA_SERIES.name(), storeSeries(regatta.getSeries())); - - if (regatta.getDefaultCourseArea() != null) { - dbRegatta.put(FieldNames.COURSE_AREA_ID.name(), regatta.getDefaultCourseArea().getId().toString()); - } else { - dbRegatta.put(FieldNames.COURSE_AREA_ID.name(), null); - } - if (regatta.getRegattaConfiguration() != null) { - JsonSerializer serializer = RegattaConfigurationJsonSerializer.create(); - JSONObject json = serializer.serialize(regatta.getRegattaConfiguration()); - DBObject configurationObject = (DBObject) JSON.parse(json.toString()); - dbRegatta.put(FieldNames.REGATTA_REGATTA_CONFIGURATION.name(), configurationObject); - } - - regattasCollection.update(query, dbRegatta, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); - } - - @Override - public void removeRegatta(Regatta regatta) { - DBCollection regattasCollection = database.getCollection(CollectionNames.REGATTAS.name()); - DBObject query = new BasicDBObject(FieldNames.REGATTA_NAME.name(), regatta.getName()); - regattasCollection.remove(query); - } - - private BasicDBList storeSeries(Iterable series) { - BasicDBList dbSeries = new BasicDBList(); - for (Series s : series) { - dbSeries.add(storeSeries(s)); - } - return dbSeries; - } - - private DBObject storeSeries(Series s) { - DBObject dbSeries = new BasicDBObject(); - dbSeries.put(FieldNames.SERIES_NAME.name(), s.getName()); - dbSeries.put(FieldNames.SERIES_IS_MEDAL.name(), s.isMedal()); - dbSeries.put(FieldNames.SERIES_HAS_SPLIT_FLEET_CONTIGUOUS_SCORING.name(), s.hasSplitFleetContiguousScoring()); - dbSeries.put(FieldNames.SERIES_STARTS_WITH_ZERO_SCORE.name(), s.isStartsWithZeroScore()); - dbSeries.put(FieldNames.SERIES_STARTS_WITH_NON_DISCARDABLE_CARRY_FORWARD.name(), s.isFirstColumnIsNonDiscardableCarryForward()); - BasicDBList dbFleets = new BasicDBList(); - for (Fleet fleet : s.getFleets()) { - dbFleets.add(storeFleet(fleet)); - } - dbSeries.put(FieldNames.SERIES_FLEETS.name(), dbFleets); - BasicDBList dbRaceColumns = new BasicDBList(); - for (RaceColumn raceColumn : s.getRaceColumns()) { - dbRaceColumns.add(storeRaceColumn(raceColumn)); - } - dbSeries.put(FieldNames.SERIES_RACE_COLUMNS.name(), dbRaceColumns); - if (s.getResultDiscardingRule() != null) { - storeResultDiscardingRule(dbSeries, s.getResultDiscardingRule(), FieldNames.SERIES_DISCARDING_THRESHOLDS); - } - return dbSeries; - } - - private DBObject storeFleet(Fleet fleet) { - DBObject dbFleet = new BasicDBObject(FieldNames.FLEET_NAME.name(), fleet.getName()); - if (fleet instanceof FleetImpl) { - dbFleet.put(FieldNames.FLEET_ORDERING.name(), ((FleetImpl) fleet).getOrdering()); - if(fleet.getColor() != null) { - Triple colorAsRGB = fleet.getColor().getAsRGB(); - // we save the color as a integer value representing the RGB values - int colorAsInt = (256 * 256 * colorAsRGB.getC()) + colorAsRGB.getB() * 256 + colorAsRGB.getA(); - dbFleet.put(FieldNames.FLEET_COLOR.name(), colorAsInt); - } else { - dbFleet.put(FieldNames.FLEET_COLOR.name(), null); - } - } - return dbFleet; - } - - @Override - public void storeRegattaForRaceID(String raceIDAsString, Regatta regatta) { - DBCollection regattaForRaceIDCollection = database.getCollection(CollectionNames.REGATTA_FOR_RACE_ID.name()); - DBObject query = new BasicDBObject(FieldNames.RACE_ID_AS_STRING.name(), raceIDAsString); - DBObject entry = new BasicDBObject(FieldNames.RACE_ID_AS_STRING.name(), raceIDAsString); - entry.put(FieldNames.REGATTA_NAME.name(), regatta.getName()); - regattaForRaceIDCollection.update(query, entry, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); - } - - @Override - public void removeRegattaForRaceID(String raceIDAsString, Regatta regatta) { - DBCollection regattaForRaceIDCollection = database.getCollection(CollectionNames.REGATTA_FOR_RACE_ID.name()); - DBObject query = new BasicDBObject(FieldNames.RACE_ID_AS_STRING.name(), raceIDAsString); - regattaForRaceIDCollection.remove(query); - } - - public DBCollection getRaceLogCollection() { - DBCollection result = database.getCollection(CollectionNames.RACE_LOGS.name()); - result.ensureIndex(new BasicDBObject(FieldNames.RACE_LOG_IDENTIFIER.name(), null)); - return result; - } - - private void storeRaceLogEventAuthor(DBObject dbObject, RaceLogEventAuthor author) { - if (author != null) { - dbObject.put(FieldNames.RACE_LOG_EVENT_AUTHOR_NAME.name(), author.getName()); - dbObject.put(FieldNames.RACE_LOG_EVENT_AUTHOR_PRIORITY.name(), author.getPriority()); - } - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogFlagEvent flagEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogFlagEvent(flagEvent)); - return result; - } - - private void storeRaceLogIdentifier(RaceLogIdentifier raceLogIdentifier, DBObject result) { - result.put(FieldNames.RACE_LOG_IDENTIFIER.name(), TripleSerializer.serialize(raceLogIdentifier.getIdentifier())); - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogStartTimeEvent startTimeEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogStartTimeEvent(startTimeEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogPassChangeEvent passChangeEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogPassChangeEvent(passChangeEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogRaceStatusEvent raceStatusEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogRaceStatusEvent(raceStatusEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogCourseAreaChangedEvent courseAreaChangedEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogCourseAreaChangedEvent(courseAreaChangedEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogCourseDesignChangedEvent courseDesignChangedEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogCourseDesignChangedEvent(courseDesignChangedEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogFinishPositioningListChangedEvent finishPositioningListChangedEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogFinishPositioningListChangedEvent(finishPositioningListChangedEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogFinishPositioningConfirmedEvent finishPositioningConfirmedEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogFinishPositioningConfirmedEvent(finishPositioningConfirmedEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogPathfinderEvent pathfinderEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogPathfinderEvent(pathfinderEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogGateLineOpeningTimeEvent gateLineOpeningTimeEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogGateLineOpeningTimeEvent(gateLineOpeningTimeEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogStartProcedureChangedEvent event) { - DBObject result = new BasicDBObject(); - result.put(FieldNames.RACE_LOG_IDENTIFIER.name(), TripleSerializer.serialize(raceLogIdentifier.getIdentifier())); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogStartProcedureChangedEvent(event)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogProtestStartTimeEvent event) { - DBObject result = new BasicDBObject(); - result.put(FieldNames.RACE_LOG_IDENTIFIER.name(), TripleSerializer.serialize(raceLogIdentifier.getIdentifier())); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogProtestStartTimeEvent(event)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogWindFixEvent event) { - DBObject result = new BasicDBObject(); - result.put(FieldNames.RACE_LOG_IDENTIFIER.name(), TripleSerializer.serialize(raceLogIdentifier.getIdentifier())); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogWindFix(event)); - return result; - } - - private Object storeRaceLogWindFix(RaceLogWindFixEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogWindFixEvent.class.getSimpleName()); - result.put(FieldNames.WIND.name(), storeWind(event.getWindFix())); - return result; - } - - private Object storeRaceLogProtestStartTimeEvent(RaceLogProtestStartTimeEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogProtestStartTimeEvent.class.getSimpleName()); - storeTimePoint(event.getProtestStartTime(), result, FieldNames.RACE_LOG_PROTEST_START_TIME); - return result; - } - - private Object storeRaceLogStartProcedureChangedEvent(RaceLogStartProcedureChangedEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogStartProcedureChangedEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_START_PROCEDURE_TYPE.name(), event.getStartProcedureType().name()); - return result; - } - - private Object storeRaceLogPathfinderEvent(RaceLogPathfinderEvent pathfinderEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(pathfinderEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogPathfinderEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_PATHFINDER_ID.name(), pathfinderEvent.getPathfinderId()); - return result; - } - - private DBObject storeRaceLogStartTimeEvent(RaceLogStartTimeEvent startTimeEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(startTimeEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogStartTimeEvent.class.getSimpleName()); - storeTimePoint(startTimeEvent.getStartTime(), result, FieldNames.RACE_LOG_EVENT_START_TIME); - result.put(FieldNames.RACE_LOG_EVENT_NEXT_STATUS.name(), startTimeEvent.getNextStatus().name()); - return result; - } - - public DBObject storeRaceLogFlagEvent(RaceLogFlagEvent flagEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(flagEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogFlagEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_EVENT_FLAG_UPPER.name(), flagEvent.getUpperFlag().name()); - result.put(FieldNames.RACE_LOG_EVENT_FLAG_LOWER.name(), flagEvent.getLowerFlag().name()); - result.put(FieldNames.RACE_LOG_EVENT_FLAG_DISPLAYED.name(), String.valueOf(flagEvent.isDisplayed())); - return result; - } - - private void storeRaceLogEventProperties(RaceLogEvent event, DBObject result) { - // for compatibility reasons we reuse the field name of Timed - storeTimePoint(event.getLogicalTimePoint(), result, FieldNames.TIME_AS_MILLIS); - storeTimePoint(event.getCreatedAt(), result, FieldNames.RACE_LOG_EVENT_CREATED_AT); - result.put(FieldNames.RACE_LOG_EVENT_ID.name(), event.getId()); - result.put(FieldNames.RACE_LOG_EVENT_PASS_ID.name(), event.getPassId()); - result.put(FieldNames.RACE_LOG_EVENT_INVOLVED_BOATS.name(), storeInvolvedBoatsForRaceLogEvent(event.getInvolvedBoats())); - storeRaceLogEventAuthor(result, event.getAuthor()); - } - - - private BasicDBList storeInvolvedBoatsForRaceLogEvent(List competitors) { - BasicDBList dbInvolvedCompetitorIds = new BasicDBList(); - for (Competitor competitor : competitors) { - dbInvolvedCompetitorIds.add(competitor.getId()); - } - return dbInvolvedCompetitorIds; - } - - private DBObject storeRaceLogPassChangeEvent(RaceLogPassChangeEvent passChangeEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(passChangeEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogPassChangeEvent.class.getSimpleName()); - return result; - } - - private DBObject storeRaceLogRaceStatusEvent(RaceLogRaceStatusEvent raceStatusEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(raceStatusEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogRaceStatusEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_EVENT_NEXT_STATUS.name(), raceStatusEvent.getNextStatus().name()); - return result; - } - - private DBObject storeRaceLogCourseAreaChangedEvent(RaceLogCourseAreaChangedEvent courseAreaChangedEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(courseAreaChangedEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogCourseAreaChangedEvent.class.getSimpleName()); - result.put(FieldNames.COURSE_AREA_ID.name(), courseAreaChangedEvent.getCourseAreaId()); - return result; - } - - private DBObject storeRaceLogCourseDesignChangedEvent(RaceLogCourseDesignChangedEvent courseDesignChangedEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(courseDesignChangedEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogCourseDesignChangedEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_COURSE_DESIGN_NAME.name(), courseDesignChangedEvent.getCourseDesign().getName()); - result.put(FieldNames.RACE_LOG_COURSE_DESIGN.name(), storeCourseBase(courseDesignChangedEvent.getCourseDesign())); - return result; - } - - private Object storeRaceLogFinishPositioningListChangedEvent(RaceLogFinishPositioningListChangedEvent finishPositioningListChangedEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(finishPositioningListChangedEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogFinishPositioningListChangedEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_POSITIONED_COMPETITORS.name(), storePositionedCompetitors(finishPositioningListChangedEvent.getPositionedCompetitorsIDsNamesMaxPointsReasons())); - - return result; - } - - private Object storeRaceLogFinishPositioningConfirmedEvent(RaceLogFinishPositioningConfirmedEvent finishPositioningConfirmedEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(finishPositioningConfirmedEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogFinishPositioningConfirmedEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_POSITIONED_COMPETITORS.name(), storePositionedCompetitors(finishPositioningConfirmedEvent.getPositionedCompetitorsIDsNamesMaxPointsReasons())); - - return result; - } - - private Object storeRaceLogGateLineOpeningTimeEvent(RaceLogGateLineOpeningTimeEvent gateLineOpeningTimeEvent){ - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(gateLineOpeningTimeEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogGateLineOpeningTimeEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_GATE_LINE_OPENING_TIME.name(), gateLineOpeningTimeEvent.getGateLineOpeningTimes().getGateLaunchStopTime()); - result.put(FieldNames.RACE_LOG_GOLF_DOWN_TIME.name(), gateLineOpeningTimeEvent.getGateLineOpeningTimes().getGolfDownTime()); - return result; - } - - private BasicDBList storePositionedCompetitors(List> positionedCompetitors) { - BasicDBList dbList = new BasicDBList(); - if (positionedCompetitors != null) { - for (Triple competitorPair : positionedCompetitors) { - dbList.add(storePositionedCompetitor(competitorPair)); - } - } - return dbList; - } - - private DBObject storePositionedCompetitor(Triple competitorTriple) { - DBObject result = new BasicDBObject(); - result.put(FieldNames.COMPETITOR_ID.name(), competitorTriple.getA()); - result.put(FieldNames.COMPETITOR_DISPLAY_NAME.name(), competitorTriple.getB()); - result.put(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name(), competitorTriple.getC().name()); - - return result; - } - - private BasicDBList storeCourseBase(CourseBase courseData) { - BasicDBList dbList = new BasicDBList(); - - for (Waypoint waypoint : courseData.getWaypoints()) { - dbList.add(storeWaypoint(waypoint)); - } - return dbList; - } - - private DBObject storeWaypoint(Waypoint waypoint) { - DBObject result = new BasicDBObject(); - result.put(FieldNames.WAYPOINT_PASSINGINSTRUCTIONS.name(), getPassingInstructions(waypoint.getPassingInstructions())); - result.put(FieldNames.CONTROLPOINT.name(), storeControlPoint(waypoint.getControlPoint())); - return result; - } - - private DBObject storeControlPoint(ControlPoint controlPoint) { - DBObject result = new BasicDBObject(); - if (controlPoint instanceof Mark) { - result.put(FieldNames.CONTROLPOINT_CLASS.name(), Mark.class.getSimpleName()); - result.put(FieldNames.CONTROLPOINT_VALUE.name(), storeMark((Mark) controlPoint)); - } else if (controlPoint instanceof ControlPointWithTwoMarks) { - result.put(FieldNames.CONTROLPOINT_CLASS.name(), ControlPointWithTwoMarks.class.getSimpleName()); - result.put(FieldNames.CONTROLPOINT_VALUE.name(), storeControlPointWithTwoMarks((ControlPointWithTwoMarks) controlPoint)); - } - return result; - } - - private DBObject storeControlPointWithTwoMarks(ControlPointWithTwoMarks cpwtm) { - DBObject result = new BasicDBObject(); - result.put(FieldNames.CONTROLPOINTWITHTWOMARKS_ID.name(), cpwtm.getId()); - result.put(FieldNames.CONTROLPOINTWITHTWOMARKS_NAME.name(), cpwtm.getName()); - result.put(FieldNames.CONTROLPOINTWITHTWOMARKS_LEFT.name(), storeMark(cpwtm.getLeft())); - result.put(FieldNames.CONTROLPOINTWITHTWOMARKS_RIGHT.name(), storeMark(cpwtm.getRight())); - return result; - } - - private DBObject storeMark(Mark mark) { - DBObject result = new BasicDBObject(); - result.put(FieldNames.MARK_ID.name(), mark.getId()); - result.put(FieldNames.MARK_COLOR.name(), mark.getColor()); - result.put(FieldNames.MARK_NAME.name(), mark.getName()); - result.put(FieldNames.MARK_PATTERN.name(), mark.getPattern()); - result.put(FieldNames.MARK_SHAPE.name(), mark.getShape()); - result.put(FieldNames.MARK_TYPE.name(), mark.getType().name()); - return result; - } - - private String getPassingInstructions(PassingInstruction passingInstructions) { - final String passing; - if (passingInstructions != null) { - passing = passingInstructions.name(); - } else { - passing = null; - } - return passing; - } - @Override - public void storeCompetitor(Competitor competitor) { - DBCollection collection = database.getCollection(CollectionNames.COMPETITORS.name()); - JSONObject json = competitorSerializer.serialize(competitor); - DBObject query = (DBObject) JSON.parse(CompetitorJsonSerializer.getCompetitorIdQuery(competitor).toString()); - DBObject entry = (DBObject) JSON.parse(json.toString()); - collection.update(query, entry, /* upsrt */true, /* multi */false, WriteConcern.SAFE); - } - - @Override - public void removeAllCompetitors() { - logger.info("Removing all persistent competitor info"); - DBCollection collection = database.getCollection(CollectionNames.COMPETITORS.name()); - collection.drop(); - } - - @Override - public void removeCompetitor(Competitor competitor) { - logger.info("Removing persistent competitor info for competitor "+competitor.getName()+" with ID "+competitor.getId()); - DBCollection collection = database.getCollection(CollectionNames.COMPETITORS.name()); - DBObject query = (DBObject) JSON.parse(CompetitorJsonSerializer.getCompetitorIdQuery(competitor).toString()); - collection.remove(query); - } - - @Override - public void storeDeviceConfiguration(DeviceConfigurationMatcher matcher, DeviceConfiguration configuration) { - DBCollection configurationsCollections = database.getCollection(CollectionNames.CONFIGURATIONS.name()); - - DBObject query = new BasicDBObject(); - query.put(FieldNames.CONFIGURATION_MATCHER_ID.name(), matcher.getMatcherIdentifier()); - - DBObject entryObject = new BasicDBObject(); - entryObject.put(FieldNames.CONFIGURATION_MATCHER_ID.name(), matcher.getMatcherIdentifier()); - entryObject.put(FieldNames.CONFIGURATION_MATCHER.name(), createDeviceConfigurationMatcherObject(matcher)); - entryObject.put(FieldNames.CONFIGURATION_CONFIG.name(), createDeviceConfigurationObject(configuration)); - - configurationsCollections.update(query, entryObject, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); - } - - private DBObject createDeviceConfigurationMatcherObject(DeviceConfigurationMatcher matcher) { - DBObject matcherObject = new BasicDBObject(); - matcherObject.put(FieldNames.CONFIGURATION_MATCHER_TYPE.name(), matcher.getMatcherType().name()); - if (matcher instanceof DeviceConfigurationMatcherSingle) { - BasicDBList client = new BasicDBList(); - client.add(((DeviceConfigurationMatcherSingle)matcher).getClientIdentifier()); - matcherObject.put(FieldNames.CONFIGURATION_MATCHER_CLIENTS.name(), client); - } else if (matcher instanceof DeviceConfigurationMatcherMulti) { - BasicDBList clients = new BasicDBList(); - Util.addAll(((DeviceConfigurationMatcherMulti)matcher).getClientIdentifiers(), clients); - matcherObject.put(FieldNames.CONFIGURATION_MATCHER_CLIENTS.name(), clients); - } - return matcherObject; - } - - private DBObject createDeviceConfigurationObject(DeviceConfiguration configuration) { - JsonSerializer serializer = DeviceConfigurationJsonSerializer.create(); - JSONObject json = serializer.serialize(configuration); - DBObject entry = (DBObject) JSON.parse(json.toString()); - return entry; - } - - @Override - public void removeDeviceConfiguration(DeviceConfigurationMatcher matcher) { - DBCollection configurationsCollections = database.getCollection(CollectionNames.CONFIGURATIONS.name()); - DBObject query = new BasicDBObject(); - query.put(FieldNames.CONFIGURATION_MATCHER_ID.name(), matcher.getMatcherIdentifier()); - configurationsCollections.remove(query); - } - - void storeRaceLogEventEvent(DBObject eventEntry) { - getRaceLogCollection().insert(eventEntry); - logger.fine("Inserted event entry into mongo race log collection"); - } - - @Override - public void removeRaceLog(RaceLogIdentifier identifier) { - DBObject query = new BasicDBObject(); - storeRaceLogIdentifier(identifier, query); - getRaceLogCollection().remove(query); - } -} +package com.sap.sailing.domain.persistence.impl; + +import java.io.Serializable; +import java.util.List; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bson.types.ObjectId; +import org.json.simple.JSONObject; + +import com.mongodb.BasicDBList; +import com.mongodb.BasicDBObject; +import com.mongodb.BasicDBObjectBuilder; +import com.mongodb.DB; +import com.mongodb.DBCollection; +import com.mongodb.DBObject; +import com.mongodb.WriteConcern; +import com.mongodb.util.JSON; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.ControlPoint; +import com.sap.sailing.domain.base.ControlPointWithTwoMarks; +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.base.CourseBase; +import com.sap.sailing.domain.base.Event; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.Timed; +import com.sap.sailing.domain.base.Venue; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.base.configuration.DeviceConfiguration; +import com.sap.sailing.domain.base.configuration.DeviceConfigurationMatcher; +import com.sap.sailing.domain.base.configuration.RegattaConfiguration; +import com.sap.sailing.domain.base.configuration.impl.DeviceConfigurationMatcherMulti; +import com.sap.sailing.domain.base.configuration.impl.DeviceConfigurationMatcherSingle; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.MaxPointsReason; +import com.sap.sailing.domain.common.PassingInstruction; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.WithID; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.impl.Util; +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.common.impl.Util.Triple; +import com.sap.sailing.domain.common.racelog.tracking.NoCorrespondingServiceRegisteredException; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinder; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinderFactory; +import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.LeaderboardGroup; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; +import com.sap.sailing.domain.leaderboard.SettableScoreCorrection; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sailing.domain.persistence.MongoObjectFactory; +import com.sap.sailing.domain.persistence.racelog.tracking.DeviceIdentifierMongoHandler; +import com.sap.sailing.domain.persistence.racelog.tracking.impl.PlaceHolderDeviceIdentifierMongoHandler; +import com.sap.sailing.domain.racelog.RaceLogCourseAreaChangedEvent; +import com.sap.sailing.domain.racelog.RaceLogCourseDesignChangedEvent; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogFinishPositioningConfirmedEvent; +import com.sap.sailing.domain.racelog.RaceLogFinishPositioningListChangedEvent; +import com.sap.sailing.domain.racelog.RaceLogFlagEvent; +import com.sap.sailing.domain.racelog.RaceLogGateLineOpeningTimeEvent; +import com.sap.sailing.domain.racelog.RaceLogIdentifier; +import com.sap.sailing.domain.racelog.RaceLogPassChangeEvent; +import com.sap.sailing.domain.racelog.RaceLogPathfinderEvent; +import com.sap.sailing.domain.racelog.RaceLogProtestStartTimeEvent; +import com.sap.sailing.domain.racelog.RaceLogRaceStatusEvent; +import com.sap.sailing.domain.racelog.RaceLogStartProcedureChangedEvent; +import com.sap.sailing.domain.racelog.RaceLogStartTimeEvent; +import com.sap.sailing.domain.racelog.RaceLogWindFixEvent; +import com.sap.sailing.domain.racelog.RevokeEvent; +import com.sap.sailing.domain.racelog.tracking.CloseOpenEndedDeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DefineMarkEvent; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceCompetitorMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.DeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceMarkMappingEvent; +import com.sap.sailing.domain.racelog.tracking.RegisterCompetitorEvent; +import com.sap.sailing.domain.racelog.tracking.StartTrackingEvent; +import com.sap.sailing.domain.tracking.Positioned; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.domain.tracking.TrackedRegatta; +import com.sap.sailing.domain.tracking.Wind; +import com.sap.sailing.domain.tracking.WindTrack; +import com.sap.sailing.server.gateway.serialization.JsonSerializer; +import com.sap.sailing.server.gateway.serialization.impl.CompetitorJsonSerializer; +import com.sap.sailing.server.gateway.serialization.impl.DeviceConfigurationJsonSerializer; +import com.sap.sailing.server.gateway.serialization.impl.RegattaConfigurationJsonSerializer; + +public class MongoObjectFactoryImpl implements MongoObjectFactory { + private static Logger logger = Logger.getLogger(MongoObjectFactoryImpl.class.getName()); + private final DB database; + private final CompetitorJsonSerializer competitorSerializer = CompetitorJsonSerializer.create(); + private final TypeBasedServiceFinder deviceIdentifierServiceFinder; + + /** + * Uses null for the device type service finder and hence will be unable to store device identifiers. + * Use this constructor only for testing purposes or in cases where there will happen absolutely no access to + * {@link DeviceIdentifier} objects. + */ + public MongoObjectFactoryImpl(DB database) { + this(database, /* deviceTypeServiceFinder */ null); + } + + public MongoObjectFactoryImpl(DB database, TypeBasedServiceFinderFactory serviceFinderFactory) { + this.database = database; + if (serviceFinderFactory != null) { + this.deviceIdentifierServiceFinder = serviceFinderFactory.createServiceFinder(DeviceIdentifierMongoHandler.class); + this.deviceIdentifierServiceFinder.setFallbackService(new PlaceHolderDeviceIdentifierMongoHandler()); + } else { + this.deviceIdentifierServiceFinder = null; + } + } + + @Override + public DB getDatabase() { + return database; + } + + public DBObject storeWind(Wind wind) { + DBObject result = new BasicDBObject(); + storePositioned(wind, result); + storeTimed(wind, result); + storeSpeedWithBearing(wind, result); + return result; + } + + public static void storeTimePoint(TimePoint timePoint, DBObject result, String fieldName) { + if (timePoint != null) { + result.put(fieldName, timePoint.asMillis()); + } + } + + public static void storeTimePoint(TimePoint timePoint, DBObject result, FieldNames field) { + storeTimePoint(timePoint, result, field.name()); + } + + public void storeTimed(Timed timed, DBObject result) { + if (timed.getTimePoint() != null) { + storeTimePoint(timed.getTimePoint(), result, FieldNames.TIME_AS_MILLIS); + } + } + + public void storeSpeedWithBearing(SpeedWithBearing speedWithBearing, DBObject result) { + storeSpeed(speedWithBearing, result); + storeBearing(speedWithBearing.getBearing(), result); + + } + + public void storeBearing(Bearing bearing, DBObject result) { + result.put(FieldNames.DEGREE_BEARING.name(), bearing.getDegrees()); + } + + public void storeSpeed(Speed speed, DBObject result) { + result.put(FieldNames.KNOT_SPEED.name(), speed.getKnots()); + } + + public void storePositioned(Positioned positioned, DBObject result) { + if (positioned.getPosition() != null) { + result.put(FieldNames.LAT_DEG.name(), positioned.getPosition().getLatDeg()); + result.put(FieldNames.LNG_DEG.name(), positioned.getPosition().getLngDeg()); + } + } + + @Override + public void addWindTrackDumper(TrackedRegatta trackedRegatta, TrackedRace trackedRace, WindSource windSource) { + WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource); + windTrack.addListener(new MongoWindListener(trackedRace, trackedRegatta.getRegatta().getName(), windSource, this, database)); + } + + public DBCollection getWindTrackCollection() { + DBCollection result = database.getCollection(CollectionNames.WIND_TRACKS.name()); + result.ensureIndex(new BasicDBObject(FieldNames.REGATTA_NAME.name(), null)); + return result; + } + + public DBCollection getGPSFixCollection() { + DBCollection gpsFixCollection = database.getCollection(CollectionNames.GPS_FIXES.name()); + DBObject index = new BasicDBObject(); + index.put(FieldNames.DEVICE_ID.name(), null); + index.put(FieldNames.TIME_AS_MILLIS.name(), null); + return gpsFixCollection; + } + + /** + * @param regattaName + * the regatta name is stored only for human readability purposes because a time stamp may be a bit unhandy for + * identifying where the wind fix was collected + */ + public DBObject storeWindTrackEntry(RaceDefinition race, String regattaName, WindSource windSource, Wind wind) { + BasicDBObject result = new BasicDBObject(); + result.put(FieldNames.RACE_ID.name(), race.getId()); + result.put(FieldNames.REGATTA_NAME.name(), regattaName); + result.put(FieldNames.WIND_SOURCE_NAME.name(), windSource.name()); + if (windSource.getId() != null) { + result.put(FieldNames.WIND_SOURCE_ID.name(), windSource.getId()); + } + result.put(FieldNames.WIND.name(), storeWind(wind)); + return result; + } + + private void storeRaceIdentifiers(RaceColumn raceColumn, DBObject dbObject) { + BasicDBObject raceIdentifiersPerFleet = new BasicDBObject(); + for (Fleet fleet : raceColumn.getFleets()) { + RaceIdentifier raceIdentifier = raceColumn.getRaceIdentifier(fleet); + if (raceIdentifier != null) { + DBObject raceIdentifierForFleet = new BasicDBObject(); + storeRaceIdentifier(raceIdentifierForFleet, raceIdentifier); + raceIdentifiersPerFleet.put(MongoUtils.escapeDollarAndDot(fleet.getName()), raceIdentifierForFleet); + } + } + dbObject.put(FieldNames.RACE_IDENTIFIERS.name(), raceIdentifiersPerFleet); + } + + private void storeRaceIdentifier(DBObject dbObject, RaceIdentifier raceIdentifier) { + if (raceIdentifier != null) { + dbObject.put(FieldNames.EVENT_NAME.name(), raceIdentifier.getRegattaName()); + dbObject.put(FieldNames.RACE_NAME.name(), raceIdentifier.getRaceName()); + } + } + + @Override + public void storeLeaderboard(Leaderboard leaderboard) { + DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); + try { + leaderboardCollection.ensureIndex(FieldNames.LEADERBOARD_NAME.name()); + } catch (NullPointerException npe) { + // sometimes, for reasons yet to be clarified, ensuring an index on the name field causes an NPE + logger.log(Level.SEVERE, "storeLeaderboard", npe); + } + BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), leaderboard.getName()); + BasicDBObject dbLeaderboard = new BasicDBObject(); + dbLeaderboard.put(FieldNames.LEADERBOARD_NAME.name(), leaderboard.getName()); + if (leaderboard.getDisplayName() != null) { + dbLeaderboard.put(FieldNames.LEADERBOARD_DISPLAY_NAME.name(), leaderboard.getDisplayName()); + } + BasicDBList dbSuppressedCompetitorIds = new BasicDBList(); + for (Competitor suppressedCompetitor : leaderboard.getSuppressedCompetitors()) { + dbSuppressedCompetitorIds.add(suppressedCompetitor.getId()); + } + dbLeaderboard.put(FieldNames.LEADERBOARD_SUPPRESSED_COMPETITOR_IDS.name(), dbSuppressedCompetitorIds); + if (leaderboard instanceof FlexibleLeaderboard) { + storeFlexibleLeaderboard((FlexibleLeaderboard) leaderboard, dbLeaderboard); + } else if (leaderboard instanceof RegattaLeaderboard) { + storeRegattaLeaderboard((RegattaLeaderboard) leaderboard, dbLeaderboard); + } else { + // at least store the scoring scheme + dbLeaderboard.put(FieldNames.SCORING_SCHEME_TYPE.name(), leaderboard.getScoringScheme().getType().name()); + } + if (leaderboard.getDefaultCourseArea() != null) { + dbLeaderboard.put(FieldNames.COURSE_AREA_ID.name(), leaderboard.getDefaultCourseArea().getId().toString()); + } else { + dbLeaderboard.put(FieldNames.COURSE_AREA_ID.name(), null); + } + storeColumnFactors(leaderboard, dbLeaderboard); + storeLeaderboardCorrectionsAndDiscards(leaderboard, dbLeaderboard); + leaderboardCollection.update(query, dbLeaderboard, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); + } + + private void storeColumnFactors(Leaderboard leaderboard, BasicDBObject dbLeaderboard) { + DBObject raceColumnFactors = new BasicDBObject(); + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + Double explicitFactor = raceColumn.getExplicitFactor(); + if (explicitFactor != null) { + raceColumnFactors.put(MongoUtils.escapeDollarAndDot(raceColumn.getName()), explicitFactor); + } + } + dbLeaderboard.put(FieldNames.LEADERBOARD_COLUMN_FACTORS.name(), raceColumnFactors); + } + + private void storeRegattaLeaderboard(RegattaLeaderboard leaderboard, DBObject dbLeaderboard) { + dbLeaderboard.put(FieldNames.REGATTA_NAME.name(), leaderboard.getRegatta().getName()); + } + + private void storeFlexibleLeaderboard(FlexibleLeaderboard leaderboard, BasicDBObject dbLeaderboard) { + BasicDBList dbRaceColumns = new BasicDBList(); + dbLeaderboard.put(FieldNames.SCORING_SCHEME_TYPE.name(), leaderboard.getScoringScheme().getType().name()); + dbLeaderboard.put(FieldNames.LEADERBOARD_COLUMNS.name(), dbRaceColumns); + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + BasicDBObject dbRaceColumn = storeRaceColumn(raceColumn); + dbRaceColumns.add(dbRaceColumn); + } + } + + private void storeLeaderboardCorrectionsAndDiscards(Leaderboard leaderboard, BasicDBObject dbLeaderboard) { + if (leaderboard.hasCarriedPoints()) { + BasicDBList dbCarriedPoints = new BasicDBList(); + dbLeaderboard.put(FieldNames.LEADERBOARD_CARRIED_POINTS_BY_ID.name(), dbCarriedPoints); + for (Entry competitorWithCarriedPoints : leaderboard + .getCompetitorsForWhichThereAreCarriedPoints().entrySet()) { + double carriedPoints = competitorWithCarriedPoints.getValue(); + Competitor competitor = competitorWithCarriedPoints.getKey(); + DBObject dbCarriedPointsForCompetitor = new BasicDBObject(); + dbCarriedPointsForCompetitor.put(FieldNames.COMPETITOR_ID.name(), competitor.getId()); + dbCarriedPointsForCompetitor.put(FieldNames.LEADERBOARD_CARRIED_POINTS.name(), carriedPoints); + dbCarriedPoints.add(dbCarriedPointsForCompetitor); + } + } + BasicDBObject dbScoreCorrections = new BasicDBObject(); + storeScoreCorrections(leaderboard, dbScoreCorrections); + dbLeaderboard.put(FieldNames.LEADERBOARD_SCORE_CORRECTIONS.name(), dbScoreCorrections); + final ResultDiscardingRule resultDiscardingRule = leaderboard.getResultDiscardingRule(); + storeResultDiscardingRule(dbLeaderboard, resultDiscardingRule, FieldNames.LEADERBOARD_DISCARDING_THRESHOLDS); + BasicDBList competitorDisplayNames = new BasicDBList(); + for (Competitor competitor : leaderboard.getCompetitors()) { + String displayNameForCompetitor = leaderboard.getDisplayName(competitor); + if (displayNameForCompetitor != null) { + DBObject dbDisplayName = new BasicDBObject(); + dbDisplayName.put(FieldNames.COMPETITOR_ID.name(), competitor.getId()); + dbDisplayName.put(FieldNames.COMPETITOR_DISPLAY_NAME.name(), displayNameForCompetitor); + competitorDisplayNames.add(dbDisplayName); + } + } + dbLeaderboard.put(FieldNames.LEADERBOARD_COMPETITOR_DISPLAY_NAMES.name(), competitorDisplayNames); + } + + /** + * Stores the result discarding rule to dbObject's field identified by field if the result discarding + * rule is not null and is of type {@link ThresholdBasedResultDiscardingRule}. Otherwise, it is assumed that the + * result discarding rule is otherwise implicitly obtained, e.g., from a definition of a regatta with its series, stored elsewhere. + */ + private void storeResultDiscardingRule(DBObject dbObject, + final ResultDiscardingRule resultDiscardingRule, FieldNames field) { + if (resultDiscardingRule != null && resultDiscardingRule instanceof ThresholdBasedResultDiscardingRule) { + BasicDBList dbResultDiscardingThresholds = new BasicDBList(); + for (int threshold : ((ThresholdBasedResultDiscardingRule) resultDiscardingRule).getDiscardIndexResultsStartingWithHowManyRaces()) { + dbResultDiscardingThresholds.add(threshold); + } + dbObject.put(field.name(), dbResultDiscardingThresholds); + } + } + + private BasicDBObject storeRaceColumn(RaceColumn raceColumn) { + BasicDBObject dbRaceColumn = new BasicDBObject(); + dbRaceColumn.put(FieldNames.LEADERBOARD_COLUMN_NAME.name(), raceColumn.getName()); + dbRaceColumn.put(FieldNames.LEADERBOARD_IS_MEDAL_RACE_COLUMN.name(), raceColumn.isMedalRace()); + storeRaceIdentifiers(raceColumn, dbRaceColumn); + return dbRaceColumn; + } + + private void storeScoreCorrections(Leaderboard leaderboard, BasicDBObject dbScoreCorrections) { + TimePoint now = MillisecondsTimePoint.now(); + SettableScoreCorrection scoreCorrection = leaderboard.getScoreCorrection(); + for (RaceColumn raceColumn : scoreCorrection.getRaceColumnsThatHaveCorrections()) { + BasicDBList dbCorrectionForRace = new BasicDBList(); + for (Competitor competitor : scoreCorrection.getCompetitorsThatHaveCorrectionsIn(raceColumn)) { + // TODO bug 655: make score corrections time dependent + if (scoreCorrection.isScoreCorrected(competitor, raceColumn, now)) { + BasicDBObject dbCorrectionForCompetitor = new BasicDBObject(); + dbCorrectionForCompetitor.put(FieldNames.COMPETITOR_ID.name(), competitor.getId()); + MaxPointsReason maxPointsReason = scoreCorrection.getMaxPointsReason(competitor, raceColumn, now); + if (maxPointsReason != MaxPointsReason.NONE) { + dbCorrectionForCompetitor.put(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name(), + maxPointsReason.name()); + } + Double explicitScoreCorrection = scoreCorrection + .getExplicitScoreCorrection(competitor, raceColumn); + if (explicitScoreCorrection != null) { + dbCorrectionForCompetitor.put(FieldNames.LEADERBOARD_CORRECTED_SCORE.name(), + explicitScoreCorrection); + } + dbCorrectionForRace.add(dbCorrectionForCompetitor); + } + } + if (!dbCorrectionForRace.isEmpty()) { + // using the column name as the key for the score corrections requires re-writing the score corrections + // of a meta-leaderboard if the name of one of its leaderboards changes + dbScoreCorrections.put(MongoUtils.escapeDollarAndDot(raceColumn.getName()), dbCorrectionForRace); + } + } + final TimePoint timePointOfLastCorrectionsValidity = scoreCorrection.getTimePointOfLastCorrectionsValidity(); + if (timePointOfLastCorrectionsValidity != null) { + dbScoreCorrections.put(FieldNames.LEADERBOARD_SCORE_CORRECTION_TIMESTAMP.name(), timePointOfLastCorrectionsValidity.asMillis()); + } + if (scoreCorrection.getComment() != null) { + dbScoreCorrections.put(FieldNames.LEADERBOARD_SCORE_CORRECTION_COMMENT.name(), scoreCorrection.getComment()); + } + } + + @Override + public void removeLeaderboard(String leaderboardName) { + DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); + BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), leaderboardName); + leaderboardCollection.remove(query); + } + + @Override + public void renameLeaderboard(String oldName, String newName) { + DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); + BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), oldName); + BasicDBObject renameUpdate = new BasicDBObject("$set", new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), newName)); + leaderboardCollection.update(query, renameUpdate, /* upsert */ true, /* multi */ false, WriteConcern.SAFE); + } + + @Override + public void storeLeaderboardGroup(LeaderboardGroup leaderboardGroup) { + DBCollection leaderboardGroupCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()); + DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); + + try { + leaderboardGroupCollection.ensureIndex(FieldNames.LEADERBOARD_GROUP_NAME.name()); + } catch (NullPointerException npe) { + // sometimes, for reasons yet to be clarified, ensuring an index on the name field causes an NPE + logger.log(Level.SEVERE, "storeLeaderboardGroup", npe); + } + BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), leaderboardGroup.getName()); + BasicDBObject dbLeaderboardGroup = new BasicDBObject(); + dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_NAME.name(), leaderboardGroup.getName()); + dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_DESCRIPTION.name(), leaderboardGroup.getDescription()); + dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_DISPLAY_IN_REVERSE_ORDER.name(), leaderboardGroup.isDisplayGroupsInReverseOrder()); + final Leaderboard overallLeaderboard = leaderboardGroup.getOverallLeaderboard(); + if (overallLeaderboard != null) { + BasicDBObject overallLeaderboardQuery = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), overallLeaderboard.getName()); + DBObject dbOverallLeaderboard = leaderboardCollection.findOne(overallLeaderboardQuery); + if (dbOverallLeaderboard == null) { + storeLeaderboard(overallLeaderboard); + dbOverallLeaderboard = leaderboardCollection.findOne(overallLeaderboardQuery); + } + ObjectId dbOverallLeaderboardId = (ObjectId) dbOverallLeaderboard.get("_id"); + dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_OVERALL_LEADERBOARD.name(), dbOverallLeaderboardId); + } + BasicDBList dbLeaderboardIds = new BasicDBList(); + for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { + BasicDBObject leaderboardQuery = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), leaderboard.getName()); + DBObject dbLeaderboard = leaderboardCollection.findOne(leaderboardQuery); + if (dbLeaderboard == null) { + storeLeaderboard(leaderboard); + dbLeaderboard = leaderboardCollection.findOne(leaderboardQuery); + } + ObjectId dbLeaderboardId = (ObjectId) dbLeaderboard.get("_id"); + dbLeaderboardIds.add(dbLeaderboardId); + } + dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_LEADERBOARDS.name(), dbLeaderboardIds); + leaderboardGroupCollection.update(query, dbLeaderboardGroup, true, false, WriteConcern.SAFE); + } + + @Override + public void removeLeaderboardGroup(String groupName) { + DBCollection leaderboardGroupCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()); + BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), groupName); + leaderboardGroupCollection.remove(query); + } + + @Override + public void renameLeaderboardGroup(String oldName, String newName) { + DBCollection leaderboardGroupCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()); + BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), oldName); + BasicDBObject update = new BasicDBObject("$set", new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), newName)); + leaderboardGroupCollection.update(query, update, /* upsert */ true, /* multi */ false, WriteConcern.SAFE); + } + + @Override + public void storeEvent(Event event) { + DBCollection eventCollection = database.getCollection(CollectionNames.EVENTS.name()); + eventCollection.ensureIndex(FieldNames.EVENT_ID.name()); + DBObject query = new BasicDBObject(); + query.put(FieldNames.EVENT_ID.name(), event.getId()); + DBObject eventDBObject = new BasicDBObject(); + eventDBObject.put(FieldNames.EVENT_NAME.name(), event.getName()); + eventDBObject.put(FieldNames.EVENT_ID.name(), event.getId()); + storeTimePoint(event.getStartDate(), eventDBObject, FieldNames.EVENT_START_DATE); + storeTimePoint(event.getEndDate(), eventDBObject, FieldNames.EVENT_END_DATE); + eventDBObject.put(FieldNames.EVENT_IS_PUBLIC.name(), event.isPublic()); + DBObject venueDBObject = getVenueAsDBObject(event.getVenue()); + eventDBObject.put(FieldNames.VENUE.name(), venueDBObject); + eventCollection.update(query, eventDBObject, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); + } + + @Override + public void renameEvent(Serializable id, String newName) { + DBCollection eventCollection = database.getCollection(CollectionNames.EVENTS.name()); + BasicDBObject query = new BasicDBObject(FieldNames.EVENT_ID.name(), id); + BasicDBObject renameUpdate = new BasicDBObject("$set", new BasicDBObject(FieldNames.EVENT_NAME.name(), newName)); + eventCollection.update(query, renameUpdate, /* upsert */ true, /* multi */ false, WriteConcern.SAFE); + } + + @Override + public void removeEvent(Serializable id) { + DBCollection eventsCollection = database.getCollection(CollectionNames.EVENTS.name()); + BasicDBObject query = new BasicDBObject(FieldNames.EVENT_ID.name(), id); + eventsCollection.remove(query); + } + + private DBObject getVenueAsDBObject(Venue venue) { + DBObject result = new BasicDBObject(); + result.put(FieldNames.VENUE_NAME.name(), venue.getName()); + BasicDBList courseAreaList = new BasicDBList(); + result.put(FieldNames.COURSE_AREAS.name(), courseAreaList); + for (CourseArea courseArea : venue.getCourseAreas()) { + DBObject dbCourseArea = new BasicDBObject(); + courseAreaList.add(dbCourseArea); + dbCourseArea.put(FieldNames.COURSE_AREA_NAME.name(), courseArea.getName()); + dbCourseArea.put(FieldNames.COURSE_AREA_ID.name(), courseArea.getId()); + } + return result; + } + + @Override + public void storeRegatta(Regatta regatta) { + DBCollection regattasCollection = database.getCollection(CollectionNames.REGATTAS.name()); + regattasCollection.ensureIndex(FieldNames.REGATTA_NAME.name()); + regattasCollection.ensureIndex(FieldNames.REGATTA_ID.name()); + DBObject dbRegatta = new BasicDBObject(); + DBObject query = new BasicDBObject(FieldNames.REGATTA_NAME.name(), regatta.getName()); + dbRegatta.put(FieldNames.REGATTA_NAME.name(), regatta.getName()); + dbRegatta.put(FieldNames.REGATTA_BASE_NAME.name(), regatta.getBaseName()); + dbRegatta.put(FieldNames.REGATTA_ID.name(), regatta.getId()); + dbRegatta.put(FieldNames.SCORING_SCHEME_TYPE.name(), regatta.getScoringScheme().getType().name()); + if (regatta.getBoatClass() != null) { + dbRegatta.put(FieldNames.BOAT_CLASS_NAME.name(), regatta.getBoatClass().getName()); + dbRegatta.put(FieldNames.BOAT_CLASS_TYPICALLY_STARTS_UPWIND.name(), regatta.getBoatClass().typicallyStartsUpwind()); + } + dbRegatta.put(FieldNames.REGATTA_SERIES.name(), storeSeries(regatta.getSeries())); + + if (regatta.getDefaultCourseArea() != null) { + dbRegatta.put(FieldNames.COURSE_AREA_ID.name(), regatta.getDefaultCourseArea().getId().toString()); + } else { + dbRegatta.put(FieldNames.COURSE_AREA_ID.name(), null); + } + if (regatta.getRegattaConfiguration() != null) { + JsonSerializer serializer = RegattaConfigurationJsonSerializer.create(); + JSONObject json = serializer.serialize(regatta.getRegattaConfiguration()); + DBObject configurationObject = (DBObject) JSON.parse(json.toString()); + dbRegatta.put(FieldNames.REGATTA_REGATTA_CONFIGURATION.name(), configurationObject); + } + + regattasCollection.update(query, dbRegatta, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); + } + + @Override + public void removeRegatta(Regatta regatta) { + DBCollection regattasCollection = database.getCollection(CollectionNames.REGATTAS.name()); + DBObject query = new BasicDBObject(FieldNames.REGATTA_NAME.name(), regatta.getName()); + regattasCollection.remove(query); + } + + private BasicDBList storeSeries(Iterable series) { + BasicDBList dbSeries = new BasicDBList(); + for (Series s : series) { + dbSeries.add(storeSeries(s)); + } + return dbSeries; + } + + private DBObject storeSeries(Series s) { + DBObject dbSeries = new BasicDBObject(); + dbSeries.put(FieldNames.SERIES_NAME.name(), s.getName()); + dbSeries.put(FieldNames.SERIES_IS_MEDAL.name(), s.isMedal()); + dbSeries.put(FieldNames.SERIES_HAS_SPLIT_FLEET_CONTIGUOUS_SCORING.name(), s.hasSplitFleetContiguousScoring()); + dbSeries.put(FieldNames.SERIES_STARTS_WITH_ZERO_SCORE.name(), s.isStartsWithZeroScore()); + dbSeries.put(FieldNames.SERIES_STARTS_WITH_NON_DISCARDABLE_CARRY_FORWARD.name(), s.isFirstColumnIsNonDiscardableCarryForward()); + BasicDBList dbFleets = new BasicDBList(); + for (Fleet fleet : s.getFleets()) { + dbFleets.add(storeFleet(fleet)); + } + dbSeries.put(FieldNames.SERIES_FLEETS.name(), dbFleets); + BasicDBList dbRaceColumns = new BasicDBList(); + for (RaceColumn raceColumn : s.getRaceColumns()) { + dbRaceColumns.add(storeRaceColumn(raceColumn)); + } + dbSeries.put(FieldNames.SERIES_RACE_COLUMNS.name(), dbRaceColumns); + if (s.getResultDiscardingRule() != null) { + storeResultDiscardingRule(dbSeries, s.getResultDiscardingRule(), FieldNames.SERIES_DISCARDING_THRESHOLDS); + } + return dbSeries; + } + + private DBObject storeFleet(Fleet fleet) { + DBObject dbFleet = new BasicDBObject(FieldNames.FLEET_NAME.name(), fleet.getName()); + if (fleet instanceof FleetImpl) { + dbFleet.put(FieldNames.FLEET_ORDERING.name(), ((FleetImpl) fleet).getOrdering()); + if(fleet.getColor() != null) { + Triple colorAsRGB = fleet.getColor().getAsRGB(); + // we save the color as a integer value representing the RGB values + int colorAsInt = (256 * 256 * colorAsRGB.getC()) + colorAsRGB.getB() * 256 + colorAsRGB.getA(); + dbFleet.put(FieldNames.FLEET_COLOR.name(), colorAsInt); + } else { + dbFleet.put(FieldNames.FLEET_COLOR.name(), null); + } + } + return dbFleet; + } + + @Override + public void storeRegattaForRaceID(String raceIDAsString, Regatta regatta) { + DBCollection regattaForRaceIDCollection = database.getCollection(CollectionNames.REGATTA_FOR_RACE_ID.name()); + DBObject query = new BasicDBObject(FieldNames.RACE_ID_AS_STRING.name(), raceIDAsString); + DBObject entry = new BasicDBObject(FieldNames.RACE_ID_AS_STRING.name(), raceIDAsString); + entry.put(FieldNames.REGATTA_NAME.name(), regatta.getName()); + regattaForRaceIDCollection.update(query, entry, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); + } + + @Override + public void removeRegattaForRaceID(String raceIDAsString, Regatta regatta) { + DBCollection regattaForRaceIDCollection = database.getCollection(CollectionNames.REGATTA_FOR_RACE_ID.name()); + DBObject query = new BasicDBObject(FieldNames.RACE_ID_AS_STRING.name(), raceIDAsString); + regattaForRaceIDCollection.remove(query); + } + + public DBCollection getRaceLogCollection() { + DBCollection result = database.getCollection(CollectionNames.RACE_LOGS.name()); + result.ensureIndex(new BasicDBObject(FieldNames.RACE_LOG_IDENTIFIER.name(), null)); + return result; + } + + private void storeRaceLogEventAuthor(DBObject dbObject, RaceLogEventAuthor author) { + if (author != null) { + dbObject.put(FieldNames.RACE_LOG_EVENT_AUTHOR_NAME.name(), author.getName()); + dbObject.put(FieldNames.RACE_LOG_EVENT_AUTHOR_PRIORITY.name(), author.getPriority()); + } + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogFlagEvent flagEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogFlagEvent(flagEvent)); + return result; + } + + private void storeRaceLogIdentifier(RaceLogIdentifier raceLogIdentifier, DBObject result) { + result.put(FieldNames.RACE_LOG_IDENTIFIER.name(), TripleSerializer.serialize(raceLogIdentifier.getIdentifier())); + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogStartTimeEvent startTimeEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogStartTimeEvent(startTimeEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogPassChangeEvent passChangeEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogPassChangeEvent(passChangeEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogRaceStatusEvent raceStatusEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogRaceStatusEvent(raceStatusEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogCourseAreaChangedEvent courseAreaChangedEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogCourseAreaChangedEvent(courseAreaChangedEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogCourseDesignChangedEvent courseDesignChangedEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogCourseDesignChangedEvent(courseDesignChangedEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogFinishPositioningListChangedEvent finishPositioningListChangedEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogFinishPositioningListChangedEvent(finishPositioningListChangedEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogFinishPositioningConfirmedEvent finishPositioningConfirmedEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogFinishPositioningConfirmedEvent(finishPositioningConfirmedEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogPathfinderEvent pathfinderEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogPathfinderEvent(pathfinderEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogGateLineOpeningTimeEvent gateLineOpeningTimeEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogGateLineOpeningTimeEvent(gateLineOpeningTimeEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogStartProcedureChangedEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogStartProcedureChangedEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogProtestStartTimeEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogProtestStartTimeEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogWindFixEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogWindFix(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, DeviceCompetitorMappingEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogDeviceCompetitorMappingEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, DeviceMarkMappingEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogDeviceMarkMappingEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, DenoteForTrackingEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogDenoteForTrackingEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, StartTrackingEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogStartTrackingEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RevokeEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogRevokeEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RegisterCompetitorEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogRegisterCompetitorEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, DefineMarkEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogDefineMarkEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, CloseOpenEndedDeviceMappingEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogCloseOpenEndedDeviceMappingEvent(event)); + return result; + } + + private Object storeRaceLogWindFix(RaceLogWindFixEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogWindFixEvent.class.getSimpleName()); + result.put(FieldNames.WIND.name(), storeWind(event.getWindFix())); + return result; + } + + private Object storeRaceLogProtestStartTimeEvent(RaceLogProtestStartTimeEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogProtestStartTimeEvent.class.getSimpleName()); + storeTimePoint(event.getProtestStartTime(), result, FieldNames.RACE_LOG_PROTEST_START_TIME); + return result; + } + + private Object storeRaceLogStartProcedureChangedEvent(RaceLogStartProcedureChangedEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogStartProcedureChangedEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_START_PROCEDURE_TYPE.name(), event.getStartProcedureType().name()); + return result; + } + + private Object storeRaceLogPathfinderEvent(RaceLogPathfinderEvent pathfinderEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(pathfinderEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogPathfinderEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_PATHFINDER_ID.name(), pathfinderEvent.getPathfinderId()); + return result; + } + + private void storeRaceLogDeviceMappingEvent(DeviceMappingEvent event, DBObject result) { + DBObject deviceId = null; + try { + deviceId = storeDeviceId(deviceIdentifierServiceFinder, event.getDevice()); + } catch (Exception e) { + logger.log(Level.WARNING, "Could not store deviceId for RaceLogEvent", e); + e.printStackTrace(); + } + result.put(FieldNames.DEVICE_ID.name(), deviceId); + if (event.getFrom() != null) { + storeTimePoint(event.getFrom(), result, FieldNames.RACE_LOG_FROM); + } + if (event.getTo() != null) { + storeTimePoint(event.getTo(), result, FieldNames.RACE_LOG_TO); + } + } + + private Object storeRaceLogDeviceCompetitorMappingEvent(DeviceCompetitorMappingEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), DeviceCompetitorMappingEvent.class.getSimpleName()); + storeRaceLogDeviceMappingEvent(event, result); + result.put(FieldNames.COMPETITOR_ID.name(), event.getMappedTo().getId()); + return result; + } + + private Object storeRaceLogDeviceMarkMappingEvent(DeviceMarkMappingEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), DeviceMarkMappingEvent.class.getSimpleName()); + storeRaceLogDeviceMappingEvent(event, result); + result.put(FieldNames.MARK_ID.name(), event.getMappedTo().getId()); + return result; + } + + private Object storeRaceLogDenoteForTrackingEvent(DenoteForTrackingEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), DenoteForTrackingEvent.class.getSimpleName()); + result.put(FieldNames.RACE_NAME.name(), event.getRaceName()); + result.put(FieldNames.BOAT_CLASS_NAME.name(), event.getBoatClass().getName()); + result.put(FieldNames.RACE_ID.name(), event.getRaceId()); + return result; + } + + private Object storeRaceLogStartTrackingEvent(StartTrackingEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), StartTrackingEvent.class.getSimpleName()); + return result; + } + + private Object storeRaceLogRevokeEvent(RevokeEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RevokeEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_REVOKED_EVENT_ID.name(), event.getRevokedEventId()); + return result; + } + + private Object storeRaceLogRegisterCompetitorEvent(RegisterCompetitorEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RegisterCompetitorEvent.class.getSimpleName()); + return result; + } + + private Object storeRaceLogDefineMarkEvent(DefineMarkEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), DefineMarkEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_MARK.name(), storeMark(event.getMark())); + return result; + } + + private Object storeRaceLogCloseOpenEndedDeviceMappingEvent(CloseOpenEndedDeviceMappingEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), CloseOpenEndedDeviceMappingEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_DEVICE_MAPPING_EVENT_ID.name(), event.getDeviceMappingEventId()); + storeTimePoint(event.getClosingTimePoint(), result, FieldNames.RACE_LOG_CLOSING_TIMEPOINT); + return result; + } + + public DBObject storeRaceLogFlagEvent(RaceLogFlagEvent flagEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(flagEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogFlagEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_EVENT_FLAG_UPPER.name(), flagEvent.getUpperFlag().name()); + result.put(FieldNames.RACE_LOG_EVENT_FLAG_LOWER.name(), flagEvent.getLowerFlag().name()); + result.put(FieldNames.RACE_LOG_EVENT_FLAG_DISPLAYED.name(), String.valueOf(flagEvent.isDisplayed())); + return result; + } + + private DBObject storeRaceLogStartTimeEvent(RaceLogStartTimeEvent startTimeEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(startTimeEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogStartTimeEvent.class.getSimpleName()); + storeTimePoint(startTimeEvent.getStartTime(), result, FieldNames.RACE_LOG_EVENT_START_TIME); + result.put(FieldNames.RACE_LOG_EVENT_NEXT_STATUS.name(), startTimeEvent.getNextStatus().name()); + return result; + } + + private void storeRaceLogEventProperties(RaceLogEvent event, DBObject result) { + // for compatibility reasons we reuse the field name of Timed + storeTimePoint(event.getLogicalTimePoint(), result, FieldNames.TIME_AS_MILLIS); + storeTimePoint(event.getCreatedAt(), result, FieldNames.RACE_LOG_EVENT_CREATED_AT); + result.put(FieldNames.RACE_LOG_EVENT_ID.name(), event.getId()); + result.put(FieldNames.RACE_LOG_EVENT_PASS_ID.name(), event.getPassId()); + result.put(FieldNames.RACE_LOG_EVENT_INVOLVED_BOATS.name(), storeInvolvedBoatsForRaceLogEvent(event.getInvolvedBoats())); + storeRaceLogEventAuthor(result, event.getAuthor()); + } + + + private BasicDBList storeInvolvedBoatsForRaceLogEvent(List competitors) { + BasicDBList dbInvolvedCompetitorIds = new BasicDBList(); + for (Competitor competitor : competitors) { + dbInvolvedCompetitorIds.add(competitor.getId()); + } + return dbInvolvedCompetitorIds; + } + + private DBObject storeRaceLogPassChangeEvent(RaceLogPassChangeEvent passChangeEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(passChangeEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogPassChangeEvent.class.getSimpleName()); + return result; + } + + private DBObject storeRaceLogRaceStatusEvent(RaceLogRaceStatusEvent raceStatusEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(raceStatusEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogRaceStatusEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_EVENT_NEXT_STATUS.name(), raceStatusEvent.getNextStatus().name()); + return result; + } + + private DBObject storeRaceLogCourseAreaChangedEvent(RaceLogCourseAreaChangedEvent courseAreaChangedEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(courseAreaChangedEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogCourseAreaChangedEvent.class.getSimpleName()); + result.put(FieldNames.COURSE_AREA_ID.name(), courseAreaChangedEvent.getCourseAreaId()); + return result; + } + + private DBObject storeRaceLogCourseDesignChangedEvent(RaceLogCourseDesignChangedEvent courseDesignChangedEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(courseDesignChangedEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogCourseDesignChangedEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_COURSE_DESIGN_NAME.name(), courseDesignChangedEvent.getCourseDesign().getName()); + result.put(FieldNames.RACE_LOG_COURSE_DESIGN.name(), storeCourseBase(courseDesignChangedEvent.getCourseDesign())); + return result; + } + + private Object storeRaceLogFinishPositioningListChangedEvent(RaceLogFinishPositioningListChangedEvent finishPositioningListChangedEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(finishPositioningListChangedEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogFinishPositioningListChangedEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_POSITIONED_COMPETITORS.name(), storePositionedCompetitors(finishPositioningListChangedEvent.getPositionedCompetitorsIDsNamesMaxPointsReasons())); + + return result; + } + + private Object storeRaceLogFinishPositioningConfirmedEvent(RaceLogFinishPositioningConfirmedEvent finishPositioningConfirmedEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(finishPositioningConfirmedEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogFinishPositioningConfirmedEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_POSITIONED_COMPETITORS.name(), storePositionedCompetitors(finishPositioningConfirmedEvent.getPositionedCompetitorsIDsNamesMaxPointsReasons())); + + return result; + } + + private Object storeRaceLogGateLineOpeningTimeEvent(RaceLogGateLineOpeningTimeEvent gateLineOpeningTimeEvent){ + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(gateLineOpeningTimeEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogGateLineOpeningTimeEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_GATE_LINE_OPENING_TIME.name(), gateLineOpeningTimeEvent.getGateLineOpeningTimes().getGateLaunchStopTime()); + result.put(FieldNames.RACE_LOG_GOLF_DOWN_TIME.name(), gateLineOpeningTimeEvent.getGateLineOpeningTimes().getGolfDownTime()); + return result; + } + + private BasicDBList storePositionedCompetitors(List> positionedCompetitors) { + BasicDBList dbList = new BasicDBList(); + if (positionedCompetitors != null) { + for (Triple competitorPair : positionedCompetitors) { + dbList.add(storePositionedCompetitor(competitorPair)); + } + } + return dbList; + } + + private DBObject storePositionedCompetitor(Triple competitorTriple) { + DBObject result = new BasicDBObject(); + result.put(FieldNames.COMPETITOR_ID.name(), competitorTriple.getA()); + result.put(FieldNames.COMPETITOR_DISPLAY_NAME.name(), competitorTriple.getB()); + result.put(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name(), competitorTriple.getC().name()); + + return result; + } + + private BasicDBList storeCourseBase(CourseBase courseData) { + BasicDBList dbList = new BasicDBList(); + + for (Waypoint waypoint : courseData.getWaypoints()) { + dbList.add(storeWaypoint(waypoint)); + } + return dbList; + } + + private DBObject storeWaypoint(Waypoint waypoint) { + DBObject result = new BasicDBObject(); + result.put(FieldNames.WAYPOINT_PASSINGINSTRUCTIONS.name(), getPassingInstructions(waypoint.getPassingInstructions())); + result.put(FieldNames.CONTROLPOINT.name(), storeControlPoint(waypoint.getControlPoint())); + return result; + } + + private DBObject storeControlPoint(ControlPoint controlPoint) { + DBObject result = new BasicDBObject(); + if (controlPoint instanceof Mark) { + result.put(FieldNames.CONTROLPOINT_CLASS.name(), Mark.class.getSimpleName()); + result.put(FieldNames.CONTROLPOINT_VALUE.name(), storeMark((Mark) controlPoint)); + } else if (controlPoint instanceof ControlPointWithTwoMarks) { + result.put(FieldNames.CONTROLPOINT_CLASS.name(), ControlPointWithTwoMarks.class.getSimpleName()); + result.put(FieldNames.CONTROLPOINT_VALUE.name(), storeControlPointWithTwoMarks((ControlPointWithTwoMarks) controlPoint)); + } + return result; + } + + private DBObject storeControlPointWithTwoMarks(ControlPointWithTwoMarks cpwtm) { + DBObject result = new BasicDBObject(); + result.put(FieldNames.CONTROLPOINTWITHTWOMARKS_ID.name(), cpwtm.getId()); + result.put(FieldNames.CONTROLPOINTWITHTWOMARKS_NAME.name(), cpwtm.getName()); + result.put(FieldNames.CONTROLPOINTWITHTWOMARKS_LEFT.name(), storeMark(cpwtm.getLeft())); + result.put(FieldNames.CONTROLPOINTWITHTWOMARKS_RIGHT.name(), storeMark(cpwtm.getRight())); + return result; + } + + private DBObject storeMark(Mark mark) { + DBObject result = new BasicDBObject(); + result.put(FieldNames.MARK_ID.name(), mark.getId()); + result.put(FieldNames.MARK_COLOR.name(), mark.getColor()); + result.put(FieldNames.MARK_NAME.name(), mark.getName()); + result.put(FieldNames.MARK_PATTERN.name(), mark.getPattern()); + result.put(FieldNames.MARK_SHAPE.name(), mark.getShape()); + result.put(FieldNames.MARK_TYPE.name(), mark.getType().name()); + return result; + } + + private String getPassingInstructions(PassingInstruction passingInstructions) { + final String passing; + if (passingInstructions != null) { + passing = passingInstructions.name(); + } else { + passing = null; + } + return passing; + } + @Override + public void storeCompetitor(Competitor competitor) { + DBCollection collection = database.getCollection(CollectionNames.COMPETITORS.name()); + JSONObject json = competitorSerializer.serialize(competitor); + DBObject query = (DBObject) JSON.parse(CompetitorJsonSerializer.getCompetitorIdQuery(competitor).toString()); + DBObject entry = (DBObject) JSON.parse(json.toString()); + collection.update(query, entry, /* upsrt */true, /* multi */false, WriteConcern.SAFE); + } + + @Override + public void removeAllCompetitors() { + logger.info("Removing all persistent competitor info"); + DBCollection collection = database.getCollection(CollectionNames.COMPETITORS.name()); + collection.drop(); + } + + @Override + public void removeCompetitor(Competitor competitor) { + logger.info("Removing persistent competitor info for competitor "+competitor.getName()+" with ID "+competitor.getId()); + DBCollection collection = database.getCollection(CollectionNames.COMPETITORS.name()); + DBObject query = (DBObject) JSON.parse(CompetitorJsonSerializer.getCompetitorIdQuery(competitor).toString()); + collection.remove(query); + } + + @Override + public void storeDeviceConfiguration(DeviceConfigurationMatcher matcher, DeviceConfiguration configuration) { + DBCollection configurationsCollections = database.getCollection(CollectionNames.CONFIGURATIONS.name()); + + DBObject query = new BasicDBObject(); + query.put(FieldNames.CONFIGURATION_MATCHER_ID.name(), matcher.getMatcherIdentifier()); + + DBObject entryObject = new BasicDBObject(); + entryObject.put(FieldNames.CONFIGURATION_MATCHER_ID.name(), matcher.getMatcherIdentifier()); + entryObject.put(FieldNames.CONFIGURATION_MATCHER.name(), createDeviceConfigurationMatcherObject(matcher)); + entryObject.put(FieldNames.CONFIGURATION_CONFIG.name(), createDeviceConfigurationObject(configuration)); + + configurationsCollections.update(query, entryObject, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); + } + + private DBObject createDeviceConfigurationMatcherObject(DeviceConfigurationMatcher matcher) { + DBObject matcherObject = new BasicDBObject(); + matcherObject.put(FieldNames.CONFIGURATION_MATCHER_TYPE.name(), matcher.getMatcherType().name()); + if (matcher instanceof DeviceConfigurationMatcherSingle) { + BasicDBList client = new BasicDBList(); + client.add(((DeviceConfigurationMatcherSingle)matcher).getClientIdentifier()); + matcherObject.put(FieldNames.CONFIGURATION_MATCHER_CLIENTS.name(), client); + } else if (matcher instanceof DeviceConfigurationMatcherMulti) { + BasicDBList clients = new BasicDBList(); + Util.addAll(((DeviceConfigurationMatcherMulti)matcher).getClientIdentifiers(), clients); + matcherObject.put(FieldNames.CONFIGURATION_MATCHER_CLIENTS.name(), clients); + } + return matcherObject; + } + + private DBObject createDeviceConfigurationObject(DeviceConfiguration configuration) { + JsonSerializer serializer = DeviceConfigurationJsonSerializer.create(); + JSONObject json = serializer.serialize(configuration); + DBObject entry = (DBObject) JSON.parse(json.toString()); + return entry; + } + + @Override + public void removeDeviceConfiguration(DeviceConfigurationMatcher matcher) { + DBCollection configurationsCollections = database.getCollection(CollectionNames.CONFIGURATIONS.name()); + DBObject query = new BasicDBObject(); + query.put(FieldNames.CONFIGURATION_MATCHER_ID.name(), matcher.getMatcherIdentifier()); + configurationsCollections.remove(query); + } + + public static DBObject storeDeviceId( + TypeBasedServiceFinder deviceIdentifierServiceFinder, DeviceIdentifier device) + throws TransformationException, NoCorrespondingServiceRegisteredException { + String type = device.getIdentifierType(); + DeviceIdentifierMongoHandler handler = deviceIdentifierServiceFinder.findService(type); + Pair pair = handler.serialize(device); + type = pair.getA(); + Object deviceTypeSpecificId = pair.getB(); + return new BasicDBObjectBuilder() + .add(FieldNames.DEVICE_TYPE.name(), type) + .add(FieldNames.DEVICE_TYPE_SPECIFIC_ID.name(), deviceTypeSpecificId) + .add(FieldNames.DEVICE_STRING_REPRESENTATION.name(), device.getStringRepresentation()).get(); + } + + void storeRaceLogEventEvent(DBObject eventEntry) { + getRaceLogCollection().insert(eventEntry); + logger.fine("Inserted event entry into mongo race log collection"); + } + + @Override + public void removeRaceLog(RaceLogIdentifier identifier) { + DBObject query = new BasicDBObject(); + storeRaceLogIdentifier(identifier, query); + getRaceLogCollection().remove(query); + } +} diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoRaceLogStoreVisitor.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoRaceLogStoreVisitor.java index 5e0e9bc25ba..39b0108f90e 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoRaceLogStoreVisitor.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoRaceLogStoreVisitor.java @@ -1,114 +1,169 @@ -package com.sap.sailing.domain.persistence.impl; - -import java.util.logging.Logger; - -import com.mongodb.DBObject; -import com.sap.sailing.domain.persistence.MongoObjectFactory; -import com.sap.sailing.domain.racelog.RaceLogCourseAreaChangedEvent; -import com.sap.sailing.domain.racelog.RaceLogCourseDesignChangedEvent; -import com.sap.sailing.domain.racelog.RaceLogEventVisitor; -import com.sap.sailing.domain.racelog.RaceLogFinishPositioningConfirmedEvent; -import com.sap.sailing.domain.racelog.RaceLogFinishPositioningListChangedEvent; -import com.sap.sailing.domain.racelog.RaceLogFlagEvent; -import com.sap.sailing.domain.racelog.RaceLogGateLineOpeningTimeEvent; -import com.sap.sailing.domain.racelog.RaceLogIdentifier; -import com.sap.sailing.domain.racelog.RaceLogPassChangeEvent; -import com.sap.sailing.domain.racelog.RaceLogPathfinderEvent; -import com.sap.sailing.domain.racelog.RaceLogProtestStartTimeEvent; -import com.sap.sailing.domain.racelog.RaceLogRaceStatusEvent; -import com.sap.sailing.domain.racelog.RaceLogStartProcedureChangedEvent; -import com.sap.sailing.domain.racelog.RaceLogStartTimeEvent; -import com.sap.sailing.domain.racelog.RaceLogWindFixEvent; - -public class MongoRaceLogStoreVisitor implements RaceLogEventVisitor { - - private final RaceLogIdentifier raceLogIdentifier; - private final MongoObjectFactoryImpl mongoObjectFactory; - - final static Logger logger = Logger.getLogger(MongoRaceLogStoreVisitor.class.getName()); - - public MongoRaceLogStoreVisitor(RaceLogIdentifier identifier, MongoObjectFactory mongoObjectFactory) { - super(); - this.raceLogIdentifier = identifier; - this.mongoObjectFactory = (MongoObjectFactoryImpl) mongoObjectFactory; - } - - @Override - public void visit(RaceLogFlagEvent event) { - DBObject flagEventTrackEntry = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); - mongoObjectFactory.storeRaceLogEventEvent(flagEventTrackEntry); - } - - @Override - public void visit(RaceLogPassChangeEvent event) { - DBObject passChangeEventTrackEntry = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); - mongoObjectFactory.storeRaceLogEventEvent(passChangeEventTrackEntry); - } - - @Override - public void visit(RaceLogRaceStatusEvent event) { - DBObject raceStatusEventTrackEntry = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); - mongoObjectFactory.storeRaceLogEventEvent(raceStatusEventTrackEntry); - } - - @Override - public void visit(RaceLogStartTimeEvent event) { - DBObject startTimeEventTrackEntry = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); - mongoObjectFactory.storeRaceLogEventEvent(startTimeEventTrackEntry); - } - - @Override - public void visit(RaceLogCourseAreaChangedEvent event) { - DBObject courseAreaChangedEventTrackEntry = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); - mongoObjectFactory.storeRaceLogEventEvent(courseAreaChangedEventTrackEntry); - } - - @Override - public void visit(RaceLogCourseDesignChangedEvent event) { - DBObject courseDesignChangedEventTrackEntry = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); - mongoObjectFactory.storeRaceLogEventEvent(courseDesignChangedEventTrackEntry); - } - - @Override - public void visit(RaceLogFinishPositioningListChangedEvent event) { - DBObject finishPositioningListChangedEventTrackEntry = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); - mongoObjectFactory.storeRaceLogEventEvent(finishPositioningListChangedEventTrackEntry); - } - - @Override - public void visit(RaceLogFinishPositioningConfirmedEvent event) { - DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); - mongoObjectFactory.storeRaceLogEventEvent(object); - } - - @Override - public void visit(RaceLogPathfinderEvent event) { - DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); - mongoObjectFactory.storeRaceLogEventEvent(object); - } - - @Override - public void visit(RaceLogGateLineOpeningTimeEvent event) { - DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); - mongoObjectFactory.storeRaceLogEventEvent(object); - } - - @Override - public void visit(RaceLogStartProcedureChangedEvent event) { - DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); - mongoObjectFactory.storeRaceLogEventEvent(object); - } - - @Override - public void visit(RaceLogProtestStartTimeEvent event) { - DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); - mongoObjectFactory.storeRaceLogEventEvent(object); - } - - @Override - public void visit(RaceLogWindFixEvent event) { - DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); - mongoObjectFactory.storeRaceLogEventEvent(object); - } - -} +package com.sap.sailing.domain.persistence.impl; + +import java.util.logging.Logger; + +import com.mongodb.DBObject; +import com.sap.sailing.domain.persistence.MongoObjectFactory; +import com.sap.sailing.domain.racelog.RaceLogCourseAreaChangedEvent; +import com.sap.sailing.domain.racelog.RaceLogCourseDesignChangedEvent; +import com.sap.sailing.domain.racelog.RaceLogEventVisitor; +import com.sap.sailing.domain.racelog.RaceLogFinishPositioningConfirmedEvent; +import com.sap.sailing.domain.racelog.RaceLogFinishPositioningListChangedEvent; +import com.sap.sailing.domain.racelog.RaceLogFlagEvent; +import com.sap.sailing.domain.racelog.RaceLogGateLineOpeningTimeEvent; +import com.sap.sailing.domain.racelog.RaceLogIdentifier; +import com.sap.sailing.domain.racelog.RaceLogPassChangeEvent; +import com.sap.sailing.domain.racelog.RaceLogPathfinderEvent; +import com.sap.sailing.domain.racelog.RaceLogProtestStartTimeEvent; +import com.sap.sailing.domain.racelog.RaceLogRaceStatusEvent; +import com.sap.sailing.domain.racelog.RaceLogStartProcedureChangedEvent; +import com.sap.sailing.domain.racelog.RaceLogStartTimeEvent; +import com.sap.sailing.domain.racelog.RaceLogWindFixEvent; +import com.sap.sailing.domain.racelog.RevokeEvent; +import com.sap.sailing.domain.racelog.tracking.CloseOpenEndedDeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DefineMarkEvent; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceCompetitorMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceMarkMappingEvent; +import com.sap.sailing.domain.racelog.tracking.RegisterCompetitorEvent; +import com.sap.sailing.domain.racelog.tracking.StartTrackingEvent; + +public class MongoRaceLogStoreVisitor implements RaceLogEventVisitor { + + private final RaceLogIdentifier raceLogIdentifier; + private final MongoObjectFactoryImpl mongoObjectFactory; + + final static Logger logger = Logger.getLogger(MongoRaceLogStoreVisitor.class.getName()); + + public MongoRaceLogStoreVisitor(RaceLogIdentifier identifier, MongoObjectFactory mongoObjectFactory) { + super(); + this.raceLogIdentifier = identifier; + this.mongoObjectFactory = (MongoObjectFactoryImpl) mongoObjectFactory; + } + + @Override + public void visit(RaceLogFlagEvent event) { + DBObject flagEventTrackEntry = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(flagEventTrackEntry); + } + + @Override + public void visit(RaceLogPassChangeEvent event) { + DBObject passChangeEventTrackEntry = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(passChangeEventTrackEntry); + } + + @Override + public void visit(RaceLogRaceStatusEvent event) { + DBObject raceStatusEventTrackEntry = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(raceStatusEventTrackEntry); + } + + @Override + public void visit(RaceLogStartTimeEvent event) { + DBObject startTimeEventTrackEntry = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(startTimeEventTrackEntry); + } + + @Override + public void visit(RaceLogCourseAreaChangedEvent event) { + DBObject courseAreaChangedEventTrackEntry = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(courseAreaChangedEventTrackEntry); + } + + @Override + public void visit(RaceLogCourseDesignChangedEvent event) { + DBObject courseDesignChangedEventTrackEntry = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(courseDesignChangedEventTrackEntry); + } + + @Override + public void visit(RaceLogFinishPositioningListChangedEvent event) { + DBObject finishPositioningListChangedEventTrackEntry = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(finishPositioningListChangedEventTrackEntry); + } + + @Override + public void visit(RaceLogFinishPositioningConfirmedEvent event) { + DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(object); + } + + @Override + public void visit(RaceLogPathfinderEvent event) { + DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(object); + } + + @Override + public void visit(RaceLogGateLineOpeningTimeEvent event) { + DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(object); + } + + @Override + public void visit(RaceLogStartProcedureChangedEvent event) { + DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(object); + } + + @Override + public void visit(RaceLogProtestStartTimeEvent event) { + DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(object); + } + + @Override + public void visit(RaceLogWindFixEvent event) { + DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(object); + } + + @Override + public void visit(DeviceCompetitorMappingEvent event) { + DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(object); + } + + @Override + public void visit(DeviceMarkMappingEvent event) { + DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(object); + } + + @Override + public void visit(DenoteForTrackingEvent event) { + DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(object); + } + + @Override + public void visit(StartTrackingEvent event) { + DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(object); + } + + @Override + public void visit(RevokeEvent event) { + DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(object); + } + + @Override + public void visit(RegisterCompetitorEvent event) { + DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(object); + } + + @Override + public void visit(DefineMarkEvent event) { + DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(object); + } + + @Override + public void visit(CloseOpenEndedDeviceMappingEvent event) { + DBObject object = mongoObjectFactory.storeRaceLogEntry(raceLogIdentifier, event); + mongoObjectFactory.storeRaceLogEventEvent(object); + } +} diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/PersistenceFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/PersistenceFactoryImpl.java index adfd2ce63e6..f6906190d71 100755 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/PersistenceFactoryImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/PersistenceFactoryImpl.java @@ -1,6 +1,7 @@ package com.sap.sailing.domain.persistence.impl; import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinderFactory; import com.sap.sailing.domain.persistence.DomainObjectFactory; import com.sap.sailing.domain.persistence.MongoObjectFactory; import com.sap.sailing.domain.persistence.PersistenceFactory; @@ -11,7 +12,6 @@ public class PersistenceFactoryImpl implements PersistenceFactory { private final MongoObjectFactory defaultMongoObjectFactory; public PersistenceFactoryImpl() { - super(); this.defaultDomainObjectFactory = new DomainObjectFactoryImpl(MongoDBService.INSTANCE.getDB(), DomainFactory.INSTANCE); this.defaultMongoObjectFactory = new MongoObjectFactoryImpl(MongoDBService.INSTANCE.getDB()); } @@ -31,14 +31,29 @@ public class PersistenceFactoryImpl implements PersistenceFactory { return new DomainObjectFactoryImpl(mongoDBService.getDB(), baseDomainFactory); } + @Override + public DomainObjectFactory getDomainObjectFactory(MongoDBService mongoDBService, DomainFactory baseDomainFactory, + TypeBasedServiceFinderFactory serviceFinderFactory) { + return new DomainObjectFactoryImpl(mongoDBService.getDB(), baseDomainFactory, serviceFinderFactory); + } + @Override public MongoObjectFactory getDefaultMongoObjectFactory() { return defaultMongoObjectFactory; } + + @Override + public MongoObjectFactory getDefaultMongoObjectFactory(TypeBasedServiceFinderFactory serviceFinderFactory) { + return new MongoObjectFactoryImpl(this.defaultMongoObjectFactory.getDatabase(), serviceFinderFactory); + } @Override public MongoObjectFactory getMongoObjectFactory(MongoDBService mongoDBService) { return new MongoObjectFactoryImpl(mongoDBService.getDB()); } + @Override + public MongoObjectFactory getMongoObjectFactory(MongoDBService mongoDBService, TypeBasedServiceFinderFactory serviceFinderFactory) { + return new MongoObjectFactoryImpl(mongoDBService.getDB(), serviceFinderFactory); + } } diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/DeviceIdentifierMongoHandler.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/DeviceIdentifierMongoHandler.java new file mode 100644 index 00000000000..13b310f404d --- /dev/null +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/DeviceIdentifierMongoHandler.java @@ -0,0 +1,6 @@ +package com.sap.sailing.domain.persistence.racelog.tracking; + +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifierSerializationHandler; + +public interface DeviceIdentifierMongoHandler extends DeviceIdentifierSerializationHandler { +} diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/GPSFixMongoHandler.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/GPSFixMongoHandler.java new file mode 100644 index 00000000000..4f84f42ff20 --- /dev/null +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/GPSFixMongoHandler.java @@ -0,0 +1,16 @@ +package com.sap.sailing.domain.persistence.racelog.tracking; + +import com.mongodb.DBObject; +import com.sap.sailing.domain.common.racelog.tracking.TransformationHandler; +import com.sap.sailing.domain.tracking.GPSFix; + +/** + * For GPS Fixes, the fully qualified class name is the type used to identify + * the handlers, as the fixes are domain objects and are not intended to carry + * some separate type attribute. + * + * @author Fredrik Teschke + * + */ +public interface GPSFixMongoHandler extends TransformationHandler { +} diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/MongoGPSFixStore.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/MongoGPSFixStore.java new file mode 100644 index 00000000000..8e6a07efb10 --- /dev/null +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/MongoGPSFixStore.java @@ -0,0 +1,7 @@ +package com.sap.sailing.domain.persistence.racelog.tracking; + +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; + +public interface MongoGPSFixStore extends GPSFixStore { + +} diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/MongoGPSFixStoreFactory.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/MongoGPSFixStoreFactory.java new file mode 100644 index 00000000000..071feef5ef5 --- /dev/null +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/MongoGPSFixStoreFactory.java @@ -0,0 +1,22 @@ +package com.sap.sailing.domain.persistence.racelog.tracking; + +import java.net.UnknownHostException; + +import com.mongodb.MongoException; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinderFactory; +import com.sap.sailing.domain.persistence.DomainObjectFactory; +import com.sap.sailing.domain.persistence.MongoObjectFactory; +import com.sap.sailing.domain.persistence.racelog.tracking.impl.MongoGPSFixStoreFactoryImpl; + +public interface MongoGPSFixStoreFactory { + MongoGPSFixStoreFactory INSTANCE = new MongoGPSFixStoreFactoryImpl(); + + /** + * Gets the default Mongo store based on the properties mongo.hostname[=localhost], mongo.port[=27017] + * and mongo.dbName[=<contents of constant {@link #DEFAULT_DB_NAME}>] specified in the bundle context + */ + MongoGPSFixStore getMongoGPSFixStore(MongoObjectFactory mongoObjectFactory, DomainObjectFactory domainObjectFactory, + TypeBasedServiceFinderFactory serviceFinderFactory) + throws UnknownHostException, MongoException; + +} \ No newline at end of file diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/GPSFixMongoHandlerImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/GPSFixMongoHandlerImpl.java new file mode 100644 index 00000000000..6f768e29d1f --- /dev/null +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/GPSFixMongoHandlerImpl.java @@ -0,0 +1,40 @@ +package com.sap.sailing.domain.persistence.racelog.tracking.impl; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.persistence.DomainObjectFactory; +import com.sap.sailing.domain.persistence.MongoObjectFactory; +import com.sap.sailing.domain.persistence.impl.DomainObjectFactoryImpl; +import com.sap.sailing.domain.persistence.impl.MongoObjectFactoryImpl; +import com.sap.sailing.domain.persistence.racelog.tracking.GPSFixMongoHandler; +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.domain.tracking.impl.GPSFixImpl; + +public class GPSFixMongoHandlerImpl implements GPSFixMongoHandler { + private final MongoObjectFactoryImpl mof; + private final DomainObjectFactoryImpl dof; + + public GPSFixMongoHandlerImpl(MongoObjectFactory mof, DomainObjectFactory dof) { + this.mof = (MongoObjectFactoryImpl) mof; + this.dof = (DomainObjectFactoryImpl) dof; + } + + @Override + public DBObject transformForth(GPSFix fix) throws IllegalArgumentException { + DBObject result = new BasicDBObject(); + mof.storeTimed(fix, result); + mof.storePositioned(fix, result); + return result; + } + + @Override + public GPSFix transformBack(DBObject object) { + DBObject dbObject = (DBObject) object; + TimePoint timePoint = dof.loadTimePoint(dbObject); + Position position = dof.loadPosition(dbObject); + return new GPSFixImpl(position, timePoint); + } + +} diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/GPSFixMovingMongoHandlerImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/GPSFixMovingMongoHandlerImpl.java new file mode 100644 index 00000000000..df778585c8a --- /dev/null +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/GPSFixMovingMongoHandlerImpl.java @@ -0,0 +1,43 @@ +package com.sap.sailing.domain.persistence.racelog.tracking.impl; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.persistence.DomainObjectFactory; +import com.sap.sailing.domain.persistence.MongoObjectFactory; +import com.sap.sailing.domain.persistence.impl.DomainObjectFactoryImpl; +import com.sap.sailing.domain.persistence.impl.MongoObjectFactoryImpl; +import com.sap.sailing.domain.persistence.racelog.tracking.GPSFixMongoHandler; +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.impl.GPSFixMovingImpl; + +public class GPSFixMovingMongoHandlerImpl implements GPSFixMongoHandler { + private final MongoObjectFactoryImpl mof; + private final DomainObjectFactoryImpl dof; + + public GPSFixMovingMongoHandlerImpl(MongoObjectFactory mof, DomainObjectFactory dof) { + this.mof = (MongoObjectFactoryImpl) mof; + this.dof = (DomainObjectFactoryImpl) dof; + } + + @Override + public DBObject transformForth(GPSFix fix) throws IllegalArgumentException { + DBObject result = new BasicDBObject(); + mof.storeTimed(fix, result); + mof.storePositioned(fix, result); + mof.storeSpeedWithBearing(((GPSFixMoving) fix).getSpeed(), result); + return result; + } + + @Override + public GPSFix transformBack(DBObject dbObject) { + TimePoint timePoint = dof.loadTimePoint(dbObject); + Position position = dof.loadPosition(dbObject); + SpeedWithBearing speed = dof.loadSpeedWithBearing(dbObject); + return new GPSFixMovingImpl(position, timePoint, speed); + } + +} diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/MongoGPSFixStoreFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/MongoGPSFixStoreFactoryImpl.java new file mode 100644 index 00000000000..25bc965db36 --- /dev/null +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/MongoGPSFixStoreFactoryImpl.java @@ -0,0 +1,19 @@ +package com.sap.sailing.domain.persistence.racelog.tracking.impl; + +import java.net.UnknownHostException; + +import com.mongodb.MongoException; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinderFactory; +import com.sap.sailing.domain.persistence.DomainObjectFactory; +import com.sap.sailing.domain.persistence.MongoObjectFactory; +import com.sap.sailing.domain.persistence.racelog.tracking.MongoGPSFixStore; +import com.sap.sailing.domain.persistence.racelog.tracking.MongoGPSFixStoreFactory; + +public class MongoGPSFixStoreFactoryImpl implements MongoGPSFixStoreFactory { + @Override + public MongoGPSFixStore getMongoGPSFixStore(MongoObjectFactory mongoObjectFactory, + DomainObjectFactory domainObjectFactory, TypeBasedServiceFinderFactory serviceFinderFactory) + throws UnknownHostException, MongoException { + return new MongoGPSFixStoreImpl(mongoObjectFactory, domainObjectFactory, serviceFinderFactory); + } +} diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/MongoGPSFixStoreImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/MongoGPSFixStoreImpl.java new file mode 100644 index 00000000000..6d3680cb8c3 --- /dev/null +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/MongoGPSFixStoreImpl.java @@ -0,0 +1,165 @@ +package com.sap.sailing.domain.persistence.racelog.tracking.impl; + +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.mongodb.BasicDBObjectBuilder; +import com.mongodb.DBCollection; +import com.mongodb.DBCursor; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.racelog.tracking.NoCorrespondingServiceRegisteredException; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinder; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinderFactory; +import com.sap.sailing.domain.persistence.DomainObjectFactory; +import com.sap.sailing.domain.persistence.MongoObjectFactory; +import com.sap.sailing.domain.persistence.impl.FieldNames; +import com.sap.sailing.domain.persistence.impl.MongoObjectFactoryImpl; +import com.sap.sailing.domain.persistence.racelog.tracking.DeviceIdentifierMongoHandler; +import com.sap.sailing.domain.persistence.racelog.tracking.GPSFixMongoHandler; +import com.sap.sailing.domain.persistence.racelog.tracking.MongoGPSFixStore; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.DeviceMapping; +import com.sap.sailing.domain.racelog.tracking.GPSFixReceivedListener; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.DeviceCompetitorMappingFinder; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.DeviceMarkMappingFinder; +import com.sap.sailing.domain.tracking.DynamicGPSFixTrack; +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.domain.tracking.GPSFixMoving; + +public class MongoGPSFixStoreImpl implements MongoGPSFixStore { + private static final Logger logger = Logger.getLogger(MongoGPSFixStore.class.getName()); + private final TypeBasedServiceFinder fixServiceFinder; + private final TypeBasedServiceFinder deviceServiceFinder; + private final DBCollection collection; + private final MongoObjectFactoryImpl mongoOF; + private final ConcurrentHashMap listeners = new ConcurrentHashMap<>(); + + public MongoGPSFixStoreImpl(MongoObjectFactory mongoObjectFactory, + DomainObjectFactory domainObjectFactory, TypeBasedServiceFinderFactory serviceFinderFactory) { + mongoOF = (MongoObjectFactoryImpl) mongoObjectFactory; + if (serviceFinderFactory != null) { + fixServiceFinder = serviceFinderFactory.createServiceFinder(GPSFixMongoHandler.class); + deviceServiceFinder = serviceFinderFactory.createServiceFinder(DeviceIdentifierMongoHandler.class); + } else { + fixServiceFinder = null; + deviceServiceFinder = null; + } + collection = mongoOF.getGPSFixCollection(); + } + + private GPSFix loadGPSFix(DBObject object) throws TransformationException, NoCorrespondingServiceRegisteredException { + String type = (String) object.get(FieldNames.GPSFIX_TYPE.name()); + DBObject fixObject = (DBObject) object.get(FieldNames.GPSFIX.name()); + return fixServiceFinder.findService(type).transformBack(fixObject); + } + + private void loadTrack(DynamicGPSFixTrack track, DeviceIdentifier device, + TimePoint from, TimePoint to, boolean inclusive) throws NoCorrespondingServiceRegisteredException { + long fromMillis = from.asMillis() - (inclusive ? 1 : 0); + long toMillis = to.asMillis() + (inclusive ? 1 : 0); + + Object dbDeviceId = null; + try { + dbDeviceId = MongoObjectFactoryImpl.storeDeviceId(deviceServiceFinder, device); + } catch (TransformationException | NoCorrespondingServiceRegisteredException e) { + logger.log(Level.WARNING, "Could not serialize Device ID for MongoDB: " + e.getMessage()); + return; + } + + DBObject query = QueryBuilder.start(FieldNames.DEVICE_ID.name()).is(dbDeviceId) + .and(FieldNames.TIME_AS_MILLIS.name()).greaterThan(fromMillis) + .and(FieldNames.TIME_AS_MILLIS.name()).lessThan(toMillis).get(); + + DBCursor result = collection.find(query); + for (DBObject fixObject : result) { + try { + @SuppressWarnings("unchecked") + FixT fix = (FixT) loadGPSFix(fixObject); + track.add(fix); + } catch (TransformationException e) { + logger.log(Level.WARNING, "Could not read fix from MongoDB: " + fixObject); + } catch (ClassCastException e) { + String type = (String) fixObject.get(FieldNames.GPSFIX_TYPE.name()); + logger.log(Level.WARNING, "Unexpected fix type (" + type + ") encountered when trying to load track for " + track.getTrackedItem()); + } + } + } + + @Override + public void loadCompetitorTrack(DynamicGPSFixTrack track, RaceLog raceLog, Competitor competitor) { + List> mappings = new DeviceCompetitorMappingFinder(raceLog).analyze().get(competitor); + + if (mappings != null) { + for (DeviceMapping mapping : mappings) { + loadTrack(track, mapping.getDevice(), mapping.getTimeRange().from(), mapping.getTimeRange().to(), true /*inclusive*/); + } + } + } + + @Override + public void loadMarkTrack(DynamicGPSFixTrack track, RaceLog raceLog, Mark mark) { + List> mappings = new DeviceMarkMappingFinder(raceLog).analyze().get(mark); + + if (mappings != null) { + for (DeviceMapping mapping : mappings) { + loadTrack(track, mapping.getDevice(), mapping.getTimeRange().from(), mapping.getTimeRange().to(), true /*inclusive*/); + } + } + } + + @Override + public void storeFix(DeviceIdentifier device, GPSFix fix) { + try { + Object dbDeviceId = MongoObjectFactoryImpl.storeDeviceId(deviceServiceFinder, device); + String type = fix.getClass().getName(); + + Object fixObject = fixServiceFinder.findService(type).transformForth(fix); + DBObject entry = new BasicDBObjectBuilder() + .add(FieldNames.DEVICE_ID.name(), dbDeviceId) + .add(FieldNames.GPSFIX_TYPE.name(), type) + .add(FieldNames.GPSFIX.name(), fixObject).get(); + mongoOF.storeTimed(fix, entry); + + collection.insert(entry); + } catch (TransformationException e) { + logger.log(Level.WARNING, "Could not store fix in MongoDB"); + e.printStackTrace(); + } + + notifyListeners(device, fix); + } + + private void notifyListeners(DeviceIdentifier device, GPSFix fix) { + for (GPSFixReceivedListener listener : listeners.values()) { + listener.fixReceived(device, fix); + } + } + + @Override + public void addListener(GPSFixReceivedListener listener) { + listeners.put(listener.hashCode(), listener); + } + + @Override + public void removeListener(GPSFixReceivedListener listener) { + listeners.remove(listener.hashCode()); + } + + @Override + public void loadCompetitorTrack(DynamicGPSFixTrack track, DeviceMapping mapping) { + loadTrack(track, mapping.getDevice(), mapping.getTimeRange().from(), mapping.getTimeRange().to(), true /*inclusive*/); + } + + @Override + public void loadMarkTrack(DynamicGPSFixTrack track, DeviceMapping mapping) { + loadTrack(track, mapping.getDevice(), mapping.getTimeRange().from(), mapping.getTimeRange().to(), true /*inclusive*/); + } +} diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/PlaceHolderDeviceIdentifierMongoHandler.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/PlaceHolderDeviceIdentifierMongoHandler.java new file mode 100644 index 00000000000..5657f067858 --- /dev/null +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/racelog/tracking/impl/PlaceHolderDeviceIdentifierMongoHandler.java @@ -0,0 +1,17 @@ +package com.sap.sailing.domain.persistence.racelog.tracking.impl; + +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.persistence.racelog.tracking.DeviceIdentifierMongoHandler; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.impl.PlaceHolderDeviceIdentifierSerializationHandler; + +public class PlaceHolderDeviceIdentifierMongoHandler extends PlaceHolderDeviceIdentifierSerializationHandler +implements DeviceIdentifierMongoHandler { + + @Override + public DeviceIdentifier deserialize(Object serialized, String type, String stringRepresentation) + throws TransformationException { + return deserialize((String) serialized, type, stringRepresentation); + } + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/.classpath b/java/com.sap.sailing.domain.racelogtrackingadapter.test/.classpath new file mode 100644 index 00000000000..e8ea977a694 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/.gitignore b/java/com.sap.sailing.domain.racelogtrackingadapter.test/.gitignore new file mode 100644 index 00000000000..ba077a4031a --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/.gitignore @@ -0,0 +1 @@ +bin diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/.project b/java/com.sap.sailing.domain.racelogtrackingadapter.test/.project new file mode 100644 index 00000000000..30f37c10090 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/.project @@ -0,0 +1,28 @@ + + + com.sap.sailing.domain.racelogtrackingadapter.test + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/.settings/org.eclipse.jdt.core.prefs b/java/com.sap.sailing.domain.racelogtrackingadapter.test/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..eb01f948334 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +#Thu Jan 19 09:52:18 CET 2012 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/.settings/org.eclipse.pde.core.prefs b/java/com.sap.sailing.domain.racelogtrackingadapter.test/.settings/org.eclipse.pde.core.prefs new file mode 100644 index 00000000000..82689ad754c --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/.settings/org.eclipse.pde.core.prefs @@ -0,0 +1,4 @@ +#Thu Aug 25 17:11:24 CEST 2011 +eclipse.preferences.version=1 +pluginProject.extensions=false +resolve.requirebundle=false diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.racelogtrackingadapter.test/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..a6166acc12a --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/META-INF/MANIFEST.MF @@ -0,0 +1,19 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Smartphone Adapter Test +Bundle-SymbolicName: com.sap.sailing.domain.racelogtrackingadapter.test +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: SAP +Bundle-RequiredExecutionEnvironment: JavaSE-1.7 +Fragment-Host: com.sap.sailing.domain.racelogtrackingadapter +Require-Bundle: org.junit4;bundle-version="4.8.2", + com.sap.sailing.domain.racelogtrackingadapter.testsupport, + org.mockito.mockito-core;bundle-version="1.9.5", + org.objenesis;bundle-version="1.3.0", + org.hamcrest;bundle-version="1.1.0", + com.mongodb.driver;bundle-version="2.6.2" +Import-Package: com.sap.sailing.domain.persistence.impl, + com.sap.sailing.domain.persistence.racelog.tracking.impl, + com.sap.sailing.domain.tractracadapter, + com.sap.sailing.server.impl, + javax.servlet.http;version="2.5.0" diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/build.properties b/java/com.sap.sailing.domain.racelogtrackingadapter.test/build.properties new file mode 100644 index 00000000000..6b53438d1ce --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + ., diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/pom.xml b/java/com.sap.sailing.domain.racelogtrackingadapter.test/pom.xml new file mode 100644 index 00000000000..ebb5dc2927c --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sailing.domain.racelogtrackingadapter.test + eclipse-test-plugin + diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/.gitignore b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/AbstractGPSFixStoreTest.java b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/AbstractGPSFixStoreTest.java new file mode 100644 index 00000000000..c117cc83f2c --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/AbstractGPSFixStoreTest.java @@ -0,0 +1,82 @@ +package com.sap.sailing.domain.racelogtracking.test; + +import static com.sap.sailing.domain.common.impl.Util.size; +import static junit.framework.Assert.assertEquals; + +import org.junit.After; +import org.junit.Before; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.persistence.impl.MongoObjectFactoryImpl; +import com.sap.sailing.domain.persistence.racelog.tracking.impl.MongoGPSFixStoreImpl; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventFactory; +import com.sap.sailing.domain.racelog.impl.RaceLogEventAuthorImpl; +import com.sap.sailing.domain.racelog.impl.RaceLogImpl; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; +import com.sap.sailing.domain.racelog.tracking.test.mock.MockDeviceAndSessioinIdentifierWithGPSFixesDeserializer; +import com.sap.sailing.domain.racelog.tracking.test.mock.MockSmartphoneImeiServiceFinderFactory; +import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifier; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.Track; +import com.sap.sailing.domain.tracking.impl.GPSFixMovingImpl; +import com.sap.sailing.server.RacingEventService; +import com.sap.sailing.server.gateway.deserialization.impl.DeviceAndSessionIdentifierWithGPSFixesDeserializer; +import com.sap.sailing.server.impl.RacingEventServiceImpl; + +public class AbstractGPSFixStoreTest { + protected RacingEventService service; + protected final MockSmartphoneImeiServiceFinderFactory serviceFinderFactory = new MockSmartphoneImeiServiceFinderFactory(); + DeviceAndSessionIdentifierWithGPSFixesDeserializer deserializer = + new MockDeviceAndSessioinIdentifierWithGPSFixesDeserializer(); + protected final DeviceIdentifier device = new SmartphoneImeiIdentifier("a"); + protected RaceLog raceLog; + protected GPSFixStore store; + protected final Competitor comp = DomainFactory.INSTANCE.getOrCreateCompetitor("comp", "comp", null, null, null); + protected final Mark mark = DomainFactory.INSTANCE.getOrCreateMark("mark"); + + private final RaceLogEventAuthor author = new RaceLogEventAuthorImpl("author", 0); + + protected GPSFixMoving createFix(long millis, double lat, double lng, double knots, double degrees) { + return new GPSFixMovingImpl(new DegreePosition(lat, lng), + new MillisecondsTimePoint(millis), new KnotSpeedWithBearingImpl(knots, new DegreeBearingImpl(degrees))); + } + + @Before + public void setServiceAndRaceLog() { + service = new RacingEventServiceImpl(null, null, serviceFinderFactory); + raceLog = new RaceLogImpl("racelog"); + store = new MongoGPSFixStoreImpl(service.getMongoObjectFactory(), service.getDomainObjectFactory(), + serviceFinderFactory); + } + + @After + public void after() { + MongoObjectFactoryImpl mongoOF = (MongoObjectFactoryImpl) service.getMongoObjectFactory(); + mongoOF.getGPSFixCollection().drop(); + } + + protected void map(Competitor comp, DeviceIdentifier device, long from, long to) { + raceLog.add(RaceLogEventFactory.INSTANCE.createDeviceCompetitorMappingEvent(MillisecondsTimePoint.now(), author, device, + comp, 0, new MillisecondsTimePoint(from), new MillisecondsTimePoint(to))); + } + + protected void map(Mark mark, DeviceIdentifier device, long from, long to) { + raceLog.add(RaceLogEventFactory.INSTANCE.createDeviceMarkMappingEvent(MillisecondsTimePoint.now(), author, device, + mark, 0, new MillisecondsTimePoint(from), new MillisecondsTimePoint(to))); + } + + protected void testLength(Track track, long expected) { + track.lockForRead(); + assertEquals(expected, size(track.getRawFixes())); + track.unlockAfterRead(); + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/AbstractJsonOverHttpTest.java b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/AbstractJsonOverHttpTest.java new file mode 100644 index 00000000000..0014db454a9 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/AbstractJsonOverHttpTest.java @@ -0,0 +1,76 @@ +package com.sap.sailing.domain.racelogtracking.test; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; + +import org.json.simple.JSONValue; +import org.json.simple.parser.ParseException; + +public abstract class AbstractJsonOverHttpTest { + protected static final String URL_BASE = "127.0.0.1"; + protected static final int PORT = 8888; + protected static final String URL_SS = "/sailingserver"; + protected static final String URL_TR = "/tracking"; + protected static final String URL_RF = "/recordFixes"; + + protected String getUrl(String endOfUrl) { + return "http://" + URL_BASE + ":" + PORT + endOfUrl; + } + + protected String executeRequest(String method, String targetURL, String body) throws IOException { + // Create connection + URL url = new URL(targetURL); + System.out.println(url); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod(method); + connection.setRequestProperty("Content-Type", "application/json"); + + connection.setUseCaches(false); + connection.setDoInput(true); + + // Send request + if (method.equals("POST")) { + connection.setDoOutput(true); + DataOutputStream wr = new DataOutputStream(connection.getOutputStream()); + wr.writeBytes(body); + } + + InputStream is = null; + boolean error = false; + if (connection.getResponseCode() >= 400) { + is = connection.getErrorStream(); + error = true; + } else { + is = connection.getInputStream(); + } + + if (is == null) { + return ""; + } + + BufferedReader rd = new BufferedReader(new InputStreamReader(is)); + String line; + StringBuffer response = new StringBuffer(); + while ((line = rd.readLine()) != null) { + response.append(line); + response.append('\r'); + } + rd.close(); + if (error) { + System.err.println(response.toString()); + return ""; + } + + return response.toString(); + } + + protected Object getJson(String url) throws IllegalStateException, IOException, ParseException { + return JSONValue.parseWithException(executeRequest("GET", url, "")); + } + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/CreateAndTrackWithRaceLogTest.java b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/CreateAndTrackWithRaceLogTest.java new file mode 100644 index 00000000000..b562db1e9ce --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/CreateAndTrackWithRaceLogTest.java @@ -0,0 +1,159 @@ +package com.sap.sailing.domain.racelogtracking.test.impl; + +import static com.sap.sailing.domain.common.impl.Util.size; +import static junit.framework.Assert.assertNotNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.FileNotFoundException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CourseBase; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.impl.CourseDataImpl; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.base.impl.MarkImpl; +import com.sap.sailing.domain.base.impl.SeriesImpl; +import com.sap.sailing.domain.base.impl.WaypointImpl; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.racelog.tracking.RaceNotCreatedException; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.leaderboard.impl.HighPoint; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventFactory; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; +import com.sap.sailing.domain.racelog.tracking.test.mock.MockSmartphoneImeiServiceFinderFactory; +import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifier; +import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapter; +import com.sap.sailing.domain.racelogtracking.impl.RaceLogTrackingAdapterFactoryImpl; +import com.sap.sailing.domain.tracking.Track; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.domain.tracking.impl.GPSFixMovingImpl; +import com.sap.sailing.server.RacingEventService; +import com.sap.sailing.server.impl.RacingEventServiceImpl; + +public class CreateAndTrackWithRaceLogTest { + private RacingEventService service; + + private final Fleet fleet = new FleetImpl("fleet"); + private final String columnName = "column"; + private RegattaLeaderboard leaderboard; + private RaceLogTrackingAdapter adapter; + private Regatta regatta; + private RaceLogEventAuthor author; + private GPSFixStore gpsFixStore; + + private long time = 0; + + private static final RaceLogEventFactory factory = RaceLogEventFactory.INSTANCE; + + @Before + public void setup() { + service = new RacingEventServiceImpl(true, new MockSmartphoneImeiServiceFinderFactory()); + gpsFixStore = service.getGPSFixStore(); + service.getMongoObjectFactory().getDatabase().dropDatabase(); + author = service.getServerAuthor(); + Series series = new SeriesImpl("series", false, Collections.singletonList(fleet), + Collections.singletonList(columnName), service); + regatta = service.createRegatta("regatta", "Laser", UUID.randomUUID(), Collections.singletonList(series), + false, new HighPoint(), UUID.randomUUID()); + leaderboard = service.addRegattaLeaderboard(regatta.getRegattaIdentifier(), "RegattaLeaderboard", new int[] {}); + adapter = RaceLogTrackingAdapterFactoryImpl.INSTANCE.getAdapter(DomainFactory.INSTANCE); + } + + @Test + public void hasRaceLog() { + assertNotNull(leaderboard.getRaceColumnByName(columnName).getRaceLog(fleet)); + } + + private TimePoint t() { + return new MillisecondsTimePoint(time++); + } + + @Rule + public ExpectedException exception = ExpectedException.none(); + @Test + public void cantAddBeforeDenoting() throws MalformedURLException, FileNotFoundException, URISyntaxException, Exception { + RaceColumn column = leaderboard.getRaceColumnByName(columnName); + exception.expect(RaceNotCreatedException.class); + adapter.startTracking(service, leaderboard, column, fleet); + } + + private void testSize(Track track, int expected) { + track.lockForRead(); + assertEquals(expected, size(track.getRawFixes())); + track.unlockAfterRead(); + } + + @Test + public void canDenote_Add_Track() throws MalformedURLException, FileNotFoundException, URISyntaxException, Exception { + + RaceColumn column = leaderboard.getRaceColumnByName(columnName); + RaceLog raceLog = column.getRaceLog(fleet); + + //can denote racelog for tracking + assertTrue(raceLog.isEmpty()); + adapter.denoteRaceForRaceLogTracking(service, leaderboard, column, fleet, "race"); + assertFalse(raceLog.isEmpty()); + + //add a mapping and one fix in, one out of mapping + Competitor comp1 = DomainFactory.INSTANCE.getOrCreateCompetitor("comp1", "comp1", null, null, null); + DeviceIdentifier dev1 = new SmartphoneImeiIdentifier("dev1"); + raceLog.add(factory.createDeviceCompetitorMappingEvent(t(), author, dev1, comp1, 0, new MillisecondsTimePoint(0), new MillisecondsTimePoint(10))); + gpsFixStore.storeFix(dev1, new GPSFixMovingImpl(new DegreePosition(0, 0), new MillisecondsTimePoint(5), new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(5)))); + gpsFixStore.storeFix(dev1, new GPSFixMovingImpl(new DegreePosition(0, 0), new MillisecondsTimePoint(15), new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(5)))); + + CourseBase course = new CourseDataImpl("test"); + course.addWaypoint(0, new WaypointImpl(new MarkImpl("mark"))); + raceLog.add(factory.createCourseDesignChangedEvent(t(), author, 0, course)); + raceLog.add(factory.createRegisterCompetitorEvent(t(), author, 0, comp1)); + + //start tracking + adapter.startTracking(service, leaderboard, column, fleet); + + //now there is a trackedrace + TrackedRace race = column.getTrackedRace(fleet); + assertNotNull(race); + + //one fix should have been loaded from store + testSize(race.getTrack(comp1), 1); + + //further fix arrives in race + gpsFixStore.storeFix(dev1, new GPSFixMovingImpl(new DegreePosition(0, 0), new MillisecondsTimePoint(7), new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(5)))); + gpsFixStore.storeFix(dev1, new GPSFixMovingImpl(new DegreePosition(0, 0), new MillisecondsTimePoint(14), new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(5)))); //outside mapping range + testSize(race.getTrack(comp1), 2); + + //add another mapping on the fly, other old fixes should be loaded + raceLog.add(factory.createDeviceCompetitorMappingEvent(t(), author, dev1, comp1, 0, new MillisecondsTimePoint(11), new MillisecondsTimePoint(20))); + testSize(race.getTrack(comp1), 4); + + //add another fix in new mapping range + gpsFixStore.storeFix(dev1, new GPSFixMovingImpl(new DegreePosition(0, 0), new MillisecondsTimePoint(18), new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(5)))); + testSize(race.getTrack(comp1), 5); + + //stop tracking, then no more fixes arrive at race + service.getRaceTrackerById(raceLog.getId()).stop(); + gpsFixStore.storeFix(dev1, new GPSFixMovingImpl(new DegreePosition(0, 0), new MillisecondsTimePoint(8), new KnotSpeedWithBearingImpl(10, new DegreeBearingImpl(5)))); + testSize(race.getTrack(comp1), 5); + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/RecordFixesTest.java b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/RecordFixesTest.java new file mode 100644 index 00000000000..afc8c4599a6 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/RecordFixesTest.java @@ -0,0 +1,64 @@ +package com.sap.sailing.domain.racelogtracking.test.impl; + +import static com.sap.sailing.domain.common.impl.Util.size; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.common.impl.Util.Triple; +import com.sap.sailing.domain.common.racelog.tracking.NoCorrespondingServiceRegisteredException; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.test.mock.MockDeviceAndSessioinIdentifierWithGPSFixesDeserializer; +import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifier; +import com.sap.sailing.domain.racelogtracking.servlet.RecordFixesPostServlet; +import com.sap.sailing.domain.racelogtracking.test.AbstractGPSFixStoreTest; +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.impl.DynamicGPSFixMovingTrackImpl; +import com.sap.sailing.server.gateway.deserialization.impl.DeviceAndSessionIdentifierWithGPSFixesDeserializer; + +public class RecordFixesTest extends AbstractGPSFixStoreTest { + private RecordFixesPostServlet servlet; + private final DeviceAndSessionIdentifierWithGPSFixesDeserializer deserializer = + new MockDeviceAndSessioinIdentifierWithGPSFixesDeserializer(); + private final DeviceIdentifier device = new SmartphoneImeiIdentifier("a"); + + @Before + public void setupServlet() { + servlet = new RecordFixesPostServlet(); + servlet = spy(servlet); + when(servlet.getRequestDeserializer()).thenReturn(deserializer); + doReturn(service).when(servlet).getService(); + } + + @Test + public void areFixesStoredInDb() throws TransformationException, NoCorrespondingServiceRegisteredException { + map(comp, device, 0, 600); + + int timepoint = 343; + List fixes = new ArrayList<>(); + fixes.add(createFix(timepoint, 0, 0, 0, 0)); + fixes.add(createFix(timepoint+1, 0, 0, 0, 0)); + Triple> data = new Triple<>(device, null, fixes); + servlet.process(null, data); + + DynamicGPSFixMovingTrackImpl track = new DynamicGPSFixMovingTrackImpl<>(comp, 0); + store.loadCompetitorTrack(track, raceLog, comp); + track.lockForRead(); + assertEquals(2, size(track.getRawFixes())); + assertEquals(timepoint, track.getFirstRawFix().getTimePoint().asMillis()); + assertTrue(track.getFirstRawFix() instanceof GPSFixMoving); + track.unlockAfterRead(); + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/TrackedRaceLoadsFixesTest.java b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/TrackedRaceLoadsFixesTest.java new file mode 100644 index 00000000000..51c27045937 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/impl/TrackedRaceLoadsFixesTest.java @@ -0,0 +1,67 @@ +package com.sap.sailing.domain.racelogtracking.test.impl; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Course; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Sideline; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.base.impl.CourseImpl; +import com.sap.sailing.domain.base.impl.RaceDefinitionImpl; +import com.sap.sailing.domain.base.impl.RegattaImpl; +import com.sap.sailing.domain.base.impl.WaypointImpl; +import com.sap.sailing.domain.common.racelog.tracking.NoCorrespondingServiceRegisteredException; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifier; +import com.sap.sailing.domain.racelogtracking.test.AbstractGPSFixStoreTest; +import com.sap.sailing.domain.tracking.TrackedRegatta; +import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl; +import com.sap.sailing.domain.tracking.impl.EmptyWindStore; +import com.sap.sailing.domain.tracking.impl.TrackedRegattaImpl; + +public class TrackedRaceLoadsFixesTest extends AbstractGPSFixStoreTest { + @Test + public void areFixesStoredInDb() throws TransformationException, NoCorrespondingServiceRegisteredException, InterruptedException { + Competitor comp2 = DomainFactory.INSTANCE.getOrCreateCompetitor("comp2", "comp2", null, null, null); + Mark mark2 = DomainFactory.INSTANCE.getOrCreateMark("mark2"); + DeviceIdentifier device2 = new SmartphoneImeiIdentifier("imei2"); + DeviceIdentifier device3 = new SmartphoneImeiIdentifier("imei3"); + BoatClass boatClass = DomainFactory.INSTANCE.getOrCreateBoatClass("49er"); + + Course course = new CourseImpl("course", Arrays.asList(new Waypoint[] {new WaypointImpl(mark), new WaypointImpl(mark2)})); + RaceDefinition race = new RaceDefinitionImpl("race", course, boatClass, Arrays.asList(new Competitor[] {comp, comp2})); + + map(comp, device, 0, 600); + map(comp2, device2, 0, 600); + //reuse device for two marks + map(mark, device3, 0, 600); + map(mark2, device3, 0, 600); + + store.storeFix(device, createFix(100, 10, 20, 30, 40)); + store.storeFix(device, createFix(101, 10, 20, 30, 40)); + store.storeFix(device2, createFix(100, 10, 20, 30, 40)); + store.storeFix(device3, createFix(100, 10, 20, 30, 40)); + store.storeFix(device3, createFix(100, 10, 20, 30, 40)); + + TrackedRegatta regatta = new TrackedRegattaImpl(new RegattaImpl(EmptyRaceLogStore.INSTANCE, "regatta", boatClass, null, null, "a", null)); + DynamicTrackedRaceImpl trackedRace = new DynamicTrackedRaceImpl(regatta, race, Collections.emptyList(), + EmptyWindStore.INSTANCE, store, 0, 0, 0); + trackedRace.attachRaceLog(raceLog); + trackedRace.waitUntilNotLoading(); + trackedRace.waitUntilLoadingFromGPSFixStoreComplete(); + + testLength(trackedRace.getTrack(comp), 2); + testLength(trackedRace.getTrack(comp2), 1); + testLength(trackedRace.getOrCreateTrack(mark), 1); + testLength(trackedRace.getOrCreateTrack(mark2), 1); + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/manual/PostFixes.java b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/manual/PostFixes.java new file mode 100644 index 00000000000..505b6ff9b37 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.test/src/com/sap/sailing/domain/racelogtracking/test/manual/PostFixes.java @@ -0,0 +1,107 @@ +package com.sap.sailing.domain.racelogtracking.test.manual; + +import java.io.IOException; +import java.io.Serializable; +import java.text.ParseException; +import java.util.Arrays; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; + +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.impl.Util.Triple; +import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifier; +import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiJsonHandler; +import com.sap.sailing.domain.racelogtracking.test.AbstractJsonOverHttpTest; +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.impl.GPSFixImpl; +import com.sap.sailing.domain.tracking.impl.GPSFixMovingImpl; +import com.sap.sailing.server.gateway.serialization.impl.DeviceAndSessionIdentifierWithGPSFixesSerializer; +import com.sap.sailing.server.gateway.serialization.impl.DeviceIdentifierJsonSerializer; +import com.sap.sailing.server.gateway.serialization.impl.GPSFixMovingNmeaDTOJsonSerializer; + +public class PostFixes extends AbstractJsonOverHttpTest { + protected static final SmartphoneImeiIdentifier device = new SmartphoneImeiIdentifier("a"); + + private final DeviceAndSessionIdentifierWithGPSFixesSerializer fixSerializer = + new DeviceAndSessionIdentifierWithGPSFixesSerializer<>(DeviceIdentifierJsonSerializer.create( + new SmartphoneImeiJsonHandler(), SmartphoneImeiIdentifier.TYPE), new GPSFixMovingNmeaDTOJsonSerializer()); + + private void recordFix(GPSFixMoving... fix) throws IOException { + String request = fixSerializer.serialize( + new Triple>( + device, null, Arrays.asList(fix))).toJSONString(); + + executeRequest("POST", getUrl(URL_TR + "/recordFixes"), request); + } + + private void recordFixes() { + final Timer timer = new Timer(); + final Position p1 = new DegreePosition(54.427119, 10.182583); + final Position p2 = new DegreePosition(54.435707, 10.200779); + final Position p3 = new DegreePosition(54.425921, 10.188248); + + final int nFirstLeg = 100; + final int nSecondLeg = 100; + final int n = nFirstLeg + nSecondLeg; + long period = 1000; + + timer.scheduleAtFixedRate(new TimerTask() { + int i = 0; + GPSFix lastFix; + + @Override + public void run() { + if (i++ > n) + timer.cancel(); + + Position start; + Position end; + int iLeg = i; + int nLeg; + + if (i < nFirstLeg) { + start = p1; + end = p2; + nLeg = nFirstLeg; + } else { + start = p2; + end = p3; + iLeg -= nFirstLeg; + nLeg = nSecondLeg; + } + + Distance distance = start.getDistance(end); + Bearing bearing = start.getBearingGreatCircle(end); + Position boat = start.translateGreatCircle(bearing, distance.scale(((double) iLeg) / nLeg)); + GPSFix fix = new GPSFixImpl(boat, MillisecondsTimePoint.now()); + SpeedWithBearing speed = lastFix == null ? new KnotSpeedWithBearingImpl(0, new DegreeBearingImpl(0)) + : lastFix.getSpeedAndBearingRequiredToReach(fix); + lastFix = fix; + + try { + recordFix(new GPSFixMovingImpl(fix.getPosition(), fix.getTimePoint(), speed)); + } catch (IOException e) { + e.printStackTrace(); + } + } + }, 0, period); + } + + public void run() throws IOException, IllegalStateException, ParseException, InterruptedException { + recordFixes(); + } + + public static void main(String[] args) throws IllegalStateException, IOException, ParseException, + InterruptedException { + new PostFixes().run(); + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/.classpath b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/.classpath new file mode 100644 index 00000000000..4c62a8048e6 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/.project b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/.project new file mode 100644 index 00000000000..f3c0beddc15 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/.project @@ -0,0 +1,28 @@ + + + com.sap.sailing.domain.racelogtrackingadapter.testsupport + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/java/com.sap.sailing.gwt.ui.test/.settings/org.eclipse.jdt.core.prefs b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/.settings/org.eclipse.jdt.core.prefs old mode 100755 new mode 100644 similarity index 90% rename from java/com.sap.sailing.gwt.ui.test/.settings/org.eclipse.jdt.core.prefs rename to java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/.settings/org.eclipse.jdt.core.prefs index 48319e0614a..c537b63063c --- a/java/com.sap.sailing.gwt.ui.test/.settings/org.eclipse.jdt.core.prefs +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/.settings/org.eclipse.jdt.core.prefs @@ -1,8 +1,7 @@ -#Wed Nov 02 13:57:42 CET 2011 -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/.settings/org.eclipse.pde.core.prefs b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/.settings/org.eclipse.pde.core.prefs new file mode 100644 index 00000000000..f29e940a005 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/.settings/org.eclipse.pde.core.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +pluginProject.extensions=false +resolve.requirebundle=false diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..8fdb50d5cc4 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/META-INF/MANIFEST.MF @@ -0,0 +1,19 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Testsupport +Bundle-SymbolicName: com.sap.sailing.domain.racelogtrackingadapter.testsupport +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: SAP +Bundle-RequiredExecutionEnvironment: JavaSE-1.6 +Require-Bundle: com.sap.sailing.domain.shared.android, + com.sap.sailing.domain, + com.sap.sailing.domain.common, + com.mongodb.driver, + com.sap.sailing.domain.persistence, + com.sap.sailing.domain.racelogtrackingadapter, + com.sap.sailing.server.gateway.serialization;bundle-version="1.0.0", + org.json.simple;bundle-version="1.1.0", + org.mockito.mockito-core;bundle-version="1.9.5", + org.hamcrest;bundle-version="1.1.0", + org.junit4;bundle-version="4.8.2" +Export-Package: com.sap.sailing.domain.racelog.tracking.test.mock diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/build.properties b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/build.properties new file mode 100644 index 00000000000..34d2e4d2dad --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/pom.xml b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/pom.xml new file mode 100644 index 00000000000..d7d10b95748 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sailing.domain.racelogtrackingadapter.testsupport + eclipse-plugin + diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockDeviceAndSessioinIdentifierWithGPSFixesDeserializer.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockDeviceAndSessioinIdentifierWithGPSFixesDeserializer.java new file mode 100644 index 00000000000..8d2b930fbe6 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockDeviceAndSessioinIdentifierWithGPSFixesDeserializer.java @@ -0,0 +1,14 @@ +package com.sap.sailing.domain.racelog.tracking.test.mock; + +import com.sap.sailing.server.gateway.deserialization.impl.DeviceAndSessionIdentifierWithGPSFixesDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.DeviceIdentifierJsonDeserializer; + +public class MockDeviceAndSessioinIdentifierWithGPSFixesDeserializer extends +DeviceAndSessionIdentifierWithGPSFixesDeserializer { + + public MockDeviceAndSessioinIdentifierWithGPSFixesDeserializer() { + super(new MockGPSFixJsonServiceFinder(), + DeviceIdentifierJsonDeserializer.create(new SmartphoneImeiJsonHandler(), SmartphoneImeiIdentifier.TYPE)); + } + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockEmptyServiceFinder.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockEmptyServiceFinder.java new file mode 100644 index 00000000000..a7ece4343d5 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockEmptyServiceFinder.java @@ -0,0 +1,19 @@ +package com.sap.sailing.domain.racelog.tracking.test.mock; + +import com.sap.sailing.domain.common.racelog.tracking.NoCorrespondingServiceRegisteredException; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinder; + +public class MockEmptyServiceFinder implements TypeBasedServiceFinder { + private ServiceType fallback; + + @Override + public ServiceType findService(String type) throws NoCorrespondingServiceRegisteredException { + return fallback; + } + + @Override + public void setFallbackService(ServiceType fallback) { + this.fallback = fallback; + } + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockEmptyServiceFinderFactory.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockEmptyServiceFinderFactory.java new file mode 100644 index 00000000000..e2a149c1fdb --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockEmptyServiceFinderFactory.java @@ -0,0 +1,19 @@ +package com.sap.sailing.domain.racelog.tracking.test.mock; + +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinder; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinderFactory; + +/** + * A simplified implementation of the {@link TypeBasedServiceFinder} interface that, when the device type + * {@link SmartphoneImeiIdentifier#TYPE} is requested, returns a specific handler that was passed to this object's + * constructor. + * + * @author Fredrik Teschke + * + */ +public class MockEmptyServiceFinderFactory implements TypeBasedServiceFinderFactory { + @Override + public TypeBasedServiceFinder createServiceFinder(Class clazz) { + return new MockEmptyServiceFinder(); + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockGPSFixJsonHandler.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockGPSFixJsonHandler.java new file mode 100644 index 00000000000..4fe20028382 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockGPSFixJsonHandler.java @@ -0,0 +1,24 @@ +package com.sap.sailing.domain.racelog.tracking.test.mock; + +import org.json.simple.JSONObject; + +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.server.gateway.deserialization.JsonDeserializationException; +import com.sap.sailing.server.gateway.deserialization.impl.GPSFixMovingJsonDeserializer; +import com.sap.sailing.server.gateway.serialization.impl.GPSFixMovingJsonSerializer; +import com.sap.sailing.server.gateway.serialization.racelog.tracking.GPSFixJsonHandler; + +public class MockGPSFixJsonHandler implements GPSFixJsonHandler { + + @Override + public JSONObject transformForth(GPSFix object) { + return new GPSFixMovingJsonSerializer().serialize((GPSFixMoving) object); + } + + @Override + public GPSFix transformBack(JSONObject json) throws JsonDeserializationException { + return new GPSFixMovingJsonDeserializer().deserialize(json); + } + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockGPSFixJsonServiceFinder.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockGPSFixJsonServiceFinder.java new file mode 100644 index 00000000000..c8a49b5cc75 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockGPSFixJsonServiceFinder.java @@ -0,0 +1,21 @@ +package com.sap.sailing.domain.racelog.tracking.test.mock; + +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinder; +import com.sap.sailing.server.gateway.deserialization.impl.GPSFixMovingJsonDeserializer; +import com.sap.sailing.server.gateway.serialization.racelog.tracking.GPSFixJsonHandler; + +public class MockGPSFixJsonServiceFinder implements TypeBasedServiceFinder { + private final MockGPSFixJsonHandler handler = new MockGPSFixJsonHandler(); + + @Override + public GPSFixJsonHandler findService(String fixType) { + if (fixType.equals(GPSFixMovingJsonDeserializer.TYPE)) { + return handler; + } + return null; + } + + @Override + public void setFallbackService(GPSFixJsonHandler fallback) { + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockGPSFixMongoServiceFinder.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockGPSFixMongoServiceFinder.java new file mode 100644 index 00000000000..6fba478c88e --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockGPSFixMongoServiceFinder.java @@ -0,0 +1,26 @@ +package com.sap.sailing.domain.racelog.tracking.test.mock; + +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinder; +import com.sap.sailing.domain.persistence.impl.DomainObjectFactoryImpl; +import com.sap.sailing.domain.persistence.impl.MongoObjectFactoryImpl; +import com.sap.sailing.domain.persistence.racelog.tracking.GPSFixMongoHandler; +import com.sap.sailing.domain.persistence.racelog.tracking.impl.GPSFixMongoHandlerImpl; +import com.sap.sailing.domain.persistence.racelog.tracking.impl.GPSFixMovingMongoHandlerImpl; +import com.sap.sailing.domain.tracking.impl.GPSFixImpl; +import com.sap.sailing.domain.tracking.impl.GPSFixMovingImpl; + +public class MockGPSFixMongoServiceFinder implements TypeBasedServiceFinder { + private final MongoObjectFactoryImpl mof = new MongoObjectFactoryImpl(null); + private final DomainObjectFactoryImpl dof = new DomainObjectFactoryImpl(null, null); + + @Override + public GPSFixMongoHandler findService(String fixType) { + if (fixType.equals(GPSFixMovingImpl.class.getName())) return new GPSFixMovingMongoHandlerImpl(mof, dof); + if (fixType.equals(GPSFixImpl.class.getName())) return new GPSFixMongoHandlerImpl(mof, dof); + return null; + } + + @Override + public void setFallbackService(GPSFixMongoHandler fallback) { + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockSmartphoneImeiJsonServiceFinder.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockSmartphoneImeiJsonServiceFinder.java new file mode 100644 index 00000000000..2a9622b0b52 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockSmartphoneImeiJsonServiceFinder.java @@ -0,0 +1,28 @@ +package com.sap.sailing.domain.racelog.tracking.test.mock; + +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinder; +import com.sap.sailing.server.gateway.serialization.racelog.tracking.DeviceIdentifierJsonHandler; + +/** + * A simplified implementation of the {@link TypeBasedServiceFinder} interface that, when the device type + * {@link SmartphoneImeiIdentifier#TYPE} is requested, returns a specific handler that was passed to this object's + * constructor. + * + * @author Fredrik Teschke + * + */ +public class MockSmartphoneImeiJsonServiceFinder implements TypeBasedServiceFinder { + private final SmartphoneImeiJsonHandler handler = new SmartphoneImeiJsonHandler(); + private DeviceIdentifierJsonHandler fallback; + + @Override + public DeviceIdentifierJsonHandler findService(String deviceType) { + if (deviceType.equals(SmartphoneImeiIdentifier.TYPE)) return handler; + return fallback; + } + + @Override + public void setFallbackService(DeviceIdentifierJsonHandler fallback) { + this.fallback = fallback; + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockSmartphoneImeiMongoServiceFinder.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockSmartphoneImeiMongoServiceFinder.java new file mode 100644 index 00000000000..a1353e1813d --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockSmartphoneImeiMongoServiceFinder.java @@ -0,0 +1,28 @@ +package com.sap.sailing.domain.racelog.tracking.test.mock; + +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinder; +import com.sap.sailing.domain.persistence.racelog.tracking.DeviceIdentifierMongoHandler; + +/** + * A simplified implementation of the {@link TypeBasedServiceFinder} interface that, when the device type + * {@link SmartphoneImeiIdentifier#TYPE} is requested, returns a specific handler that was passed to this object's + * constructor. + * + * @author Fredrik Teschke + * + */ +public class MockSmartphoneImeiMongoServiceFinder implements TypeBasedServiceFinder { + private final SmartphoneImeiMongoHandler handler = new SmartphoneImeiMongoHandler(); + private DeviceIdentifierMongoHandler fallback; + + @Override + public DeviceIdentifierMongoHandler findService(String deviceType) { + if (deviceType.equals(SmartphoneImeiIdentifier.TYPE)) return handler; + return fallback; + } + + @Override + public void setFallbackService(DeviceIdentifierMongoHandler fallback) { + this.fallback = fallback; + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockSmartphoneImeiServiceFinderFactory.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockSmartphoneImeiServiceFinderFactory.java new file mode 100644 index 00000000000..0a7c9ef35ab --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/MockSmartphoneImeiServiceFinderFactory.java @@ -0,0 +1,36 @@ +package com.sap.sailing.domain.racelog.tracking.test.mock; + +import java.util.HashMap; +import java.util.Map; + +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinder; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinderFactory; +import com.sap.sailing.domain.persistence.racelog.tracking.DeviceIdentifierMongoHandler; +import com.sap.sailing.domain.persistence.racelog.tracking.GPSFixMongoHandler; +import com.sap.sailing.server.gateway.serialization.racelog.tracking.DeviceIdentifierJsonHandler; +import com.sap.sailing.server.gateway.serialization.racelog.tracking.GPSFixJsonHandler; + +/** + * A simplified implementation of the {@link TypeBasedServiceFinder} interface that, when the device type + * {@link SmartphoneImeiIdentifier#TYPE} is requested, returns a specific handler that was passed to this object's + * constructor. + * + * @author Fredrik Teschke + * + */ +public class MockSmartphoneImeiServiceFinderFactory implements TypeBasedServiceFinderFactory { + Map, TypeBasedServiceFinder> serviceFinders = new HashMap, TypeBasedServiceFinder>(); + + public MockSmartphoneImeiServiceFinderFactory() { + serviceFinders.put(DeviceIdentifierMongoHandler.class, new MockSmartphoneImeiMongoServiceFinder()); + serviceFinders.put(DeviceIdentifierJsonHandler.class, new MockSmartphoneImeiJsonServiceFinder()); + serviceFinders.put(GPSFixMongoHandler.class, new MockGPSFixMongoServiceFinder()); + serviceFinders.put(GPSFixJsonHandler.class, new MockGPSFixJsonServiceFinder()); + } + + @SuppressWarnings("unchecked") + @Override + public TypeBasedServiceFinder createServiceFinder(Class clazz) { + return (TypeBasedServiceFinder) serviceFinders.get(clazz); + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/SmartphoneImeiIdentifier.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/SmartphoneImeiIdentifier.java new file mode 100644 index 00000000000..b4a50799206 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/SmartphoneImeiIdentifier.java @@ -0,0 +1,48 @@ +package com.sap.sailing.domain.racelog.tracking.test.mock; + +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; + + +public class SmartphoneImeiIdentifier implements DeviceIdentifier { + private static final long serialVersionUID = -1830014229310181702L; + + public static final String TYPE = "smartphoneImei"; + + private final String imei; + + public SmartphoneImeiIdentifier(String imei) { + this.imei = imei; + } + + public String getImei() { + return imei; + } + + @Override + public String getIdentifierType() { + return TYPE; + } + + @Override + public String toString() { + return "IMEI " + imei; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SmartphoneImeiIdentifier) { + return ((SmartphoneImeiIdentifier) obj).imei.equals(imei); + } + return false; + } + + @Override + public int hashCode() { + return imei.hashCode(); + } + + @Override + public String getStringRepresentation() { + return imei; + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/SmartphoneImeiJsonHandler.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/SmartphoneImeiJsonHandler.java new file mode 100644 index 00000000000..2bb12bbfd3b --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/SmartphoneImeiJsonHandler.java @@ -0,0 +1,16 @@ +package com.sap.sailing.domain.racelog.tracking.test.mock; + +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.server.gateway.serialization.racelog.tracking.DeviceIdentifierJsonHandler; + +public class SmartphoneImeiJsonHandler extends SmartphoneImeiSerializationHandler +implements DeviceIdentifierJsonHandler { + + @Override + public DeviceIdentifier deserialize(Object serialized, String type, String stringRepresentation) + throws TransformationException { + return deserialize((String) serialized, type, stringRepresentation); + } + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/SmartphoneImeiMongoHandler.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/SmartphoneImeiMongoHandler.java new file mode 100644 index 00000000000..39c94d2fac2 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/SmartphoneImeiMongoHandler.java @@ -0,0 +1,16 @@ +package com.sap.sailing.domain.racelog.tracking.test.mock; + +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.persistence.racelog.tracking.DeviceIdentifierMongoHandler; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; + +public class SmartphoneImeiMongoHandler extends SmartphoneImeiSerializationHandler +implements DeviceIdentifierMongoHandler { + + @Override + public DeviceIdentifier deserialize(Object serialized, String type, String stringRepresentation) + throws TransformationException { + return deserialize((String) serialized, type, stringRepresentation); + } + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/SmartphoneImeiSerializationHandler.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/SmartphoneImeiSerializationHandler.java new file mode 100644 index 00000000000..6a6047eb887 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/SmartphoneImeiSerializationHandler.java @@ -0,0 +1,21 @@ +package com.sap.sailing.domain.racelog.tracking.test.mock; + +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; + +public class SmartphoneImeiSerializationHandler { + private SmartphoneImeiIdentifier castIdentifier(DeviceIdentifier identifier) throws TransformationException { + if (!(identifier instanceof SmartphoneImeiIdentifier)) + throw new TransformationException("Expected a SmartphoneImeiIdentifier, got instead: " + identifier); + return (SmartphoneImeiIdentifier) identifier; + } + + public Pair serialize(DeviceIdentifier deviceIdentifier) throws TransformationException { + return new Pair(SmartphoneImeiIdentifier.TYPE, castIdentifier(deviceIdentifier).getImei()); + } + + public DeviceIdentifier deserialize(String input, String type, String stringRep) throws TransformationException { + return new SmartphoneImeiIdentifier(stringRep); + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/SmartphoneImeiStringSerializationHandler.java b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/SmartphoneImeiStringSerializationHandler.java new file mode 100644 index 00000000000..c6fcc9c94e6 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter.testsupport/src/com/sap/sailing/domain/racelog/tracking/test/mock/SmartphoneImeiStringSerializationHandler.java @@ -0,0 +1,8 @@ +package com.sap.sailing.domain.racelog.tracking.test.mock; + +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifierStringSerializationHandler; + +public class SmartphoneImeiStringSerializationHandler extends SmartphoneImeiSerializationHandler +implements DeviceIdentifierStringSerializationHandler { + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/.classpath b/java/com.sap.sailing.domain.racelogtrackingadapter/.classpath new file mode 100644 index 00000000000..e8ea977a694 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/.gitignore b/java/com.sap.sailing.domain.racelogtrackingadapter/.gitignore new file mode 100644 index 00000000000..ba077a4031a --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/.gitignore @@ -0,0 +1 @@ +bin diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/.project b/java/com.sap.sailing.domain.racelogtrackingadapter/.project new file mode 100644 index 00000000000..6d8ea47c43e --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/.project @@ -0,0 +1,28 @@ + + + com.sap.sailing.domain.racelogtrackingadapter + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/.settings/org.eclipse.jdt.core.prefs b/java/com.sap.sailing.domain.racelogtrackingadapter/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..9e859eb5b06 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +#Thu Jan 19 09:51:40 CET 2012 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/.settings/org.eclipse.pde.core.prefs b/java/com.sap.sailing.domain.racelogtrackingadapter/.settings/org.eclipse.pde.core.prefs new file mode 100644 index 00000000000..eb2d1c6a2b2 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/.settings/org.eclipse.pde.core.prefs @@ -0,0 +1,4 @@ +#Thu Aug 25 16:46:04 CEST 2011 +eclipse.preferences.version=1 +pluginProject.extensions=false +resolve.requirebundle=false diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.racelogtrackingadapter/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..a5c48f4fc61 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/META-INF/MANIFEST.MF @@ -0,0 +1,27 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: RaceLog Tracking Adapter +Bundle-SymbolicName: com.sap.sailing.domain.racelogtrackingadapter +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: SAP +Bundle-RequiredExecutionEnvironment: JavaSE-1.7 +Bundle-ActivationPolicy: lazy +Require-Bundle: com.sap.sailing.domain, + com.sap.sailing.domain.common, + com.sap.sailing.domain.shared.android, + com.sap.sailing.server.gateway.serialization;bundle-version="1.0.0", + com.sap.sailing.domain.persistence;bundle-version="1.0.0", + com.sap.sailing.server.gateway;bundle-version="1.0.0", + com.sap.sailing.server;bundle-version="1.0.0", + com.googlecode.java-diff-utils;bundle-version="1.3.0", + com.mongodb.driver +Import-Package: com.mongodb, + javax.servlet;version="2.6.0", + org.json.simple, + org.json.simple.parser, + org.osgi.framework;version="1.6.0" +Export-Package: com.sap.sailing.domain.racelogtracking, + com.sap.sailing.domain.racelogtracking.impl;x-friends:="com.sap.sailing.domain.racelogtrackingadapter.testsupport,com.sap.sailing.gwt.ui.test", + com.sap.sailing.domain.racelogtracking.servlet;x-friends:="com.sap.sailing.domain.racelogtrackingadapter.test" +Web-ContextPath: /tracking +Bundle-Activator: com.sap.sailing.domain.racelogtracking.impl.Activator diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/WEB-INF/web.xml b/java/com.sap.sailing.domain.racelogtrackingadapter/WEB-INF/web.xml new file mode 100644 index 00000000000..7f11b027765 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/WEB-INF/web.xml @@ -0,0 +1,17 @@ + + + + + RecordFixesPostServlet + com.sap.sailing.domain.racelogtracking.servlet.RecordFixesPostServlet + + + RecordFixesPostServlet + /recordFixes + + + diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/build.properties b/java/com.sap.sailing.domain.racelogtrackingadapter/build.properties new file mode 100644 index 00000000000..34d2e4d2dad --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/pom.xml b/java/com.sap.sailing.domain.racelogtrackingadapter/pom.xml new file mode 100644 index 00000000000..d51b52b606d --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sailing.domain.racelogtrackingadapter + eclipse-plugin + diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/PingDeviceIdentifier.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/PingDeviceIdentifier.java new file mode 100644 index 00000000000..cbedce015fd --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/PingDeviceIdentifier.java @@ -0,0 +1,17 @@ +package com.sap.sailing.domain.racelogtracking; + +import java.util.UUID; + +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; + +/** + * A device identifier used to identify a non-existent, virtual device, which is used only once as an + * identifier to add a single fix to a track (i.e. pinging the location of that track's item). + * + * @author Fredrik Teschke + * + */ +public interface PingDeviceIdentifier extends DeviceIdentifier { + public static final String TYPE = "PING"; + UUID getId(); +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/PingDeviceIdentifierImpl.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/PingDeviceIdentifierImpl.java new file mode 100644 index 00000000000..d08d7f72ff8 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/PingDeviceIdentifierImpl.java @@ -0,0 +1,46 @@ +package com.sap.sailing.domain.racelogtracking; + +import java.util.UUID; + +public class PingDeviceIdentifierImpl implements PingDeviceIdentifier { + private static final long serialVersionUID = -4049961972156611640L; + + + private final UUID id; + + public PingDeviceIdentifierImpl() { + this.id = UUID.randomUUID(); + } + + public PingDeviceIdentifierImpl(UUID id) { + this.id = id; + } + + @Override + public String getIdentifierType() { + return TYPE; + } + + @Override + public UUID getId() { + return id; + } + + @Override + public String getStringRepresentation() { + return id.toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof PingDeviceIdentifier) { + return id.equals(((PingDeviceIdentifier) obj).getId()); + } + return false; + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/RaceLogTrackingAdapter.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/RaceLogTrackingAdapter.java new file mode 100644 index 00000000000..40bbd2c82dc --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/RaceLogTrackingAdapter.java @@ -0,0 +1,93 @@ +package com.sap.sailing.domain.racelogtracking; + +import java.util.Set; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.ControlPoint; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.SharedDomainFactory; +import com.sap.sailing.domain.common.racelog.tracking.NotDenotableForRaceLogTrackingException; +import com.sap.sailing.domain.common.racelog.tracking.NotDenotedForRaceLogTrackingException; +import com.sap.sailing.domain.common.racelog.tracking.RaceLogTrackingState; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogCourseDesignChangedEvent; +import com.sap.sailing.domain.racelog.tracking.DefineMarkEvent; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; +import com.sap.sailing.domain.racelog.tracking.StartTrackingEvent; +import com.sap.sailing.domain.racelogtracking.impl.RaceLogRaceTracker; +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.server.RacingEventService; + +public interface RaceLogTrackingAdapter { + /** + * Performs the necessary steps to ensure that the race is tracked (aka that a {@link TrackedRace} + * is created from the data in this {@code RaceLog}).

+ * The following steps are performed to achieve this: + *

    + *
  • Is the racelog denoted for tracking? If not, throw exception.
  • + *
  • Is a {@link RaceLogRaceTracker} already listening for this racelog? If not, add one.
  • + *
  • Is a {@link StartTrackingEvent} present in the racelog? If not, add one
  • + *
+ */ + void startTracking(RacingEventService service, Leaderboard leaderboard, RaceColumn raceColumn, Fleet fleet) + throws NotDenotedForRaceLogTrackingException, Exception; + + RaceLogTrackingState getRaceLogTrackingState(RacingEventService service, RaceColumn raceColumn, Fleet fleet); + + /** + * Is a {@link RaceLogRaceTracker} already listening on this {@code raceLog}? + */ + boolean isRaceLogRaceTrackerAttached(RacingEventService service, RaceLog raceLog); + + /** + * Denotes the {@link RaceLog} for racelog-tracking, by inserting a {@link DenoteForTrackingEvent}. + * + * @throws NotDenotableForRaceLogTrackingException + * Fails, if no {@link RaceLog}, or a non-empty {@link RaceLog}, or one with attached + * {@link TrackedRace} is found already in place. + * Also fails, if the {@code leaderboard} is not a {@link RegattaLeaderboard}. + */ + void denoteRaceForRaceLogTracking(RacingEventService service, Leaderboard leaderboard, RaceColumn raceColumn, + Fleet fleet, String raceName) throws NotDenotableForRaceLogTrackingException; + + /** + * Revoke the {@link DenoteForTrackingEvent}, and if it exists the {@link StartTrackingEvent}. + * This does not affect existing an {@link RaceLogRaceTracker} + * or {@link TrackedRace} for this {@code RaceLog}. + */ + void removeDenotationForRaceLogTracking(RacingEventService service, RaceLog raceLog); + + /** + * Denotes the entire {@link Leaderboard} for racelog-tracking, by calling the + * {@link #denoteRaceForRaceLogTracking(RacingEventService, Leaderboard, RaceColumn, Fleet, String)} method for each + * {@link RaceLog}. + */ + void denoteAllRacesForRaceLogTracking(RacingEventService service, Leaderboard leaderboard) + throws NotDenotableForRaceLogTrackingException; + + /** + * Add a fix to the {@link GPSFixStore}, and create a mapping with a virtual device for exactly that timepoint. + */ + void pingMark(RaceLog raceLogToAddTo, Mark mark, GPSFix gpsFix, RacingEventService service); + + /** + * Duplicate the course and competitor registrations in the newest {@link RaceLogCourseDesignChangedEvent} + * in {@code from} race log to the {@code to} race logs. + * The {@link Mark}s and {@link ControlPoint}s are duplicated and not reused. This also inserts the necessary + * {@link DefineMarkEvent}s into the {@code to} race logs. + */ + void copyCourseAndCompetitors(RaceLog from, Set to, SharedDomainFactory baseDomainFactory, + RacingEventService service); + + /** + * If not yet registered, register the competitors in {@code competitors}, + * and unregister all already registered competitors not in {@code competitors}. + */ + void registerCompetitors(RacingEventService service, RaceLog raceLog, Set competitors); +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/RaceLogTrackingAdapterFactory.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/RaceLogTrackingAdapterFactory.java new file mode 100644 index 00000000000..34b223fc676 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/RaceLogTrackingAdapterFactory.java @@ -0,0 +1,7 @@ +package com.sap.sailing.domain.racelogtracking; + +import com.sap.sailing.domain.base.DomainFactory; + +public interface RaceLogTrackingAdapterFactory { + RaceLogTrackingAdapter getAdapter(DomainFactory baseDomainFactory); +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/Activator.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/Activator.java new file mode 100644 index 00000000000..34d9a7fd415 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/Activator.java @@ -0,0 +1,83 @@ +package com.sap.sailing.domain.racelogtracking.impl; + +import java.util.Dictionary; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; + +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinder; +import com.sap.sailing.domain.persistence.racelog.tracking.DeviceIdentifierMongoHandler; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifierStringSerializationHandler; +import com.sap.sailing.domain.racelog.tracking.SmartphoneUUIDIdentifier; +import com.sap.sailing.domain.racelogtracking.PingDeviceIdentifierImpl; +import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapterFactory; +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.server.gateway.deserialization.JsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.GPSFixJsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.GPSFixMovingJsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.GPSFixMovingNmeaDTOJsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.GPSFixNmeaDTOJsonDeserializer; +import com.sap.sailing.server.gateway.serialization.JsonSerializer; +import com.sap.sailing.server.gateway.serialization.impl.GPSFixJsonSerializer; +import com.sap.sailing.server.gateway.serialization.impl.GPSFixMovingJsonSerializer; +import com.sap.sailing.server.gateway.serialization.impl.GPSFixMovingNmeaDTOJsonSerializer; +import com.sap.sailing.server.gateway.serialization.impl.GPSFixNmeaDTOJsonSerializer; +import com.sap.sailing.server.gateway.serialization.racelog.tracking.DeviceIdentifierJsonHandler; +import com.sap.sailing.server.gateway.serialization.racelog.tracking.GPSFixJsonHandler; +import com.sap.sailing.server.gateway.serialization.racelog.tracking.impl.GPSFixJsonHandlerImpl; +import com.sap.sailing.server.gateway.serialization.racelog.tracking.impl.SmartphoneUUIDJsonHandler; +import com.sap.sailing.server.gateway.trackfiles.TrackFileImportDeviceIdentifier; + +public class Activator implements BundleActivator { + private static final Logger logger = Logger.getLogger(Activator.class.getName()); + + private Set> registrations = new HashSet<>(); + + private Dictionary getDict(String type) { + Dictionary properties = new Hashtable(); + properties.put(TypeBasedServiceFinder.TYPE, type); + return properties; + } + + private void registerGPSFixJsonService(BundleContext context, JsonDeserializer deserializer, + JsonSerializer serializer, String type) { + registrations.add(context.registerService(GPSFixJsonHandler.class, + new GPSFixJsonHandlerImpl(deserializer, serializer), getDict(type))); + } + + @Override + public void start(BundleContext context) throws Exception { + registrations.add(context.registerService(DeviceIdentifierMongoHandler.class, new SmartphoneUUIDMongoHandler(), getDict(SmartphoneUUIDIdentifier.TYPE))); + registrations.add(context.registerService(DeviceIdentifierJsonHandler.class, new SmartphoneUUIDJsonHandler(), getDict(SmartphoneUUIDIdentifier.TYPE))); + registrations.add(context.registerService(DeviceIdentifierStringSerializationHandler.class, new SmartphoneUUIDStringSerializationHandler(), getDict(SmartphoneUUIDIdentifier.TYPE))); + + registrations.add(context.registerService(DeviceIdentifierMongoHandler.class, new PingDeviceIdentifierMongoHandler(), getDict(PingDeviceIdentifierImpl.TYPE))); + registrations.add(context.registerService(DeviceIdentifierJsonHandler.class, new PingDeviceIdentifierJsonHandler(), getDict(PingDeviceIdentifierImpl.TYPE))); + + registrations.add(context.registerService(DeviceIdentifierMongoHandler.class, new TrackFileImportDeviceIdentifierMongoHandler(), getDict(TrackFileImportDeviceIdentifier.TYPE))); + registrations.add(context.registerService(DeviceIdentifierJsonHandler.class, new TrackFileImportDeviceIdentifierJsonHandler(), getDict(TrackFileImportDeviceIdentifier.TYPE))); + registrations.add(context.registerService(DeviceIdentifierStringSerializationHandler.class, new TrackFileImportDeviceIdentifierStringSerializationHandler(), getDict(TrackFileImportDeviceIdentifier.TYPE))); + + registerGPSFixJsonService(context, new GPSFixJsonDeserializer(), new GPSFixJsonSerializer(), GPSFixJsonDeserializer.TYPE); + registerGPSFixJsonService(context, new GPSFixMovingJsonDeserializer(), new GPSFixMovingJsonSerializer(), GPSFixMovingJsonDeserializer.TYPE); + registerGPSFixJsonService(context, new GPSFixNmeaDTOJsonDeserializer(), new GPSFixNmeaDTOJsonSerializer(), GPSFixNmeaDTOJsonDeserializer.TYPE); + registerGPSFixJsonService(context, new GPSFixMovingNmeaDTOJsonDeserializer(), new GPSFixMovingNmeaDTOJsonSerializer(), GPSFixMovingNmeaDTOJsonDeserializer.TYPE); + + registrations.add(context.registerService(RaceLogTrackingAdapterFactory.class, RaceLogTrackingAdapterFactoryImpl.INSTANCE, null)); + logger.log(Level.INFO, "Started "+context.getBundle().getSymbolicName()); + } + + @Override + public void stop(BundleContext context) throws Exception { + for (ServiceRegistration reg : registrations) { + reg.unregister(); + } + registrations.clear(); + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/PingDeviceIdentifierJsonHandler.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/PingDeviceIdentifierJsonHandler.java new file mode 100644 index 00000000000..aa1e690e85a --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/PingDeviceIdentifierJsonHandler.java @@ -0,0 +1,16 @@ +package com.sap.sailing.domain.racelogtracking.impl; + +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.server.gateway.serialization.racelog.tracking.DeviceIdentifierJsonHandler; + +public class PingDeviceIdentifierJsonHandler extends PingDeviceIdentifierSerializationHandler +implements DeviceIdentifierJsonHandler { + + @Override + public DeviceIdentifier deserialize(Object serialized, String type, String stringRepresentation) + throws TransformationException { + return deserialize((String) serialized, type, stringRepresentation); + } + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/PingDeviceIdentifierMongoHandler.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/PingDeviceIdentifierMongoHandler.java new file mode 100644 index 00000000000..2a95aa119f7 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/PingDeviceIdentifierMongoHandler.java @@ -0,0 +1,16 @@ +package com.sap.sailing.domain.racelogtracking.impl; + +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.persistence.racelog.tracking.DeviceIdentifierMongoHandler; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; + +public class PingDeviceIdentifierMongoHandler extends PingDeviceIdentifierSerializationHandler +implements DeviceIdentifierMongoHandler { + + @Override + public DeviceIdentifier deserialize(Object serialized, String type, String stringRepresentation) + throws TransformationException { + return deserialize((String) serialized, type, stringRepresentation); + } + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/PingDeviceIdentifierSerializationHandler.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/PingDeviceIdentifierSerializationHandler.java new file mode 100644 index 00000000000..828b4d7a796 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/PingDeviceIdentifierSerializationHandler.java @@ -0,0 +1,29 @@ +package com.sap.sailing.domain.racelogtracking.impl; + +import java.util.UUID; + +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelogtracking.PingDeviceIdentifier; +import com.sap.sailing.domain.racelogtracking.PingDeviceIdentifierImpl; + +public class PingDeviceIdentifierSerializationHandler { + private PingDeviceIdentifier castIdentifier(DeviceIdentifier identifier) throws TransformationException { + if (!(identifier instanceof PingDeviceIdentifier)) + throw new TransformationException("Expected a PingDeviceIdentifier, got instead: " + identifier); + return (PingDeviceIdentifier) identifier; + } + + public Pair serialize(DeviceIdentifier deviceIdentifier) throws TransformationException { + return new Pair(PingDeviceIdentifier.TYPE, castIdentifier(deviceIdentifier).getId().toString()); + } + + public DeviceIdentifier deserialize(String input, String type, String stringRep) throws TransformationException { + try { + return new PingDeviceIdentifierImpl(UUID.fromString(stringRep)); + } catch (IllegalArgumentException e) { + throw new TransformationException(e); + } + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/PingDeviceIdentifierStringSerializationHandler.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/PingDeviceIdentifierStringSerializationHandler.java new file mode 100644 index 00000000000..b039e9c656c --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/PingDeviceIdentifierStringSerializationHandler.java @@ -0,0 +1,8 @@ +package com.sap.sailing.domain.racelogtracking.impl; + +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifierStringSerializationHandler; + +public class PingDeviceIdentifierStringSerializationHandler extends PingDeviceIdentifierSerializationHandler +implements DeviceIdentifierStringSerializationHandler { + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogConnectivityParams.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogConnectivityParams.java new file mode 100644 index 00000000000..fb4e6128a5f --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogConnectivityParams.java @@ -0,0 +1,101 @@ +package com.sap.sailing.domain.racelogtracking.impl; + +import java.util.UUID; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.common.racelog.tracking.RaceNotCreatedException; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.RaceInformationFinder; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.RaceLogTrackingStateAnalyzer; +import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; +import com.sap.sailing.domain.tracking.RaceTracker; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; +import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; +import com.sap.sailing.domain.tracking.WindStore; +import com.sap.sailing.server.RacingEventService; + +public class RaceLogConnectivityParams implements RaceTrackingConnectivityParameters { + private final RacingEventService service; + private final RaceColumn raceColumn; + private final Fleet fleet; + private final Leaderboard leaderboard; + private final long delayToLiveInMillis; + private final Regatta regatta; + private final DomainFactory domainFactory; + + public RaceLogConnectivityParams(RacingEventService service, Regatta regatta, RaceColumn raceColumn, Fleet fleet, + Leaderboard leaderboard, long delayToLiveInMillis, DomainFactory domainFactory) throws RaceNotCreatedException { + this.service = service; + this.regatta = regatta; + this.raceColumn = raceColumn; + this.fleet = fleet; + this.leaderboard = leaderboard; + this.delayToLiveInMillis = delayToLiveInMillis; + this.domainFactory = domainFactory; + + if (!new RaceLogTrackingStateAnalyzer(getRaceLog()).analyze().isForTracking()) { + throw new RaceNotCreatedException(String.format("Racelog (%s) is not denoted for tracking", getRaceLog())); + } + } + + @Override + public RaceTracker createRaceTracker(TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore, + GPSFixStore gpsFixStore) { + return createRaceTracker(regatta, trackedRegattaRegistry, windStore, gpsFixStore); + } + + @Override + public RaceTracker createRaceTracker(Regatta regatta, TrackedRegattaRegistry trackedRegattaRegistry, + WindStore windStore, GPSFixStore gpsFixStore) { + if (regatta == null) { + BoatClass boatClass = new RaceInformationFinder(getRaceLog()).analyze().getBoatClass(); + regatta = service.getOrCreateDefaultRegatta("RaceLog-tracking default Regatta", boatClass.getName(), + UUID.randomUUID()); + } + if (regatta == null) { + throw new RaceNotCreatedException("No regatta for race-log tracked race"); + } + DynamicTrackedRegatta trackedRegatta = trackedRegattaRegistry.getOrCreateTrackedRegatta(regatta); + return new RaceLogRaceTracker(trackedRegatta, this, windStore, gpsFixStore); + } + + @Override + public Object getTrackerID() { + return getRaceLog().getId(); + } + + @Override + public long getDelayToLiveInMillis() { + return delayToLiveInMillis; + } + + public RaceLog getRaceLog() { + return raceColumn.getRaceLog(fleet); + } + + public RaceColumn getRaceColumn() { + return raceColumn; + } + + public Fleet getFleet() { + return fleet; + } + + public Leaderboard getLeaderboard() { + return leaderboard; + } + + public RacingEventService getService() { + return service; + } + + public DomainFactory getDomainFactory() { + return domainFactory; + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogRaceTracker.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogRaceTracker.java new file mode 100644 index 00000000000..6585b1d0095 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogRaceTracker.java @@ -0,0 +1,428 @@ +package com.sap.sailing.domain.racelogtracking.impl; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.ControlPoint; +import com.sap.sailing.domain.base.Course; +import com.sap.sailing.domain.base.CourseBase; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Sideline; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.base.impl.CourseDataImpl; +import com.sap.sailing.domain.base.impl.CourseImpl; +import com.sap.sailing.domain.base.impl.RaceDefinitionImpl; +import com.sap.sailing.domain.common.PassingInstruction; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.TrackedRaceStatusEnum; +import com.sap.sailing.domain.common.WithID; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.common.racelog.tracking.NotRevokableException; +import com.sap.sailing.domain.common.racelog.tracking.RaceLogTrackingState; +import com.sap.sailing.domain.common.racelog.tracking.RaceNotCreatedException; +import com.sap.sailing.domain.markpassingcalculation.MarkPassingCalculator; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogCourseDesignChangedEvent; +import com.sap.sailing.domain.racelog.analyzing.impl.LastPublishedCourseDesignFinder; +import com.sap.sailing.domain.racelog.impl.BaseRaceLogEventVisitor; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceCompetitorMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.DeviceMapping; +import com.sap.sailing.domain.racelog.tracking.DeviceMarkMappingEvent; +import com.sap.sailing.domain.racelog.tracking.GPSFixReceivedListener; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; +import com.sap.sailing.domain.racelog.tracking.StartTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.DeviceCompetitorMappingFinder; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.DeviceMarkMappingFinder; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.RaceInformationFinder; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.RaceLogTrackingStateAnalyzer; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.RegisteredCompetitorsAnalyzer; +import com.sap.sailing.domain.tracking.DynamicGPSFixTrack; +import com.sap.sailing.domain.tracking.DynamicTrackedRace; +import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.RaceTracker; +import com.sap.sailing.domain.tracking.RacesHandle; +import com.sap.sailing.domain.tracking.WindStore; +import com.sap.sailing.domain.tracking.WindTrack; +import com.sap.sailing.domain.tracking.impl.TrackedRaceStatusImpl; + +import difflib.PatchFailedException; + +/** + * Track a race using the data defined in the {@link RaceLog}. If the events suggest that the race is already in the + * {@link RaceLogTrackingState#TRACKING} state, tracking commences immediately and existing fixes are loaded immediately + * from the database. + *

+ * Otherwise, the tracker waits until a {@link StartTrackingEvent} is received to perform these tasks. + * + * @author Fredrik Teschke + * + */ +public class RaceLogRaceTracker extends BaseRaceLogEventVisitor implements RaceTracker, GPSFixReceivedListener { + private final RaceLogConnectivityParams params; + private final WindStore windStore; + private final GPSFixStore gpsFixStore; + private final DynamicTrackedRegatta regatta; + + private Map>> competitorMappings = new HashMap<>(); + private Map>> markMappings = new HashMap<>(); + + private Map>> markMappingsByDevices = new HashMap<>(); + private Map>> competitorMappingsByDevices = new HashMap<>(); + + private DynamicTrackedRace trackedRace; + + private static final Logger logger = Logger.getLogger(RaceLogRaceTracker.class.getName()); + + public RaceLogRaceTracker(DynamicTrackedRegatta regatta, RaceLogConnectivityParams params, WindStore windStore, + GPSFixStore gpsFixStore) { + this.params = params; + this.windStore = windStore; + this.gpsFixStore = gpsFixStore; + this.regatta = regatta; + + // add a listener on the racelog, to be informed about relevant events + params.getRaceLog().addListener(this); + + logger.info(String.format("Created race-log tracker for: %s %s %s", params.getLeaderboard(), + params.getRaceColumn(), params.getFleet())); + + // load race for which tracking already started + if (new RaceLogTrackingStateAnalyzer(params.getRaceLog()).analyze() == RaceLogTrackingState.TRACKING) { + startTracking(null); + } + } + + @Override + public void stop() { + // mark passing calculator is automatically stopped, when the race status is set to {@link TrackedRaceStatusEnum#FINISHED} + trackedRace.setStatus(new TrackedRaceStatusImpl(TrackedRaceStatusEnum.FINISHED, 100)); + + // remove listener on racelog + params.getRaceLog().removeListener(this); + + // remove listener for fixes + gpsFixStore.removeListener(this); + + logger.info(String.format("Stopped tracking race-log race %s %s %s", params.getLeaderboard(), + params.getRaceColumn(), params.getFleet())); + } + + @Override + public Regatta getRegatta() { + return regatta.getRegatta(); + } + + @Override + public Set getRaces() { + return trackedRace == null ? null : Collections.singleton(trackedRace.getRace()); + } + + @Override + public Set getRaceIdentifiers() { + return trackedRace == null ? null : Collections.singleton(trackedRace.getRaceIdentifier()); + } + + @Override + public RacesHandle getRacesHandle() { + return new RaceLogRacesHandle(this); + } + + @Override + public DynamicTrackedRegatta getTrackedRegatta() { + return regatta; + } + + @Override + public WindStore getWindStore() { + return windStore; + } + + @Override + public GPSFixStore getGPSFixStore() { + return gpsFixStore; + } + + @Override + public Object getID() { + return params.getRaceLog().getId(); + } + + private Map>> getNewCompetitorMappings() { + return new DeviceCompetitorMappingFinder(params.getRaceLog()).analyze(); + } + + private Map>> getNewMarkMappings() { + return new DeviceMarkMappingFinder(params.getRaceLog()).analyze(); + } + + /** + * Use mapping time ranges to set time start and end of tracking time for race + */ + private void updateStartAndEndOfTracking() { + TimePoint earliestMappingStart = new MillisecondsTimePoint(Long.MAX_VALUE); + TimePoint latestMappingEnd = new MillisecondsTimePoint(Long.MIN_VALUE); + + synchronized (competitorMappings) { + for (List> list : competitorMappings.values()) { + for (DeviceMapping mapping : list) { + if (mapping.getTimeRange().from().before(earliestMappingStart)) { + earliestMappingStart = mapping.getTimeRange().from(); + } + if (mapping.getTimeRange().to().after(latestMappingEnd)) { + latestMappingEnd = mapping.getTimeRange().to(); + } + } + } + } + + synchronized (markMappings) { + for (List> list : markMappings.values()) { + for (DeviceMapping mapping : list) { + if (mapping.getTimeRange().from().before(earliestMappingStart)) { + earliestMappingStart = mapping.getTimeRange().from(); + } + if (mapping.getTimeRange().to().after(latestMappingEnd)) { + latestMappingEnd = mapping.getTimeRange().to(); + } + } + } + } + + trackedRace.setStartOfTrackingReceived(latestMappingEnd); + } + + private boolean hasMappingAlreadyBeenLoaded( + DeviceMapping newMapping, List> oldMappings) { + for (DeviceMapping oldMapping : oldMappings) { + if (newMapping.getDevice() == oldMapping.getDevice() + && newMapping.getTimeRange().liesWithin(oldMapping.getTimeRange())) { + return true; + } + } + return false; + } + + private Map>> transformToMappingsByDevice( + Map>> mappings) { + + Map>> result = new HashMap<>(); + for (ItemT item : mappings.keySet()) { + for (DeviceMapping mapping : mappings.get(item)) { + List> list = result.get(mapping.getDevice()); + if (list == null) { + list = new ArrayList<>(); + result.put(mapping.getDevice(), list); + } + list.add(mapping); + } + } + + return result; + } + + private void updateMarkMappings(boolean loadIfNotCovered) { + // TODO remove fixes, if mappings have been removed + + synchronized (markMappings) { + // check if there are new time ranges not covered so far + Map>> newMappings = getNewMarkMappings(); + + if (loadIfNotCovered) { + for (Mark mark : newMappings.keySet()) { + DynamicGPSFixTrack track = trackedRace.getOrCreateTrack(mark); + List> oldMappings = markMappings.get(mark); + + if (oldMappings != null) { + for (DeviceMapping newMapping : newMappings.get(mark)) { + if (!hasMappingAlreadyBeenLoaded(newMapping, oldMappings)) { + gpsFixStore.loadMarkTrack(track, newMapping); + } + } + } + } + } + + markMappings.clear(); + markMappings.putAll(newMappings); + + markMappingsByDevices.clear(); + markMappingsByDevices.putAll(transformToMappingsByDevice(markMappings)); + } + + updateStartAndEndOfTracking(); + } + + @Override + public void visit(DeviceMarkMappingEvent event) { + if (trackedRace != null) + updateMarkMappings(true); + } + + private void updateCompetitorMappings(boolean loadIfNotCovered) { + // TODO remove fixes, if mappings have been removed + + synchronized (competitorMappings) { + // check if there are new time ranges not covered so far + Map>> newMappings = getNewCompetitorMappings(); + + if (loadIfNotCovered) { + for (Competitor competitor : newMappings.keySet()) { + DynamicGPSFixTrack track = trackedRace.getTrack(competitor); + List> oldMappings = competitorMappings.get(competitor); + + for (DeviceMapping newMapping : newMappings.get(competitor)) { + if (!hasMappingAlreadyBeenLoaded(newMapping, oldMappings)) { + gpsFixStore.loadCompetitorTrack(track, newMapping); + } + } + } + } + + competitorMappings.clear(); + competitorMappings.putAll(newMappings); + + competitorMappingsByDevices.clear(); + competitorMappingsByDevices.putAll(transformToMappingsByDevice(competitorMappings)); + } + + updateStartAndEndOfTracking(); + } + + @Override + public void visit(DeviceCompetitorMappingEvent event) { + if (trackedRace != null) + updateCompetitorMappings(true); + } + + @Override + public void visit(StartTrackingEvent event) { + if (trackedRace == null) + startTracking(event); + } + + @Override + public void visit(RaceLogCourseDesignChangedEvent event) { + if (trackedRace == null) return; + + CourseBase base = new LastPublishedCourseDesignFinder(params.getRaceLog()).analyze(); + List> update = new ArrayList<>(); + + for (Waypoint waypoint : base.getWaypoints()) { + update.add(new Pair<>(waypoint.getControlPoint(), waypoint.getPassingInstructions())); + } + + try { + trackedRace.getRace().getCourse().update(update, params.getDomainFactory()); + } catch (PatchFailedException e) { + logger.log(Level.WARNING, "Could not update course for race " + trackedRace.getRace().getName()); + e.printStackTrace(); + } + } + + private void startTracking(StartTrackingEvent event) { + RaceLog raceLog = params.getRaceLog(); + RaceColumn raceColumn = params.getRaceColumn(); + Fleet fleet = params.getFleet(); + + DenoteForTrackingEvent denoteEvent = new RaceInformationFinder(raceLog).analyze(); + BoatClass boatClass = denoteEvent.getBoatClass(); + String raceName = denoteEvent.getRaceName(); + CourseBase courseBase = new LastPublishedCourseDesignFinder(raceLog).analyze(); + if (courseBase == null) { + courseBase = new CourseDataImpl("Default course for " + raceName); + logger.log(Level.FINE, "Using empty course in creation of race " + raceName); + } + + Course course = new CourseImpl(raceName + " course", courseBase.getWaypoints()); + + if (raceColumn.getTrackedRace(fleet) != null) { + try { + raceLog.revokeEvent(params.getService().getServerAuthor(), event); + } catch (NotRevokableException e) {} + throw new RaceNotCreatedException(String.format("Race for racelog (%s) has already been created", raceLog)); + } + + Iterable competitors = new RegisteredCompetitorsAnalyzer(raceLog).analyze(); + Serializable raceId = denoteEvent.getRaceId(); + final RaceDefinition raceDef = new RaceDefinitionImpl(raceName, course, boatClass, competitors, raceId); + + Iterable sidelines = Collections. emptyList(); + + // set race definition, so race is linked to leaderboard automatically + regatta.getRegatta().addRace(raceDef); + raceColumn.setRaceIdentifier(fleet, regatta.getRegatta().getRaceIdentifier(raceDef)); + + trackedRace = regatta.createTrackedRace(raceDef, sidelines, windStore, gpsFixStore, + params.getDelayToLiveInMillis(), WindTrack.DEFAULT_MILLISECONDS_OVER_WHICH_TO_AVERAGE_WIND, + boatClass.getApproximateManeuverDurationInMilliseconds(), null); + + trackedRace.setStatus(new TrackedRaceStatusImpl(TrackedRaceStatusEnum.TRACKING, 0)); + + // update the device mappings (without loading the fixes, as the TrackedRace does this itself on startup) + updateCompetitorMappings(false); + updateMarkMappings(false); + + // add a listener, so new fixes will also be forwarded to the race + gpsFixStore.addListener(this); + + // add mark passing detection + new MarkPassingCalculator(trackedRace, true); + + logger.info(String.format("Started tracking race-log race (%s)", raceLog)); + // this wakes up all waiting race handles + synchronized (this) { + this.notifyAll(); + } + } + + public RaceLogConnectivityParams getParams() { + return params; + } + + @Override + public void fixReceived(DeviceIdentifier device, GPSFix fix) { + TimePoint timePoint = fix.getTimePoint(); + if (markMappingsByDevices.get(device) != null) { + for (DeviceMapping mapping : markMappingsByDevices.get(device)) { + Mark mark = mapping.getMappedTo(); + if (mapping.getTimeRange().includes(timePoint)) { + trackedRace.recordFix(mark, fix); + } + } + } + + if (competitorMappingsByDevices.get(device) != null) { + for (DeviceMapping mapping : competitorMappingsByDevices.get(device)) { + Competitor comp = mapping.getMappedTo(); + if (mapping.getTimeRange().includes(timePoint)) { + if (fix instanceof GPSFixMoving) { + trackedRace.recordFix(comp, (GPSFixMoving) fix); + } else { + logger.log(Level.WARNING, String.format( + "Could not add fix for competitor (%s) in race (%s), as it is no GPSFixMoving", comp, + params.getRaceLog())); + } + } + } + } + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogRacesHandle.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogRacesHandle.java new file mode 100644 index 00000000000..c3d20496915 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogRacesHandle.java @@ -0,0 +1,70 @@ +package com.sap.sailing.domain.racelogtracking.impl; + +import java.util.Collections; +import java.util.Set; + +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; +import com.sap.sailing.domain.tracking.RaceTracker; +import com.sap.sailing.domain.tracking.RacesHandle; + +public class RaceLogRacesHandle implements RacesHandle { + private final RaceLogRaceTracker tracker; + + public RaceLogRacesHandle(RaceLogRaceTracker tracker) { + this.tracker = tracker; + } + + @Override + public Regatta getRegatta() { + return tracker.getRegatta(); + } + + @Override + public Set getRaces() { + return getRaces(-1); + } + + @Override + public Set getRaces(long timeoutInMilliseconds) { + long start = System.currentTimeMillis(); + Set raceDefs = tracker.getRaces(); + + RaceDefinition result = null; + if (raceDefs != null && !raceDefs.isEmpty()) { + result = tracker.getRaces().iterator().next(); + } + boolean interrupted = false; + synchronized (tracker) { + while ((timeoutInMilliseconds == -1 || System.currentTimeMillis() - start < timeoutInMilliseconds) + && !interrupted && result == null) { + try { + if (timeoutInMilliseconds == -1) { + tracker.wait(); + } else { + long timeToWait = timeoutInMilliseconds - (System.currentTimeMillis() - start); + if (timeToWait > 0) { + tracker.wait(timeToWait); + } + } + result = tracker.getRaces().isEmpty() ? null : tracker.getRaces().iterator().next(); + } catch (InterruptedException e) { + interrupted = true; + } + } + } + return result == null ? null : Collections.singleton(result); + } + + @Override + public DynamicTrackedRegatta getTrackedRegatta() { + return tracker.getTrackedRegatta(); + } + + @Override + public RaceTracker getRaceTracker() { + return tracker; + } + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogTrackingAdapterFactoryImpl.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogTrackingAdapterFactoryImpl.java new file mode 100644 index 00000000000..fbd1048b99a --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogTrackingAdapterFactoryImpl.java @@ -0,0 +1,19 @@ +package com.sap.sailing.domain.racelogtracking.impl; + +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapter; +import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapterFactory; + +public enum RaceLogTrackingAdapterFactoryImpl implements RaceLogTrackingAdapterFactory { + INSTANCE; + private RaceLogTrackingAdapter adapter; + + @Override + public RaceLogTrackingAdapter getAdapter(DomainFactory baseDomainFactory) { + if (adapter == null) { + adapter = new RaceLogTrackingAdapterImpl(baseDomainFactory); + } + return adapter; + } + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogTrackingAdapterImpl.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogTrackingAdapterImpl.java new file mode 100644 index 00000000000..c84b051103d --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogTrackingAdapterImpl.java @@ -0,0 +1,295 @@ +package com.sap.sailing.domain.racelogtracking.impl; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.ControlPoint; +import com.sap.sailing.domain.base.CourseBase; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.SharedDomainFactory; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.base.impl.CourseDataImpl; +import com.sap.sailing.domain.common.PassingInstruction; +import com.sap.sailing.domain.common.RegattaIdentifier; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.impl.Util; +import com.sap.sailing.domain.common.racelog.tracking.NotDenotableForRaceLogTrackingException; +import com.sap.sailing.domain.common.racelog.tracking.NotDenotedForRaceLogTrackingException; +import com.sap.sailing.domain.common.racelog.tracking.NotRevokableException; +import com.sap.sailing.domain.common.racelog.tracking.RaceLogRaceTrackerExistsException; +import com.sap.sailing.domain.common.racelog.tracking.RaceLogTrackingState; +import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventFactory; +import com.sap.sailing.domain.racelog.Revokable; +import com.sap.sailing.domain.racelog.analyzing.impl.LastPublishedCourseDesignFinder; +import com.sap.sailing.domain.racelog.tracking.DefineMarkEvent; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.RegisterCompetitorEvent; +import com.sap.sailing.domain.racelog.tracking.StartTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.AllEventsOfTypeFinder; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.LastEventOfTypeFinder; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.RaceLogTrackingStateAnalyzer; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.RegisteredCompetitorsAnalyzer; +import com.sap.sailing.domain.racelogtracking.PingDeviceIdentifierImpl; +import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapter; +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.domain.tracking.RacesHandle; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.server.RacingEventService; + +public class RaceLogTrackingAdapterImpl implements RaceLogTrackingAdapter { + private static final Logger logger = Logger.getLogger(RaceLogTrackingAdapterImpl.class.getName()); + + private final DomainFactory domainFactory; + private final long delayToLiveInMillis; + + public RaceLogTrackingAdapterImpl(DomainFactory domainFactory) { + this.domainFactory = domainFactory; + this.delayToLiveInMillis = TrackedRace.DEFAULT_LIVE_DELAY_IN_MILLISECONDS; + } + + @Override + public void startTracking(RacingEventService service, Leaderboard leaderboard, RaceColumn raceColumn, Fleet fleet) + throws NotDenotedForRaceLogTrackingException, Exception { + RaceLog raceLog = raceColumn.getRaceLog(fleet); + RaceLogTrackingState raceLogTrackingState = new RaceLogTrackingStateAnalyzer(raceLog).analyze(); + assert raceLogTrackingState.isForTracking() : new NotDenotedForRaceLogTrackingException(); + RegattaIdentifier regatta = ((RegattaLeaderboard) leaderboard).getRegatta().getRegattaIdentifier(); + + if (! isRaceLogRaceTrackerAttached(service, raceLog)) { + addTracker(service, regatta, leaderboard, raceColumn, fleet, -1); + } + + if (raceLogTrackingState != RaceLogTrackingState.TRACKING) { + RaceLogEvent event = RaceLogEventFactory.INSTANCE.createStartTrackingEvent(MillisecondsTimePoint.now(), + service.getServerAuthor(), raceLog.getCurrentPassId()); + raceLog.add(event); + } + } + + /** + * Adds a {@link RaceLogRaceTracker}. If a {@link StartTrackingEvent} is already present in the {@code RaceLog} + * linked to the {@code raceColumn} and {@code fleet}, a {@code TrackedRace} is created immediately and tracking begins. + * Otherwise, the {@code RaceLogRaceTracker} waits until a {@code StartTrackingEvent} is added to perform these actions. + * The race first has to be denoted for racelog tracking. + */ + private RacesHandle addTracker(RacingEventService service, RegattaIdentifier regattaToAddTo, Leaderboard leaderboard, + RaceColumn raceColumn, Fleet fleet, long timeoutInMilliseconds) throws RaceLogRaceTrackerExistsException, Exception { + RaceLog raceLog = raceColumn.getRaceLog(fleet); + assert ! isRaceLogRaceTrackerAttached(service, raceLog) : new RaceLogRaceTrackerExistsException( + leaderboard.getName() + " - " + raceColumn.getName() + " - " + fleet.getName()); + + Regatta regatta = regattaToAddTo == null ? null : service.getRegatta(regattaToAddTo); + RaceLogConnectivityParams params = new RaceLogConnectivityParams(service, regatta, raceColumn, fleet, + leaderboard, delayToLiveInMillis, domainFactory); + return service.addRace(regattaToAddTo, params, timeoutInMilliseconds); + } + + @Override + public void denoteRaceForRaceLogTracking(RacingEventService service, Leaderboard leaderboard, RaceColumn raceColumn, + Fleet fleet, String raceName) throws NotDenotableForRaceLogTrackingException { + + BoatClass boatClass = null; + if (leaderboard instanceof RegattaLeaderboard) { + RegattaLeaderboard rLeaderboard = (RegattaLeaderboard) leaderboard; + boatClass = rLeaderboard.getRegatta().getBoatClass(); + } else { + throw new NotDenotableForRaceLogTrackingException("Can only denote races in RegattaLeaderboards for RaceLog-tracking"); + } + + if (raceName == null) { + raceName = leaderboard.getName() + " " + raceColumn.getName() + " " + fleet.getName(); + } + + RaceLog raceLog = raceColumn.getRaceLog(fleet); + assert raceLog != null : new NotDenotableForRaceLogTrackingException("No RaceLog found in place"); + + if (new RaceLogTrackingStateAnalyzer(raceLog).analyze().isForTracking()) { + throw new NotDenotableForRaceLogTrackingException("Already denoted for tracking"); + } + + RaceLogEvent event = RaceLogEventFactory.INSTANCE.createDenoteForTrackingEvent(MillisecondsTimePoint.now(), + service.getServerAuthor(), raceLog.getCurrentPassId(), raceName, boatClass, UUID.randomUUID()); + raceLog.add(event); + } + + @Override + public void denoteAllRacesForRaceLogTracking(final RacingEventService service, final Leaderboard leaderboard) + throws NotDenotableForRaceLogTrackingException { + if (leaderboard instanceof FlexibleLeaderboard) { + throw new NotDenotableForRaceLogTrackingException("Can only use regatta leaderboards for RaceLog-tracking"); + } + for (RaceColumn column : leaderboard.getRaceColumns()) { + for (Fleet fleet : column.getFleets()) { + try { + denoteRaceForRaceLogTracking(service, leaderboard, column, fleet, null); + } catch (NotDenotableForRaceLogTrackingException e) {} + } + } + } + + @Override + public boolean isRaceLogRaceTrackerAttached(RacingEventService service, RaceLog raceLog) { + return service.getRaceTrackerById(raceLog.getId()) != null; + } + + @Override + public RaceLogTrackingState getRaceLogTrackingState(RacingEventService service, RaceColumn raceColumn, Fleet fleet) { + return new RaceLogTrackingStateAnalyzer(raceColumn.getRaceLog(fleet)).analyze(); + } + + private Waypoint duplicateWaypoint(Waypoint waypoint, Map controlPointDuplicationCache, + Map markDuplicationCache, SharedDomainFactory baseDomainFactory) { + ControlPoint oldCP = waypoint.getControlPoint(); + ControlPoint newCP = null; + PassingInstruction pi = waypoint.getPassingInstructions(); + if (controlPointDuplicationCache.get(oldCP) != null) { + newCP = controlPointDuplicationCache.get(oldCP); + } else { + Mark[] newMarks = new Mark[Util.size(oldCP.getMarks())]; + int i = 0; + for (Mark oldMark : oldCP.getMarks()) { + newMarks[i] = null; + if (markDuplicationCache.get(oldMark) != null) { + newMarks[i] = markDuplicationCache.get(oldMark); + } else { + newMarks[i] = baseDomainFactory.getOrCreateMark(UUID.randomUUID(), oldMark.getName(), oldMark.getType(), + oldMark.getColor(), oldMark.getShape(), oldMark.getPattern()); + markDuplicationCache.put(oldMark, newMarks[i]); + } + i++; + } + switch (newMarks.length) { + case 1: + newCP = newMarks[0]; + break; + case 2: + newCP = baseDomainFactory.createControlPointWithTwoMarks(newMarks[0], newMarks[1], oldCP.getName()); + break; + default: + logger.log(Level.WARNING, "Don't know how to duplicate CP with more than 2 marks"); + throw new RuntimeException("Don't know how to duplicate CP with more than 2 marks"); + } + controlPointDuplicationCache.put(oldCP, newCP); + } + + return baseDomainFactory.createWaypoint(newCP, pi); + } + + private void revokeAlreadyDefinedMarks(RaceLog raceLog, RaceLogEventAuthor author) { + List markEvents = new AllEventsOfTypeFinder(raceLog, true, DefineMarkEvent.class).analyze(); + for (RaceLogEvent event : markEvents) { + try { + raceLog.revokeEvent(author, event); + } catch (NotRevokableException e) {} + } + } + + @Override + public void copyCourseAndCompetitors(RaceLog fromRaceLog, Set toRaceLogs, SharedDomainFactory baseDomainFactory, + RacingEventService service) { + CourseBase course = new LastPublishedCourseDesignFinder(fromRaceLog).analyze(); + Set competitors = new RegisteredCompetitorsAnalyzer(fromRaceLog).analyze(); + + for (RaceLog toRaceLog : toRaceLogs) { + CourseBase to = new CourseDataImpl("Copy of \"" + course.getName()); + TimePoint now = MillisecondsTimePoint.now(); + if (course == null || ! new RaceLogTrackingStateAnalyzer(toRaceLog).analyze().isForTracking()) { + continue; + } + int i = 0; + Map controlPointDuplicationCache = new HashMap(); + revokeAlreadyDefinedMarks(toRaceLog, service.getServerAuthor()); + Map markDuplicationCache = new HashMap(); + for (Waypoint oldWaypoint : course.getWaypoints()) { + to.addWaypoint(i++, duplicateWaypoint(oldWaypoint, controlPointDuplicationCache, markDuplicationCache, baseDomainFactory)); + } + + for (Mark mark : markDuplicationCache.values()) { + RaceLogEvent event = RaceLogEventFactory.INSTANCE.createDefineMarkEvent(now, service.getServerAuthor(), + toRaceLog.getCurrentPassId(), mark); + toRaceLog.add(event); + } + + int passId = toRaceLog.getCurrentPassId(); + + RaceLogEvent newCourseEvent = RaceLogEventFactory.INSTANCE.createCourseDesignChangedEvent( + now, service.getServerAuthor(), passId, to); + toRaceLog.add(newCourseEvent); + + registerCompetitors(service, toRaceLog, competitors); + } + } + + @Override + public void pingMark(RaceLog raceLog, Mark mark, GPSFix gpsFix, RacingEventService service) { + DeviceIdentifier device = new PingDeviceIdentifierImpl(); + TimePoint time = gpsFix.getTimePoint(); + + RaceLogEvent mapping = RaceLogEventFactory.INSTANCE.createDeviceMarkMappingEvent(time, + service.getServerAuthor(), device, mark, raceLog.getCurrentPassId(), time, time); + raceLog.add(mapping); + service.getGPSFixStore().storeFix(device, gpsFix); + } + + @Override + public void removeDenotationForRaceLogTracking(RacingEventService service, RaceLog raceLog) { + Revokable denoteForTrackingEvent = (Revokable) new LastEventOfTypeFinder(raceLog, true, DenoteForTrackingEvent.class).analyze(); + Revokable startTrackingEvent = (Revokable) new LastEventOfTypeFinder(raceLog, true, StartTrackingEvent.class).analyze(); + try { + raceLog.revokeEvent(service.getServerAuthor(), denoteForTrackingEvent); + raceLog.revokeEvent(service.getServerAuthor(), startTrackingEvent); + } catch (NotRevokableException e) {} + } + + @Override + public void registerCompetitors(RacingEventService service, RaceLog raceLog, Set competitors) { + Set alreadyRegistered = new HashSet(new RegisteredCompetitorsAnalyzer(raceLog).analyze()); + Set toBeRegistered = new HashSet(); + + for (Competitor c : competitors) { + toBeRegistered.add(c); + } + + Set toBeRemoved = new HashSet(alreadyRegistered); + toBeRemoved.removeAll(toBeRegistered); + toBeRegistered.removeAll(alreadyRegistered); + + //register + for (Competitor c : toBeRegistered) { + raceLog.add(RaceLogEventFactory.INSTANCE.createRegisterCompetitorEvent(MillisecondsTimePoint.now(), + service.getServerAuthor(), raceLog.getCurrentPassId(), c)); + } + + //unregister + for (RaceLogEvent event : raceLog.getUnrevokedEventsDescending()) { + if (event instanceof RegisterCompetitorEvent) { + RegisterCompetitorEvent registerEvent = (RegisterCompetitorEvent) event; + if (toBeRemoved.contains(registerEvent.getCompetitor())) { + try { + raceLog.revokeEvent(service.getServerAuthor(), (RegisterCompetitorEvent) event); + } catch (NotRevokableException e) {} + } + } + } + } +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/SmartphoneUUIDMongoHandler.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/SmartphoneUUIDMongoHandler.java new file mode 100644 index 00000000000..00c500d4baa --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/SmartphoneUUIDMongoHandler.java @@ -0,0 +1,17 @@ +package com.sap.sailing.domain.racelogtracking.impl; + +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.persistence.racelog.tracking.DeviceIdentifierMongoHandler; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.SmartphoneUUIDSerializationHandler; + +public class SmartphoneUUIDMongoHandler extends SmartphoneUUIDSerializationHandler +implements DeviceIdentifierMongoHandler { + + @Override + public DeviceIdentifier deserialize(Object serialized, String type, String stringRepresentation) + throws TransformationException { + return deserialize((String) serialized, type, stringRepresentation); + } + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/SmartphoneUUIDStringSerializationHandler.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/SmartphoneUUIDStringSerializationHandler.java new file mode 100644 index 00000000000..9f47a25f841 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/SmartphoneUUIDStringSerializationHandler.java @@ -0,0 +1,9 @@ +package com.sap.sailing.domain.racelogtracking.impl; + +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifierStringSerializationHandler; +import com.sap.sailing.domain.racelog.tracking.SmartphoneUUIDSerializationHandler; + +public class SmartphoneUUIDStringSerializationHandler extends SmartphoneUUIDSerializationHandler +implements DeviceIdentifierStringSerializationHandler { + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/TrackFileImportDeviceIdentifierJsonHandler.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/TrackFileImportDeviceIdentifierJsonHandler.java new file mode 100644 index 00000000000..43fd52957d6 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/TrackFileImportDeviceIdentifierJsonHandler.java @@ -0,0 +1,68 @@ +package com.sap.sailing.domain.racelogtracking.impl; + +import java.util.UUID; + +import org.json.simple.JSONObject; + +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.TimeRange; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.impl.TimeRangeImpl; +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.server.gateway.serialization.racelog.tracking.DeviceIdentifierJsonHandler; +import com.sap.sailing.server.gateway.trackfiles.TrackFileImportDeviceIdentifier; +import com.sap.sailing.server.gateway.trackfiles.TrackFileImportDeviceIdentifierImpl; + +public class TrackFileImportDeviceIdentifierJsonHandler implements DeviceIdentifierJsonHandler { + private static enum Fields {UUID, FILE_NAME, TRACK_NAME, UPLOADED_MILLIS, FROM_MILLIS, TO_MILLIS, NUM_FIXES}; + + public static TimePoint loadTimePoint(Object object) { + if (object == null) { + return null; + } + return new MillisecondsTimePoint(((Number) object).longValue()); + } + + public static Long storeTimePoint(TimePoint timePoint) { + if (timePoint == null) { + return null; + } + return timePoint.asMillis(); + } + + @Override + public DeviceIdentifier deserialize(Object serialized, String type, String stringRepresentation) + throws TransformationException { + if (serialized == null) { + throw new TransformationException("Received nothing to deserialize"); + } + JSONObject json = (JSONObject) serialized; + UUID uuid = UUID.fromString((String) json.get(Fields.UUID.name())); + String fileName = (String) json.get(Fields.FILE_NAME.name()); + String trackName = (String) json.get(Fields.TRACK_NAME.name()); + TimePoint uploaded = loadTimePoint(json.get(Fields.UPLOADED_MILLIS.name())); + TimePoint from = loadTimePoint(json.get(Fields.FROM_MILLIS.name())); + TimePoint to = loadTimePoint(json.get(Fields.TO_MILLIS.name())); + TimeRange timeRange = from != null && to != null ? new TimeRangeImpl(from, to) : null; + Long numberOfFixes = (Long) json.get(Fields.NUM_FIXES.name()); + return new TrackFileImportDeviceIdentifierImpl(uuid, fileName, trackName, uploaded, timeRange, + numberOfFixes == null ? -1 : numberOfFixes); + } + + @Override + public Pair serialize(DeviceIdentifier deviceIdentifier) throws TransformationException { + TrackFileImportDeviceIdentifier id = TrackFileImportDeviceIdentifierImpl.cast(deviceIdentifier); + JSONObject result = new JSONObject(); + result.put(Fields.UUID.name(), id.getId().toString()); + result.put(Fields.FILE_NAME.name(), id.getFileName()); + result.put(Fields.TRACK_NAME.name(), id.getTrackName()); + result.put(Fields.UPLOADED_MILLIS.name(), storeTimePoint(id.getUploadedAt())); + result.put(Fields.FROM_MILLIS.name(), storeTimePoint(id.getFixesTimeRange().from())); + result.put(Fields.TO_MILLIS.name(), storeTimePoint(id.getFixesTimeRange().to())); + result.put(Fields.NUM_FIXES.name(), id.getNumberOfFixes()); + return new Pair(TrackFileImportDeviceIdentifier.TYPE, result); + } + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/TrackFileImportDeviceIdentifierMongoHandler.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/TrackFileImportDeviceIdentifierMongoHandler.java new file mode 100644 index 00000000000..c165607f866 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/TrackFileImportDeviceIdentifierMongoHandler.java @@ -0,0 +1,56 @@ +package com.sap.sailing.domain.racelogtracking.impl; + +import java.util.UUID; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.TimeRange; +import com.sap.sailing.domain.common.impl.TimeRangeImpl; +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.persistence.racelog.tracking.DeviceIdentifierMongoHandler; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.server.gateway.trackfiles.TrackFileImportDeviceIdentifier; +import com.sap.sailing.server.gateway.trackfiles.TrackFileImportDeviceIdentifierImpl; + +public class TrackFileImportDeviceIdentifierMongoHandler implements DeviceIdentifierMongoHandler { + private static enum Fields {UUID, FILE_NAME, TRACK_NAME, UPLOADED_MILLIS, FROM_MILLIS, TO_MILLIS, NUM_FIXES}; + + @Override + public DeviceIdentifier deserialize(Object serialized, String type, String stringRepresentation) + throws TransformationException { + DBObject dbObject = (DBObject) serialized; + UUID uuid = UUID.fromString((String) dbObject.get(Fields.UUID.name())); + String fileName = (String) dbObject.get(Fields.FILE_NAME.name()); + String trackName = (String) dbObject.get(Fields.TRACK_NAME.name()); + TimePoint uploaded = TrackFileImportDeviceIdentifierJsonHandler. + loadTimePoint(dbObject.get(Fields.UPLOADED_MILLIS.name())); + TimePoint from = TrackFileImportDeviceIdentifierJsonHandler. + loadTimePoint(dbObject.get(Fields.FROM_MILLIS.name())); + TimePoint to = TrackFileImportDeviceIdentifierJsonHandler. + loadTimePoint(dbObject.get(Fields.TO_MILLIS.name())); + TimeRange timeRange = from != null && to != null ? new TimeRangeImpl(from, to) : null; + Long numberOfFixes = (Long) dbObject.get(Fields.NUM_FIXES.name()); + return new TrackFileImportDeviceIdentifierImpl(uuid, fileName, trackName, uploaded, timeRange, + numberOfFixes == null ? -1 : numberOfFixes); + } + + @Override + public Pair serialize(DeviceIdentifier deviceIdentifier) throws TransformationException { + TrackFileImportDeviceIdentifier id = TrackFileImportDeviceIdentifierImpl.cast(deviceIdentifier); + DBObject result = new BasicDBObject(); + result.put(Fields.UUID.name(), id.getId().toString()); + result.put(Fields.FILE_NAME.name(), id.getFileName()); + result.put(Fields.TRACK_NAME.name(), id.getTrackName()); + result.put(Fields.UPLOADED_MILLIS.name(), TrackFileImportDeviceIdentifierJsonHandler. + storeTimePoint(id.getUploadedAt())); + result.put(Fields.FROM_MILLIS.name(), TrackFileImportDeviceIdentifierJsonHandler. + storeTimePoint(id.getFixesTimeRange().from())); + result.put(Fields.TO_MILLIS.name(), TrackFileImportDeviceIdentifierJsonHandler. + storeTimePoint(id.getFixesTimeRange().to())); + result.put(Fields.NUM_FIXES.name(), id.getNumberOfFixes()); + return new Pair(TrackFileImportDeviceIdentifier.TYPE, result); + } + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/TrackFileImportDeviceIdentifierStringSerializationHandler.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/TrackFileImportDeviceIdentifierStringSerializationHandler.java new file mode 100644 index 00000000000..4e09e829a38 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/TrackFileImportDeviceIdentifierStringSerializationHandler.java @@ -0,0 +1,31 @@ +package com.sap.sailing.domain.racelogtracking.impl; + +import java.util.UUID; + +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifierStringSerializationHandler; +import com.sap.sailing.server.gateway.trackfiles.TrackFileImportDeviceIdentifier; +import com.sap.sailing.server.gateway.trackfiles.TrackFileImportDeviceIdentifierImpl; + +public class TrackFileImportDeviceIdentifierStringSerializationHandler implements + DeviceIdentifierStringSerializationHandler { + + @Override + public Pair serialize(DeviceIdentifier deviceIdentifier) throws TransformationException { + TrackFileImportDeviceIdentifier id = TrackFileImportDeviceIdentifierImpl.cast(deviceIdentifier); + return new Pair(TrackFileImportDeviceIdentifier.TYPE, id.getStringRepresentation()); + } + + @Override + /** + * Only uses the first 36 characters in the string to construct the UUID, ignoring file, track and date info. + */ + public DeviceIdentifier deserialize(String serialized, String type, String stringRepresentation) + throws TransformationException { + UUID uuid = UUID.fromString(serialized.substring(0, 36)); + return TrackFileImportDeviceIdentifierImpl.getOrCreate(uuid); + } + +} diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/servlet/RecordFixesPostServlet.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/servlet/RecordFixesPostServlet.java new file mode 100644 index 00000000000..ee12da74fe0 --- /dev/null +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/servlet/RecordFixesPostServlet.java @@ -0,0 +1,63 @@ +package com.sap.sailing.domain.racelogtracking.servlet; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; + +import com.sap.sailing.domain.common.impl.Util.Triple; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinder; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.server.gateway.AbstractJsonPostServlet; +import com.sap.sailing.server.gateway.HttpExceptionWithMessage; +import com.sap.sailing.server.gateway.deserialization.JsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.DeviceAndSessionIdentifierWithGPSFixesDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.DeviceIdentifierJsonDeserializer; +import com.sap.sailing.server.gateway.serialization.JsonSerializer; +import com.sap.sailing.server.gateway.serialization.racelog.tracking.DeviceIdentifierJsonHandler; +import com.sap.sailing.server.gateway.serialization.racelog.tracking.GPSFixJsonHandler; + +public class RecordFixesPostServlet extends + AbstractJsonPostServlet>, Void> { + private static final long serialVersionUID = 2778739335260621119L; + private DeviceAndSessionIdentifierWithGPSFixesDeserializer deserializer; + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + TypeBasedServiceFinder fixServiceFinder = getServiceFinderFactory().createServiceFinder( + GPSFixJsonHandler.class); + TypeBasedServiceFinder deviceServiceFinder = getServiceFinderFactory() + .createServiceFinder(DeviceIdentifierJsonHandler.class); + deserializer = new DeviceAndSessionIdentifierWithGPSFixesDeserializer(fixServiceFinder, + new DeviceIdentifierJsonDeserializer(deviceServiceFinder)); + } + + @Override + public JsonDeserializer>> getRequestDeserializer() { + return deserializer; + } + + @Override + public JsonSerializer getResponseSerializer() { + return null; + } + + @Override + public Void process(Map parameterValues, + Triple> domainObject) throws HttpExceptionWithMessage { + DeviceIdentifier device = domainObject.getA(); + // might use the session id in the future + // Serializable sessionId = domainObject.getB(); + List fixes = domainObject.getC(); + + for (GPSFix fix : fixes) { + getService().getGPSFixStore().storeFix(device, fix); + } + + return null; + } +} diff --git a/java/com.sap.sailing.domain.shared.android.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.shared.android.test/META-INF/MANIFEST.MF index c0155eeec7d..7f1fd88c57b 100644 --- a/java/com.sap.sailing.domain.shared.android.test/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.shared.android.test/META-INF/MANIFEST.MF @@ -10,4 +10,6 @@ Require-Bundle: com.sap.sailing.domain.common;bundle-version="1.0.0", org.junit4;bundle-version="4.8.2", org.objenesis;bundle-version="1.3.0", org.hamcrest;bundle-version="1.1.0", - org.mockito.mockito-core;bundle-version="1.9.5" + org.mockito.mockito-core;bundle-version="1.9.5", + com.sap.sailing.domain.test, + com.sap.sailing.domain.racelogtrackingadapter.testsupport;bundle-version="1.0.0" diff --git a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/test/SerializeRaceLogEventsTest.java b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/test/SerializeRaceLogEventsTest.java new file mode 100755 index 00000000000..d63f926d881 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/test/SerializeRaceLogEventsTest.java @@ -0,0 +1,106 @@ +package com.sap.sailing.domain.racelog.test; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.impl.Util; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEventFactory; +import com.sap.sailing.domain.racelog.RaceLogStartTimeEvent; +import com.sap.sailing.domain.racelog.impl.RaceLogEventAuthorImpl; +import com.sap.sailing.domain.racelog.impl.RaceLogImpl; +import com.sap.sailing.domain.racelog.tracking.DeviceCompetitorMappingEvent; +import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifier; +import com.sap.sailing.domain.test.AbstractLeaderboardTest; + +/** + * Perform a few tests around race log and race log event serialization / deserialization + * + * @author Axel Uhl (d043530) + * + */ +public class SerializeRaceLogEventsTest { + private ObjectOutputStream oos; + private ByteArrayOutputStream bos; + private RaceLog raceLog; + private RaceLogEventFactory eventFactory; + + @Before + public void setUp() throws IOException { + bos = new ByteArrayOutputStream(); + oos = new ObjectOutputStream(bos); + raceLog = new RaceLogImpl("Test Race Log"); + eventFactory = RaceLogEventFactory.INSTANCE; + } + + @Test + public void testSimpleRaceLogSerialization() throws IOException, ClassNotFoundException { + oos.writeObject(raceLog); + ObjectInputStream ois = getObjectInputStream(); + RaceLog rl = (RaceLog) ois.readObject(); + raceLog.lockForRead(); + rl.lockForRead(); + try { + assertEquals(Util.size(raceLog.getRawFixes()), Util.size(rl.getRawFixes())); + } finally { + rl.unlockAfterRead(); + raceLog.unlockAfterRead(); + } + } + + @Test + public void testRaceLogSerializationWithSingleEvent() throws IOException, ClassNotFoundException { + RaceLogStartTimeEvent startTimeEvent = eventFactory.createStartTimeEvent(MillisecondsTimePoint.now(), + new RaceLogEventAuthorImpl("Author Name", /* priority */0), /* passId */1, /* startTime */ + MillisecondsTimePoint.now()); + raceLog.add(startTimeEvent); + oos.writeObject(raceLog); + ObjectInputStream ois = getObjectInputStream(); + RaceLog rl = (RaceLog) ois.readObject(); + raceLog.lockForRead(); + rl.lockForRead(); + try { + assertEquals(Util.size(raceLog.getRawFixes()), Util.size(rl.getRawFixes())); + assertEquals(((RaceLogStartTimeEvent) raceLog.getFirstRawFix()).getStartTime(), ((RaceLogStartTimeEvent) rl.getFirstRawFix()).getStartTime()); + } finally { + rl.unlockAfterRead(); + raceLog.unlockAfterRead(); + } + } + + @Test + public void testRaceLogSerializationWithEventContainingDeviceIdentifier() throws IOException, ClassNotFoundException { + DeviceCompetitorMappingEvent mappingEvent = eventFactory.createDeviceCompetitorMappingEvent( + MillisecondsTimePoint.now(), new RaceLogEventAuthorImpl("Author Name", /* priority */0), new SmartphoneImeiIdentifier("1948364938463903"), + AbstractLeaderboardTest.createCompetitor("Test Competitor"), 0, new MillisecondsTimePoint(0), new MillisecondsTimePoint(10)); + raceLog.add(mappingEvent); + oos.writeObject(raceLog); + ObjectInputStream ois = getObjectInputStream(); + RaceLog rl = (RaceLog) ois.readObject(); + raceLog.lockForRead(); + rl.lockForRead(); + try { + DeviceCompetitorMappingEvent expected = ((DeviceCompetitorMappingEvent) raceLog.getFirstRawFix()); + DeviceCompetitorMappingEvent actual = ((DeviceCompetitorMappingEvent) rl.getFirstRawFix()); + + assertEquals(Util.size(raceLog.getRawFixes()), Util.size(rl.getRawFixes())); + assertEquals(expected.getDevice().getIdentifierType(), actual.getDevice().getIdentifierType()); + } finally { + rl.unlockAfterRead(); + raceLog.unlockAfterRead(); + } + } + + private ObjectInputStream getObjectInputStream() throws IOException { + return new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); + } +} diff --git a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/tracking/analyzing/test/AbstractRaceLogTrackingTest.java b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/tracking/analyzing/test/AbstractRaceLogTrackingTest.java new file mode 100644 index 00000000000..3a42509c476 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/tracking/analyzing/test/AbstractRaceLogTrackingTest.java @@ -0,0 +1,31 @@ +package com.sap.sailing.domain.racelog.tracking.analyzing.test; + +import org.junit.Before; + +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventRestoreFactory; +import com.sap.sailing.domain.racelog.impl.RaceLogEventAuthorImpl; +import com.sap.sailing.domain.racelog.impl.RaceLogImpl; + +public abstract class AbstractRaceLogTrackingTest { + + protected RaceLog log; + protected final RaceLogEventAuthor author = new RaceLogEventAuthorImpl("author0", 0); + protected final RaceLogEventAuthor author1 = new RaceLogEventAuthorImpl("author1", 1); + + protected final TimePoint now = MillisecondsTimePoint.now(); + protected final RaceLogEventRestoreFactory factory = RaceLogEventRestoreFactory.INSTANCE; + + public AbstractRaceLogTrackingTest() { + super(); + } + + @Before + public void setup() { + log = new RaceLogImpl("log"); + } + +} \ No newline at end of file diff --git a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/tracking/analyzing/test/DeviceMappingFinderTest.java b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/tracking/analyzing/test/DeviceMappingFinderTest.java new file mode 100644 index 00000000000..9a561e23d91 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/tracking/analyzing/test/DeviceMappingFinderTest.java @@ -0,0 +1,96 @@ +package com.sap.sailing.domain.racelog.tracking.analyzing.test; + +import static org.junit.Assert.assertEquals; + +import java.io.Serializable; +import java.util.List; +import java.util.UUID; + +import junit.framework.Assert; + +import org.junit.Test; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.impl.CompetitorImpl; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.DeviceMapping; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.DeviceCompetitorMappingFinder; +import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifier; + +public class DeviceMappingFinderTest extends AbstractRaceLogTrackingTest { + private final Competitor competitor = new CompetitorImpl("comp", "Comp", null, null, null); + private final Competitor competitor2 = new CompetitorImpl("comp2", "Comp2", null, null, null); + private final DeviceIdentifier device = new SmartphoneImeiIdentifier("imei"); + + private int time = 0; + + private List> getMappings() { + return new DeviceCompetitorMappingFinder(log).analyze().get(competitor); + } + + private TimePoint t() { + return new MillisecondsTimePoint(time++); + } + + private TimePoint t(Long millis) { + return millis == null ? null : new MillisecondsTimePoint(millis); + } + + private Serializable addMapping(RaceLogEventAuthor author, DeviceIdentifier device, Long from, Long to, Competitor item) { + RaceLogEvent mapping = factory.createDeviceCompetitorMappingEvent(t(), author, t(), UUID.randomUUID(), device, item, 0, + t(from), t(to)); + log.add(mapping); + return mapping.getId(); + } + + private void closeMapping(RaceLogEventAuthor author, DeviceIdentifier device, Serializable mappingId, long millis) { + RaceLogEvent mapping = factory.createCloseOpenEndedDeviceMappingEvent(t(), author, t(), UUID.randomUUID(), 0, mappingId, + new MillisecondsTimePoint(millis)); + log.add(mapping); + } + + @Test + public void notDisturbedByMappingsForOtherItemsAndDevices() { + //two mappings for first competitor found? (competitor2 mappings should not "distract") + addMapping(author, device, 10L, 40L, competitor); + addMapping(author, device, 10L, 40L, competitor2); + addMapping(author, device, 20L, 30L, competitor2); + addMapping(author, device, 50L, 90L, competitor); + Assert.assertEquals(2, getMappings().size()); + + //non-overlap with new device + addMapping(author, new SmartphoneImeiIdentifier("imei2"), 110L, 150L, competitor); + Assert.assertEquals(3, getMappings().size()); + } + + @Test + public void openRangesAreClosed() { + //close one range + Serializable id = addMapping(author, device, 10L, null, competitor); + closeMapping(author, device, id, 20); + List> mappings = getMappings(); + assertEquals(1, mappings.size()); + DeviceMapping mapping = mappings.get(0); + assertEquals(10, mapping.getTimeRange().from().asMillis()); + assertEquals(20, mapping.getTimeRange().to().asMillis()); + + //another independent range + id = addMapping(author, device, 0L, null, competitor); + closeMapping(author, device, id, 5); + mappings = getMappings(); + assertEquals(2, mappings.size()); + + //another closing mapping, that should take precedence (because added later) + closeMapping(author, device, id, 3); + assertEquals(2, getMappings().size()); + assertEquals(3, getMappings().get(1).getTimeRange().to().asMillis()); + + //open-ended at end, not closed + addMapping(author, device, 30L, null, competitor); + assertEquals(3, getMappings().size()); + } +} diff --git a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/tracking/analyzing/test/RevokeEventTest.java b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/tracking/analyzing/test/RevokeEventTest.java new file mode 100644 index 00000000000..faa9531d0f9 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/tracking/analyzing/test/RevokeEventTest.java @@ -0,0 +1,55 @@ +package com.sap.sailing.domain.racelog.tracking.analyzing.test; + +import java.util.UUID; + +import junit.framework.Assert; + +import org.junit.Test; + +import com.sap.sailing.domain.common.impl.Util; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.RevokeEvent; + +public class RevokeEventTest extends AbstractRaceLogTrackingTest { + @Test + public void revokedEventIsExcluded() { + RaceLogEvent event = factory.createDeviceCompetitorMappingEvent(now, author, now, UUID.randomUUID(), null, null, 0, null, null); + RevokeEvent revokeEvent = factory.createRevokeEvent(now.plus(1), author, now.plus(1), UUID.randomUUID(), 0, event.getId()); + + log.add(event); + log.add(revokeEvent); + + log.lockForRead(); + Assert.assertFalse(Util.contains(log.getUnrevokedEvents(), event)); + log.unlockAfterRead(); + } + + @Test + public void eventRevokedByAuthorWithLowerPrioIsNotExcluded() { + RaceLogEvent event = factory.createDeviceCompetitorMappingEvent(now, author, now, UUID.randomUUID(), null, null, 0, null, null); + RevokeEvent revokeEvent = factory.createRevokeEvent(now.plus(1), author1, now.plus(1), UUID.randomUUID(), 0, event.getId()); + + log.add(event); + log.add(revokeEvent); + + log.lockForRead(); + Assert.assertTrue(Util.contains(log.getUnrevokedEvents(), event)); + log.unlockAfterRead(); + } + + @Test + public void revokingRevokeEventIsNotHarmful() { + RaceLogEvent event = factory.createDeviceCompetitorMappingEvent(now, author, now, UUID.randomUUID(), null, null, 0, null, null); + RevokeEvent revokeEvent = factory.createRevokeEvent(now.plus(1), author, now.plus(1), UUID.randomUUID(), 0, event.getId()); + RevokeEvent revokeEvent2 = factory.createRevokeEvent(now.plus(2), author, now.plus(2), UUID.randomUUID(), 2, revokeEvent.getId()); + + log.add(event); + log.add(revokeEvent); + + log.lockForRead(); + Assert.assertFalse(Util.contains(log.getUnrevokedEvents(), event)); + Assert.assertFalse(Util.contains(log.getUnrevokedEvents(), revokeEvent)); + Assert.assertFalse(Util.contains(log.getUnrevokedEvents(), revokeEvent2)); + log.unlockAfterRead(); + } +} diff --git a/java/com.sap.sailing.domain.shared.android/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.shared.android/META-INF/MANIFEST.MF index d4dca443728..00af8cb8d4b 100644 --- a/java/com.sap.sailing.domain.shared.android/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.shared.android/META-INF/MANIFEST.MF @@ -26,6 +26,10 @@ Export-Package: com.sap.sailing.domain.base, com.sap.sailing.domain.racelog.state.racingprocedure.impl, com.sap.sailing.domain.racelog.state.racingprocedure.rrs26, com.sap.sailing.domain.racelog.state.racingprocedure.rrs26.impl, + com.sap.sailing.domain.racelog.tracking, + com.sap.sailing.domain.racelog.tracking.analyzing.impl, + com.sap.sailing.domain.racelog.tracking.events, + com.sap.sailing.domain.racelog.tracking.impl, com.sap.sailing.domain.tracking, com.sap.sailing.domain.tracking.impl, com.sap.sailing.util.impl diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/BoatChangeListener.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/BoatChangeListener.java new file mode 100755 index 00000000000..cc86744e89b --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/BoatChangeListener.java @@ -0,0 +1,5 @@ +package com.sap.sailing.domain.base; + +public interface BoatChangeListener { + void sailIdChanged(String oldSailId, String newSailId); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/Competitor.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/Competitor.java index b078bf8162c..0a0d508cdc2 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/Competitor.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/Competitor.java @@ -10,4 +10,17 @@ public interface Competitor extends Named, WithID, IsManagedBySharedDomainFactor Boat getBoat(); Color getColor(); + + /** + * Adds a listener to this competitor. The listener is also added to the boat and the team for changes. + * Adding a listener that is already part of this competitor's listeners set remains without effect. + */ + void addCompetitorChangeListener(CompetitorChangeListener listener); + + /** + * Removes a listener from this competitor. The listener is also removed from the boat and the team. + * Trying to remove a listener that is not currently in the set of this competitor's listener remains + * without effect. + */ + void removeCompetitorChangeListener(CompetitorChangeListener listener); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/CompetitorChangeListener.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/CompetitorChangeListener.java new file mode 100755 index 00000000000..f5a0b722976 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/CompetitorChangeListener.java @@ -0,0 +1,9 @@ +package com.sap.sailing.domain.base; + +import com.sap.sailing.domain.common.Color; + +public interface CompetitorChangeListener extends BoatChangeListener, NationalityChangeListener { + void colorChanged(Color oldColor, Color newColor); + + void nameChanged(String oldName, String newName); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/NationalityChangeListener.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/NationalityChangeListener.java new file mode 100755 index 00000000000..04171c3b454 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/NationalityChangeListener.java @@ -0,0 +1,7 @@ +package com.sap.sailing.domain.base; + +import java.io.Serializable; + +public interface NationalityChangeListener extends Serializable { + void nationalityChanged(WithNationality what, Nationality oldNationality, Nationality newNationality); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/SharedDomainFactory.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/SharedDomainFactory.java index 7df5596b864..ac14d39c9a2 100755 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/SharedDomainFactory.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/SharedDomainFactory.java @@ -95,4 +95,8 @@ public interface SharedDomainFactory extends CompetitorFactory { * Hm. */ DeviceConfigurationMatcher getOrCreateDeviceConfigurationMatcher(DeviceConfigurationMatcherType type, List clientIdentifiers); + + Mark getExistingMarkByIdAsString(String toStringRepresentationOfID); + + Mark getExistingMarkById(Serializable id); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/BoatImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/BoatImpl.java index a16c72f28e1..1d9fc3bbb25 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/BoatImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/BoatImpl.java @@ -1,17 +1,31 @@ package com.sap.sailing.domain.base.impl; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.HashSet; +import java.util.Set; + +import com.sap.sailing.domain.base.BoatChangeListener; import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.common.impl.NamedImpl; +import com.sap.sailing.domain.common.impl.Util; public class BoatImpl extends NamedImpl implements DynamicBoat { private static final long serialVersionUID = 3489730487528955788L; private final BoatClass boatClass; private String sailID; + private transient Set listeners; public BoatImpl(String name, BoatClass boatClass, String sailID) { super(name); this.boatClass = boatClass; this.sailID = sailID; + this.listeners = new HashSet(); + } + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + ois.defaultReadObject(); + listeners = new HashSet(); } @Override @@ -26,6 +40,32 @@ public class BoatImpl extends NamedImpl implements DynamicBoat { @Override public void setSailId(String newSailId) { - this.sailID = newSailId; + final String oldSailId = this.sailID; + if (!Util.equalsWithNull(oldSailId, newSailId)) { + this.sailID = newSailId; + for (BoatChangeListener listener : getListeners()) { + listener.sailIdChanged(oldSailId, newSailId); + } + } + } + + @Override + public void addBoatChangeListener(BoatChangeListener listener) { + synchronized (listeners) { + listeners.add(listener); + } + } + + @Override + public void removeCompetitorChangeListener(BoatChangeListener listener) { + synchronized (listeners) { + listeners.remove(listener); + } + } + + private Iterable getListeners() { + synchronized (listeners) { + return new HashSet(listeners); + } } } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/CompetitorImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/CompetitorImpl.java index 38ed945fc75..3426e4d22ac 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/CompetitorImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/CompetitorImpl.java @@ -1,10 +1,16 @@ package com.sap.sailing.domain.base.impl; +import java.io.IOException; +import java.io.ObjectInputStream; import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CompetitorChangeListener; import com.sap.sailing.domain.base.SharedDomainFactory; import com.sap.sailing.domain.common.Color; +import com.sap.sailing.domain.common.impl.Util; public class CompetitorImpl implements DynamicCompetitor { private static final long serialVersionUID = 294603681016643157L; @@ -12,8 +18,8 @@ public class CompetitorImpl implements DynamicCompetitor { private final DynamicBoat boat; private final Serializable id; private String name; - private Color color; + private transient Set listeners; public CompetitorImpl(Serializable id, String name, Color color, DynamicTeam team, DynamicBoat boat) { this.id = id; @@ -21,6 +27,12 @@ public class CompetitorImpl implements DynamicCompetitor { this.team = team; this.boat = boat; this.color = color; + this.listeners = new HashSet(); + } + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + ois.defaultReadObject(); + listeners = new HashSet(); } @Override @@ -35,7 +47,13 @@ public class CompetitorImpl implements DynamicCompetitor { @Override public void setName(String newName) { - this.name = newName; + final String oldName = this.name; + if (!Util.equalsWithNull(oldName, newName)) { + this.name = newName; + for (CompetitorChangeListener listener : getListeners()) { + listener.nameChanged(oldName, newName); + } + } } @Override @@ -64,6 +82,36 @@ public class CompetitorImpl implements DynamicCompetitor { } public void setColor(Color color) { - this.color = color; + final Color oldColor = this.color; + if (!Util.equalsWithNull(oldColor, color)) { + this.color = color; + for (CompetitorChangeListener listener : getListeners()) { + listener.colorChanged(oldColor, color); + } + } + } + + @Override + public void addCompetitorChangeListener(CompetitorChangeListener listener) { + synchronized (listeners) { + listeners.add(listener); + } + getBoat().addBoatChangeListener(listener); + getTeam().addNationalityChangeListener(listener); + } + + @Override + public void removeCompetitorChangeListener(CompetitorChangeListener listener) { + synchronized (listeners) { + listeners.remove(listener); + } + getBoat().removeCompetitorChangeListener(listener); + getTeam().removeNationalityChangeListener(listener); + } + + private Iterable getListeners() { + synchronized (listeners) { + return new HashSet(listeners); + } } } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/DynamicBoat.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/DynamicBoat.java index 0b4e8f1e278..b3131fc9e4c 100755 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/DynamicBoat.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/DynamicBoat.java @@ -1,7 +1,13 @@ package com.sap.sailing.domain.base.impl; import com.sap.sailing.domain.base.Boat; +import com.sap.sailing.domain.base.BoatChangeListener; public interface DynamicBoat extends Boat { void setSailId(String newSailId); + + void addBoatChangeListener(BoatChangeListener listener); + + void removeCompetitorChangeListener(BoatChangeListener listener); + } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/DynamicPerson.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/DynamicPerson.java index d5d13c3b45c..231192564e6 100755 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/DynamicPerson.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/DynamicPerson.java @@ -1,8 +1,13 @@ package com.sap.sailing.domain.base.impl; import com.sap.sailing.domain.base.Nationality; +import com.sap.sailing.domain.base.NationalityChangeListener; import com.sap.sailing.domain.base.Person; public interface DynamicPerson extends Person { void setNationality(Nationality newNationality); + + void addNationalityChangeListener(NationalityChangeListener listener); + + void removeNationalityChangeListener(NationalityChangeListener listener); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/DynamicTeam.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/DynamicTeam.java index 807b159842a..fb0e14ba7a2 100755 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/DynamicTeam.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/DynamicTeam.java @@ -1,8 +1,13 @@ package com.sap.sailing.domain.base.impl; import com.sap.sailing.domain.base.Nationality; +import com.sap.sailing.domain.base.NationalityChangeListener; import com.sap.sailing.domain.base.Team; public interface DynamicTeam extends Team { void setNationality(Nationality newNationality); + + void addNationalityChangeListener(NationalityChangeListener listener); + + void removeNationalityChangeListener(NationalityChangeListener listener); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/PersonImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/PersonImpl.java index 48115fd27f7..87297b31df3 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/PersonImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/PersonImpl.java @@ -1,24 +1,37 @@ package com.sap.sailing.domain.base.impl; +import java.io.IOException; import java.io.InputStream; +import java.io.ObjectInputStream; import java.util.Date; +import java.util.HashSet; +import java.util.Set; import com.sap.sailing.domain.base.Nationality; +import com.sap.sailing.domain.base.NationalityChangeListener; import com.sap.sailing.domain.common.impl.NamedImpl; +import com.sap.sailing.domain.common.impl.Util; public class PersonImpl extends NamedImpl implements DynamicPerson { private static final long serialVersionUID = -2104903799224233508L; private Nationality nationality; private final Date dateOfBirth; private final String description; + private transient Set listeners; public PersonImpl(String name, Nationality nationality, Date dateOfBirth, String description) { super(name); this.nationality = nationality; this.dateOfBirth = dateOfBirth; this.description = description; + this.listeners = new HashSet(); } + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + ois.defaultReadObject(); + listeners = new HashSet(); + } + @Override public InputStream getImage() { throw new UnsupportedOperationException("Image for person not yet implemented"); @@ -31,7 +44,13 @@ public class PersonImpl extends NamedImpl implements DynamicPerson { @Override public void setNationality(Nationality newNationality) { - this.nationality = newNationality; + final Nationality oldNationality = this.nationality; + if (!Util.equalsWithNull(oldNationality, newNationality)) { + this.nationality = newNationality; + for (NationalityChangeListener listener : getListeners()) { + listener.nationalityChanged(this, oldNationality, newNationality); + } + } } @Override @@ -44,4 +63,23 @@ public class PersonImpl extends NamedImpl implements DynamicPerson { return description; } + @Override + public void addNationalityChangeListener(NationalityChangeListener listener) { + synchronized (listeners) { + listeners.add(listener); + } + } + + @Override + public void removeNationalityChangeListener(NationalityChangeListener listener) { + synchronized (listeners) { + listeners.remove(listener); + } + } + + private Iterable getListeners() { + synchronized (listeners) { + return new HashSet(listeners); + } + } } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/SharedDomainFactoryImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/SharedDomainFactoryImpl.java index 03a0c1f1858..21833028f20 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/SharedDomainFactoryImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/SharedDomainFactoryImpl.java @@ -312,5 +312,15 @@ public class SharedDomainFactoryImpl implements SharedDomainFactory { } return matcher; } + + @Override + public Mark getExistingMarkById(Serializable id) { + return markCache.get(id); + } + + @Override + public Mark getExistingMarkByIdAsString(String toStringRepresentationOfID) { + return markCache.get(markIdCache.get(toStringRepresentationOfID)); + } } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/TeamImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/TeamImpl.java index d9332f79763..f2bb400b44e 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/TeamImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/TeamImpl.java @@ -1,18 +1,80 @@ package com.sap.sailing.domain.base.impl; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.HashSet; +import java.util.Set; + import com.sap.sailing.domain.base.Nationality; +import com.sap.sailing.domain.base.NationalityChangeListener; import com.sap.sailing.domain.base.Person; +import com.sap.sailing.domain.base.WithNationality; import com.sap.sailing.domain.common.impl.NamedImpl; +import com.sap.sailing.domain.common.impl.Util; public class TeamImpl extends NamedImpl implements DynamicTeam { private static final long serialVersionUID = 4646922280429210183L; private final Iterable sailors; - private final Person coach; + private final DynamicPerson coach; + private final NationalityChangeListener personNationalityChangeForwarder; + private transient Set listeners; - public TeamImpl(String name, Iterable sailors, Person coach) { + public TeamImpl(String name, Iterable sailors, DynamicPerson coach) { super(name); + listeners = new HashSet(); this.sailors = sailors; this.coach = coach; + this.personNationalityChangeForwarder = new NationalityChangeListener() { + private static final long serialVersionUID = -6024941913105410444L; + @Override + public void nationalityChanged(WithNationality what, Nationality oldNationality, Nationality newNationality) { + if (what == getNationalityDonor()) { + for (NationalityChangeListener listener : getNationalityChangeListeners()) { + listener.nationalityChanged(TeamImpl.this, oldNationality, newNationality); + } + } + } + }; + setTransitiveListeners(); + } + + /** + * The listeners on {@link DynamicPerson} are transient. Our {@link #personNationalityChangeForwarder} needs to + * be restored after deserialization. + */ + private void setTransitiveListeners() { + if (this.sailors != null) { + for (DynamicPerson sailor : this.sailors) { + sailor.addNationalityChangeListener(personNationalityChangeForwarder); + } + } + if (this.coach != null) { + this.coach.addNationalityChangeListener(personNationalityChangeForwarder); + } + } + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + ois.defaultReadObject(); + listeners = new HashSet(); + setTransitiveListeners(); + } + + public void addNationalityChangeListener(NationalityChangeListener listener) { + synchronized (listeners) { + listeners.add(listener); + } + } + + public void removeNationalityChangeListener(NationalityChangeListener listener) { + synchronized (listeners) { + listeners.remove(listener); + } + } + + private Iterable getNationalityChangeListeners() { + synchronized (listeners) { + return new HashSet(listeners); + } } @Override @@ -21,28 +83,38 @@ public class TeamImpl extends NamedImpl implements DynamicTeam { } @Override - public Person getCoach() { + public DynamicPerson getCoach() { return coach; } @Override public Nationality getNationality() { + return getNationalityDonor().getNationality(); + } + + private WithNationality getNationalityDonor() { for (Person sailor : getSailors()) { if (sailor.getNationality() != null) { - return sailor.getNationality(); + return sailor; } } if (getCoach() != null) { - return getCoach().getNationality(); + return getCoach(); } return null; } @Override public void setNationality(Nationality newNationality) { - for (Person sailor : getSailors()) { - ((DynamicPerson) sailor).setNationality(newNationality); + Nationality oldNationality = getNationality(); + if (!Util.equalsWithNull(oldNationality, newNationality)) { + for (Person sailor : getSailors()) { + ((DynamicPerson) sailor).setNationality(newNationality); + } + for (NationalityChangeListener listener : getNationalityChangeListeners()) { + listener.nationalityChanged(this, oldNationality, newNationality); + } } } - + } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RaceLog.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RaceLog.java index f364f7cc475..3eb78ae1972 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RaceLog.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RaceLog.java @@ -1,106 +1,135 @@ -package com.sap.sailing.domain.racelog; - -import java.util.HashSet; -import java.util.UUID; - -import com.sap.sailing.domain.common.WithID; -import com.sap.sailing.domain.racelog.impl.RaceLogEventComparator; -import com.sap.sailing.domain.tracking.Track; - -/** - * Special kind of {@link Track} for recording {@link RaceLogEvent}s. - * - *

- * Keeps track of the {@link RaceLogEvent}'s pass and returns only the events of the current pass on - * {@link RaceLog#getFixes()}. Use {@link RaceLog#getRawFixes()} to receive all events in a {@link RaceLog}. - *

- * - *

- * Implementations should use the {@link RaceLogEventComparator} for sorting its content. - *

- */ -public interface RaceLog extends Track, WithID { - - public static final int DefaultPassId = 0; - - /** - * Gets the current pass id. - * - * @return the pass id. - */ - int getCurrentPassId(); - - /** - * Adds a {@link RaceLogEvent} to the {@link RaceLog}. - * - * @param event - * {@link RaceLogEvent} to be added. - * @return true if the element was added, false otherwise. - */ - boolean add(RaceLogEvent event); - - /** - * Add a {@link RaceLogEventVisitor} as a listener for additions. - */ - void addListener(RaceLogEventVisitor listener); - - /** - * Remove a listener. - */ - void removeListener(RaceLogEventVisitor listener); - - /** - * Removes all listeners - * @return - */ - HashSet removeAllListeners(); - - /** - * Checks if the race log is empty. - */ - boolean isEmpty(); - - Iterable getRawFixesDescending(); - - Iterable getFixesDescending(); - - void addAllListeners(HashSet listeners); - - Iterable getAllListeners(); - - /** - * Adds an event to this race log and returns {@link RaceLog#getEventsToDeliver(UUID)} - * (excluding the new event) - */ - Iterable add(RaceLogEvent event, UUID clientId); - - /** - * Returns a superset of all race log events that were added to this race log but not yet returned to - * the client with ID clientId by this method. In general, the list returned is not a true - * superset but equals exactly those events not yet delivered to the client. However, if the server - * was re-started since the client last called this method, and since the underlying data structures - * are not durably stored, the entire set of all race log events would be delivered to the client once. - */ - Iterable getEventsToDeliver(UUID clientId); - - /** - * Returns all {@link #getRawFixes() raw fixes} and marks them as delivered to the client identified by clientId - * so that when that ID appears in a subsequent call to {@link #add(RaceLogEvent, UUID)}, the fixes returned by this call - * are already considered delivered to the client identified by clientId. - */ - Iterable getRawFixes(UUID clientId); - - /** - * Like {@link #add(RaceLogEvent)}, only that no events are triggered. Use this method only when loading a race log, - * e.g., from a replication or master data import or when loading from the database. - * - * @return true if the event was actually added which is the case if there was no equal event contained - * in this race log yet - */ - boolean load(RaceLogEvent event); - - /** - * Merges all events from the other race log into this. - */ - void merge(RaceLog other); -} +package com.sap.sailing.domain.racelog; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.NavigableSet; +import java.util.UUID; + +import com.sap.sailing.domain.common.WithID; +import com.sap.sailing.domain.common.racelog.tracking.NotRevokableException; +import com.sap.sailing.domain.racelog.impl.RaceLogEventComparator; +import com.sap.sailing.domain.tracking.Track; + +/** + * Special kind of {@link Track} for recording {@link RaceLogEvent}s. + * + *

+ * Keeps track of the {@link RaceLogEvent}'s pass and returns only the events of the current pass on + * {@link RaceLog#getFixes()}. Use {@link RaceLog#getRawFixes()} to receive all events in a {@link RaceLog}. + *

+ * + *

+ * Implementations should use the {@link RaceLogEventComparator} for sorting its content. + *

+ */ +public interface RaceLog extends Track, WithID { + + public static final int DefaultPassId = 0; + + /** + * Gets the current pass id. + * + * @return the pass id. + */ + int getCurrentPassId(); + + /** + * Adds a {@link RaceLogEvent} to the {@link RaceLog}. + * + * @param event + * {@link RaceLogEvent} to be added. + * @return true if the element was added, false otherwise. + */ + boolean add(RaceLogEvent event); + + /** + * Add a {@link RaceLogEventVisitor} as a listener for additions. + */ + void addListener(RaceLogEventVisitor listener); + + /** + * Remove a listener. + */ + void removeListener(RaceLogEventVisitor listener); + + /** + * Removes all listeners + * @return + */ + HashSet removeAllListeners(); + + /** + * Checks if the race log is empty. + */ + boolean isEmpty(); + + Iterable getRawFixesDescending(); + + Iterable getFixesDescending(); + + void addAllListeners(HashSet listeners); + + Iterable getAllListeners(); + + /** + * Adds an event to this race log and returns {@link RaceLog#getEventsToDeliver(UUID)} + * (excluding the new event) + */ + Iterable add(RaceLogEvent event, UUID clientId); + + /** + * Returns a superset of all race log events that were added to this race log but not yet returned to + * the client with ID clientId by this method. In general, the list returned is not a true + * superset but equals exactly those events not yet delivered to the client. However, if the server + * was re-started since the client last called this method, and since the underlying data structures + * are not durably stored, the entire set of all race log events would be delivered to the client once. + */ + Iterable getEventsToDeliver(UUID clientId); + + /** + * Returns all {@link #getRawFixes() raw fixes} and marks them as delivered to the client identified by clientId + * so that when that ID appears in a subsequent call to {@link #add(RaceLogEvent, UUID)}, the fixes returned by this call + * are already considered delivered to the client identified by clientId. + */ + Iterable getRawFixes(UUID clientId); + + /** + * Like {@link #add(RaceLogEvent)}, only that no events are triggered. Use this method only when loading a race log, + * e.g., from a replication or master data import or when loading from the database. + * + * @return true if the event was actually added which is the case if there was no equal event contained + * in this race log yet + */ + boolean load(RaceLogEvent event); + + /** + * Search for the event by its {@link RaceLogEvent#getId() id}. + * Caller needs to hold the read lock. + */ + RaceLogEvent getEventById(Serializable id); + + /** + * Get a {@link NavigableSet} of unrevoked events regardless of the {@code pass}. + * @return + */ + NavigableSet getUnrevokedEvents(); + + /** + * Get a {@link NavigableSet} of unrevoked events regardless of the {@code pass}. + * @return + */ + NavigableSet getUnrevokedEventsDescending(); + + /** + * Inserts a {@link RevokeEvent} for {@code toRevoke}, if latter is revokable, exists in the racelog + * and has not yet been revoked. + * + * @param author The author for the {@code RevokeEvent}. + */ + void revokeEvent(RaceLogEventAuthor author, RaceLogEvent toRevoke) throws NotRevokableException; + + /** + * Merges all events from the other race log into this. + */ + void merge(RaceLog other); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RaceLogEventFactory.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RaceLogEventFactory.java index 7cf001ce9a5..26ec9195352 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RaceLogEventFactory.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RaceLogEventFactory.java @@ -3,13 +3,23 @@ package com.sap.sailing.domain.racelog; import java.io.Serializable; import java.util.List; +import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.CourseBase; +import com.sap.sailing.domain.base.Mark; import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.racelog.Flags; import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; import com.sap.sailing.domain.common.racelog.RacingProcedureType; import com.sap.sailing.domain.racelog.impl.RaceLogEventFactoryImpl; +import com.sap.sailing.domain.racelog.tracking.CloseOpenEndedDeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DefineMarkEvent; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceCompetitorMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.DeviceMarkMappingEvent; +import com.sap.sailing.domain.racelog.tracking.RegisterCompetitorEvent; +import com.sap.sailing.domain.racelog.tracking.StartTrackingEvent; import com.sap.sailing.domain.tracking.Wind; public interface RaceLogEventFactory { @@ -88,4 +98,23 @@ public interface RaceLogEventFactory { RaceLogWindFixEvent createWindFixEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, Serializable id, List competitors, int passId, Wind wind); + DeviceCompetitorMappingEvent createDeviceCompetitorMappingEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + DeviceIdentifier device, Competitor mappedTo, int passId, TimePoint from, TimePoint to); + + DeviceMarkMappingEvent createDeviceMarkMappingEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + DeviceIdentifier device, Mark mappedTo, int passId, TimePoint from, TimePoint to); + + DenoteForTrackingEvent createDenoteForTrackingEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId, + String raceName, BoatClass boatClass, Serializable raceId); + + StartTrackingEvent createStartTrackingEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId); + + RevokeEvent createRevokeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId, Serializable revokedEventId); + + RegisterCompetitorEvent createRegisterCompetitorEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId, Competitor competitor); + + DefineMarkEvent createDefineMarkEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId, Mark mark); + + CloseOpenEndedDeviceMappingEvent createCloseOpenEndedDeviceMappingEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId, + Serializable deviceMappingEventId, TimePoint closingTimePoint); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RaceLogEventRestoreFactory.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RaceLogEventRestoreFactory.java index b277143e39c..9c73db4e60e 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RaceLogEventRestoreFactory.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RaceLogEventRestoreFactory.java @@ -3,13 +3,23 @@ package com.sap.sailing.domain.racelog; import java.io.Serializable; import java.util.List; +import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.CourseBase; +import com.sap.sailing.domain.base.Mark; import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.racelog.Flags; import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; import com.sap.sailing.domain.common.racelog.RacingProcedureType; import com.sap.sailing.domain.racelog.impl.RaceLogEventRestoreFactoryImpl; +import com.sap.sailing.domain.racelog.tracking.CloseOpenEndedDeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DefineMarkEvent; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceCompetitorMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.DeviceMarkMappingEvent; +import com.sap.sailing.domain.racelog.tracking.RegisterCompetitorEvent; +import com.sap.sailing.domain.racelog.tracking.StartTrackingEvent; import com.sap.sailing.domain.tracking.Wind; public interface RaceLogEventRestoreFactory extends RaceLogEventFactory { @@ -56,4 +66,28 @@ public interface RaceLogEventRestoreFactory extends RaceLogEventFactory { RaceLogWindFixEvent createWindFixEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, Serializable id, List competitors, Integer passId, Wind wind); + + DeviceCompetitorMappingEvent createDeviceCompetitorMappingEvent(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable pId, DeviceIdentifier device, Competitor mappedTo, int passId, TimePoint from, TimePoint to); + + DeviceMarkMappingEvent createDeviceMarkMappingEvent(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable pId, DeviceIdentifier device, Mark mappedTo, int passId, TimePoint from, TimePoint to); + + DenoteForTrackingEvent createDenoteForTrackingEvent(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable pId, int passId, String raceName, BoatClass boatClass, Serializable raceId); + + StartTrackingEvent createStartTrackingEvent(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable pId, int passId); + + RevokeEvent createRevokeEvent(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable pId, int passId, Serializable revokedEventId); + + RegisterCompetitorEvent createRegisterCompetitorEvent(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable pId, int passId, Competitor competitor); + + DefineMarkEvent createDefineMarkEvent(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable pId, int passId, Mark mark); + + CloseOpenEndedDeviceMappingEvent createCloseOpenEndedDeviceMappingEvent(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable pId, int passId, Serializable deviceMappingEventId, TimePoint closingTimePoint); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RaceLogEventVisitor.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RaceLogEventVisitor.java index 45f58b5f642..21f4c7f52c4 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RaceLogEventVisitor.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RaceLogEventVisitor.java @@ -1,5 +1,13 @@ package com.sap.sailing.domain.racelog; +import com.sap.sailing.domain.racelog.tracking.CloseOpenEndedDeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DefineMarkEvent; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceCompetitorMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceMarkMappingEvent; +import com.sap.sailing.domain.racelog.tracking.RegisterCompetitorEvent; +import com.sap.sailing.domain.racelog.tracking.StartTrackingEvent; + public interface RaceLogEventVisitor { public void visit(RaceLogFlagEvent event); @@ -27,4 +35,20 @@ public interface RaceLogEventVisitor { public void visit(RaceLogProtestStartTimeEvent event); public void visit(RaceLogWindFixEvent event); + + public void visit(DeviceCompetitorMappingEvent event); + + public void visit(DeviceMarkMappingEvent event); + + public void visit(DenoteForTrackingEvent event); + + public void visit(StartTrackingEvent event); + + public void visit(RevokeEvent event); + + public void visit(RegisterCompetitorEvent event); + + public void visit(DefineMarkEvent event); + + public void visit(CloseOpenEndedDeviceMappingEvent event); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/Revokable.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/Revokable.java new file mode 100644 index 00000000000..bd006f6138a --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/Revokable.java @@ -0,0 +1,18 @@ +package com.sap.sailing.domain.racelog; + + +/** + * Any {@link RaceLogEvent} marked as {@code Revokable} can be revoked by an {@link RevokeEvent}. + * This means that if an {@code RevokeEvent} {@code r} is added to the {@code RaceLog}, that + * {@link RevokeEvent#getRevokedEventId() revokes} an already existing event {@code e}, then the + * revoked event {@code e} will subsequently not appear in the event-iterators of the {@code RaceLog} + * ({@link RaceLog#getUnrevokedEvents()}, {@link RaceLog#getUnrevokedEventsDescending()}). + *

+ * The event {@code r} can only successufully revoke {@code e}, if {@code r}'s priority is higher + * (author prio, timepoint), and if {@code e} is of an event type that implements {@code Revokable}. + * @author Fredrik Teschke + * + */ +public interface Revokable extends RaceLogEvent { + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RevokeEvent.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RevokeEvent.java new file mode 100644 index 00000000000..23a87ff845a --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/RevokeEvent.java @@ -0,0 +1,14 @@ +package com.sap.sailing.domain.racelog; + +import java.io.Serializable; + +/** + * Revokes the event referenced by {@link #getRevokedEventId()}. + * A {@link RevokeEvent} cannot itself be revoked. Instead, e.g. the original event should be resubmitted. + * @author Fredrik Teschke + * @see Revokable + * + */ +public interface RevokeEvent extends RaceLogEvent { + Serializable getRevokedEventId(); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/BaseRaceLogEventVisitor.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/BaseRaceLogEventVisitor.java index bcd55c81e54..04f6c2af5a2 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/BaseRaceLogEventVisitor.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/BaseRaceLogEventVisitor.java @@ -14,6 +14,14 @@ import com.sap.sailing.domain.racelog.RaceLogRaceStatusEvent; import com.sap.sailing.domain.racelog.RaceLogStartProcedureChangedEvent; import com.sap.sailing.domain.racelog.RaceLogStartTimeEvent; import com.sap.sailing.domain.racelog.RaceLogWindFixEvent; +import com.sap.sailing.domain.racelog.RevokeEvent; +import com.sap.sailing.domain.racelog.tracking.CloseOpenEndedDeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DefineMarkEvent; +import com.sap.sailing.domain.racelog.tracking.RegisterCompetitorEvent; +import com.sap.sailing.domain.racelog.tracking.StartTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceCompetitorMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceMarkMappingEvent; /** * Base class implementing {@link RaceLogEventVisitor}. Every method is implemented as a no-op. @@ -84,5 +92,44 @@ public abstract class BaseRaceLogEventVisitor implements RaceLogEventVisitor { public void visit(RaceLogWindFixEvent event) { } + + @Override + public void visit(DeviceCompetitorMappingEvent event) { + + } + + @Override + public void visit(DeviceMarkMappingEvent event) { + + } + + @Override + public void visit(DenoteForTrackingEvent event) { + + } + + @Override + public void visit(StartTrackingEvent event) { + + } + + @Override + public void visit(RevokeEvent event) { + + } + + @Override + public void visit(RegisterCompetitorEvent event) { + } + + @Override + public void visit(DefineMarkEvent event) { + + } + + @Override + public void visit(CloseOpenEndedDeviceMappingEvent event) { + + } } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/NoAddingRaceLogWrapper.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/NoAddingRaceLogWrapper.java index 4a29b026ce7..cdbdf1f7eea 100755 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/NoAddingRaceLogWrapper.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/NoAddingRaceLogWrapper.java @@ -4,14 +4,17 @@ import java.io.Serializable; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; +import java.util.NavigableSet; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import com.sap.sailing.domain.common.Duration; import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.racelog.tracking.NotRevokableException; import com.sap.sailing.domain.racelog.RaceLog; import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; import com.sap.sailing.domain.racelog.RaceLogEventVisitor; /** @@ -195,12 +198,17 @@ public class NoAddingRaceLogWrapper implements RaceLog { public Iterable getEventsToDeliver(UUID clientId) { return innerRaceLog.getEventsToDeliver(clientId); } - + @Override public Iterable getFixes(TimePoint from, boolean fromInclusive, TimePoint to, boolean toInclusive) { return innerRaceLog.getFixes(from, fromInclusive, to, toInclusive); } + @Override + public NavigableSet getUnrevokedEvents() { + return innerRaceLog.getUnrevokedEvents(); + } + @Override public Duration getAverageIntervalBetweenFixes() { return innerRaceLog.getAverageIntervalBetweenFixes(); @@ -211,8 +219,22 @@ public class NoAddingRaceLogWrapper implements RaceLog { return innerRaceLog.getAverageIntervalBetweenRawFixes(); } + @Override + public NavigableSet getUnrevokedEventsDescending() { + return innerRaceLog.getUnrevokedEventsDescending(); + } + + public RaceLogEvent getEventById(Serializable id) { + return innerRaceLog.getEventById(id); + } + @Override public void merge(RaceLog other) { innerRaceLog.merge(other); } + + @Override + public void revokeEvent(RaceLogEventAuthor author, RaceLogEvent toRevoke) throws NotRevokableException { + innerRaceLog.revokeEvent(author, toRevoke); + } } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/RaceLogChangedVisitor.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/RaceLogChangedVisitor.java index 59e8d6fbd44..c589eccb3c5 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/RaceLogChangedVisitor.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/RaceLogChangedVisitor.java @@ -15,6 +15,14 @@ import com.sap.sailing.domain.racelog.RaceLogRaceStatusEvent; import com.sap.sailing.domain.racelog.RaceLogStartProcedureChangedEvent; import com.sap.sailing.domain.racelog.RaceLogStartTimeEvent; import com.sap.sailing.domain.racelog.RaceLogWindFixEvent; +import com.sap.sailing.domain.racelog.RevokeEvent; +import com.sap.sailing.domain.racelog.tracking.CloseOpenEndedDeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DefineMarkEvent; +import com.sap.sailing.domain.racelog.tracking.RegisterCompetitorEvent; +import com.sap.sailing.domain.racelog.tracking.StartTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceCompetitorMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceMarkMappingEvent; public class RaceLogChangedVisitor implements RaceLogEventVisitor { @@ -83,4 +91,43 @@ public class RaceLogChangedVisitor implements RaceLogEventVisitor { listener.eventAdded(event); } + @Override + public void visit(DeviceCompetitorMappingEvent event) { + listener.eventAdded(event); + } + + @Override + public void visit(DeviceMarkMappingEvent event) { + listener.eventAdded(event); + } + + @Override + public void visit(DenoteForTrackingEvent event) { + listener.eventAdded(event); + } + + @Override + public void visit(StartTrackingEvent event) { + listener.eventAdded(event); + } + + @Override + public void visit(RevokeEvent event) { + listener.eventAdded(event); + } + + @Override + public void visit(RegisterCompetitorEvent event) { + listener.eventAdded(event); + } + + @Override + public void visit(DefineMarkEvent event) { + listener.eventAdded(event); + } + + @Override + public void visit(CloseOpenEndedDeviceMappingEvent event) { + listener.eventAdded(event); + } } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/RaceLogEventFactoryImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/RaceLogEventFactoryImpl.java index 6ca7b073a19..bd1c391bd5e 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/RaceLogEventFactoryImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/RaceLogEventFactoryImpl.java @@ -1,203 +1,271 @@ -package com.sap.sailing.domain.racelog.impl; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.CourseBase; -import com.sap.sailing.domain.common.TimePoint; -import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; -import com.sap.sailing.domain.common.racelog.Flags; -import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; -import com.sap.sailing.domain.common.racelog.RacingProcedureType; -import com.sap.sailing.domain.racelog.CompetitorResults; -import com.sap.sailing.domain.racelog.RaceLogCourseAreaChangedEvent; -import com.sap.sailing.domain.racelog.RaceLogCourseDesignChangedEvent; -import com.sap.sailing.domain.racelog.RaceLogEventAuthor; -import com.sap.sailing.domain.racelog.RaceLogEventFactory; -import com.sap.sailing.domain.racelog.RaceLogFinishPositioningConfirmedEvent; -import com.sap.sailing.domain.racelog.RaceLogFlagEvent; -import com.sap.sailing.domain.racelog.RaceLogGateLineOpeningTimeEvent; -import com.sap.sailing.domain.racelog.RaceLogPassChangeEvent; -import com.sap.sailing.domain.racelog.RaceLogPathfinderEvent; -import com.sap.sailing.domain.racelog.RaceLogProtestStartTimeEvent; -import com.sap.sailing.domain.racelog.RaceLogRaceStatusEvent; -import com.sap.sailing.domain.racelog.RaceLogStartProcedureChangedEvent; -import com.sap.sailing.domain.racelog.RaceLogStartTimeEvent; -import com.sap.sailing.domain.racelog.RaceLogWindFixEvent; -import com.sap.sailing.domain.tracking.Wind; - -public class RaceLogEventFactoryImpl implements RaceLogEventFactory { - - @Override - public RaceLogFlagEvent createFlagEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, Serializable id, - List involvedBoats, int passId, Flags upperFlag, Flags lowerFlag, boolean isDisplayed) { - return new RaceLogFlagEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, involvedBoats, passId, - upperFlag, lowerFlag, isDisplayed); - } - - @Override - public RaceLogFlagEvent createFlagEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId, Flags upperFlag, - Flags lowerFlag, boolean isDisplayed) { - return createFlagEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), passId, upperFlag, - lowerFlag, isDisplayed); - } - - @Override - public RaceLogStartTimeEvent createStartTimeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, - Serializable id, List involvedBoats, int passId, TimePoint startTime) { - return new RaceLogStartTimeEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, involvedBoats, - passId, startTime); - } - - @Override - public RaceLogStartTimeEvent createStartTimeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId, TimePoint startTime) { - return createStartTimeEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), passId, startTime); - } - - @Override - public RaceLogRaceStatusEvent createRaceStatusEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, - Serializable id, List competitors, int passId, RaceLogRaceStatus nextStatus) { - return new RaceLogRaceStatusEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, competitors, - passId, nextStatus); - } - - @Override - public RaceLogRaceStatusEvent createRaceStatusEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId, RaceLogRaceStatus nextStatus) { - return createRaceStatusEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), passId, nextStatus); - } - - @Override - public RaceLogPassChangeEvent createPassChangeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, - Serializable id, List competitors, int passId) { - return new RaceLogPassChangeEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, competitors, passId); - } - - @Override - public RaceLogPassChangeEvent createPassChangeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId) { - return createPassChangeEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), passId); - } - - @Override - public RaceLogCourseAreaChangedEvent createCourseAreaChangedEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, - Serializable id, List competitors, int passId, Serializable courseAreaId) { - return new RaceLogCourseAreaChangeEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, competitors, - passId, courseAreaId); - } - - @Override - public RaceLogCourseAreaChangedEvent createCourseAreaChangedEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, - int passId, Serializable courseAreaId) { - return createCourseAreaChangedEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), - passId, courseAreaId); - } - - @Override - public RaceLogCourseDesignChangedEvent createCourseDesignChangedEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, - Serializable id, List competitors, int passId, CourseBase courseData) { - return new RaceLogCourseDesignChangedEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, competitors, - passId, courseData); - } - - @Override - public RaceLogCourseDesignChangedEvent createCourseDesignChangedEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, - int passId, CourseBase courseData) { - return createCourseDesignChangedEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), - passId, courseData); - } - - @Override - public RaceLogFinishPositioningListChangedEventImpl createFinishPositioningListChangedEvent(TimePoint logicalTimePoint, - RaceLogEventAuthor author, Serializable id, List competitors, - int passId, CompetitorResults positionedCompetitors) { - return new RaceLogFinishPositioningListChangedEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, - id, competitors, passId, positionedCompetitors); - } - - @Override - public RaceLogFinishPositioningListChangedEventImpl createFinishPositioningListChangedEvent(TimePoint logicalTimePoint, - RaceLogEventAuthor author, int passId, CompetitorResults positionedCompetitors) { - return createFinishPositioningListChangedEvent(logicalTimePoint, author, UUID.randomUUID(), - new ArrayList(), passId, positionedCompetitors); - } - - @Override - public RaceLogFinishPositioningConfirmedEvent createFinishPositioningConfirmedEvent(TimePoint logicalTimePoint, - RaceLogEventAuthor author, Serializable id, List competitors, int passId, CompetitorResults positionedCompetitors) { - return new RaceLogFinishPositioningConfirmedEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, - competitors, passId, positionedCompetitors); - } - - @Override - public RaceLogFinishPositioningConfirmedEvent createFinishPositioningConfirmedEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, - int passId, CompetitorResults positionedCompetitors) { - return createFinishPositioningConfirmedEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), passId, positionedCompetitors); - } - - @Override - public RaceLogPathfinderEvent createPathfinderEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, - Serializable id, List competitors, int passId, String pathfinderId) { - return new RaceLogPathfinderEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, competitors, - passId, pathfinderId); - } - - @Override - public RaceLogPathfinderEvent createPathfinderEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId, String pathfinderId) { - return createPathfinderEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), passId, pathfinderId); - } - - @Override - public RaceLogGateLineOpeningTimeEvent createGateLineOpeningTimeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, - Serializable id, List competitors, int passId, Long gateLaunchStopTime, Long golfDownTime) { - return new RaceLogGateLineOpeningTimeEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, competitors, - passId, gateLaunchStopTime, golfDownTime); - } - - @Override - public RaceLogGateLineOpeningTimeEvent createGateLineOpeningTimeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, - int passId, Long gateLaunchStopTime, Long golfDownTime) { - return createGateLineOpeningTimeEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), - passId, gateLaunchStopTime, golfDownTime); - } - - @Override - public RaceLogStartProcedureChangedEvent createStartProcedureChangedEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, - Serializable id, List competitors, int passId, RacingProcedureType type) { - return new RaceLogStartProcedureChangedEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, - competitors, passId, type); - } - - @Override - public RaceLogStartProcedureChangedEvent createStartProcedureChangedEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, - int passId, RacingProcedureType type) { - return createStartProcedureChangedEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), passId, type); - } - - @Override - public RaceLogProtestStartTimeEvent createProtestStartTimeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, - Serializable id, List competitors, int passId, TimePoint protestStartTime) { - return new RaceLogProtestStartTimeEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, - competitors, passId, protestStartTime); - } - - @Override - public RaceLogProtestStartTimeEvent createProtestStartTimeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, - int passId, TimePoint protestStartTime) { - return createProtestStartTimeEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), - passId, protestStartTime); - } - - @Override - public RaceLogWindFixEvent createWindFixEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId, Wind wind) { - return createWindFixEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), passId, wind); - } - - @Override - public RaceLogWindFixEvent createWindFixEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, Serializable id, - List competitors, int passId, Wind wind) { - return new RaceLogWindFixEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, competitors, passId, wind); - } - -} +package com.sap.sailing.domain.racelog.impl; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CourseBase; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.racelog.Flags; +import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; +import com.sap.sailing.domain.common.racelog.RacingProcedureType; +import com.sap.sailing.domain.racelog.CompetitorResults; +import com.sap.sailing.domain.racelog.RaceLogCourseAreaChangedEvent; +import com.sap.sailing.domain.racelog.RaceLogCourseDesignChangedEvent; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventFactory; +import com.sap.sailing.domain.racelog.RaceLogFinishPositioningConfirmedEvent; +import com.sap.sailing.domain.racelog.RaceLogFlagEvent; +import com.sap.sailing.domain.racelog.RaceLogGateLineOpeningTimeEvent; +import com.sap.sailing.domain.racelog.RaceLogPassChangeEvent; +import com.sap.sailing.domain.racelog.RaceLogPathfinderEvent; +import com.sap.sailing.domain.racelog.RaceLogProtestStartTimeEvent; +import com.sap.sailing.domain.racelog.RaceLogRaceStatusEvent; +import com.sap.sailing.domain.racelog.RaceLogStartProcedureChangedEvent; +import com.sap.sailing.domain.racelog.RaceLogStartTimeEvent; +import com.sap.sailing.domain.racelog.RaceLogWindFixEvent; +import com.sap.sailing.domain.racelog.RevokeEvent; +import com.sap.sailing.domain.racelog.tracking.CloseOpenEndedDeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DefineMarkEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.RegisterCompetitorEvent; +import com.sap.sailing.domain.racelog.tracking.StartTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceCompetitorMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceMarkMappingEvent; +import com.sap.sailing.domain.racelog.tracking.events.CloseOpenEndedDeviceMappingEventImpl; +import com.sap.sailing.domain.racelog.tracking.events.DefineMarkEventImpl; +import com.sap.sailing.domain.racelog.tracking.events.RegisterCompetitorEventImpl; +import com.sap.sailing.domain.racelog.tracking.events.StartTrackingEventImpl; +import com.sap.sailing.domain.racelog.tracking.events.DenoteForTrackingEventImpl; +import com.sap.sailing.domain.racelog.tracking.events.DeviceCompetitorMappingEventImpl; +import com.sap.sailing.domain.racelog.tracking.events.DeviceMarkMappingEventImpl; +import com.sap.sailing.domain.racelog.tracking.events.RevokeEventImpl; +import com.sap.sailing.domain.tracking.Wind; + +public class RaceLogEventFactoryImpl implements RaceLogEventFactory { + + @Override + public RaceLogFlagEvent createFlagEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, Serializable id, + List involvedBoats, int passId, Flags upperFlag, Flags lowerFlag, boolean isDisplayed) { + return new RaceLogFlagEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, involvedBoats, passId, + upperFlag, lowerFlag, isDisplayed); + } + + @Override + public RaceLogFlagEvent createFlagEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId, Flags upperFlag, + Flags lowerFlag, boolean isDisplayed) { + return createFlagEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), passId, upperFlag, + lowerFlag, isDisplayed); + } + + @Override + public RaceLogStartTimeEvent createStartTimeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + Serializable id, List involvedBoats, int passId, TimePoint startTime) { + return new RaceLogStartTimeEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, involvedBoats, + passId, startTime); + } + + @Override + public RaceLogStartTimeEvent createStartTimeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId, TimePoint startTime) { + return createStartTimeEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), passId, startTime); + } + + @Override + public RaceLogRaceStatusEvent createRaceStatusEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + Serializable id, List competitors, int passId, RaceLogRaceStatus nextStatus) { + return new RaceLogRaceStatusEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, competitors, + passId, nextStatus); + } + + @Override + public RaceLogRaceStatusEvent createRaceStatusEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId, RaceLogRaceStatus nextStatus) { + return createRaceStatusEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), passId, nextStatus); + } + + @Override + public RaceLogPassChangeEvent createPassChangeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + Serializable id, List competitors, int passId) { + return new RaceLogPassChangeEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, competitors, passId); + } + + @Override + public RaceLogPassChangeEvent createPassChangeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId) { + return createPassChangeEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), passId); + } + + @Override + public RaceLogCourseAreaChangedEvent createCourseAreaChangedEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + Serializable id, List competitors, int passId, Serializable courseAreaId) { + return new RaceLogCourseAreaChangeEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, competitors, + passId, courseAreaId); + } + + @Override + public RaceLogCourseAreaChangedEvent createCourseAreaChangedEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + int passId, Serializable courseAreaId) { + return createCourseAreaChangedEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), + passId, courseAreaId); + } + + @Override + public RaceLogCourseDesignChangedEvent createCourseDesignChangedEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + Serializable id, List competitors, int passId, CourseBase courseData) { + return new RaceLogCourseDesignChangedEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, competitors, + passId, courseData); + } + + @Override + public RaceLogCourseDesignChangedEvent createCourseDesignChangedEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + int passId, CourseBase courseData) { + return createCourseDesignChangedEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), + passId, courseData); + } + + @Override + public RaceLogFinishPositioningListChangedEventImpl createFinishPositioningListChangedEvent(TimePoint logicalTimePoint, + RaceLogEventAuthor author, Serializable id, List competitors, + int passId, CompetitorResults positionedCompetitors) { + return new RaceLogFinishPositioningListChangedEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, + id, competitors, passId, positionedCompetitors); + } + + @Override + public RaceLogFinishPositioningListChangedEventImpl createFinishPositioningListChangedEvent(TimePoint logicalTimePoint, + RaceLogEventAuthor author, int passId, CompetitorResults positionedCompetitors) { + return createFinishPositioningListChangedEvent(logicalTimePoint, author, UUID.randomUUID(), + new ArrayList(), passId, positionedCompetitors); + } + + @Override + public RaceLogFinishPositioningConfirmedEvent createFinishPositioningConfirmedEvent(TimePoint logicalTimePoint, + RaceLogEventAuthor author, Serializable id, List competitors, int passId, CompetitorResults positionedCompetitors) { + return new RaceLogFinishPositioningConfirmedEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, + competitors, passId, positionedCompetitors); + } + + @Override + public RaceLogFinishPositioningConfirmedEvent createFinishPositioningConfirmedEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + int passId, CompetitorResults positionedCompetitors) { + return createFinishPositioningConfirmedEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), passId, positionedCompetitors); + } + + @Override + public RaceLogPathfinderEvent createPathfinderEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + Serializable id, List competitors, int passId, String pathfinderId) { + return new RaceLogPathfinderEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, competitors, + passId, pathfinderId); + } + + @Override + public RaceLogPathfinderEvent createPathfinderEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId, String pathfinderId) { + return createPathfinderEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), passId, pathfinderId); + } + + @Override + public RaceLogGateLineOpeningTimeEvent createGateLineOpeningTimeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + Serializable id, List competitors, int passId, Long gateLaunchStopTime, Long golfDownTime) { + return new RaceLogGateLineOpeningTimeEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, competitors, + passId, gateLaunchStopTime, golfDownTime); + } + + @Override + public RaceLogGateLineOpeningTimeEvent createGateLineOpeningTimeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + int passId, Long gateLaunchStopTime, Long golfDownTime) { + return createGateLineOpeningTimeEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), + passId, gateLaunchStopTime, golfDownTime); + } + + @Override + public RaceLogStartProcedureChangedEvent createStartProcedureChangedEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + Serializable id, List competitors, int passId, RacingProcedureType type) { + return new RaceLogStartProcedureChangedEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, + competitors, passId, type); + } + + @Override + public RaceLogStartProcedureChangedEvent createStartProcedureChangedEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + int passId, RacingProcedureType type) { + return createStartProcedureChangedEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), passId, type); + } + + @Override + public RaceLogProtestStartTimeEvent createProtestStartTimeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + Serializable id, List competitors, int passId, TimePoint protestStartTime) { + return new RaceLogProtestStartTimeEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, + competitors, passId, protestStartTime); + } + + @Override + public RaceLogProtestStartTimeEvent createProtestStartTimeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + int passId, TimePoint protestStartTime) { + return createProtestStartTimeEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), + passId, protestStartTime); + } + + @Override + public RaceLogWindFixEvent createWindFixEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId, Wind wind) { + return createWindFixEvent(logicalTimePoint, author, UUID.randomUUID(), new ArrayList(), passId, wind); + } + + @Override + public RaceLogWindFixEvent createWindFixEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, Serializable id, + List competitors, int passId, Wind wind) { + return new RaceLogWindFixEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, id, competitors, passId, wind); + } + + @Override + public DeviceCompetitorMappingEvent createDeviceCompetitorMappingEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + DeviceIdentifier device, Competitor mappedTo, int passId, TimePoint from, TimePoint to) { + return new DeviceCompetitorMappingEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, UUID.randomUUID(), + passId, mappedTo, device, from, to); + } + + @Override + public DeviceMarkMappingEvent createDeviceMarkMappingEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + DeviceIdentifier device, Mark mappedTo, int passId, TimePoint from, TimePoint to) { + return new DeviceMarkMappingEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, UUID.randomUUID(), + passId, mappedTo, device, from, to); + } + + @Override + public DenoteForTrackingEvent createDenoteForTrackingEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId, + String raceName, BoatClass boatClass, Serializable raceId) { + return new DenoteForTrackingEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, UUID.randomUUID(), passId, raceName, + boatClass, raceId); + } + + @Override + public StartTrackingEvent createStartTrackingEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId) { + return new StartTrackingEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, UUID.randomUUID(), passId); + } + + @Override + public RevokeEvent createRevokeEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, int passId, Serializable revokedEventId) { + return new RevokeEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, UUID.randomUUID(), passId, revokedEventId); + } + + @Override + public RegisterCompetitorEvent createRegisterCompetitorEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + int passId, Competitor competitor) { + return new RegisterCompetitorEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, UUID.randomUUID(), passId, competitor); + } + + @Override + public DefineMarkEvent createDefineMarkEvent(TimePoint logicalTimePoint, RaceLogEventAuthor author, + int passId, Mark mark) { + return new DefineMarkEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, UUID.randomUUID(), passId, mark); + } + + @Override + public CloseOpenEndedDeviceMappingEvent createCloseOpenEndedDeviceMappingEvent(TimePoint logicalTimePoint, + RaceLogEventAuthor author, int passId, Serializable deviceMappingEventId, TimePoint closingTimePoint) { + return new CloseOpenEndedDeviceMappingEventImpl(MillisecondsTimePoint.now(), author, logicalTimePoint, + UUID.randomUUID(), passId, deviceMappingEventId, closingTimePoint); + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/RaceLogEventRestoreFactoryImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/RaceLogEventRestoreFactoryImpl.java index cebfa95dd57..0f61f469175 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/RaceLogEventRestoreFactoryImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/RaceLogEventRestoreFactoryImpl.java @@ -3,8 +3,10 @@ package com.sap.sailing.domain.racelog.impl; import java.io.Serializable; import java.util.List; +import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.CourseBase; +import com.sap.sailing.domain.base.Mark; import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.racelog.Flags; import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; @@ -25,6 +27,23 @@ import com.sap.sailing.domain.racelog.RaceLogRaceStatusEvent; import com.sap.sailing.domain.racelog.RaceLogStartProcedureChangedEvent; import com.sap.sailing.domain.racelog.RaceLogStartTimeEvent; import com.sap.sailing.domain.racelog.RaceLogWindFixEvent; +import com.sap.sailing.domain.racelog.RevokeEvent; +import com.sap.sailing.domain.racelog.tracking.CloseOpenEndedDeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DefineMarkEvent; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceCompetitorMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.DeviceMarkMappingEvent; +import com.sap.sailing.domain.racelog.tracking.RegisterCompetitorEvent; +import com.sap.sailing.domain.racelog.tracking.StartTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.events.CloseOpenEndedDeviceMappingEventImpl; +import com.sap.sailing.domain.racelog.tracking.events.DefineMarkEventImpl; +import com.sap.sailing.domain.racelog.tracking.events.DenoteForTrackingEventImpl; +import com.sap.sailing.domain.racelog.tracking.events.DeviceCompetitorMappingEventImpl; +import com.sap.sailing.domain.racelog.tracking.events.DeviceMarkMappingEventImpl; +import com.sap.sailing.domain.racelog.tracking.events.RegisterCompetitorEventImpl; +import com.sap.sailing.domain.racelog.tracking.events.RevokeEventImpl; +import com.sap.sailing.domain.racelog.tracking.events.StartTrackingEventImpl; import com.sap.sailing.domain.tracking.Wind; public class RaceLogEventRestoreFactoryImpl extends RaceLogEventFactoryImpl implements RaceLogEventRestoreFactory { @@ -109,5 +128,54 @@ public class RaceLogEventRestoreFactoryImpl extends RaceLogEventFactoryImpl impl return new RaceLogWindFixEventImpl(createdAt, author, logicalTimePoint, id, competitors, passId, wind); } + @Override + public DeviceCompetitorMappingEvent createDeviceCompetitorMappingEvent(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable pId, DeviceIdentifier device, Competitor mappedTo, int passId, TimePoint from, TimePoint to) { + return new DeviceCompetitorMappingEventImpl(createdAt, author, logicalTimePoint, pId, passId, mappedTo, device, from, to); + } + @Override + public DeviceMarkMappingEvent createDeviceMarkMappingEvent(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable pId, DeviceIdentifier device, Mark mappedTo, int passId, TimePoint from, + TimePoint to) { + return new DeviceMarkMappingEventImpl(createdAt, author, logicalTimePoint, pId, passId, mappedTo, device, from, to); + } + + @Override + public DenoteForTrackingEvent createDenoteForTrackingEvent(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable pId, int passId, String raceName, BoatClass boatClass, Serializable raceId) { + return new DenoteForTrackingEventImpl(createdAt, author, logicalTimePoint, pId, passId, raceName, boatClass, raceId); + } + + @Override + public StartTrackingEvent createStartTrackingEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable pId, int passId) { + return new StartTrackingEventImpl(createdAt, author, logicalTimePoint, pId, passId); + } + + @Override + public RevokeEvent createRevokeEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable pId, int passId, Serializable revokedEventId) { + return new RevokeEventImpl(createdAt, author, logicalTimePoint, pId, passId, revokedEventId); + } + + @Override + public RegisterCompetitorEvent createRegisterCompetitorEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable pId, int passId, Competitor competitor) { + return new RegisterCompetitorEventImpl(createdAt, author, logicalTimePoint, pId, passId, competitor); + } + + @Override + public DefineMarkEvent createDefineMarkEvent(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable pId, int passId, Mark mark) { + return new DefineMarkEventImpl(createdAt, author, logicalTimePoint, pId, passId, mark); + } + + @Override + public CloseOpenEndedDeviceMappingEvent createCloseOpenEndedDeviceMappingEvent(TimePoint createdAt, + RaceLogEventAuthor author, TimePoint logicalTimePoint, Serializable pId, int passId, + Serializable deviceMappingEventId, TimePoint closingTimePoint) { + return new CloseOpenEndedDeviceMappingEventImpl(createdAt, author, logicalTimePoint, pId, passId, + deviceMappingEventId, closingTimePoint); + } } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/RaceLogImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/RaceLogImpl.java index 6e8d605cc8b..4ebf7efd341 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/RaceLogImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/impl/RaceLogImpl.java @@ -1,326 +1,414 @@ -package com.sap.sailing.domain.racelog.impl; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.Serializable; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.NavigableSet; -import java.util.Set; -import java.util.UUID; -import java.util.logging.Level; -import java.util.logging.Logger; - -import com.sap.sailing.domain.base.Timed; -import com.sap.sailing.domain.racelog.RaceLog; -import com.sap.sailing.domain.racelog.RaceLogEvent; -import com.sap.sailing.domain.racelog.RaceLogEventVisitor; -import com.sap.sailing.domain.tracking.Track; -import com.sap.sailing.domain.tracking.impl.PartialNavigableSetView; -import com.sap.sailing.domain.tracking.impl.TrackImpl; -import com.sap.sailing.util.impl.ArrayListNavigableSet; - -/** - * {@link Track} implementation for {@link RaceLogEvent}s. - * - *

- * "Fix" validity is decided based on the {@link #getCurrentPassId() current pass}. The validity is not cached. - *

- * - *

- * {@link TrackImpl#getDummyFix(com.sap.sailing.domain.common.TimePoint)} is not overridden, see - * {@link RaceLogEventComparator} for sorting when interface methods like - * {@link Track#getFirstFixAfter(com.sap.sailing.domain.common.TimePoint)} are used. - *

- * - */ -public class RaceLogImpl extends TrackImpl implements RaceLog { - private static final long serialVersionUID = -176745401321893502L; - private static final String DefaultLockName = RaceLogImpl.class.getName() + ".lock"; - private final static Logger logger = Logger.getLogger(RaceLogImpl.class.getName()); - - /** - * Clients can use the {@link #add(RaceLogEvent, UUID)} method - */ - private transient Map> eventsDeliveredToClient = new HashMap>(); - - private final Serializable id; - private transient Set listeners; - private int currentPassId; - - /** - * Initializes a new {@link RaceLogImpl} with the default lock name. - */ - public RaceLogImpl(Serializable identifier) { - this(DefaultLockName, identifier); - } - - /** - * Initializes a new {@link RaceLogImpl}. - * - * @param nameForReadWriteLock - * name of lock. - */ - public RaceLogImpl(String nameForReadWriteLock, Serializable identifier) { - super(new ArrayListNavigableSet(RaceLogEventComparator.INSTANCE), nameForReadWriteLock); - - this.listeners = new HashSet(); - this.currentPassId = DefaultPassId; - this.id = identifier; - } - - @Override - public Serializable getId() { - return this.id; - } - - @Override - public int getCurrentPassId() { - return currentPassId; - } - - /** - * Sets a new active pass id. Ignored if new and current are equal. - * - * @param newPassId - * to be set. - */ - public void setCurrentPassId(int newPassId) { - if (newPassId != this.currentPassId) { - logger.finer(String.format("Changing pass id to %d", newPassId)); - this.currentPassId = newPassId; - } - } - - @Override - public boolean add(RaceLogEvent event) { - boolean isAdded = false; - lockForWrite(); - try { - isAdded = getInternalRawFixes().add(event); - } finally { - unlockAfterWrite(); - } - if (isAdded) { - logger.finer(String.format("%s (%s) was added to log %s.", event, event.getClass().getName(), getId())); - // FIXME with out-of-order delivery would destroy currentPassId; need to check at least the createdAt time - // point - setCurrentPassId(Math.max(event.getPassId(), this.currentPassId)); - notifyListenersAboutReceive(event); - } else { - logger.warning(String.format("%s (%s) was not added to race log %s. Ignoring", event, event.getClass().getName(), getId())); - } - return isAdded; - } - - @Override - public boolean load(RaceLogEvent event) { - boolean isAdded = false; - lockForWrite(); - try { - isAdded = getInternalRawFixes().add(event); - } finally { - unlockAfterWrite(); - } - if (isAdded) { - logger.finer(String.format("%s (%s) was loaded into log.", event, event.getClass().getName())); - setCurrentPassId(Math.max(event.getPassId(), this.currentPassId)); - } else { - logger.warning(String - .format("%s (%s) was not loaded into log. Ignoring", event, event.getClass().getName())); - } - return isAdded; - } - - @Override - public Iterable add(RaceLogEvent event, UUID clientId) { - boolean isAdded = false; - lockForWrite(); - try { - isAdded = getInternalRawFixes().add(event); - } finally { - unlockAfterWrite(); - } - if (isAdded) { - logger.finer(String.format("%s (%s) was added to log.", event, event.getClass().getName())); - // FIXME with out-of-order delivery would destroy currentPassId; need to check at least the createdAt time - // point - setCurrentPassId(Math.max(event.getPassId(), this.currentPassId)); - notifyListenersAboutReceive(event); - } else { - logger.warning(String.format("%s (%s) was not added to log. Ignoring", event, event.getClass().getName())); - } - return getEventsToDeliver(clientId, event); - } - - @Override - public Iterable getEventsToDeliver(UUID clientId) { - return getEventsToDeliver(clientId, null); - } - - protected Iterable getEventsToDeliver(UUID clientId, RaceLogEvent suppressedEvent) { - final LinkedHashSet stillToDeliverToClient; - lockForRead(); - try { - stillToDeliverToClient = new LinkedHashSet(getInternalRawFixes()); - } finally { - unlockAfterRead(); - } - stillToDeliverToClient.remove(suppressedEvent); - Set deliveredToClient = eventsDeliveredToClient.get(clientId); - if (deliveredToClient != null) { - stillToDeliverToClient.removeAll(deliveredToClient); - } else { - deliveredToClient = new HashSet(); - eventsDeliveredToClient.put(clientId, deliveredToClient); - } - deliveredToClient.addAll(stillToDeliverToClient); - deliveredToClient.add(suppressedEvent); - return stillToDeliverToClient; - } - - protected void notifyListenersAboutReceive(RaceLogEvent event) { - Set workingListeners = new HashSet(); - synchronized (listeners) { - workingListeners.addAll(listeners); - } - for (RaceLogEventVisitor listener : workingListeners) { - try { - event.accept(listener); - } catch (Throwable t) { - logger.log(Level.SEVERE, "RaceLogEventVisitor " + listener + " threw exception " + t.getMessage()); - logger.throwing(RaceLogImpl.class.getName(), "notifyListenersAboutReceive(RaceLogEvent)", t); - t.printStackTrace(); - } - } - } - - @Override - public boolean isEmpty() { - return getFirstRawFix() == null; - } - - @Override - public void addListener(RaceLogEventVisitor listener) { - synchronized (listeners) { - listeners.add(listener); - } - } - - @Override - public void removeListener(RaceLogEventVisitor listener) { - synchronized (listeners) { - listeners.remove(listener); - } - } - - @Override - protected NavigableSet getInternalFixes() { - return new PartialNavigableSetView(super.getInternalFixes()) { - @Override - protected boolean isValid(RaceLogEvent e) { - return e.getPassId() == getCurrentPassId(); - } - }; - } - - /** - * When deserializing, needs to initialize empty set of listeners. - */ - private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { - ois.defaultReadObject(); - listeners = new HashSet(); - eventsDeliveredToClient = new HashMap>(); - } - - @Override - public Iterable getRawFixesDescending() { - return getRawFixes().descendingSet(); - } - - @Override - public Iterable getFixesDescending() { - return getFixes().descendingSet(); - } - - @Override - public HashSet removeAllListeners() { - synchronized (listeners) { - HashSet clonedListeners = new HashSet(listeners); - listeners = new HashSet(); - return clonedListeners; - } - } - - @Override - public void addAllListeners(HashSet listeners) { - synchronized (listeners) { - this.listeners.addAll(listeners); - } - } - - @Override - public Iterable getAllListeners() { - return this.listeners; - } - - @Override - public Iterable getRawFixes(UUID clientId) { - assertReadLock(); - NavigableSet result = getRawFixes(); - Set edtc = eventsDeliveredToClient.get(clientId); - if (edtc == null) { - edtc = new HashSet(); - eventsDeliveredToClient.put(clientId, edtc); - } - edtc.addAll(result); - return result; - } - - @Override - public void merge(RaceLog other) { - lockForWrite(); - other.lockForRead(); - try { - RaceLogEventComparator comparator = RaceLogEventComparator.INSTANCE; - Iterator thisIter = getRawFixes().iterator(); - Iterator otherIter = other.getRawFixes().iterator(); - RaceLogEvent thisEvent = null; - RaceLogEvent otherEvent = null; - while (otherIter.hasNext() || otherEvent != null) { - if (thisEvent == null && thisIter.hasNext()) { - thisEvent = thisIter.next(); - } - if (otherEvent == null) { - otherEvent = otherIter.next(); - } - if (thisEvent == null) { - // All events of this race log have been consumed; simply keep adding the events - // from the other race log to this race log. - // otherEvent has to be non-null because if thisIter didn't have a next, otherIter must have had a next - add(otherEvent); - otherEvent = null; // "consumed" otherEvent; try to grab next if a next element exists in otherIter - } else { - final int comparison = comparator.compare(thisEvent, otherEvent); - if (comparison < 0) { - thisEvent = null; // skip the "lesser" race log event on this race log - } else if (comparison == 0) { - // the race log event from the other log is already contained in this log; skip both - thisEvent = null; - otherEvent = null; - } else { - // comparison > 0; we skipped on this race log until we found a "greater" event on this race log; insert otherEvent - add(otherEvent); - otherEvent = null; // "consumed" - } - } - } - } finally { - other.unlockAfterRead(); - unlockAfterWrite(); - } - } -} +package com.sap.sailing.domain.racelog.impl; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.NavigableSet; +import java.util.Set; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.sap.sailing.domain.base.Timed; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.racelog.tracking.NotRevokableException; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventFactory; +import com.sap.sailing.domain.racelog.RaceLogEventVisitor; +import com.sap.sailing.domain.racelog.Revokable; +import com.sap.sailing.domain.racelog.RevokeEvent; +import com.sap.sailing.domain.tracking.Track; +import com.sap.sailing.domain.tracking.impl.PartialNavigableSetView; +import com.sap.sailing.domain.tracking.impl.TrackImpl; +import com.sap.sailing.util.impl.ArrayListNavigableSet; + +/** + * {@link Track} implementation for {@link RaceLogEvent}s. + * + *

+ * "Fix" validity is decided based on the {@link #getCurrentPassId() current pass}. The validity is not cached. + *

+ * + *

+ * {@link TrackImpl#getDummyFix(com.sap.sailing.domain.common.TimePoint)} is not overridden, see + * {@link RaceLogEventComparator} for sorting when interface methods like + * {@link Track#getFirstFixAfter(com.sap.sailing.domain.common.TimePoint)} are used. + *

+ * + */ +public class RaceLogImpl extends TrackImpl implements RaceLog { + private static final long serialVersionUID = -176745401321893502L; + private static final String DefaultLockName = RaceLogImpl.class.getName() + ".lock"; + private final static Logger logger = Logger.getLogger(RaceLogImpl.class.getName()); + private final Set revokedEventIds = new HashSet(); + + /** + * Clients can use the {@link #add(RaceLogEvent, UUID)} method + */ + private transient Map> eventsDeliveredToClient = new HashMap>(); + + private Map eventsById = new HashMap(); + + private final Serializable id; + private transient Set listeners; + private int currentPassId; + + /** + * Initializes a new {@link RaceLogImpl} with the default lock name. + */ + public RaceLogImpl(Serializable identifier) { + this(DefaultLockName, identifier); + } + + /** + * Initializes a new {@link RaceLogImpl}. + * + * @param nameForReadWriteLock + * name of lock. + */ + public RaceLogImpl(String nameForReadWriteLock, Serializable identifier) { + super(new ArrayListNavigableSet(RaceLogEventComparator.INSTANCE), nameForReadWriteLock); + + this.listeners = new HashSet(); + this.currentPassId = DefaultPassId; + this.id = identifier; + } + + @Override + public Serializable getId() { + return this.id; + } + + @Override + public int getCurrentPassId() { + return currentPassId; + } + + /** + * Sets a new active pass id. Ignored if new and current are equal. + * + * @param newPassId + * to be set. + */ + public void setCurrentPassId(int newPassId) { + if (newPassId != this.currentPassId) { + logger.finer(String.format("Changing pass id to %d", newPassId)); + this.currentPassId = newPassId; + } + } + + @Override + public boolean add(RaceLogEvent event) { + boolean isAdded = false; + lockForWrite(); + try { + isAdded = getInternalRawFixes().add(event); + } finally { + unlockAfterWrite(); + } + if (isAdded) { + logger.finer(String.format("%s (%s) was added to log %s.", event, event.getClass().getName(), getId())); + // FIXME with out-of-order delivery would destroy currentPassId; need to check at least the createdAt time + // point + setCurrentPassId(Math.max(event.getPassId(), this.currentPassId)); + revokeIfNecessary(event); + eventsById.put(event.getId(), event); + notifyListenersAboutReceive(event); + } else { + logger.fine(String.format("%s (%s) was not added to race log %s because it already existed there.", event, event.getClass().getName(), getId())); + } + return isAdded; + } + + @Override + public boolean load(RaceLogEvent event) { + boolean isAdded = false; + lockForWrite(); + try { + isAdded = getInternalRawFixes().add(event); + } finally { + unlockAfterWrite(); + } + if (isAdded) { + logger.finer(String.format("%s (%s) was loaded into log.", event, event.getClass().getName())); + setCurrentPassId(Math.max(event.getPassId(), this.currentPassId)); + revokeIfNecessary(event); + eventsById.put(event.getId(), event); + } else { + logger.fine(String + .format("%s (%s) was not loaded into log because it already existed there.", event, event.getClass().getName())); + } + return isAdded; + } + + private void revokeIfNecessary(RaceLogEvent newEvent) { + if (newEvent instanceof RevokeEvent) { + RevokeEvent revokeEvent = (RevokeEvent) newEvent; + try { + checkIfSuccessfullyRevokes(revokeEvent); + + lockForWrite(); + revokedEventIds.add(revokeEvent.getRevokedEventId()); + unlockAfterWrite(); + } catch (NotRevokableException e) { + logger.log(Level.WARNING, e.getMessage()); + } + } + } + + private void checkIfSuccessfullyRevokes(RevokeEvent revokeEvent) throws NotRevokableException { + lockForRead(); + RaceLogEvent revokedEvent = getEventById(revokeEvent.getRevokedEventId()); + unlockAfterRead(); + + if (revokedEvent == null) { + throw new NotRevokableException("RevokeEvent added, that refers to non-existent event to be revoked"); + } + + if (revokedEventIds.contains(revokedEvent.getId())) { + throw new NotRevokableException("Event has already been revoked"); + } + + if (! (revokedEvent instanceof Revokable)) { + throw new NotRevokableException("RevokeEvent trying to revoke non-revokable event"); + } + + if (getInternalRawFixes().comparator().compare(revokeEvent, revokedEvent) <= 0) { + throw new NotRevokableException("RevokeEvent does not have sufficient priority"); + } + } + + @Override + public Iterable add(RaceLogEvent event, UUID clientId) { + boolean isAdded = false; + lockForWrite(); + try { + isAdded = getInternalRawFixes().add(event); + } finally { + unlockAfterWrite(); + } + if (isAdded) { + logger.finer(String.format("%s (%s) was added to log.", event, event.getClass().getName())); + // FIXME with out-of-order delivery would destroy currentPassId; need to check at least the createdAt time + // point + setCurrentPassId(Math.max(event.getPassId(), this.currentPassId)); + revokeIfNecessary(event); + notifyListenersAboutReceive(event); + } else { + logger.fine(String.format("%s (%s) was not added to log because it already eists there.", event, event.getClass().getName())); + } + return getEventsToDeliver(clientId, event); + } + + @Override + public Iterable getEventsToDeliver(UUID clientId) { + return getEventsToDeliver(clientId, null); + } + + protected Iterable getEventsToDeliver(UUID clientId, RaceLogEvent suppressedEvent) { + final LinkedHashSet stillToDeliverToClient; + lockForRead(); + try { + stillToDeliverToClient = new LinkedHashSet(getInternalRawFixes()); + } finally { + unlockAfterRead(); + } + stillToDeliverToClient.remove(suppressedEvent); + Set deliveredToClient = eventsDeliveredToClient.get(clientId); + if (deliveredToClient != null) { + stillToDeliverToClient.removeAll(deliveredToClient); + } else { + deliveredToClient = new HashSet(); + eventsDeliveredToClient.put(clientId, deliveredToClient); + } + deliveredToClient.addAll(stillToDeliverToClient); + deliveredToClient.add(suppressedEvent); + return stillToDeliverToClient; + } + + protected void notifyListenersAboutReceive(RaceLogEvent event) { + Set workingListeners = new HashSet(); + synchronized (listeners) { + workingListeners.addAll(listeners); + } + for (RaceLogEventVisitor listener : workingListeners) { + try { + event.accept(listener); + } catch (Throwable t) { + logger.log(Level.SEVERE, "RaceLogEventVisitor " + listener + " threw exception " + t.getMessage()); + logger.throwing(RaceLogImpl.class.getName(), "notifyListenersAboutReceive(RaceLogEvent)", t); + t.printStackTrace(); + } + } + } + + @Override + public boolean isEmpty() { + return getFirstRawFix() == null; + } + + @Override + public void addListener(RaceLogEventVisitor listener) { + synchronized (listeners) { + listeners.add(listener); + } + } + + @Override + public void removeListener(RaceLogEventVisitor listener) { + synchronized (listeners) { + listeners.remove(listener); + } + } + + @Override + protected NavigableSet getInternalFixes() { + return new PartialNavigableSetView(super.getInternalFixes()) { + @Override + protected boolean isValid(RaceLogEvent e) { + return e.getPassId() == getCurrentPassId(); + } + }; + } + + /** + * When deserializing, needs to initialize empty set of listeners. + */ + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + listeners = new HashSet(); + eventsDeliveredToClient = new HashMap>(); + } + + @Override + public Iterable getRawFixesDescending() { + return getRawFixes().descendingSet(); + } + + @Override + public Iterable getFixesDescending() { + return getFixes().descendingSet(); + } + + @Override + public HashSet removeAllListeners() { + synchronized (listeners) { + HashSet clonedListeners = new HashSet(listeners); + listeners = new HashSet(); + return clonedListeners; + } + } + + @Override + public void addAllListeners(HashSet listeners) { + synchronized (listeners) { + this.listeners.addAll(listeners); + } + } + + @Override + public Iterable getAllListeners() { + return this.listeners; + } + + @Override + public Iterable getRawFixes(UUID clientId) { + assertReadLock(); + NavigableSet result = getRawFixes(); + Set edtc = eventsDeliveredToClient.get(clientId); + if (edtc == null) { + edtc = new HashSet(); + eventsDeliveredToClient.put(clientId, edtc); + } + edtc.addAll(result); + return result; + } + + @Override + public RaceLogEvent getEventById(Serializable id) { + assertReadLock(); + return eventsById.get(id); + } + + @Override + public NavigableSet getUnrevokedEvents() { + return new PartialNavigableSetView(super.getInternalFixes()) { + @Override + protected boolean isValid(RaceLogEvent e) { + return ! (e instanceof RevokeEvent) && ! revokedEventIds.contains(e.getId()); + } + }; + } + + @Override + public NavigableSet getUnrevokedEventsDescending() { + return new PartialNavigableSetView(super.getInternalFixes().descendingSet()) { + @Override + protected boolean isValid(RaceLogEvent e) { + return ! (e instanceof RevokeEvent) && ! revokedEventIds.contains(e.getId()); + } + }; + } + + @Override + public void merge(RaceLog other) { + lockForWrite(); + other.lockForRead(); + try { + RaceLogEventComparator comparator = RaceLogEventComparator.INSTANCE; + Iterator thisIter = getRawFixes().iterator(); + Iterator otherIter = other.getRawFixes().iterator(); + RaceLogEvent thisEvent = null; + RaceLogEvent otherEvent = null; + while (otherIter.hasNext() || otherEvent != null) { + if (thisEvent == null && thisIter.hasNext()) { + thisEvent = thisIter.next(); + } + if (otherEvent == null) { + otherEvent = otherIter.next(); + } + if (thisEvent == null) { + // All events of this race log have been consumed; simply keep adding the events + // from the other race log to this race log. + // otherEvent has to be non-null because if thisIter didn't have a next, otherIter must have had a next + add(otherEvent); + otherEvent = null; // "consumed" otherEvent; try to grab next if a next element exists in otherIter + } else { + final int comparison = comparator.compare(thisEvent, otherEvent); + if (comparison < 0) { + thisEvent = null; // skip the "lesser" race log event on this race log + } else if (comparison == 0) { + // the race log event from the other log is already contained in this log; skip both + thisEvent = null; + otherEvent = null; + } else { + // comparison > 0; we skipped on this race log until we found a "greater" event on this race log; insert otherEvent + add(otherEvent); + otherEvent = null; // "consumed" + } + } + } + } finally { + other.unlockAfterRead(); + unlockAfterWrite(); + } + } + + @Override + public void revokeEvent(RaceLogEventAuthor author, RaceLogEvent toRevoke) throws NotRevokableException { + if (toRevoke == null) { + throw new NotRevokableException("Received null as event to revoke"); + } + RevokeEvent revokeEvent = RaceLogEventFactory.INSTANCE.createRevokeEvent(MillisecondsTimePoint.now(), author, + getCurrentPassId(), toRevoke.getId()); + checkIfSuccessfullyRevokes(revokeEvent); + add(revokeEvent); + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/CloseOpenEndedDeviceMappingEvent.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/CloseOpenEndedDeviceMappingEvent.java new file mode 100644 index 00000000000..02bb1b851f3 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/CloseOpenEndedDeviceMappingEvent.java @@ -0,0 +1,26 @@ +package com.sap.sailing.domain.racelog.tracking; + +import java.io.Serializable; + +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.Revokable; + +/** + * Closes the time range of a {@link DeviceMappingEvent}. This means that the {@link #getClosingTimePoint() timepoint} provided + * by this event is substituted for the missing timepoint in the {@code DeviceMappingEvent} it refers to. + * @author Fredrik Teschke + * + */ +public interface CloseOpenEndedDeviceMappingEvent extends RaceLogEvent, Revokable { + /** + * Returns the timepoint that shall substitute the missing one in the corresponding {@link DeviceMappingEvent}. + */ + TimePoint getClosingTimePoint(); + + /** + * Returns the {@code id} of the event for which this event provides the closing timepoint. + * @return + */ + Serializable getDeviceMappingEventId(); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DefineMarkEvent.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DefineMarkEvent.java new file mode 100644 index 00000000000..8434c919775 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DefineMarkEvent.java @@ -0,0 +1,14 @@ +package com.sap.sailing.domain.racelog.tracking; + +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.Revokable; + +/** + * Defines a mark, that can then be used to build a course. + * @author Fredrik Teschke + * + */ +public interface DefineMarkEvent extends RaceLogEvent, Revokable { + Mark getMark(); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DenoteForTrackingEvent.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DenoteForTrackingEvent.java new file mode 100644 index 00000000000..afdf81449c7 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DenoteForTrackingEvent.java @@ -0,0 +1,23 @@ +package com.sap.sailing.domain.racelog.tracking; + +import java.io.Serializable; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.Revokable; + +/** + * The existence of this event in a {@link RaceLog} indicates that the necessary data + * for creating this race will be accumulated in this RaceLog. Once it is present, the + * actual {@link TrackedRace} can then be created from this information, and fixes for + * competitors and marks that arrive on the server, for which a corresponding + * {@link DeviceMappingEvent} exists will be added to the race. + * @author Fredrik Teschke + * + */ +public interface DenoteForTrackingEvent extends RaceLogEvent, Revokable { + String getRaceName(); + BoatClass getBoatClass(); + Serializable getRaceId(); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceCompetitorMappingEvent.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceCompetitorMappingEvent.java new file mode 100644 index 00000000000..a6b87fe3126 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceCompetitorMappingEvent.java @@ -0,0 +1,7 @@ +package com.sap.sailing.domain.racelog.tracking; + +import com.sap.sailing.domain.base.Competitor; + +public interface DeviceCompetitorMappingEvent extends DeviceMappingEvent { + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceIdentifier.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceIdentifier.java new file mode 100644 index 00000000000..119532ce1fa --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceIdentifier.java @@ -0,0 +1,58 @@ +package com.sap.sailing.domain.racelog.tracking; + +import java.io.Serializable; + +/** + * Identifies any kind of tracking device. Should be implemented accordingly for different tracking + * adapters, e.g. for smartphones, Igtimi trackers etc.

+ * + * Device identifiers are integrated in such a way, that a third party wishing to use their own + * types of device identifiers can do so without touching the existing code base. Instead, handlers + * for serializing device identifiers can be registered via the OSGi service registry. When adding a + * new device identifier, the following handlers have to be implemented and registered: + *

    + *
  • {@code DeviceIdentifierMongoHandler}
  • + *
+ * The following handlers are optional: + *
    + *
  • {@code DeviceIdentifierJsonHandler}: + * Enables managing such identifiers from a smartphone or tablet through the {@link RaceLog}.
  • + *
  • {@code DeviceIdentifierStringSerializationHandler}: + * Enables device identifiers to be created and managed in the admin console.
  • + *
+ *

+ * + * Refer to {@link SmartphoneUUIDIdentifier} and the related handlers as an implementation example. + * An example on how to register the handlers in the OSGi service registry is given in + * {@link DeviceIdentifierSerializationHandler}.

+ * + * The handler is chosen based on the {@code identifier type} of the device identifier. This identifier + * type should be unique, consider using a fully qualified class name.

+ * + * The {@link #getStringRepresentation() string representation} of a device identifier is used to generate + * a {@link PlaceHolderDeviceIdentifier} in case an appropriate handler is missing.

+ * + * {@link Object#equals(Object)} and {@link Object#hashCode()} should be implemented so that multiple + * deserializations of same device identifier (conceptually) result in objects that are be identified + * as being equal through these two methods. + * @author Fredrik Teschke + * + */ +public interface DeviceIdentifier extends Serializable { + /** + * The returned {@link String} is used to look up corresponding services for serialization + * and persistence. + * + * The reason for this design choice is that in future, third parties could easily write their own adapter and only + * have to register new OSGi service, and not touch the SAP Sailing Analytics code. + */ + String getIdentifierType(); + + /** + * Create a string representation, that can identify this device. + * The returned values should be unique for this identifier within its {@link #getIdentifierType() type}, + * but need not include the {@link #getIdentifierType() type} itself in the representation. + */ + String getStringRepresentation(); + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceIdentifierSerializationHandler.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceIdentifierSerializationHandler.java new file mode 100644 index 00000000000..40465a62bdb --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceIdentifierSerializationHandler.java @@ -0,0 +1,41 @@ +package com.sap.sailing.domain.racelog.tracking; + +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; + +/** + * Base interface for handlers of serialization and deserialization of {@link DeviceIdentifier}s. + * Additional information is passed around to enable fallback mechanisms (see {@link PlaceHolderDeviceIdentifier}) + * in case no appropriate handler for a {@code type} is registered.

+ * + * Handlers that implement child-interfaces (e.g. {@link DeviceIdentifierStringSerializationHandler} have to be + * registered in the OSGi service registry for the respective child-interface.Also, the device identifier type has to be + * added as a property to that service registration, as in this example:

+ * + *

+ * Dictionary properties = new Hashtable();
+ * properties.put(TypeBasedServiceFinder.TYPE, type);
+ * context.registerService(DeviceIdentifierMongoHandler.class, new SmartphoneUUIDMongoHandler(), properties)}.
+ * 

+ * + * @author Fredrik Teschke + * + */ +public interface DeviceIdentifierSerializationHandler { + /** + * Serialize the identifier. Also returns additional information, in case this is a fallback + * serializer, that actually returns some generic serializiation (e.g. {@link PlaceHolderDeviceIdentifier}). + * + * @param deviceIdentifier + * @return A pair: <{@code device type}, {@code serialized identifier}> + */ + Pair serialize(DeviceIdentifier deviceIdentifier) throws TransformationException; + + /** + * Deserialize the identifier. In case this is a fallback deserializer (e.g. for {@link PlaceHolderDeviceIdentifier}), + * additional information such as {@code type} and {@code stringRepresentation} is passed in.

+ * The handler should check whether all necessary information is contained (specifically, if the {@code serialized} + * object is not {@code null}, and otherwise throw an {@link TransformationException}. + */ + DeviceIdentifier deserialize(T serialized, String type, String stringRepresentation) throws TransformationException; +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceIdentifierStringSerializationHandler.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceIdentifierStringSerializationHandler.java new file mode 100644 index 00000000000..ab9b33eeebf --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceIdentifierStringSerializationHandler.java @@ -0,0 +1,17 @@ +package com.sap.sailing.domain.racelog.tracking; + + +/** + * Handle serialization to a {@code String}, for passing the identifier to the GWT frontend. + * Implementing this is not mandatory - if it were, there would be no need to have separate Mongo, JSON and other + * serialiazation handlers. + * + * This way, at least any device identifier that can be serialized to a string can also be exposed to the GWT frontend + * (can both be displayed and especially be created there!). + * + * @author Fredrik Teschke + * + */ +public interface DeviceIdentifierStringSerializationHandler extends DeviceIdentifierSerializationHandler { + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceMapping.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceMapping.java new file mode 100644 index 00000000000..3e1b2429ac2 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceMapping.java @@ -0,0 +1,26 @@ +package com.sap.sailing.domain.racelog.tracking; + +import java.io.Serializable; +import java.util.List; + +import com.sap.sailing.domain.base.Timed; +import com.sap.sailing.domain.common.TimeRange; +import com.sap.sailing.domain.common.WithID; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.DeviceMappingFinder; + +/** + * @see DeviceMappingFinder {@code DeviceMappings} are derived from {@code DeviceMappingEvents}. + * @author Fredrik Teschke + */ +public interface DeviceMapping extends Timed { + ItemType getMappedTo(); + DeviceIdentifier getDevice(); + + TimeRange getTimeRange(); + + /** + * List the ids of those race log events from which this mapping was generated. + * In the simplest case this is a single event. + */ + List getOriginalRaceLogEventIds(); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceMappingEvent.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceMappingEvent.java new file mode 100644 index 00000000000..853712e28f1 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceMappingEvent.java @@ -0,0 +1,38 @@ +package com.sap.sailing.domain.racelog.tracking; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.WithID; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.Revokable; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.DefinedMarkFinder; + +/** + * Event type for mapping {@link DeviceIdentifier devices} to {@code items} ({@link Competitor competitors} or {@link Mark marks}). + *

+ * Not both {@link #getFrom()} and {@link #getTo()} may be {@code null} at the same time (which would be a mapping for "all times", + * as the semantics of somehow later limiting this range would become unclear + *

+ * If one end of the time range enclosed by {@link #getFrom()} and {@link #getTo()} is open, this can be closed + * by a {@link CloseOpenEndedDeviceMappingEvent}. + * + * @see DefinedMarkFinder for rules by which {@link DeviceMapping}s are derived from these events. + * @author Fredrik Teschke + */ +public interface DeviceMappingEvent extends RaceLogEvent, Revokable { + ItemType getMappedTo(); + DeviceIdentifier getDevice(); + + /** + * The {@code return} value may be {@code null}, symbolizing an open range. + * In that case the device should be mapped to the item for any timepoint before that specified by {@link #getTo()}. + */ + TimePoint getFrom(); + + /** + * The {@code return} value may be {@code null}, symbolizing an open range. + * In that case the device should be mapped to the item for any timepoint after that specified by {@link #getFrom()}. + */ + TimePoint getTo(); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceMarkMappingEvent.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceMarkMappingEvent.java new file mode 100644 index 00000000000..6160fe62756 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/DeviceMarkMappingEvent.java @@ -0,0 +1,7 @@ +package com.sap.sailing.domain.racelog.tracking; + +import com.sap.sailing.domain.base.Mark; + +public interface DeviceMarkMappingEvent extends DeviceMappingEvent { + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/PlaceHolderDeviceIdentifier.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/PlaceHolderDeviceIdentifier.java new file mode 100644 index 00000000000..7bcc051ccb6 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/PlaceHolderDeviceIdentifier.java @@ -0,0 +1,65 @@ +package com.sap.sailing.domain.racelog.tracking; + + + +/** + * If the proper identifier cannot be resolved (with an approriate {@link DeviceIdentifierSerializationHandler}), for + * example because the OSGi context is not available on the smartphone receiving the {@link DevicieIdentifier} via its + * {@link RaceLog}, then this class can be used as a placeholder, that will at least offer the {@link #type} and + * {@link #stringRepresentation string representation} of the original identifier. + * + * @author Fredrik Teschke + * + */ +public class PlaceHolderDeviceIdentifier implements DeviceIdentifier { + private static final long serialVersionUID = 8642469389180406246L; + + private final String type; + private final String stringRepresentation; + + public PlaceHolderDeviceIdentifier(String type, String stringRepresentation) { + this.type = type; + this.stringRepresentation = stringRepresentation; + } + + @Override + public String getIdentifierType() { + return type; + } + + @Override + public String getStringRepresentation() { + return stringRepresentation; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((stringRepresentation == null) ? 0 : stringRepresentation.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + PlaceHolderDeviceIdentifier other = (PlaceHolderDeviceIdentifier) obj; + if (stringRepresentation == null) { + if (other.stringRepresentation != null) + return false; + } else if (!stringRepresentation.equals(other.stringRepresentation)) + return false; + if (type == null) { + if (other.type != null) + return false; + } else if (!type.equals(other.type)) + return false; + return true; + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/RegisterCompetitorEvent.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/RegisterCompetitorEvent.java new file mode 100644 index 00000000000..e6a3e256278 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/RegisterCompetitorEvent.java @@ -0,0 +1,20 @@ +package com.sap.sailing.domain.racelog.tracking; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.IsManagedBySharedDomainFactory; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.Revokable; + +/** + * Register a competitor for that race using the {@link RaceLog} in racelog-tracked races. + * + * A dummy {@link Competitor} implementation with only an {@link Competitor#getId() id} may be used, + * if the competitor is known to already exist on the server, as it is + * {@link IsManagedBySharedDomainFactory#resolve(com.sap.sailing.domain.base.SharedDomainFactory) resolved} + * on arrival. + * @author Fredrik Teschke + * + */ +public interface RegisterCompetitorEvent extends RaceLogEvent, Revokable { + Competitor getCompetitor(); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/SmartphoneUUIDIdentifier.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/SmartphoneUUIDIdentifier.java new file mode 100644 index 00000000000..7b2d16103d6 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/SmartphoneUUIDIdentifier.java @@ -0,0 +1,10 @@ +package com.sap.sailing.domain.racelog.tracking; + +import java.util.UUID; + + +public interface SmartphoneUUIDIdentifier extends DeviceIdentifier { + String TYPE = "smartphoneUUID"; + + UUID getUUID(); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/SmartphoneUUIDSerializationHandler.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/SmartphoneUUIDSerializationHandler.java new file mode 100644 index 00000000000..0f8050eba2f --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/SmartphoneUUIDSerializationHandler.java @@ -0,0 +1,27 @@ +package com.sap.sailing.domain.racelog.tracking; + +import java.util.UUID; + +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.racelog.tracking.impl.SmartphoneUUIDIdentifierImpl; + +public class SmartphoneUUIDSerializationHandler { + private SmartphoneUUIDIdentifier castIdentifier(DeviceIdentifier identifier) throws TransformationException { + if (!(identifier instanceof SmartphoneUUIDIdentifier)) + throw new TransformationException("Expected a SmartphoneUUIDIdentifier, got instead: " + identifier); + return (SmartphoneUUIDIdentifier) identifier; + } + + public Pair serialize(DeviceIdentifier deviceIdentifier) throws TransformationException { + return new Pair(castIdentifier(deviceIdentifier).getUUID().toString(), SmartphoneUUIDIdentifier.TYPE); + } + + public DeviceIdentifier deserialize(String input, String type, String stringRep) throws TransformationException { + try { + return new SmartphoneUUIDIdentifierImpl(UUID.fromString(stringRep)); + } catch (IllegalArgumentException e) { + throw new TransformationException("Invalid string representation of smartphone UUID", e); + } + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/StartTrackingEvent.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/StartTrackingEvent.java new file mode 100644 index 00000000000..ba607148ecd --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/StartTrackingEvent.java @@ -0,0 +1,16 @@ +package com.sap.sailing.domain.racelog.tracking; + +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.Revokable; +import com.sap.sailing.domain.racelog.tracking.events.DeviceCompetitorMappingEventImpl; + +/** + * Closes the phase during which {@link DeviceCompetitorMappingEventImpl}s actually result + * in competitors being added, and indicates that the server should create a {@link TrackedRace} + * using the information in this {@link RaceLog}. + * + * @author Fredrik Teschke + * + */ +public interface StartTrackingEvent extends RaceLogEvent, Revokable { +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/AllEventsOfTypeFinder.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/AllEventsOfTypeFinder.java new file mode 100644 index 00000000000..1edf0fca849 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/AllEventsOfTypeFinder.java @@ -0,0 +1,37 @@ +package com.sap.sailing.domain.racelog.tracking.analyzing.impl; + +import java.util.ArrayList; +import java.util.List; + +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.analyzing.impl.RaceLogAnalyzer; + +/** + * Finds all events in the racelog which is an {@code instanceof} {@link #ofType}. + * @author Fredrik Teschke + * + */ +public class AllEventsOfTypeFinder extends RaceLogAnalyzer> { + private final Class ofType; + private final boolean onlyUnrevoked; + + public AllEventsOfTypeFinder(RaceLog raceLog, boolean onlyUnrevoked, Class ofType) { + super(raceLog); + this.ofType = ofType; + this.onlyUnrevoked = onlyUnrevoked; + } + + @Override + protected List performAnalysis() { + List result = new ArrayList(); + Iterable set = onlyUnrevoked ? raceLog.getUnrevokedEventsDescending() : getAllEventsDescending(); + for (RaceLogEvent event : set) { + if (ofType.isAssignableFrom(event.getClass())) { + result.add(event); + } + } + return result; + } + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/DefinedMarkFinder.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/DefinedMarkFinder.java new file mode 100644 index 00000000000..5bd0b68e005 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/DefinedMarkFinder.java @@ -0,0 +1,30 @@ +package com.sap.sailing.domain.racelog.tracking.analyzing.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.analyzing.impl.RaceLogAnalyzer; +import com.sap.sailing.domain.racelog.tracking.DefineMarkEvent; + +public class DefinedMarkFinder extends RaceLogAnalyzer> { + public DefinedMarkFinder(RaceLog raceLog) { + super(raceLog); + } + + @Override + protected Collection performAnalysis() { + List< Mark> result = new ArrayList(); + for (RaceLogEvent event : raceLog.getUnrevokedEvents()) { + if (event instanceof DefineMarkEvent) { + DefineMarkEvent dME = (DefineMarkEvent) event; + result.add(dME.getMark()); + } + } + return result; + } + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/DeviceCompetitorMappingFinder.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/DeviceCompetitorMappingFinder.java new file mode 100644 index 00000000000..6e177a3e5fc --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/DeviceCompetitorMappingFinder.java @@ -0,0 +1,18 @@ +package com.sap.sailing.domain.racelog.tracking.analyzing.impl; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.tracking.DeviceCompetitorMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceMappingEvent; + +public class DeviceCompetitorMappingFinder extends DeviceMappingFinder { + + public DeviceCompetitorMappingFinder(RaceLog raceLog) { + super(raceLog); + } + + @Override + protected boolean isValidMapping(DeviceMappingEvent mapping) { + return mapping instanceof DeviceCompetitorMappingEvent; + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/DeviceMappingFinder.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/DeviceMappingFinder.java new file mode 100644 index 00000000000..80b5b6c6d61 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/DeviceMappingFinder.java @@ -0,0 +1,101 @@ +package com.sap.sailing.domain.racelog.tracking.analyzing.impl; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.TimeRange; +import com.sap.sailing.domain.common.WithID; +import com.sap.sailing.domain.common.impl.TimeRangeImpl; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.analyzing.impl.RaceLogAnalyzer; +import com.sap.sailing.domain.racelog.tracking.CloseOpenEndedDeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.DeviceMapping; +import com.sap.sailing.domain.racelog.tracking.DeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.impl.DeviceMappingImpl; + +/** + * Extracts the {@link DeviceMapping}s with the appropriate {@link TimeRange}s from the {@link RaceLog}. + * Based on the {@link DeviceMappingEvent}s found in the {@code RaceLog} for the tracked {@code items} + * ({@code Competitor}s and {@code Marks}), the actual mappings are created. + *

+ * Mappings can be defined with an open-ended time range (e.g. track this from now on), which should then be + * closed by a corresponding {@link CloseOpenEndedDeviceMappingEvent}. + *

+ * The process of removing conflicts between mappings to ensure that only one mapping was active for + * one tracked item has been removed. Instead, every {@code DeviceMappingEvent} now results directly in one + * {@code DeviceMapping}, the only changes being introduced by closing open time ranges in cases where an + * according {@code CloseOpenEndedDeviceMappingEvent} exists. + */ +public class DeviceMappingFinder extends RaceLogAnalyzer>>> { + public DeviceMappingFinder(RaceLog raceLog) { + super(raceLog); + } + + protected boolean isValidMapping(DeviceMappingEvent mapping) { + return mapping instanceof DeviceMappingEvent; + } + + protected DeviceMapping getMapping(DeviceIdentifier device, ItemT item, + TimePoint from, TimePoint to, Serializable originalEventId) { + return new DeviceMappingImpl(item, device, new TimeRangeImpl(from, to), + Collections.singletonList(originalEventId)); + } + + private List getItemSet(Map> map, T item) { + List list = map.get(item); + if (list == null) { + list = new ArrayList(); + map.put(item, list); + } + return list; + } + @Override + protected Map>> performAnalysis() { + Map>> events = new HashMap>>(); + Map>> mappings = new HashMap>>(); + Map closingEvents = new HashMap(); + + for (RaceLogEvent e : raceLog.getUnrevokedEvents()) { + if (e instanceof DeviceMappingEvent && isValidMapping(((DeviceMappingEvent) e))) { + @SuppressWarnings("unchecked") + DeviceMappingEvent mappingEvent = (DeviceMappingEvent) e; + getItemSet(events, mappingEvent.getMappedTo()).add(mappingEvent); + } else if (e instanceof CloseOpenEndedDeviceMappingEvent) { + //a higher priority closing events for the same mapping event overwrites the lower priority one + CloseOpenEndedDeviceMappingEvent closingEvent = (CloseOpenEndedDeviceMappingEvent) e; + closingEvents.put(closingEvent.getDeviceMappingEventId(), closingEvent); + } + } + + for (ItemT item : events.keySet()) { + mappings.put(item, closeOpenRanges(events.get(item), item, closingEvents)); + } + + return mappings; + } + + private List> closeOpenRanges(List> events, ItemT item, + Map closingEvents) { + List> result = new ArrayList>(); + + for (DeviceMappingEvent event : events) { + TimePoint from = event.getFrom(); + TimePoint to = event.getTo(); + TimePoint closingTimePoint = closingEvents.containsKey(event.getId()) ? + closingEvents.get(event.getId()).getClosingTimePoint() : null; + if (from == null) from = closingTimePoint; + if (to == null) to = closingTimePoint; + + result.add(getMapping(event.getDevice(), item, from, to, event.getId())); + } + + return result; + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/DeviceMarkMappingFinder.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/DeviceMarkMappingFinder.java new file mode 100644 index 00000000000..9832673f295 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/DeviceMarkMappingFinder.java @@ -0,0 +1,18 @@ +package com.sap.sailing.domain.racelog.tracking.analyzing.impl; + +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.tracking.DeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceMarkMappingEvent; + +public class DeviceMarkMappingFinder extends DeviceMappingFinder { + + public DeviceMarkMappingFinder(RaceLog raceLog) { + super(raceLog); + } + + @Override + protected boolean isValidMapping(DeviceMappingEvent mapping) { + return mapping instanceof DeviceMarkMappingEvent; + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/LastEventOfTypeFinder.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/LastEventOfTypeFinder.java new file mode 100644 index 00000000000..16546f027c8 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/LastEventOfTypeFinder.java @@ -0,0 +1,33 @@ +package com.sap.sailing.domain.racelog.tracking.analyzing.impl; + +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.analyzing.impl.RaceLogAnalyzer; + +/** + * Finds the most recent event in the racelog which is an {@code instanceof} {@link #ofType}. + * @author Fredrik Teschke + * + */ +public class LastEventOfTypeFinder extends RaceLogAnalyzer { + private final Class ofType; + private final boolean onlyUnrevoked; + + public LastEventOfTypeFinder(RaceLog raceLog, boolean onlyUnrevoked, Class ofType) { + super(raceLog); + this.ofType = ofType; + this.onlyUnrevoked = onlyUnrevoked; + } + + @Override + protected RaceLogEvent performAnalysis() { + Iterable set = onlyUnrevoked ? raceLog.getUnrevokedEventsDescending() : getAllEventsDescending(); + for (RaceLogEvent event : set) { + if (ofType.isAssignableFrom(event.getClass())) { + return event; + } + } + return null; + } + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/LastPingMappingEventFinder.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/LastPingMappingEventFinder.java new file mode 100644 index 00000000000..f88720758be --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/LastPingMappingEventFinder.java @@ -0,0 +1,33 @@ +package com.sap.sailing.domain.racelog.tracking.analyzing.impl; + +import com.sap.sailing.domain.common.WithID; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.analyzing.impl.RaceLogAnalyzer; +import com.sap.sailing.domain.racelog.tracking.DeviceMappingEvent; + +public class LastPingMappingEventFinder extends RaceLogAnalyzer> { + private final WithID item; + + public LastPingMappingEventFinder(RaceLog raceLog, WithID forItem) { + super(raceLog); + this.item = forItem; + } + + @Override + protected DeviceMappingEvent performAnalysis() { + for (RaceLogEvent e : raceLog.getUnrevokedEventsDescending()) { + if (e instanceof DeviceMappingEvent) { + DeviceMappingEvent mappingEvent = (DeviceMappingEvent) e; + if (item.equals(mappingEvent.getMappedTo()) && mappingEvent.getFrom() != null && + mappingEvent.getTo() != null && mappingEvent.getFrom().equals(mappingEvent.getTo())) { + @SuppressWarnings("unchecked") + DeviceMappingEvent cast = (DeviceMappingEvent) mappingEvent; + return cast; + } + } + } + return null; + } + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/OpenEndedDeviceMappingCloser.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/OpenEndedDeviceMappingCloser.java new file mode 100644 index 00000000000..48ab1c6cabc --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/OpenEndedDeviceMappingCloser.java @@ -0,0 +1,49 @@ +package com.sap.sailing.domain.racelog.tracking.analyzing.impl; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventFactory; +import com.sap.sailing.domain.racelog.analyzing.impl.RaceLogAnalyzer; +import com.sap.sailing.domain.racelog.tracking.CloseOpenEndedDeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceMapping; +import com.sap.sailing.domain.racelog.tracking.DeviceMappingEvent; + +/** + * Generates events that close the events that are responsible for the {@link #mapping} + * that is passed to the constructor. + * @author Fredrik Teschke + * + */ +public class OpenEndedDeviceMappingCloser extends RaceLogAnalyzer> { + private final DeviceMapping mapping; + private final RaceLogEventAuthor author; + private final TimePoint closingTimePoint; + + public OpenEndedDeviceMappingCloser(RaceLog raceLog, DeviceMapping mapping, RaceLogEventAuthor author, + TimePoint closingTimePoint) { + super(raceLog); + this.mapping = mapping; + this.author = author; + this.closingTimePoint = closingTimePoint; + } + + @Override + protected List performAnalysis() { + List result = new ArrayList(); + + for (Serializable eventId : mapping.getOriginalRaceLogEventIds()) { + DeviceMappingEvent event = (DeviceMappingEvent) raceLog.getEventById(eventId); + if (event.getFrom() == null || event.getTo() == null) { + result.add(RaceLogEventFactory.INSTANCE.createCloseOpenEndedDeviceMappingEvent( + MillisecondsTimePoint.now(), author, raceLog.getCurrentPassId(), event.getId(), closingTimePoint)); + } + } + return result; + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/RaceInformationFinder.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/RaceInformationFinder.java new file mode 100644 index 00000000000..a21e518d268 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/RaceInformationFinder.java @@ -0,0 +1,23 @@ +package com.sap.sailing.domain.racelog.tracking.analyzing.impl; + +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.analyzing.impl.RaceLogAnalyzer; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; + +public class RaceInformationFinder extends RaceLogAnalyzer { + public RaceInformationFinder(RaceLog raceLog) { + super(raceLog); + } + + @Override + protected DenoteForTrackingEvent performAnalysis() { + for (RaceLogEvent event : getAllEvents()) { + if (event instanceof DenoteForTrackingEvent) { + return (DenoteForTrackingEvent) event; + } + } + + return null; + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/RaceLogTrackingStateAnalyzer.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/RaceLogTrackingStateAnalyzer.java new file mode 100644 index 00000000000..712cde0e9a4 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/RaceLogTrackingStateAnalyzer.java @@ -0,0 +1,28 @@ +package com.sap.sailing.domain.racelog.tracking.analyzing.impl; + +import com.sap.sailing.domain.common.racelog.tracking.RaceLogTrackingState; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.analyzing.impl.RaceLogAnalyzer; +import com.sap.sailing.domain.racelog.tracking.StartTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; + +public class RaceLogTrackingStateAnalyzer extends RaceLogAnalyzer { + + public RaceLogTrackingStateAnalyzer(RaceLog raceLog) { + super(raceLog); + } + + @Override + protected RaceLogTrackingState performAnalysis() { + for (RaceLogEvent event : raceLog.getUnrevokedEventsDescending()) { + if (event instanceof StartTrackingEvent) { + return RaceLogTrackingState.TRACKING; + } else if (event instanceof DenoteForTrackingEvent) { + return RaceLogTrackingState.AWAITING_RACE_DEFINITION; + } + } + return RaceLogTrackingState.NOT_A_RACELOG_TRACKED_RACE; + } + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/RegisteredCompetitorsAnalyzer.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/RegisteredCompetitorsAnalyzer.java new file mode 100644 index 00000000000..476588d7026 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/analyzing/impl/RegisteredCompetitorsAnalyzer.java @@ -0,0 +1,37 @@ +package com.sap.sailing.domain.racelog.tracking.analyzing.impl; + +import java.util.HashSet; +import java.util.Set; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.analyzing.impl.RaceLogAnalyzer; +import com.sap.sailing.domain.racelog.tracking.RegisterCompetitorEvent; + +/** + * Uses the {@link RegisterCompetitorEvent}s in the {@link RaceLog} + * to determine the competitors registered for this event. + * @author Fredrik Teschke + * + */ +public class RegisteredCompetitorsAnalyzer extends RaceLogAnalyzer> { + + public RegisteredCompetitorsAnalyzer(RaceLog raceLog) { + super(raceLog); + } + + @Override + protected Set performAnalysis() { + Set result = new HashSet(); + + for (RaceLogEvent event : getRaceLog().getUnrevokedEvents()) { + if (event instanceof RegisterCompetitorEvent) { + result.add(((RegisterCompetitorEvent) event).getCompetitor()); + } + } + + return result; + } + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/AbstractDeviceMappingEventImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/AbstractDeviceMappingEventImpl.java new file mode 100644 index 00000000000..4c4320912a8 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/AbstractDeviceMappingEventImpl.java @@ -0,0 +1,50 @@ +package com.sap.sailing.domain.racelog.tracking.events; + +import java.io.Serializable; +import java.util.Collections; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.WithID; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.impl.RaceLogEventImpl; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.DeviceMappingEvent; + +public abstract class AbstractDeviceMappingEventImpl extends RaceLogEventImpl implements +DeviceMappingEvent { + private static final long serialVersionUID = -8439653251231710356L; + + private final ItemType mappedTo; + private final DeviceIdentifier device; + private final TimePoint from; + private final TimePoint to; + + public AbstractDeviceMappingEventImpl(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable pId, int pPassId, ItemType mappedTo, + DeviceIdentifier device, TimePoint from, TimePoint to) { + super(createdAt, author, logicalTimePoint, pId, Collections.emptyList(), pPassId); + this.mappedTo = mappedTo; + this.device = device; + this.from = from; + this.to = to; + } + + public ItemType getMappedTo() { + return mappedTo; + } + public DeviceIdentifier getDevice() { + return device; + } + public TimePoint getFrom() { + return from; + } + public TimePoint getTo() { + return to; + } + + @Override + public String getShortInfo() { + return "device: " + device + ", mapped to: " + mappedTo + ", from: " + from + ", to: " + to; + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/CloseOpenEndedDeviceMappingEventImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/CloseOpenEndedDeviceMappingEventImpl.java new file mode 100644 index 00000000000..6ae55f525c7 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/CloseOpenEndedDeviceMappingEventImpl.java @@ -0,0 +1,42 @@ +package com.sap.sailing.domain.racelog.tracking.events; + +import java.io.Serializable; +import java.util.Collections; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventVisitor; +import com.sap.sailing.domain.racelog.impl.RaceLogEventImpl; +import com.sap.sailing.domain.racelog.tracking.CloseOpenEndedDeviceMappingEvent; + +public class CloseOpenEndedDeviceMappingEventImpl extends RaceLogEventImpl implements CloseOpenEndedDeviceMappingEvent { + private static final long serialVersionUID = -2401732623610224918L; + + private final Serializable deviceMappingEventId; + private final TimePoint closingTimePoint; + + public CloseOpenEndedDeviceMappingEventImpl(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable pId, int pPassId, + Serializable deviceMappingEventId, TimePoint closingTimePoint) { + super(createdAt, author, logicalTimePoint, pId, Collections.emptyList(), pPassId); + this.deviceMappingEventId = deviceMappingEventId; + this.closingTimePoint = closingTimePoint; + } + + @Override + public void accept(RaceLogEventVisitor visitor) { + visitor.visit(this); + } + + @Override + public Serializable getDeviceMappingEventId() { + return deviceMappingEventId; + } + + @Override + public TimePoint getClosingTimePoint() { + return closingTimePoint; + } + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/DefineMarkEventImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/DefineMarkEventImpl.java new file mode 100644 index 00000000000..5bed57f7fef --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/DefineMarkEventImpl.java @@ -0,0 +1,35 @@ +package com.sap.sailing.domain.racelog.tracking.events; + +import java.io.Serializable; +import java.util.Collections; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventVisitor; +import com.sap.sailing.domain.racelog.impl.RaceLogEventImpl; +import com.sap.sailing.domain.racelog.tracking.DefineMarkEvent; + +public class DefineMarkEventImpl extends RaceLogEventImpl implements DefineMarkEvent { + private static final long serialVersionUID = 277007856878002208L; + + private final Mark mark; + + public DefineMarkEventImpl(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable pId, int pPassId, Mark mark) { + super(createdAt, author, logicalTimePoint, pId, Collections.emptyList(), pPassId); + this.mark = mark; + } + + @Override + public void accept(RaceLogEventVisitor visitor) { + visitor.visit(this); + } + + @Override + public Mark getMark() { + return mark; + } + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/DenoteForTrackingEventImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/DenoteForTrackingEventImpl.java new file mode 100644 index 00000000000..c3600530b63 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/DenoteForTrackingEventImpl.java @@ -0,0 +1,51 @@ +package com.sap.sailing.domain.racelog.tracking.events; + +import java.io.Serializable; +import java.util.Collections; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventVisitor; +import com.sap.sailing.domain.racelog.impl.RaceLogEventImpl; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; + +public class DenoteForTrackingEventImpl extends RaceLogEventImpl implements +DenoteForTrackingEvent { + private static final long serialVersionUID = 6937741283401976385L; + + private final String raceName; + private final BoatClass boatClass; + private final Serializable raceId; + + public DenoteForTrackingEventImpl(TimePoint createdAt, + RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable pId, int pPassId, + String raceName, BoatClass boatClass, Serializable raceId) { + super(createdAt, author, logicalTimePoint, pId, Collections.emptyList(), pPassId); + this.raceName = raceName; + this.boatClass = boatClass; + this.raceId = raceId; + } + + @Override + public void accept(RaceLogEventVisitor visitor) { + visitor.visit(this); + } + + @Override + public String getRaceName() { + return raceName; + } + + @Override + public BoatClass getBoatClass() { + return boatClass; + } + + @Override + public Serializable getRaceId() { + return raceId; + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/DeviceCompetitorMappingEventImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/DeviceCompetitorMappingEventImpl.java new file mode 100644 index 00000000000..9338ba836c5 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/DeviceCompetitorMappingEventImpl.java @@ -0,0 +1,25 @@ +package com.sap.sailing.domain.racelog.tracking.events; + +import java.io.Serializable; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventVisitor; +import com.sap.sailing.domain.racelog.tracking.DeviceCompetitorMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; + +public class DeviceCompetitorMappingEventImpl extends AbstractDeviceMappingEventImpl +implements DeviceCompetitorMappingEvent { + public DeviceCompetitorMappingEventImpl(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable pId, int pPassId, Competitor mappedTo, DeviceIdentifier device, TimePoint from, TimePoint to) { + super(createdAt, author, logicalTimePoint, pId, pPassId, mappedTo, device, from, to); + } + + private static final long serialVersionUID = -1494030544804758753L; + + @Override + public void accept(RaceLogEventVisitor visitor) { + visitor.visit(this); + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/DeviceMarkMappingEventImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/DeviceMarkMappingEventImpl.java new file mode 100644 index 00000000000..539eff2d742 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/DeviceMarkMappingEventImpl.java @@ -0,0 +1,25 @@ +package com.sap.sailing.domain.racelog.tracking.events; + +import java.io.Serializable; + +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventVisitor; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.DeviceMarkMappingEvent; + +public class DeviceMarkMappingEventImpl extends AbstractDeviceMappingEventImpl +implements DeviceMarkMappingEvent { + public DeviceMarkMappingEventImpl(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable pId, int pPassId, Mark mappedTo, DeviceIdentifier device, TimePoint from, TimePoint to) { + super(createdAt, author, logicalTimePoint, pId, pPassId, mappedTo, device, from, to); + } + + private static final long serialVersionUID = -1494030544804758753L; + + @Override + public void accept(RaceLogEventVisitor visitor) { + visitor.visit(this); + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/RegisterCompetitorEventImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/RegisterCompetitorEventImpl.java new file mode 100644 index 00000000000..3db0e10a290 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/RegisterCompetitorEventImpl.java @@ -0,0 +1,30 @@ +package com.sap.sailing.domain.racelog.tracking.events; + +import java.io.Serializable; +import java.util.Collections; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventVisitor; +import com.sap.sailing.domain.racelog.impl.RaceLogEventImpl; +import com.sap.sailing.domain.racelog.tracking.RegisterCompetitorEvent; + +public class RegisterCompetitorEventImpl extends RaceLogEventImpl implements RegisterCompetitorEvent { + private static final long serialVersionUID = -30864810737555657L; + + public RegisterCompetitorEventImpl(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable pId, int pPassId, Competitor competitor) { + super(createdAt, author, logicalTimePoint, pId, Collections.singletonList(competitor), pPassId); + } + + @Override + public void accept(RaceLogEventVisitor visitor) { + visitor.visit(this); + } + + @Override + public Competitor getCompetitor() { + return getInvolvedBoats().get(0); + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/RevokeEventImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/RevokeEventImpl.java new file mode 100644 index 00000000000..01c45e24e86 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/RevokeEventImpl.java @@ -0,0 +1,33 @@ +package com.sap.sailing.domain.racelog.tracking.events; + +import java.io.Serializable; +import java.util.Collections; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventVisitor; +import com.sap.sailing.domain.racelog.RevokeEvent; +import com.sap.sailing.domain.racelog.impl.RaceLogEventImpl; + +public class RevokeEventImpl extends RaceLogEventImpl implements RevokeEvent { + private static final long serialVersionUID = -30864810737555657L; + + private final Serializable revokedEventId; + + public RevokeEventImpl(TimePoint createdAt, RaceLogEventAuthor author, TimePoint logicalTimePoint, + Serializable pId, int pPassId, Serializable revokedEventId) { + super(createdAt, author, logicalTimePoint, pId, Collections.emptyList(), pPassId); + this.revokedEventId = revokedEventId; + } + + @Override + public void accept(RaceLogEventVisitor visitor) { + visitor.visit(this); + } + + @Override + public Serializable getRevokedEventId() { + return revokedEventId; + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/StartTrackingEventImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/StartTrackingEventImpl.java new file mode 100644 index 00000000000..6cebf36d276 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/events/StartTrackingEventImpl.java @@ -0,0 +1,28 @@ +package com.sap.sailing.domain.racelog.tracking.events; + +import java.io.Serializable; +import java.util.Collections; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventVisitor; +import com.sap.sailing.domain.racelog.impl.RaceLogEventImpl; +import com.sap.sailing.domain.racelog.tracking.StartTrackingEvent; + +public class StartTrackingEventImpl extends RaceLogEventImpl implements +StartTrackingEvent { + private static final long serialVersionUID = 6017954368580125221L; + + public StartTrackingEventImpl(TimePoint createdAt, RaceLogEventAuthor author, + TimePoint logicalTimePoint, Serializable pId, int pPassId) { + super(createdAt, author, logicalTimePoint, pId, Collections.emptyList(), pPassId); + } + + + @Override + public void accept(RaceLogEventVisitor visitor) { + visitor.visit(this); + } + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/impl/DeviceMappingImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/impl/DeviceMappingImpl.java new file mode 100644 index 00000000000..2ffbab79491 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/impl/DeviceMappingImpl.java @@ -0,0 +1,73 @@ +package com.sap.sailing.domain.racelog.tracking.impl; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.TimeRange; +import com.sap.sailing.domain.common.WithID; +import com.sap.sailing.domain.common.impl.TimeRangeImpl; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.DeviceMapping; +import com.sap.sailing.domain.racelog.tracking.DeviceMappingEvent; + +public class DeviceMappingImpl implements DeviceMapping { + private static final long serialVersionUID = -4602948402371623654L; + + private final ItemType mappedTo; + private final DeviceIdentifier device; + private final TimeRange timeRange; + private final List originalRaceLogEventIds = new ArrayList(); + + public DeviceMappingImpl(ItemType mappedTo, DeviceIdentifier device, TimeRange timeRange) { + this.mappedTo = mappedTo; + this.device = device; + this.timeRange = timeRange; + } + + public DeviceMappingImpl(ItemType mappedTo, DeviceIdentifier device, TimeRange timeRange, Serializable originalRaceLogEventId) { + this(mappedTo, device, timeRange); + if (originalRaceLogEventId != null) originalRaceLogEventIds.add(originalRaceLogEventId); + } + + public DeviceMappingImpl(ItemType mappedTo, DeviceIdentifier device, TimeRange timeRange, List originalRaceLogEventIds) { + this(mappedTo, device, timeRange); + if (originalRaceLogEventIds != null) this.originalRaceLogEventIds.addAll(originalRaceLogEventIds); + } + + @Override + public DeviceIdentifier getDevice() { + return device; + } + + @Override + public TimeRange getTimeRange() { + return timeRange; + } + + @Override + public ItemType getMappedTo() { + return mappedTo; + } + + @Override + public TimePoint getTimePoint() { + return timeRange.from(); + } + + public static DeviceMapping convertToDeviceMapping(DeviceMappingEvent event) { + return new DeviceMappingImpl(event.getMappedTo(), event.getDevice(), new TimeRangeImpl(event.getFrom(), event.getTo()), + event.getId()); + } + + @Override + public String toString() { + return "Device Mapping - device: " + device + ", mapped to: " + mappedTo + ", time range: " + timeRange; + } + + @Override + public List getOriginalRaceLogEventIds() { + return originalRaceLogEventIds; + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/impl/PlaceHolderDeviceIdentifierSerializationHandler.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/impl/PlaceHolderDeviceIdentifierSerializationHandler.java new file mode 100644 index 00000000000..25ac6c22cea --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/impl/PlaceHolderDeviceIdentifierSerializationHandler.java @@ -0,0 +1,28 @@ +package com.sap.sailing.domain.racelog.tracking.impl; + +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.PlaceHolderDeviceIdentifier; + +/** + * Can be used as the last option, if no other service is available when trying to load an identifier + * from, or save it to the database. + * The handler itself doesn't do much, but relies on the {@code type} and {@code stringRepresentation} + * to be saved and loaded. This way, the {@code stringRepresentation} is always available, even if the + * appropriate persistence service was available when writing, and is now unavailable when loading. + * + * @author Fredrik Teschke + * + */ +public class PlaceHolderDeviceIdentifierSerializationHandler { + + public DeviceIdentifier deserialize(String object, String type, String stringRepresentation) throws TransformationException { + return new PlaceHolderDeviceIdentifier(type, stringRepresentation); + } + + public Pair serialize(DeviceIdentifier object) throws TransformationException { + return new Pair(object.getIdentifierType(), null); + } + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/impl/SmartphoneUUIDIdentifierImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/impl/SmartphoneUUIDIdentifierImpl.java new file mode 100644 index 00000000000..27bedb74bf7 --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/racelog/tracking/impl/SmartphoneUUIDIdentifierImpl.java @@ -0,0 +1,48 @@ +package com.sap.sailing.domain.racelog.tracking.impl; + +import java.util.UUID; + +import com.sap.sailing.domain.racelog.tracking.SmartphoneUUIDIdentifier; + + +public class SmartphoneUUIDIdentifierImpl implements SmartphoneUUIDIdentifier { + private static final long serialVersionUID = -1747763242191049428L; + + private final UUID uuid; + + public SmartphoneUUIDIdentifierImpl(UUID uuid) { + this.uuid = uuid; + } + + public UUID getUUID() { + return uuid; + } + + @Override + public String getIdentifierType() { + return TYPE; + } + + @Override + public String toString() { + return "UUID " + uuid; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SmartphoneUUIDIdentifier) { + return ((SmartphoneUUIDIdentifier) obj).getUUID().equals(uuid); + } + return false; + } + + @Override + public int hashCode() { + return uuid.hashCode(); + } + + @Override + public String getStringRepresentation() { + return uuid.toString(); + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/Track.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/Track.java index 4ab66c047bd..62cd5b9c7ae 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/Track.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/Track.java @@ -1,147 +1,147 @@ -package com.sap.sailing.domain.tracking; - -import java.io.Serializable; -import java.util.Iterator; -import java.util.concurrent.locks.ReadWriteLock; - -import com.sap.sailing.domain.base.Timed; -import com.sap.sailing.domain.common.Duration; -import com.sap.sailing.domain.common.TimePoint; - -/** - * A track records {@link Timed} items for an object of type ItemType. It allows clients to ask for a value - * close to a given {@link TimePoint}. The track manages a time-based set of raw fixes. An implementation may have an - * understanding of how to eliminate outliers. For example, if a track implementation knows it's tracking boats, it may - * consider fixes that the boat cannot possibly have reached due to its speed and direction change limitations as - * outliers. The set of fixes with outliers filtered out can be obtained using {@link #getFixes} whereas - * {@link #getRawFixes()} returns the unfiltered, raw fixes. If an implementation has no idea what an outlier is, - * both methods will return the same fix sequence.

- * - * With tracks, concurrency is an important issue. Threads may want to modify a track while other threads may want to - * read from it. Several methods such as {@link #getLastFixAtOrBefore(TimePoint)} return a single fix and can manage - * concurrency internally. However, those methods returning a collection of fixes, such as {@link #getFixes()} or an - * iterator over a collection of fixes, such as {@link #getFixesIterator(TimePoint, boolean)}, need special treatment. - * Until we internalize such iterations (see bug 824, http://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=824), - * callers need to manage a read lock which is part of a {@link ReadWriteLock} managed by this track. Callers do so - * by calling {@link #lockForRead} and {@link #unlockAfterRead}. - * - * @author Axel Uhl (d043530) - */ -public interface Track extends Serializable { - /** - * Locks this track for reading by the calling thread. If the thread already holds the lock for this track, - * the hold count will be incremented. Make sure to call {@link #unlockAfterRead()} in a finally - * block to release the lock under all possible circumstances. Failure to do so will inevitably lead to - * deadlocks! - */ - void lockForRead(); - - /** - * Decrements the hold count for this track's read lock for the calling thread. If it goes to zero, the lock will be - * released and other readers or a writer can obtain the lock. Make sure to call this method in a - * finally block for each {@link #lockForRead()} invocation. - */ - void unlockAfterRead(); - - /** - * Callers must have called {@link #lockForRead()} before calling this method. This will be checked, and an exception - * will be thrown in case the caller has failed to do so. - * - * @return the smoothened fixes - */ - Iterable getFixes(); - - /** - * Callers must have called {@link #lockForRead()} before calling this method. This will be checked, and an exception - * will be thrown in case the caller has failed to do so. - * - * @return The smoothened fixes between from and to. - */ - Iterable getFixes(TimePoint from, boolean fromInclusive, TimePoint to, boolean toInclusive); - - /** - * Callers must have called {@link #lockForRead()} before calling this method. This will be checked, and an exception - * will be thrown in case the caller has failed to do so. - */ - Iterable getRawFixes(); - - FixType getLastFixAtOrBefore(TimePoint timePoint); - - FixType getLastFixBefore(TimePoint timePoint); - - FixType getLastRawFixAtOrBefore(TimePoint timePoint); - - FixType getFirstFixAtOrAfter(TimePoint timePoint); - - FixType getFirstRawFixAtOrAfter(TimePoint timePoint); - - FixType getLastRawFixBefore(TimePoint timePoint); - - FixType getFirstRawFixAfter(TimePoint timePoint); - - FixType getFirstFixAfter(TimePoint timePoint); - - /** - * The first fix in this track or null if the track is empty. The fix returned may - * be an outlier that is not returned by calls operating on the smoothened version of the track. - */ - FixType getFirstRawFix(); - - /** - * The last fix in this track or null if the track is empty. The fix returned may - * be an outlier that is not returned by calls operating on the smoothened version of the track. - */ - FixType getLastRawFix(); - - /** - * Returns an iterator starting at the first fix after startingAt (or "at or after" in case - * inclusive is true). The fixes returned by the iterator are the smoothened fixes (see - * also {@link #getFixes()}, without any smoothening or dampening applied. - * - * Callers must have called {@link #lockForRead()} before calling this method. This will be checked, and an exception - * will be thrown in case the caller has failed to do so. - */ - Iterator getFixesIterator(TimePoint startingAt, boolean inclusive); - - /** - * Returns an iterator starting at the first raw fix after startingAt (or "at or after" in case - * inclusive is true). The fixes returned by the iterator are the raw fixes (see also - * {@link #getRawFixes()}, without any smoothening or dampening applied. - * - * Callers must have called {@link #lockForRead()} before calling this method. This will be checked, and an exception - * will be thrown in case the caller has failed to do so. - */ - Iterator getRawFixesIterator(TimePoint startingAt, boolean inclusive); - - /** - * Returns a descending iterator starting at the first fix before startingAt (or "at or before" in case - * inclusive is true). The fixes returned by the iterator are the smoothened fixes (see - * also {@link #getFixes()}, without any smoothening or dampening applied. - * - * Callers must have called {@link #lockForRead()} before calling this method. This will be checked, and an exception - * will be thrown in case the caller has failed to do so. - */ - Iterator getFixesDescendingIterator(TimePoint startingAt, boolean inclusive); - - /** - * Returns a descending iterator starting at the first raw fix before startingAt (or "at or before" in case - * inclusive is true). The fixes returned by the iterator are the raw fixes (see also - * {@link #getRawFixes()}, without any smoothening or dampening applied. - * - * Callers must have called {@link #lockForRead()} before calling this method. This will be checked, and an exception - * will be thrown in case the caller has failed to do so. - */ - Iterator getRawFixesDescendingIterator(TimePoint startingAt, boolean inclusive); - - /** - * @return the average duration between two fixes (outliers removed) in this track or null if there is not - * more than one fix in the track - */ - Duration getAverageIntervalBetweenFixes(); - - /** - * @return the average duration between two fixes (outliers not removed) in this track or null if there is not - * more than one raw fix in the track - */ - Duration getAverageIntervalBetweenRawFixes(); -} +package com.sap.sailing.domain.tracking; + +import java.io.Serializable; +import java.util.Iterator; +import java.util.concurrent.locks.ReadWriteLock; + +import com.sap.sailing.domain.base.Timed; +import com.sap.sailing.domain.common.Duration; +import com.sap.sailing.domain.common.TimePoint; + +/** + * A track records {@link Timed} items for an object of type ItemType. It allows clients to ask for a value + * close to a given {@link TimePoint}. The track manages a time-based set of raw fixes. An implementation may have an + * understanding of how to eliminate outliers. For example, if a track implementation knows it's tracking boats, it may + * consider fixes that the boat cannot possibly have reached due to its speed and direction change limitations as + * outliers. The set of fixes with outliers filtered out can be obtained using {@link #getFixes} whereas + * {@link #getRawFixes()} returns the unfiltered, raw fixes. If an implementation has no idea what an outlier is, + * both methods will return the same fix sequence.

+ * + * With tracks, concurrency is an important issue. Threads may want to modify a track while other threads may want to + * read from it. Several methods such as {@link #getLastFixAtOrBefore(TimePoint)} return a single fix and can manage + * concurrency internally. However, those methods returning a collection of fixes, such as {@link #getFixes()} or an + * iterator over a collection of fixes, such as {@link #getFixesIterator(TimePoint, boolean)}, need special treatment. + * Until we internalize such iterations (see bug 824, http://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=824), + * callers need to manage a read lock which is part of a {@link ReadWriteLock} managed by this track. Callers do so + * by calling {@link #lockForRead} and {@link #unlockAfterRead}. + * + * @author Axel Uhl (d043530) + */ +public interface Track extends Serializable { + /** + * Locks this track for reading by the calling thread. If the thread already holds the lock for this track, + * the hold count will be incremented. Make sure to call {@link #unlockAfterRead()} in a finally + * block to release the lock under all possible circumstances. Failure to do so will inevitably lead to + * deadlocks! + */ + void lockForRead(); + + /** + * Decrements the hold count for this track's read lock for the calling thread. If it goes to zero, the lock will be + * released and other readers or a writer can obtain the lock. Make sure to call this method in a + * finally block for each {@link #lockForRead()} invocation. + */ + void unlockAfterRead(); + + /** + * Callers must have called {@link #lockForRead()} before calling this method. This will be checked, and an exception + * will be thrown in case the caller has failed to do so. + * + * @return the smoothened fixes + */ + Iterable getFixes(); + + /** + * Callers must have called {@link #lockForRead()} before calling this method. This will be checked, and an exception + * will be thrown in case the caller has failed to do so. + * + * @return The smoothened fixes between from and to. + */ + Iterable getFixes(TimePoint from, boolean fromInclusive, TimePoint to, boolean toInclusive); + + /** + * Callers must have called {@link #lockForRead()} before calling this method. This will be checked, and an exception + * will be thrown in case the caller has failed to do so. + */ + Iterable getRawFixes(); + + FixType getLastFixAtOrBefore(TimePoint timePoint); + + FixType getLastFixBefore(TimePoint timePoint); + + FixType getLastRawFixAtOrBefore(TimePoint timePoint); + + FixType getFirstFixAtOrAfter(TimePoint timePoint); + + FixType getFirstRawFixAtOrAfter(TimePoint timePoint); + + FixType getLastRawFixBefore(TimePoint timePoint); + + FixType getFirstRawFixAfter(TimePoint timePoint); + + FixType getFirstFixAfter(TimePoint timePoint); + + /** + * The first fix in this track or null if the track is empty. The fix returned may + * be an outlier that is not returned by calls operating on the smoothened version of the track. + */ + FixType getFirstRawFix(); + + /** + * The last fix in this track or null if the track is empty. The fix returned may + * be an outlier that is not returned by calls operating on the smoothened version of the track. + */ + FixType getLastRawFix(); + + /** + * Returns an iterator starting at the first fix after startingAt (or "at or after" in case + * inclusive is true). The fixes returned by the iterator are the smoothened fixes (see + * also {@link #getFixes()}, without any smoothening or dampening applied. + * + * Callers must have called {@link #lockForRead()} before calling this method. This will be checked, and an exception + * will be thrown in case the caller has failed to do so. + */ + Iterator getFixesIterator(TimePoint startingAt, boolean inclusive); + + /** + * Returns an iterator starting at the first raw fix after startingAt (or "at or after" in case + * inclusive is true). The fixes returned by the iterator are the raw fixes (see also + * {@link #getRawFixes()}, without any smoothening or dampening applied. + * + * Callers must have called {@link #lockForRead()} before calling this method. This will be checked, and an exception + * will be thrown in case the caller has failed to do so. + */ + Iterator getRawFixesIterator(TimePoint startingAt, boolean inclusive); + + /** + * Returns a descending iterator starting at the first fix before startingAt (or "at or before" in case + * inclusive is true). The fixes returned by the iterator are the smoothened fixes (see + * also {@link #getFixes()}, without any smoothening or dampening applied. + * + * Callers must have called {@link #lockForRead()} before calling this method. This will be checked, and an exception + * will be thrown in case the caller has failed to do so. + */ + Iterator getFixesDescendingIterator(TimePoint startingAt, boolean inclusive); + + /** + * Returns a descending iterator starting at the first raw fix before startingAt (or "at or before" in case + * inclusive is true). The fixes returned by the iterator are the raw fixes (see also + * {@link #getRawFixes()}, without any smoothening or dampening applied. + * + * Callers must have called {@link #lockForRead()} before calling this method. This will be checked, and an exception + * will be thrown in case the caller has failed to do so. + */ + Iterator getRawFixesDescendingIterator(TimePoint startingAt, boolean inclusive); + + /** + * @return the average duration between two fixes (outliers removed) in this track or null if there is not + * more than one fix in the track + */ + Duration getAverageIntervalBetweenFixes(); + + /** + * @return the average duration between two fixes (outliers not removed) in this track or null if there is not + * more than one raw fix in the track + */ + Duration getAverageIntervalBetweenRawFixes(); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/impl/TrackImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/impl/TrackImpl.java index f8d21861adf..152e4071625 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/impl/TrackImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/impl/TrackImpl.java @@ -1,341 +1,348 @@ -package com.sap.sailing.domain.tracking.impl; - -import java.io.IOException; -import java.io.ObjectOutputStream; -import java.util.ConcurrentModificationException; -import java.util.Iterator; -import java.util.NavigableSet; - -import com.sap.sailing.domain.base.Timed; -import com.sap.sailing.domain.common.Duration; -import com.sap.sailing.domain.common.TimePoint; -import com.sap.sailing.domain.tracking.Track; -import com.sap.sailing.util.impl.ArrayListNavigableSet; -import com.sap.sailing.util.impl.LockUtil; -import com.sap.sailing.util.impl.NamedReentrantReadWriteLock; -import com.sap.sailing.util.impl.UnmodifiableNavigableSet; - -public class TrackImpl implements Track { - private static final long serialVersionUID = -4075853657857657528L; - /** - * The fixes, ordered by their time points - */ - private final ArrayListNavigableSet fixes; - - private final NamedReentrantReadWriteLock readWriteLock; - - protected static class DummyTimed implements Timed { - private static final long serialVersionUID = 6047311973718918856L; - private final TimePoint timePoint; - public DummyTimed(TimePoint timePoint) { - super(); - this.timePoint = timePoint; - } - @Override - public TimePoint getTimePoint() { - return timePoint; - } - @Override - public String toString() { - return timePoint.toString(); - } - } - - public TrackImpl(String nameForReadWriteLock) { - this(new ArrayListNavigableSet(TimedComparator.INSTANCE), nameForReadWriteLock); - } - - protected TrackImpl(ArrayListNavigableSet fixes, String nameForReadWriteLock) { - this.readWriteLock = new NamedReentrantReadWriteLock(nameForReadWriteLock, /* fair */ false); - this.fixes = fixes; - } - - /** - * Synchronize the serialization such that no fixes are added while serializing - */ - private void writeObject(ObjectOutputStream s) throws IOException { - lockForRead(); - try { - s.defaultWriteObject(); - } finally { - unlockAfterRead(); - } - } - - @Override - public void lockForRead() { - LockUtil.lockForRead(readWriteLock); - } - - @Override - public void unlockAfterRead() { - LockUtil.unlockAfterRead(readWriteLock); - } - - protected void lockForWrite() { - LockUtil.lockForWrite(readWriteLock); - } - - protected void unlockAfterWrite() { - LockUtil.unlockAfterWrite(readWriteLock); - } - - /** - * Callers that want to iterate over the collection returned need to use {@link #lockForRead()} and {@link #unlockAfterRead()} - * to avoid {@link ConcurrentModificationException}s. Should they modify the structure returned, they have to use - * {@link #lockForWrite()} and {@link #unlockAfterWrite()}, respectively. - */ - protected NavigableSet getInternalRawFixes() { - @SuppressWarnings("unchecked") - NavigableSet result = (NavigableSet) fixes; - return result; - } - - /** - * asserts that the calling thread holds at least one of read and write lock - */ - protected void assertReadLock() { - if (readWriteLock.getReadHoldCount() < 1 && readWriteLock.getWriteHoldCount() < 1) { - throw new IllegalStateException("Caller must obtain read lock using lockForRead() before calling this method"); - } - } - - protected void assertWriteLock() { - if (readWriteLock.getWriteHoldCount() < 1) { - throw new IllegalStateException("Caller must obtain write lock using lockForWrite() before calling this method"); - } - } - - /** - * Callers that want to iterate over the collection returned need to synchronize on this object to - * avoid {@link ConcurrentModificationException}s. - * - * @return the smoothened fixes; this implementation simply delegates to {@link #getInternalRawFixes()} because for - * only {@link Timed} fixes we can't know how to remove outliers. Subclasses that constrain the - * FixType may provide smoothening implementations. - */ - protected NavigableSet getInternalFixes() { - NavigableSet result = getInternalRawFixes(); - return result; - } - - /** - * Iterates the fixes with outliers getting skipped, in the order of their time points. - * Relies on {@link #getInternalFixes()} to void the track view from outliers. - */ - @Override - public NavigableSet getFixes() { - assertReadLock(); - return new UnmodifiableNavigableSet(getInternalFixes()); - } - - @Override - public Iterable getFixes(TimePoint from, boolean fromInclusive, TimePoint to, boolean toInclusive) { - return getFixes().subSet(getDummyFix(from), fromInclusive, getDummyFix(to), toInclusive); - } - - /** - * Iterates over the raw sequence of fixes, all potential outliers included - */ - @Override - public NavigableSet getRawFixes() { - assertReadLock(); - return new UnmodifiableNavigableSet(getInternalRawFixes()); - } - - @Override - public FixType getLastFixAtOrBefore(TimePoint timePoint) { - lockForRead(); - try { - return (FixType) getInternalFixes().floor(getDummyFix(timePoint)); - } finally { - unlockAfterRead(); - } - } - - @Override - public FixType getLastFixBefore(TimePoint timePoint) { - lockForRead(); - try { - return (FixType) getInternalFixes().lower(getDummyFix(timePoint)); - } finally { - unlockAfterRead(); - } - } - - @Override - public FixType getLastRawFixAtOrBefore(TimePoint timePoint) { - lockForRead(); - try { - return (FixType) getInternalRawFixes().floor(getDummyFix(timePoint)); - } finally { - unlockAfterRead(); - } - } - - @Override - public FixType getFirstRawFixAtOrAfter(TimePoint timePoint) { - lockForRead(); - try { - return (FixType) getInternalRawFixes().ceiling(getDummyFix(timePoint)); - } finally { - unlockAfterRead(); - } - } - - @Override - public FixType getFirstFixAtOrAfter(TimePoint timePoint) { - lockForRead(); - try { - return (FixType) getInternalFixes().ceiling(getDummyFix(timePoint)); - } finally { - unlockAfterRead(); - } - } - - @Override - public FixType getLastRawFixBefore(TimePoint timePoint) { - lockForRead(); - try { - return (FixType) getInternalRawFixes().lower(getDummyFix(timePoint)); - } finally { - unlockAfterRead(); - } - } - - @Override - public FixType getFirstFixAfter(TimePoint timePoint) { - lockForRead(); - try { - return (FixType) getInternalFixes().higher(getDummyFix(timePoint)); - } finally { - unlockAfterRead(); - } - } - - @Override - public FixType getFirstRawFixAfter(TimePoint timePoint) { - lockForRead(); - try { - return (FixType) getInternalRawFixes().higher(getDummyFix(timePoint)); - } finally { - unlockAfterRead(); - } - } - - @Override - public FixType getFirstRawFix() { - lockForRead(); - try { - if (getInternalFixes().isEmpty()) { - return null; - } else { - return (FixType) getInternalFixes().first(); - } - } finally { - unlockAfterRead(); - } - } - - @Override - public FixType getLastRawFix() { - lockForRead(); - try { - if (getInternalRawFixes().isEmpty()) { - return null; - } else { - return (FixType) getInternalRawFixes().last(); - } - } finally { - unlockAfterRead(); - } - } - - @Override - public Iterator getFixesIterator(TimePoint startingAt, boolean inclusive) { - assertReadLock(); - Iterator result = (Iterator) getInternalFixes().tailSet( - getDummyFix(startingAt), inclusive).iterator(); - return result; - } - - @Override - public Iterator getFixesDescendingIterator(TimePoint startingAt, boolean inclusive) { - assertReadLock(); - Iterator result = (Iterator) getInternalFixes().headSet( - getDummyFix(startingAt), inclusive).descendingIterator(); - return result; - } - - /** - * Creates a dummy fix that conforms to FixType. This in particular means that subclasses - * instantiating FixType with a specific class need to redefine this method so as to return - * a dummy fix complying with their instantiation type used for FixType. Otherwise, a - * {@link ClassCastException} may result upon certain operations performed with the fix returned by - * this method. - */ - protected FixType getDummyFix(TimePoint timePoint) { - @SuppressWarnings("unchecked") - FixType result = (FixType) new DummyTimed(timePoint); - return result; - } - - @Override - public Iterator getRawFixesIterator(TimePoint startingAt, boolean inclusive) { - assertReadLock(); - Iterator result = (Iterator) getInternalRawFixes().tailSet( - getDummyFix(startingAt), inclusive).iterator(); - return result; - } - - @Override - public Iterator getRawFixesDescendingIterator(TimePoint startingAt, boolean inclusive) { - assertReadLock(); - Iterator result = (Iterator) getInternalRawFixes().headSet( - getDummyFix(startingAt), inclusive).descendingIterator(); - return result; - } - - protected boolean add(FixType fix) { - lockForWrite(); - try { - return getInternalRawFixes().add(fix); - } finally { - unlockAfterWrite(); - } - } - - @Override - public Duration getAverageIntervalBetweenFixes() { - lockForRead(); - try { - final Duration result; - final int size = getFixes().size(); - if (size > 1) { - result = getFixes().first().getTimePoint().until(getFixes().last().getTimePoint()).divide(size-1); - } else { - result = null; - } - return result; - } finally { - unlockAfterRead(); - } - } - - @Override - public Duration getAverageIntervalBetweenRawFixes() { - lockForRead(); - try { - final Duration result; - final int size = getRawFixes().size(); - if (size > 1) { - result = getRawFixes().first().getTimePoint().until(getRawFixes().last().getTimePoint()).divide(size-1); - } else { - result = null; - } - return result; - } finally { - unlockAfterRead(); - } - } -} +package com.sap.sailing.domain.tracking.impl; + +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.NavigableSet; + +import com.sap.sailing.domain.base.Timed; +import com.sap.sailing.domain.common.Duration; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.tracking.Track; +import com.sap.sailing.util.impl.ArrayListNavigableSet; +import com.sap.sailing.util.impl.LockUtil; +import com.sap.sailing.util.impl.NamedReentrantReadWriteLock; +import com.sap.sailing.util.impl.UnmodifiableNavigableSet; + +public class TrackImpl implements Track { + private static final long serialVersionUID = -4075853657857657528L; + /** + * The fixes, ordered by their time points + */ + private final ArrayListNavigableSet fixes; + + private final NamedReentrantReadWriteLock readWriteLock; + + protected static class DummyTimed implements Timed { + private static final long serialVersionUID = 6047311973718918856L; + private final TimePoint timePoint; + public DummyTimed(TimePoint timePoint) { + super(); + this.timePoint = timePoint; + } + @Override + public TimePoint getTimePoint() { + return timePoint; + } + @Override + public String toString() { + return timePoint.toString(); + } + } + + public TrackImpl(String nameForReadWriteLock) { + this(new ArrayListNavigableSet(TimedComparator.INSTANCE), nameForReadWriteLock); + } + + protected TrackImpl(ArrayListNavigableSet fixes, String nameForReadWriteLock) { + this.readWriteLock = new NamedReentrantReadWriteLock(nameForReadWriteLock, /* fair */ false); + this.fixes = fixes; + } + + /** + * Synchronize the serialization such that no fixes are added while serializing + */ + private void writeObject(ObjectOutputStream s) throws IOException { + lockForRead(); + try { + s.defaultWriteObject(); + } finally { + unlockAfterRead(); + } + } + + @Override + public void lockForRead() { + LockUtil.lockForRead(readWriteLock); + } + + @Override + public void unlockAfterRead() { + LockUtil.unlockAfterRead(readWriteLock); + } + + protected void lockForWrite() { + LockUtil.lockForWrite(readWriteLock); + } + + protected void unlockAfterWrite() { + LockUtil.unlockAfterWrite(readWriteLock); + } + + /** + * Callers that want to iterate over the collection returned need to use {@link #lockForRead()} and {@link #unlockAfterRead()} + * to avoid {@link ConcurrentModificationException}s. Should they modify the structure returned, they have to use + * {@link #lockForWrite()} and {@link #unlockAfterWrite()}, respectively. + */ + protected NavigableSet getInternalRawFixes() { + @SuppressWarnings("unchecked") + NavigableSet result = (NavigableSet) fixes; + return result; + } + + /** + * asserts that the calling thread holds at least one of read and write lock + */ + protected void assertReadLock() { + if (readWriteLock.getReadHoldCount() < 1 && readWriteLock.getWriteHoldCount() < 1) { + throw new IllegalStateException("Caller must obtain read lock using lockForRead() before calling this method"); + } + } + + protected void assertWriteLock() { + if (readWriteLock.getWriteHoldCount() < 1) { + throw new IllegalStateException("Caller must obtain write lock using lockForWrite() before calling this method"); + } + } + + /** + * Callers that want to iterate over the collection returned need to synchronize on this object to + * avoid {@link ConcurrentModificationException}s. + * + * @return the smoothened fixes; this implementation simply delegates to {@link #getInternalRawFixes()} because for + * only {@link Timed} fixes we can't know how to remove outliers. Subclasses that constrain the + * FixType may provide smoothening implementations. + */ + protected NavigableSet getInternalFixes() { + NavigableSet result = getInternalRawFixes(); + return result; + } + + /** + * Iterates the fixes with outliers getting skipped, in the order of their time points. + * Relies on {@link #getInternalFixes()} to void the track view from outliers. + */ + @Override + public NavigableSet getFixes() { + assertReadLock(); + return new UnmodifiableNavigableSet(getInternalFixes()); + } + + @Override + public Iterable getFixes(TimePoint from, boolean fromInclusive, TimePoint to, boolean toInclusive) { + return getFixes().subSet(getDummyFix(from), fromInclusive, getDummyFix(to), toInclusive); + } + + /** + * Iterates over the raw sequence of fixes, all potential outliers included + */ + @Override + public NavigableSet getRawFixes() { + assertReadLock(); + return new UnmodifiableNavigableSet(getInternalRawFixes()); + } + + @Override + public FixType getLastFixAtOrBefore(TimePoint timePoint) { + lockForRead(); + try { + return (FixType) getInternalFixes().floor(getDummyFix(timePoint)); + } finally { + unlockAfterRead(); + } + } + + @Override + public FixType getLastFixBefore(TimePoint timePoint) { + lockForRead(); + try { + return (FixType) getInternalFixes().lower(getDummyFix(timePoint)); + } finally { + unlockAfterRead(); + } + } + + @Override + public FixType getLastRawFixAtOrBefore(TimePoint timePoint) { + lockForRead(); + try { + return (FixType) getInternalRawFixes().floor(getDummyFix(timePoint)); + } finally { + unlockAfterRead(); + } + } + + @Override + public FixType getFirstRawFixAtOrAfter(TimePoint timePoint) { + lockForRead(); + try { + return (FixType) getInternalRawFixes().ceiling(getDummyFix(timePoint)); + } finally { + unlockAfterRead(); + } + } + + @Override + public FixType getFirstFixAtOrAfter(TimePoint timePoint) { + lockForRead(); + try { + return (FixType) getInternalFixes().ceiling(getDummyFix(timePoint)); + } finally { + unlockAfterRead(); + } + } + + @Override + public FixType getLastRawFixBefore(TimePoint timePoint) { + lockForRead(); + try { + return (FixType) getInternalRawFixes().lower(getDummyFix(timePoint)); + } finally { + unlockAfterRead(); + } + } + + @Override + public FixType getFirstFixAfter(TimePoint timePoint) { + lockForRead(); + try { + return (FixType) getInternalFixes().higher(getDummyFix(timePoint)); + } finally { + unlockAfterRead(); + } + } + + @Override + public FixType getFirstRawFixAfter(TimePoint timePoint) { + lockForRead(); + try { + return (FixType) getInternalRawFixes().higher(getDummyFix(timePoint)); + } finally { + unlockAfterRead(); + } + } + + @Override + public FixType getFirstRawFix() { + lockForRead(); + try { + if (getInternalFixes().isEmpty()) { + return null; + } else { + return (FixType) getInternalFixes().first(); + } + } finally { + unlockAfterRead(); + } + } + + @Override + public FixType getLastRawFix() { + lockForRead(); + try { + if (getInternalRawFixes().isEmpty()) { + return null; + } else { + return (FixType) getInternalRawFixes().last(); + } + } finally { + unlockAfterRead(); + } + } + + @Override + public Iterator getFixesIterator(TimePoint startingAt, boolean inclusive) { + assertReadLock(); + Iterator result = (Iterator) getInternalFixes().tailSet( + getDummyFix(startingAt), inclusive).iterator(); + return result; + } + + @Override + public Iterator getFixesDescendingIterator(TimePoint startingAt, boolean inclusive) { + assertReadLock(); + Iterator result = (Iterator) getInternalFixes().headSet( + getDummyFix(startingAt), inclusive).descendingIterator(); + return result; + } + + /** + * Creates a dummy fix that conforms to FixType. This in particular means that subclasses + * instantiating FixType with a specific class need to redefine this method so as to return + * a dummy fix complying with their instantiation type used for FixType. Otherwise, a + * {@link ClassCastException} may result upon certain operations performed with the fix returned by + * this method. + */ + protected FixType getDummyFix(TimePoint timePoint) { + @SuppressWarnings("unchecked") + FixType result = (FixType) new DummyTimed(timePoint); + return result; + } + + @Override + public Iterator getRawFixesIterator(TimePoint startingAt, boolean inclusive) { + assertReadLock(); + Iterator result = (Iterator) getInternalRawFixes().tailSet( + getDummyFix(startingAt), inclusive).iterator(); + return result; + } + + @Override + public Iterator getRawFixesDescendingIterator(TimePoint startingAt, boolean inclusive) { + assertReadLock(); + Iterator result = (Iterator) getInternalRawFixes().headSet( + getDummyFix(startingAt), inclusive).descendingIterator(); + return result; + } + + protected boolean add(FixType fix) { + lockForWrite(); + try { + return addWithoutLocking(fix); + } finally { + unlockAfterWrite(); + } + } + + /** + * The caller must ensure to hold the write lock for this track when calling this methos + */ + protected boolean addWithoutLocking(FixType fix) { + return getInternalRawFixes().add(fix); + } + + @Override + public Duration getAverageIntervalBetweenFixes() { + lockForRead(); + try { + final Duration result; + final int size = getFixes().size(); + if (size > 1) { + result = getFixes().first().getTimePoint().until(getFixes().last().getTimePoint()).divide(size-1); + } else { + result = null; + } + return result; + } finally { + unlockAfterRead(); + } + } + + @Override + public Duration getAverageIntervalBetweenRawFixes() { + lockForRead(); + try { + final Duration result; + final int size = getRawFixes().size(); + if (size > 1) { + result = getRawFixes().first().getTimePoint().until(getRawFixes().last().getTimePoint()).divide(size-1); + } else { + result = null; + } + return result; + } finally { + unlockAfterRead(); + } + } +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/util/impl/ConcurrentWeakHashMap.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/util/impl/ConcurrentWeakHashMap.java new file mode 100755 index 00000000000..c2de093226f --- /dev/null +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/util/impl/ConcurrentWeakHashMap.java @@ -0,0 +1,243 @@ +package com.sap.sailing.util.impl; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.sap.sailing.domain.common.impl.Util; + +/** + * A map that is concurrency-safe as provided by the {@link ConcurrentHashMap} implementation, and referencing + * keys only weakly. Like {@link ConcurrentHashMap}, this map does not support null keys. + * + * @author Axel Uhl (D043530) + */ +public class ConcurrentWeakHashMap implements Map { + private final ConcurrentHashMap map; + + private final ReferenceQueue queue; + + /** + * A reference whose hash code is based on the hash code of the original referent, also after the + * reference has been enqueued. Equality is defined such that this reference is equal to itself which is + * helpful to remove the entries from the {@link ConcurrentHashMap}, as well as equal to any other reference + * of this type if their referents are equal. + * + * @author Axel Uhl (D043530) + * + */ + private class WeakReferenceThatIsEqualToItselfAndItsReferent extends WeakReference { + private final int hashCode; + + public WeakReferenceThatIsEqualToItselfAndItsReferent(K referent, ReferenceQueue q) { + super(referent, q); + hashCode = referent.hashCode(); + } + + @Override + public int hashCode() { + return hashCode; + } + + /** + * If other is this reference, it it considered equal. Otherwise, other is considered + * equal to this reference if the referent is still set and the referent equals other. + */ + @Override + public boolean equals(Object other) { + final boolean result; + if (other == this) { + result = true; + } else { + @SuppressWarnings("unchecked") // don't care about the generics here; important is that we can get at the referent + WeakReferenceThatIsEqualToItselfAndItsReferent otherAsRef = (WeakReferenceThatIsEqualToItselfAndItsReferent) other; + K referent = get(); + K otherReferent = otherAsRef.get(); + result = Util.equalsWithNull(referent, otherReferent); + } + return result; + } + } + + private class KeySet extends AbstractSet { + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return map.containsKey(o); + } + + @Override + public boolean remove(Object o) { + return map.remove(o) != null; + } + + @Override + public Iterator iterator() { + purgeStaleEntries(); + final Iterator> iterator = map.entrySet().iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public K next() { + return iterator.next().getKey().get(); + } + + @Override + public void remove() { + iterator.remove(); + } + }; + } + } + + private class EntrySet extends AbstractSet> { + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public Iterator> iterator() { + purgeStaleEntries(); + final Iterator> iterator = map.entrySet().iterator(); + return new Iterator>() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Map.Entry next() { + final Map.Entry e = iterator.next(); + return new Map.Entry() { + @Override + public K getKey() { + return e.getKey().get(); + } + + @Override + public V getValue() { + return e.getValue(); + } + + @Override + public V setValue(V value) { + return e.setValue(value); + } + }; + } + + @Override + public void remove() { + iterator.remove(); + } + }; + } + } + + public ConcurrentWeakHashMap() { + map = new ConcurrentHashMap(); + queue = new ReferenceQueue(); + } + + private void purgeStaleEntries() { + Reference ref; + while ((ref=queue.poll()) != null) { + map.remove(ref); + } + } + + @Override + public int size() { + purgeStaleEntries(); + return map.size(); + } + + @Override + public boolean isEmpty() { + purgeStaleEntries(); + return map.isEmpty(); + } + + @SuppressWarnings("unchecked") // if the cast doesn't work then neither will the equals call, so false will result which is ok + @Override + public boolean containsKey(Object key) { + purgeStaleEntries(); + return map.containsKey(new WeakReferenceThatIsEqualToItselfAndItsReferent((K) key, /* queue */ null)); + } + + @Override + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + @SuppressWarnings("unchecked") // if the cast doesn't work then neither will the equals call, so null will result which is ok + @Override + public V get(Object key) { + purgeStaleEntries(); + return map.get(new WeakReferenceThatIsEqualToItselfAndItsReferent((K) key, /* queue */ null)); + } + + @Override + public V put(K key, V value) { + return map.put(new WeakReferenceThatIsEqualToItselfAndItsReferent(key, queue), value); + } + + @SuppressWarnings("unchecked") // if the cast doesn't work then neither will the equals call, so null will result which is ok + @Override + public V remove(Object key) { + return map.remove(new WeakReferenceThatIsEqualToItselfAndItsReferent((K) key, /* queue */ null)); + } + + @Override + public void putAll(Map m) { + for (Map.Entry e : m.entrySet()) { + put(e.getKey(), e.getValue()); + } + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public Set keySet() { + return new KeySet(); + } + + @Override + public Collection values() { + return map.values(); + } + + @Override + public Set> entrySet() { + return new EntrySet(); + } + +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/util/impl/LockUtil.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/util/impl/LockUtil.java index f94ca339ae6..48b0801a73c 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/util/impl/LockUtil.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/util/impl/LockUtil.java @@ -1,9 +1,10 @@ package com.sap.sailing.util.impl; +import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; -import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; @@ -51,7 +52,7 @@ public class LockUtil { private static final int NUMBER_OF_SECONDS_TO_WAIT_FOR_LOCK = 5; private static final Logger logger = Logger.getLogger(Util.class.getName()); - private static final Map lastTimeWriteLockWasObtained = new WeakHashMap(); + private static final Map lastTimeWriteLockWasObtained = new ConcurrentWeakHashMap(); /** * Tells how many other threads propagated which held lock to the key thread. During propagation, a lock is @@ -63,20 +64,20 @@ public class LockUtil { * The thread-specific value maps are used as monitor objects whenever decisions about thread-specific lock counts * need to be made. */ - private static final Map> propagationCounts = new ConcurrentHashMap>(); + private static final Map> propagationCounts = new ConcurrentWeakHashMap>(); /** * Counts the "virtual" locks. A "virtual" lock is obtained if and only if at the time of calling * {@link #lockForRead(NamedReentrantReadWriteLock)} or {@link #lockForWrite(NamedReentrantReadWriteLock)} the * respective lock has a positive {@link #propagationCounts propagation count} for the current thread. */ - private static final Map> virtualLockCounts = new ConcurrentHashMap>(); + private static final Map> virtualLockCounts = new ConcurrentWeakHashMap>(); /** * Redundant but easily accessible hold count per thread and lock. These are the actual lock hold counts as they are * recorded in the actual {@link NamedReentrantReadWriteLock} locks. */ - private static final Map> lockCounts = new ConcurrentHashMap>(); + private static final Map> lockCounts = new ConcurrentWeakHashMap>(); public static void lockForRead(NamedReentrantReadWriteLock lock) { acquireLockVirtuallyOrActually(lock, lock.readLock(), ReadOrWrite.READ); @@ -84,9 +85,7 @@ public class LockUtil { public static void lockForWrite(NamedReentrantReadWriteLock lock) { acquireLockVirtuallyOrActually(lock, lock.writeLock(), ReadOrWrite.WRITE); - synchronized (lastTimeWriteLockWasObtained) { - lastTimeWriteLockWasObtained.put(lock, MillisecondsTimePoint.now()); - } + lastTimeWriteLockWasObtained.put(lock, MillisecondsTimePoint.now()); } private static void acquireLockVirtuallyOrActually(NamedReentrantReadWriteLock lock, final Lock readOrWriteLock, final ReadOrWrite readOrWrite) { @@ -147,9 +146,7 @@ public class LockUtil { unlockVirtuallyOrActually(lock, lock.writeLock()); } final TimePoint timePointWriteLockWasObtained; - synchronized (lastTimeWriteLockWasObtained) { - timePointWriteLockWasObtained = lastTimeWriteLockWasObtained.get(lock); - } + timePointWriteLockWasObtained = lastTimeWriteLockWasObtained.get(lock); if (timePointWriteLockWasObtained == null) { logger.info("Internal error: write lock " + lock.getName() + " to be unlocked but no time recorded for when it was last obtained.\n" @@ -355,9 +352,9 @@ public class LockUtil { } - private static String getStackTrace(Thread thread) { + private static String formatStackTrace(StackTraceElement[] stackTrace) { StringBuilder sb = new StringBuilder(); - for (StackTraceElement sf : thread.getStackTrace()) { + for (StackTraceElement sf : stackTrace) { sb.append(sf.toString()); sb.append('\n'); } @@ -365,7 +362,7 @@ public class LockUtil { } private static String getCurrentStackTrace() { - return getStackTrace(Thread.currentThread()); + return formatStackTrace(Thread.currentThread().getStackTrace()); } /** @@ -381,6 +378,14 @@ public class LockUtil { try { locked = lock.tryLock(NUMBER_OF_SECONDS_TO_WAIT_FOR_LOCK, TimeUnit.SECONDS); if (!locked) { + Thread writer = lockParent.getWriter(); + // capture the stack traces as quickly as possible to try to reflect the situation as it was when the lock couuldn't be obtained + StackTraceElement[] writerStackTrace = writer != null ? writer.getStackTrace() : null; + Map readerStackTraces = new HashMap(); + final List readers = lockParent.getReaders(); + for (Thread reader : readers) { + readerStackTraces.put(reader, reader.getStackTrace()); + } StringBuilder message = new StringBuilder(); message.append("Couldn't acquire lock "); message.append(lockDescriptionForTimeoutLogMessage); @@ -388,14 +393,15 @@ public class LockUtil { message.append(NUMBER_OF_SECONDS_TO_WAIT_FOR_LOCK); message.append("s in thread " + Thread.currentThread().getName() + " at "); message.append(getCurrentStackTrace()); - Thread writer = lockParent.getWriter(); if (writer != null) { message.append("\nThe current writer is:\n"); - appendThreadData(message, writer); + appendThreadData(message, writer, writerStackTrace); } - message.append("\nThe current readers are:\n"); - for (Thread reader : lockParent.getReaders()) { - appendThreadData(message, reader); + if (readers != null && !readers.isEmpty()) { + message.append("\nThe current readers are:\n"); + for (Thread reader : readers) { + appendThreadData(message, reader, readerStackTraces.get(reader)); + } } message.append("Trying again..."); logger.info(message.toString()); @@ -406,10 +412,10 @@ public class LockUtil { return locked; } - private static void appendThreadData(StringBuilder message, Thread writer) { + private static void appendThreadData(StringBuilder message, Thread writer, StackTraceElement[] stackTrace) { message.append(writer); message.append('\n'); - message.append(getStackTrace(writer)); + message.append(formatStackTrace(stackTrace)); message.append('\n'); } diff --git a/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/classes/services/MessageFileServiceImpl.java b/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/classes/services/MessageFileServiceImpl.java index 748de273a20..0a05cb6ed7b 100644 --- a/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/classes/services/MessageFileServiceImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/classes/services/MessageFileServiceImpl.java @@ -36,7 +36,7 @@ import com.sap.sailing.domain.swisstimingadapter.classes.messages.STLMessage; import com.sap.sailing.domain.swisstimingadapter.classes.messages.TMDMessage; import com.sap.sailing.domain.swisstimingadapter.classes.messages.TimingDataElement; import com.sap.sailing.domain.swisstimingadapter.classes.services.Exceptions.MessageScriptParsingException; -import com.sap.sailing.domain.swisstimingadapter.impl.CompetitorImpl; +import com.sap.sailing.domain.swisstimingadapter.impl.CompetitorWithoutID; import com.sap.sailing.domain.swisstimingadapter.impl.MarkImpl; import com.sap.sailing.domain.swisstimingadapter.impl.RaceImpl; import com.sap.sailing.domain.swisstimingadapter.impl.SailMasterMessageImpl; @@ -218,7 +218,7 @@ public class MessageFileServiceImpl implements MessageFileService { int count = Integer.valueOf(message.getSections()[2]); for (int i = 0; i < count; i++) { String[] competitorDetails = message.getSections()[3 + i].split(";"); - competitors.add(new CompetitorImpl(competitorDetails[0], competitorDetails[1], competitorDetails[2])); + competitors.add(new CompetitorWithoutID(competitorDetails[0], competitorDetails[1], competitorDetails[2])); } return new STLMessage(raceId, competitors); } diff --git a/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/SailMasterConnectivityTest.java b/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/SailMasterConnectivityTest.java index d33087be848..c664a3d10ae 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/SailMasterConnectivityTest.java +++ b/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/SailMasterConnectivityTest.java @@ -61,12 +61,12 @@ public class SailMasterConnectivityTest { @Before public void setUp() throws InterruptedException, ParseException { startSailMasterDummy(); - Race race4702 = new RaceImpl("W4702", "470 Women Race 2"); - Race race4711 = new RaceImpl("4711", "A wonderful test race"); - Race race4712 = new RaceImpl("4712", "Not such a wonderful race"); - connector4702 = SwissTimingFactory.INSTANCE.getOrCreateSailMasterConnector("localhost", port, race4702.getRaceID(), race4702.getDescription(), null /* boat class */); - connector4711 = SwissTimingFactory.INSTANCE.getOrCreateSailMasterConnector("localhost", port, race4711.getRaceID(), race4711.getDescription(), null /* boat class */); - connector4712 = SwissTimingFactory.INSTANCE.getOrCreateSailMasterConnector("localhost", port, race4712.getRaceID(), race4712.getDescription(), null /* boat class */); + Race race4702 = new RaceImpl("W4702", "R2", "470 Women Race 2"); + Race race4711 = new RaceImpl("4711", "R2", "A wonderful test race"); + Race race4712 = new RaceImpl("4712", "R2", "Not such a wonderful race"); + connector4702 = SwissTimingFactory.INSTANCE.getOrCreateSailMasterConnector("localhost", port, race4702.getRaceID(), race4702.getRaceName(), race4702.getDescription(), null /* boat class */); + connector4711 = SwissTimingFactory.INSTANCE.getOrCreateSailMasterConnector("localhost", port, race4711.getRaceID(), race4702.getRaceName(), race4711.getDescription(), null /* boat class */); + connector4712 = SwissTimingFactory.INSTANCE.getOrCreateSailMasterConnector("localhost", port, race4712.getRaceID(), race4702.getRaceName(), race4712.getDescription(), null /* boat class */); Thread.sleep(1000); // wait until all connectors are really connected to the SailMasterDummy } @@ -112,7 +112,8 @@ public class SailMasterConnectivityTest { public void receivedAvailableRaces(Iterable races) {} @Override public void storedDataProgress(String raceID, double progress) {} - + @Override + public void receivedWindData(String raceID, int zeroBasedMarkIndex, double windDirectionTrueDegrees, double windSpeedInKnots) {} @Override public void receivedRacePositionData(String raceID, RaceStatus status, TimePoint timePoint, TimePoint startTime, Long millisecondsSinceRaceStart, Integer nextMarkIndexForLeader, Distance distanceToNextMarkForLeaderInMeters, diff --git a/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/SimpleDomainFactoryTest.java b/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/SimpleDomainFactoryTest.java index b72affde3e8..cba41d60e18 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/SimpleDomainFactoryTest.java +++ b/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/SimpleDomainFactoryTest.java @@ -57,24 +57,26 @@ public class SimpleDomainFactoryTest { DomainFactory domainFactory = DomainFactory.INSTANCE; final String raceID = "SAX920103"; RaceDefinition raceDefinition = domainFactory.createRaceDefinition(domainFactory.getOrCreateDefaultRegatta( - EmptyRaceLogStore.INSTANCE, - raceID, null /* boat class */, - new RacingEventServiceImpl()), - new Race() { - @Override - public String getDescription() { - return null; - } + EmptyRaceLogStore.INSTANCE, raceID, null /* boat class */, new RacingEventServiceImpl()), new Race() { + @Override + public String getDescription() { + return null; + } - @Override - public String getRaceID() { - return raceID; - } + @Override + public String getRaceID() { + return raceID; + } - @Override - public BoatClass getBoatClass() { - return null; - } + @Override + public BoatClass getBoatClass() { + return null; + } + + @Override + public String getRaceName() { + return "The famous SAX920103 race"; + } }, new StartList() { @Override public String getRaceID() { @@ -106,7 +108,7 @@ public class SimpleDomainFactoryTest { DomainFactory domainFactory = DomainFactory.INSTANCE; Regatta regatta = new RegattaImpl(EmptyRaceLogStore.INSTANCE, "TestEvent", /* boatClass */ null, new RacingEventServiceImpl(), com.sap.sailing.domain.base.DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT), "123", null); - Race race = new RaceImpl("1234", "Race 1234"); + Race race = new RaceImpl("1234", "R1", "Race 1234"); Iterable competitors = Collections.emptyList(); StartList startList = new StartListImpl("1234", competitors); Mark mark1 = new MarkImpl("M1", 0, Arrays.asList("D1", "D2"), /* markType */ null); @@ -131,7 +133,7 @@ public class SimpleDomainFactoryTest { DomainFactory domainFactory = DomainFactory.INSTANCE; Regatta regatta = new RegattaImpl(EmptyRaceLogStore.INSTANCE, "TestEvent", /* boatClass */ null, new RacingEventServiceImpl(), com.sap.sailing.domain.base.DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT), "123", null); - Race race = new RaceImpl("1234", "Race 1234"); + Race race = new RaceImpl("1234", "R1", "Race 1234"); Iterable competitors = Collections.emptyList(); StartList startList = new StartListImpl("1234", competitors); Mark mark1 = new MarkImpl("M1", 0, Arrays.asList("D1", "D2"), /* markType */ null); diff --git a/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/SwissTimingSailMasterLiveTest.java b/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/SwissTimingSailMasterLiveTest.java index 6fd62de9b7f..6937d2ce805 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/SwissTimingSailMasterLiveTest.java +++ b/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/SwissTimingSailMasterLiveTest.java @@ -42,7 +42,7 @@ public class SwissTimingSailMasterLiveTest implements SailMasterListener { @Before public void connect() throws InterruptedException, ParseException { - connector = SwissTimingFactory.INSTANCE.getOrCreateSailMasterConnector("gps.sportresult.com", 40300, "W4702", "Women 470 Race 2", null /* boat class*/); + connector = SwissTimingFactory.INSTANCE.getOrCreateSailMasterConnector("gps.sportresult.com", 40300, "W4702", "R2", "Women 470 Race 2", null /* boat class*/); } @After @@ -198,4 +198,9 @@ public class SwissTimingSailMasterLiveTest implements SailMasterListener { @Override public void storedDataProgress(String raceID, double progress) { } + + @Override + public void receivedWindData(String raceID, int zeroBasedMarkIndex, double windDirectionTrueDegrees, + double windSpeedInKnots) { + } } diff --git a/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/ui/EditRAC.java b/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/ui/EditRAC.java index f9ac33eda8f..9b4679e08ad 100644 --- a/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/ui/EditRAC.java +++ b/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/ui/EditRAC.java @@ -149,7 +149,7 @@ public class EditRAC extends javax.swing.JDialog { private void jAddActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jAddActionPerformed // TODO add your handling code here: - races.add(new DescriptionRace(jRaceID.getText(), jRaceDesc.getText())); + races.add(new DescriptionRace(jRaceID.getText(), jRaceID.getText(), jRaceDesc.getText())); jRaceList.setListData(races.toArray(new Race[0])); }//GEN-LAST:event_jAddActionPerformed @@ -173,8 +173,8 @@ public class EditRAC extends javax.swing.JDialog { private class DescriptionRace extends RaceImpl { - public DescriptionRace(String raceId, String desc){ - super(raceId, desc); + public DescriptionRace(String raceId, String raceName, String desc){ + super(raceId, raceName, desc); } @Override diff --git a/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/ui/EditSTL.java b/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/ui/EditSTL.java index 76420dde43d..5bf8c25c32f 100644 --- a/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/ui/EditSTL.java +++ b/java/com.sap.sailing.domain.swisstimingadapter.test/src/com/sap/sailing/domain/swisstimingadapter/test/ui/EditSTL.java @@ -12,7 +12,7 @@ package com.sap.sailing.domain.swisstimingadapter.test.ui; import com.sap.sailing.domain.swisstimingadapter.Competitor; import com.sap.sailing.domain.swisstimingadapter.classes.messages.STLMessage; -import com.sap.sailing.domain.swisstimingadapter.impl.CompetitorImpl; +import com.sap.sailing.domain.swisstimingadapter.impl.CompetitorWithoutID; import java.util.List; /** @@ -107,11 +107,11 @@ public class EditSTL extends javax.swing.JDialog { }); jCompetitorList.setModel(new javax.swing.AbstractListModel() { - Competitor[] competitors = { new CompetitorImpl("Item 1", "DEU", "Item 1"), - new CompetitorImpl("Item 2", "DEU", "Item 2"), - new CompetitorImpl("Item 3", "DEU", "Item 3"), - new CompetitorImpl("Item 4", "DEU", "Item 4"), - new CompetitorImpl("Item 5", "DEU", "Item 5") }; + Competitor[] competitors = { new CompetitorWithoutID("Item 1", "DEU", "Item 1"), + new CompetitorWithoutID("Item 2", "DEU", "Item 2"), + new CompetitorWithoutID("Item 3", "DEU", "Item 3"), + new CompetitorWithoutID("Item 4", "DEU", "Item 4"), + new CompetitorWithoutID("Item 5", "DEU", "Item 5") }; public int getSize() { return competitors.length; } public Competitor getElementAt(int i) { return competitors[i]; } }); @@ -209,7 +209,7 @@ public class EditSTL extends javax.swing.JDialog { return stl; } - private class CompetitorWithDesc extends CompetitorImpl { + private class CompetitorWithDesc extends CompetitorWithoutID { public CompetitorWithDesc(String boatID, String NOC, String name){ super(boatID, NOC, name); diff --git a/java/com.sap.sailing.domain.swisstimingadapter/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.swisstimingadapter/META-INF/MANIFEST.MF index 4d2d250eeed..d676872b8d4 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.swisstimingadapter/META-INF/MANIFEST.MF @@ -9,7 +9,8 @@ Bundle-ActivationPolicy: lazy Require-Bundle: com.sap.sailing.domain, com.sap.sailing.udpconnector, com.googlecode.java-diff-utils;bundle-version="1.3.0", - com.sap.sailing.domain.common + com.sap.sailing.domain.common, + com.sap.sailing.xrr.resultimport Export-Package: com.sap.sailing.domain.swisstimingadapter, com.sap.sailing.domain.swisstimingadapter.impl;x-friends:="com.sap.sailing.server.test" Import-Package: org.osgi.framework;version="1.7.0" diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/Competitor.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/Competitor.java index b63ec027a2f..48641dd6dbc 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/Competitor.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/Competitor.java @@ -1,9 +1,15 @@ package com.sap.sailing.domain.swisstimingadapter; +import java.util.List; + public interface Competitor { String getBoatID(); String getThreeLetterIOCCode(); + List getCrew(); + String getName(); + + String getID(); } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/CrewMember.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/CrewMember.java new file mode 100644 index 00000000000..0406a38b8ff --- /dev/null +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/CrewMember.java @@ -0,0 +1,9 @@ +package com.sap.sailing.domain.swisstimingadapter; + +public interface CrewMember { + public String getName(); + + public String getNationality(); + + public String getPosition(); +} diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/DomainFactory.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/DomainFactory.java index 9906b5a0603..9ca4001ee67 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/DomainFactory.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/DomainFactory.java @@ -34,12 +34,16 @@ public interface DomainFactory { Nationality getOrCreateNationality(String threeLetterIOCCode); - Competitor getOrCreateCompetitor(com.sap.sailing.domain.swisstimingadapter.Competitor competitor, String raceId, BoatClass boatClass); - - Competitor getOrCreateCompetitor(String boatID, String threeLetterIOCCode, String name, String raceId, BoatClass boatClass); + Competitor createCompetitorWithoutID(com.sap.sailing.domain.swisstimingadapter.Competitor competitor, String raceId, BoatClass boatClass); - Competitor getCompetitorByBoatIDAndRaceIDOrBoatClass(String boatID, String raceID, BoatClass boatClass); + Competitor createCompetitorWithID(com.sap.sailing.domain.swisstimingadapter.Competitor competitor, BoatClass boatClass); + + Competitor createCompetitorWithoutID(String boatID, String threeLetterIOCCode, String name, String raceId, BoatClass boatClass); + String getCompetitorID(String boatID, RaceType raceType); + + String getCompetitorID(String boatID, BoatClass boatClass); + RaceDefinition createRaceDefinition(Regatta regatta, Race race, StartList startList, com.sap.sailing.domain.swisstimingadapter.Course course); com.sap.sailing.domain.base.Mark getOrCreateMark(String trackerID); @@ -54,8 +58,8 @@ public interface DomainFactory { RaceType getRaceTypeFromRaceID(String raceID); - RaceTrackingConnectivityParameters createTrackingConnectivityParameters(String hostname, int port, String raceID, - String raceDescription, BoatClass boatClass, long delayToLiveInMillis, + RaceTrackingConnectivityParameters createTrackingConnectivityParameters(String hostname, int port, String raceID, String raceName, + String raceDescription, BoatClass boatClass, StartList startList, long delayToLiveInMillis, SwissTimingFactory swissTimingFactory, DomainFactory domainFactory, RaceLogStore raceLogStore); ControlPoint getOrCreateControlPoint(Iterable devices); diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/Race.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/Race.java index ea2d70610ab..a505eb1092a 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/Race.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/Race.java @@ -5,5 +5,6 @@ import com.sap.sailing.domain.base.BoatClass; public interface Race extends WithDescription { String getRaceID(); + String getRaceName(); BoatClass getBoatClass(); } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SailMasterAdapter.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SailMasterAdapter.java index 746fd834564..488ce756399 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SailMasterAdapter.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SailMasterAdapter.java @@ -41,4 +41,8 @@ public abstract class SailMasterAdapter implements SailMasterListener { public void storedDataProgress(String raceID, double progress) { } + @Override + public void receivedWindData(String raceID, int markIndex, double windDirectionTrueDegrees, double windSpeedInKnots) { + } + } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SailMasterConnector.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SailMasterConnector.java index 473c3beb6ff..c4d24c38ff2 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SailMasterConnector.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SailMasterConnector.java @@ -48,7 +48,7 @@ public interface SailMasterConnector { */ void addSailMasterListener(SailMasterListener listener) throws UnknownHostException, IOException, InterruptedException; - void removeSailMasterListener(SailMasterListener listener); + void removeSailMasterListener(SailMasterListener listener) throws IOException; SailMasterMessage receiveMessage(MessageType type) throws InterruptedException; @@ -78,4 +78,11 @@ public interface SailMasterConnector { void stop() throws IOException; boolean isStopped(); + + /** + * Some messages in the SwissTiming SailMaster protocol lack proper time stamp information. It is therefore + * necessary to keep track of time stamps received from other messages and use them as an approximation for + * the time point of messages received without explicit time stamp. + */ + TimePoint getLastRPDMessageTimePoint(); } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SailMasterListener.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SailMasterListener.java index 14535538728..5bed6cb229a 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SailMasterListener.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SailMasterListener.java @@ -25,7 +25,8 @@ public interface SailMasterListener { /** * Announces the progress of data loaded from a local persistent store (as opposed to receiving it live from a * SailMaster instance). 0.0 means no progress so far, 1.0 means loading from persistent store completed. - * @param raceID TODO */ void storedDataProgress(String raceID, double progress); + + void receivedWindData(String raceID, int zeroBasedMarkIndex, double windDirectionTrueDegrees, double windSpeedInKnots); } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingAdapter.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingAdapter.java index 6b20bbc2d43..a07040a69b6 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingAdapter.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingAdapter.java @@ -5,11 +5,14 @@ import java.net.UnknownHostException; import java.text.ParseException; import java.util.List; +import javax.xml.bind.JAXBException; + import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sailing.domain.racelog.RaceLogStore; import com.sap.sailing.domain.tracking.RacesHandle; import com.sap.sailing.domain.tracking.TrackerManager; +import com.sap.sailing.xrr.resultimport.schema.RegattaResults; public interface SwissTimingAdapter { List getSwissTimingRaceRecords(String hostname, int port) throws InterruptedException, UnknownHostException, IOException, ParseException; @@ -23,10 +26,14 @@ public interface SwissTimingAdapter { * to add the races to. Otherwise, a default regatta as described above will be created and used. * @param raceDescription TODO */ - RacesHandle addSwissTimingRace(TrackerManager trackerManager, RegattaIdentifier regattaToAddTo, String raceID, String raceDescription, - BoatClass boatClass, String hostname, int port, RaceLogStore logStore, long timeoutInMilliseconds) + RacesHandle addSwissTimingRace(TrackerManager trackerManager, RegattaIdentifier regattaToAddTo, String raceID, String raceName, String raceDescription, + BoatClass boatClass, String hostname, int port, StartList startList, RaceLogStore logStore, long timeoutInMilliseconds) throws InterruptedException, UnknownHostException, IOException, ParseException, Exception; + StartList readStartListForRace(String raceId, RegattaResults regattaResults); + + RegattaResults readRegattaEntryListFromXrrUrl(String xrrEntryListUrl) throws IOException, JAXBException; + SwissTimingFactory getSwissTimingFactory(); com.sap.sailing.domain.swisstimingadapter.DomainFactory getSwissTimingDomainFactory(); diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingFactory.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingFactory.java index 2662e47b988..d05976732cb 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingFactory.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingFactory.java @@ -1,66 +1,67 @@ -package com.sap.sailing.domain.swisstimingadapter; - -import java.io.IOException; -import java.net.UnknownHostException; -import java.text.ParseException; - -import com.sap.sailing.domain.base.BoatClass; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.racelog.RaceLogStore; -import com.sap.sailing.domain.swisstimingadapter.impl.SwissTimingFactoryImpl; -import com.sap.sailing.domain.tracking.RaceTracker; -import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; -import com.sap.sailing.domain.tracking.WindStore; - -public interface SwissTimingFactory { - SwissTimingFactory INSTANCE = new SwissTimingFactoryImpl(); - - SwissTimingMessageParser createMessageParser(); - - /** - * Obtains a connector to one or more SwissTiming Sail Master system(s). Such a connector uses a host name and port - * number to establish the connecting via TCP. The connector offers a number of explicit service request methods. - * Additionally, the connector can receive "spontaneous" events sent by the sail master system. Clients can register - * for those spontaneous events (see {@link SailMasterConnector#addSailMasterListener}). - *

- * - * When the connector is used with SailMaster instances hidden behind a "bridge" / firewall, no explicit requests - * are possible, and the connector has to rely solely on the events it receives. It may, though, load recorded - * race-specific messages through a {@link RaceSpecificMessageLoader} object. If a non-null - * {@link RaceSpecificMessageLoader} is provided, the connector will fetch the {@link #getRaces() list of races} - * from that loader. Additionally, the connector will use the loader upon each - * {@link SailMasterConnector#trackRace(String)} to load all messages recorded by the loader for the race requested - * so far. - *

- * - * Generally, the connector needs to be instructed for which races it shall handle events using calls to the - * {@link SailMasterConnector#trackRace} and {@link SailMasterConnector#stopTrackingRace} operations. - * {@link MessageType#isRaceSpecific() Race-specific messages} for other races are ignored and not forwarded to any - * listener. - */ - SailMasterConnector getOrCreateSailMasterConnector(String hostname, int port, String raceId, String raceDescription, BoatClass boatClass) - throws InterruptedException, ParseException; - - SailMasterConnector getOrCreateSailMasterLiveSimulatorConnector(String host, int port, String raceId, - String raceDescription, BoatClass boatClass) throws InterruptedException, ParseException; - - SailMasterTransceiver createSailMasterTransceiver(); - - SwissTimingConfiguration createSwissTimingConfiguration(String name, String jsonURL, String hostname, int port); - - SwissTimingRaceTracker createRaceTracker(String raceID, String raceDescription, BoatClass boatClass, String hostname, int port, - long delayToLiveInMillis, RaceLogStore raceLogStore, WindStore windStore, - DomainFactory domainFactory, TrackedRegattaRegistry trackedRegattaRegistry) throws InterruptedException, UnknownHostException, - IOException, ParseException; - - RaceTracker createRaceTracker(Regatta regatta, String raceID, String raceDescription, BoatClass boatClass, String hostname, int port, long delayToLiveInMillis, - WindStore windStore, DomainFactory domainFactory, TrackedRegattaRegistry trackedRegattaRegistry) throws UnknownHostException, InterruptedException, - IOException, ParseException; - - Race createRace(String raceId, String description, BoatClass boatClass); - - SailMasterMessage createMessage(String message); - - SwissTimingArchiveConfiguration createSwissTimingArchiveConfiguration(String string); - -} +package com.sap.sailing.domain.swisstimingadapter; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.text.ParseException; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; +import com.sap.sailing.domain.swisstimingadapter.impl.SwissTimingFactoryImpl; +import com.sap.sailing.domain.tracking.RaceTracker; +import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; +import com.sap.sailing.domain.tracking.WindStore; + +public interface SwissTimingFactory { + SwissTimingFactory INSTANCE = new SwissTimingFactoryImpl(); + + SwissTimingMessageParser createMessageParser(); + + /** + * Obtains a connector to one or more SwissTiming Sail Master system(s). Such a connector uses a host name and port + * number to establish the connecting via TCP. The connector offers a number of explicit service request methods. + * Additionally, the connector can receive "spontaneous" events sent by the sail master system. Clients can register + * for those spontaneous events (see {@link SailMasterConnector#addSailMasterListener}). + *

+ * + * When the connector is used with SailMaster instances hidden behind a "bridge" / firewall, no explicit requests + * are possible, and the connector has to rely solely on the events it receives. It may, though, load recorded + * race-specific messages through a {@link RaceSpecificMessageLoader} object. If a non-null + * {@link RaceSpecificMessageLoader} is provided, the connector will fetch the {@link #getRaces() list of races} + * from that loader. Additionally, the connector will use the loader upon each + * {@link SailMasterConnector#trackRace(String)} to load all messages recorded by the loader for the race requested + * so far. + *

+ * + * Generally, the connector needs to be instructed for which races it shall handle events using calls to the + * {@link SailMasterConnector#trackRace} and {@link SailMasterConnector#stopTrackingRace} operations. + * {@link MessageType#isRaceSpecific() Race-specific messages} for other races are ignored and not forwarded to any + * listener. + */ + SailMasterConnector getOrCreateSailMasterConnector(String hostname, int port, String raceId, String raceName, String raceDescription, BoatClass boatClass) + throws InterruptedException, ParseException; + + SailMasterConnector getOrCreateSailMasterLiveSimulatorConnector(String host, int port, String raceId, String raceName, + String raceDescription, BoatClass boatClass) throws InterruptedException, ParseException; + + SailMasterTransceiver createSailMasterTransceiver(); + + SwissTimingConfiguration createSwissTimingConfiguration(String name, String jsonURL, String hostname, int port); + + SwissTimingRaceTracker createRaceTracker(String raceID, String raceName, String raceDescription, BoatClass boatClass, String hostname, int port, + StartList startList, long delayToLiveInMillis, RaceLogStore raceLogStore, WindStore windStore, GPSFixStore gpsFixStore, + DomainFactory domainFactory, TrackedRegattaRegistry trackedRegattaRegistry) throws InterruptedException, UnknownHostException, + IOException, ParseException; + + RaceTracker createRaceTracker(Regatta regatta, String raceID, String raceName, String raceDescription, BoatClass boatClass, String hostname, int port, + StartList startList, long delayToLiveInMillis, WindStore windStore, GPSFixStore gpsFixStore, DomainFactory domainFactory, TrackedRegattaRegistry trackedRegattaRegistry) throws UnknownHostException, InterruptedException, + IOException, ParseException; + + Race createRace(String raceId, String raceName, String description, BoatClass boatClass); + + SailMasterMessage createMessage(String message); + + SwissTimingArchiveConfiguration createSwissTimingArchiveConfiguration(String string); + +} diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingRaceTracker.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingRaceTracker.java index e1d5dce08d4..bd749cd4e2c 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingRaceTracker.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingRaceTracker.java @@ -1,5 +1,7 @@ package com.sap.sailing.domain.swisstimingadapter; +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.common.impl.Util.Triple; import com.sap.sailing.domain.tracking.RaceTracker; @@ -8,4 +10,6 @@ public interface SwissTimingRaceTracker extends RaceTracker { * The key for a SwissTiming race tracker is the triple of raceID, host name and port number. */ Triple getID(); + + Competitor getCompetitorByBoatIDAndRaceIDOrBoatClass(String boatID, String raceID, BoatClass boatClass); } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CompetitorWithID.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CompetitorWithID.java new file mode 100644 index 00000000000..1b0d9499523 --- /dev/null +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CompetitorWithID.java @@ -0,0 +1,55 @@ +package com.sap.sailing.domain.swisstimingadapter.impl; + +import java.util.List; + +import com.sap.sailing.domain.swisstimingadapter.Competitor; +import com.sap.sailing.domain.swisstimingadapter.CrewMember; + +public class CompetitorWithID implements Competitor { + private final String boatID; + private final String threeLetterIOCCode; + private final String name; + private final String id; + + private List crew; + + public CompetitorWithID(String id, String boatID, String threeLetterIOCCode, String name, List crew) { + super(); + this.boatID = boatID; + this.threeLetterIOCCode = threeLetterIOCCode; + this.name = name; + this.id = id; + this.crew = crew; + } + + @Override + public String getBoatID() { + return boatID; + } + + @Override + public String getThreeLetterIOCCode() { + return threeLetterIOCCode; + } + + @Override + public String getName() { + return name; + } + + public String getID() { + return id; + } + + @Override + public String toString() { + return "CompetitorWithID [boatID=" + boatID + ", threeLetterIOCCode=" + + threeLetterIOCCode + ", name=" + name + ", id=" + id + "]"; + } + + @Override + public List getCrew() { + return crew; + } + +} diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CompetitorImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CompetitorWithoutID.java similarity index 56% rename from java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CompetitorImpl.java rename to java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CompetitorWithoutID.java index dd2290e1e3f..80b484c798f 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CompetitorImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CompetitorWithoutID.java @@ -1,13 +1,16 @@ package com.sap.sailing.domain.swisstimingadapter.impl; -import com.sap.sailing.domain.swisstimingadapter.Competitor; +import java.util.List; -public class CompetitorImpl implements Competitor { +import com.sap.sailing.domain.swisstimingadapter.Competitor; +import com.sap.sailing.domain.swisstimingadapter.CrewMember; + +public class CompetitorWithoutID implements Competitor { private final String boatID; private final String threeLetterIOCCode; private final String name; - public CompetitorImpl(String boatID, String threeLetterIOCCode, String name) { + public CompetitorWithoutID(String boatID, String threeLetterIOCCode, String name) { super(); this.boatID = boatID; this.threeLetterIOCCode = threeLetterIOCCode; @@ -29,13 +32,19 @@ public class CompetitorImpl implements Competitor { return name; } - @Override - public String toString() { - return name+"/"+boatID; - } - public String getID() { return null; } + @Override + public String toString() { + return "CompetitorWithoutID [boatID=" + boatID + ", threeLetterIOCCode=" + + threeLetterIOCCode + ", name=" + name + "]"; + } + + @Override + public List getCrew() { + return null; + } + } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CrewMemberImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CrewMemberImpl.java new file mode 100644 index 00000000000..edd06e5ca15 --- /dev/null +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/CrewMemberImpl.java @@ -0,0 +1,31 @@ +package com.sap.sailing.domain.swisstimingadapter.impl; + +import com.sap.sailing.domain.swisstimingadapter.CrewMember; + +public class CrewMemberImpl implements CrewMember { + private String name; + private String nationality; + private String position; + + public CrewMemberImpl(String name, String nationality, String position) { + super(); + this.name = name; + this.nationality = nationality; + this.position = position; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getNationality() { + return nationality; + } + + public String getPosition() { + return position; + } + +} diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/DomainFactoryImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/DomainFactoryImpl.java index d0629b12d42..d1208426348 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/DomainFactoryImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/DomainFactoryImpl.java @@ -1,6 +1,7 @@ package com.sap.sailing.domain.swisstimingadapter.impl; import java.util.ArrayList; +import java.util.Calendar; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -36,6 +37,7 @@ import com.sap.sailing.domain.common.impl.Util; import com.sap.sailing.domain.common.impl.Util.Pair; import com.sap.sailing.domain.racelog.RaceLogStore; import com.sap.sailing.domain.swisstimingadapter.Course; +import com.sap.sailing.domain.swisstimingadapter.CrewMember; import com.sap.sailing.domain.swisstimingadapter.DomainFactory; import com.sap.sailing.domain.swisstimingadapter.Fix; import com.sap.sailing.domain.swisstimingadapter.Mark; @@ -97,7 +99,8 @@ public class DomainFactoryImpl implements DomainFactory { } if (result == null) { BoatClass regattaBoatClass = boatClass != null ? boatClass : getRaceTypeFromRaceID(raceID).getBoatClass(); - result = new RegattaImpl(raceLogStore, raceID, regattaBoatClass, trackedRegattaRegistry, + Calendar calendar = Calendar.getInstance(); + result = new RegattaImpl(raceLogStore, "ST Regatta " + calendar.get(Calendar.YEAR)+" for race "+raceID, regattaBoatClass, trackedRegattaRegistry, getBaseDomainFactory().createScoringScheme(ScoringSchemeType.LOW_POINT), raceID, null); logger.info("Created regatta "+result.getName()+" ("+result.hashCode()+")"); raceIDToRegattaCache.put(raceID, result); @@ -105,28 +108,44 @@ public class DomainFactoryImpl implements DomainFactory { return result; } - private Competitor getCompetitorByBoatIDAndRaceType(String boatID, RaceType raceType) { - return baseDomainFactory.getExistingCompetitorById(getCompetitorID(boatID, raceType)); - } - - private Competitor getCompetitorByBoatIDAndBoatClass(String boatID, BoatClass boatClass) { - return baseDomainFactory.getExistingCompetitorById(getCompetitorID(boatID, boatClass)); + @Override + public Competitor createCompetitorWithID(com.sap.sailing.domain.swisstimingadapter.Competitor competitor, BoatClass boatClass) { + CompetitorStore competitorStore = baseDomainFactory.getCompetitorStore(); + Competitor result = competitorStore.getExistingCompetitorByIdAsString(competitor.getID()); + if (result == null || competitorStore.isCompetitorToUpdateDuringGetOrCreate(result)) { + DynamicBoat boat = new BoatImpl(competitor.getName(), boatClass, competitor.getBoatID()); + List teamMembers = new ArrayList(); + for (CrewMember crewMember: competitor.getCrew()) { + DynamicPerson person = new PersonImpl(crewMember.getName().trim(), getOrCreateNationality(crewMember.getNationality()), + /* dateOfBirth */ null, crewMember.getPosition()); + teamMembers.add(person); + } + DynamicTeam team = new TeamImpl(competitor.getName(), teamMembers, /* coach */ null); + result = competitorStore.getOrCreateCompetitor(competitor.getID(), competitor.getName(), null /*displayColor*/, team, boat); + } + return result; } @Override - public Competitor getCompetitorByBoatIDAndRaceIDOrBoatClass(String boatID, String raceID, BoatClass boatClass) { - Competitor result = null; - if(boatClass != null) { - result = getCompetitorByBoatIDAndBoatClass(boatID, boatClass); - } else { - RaceType raceType = getRaceTypeFromRaceID(raceID); - if (raceType != null) { - result = getCompetitorByBoatIDAndRaceType(boatID, raceType); - } + public Competitor createCompetitorWithoutID(com.sap.sailing.domain.swisstimingadapter.Competitor competitor, String raceId, BoatClass boatClass) { + Competitor result = null; + DynamicBoat boat = new BoatImpl(competitor.getName(), boatClass, competitor.getBoatID()); + List teamMembers = new ArrayList(); + for (String teamMemberName : competitor.getName().split("[-+&]")) { + teamMembers.add(new PersonImpl(teamMemberName.trim(), getOrCreateNationality(competitor.getThreeLetterIOCCode()), + /* dateOfBirth */ null, teamMemberName.trim())); } + DynamicTeam team = new TeamImpl(competitor.getName(), teamMembers, /* coach */ null); + result = baseDomainFactory.getCompetitorStore().getOrCreateCompetitor(getCompetitorID(competitor.getBoatID(), raceId, boatClass), + competitor.getName(), null /*displayColor*/, team, boat); return result; } + @Override + public Competitor createCompetitorWithoutID(String boatID, String threeLetterIOCCode, String name, String raceId, BoatClass boatClass) { + return createCompetitorWithoutID(new CompetitorWithoutID(boatID, threeLetterIOCCode, name), raceId, boatClass); + } + private String getCompetitorID(String boatID, String raceId, BoatClass boatClass) { String result = null; if(boatClass != null) { @@ -140,36 +159,15 @@ public class DomainFactoryImpl implements DomainFactory { return result; } - private String getCompetitorID(String boatID, RaceType raceType) { + @Override + public String getCompetitorID(String boatID, RaceType raceType) { return boatID + "/" + raceType.getRaceCode(); } - private String getCompetitorID(String boatID, BoatClass boatClass) { + @Override + public String getCompetitorID(String boatID, BoatClass boatClass) { return boatID + "/" + boatClass.getName(); } - - @Override - public Competitor getOrCreateCompetitor(com.sap.sailing.domain.swisstimingadapter.Competitor competitor, String raceId, BoatClass boatClass) { - Competitor result = getCompetitorByBoatIDAndRaceIDOrBoatClass(competitor.getBoatID(), raceId, boatClass); - CompetitorStore competitorStore = baseDomainFactory.getCompetitorStore(); - if (result == null || competitorStore.isCompetitorToUpdateDuringGetOrCreate(result)) { - DynamicBoat boat = new BoatImpl(competitor.getName(), boatClass, competitor.getBoatID()); - List teamMembers = new ArrayList(); - for (String teamMemberName : competitor.getName().split("[-+&]")) { - teamMembers.add(new PersonImpl(teamMemberName.trim(), getOrCreateNationality(competitor.getThreeLetterIOCCode()), - /* dateOfBirth */ null, teamMemberName.trim())); - } - DynamicTeam team = new TeamImpl(competitor.getName(), teamMembers, /* coach */ null); - result = competitorStore.getOrCreateCompetitor(getCompetitorID(competitor.getBoatID(), raceId, boatClass), - competitor.getName(), null /*displayColor*/, team, boat); - } - return result; - } - - @Override - public Competitor getOrCreateCompetitor(String boatID, String threeLetterIOCCode, String name, String raceId, BoatClass boatClass) { - return getOrCreateCompetitor(new CompetitorImpl(boatID, threeLetterIOCCode, name), raceId, boatClass); - } @Override public RaceDefinition createRaceDefinition(Regatta regatta, String raceID, Iterable competitors, @@ -193,7 +191,7 @@ public class DomainFactoryImpl implements DomainFactory { Iterable competitors = createCompetitorList(startList, race.getRaceID(), race.getBoatClass()); logger.info("Creating RaceDefinitionImpl for race "+race.getRaceID()); BoatClass boatClass = race.getBoatClass() != null ? race.getBoatClass() : getRaceTypeFromRaceID(race.getRaceID()).getBoatClass(); - RaceDefinition result = new RaceDefinitionImpl(race.getRaceID(), domainCourse, boatClass, competitors); + RaceDefinition result = new RaceDefinitionImpl(race.getRaceName(), domainCourse, boatClass, competitors, race.getRaceID()); regatta.addRace(result); return result; } @@ -246,7 +244,12 @@ public class DomainFactoryImpl implements DomainFactory { private Iterable createCompetitorList(StartList startList, String raceId, BoatClass boatClass) { List result = new ArrayList(); for (com.sap.sailing.domain.swisstimingadapter.Competitor swissTimingCompetitor : startList.getCompetitors()) { - Competitor domainCompetitor = getOrCreateCompetitor(swissTimingCompetitor, raceId, boatClass); + Competitor domainCompetitor; + if(swissTimingCompetitor.getID() != null) { + domainCompetitor = createCompetitorWithID(swissTimingCompetitor, boatClass); + } else { + domainCompetitor = createCompetitorWithoutID(swissTimingCompetitor, raceId, boatClass); + } result.add(domainCompetitor); } return result; @@ -348,10 +351,10 @@ public class DomainFactoryImpl implements DomainFactory { } @Override - public RaceTrackingConnectivityParameters createTrackingConnectivityParameters(String hostname, int port, String raceID, - String raceDescription, BoatClass boatClass, long delayToLiveInMillis, + public RaceTrackingConnectivityParameters createTrackingConnectivityParameters(String hostname, int port, String raceID, String raceName, + String raceDescription, BoatClass boatClass, StartList startList, long delayToLiveInMillis, SwissTimingFactory swissTimingFactory, DomainFactory domainFactory, RaceLogStore raceLogStore) { - return new SwissTimingTrackingConnectivityParameters(hostname, port, raceID, raceDescription, boatClass, delayToLiveInMillis, + return new SwissTimingTrackingConnectivityParameters(hostname, port, raceID, raceName, raceDescription, boatClass, startList, delayToLiveInMillis, swissTimingFactory, domainFactory, raceLogStore); } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/RaceImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/RaceImpl.java index 6eb078fc334..94cb843f583 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/RaceImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/RaceImpl.java @@ -6,16 +6,18 @@ import com.sap.sailing.domain.swisstimingadapter.Race; public class RaceImpl implements Race { private final String description; private final String raceId; + private final String raceName; private final BoatClass boatClass; - public RaceImpl(String raceId, String description, BoatClass boatClass) { + public RaceImpl(String raceId, String raceName, String description, BoatClass boatClass) { this.raceId = raceId; + this.raceName = raceName; this.description = description; this.boatClass = boatClass; } - public RaceImpl(String raceId, String description) { - this(raceId, description, null); + public RaceImpl(String raceId, String raceName, String description) { + this(raceId, raceName, description, null); } @Override @@ -32,4 +34,9 @@ public class RaceImpl implements Race { public BoatClass getBoatClass() { return boatClass; } + + @Override + public String getRaceName() { + return raceName; + } } 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 a2a82fe2702..7c356bc833f 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 @@ -90,8 +90,8 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement private final Set listeners; private final Thread receiverThread; private boolean stopped; - private boolean connected; private final String raceId; + private final String raceName; private final String raceDescription; private final BoatClass boatClass; @@ -108,6 +108,13 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement */ private String lastTimeZoneSuffix; + /** + * Some messages in the SwissTiming SailMaster protocol lack proper time stamp information. It is therefore + * necessary to keep track of time stamps received from other messages and use them as an approximation for + * the time point of messages received without explicit time stamp. + */ + private TimePoint lastRPDMessageTimePoint; + private TimePoint startTime; /** @@ -121,10 +128,11 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement private Long numberOfStoredMessages; - public SailMasterConnectorImpl(String host, int port, String raceId, String raceDescription, BoatClass boatClass) throws InterruptedException, ParseException { + public SailMasterConnectorImpl(String host, int port, String raceId, String raceName, String raceDescription, BoatClass boatClass) throws InterruptedException, ParseException { super(); maxSequenceNumber = -1l; this.raceId = raceId; // from this time on, the connector interprets messages for raceID + this.raceName = raceName; this.raceDescription = raceDescription; this.boatClass = boatClass; dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); @@ -134,45 +142,43 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement this.unprocessedMessagesByType = new HashMap>(); receiverThread = new Thread(this, "SwissTiming SailMaster Receiver"); receiverThread.start(); - synchronized (this) { - while (!connected) { - wait(); - } - } } public void run() { try { while (!stopped) { try { - ensureSocketIsOpen(); - String receivedMessage = receiveMessage(socket.getInputStream()); - if (receivedMessage == null) { - // reached EOF; this means the socket is or can be closed - if (socket != null && !socket.isClosed()) { - socket.close(); - } - socket = null; - } else { - SailMasterMessage message = new SailMasterMessageImpl(receivedMessage); - // drop race-specific messages for non-tracked races - if (message.getSequenceNumber() != null) { - maxSequenceNumber = Math.max(maxSequenceNumber, message.getSequenceNumber()); - if (maxSequenceNumber <= numberOfStoredMessages) { - notifyListenersStoredDataProgress(raceId, (double) maxSequenceNumber / (double) numberOfStoredMessages); + ensureSocketIsOpen(); // the result of this may be that the connector w\s stopped in between, so another check is required + if (!stopped && socket != null) { + String receivedMessage = receiveMessage(socket.getInputStream()); + if (receivedMessage == null) { + // reached EOF; this means the socket is or can be closed + if (socket != null && !socket.isClosed()) { + socket.close(); + } + socket = null; + } else { + SailMasterMessage message = new SailMasterMessageImpl(receivedMessage); + // drop race-specific messages for non-tracked races + if (message.getSequenceNumber() != null) { + maxSequenceNumber = Math.max(maxSequenceNumber, message.getSequenceNumber()); + if (maxSequenceNumber <= numberOfStoredMessages) { + notifyListenersStoredDataProgress(raceId, (double) maxSequenceNumber + / (double) numberOfStoredMessages); + } + } + if (message.isResponse()) { + // this is a response for an explicit request + rendevouz(message); + } else if (message.isEvent()) { + // a spontaneous event + logger.fine("notifying message " + message); + notifyListeners(message); + } + if (message.getType() == MessageType._STOPSERVER) { + logger.info("SailMasterConnector received " + MessageType._STOPSERVER.name()); + stop(); } - } - if (message.isResponse()) { - // this is a response for an explicit request - rendevouz(message); - } else if (message.isEvent()) { - // a spontaneous event - logger.fine("notifying message " + message); - notifyListeners(message); - } - if (message.getType() == MessageType._STOPSERVER) { - logger.info("SailMasterConnector received " + MessageType._STOPSERVER.name()); - stop(); } } } catch (SocketException se) { @@ -183,9 +189,9 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement } } } catch (Exception e) { - logger.log(Level.SEVERE, "Exception in sail master connector "+SailMasterConnectorImpl.class.getName()+".run", e); + logger.log(Level.SEVERE, "Exception in sail master connector "+SailMasterConnectorImpl.class.getName()+".run for "+this, e); } - logger.info("Stopping Sail Master connector thread"); + logger.info("Stopping Sail Master connector thread for "+this); stopped = true; } @@ -196,14 +202,16 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement return result; } - private synchronized BlockingQueue getBlockingQueue(MessageType type) { - BlockingQueue blockingQueue; - blockingQueue = unprocessedMessagesByType.get(type); - if (blockingQueue == null) { - blockingQueue = new LinkedBlockingQueue(); - unprocessedMessagesByType.put(type, blockingQueue); + private BlockingQueue getBlockingQueue(MessageType type) { + synchronized (unprocessedMessagesByType) { + BlockingQueue blockingQueue; + blockingQueue = unprocessedMessagesByType.get(type); + if (blockingQueue == null) { + blockingQueue = new LinkedBlockingQueue(); + unprocessedMessagesByType.put(type, blockingQueue); + } + return blockingQueue; } - return blockingQueue; } private void rendevouz(SailMasterMessage message) { @@ -232,6 +240,9 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement case TMD: notifyListenersTMD(message); break; + case WND: + notifyListenersWND(message); + break; default: // ignore all other messages because there are no notification patterns for those } @@ -242,7 +253,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement } private void notifyListenersStoredDataProgress(String raceID, double progress) { - for (SailMasterListener listener : getListeners(raceID)) { + for (SailMasterListener listener : getListeners()) { try { listener.storedDataProgress(raceID, progress); } catch (Exception e) { @@ -251,6 +262,22 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement } } } + + private void notifyListenersWND(SailMasterMessage message) { + // example message: WND|W4702|1|320|5.4 + String raceID = message.getSections()[1]; + int zeroBasedMarkIndex = Integer.valueOf(message.getSections()[2]); + double windDirectionTrueDegrees = Double.valueOf(message.getSections()[3]); + double windSpeedInKnots = Double.valueOf(message.getSections()[4]); + for (SailMasterListener listener : getListeners()) { + try { + listener.receivedWindData(raceID, zeroBasedMarkIndex, windDirectionTrueDegrees, windSpeedInKnots); + } catch (Exception e) { + logger.info("Exception occurred trying to notify listener "+listener+" about "+message+": "+e.getMessage()); + logger.throwing(SailMasterConnectorImpl.class.getName(), "notifyListenersWND", e); + } + } + } private void notifyListenersTMD(SailMasterMessage message) { // example message: TMD|W4702|NZL 75|1|4;;00:49:43 @@ -266,7 +293,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement parseHHMMSSToMilliseconds(details[2]); markIndicesRanksAndTimesSinceStartInMilliseconds.add(new Triple(markIndex, rank, timeSinceStartInMilliseconds)); } - for (SailMasterListener listener : getListeners(message.getRaceID())) { + for (SailMasterListener listener : getListeners()) { try { listener.receivedTimingData(raceID, boatID, markIndicesRanksAndTimesSinceStartInMilliseconds); } catch (Exception e) { @@ -278,7 +305,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement private void notifyListenersCAM(SailMasterMessage message) throws ParseException { List> clockAtMarkResults = parseClockAtMarkMessage(message); - for (SailMasterListener listener : getListeners(message.getRaceID())) { + for (SailMasterListener listener : getListeners()) { try { listener.receivedClockAtMark(message.getSections()[1], clockAtMarkResults); } catch (Exception e) { @@ -290,7 +317,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement private void notifyListenersSTL(SailMasterMessage message) { StartList startListMessage = parseStartListMessage(message); - for (SailMasterListener listener : getListeners(message.getRaceID())) { + for (SailMasterListener listener : getListeners()) { try { listener.receivedStartList(message.getSections()[1], startListMessage); } catch (Exception e) { @@ -302,7 +329,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement private void notifyListenersCCG(SailMasterMessage message) { Course course = parseCourseConfigurationMessage(message); - for (SailMasterListener listener : getListeners(message.getRaceID())) { + for (SailMasterListener listener : getListeners()) { try { listener.receivedCourseConfiguration(message.getSections()[1], course); } catch (Exception e) { @@ -314,7 +341,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement private void notifyListenersRAC(SailMasterMessage message) { Iterable races = parseAvailableRacesMessage(message); - for (SailMasterListener listener : listeners) { + for (SailMasterListener listener : getListeners()) { try { listener.receivedAvailableRaces(races); } catch (Exception e) { @@ -331,6 +358,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement String raceID = sections[1]; RaceStatus status = RaceStatus.values()[Integer.valueOf(sections[2])]; TimePoint timePoint = new MillisecondsTimePoint(parseTimeAndDateISO(sections[3], raceID)); + lastRPDMessageTimePoint = timePoint; String dateISO = sections[3].substring(0, sections[3].indexOf('T')); String startTimeEstimatedStartTimeISO = dateISO+"T"+sections[4]+lastTimeZoneSuffix; TimePoint startTimeEstimatedStartTime = sections[4].trim().length() == 0 ? null : new MillisecondsTimePoint( @@ -389,7 +417,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement rank, averageSpeedOverGround, velocityMadeGood, distanceToLeader, distanceToNextMark, boatIRM)); } } - Set allListeners = getListeners(message.getRaceID()); + Set allListeners = getListeners(); for (SailMasterListener listener : allListeners) { try { listener.receivedRacePositionData(raceID, status, timePoint, startTimeEstimatedStartTime, millisecondsSinceRaceStart, @@ -401,17 +429,28 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement } } - private Set getListeners(String raceID) { - return Collections.unmodifiableSet(listeners); + private Set getListeners() { + synchronized (listeners) { + return Collections.unmodifiableSet(listeners); + } } @Override - public synchronized void stop() throws IOException { - logger.info("Stopping SailMasterConnector listening on port "+port+" with socket "+socket); + public String toString() { + return "SailMasterConnector listening on port "+port+" with socket "+socket+" for race "+raceName+" with ID "+raceId+" ("+raceDescription+")"; + } + + @Override + public void stop() throws IOException { + logger.info("Stopping "+this); stopped = true; - socket.close(); - socket = null; - notifyAll(); + if (socket != null) { + socket.close(); + socket = null; + } + synchronized (this) { + notifyAll(); + } } @Override @@ -421,13 +460,19 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement @Override public void addSailMasterListener(SailMasterListener listener) throws UnknownHostException, IOException, InterruptedException { - ensureSocketIsOpen(); - listeners.add(listener); + synchronized (listeners) { + listeners.add(listener); + } } @Override - public void removeSailMasterListener(SailMasterListener listener) { - listeners.remove(listener); + public void removeSailMasterListener(SailMasterListener listener) throws IOException { + synchronized (listeners) { + listeners.remove(listener); + if (listeners.isEmpty()) { + stop(); + } + } } public SailMasterMessage sendRequestAndGetResponse(MessageType messageType, String... args) throws UnknownHostException, IOException, InterruptedException { @@ -458,68 +503,75 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement return sailMasterMessage; } - private synchronized void ensureSocketIsOpen() throws InterruptedException { - while (!stopped && socket == null) { - try { - logger.info("Opening socket to " + host + ":" + port + " and sending " + MessageType.OPN.name() + " message..."); - socket = new Socket(host, port); - final OutputStream os = socket.getOutputStream(); - final InputStream is = socket.getInputStream(); - final SailMasterMessage opnRequest = createSailMasterMessage(MessageType.OPN, raceId); - sendMessage(opnRequest, os); - SailMasterMessage opnResponse = new SailMasterMessageImpl(receiveMessage(is)); - if (opnResponse.getType() != MessageType.OPN || !"OK".equals(opnResponse.getSections()[1])) { - logger.info("Recevied non-OK response " + opnResponse + " for our request " - + opnRequest + ". Closing socket and trying again in 1s..."); - closeAndNullSocketAndWaitABit(); - } else { - logger.info("Received " + opnResponse + " which seems OK. Continuing with " + MessageType.LSN.name() + " request..."); - numberOfStoredMessages = Long.valueOf(opnResponse.getSections()[2]); - List lsnArgs = new ArrayList<>(); - lsnArgs.add("ON"); // request live messages always; why not? - if (maxSequenceNumber != -1) { - logger.info("Requesting messages starting from sequence number "+(maxSequenceNumber+1)); - // already received a numbered message; ask only for newer messages with greater sequence number - lsnArgs.add(new Long(maxSequenceNumber+1).toString()); - } - final SailMasterMessage lsnRequest = createSailMasterMessage(MessageType.LSN, lsnArgs.toArray(new String[0])); - sendMessage(lsnRequest, os); - SailMasterMessageImpl lsnResponse = new SailMasterMessageImpl(receiveMessage(is)); - if (lsnResponse.getType() != MessageType.LSN || !"OK".equals(lsnResponse.getSections()[1])) { - logger.info("Received non-OK response " + lsnResponse + " for our request " + lsnRequest - + ". Closing socket and trying again in 1s..."); - closeAndNullSocketAndWaitABit(); + private final Object ensureSocketIsOpenSemaphor = new Object(); + private void ensureSocketIsOpen() throws InterruptedException { + synchronized (ensureSocketIsOpenSemaphor) { + while (!stopped && socket == null) { + try { + logger.info("Opening socket to " + host + ":" + port + " and sending " + MessageType.OPN.name() + + " message..."); + socket = new Socket(host, port); + final OutputStream os = socket.getOutputStream(); + final InputStream is = socket.getInputStream(); + final SailMasterMessage opnRequest = createSailMasterMessage(MessageType.OPN, raceId); + sendMessage(opnRequest, os); + SailMasterMessage opnResponse = new SailMasterMessageImpl(receiveMessage(is)); + if (opnResponse.getType() != MessageType.OPN || !"OK".equals(opnResponse.getSections()[1])) { + logger.info("Recevied non-OK response " + opnResponse + " in "+this+" for our request " + opnRequest + + ". Closing socket and stopping because we have no hope for recovery"); + stopped = true; + closeSocket(); } else { - logger.info("Received "+lsnResponse+" which seems to be OK. I think we're connected!"); - synchronized (this) { - logger.info("...successfully opened socket to " + host + ":" + port); - // TODO now request all messages starting after the last message received so far - connected = true; - notifyAll(); + logger.info("Received " + opnResponse + " in "+this+" which seems OK. Continuing with " + + MessageType.LSN.name() + " request..."); + numberOfStoredMessages = Long.valueOf(opnResponse.getSections()[2]); + List lsnArgs = new ArrayList<>(); + lsnArgs.add("ON"); // request live messages always; why not? + if (maxSequenceNumber != -1) { + logger.info("Requesting messages starting from sequence number " + (maxSequenceNumber + 1)+" in "+this); + // already received a numbered message; ask only for newer messages with greater sequence number + lsnArgs.add(new Long(maxSequenceNumber + 1).toString()); + } + final SailMasterMessage lsnRequest = createSailMasterMessage(MessageType.LSN, + lsnArgs.toArray(new String[0])); + sendMessage(lsnRequest, os); + SailMasterMessageImpl lsnResponse = new SailMasterMessageImpl(receiveMessage(is)); + if (lsnResponse.getType() != MessageType.LSN || !"OK".equals(lsnResponse.getSections()[1])) { + logger.info("Received non-OK response " + lsnResponse + " for our request " + lsnRequest + + " in "+this+". Closing socket and trying again in 1s..."); + closeAndNullSocketAndWaitABit(); + } else { + logger.info("Received " + lsnResponse + + " which seems to be OK. I think we're connected in "+this+"!"); } } + } catch (IOException e) { + logger.log(Level.INFO, "Exception trying to establish connection in "+this+". Trying again in 1s.", e); + closeAndNullSocketAndWaitABit(); } - } catch (IOException e) { - logger.log(Level.INFO, "Exception trying to establish SailMaster connection to " + host + ":" + port - + ". Trying again in 1s.", e); - closeAndNullSocketAndWaitABit(); } } } private void closeAndNullSocketAndWaitABit() throws InterruptedException { - try { - socket.close(); - } catch (IOException e) { - logger.log(Level.INFO, "Exception trying to close socket. Maybe already closed. Continuing", e); - } - socket = null; + closeSocket(); Thread.sleep(1000); } + private void closeSocket() { + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + logger.log(Level.INFO, "Exception trying to close socket. Maybe already closed. Continuing", e); + } + socket = null; + } + } + @Override public Race getRace() { - return new RaceImpl(raceId, raceDescription, boatClass); + return new RaceImpl(raceId, raceName, raceDescription, boatClass); } private List parseAvailableRacesMessage(SailMasterMessage availableRacesMessage) { @@ -528,7 +580,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement List result = new ArrayList(); for (int i=0; i()); Thread messageDeliveryThread = new Thread("SailMasterLiveSimulatorConnector") { public void run() { diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterMessageImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterMessageImpl.java index f51285157c7..b98dab16877 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterMessageImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SailMasterMessageImpl.java @@ -68,7 +68,11 @@ public class SailMasterMessageImpl implements SailMasterMessage { } else { typeAsString = typeAsStringWithOptionalSuffix.substring(0, typeAsStringWithOptionalSuffix.length()-1); } - return MessageType.valueOf(typeAsString); + try { + return MessageType.valueOf(typeAsString); + } catch (IllegalArgumentException e) { + return null; + } } @Override diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingAdapterImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingAdapterImpl.java index 18807630ba3..544a7b682b7 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingAdapterImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingAdapterImpl.java @@ -1,26 +1,46 @@ package com.sap.sailing.domain.swisstimingadapter.impl; import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; import java.net.UnknownHostException; import java.text.ParseException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; + +import javax.xml.bind.JAXBException; import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.DomainFactory; import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.swisstimingadapter.Competitor; +import com.sap.sailing.domain.swisstimingadapter.CrewMember; +import com.sap.sailing.domain.swisstimingadapter.StartList; import com.sap.sailing.domain.swisstimingadapter.SwissTimingAdapter; import com.sap.sailing.domain.swisstimingadapter.SwissTimingFactory; import com.sap.sailing.domain.tracking.RacesHandle; -import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.TrackerManager; +import com.sap.sailing.xrr.resultimport.ParserFactory; +import com.sap.sailing.xrr.resultimport.schema.Boat; +import com.sap.sailing.xrr.resultimport.schema.Crew; +import com.sap.sailing.xrr.resultimport.schema.Division; +import com.sap.sailing.xrr.resultimport.schema.Event; +import com.sap.sailing.xrr.resultimport.schema.Person; +import com.sap.sailing.xrr.resultimport.schema.RaceResult; +import com.sap.sailing.xrr.resultimport.schema.RegattaResults; +import com.sap.sailing.xrr.resultimport.schema.Team; public class SwissTimingAdapterImpl implements SwissTimingAdapter { private final SwissTimingFactory swissTimingFactory; private final com.sap.sailing.domain.swisstimingadapter.DomainFactory swissTimingDomainFactory; + private final long DEFAULT_SWISSTIMING_LIVE_DELAY_IN_MILLISECONDS = 6000; + public SwissTimingAdapterImpl(DomainFactory baseDomainFactory) { swissTimingFactory = SwissTimingFactory.INSTANCE; swissTimingDomainFactory = new DomainFactoryImpl(baseDomainFactory); @@ -46,13 +66,84 @@ public class SwissTimingAdapterImpl implements SwissTimingAdapter { } @Override - public RacesHandle addSwissTimingRace(TrackerManager trackerManager, RegattaIdentifier regattaToAddTo, String raceID, String raceDescription, - BoatClass boatClass, String hostname, int port, RaceLogStore logStore, long timeoutInMilliseconds) + public RacesHandle addSwissTimingRace(TrackerManager trackerManager, RegattaIdentifier regattaToAddTo, String raceID, String raceName, String raceDescription, + BoatClass boatClass, String hostname, int port, StartList startList, RaceLogStore logStore, long timeoutInMilliseconds) throws Exception { return trackerManager.addRace(regattaToAddTo, swissTimingDomainFactory.createTrackingConnectivityParameters(hostname, port, - raceID, raceDescription, boatClass, TrackedRace.DEFAULT_LIVE_DELAY_IN_MILLISECONDS, swissTimingFactory, + raceID, raceName, raceDescription, boatClass, startList, DEFAULT_SWISSTIMING_LIVE_DELAY_IN_MILLISECONDS, swissTimingFactory, swissTimingDomainFactory, logStore), timeoutInMilliseconds); } + @Override + public StartList readStartListForRace(String raceId, RegattaResults regattaResults) { + StartList result = null; + List competitors = new ArrayList(); + + Map persons = new HashMap(); + Map boats = new HashMap(); + Map teams = new HashMap(); + // collect boats, persons and teams + for (Object o : regattaResults.getPersonOrBoatOrTeam()) { + if(o instanceof Person) { + Person p = (Person) o; + persons.put(p.getPersonID(), p); + } else if (o instanceof Boat) { + Boat b = (Boat) o; + boats.put(b.getBoatID(), b); + } else if (o instanceof Team) { + Team t = (Team) o; + teams.put(t.getTeamID(), t); + } + } + + for (Object o : regattaResults.getPersonOrBoatOrTeam()) { + if (o instanceof Event) { + Event event = (Event) o; + for (Object eventO : event.getRaceOrDivisionOrRegattaSeriesResult()) { + if (eventO instanceof Division) { + Division division = (Division) eventO; + for(Object seriesResultOrRaceResultOrTRResult: division.getSeriesResultOrRaceResultOrTRResult()) { + if(seriesResultOrRaceResultOrTRResult instanceof RaceResult) { + RaceResult raceResult = (RaceResult) seriesResultOrRaceResultOrTRResult; + if(raceResult.getRaceID().equals(raceId)) { + Team team = teams.get(raceResult.getTeamID()); + if(team != null) { + Boat boat = boats.get(team.getBoatID()); + if(boat != null) { + List crew = new ArrayList(); + for(Crew crewMember: team.getCrew()) { + Person person = persons.get(crewMember.getPersonID()); + crew.add(new CrewMemberImpl(person.getGivenName() + " " + person.getFamilyName(), + person.getNOC().name(), crewMember.getPosition().name())); + } + Crew firstCrewMember = team.getCrew().get(0); + Person person = persons.get(firstCrewMember.getPersonID()); + String nationality = person.getNOC().name(); + CompetitorWithID competitor = new CompetitorWithID(team.getTeamID(), boat.getSailNumber(), nationality, team.getTeamName(), crew); + competitors.add(competitor); + } + } + } + } + } + } + } + } + } + if(!competitors.isEmpty()) { + result = new StartListImpl(raceId, competitors); + } + + return result; + } + + @Override + public RegattaResults readRegattaEntryListFromXrrUrl(String xrrEntryListUrl) throws IOException, JAXBException { + // try to read the entry list from manage2sail + URL regattaEntryListUrl = new URL(xrrEntryListUrl); + URLConnection regattaEntryListConn = regattaEntryListUrl.openConnection(); + RegattaResults regattaResults = ParserFactory.INSTANCE.createParser((InputStream) regattaEntryListConn.getContent(), xrrEntryListUrl).parse(); + return regattaResults; + } } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java index 0af5164a34b..825a2ab771a 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java @@ -1,124 +1,126 @@ -package com.sap.sailing.domain.swisstimingadapter.impl; - -import java.io.IOException; -import java.net.UnknownHostException; -import java.text.ParseException; -import java.util.HashMap; -import java.util.Map; -import java.util.logging.Logger; - -import com.sap.sailing.domain.base.BoatClass; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.common.impl.Util.Triple; -import com.sap.sailing.domain.racelog.RaceLogStore; -import com.sap.sailing.domain.swisstimingadapter.DomainFactory; -import com.sap.sailing.domain.swisstimingadapter.Race; -import com.sap.sailing.domain.swisstimingadapter.SailMasterConnector; -import com.sap.sailing.domain.swisstimingadapter.SailMasterMessage; -import com.sap.sailing.domain.swisstimingadapter.SailMasterTransceiver; -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.SwissTimingMessageParser; -import com.sap.sailing.domain.swisstimingadapter.SwissTimingRaceTracker; -import com.sap.sailing.domain.tracking.RaceTracker; -import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; -import com.sap.sailing.domain.tracking.WindStore; - -public class SwissTimingFactoryImpl implements SwissTimingFactory { - private static final Logger logger = Logger.getLogger(SwissTimingFactoryImpl.class.getName()); - - private final Map, SailMasterConnector> connectors; - - public SwissTimingFactoryImpl() { - connectors = new HashMap<>(); - } - - @Override - public SwissTimingMessageParser createMessageParser() { - return new SwissTimingMessageParserImpl(); - } - - @Override - public SailMasterConnector getOrCreateSailMasterConnector(String host, int port, String raceId, - String raceDescription, BoatClass boatClass) throws InterruptedException, ParseException { - if (Boolean.valueOf(System.getProperty("simulateLiveMode", "false"))) { - return getOrCreateSailMasterLiveSimulatorConnector(host, port, raceId, raceDescription, boatClass); - } else { - Triple key = new Triple(host, port, raceId); - SailMasterConnector result = connectors.get(key); - if (result == null || result.isStopped()) { - if (result == null) { - logger.info("Creating a new connector for "+key+" because none found"); - } else { - logger.info("Creating a new connector for "+key+" because the old one was stopped"); - } - result = new SailMasterConnectorImpl(host, port, raceId, raceDescription, boatClass); - connectors.put(key, result); - // TODO how do connectors get stopped, terminated and removed from the connectors map again? - } else { - logger.info("Re-using connector for "+key+" because it wasn't stopped"); - } - return result; - } - } - - @Override - public SailMasterConnector getOrCreateSailMasterLiveSimulatorConnector(String host, int port, String raceId, - String raceDescription, BoatClass boatClass) throws InterruptedException, ParseException { - Triple key = new Triple<>(host, port, raceId); - SailMasterConnector result = connectors.get(key); - if (result == null) { - result = new SailMasterLiveSimulatorConnectorImpl(host, port, raceId, raceDescription, boatClass); - connectors.put(key, result); - // TODO how do connectors get stopped, terminated and removed from the connectors map again? - } else if (result.isStopped()) { - result = new SailMasterLiveSimulatorConnectorImpl(host, port, raceId, raceDescription, boatClass); - connectors.put(key, result); - } - return result; - } - - @Override - public SwissTimingConfiguration createSwissTimingConfiguration(String name, String jsonURL, String hostname, int port) { - return new SwissTimingConfigurationImpl(name, jsonURL, hostname, port); - } - - @Override - public SwissTimingRaceTracker createRaceTracker(String raceID, String raceDescription, BoatClass boatClass, String hostname, int port, - long delayToLiveInMillis, RaceLogStore raceLogStore, WindStore windStore, DomainFactory domainFactory, - TrackedRegattaRegistry trackedRegattaRegistry) throws InterruptedException, - UnknownHostException, IOException, ParseException { - return new SwissTimingRaceTrackerImpl(raceID, raceDescription, boatClass, hostname, port, raceLogStore, windStore, domainFactory, this, - trackedRegattaRegistry, delayToLiveInMillis); - } - - @Override - public RaceTracker createRaceTracker(Regatta regatta, String raceID, String raceDescription, BoatClass boatClass, String hostname, - int port, long delayToLiveInMillis, WindStore windStore, DomainFactory domainFactory, - TrackedRegattaRegistry trackedRegattaRegistry) throws UnknownHostException, - InterruptedException, IOException, ParseException { - return new SwissTimingRaceTrackerImpl(regatta, raceID, raceDescription, boatClass, hostname, port, windStore, domainFactory, - this, trackedRegattaRegistry, delayToLiveInMillis); - } - - @Override - public SailMasterTransceiver createSailMasterTransceiver() { - return new SailMasterTransceiverImpl(); - } - - @Override - public SailMasterMessage createMessage(String message) { - return new SailMasterMessageImpl(message); - } - - @Override - public Race createRace(String raceId, String description, BoatClass boatClass) { - return new RaceImpl(raceId, description, boatClass); - } - - @Override - public SwissTimingArchiveConfiguration createSwissTimingArchiveConfiguration(String jsonUrl) { - return new SwissTimingArchiveConfigurationImpl(jsonUrl); - } -} +package com.sap.sailing.domain.swisstimingadapter.impl; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.text.ParseException; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.common.impl.Util.Triple; +import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; +import com.sap.sailing.domain.swisstimingadapter.DomainFactory; +import com.sap.sailing.domain.swisstimingadapter.Race; +import com.sap.sailing.domain.swisstimingadapter.SailMasterConnector; +import com.sap.sailing.domain.swisstimingadapter.SailMasterMessage; +import com.sap.sailing.domain.swisstimingadapter.SailMasterTransceiver; +import com.sap.sailing.domain.swisstimingadapter.StartList; +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.SwissTimingMessageParser; +import com.sap.sailing.domain.swisstimingadapter.SwissTimingRaceTracker; +import com.sap.sailing.domain.tracking.RaceTracker; +import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; +import com.sap.sailing.domain.tracking.WindStore; + +public class SwissTimingFactoryImpl implements SwissTimingFactory { + private static final Logger logger = Logger.getLogger(SwissTimingFactoryImpl.class.getName()); + + private final Map, SailMasterConnector> connectors; + + public SwissTimingFactoryImpl() { + connectors = new HashMap<>(); + } + + @Override + public SwissTimingMessageParser createMessageParser() { + return new SwissTimingMessageParserImpl(); + } + + @Override + public SailMasterConnector getOrCreateSailMasterConnector(String host, int port, String raceId, String raceName, + String raceDescription, BoatClass boatClass) throws InterruptedException, ParseException { + if (Boolean.valueOf(System.getProperty("simulateLiveMode", "false"))) { + return getOrCreateSailMasterLiveSimulatorConnector(host, port, raceId, raceName, raceDescription, boatClass); + } else { + Triple key = new Triple(host, port, raceId); + SailMasterConnector result = connectors.get(key); + if (result == null || result.isStopped()) { + if (result == null) { + logger.info("Creating a new connector for "+key+" because none found"); + } else { + logger.info("Creating a new connector for "+key+" because the old one was stopped"); + } + result = new SailMasterConnectorImpl(host, port, raceId, raceName, raceDescription, boatClass); + connectors.put(key, result); + // TODO how do connectors get stopped, terminated and removed from the connectors map again? + } else { + logger.info("Re-using connector for "+key+" because it wasn't stopped"); + } + return result; + } + } + + @Override + public SailMasterConnector getOrCreateSailMasterLiveSimulatorConnector(String host, int port, String raceId, String raceName, + String raceDescription, BoatClass boatClass) throws InterruptedException, ParseException { + Triple key = new Triple<>(host, port, raceId); + SailMasterConnector result = connectors.get(key); + if (result == null) { + result = new SailMasterLiveSimulatorConnectorImpl(host, port, raceId, raceName, raceDescription, boatClass); + connectors.put(key, result); + // TODO how do connectors get stopped, terminated and removed from the connectors map again? + } else if (result.isStopped()) { + result = new SailMasterLiveSimulatorConnectorImpl(host, port, raceId, raceName, raceDescription, boatClass); + connectors.put(key, result); + } + return result; + } + + @Override + public SwissTimingConfiguration createSwissTimingConfiguration(String name, String jsonURL, String hostname, int port) { + return new SwissTimingConfigurationImpl(name, jsonURL, hostname, port); + } + + @Override + public SwissTimingRaceTracker createRaceTracker(String raceID, String raceName, String raceDescription, BoatClass boatClass, String hostname, int port, + StartList startList, long delayToLiveInMillis, RaceLogStore raceLogStore, WindStore windStore, GPSFixStore gpsFixStore, DomainFactory domainFactory, + TrackedRegattaRegistry trackedRegattaRegistry) throws InterruptedException, + UnknownHostException, IOException, ParseException { + return new SwissTimingRaceTrackerImpl(raceID, raceName, raceDescription, boatClass, hostname, port, startList, raceLogStore, windStore, gpsFixStore, domainFactory, this, + trackedRegattaRegistry, delayToLiveInMillis); + } + + @Override + public RaceTracker createRaceTracker(Regatta regatta, String raceID, String raceName, String raceDescription, BoatClass boatClass, String hostname, + int port, StartList startList, long delayToLiveInMillis, WindStore windStore, GPSFixStore gpsFixStore, DomainFactory domainFactory, + TrackedRegattaRegistry trackedRegattaRegistry) throws UnknownHostException, + InterruptedException, IOException, ParseException { + return new SwissTimingRaceTrackerImpl(regatta, raceID, raceName, raceDescription, boatClass, hostname, port, startList, windStore, gpsFixStore, domainFactory, + this, trackedRegattaRegistry, delayToLiveInMillis); + } + + @Override + public SailMasterTransceiver createSailMasterTransceiver() { + return new SailMasterTransceiverImpl(); + } + + @Override + public SailMasterMessage createMessage(String message) { + return new SailMasterMessageImpl(message); + } + + @Override + public Race createRace(String raceId, String raceName, String description, BoatClass boatClass) { + return new RaceImpl(raceId, raceName, description, boatClass); + } + + @Override + public SwissTimingArchiveConfiguration createSwissTimingArchiveConfiguration(String jsonUrl) { + return new SwissTimingArchiveConfigurationImpl(jsonUrl); + } +} diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java index f81771cfa4d..0bb7d25bdbf 100644 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java @@ -6,7 +6,9 @@ import java.net.UnknownHostException; import java.text.ParseException; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.NavigableSet; import java.util.Set; import java.util.TreeMap; @@ -15,23 +17,32 @@ import java.util.logging.Logger; import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CompetitorStore; import com.sap.sailing.domain.base.Mark; import com.sap.sailing.domain.base.RaceDefinition; import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.base.Sideline; import com.sap.sailing.domain.base.Waypoint; import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.TrackedRaceStatusEnum; +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.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; import com.sap.sailing.domain.common.impl.Util; import com.sap.sailing.domain.common.impl.Util.Triple; +import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID; import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; import com.sap.sailing.domain.swisstimingadapter.Course; import com.sap.sailing.domain.swisstimingadapter.DomainFactory; import com.sap.sailing.domain.swisstimingadapter.Fix; import com.sap.sailing.domain.swisstimingadapter.Race; import com.sap.sailing.domain.swisstimingadapter.RaceStatus; +import com.sap.sailing.domain.swisstimingadapter.RaceType; import com.sap.sailing.domain.swisstimingadapter.SailMasterConnector; import com.sap.sailing.domain.swisstimingadapter.SailMasterListener; import com.sap.sailing.domain.swisstimingadapter.StartList; @@ -49,9 +60,11 @@ import com.sap.sailing.domain.tracking.RacesHandle; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.TrackedRaceStatus; import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; +import com.sap.sailing.domain.tracking.Wind; import com.sap.sailing.domain.tracking.WindStore; import com.sap.sailing.domain.tracking.WindTrack; import com.sap.sailing.domain.tracking.impl.TrackedRaceStatusImpl; +import com.sap.sailing.domain.tracking.impl.WindImpl; import difflib.PatchFailedException; @@ -60,12 +73,15 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen private final SailMasterConnector connector; private final String raceID; + private final String raceName; private final String raceDescription; private final BoatClass boatClass; private final DomainFactory domainFactory; private final Triple id; private final Regatta regatta; private final WindStore windStore; + private final GPSFixStore gpsFixStore; + private final boolean startListFromManage2Sail; /** * Starts out as null and is set when the race definition has been created. When this happens, this object is @@ -75,6 +91,7 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen private Course course; private StartList startList; + private Map competitorsByBoatId; private DynamicTrackedRace trackedRace; private final DynamicTrackedRegatta trackedRegatta; @@ -87,33 +104,38 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen * tracker by calling {@link #receivedTimingData(String, String, List)}. */ private final TMDMessageQueue tmdMessageQueue; - - protected SwissTimingRaceTrackerImpl(String raceID, String raceDescription, BoatClass boatClass, String hostname, int port, RaceLogStore raceLogStore, - WindStore windStore, DomainFactory domainFactory, SwissTimingFactory factory, + + protected SwissTimingRaceTrackerImpl(String raceID, String raceName, String raceDescription, BoatClass boatClass, String hostname, int port, StartList startList, RaceLogStore raceLogStore, + WindStore windStore, GPSFixStore gpsFixStore, DomainFactory domainFactory, SwissTimingFactory factory, TrackedRegattaRegistry trackedRegattaRegistry, long delayToLiveInMillis) throws InterruptedException, - UnknownHostException, IOException, ParseException { - this(domainFactory.getOrCreateDefaultRegatta(raceLogStore, raceID, boatClass, trackedRegattaRegistry), raceID, - raceDescription, boatClass, hostname, port, windStore, domainFactory, factory, trackedRegattaRegistry, + UnknownHostException, IOException, ParseException { + this(domainFactory.getOrCreateDefaultRegatta(raceLogStore, raceID, boatClass, trackedRegattaRegistry), raceID, raceName, + raceDescription, boatClass, hostname, port, startList, windStore, gpsFixStore, domainFactory, factory, trackedRegattaRegistry, delayToLiveInMillis); } - - protected SwissTimingRaceTrackerImpl(Regatta regatta, String raceID, String raceDescription, BoatClass boatClass, String hostname, int port, - WindStore windStore, DomainFactory domainFactory, SwissTimingFactory factory, + + protected SwissTimingRaceTrackerImpl(Regatta regatta, String raceID, String raceName, String raceDescription, BoatClass boatClass, String hostname, int port, StartList startList, + WindStore windStore, GPSFixStore gpsFixStore, DomainFactory domainFactory, SwissTimingFactory factory, TrackedRegattaRegistry trackedRegattaRegistry, long delayToLiveInMillis) throws InterruptedException, UnknownHostException, IOException, ParseException { super(); this.tmdMessageQueue = new TMDMessageQueue(this); this.regatta = regatta; - this.connector = factory.getOrCreateSailMasterConnector(hostname, port, raceID, raceDescription, boatClass); + this.connector = factory.getOrCreateSailMasterConnector(hostname, port, raceID, raceName, raceDescription, boatClass); this.domainFactory = domainFactory; this.raceID = raceID; + this.raceName = raceName; + this.startList = startList; + this.startListFromManage2Sail = startList != null; this.raceDescription = raceDescription; this.boatClass = boatClass; this.windStore = windStore; + this.gpsFixStore = gpsFixStore; this.id = createID(raceID, hostname, port); connector.addSailMasterListener(this); trackedRegatta = trackedRegattaRegistry.getOrCreateTrackedRegatta(regatta); this.delayToLiveInMillis = delayToLiveInMillis; + this.competitorsByBoatId = new HashMap(); } @Override @@ -166,20 +188,26 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen public Set getRaces(long timeoutInMilliseconds) { long start = System.currentTimeMillis(); synchronized (this) { - RaceDefinition result = race; + RaceDefinition preResult = race; boolean interrupted = false; - while ((System.currentTimeMillis()-start < timeoutInMilliseconds) && !interrupted && result == null) { + while ((System.currentTimeMillis()-start < timeoutInMilliseconds) && !interrupted && preResult == null) { try { long timeToWait = timeoutInMilliseconds - (System.currentTimeMillis() - start); if (timeToWait > 0) { this.wait(timeToWait); } - result = race; + preResult = race; } catch (InterruptedException e) { interrupted = true; } } - return result == null ? null : Collections.singleton(result); + final Set result; + if (preResult == null) { + result = Collections.emptySet(); + } else { + result = Collections.singleton(preResult); + } + return result; } } @@ -200,6 +228,11 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen return windStore; } + @Override + public GPSFixStore getGPSFixStore() { + return gpsFixStore; + } + @Override public Regatta getRegatta() { return regatta; @@ -234,10 +267,18 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen trackedRace.recordFix(mark, gpsFix); break; case COMPETITOR: - Competitor competitor = domainFactory.getCompetitorByBoatIDAndRaceIDOrBoatClass( - fix.getBoatID(), raceID, boatClass); - DynamicGPSFixTrack competitorTrack = trackedRace.getTrack(competitor); - competitorTrack.addGPSFix(gpsFix); + String boatID = fix.getBoatID(); + Competitor competitor = getCompetitorByBoatIDAndRaceIDOrBoatClass(boatID, raceID, boatClass); + if(competitor == null) { + // TODO: read startlist again from Manage2Sail + // use competitorStore.isCompetitorToUpdateDuringGetOrCreate(result) + } + if(competitor != null) { + DynamicGPSFixTrack competitorTrack = trackedRace.getTrack(competitor); + competitorTrack.addGPSFix(gpsFix); + } else { + logger.info("Unknown competitor " + boatID + " found for race with id " + raceID); + } break; default: logger.info("Unknown tracker type " + fix.getTrackerType()); @@ -255,13 +296,38 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen } + private Competitor getCompetitorByBoatIDAndRaceType(String boatID, RaceType raceType) { + return domainFactory.getBaseDomainFactory().getExistingCompetitorById(domainFactory.getCompetitorID(boatID, raceType)); + } + + private Competitor getCompetitorByBoatIDAndBoatClass(String boatID, BoatClass boatClass) { + return domainFactory.getBaseDomainFactory().getExistingCompetitorById(domainFactory.getCompetitorID(boatID, boatClass)); + } + + @Override + public Competitor getCompetitorByBoatIDAndRaceIDOrBoatClass(String boatID, String raceID, BoatClass boatClass) { + Competitor result = null; + // first look into the temp cache + result = competitorsByBoatId.get(boatID); + if(result == null) { + if(boatClass != null) { + result = getCompetitorByBoatIDAndBoatClass(boatID, boatClass); + } else { + RaceType raceType = domainFactory.getRaceTypeFromRaceID(raceID); + if (raceType != null) { + result = getCompetitorByBoatIDAndRaceType(boatID, raceType); + } + } + } + return result; + } + @Override public void receivedTimingData(String raceID, String boatID, List> markIndicesRanksAndTimesSinceStartInMilliseconds) { assert this.raceID.equals(raceID); if (isTrackedRaceStillReachable()) { - Competitor competitor = domainFactory.getCompetitorByBoatIDAndRaceIDOrBoatClass(boatID, - raceID, boatClass); + Competitor competitor = getCompetitorByBoatIDAndRaceIDOrBoatClass(boatID, raceID, boatClass); // the list of mark indices and time stamps is partial and usually only shows the last mark passing; // we need to use this to *update* the competitor's mark passings list, not *replace* it TreeMap markPassingsByMarkIndex = new TreeMap(); @@ -281,22 +347,28 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen for (Triple markIndexRankAndTimeSinceStartInMilliseconds : markIndicesRanksAndTimesSinceStartInMilliseconds) { Waypoint waypoint = Util.get(trackedRace.getRace().getCourse().getWaypoints(), markIndexRankAndTimeSinceStartInMilliseconds.getA()); - // update mark passing only if we have a start time; guessed start times don't make sense and - // for the start line would lead subsequent calls to getStartOfRace() return that guessed start time - // which then cannot be identified as "guessed" anymore... - if (trackedRace.getStartOfRace() != null) { - final TimePoint startTime = trackedRace.getStartOfRace(); - MillisecondsTimePoint timePoint = new MillisecondsTimePoint( - startTime.asMillis() + markIndexRankAndTimeSinceStartInMilliseconds.getC()); - MarkPassing markPassing = domainFactory.createMarkPassing(timePoint, waypoint, - domainFactory.getCompetitorByBoatIDAndRaceIDOrBoatClass(boatID, raceID, boatClass)); - markPassingsByMarkIndex.put(markIndexRankAndTimeSinceStartInMilliseconds.getA(), markPassing); + // If the rank and time information is empty, we interpret this by clearing the mark rounding if any (see + // also bug 1911): + if (markIndexRankAndTimeSinceStartInMilliseconds.getC() == null) { + markPassingsByMarkIndex.remove(markIndexRankAndTimeSinceStartInMilliseconds.getA()); } else { - // - logger.warning("Received mark passing with time relative to start of race "+trackedRace.getRace().getName()+ - " before having received a race start time." - + " Queueing message for re-application when a start time has been received."); - tmdMessageQueue.enqueue(raceID, boatID, markIndicesRanksAndTimesSinceStartInMilliseconds); + // update mark passing only if we have a start time; guessed start times don't make sense and + // for the start line would lead subsequent calls to getStartOfRace() return that guessed start time + // which then cannot be identified as "guessed" anymore... + if (trackedRace.getStartOfRace() != null) { + final TimePoint startTime = trackedRace.getStartOfRace(); + MillisecondsTimePoint timePoint = new MillisecondsTimePoint(startTime.asMillis() + + markIndexRankAndTimeSinceStartInMilliseconds.getC()); + MarkPassing markPassing = domainFactory.createMarkPassing(timePoint, waypoint, + getCompetitorByBoatIDAndRaceIDOrBoatClass(boatID, raceID, boatClass)); + markPassingsByMarkIndex.put(markIndexRankAndTimeSinceStartInMilliseconds.getA(), markPassing); + } else { + // + logger.warning("Received mark passing with time relative to start of race " + + trackedRace.getRace().getName() + " before having received a race start time." + + " Queueing message for re-application when a start time has been received."); + tmdMessageQueue.enqueue(raceID, boatID, markIndicesRanksAndTimesSinceStartInMilliseconds); + } } } trackedRace.updateMarkPassings(competitor, markPassingsByMarkIndex.values()); @@ -336,7 +408,8 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen @Override public void receivedStartList(String raceID, StartList startList) { - if (this.raceID.equals(raceID)) { + // ignore STL messages if the startlist has been already provided by Manage2Sail + if (!startListFromManage2Sail && this.raceID.equals(raceID)) { StartList oldStartList = this.startList; this.startList = startList; if (oldStartList == null && course != null) { @@ -345,14 +418,40 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen } } + @Override + public void receivedWindData(String raceID, int zeroBasedMarkIndex, double windDirectionTrueDegrees, double windSpeedInKnots) { + if (this.raceID.equals(raceID)) { + DynamicTrackedRace trackedRace = getTrackedRace(); + if (trackedRace == null) { + logger.warning("Received wind data at mark " + zeroBasedMarkIndex + ": " + windDirectionTrueDegrees + "deg at " + + windSpeedInKnots + "kts but didn't find tracked race; ignoring"); + } else { + Waypoint wp = Util.get(trackedRace.getRace().getCourse().getWaypoints(), zeroBasedMarkIndex); + final TimePoint timePoint = connector.getLastRPDMessageTimePoint(); + Position waypointPosition = trackedRace.getApproximatePosition(wp, timePoint); + Wind wind = new WindImpl(waypointPosition, timePoint, new KnotSpeedWithBearingImpl(windSpeedInKnots, new DegreeBearingImpl(windDirectionTrueDegrees))); + WindSource windSource = new WindSourceWithAdditionalID(WindSourceType.RACECOMMITTEE, "@"+zeroBasedMarkIndex); + trackedRace.recordWind(wind, windSource); + } + } + } + private void createRaceDefinition(Course course) { assert this.raceID.equals(raceID); assert startList != null; assert course != null; // now we can create the RaceDefinition and most other things - Race swissTimingRace = new RaceImpl(raceID, raceDescription, boatClass); + Race swissTimingRace = new RaceImpl(raceID, raceName, raceDescription, boatClass); race = domainFactory.createRaceDefinition(regatta, swissTimingRace, startList, course); - trackedRace = getTrackedRegatta().createTrackedRace(race, Collections. emptyList(), windStore, delayToLiveInMillis, + // temp + CompetitorStore competitorStore = domainFactory.getBaseDomainFactory().getCompetitorStore(); + for (com.sap.sailing.domain.swisstimingadapter.Competitor c : startList.getCompetitors()) { + Competitor existingCompetitor = competitorStore.getExistingCompetitorByIdAsString(c.getID()); + if (existingCompetitor != null) { + competitorsByBoatId.put(c.getBoatID(), existingCompetitor); + } + } + trackedRace = getTrackedRegatta().createTrackedRace(race, Collections. emptyList(), windStore, gpsFixStore, delayToLiveInMillis, WindTrack.DEFAULT_MILLISECONDS_OVER_WHICH_TO_AVERAGE_WIND, /* time over which to average speed */ race.getBoatClass().getApproximateManeuverDurationInMilliseconds(), new DynamicRaceDefinitionSet() { @@ -404,6 +503,7 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl implemen } } + @Override public void receivedAvailableRaces(Iterable races) { // don't care diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingTrackingConnectivityParameters.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingTrackingConnectivityParameters.java index 3242641c9c1..ae5104d67c1 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingTrackingConnectivityParameters.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingTrackingConnectivityParameters.java @@ -1,62 +1,68 @@ -package com.sap.sailing.domain.swisstimingadapter.impl; - - -import com.sap.sailing.domain.base.BoatClass; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.racelog.RaceLogStore; -import com.sap.sailing.domain.swisstimingadapter.DomainFactory; -import com.sap.sailing.domain.swisstimingadapter.SwissTimingFactory; -import com.sap.sailing.domain.tracking.RaceTracker; -import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; -import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; -import com.sap.sailing.domain.tracking.WindStore; - -public class SwissTimingTrackingConnectivityParameters implements RaceTrackingConnectivityParameters { - private final String hostname; - private final int port; - private final String raceID; - private final String raceDescription; - private final BoatClass boatClass; - private final SwissTimingFactory swissTimingFactory; - private final DomainFactory domainFactory; - private final RaceLogStore raceLogStore; - private final long delayToLiveInMillis; - - public SwissTimingTrackingConnectivityParameters(String hostname, int port, String raceID, String raceDescription, BoatClass boatClass, - long delayToLiveInMillis, SwissTimingFactory swissTimingFactory, DomainFactory domainFactory, - RaceLogStore raceLogStore) { - super(); - this.hostname = hostname; - this.port = port; - this.raceID = raceID; - this.raceDescription = raceDescription; - this.boatClass = boatClass; - this.delayToLiveInMillis = delayToLiveInMillis; - this.swissTimingFactory = swissTimingFactory; - this.domainFactory = domainFactory; - this.raceLogStore = raceLogStore; - } - - @Override - public RaceTracker createRaceTracker(TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore) throws Exception { - return swissTimingFactory.createRaceTracker(raceID, raceDescription, boatClass, hostname, port, delayToLiveInMillis, raceLogStore, windStore, domainFactory, - trackedRegattaRegistry); - } - - @Override - public RaceTracker createRaceTracker(Regatta regatta, TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore) - throws Exception { - return swissTimingFactory.createRaceTracker(regatta, raceID, raceDescription, boatClass, hostname, port, delayToLiveInMillis, windStore, domainFactory, - trackedRegattaRegistry); - } - - @Override - public Object getTrackerID() { - return SwissTimingRaceTrackerImpl.createID(raceID, hostname, port); - } - - @Override - public long getDelayToLiveInMillis() { - return delayToLiveInMillis; - } -} +package com.sap.sailing.domain.swisstimingadapter.impl; + + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; +import com.sap.sailing.domain.swisstimingadapter.DomainFactory; +import com.sap.sailing.domain.swisstimingadapter.StartList; +import com.sap.sailing.domain.swisstimingadapter.SwissTimingFactory; +import com.sap.sailing.domain.tracking.RaceTracker; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; +import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; +import com.sap.sailing.domain.tracking.WindStore; + +public class SwissTimingTrackingConnectivityParameters implements RaceTrackingConnectivityParameters { + private final String hostname; + private final int port; + private final String raceID; + private final String raceName; + private final String raceDescription; + private final BoatClass boatClass; + private final SwissTimingFactory swissTimingFactory; + private final DomainFactory domainFactory; + private final RaceLogStore raceLogStore; + private final long delayToLiveInMillis; + private final StartList startList; + + public SwissTimingTrackingConnectivityParameters(String hostname, int port, String raceID, String raceName, String raceDescription, BoatClass boatClass, + StartList startList, long delayToLiveInMillis, SwissTimingFactory swissTimingFactory, DomainFactory domainFactory, + RaceLogStore raceLogStore) { + super(); + this.hostname = hostname; + this.port = port; + this.raceID = raceID; + this.raceName = raceName; + this.raceDescription = raceDescription; + this.boatClass = boatClass; + this.startList = startList; + this.delayToLiveInMillis = delayToLiveInMillis; + this.swissTimingFactory = swissTimingFactory; + this.domainFactory = domainFactory; + this.raceLogStore = raceLogStore; + } + + @Override + public RaceTracker createRaceTracker(TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore, GPSFixStore gpsFixStore) throws Exception { + return swissTimingFactory.createRaceTracker(raceID, raceName, raceDescription, boatClass, hostname, port, startList, delayToLiveInMillis, raceLogStore, windStore, gpsFixStore, domainFactory, + trackedRegattaRegistry); + } + + @Override + public RaceTracker createRaceTracker(Regatta regatta, TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore, GPSFixStore gpsFixStore) + throws Exception { + return swissTimingFactory.createRaceTracker(regatta, raceID, raceName, raceDescription, boatClass, hostname, port, startList, delayToLiveInMillis, windStore, gpsFixStore, domainFactory, + trackedRegattaRegistry); + } + + @Override + public Object getTrackerID() { + return SwissTimingRaceTrackerImpl.createID(raceID, hostname, port); + } + + @Override + public long getDelayToLiveInMillis() { + return delayToLiveInMillis; + } +} diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/TMDMessageQueue.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/TMDMessageQueue.java index fd4a5c5003f..063281dc1c6 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/TMDMessageQueue.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/TMDMessageQueue.java @@ -101,7 +101,7 @@ public class TMDMessageQueue { // otherwise, this could lead to the "guessed time" to while (i.hasNext()) { TMDMessageContents messageContents = i.next(); - Competitor competitor = raceTracker.getDomainFactory().getCompetitorByBoatIDAndRaceIDOrBoatClass(messageContents.getBoatID(), + Competitor competitor = raceTracker.getCompetitorByBoatIDAndRaceIDOrBoatClass(messageContents.getBoatID(), messageContents.getRaceID(), raceTracker.getBoatClass()); NavigableSet oldMarkPassings = trackedRace.getMarkPassings(competitor); TreeMap cleansedMarkPassingsForCompetitor = cleansedMarkPassings.get(competitor); 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 index b0195e434f7..3dd2b14e647 100755 --- 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 @@ -38,6 +38,7 @@ import com.sap.sailing.domain.common.impl.Util; import com.sap.sailing.domain.common.impl.Util.Pair; import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID; import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; +import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; import com.sap.sailing.domain.swisstimingadapter.DomainFactory; import com.sap.sailing.domain.swisstimingreplayadapter.CompetitorStatus; import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayListener; @@ -231,7 +232,7 @@ public class SwissTimingReplayToDomainAdapter extends SwissTimingReplayAdapter { CompetitorStatus competitorStatus, BoatType boatType, short cRank_Bracket, short cnPoints_x10_Bracket, short ctPoints_x10_Winner) { if (boatType == BoatType.Competitor) { - Competitor competitor = domainFactory.getOrCreateCompetitor(sailNumberOrTrackerID, threeLetterIOCCode.trim(), name.trim(), + Competitor competitor = domainFactory.createCompetitorWithoutID(sailNumberOrTrackerID, threeLetterIOCCode.trim(), name.trim(), currentRaceID, null /* boat class */); Set competitorsOfCurrentRace = competitorsPerRaceID.get(currentRaceID); if (competitorsOfCurrentRace == null) { @@ -310,7 +311,7 @@ public class SwissTimingReplayToDomainAdapter extends SwissTimingReplayAdapter { currentRaceID, competitorsPerRaceID.get(currentRaceID), currentCourseDefinition); racePerRaceID.put(currentRaceID, race); DynamicTrackedRace trackedRace = trackedRegattaRegistry.getOrCreateTrackedRegatta(myRegatta). - createTrackedRace(race, Collections. emptyList(), EmptyWindStore.INSTANCE, TrackedRace.DEFAULT_LIVE_DELAY_IN_MILLISECONDS, + createTrackedRace(race, Collections. emptyList(), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE,TrackedRace.DEFAULT_LIVE_DELAY_IN_MILLISECONDS, WindTrack.DEFAULT_MILLISECONDS_OVER_WHICH_TO_AVERAGE_WIND, /* time over which to average speed: */ race.getBoatClass().getApproximateManeuverDurationInMilliseconds(), /* raceDefinitionSetToUpdate */ null); diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/TimePointTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/TimePointTest.java new file mode 100644 index 00000000000..345f922c499 --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/TimePointTest.java @@ -0,0 +1,17 @@ +package com.sap.sailing.domain.common.test; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; + +public class TimePointTest { + @Test + public void compare() { + TimePoint one = new MillisecondsTimePoint(Long.MIN_VALUE); + TimePoint two = new MillisecondsTimePoint(Long.MAX_VALUE); + assertTrue(one.before(two)); + } +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/TimeRangeTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/TimeRangeTest.java new file mode 100644 index 00000000000..d28decf1b2c --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/common/test/TimeRangeTest.java @@ -0,0 +1,132 @@ +package com.sap.sailing.domain.common.test; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNull; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import junit.framework.Assert; + +import org.junit.Test; + +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.TimeRange; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.impl.TimeRangeImpl; + +public class TimeRangeTest { + protected TimeRange create(long from, long to) { + return new TimeRangeImpl(new MillisecondsTimePoint(from), new MillisecondsTimePoint(to)); + } + + protected TimePoint create(long millis) { + return new MillisecondsTimePoint(millis); + } + + private void liesWithin(long fromOuter, long toOuter, long fromInner, long toInner) { + TimeRange outer = create(fromOuter, toOuter); + TimeRange inner = create(fromInner, toInner); + + Assert.assertTrue(inner.liesWithin(outer)); + Assert.assertTrue(outer.includes(inner)); + Assert.assertFalse(inner.includes(outer)); + Assert.assertFalse(outer.liesWithin(inner)); + } + + @Test + public void liesWithin() { + liesWithin(0, 100, 10, 90); + liesWithin(0, 100, 0, 90); + liesWithin(0, 100, 10, 100); + } + + @Test + public void includesTimePoint() { + TimeRange range = create(0, 100); + Assert.assertTrue(range.includes(create(0))); + Assert.assertTrue(range.includes(create(1))); + Assert.assertTrue(range.includes(create(100))); + Assert.assertFalse(range.includes(create(101))); + } + + @Test + public void intersects() { + TimeRange one = create(0, 50); + TimeRange two = create(20, 70); + + Assert.assertFalse(one.liesWithin(two)); + Assert.assertFalse(two.liesWithin(one)); + Assert.assertTrue(one.intersects(two)); + Assert.assertTrue(two.intersects(one)); + } + + @Test + public void openRanges() { + TimeRange one = new TimeRangeImpl(new MillisecondsTimePoint(0), null); + TimeRange two = create(5, 10); + TimeRange three = new TimeRangeImpl(null, new MillisecondsTimePoint(15)); + + assertTrue(two.liesWithin(one)); + assertTrue(two.liesWithin(three)); + assertTrue(one.intersects(three)); + assertTrue(one.endsAfter(three)); + assertFalse(one.startsBefore(three)); + } + + @Test + public void union() { + TimeRange one = create(5, 10); + TimeRange two = create(7, 12); + + TimeRange union = one.union(two); + assertEquals(5, union.from().asMillis()); + assertEquals(12, union.to().asMillis()); + + two = create(11, 16); + assertNull(one.union(two)); + + two = create(7, Long.MAX_VALUE); + union = two.union(one); + TimeRange union2 = one.union(two); + assertEquals(5, union.from().asMillis()); + assertEquals(5, union2.from().asMillis()); + assertEquals(Long.MAX_VALUE, union.to().asMillis()); + assertEquals(Long.MAX_VALUE, union2.to().asMillis()); + + one = create(Long.MIN_VALUE, 10); + union = two.union(one); + assertTrue(union.openBeginning()); + assertTrue(union.openEnd()); + } + + @Test + public void intersection() { + TimeRange one = create(5, 10); + TimeRange two = create(7, 12); + + TimeRange intersection = one.intersection(two); + assertEquals(7, intersection.from().asMillis()); + assertEquals(10, intersection.to().asMillis()); + + two = create(11, 16); + assertNull(one.intersection(two)); + + two = create(7, Long.MAX_VALUE); + intersection = two.intersection(one); + assertEquals(7, intersection.from().asMillis()); + assertEquals(10, intersection.to().asMillis()); + + one = create(0, Long.MAX_VALUE); + two = create(Long.MIN_VALUE, 10); + intersection = two.intersection(one); + assertEquals(0, intersection.from().asMillis()); + assertEquals(10, intersection.to().asMillis()); + } + + @Test + public void timeDifferenceTest() { + TimeRange one = create(5, 10); + assertEquals(2, one.timeDifference(new MillisecondsTimePoint(3)).asMillis()); + assertEquals(0, one.timeDifference(new MillisecondsTimePoint(7)).asMillis()); + assertEquals(2, one.timeDifference(new MillisecondsTimePoint(12)).asMillis()); + } +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/BearingTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/BearingTest.java index 396a14828b3..06c3d47cdbd 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/BearingTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/BearingTest.java @@ -12,12 +12,12 @@ import org.junit.Test; import com.sap.sailing.domain.base.BearingWithConfidence; import com.sap.sailing.domain.base.impl.BearingWithConfidenceImpl; import com.sap.sailing.domain.base.impl.PositionWithConfidenceImpl; -import com.sap.sailing.domain.base.impl.ScalablePosition; import com.sap.sailing.domain.common.Bearing; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.impl.DegreeBearingImpl; import com.sap.sailing.domain.common.impl.DegreePosition; import com.sap.sailing.domain.common.impl.RadianBearingImpl; +import com.sap.sailing.domain.common.scalablevalue.impl.ScalablePosition; import com.sap.sailing.domain.confidence.ConfidenceBasedAverager; import com.sap.sailing.domain.confidence.ConfidenceFactory; import com.sap.sailing.domain.confidence.HasConfidence; diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CompetitorListenerTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CompetitorListenerTest.java new file mode 100755 index 00000000000..08741bc72b1 --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CompetitorListenerTest.java @@ -0,0 +1,197 @@ +package com.sap.sailing.domain.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.base.CompetitorChangeListener; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Nationality; +import com.sap.sailing.domain.base.Team; +import com.sap.sailing.domain.base.WithNationality; +import com.sap.sailing.domain.base.impl.DomainFactoryImpl; +import com.sap.sailing.domain.base.impl.DynamicCompetitor; +import com.sap.sailing.domain.common.Color; +import com.sap.sailing.domain.common.impl.RGBColor; + +/** + * Tests the {@link CompetitorChangeListener} stuff including the serialization of the transitive listener + * pattern on the {@link Team} structure. + * + * @author Axel Uhl (D043530) + * + */ +public class CompetitorListenerTest extends AbstractSerializationTest { + private CompetitorChangeListener listener; + private boolean nationalityChanged; + private Object whatChangedNationality; + private Object oldNationality; + private Object newNationality; + private boolean sailIdChanged; + private String oldSailId; + private String newSailId; + private boolean nameChanged; + private String oldName; + private String newName; + private boolean colorChanged; + private Color oldColor; + private Color newColor; + private DynamicCompetitor competitor; + private DomainFactoryImpl baseDomainFactory; + + @Before + public void setUp() { + baseDomainFactory = new DomainFactoryImpl(); + competitor = TrackBasedTest.createCompetitor("Hasso"); + nationalityChanged = false; + whatChangedNationality = null; + oldNationality = null; + newNationality = null; + sailIdChanged = false; + oldSailId = null; + newSailId = null; + nameChanged = false; + oldName = null; + newName = null; + colorChanged = false; + oldColor = null; + newColor = null; + listener = new CompetitorChangeListener() { + private static final long serialVersionUID = 4581029778988240209L; + + @Override + public void nationalityChanged(WithNationality what, Nationality oldNationality, Nationality newNationality) { + nationalityChanged = true; + whatChangedNationality = what; + CompetitorListenerTest.this.oldNationality = oldNationality; + CompetitorListenerTest.this.newNationality = newNationality; + } + + @Override + public void sailIdChanged(String oldSailId, String newSailId) { + sailIdChanged = true; + CompetitorListenerTest.this.oldSailId = oldSailId; + CompetitorListenerTest.this.newSailId = newSailId; + } + + @Override + public void nameChanged(String oldName, String newName) { + nameChanged = true; + CompetitorListenerTest.this.oldName = oldName; + CompetitorListenerTest.this.newName = newName; + } + + @Override + public void colorChanged(Color oldColor, Color newColor) { + colorChanged = true; + CompetitorListenerTest.this.oldColor = oldColor; + CompetitorListenerTest.this.newColor = newColor; + } + }; + competitor.addCompetitorChangeListener(listener); + } + + @Test + public void testSimpleCompetitorListenerPatternForName() { + competitor.setName("Dr. Hasso Plattner"); + assertTrue(nameChanged); + assertEquals("Hasso", oldName); + assertEquals("Dr. Hasso Plattner", newName); + assertFalse(sailIdChanged); + assertFalse(colorChanged); + assertFalse(nationalityChanged); + } + + @Test + public void testSimpleCompetitorListenerPatternForSailId() { + final String myOldSailId = competitor.getBoat().getSailID(); + competitor.getBoat().setSailId("POR 40"); + assertFalse(nameChanged); + assertTrue(sailIdChanged); + assertEquals(myOldSailId, oldSailId); + assertEquals("POR 40", newSailId); + assertFalse(colorChanged); + assertFalse(nationalityChanged); + } + + @Test + public void testSimpleCompetitorListenerPatternForColor() { + final Color myOldColor = competitor.getColor(); + final RGBColor myNewColor = new RGBColor(123, 12, 234); + competitor.setColor(myNewColor); + assertFalse(nameChanged); + assertFalse(sailIdChanged); + assertTrue(colorChanged); + assertEquals(myOldColor, oldColor); + assertEquals(myNewColor, newColor); + assertFalse(nationalityChanged); + } + + @Test + public void testSimpleCompetitorListenerPatternForNationality() { + final Nationality myOldNationality = competitor.getTeam().getNationality(); + final Nationality myNewNationality = DomainFactory.INSTANCE.getOrCreateNationality("POR"); + competitor.getTeam().setNationality(myNewNationality); + assertFalse(nameChanged); + assertFalse(sailIdChanged); + assertFalse(colorChanged); + assertTrue(nationalityChanged); + assertEquals(myOldNationality, oldNationality); + assertEquals(myNewNationality, newNationality); + assertSame(competitor.getTeam(), whatChangedNationality); + } + + @Test + public void testCompetitorSerializationLosesAllExternalListeners() throws ClassNotFoundException, IOException { + DomainFactory baseDomainFactory = new DomainFactoryImpl(); + DynamicCompetitor clonedCompetitor = cloneBySerialization(competitor, baseDomainFactory); + clonedCompetitor.setName("Dr. Hasso Plattner"); + clonedCompetitor.getBoat().setSailId("POR 40"); + final RGBColor myNewColor = new RGBColor(123, 12, 234); + clonedCompetitor.setColor(myNewColor); + final Nationality myNewNationality = baseDomainFactory.getOrCreateNationality("POR"); + clonedCompetitor.getTeam().setNationality(myNewNationality); + assertFalse(nameChanged); + assertFalse(sailIdChanged); + assertFalse(colorChanged); + assertFalse(nationalityChanged); + } + + @Test + public void testCompetitorSerializationListenerPatternForNameOnSerializedCompetitor() throws ClassNotFoundException, IOException { + DynamicCompetitor clonedCompetitor = cloneBySerialization(competitor, baseDomainFactory); + competitor = clonedCompetitor; + competitor.addCompetitorChangeListener(listener); + testSimpleCompetitorListenerPatternForName(); + } + + @Test + public void testCompetitorSerializationListenerPatternForColorOnSerializedCompetitor() throws ClassNotFoundException, IOException { + DynamicCompetitor clonedCompetitor = cloneBySerialization(competitor, baseDomainFactory); + competitor = clonedCompetitor; + competitor.addCompetitorChangeListener(listener); + testSimpleCompetitorListenerPatternForColor(); + } + + @Test + public void testCompetitorSerializationListenerPatternForSailIdOnSerializedCompetitor() throws ClassNotFoundException, IOException { + DynamicCompetitor clonedCompetitor = cloneBySerialization(competitor, baseDomainFactory); + competitor = clonedCompetitor; + competitor.addCompetitorChangeListener(listener); + testSimpleCompetitorListenerPatternForSailId(); + } + + @Test + public void testCompetitorSerializationListenerPatternForNationalityOnSerializedCompetitor() throws ClassNotFoundException, IOException { + DynamicCompetitor clonedCompetitor = cloneBySerialization(competitor, baseDomainFactory); + competitor = clonedCompetitor; + competitor.addCompetitorChangeListener(listener); + testSimpleCompetitorListenerPatternForNationality(); + } +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ConcurrentWeakHashMapTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ConcurrentWeakHashMapTest.java new file mode 100755 index 00000000000..b9f0cdb1937 --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ConcurrentWeakHashMapTest.java @@ -0,0 +1,133 @@ +package com.sap.sailing.domain.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.Set; + +import org.junit.Test; + +import com.sap.sailing.util.impl.ConcurrentWeakHashMap; + +/** + * Tests the class {@link ConcurrentWeakHashMap}. + * + * @author Axel Uhl (D043530) + * + */ +public class ConcurrentWeakHashMapTest { + @Test + public void testPutAndGet() { + ConcurrentWeakHashMap m = new ConcurrentWeakHashMap<>(); + m.put("a", "b"); + assertEquals("b", m.get("a")); + } + + @Test + public void testPutAndIterate() { + ConcurrentWeakHashMap m = new ConcurrentWeakHashMap<>(); + m.put("a", "b"); + assertEquals("b", m.values().iterator().next()); + assertEquals("a", m.keySet().iterator().next()); + assertEquals("a", m.entrySet().iterator().next().getKey()); + } + + @Test + public void testPutAndPutAgain() { + ConcurrentWeakHashMap m = new ConcurrentWeakHashMap<>(); + Integer one1 = new Integer(1); + Integer one2 = new Integer(1); + assertNotSame(one1, one2); + m.put(one1, "a"); + String replaced = m.put(one2, "b"); + assertEquals("a", replaced); + assertEquals(1, m.size()); + assertEquals(m.keySet().iterator().next(), one2); + } + + @Test + public void testRemoveWithSame() { + ConcurrentWeakHashMap m = new ConcurrentWeakHashMap<>(); + Integer one = new Integer(1); + m.put(one, "a"); + m.remove(one); + assertTrue(m.isEmpty()); + } + + @Test + public void testRemoveImpactOnKeySetAndEntrySet() { + ConcurrentWeakHashMap m = new ConcurrentWeakHashMap<>(); + Integer one = new Integer(1); + m.put(one, "a"); + Set keySet = m.keySet(); + Set> entrySet = m.entrySet(); + assertEquals(1, keySet.size()); + assertEquals(1, entrySet.size()); + m.remove(one); + assertTrue(m.isEmpty()); + assertTrue(keySet.isEmpty()); + assertTrue(entrySet.isEmpty()); + } + + @Test + public void testRemoveWithEqual() { + ConcurrentWeakHashMap m = new ConcurrentWeakHashMap<>(); + Integer one1 = new Integer(1); + Integer one2 = new Integer(1); + assertNotSame(one1, one2); + m.put(one1, "a"); + m.remove(one2); + assertTrue(m.isEmpty()); + } + + @Test + public void removeThrougKeySet() { + ConcurrentWeakHashMap m = new ConcurrentWeakHashMap<>(); + Integer one1 = new Integer(1); + m.put(one1, "a"); + final Set keySet = m.keySet(); + Iterator keySetIter = keySet.iterator(); + keySetIter.next(); + keySetIter.remove(); + assertTrue(m.isEmpty()); + assertTrue(keySet.isEmpty()); + assertTrue(m.entrySet().isEmpty()); + } + + @Test + public void testPutAndGetWithSubsequentGC() { + Object key = createKeyObject(); + ConcurrentWeakHashMap m = new ConcurrentWeakHashMap<>(); + m.put(key, "b"); + assertEquals("b", m.get(key)); + key = null; + forceEnqueuableWeakReferencesToBeEnqueued(); + assertTrue(m.isEmpty()); + } + + private void forceEnqueuableWeakReferencesToBeEnqueued() { + System.gc(); + try { + long[] hugeArrayThatWontGetAllocated = new long[Integer.MAX_VALUE]; + assertTrue(hugeArrayThatWontGetAllocated.length > 0); + } catch (OutOfMemoryError e) { + // Ignore OME + } + try { + Thread.sleep(50); // give GC some time to finish enqueuing and finalizing all weakly-referenced objects + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + /** + * Use a separate method for this, assuming that the compiler won't put the object on the using method's stack + * @return + */ + private Object createKeyObject() { + return new Object(); + } +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ConfidenceTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ConfidenceTest.java index 4fef5c8f3b4..a9936dcb80b 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ConfidenceTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ConfidenceTest.java @@ -15,7 +15,6 @@ import com.sap.sailing.domain.base.BearingWithConfidence; import com.sap.sailing.domain.base.PositionWithConfidence; import com.sap.sailing.domain.base.impl.BearingWithConfidenceImpl; import com.sap.sailing.domain.base.impl.PositionWithConfidenceImpl; -import com.sap.sailing.domain.base.impl.ScalablePosition; import com.sap.sailing.domain.common.Bearing; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.TimePoint; @@ -24,12 +23,13 @@ import com.sap.sailing.domain.common.impl.DegreePosition; import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; import com.sap.sailing.domain.common.impl.RadianBearingImpl; +import com.sap.sailing.domain.common.scalablevalue.ScalableValue; +import com.sap.sailing.domain.common.scalablevalue.impl.ScalablePosition; import com.sap.sailing.domain.confidence.ConfidenceBasedAverager; import com.sap.sailing.domain.confidence.ConfidenceBasedWindAverager; import com.sap.sailing.domain.confidence.ConfidenceFactory; import com.sap.sailing.domain.confidence.HasConfidence; import com.sap.sailing.domain.confidence.HasConfidenceAndIsScalable; -import com.sap.sailing.domain.confidence.ScalableValue; import com.sap.sailing.domain.confidence.Weigher; import com.sap.sailing.domain.confidence.impl.ScalableDoubleWithConfidence; import com.sap.sailing.domain.tracking.Wind; diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CourseTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CourseTest.java index 2f7f059076f..7589ca46242 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CourseTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CourseTest.java @@ -32,6 +32,7 @@ import com.sap.sailing.domain.base.impl.WaypointImpl; import com.sap.sailing.domain.common.PassingInstruction; import com.sap.sailing.domain.common.impl.Util; import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.TrackedLeg; import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl; @@ -252,7 +253,7 @@ public class CourseTest { DynamicTrackedRace trackedRace = new DynamicTrackedRaceImpl(/* trackedRegatta */new TrackedRegattaImpl( new RegattaImpl("test", null, new HashSet(), false, null, "test", null)), new RaceDefinitionImpl("Test Race", course, new BoatClassImpl("49er", /* upwind start */true), hasso), - Collections. emptyList(), EmptyWindStore.INSTANCE, /* delayToLiveInMillis */3000, + Collections. emptyList(), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE,/* delayToLiveInMillis */3000, /* millisecondsOverWhichToAverageWind */30000, /* millisecondsOverWhichToAverageSpeed */8000); assertLegStructure(course, trackedRace); @@ -283,7 +284,7 @@ public class CourseTest { new RegattaImpl("test", null, new HashSet(), false, null, "test", null)), new RaceDefinitionImpl("Test Race", course, new BoatClassImpl("49er", /* upwind start */ true), hasso), Collections. emptyList(), - EmptyWindStore.INSTANCE, /* delayToLiveInMillis */ 3000, + EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 3000, /* millisecondsOverWhichToAverageWind */ 30000, /* millisecondsOverWhichToAverageSpeed */ 8000); assertLegStructure(course, trackedRace); diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardOfflineTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardOfflineTest.java index 4df7ca3ee3b..dc4dfba57e4 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardOfflineTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardOfflineTest.java @@ -148,17 +148,17 @@ public class LeaderboardOfflineTest extends AbstractLeaderboardTest { InterruptedException { } }; - LeaderboardDTO leaderboardDTO = leaderboard.getLeaderboardDTO(now, emptySet, trackedRegattaRegistry, DomainFactory.INSTANCE); + LeaderboardDTO leaderboardDTO = leaderboard.getLeaderboardDTO(now, emptySet, /* addOverallDetails */ true, trackedRegattaRegistry, DomainFactory.INSTANCE); assertNotNull(leaderboardDTO); - assertSame(leaderboardDTO, leaderboard.getLeaderboardDTO(now, emptySet, trackedRegattaRegistry, DomainFactory.INSTANCE)); // assert it's cached + assertSame(leaderboardDTO, leaderboard.getLeaderboardDTO(now, emptySet, /* addOverallDetails */ true, trackedRegattaRegistry, DomainFactory.INSTANCE)); // assert it's cached leaderboard.getRaceColumnByName(columnName).releaseTrackedRace(defaultFleet); // this should clear the cache - LeaderboardDTO leaderboardDTO2 = leaderboard.getLeaderboardDTO(now, emptySet, trackedRegattaRegistry, DomainFactory.INSTANCE); + LeaderboardDTO leaderboardDTO2 = leaderboard.getLeaderboardDTO(now, emptySet, /* addOverallDetails */ true, trackedRegattaRegistry, DomainFactory.INSTANCE); assertNotSame(leaderboardDTO, leaderboardDTO2); - assertSame(leaderboardDTO2, leaderboard.getLeaderboardDTO(now, emptySet, trackedRegattaRegistry, DomainFactory.INSTANCE)); // and cached again + assertSame(leaderboardDTO2, leaderboard.getLeaderboardDTO(now, emptySet, /* addOverallDetails */ true, trackedRegattaRegistry, DomainFactory.INSTANCE)); // and cached again leaderboard.getRaceColumnByName(columnName).setTrackedRace(defaultFleet, testRaces.iterator().next()); // clear cache again; requires listener(s) to still be attached - LeaderboardDTO leaderboardDTO3 = leaderboard.getLeaderboardDTO(now, emptySet, trackedRegattaRegistry, DomainFactory.INSTANCE); + LeaderboardDTO leaderboardDTO3 = leaderboard.getLeaderboardDTO(now, emptySet, /* addOverallDetails */ true, trackedRegattaRegistry, DomainFactory.INSTANCE); assertNotSame(leaderboardDTO2, leaderboardDTO3); - assertSame(leaderboardDTO3, leaderboard.getLeaderboardDTO(now, emptySet, trackedRegattaRegistry, DomainFactory.INSTANCE)); // and cached again + assertSame(leaderboardDTO3, leaderboard.getLeaderboardDTO(now, emptySet, /* addOverallDetails */ true, trackedRegattaRegistry, DomainFactory.INSTANCE)); // and cached again } @Test diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java index bc3427869e9..dc77cb414fb 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java @@ -32,6 +32,7 @@ import com.sap.sailing.domain.base.impl.MarkImpl; import com.sap.sailing.domain.base.impl.RegattaImpl; import com.sap.sailing.domain.base.impl.SeriesImpl; import com.sap.sailing.domain.base.impl.WaypointImpl; +import com.sap.sailing.domain.common.Duration; import com.sap.sailing.domain.common.MaxPointsReason; import com.sap.sailing.domain.common.NoWindException; import com.sap.sailing.domain.common.ScoringSchemeType; @@ -600,16 +601,16 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest { lastMarkPassingTimesForCompetitors[2].put(c[2], finish); createAndAttachTrackedRacesWithStartTimeAndLastMarkPassingTimes(series.get(1), "Default", new Competitor[][] { f1, f2, f3 }, new TimePoint[] { earlier, now, later }, lastMarkPassingTimesForCompetitors); - long totalTimeSailedC0_InRace1 = leaderboard.getTotalTimeSailedInMilliseconds(c[0], earlier.plus(1000)); - assertEquals(1000l, totalTimeSailedC0_InRace1); - long totalTimeSailedC0_InRace2 = leaderboard.getTotalTimeSailedInMilliseconds(c[0], now.plus(1000)); - assertEquals(now.asMillis()-earlier.asMillis() + 1000, totalTimeSailedC0_InRace2); - long totalTimeSailedC0_InRace3 = leaderboard.getTotalTimeSailedInMilliseconds(c[0], later.plus(1000)); - assertEquals(later.asMillis()-earlier.asMillis() + 1000, totalTimeSailedC0_InRace3); - long totalTimeSailedC0_AtEndOfRace3 = leaderboard.getTotalTimeSailedInMilliseconds(c[0], finish); - assertEquals(finish.asMillis()-earlier.asMillis(), totalTimeSailedC0_AtEndOfRace3); - long totalTimeSailedC0_AfterRace3 = leaderboard.getTotalTimeSailedInMilliseconds(c[0], finish.plus(1000)); - assertEquals(finish.asMillis()-earlier.asMillis(), totalTimeSailedC0_AfterRace3); + Duration totalTimeSailedC0_InRace1 = leaderboard.getTotalTimeSailed(c[0], earlier.plus(1000)); + assertEquals(1000l, totalTimeSailedC0_InRace1.asMillis()); + Duration totalTimeSailedC0_InRace2 = leaderboard.getTotalTimeSailed(c[0], now.plus(1000)); + assertEquals(now.asMillis()-earlier.asMillis() + 1000, totalTimeSailedC0_InRace2.asMillis()); + Duration totalTimeSailedC0_InRace3 = leaderboard.getTotalTimeSailed(c[0], later.plus(1000)); + assertEquals(later.asMillis()-earlier.asMillis() + 1000, totalTimeSailedC0_InRace3.asMillis()); + Duration totalTimeSailedC0_AtEndOfRace3 = leaderboard.getTotalTimeSailed(c[0], finish); + assertEquals(finish.asMillis()-earlier.asMillis(), totalTimeSailedC0_AtEndOfRace3.asMillis()); + Duration totalTimeSailedC0_AfterRace3 = leaderboard.getTotalTimeSailed(c[0], finish.plus(1000)); + assertEquals(finish.asMillis()-earlier.asMillis(), totalTimeSailedC0_AfterRace3.asMillis()); } @Test diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/MultipleClassesInRegattaTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/MultipleClassesInRegattaTest.java index 74096a8454c..149bc97de85 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/MultipleClassesInRegattaTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/MultipleClassesInRegattaTest.java @@ -15,8 +15,8 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; - import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; +import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; import com.sap.sailing.domain.tracking.impl.EmptyWindStore; import com.sap.sailing.domain.tractracadapter.DomainFactory; import com.sap.sailing.domain.tractracadapter.TracTracConnectionConstants; @@ -55,25 +55,25 @@ public class MultipleClassesInRegattaTest { httpAndHost+"/events/event_20110505_SailingTea/clientparams.php?event=event_20110505_SailingTea&race=cce678c8-97e6-11e0-9aed-406186cbf87c"), new URI(liveURI), new URI(storedURI), new URI(courseDesignUpdateURI), /* startOfTracking */ null, /* endOfTracking */ null, /* delayToLiveInMillis */ 0l, /* simulateWithStartTimeNow */ false, - EmptyRaceLogStore.INSTANCE, EmptyWindStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, new DummyTrackedRegattaRegistry()); + EmptyRaceLogStore.INSTANCE, EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, new DummyTrackedRegattaRegistry()); kiwotest2 = domainFactory .createRaceTracker( new URL( httpAndHost+"/events/event_20110505_SailingTea/clientparams.php?event=event_20110505_SailingTea&race=11290bd6-97e7-11e0-9aed-406186cbf87c"), new URI(liveURI), new URI(storedURI), new URI(courseDesignUpdateURI), - /* startOfTracking */ null, /* endOfTracking */ null, /* delayToLiveInMillis */ 0l, /* simulateWithStartTimeNow */ false, EmptyRaceLogStore.INSTANCE, EmptyWindStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, new DummyTrackedRegattaRegistry()); + /* startOfTracking */ null, /* endOfTracking */ null, /* delayToLiveInMillis */ 0l, /* simulateWithStartTimeNow */ false, EmptyRaceLogStore.INSTANCE, EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, new DummyTrackedRegattaRegistry()); kiwotest3 = domainFactory .createRaceTracker( new URL( httpAndHost+"/events/event_20110505_SailingTea/clientparams.php?event=event_20110505_SailingTea&race=39635b24-97e7-11e0-9aed-406186cbf87c"), new URI(liveURI), new URI(storedURI), new URI(courseDesignUpdateURI), - /* startOfTracking */ null, /* endOfTracking */ null, /* delayToLiveInMillis */ 0l, /* simulateWithStartTimeNow */ false, EmptyRaceLogStore.INSTANCE, EmptyWindStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, new DummyTrackedRegattaRegistry()); + /* startOfTracking */ null, /* endOfTracking */ null, /* delayToLiveInMillis */ 0l, /* simulateWithStartTimeNow */ false, EmptyRaceLogStore.INSTANCE, EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, new DummyTrackedRegattaRegistry()); weym470may112014_2 = domainFactory .createRaceTracker( new URL( httpAndHost+"/events/event_20110505_SailingTea/clientparams.php?event=event_20110505_SailingTea&race=04498426-7dfd-11e0-8236-406186cbf87c"), new URI(liveURI), new URI(storedURI), new URI(courseDesignUpdateURI), - /* startOfTracking */ null, /* endOfTracking */ null, /* delayToLiveInMillis */ 0l, /* simulateWithStartTimeNow */ false, EmptyRaceLogStore.INSTANCE, EmptyWindStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, new DummyTrackedRegattaRegistry()); + /* startOfTracking */ null, /* endOfTracking */ null, /* delayToLiveInMillis */ 0l, /* simulateWithStartTimeNow */ false, EmptyRaceLogStore.INSTANCE, EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, new DummyTrackedRegattaRegistry()); assertEquals("STG", kiwotest1.getRegatta().getBoatClass().getName()); assertEquals("505", kiwotest2.getRegatta().getBoatClass().getName()); diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ReachingLegTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ReachingLegTest.java index c6f20d197ea..6c7ebc395c4 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ReachingLegTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/ReachingLegTest.java @@ -23,8 +23,8 @@ import com.sap.sailing.domain.base.Sideline; import com.sap.sailing.domain.base.Waypoint; import com.sap.sailing.domain.base.impl.BoatClassImpl; import com.sap.sailing.domain.base.impl.CompetitorImpl; -import com.sap.sailing.domain.base.impl.CourseImpl; import com.sap.sailing.domain.base.impl.ControlPointWithTwoMarksImpl; +import com.sap.sailing.domain.base.impl.CourseImpl; import com.sap.sailing.domain.base.impl.MarkImpl; import com.sap.sailing.domain.base.impl.RaceDefinitionImpl; import com.sap.sailing.domain.base.impl.RegattaImpl; @@ -43,6 +43,7 @@ import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; import com.sap.sailing.domain.common.impl.WindSourceImpl; import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; +import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.MarkPassing; import com.sap.sailing.domain.tracking.TrackedLeg; @@ -102,7 +103,7 @@ public class ReachingLegTest extends TrackBasedTest { Course course = new CourseImpl(raceName, waypoints); RaceDefinition race = new RaceDefinitionImpl(raceName, course, boatClass, competitors); DynamicTrackedRace trackedRace = new DynamicTrackedRaceImpl(trackedRegatta, race, Collections. emptyList(), EmptyWindStore.INSTANCE, - /* delayToLiveInMillis */ 0, + EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 0, /* millisecondsOverWhichToAverageWind */ 30000, /* millisecondsOverWhichToAverageSpeed */ 30000, /* delay for wind estimation cache invalidation */ 0); // in this simplified artificial course, the top mark is exactly north of the right leeward gate, the offset diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/SerializationTestWithOnlineRace.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/SerializationWithOnlineRaceTest.java similarity index 94% rename from java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/SerializationTestWithOnlineRace.java rename to java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/SerializationWithOnlineRaceTest.java index dca893da376..a5d42cb45c4 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/SerializationTestWithOnlineRace.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/SerializationWithOnlineRaceTest.java @@ -32,8 +32,8 @@ import com.sap.sailing.domain.tracking.GPSFixMoving; import com.sap.sailing.domain.tracking.impl.WindImpl; import com.sap.sailing.domain.tractracadapter.ReceiverType; -public class SerializationTestWithOnlineRace extends OnlineTracTracBasedTest { - public SerializationTestWithOnlineRace() throws MalformedURLException, URISyntaxException { +public class SerializationWithOnlineRaceTest extends OnlineTracTracBasedTest { + public SerializationWithOnlineRaceTest() throws MalformedURLException, URISyntaxException { super(); } diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/StarbordSideOfStartLineRecognitionTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/StarbordSideOfStartLineRecognitionTest.java index 93eb7995f62..101ebaedbb4 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/StarbordSideOfStartLineRecognitionTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/StarbordSideOfStartLineRecognitionTest.java @@ -86,7 +86,7 @@ public class StarbordSideOfStartLineRecognitionTest { private static final long serialVersionUID = -8007932232555073829L; public MockedTrackedRaceImpl() { - super(null, null, Collections. emptyList(), null, 0, 0, 0, 0); + super(null, null, Collections. emptyList(), null, null, 0, 0, 0, 0); } @Override diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TimeInLegProgressesTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TimeInLegProgressesTest.java new file mode 100755 index 00000000000..29d8066a0c4 --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TimeInLegProgressesTest.java @@ -0,0 +1,56 @@ +package com.sap.sailing.domain.test; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.text.ParseException; +import java.util.GregorianCalendar; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.WindSourceType; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.impl.WindSourceImpl; +import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor; +import com.sap.sailing.domain.tracking.impl.WindImpl; +import com.sap.sailing.domain.tractracadapter.ReceiverType; + +public class TimeInLegProgressesTest extends OnlineTracTracBasedTest { + public TimeInLegProgressesTest() throws MalformedURLException, URISyntaxException { + super(); + } + + @Before + public void setUp() throws URISyntaxException, IOException, InterruptedException, ParseException { + super.setUp(); + URI storedUri = new URI("file:///"+new File("resources/event_20110609_KielerWoch-505_Race_2.mtb").getCanonicalPath().replace('\\', '/')); + super.setUp(new URL("file:///"+new File("resources/event_20110609_KielerWoch-505_Race_2.txt").getCanonicalPath()), + /* liveUri */ null, /* storedUri */ storedUri, + new ReceiverType[] { ReceiverType.MARKPASSINGS, ReceiverType.RACECOURSE, ReceiverType.RAWPOSITIONS }); + OnlineTracTracBasedTest.fixApproximateMarkPositionsForWindReadOut(getTrackedRace(), new MillisecondsTimePoint( + new GregorianCalendar(2011, 05, 23).getTime())); + getTrackedRace().recordWind( + new WindImpl(/* position */null, MillisecondsTimePoint.now(), new KnotSpeedWithBearingImpl(12, + new DegreeBearingImpl(65))), new WindSourceImpl(WindSourceType.WEB)); + } + + @Test + public void testDifferentTimesSpentInLegAtDifferentTimes() throws ClassNotFoundException, IOException { + final Competitor findel = getCompetitorByName("Findel"); + TrackedLegOfCompetitor findelsSecondLeg = getTrackedRace().getTrackedLeg(getTrackedRace().getRace().getCourse().getLegs().get(1)).getTrackedLeg(findel); + TimePoint findelStartedHisSecondLegAt = findelsSecondLeg.getStartTime(); + assertEquals(null, findelsSecondLeg.getTime(findelStartedHisSecondLegAt.minus(1))); + assertEquals(0.0, findelsSecondLeg.getTime(findelStartedHisSecondLegAt).asMillis(), 0.00001); + assertEquals(10000.0, findelsSecondLeg.getTime(findelStartedHisSecondLegAt.plus(10000)).asMillis(), 0.00001); + } +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TrackBasedTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TrackBasedTest.java index c97769584b6..f02a9a42231 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TrackBasedTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/TrackBasedTest.java @@ -36,6 +36,7 @@ import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; import com.sap.sailing.domain.common.impl.WindSourceImpl; import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; +import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.MarkPassing; import com.sap.sailing.domain.tracking.TrackedRegatta; @@ -112,7 +113,7 @@ public abstract class TrackBasedTest { Course course = new CourseImpl(raceName, waypoints); RaceDefinition race = new RaceDefinitionImpl(raceName, course, boatClass, competitors); DynamicTrackedRace trackedRace = new DynamicTrackedRaceImpl(trackedRegatta, race, Collections. emptyList(), EmptyWindStore.INSTANCE, - /* delayToLiveInMillis */ 0, + EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 0, /* millisecondsOverWhichToAverageWind */ 30000, /* millisecondsOverWhichToAverageSpeed */ 30000, /* delay for wind estimation cache invalidation */ 0); // in this simplified artificial course, the top mark is exactly north of the right leeward gate diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/UnicodeCharactersInCompetitorNamesTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/UnicodeCharactersInCompetitorNamesTest.java index 22e6db0c9fc..05eab5a3c2d 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/UnicodeCharactersInCompetitorNamesTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/UnicodeCharactersInCompetitorNamesTest.java @@ -15,6 +15,7 @@ import org.junit.Test; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; +import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; import com.sap.sailing.domain.tracking.impl.EmptyWindStore; import com.sap.sailing.domain.tractracadapter.DomainFactory; import com.sap.sailing.domain.tractracadapter.TracTracConnectionConstants; @@ -55,7 +56,7 @@ public class UnicodeCharactersInCompetitorNamesTest { + TracTracConnectionConstants.PORT_STORED), new URI("http://tracms.traclive.dk/update_course"), /* startOfTracking */null, /* endOfTracking */null, /* delayToLiveInMillis */0l, - /* simulateWithStartTimeNow */ false, EmptyRaceLogStore.INSTANCE, EmptyWindStore.INSTANCE, "tracTest", "tracTest", "", + /* simulateWithStartTimeNow */ false, EmptyRaceLogStore.INSTANCE, EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, "tracTest", "tracTest", "", new DummyTrackedRegattaRegistry()); Iterable competitors = fourtyninerYellow_2.getRacesHandle().getRaces().iterator().next() diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/UpdateMarkPassingTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/UpdateMarkPassingTest.java index 479d9109b0f..e4aa39837b6 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/UpdateMarkPassingTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/UpdateMarkPassingTest.java @@ -24,6 +24,7 @@ import com.sap.sailing.domain.base.impl.WaypointImpl; import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; import com.sap.sailing.domain.common.impl.Util; +import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; import com.sap.sailing.domain.tracking.MarkPassing; import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl; import com.sap.sailing.domain.tracking.impl.EmptyWindStore; @@ -43,7 +44,7 @@ public class UpdateMarkPassingTest { when(race.getCompetitors()).thenReturn(Collections.singleton(competitor)); DynamicTrackedRaceImpl trackedRace = new DynamicTrackedRaceImpl( /* trackedRegatta */new TrackedRegattaImpl(new RegattaImpl("test", null, new HashSet(), false, null, - "test", null)), race, Collections. emptyList(), EmptyWindStore.INSTANCE, + "test", null)), race, Collections. emptyList(), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */1000, /* millisecondsOverWhichToAverageWind */30000, /* millisecondsOverWhichToAverageSpeed */30000); TimePoint now = MillisecondsTimePoint.now(); diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/WindEstimationLockingUnderLoadTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/WindEstimationLockingUnderLoadTest.java new file mode 100755 index 00000000000..80eb42a61a9 --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/WindEstimationLockingUnderLoadTest.java @@ -0,0 +1,182 @@ +package com.sap.sailing.domain.test; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.TrackedRaceStatusEnum; +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.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID; +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.TrackBasedEstimationWindTrackImpl; +import com.sap.sailing.domain.tracking.impl.TrackedRaceStatusImpl; +import com.sap.sailing.domain.tracking.impl.WindImpl; +import com.sap.sailing.domain.tracking.impl.WindTrackImpl; +import com.sap.sailing.domain.tracking.impl.WindWithConfidenceImpl; + +/** + * See the issues documented in bug 1923 (http://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=1923). + * This test mocks {@link TrackedRace} to the degree necessary and fires more or less random "measured" + * wind fixes against the {@link TrackBasedEstimationWindTrackImpl} while concurrently reading massively + * from it. With this, the test is supposed to reproduce conditions as observed during events, leading + * up to bug 1923. This can then be the basis for a fix. + * + * @author Axel Uhl (D043530) + * + */ +public class WindEstimationLockingUnderLoadTest { + private static final Logger logger = Logger.getLogger(WindEstimationLockingUnderLoadTest.class.getName()); + + private static final int MIN_WIND_SPEED_IN_KNOTS = 5; + + private static final int MAX_WIND_SPEED_IN_KNOTS = 20; + + private TrackedRace mockedTrackedRace; + + private WindSource realWindSource; + + private TrackBasedEstimationWindTrackImpl estimationTrack; + + private WindTrackImpl measuredTrack; + + @Before + public void setUp() { + realWindSource = new WindSourceWithAdditionalID(WindSourceType.EXPEDITION, "1"); + measuredTrack = new WindTrackImpl(WindTrack.DEFAULT_MILLISECONDS_OVER_WHICH_TO_AVERAGE_WIND, /* useSpeed */ true, /* nameForReadWriteLock */ "Test wind track in "+getClass().getName()); + mockedTrackedRace = mockTrackedRace(); + estimationTrack = new TrackBasedEstimationWindTrackImpl(mockedTrackedRace, WindTrack.DEFAULT_MILLISECONDS_OVER_WHICH_TO_AVERAGE_WIND, 0.5); + } + + @After + public void tearDown() { + // clean up all Mockito stubbing leaks, particularly the InvocationImpl objects attached to any ThreadLocal; + // see also bug 1923, comment #9. + Mockito.reset(); + } + + private TrackedRace mockTrackedRace() { + TrackedRace result = mock(TrackedRace.class); + RaceDefinition mockedRaceDefinition = mock(RaceDefinition.class); + when(result.getRace()).thenReturn(mockedRaceDefinition); + when(mockedRaceDefinition.getName()).thenReturn("Test Race"); + when(result.getEstimatedWindDirectionWithConfidence((Position) any(), (TimePoint) any())).thenAnswer(new Answer>() { + public WindWithConfidence answer(InvocationOnMock invocation) { + return randomWindOrNull(); + } + }); + when(result.getOrCreateWindTrack(realWindSource)).thenReturn(measuredTrack); + when(result.getStatus()).thenReturn(new TrackedRaceStatusImpl(TrackedRaceStatusEnum.TRACKING, /* loadingProgress */ 1.0)); + when(result.getMillisecondsOverWhichToAverageWind()).thenReturn(30000l); + return result; + } + + private WindWithConfidence randomWindOrNull() { + final WindWithConfidence result; + double speedInKnots = MAX_WIND_SPEED_IN_KNOTS*Math.random(); + if (speedInKnots < MIN_WIND_SPEED_IN_KNOTS) { + result = null; + } else { + double directionInDegrees = 360*Math.random(); + double confidence = Math.random(); + final MillisecondsTimePoint now = MillisecondsTimePoint.now(); + result = new WindWithConfidenceImpl(new WindImpl(new DegreePosition(49, 8), now, new KnotSpeedWithBearingImpl(speedInKnots, new DegreeBearingImpl(directionInDegrees))), + confidence, now, /* useSpeed */ true); + } + return result; + } + + @Test + public void testSimpleWindEstimation() { + // We can expect MIN_WIND_SPEED_IN_KNOTS/MAX_WIND_SPEED_IN_KNOTS results to be null, but as there is a significant + // difference between the two, with a sufficient amount of random samples there has to be at least one non-null + // result + int notNullCount = 0; + for (int i=0; i<1000; i++) { + Wind estimatedWind = estimationTrack.getAveragedWind(/* position */ null, MillisecondsTimePoint.now().minus(10000*i)); + if (estimatedWind != null) { + notNullCount++; + } + } + assertTrue(notNullCount > 0); + } + + @Test + public void testSendingNewWindFixToEstimationTrack() { + final WindImpl wind = new WindImpl(new DegreePosition(49, 8), MillisecondsTimePoint.now(), + new KnotSpeedWithBearingImpl(/* speedInKnots */12., new DegreeBearingImpl(47))); + addMeasuredWindFix(wind); + // no verdict, really; just expecting that there is no exception raised + } + + @Test + public void testAddingManyWindFixesWhileReadingLikeCrazy() throws InterruptedException { + Thread inserter = new Thread("Inserter thread in "+getClass().getName()+".testAddingManyWindFixesWhileReadingLikeCrazy") { + @Override + public void run() { + TimePoint now = MillisecondsTimePoint.now(); + for (int i = 0; i < 100000; i++) { + final WindImpl wind = new WindImpl(new DegreePosition(49, 8), now, new KnotSpeedWithBearingImpl( + /* speedInKnots */12., new DegreeBearingImpl(47))); + addMeasuredWindFix(wind); + now = now.plus(10); + } + logger.info("Inserter thread done"); + } + }; + Runnable readerRunnable = new Runnable() { + @Override + public void run() { + TimePoint now = MillisecondsTimePoint.now(); + for (int i=0; i<100000; i++) { + estimationTrack.getAveragedWind(/* position */ null, now); + now = now.plus(10); // the cache quantizes to a full second; so not all time points will automatically fetch uncached values + } + logger.info(Thread.currentThread().getName()+" done"); + } + }; + inserter.start(); + List readers = new ArrayList(); + for (int i = 0; i < 4; i++) { + Thread reader = new Thread(readerRunnable, "Reader thread "+i+" in " + getClass().getName() + + ".testAddingManyWindFixesWhileReadingLikeCrazy"); + reader.start(); + readers.add(reader); + } + inserter.join(); + for (Thread reader : readers) { + reader.join(); + } + } + + /** + * Adds the wind fix to the {@link #measuredTrack} and explicitly notifies {@link #estimationTrack} which is + * not a listener on the mocked tracked race or the wind track and therefore otherwise wouldn't receive the fix + */ + private void addMeasuredWindFix(final WindImpl wind) { + measuredTrack.add(wind); + estimationTrack.windDataReceived(wind, realWindSource); + } +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/WindTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/WindTest.java index bf1c83dd34e..e6df78eeb82 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/WindTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/WindTest.java @@ -39,6 +39,7 @@ import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; import com.sap.sailing.domain.common.impl.Util; import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; +import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.MarkPassing; import com.sap.sailing.domain.tracking.Wind; @@ -269,7 +270,7 @@ public class WindTest { new RaceDefinitionImpl("Test Race", new CourseImpl("Test Course", Arrays.asList(new Waypoint[] { w1, w2, w3 })), boatClass, Collections.singleton(competitor)), Collections. emptyList(), - EmptyWindStore.INSTANCE, /* delayToLiveInMillis */ 1000, + EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 1000, /* millisecondsOverWhichToAverageWind */ 30000, /* millisecondsOverWhichToAverageSpeed */ 30000); TimePoint start = MillisecondsTimePoint.now(); diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/AbstractMockedRaceMarkPassingTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/AbstractMockedRaceMarkPassingTest.java index a31c44bb931..14d19636981 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/AbstractMockedRaceMarkPassingTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/AbstractMockedRaceMarkPassingTest.java @@ -1,92 +1,93 @@ -package com.sap.sailing.domain.test.markpassing; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Random; -import java.util.UUID; - -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.ControlPointWithTwoMarks; -import com.sap.sailing.domain.base.Course; -import com.sap.sailing.domain.base.Mark; -import com.sap.sailing.domain.base.RaceDefinition; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.Sideline; -import com.sap.sailing.domain.base.Waypoint; -import com.sap.sailing.domain.base.impl.BoatClassImpl; -import com.sap.sailing.domain.base.impl.CompetitorImpl; -import com.sap.sailing.domain.base.impl.ControlPointWithTwoMarksImpl; -import com.sap.sailing.domain.base.impl.CourseAreaImpl; -import com.sap.sailing.domain.base.impl.CourseImpl; -import com.sap.sailing.domain.base.impl.FleetImpl; -import com.sap.sailing.domain.base.impl.MarkImpl; -import com.sap.sailing.domain.base.impl.RaceDefinitionImpl; -import com.sap.sailing.domain.base.impl.RegattaImpl; -import com.sap.sailing.domain.base.impl.SeriesImpl; -import com.sap.sailing.domain.base.impl.WaypointImpl; -import com.sap.sailing.domain.common.PassingInstruction; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.SpeedWithBearing; -import com.sap.sailing.domain.common.TimePoint; -import com.sap.sailing.domain.common.impl.DegreeBearingImpl; -import com.sap.sailing.domain.common.impl.DegreePosition; -import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; -import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; -import com.sap.sailing.domain.common.impl.Util.Pair; -import com.sap.sailing.domain.leaderboard.impl.HighPoint; -import com.sap.sailing.domain.tracking.DynamicTrackedRace; -import com.sap.sailing.domain.tracking.GPSFixMoving; -import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl; -import com.sap.sailing.domain.tracking.impl.EmptyWindStore; -import com.sap.sailing.domain.tracking.impl.GPSFixImpl; -import com.sap.sailing.domain.tracking.impl.GPSFixMovingImpl; -import com.sap.sailing.domain.tracking.impl.TrackedRegattaImpl; - -public class AbstractMockedRaceMarkPassingTest { - protected Competitor bob = new CompetitorImpl("Bob", "Bob", null, null, null); - protected Competitor joe = new CompetitorImpl("Joe", "Joe", null, null, null); - protected Competitor mike = new CompetitorImpl("Mike", "Mike", null, null, null); - - protected Mark m = new MarkImpl("black"); - protected Mark m2 = new MarkImpl("white 1"); - protected Mark m3 = new MarkImpl("white 2"); - - protected List waypoints = new ArrayList<>(); - protected DynamicTrackedRace trackedRace; - protected long time; - protected Random rnd = new Random(); - - public AbstractMockedRaceMarkPassingTest() { - ControlPointWithTwoMarks cp = new ControlPointWithTwoMarksImpl(m2, m3, "cp"); - Waypoint w1 = new WaypointImpl(cp, PassingInstruction.Line); - Waypoint w2 = new WaypointImpl(m, PassingInstruction.Port); - Waypoint w3 = new WaypointImpl(cp, PassingInstruction.Line); - Regatta r = new RegattaImpl("regatta", new BoatClassImpl("boat", true), Arrays.asList(new SeriesImpl("Series", true, Arrays.asList(new FleetImpl("fleet")), - new ArrayList(), null)), true, new HighPoint(), "ID", new CourseAreaImpl("area", new UUID(5, 5))); - Course course = new CourseImpl("course", Arrays.asList(w1, w2, w3)); - RaceDefinition race = new RaceDefinitionImpl("Performance Race", course, new BoatClassImpl("boat", true), Arrays.asList(bob)); - trackedRace = new DynamicTrackedRaceImpl(new TrackedRegattaImpl(r), race, new ArrayList(), new EmptyWindStore(), 0, 10000, 10000); - trackedRace.setStartTimeReceived(new MillisecondsTimePoint(System.currentTimeMillis() - 120000)); - List tps = Arrays.asList(new MillisecondsTimePoint(System.currentTimeMillis()), new MillisecondsTimePoint(System.currentTimeMillis() - 30000), - new MillisecondsTimePoint(System.currentTimeMillis() + 30000)); - List> pos = Arrays.asList(new Pair(m, new DegreePosition(0, 0)), new Pair(m2, new DegreePosition(-0.001, -0.00005)), - new Pair(m3, new DegreePosition(-0.001, 0.00005))); - for (Pair pair : pos) { - for (TimePoint t : tps) { - trackedRace.recordFix(pair.getA(), new GPSFixImpl(pair.getB(), t)); - } - } - for (Waypoint w : trackedRace.getRace().getCourse().getWaypoints()) { - waypoints.add(w); - } - } - - protected GPSFixMoving rndFix() { - DegreePosition position = new DegreePosition(37.8878 + rnd.nextDouble() * 0.0019, -122.268 - rnd.nextDouble() * 0.012); - TimePoint p = new MillisecondsTimePoint((long) (System.currentTimeMillis() - 300000 + (Math.random() * (7800000)))); - SpeedWithBearing speed = new KnotSpeedWithBearingImpl(rnd.nextInt(11), new DegreeBearingImpl(rnd.nextInt(360))); - - return new GPSFixMovingImpl(position, p, speed); - } -} +package com.sap.sailing.domain.test.markpassing; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.UUID; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.ControlPointWithTwoMarks; +import com.sap.sailing.domain.base.Course; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Sideline; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.base.impl.BoatClassImpl; +import com.sap.sailing.domain.base.impl.CompetitorImpl; +import com.sap.sailing.domain.base.impl.ControlPointWithTwoMarksImpl; +import com.sap.sailing.domain.base.impl.CourseAreaImpl; +import com.sap.sailing.domain.base.impl.CourseImpl; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.base.impl.MarkImpl; +import com.sap.sailing.domain.base.impl.RaceDefinitionImpl; +import com.sap.sailing.domain.base.impl.RegattaImpl; +import com.sap.sailing.domain.base.impl.SeriesImpl; +import com.sap.sailing.domain.base.impl.WaypointImpl; +import com.sap.sailing.domain.common.PassingInstruction; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.leaderboard.impl.HighPoint; +import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; +import com.sap.sailing.domain.tracking.DynamicTrackedRace; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl; +import com.sap.sailing.domain.tracking.impl.EmptyWindStore; +import com.sap.sailing.domain.tracking.impl.GPSFixImpl; +import com.sap.sailing.domain.tracking.impl.GPSFixMovingImpl; +import com.sap.sailing.domain.tracking.impl.TrackedRegattaImpl; + +public class AbstractMockedRaceMarkPassingTest { + protected Competitor bob = new CompetitorImpl("Bob", "Bob", null, null, null); + protected Competitor joe = new CompetitorImpl("Joe", "Joe", null, null, null); + protected Competitor mike = new CompetitorImpl("Mike", "Mike", null, null, null); + + protected Mark m = new MarkImpl("black"); + protected Mark m2 = new MarkImpl("white 1"); + protected Mark m3 = new MarkImpl("white 2"); + + protected List waypoints = new ArrayList<>(); + protected DynamicTrackedRace trackedRace; + protected long time; + protected Random rnd = new Random(); + + public AbstractMockedRaceMarkPassingTest() { + ControlPointWithTwoMarks cp = new ControlPointWithTwoMarksImpl(m2, m3, "cp"); + Waypoint w1 = new WaypointImpl(cp, PassingInstruction.Line); + Waypoint w2 = new WaypointImpl(m, PassingInstruction.Port); + Waypoint w3 = new WaypointImpl(cp, PassingInstruction.Line); + Regatta r = new RegattaImpl("regatta", new BoatClassImpl("boat", true), Arrays.asList(new SeriesImpl("Series", true, Arrays.asList(new FleetImpl("fleet")), + new ArrayList(), null)), true, new HighPoint(), "ID", new CourseAreaImpl("area", new UUID(5, 5))); + Course course = new CourseImpl("course", Arrays.asList(w1, w2, w3)); + RaceDefinition race = new RaceDefinitionImpl("Performance Race", course, new BoatClassImpl("boat", true), Arrays.asList(bob)); + trackedRace = new DynamicTrackedRaceImpl(new TrackedRegattaImpl(r), race, new ArrayList(), new EmptyWindStore(), EmptyGPSFixStore.INSTANCE, 0, 10000, 10000); + trackedRace.setStartTimeReceived(new MillisecondsTimePoint(System.currentTimeMillis() - 120000)); + List tps = Arrays.asList(new MillisecondsTimePoint(System.currentTimeMillis()), new MillisecondsTimePoint(System.currentTimeMillis() - 30000), + new MillisecondsTimePoint(System.currentTimeMillis() + 30000)); + List> pos = Arrays.asList(new Pair(m, new DegreePosition(0, 0)), new Pair(m2, new DegreePosition(-0.001, -0.00005)), + new Pair(m3, new DegreePosition(-0.001, 0.00005))); + for (Pair pair : pos) { + for (TimePoint t : tps) { + trackedRace.recordFix(pair.getA(), new GPSFixImpl(pair.getB(), t)); + } + } + for (Waypoint w : trackedRace.getRace().getCourse().getWaypoints()) { + waypoints.add(w); + } + } + + protected GPSFixMoving rndFix() { + DegreePosition position = new DegreePosition(37.8878 + rnd.nextDouble() * 0.0019, -122.268 - rnd.nextDouble() * 0.012); + TimePoint p = new MillisecondsTimePoint((long) (System.currentTimeMillis() - 300000 + (Math.random() * (7800000)))); + SpeedWithBearing speed = new KnotSpeedWithBearingImpl(rnd.nextInt(11), new DegreeBearingImpl(rnd.nextInt(360))); + + return new GPSFixMovingImpl(position, p, speed); + } +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/FlorianopolisMarkPassingTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/FlorianopolisMarkPassingTest.java index 9432a1295a8..e3f1c91081a 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/FlorianopolisMarkPassingTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/FlorianopolisMarkPassingTest.java @@ -1,147 +1,147 @@ -package com.sap.sailing.domain.test.markpassing; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URISyntaxException; -import java.text.ParseException; - -import org.junit.Test; - -public class FlorianopolisMarkPassingTest extends AbstractMarkPassingTest { - - public FlorianopolisMarkPassingTest() throws MalformedURLException, URISyntaxException { - super(); - } - - @Test - public void testRace1() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("1"); - } - - @Test - public void testRace2() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("2"); - } - - @Test - public void testRace3() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("3"); - } - - @Test - public void testRace4() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("4"); - } - - @Test - public void testRace5() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("5"); - } - - @Test - public void testRace6() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("6"); - } - - @Test - public void testRace7() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("7"); - } - - @Test - public void testRace8() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("8"); - } - - @Test - public void testRace10() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("10"); - } - - @Test - public void testRace11() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("11"); - } - - @Test - public void testRace12() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("12"); - } - - @Test - public void testRace13() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("13"); - } - - @Test - public void testRace14() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("14"); - } - - @Test - public void testRace16() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("16"); - } - - @Test - public void testRace17() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("17"); - } - - @Test - public void testRace19() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("19"); - } - - @Test - public void testRace22() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("22"); - } - - @Test - public void testRace24() throws IOException, InterruptedException, URISyntaxException, ParseException { - testRace("24"); - } - - /* - * Alinghi acting wierd - * - * @Test public void testRace23() throws IOException, InterruptedException, URISyntaxException { testRace(23); } - */ - - /* - * Wrong start from TracTrac fails start test. - * - * @Test public void testRace18() throws IOException, InterruptedException, URISyntaxException { testRace("18"); } - */ - - /* - * All mark data seems to be missing - * - * @Test public void testRace9() throws IOException, InterruptedException, URISyntaxException { testRace(9); } - * - * @Test public void testRace20() throws IOException, InterruptedException, URISyntaxException { testRace(20); } - */ - - /* - * Missing static mark info leads to skipping of all finishes. - * - * @Test public void testRace25() throws IOException, InterruptedException, URISyntaxException { testRace(25); } - * - * @Test public void testRace21() throws IOException, InterruptedException, URISyntaxException { testRace(21); } - * - * @Test public void testRace15() throws IOException, InterruptedException, URISyntaxException { testRace(15); } - * - * @Test public void testRace26() throws IOException, InterruptedException, URISyntaxException { testRace(26); } - */ - - @Override - protected String getExpectedEventName() { - return "ESS Florianopolis 2013"; - } - - @Override - protected String getFileName() { - return "event_20131112_ESSFlorian-Race_"; - } -} +package com.sap.sailing.domain.test.markpassing; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.text.ParseException; + +import org.junit.Test; + +public class FlorianopolisMarkPassingTest extends AbstractMarkPassingTest { + + public FlorianopolisMarkPassingTest() throws MalformedURLException, URISyntaxException { + super(); + } + + @Test + public void testRace1() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("1"); + } + + @Test + public void testRace2() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("2"); + } + + @Test + public void testRace3() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("3"); + } + + @Test + public void testRace4() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("4"); + } + + @Test + public void testRace5() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("5"); + } + + @Test + public void testRace6() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("6"); + } + + @Test + public void testRace7() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("7"); + } + + @Test + public void testRace8() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("8"); + } + + @Test + public void testRace10() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("10"); + } + + @Test + public void testRace11() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("11"); + } + + @Test + public void testRace12() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("12"); + } + + @Test + public void testRace13() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("13"); + } + + @Test + public void testRace14() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("14"); + } + + @Test + public void testRace16() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("16"); + } + + @Test + public void testRace17() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("17"); + } + + @Test + public void testRace19() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("19"); + } + + @Test + public void testRace22() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("22"); + } + + @Test + public void testRace24() throws IOException, InterruptedException, URISyntaxException, ParseException { + testRace("24"); + } + + /* + * Alinghi acting wierd + * + * @Test public void testRace23() throws IOException, InterruptedException, URISyntaxException { testRace(23); } + */ + + /* + * Wrong start from TracTrac fails start test. + * + * @Test public void testRace18() throws IOException, InterruptedException, URISyntaxException { testRace("18"); } + */ + + /* + * All mark data seems to be missing + * + * @Test public void testRace9() throws IOException, InterruptedException, URISyntaxException { testRace(9); } + * + * @Test public void testRace20() throws IOException, InterruptedException, URISyntaxException { testRace(20); } + */ + + /* + * Missing static mark info leads to skipping of all finishes. + * + * @Test public void testRace25() throws IOException, InterruptedException, URISyntaxException { testRace(25); } + * + * @Test public void testRace21() throws IOException, InterruptedException, URISyntaxException { testRace(21); } + * + * @Test public void testRace15() throws IOException, InterruptedException, URISyntaxException { testRace(15); } + * + * @Test public void testRace26() throws IOException, InterruptedException, URISyntaxException { testRace(26); } + */ + + @Override + protected String getExpectedEventName() { + return "ESS Florianopolis 2013"; + } + + @Override + protected String getFileName() { + return "event_20131112_ESSFlorian-Race_"; + } +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRace.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRace.java index f22fd0d1a57..ebf828ee700 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRace.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRace.java @@ -35,6 +35,7 @@ import com.sap.sailing.domain.common.WindSourceType; import com.sap.sailing.domain.common.impl.Util.Pair; import com.sap.sailing.domain.leaderboard.ScoringScheme; import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; import com.sap.sailing.domain.tracking.CourseDesignChangedListener; import com.sap.sailing.domain.tracking.DynamicGPSFixTrack; import com.sap.sailing.domain.tracking.DynamicRaceDefinitionSet; @@ -220,7 +221,8 @@ public class MockedTrackedRace implements DynamicTrackedRace { } @Override - public void addListener(RaceChangeListener listener, boolean notifyAboutWindFixesAlreadyLoaded) { + public void addListener(RaceChangeListener listener, boolean notifyAboutWindFixesAlreadyLoaded, + boolean notifyAboutGPSFixesAlreadyLoaded) { // TODO Auto-generated method stub } @@ -517,7 +519,7 @@ public class MockedTrackedRace implements DynamicTrackedRace { @Override public DynamicTrackedRace createTrackedRace(RaceDefinition raceDefinition, Iterable sidelines, WindStore windStore, - long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed, + GPSFixStore gpsFixStore, long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed, DynamicRaceDefinitionSet raceDefinitionSetToUpdate) { // TODO Auto-generated method stub return null; @@ -671,7 +673,7 @@ public class MockedTrackedRace implements DynamicTrackedRace { } @Override - public Distance getAverageCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) throws NoWindException { + public Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) throws NoWindException { // TODO Auto-generated method stub return null; } @@ -736,14 +738,14 @@ public class MockedTrackedRace implements DynamicTrackedRace { } @Override - public Distance getAverageCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, boolean waitForLatestAnalyses) + public Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, boolean waitForLatestAnalyses) throws NoWindException { // TODO Auto-generated method stub return null; } @Override - public void waitUntilWindLoadingComplete() throws InterruptedException { + public void waitUntilLoadingFromWindStoreComplete() throws InterruptedException { // TODO Auto-generated method stub } @@ -930,6 +932,12 @@ public class MockedTrackedRace implements DynamicTrackedRace { return null; } + @Override + public GPSFixStore getGPSFixStore() { + // TODO Auto-generated method stub + return null; + } + @Override public Bearing getCrossingBearing(Waypoint w, TimePoint t) { // TODO Auto-generated method stub @@ -942,4 +950,24 @@ public class MockedTrackedRace implements DynamicTrackedRace { return null; } + @Override + public Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint timePoint, + boolean waitForLatestAnalysis) throws NoWindException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, + boolean upwindOnly, boolean waitForLatestAnalysis) throws NoWindException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void waitUntilLoadingFromGPSFixStoreComplete() throws InterruptedException { + // TODO Auto-generated method stub + + } + } diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRaceWithStartTimeAndRanks.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRaceWithStartTimeAndRanks.java index bcf823ee25b..ef9f70252a2 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRaceWithStartTimeAndRanks.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/mock/MockedTrackedRaceWithStartTimeAndRanks.java @@ -26,6 +26,7 @@ import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.WindSourceType; import com.sap.sailing.domain.common.impl.Util.Pair; import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; import com.sap.sailing.domain.tracking.CourseDesignChangedListener; import com.sap.sailing.domain.tracking.GPSFix; import com.sap.sailing.domain.tracking.GPSFixMoving; @@ -352,7 +353,8 @@ public class MockedTrackedRaceWithStartTimeAndRanks implements TrackedRace { } @Override - public void addListener(RaceChangeListener listener, boolean notifyAboutWindFixesAlreadyLoaded) { + public void addListener(RaceChangeListener listener, boolean notifyAboutWindFixesAlreadyLoaded, + boolean notifyAboutGPSFixesAlreadyLoaded) { // TODO Auto-generated method stub } @@ -408,7 +410,7 @@ public class MockedTrackedRaceWithStartTimeAndRanks implements TrackedRace { } @Override - public Distance getAverageCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) throws NoWindException { + public Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) throws NoWindException { // TODO Auto-generated method stub return null; } @@ -431,14 +433,14 @@ public class MockedTrackedRaceWithStartTimeAndRanks implements TrackedRace { } @Override - public Distance getAverageCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, boolean waitForLatestAnalyses) + public Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, boolean waitForLatestAnalyses) throws NoWindException { // TODO Auto-generated method stub return null; } @Override - public void waitUntilWindLoadingComplete() { + public void waitUntilLoadingFromWindStoreComplete() { // TODO Auto-generated method stub } @@ -536,7 +538,7 @@ public class MockedTrackedRaceWithStartTimeAndRanks implements TrackedRace { return null; } - @Override + @Override public Distance getDistanceToStartLine(Competitor competitor, long millisecondsBeforeRaceStart) { // TODO Auto-generated method stub return null; @@ -547,14 +549,14 @@ public class MockedTrackedRaceWithStartTimeAndRanks implements TrackedRace { // TODO Auto-generated method stub return null; } - + public void addStartTimeChangedListener(StartTimeChangedListener listener) { // TODO Auto-generated method stub } @Override - public TimePoint getStartTimeReceived() { + public TimePoint getStartTimeReceived() { // TODO Auto-generated method stub return null; } @@ -595,4 +597,30 @@ public class MockedTrackedRaceWithStartTimeAndRanks implements TrackedRace { return null; } + @Override + public Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint timePoint, + boolean waitForLatestAnalysis) throws NoWindException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, + boolean upwindOnly, boolean waitForLatestAnalysis) throws NoWindException { + // TODO Auto-generated method stub + return null; + } + + @Override + public GPSFixStore getGPSFixStore() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void waitUntilLoadingFromGPSFixStoreComplete() throws InterruptedException { + // TODO Auto-generated method stub + + } + } diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/DomainFactory.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/DomainFactory.java index 7c8840df88f..16301840c45 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/DomainFactory.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/DomainFactory.java @@ -25,6 +25,7 @@ import com.sap.sailing.domain.common.PassingInstruction; import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.impl.Util.Pair; import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; import com.sap.sailing.domain.tracking.DynamicRaceDefinitionSet; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; @@ -132,7 +133,7 @@ public interface DomainFactory { */ TracTracRaceTracker createRaceTracker(URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, boolean simulateWithStartTimeNow, RaceLogStore raceLogStore, - WindStore windStore, String tracTracUsername, String tracTracPassword, String raceStatus, TrackedRegattaRegistry trackedRegattaRegistry) throws MalformedURLException, FileNotFoundException, + WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, TrackedRegattaRegistry trackedRegattaRegistry) throws MalformedURLException, FileNotFoundException, URISyntaxException; /** @@ -142,7 +143,7 @@ public interface DomainFactory { */ RaceTracker createRaceTracker(Regatta regatta, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, boolean simulateWithStartTimeNow, RaceLogStore raceLogStore, - WindStore windStore, String tracTracUsername, String tracTracPassword, String raceStatus, TrackedRegattaRegistry trackedRegattaRegistry) throws MalformedURLException, FileNotFoundException, + WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, TrackedRegattaRegistry trackedRegattaRegistry) throws MalformedURLException, FileNotFoundException, URISyntaxException; BoatClass getOrCreateBoatClass(String competitorClassName); @@ -241,10 +242,10 @@ public interface DomainFactory { * timeoutInMilliseconds milliseconds have passed and the race definition is found not to have shown up * until then, null is returned. The unblocking may be deferred even beyond * timeoutInMilliseconds in case no modifications happen on the set of races cached by this factory. - * @param raceId TODO + * * @param timeoutInMilliseconds - * passing -1 means an infinite timeout; 0 means return immediately with null as result if no - * race definition is found for race. + * passing -1 means an infinite timeout; 0 means return immediately with null as result if + * no race definition is found for race. */ RaceDefinition getAndWaitForRaceDefinition(UUID raceId, long timeoutInMilliseconds); diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java index c355018b3eb..8b66996d840 100644 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java @@ -53,6 +53,8 @@ import com.sap.sailing.domain.common.impl.Util; import com.sap.sailing.domain.common.impl.Util.Pair; import com.sap.sailing.domain.markpassingcalculation.MarkPassingCalculator; import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; import com.sap.sailing.domain.tracking.DynamicRaceDefinitionSet; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; @@ -498,7 +500,7 @@ public class DomainFactoryImpl implements DomainFactory { // add to existing regatta only if boat class matches if (raceDefinition.getBoatClass() == trackedRegatta.getRegatta().getBoatClass()) { trackedRegatta.getRegatta().addRace(raceDefinition); - trackedRace = createTrackedRace(trackedRegatta, raceDefinition, sidelines, windStore, + trackedRace = createTrackedRace(trackedRegatta, raceDefinition, sidelines, windStore, EmptyGPSFixStore.INSTANCE, delayToLiveInMillis, millisecondsOverWhichToAverageWind, raceDefinitionSetToUpdate); logger.info("Added race " + raceDefinition + " to regatta " + trackedRegatta.getRegatta()); @@ -528,10 +530,10 @@ public class DomainFactoryImpl implements DomainFactory { } } - private DynamicTrackedRace createTrackedRace(TrackedRegatta trackedRegatta, RaceDefinition race, Iterable sidelines, WindStore windStore, + private DynamicTrackedRace createTrackedRace(TrackedRegatta trackedRegatta, RaceDefinition race, Iterable sidelines, WindStore windStore, GPSFixStore gpsFixStore, long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, DynamicRaceDefinitionSet raceDefinitionSetToUpdate) { return trackedRegatta.createTrackedRace(race, sidelines, - windStore, delayToLiveInMillis, millisecondsOverWhichToAverageWind, + windStore, gpsFixStore, delayToLiveInMillis, millisecondsOverWhichToAverageWind, /* time over which to average speed: */ race.getBoatClass().getApproximateManeuverDurationInMilliseconds(), raceDefinitionSetToUpdate); } @@ -608,20 +610,20 @@ public class DomainFactoryImpl implements DomainFactory { @Override public TracTracRaceTracker createRaceTracker(URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, boolean simulateWithStartTimeNow, - RaceLogStore raceLogStore, WindStore windStore, String tracTracUsername, String tracTracPassword, String raceStatus, + RaceLogStore raceLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, TrackedRegattaRegistry trackedRegattaRegistry) throws MalformedURLException, FileNotFoundException, URISyntaxException { return new TracTracRaceTrackerImpl(this, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, - simulateWithStartTimeNow, raceLogStore, windStore, tracTracUsername, tracTracPassword, raceStatus, trackedRegattaRegistry); + simulateWithStartTimeNow, raceLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, trackedRegattaRegistry); } @Override public RaceTracker createRaceTracker(Regatta regatta, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, - boolean simulateWithStartTimeNow, RaceLogStore raceLogStore, WindStore windStore, String tracTracUsername, String tracTracPassword, String raceStatus, TrackedRegattaRegistry trackedRegattaRegistry) + boolean simulateWithStartTimeNow, RaceLogStore raceLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, TrackedRegattaRegistry trackedRegattaRegistry) throws MalformedURLException, FileNotFoundException, URISyntaxException { return new TracTracRaceTrackerImpl(regatta, this, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, - simulateWithStartTimeNow, raceLogStore, windStore, tracTracUsername, tracTracPassword, raceStatus, trackedRegattaRegistry); + simulateWithStartTimeNow, raceLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, trackedRegattaRegistry); } @Override diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceCourseReceiver.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceCourseReceiver.java index b00f7560cda..f82cb43df70 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceCourseReceiver.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceCourseReceiver.java @@ -21,6 +21,7 @@ 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.markpassingcalculation.MarkPassingCalculator; +import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; import com.sap.sailing.domain.tracking.DynamicRaceDefinitionSet; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; @@ -155,7 +156,7 @@ public class RaceCourseReceiver extends AbstractReceiverWithQueue sidelines) { DynamicTrackedRace trackedRace = getTrackedRegatta().createTrackedRace(race, sidelines, - windStore, delayToLiveInMillis, millisecondsOverWhichToAverageWind, + windStore, EmptyGPSFixStore.INSTANCE, delayToLiveInMillis, millisecondsOverWhichToAverageWind, /* time over which to average speed: */ race.getBoatClass().getApproximateManeuverDurationInMilliseconds(), raceDefinitionSetToUpdate); diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceHandleImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceHandleImpl.java index e2c364d6901..8f6940fe5b6 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceHandleImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceHandleImpl.java @@ -52,7 +52,10 @@ public class RaceHandleImpl implements RacesHandle { public Set getRaces(long timeoutInMilliseconds) { Set result = new HashSet(); for (Race race : tractracEvent.getRaceList()) { - result.add(domainFactory.getAndWaitForRaceDefinition(race.getId(), timeoutInMilliseconds)); + final RaceDefinition raceDefinition = domainFactory.getAndWaitForRaceDefinition(race.getId(), timeoutInMilliseconds); + if (raceDefinition != null) { // may have time-outed + result.add(raceDefinition); + } } return result; } diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceTrackingConnectivityParametersImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceTrackingConnectivityParametersImpl.java index 5974079a7cd..ef3eae7c1d5 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceTrackingConnectivityParametersImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceTrackingConnectivityParametersImpl.java @@ -10,6 +10,7 @@ import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.impl.Util; import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; import com.sap.sailing.domain.tracking.RaceTracker; import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; @@ -51,18 +52,18 @@ public class RaceTrackingConnectivityParametersImpl implements RaceTrackingConne } @Override - public RaceTracker createRaceTracker(TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore) throws MalformedURLException, + public RaceTracker createRaceTracker(TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore, GPSFixStore gpsFixStore) throws MalformedURLException, FileNotFoundException, URISyntaxException { RaceTracker tracker = domainFactory.createRaceTracker(paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, - endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, raceLogStore, windStore, tracTracUsername, tracTracPassword, raceStatus, trackedRegattaRegistry); + endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, raceLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, trackedRegattaRegistry); return tracker; } @Override - public RaceTracker createRaceTracker(Regatta regatta, TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore) + public RaceTracker createRaceTracker(Regatta regatta, TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore, GPSFixStore gpsFixStore) throws Exception { RaceTracker tracker = domainFactory.createRaceTracker(regatta, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, - endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, raceLogStore, windStore, tracTracUsername, tracTracPassword, raceStatus, trackedRegattaRegistry); + endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, raceLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, trackedRegattaRegistry); return tracker; } diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java index 3d0f211b1be..88f68f8023e 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java @@ -45,6 +45,7 @@ import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; import com.sap.sailing.domain.common.impl.Util.Pair; import com.sap.sailing.domain.common.impl.Util.Triple; import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; import com.sap.sailing.domain.tracking.AbstractRaceTrackerImpl; import com.sap.sailing.domain.tracking.DynamicGPSFixTrack; import com.sap.sailing.domain.tracking.DynamicRaceDefinitionSet; @@ -198,6 +199,7 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements private final Set receivers; private final DomainFactory domainFactory; private final WindStore windStore; + private final GPSFixStore gpsFixStore; private final Set races; private final DynamicTrackedRegatta trackedRegatta; private TrackedRaceStatus lastStatus; @@ -249,18 +251,18 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements */ protected TracTracRaceTrackerImpl(DomainFactory domainFactory, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, - boolean simulateWithStartTimeNow, RaceLogStore raceLogStore, WindStore windStore, String tracTracUsername, String tracTracPassword, String raceStatus, TrackedRegattaRegistry trackedRegattaRegistry) + boolean simulateWithStartTimeNow, RaceLogStore raceLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, TrackedRegattaRegistry trackedRegattaRegistry) throws URISyntaxException, MalformedURLException, FileNotFoundException { this(KeyValue.setup(paramURL), domainFactory, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, - delayToLiveInMillis, simulateWithStartTimeNow, raceLogStore, windStore, tracTracUsername, tracTracPassword, raceStatus, trackedRegattaRegistry); + delayToLiveInMillis, simulateWithStartTimeNow, raceLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, trackedRegattaRegistry); } private TracTracRaceTrackerImpl(Event tractracEvent, DomainFactory domainFactory, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, boolean simulateWithStartTimeNow, - RaceLogStore raceLogStore, WindStore windStore, String tracTracUsername, String tracTracPassword, String raceStatus, TrackedRegattaRegistry trackedRegattaRegistry) + RaceLogStore raceLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, TrackedRegattaRegistry trackedRegattaRegistry) throws URISyntaxException, MalformedURLException, FileNotFoundException { this(tractracEvent, null, domainFactory, paramURL, liveURI, storedURI, courseDesignUpdateURI, - startOfTracking, endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, raceLogStore, windStore, + startOfTracking, endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, raceLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, trackedRegattaRegistry); } @@ -272,10 +274,10 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements */ protected TracTracRaceTrackerImpl(Regatta regatta, DomainFactory domainFactory, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, boolean simulateWithStartTimeNow, - RaceLogStore raceLogStore, WindStore windStore, String tracTracUsername, String tracTracPassword, String raceStatus, TrackedRegattaRegistry trackedRegattaRegistry) + RaceLogStore raceLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, TrackedRegattaRegistry trackedRegattaRegistry) throws URISyntaxException, MalformedURLException, FileNotFoundException { this(KeyValue.setup(paramURL), regatta, domainFactory, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, - endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, raceLogStore, windStore, + endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, raceLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, trackedRegattaRegistry); } @@ -293,7 +295,7 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements private TracTracRaceTrackerImpl(Event tractracEvent, final Regatta regatta, DomainFactory domainFactory, URL paramURL, URI liveURI, URI storedURI, URI tracTracUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, boolean simulateWithStartTimeNow, RaceLogStore raceLogStore, - WindStore windStore, String tracTracUsername, String tracTracPassword, String raceStatus, TrackedRegattaRegistry trackedRegattaRegistry) + WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, TrackedRegattaRegistry trackedRegattaRegistry) throws URISyntaxException, MalformedURLException, FileNotFoundException { super(); this.tractracEvent = tractracEvent; @@ -301,6 +303,7 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements isLiveTracking = liveURI != null; this.races = new HashSet(); this.windStore = windStore; + this.gpsFixStore = gpsFixStore; this.domainFactory = domainFactory; this.lastProgressPerID = new HashMap, Pair>(); final Simulator simulator; @@ -850,4 +853,9 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements updateStatusOfTrackedRace(trackedRace); } + @Override + public GPSFixStore getGPSFixStore() { + return gpsFixStore; + } + } diff --git a/java/com.sap.sailing.domain/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain/META-INF/MANIFEST.MF index 29e285ecbdc..f91a5bc04c6 100755 --- a/java/com.sap.sailing.domain/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain/META-INF/MANIFEST.MF @@ -20,6 +20,7 @@ Export-Package: com.sap.sailing.domain.base, com.sap.sailing.domain.polarsheets, com.sap.sailing.domain.racelog, com.sap.sailing.domain.racelog.impl, + com.sap.sailing.domain.racelog.tracking, com.sap.sailing.domain.tracking, com.sap.sailing.domain.tracking.impl, com.sap.sailing.util, diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/BearingWithConfidence.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/BearingWithConfidence.java index 63f0e5852af..2ca2bc789f5 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/BearingWithConfidence.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/BearingWithConfidence.java @@ -1,27 +1,9 @@ package com.sap.sailing.domain.base; -import com.sap.sailing.domain.base.BearingWithConfidence.DoublePair; import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.DoublePair; import com.sap.sailing.domain.confidence.HasConfidenceAndIsScalable; public interface BearingWithConfidence extends HasConfidenceAndIsScalable { - public static class DoublePair { - private final double a; - private final double b; - - public DoublePair(double a, double b) { - super(); - this.a = a; - this.b = b; - } - - public double getA() { - return a; - } - - public double getB() { - return b; - } - } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/PositionWithConfidence.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/PositionWithConfidence.java index 4efc40f2b4b..c4b345fafa4 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/PositionWithConfidence.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/PositionWithConfidence.java @@ -1,7 +1,7 @@ package com.sap.sailing.domain.base; -import com.sap.sailing.domain.base.impl.ScalablePosition; import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.scalablevalue.impl.ScalablePosition; import com.sap.sailing.domain.confidence.HasConfidenceAndIsScalable; /** diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/ScalableInteger.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/ScalableInteger.java index eee9f30d6ec..0bde779cb1c 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/ScalableInteger.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/ScalableInteger.java @@ -1,7 +1,7 @@ package com.sap.sailing.domain.base; -import com.sap.sailing.domain.confidence.AbstractScalarValue; -import com.sap.sailing.domain.confidence.ScalableValue; +import com.sap.sailing.domain.common.scalablevalue.AbstractScalarValue; +import com.sap.sailing.domain.common.scalablevalue.ScalableValue; public class ScalableInteger implements AbstractScalarValue { diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/BearingWithConfidenceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/BearingWithConfidenceImpl.java index 770abfb6306..b1128e35ee4 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/BearingWithConfidenceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/BearingWithConfidenceImpl.java @@ -1,10 +1,11 @@ package com.sap.sailing.domain.base.impl; import com.sap.sailing.domain.base.BearingWithConfidence; -import com.sap.sailing.domain.base.BearingWithConfidence.DoublePair; import com.sap.sailing.domain.common.Bearing; -import com.sap.sailing.domain.confidence.IsScalable; -import com.sap.sailing.domain.confidence.ScalableValue; +import com.sap.sailing.domain.common.DoublePair; +import com.sap.sailing.domain.common.scalablevalue.IsScalable; +import com.sap.sailing.domain.common.scalablevalue.ScalableValue; +import com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing; public class BearingWithConfidenceImpl extends HasConfidenceImpl implements BearingWithConfidence, IsScalable { diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/DomainFactoryImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/DomainFactoryImpl.java index c6490602736..e2fa9327610 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/DomainFactoryImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/DomainFactoryImpl.java @@ -35,6 +35,7 @@ import com.sap.sailing.domain.leaderboard.impl.HighPointExtremeSailingSeriesOver import com.sap.sailing.domain.leaderboard.impl.HighPointFirstGets10LastBreaksTie; import com.sap.sailing.domain.leaderboard.impl.HighPointFirstGets1LastBreaksTie; import com.sap.sailing.domain.leaderboard.impl.HighPointLastBreaksTie; +import com.sap.sailing.domain.leaderboard.impl.HighPointWinnerGetsSix; import com.sap.sailing.domain.leaderboard.impl.LowPoint; import com.sap.sailing.domain.leaderboard.impl.LowPointWinnerGetsZero; import com.sap.sailing.domain.tracking.GPSFix; @@ -86,6 +87,8 @@ public class DomainFactoryImpl extends SharedDomainFactoryImpl implements Domain return new HighPointFirstGets10LastBreaksTie(); case LOW_POINT_WINNER_GETS_ZERO: return new LowPointWinnerGetsZero(); + case HIGH_POINT_WINNER_GETS_SIX: + return new HighPointWinnerGetsSix(); } throw new RuntimeException("Unknown scoring scheme type "+scoringSchemeType.name()); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/PositionWithConfidenceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/PositionWithConfidenceImpl.java index 922f33f6339..0f8e5fc228f 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/PositionWithConfidenceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/PositionWithConfidenceImpl.java @@ -2,7 +2,8 @@ package com.sap.sailing.domain.base.impl; import com.sap.sailing.domain.base.PositionWithConfidence; import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.confidence.IsScalable; +import com.sap.sailing.domain.common.scalablevalue.IsScalable; +import com.sap.sailing.domain.common.scalablevalue.impl.ScalablePosition; public class PositionWithConfidenceImpl extends HasConfidenceImpl implements PositionWithConfidence, IsScalable { diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RaceColumnRaceLogReplicator.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RaceColumnRaceLogReplicator.java index 2f343bdf068..c540533f9dc 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RaceColumnRaceLogReplicator.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/RaceColumnRaceLogReplicator.java @@ -19,6 +19,14 @@ import com.sap.sailing.domain.racelog.RaceLogRaceStatusEvent; import com.sap.sailing.domain.racelog.RaceLogStartProcedureChangedEvent; import com.sap.sailing.domain.racelog.RaceLogStartTimeEvent; import com.sap.sailing.domain.racelog.RaceLogWindFixEvent; +import com.sap.sailing.domain.racelog.RevokeEvent; +import com.sap.sailing.domain.racelog.tracking.CloseOpenEndedDeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DefineMarkEvent; +import com.sap.sailing.domain.racelog.tracking.RegisterCompetitorEvent; +import com.sap.sailing.domain.racelog.tracking.StartTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceCompetitorMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceMarkMappingEvent; public class RaceColumnRaceLogReplicator implements RaceLogEventVisitor, Serializable { private static final long serialVersionUID = 4752330236765192592L; @@ -102,4 +110,44 @@ public class RaceColumnRaceLogReplicator implements RaceLogEventVisitor, Seriali public void visit(RaceLogWindFixEvent event) { notifyOnAdd(event); } + + @Override + public void visit(DeviceCompetitorMappingEvent event) { + notifyOnAdd(event); + } + + @Override + public void visit(DeviceMarkMappingEvent event) { + notifyOnAdd(event); + } + + @Override + public void visit(DenoteForTrackingEvent event) { + notifyOnAdd(event); + } + + @Override + public void visit(StartTrackingEvent event) { + notifyOnAdd(event); + } + + @Override + public void visit(RevokeEvent event) { + notifyOnAdd(event); + } + + @Override + public void visit(RegisterCompetitorEvent event) { + notifyOnAdd(event); + } + + @Override + public void visit(DefineMarkEvent event) { + notifyOnAdd(event); + } + + @Override + public void visit(CloseOpenEndedDeviceMappingEvent event) { + notifyOnAdd(event); + } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/SpeedWithBearingWithConfidenceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/SpeedWithBearingWithConfidenceImpl.java index 0714327dd6c..d20414de99b 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/SpeedWithBearingWithConfidenceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/SpeedWithBearingWithConfidenceImpl.java @@ -4,8 +4,9 @@ import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence; import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.SpeedWithBearing; import com.sap.sailing.domain.common.impl.Util.Triple; -import com.sap.sailing.domain.confidence.IsScalable; -import com.sap.sailing.domain.confidence.ScalableValue; +import com.sap.sailing.domain.common.scalablevalue.IsScalable; +import com.sap.sailing.domain.common.scalablevalue.ScalableValue; +import com.sap.sailing.domain.common.scalablevalue.impl.ScalableSpeedWithBearing; public class SpeedWithBearingWithConfidenceImpl extends HasConfidenceImpl, SpeedWithBearing, RelativeTo> implements diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/SpeedWithConfidenceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/SpeedWithConfidenceImpl.java index 1e4d7815607..979049aa222 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/SpeedWithConfidenceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/SpeedWithConfidenceImpl.java @@ -2,7 +2,8 @@ package com.sap.sailing.domain.base.impl; import com.sap.sailing.domain.base.SpeedWithConfidence; import com.sap.sailing.domain.common.Speed; -import com.sap.sailing.domain.confidence.ScalableValue; +import com.sap.sailing.domain.common.scalablevalue.ScalableValue; +import com.sap.sailing.domain.common.scalablevalue.impl.ScalableSpeed; public class SpeedWithConfidenceImpl extends HasConfidenceImpl implements SpeedWithConfidence { private static final long serialVersionUID = -3122580369653833748L; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/HasConfidence.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/HasConfidence.java index c4703252a18..b488c013080 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/HasConfidence.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/HasConfidence.java @@ -4,7 +4,7 @@ import java.io.Serializable; import com.sap.sailing.domain.common.Distance; import com.sap.sailing.domain.common.TimePoint; -import com.sap.sailing.domain.tracking.GPSFixTrack; +import com.sap.sailing.domain.common.scalablevalue.ScalableValue; /** * Some values, particularly those obtained from real-world measurements, are not always accurate. Some values are @@ -72,7 +72,7 @@ public interface HasConfidence extends Serializ /** * The confidence attached to a value is usually relative to some reference point, such as a time point or - * a position. For example, when a {@link GPSFixTrack} is asked to deliver an estimation for the tracked item's + * a position. For example, when a GPSFixTrack is asked to deliver an estimation for the tracked item's * {@link Position} at some given {@link TimePoint}, the track computes some average from a number of GPS fixes. The resulting * position has a certain confidence, depending on the time differences between the fixes and the time point for which * the position estimation was requested. The result therefore carries this reference time point for which the estimation diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/HasConfidenceAndIsScalable.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/HasConfidenceAndIsScalable.java index 373bd7c8e6a..cdc5edbbdab 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/HasConfidenceAndIsScalable.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/HasConfidenceAndIsScalable.java @@ -1,5 +1,7 @@ package com.sap.sailing.domain.confidence; +import com.sap.sailing.domain.common.scalablevalue.IsScalable; + public interface HasConfidenceAndIsScalable extends IsScalable, HasConfidence { } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/impl/ConfidenceBasedAveragerImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/impl/ConfidenceBasedAveragerImpl.java index 7ce80252ab9..1104c24f8f7 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/impl/ConfidenceBasedAveragerImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/impl/ConfidenceBasedAveragerImpl.java @@ -2,10 +2,10 @@ package com.sap.sailing.domain.confidence.impl; import com.sap.sailing.domain.base.impl.HasConfidenceImpl; import com.sap.sailing.domain.common.impl.Util; +import com.sap.sailing.domain.common.scalablevalue.ScalableValue; import com.sap.sailing.domain.confidence.ConfidenceBasedAverager; import com.sap.sailing.domain.confidence.HasConfidence; import com.sap.sailing.domain.confidence.HasConfidenceAndIsScalable; -import com.sap.sailing.domain.confidence.ScalableValue; import com.sap.sailing.domain.confidence.Weigher; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/impl/ScalableDouble.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/impl/ScalableDouble.java index 3727713e958..ae6ab5eb3a7 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/impl/ScalableDouble.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/confidence/impl/ScalableDouble.java @@ -1,7 +1,7 @@ package com.sap.sailing.domain.confidence.impl; -import com.sap.sailing.domain.confidence.AbstractScalarValue; -import com.sap.sailing.domain.confidence.ScalableValue; +import com.sap.sailing.domain.common.scalablevalue.AbstractScalarValue; +import com.sap.sailing.domain.common.scalablevalue.ScalableValue; public class ScalableDouble implements AbstractScalarValue { private final double value; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/Leaderboard.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/Leaderboard.java index bbeae7174cb..9e968758d4c 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/Leaderboard.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/Leaderboard.java @@ -13,6 +13,7 @@ import com.sap.sailing.domain.base.RaceColumn; import com.sap.sailing.domain.base.RaceColumnListener; import com.sap.sailing.domain.base.Series; import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.Duration; import com.sap.sailing.domain.common.LegType; import com.sap.sailing.domain.common.MaxPointsReason; import com.sap.sailing.domain.common.Named; @@ -70,8 +71,9 @@ public interface Leaderboard extends Named { } LeaderboardDTO computeDTO(final TimePoint timePoint, - final Collection namesOfRaceColumnsForWhichToLoadLegDetails, final boolean waitForLatestAnalyses, TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory) - throws NoWindException; + final Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, + final boolean waitForLatestAnalyses, TrackedRegattaRegistry trackedRegattaRegistry, + DomainFactory baseDomainFactory) throws NoWindException; /** * Obtains the unique set of {@link Competitor} objects from all {@link TrackedRace}s currently linked to this @@ -386,7 +388,7 @@ public interface Leaderboard extends Named { * or the competitor hasn't started sailing a single downwind leg at timePoint for any of the tracked * races attached to this leaderboard */ - Long getTotalTimeSailedInLegTypeInMilliseconds(Competitor competitor, LegType legType, TimePoint timePoint) throws NoWindException; + Duration getTotalTimeSailedInLegType(Competitor competitor, LegType legType, TimePoint timePoint) throws NoWindException; /** * Starts counting when the gun goes off, not when the competitor passed the line. @@ -395,7 +397,7 @@ public interface Leaderboard extends Named { * or the competitor hasn't started sailing a single race at timePoint for any of the tracked * races attached to this leaderboard */ - Long getTotalTimeSailedInMilliseconds(Competitor competitor, TimePoint timePoint); + Duration getTotalTimeSailed(Competitor competitor, TimePoint timePoint); /** * Computes the distance the competitor has sailed in the tracked races in this leaderboard, starting @@ -451,6 +453,7 @@ public interface Leaderboard extends Named { * point at which to evaluate the leaderboard status * @param namesOfRaceColumnsForWhichToLoadLegDetails * the names of the race columns of which to expand the details in the result + * @param addOverallDetails TODO * @param trackedRegattaRegistry * used to determine which of the races are still being tracked and which ones are not * @param baseDomainFactory @@ -458,7 +461,7 @@ public interface Leaderboard extends Named { */ LeaderboardDTO getLeaderboardDTO(TimePoint timePoint, Collection namesOfRaceColumnsForWhichToLoadLegDetails, - TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory) throws NoWindException, + boolean addOverallDetails, TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory) throws NoWindException, InterruptedException, ExecutionException; NumberOfCompetitorsInLeaderboardFetcher getNumberOfCompetitorsInLeaderboardFetcher(); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/LeaderboardCacheManager.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/LeaderboardCacheManager.java index 4e92d3e1f1c..a9db6d4d6a9 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/LeaderboardCacheManager.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/LeaderboardCacheManager.java @@ -7,11 +7,15 @@ import java.util.Set; import java.util.WeakHashMap; import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CompetitorChangeListener; import com.sap.sailing.domain.base.Fleet; import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.Nationality; import com.sap.sailing.domain.base.RaceColumn; import com.sap.sailing.domain.base.RaceColumnListener; import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.base.WithNationality; +import com.sap.sailing.domain.common.Color; import com.sap.sailing.domain.common.MaxPointsReason; import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.WindSource; @@ -39,7 +43,8 @@ import com.sap.sailing.util.impl.NamedReentrantReadWriteLock; */ public class LeaderboardCacheManager { private final WeakHashMap scoreCorrectionListeners; - private final NamedReentrantReadWriteLock scoreCorrectionListenersLock; + private final NamedReentrantReadWriteLock scoreCorrectionAndCompetitorChangeListenersLock; + private final WeakHashMap competitorChangeListeners; private final WeakHashMap>> invalidationListenersPerLeaderboard; private final WeakHashMap raceColumnListeners; @@ -113,6 +118,64 @@ public class LeaderboardCacheManager { } } + private class CacheInvalidationUponCompetitorChangeListener implements CompetitorChangeListener { + private static final long serialVersionUID = 8308312509904366143L; + private final Leaderboard leaderboard; + private final Set observedCompetitors; + + /** + * Registers this listener as listener for all of the leaderboard's current + * {@link Leaderboard#getCompetitors() competitors} + */ + public CacheInvalidationUponCompetitorChangeListener(Leaderboard leaderboard) { + this.leaderboard = leaderboard; + observedCompetitors = new HashSet<>(); + updateCompetitorListeners(); + } + + @Override + public void sailIdChanged(String oldSailId, String newSailId) { + removeFromCache(leaderboard); + } + + @Override + public void nationalityChanged(WithNationality what, Nationality oldNationality, Nationality newNationality) { + removeFromCache(leaderboard); + } + + @Override + public void colorChanged(Color oldColor, Color newColor) { + removeFromCache(leaderboard); + } + + @Override + public void nameChanged(String oldName, String newName) { + removeFromCache(leaderboard); + } + + public synchronized void updateCompetitorListeners() { + Set competitorsToStopObserving = new HashSet<>(observedCompetitors); + for (Competitor competitor : leaderboard.getCompetitors()) { + if (!observedCompetitors.contains(competitor)) { + competitor.addCompetitorChangeListener(this); // start observing competitor + } else { + competitorsToStopObserving.remove(competitor); // keep observing competitor + } + } + for (Competitor competitorToStopObserving : competitorsToStopObserving) { + competitorToStopObserving.removeCompetitorChangeListener(this); + observedCompetitors.remove(competitorToStopObserving); + } + } + + public synchronized void removeFromAllCompetitors() { + for (Competitor competitor : observedCompetitors) { + competitor.removeCompetitorChangeListener(this); + } + } + + } + private class CacheInvalidationUponScoreCorrectionListener implements ScoreCorrectionListener { private final Leaderboard leaderboard; @@ -146,10 +209,12 @@ public class LeaderboardCacheManager { public LeaderboardCacheManager(LeaderboardCache leaderboardCache) { this.leaderboardCache = leaderboardCache; - this.invalidationListenersPerLeaderboard = new WeakHashMap>>(); + this.invalidationListenersPerLeaderboard = new WeakHashMap<>(); this.raceColumnListeners = new WeakHashMap(); - this.scoreCorrectionListeners = new WeakHashMap(); - this.scoreCorrectionListenersLock = new NamedReentrantReadWriteLock("Score correction listeners", /* fair */ false); + this.scoreCorrectionListeners = new WeakHashMap<>(); + this.scoreCorrectionAndCompetitorChangeListenersLock = new NamedReentrantReadWriteLock( + "Score correction and competitor change listeners", /* fair */false); + this.competitorChangeListeners = new WeakHashMap<>(); } private void removeFromCache(Leaderboard leaderboard) { @@ -167,14 +232,21 @@ public class LeaderboardCacheManager { synchronized (raceColumnListeners) { leaderboard.removeRaceColumnListener(raceColumnListeners.remove(leaderboard)); } - LockUtil.lockForWrite(scoreCorrectionListenersLock); + LockUtil.lockForWrite(scoreCorrectionAndCompetitorChangeListenersLock); final CacheInvalidationUponScoreCorrectionListener removedScoreCorrectionListener; + final CacheInvalidationUponCompetitorChangeListener removedCompetitorChangeListener; try { removedScoreCorrectionListener = scoreCorrectionListeners.remove(leaderboard); + removedCompetitorChangeListener = competitorChangeListeners.remove(leaderboard); } finally { - LockUtil.unlockAfterWrite(scoreCorrectionListenersLock); + LockUtil.unlockAfterWrite(scoreCorrectionAndCompetitorChangeListenersLock); + } + if (removedScoreCorrectionListener != null) { + leaderboard.getScoreCorrection().removeScoreCorrectionListener(removedScoreCorrectionListener); + } + if (removedCompetitorChangeListener != null) { + removedCompetitorChangeListener.removeFromAllCompetitors(); } - leaderboard.getScoreCorrection().removeScoreCorrectionListener(removedScoreCorrectionListener); // invalidate after the listeners have been removed; this is important because invalidation may trigger a // re-calculation which then in turn may call add(leaderboard) asynchronously again which may be executed // before the listener removal happens here. This could lead to a race condition where listeners are @@ -195,22 +267,25 @@ public class LeaderboardCacheManager { private void registerAsListener(final Leaderboard leaderboard) { // only add as listener again if not yet added final boolean containsKey; - LockUtil.lockForRead(scoreCorrectionListenersLock); + LockUtil.lockForRead(scoreCorrectionAndCompetitorChangeListenersLock); try { containsKey = scoreCorrectionListeners.containsKey(leaderboard); } finally { - LockUtil.unlockAfterRead(scoreCorrectionListenersLock); + LockUtil.unlockAfterRead(scoreCorrectionAndCompetitorChangeListenersLock); } if (!containsKey) { - LockUtil.lockForWrite(scoreCorrectionListenersLock); + LockUtil.lockForWrite(scoreCorrectionAndCompetitorChangeListenersLock); try { for (TrackedRace trackedRace : leaderboard.getTrackedRaces()) { registerListener(leaderboard, trackedRace); } final CacheInvalidationUponScoreCorrectionListener scoreCorrectionListener = new CacheInvalidationUponScoreCorrectionListener( leaderboard); + final CacheInvalidationUponCompetitorChangeListener competitorChangeListener = new CacheInvalidationUponCompetitorChangeListener( + leaderboard); leaderboard.getScoreCorrection().addScoreCorrectionListener(scoreCorrectionListener); scoreCorrectionListeners.put(leaderboard, scoreCorrectionListener); + competitorChangeListeners.put(leaderboard, competitorChangeListener); final RaceColumnListener raceColumnListener = new RaceColumnListener() { private static final long serialVersionUID = 8165124797028386317L; @@ -312,7 +387,7 @@ public class LeaderboardCacheManager { raceColumnListeners.put(leaderboard, raceColumnListener); } } finally { - LockUtil.unlockAfterWrite(scoreCorrectionListenersLock); + LockUtil.unlockAfterWrite(scoreCorrectionAndCompetitorChangeListenersLock); } } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/caching/LeaderboardDTOCache.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/caching/LeaderboardDTOCache.java index af722f75216..3190c45bf84 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/caching/LeaderboardDTOCache.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/caching/LeaderboardDTOCache.java @@ -19,8 +19,7 @@ import com.sap.sailing.domain.base.RaceColumnListener; import com.sap.sailing.domain.common.NoWindException; import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.dto.LeaderboardDTO; -import com.sap.sailing.domain.common.impl.Util; -import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.common.impl.Util.Triple; import com.sap.sailing.domain.leaderboard.Leaderboard; import com.sap.sailing.domain.leaderboard.LeaderboardCacheManager; import com.sap.sailing.domain.leaderboard.ScoreCorrection; @@ -47,13 +46,15 @@ public class LeaderboardDTOCache implements LeaderboardCache { private static final Logger logger = Logger.getLogger(LeaderboardDTOCache.class.getName()); /** - * In live operations, {@link #getLeaderboardByName(Date, Collection, DomainFactory, TrackedRegattaRegistry)} is the application's + * In live operations, {@link #getLeaderboardByName(Date, Collection, boolean, DomainFactory, TrackedRegattaRegistry)} 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. + * all clients asking the same wait for the single result. Results are cached in this LRU-based evicting cache. The key is + * the time point for the query, the names of the races for which details are requested and the flag telling + * whether overall details for the leaderboard are requested. */ - private final Map>, FutureTask> leaderboardCache; + private final Map, Boolean>, FutureTask> leaderboardCache; private final NamedReentrantReadWriteLock leaderboardCacheLock; private int leaderboardByNameCacheHitCount; private int leaderboardByNameCacheMissCount; @@ -83,10 +84,10 @@ public class LeaderboardDTOCache implements LeaderboardCache { this.leaderboard = leaderboard; this.waitForLatestAnalyses = waitForLatestAnalyses; // Note: don't use access-based ordering as it turns the get(...) call into a "write" access - this.leaderboardCache = new LinkedHashMap>, FutureTask>(16, 0.75f) { + this.leaderboardCache = new LinkedHashMap, Boolean>, FutureTask>(16, 0.75f) { private static final long serialVersionUID = 7287916997229815039L; @Override - protected boolean removeEldestEntry(Map.Entry>, FutureTask> e) { + protected boolean removeEldestEntry(Map.Entry, Boolean>, FutureTask> e) { return size() > 10; // remember 10 LeaderboardDTOs per leaderborad } }; @@ -118,10 +119,14 @@ public class LeaderboardDTOCache implements LeaderboardCache { * The {@link #waitForLatestAnalyses} field is passed on to * {@link SailingServiceImpl#computeLeaderboardByName(Leaderboard, TimePoint, Collection, boolean)} if a new cache * entry needs to be computed. Caching distinguished between + * + * @param addOverallDetails + * tells whether the columns containing the overall details shall be added */ - public LeaderboardDTO getLeaderboardByName(final TimePoint timePoint, final Collection namesOfRaceColumnsForWhichToLoadLegDetails, - final DomainFactory baseDomainFactory, final TrackedRegattaRegistry trackedRegattaRegistry) throws NoWindException, InterruptedException, - ExecutionException { + public LeaderboardDTO getLeaderboardByName(final TimePoint timePoint, + final Collection namesOfRaceColumnsForWhichToLoadLegDetails, final boolean addOverallDetails, + final DomainFactory baseDomainFactory, final TrackedRegattaRegistry trackedRegattaRegistry) + throws NoWindException, InterruptedException, ExecutionException { long startOfRequestHandling = System.currentTimeMillis(); final TimePoint adjustedTimePoint; TimePoint timePointOfLastModification = leaderboard.getTimePointOfLatestModification(); @@ -131,8 +136,8 @@ public class LeaderboardDTOCache implements LeaderboardCache { } else { adjustedTimePoint = timePoint; } - Util.Pair> key = new Util.Pair>(adjustedTimePoint, - namesOfRaceColumnsForWhichToLoadLegDetails); + Triple, Boolean> key = new Triple, Boolean>(adjustedTimePoint, + namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails); FutureTask future = null; boolean cacheHit = false; /* @@ -161,8 +166,8 @@ public class LeaderboardDTOCache implements LeaderboardCache { LockUtil.propagateLockSetFrom(callerThread); try { LeaderboardDTO result = leaderboard.computeDTO(adjustedTimePoint, - namesOfRaceColumnsForWhichToLoadLegDetails, waitForLatestAnalyses, - trackedRegattaRegistry, baseDomainFactory); + namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, + waitForLatestAnalyses, trackedRegattaRegistry, baseDomainFactory); return result; } finally { LockUtil.unpropagateLockSetFrom(callerThread); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/caching/LiveLeaderboardUpdater.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/caching/LiveLeaderboardUpdater.java index 266aa1093a7..298876d4a76 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/caching/LiveLeaderboardUpdater.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/caching/LiveLeaderboardUpdater.java @@ -71,11 +71,13 @@ public class LiveLeaderboardUpdater implements Runnable { */ private final Set columnNamesForWhichCurrentLiveLeaderboardHasTheDetails; + private boolean currentLiveLeaderboardHasOverallDetails; + private boolean running; /** * For each String from namesOfRaceColumnsForWhichToLoadLegDetails passed to - * {@link #getLiveLeaderboard(Collection)}, records the validity time point of the last request here when the + * {@link #getLiveLeaderboard(Collection, boolean)}, records the validity time point of the last request here when the * {@link #currentLiveLeaderboard} has been updated with results containing this column details. This updater will * stop computing the details for that column if the validity time for the update calculation is more than * {@link #UPDATE_TIMEOUT_IN_MILLIS} milliseconds after the time point recorded here. @@ -84,7 +86,12 @@ public class LiveLeaderboardUpdater implements Runnable { private final Map timePointOfLastRequestForColumnDetails; /** - * As soon as the first {@link #getLiveLeaderboard(Collection)} request has been received, this field tells the + * Tells when {@link #updateRequestTimes(Collection, boolean)} was last called with the request for overall details + */ + private TimePoint timePointOfLastRequestForOverallDetails; + + /** + * As soon as the first {@link #getLiveLeaderboard(Collection, boolean)} request has been received, this field tells the * "validity time point" (as opposed to the request time point; so not when the request was received but for which * time point the request was asking the data) for which the last general request asked the leaderboard contents, * regardless the combination of column details requested. This is used to decide when to stop a thread running this @@ -117,14 +124,15 @@ public class LiveLeaderboardUpdater implements Runnable { return leaderboard; } - public LeaderboardDTO getLiveLeaderboard(Collection namesOfRaceColumnsForWhichToLoadLegDetails) throws NoWindException { + public LeaderboardDTO getLiveLeaderboard(Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails) throws NoWindException { LeaderboardDTO result = null; - updateRequestTimes(namesOfRaceColumnsForWhichToLoadLegDetails); + updateRequestTimes(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails); ensureRunning(); synchronized (this) { if (running && columnNamesForWhichCurrentLiveLeaderboardHasTheDetails - .containsAll(namesOfRaceColumnsForWhichToLoadLegDetails)) { + .containsAll(namesOfRaceColumnsForWhichToLoadLegDetails) + && (!addOverallDetails || currentLiveLeaderboardHasOverallDetails)) { result = currentLiveLeaderboard; if (result != null) { cacheHitCount++; @@ -140,12 +148,13 @@ public class LiveLeaderboardUpdater implements Runnable { cacheMissCount++; synchronized (this) { while (result == null) { - if (columnNamesForWhichCurrentLiveLeaderboardHasTheDetails.containsAll(namesOfRaceColumnsForWhichToLoadLegDetails)) { + if (columnNamesForWhichCurrentLiveLeaderboardHasTheDetails.containsAll(namesOfRaceColumnsForWhichToLoadLegDetails) && + (!addOverallDetails || currentLiveLeaderboardHasOverallDetails)) { result = currentLiveLeaderboard; } if (result == null) { if (logger.isLoggable(Level.FINEST)) { - logger.finest("waiting for leaderboard for "+namesOfRaceColumnsForWhichToLoadLegDetails); + logger.finest("waiting for leaderboard for "+namesOfRaceColumnsForWhichToLoadLegDetails+" and addOverallDetails="+addOverallDetails); } ensureRunning(); try { @@ -153,26 +162,27 @@ public class LiveLeaderboardUpdater implements Runnable { } catch (InterruptedException e) { logger.log(Level.INFO, "interrupted while waiting for LiveLeaderboardCache update", e); } - if (columnNamesForWhichCurrentLiveLeaderboardHasTheDetails.containsAll(namesOfRaceColumnsForWhichToLoadLegDetails)) { + if (columnNamesForWhichCurrentLiveLeaderboardHasTheDetails.containsAll(namesOfRaceColumnsForWhichToLoadLegDetails) && + (!addOverallDetails || currentLiveLeaderboardHasOverallDetails)) { result = currentLiveLeaderboard; if (logger.isLoggable(Level.FINEST)) { - logger.finest("successfully waited for leaderboard for "+namesOfRaceColumnsForWhichToLoadLegDetails); + logger.finest("successfully waited for leaderboard for "+namesOfRaceColumnsForWhichToLoadLegDetails+" and addOverallDetails="+addOverallDetails); } } else { if (logger.isLoggable(Level.FINEST)) { - logger.finest("waiting for leaderboard for "+namesOfRaceColumnsForWhichToLoadLegDetails+" unsuccessful. Need to try again..."); + logger.finest("waiting for leaderboard for "+namesOfRaceColumnsForWhichToLoadLegDetails+" and addOverallDetails="+addOverallDetails+" unsuccessful. Need to try again..."); } } } else { if (logger.isLoggable(Level.FINEST)) { - logger.finest("leaderboard for "+namesOfRaceColumnsForWhichToLoadLegDetails+" was provided in the meantime"); + logger.finest("leaderboard for "+namesOfRaceColumnsForWhichToLoadLegDetails+" and addOverallDetails="+addOverallDetails+" was provided in the meantime"); } } // now we either have a result (good, we're done), or the thread stopped running (then we need to renew the request) // or the thread still runs but the result may have expired and therefore be null (renew the request) if (result == null) { // need to renew the request - updateRequestTimes(namesOfRaceColumnsForWhichToLoadLegDetails); + updateRequestTimes(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails); ensureRunning(); } } @@ -205,8 +215,11 @@ public class LiveLeaderboardUpdater implements Runnable { * * This method assumes that namesOfRaceColumnsForWhichToLoadLegDetails tells the column names for which * result has the details. + * + * @param addOverallDetails tells whether overall details are requested for the resulting leaderboard; see also + * {@link Leaderboard#computeDTO(TimePoint, Collection, boolean, boolean, TrackedRegattaRegistry, DomainFactory)}. */ - private synchronized void updateRequestTimes(Collection namesOfRaceColumnsForWhichToLoadLegDetails) { + private synchronized void updateRequestTimes(Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails) { lastRequest = getLeaderboard().getNowMinusDelay(); for (String nameOfRaceColumn : namesOfRaceColumnsForWhichToLoadLegDetails) { if (!timePointOfLastRequestForColumnDetails.containsKey(nameOfRaceColumn) || @@ -214,22 +227,28 @@ public class LiveLeaderboardUpdater implements Runnable { timePointOfLastRequestForColumnDetails.put(nameOfRaceColumn, lastRequest); } } + if (addOverallDetails && (timePointOfLastRequestForOverallDetails == null || lastRequest.after(timePointOfLastRequestForOverallDetails))) { + timePointOfLastRequestForOverallDetails = lastRequest; + } } /** * Updates the cache contents and notifies all waiters on this object. */ private synchronized void updateCacheContents(Collection namesOfRaceColumnsForWhichToLoadLegDetails, - LeaderboardDTO result) { + boolean addOverallDetails, LeaderboardDTO result) { columnNamesForWhichCurrentLiveLeaderboardHasTheDetails.clear(); columnNamesForWhichCurrentLiveLeaderboardHasTheDetails.addAll(namesOfRaceColumnsForWhichToLoadLegDetails); currentLiveLeaderboard = result; + currentLiveLeaderboardHasOverallDetails = addOverallDetails; notifyAll(); } /** - * Keeps computing the leaderboard for the column names that appear as key in {@link #timePointOfLastRequestForColumnDetails} - * until no request has happened + * Keeps computing the leaderboard for the column names that appear as key in + * {@link #timePointOfLastRequestForColumnDetails} and for the overall details as noted in + * {@link #timePointOfLastRequestForOverallDetails} until no request has happened during the + * {@link #UPDATE_TIMEOUT_IN_MILLIS timeout period}. */ @Override public void run() { @@ -253,10 +272,11 @@ public class LiveLeaderboardUpdater implements Runnable { TimePoint timeLastUpdateWasStarted = now; try { final Set namesOfRaceColumnsForWhichToLoadLegDetails = getColumnNamesForWhichToFetchDetails(timePoint); + final boolean addOverallDetails = getOverallDetails(timePoint); LeaderboardDTO newCacheValue = leaderboard.computeDTO(timePoint, - namesOfRaceColumnsForWhichToLoadLegDetails, /* waitForLatestAnalyses */false, - trackedRegattaRegistry, baseDomainFactory); - updateCacheContents(namesOfRaceColumnsForWhichToLoadLegDetails, newCacheValue); + namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, + /* waitForLatestAnalyses */false, trackedRegattaRegistry, baseDomainFactory); + updateCacheContents(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, newCacheValue); } catch (NoWindException e) { logger.info("Unable to update cached leaderboard results for leaderboard " + leaderboard.getName() + ": " + e.getMessage()); @@ -290,6 +310,17 @@ public class LiveLeaderboardUpdater implements Runnable { } } + /** + * Determines based on {@link #timePointOfLastRequestForOverallDetails} and the timePoint parameter + * if the duration between then is less than {@link #UPDATE_TIMEOUT_IN_MILLIS} in which case overall details are to + * be loaded. If {@link #timePointOfLastRequestForOverallDetails} is null, meaning that overall details + * have never been requested for the leaderboard before, false is returned immediately. + */ + private boolean getOverallDetails(TimePoint timePoint) { + return timePointOfLastRequestForOverallDetails != null && + !timePoint.after(timePointOfLastRequestForOverallDetails.plus(UPDATE_TIMEOUT_IN_MILLIS)); + } + /** * Determines those column names from {@link #timePointOfLastRequestForColumnDetails}'s keys for which the last * request is less then {@link #UPDATE_TIMEOUT_IN_MILLIS} milliseconds before timePoint diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java index 072f7ed20a3..d2d529e8971 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java @@ -39,6 +39,7 @@ import com.sap.sailing.domain.base.RaceColumn; import com.sap.sailing.domain.base.RaceColumnListener; import com.sap.sailing.domain.base.Waypoint; import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.Duration; import com.sap.sailing.domain.common.LegType; import com.sap.sailing.domain.common.ManeuverType; import com.sap.sailing.domain.common.MaxPointsReason; @@ -56,7 +57,6 @@ import com.sap.sailing.domain.common.dto.LeaderboardDTO; import com.sap.sailing.domain.common.dto.LeaderboardEntryDTO; import com.sap.sailing.domain.common.dto.LeaderboardRowDTO; import com.sap.sailing.domain.common.dto.LegEntryDTO; -import com.sap.sailing.domain.common.dto.RaceColumnDTO; import com.sap.sailing.domain.common.dto.RaceDTO; import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; import com.sap.sailing.domain.common.impl.Util; @@ -911,8 +911,12 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race } @Override - public Long getTotalTimeSailedInLegTypeInMilliseconds(Competitor competitor, LegType legType, TimePoint timePoint) throws NoWindException { - Long result = null; + public Duration getTotalTimeSailedInLegType(Competitor competitor, LegType legType, TimePoint timePoint) throws NoWindException { + return getTotalTimeSailedInLegType(competitor, legType, timePoint, new HashMap()); + } + + private Duration getTotalTimeSailedInLegType(Competitor competitor, LegType legType, TimePoint timePoint, Map legTypeCache) throws NoWindException { + Duration result = null; // TODO should we ensure that competitor participated in all race columns? outerLoop: for (TrackedRace trackedRace : getTrackedRaces()) { @@ -920,26 +924,33 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race trackedRace.getRace().getCourse().lockForRead(); try { for (Leg leg : trackedRace.getRace().getCourse().getLegs()) { - TrackedLegOfCompetitor trackedLeg = trackedRace.getTrackedLeg(competitor, leg); - if (trackedLeg.hasStartedLeg(timePoint)) { + TrackedLegOfCompetitor trackedLegOfCompetitor = trackedRace.getTrackedLeg(competitor, leg); + if (trackedLegOfCompetitor.hasStartedLeg(timePoint)) { // find out leg type at the time the competitor started the leg try { - LegType trackedLegType = trackedRace.getTrackedLeg(leg).getLegType( - trackedLeg.getStartTime()); + final TrackedLeg trackedLeg = trackedRace.getTrackedLeg(leg); + LegType trackedLegType = legTypeCache.get(trackedLeg); + if (trackedLegType == null) { + final TimePoint startTime = trackedLegOfCompetitor.getStartTime(); + TimePoint finishTime = trackedLegOfCompetitor.getFinishTime(); + if (finishTime == null) { + finishTime = timePoint; + } + trackedLegType = trackedLeg.getLegType(startTime.plus(startTime.until(finishTime).divide(2))); // middle of the leg + legTypeCache.put(trackedLeg, trackedLegType); + } if (legType == trackedLegType) { - Long millisecondsSpendOnDownwind = trackedLeg.getTimeInMilliSeconds(timePoint); - if (millisecondsSpendOnDownwind != null) { + Duration timeSpentOnDownwind = trackedLegOfCompetitor.getTime(timePoint); + if (timeSpentOnDownwind != null) { if (result == null) { - result = millisecondsSpendOnDownwind; + result = timeSpentOnDownwind; } else { - result += millisecondsSpendOnDownwind; + result = result.plus(timeSpentOnDownwind); } } else { // Although the competitor has started the leg, no value was produced. This - // means that - // the competitor didn't finish the leg before tracking ended. No useful value - // can - // be obtained for this competitor anymore. + // means that the competitor didn't finish the leg before tracking ended. No useful value + // can be obtained for this competitor anymore. result = null; break outerLoop; } @@ -960,8 +971,8 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race } @Override - public Long getTotalTimeSailedInMilliseconds(Competitor competitor, TimePoint timePoint) { - Long result = null; + public Duration getTotalTimeSailed(Competitor competitor, TimePoint timePoint) { + Duration result = null; for (TrackedRace trackedRace : getTrackedRaces()) { if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { NavigableSet markPassings = trackedRace.getMarkPassings(competitor); @@ -984,11 +995,11 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race to = timePoint; } } - long timeSpent = to.asMillis() - from.asMillis(); + Duration timeSpent = from.until(to); if (result == null) { result = timeSpent; } else { - result += timeSpent; + result=result.plus(timeSpent); } } } @@ -1091,12 +1102,11 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race @Override public LeaderboardDTO computeDTO(final TimePoint timePoint, - final Collection namesOfRaceColumnsForWhichToLoadLegDetails, final boolean waitForLatestAnalyses, - TrackedRegattaRegistry trackedRegattaRegistry, final DomainFactory baseDomainFactory) + final Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, + final boolean waitForLatestAnalyses, TrackedRegattaRegistry trackedRegattaRegistry, final DomainFactory baseDomainFactory) throws NoWindException { long startOfRequestHandling = System.currentTimeMillis(); - LeaderboardDTO result = null; - result = new LeaderboardDTO(this.getScoreCorrection().getTimePointOfLastCorrectionsValidity() == null ? null + final LeaderboardDTO result = new LeaderboardDTO(this.getScoreCorrection().getTimePointOfLastCorrectionsValidity() == null ? null : this.getScoreCorrection().getTimePointOfLastCorrectionsValidity().asDate(), this.getScoreCorrection() == null ? null : this.getScoreCorrection().getComment(), this .getScoringScheme().isHigherBetter(), new UUIDGenerator()); @@ -1106,28 +1116,53 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race for (Competitor suppressedCompetitor : this.getSuppressedCompetitors()) { result.setSuppressed(baseDomainFactory.convertToCompetitorDTO(suppressedCompetitor), true); } - for (RaceColumn raceColumn : this.getRaceColumns()) { - RaceColumnDTO raceColumnDTO = result.createEmptyRaceColumn(raceColumn.getName(), raceColumn.isMedalRace(), + // Now create the race columns and, as a future task, set their competitorsFromBestToWorst, then wait for all these + // futures to finish: + Map>> competitorsFromBestToWorstTasks = new HashMap<>(); + for (final RaceColumn raceColumn : this.getRaceColumns()) { + result.createEmptyRaceColumn(raceColumn.getName(), raceColumn.isMedalRace(), this.getScoringScheme().isValidInTotalScore(this, raceColumn, timePoint)); for (Fleet fleet : raceColumn.getFleets()) { RegattaAndRaceIdentifier raceIdentifier = null; RaceDTO race = null; TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - final FleetDTO fleetDTO = baseDomainFactory.convertToFleetDTO(fleet); if (trackedRace != null) { raceIdentifier = new RegattaNameAndRaceName(trackedRace.getTrackedRegatta().getRegatta().getName(), trackedRace.getRace().getName()); race = baseDomainFactory.createRaceDTO(trackedRegattaRegistry, /* withGeoLocationData */ false, raceIdentifier, trackedRace); } - // 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(), raceColumn.getFactor(), fleetDTO, raceColumn.isMedalRace(), raceIdentifier, race); } - result.setCompetitorsFromBestToWorst(raceColumnDTO, - baseDomainFactory.getCompetitorDTOList(this.getCompetitorsFromBestToWorst(raceColumn, timePoint))); + FutureTask> task = new FutureTask>(new Callable>() { + @Override + public List call() throws Exception { + return baseDomainFactory.getCompetitorDTOList(AbstractSimpleLeaderboardImpl.this.getCompetitorsFromBestToWorst(raceColumn, timePoint)); + } + }); + executor.execute(task); + competitorsFromBestToWorstTasks.put(raceColumn, task); + } + // wait for the competitor orderings to have been computed for all race columns before continuing; subsequent tasks may depend on these data + for (Map.Entry>> raceColumnAndTaskToJoin : competitorsFromBestToWorstTasks.entrySet()) { + try { + result.setCompetitorsFromBestToWorst(raceColumnAndTaskToJoin.getKey().getName(), raceColumnAndTaskToJoin.getValue().get()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + // See also bug 1371: for stability reasons, don't let the exception percolate but rather accept + // null values. + // If new evidence is provided, a re-calculation of the leaderboard will be triggered anyway. So + // this helps robustness from a user's perspective. + logger.log( + Level.SEVERE, + AbstractSimpleLeaderboardImpl.class.getName() + ".computeDTO(" + this.getName() + ", " + + timePoint + ", " + namesOfRaceColumnsForWhichToLoadLegDetails+", addOverallDetails="+addOverallDetails + + "): exception during computing competitor ordering for race column "+raceColumnAndTaskToJoin.getKey().getName(), e); + } } result.setDelayToLiveInMillisForLatestRace(this.getDelayToLiveInMillis()); result.rows = new HashMap(); @@ -1139,8 +1174,7 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race result.discardThresholds = null; } // 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 + // details are requested only once and pass them into getLeaderboardEntryDTO final Map> legRanksCache = new HashMap>(); for (final RaceColumn raceColumn : this.getRaceColumns()) { // if details for the column are requested, cache the leg's ranks @@ -1168,7 +1202,9 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race row.fieldsByRaceColumnName = new HashMap(); row.carriedPoints = this.hasCarriedPoints(competitor) ? this.getCarriedPoints(competitor) : null; row.totalPoints = this.getTotalPoints(competitor, timePoint); - addOverallDetailsToRow(timePoint, competitor, row); + if (addOverallDetails) { + addOverallDetailsToRow(timePoint, competitor, row); + } result.competitors.add(competitorDTO); Map> futuresForColumnName = new HashMap>(); for (final RaceColumn raceColumn : this.getRaceColumns()) { @@ -1177,17 +1213,11 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race @Override public LeaderboardEntryDTO call() { try { - Entry entry = AbstractSimpleLeaderboardImpl.this.getEntry(competitor, raceColumn, - timePoint); - return getLeaderboardEntryDTO( - entry, - raceColumn, - competitor, - timePoint, + Entry entry = AbstractSimpleLeaderboardImpl.this.getEntry(competitor, raceColumn, timePoint); + return getLeaderboardEntryDTO(entry, raceColumn, competitor, timePoint, namesOfRaceColumnsForWhichToLoadLegDetails != null && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn - .getName()), waitForLatestAnalyses, legRanksCache, - baseDomainFactory); + .getName()), waitForLatestAnalyses, legRanksCache, baseDomainFactory); } catch (NoWindException e) { logger.info("Exception trying to compute leaderboard entry for competitor " + competitor.getName() + " in race column " + raceColumn.getName() + ": " @@ -1212,12 +1242,11 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race // See also bug 1371: for stability reasons, don't let the exception percolate but rather accept // null values. // If new evidence is provided, a re-calculation of the leaderboard will be triggered anyway. So - // this helps - // robustness from a user's perspective. + // this helps robustness from a user's perspective. logger.log( Level.SEVERE, AbstractSimpleLeaderboardImpl.class.getName() + ".computeDTO(" + this.getName() + ", " - + timePoint + ", " + namesOfRaceColumnsForWhichToLoadLegDetails + + timePoint + ", " + namesOfRaceColumnsForWhichToLoadLegDetails+", addOverallDetails="+addOverallDetails + "): exception during computing leaderboard entry for competitor " + competitor.getName() + " in race column " + raceColumnNameAndFuture.getKey() + ". Leaving empty.", e); @@ -1229,8 +1258,9 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race result.competitorDisplayNames.put(competitorDTO, displayName); } } - logger.info("computeLeaderboardByName("+this.getName()+", "+timePoint+", "+namesOfRaceColumnsForWhichToLoadLegDetails+") took "+ - (System.currentTimeMillis()-startOfRequestHandling)+"ms"); + logger.info("computeLeaderboardByName(" + this.getName() + ", " + timePoint + ", " + + namesOfRaceColumnsForWhichToLoadLegDetails + ", addOverallDetails=" + addOverallDetails + ") took " + + (System.currentTimeMillis() - startOfRequestHandling) + "ms"); return result; } @@ -1241,14 +1271,15 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race row.maximumSpeedOverGroundInKnots = maximumSpeedOverGround.getB().getKnots(); row.whenMaximumSpeedOverGroundWasAchieved = maximumSpeedOverGround.getA().getTimePoint().asDate(); } - final Long totalTimeSailedDownwindInMilliseconds = this.getTotalTimeSailedInLegTypeInMilliseconds(competitor, LegType.DOWNWIND, timePoint); - row.totalTimeSailedDownwindInSeconds = totalTimeSailedDownwindInMilliseconds==null?null:1./1000.*totalTimeSailedDownwindInMilliseconds; - final Long totalTimeSailedUpwindInMilliseconds = this.getTotalTimeSailedInLegTypeInMilliseconds(competitor, LegType.UPWIND, timePoint); - row.totalTimeSailedUpwindInSeconds = totalTimeSailedUpwindInMilliseconds==null?null:1./1000.*totalTimeSailedUpwindInMilliseconds; - final Long totalTimeSailedReachingInMilliseconds = this.getTotalTimeSailedInLegTypeInMilliseconds(competitor, LegType.REACHING, timePoint); - row.totalTimeSailedReachingInSeconds = totalTimeSailedReachingInMilliseconds==null?null:1./1000.*totalTimeSailedReachingInMilliseconds; - final Long totalTimeSailedInMilliseconds = this.getTotalTimeSailedInMilliseconds(competitor, timePoint); - row.totalTimeSailedInSeconds = totalTimeSailedInMilliseconds==null?null:1./1000.*totalTimeSailedInMilliseconds; + Map legTypeCache = new HashMap<>(); + final Duration totalTimeSailedDownwind = this.getTotalTimeSailedInLegType(competitor, LegType.DOWNWIND, timePoint, legTypeCache); + row.totalTimeSailedDownwindInSeconds = totalTimeSailedDownwind==null?null:totalTimeSailedDownwind.asSeconds(); + final Duration totalTimeSailedUpwind = this.getTotalTimeSailedInLegType(competitor, LegType.UPWIND, timePoint, legTypeCache); + row.totalTimeSailedUpwindInSeconds = totalTimeSailedUpwind==null?null:totalTimeSailedUpwind.asSeconds(); + final Duration totalTimeSailedReaching = this.getTotalTimeSailedInLegType(competitor, LegType.REACHING, timePoint, legTypeCache); + row.totalTimeSailedReachingInSeconds = totalTimeSailedReaching==null?null:totalTimeSailedReaching.asSeconds(); + final Duration totalTimeSailed = this.getTotalTimeSailed(competitor, timePoint); + row.totalTimeSailedInSeconds = totalTimeSailed==null?null:totalTimeSailed.asSeconds(); final Distance totalDistanceTraveledInMeters = this.getTotalDistanceTraveled(competitor, timePoint); row.totalDistanceTraveledInMeters = totalDistanceTraveledInMeters==null?null:totalDistanceTraveledInMeters.getMeters(); } @@ -1287,8 +1318,10 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race entryDTO.legDetails = raceDetails.getLegDetails(); entryDTO.windwardDistanceToOverallLeaderInMeters = raceDetails.getWindwardDistanceToOverallLeader() == null ? null : raceDetails.getWindwardDistanceToOverallLeader().getMeters(); - entryDTO.averageCrossTrackErrorInMeters = raceDetails.getAverageCrossTrackError() == null ? null - : raceDetails.getAverageCrossTrackError().getMeters(); + entryDTO.averageAbsoluteCrossTrackErrorInMeters = raceDetails.getAverageAbsoluteCrossTrackError() == null ? null + : raceDetails.getAverageAbsoluteCrossTrackError().getMeters(); + entryDTO.averageSignedCrossTrackErrorInMeters = raceDetails.getAverageSignedCrossTrackError() == null ? null + : raceDetails.getAverageSignedCrossTrackError().getMeters(); final TimePoint startOfRace = trackedRace.getStartOfRace(); if (startOfRace != null) { Waypoint startWaypoint = trackedRace.getRace().getCourse().getFirstWaypoint(); @@ -1365,13 +1398,15 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race private static class RaceDetails { private final List legDetails; private final Distance windwardDistanceToOverallLeader; - private final Distance averageCrossTrackError; + private final Distance averageAbsoluteCrossTrackError; + private final Distance averageSignedCrossTrackError; public RaceDetails(List legDetails, Distance windwardDistanceToOverallLeader, - Distance averageCrossTrackError) { + Distance averageAbsoluteCrossTrackError, Distance averageSignedCrossTrackError) { super(); this.legDetails = legDetails; this.windwardDistanceToOverallLeader = windwardDistanceToOverallLeader; - this.averageCrossTrackError = averageCrossTrackError; + this.averageAbsoluteCrossTrackError = averageAbsoluteCrossTrackError; + this.averageSignedCrossTrackError = averageSignedCrossTrackError; } public List getLegDetails() { return legDetails; @@ -1379,8 +1414,11 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race public Distance getWindwardDistanceToOverallLeader() { return windwardDistanceToOverallLeader; } - public Distance getAverageCrossTrackError() { - return averageCrossTrackError; + public Distance getAverageAbsoluteCrossTrackError() { + return averageAbsoluteCrossTrackError; + } + public Distance getAverageSignedCrossTrackError() { + return averageSignedCrossTrackError; } } @@ -1460,9 +1498,11 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race } final Distance windwardDistanceToOverallLeader = trackedRace == null ? null : trackedRace .getWindwardDistanceToOverallLeader(competitor, timePoint); - final Distance averageCrossTrackError = trackedRace == null ? null : trackedRace.getAverageCrossTrackError( + final Distance averageAbsoluteCrossTrackError = trackedRace == null ? null : trackedRace.getAverageAbsoluteCrossTrackError( competitor, timePoint, waitForLatestAnalyses); - return new RaceDetails(legDetails, windwardDistanceToOverallLeader, averageCrossTrackError); + final Distance averageSignedCrossTrackError = trackedRace == null ? null : trackedRace.getAverageSignedCrossTrackError( + competitor, timePoint, waitForLatestAnalyses); + return new RaceDetails(legDetails, windwardDistanceToOverallLeader, averageAbsoluteCrossTrackError, averageSignedCrossTrackError); } finally { course.unlockAfterRead(); } @@ -1478,8 +1518,10 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race result.legType = trackedLeg.getTrackedLeg().getLegType(timePoint); 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(); + final Distance averageAbsoluteCrossTrackError = trackedLeg.getAverageAbsoluteCrossTrackError(timePoint, waitForLatestAnalyses); + result.averageAbsoluteCrossTrackErrorInMeters = averageAbsoluteCrossTrackError == null ? null : averageAbsoluteCrossTrackError.getMeters(); + final Distance averageSignedCrossTrackError = trackedLeg.getAverageSignedCrossTrackError(timePoint, waitForLatestAnalyses); + result.averageSignedCrossTrackErrorInMeters = averageSignedCrossTrackError == null ? null : averageSignedCrossTrackError.getMeters(); Double speedOverGroundInKnots; if (trackedLeg.hasFinishedLeg(timePoint)) { speedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); @@ -1491,7 +1533,7 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race Distance distanceTraveled = trackedLeg.getDistanceTraveled(timePoint); result.distanceTraveledInMeters = distanceTraveled == null ? null : distanceTraveled.getMeters(); result.estimatedTimeToNextWaypointInSeconds = trackedLeg.getEstimatedTimeToNextMarkInSeconds(timePoint); - result.timeInMilliseconds = trackedLeg.getTimeInMilliSeconds(timePoint); + result.timeInMilliseconds = trackedLeg.getTime(timePoint).asMillis(); result.finished = trackedLeg.hasFinishedLeg(timePoint); result.gapToLeaderInSeconds = trackedLeg.getGapToLeaderInSeconds(timePoint, legRanksCache.get(trackedLeg.getLeg()).entrySet().iterator().next().getKey()); @@ -1624,10 +1666,10 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race } private LeaderboardDTO getLiveLeaderboard(Collection namesOfRaceColumnsForWhichToLoadLegDetails, - TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory) throws NoWindException { + boolean addOverallDetails, TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory) throws NoWindException { LiveLeaderboardUpdater liveLeaderboardUpdater = getLiveLeaderboardUpdater(trackedRegattaRegistry, baseDomainFactory); - return liveLeaderboardUpdater.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails); + return liveLeaderboardUpdater.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails); } private LiveLeaderboardUpdater getLiveLeaderboardUpdater(TrackedRegattaRegistry trackedRegattaRegistry, @@ -1664,8 +1706,10 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race } @Override - public LeaderboardDTO getLeaderboardDTO(TimePoint timePoint, Collection namesOfRaceColumnsForWhichToLoadLegDetails, - TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory) throws NoWindException, InterruptedException, ExecutionException { + public LeaderboardDTO getLeaderboardDTO(TimePoint timePoint, + Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, + TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory) throws NoWindException, + InterruptedException, ExecutionException { LeaderboardDTO result = null; if (timePoint == null) { // date==null means live mode; however, if we're after the end of all races and after all score @@ -1681,7 +1725,7 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race } else { // don't use the regular leaderboard cache; the race still seems to be on; use the live leaderboard updater instead: timePoint = null; - result = this.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, trackedRegattaRegistry, baseDomainFactory); + result = this.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, trackedRegattaRegistry, baseDomainFactory); } } if (timePoint != null) { @@ -1689,7 +1733,7 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race // which is OK because the cache is invalidated whenever any of the tracked races attached to the // leaderboard changes. result = getLeaderboardDTOCache().getLeaderboardByName(timePoint, namesOfRaceColumnsForWhichToLoadLegDetails, - baseDomainFactory, trackedRegattaRegistry); + addOverallDetails, baseDomainFactory, trackedRegattaRegistry); } return result; } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointFirstGetsFixedScore.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointFirstGetsFixedScore.java index 247e7ae14b1..af1244609fa 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointFirstGetsFixedScore.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointFirstGetsFixedScore.java @@ -24,11 +24,25 @@ public abstract class HighPointFirstGetsFixedScore extends HighPoint { public HighPointFirstGetsFixedScore(double scoreForRaceWinner) { this.scoreForRaceWinner = scoreForRaceWinner; } + + protected double getScoreForRaceWinner() { + return scoreForRaceWinner; + } @Override public Double getScoreForRank(RaceColumn raceColumn, Competitor competitor, int rank, Callable numberOfCompetitorsInRaceFetcher, NumberOfCompetitorsInLeaderboardFetcher numberOfCompetitorsInLeaderboardFetcher) { - final int effectiveRank; + final int effectiveRank = getEffectiveRank(raceColumn, competitor, rank); final Double result; + if (effectiveRank == 0) { + result = null; + } else { + result = Math.max(1.0, (double) (getScoreForRaceWinner() - effectiveRank + 1)); + } + return result; + } + + protected int getEffectiveRank(RaceColumn raceColumn, Competitor competitor, int rank) { + final int effectiveRank; int competitorFleetOrdering; if (rank == 0) { effectiveRank = 0; @@ -41,11 +55,6 @@ public abstract class HighPointFirstGetsFixedScore extends HighPoint { effectiveRank = rank; } } - if (effectiveRank == 0) { - result = null; - } else { - result = Math.max(1.0, (double) (scoreForRaceWinner - effectiveRank + 1)); - } - return result; + return effectiveRank; } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointWinnerGetsSix.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointWinnerGetsSix.java new file mode 100755 index 00000000000..bf116c8f4a0 --- /dev/null +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointWinnerGetsSix.java @@ -0,0 +1,16 @@ +package com.sap.sailing.domain.leaderboard.impl; + +import com.sap.sailing.domain.common.ScoringSchemeType; + +public class HighPointWinnerGetsSix extends HighPointFirstGetsFixedScore { + private static final long serialVersionUID = -4116295702464748719L; + + public HighPointWinnerGetsSix() { + super(/* scoreForRaceWinner */ 6); + } + + @Override + public ScoringSchemeType getType() { + return ScoringSchemeType.HIGH_POINT_WINNER_GETS_SIX; + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/Candidate.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/Candidate.java index 88f5cc28d66..041869b02c6 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/Candidate.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/Candidate.java @@ -6,27 +6,26 @@ import com.sap.sailing.domain.common.TimePoint; /** * A possible passing of a {@link Waypoint}. It contains the {@link Waypoint} that it might be passing, a * {@link TimePoint}, the probability that this candidate is a passing (e.g. based on the distance to the - * {@link Waypoint}) and the one-based(!) ID of this Waypoint. The ID is one based because the standard implemantation + * {@link Waypoint}) and the one-based(!) ID of this Waypoint. The ID is one based because the standard implementation * of {@link AbstractCandidateChooser} (see {@link CandidateChooser}) uses a proxy Candidates at the end and the - * beginning of the race, the one at the beginning recieves the ID 0. Candidates are created in an + * beginning of the race, the one at the beginning receives the ID 0. Candidates are created in an * {@link AbstractCandidateFinder}, which determines where a passing could be and assigns them * * @author Nicolas Klose * */ - public class Candidate implements Comparable { - private Waypoint w; - private TimePoint p; + private Waypoint waypoint; + private TimePoint timePoint; private double distanceProbability; private Integer id; private boolean rightSide; private boolean rightDirection; private String type; - public Candidate(int id, TimePoint p, double distanceProbability, Waypoint w, boolean rightSide, boolean rightDirection, String type) { - this.w = w; - this.p = p; + public Candidate(int id, TimePoint timePoint, double distanceProbability, Waypoint w, boolean rightSide, boolean rightDirection, String type) { + this.waypoint = w; + this.timePoint = timePoint; this.distanceProbability = distanceProbability; this.id = id; this.rightSide = rightSide; @@ -39,7 +38,7 @@ public class Candidate implements Comparable { } public TimePoint getTimePoint() { - return p; + return timePoint; } public Double getProbability() { @@ -49,15 +48,15 @@ public class Candidate implements Comparable { } public Waypoint getWaypoint() { - return w; + return waypoint; } public String toString() { - return type + "-Candidate for " + id + " with cost " + getProbability() + "and Timepoint " + p; + return type + "-Candidate for " + id + " with cost " + getProbability() + "and Timepoint " + timePoint; } @Override public int compareTo(Candidate arg0) { - return id != arg0.getID() ? id.compareTo(arg0.getID()) : p != arg0.getTimePoint() ? p.compareTo(arg0.getTimePoint()) : getProbability().compareTo(arg0.getProbability()); + return id != arg0.getID() ? id.compareTo(arg0.getID()) : timePoint != arg0.getTimePoint() ? timePoint.compareTo(arg0.getTimePoint()) : getProbability().compareTo(arg0.getProbability()); } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/CandidateChooser.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/CandidateChooser.java index 5a97cf70f40..7b33f328c6f 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/CandidateChooser.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/CandidateChooser.java @@ -68,7 +68,7 @@ public class CandidateChooser implements AbstractCandidateChooser { @Override public void calculateMarkPassDeltas(Competitor c, Pair, Iterable> candidateDeltas) { - if (race.getStartOfRace()!=null&&!race.getStartOfRace().minus(2000).equals(raceStartTime)) { + if (race.getStartOfRace() != null && (raceStartTime == null || !race.getStartOfRace().minus(2000).equals(raceStartTime))) { raceStartTime = race.getStartOfRace().minus(2000); for (Competitor com : allEdges.keySet()) { removeCandidates(Arrays.asList(start), com); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java index 323f7fc5e7b..13d99f47ca5 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java @@ -28,33 +28,33 @@ import com.sap.sailing.util.impl.ThreadFactoryWithPriority; /** * Calculates the {@link MarkPassing}s for a {@link DynamicTrackedRace} using a {@link AbstractCandidateFinder} and a - * {@link AbstractCandidateChooser}. The finder evaluates the fixes and finds possible MarkPassings as {@link Candidate} - * s. The chooser than finds the most likely sequence of {@link Candidate}s and creates the {@link MarkPassing}s for + * {@link AbstractCandidateChooser}. The finder evaluates the fixes and finds possible MarkPassings as {@link Candidate}s. + * The chooser then finds the most likely sequence of {@link Candidate}s and creates the {@link MarkPassing}s for * this sequence. If listen is true, a {@link MarkPassingUpdateListener} is initialized which puts new - * fixes into a queue. Additionally a new Thread is started, which takes the fixes out of the queue and sorts them so + * fixes into a queue. Additionally a new thread is started, which takes the fixes out of the queue and sorts them so * that each object (Competitor or Mark) has a list of Fixes in combinedFixes. If suspended is * false, the new Fixes are handed to the executor as FutureTasks, first to calculate the affected Fixes * and then the actual MarkPassings (See {@link CandidateFinder} and {@link CandidateChooser}). Then the listening * process begins again with the queue being emptied. This continues until the end Object is put in the - * queue by the {@link MarkPassingUpdateListener}, signalising that the race is over. + * queue by the {@link MarkPassingUpdateListener}, signaling that the race is over. * * @author Nicolas Klose * */ - public class MarkPassingCalculator { - private AbstractCandidateFinder finder; - private AbstractCandidateChooser chooser; private static final Logger logger = Logger.getLogger(MarkPassingCalculator.class.getName()); - private MarkPassingUpdateListener listener; private final static Pair end = new Pair(null, null); - private boolean suspended = false; private final static ExecutorService executor = new ThreadPoolExecutor(/* corePoolSize */Math.max(Runtime.getRuntime() .availableProcessors() - 1, 3), /* maximumPoolSize */Math.max(Runtime.getRuntime().availableProcessors() - 1, 3), /* keepAliveTime */60, TimeUnit.SECONDS, /* workQueue */new LinkedBlockingQueue(), new ThreadFactoryWithPriority(Thread.NORM_PRIORITY - 1)); + private MarkPassingUpdateListener listener; + private AbstractCandidateFinder finder; + private AbstractCandidateChooser chooser; + private boolean suspended = false; + public MarkPassingCalculator(DynamicTrackedRace race, boolean listen) { if (listen) { listener = new MarkPassingUpdateListener(race, end); @@ -70,7 +70,6 @@ public class MarkPassingCalculator { } private class Listen implements Runnable { - @Override public void run() { logger.fine("MarkPassingCalculator is listening for new Fixes."); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingUpdateListener.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingUpdateListener.java index 78b778d28b1..3a4e0f03b04 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingUpdateListener.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingUpdateListener.java @@ -21,9 +21,8 @@ import com.sap.sailing.domain.tracking.impl.AbstractRaceChangeListener; * */ public class MarkPassingUpdateListener extends AbstractRaceChangeListener { - private LinkedBlockingQueue> queue; + private final LinkedBlockingQueue> queue; private final Pair end; - int i = 0; public MarkPassingUpdateListener(TrackedRace race, Pair end) { race.addListener(this); @@ -47,7 +46,6 @@ public class MarkPassingUpdateListener extends AbstractRaceChangeListener { @Override public void statusChanged(TrackedRaceStatus newStatus) { - if (newStatus.getStatus() == TrackedRaceStatusEnum.FINISHED) { queue.add(end); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/racelog/tracking/EmptyGPSFixStore.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/racelog/tracking/EmptyGPSFixStore.java new file mode 100644 index 00000000000..ee117e51fe7 --- /dev/null +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/racelog/tracking/EmptyGPSFixStore.java @@ -0,0 +1,47 @@ +package com.sap.sailing.domain.racelog.tracking; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.tracking.DynamicGPSFixTrack; +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.domain.tracking.GPSFixMoving; + +public enum EmptyGPSFixStore implements GPSFixStore { + INSTANCE; + + @Override + public void storeFix(DeviceIdentifier device, GPSFix fix) { + } + + @Override + public void loadCompetitorTrack(DynamicGPSFixTrack track, + RaceLog raceLog, Competitor competitor) { + } + + @Override + public void loadMarkTrack(DynamicGPSFixTrack track, + RaceLog raceLog, Mark mark) { + } + + @Override + public void addListener(GPSFixReceivedListener listener) { + } + + @Override + public void removeListener(GPSFixReceivedListener listener) { + } + + @Override + public void loadCompetitorTrack( + DynamicGPSFixTrack track, + DeviceMapping mapping) { + + } + + @Override + public void loadMarkTrack(DynamicGPSFixTrack track, + DeviceMapping mapping) { + + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/racelog/tracking/GPSFixReceivedListener.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/racelog/tracking/GPSFixReceivedListener.java new file mode 100644 index 00000000000..f29eeefee6c --- /dev/null +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/racelog/tracking/GPSFixReceivedListener.java @@ -0,0 +1,7 @@ +package com.sap.sailing.domain.racelog.tracking; + +import com.sap.sailing.domain.tracking.GPSFix; + +public interface GPSFixReceivedListener { + void fixReceived(DeviceIdentifier device, GPSFix fix); +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/racelog/tracking/GPSFixStore.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/racelog/tracking/GPSFixStore.java new file mode 100644 index 00000000000..4ec2d7b07c8 --- /dev/null +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/racelog/tracking/GPSFixStore.java @@ -0,0 +1,42 @@ +package com.sap.sailing.domain.racelog.tracking; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.tracking.DynamicGPSFixTrack; +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.domain.tracking.GPSFixMoving; + + +public interface GPSFixStore { + /** + * Load all fixes that correspond to the {@link DeviceCompetitorMappingEvent}s found in the {@code raceLog}. + */ + void loadCompetitorTrack(DynamicGPSFixTrack track, RaceLog raceLog, + Competitor competitor); + + /** + * Load all fixes that correspond to the {@link DeviceMarkMappingEvent}s found in the {@code raceLog}. + */ + void loadMarkTrack(DynamicGPSFixTrack track, RaceLog raceLog, Mark mark); + + /** + * Load all fixes that correspond to the {@code mapping}. + */ + void loadCompetitorTrack(DynamicGPSFixTrack track, DeviceMapping mapping); + + /** + * Load all fixes that correspond to the {@code mapping}. + */ + void loadMarkTrack(DynamicGPSFixTrack track, DeviceMapping mapping); + + void storeFix(DeviceIdentifier device, GPSFix fix); + + /** + * Listeners are notified, whenever a {@link GPSFix} is stored through the {@link #storeFix(DeviceIdentifier, GPSFix)} + * method. + */ + void addListener(GPSFixReceivedListener listener); + + void removeListener(GPSFixReceivedListener listener); +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicTrackedRegatta.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicTrackedRegatta.java index 9db1d0850a3..1bd59be77e6 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicTrackedRegatta.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicTrackedRegatta.java @@ -2,6 +2,7 @@ package com.sap.sailing.domain.tracking; import com.sap.sailing.domain.base.RaceDefinition; import com.sap.sailing.domain.base.Sideline; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; public interface DynamicTrackedRegatta extends TrackedRegatta { @@ -19,7 +20,7 @@ public interface DynamicTrackedRegatta extends TrackedRegatta { * {@link DynamicRaceDefinitionSet}. */ DynamicTrackedRace createTrackedRace(RaceDefinition raceDefinition, Iterable sidelines, WindStore windStore, - long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed, + GPSFixStore gpsFixStore, long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed, DynamicRaceDefinitionSet raceDefinitionSetToUpdate); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/GPSTrackListener.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/GPSTrackListener.java index f119d623963..dd0671903b2 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/GPSTrackListener.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/GPSTrackListener.java @@ -1,14 +1,14 @@ -package com.sap.sailing.domain.tracking; - -import java.io.Serializable; - -public interface GPSTrackListener extends Serializable { - void gpsFixReceived(FixType fix, ItemType item); - - void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage); - - /** - * Listeners can use this to skip their serialization. - */ - boolean isTransient(); -} +package com.sap.sailing.domain.tracking; + +import java.io.Serializable; + +public interface GPSTrackListener extends Serializable { + void gpsFixReceived(FixType fix, ItemType item); + + void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage); + + /** + * Listeners can use this to skip their serialization. + */ + boolean isTransient(); +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTracker.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTracker.java index 0886240e765..da9b6df6f40 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTracker.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTracker.java @@ -5,9 +5,10 @@ import java.net.MalformedURLException; import java.util.Map; import java.util.Set; -import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; /** * Centerpiece of a tracking adapter. A tracker is responsible for receiving tracking data for one or more @@ -62,6 +63,8 @@ public interface RaceTracker { DynamicTrackedRegatta getTrackedRegatta(); WindStore getWindStore(); + + GPSFixStore getGPSFixStore(); /** * returns a unique key for this tracker which can, e.g., be used as a key in a {@link Map} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTrackingConnectivityParameters.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTrackingConnectivityParameters.java index db26b99d6ad..091a285fe9e 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTrackingConnectivityParameters.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTrackingConnectivityParameters.java @@ -3,6 +3,7 @@ package com.sap.sailing.domain.tracking; import com.sap.sailing.domain.base.Fleet; import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; @@ -20,7 +21,7 @@ public interface RaceTrackingConnectivityParameters { /** * Starts a {@link RaceTracker} using the connectivity parameters provided by this object. */ - RaceTracker createRaceTracker(TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore) throws Exception; + RaceTracker createRaceTracker(TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore, GPSFixStore gpsFixStore) throws Exception; /** * Starts a {@link RaceTracker}, associating the resulting races with the {@link Regatta} passed as argument @@ -28,7 +29,7 @@ public interface RaceTrackingConnectivityParameters { * parameters. This is particularly useful if a predefined regatta with {@link Series} and {@link Fleet}s * is to be used. */ - RaceTracker createRaceTracker(Regatta regatta, TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore) throws Exception; + RaceTracker createRaceTracker(Regatta regatta, TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore, GPSFixStore gpsFixStore) throws Exception; /** * Deliver an ID object equal to that of the {@link RaceTracker#getID()} delivered by the {@link RaceTracker} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RacesHandle.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RacesHandle.java index 50ff432189a..f1aa28916b1 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RacesHandle.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RacesHandle.java @@ -27,7 +27,7 @@ public interface RacesHandle { * Fetch the race definition. If the race definition represented by this handle hasn't been created yet, the call * blocks until such a definition is provided by another call, usually by the {@link RaceCourseReceiver}. If * timeoutInMilliseconds milliseconds have passed and the race definition is found not to have - * shown up until then, null is returned. The unblocking may be deferred even beyond + * shown up until then, a valid but empty set is returned. The unblocking may be deferred even beyond * timeoutInMilliseconds in case no modifications happen on the {@link DomainFactory}'s * set of races during that time. */ diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLeg.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLeg.java index 2778b93b749..438599a9814 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLeg.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLeg.java @@ -45,7 +45,18 @@ public interface TrackedLeg extends Serializable { Bearing getLegBearing(TimePoint at); - Distance getCrossTrackError(Position p, TimePoint timePoint); + /** + * Returns the positive (absolute) distance of p to this leg's course middle line at timePoint, + * based on the position of the waypoints delimiting this leg at that time. + */ + Distance getAbsoluteCrossTrackError(Position p, TimePoint timePoint); + + /** + * Returns the (signed) distance of p to this leg's course middle line at timePoint, + * based on the position of the waypoints delimiting this leg at that time. Negative distances mean "left" of the + * course middle line looking in the direction of the leg, positive distances mean "right." + */ + Distance getSignedCrossTrackError(Position p, TimePoint timePoint); /** * Must be called when the start and finish waypoint of this leg may have changed. diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLegOfCompetitor.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLegOfCompetitor.java index 7976545e2e2..500e05acbb3 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLegOfCompetitor.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedLegOfCompetitor.java @@ -7,6 +7,7 @@ import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.Leg; import com.sap.sailing.domain.common.Bearing; import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.Duration; import com.sap.sailing.domain.common.LegType; import com.sap.sailing.domain.common.NoWindException; import com.sap.sailing.domain.common.Speed; @@ -26,7 +27,7 @@ public interface TrackedLegOfCompetitor extends Serializable { * competitor to complete the leg is returned. If the competitor didn't finish the leg before the end of tracking, * null is returned because this indicates that the tracker stopped sending valid data. */ - Long getTimeInMilliSeconds(TimePoint timePoint); + Duration getTime(TimePoint timePoint); /** * The distance over ground traveled by the competitor in this leg up to timePoint. If @@ -176,11 +177,21 @@ public interface TrackedLegOfCompetitor extends Serializable { Distance getWindwardDistanceToOverallLeader(TimePoint timePoint) throws NoWindException; /** - * Computes the average cross track error for this leg. If you provide this method with a {@link TimePoint} greater - * than the time the mark passing of the leg end mark has occurred then the time point of the mark passing - * of the leg end mark will be taken into account. + * Computes the average absolute cross track error for this leg. The cross track error for each fix is taken to be a + * positive number, thereby ignoring whether the competitor was left or right of the course middle line. If you + * provide this method with a {@link TimePoint} greater than the time the mark passing of the leg end mark has + * occurred then the time point of the mark passing of the leg end mark will be taken into account. */ - Distance getAverageCrossTrackError(TimePoint timePoint, boolean waitForLatestAnalysis) throws NoWindException; + Distance getAverageAbsoluteCrossTrackError(TimePoint timePoint, boolean waitForLatestAnalysis) throws NoWindException; + + /** + * Computes the average signed cross track error for this leg. The cross track error for each fix is taken to be a + * positive number in case the competitor was on the right side of the course middle line (looking in the direction + * of this leg), and a negative number in case the competitor was on the left side of the course middle line. If you + * provide this method with a {@link TimePoint} greater than the time the mark passing of the leg end mark has + * occurred then the time point of the mark passing of the leg end mark will be taken into account. + */ + Distance getAverageSignedCrossTrackError(TimePoint timePoint, boolean waitForLatestAnalysis) throws NoWindException; /** * Computes the maneuver loss as the distance projected onto the average course between entering and exiting the diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java index 7e46831eefd..b3307a78ed0 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java @@ -29,6 +29,7 @@ import com.sap.sailing.domain.common.WindSourceType; import com.sap.sailing.domain.common.dto.TrackedRaceDTO; import com.sap.sailing.domain.common.impl.Util.Pair; import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; /** * Live tracking data of a single race. The race follows a defined {@link Course} with a sequence of {@link Leg}s. The @@ -442,9 +443,12 @@ public interface TrackedRace extends Serializable { * tracked race. This runs synchronized with the otherwise asynchronous loading of wind tracks, triggered by the * constructor of the {@link TrackedRace} implementation classes. This procedure guarantees that eventually the * listener will have received a notification for all wind fixes, regardless of whether they were already loaded at - * the time the listener is registered or they are loaded after the registration has completed. + * the time the listener is registered or they are loaded after the registration has completed.

+ * + * The same is true for the GPS fixes for marks and competitors. */ - void addListener(RaceChangeListener listener, boolean notifyAboutWindFixesAlreadyLoaded); + void addListener(RaceChangeListener listener, boolean notifyAboutWindFixesAlreadyLoaded, + boolean notifyAboutGPSFixesAlreadyLoaded); void removeListener(RaceChangeListener listener); @@ -496,9 +500,18 @@ public interface TrackedRace extends Serializable { * if true and any cache update is currently going on, wait for the update to complete and * then fetch the updated value; otherwise, serve this requests from whatever is currently in the cache */ - Distance getAverageCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) + Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) throws NoWindException; + Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, + boolean waitForLatestAnalyses) throws NoWindException; + + Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) + throws NoWindException; + + Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, + boolean waitForLatestAnalysis) throws NoWindException; + WindStore getWindStore(); Competitor getOverallLeader(TimePoint timePoint) throws NoWindException; @@ -509,16 +522,19 @@ public interface TrackedRace extends Serializable { */ List getCompetitorsFromBestToWorst(TimePoint timePoint) throws NoWindException; - Distance getAverageCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, - boolean waitForLatestAnalyses) throws NoWindException; + /** + * When provided with a {@link WindStore} during construction, the tracked race will + * asynchronously load the wind data for this tracked race from the wind store and the GPS store in a + * background thread and update this tracked race with the results. Clients that want to wait for the wind + * loading process to complete can do so by calling this method which will block until the wind loading has + * completed. + */ + void waitUntilLoadingFromWindStoreComplete() throws InterruptedException; /** - * When provided with a {@link WindStore} during construction, the tracked race will asynchronously load the wind - * data for this tracked race from the wind store in a background thread and update this tracked race with the - * results. Clients that want to wait for the wind loading process to complete can do so by calling this method - * which will block until the wind loading has completed. + * @see #waitUntilLoadingFromWindStoreComplete(), but for fixes from a {@link GPSFixStore} */ - void waitUntilWindLoadingComplete() throws InterruptedException; + void waitUntilLoadingFromGPSFixStoreComplete() throws InterruptedException; TrackedRaceStatus getStatus(); @@ -600,7 +616,7 @@ public interface TrackedRace extends Serializable { * or---if the start waypoint was a single mark---the distance to the single start mark at the timepoint. * If the competitor hasn't started yet, null is returned. * - */ + */ Distance getDistanceFromStarboardSideOfStartLine(Competitor competitor, TimePoint timePoint); /** @@ -612,7 +628,7 @@ public interface TrackedRace extends Serializable { * The speed of the competitor when crossing the start line. It will return null if there are no recorded * mark passings for this competitor (competitor did not yet or never pass the start line). */ - Speed getSpeedWhenCrossingStartLine(Competitor competitor); + Speed getSpeedWhenCrossingStartLine(Competitor competitor); /** * Start time received by the tracking infrastructure. To determine real start time use {@link #getStartOfRace()}. @@ -641,4 +657,6 @@ public interface TrackedRace extends Serializable { * a reference point. */ SpeedWithConfidence getAverageWindSpeedWithConfidence(long resolutionInMillis); + + GPSFixStore getGPSFixStore(); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRegatta.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRegatta.java index 078886cfd50..fa71f4adf61 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRegatta.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRegatta.java @@ -9,6 +9,7 @@ import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.base.Sideline; import com.sap.sailing.domain.common.NoWindException; import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; /** * Manages a set of {@link TrackedRace} objects that belong to the same {@link Regatta} (regatta, sailing regatta for a @@ -41,7 +42,7 @@ public interface TrackedRegatta extends Serializable { * {@link DynamicRaceDefinitionSet#addRaceDefinition(RaceDefinition, DynamicTrackedRace) added} to that object. */ DynamicTrackedRace createTrackedRace(RaceDefinition raceDefinition, Iterable sidelines, WindStore windStore, - long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed, + GPSFixStore gpsFixStore, long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed, DynamicRaceDefinitionSet raceDefinitionSetToUpdate); /** diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/BearingWithConfidenceCluster.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/BearingWithConfidenceCluster.java index 34086e0fc0f..daf5d7d2a17 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/BearingWithConfidenceCluster.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/BearingWithConfidenceCluster.java @@ -5,9 +5,9 @@ import java.util.Collections; import java.util.List; import com.sap.sailing.domain.base.BearingWithConfidence; -import com.sap.sailing.domain.base.BearingWithConfidence.DoublePair; import com.sap.sailing.domain.base.impl.BearingWithConfidenceImpl; import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.DoublePair; import com.sap.sailing.domain.common.impl.Util.Pair; import com.sap.sailing.domain.confidence.ConfidenceBasedAverager; import com.sap.sailing.domain.confidence.ConfidenceFactory; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CombinedWindTrackImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CombinedWindTrackImpl.java index 966d3930cb0..a20d949f256 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CombinedWindTrackImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CombinedWindTrackImpl.java @@ -1,13 +1,22 @@ package com.sap.sailing.domain.tracking.impl; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.WindSourceType; +import com.sap.sailing.domain.common.impl.Util.Pair; import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.domain.tracking.Wind; +import com.sap.sailing.domain.tracking.WindWithConfidence; import com.sap.sailing.domain.tracking.impl.TrackBasedEstimationWindTrackImpl.EstimatedWindFixesAsNavigableSet; /** * A wind track that delivers the result of * {@link TrackedRace#getWind(com.sap.sailing.domain.common.Position, com.sap.sailing.domain.common.TimePoint)}, * using a {@link CombinedWindAsNavigableSet} as its internal raw fixes collection. Resolution of raw fixes is set to 10s. + * As the result of {@link TrackedRace#getWind(com.sap.sailing.domain.common.Position, com.sap.sailing.domain.common.TimePoint)} + * is already a smoothened, outlier-removed and averaged value, the averaging methods {@link #getAveragedWind(Position, TimePoint)} + * and {@link #getAveragedWindWithConfidence(Position, TimePoint)} are redefined such that they immediately delegate + * to {@link TrackedRace#getWindWithConfidence(Position, TimePoint)}. * * @author Axel Uhl (d043530) * @@ -37,5 +46,14 @@ public class CombinedWindTrackImpl extends VirtualWindTrackImpl { public long getMillisecondsOverWhichToAverageWind() { return 2*getInternalRawFixes().getResolutionInMilliseconds()+2; } - + + @Override + public Wind getAveragedWind(Position p, TimePoint at) { + return getTrackedRace().getWind(p, at); + } + + @Override + public WindWithConfidence> getAveragedWindWithConfidence(Position p, TimePoint at) { + return getTrackedRace().getWindWithConfidence(p, at); + } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CrossTrackErrorCache.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CrossTrackErrorCache.java index 1986dd1d884..c39dcb8f942 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CrossTrackErrorCache.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CrossTrackErrorCache.java @@ -38,7 +38,7 @@ import com.sap.sailing.util.SmartFutureCache.UpdateInterval; * The results can be cached. Cache invalidation becomes necessary as mark passings, mark positions and boat positions * change. For this purpose, this cache subscribes itself as a listener to the {@link TrackedRace} to which it belongs * and manages cache invalidations autonomously. - * + * * @author Axel Uhl (D043530) * */ @@ -48,13 +48,15 @@ public class CrossTrackErrorCache extends AbstractRaceChangeListener { private static class CrossTrackErrorSumAndNumberOfFixes implements Timed { private static final long serialVersionUID = -278130726836884454L; private final TimePoint timePoint; - private final double distanceInMetersSumFromStart; + private final double absoluteDistanceInMetersSumFromStart; + private final double signedDistanceInMetersSumFromStart; private final int fixCountFromStart; - public CrossTrackErrorSumAndNumberOfFixes(TimePoint timePoint, double distanceInMetersSumFromStart, int fixCountFromStart) { + public CrossTrackErrorSumAndNumberOfFixes(TimePoint timePoint, double absoluteDistanceInMetersSumFromStart, double signedDistanceInMetersSumFromStart, int fixCountFromStart) { super(); this.timePoint = timePoint; - this.distanceInMetersSumFromStart = distanceInMetersSumFromStart; + this.absoluteDistanceInMetersSumFromStart = absoluteDistanceInMetersSumFromStart; + this.signedDistanceInMetersSumFromStart = signedDistanceInMetersSumFromStart; this.fixCountFromStart = fixCountFromStart; } @@ -63,8 +65,12 @@ public class CrossTrackErrorCache extends AbstractRaceChangeListener { return timePoint; } - public double getDistanceInMetersSumFromStart() { - return distanceInMetersSumFromStart; + public double getAbsoluteDistanceInMetersSumFromStart() { + return absoluteDistanceInMetersSumFromStart; + } + + public double getSignedDistanceInMetersSumFromStart() { + return signedDistanceInMetersSumFromStart; } public int getFixCountFromStart() { @@ -115,7 +121,7 @@ public class CrossTrackErrorCache extends AbstractRaceChangeListener { public FromTimePointToEndUpdateInterval join(FromTimePointToEndUpdateInterval otherUpdateInterval) { return new FromTimePointToEndUpdateInterval(getFrom()==null? otherUpdateInterval.getFrom():otherUpdateInterval.getFrom()==null? - getFrom():(getFrom().compareTo(otherUpdateInterval.getFrom())<0)? + getFrom():getFrom().before(otherUpdateInterval.getFrom())? getFrom():otherUpdateInterval.getFrom()); } @@ -185,6 +191,10 @@ public class CrossTrackErrorCache extends AbstractRaceChangeListener { owner.addListener(this); } + private interface CrossTrackErrorMeterRetriever { + double getDistanceInMetersSumFromStart(CrossTrackErrorSumAndNumberOfFixes cacheEntry); + } + /** * Answers the query from the cache contents. * @@ -194,8 +204,43 @@ public class CrossTrackErrorCache extends AbstractRaceChangeListener { * whether to wait for any currently ongoing cache update calculation; if false, the current * cache entry will be used */ - public Distance getAverageCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, + public Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, boolean waitForLatest) throws NoWindException { + CrossTrackErrorMeterRetriever absoluteMetersRetriever = new CrossTrackErrorMeterRetriever() { + @Override + public double getDistanceInMetersSumFromStart(CrossTrackErrorSumAndNumberOfFixes cacheEntry) { + return cacheEntry.getAbsoluteDistanceInMetersSumFromStart(); + } + }; + return getAbsoluteOrSignedAverageCrossTrackError(competitor, from, to, upwindOnly, waitForLatest, absoluteMetersRetriever); + } + + /** + * Answers the query from the cache contents. + * + * @param upwindOnly + * if true, only fixes in upwind legs are considered during aggregation + * @param waitForLatest + * whether to wait for any currently ongoing cache update calculation; if false, the current + * cache entry will be used + */ + public Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, + boolean waitForLatest) throws NoWindException { + CrossTrackErrorMeterRetriever signedMetersRetriever = new CrossTrackErrorMeterRetriever() { + @Override + public double getDistanceInMetersSumFromStart(CrossTrackErrorSumAndNumberOfFixes cacheEntry) { + return cacheEntry.getSignedDistanceInMetersSumFromStart(); + } + }; + return getAbsoluteOrSignedAverageCrossTrackError(competitor, from, to, upwindOnly, waitForLatest, signedMetersRetriever); + } + + /** + * @param absoluteMetersRetriever determines whether the absolute or signed cross track error will be aggregated + */ + private Distance getAbsoluteOrSignedAverageCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, + boolean upwindOnly, boolean waitForLatest, CrossTrackErrorMeterRetriever absoluteMetersRetriever) + throws NoWindException { Track cacheForCompetitor = cachePerCompetitor.get(competitor, waitForLatest); double distanceInMeters = 0; int count = 0; @@ -231,11 +276,11 @@ public class CrossTrackErrorCache extends AbstractRaceChangeListener { } if (startAggregate == null) { startAggregate = new CrossTrackErrorSumAndNumberOfFixes(/* time point */ null, - /* distanceInMetersSumFromStart */ 0, /* fixCountFromStart */ 0); + /* absoluteDistanceInMetersSumFromStart */ 0, /* signedDistanceInMetersSumFromStart */ 0, /* fixCountFromStart */ 0); } CrossTrackErrorSumAndNumberOfFixes endAggregate = cacheForCompetitor.getLastFixAtOrBefore(end); if (endAggregate != null) { - distanceInMeters += endAggregate.getDistanceInMetersSumFromStart() - startAggregate.getDistanceInMetersSumFromStart(); + distanceInMeters += absoluteMetersRetriever.getDistanceInMetersSumFromStart(endAggregate) - absoluteMetersRetriever.getDistanceInMetersSumFromStart(startAggregate); count += endAggregate.getFixCountFromStart() - startAggregate.getFixCountFromStart(); startAggregate = endAggregate; } @@ -261,13 +306,16 @@ public class CrossTrackErrorCache extends AbstractRaceChangeListener { final CrossTrackErrorSumAndNumberOfFixesTrack competitorCacheEntry = cachePerCompetitor.get(competitor, /* waitForLatest */ false); final CrossTrackErrorSumAndNumberOfFixes lastCacheEntryBeforeFrom; lastCacheEntryBeforeFrom = competitorCacheEntry == null ? null : competitorCacheEntry.getLastFixBefore(from); - double distanceInMeters; + double absoluteDistanceInMeters; + double signedDistanceInMeters; int count; if (lastCacheEntryBeforeFrom != null) { - distanceInMeters = lastCacheEntryBeforeFrom.getDistanceInMetersSumFromStart(); + absoluteDistanceInMeters = lastCacheEntryBeforeFrom.getAbsoluteDistanceInMetersSumFromStart(); + signedDistanceInMeters = lastCacheEntryBeforeFrom.getSignedDistanceInMetersSumFromStart(); count = lastCacheEntryBeforeFrom.getFixCountFromStart(); } else { - distanceInMeters = 0; + absoluteDistanceInMeters = 0; + signedDistanceInMeters = 0; count = 0; } GPSFixTrack track = owner.getTrack(competitor); @@ -298,12 +346,13 @@ public class CrossTrackErrorCache extends AbstractRaceChangeListener { if (currentLeg != null && markPassingAtLegStart != null && fix.getTimePoint().compareTo(markPassingAtLegStart.getTimePoint()) >= 0) { TrackedLeg trackedLeg = owner.getTrackedLeg(currentLeg); - Distance xte = owner.getTrackedLeg(trackedLeg.getLeg()).getCrossTrackError(fix.getPosition(), - fix.getTimePoint()); + Distance xte = owner.getTrackedLeg(trackedLeg.getLeg()).getSignedCrossTrackError(fix.getPosition(), fix.getTimePoint()); if (xte != null) { - distanceInMeters += xte.getMeters(); + signedDistanceInMeters += xte.getMeters(); + absoluteDistanceInMeters += Math.abs(xte.getMeters()); count++; - CrossTrackErrorSumAndNumberOfFixes newCacheEntry = new CrossTrackErrorSumAndNumberOfFixes(fix.getTimePoint(), distanceInMeters, count); + CrossTrackErrorSumAndNumberOfFixes newCacheEntry = new CrossTrackErrorSumAndNumberOfFixes(fix.getTimePoint(), absoluteDistanceInMeters, + signedDistanceInMeters, count); result.add(newCacheEntry); } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DummyMarkPassingWithTimePointOnly.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DummyMarkPassingWithTimePointOnly.java index 6a54a72d5ba..95e1b281fc7 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DummyMarkPassingWithTimePointOnly.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DummyMarkPassingWithTimePointOnly.java @@ -2,6 +2,7 @@ package com.sap.sailing.domain.tracking.impl; import com.sap.sailing.domain.base.Boat; import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CompetitorChangeListener; import com.sap.sailing.domain.base.IsManagedBySharedDomainFactory; import com.sap.sailing.domain.base.SharedDomainFactory; import com.sap.sailing.domain.base.Team; @@ -63,6 +64,14 @@ public class DummyMarkPassingWithTimePointOnly implements MarkPassing { public Color getColor() { return null; } + + @Override + public void addCompetitorChangeListener(CompetitorChangeListener listener) { + } + + @Override + public void removeCompetitorChangeListener(CompetitorChangeListener listener) { + } }; } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackImpl.java index 91592e62e1d..47d90adebc6 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackImpl.java @@ -17,5 +17,4 @@ public class DynamicTrackImpl extends TrackImpl public boolean add(FixType fix) { return super.add(fix); } - } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java index 311476a3b32..486c6c4ef2a 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java @@ -1,861 +1,897 @@ -package com.sap.sailing.domain.tracking.impl; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.ConcurrentModificationException; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NavigableSet; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; - -import com.sap.sailing.domain.base.BoatClass; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.CourseBase; -import com.sap.sailing.domain.base.Leg; -import com.sap.sailing.domain.base.Mark; -import com.sap.sailing.domain.base.RaceDefinition; -import com.sap.sailing.domain.base.Sideline; -import com.sap.sailing.domain.base.Waypoint; -import com.sap.sailing.domain.common.Bearing; -import com.sap.sailing.domain.common.Distance; -import com.sap.sailing.domain.common.PassingInstruction; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.TimePoint; -import com.sap.sailing.domain.common.TimingConstants; -import com.sap.sailing.domain.common.WindSource; -import com.sap.sailing.domain.common.WindSourceType; -import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; -import com.sap.sailing.domain.common.impl.Util; -import com.sap.sailing.domain.common.impl.Util.Pair; -import com.sap.sailing.domain.common.impl.WindSourceImpl; -import com.sap.sailing.domain.racelog.RaceLog; -import com.sap.sailing.domain.tracking.CourseDesignChangedListener; -import com.sap.sailing.domain.tracking.DynamicGPSFixTrack; -import com.sap.sailing.domain.tracking.DynamicTrackedRace; -import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; -import com.sap.sailing.domain.tracking.GPSFix; -import com.sap.sailing.domain.tracking.GPSFixMoving; -import com.sap.sailing.domain.tracking.GPSTrackListener; -import com.sap.sailing.domain.tracking.MarkPassing; -import com.sap.sailing.domain.tracking.RaceChangeListener; -import com.sap.sailing.domain.tracking.StartTimeChangedListener; -import com.sap.sailing.domain.tracking.TrackedLeg; -import com.sap.sailing.domain.tracking.TrackedRaceStatus; -import com.sap.sailing.domain.tracking.TrackedRegatta; -import com.sap.sailing.domain.tracking.Wind; -import com.sap.sailing.domain.tracking.WindStore; -import com.sap.sailing.domain.tracking.WindTrack; -import com.sap.sailing.util.impl.LockUtil; -import com.sap.sailing.util.impl.NamedReentrantReadWriteLock; - -public class DynamicTrackedRaceImpl extends TrackedRaceImpl implements -DynamicTrackedRace, GPSTrackListener { - private static final long serialVersionUID = 1092726918239676958L; - - private static final Logger logger = Logger.getLogger(DynamicTrackedRaceImpl.class.getName()); - - private transient Set listeners; - - private boolean raceIsKnownToStartUpwind; - - private boolean delayToLiveInMillisFixed; - - private transient DynamicTrackedRaceLogListener logListener; - - private transient Set courseDesignChangedListeners; - private transient Set startTimeChangedListeners; - - public DynamicTrackedRaceImpl(TrackedRegatta trackedRegatta, RaceDefinition race, Iterable sidelines, - WindStore windStore, long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed, - long delayForCacheInvalidationOfWindEstimation) { - super(trackedRegatta, race, sidelines, windStore, delayToLiveInMillis, millisecondsOverWhichToAverageWind, millisecondsOverWhichToAverageSpeed, - delayForCacheInvalidationOfWindEstimation); - this.logListener = new DynamicTrackedRaceLogListener(this); - this.courseDesignChangedListeners = new HashSet(); - this.startTimeChangedListeners = new HashSet(); - this.raceIsKnownToStartUpwind = race.getBoatClass().typicallyStartsUpwind(); - if (!raceIsKnownToStartUpwind) { - Set windSourcesToExclude = new HashSet(); - for (WindSource windSourceToExclude : getWindSourcesToExclude()) { - windSourcesToExclude.add(windSourceToExclude); - } - windSourcesToExclude.add(new WindSourceImpl(WindSourceType.COURSE_BASED)); - setWindSourcesToExclude(windSourcesToExclude); - } - for (Competitor competitor : getRace().getCompetitors()) { - DynamicGPSFixTrack track = getTrack(competitor); - track.addListener(this); - } - // default wind tracks are observed because they are created by the superclass constructor using - // createWindTrack which adds this object as a listener - } - - /** - * After de-serialization sets a valid {@link #listeners} collection which is transient and therefore - * hasn't been serialized. - */ - private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { - ois.defaultReadObject(); - listeners = new HashSet(); - logListener = new DynamicTrackedRaceLogListener(this); - courseDesignChangedListeners = new HashSet(); - startTimeChangedListeners = new HashSet(); - } - - /** - * {@link #raceIsKnownToStartUpwind} (see also {@link #raceIsKnownToStartUpwind()}) is initialized based on the race's - * {@link RaceDefinition#getBoatClass()} boat class's {@link BoatClass#typicallyStartsUpwind()} result. It can be changed - * using {@link #setRaceIsKnownToStartUpwind(boolean)}. Uses millisecondsOverWhichToAverageWind/2 for the - * delayForCacheInvalidationOfWindEstimation argument of the constructor.

- * - * Loading wind tracks from the windStore happens asynchronously which means that when the constructor returns, - * the caller cannot assume that all wind tracks have yet been loaded completely. The caller may call {@link #waitUntilWindLoadingComplete()} - * to wait until all persistent wind sources have been successfully and completely loaded. - */ - public DynamicTrackedRaceImpl(TrackedRegatta trackedRegatta, RaceDefinition race, Iterable sidelines, - WindStore windStore, long delayToLiveInMillis, - long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed) { - this(trackedRegatta, race, sidelines, windStore, delayToLiveInMillis, millisecondsOverWhichToAverageWind, millisecondsOverWhichToAverageSpeed, - millisecondsOverWhichToAverageWind/2); - } - - @Override - public void recordFix(Competitor competitor, GPSFixMoving fix) { - DynamicGPSFixTrack track = getTrack(competitor); - if (track != null) { - track.addGPSFix(fix); // the track notifies this tracked race which in turn notifies its listeners - } - } - - @Override - public void setStatus(TrackedRaceStatus newStatus) { - super.setStatus(newStatus); - notifyListeners(newStatus); - } - - @Override - public void recordFix(Mark mark, GPSFix fix) { - getOrCreateTrack(mark).addGPSFix(fix); - } - - @Override - public void setMillisecondsOverWhichToAverageSpeed(long millisecondsOverWhichToAverageSpeed) { - this.millisecondsOverWhichToAverageSpeed = millisecondsOverWhichToAverageSpeed; - for (Competitor competitor : getRace().getCompetitors()) { - getTrack(competitor).setMillisecondsOverWhichToAverage(millisecondsOverWhichToAverageSpeed); - } - for (Waypoint waypoint : getRace().getCourse().getWaypoints()) { - for (Mark mark : waypoint.getMarks()) { - getOrCreateTrack(mark).setMillisecondsOverWhichToAverage(millisecondsOverWhichToAverageSpeed); - } - } - updated(/* time point */null); - triggerManeuverCacheRecalculationForAllCompetitors(); - } - - @Override - public void setMillisecondsOverWhichToAverageWind(long millisecondsOverWhichToAverageWind) { - long oldMillisecondsOverWhichToAverageWind = this.millisecondsOverWhichToAverageWind; - this.millisecondsOverWhichToAverageWind = millisecondsOverWhichToAverageWind; - for (WindSource windSource : getWindSources()) { - getOrCreateWindTrack(windSource).setMillisecondsOverWhichToAverage(millisecondsOverWhichToAverageWind); - } - updated(/* time point */null); - triggerManeuverCacheRecalculationForAllCompetitors(); - notifyListenersWindAveragingChanged(oldMillisecondsOverWhichToAverageWind, millisecondsOverWhichToAverageWind); - } - - - @Override - public void setAndFixDelayToLiveInMillis(long delayToLiveInMillis) { - super.setDelayToLiveInMillis(delayToLiveInMillis); - delayToLiveInMillisFixed = true; - } - - @Override - public void setDelayToLiveInMillis(long delayToLiveInMillis) { - if (!delayToLiveInMillisFixed && getDelayToLiveInMillis() != delayToLiveInMillis) { - super.setDelayToLiveInMillis(delayToLiveInMillis); - notifyListenersDelayToLiveChanged(delayToLiveInMillis); - } - } - - @Override - public DynamicGPSFixTrack getTrack(Competitor competitor) { - return (DynamicGPSFixTrack) super.getTrack(competitor); - } - - @Override - public DynamicGPSFixTrack getOrCreateTrack(Mark mark) { - return (DynamicGPSFixTrack) super.getOrCreateTrack(mark); - } - - /** - * In addition to creating the track which is performed by the superclass implementation, this implementation registers - * a {@link GPSTrackListener} with the mark's track and {@link #notifyListeners(GPSFix, Mark) notifies the listeners} - * about updates. The {@link #updated(TimePoint)} method is not called with the mark fix's time point because - * mark fixes may be received also from marks that don't belong to this race. - */ - @Override - protected DynamicGPSFixTrackImpl createMarkTrack(Mark mark) { - DynamicGPSFixTrackImpl result = super.createMarkTrack(mark); - result.addListener(new GPSTrackListener() { - private static final long serialVersionUID = -2855787105725103732L; - - @Override - public void gpsFixReceived(GPSFix fix, Mark mark) { - triggerManeuverCacheRecalculationForAllCompetitors(); - notifyListeners(fix, mark); - } - - @Override - public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, - long newMillisecondsOverWhichToAverage) { - // nobody can currently listen for the change of the mark speed averaging because mark speed is not a value used - } - - @Override - public boolean isTransient() { - return false; - } - }); - return result; - } - - /** - * Callers iterating over the result need to synchronize on the resulting collection while iterating - * to avoid {@link ConcurrentModificationException}s. - */ - private Set getListeners() { - if (listeners == null) { - listeners = new HashSet(); - } - return listeners; - } - - @Override - public void addListener(RaceChangeListener listener) { - synchronized (getListeners()) { - getListeners().add(listener); - } - } - - /** - * If the listener wants to be notified about the wind fixes already loaded, this method will obtain the read lock - * of the {@link #getWindLoadingLock() wind loading lock}, locking against the wind loading thread which uses the - * corresponding write lock. This ensures that either all wind fixes have been loaded already or that wind loading - * hasn't started yet. - */ - @Override - public void addListener(RaceChangeListener listener, final boolean notifyAboutWindFixesAlreadyLoaded) { - if (notifyAboutWindFixesAlreadyLoaded) { - LockUtil.lockForRead(getWindLoadingLock()); - } - try { - addListener(listener); - if (notifyAboutWindFixesAlreadyLoaded) { - // Now notify all wind fixes we can get from the race by now. TrackedRace.getWindSource() delivers all wind - // sources known so far. If there is a wind track being loaded, it will be separately notified later by - // DynamicTrackedRaceImpl.createWindTrack(...). - // Holding the serialization lock - for (WindSource windSource : getWindSources()) { - if (windSource.getType().canBeStored()) { - WindTrack windTrack = getOrCreateWindTrack(windSource); - // replicate all wind fixed that may have been loaded by the wind store - windTrack.lockForRead(); - try { - for (Wind wind : windTrack.getRawFixes()) { - listener.windDataReceived(wind, windSource); - } - } finally { - windTrack.unlockAfterRead(); - } - } - } - } - } finally { - if (notifyAboutWindFixesAlreadyLoaded) { - LockUtil.unlockAfterRead(getWindLoadingLock()); - } - } - } - - @Override - public void removeListener(RaceChangeListener listener) { - synchronized (getListeners()) { - getListeners().remove(listener); - } - } - - @Override - public void setWindSourcesToExclude(Iterable windSourcesToExclude) { - super.setWindSourcesToExclude(windSourcesToExclude); - notifyListenersWindSourcesToExcludeChanged(windSourcesToExclude); - } - - private void notifyListenersWindSourcesToExcludeChanged(Iterable windSourcesToExclude) { - RaceChangeListener[] listeners; - synchronized (getListeners()) { - listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); - } - for (RaceChangeListener listener : listeners) { - try { - listener.windSourcesToExcludeChanged(windSourcesToExclude); - } catch (Exception e) { - logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); - logger.log(Level.SEVERE, "notifyListenersWindSourcesToExcludeChanged(Iterable)", e); - } - } - } - - private void notifyListenersRaceTimesChanged(TimePoint startOfTracking, TimePoint endOfTracking, - TimePoint startTimeReceived) { - RaceChangeListener[] listeners; - synchronized (getListeners()) { - listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); - } - for (RaceChangeListener listener : listeners) { - try { - listener.raceTimesChanged(startOfTracking, endOfTracking, startTimeReceived); - } catch (Exception e) { - logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); - logger.log(Level.SEVERE, "notifyListenersRaceTimesChanged(TimePoint, TimePoint, TimePoint)", e); - } - } - } - - private void notifyListeners(GPSFix fix, Mark mark) { - RaceChangeListener[] listeners; - synchronized (getListeners()) { - listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); - } - for (RaceChangeListener listener : listeners) { - try { - listener.markPositionChanged(fix, mark); - } catch (Exception e) { - logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); - logger.log(Level.SEVERE, "notifyListeners(GPSFix, Competitor)", e); - } - } - } - - private void notifyListeners(GPSFixMoving fix, Competitor competitor) { - RaceChangeListener[] listeners; - synchronized (getListeners()) { - listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); - } - for (RaceChangeListener listener : listeners) { - try { - listener.competitorPositionChanged(fix, competitor); - } catch (Exception e) { - logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); - logger.log(Level.SEVERE, "notifyListeners(GPSFix, Competitor)", e); - } - } - } - - private void notifyListeners(TrackedRaceStatus status) { - RaceChangeListener[] listeners; - synchronized (getListeners()) { - listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); - } - for (RaceChangeListener listener : listeners) { - try { - listener.statusChanged(status); - } catch (Exception e) { - logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); - logger.log(Level.SEVERE, "notifyListeners(TrackedRaceStatus)", e); - } - } - } - - private void notifyListeners(Wind wind, WindSource windSource) { - RaceChangeListener[] listeners; - synchronized (getListeners()) { - listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); - } - for (RaceChangeListener listener : listeners) { - try { - listener.windDataReceived(wind, windSource); - } catch (Exception e) { - logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); - logger.log(Level.SEVERE, "notifyListeners(Wind)", e); - } - } - } - - private void notifyListenersSpeedAveragingChanged(long oldMillisecondsOverWhichToAverageSpeed, long newMillisecondsOverWhichToAverageSpeed) { - RaceChangeListener[] listeners; - synchronized (getListeners()) { - listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); - } - for (RaceChangeListener listener : listeners) { - try { - listener.speedAveragingChanged(oldMillisecondsOverWhichToAverageSpeed, - newMillisecondsOverWhichToAverageSpeed); - } catch (Exception e) { - logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); - logger.log(Level.SEVERE, - "notifyListenersSpeedAveragingChanged(long, long)", e); - } - } - } - - private void notifyListenersWindAveragingChanged(long oldMillisecondsOverWhichToAverageWind, long newMillisecondsOverWhichToAverageWind) { - RaceChangeListener[] listeners; - synchronized (getListeners()) { - listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); - } - for (RaceChangeListener listener : listeners) { - try { - listener.windAveragingChanged(oldMillisecondsOverWhichToAverageWind, - newMillisecondsOverWhichToAverageWind); - } catch (Exception e) { - logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); - logger.log(Level.SEVERE, - "notifyListenersWindAveragingChanged(long, long)", e); - } - } - } - - private void notifyListenersDelayToLiveChanged(long delayToLiveInMillis) { - RaceChangeListener[] listeners; - synchronized (getListeners()) { - listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); - } - for (RaceChangeListener listener : listeners) { - try { - listener.delayToLiveChanged(delayToLiveInMillis); - } catch (Exception e) { - logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); - logger.log(Level.SEVERE, - "notifyListenersDelayToLiveChanged(long)", e); - } - } - } - - private void notifyListenersWindRemoved(Wind wind, WindSource windSource) { - RaceChangeListener[] listeners; - synchronized (getListeners()) { - listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); - } - for (RaceChangeListener listener : listeners) { - try { - listener.windDataRemoved(wind, windSource); - } catch (Exception e) { - logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); - logger.log(Level.SEVERE, "notifyListenersWindRemoved(Wind)", e); - } - } - } - - private void notifyListeners(Competitor competitor, Map oldMarkPassings, Iterable markPassings) { - RaceChangeListener[] listeners; - synchronized (getListeners()) { - listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); - } - for (RaceChangeListener listener : listeners) { - try { - listener.markPassingReceived(competitor, oldMarkPassings, markPassings); - } catch (Exception e) { - logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); - logger.log(Level.SEVERE, "notifyListeners(MarkPassing)", e); - } - } - } - - @Override - public void updateMarkPassings(Competitor competitor, Iterable markPassings) { - Map oldMarkPassings = new HashMap(); - MarkPassing oldStartMarkPassing = null; - boolean requiresStartTimeUpdate = true; - final NavigableSet markPassingsForCompetitor = getMarkPassings(competitor); - lockForRead(markPassingsForCompetitor); - try { - for (MarkPassing oldMarkPassing : markPassingsForCompetitor) { - if (oldStartMarkPassing == null) { - oldStartMarkPassing = oldMarkPassing; - } - oldMarkPassings.put(oldMarkPassing.getWaypoint(), oldMarkPassing); - } - } finally { - unlockAfterRead(markPassingsForCompetitor); - } - final NamedReentrantReadWriteLock markPassingsLock = getMarkPassingsLock(markPassingsForCompetitor); - TimePoint timePointOfLatestEvent = new MillisecondsTimePoint(0); - // Make sure that clearMarkPassings and the re-adding of the mark passings are non-interruptible by readers. - // Note that the write lock for the mark passings in order per waypoint is obtained inside clearMarkPassings(...) - // as well as inside the subsequent for-loop. It is important to always first obtain the mark passings lock for the competitor - // mark passings before obtaining the lock for the mark passings in order for the waypoint to avoid deadlocks. - LockUtil.lockForWrite(markPassingsLock); - try { - clearMarkPassings(competitor); - for (MarkPassing markPassing : markPassings) { - // try to find corresponding old start mark passing - if (oldStartMarkPassing != null - && markPassing.getWaypoint().getName().equals(oldStartMarkPassing.getWaypoint().getName())) { - if (markPassing.getTimePoint() != null && oldStartMarkPassing.getTimePoint() != null - && markPassing.getTimePoint().equals(oldStartMarkPassing.getTimePoint())) { - requiresStartTimeUpdate = false; - } - } - if (!Util.contains(getRace().getCourse().getWaypoints(), markPassing.getWaypoint())) { - StringBuilder courseWaypointsWithID = new StringBuilder(); - boolean first = true; - for (Waypoint courseWaypoint : getRace().getCourse().getWaypoints()) { - if (first) { - first = false; - } else { - courseWaypointsWithID.append(" -> "); - } - courseWaypointsWithID.append(courseWaypoint.toString()); - courseWaypointsWithID.append(" (ID="); - courseWaypointsWithID.append(courseWaypoint.getId()); - courseWaypointsWithID.append(")"); - } - logger.severe("Received mark passing " + markPassing + " for race " + getRace() - + " for waypoint ID" + markPassing.getWaypoint().getId() - + " but the waypoint does not exist in course " + courseWaypointsWithID); - } else { - markPassingsForCompetitor.add(markPassing); - } - Collection markPassingsInOrderForWaypoint = getOrCreateMarkPassingsInOrderAsNavigableSet(markPassing - .getWaypoint()); - final NamedReentrantReadWriteLock markPassingsLock2 = getMarkPassingsLock(markPassingsInOrderForWaypoint); - LockUtil.lockForWrite(markPassingsLock2); - try { - // TODO wouldn't we need to remove a previous mark passing of competitor for the same waypoint before re-adding it? - markPassingsInOrderForWaypoint.add(markPassing); - } finally { - LockUtil.unlockAfterWrite(markPassingsLock2); - } - if (markPassing.getTimePoint().compareTo(timePointOfLatestEvent) > 0) { - timePointOfLatestEvent = markPassing.getTimePoint(); - } - } - } finally { - LockUtil.unlockAfterWrite(markPassingsLock); - } - updated(timePointOfLatestEvent); - triggerManeuverCacheRecalculation(competitor); - // update the race times like start, end and the leg times - if (requiresStartTimeUpdate) { - invalidateStartTime(); - } - invalidateMarkPassingTimes(); - invalidateEndTime(); - - // notify *after* all mark passings have been re-established; should avoid flicker - notifyListeners(competitor, oldMarkPassings, markPassings); - } - - @Override - public Iterable getMarkPassingsInOrder(Waypoint waypoint) { - return (NavigableSet) super.getMarkPassingsInOrder(waypoint); - } - - @Override - public void lockForRead(Iterable markPassings) { - getRace().getCourse().lockForRead(); - LockUtil.lockForRead(getMarkPassingsLock(markPassings)); - } - - @Override - public void unlockAfterRead(Iterable markPassings) { - LockUtil.unlockAfterRead(getMarkPassingsLock(markPassings)); - getRace().getCourse().unlockAfterRead(); - } - - private void clearMarkPassings(Competitor competitor) { - NavigableSet markPassings = getMarkPassings(competitor); - final NamedReentrantReadWriteLock markPassingsLock = getMarkPassingsLock(markPassings); - LockUtil.lockForWrite(markPassingsLock); - try { - Iterator mpIter = markPassings.iterator(); - while (mpIter.hasNext()) { - MarkPassing mp = mpIter.next(); - mpIter.remove(); - Collection markPassingsInOrder = getMarkPassingsInOrderAsNavigableSet(mp.getWaypoint()); - LockUtil.lockForWrite(getMarkPassingsLock(markPassingsInOrder)); - try { - markPassingsInOrder.remove(mp); - } finally { - LockUtil.unlockAfterWrite(getMarkPassingsLock(markPassingsInOrder)); - } - } - } finally { - LockUtil.unlockAfterWrite(markPassingsLock); - } - } - - @Override - public void setStartTimeReceived(TimePoint startTimeReceived) { - if ((startTimeReceived == null) != (getStartTimeReceived() == null) - || (startTimeReceived != null && !startTimeReceived.equals(getStartTimeReceived()))) { - super.setStartTimeReceived(startTimeReceived); - notifyListenersRaceTimesChanged(getStartOfTracking(), getEndOfTracking(), getStartTimeReceived()); - } - } - - @Override - public void setStartOfTrackingReceived(TimePoint startOfTrackingReceived) { - if ((getStartOfTracking() == null) != (startOfTrackingReceived == null) - || (startOfTrackingReceived != null && !getStartOfTracking().equals(startOfTrackingReceived))) { - super.setStartOfTrackingReceived(startOfTrackingReceived); - notifyListenersRaceTimesChanged(getStartOfTracking(), getEndOfTracking(), getStartTimeReceived()); - } - } - - @Override - public void setEndOfTrackingReceived(TimePoint endOfTrackingReceived) { - if ((getEndOfTracking() == null) != (endOfTrackingReceived == null) - || (endOfTrackingReceived != null && !getEndOfTracking().equals(endOfTrackingReceived))) { - super.setEndOfTrackingReceived(endOfTrackingReceived); - notifyListenersRaceTimesChanged(getStartOfTracking(), getEndOfTracking(), getStartTimeReceived()); - } - } - - /** - * In addition to calling the superclass implementation, for a stored wind track whose fixes were loaded by this - * call, all listeners are notified about these existing wind fixes using their - * {@link RaceChangeListener#windDataReceived(Wind, WindSource)} callback method. In particular this replicates all - * wind fixes that may have been loaded from the wind store for the new track. - */ - @Override - protected WindTrack createWindTrack(WindSource windSource, long delayForWindEstimationCacheInvalidation) { - WindTrack result = super.createWindTrack(windSource, delayForWindEstimationCacheInvalidation); - if (windSource.getType().canBeStored()) { - // replicate all wind fixes that may have been loaded by the wind store - result.lockForRead(); - try { - for (Wind wind : result.getRawFixes()) { - notifyListeners(wind, windSource); // Note that this doesn't notify the track's listeners but the tracked race's listeners. - } // In particular, the wind store won't receive events (again) for the wind fixes it already has. - } finally { - result.unlockAfterRead(); - } - } - return result; - } - - @Override - public boolean recordWind(Wind wind, WindSource windSource) { - final boolean result; - // TODO check what a good filter is; remember that start/end of tracking may change over time; what if we have discarded valuable wind fixes? - TimePoint startOfRace = getStartOfRace(); - TimePoint startOfTracking = getStartOfTracking(); - TimePoint endOfRace = getEndOfRace(); - TimePoint endOfTracking = getEndOfTracking(); - if ((startOfTracking == null || !startOfTracking.minus(TrackedRaceImpl.TIME_BEFORE_START_TO_TRACK_WIND_MILLIS).after(wind.getTimePoint()) || - (startOfRace != null && !startOfRace.minus(TrackedRaceImpl.TIME_BEFORE_START_TO_TRACK_WIND_MILLIS).after(wind.getTimePoint()))) - && - (endOfTracking == null || endOfTracking.plus(TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS).after(wind.getTimePoint()) || - (endOfRace != null && endOfRace.plus(TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS).after(wind.getTimePoint())))) { - result = getOrCreateWindTrack(windSource).add(wind); - updated(/* time point */null); // wind events shouldn't advance race time - triggerManeuverCacheRecalculationForAllCompetitors(); - notifyListeners(wind, windSource); - } else { - result = false; - } - return result; - } - - @Override - public void removeWind(Wind wind, WindSource windSource) { - getOrCreateWindTrack(windSource).remove(wind); - updated(/* time point */null); // wind events shouldn't advance race time - triggerManeuverCacheRecalculationForAllCompetitors(); - notifyListenersWindRemoved(wind, windSource); - } - - @Override - public void gpsFixReceived(GPSFixMoving fix, Competitor competitor) { - updated(fix.getTimePoint()); - triggerManeuverCacheRecalculation(competitor); - notifyListeners(fix, competitor); - } - - @Override - public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) { - notifyListenersSpeedAveragingChanged(oldMillisecondsOverWhichToAverage, newMillisecondsOverWhichToAverage); - } - - @Override - public boolean isTransient() { - return false; - } - - @Override - protected TrackedLeg createTrackedLeg(Leg leg) { - return new TrackedLegImpl(this, leg, getRace().getCompetitors()); - } - - @Override - public long getMillisecondsOverWhichToAverageSpeed() { - long result = 0; // default in case there is no competitor - Iterator compIter = getRace().getCompetitors().iterator(); - if (compIter.hasNext()) { - DynamicGPSFixTrack someTrack = getTrack(compIter.next()); - result = someTrack.getMillisecondsOverWhichToAverageSpeed(); - } - return result; - } - - @Override - public long getMillisecondsOverWhichToAverageWind() { - long result = 0; // default in case there is no competitor - for (WindSource windSource : getWindSources()) { - WindTrack someTrack = getOrCreateWindTrack(windSource); - result = someTrack.getMillisecondsOverWhichToAverageWind(); - } - return result; - } - - @Override - public DynamicTrackedRegatta getTrackedRegatta() { - return (DynamicTrackedRegatta) super.getTrackedRegatta(); - } - - @Override - public void setRaceIsKnownToStartUpwind(boolean raceIsKnownToStartUpwind) { - this.raceIsKnownToStartUpwind = raceIsKnownToStartUpwind; - } - - @Override - public boolean raceIsKnownToStartUpwind() { - return raceIsKnownToStartUpwind; - } - - @Override - public void attachRaceLog(RaceLog raceLog) { - super.attachRaceLog(raceLog); - logListener.addTo(raceLog); - } - - @Override - public void detachRaceLog(Serializable identifier) { - RaceLog attachedRaceLog = attachedRaceLogs.get(identifier); - if (attachedRaceLog != null) { - logListener.removeFrom(attachedRaceLog); - } - super.detachRaceLog(identifier); - } - - @Override - public void addCourseDesignChangedListener(CourseDesignChangedListener listener) { - this.courseDesignChangedListeners.add(listener); - } - - @Override - public void onCourseDesignChangedByRaceCommittee(CourseBase newCourseDesign) { - try { - for (CourseDesignChangedListener courseDesignChangedListener : courseDesignChangedListeners) { - courseDesignChangedListener.courseDesignChanged(newCourseDesign); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void onStartTimeChangedByRaceCommittee(TimePoint newStartTime) { - try { - for (StartTimeChangedListener startTimeChangedListener : startTimeChangedListeners) { - startTimeChangedListener.startTimeChanged(newStartTime); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void addStartTimeChangedListener(StartTimeChangedListener listener) { - this.startTimeChangedListeners.add(listener); - } - /** - * @return The Bearing of a line starting at w that needs to be crossed to pass a mark. - */ - @Override - public Bearing getCrossingBearing(Waypoint w, TimePoint t) { - Bearing result = null; - PassingInstruction instruction = w.getPassingInstructions(); - if (instruction == PassingInstruction.None || instruction == null) { - if (w.equals(getRace().getCourse().getFirstWaypoint()) || w.equals(getRace().getCourse().getLastWaypoint())) { - instruction = PassingInstruction.Line; - } else { - int numberofMarks = 0; - Iterator it = w.getMarks().iterator(); - while (it.hasNext()) { - it.next(); - numberofMarks++; - } - if (numberofMarks == 2) { - instruction = PassingInstruction.Gate; - } else if (numberofMarks == 1) { - instruction = PassingInstruction.Port; - } - } - } - if (instruction == PassingInstruction.FixedBearing) { - result = w.getFixedBearing(); - } else if (instruction == PassingInstruction.Gate || instruction == PassingInstruction.Port || instruction == PassingInstruction.Starboard) { - Bearing before = getTrackedLegFinishingAt(w).getLegBearing(t); - Bearing after = getTrackedLegStartingAt(w).getLegBearing(t); - if (before != null && after != null) { - result = before.middle(after.reverse()); - } - } else if (instruction == PassingInstruction.Line) { - Pair pos = getPortAndStarboardMarks(t, w); - if (pos.getA() != null && pos.getB() != null) { - Position portPosition = getOrCreateTrack(pos.getA()).getEstimatedPosition(t, false); - Position starboardPosition = getOrCreateTrack(pos.getB()).getEstimatedPosition(t, false); - if (portPosition != null && starboardPosition != null) { - result = portPosition.getBearingGreatCircle(starboardPosition); - } - } - } else if (instruction == PassingInstruction.Offset) { - // TODO Bug 1712 - } - return result; - } - - @Override - public Pair getPortAndStarboardMarks(TimePoint t, Waypoint w) { - List markPositions = new ArrayList(); - for (Mark lineMark : w.getMarks()) { - final Position estimatedMarkPosition = getOrCreateTrack(lineMark).getEstimatedPosition(t, /* extrapolate */ - false); - if (estimatedMarkPosition == null) { - return new Pair(null,null); - } - markPositions.add(estimatedMarkPosition); - } - final List legs = getRace().getCourse().getLegs(); - final int indexOfWaypoint = getRace().getCourse().getIndexOfWaypoint(w); - final boolean isStartLine = indexOfWaypoint == 0; - final Bearing legDeterminingDirectionBearing = getTrackedLeg(legs.get(isStartLine ? 0 : indexOfWaypoint - 1)).getLegBearing(t); - if (legDeterminingDirectionBearing == null) { - return new Pair(null, null); - } - Distance crossTrackErrorOfMark0OnLineFromMark1ToNextWaypoint = markPositions.get(0).crossTrackError(markPositions.get(1), legDeterminingDirectionBearing); - final Mark starboardMarkWhileApproachingLine; - final Mark portMarkWhileApproachingLine; - if (crossTrackErrorOfMark0OnLineFromMark1ToNextWaypoint.getMeters() < 0) { - portMarkWhileApproachingLine = Util.get(w.getMarks(), 0); - starboardMarkWhileApproachingLine = Util.get(w.getMarks(), 1); - } else { - portMarkWhileApproachingLine = Util.get(w.getMarks(), 1); - starboardMarkWhileApproachingLine = Util.get(w.getMarks(), 0); - } - return new Pair(portMarkWhileApproachingLine, starboardMarkWhileApproachingLine); - } -} +package com.sap.sailing.domain.tracking.impl; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.ConcurrentModificationException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NavigableSet; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CourseBase; +import com.sap.sailing.domain.base.Leg; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Sideline; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.PassingInstruction; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.TimingConstants; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.WindSourceType; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.impl.Util; +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.common.impl.WindSourceImpl; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; +import com.sap.sailing.domain.tracking.CourseDesignChangedListener; +import com.sap.sailing.domain.tracking.DynamicGPSFixTrack; +import com.sap.sailing.domain.tracking.DynamicTrackedRace; +import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.GPSFixTrack; +import com.sap.sailing.domain.tracking.GPSTrackListener; +import com.sap.sailing.domain.tracking.MarkPassing; +import com.sap.sailing.domain.tracking.RaceChangeListener; +import com.sap.sailing.domain.tracking.StartTimeChangedListener; +import com.sap.sailing.domain.tracking.TrackedLeg; +import com.sap.sailing.domain.tracking.TrackedRaceStatus; +import com.sap.sailing.domain.tracking.TrackedRegatta; +import com.sap.sailing.domain.tracking.Wind; +import com.sap.sailing.domain.tracking.WindStore; +import com.sap.sailing.domain.tracking.WindTrack; +import com.sap.sailing.util.impl.LockUtil; +import com.sap.sailing.util.impl.NamedReentrantReadWriteLock; + +public class DynamicTrackedRaceImpl extends TrackedRaceImpl implements +DynamicTrackedRace, GPSTrackListener { + private static final long serialVersionUID = 1092726918239676958L; + + private static final Logger logger = Logger.getLogger(DynamicTrackedRaceImpl.class.getName()); + + private transient Set listeners; + + private boolean raceIsKnownToStartUpwind; + + private boolean delayToLiveInMillisFixed; + + private transient DynamicTrackedRaceLogListener logListener; + + private transient Set courseDesignChangedListeners; + private transient Set startTimeChangedListeners; + + public DynamicTrackedRaceImpl(TrackedRegatta trackedRegatta, RaceDefinition race, Iterable sidelines, + WindStore windStore, GPSFixStore gpsFixStore, long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed, + long delayForCacheInvalidationOfWindEstimation) { + super(trackedRegatta, race, sidelines, windStore, gpsFixStore, delayToLiveInMillis, millisecondsOverWhichToAverageWind, millisecondsOverWhichToAverageSpeed, + delayForCacheInvalidationOfWindEstimation); + this.logListener = new DynamicTrackedRaceLogListener(this); + this.courseDesignChangedListeners = new HashSet(); + this.startTimeChangedListeners = new HashSet(); + this.raceIsKnownToStartUpwind = race.getBoatClass().typicallyStartsUpwind(); + if (!raceIsKnownToStartUpwind) { + Set windSourcesToExclude = new HashSet(); + for (WindSource windSourceToExclude : getWindSourcesToExclude()) { + windSourcesToExclude.add(windSourceToExclude); + } + windSourcesToExclude.add(new WindSourceImpl(WindSourceType.COURSE_BASED)); + setWindSourcesToExclude(windSourcesToExclude); + } + for (Competitor competitor : getRace().getCompetitors()) { + DynamicGPSFixTrack track = getTrack(competitor); + track.addListener(this); + } + // default wind tracks are observed because they are created by the superclass constructor using + // createWindTrack which adds this object as a listener + } + + /** + * After de-serialization sets a valid {@link #listeners} collection which is transient and therefore + * hasn't been serialized. + */ + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + listeners = new HashSet(); + logListener = new DynamicTrackedRaceLogListener(this); + courseDesignChangedListeners = new HashSet(); + startTimeChangedListeners = new HashSet(); + } + + /** + * {@link #raceIsKnownToStartUpwind} (see also {@link #raceIsKnownToStartUpwind()}) is initialized based on the race's + * {@link RaceDefinition#getBoatClass()} boat class's {@link BoatClass#typicallyStartsUpwind()} result. It can be changed + * using {@link #setRaceIsKnownToStartUpwind(boolean)}. Uses millisecondsOverWhichToAverageWind/2 for the + * delayForCacheInvalidationOfWindEstimation argument of the constructor.

+ * + * Loading wind tracks from the windStore happens asynchronously which means that when the constructor returns, + * the caller cannot assume that all wind tracks have yet been loaded completely. The caller may call {@link #waitUntilLoadingFromWindStoreComplete()} + * to wait until all persistent wind sources have been successfully and completely loaded. + */ + public DynamicTrackedRaceImpl(TrackedRegatta trackedRegatta, RaceDefinition race, Iterable sidelines, + WindStore windStore, GPSFixStore gpsFixStore, long delayToLiveInMillis, + long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed) { + this(trackedRegatta, race, sidelines, windStore, gpsFixStore, delayToLiveInMillis, millisecondsOverWhichToAverageWind, millisecondsOverWhichToAverageSpeed, + millisecondsOverWhichToAverageWind/2); + } + + @Override + public void recordFix(Competitor competitor, GPSFixMoving fix) { + DynamicGPSFixTrack track = getTrack(competitor); + if (track != null) { + if (logger != null && logger.getLevel() != null && logger.getLevel().equals(Level.FINEST)) { + logger.finest(""+competitor.getName() + ": " + fix); + } + track.addGPSFix(fix); // the track notifies this tracked race which in turn notifies its listeners + } + } + + @Override + public void setStatus(TrackedRaceStatus newStatus) { + super.setStatus(newStatus); + notifyListeners(newStatus); + } + + @Override + public void recordFix(Mark mark, GPSFix fix) { + getOrCreateTrack(mark).addGPSFix(fix); + } + + @Override + public void setMillisecondsOverWhichToAverageSpeed(long millisecondsOverWhichToAverageSpeed) { + this.millisecondsOverWhichToAverageSpeed = millisecondsOverWhichToAverageSpeed; + for (Competitor competitor : getRace().getCompetitors()) { + getTrack(competitor).setMillisecondsOverWhichToAverage(millisecondsOverWhichToAverageSpeed); + } + for (Waypoint waypoint : getRace().getCourse().getWaypoints()) { + for (Mark mark : waypoint.getMarks()) { + getOrCreateTrack(mark).setMillisecondsOverWhichToAverage(millisecondsOverWhichToAverageSpeed); + } + } + updated(/* time point */null); + triggerManeuverCacheRecalculationForAllCompetitors(); + } + + @Override + public void setMillisecondsOverWhichToAverageWind(long millisecondsOverWhichToAverageWind) { + long oldMillisecondsOverWhichToAverageWind = this.millisecondsOverWhichToAverageWind; + this.millisecondsOverWhichToAverageWind = millisecondsOverWhichToAverageWind; + for (WindSource windSource : getWindSources()) { + getOrCreateWindTrack(windSource).setMillisecondsOverWhichToAverage(millisecondsOverWhichToAverageWind); + } + updated(/* time point */null); + triggerManeuverCacheRecalculationForAllCompetitors(); + notifyListenersWindAveragingChanged(oldMillisecondsOverWhichToAverageWind, millisecondsOverWhichToAverageWind); + } + + + @Override + public void setAndFixDelayToLiveInMillis(long delayToLiveInMillis) { + super.setDelayToLiveInMillis(delayToLiveInMillis); + delayToLiveInMillisFixed = true; + } + + @Override + public void setDelayToLiveInMillis(long delayToLiveInMillis) { + if (!delayToLiveInMillisFixed && getDelayToLiveInMillis() != delayToLiveInMillis) { + super.setDelayToLiveInMillis(delayToLiveInMillis); + notifyListenersDelayToLiveChanged(delayToLiveInMillis); + } + } + + @Override + public DynamicGPSFixTrack getTrack(Competitor competitor) { + return (DynamicGPSFixTrack) super.getTrack(competitor); + } + + @Override + public DynamicGPSFixTrack getOrCreateTrack(Mark mark) { + return (DynamicGPSFixTrack) super.getOrCreateTrack(mark); + } + + /** + * In addition to creating the track which is performed by the superclass implementation, this implementation registers + * a {@link GPSTrackListener} with the mark's track and {@link #notifyListeners(GPSFix, Mark) notifies the listeners} + * about updates. The {@link #updated(TimePoint)} method is not called with the mark fix's time point because + * mark fixes may be received also from marks that don't belong to this race. + */ + @Override + protected DynamicGPSFixTrackImpl createMarkTrack(Mark mark) { + DynamicGPSFixTrackImpl result = super.createMarkTrack(mark); + result.addListener(new GPSTrackListener() { + private static final long serialVersionUID = -2855787105725103732L; + + @Override + public void gpsFixReceived(GPSFix fix, Mark mark) { + triggerManeuverCacheRecalculationForAllCompetitors(); + notifyListeners(fix, mark); + } + + @Override + public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, + long newMillisecondsOverWhichToAverage) { + // nobody can currently listen for the change of the mark speed averaging because mark speed is not a value used + } + + @Override + public boolean isTransient() { + return false; + } + }); + return result; + } + + /** + * Callers iterating over the result need to synchronize on the resulting collection while iterating + * to avoid {@link ConcurrentModificationException}s. + */ + private Set getListeners() { + if (listeners == null) { + listeners = new HashSet(); + } + return listeners; + } + + @Override + public void addListener(RaceChangeListener listener) { + synchronized (getListeners()) { + getListeners().add(listener); + } + } + + @Override + public void addListener(RaceChangeListener listener, final boolean notifyAboutWindFixesAlreadyLoaded, + final boolean notifyAboutGPSFixesAlreadyLoaded) { + if (notifyAboutWindFixesAlreadyLoaded) { + LockUtil.lockForRead(getLoadingFromWindStoreLock()); + } + if (notifyAboutGPSFixesAlreadyLoaded) { + LockUtil.lockForRead(getLoadingFromGPSFixStoreLock()); + } + try { + addListener(listener); + if (notifyAboutWindFixesAlreadyLoaded) { + // Now notify all wind fixes we can get from the race by now. TrackedRace.getWindSource() delivers all wind + // sources known so far. If there is a wind track being loaded, it will be separately notified later by + // DynamicTrackedRaceImpl.createWindTrack(...). + // Holding the serialization lock + for (WindSource windSource : getWindSources()) { + if (windSource.getType().canBeStored()) { + WindTrack windTrack = getOrCreateWindTrack(windSource); + // replicate all wind fixed that may have been loaded by the wind store + windTrack.lockForRead(); + try { + for (Wind wind : windTrack.getRawFixes()) { + listener.windDataReceived(wind, windSource); + } + } finally { + windTrack.unlockAfterRead(); + } + } + } + } + if (notifyAboutGPSFixesAlreadyLoaded) { + for (Mark mark : getMarks()) { + GPSFixTrack markTrack = getOrCreateTrack(mark); + markTrack.lockForRead(); + try { + for (GPSFix fix : markTrack.getRawFixes()) { + listener.markPositionChanged(fix, mark); + } + } finally { + markTrack.unlockAfterRead(); + } + } + + for (Competitor competitor : getRace().getCompetitors()) { + GPSFixTrack competitorTrack = getTrack(competitor); + competitorTrack.lockForRead(); + try { + for (GPSFixMoving fix : competitorTrack.getRawFixes()) { + listener.competitorPositionChanged(fix, competitor); + } + } finally { + competitorTrack.unlockAfterRead(); + } + } + } + } finally { + if (notifyAboutWindFixesAlreadyLoaded) { + LockUtil.unlockAfterRead(getLoadingFromWindStoreLock()); + } + if (notifyAboutGPSFixesAlreadyLoaded) { + LockUtil.unlockAfterRead(getLoadingFromGPSFixStoreLock()); + } + } + } + + @Override + public void removeListener(RaceChangeListener listener) { + synchronized (getListeners()) { + getListeners().remove(listener); + } + } + + @Override + public void setWindSourcesToExclude(Iterable windSourcesToExclude) { + super.setWindSourcesToExclude(windSourcesToExclude); + notifyListenersWindSourcesToExcludeChanged(windSourcesToExclude); + } + + private void notifyListenersWindSourcesToExcludeChanged(Iterable windSourcesToExclude) { + RaceChangeListener[] listeners; + synchronized (getListeners()) { + listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); + } + for (RaceChangeListener listener : listeners) { + try { + listener.windSourcesToExcludeChanged(windSourcesToExclude); + } catch (Exception e) { + logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); + logger.log(Level.SEVERE, "notifyListenersWindSourcesToExcludeChanged(Iterable)", e); + } + } + } + + private void notifyListenersRaceTimesChanged(TimePoint startOfTracking, TimePoint endOfTracking, + TimePoint startTimeReceived) { + RaceChangeListener[] listeners; + synchronized (getListeners()) { + listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); + } + for (RaceChangeListener listener : listeners) { + try { + listener.raceTimesChanged(startOfTracking, endOfTracking, startTimeReceived); + } catch (Exception e) { + logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); + logger.log(Level.SEVERE, "notifyListenersRaceTimesChanged(TimePoint, TimePoint, TimePoint)", e); + } + } + } + + private void notifyListeners(GPSFix fix, Mark mark) { + RaceChangeListener[] listeners; + synchronized (getListeners()) { + listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); + } + for (RaceChangeListener listener : listeners) { + try { + listener.markPositionChanged(fix, mark); + } catch (Exception e) { + logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); + logger.log(Level.SEVERE, "notifyListeners(GPSFix, Competitor)", e); + } + } + } + + private void notifyListeners(GPSFixMoving fix, Competitor competitor) { + RaceChangeListener[] listeners; + synchronized (getListeners()) { + listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); + } + for (RaceChangeListener listener : listeners) { + try { + listener.competitorPositionChanged(fix, competitor); + } catch (Exception e) { + logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); + logger.log(Level.SEVERE, "notifyListeners(GPSFix, Competitor)", e); + } + } + } + + private void notifyListeners(TrackedRaceStatus status) { + RaceChangeListener[] listeners; + synchronized (getListeners()) { + listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); + } + for (RaceChangeListener listener : listeners) { + try { + listener.statusChanged(status); + } catch (Exception e) { + logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); + logger.log(Level.SEVERE, "notifyListeners(TrackedRaceStatus)", e); + } + } + } + + private void notifyListeners(Wind wind, WindSource windSource) { + RaceChangeListener[] listeners; + synchronized (getListeners()) { + listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); + } + for (RaceChangeListener listener : listeners) { + try { + listener.windDataReceived(wind, windSource); + } catch (Exception e) { + logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); + logger.log(Level.SEVERE, "notifyListeners(Wind)", e); + } + } + } + + private void notifyListenersSpeedAveragingChanged(long oldMillisecondsOverWhichToAverageSpeed, long newMillisecondsOverWhichToAverageSpeed) { + RaceChangeListener[] listeners; + synchronized (getListeners()) { + listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); + } + for (RaceChangeListener listener : listeners) { + try { + listener.speedAveragingChanged(oldMillisecondsOverWhichToAverageSpeed, + newMillisecondsOverWhichToAverageSpeed); + } catch (Exception e) { + logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); + logger.log(Level.SEVERE, + "notifyListenersSpeedAveragingChanged(long, long)", e); + } + } + } + + private void notifyListenersWindAveragingChanged(long oldMillisecondsOverWhichToAverageWind, long newMillisecondsOverWhichToAverageWind) { + RaceChangeListener[] listeners; + synchronized (getListeners()) { + listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); + } + for (RaceChangeListener listener : listeners) { + try { + listener.windAveragingChanged(oldMillisecondsOverWhichToAverageWind, + newMillisecondsOverWhichToAverageWind); + } catch (Exception e) { + logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); + logger.log(Level.SEVERE, + "notifyListenersWindAveragingChanged(long, long)", e); + } + } + } + + private void notifyListenersDelayToLiveChanged(long delayToLiveInMillis) { + RaceChangeListener[] listeners; + synchronized (getListeners()) { + listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); + } + for (RaceChangeListener listener : listeners) { + try { + listener.delayToLiveChanged(delayToLiveInMillis); + } catch (Exception e) { + logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); + logger.log(Level.SEVERE, + "notifyListenersDelayToLiveChanged(long)", e); + } + } + } + + private void notifyListenersWindRemoved(Wind wind, WindSource windSource) { + RaceChangeListener[] listeners; + synchronized (getListeners()) { + listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); + } + for (RaceChangeListener listener : listeners) { + try { + listener.windDataRemoved(wind, windSource); + } catch (Exception e) { + logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); + logger.log(Level.SEVERE, "notifyListenersWindRemoved(Wind)", e); + } + } + } + + private void notifyListeners(Competitor competitor, Map oldMarkPassings, Iterable markPassings) { + RaceChangeListener[] listeners; + synchronized (getListeners()) { + listeners = getListeners().toArray(new RaceChangeListener[getListeners().size()]); + } + for (RaceChangeListener listener : listeners) { + try { + listener.markPassingReceived(competitor, oldMarkPassings, markPassings); + } catch (Exception e) { + logger.log(Level.SEVERE, "RaceChangeListener " + listener + " threw exception " + e.getMessage()); + logger.log(Level.SEVERE, "notifyListeners(MarkPassing)", e); + } + } + } + + @Override + public void updateMarkPassings(Competitor competitor, Iterable markPassings) { + Map oldMarkPassings = new HashMap(); + MarkPassing oldStartMarkPassing = null; + boolean requiresStartTimeUpdate = true; + final NavigableSet markPassingsForCompetitor = getMarkPassings(competitor); + lockForRead(markPassingsForCompetitor); + try { + for (MarkPassing oldMarkPassing : markPassingsForCompetitor) { + if (oldStartMarkPassing == null) { + oldStartMarkPassing = oldMarkPassing; + } + oldMarkPassings.put(oldMarkPassing.getWaypoint(), oldMarkPassing); + } + } finally { + unlockAfterRead(markPassingsForCompetitor); + } + final NamedReentrantReadWriteLock markPassingsLock = getMarkPassingsLock(markPassingsForCompetitor); + TimePoint timePointOfLatestEvent = new MillisecondsTimePoint(0); + // Make sure that clearMarkPassings and the re-adding of the mark passings are non-interruptible by readers. + // Note that the write lock for the mark passings in order per waypoint is obtained inside clearMarkPassings(...) + // as well as inside the subsequent for-loop. It is important to always first obtain the mark passings lock for the competitor + // mark passings before obtaining the lock for the mark passings in order for the waypoint to avoid deadlocks. + LockUtil.lockForWrite(markPassingsLock); + try { + clearMarkPassings(competitor); + for (MarkPassing markPassing : markPassings) { + // try to find corresponding old start mark passing + if (oldStartMarkPassing != null + && markPassing.getWaypoint().equals(oldStartMarkPassing.getWaypoint())) { + if (markPassing.getTimePoint() != null && oldStartMarkPassing.getTimePoint() != null + && markPassing.getTimePoint().equals(oldStartMarkPassing.getTimePoint())) { + requiresStartTimeUpdate = false; + } + } + if (!Util.contains(getRace().getCourse().getWaypoints(), markPassing.getWaypoint())) { + StringBuilder courseWaypointsWithID = new StringBuilder(); + boolean first = true; + for (Waypoint courseWaypoint : getRace().getCourse().getWaypoints()) { + if (first) { + first = false; + } else { + courseWaypointsWithID.append(" -> "); + } + courseWaypointsWithID.append(courseWaypoint.toString()); + courseWaypointsWithID.append(" (ID="); + courseWaypointsWithID.append(courseWaypoint.getId()); + courseWaypointsWithID.append(")"); + } + logger.severe("Received mark passing " + markPassing + " for race " + getRace() + + " for waypoint ID" + markPassing.getWaypoint().getId() + + " but the waypoint does not exist in course " + courseWaypointsWithID); + } else { + markPassingsForCompetitor.add(markPassing); + } + Collection markPassingsInOrderForWaypoint = getOrCreateMarkPassingsInOrderAsNavigableSet(markPassing + .getWaypoint()); + final NamedReentrantReadWriteLock markPassingsLock2 = getMarkPassingsLock(markPassingsInOrderForWaypoint); + LockUtil.lockForWrite(markPassingsLock2); + try { + // The mark passings of competitor have been removed by the call to clearMarkPassings(competitor) above + // from both, the collection that holds the mark passings by waypoint and the one that holds the + // mark passings per competitor; so we can simply add here: + markPassingsInOrderForWaypoint.add(markPassing); + } finally { + LockUtil.unlockAfterWrite(markPassingsLock2); + } + if (markPassing.getTimePoint().compareTo(timePointOfLatestEvent) > 0) { + timePointOfLatestEvent = markPassing.getTimePoint(); + } + } + } finally { + LockUtil.unlockAfterWrite(markPassingsLock); + } + updated(timePointOfLatestEvent); + triggerManeuverCacheRecalculation(competitor); + // update the race times like start, end and the leg times + if (requiresStartTimeUpdate) { + invalidateStartTime(); + } + invalidateMarkPassingTimes(); + invalidateEndTime(); + + // notify *after* all mark passings have been re-established; should avoid flicker + notifyListeners(competitor, oldMarkPassings, markPassings); + } + + @Override + public Iterable getMarkPassingsInOrder(Waypoint waypoint) { + return (NavigableSet) super.getMarkPassingsInOrder(waypoint); + } + + @Override + public void lockForRead(Iterable markPassings) { + getRace().getCourse().lockForRead(); + LockUtil.lockForRead(getMarkPassingsLock(markPassings)); + } + + @Override + public void unlockAfterRead(Iterable markPassings) { + LockUtil.unlockAfterRead(getMarkPassingsLock(markPassings)); + getRace().getCourse().unlockAfterRead(); + } + + /** + * Removes all mark passings of competitor from both, the {@link #markPassingsForCompetitor} + * and the {@link #markPassingsForWaypoint} collections. + */ + private void clearMarkPassings(Competitor competitor) { + NavigableSet markPassings = getMarkPassings(competitor); + final NamedReentrantReadWriteLock markPassingsLock = getMarkPassingsLock(markPassings); + LockUtil.lockForWrite(markPassingsLock); + try { + Iterator mpIter = markPassings.iterator(); + while (mpIter.hasNext()) { + MarkPassing mp = mpIter.next(); + mpIter.remove(); + Collection markPassingsInOrder = getMarkPassingsInOrderAsNavigableSet(mp.getWaypoint()); + LockUtil.lockForWrite(getMarkPassingsLock(markPassingsInOrder)); + try { + markPassingsInOrder.remove(mp); + } finally { + LockUtil.unlockAfterWrite(getMarkPassingsLock(markPassingsInOrder)); + } + } + } finally { + LockUtil.unlockAfterWrite(markPassingsLock); + } + } + + @Override + public void setStartTimeReceived(TimePoint startTimeReceived) { + if ((startTimeReceived == null) != (getStartTimeReceived() == null) + || (startTimeReceived != null && !startTimeReceived.equals(getStartTimeReceived()))) { + super.setStartTimeReceived(startTimeReceived); + notifyListenersRaceTimesChanged(getStartOfTracking(), getEndOfTracking(), getStartTimeReceived()); + } + } + + @Override + public void setStartOfTrackingReceived(TimePoint startOfTrackingReceived) { + if ((getStartOfTracking() == null) != (startOfTrackingReceived == null) + || (startOfTrackingReceived != null && !getStartOfTracking().equals(startOfTrackingReceived))) { + super.setStartOfTrackingReceived(startOfTrackingReceived); + notifyListenersRaceTimesChanged(getStartOfTracking(), getEndOfTracking(), getStartTimeReceived()); + } + } + + @Override + public void setEndOfTrackingReceived(TimePoint endOfTrackingReceived) { + if ((getEndOfTracking() == null) != (endOfTrackingReceived == null) + || (endOfTrackingReceived != null && !getEndOfTracking().equals(endOfTrackingReceived))) { + super.setEndOfTrackingReceived(endOfTrackingReceived); + notifyListenersRaceTimesChanged(getStartOfTracking(), getEndOfTracking(), getStartTimeReceived()); + } + } + + /** + * In addition to calling the superclass implementation, for a stored wind track whose fixes were loaded by this + * call, all listeners are notified about these existing wind fixes using their + * {@link RaceChangeListener#windDataReceived(Wind, WindSource)} callback method. In particular this replicates all + * wind fixes that may have been loaded from the wind store for the new track. + */ + @Override + protected WindTrack createWindTrack(WindSource windSource, long delayForWindEstimationCacheInvalidation) { + WindTrack result = super.createWindTrack(windSource, delayForWindEstimationCacheInvalidation); + if (windSource.getType().canBeStored()) { + // replicate all wind fixes that may have been loaded by the wind store + result.lockForRead(); + try { + for (Wind wind : result.getRawFixes()) { + notifyListeners(wind, windSource); // Note that this doesn't notify the track's listeners but the tracked race's listeners. + } // In particular, the wind store won't receive events (again) for the wind fixes it already has. + } finally { + result.unlockAfterRead(); + } + } + return result; + } + + @Override + public boolean recordWind(Wind wind, WindSource windSource) { + final boolean result; + // TODO check what a good filter is; remember that start/end of tracking may change over time; what if we have discarded valuable wind fixes? + TimePoint startOfRace = getStartOfRace(); + TimePoint startOfTracking = getStartOfTracking(); + TimePoint endOfRace = getEndOfRace(); + TimePoint endOfTracking = getEndOfTracking(); + if ((startOfTracking == null || !startOfTracking.minus(TrackedRaceImpl.TIME_BEFORE_START_TO_TRACK_WIND_MILLIS).after(wind.getTimePoint()) || + (startOfRace != null && !startOfRace.minus(TrackedRaceImpl.TIME_BEFORE_START_TO_TRACK_WIND_MILLIS).after(wind.getTimePoint()))) + && + (endOfTracking == null || endOfTracking.plus(TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS).after(wind.getTimePoint()) || + (endOfRace != null && endOfRace.plus(TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS).after(wind.getTimePoint())))) { + result = getOrCreateWindTrack(windSource).add(wind); + updated(/* time point */null); // wind events shouldn't advance race time + triggerManeuverCacheRecalculationForAllCompetitors(); + notifyListeners(wind, windSource); + } else { + result = false; + } + return result; + } + + @Override + public void removeWind(Wind wind, WindSource windSource) { + getOrCreateWindTrack(windSource).remove(wind); + updated(/* time point */null); // wind events shouldn't advance race time + triggerManeuverCacheRecalculationForAllCompetitors(); + notifyListenersWindRemoved(wind, windSource); + } + + @Override + public void gpsFixReceived(GPSFixMoving fix, Competitor competitor) { + updated(fix.getTimePoint()); + triggerManeuverCacheRecalculation(competitor); + notifyListeners(fix, competitor); + } + + @Override + public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) { + notifyListenersSpeedAveragingChanged(oldMillisecondsOverWhichToAverage, newMillisecondsOverWhichToAverage); + } + + @Override + public boolean isTransient() { + return false; + } + + @Override + protected TrackedLeg createTrackedLeg(Leg leg) { + return new TrackedLegImpl(this, leg, getRace().getCompetitors()); + } + + @Override + public long getMillisecondsOverWhichToAverageSpeed() { + long result = 0; // default in case there is no competitor + Iterator compIter = getRace().getCompetitors().iterator(); + if (compIter.hasNext()) { + DynamicGPSFixTrack someTrack = getTrack(compIter.next()); + result = someTrack.getMillisecondsOverWhichToAverageSpeed(); + } + return result; + } + + @Override + public long getMillisecondsOverWhichToAverageWind() { + long result = 0; // default in case there is no competitor + for (WindSource windSource : getWindSources()) { + WindTrack someTrack = getOrCreateWindTrack(windSource); + result = someTrack.getMillisecondsOverWhichToAverageWind(); + } + return result; + } + + @Override + public DynamicTrackedRegatta getTrackedRegatta() { + return (DynamicTrackedRegatta) super.getTrackedRegatta(); + } + + @Override + public void setRaceIsKnownToStartUpwind(boolean raceIsKnownToStartUpwind) { + this.raceIsKnownToStartUpwind = raceIsKnownToStartUpwind; + } + + @Override + public boolean raceIsKnownToStartUpwind() { + return raceIsKnownToStartUpwind; + } + + @Override + public void attachRaceLog(RaceLog raceLog) { + super.attachRaceLog(raceLog); + logListener.addTo(raceLog); + } + + @Override + public void detachRaceLog(Serializable identifier) { + RaceLog attachedRaceLog = attachedRaceLogs.get(identifier); + if (attachedRaceLog != null) { + logListener.removeFrom(attachedRaceLog); + } + super.detachRaceLog(identifier); + } + + @Override + public void addCourseDesignChangedListener(CourseDesignChangedListener listener) { + this.courseDesignChangedListeners.add(listener); + } + + @Override + public void onCourseDesignChangedByRaceCommittee(CourseBase newCourseDesign) { + try { + for (CourseDesignChangedListener courseDesignChangedListener : courseDesignChangedListeners) { + courseDesignChangedListener.courseDesignChanged(newCourseDesign); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void onStartTimeChangedByRaceCommittee(TimePoint newStartTime) { + try { + for (StartTimeChangedListener startTimeChangedListener : startTimeChangedListeners) { + startTimeChangedListener.startTimeChanged(newStartTime); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void addStartTimeChangedListener(StartTimeChangedListener listener) { + this.startTimeChangedListeners.add(listener); + } + /** + * @return The Bearing of a line starting at w that needs to be crossed to pass a mark. + */ + @Override + public Bearing getCrossingBearing(Waypoint w, TimePoint t) { + Bearing result = null; + PassingInstruction instruction = w.getPassingInstructions(); + if (instruction == PassingInstruction.None || instruction == null) { + if (w.equals(getRace().getCourse().getFirstWaypoint()) || w.equals(getRace().getCourse().getLastWaypoint())) { + instruction = PassingInstruction.Line; + } else { + int numberofMarks = 0; + Iterator it = w.getMarks().iterator(); + while (it.hasNext()) { + it.next(); + numberofMarks++; + } + if (numberofMarks == 2) { + instruction = PassingInstruction.Gate; + } else if (numberofMarks == 1) { + instruction = PassingInstruction.Port; + } + } + } + if (instruction == PassingInstruction.FixedBearing) { + result = w.getFixedBearing(); + } else if (instruction == PassingInstruction.Gate || instruction == PassingInstruction.Port || instruction == PassingInstruction.Starboard) { + Bearing before = getTrackedLegFinishingAt(w).getLegBearing(t); + Bearing after = getTrackedLegStartingAt(w).getLegBearing(t); + if (before != null && after != null) { + result = before.middle(after.reverse()); + } + } else if (instruction == PassingInstruction.Line) { + Pair pos = getPortAndStarboardMarks(t, w); + if (pos.getA() != null && pos.getB() != null) { + Position portPosition = getOrCreateTrack(pos.getA()).getEstimatedPosition(t, false); + Position starboardPosition = getOrCreateTrack(pos.getB()).getEstimatedPosition(t, false); + if (portPosition != null && starboardPosition != null) { + result = portPosition.getBearingGreatCircle(starboardPosition); + } + } + } else if (instruction == PassingInstruction.Offset) { + // TODO Bug 1712 + } + return result; + } + @Override + public Pair getPortAndStarboardMarks(TimePoint t, Waypoint w) { + List markPositions = new ArrayList(); + for (Mark lineMark : w.getMarks()) { + final Position estimatedMarkPosition = getOrCreateTrack(lineMark).getEstimatedPosition(t, /* extrapolate */ + false); + if (estimatedMarkPosition == null) { + return new Pair(null,null); + } + markPositions.add(estimatedMarkPosition); + } + final List legs = getRace().getCourse().getLegs(); + final int indexOfWaypoint = getRace().getCourse().getIndexOfWaypoint(w); + final boolean isStartLine = indexOfWaypoint == 0; + final Bearing legDeterminingDirectionBearing = getTrackedLeg(legs.get(isStartLine ? 0 : indexOfWaypoint - 1)).getLegBearing(t); + if (legDeterminingDirectionBearing == null) { + return new Pair(null, null); + } + Distance crossTrackErrorOfMark0OnLineFromMark1ToNextWaypoint = markPositions.get(0).crossTrackError(markPositions.get(1), legDeterminingDirectionBearing); + final Mark starboardMarkWhileApproachingLine; + final Mark portMarkWhileApproachingLine; + if (crossTrackErrorOfMark0OnLineFromMark1ToNextWaypoint.getMeters() < 0) { + portMarkWhileApproachingLine = Util.get(w.getMarks(), 0); + starboardMarkWhileApproachingLine = Util.get(w.getMarks(), 1); + } else { + portMarkWhileApproachingLine = Util.get(w.getMarks(), 1); + starboardMarkWhileApproachingLine = Util.get(w.getMarks(), 0); + } + return new Pair(portMarkWhileApproachingLine, starboardMarkWhileApproachingLine); + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRegattaImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRegattaImpl.java index 891af0e4772..74b99999984 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRegattaImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRegattaImpl.java @@ -3,6 +3,7 @@ package com.sap.sailing.domain.tracking.impl; import com.sap.sailing.domain.base.RaceDefinition; import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.base.Sideline; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; import com.sap.sailing.domain.tracking.DynamicRaceDefinitionSet; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; @@ -27,9 +28,9 @@ public class DynamicTrackedRegattaImpl extends TrackedRegattaImpl implements Dyn @Override public DynamicTrackedRace createTrackedRace(RaceDefinition raceDefinition, Iterable sidelines, WindStore windStore, - long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed, + GPSFixStore gpsFixStore, long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed, DynamicRaceDefinitionSet raceDefinitionSetToUpdate) { - return (DynamicTrackedRace) super.createTrackedRace(raceDefinition, sidelines, windStore, delayToLiveInMillis, millisecondsOverWhichToAverageWind, + return (DynamicTrackedRace) super.createTrackedRace(raceDefinition, sidelines, windStore, gpsFixStore, delayToLiveInMillis, millisecondsOverWhichToAverageWind, millisecondsOverWhichToAverageSpeed, raceDefinitionSetToUpdate); } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/GPSFixTrackImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/GPSFixTrackImpl.java index 9f8e9f25cbf..3c20fe1a1d2 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/GPSFixTrackImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/GPSFixTrackImpl.java @@ -1,1047 +1,1048 @@ -package com.sap.sailing.domain.tracking.impl; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.ConcurrentModificationException; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.NavigableSet; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; - -import com.sap.sailing.domain.base.BearingWithConfidence; -import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence; -import com.sap.sailing.domain.base.SpeedWithConfidence; -import com.sap.sailing.domain.base.Timed; -import com.sap.sailing.domain.base.impl.BearingWithConfidenceImpl; -import com.sap.sailing.domain.base.impl.SpeedWithBearingWithConfidenceImpl; -import com.sap.sailing.domain.base.impl.SpeedWithConfidenceImpl; -import com.sap.sailing.domain.common.Bearing; -import com.sap.sailing.domain.common.Distance; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.Speed; -import com.sap.sailing.domain.common.SpeedWithBearing; -import com.sap.sailing.domain.common.TimePoint; -import com.sap.sailing.domain.common.impl.KnotSpeedImpl; -import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; -import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; -import com.sap.sailing.domain.common.impl.NauticalMileDistance; -import com.sap.sailing.domain.common.impl.Util.Pair; -import com.sap.sailing.domain.confidence.ConfidenceBasedAverager; -import com.sap.sailing.domain.confidence.ConfidenceFactory; -import com.sap.sailing.domain.confidence.HasConfidence; -import com.sap.sailing.domain.confidence.Weigher; -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.GPSTrackListener; -import com.sap.sailing.domain.tracking.WithValidityCache; -import com.sap.sailing.util.impl.ArrayListNavigableSet; - -public class GPSFixTrackImpl extends TrackImpl implements GPSFixTrack { - private static final Logger logger = Logger.getLogger(GPSFixTrackImpl.class.getName()); - private static final long serialVersionUID = -7282869695818293745L; - private static final Speed DEFAULT_MAX_SPEED_FOR_SMOOTHING = new KnotSpeedImpl(50); - protected final Speed maxSpeedForSmoothing; - - private final ItemType trackedItem; - private long millisecondsOverWhichToAverage; - private final GPSTrackListeners listeners; - - - private static class GPSTrackListeners implements Serializable { - private static final long serialVersionUID = -7117842092078781722L; - private Set> listeners; - - public GPSTrackListeners() { - listeners = new HashSet<>(); - } - - @SuppressWarnings("unchecked") // need typed generic cast - private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { - listeners = (Set>) ois.readObject(); - } - - private void writeObject(ObjectOutputStream oos) throws IOException { - final Set> listenersToSerialize; - synchronized (listeners) { - listenersToSerialize = new HashSet<>(); - for (GPSTrackListener listener : listeners) { - if (!listener.isTransient()) { - listenersToSerialize.add(listener); - } - } - } - oos.writeObject(listenersToSerialize); - } - - public void addListener(GPSTrackListener listener) { - synchronized (listeners) { - listeners.add(listener); - } - } - - public void removeListener(GPSTrackListener listener) { - synchronized (listeners) { - listeners.remove(listener); - } - } - - /** - * To iterate over the resulting listener list, synchronize on the iterable returned. Only this will avoid - * {@link ConcurrentModificationException}s because listeners may be added on the fly, and this object will - * synchronize on the listeners collection before adding on. - */ - public Iterable> getListeners() { - synchronized (listeners) { - return new HashSet>(listeners); - } - } - } - - /** - * Computing {@link #getDistanceTraveled(TimePoint, TimePoint)} is more expensive the longer the track is and the - * higher the tracking frequency. It is called regularly, usually for the same start time. Keeping to re-evaluate - * all distances between all GPS fixes, even if they remain constant, is a waste of resources, particularly when we - * assume that usually GPS fixes are appended to a track and not usually randomly inserted, even though this can - * happen. - *

- * - * This cache looks "backwards." It contains pairs whose first component represents a to parameter used - * in {@link #getDistanceTraveled(TimePoint, TimePoint)}. It is ordered by this component. The second component is a - * navigable, ordered set of pairs where the first pair component represents a from parameter used in - * {@link #getDistanceTraveled(TimePoint, TimePoint)} and the second pair component represents the result of - * {@link #getDistanceTraveled(TimePoint, TimePoint)} for this parameter combination. Note that the cache does only - * store distances for from time points actually passed to - * {@link #getDistanceTraveled(TimePoint, TimePoint)} and does not cache all interim combinations collected while - * computing a result. As such, it differs from a typical "dynamic programming" algorithm which would store all - * in-between values too. Assuming that from values are sparse (leg start, race start, ...), there - * doesn't seem to be much use in storing all the other in-between values too. - *

- * - * For implementation efficiency in combination with using a {@link ArrayListNavigableSet} for the values and in - * order to be able to efficiently extend a cache entry for a single to fix, the navigable sets - * containing the from fixes and distances are ordered such that earlier fixes come later in the set. - * This way, extending the cache entry for a to fix to an earlier from fix only requires - * appending to the set. - *

- * - * Invalidation: When a new fix is added to this track, all distance cache entries for fixes at or later than - * the new fix's time point are removed from this cache. Additionally, the fix insertion may have an impact on the - * {@link #getEarlierFixesWhoseValidityMayBeAffected(GPSFix) previous fix's} validity (track smoothing) and - * therefore on its selection for distance aggregation. Therefore, if fix addition turned the previous fix invalid, - * the cache entries for the time points at or after the previous fix also need to be removed. - *

- * - * Cache use: When {@link #getDistanceTraveled(TimePoint, TimePoint)} is called, it first looks for a cache - * entry for the to parameter. If one is found, the earliest entry in the navigable set for the - * navigable set of from and distance values that is at or after the requested from time - * point is determined. If such an entry exists, the distance is remembered and the algorithm is repeated - * recursively, using the from value found in the cache as the new to value, and the - * from value originally passed to {@link #getDistanceTraveled(TimePoint, TimePoint)} as - * from again. If no entry is found in the cache entry for to that is at or after the - * requested from time, the distance is computed by iterating the smoothened fixes. - *

- * - * If a cache entry for to is not found, the latest cache entry before it is looked up. If one is - * found, the distance between the to time point requested and the to time point found in - * the cache is computed by iterating the smoothened fixes for this interval. If none is found, the distance is - * computed by iterating backwards all the way to from. - *

- * - * Once the {@link #getDistanceTraveled(TimePoint, TimePoint)} has computed its value, it adds the result to the - * cache. - */ - private transient DistanceCache distanceCache; - - private transient MaxSpeedCache maxSpeedCache; - - public GPSFixTrackImpl(ItemType trackedItem, long millisecondsOverWhichToAverage) { - this(trackedItem, millisecondsOverWhichToAverage, DEFAULT_MAX_SPEED_FOR_SMOOTHING); - } - - /** - * @param maxSpeedForSmoothening - * if null, any fix speed will be accepted as long as fix-provided speed (if any) and - * inferred speed can be matched. - */ - public GPSFixTrackImpl(ItemType trackedItem, long millisecondsOverWhichToAverage, Speed maxSpeedForSmoothening) { - super(/* nameForReadWriteLock */ GPSFixTrackImpl.class.getSimpleName()+(trackedItem==null?"":(" for "+trackedItem.toString()))); - this.trackedItem = trackedItem; - this.millisecondsOverWhichToAverage = millisecondsOverWhichToAverage; - this.maxSpeedForSmoothing = maxSpeedForSmoothening; - this.listeners = new GPSTrackListeners(); - this.distanceCache = new DistanceCache(trackedItem==null?"null":trackedItem.toString()); - this.maxSpeedCache = new MaxSpeedCache(this); - } - - private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { - ois.defaultReadObject(); - distanceCache = new DistanceCache(trackedItem.toString()); - maxSpeedCache = new MaxSpeedCache(this); - } - - @Override - public String toString() { - return super.toString()+" for "+getTrackedItem(); - } - - @Override - public void addListener(GPSTrackListener listener) { - listeners.addListener(listener); - } - - @Override - public void removeListener(GPSTrackListener listener) { - listeners.removeListener(listener); - } - - protected Iterable> getListeners() { - return listeners.getListeners(); - } - - private class DummyGPSFix extends DummyTimed implements GPSFix { - private static final long serialVersionUID = -6258506654181816698L; - - public DummyGPSFix(TimePoint timePoint) { - super(timePoint); - } - @Override - public Position getPosition() { - return null; - } - @Override - public SpeedWithBearing getSpeedAndBearingRequiredToReach(GPSFix to) { - return null; - } - @Override - public boolean isValidityCached() { - return false; - } - - @Override - public boolean isValid() { - return false; - } - - @Override - public void invalidateCache() { - } - - @Override - public void cacheValidity(boolean isValid) { - } - } - - - @Override - protected FixType getDummyFix(TimePoint timePoint) { - @SuppressWarnings("unchecked") - FixType result = (FixType) new DummyGPSFix(timePoint); - return result; - } - - @Override - public ItemType getTrackedItem() { - return trackedItem; - } - - @Override - public long getMillisecondsOverWhichToAverageSpeed() { - return millisecondsOverWhichToAverage; - } - - private Pair getFixesForPositionEstimation(TimePoint timePoint, boolean inclusive) { - lockForRead(); - try { - FixType lastFixBefore = inclusive ? getLastFixAtOrBefore(timePoint) : getLastFixBefore(timePoint); - FixType firstFixAfter = inclusive ? getFirstFixAtOrAfter(timePoint) : getFirstFixAfter(timePoint); - return new Pair(lastFixBefore, firstFixAfter); - } finally { - unlockAfterRead(); - } - } - - private class EstimatedPositionIterator implements Iterator { - private final Iterator timedsIter; - private final boolean extrapolate; - private final NavigableSet fixes; - private Iterator subSetIterator; - private FixType earlierFix; - private FixType laterFix; // if this is null, earlierFix is also null - - public EstimatedPositionIterator(Iterable timeds, boolean extrapolate) { - this.timedsIter = timeds.iterator(); - this.extrapolate = extrapolate; - this.fixes = getFixes(); - } - - @Override - public boolean hasNext() { - return timedsIter.hasNext(); - } - - @Override - public Position next() { - TimePoint nextTimePoint = timedsIter.next().getTimePoint(); - if (subSetIterator == null) { - earlierFix = getLastFixAtOrBefore(nextTimePoint); - subSetIterator = fixes.subSet(createDummyGPSFix(nextTimePoint), /* fromInclusive */true, fixes.last(), /* toInclusive */ - true).iterator(); - laterFix = subSetIterator.hasNext() ? subSetIterator.next() : null; - } else { - while (laterFix != null && laterFix.getTimePoint().before(nextTimePoint)) { - earlierFix = laterFix; - laterFix = subSetIterator.hasNext() ? subSetIterator.next() : null; - } - } - return getEstimatedPosition(nextTimePoint, extrapolate, earlierFix, laterFix); - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - } - - @Override - public Iterator getEstimatedPositions(Iterable timeds, boolean extrapolate) { - return new EstimatedPositionIterator(timeds, extrapolate); - } - - @Override - public Position getEstimatedPosition(TimePoint timePoint, boolean extrapolate) { - lockForRead(); - try { - Pair fixesForPositionEstimation = getFixesForPositionEstimation(timePoint, /* inclusive */ true); - return getEstimatedPosition(timePoint, extrapolate, fixesForPositionEstimation.getA(), fixesForPositionEstimation.getB()); - } finally { - unlockAfterRead(); - } - } - - @Override - public Pair getEstimatedPositionTimePeriodAffectedBy(GPSFix fix) { - if (fix == null) { - throw new IllegalArgumentException("fix must not be null"); - } - lockForRead(); - try { - Pair fixesForPositionEstimation = getFixesForPositionEstimation(fix.getTimePoint(), /* inclusive */ true); - final TimePoint start; - if (fix.equals(fixesForPositionEstimation.getA())) { - if (getLastFixBefore(fix.getTimePoint()) == null) { - // fix is the first fix in the track; all position estimation before this point may be affected - start = new MillisecondsTimePoint(0); - } else { - start = fix.getTimePoint(); - } - } else { - if (fixesForPositionEstimation.getA() == null) { - start = new MillisecondsTimePoint(0); - } else { - start = fixesForPositionEstimation.getA().getTimePoint(); - } - } - final TimePoint end; - if (fix.equals(fixesForPositionEstimation.getB())) { - if (getFirstFixAfter(fix.getTimePoint()) == null) { - // fix is the first fix in the track; all position estimation before this point may be affected - end = new MillisecondsTimePoint(Long.MAX_VALUE); - } else { - end = fix.getTimePoint(); - } - } else { - if (fixesForPositionEstimation.getB() == null) { - end = new MillisecondsTimePoint(Long.MAX_VALUE); - } else { - end = fixesForPositionEstimation.getB().getTimePoint(); - } - } - return new Pair(start, end); - } finally { - unlockAfterRead(); - } - } - - @Override - public Position getEstimatedRawPosition(TimePoint timePoint, boolean extrapolate) { - lockForRead(); - try { - FixType lastFixAtOrBefore = getLastRawFixAtOrBefore(timePoint); - FixType firstFixAtOrAfter = getFirstRawFixAtOrAfter(timePoint); - return getEstimatedPosition(timePoint, extrapolate, lastFixAtOrBefore, firstFixAtOrAfter); - } finally { - unlockAfterRead(); - } - } - - private Position getEstimatedPosition(TimePoint timePoint, boolean extrapolate, FixType lastFixAtOrBefore, - FixType firstFixAtOrAfter) { - lockForRead(); - try { - // TODO bug #346: compute a confidence value for the position returned based on time difference between fix(es) and timePoint; consider using Taylor approximation of more fixes around timePoint to predict and weigh position - if (lastFixAtOrBefore != null && lastFixAtOrBefore == firstFixAtOrAfter) { - return lastFixAtOrBefore.getPosition(); // exact match; how unlikely is that? - } else { - if (lastFixAtOrBefore == null && firstFixAtOrAfter != null) { - // TODO shouldn't this extrapolate into the past if extrapolate==true? - return firstFixAtOrAfter.getPosition(); // asking for time point before first fix: return first fix's position - } - if (firstFixAtOrAfter == null && !extrapolate) { - return lastFixAtOrBefore == null ? null : lastFixAtOrBefore.getPosition(); - } else { - SpeedWithBearing estimatedSpeed = estimateSpeed(lastFixAtOrBefore, firstFixAtOrAfter); - if (estimatedSpeed == null) { - // TODO even if extrapolation is requested, if there is no firstFixAtOrAfter, and the GPSFixes are without speed, shouldn't this at least return the position at the lastFixAtOrBefore instead of null? - return null; - } else { - if (lastFixAtOrBefore != null) { - Distance distance = estimatedSpeed.travel(lastFixAtOrBefore.getTimePoint(), timePoint); - Position result = lastFixAtOrBefore.getPosition().translateGreatCircle( - estimatedSpeed.getBearing(), distance); - return result; - } else { - // firstFixAtOrAfter can't be null because otherwise no speed could have been estimated - // TODO shouldn't this extrapolate into the past if extrapolate==true? - return firstFixAtOrAfter.getPosition(); - } - } - } - } - } finally { - unlockAfterRead(); - } - } - - @Override - public Pair getMaximumSpeedOverGround(TimePoint from, TimePoint to) { - return maxSpeedCache.getMaxSpeed(from, to); - } - - private SpeedWithBearing estimateSpeed(FixType fix1, FixType fix2) { - if (fix1 == null) { - if (fix2 instanceof GPSFixMoving) { - return ((GPSFixMoving) fix2).getSpeed(); - } else { - return null; - } - } else if (fix2 == null) { - FixType lastBeforeFix1 = getLastFixBefore(fix1.getTimePoint()); - if (lastBeforeFix1 != null) { - fix2 = fix1; - fix1 = lastBeforeFix1; // compute speed based on the last two fixes and assume constant speed - } else { - if (fix1 instanceof GPSFixMoving) { - return ((GPSFixMoving) fix1).getSpeed(); - } else { - return null; - } - } - } - Distance distance = fix1.getPosition().getDistance(fix2.getPosition()); - long millis = Math.abs(fix1.getTimePoint().asMillis() - fix2.getTimePoint().asMillis()); - SpeedWithBearing speed = new KnotSpeedWithBearingImpl(distance.getNauticalMiles() / (millis / 1000. / 3600.), - fix1.getPosition().getBearingGreatCircle(fix2.getPosition())); - return speed; - } - - - /** - * Returns the smoothened fixes (see {@link #getInternalFixes()}), type-cast such that it's a set of {@link GPSFix} - * objects - */ - private NavigableSet getGPSFixes() { - @SuppressWarnings("unchecked") - NavigableSet result = (NavigableSet) getInternalFixes(); - return result; - } - - @Override - public Distance getDistanceTraveled(TimePoint from, TimePoint to) { - return getDistanceTraveledRecursively(from, to, 0); - } - - private Distance getDistanceTraveledRecursively(TimePoint from, TimePoint to, int recursionDepth) { - Distance result; - if (!from.before(to)) { - result = Distance.NULL; - } else { - lockForRead(); - try { - Pair> bestCacheEntry = getDistanceCache() - .getEarliestFromAndDistanceAtOrAfterFrom(from, to); - if (bestCacheEntry != null) { - // compute the missing stretches between best cache entry's "from" and our "from" and the cache - // entry's "to" and our "to" - Distance distanceFromFromToBeginningOfCacheEntry = Distance.NULL; - Distance distanceFromEndOfCacheEntryToTo = Distance.NULL; - if (!bestCacheEntry.getB().getA().equals(from)) { - assert bestCacheEntry.getB().getA().after(from); - distanceFromFromToBeginningOfCacheEntry = getDistanceTraveledRecursively(from, bestCacheEntry - .getB().getA(), recursionDepth + 1); - } - if (!bestCacheEntry.getA().equals(to)) { - assert bestCacheEntry.getA().before(to); - distanceFromEndOfCacheEntryToTo = getDistanceTraveledRecursively(bestCacheEntry.getA(), to, - recursionDepth + 1); - } - result = distanceFromFromToBeginningOfCacheEntry.add(bestCacheEntry.getB().getB()).add( - distanceFromEndOfCacheEntryToTo); - } else { - double distanceInNauticalMiles = 0; - if (from.compareTo(to) < 0) { - // getEstimatedPosition's current implementation returns a position equal to that of a fix at - // "from" if there is one with exactly that time stamp - Position fromPos = getEstimatedPosition(from, /* extrapolate */false); - if (fromPos == null) { - result = Distance.NULL; - } else { - lockForRead(); - try { - NavigableSet subset = getGPSFixes().subSet(new DummyGPSFix(from), - /* fromInclusive */false, new DummyGPSFix(to), - /* toInclusive */false); - for (GPSFix fix : subset) { - double distanceBetweenAdjacentFixesInNauticalMiles = fromPos.getDistance( - fix.getPosition()).getNauticalMiles(); - distanceInNauticalMiles += distanceBetweenAdjacentFixesInNauticalMiles; - fromPos = fix.getPosition(); - } - } finally { - unlockAfterRead(); - } - Position toPos = getEstimatedPosition(to, false); - distanceInNauticalMiles += fromPos.getDistance(toPos).getNauticalMiles(); - result = new NauticalMileDistance(distanceInNauticalMiles); - } - } else { - result = Distance.NULL; - } - } - } finally { - unlockAfterRead(); - } - getDistanceCache().cache(from, to, result); - } - return result; - } - - @Override - public Distance getRawDistanceTraveled(TimePoint from, TimePoint to) { - lockForRead(); - try { - double distanceInNauticalMiles = 0; - if (from.compareTo(to) < 0) { - Position fromPos = getEstimatedRawPosition(from, false); - if (fromPos == null) { - return Distance.NULL; - } - @SuppressWarnings("unchecked") - NavigableSet subset = (NavigableSet) getInternalRawFixes().subSet((FixType) new DummyGPSFix(from), - /* fromInclusive */false, (FixType) new DummyGPSFix(to), - /* toInclusive */false); - for (GPSFix fix : subset) { - distanceInNauticalMiles += fromPos.getDistance(fix.getPosition()).getNauticalMiles(); - fromPos = fix.getPosition(); - } - Position toPos = getEstimatedRawPosition(to, false); - distanceInNauticalMiles += fromPos.getDistance(toPos).getNauticalMiles(); - return new NauticalMileDistance(distanceInNauticalMiles); - } else { - return Distance.NULL; - } - } finally { - unlockAfterRead(); - } - } - - /** - * Here we know for sure that the GPS fixes are {@link GPSFixMoving} instances, - * so we can use their {@link GPSFixMoving#getSpeed() speed} in averaging. We're still - * using an interval of {@link #getMillisecondsOverWhichToAverage()} around at, - * but this time we add the speeds and bearings provided by the fix onto the values for - * averaging, so the result considers both, the GPS-provided speeds and bearings as well as - * the speeds/bearings determined by distance/time difference of the fixes themselves. - */ - @Override - public SpeedWithBearing getEstimatedSpeed(TimePoint at) { - lockForRead(); - try { - SpeedWithBearingWithConfidence estimatedSpeed = getEstimatedSpeed(at, getInternalFixes(), - ConfidenceFactory.INSTANCE.createExponentialTimeDifferenceWeigher( - // use a minimum confidence to avoid the bearing to flip to 270deg in case all is zero - getMillisecondsOverWhichToAverageSpeed()/2, /* minimumConfidence */ 0.00000001)); // half confidence if half averaging interval apart - return estimatedSpeed == null ? null : estimatedSpeed.getObject(); - } finally { - unlockAfterRead(); - } - } - - @Override - public SpeedWithBearing getRawEstimatedSpeed(TimePoint at) { - lockForRead(); - try { - return getEstimatedSpeed(at, getRawFixes(), - ConfidenceFactory.INSTANCE.createExponentialTimeDifferenceWeigher( - // use a minimum confidence to avoid the bearing to flip to 270deg in case all is zero - getMillisecondsOverWhichToAverageSpeed(), /* minimumConfidence */ 0.00000001)).getObject(); - } finally { - unlockAfterRead(); - } - } - - @Override - public SpeedWithBearingWithConfidence getEstimatedSpeed(TimePoint at, Weigher weigher) { - lockForRead(); - try { - return getEstimatedSpeed(at, getInternalFixes(), weigher); - } finally { - unlockAfterRead(); - } - } - - @Override - public SpeedWithBearingWithConfidence getRawEstimatedSpeed(TimePoint at, Weigher weigher) { - lockForRead(); - try { - return getEstimatedSpeed(at, getRawFixes(), weigher); - } finally { - unlockAfterRead(); - } - } - - /** - * Since we don't know for sure whether the GPS fixes are {@link GPSFixMoving} instances, here we only estimate - * speed based on the distance and time between the fixes, averaged over an interval of - * {@link #millisecondsOverWhichToAverage} milliseconds around at. Subclasses that know about the - * particular fix type may redefine this to exploit a {@link SpeedWithBearing} attached, e.g., to a - * {@link GPSFixMoving}. - * - * @param weigher - * If null, a confidence of 1.0 is assumed for all fixes used in the speed/bearing - * estimation. Otherwise, the confidence of each fix contributing to the estimation is computed using the - * weigher. - * @return a speed/bearing with the average confidence attributed to the fixes that contributed to the estimation - */ - protected SpeedWithBearingWithConfidence getEstimatedSpeed(TimePoint at, - NavigableSet fixesToUseForSpeedEstimation, Weigher weigher) { - lockForRead(); - try { - List relevantFixes = getFixesRelevantForSpeedEstimation(at, fixesToUseForSpeedEstimation); - List> speeds = new ArrayList>(); - BearingWithConfidenceCluster bearingCluster = new BearingWithConfidenceCluster(weigher); - FixType last = null; - for (FixType next : relevantFixes) { - if (last != null) { - // TODO bug #346: consider time difference between next.getTimepoint() and at to compute a confidence - aggregateSpeedAndBearingFromLastToNext(speeds, bearingCluster, last, next); - } - last = next; - } - ConfidenceBasedAverager speedAverager = ConfidenceFactory.INSTANCE.createAverager(weigher); - HasConfidence speedWithConfidence = speedAverager.getAverage(speeds, at); - BearingWithConfidence bearingAverage = bearingCluster.getAverage(at); - Bearing bearing = bearingAverage == null ? null : bearingAverage.getObject(); - SpeedWithBearing avgSpeed = (speedWithConfidence == null || bearing == null) ? null : - new KnotSpeedWithBearingImpl(speedWithConfidence.getObject().getKnots(), bearing); - SpeedWithBearingWithConfidence result = avgSpeed == null ? null : - new SpeedWithBearingWithConfidenceImpl(avgSpeed, (bearingAverage.getConfidence() + speedWithConfidence.getConfidence())/2., at); - return result; - } finally { - unlockAfterRead(); - } - } - - protected void aggregateSpeedAndBearingFromLastToNext(List> speeds, - BearingWithConfidenceCluster bearingCluster, GPSFix last, GPSFix next) { - MillisecondsTimePoint relativeTo = new MillisecondsTimePoint((last.getTimePoint().asMillis() + next.getTimePoint().asMillis())/2); - Speed speed = last.getPosition().getDistance(next.getPosition()) - .inTime(next.getTimePoint().asMillis() - last.getTimePoint().asMillis()); - SpeedWithConfidenceImpl speedWithConfidence = new SpeedWithConfidenceImpl(speed, /* original confidence */ - 0.9, relativeTo); - speeds.add(speedWithConfidence); - double bearingConfidence = 0.9; - if (speed.getKnots() < 0.01) { - bearingConfidence = 0; - } - bearingCluster.add(new BearingWithConfidenceImpl(last.getPosition().getBearingGreatCircle(next.getPosition()), - bearingConfidence, // TODO use number of tracked satellites to determine confidence of single fix - relativeTo)); - } - - /** - * Computes the time interval such that {@link #getFixesRelevantForSpeedEstimation(TimePoint, NavigableSet)}, when - * passed any time point from that interval, produces "fix" as part of its result array. The algorithm for - * determining this interval co-varies with the implementation of - * {@link #getFixesRelevantForSpeedEstimation(TimePoint, NavigableSet)}.

- * - * This implementation looks for fixes {@link #getMillisecondsOverWhichToAverage()}/2 before and after fix. - * If no fix is found in a direction within half the averaging interval but there is a fix in that direction which is further - * apart, the yet next fix is checked. If there is none, or that next-next fix is further apart from the next fix than fix, - * then the next fix is added to the result interval. This is because for that next fix, fix in that case will be relevant - * for speed estimation because it's the closest fix. - */ - protected Pair getTimeIntervalWhoseEstimatedSpeedMayHaveChangedAfterAddingFix(FixType fix) { - TimePoint intervalStart = null; - TimePoint intervalEnd = null; - lockForRead(); - try { - NavigableSet beforeSet = getInternalRawFixes().headSet(fix, /* inclusive */false); - NavigableSet afterSet = getInternalRawFixes().tailSet(fix, /* inclusive */true); - FixType beforeFix = null; - Iterator beforeFixIter = beforeSet.descendingIterator(); - while (beforeFixIter.hasNext() - && fix.getTimePoint().asMillis() - (beforeFix = beforeFixIter.next()).getTimePoint().asMillis() < getMillisecondsOverWhichToAverage() / 2) { - intervalStart = beforeFix.getTimePoint(); - } - if (intervalStart == null) { - if (beforeFixIter.hasNext()) { - // No before fix within half averaging interval, but there is one further away; is its next neighbour even further away? - // If so, or no more neighbours are found, it's affected and marks the invalidation interval start; otherwise, fix' - // time point is the invalidation interval start - TimePoint intervalStartCandidate = beforeFixIter.next().getTimePoint(); - if (beforeFixIter.hasNext()) { - TimePoint nextNeighboursTimePoint = beforeFixIter.next().getTimePoint(); - if (intervalStartCandidate.asMillis()-nextNeighboursTimePoint.asMillis() > fix.getTimePoint().asMillis()-intervalStartCandidate.asMillis()) { - intervalStart = intervalStartCandidate; - } - } else { - intervalStart = intervalStartCandidate; - } - } - } - if (intervalStart == null) { - intervalStart = fix.getTimePoint(); - } - FixType afterFix = null; - Iterator afterFixIter = afterSet.iterator(); - while (afterFixIter.hasNext() - && (afterFix = afterFixIter.next()).getTimePoint().asMillis() - fix.getTimePoint().asMillis() < getMillisecondsOverWhichToAverage() / 2) { - intervalEnd = afterFix.getTimePoint(); - } - if (intervalEnd == null) { - if (afterFixIter.hasNext()) { - // No after fix within half averaging interval, but there is one further away; is its next neighbour even further away? - // If so, or no more neighbours are found, it's affected and marks the invalidation interval start; otherwise, fix's - // time point is the invalidation interval end - TimePoint intervalEndCandidate = afterFixIter.next().getTimePoint(); - if (afterFixIter.hasNext()) { - TimePoint nextNeighboursTimePoint = afterFixIter.next().getTimePoint(); - if (nextNeighboursTimePoint.asMillis()-intervalEndCandidate.asMillis() > intervalEndCandidate.asMillis()-fix.getTimePoint().asMillis()) { - intervalEnd = intervalEndCandidate; - } - } else { - intervalEnd = intervalEndCandidate; - } - } - } - if (intervalEnd == null) { - intervalEnd = fix.getTimePoint(); - } - } finally { - unlockAfterRead(); - } - return new Pair(intervalStart, intervalEnd); - } - - protected FixType createDummyGPSFix(TimePoint at) { - @SuppressWarnings("unchecked") - FixType result = (FixType) new DummyGPSFix(at); - return result; - } - - /** - * A track that doesn't have {@link GPSFixMoving} fixes and therefore needs to compute the speed using the fix time - * and position differences. It therefore needs at least two fixes to compute a result. The algorithm always uses - * at least the closest existing fix later and the closet existing fix earlier than at, if available. - *

- * - * But even for a track with {@link GPSFixMoving} fixes this is a good algorithm because in case the speed changes - * significantly between fixes, it is important to know the next fix to understand and consider the trend. - */ - protected List getFixesRelevantForSpeedEstimation(TimePoint at, NavigableSet fixesToUseForSpeedEstimation) { - lockForRead(); - try { - FixType atTimed = createDummyGPSFix(at); - List relevantFixes = new LinkedList(); - NavigableSet beforeSet = fixesToUseForSpeedEstimation.headSet(atTimed, /* inclusive */false); - NavigableSet afterSet = fixesToUseForSpeedEstimation.tailSet(atTimed, /* inclusive */true); - FixType beforeFix = null; - Iterator beforeFixIter = beforeSet.descendingIterator(); - boolean noBeforeFixUsedYet = true; - while (beforeFixIter.hasNext() && - (at.asMillis() - (beforeFix=beforeFixIter.next()).getTimePoint().asMillis() < getMillisecondsOverWhichToAverage() / 2 || noBeforeFixUsedYet)) { - relevantFixes.add(0, beforeFix); - noBeforeFixUsedYet = false; - beforeFix = null; // mark the fix as used - } - FixType afterFix = null; - Iterator afterFixIter = afterSet.iterator(); - boolean noAfterFixUsedYet = true; - while (afterFixIter.hasNext() && - ((afterFix=afterFixIter.next()).getTimePoint().asMillis() - at.asMillis() < getMillisecondsOverWhichToAverage() / 2 || noAfterFixUsedYet)) { - relevantFixes.add(afterFix); - noAfterFixUsedYet = false; - afterFix = null; // mark the fix as used - } - // now fill up relevantFixes until we have at least two fixes or we run out of fixes entirely (can't estimate speed - // with this type of fix on a track with less than two fixes) - while (relevantFixes.size() < 2 && (beforeFix != null || afterFix != null)) { - if (afterFix == null) { - if (beforeFix != null) { - relevantFixes.add(0, beforeFix); // add the last beforeFix to have at least two fixes, although outside of averaging interval - if (beforeFixIter.hasNext()) { - beforeFix = beforeFixIter.next(); - } else { - beforeFix = null; - } - } - } else if (beforeFix == null) { - relevantFixes.add(afterFix); - if (afterFixIter.hasNext()) { - afterFix = afterFixIter.next(); - } else { - afterFix = null; - } - } else { - // both, beforeFix and afterFix are available; choose the one closest to "at" - if (afterFix.getTimePoint().asMillis()-at.asMillis() < beforeFix.getTimePoint().asMillis()-at.asMillis()) { - relevantFixes.add(afterFix); - if (afterFixIter.hasNext()) { - afterFix = afterFixIter.next(); - } else { - afterFix = null; - } - } else { - relevantFixes.add(0, beforeFix); - if (beforeFixIter.hasNext()) { - beforeFix = beforeFixIter.next(); - } else { - beforeFix = null; - } - } - } - } - return relevantFixes; - } finally { - unlockAfterRead(); - } - } - - protected long getMillisecondsOverWhichToAverage() { - return millisecondsOverWhichToAverage; - } - - /** - * Smoothens the track based on a max-speed assumption. - */ - @Override - protected NavigableSet getInternalFixes() { - assertReadLock(); - return new PartialNavigableSetView(super.getInternalFixes()) { - @Override - protected boolean isValid(FixType e) { - return GPSFixTrackImpl.this.isValid(getRawFixes(), e); - } - }; - } - - /** - * When redefining this method, make sure to redefine {@link #invalidateValidityAndDistanceCaches(GPSFix)} - * accordingly. This implementation checks the immediate previous and next fix for e. Therefore, when - * adding a fix, only immediately adjacent fix's validity caches need to be invalidated. - *

- * - * The fix e is considered valid if at least one of its (not necessarily immediate) neighbors that is - * within the range of {@link #getMillisecondsOverWhichToAverageSpeed()} is in reach with less than - * {@link #maxSpeedForSmoothing}, or if there are no neighbors with the time range defined by - * {@link #getMillisecondsOverWhichToAverageSpeed()}. - */ - protected boolean isValid(NavigableSet rawFixes, FixType e) { - assertReadLock(); - boolean isValid; - if (maxSpeedForSmoothing == null) { - isValid = true; - } else { - if (e.isValidityCached()) { - isValid = e.isValid(); - } else { - FixType previous = rawFixes.lower(e); - final boolean atLeastOnePreviousFixInRange = previous != null && e.getTimePoint().asMillis() - previous.getTimePoint().asMillis() <= getMillisecondsOverWhichToAverageSpeed(); - Speed speedToPrevious = null; - boolean foundValidPreviousFixInRange = false; - while (previous != null && !foundValidPreviousFixInRange && e.getTimePoint().asMillis() - previous.getTimePoint().asMillis() <= getMillisecondsOverWhichToAverageSpeed()) { - speedToPrevious = previous.getPosition().getDistance(e.getPosition()) - .inTime(e.getTimePoint().asMillis() - previous.getTimePoint().asMillis()); - foundValidPreviousFixInRange = speedToPrevious.compareTo(maxSpeedForSmoothing) <= 0; - previous = rawFixes.lower(previous); - } - boolean foundValidNextFixInRange = false; - boolean atLeastOneNextFixInRange = false; - // only spend the effort to calculate the "next"-related predicate if the "previous"-related part of the disjunction below isn't already false - if (!atLeastOnePreviousFixInRange || foundValidPreviousFixInRange) { - FixType next = rawFixes.higher(e); - atLeastOneNextFixInRange = next != null && next.getTimePoint().asMillis() - e.getTimePoint().asMillis() <= getMillisecondsOverWhichToAverageSpeed(); - Speed speedToNext = null; - while (next != null && !foundValidNextFixInRange && next.getTimePoint().asMillis() - e.getTimePoint().asMillis() <= getMillisecondsOverWhichToAverageSpeed()) { - speedToNext = e.getPosition().getDistance(next.getPosition()) - .inTime(next.getTimePoint().asMillis() - e.getTimePoint().asMillis()); - foundValidNextFixInRange = speedToNext.compareTo(maxSpeedForSmoothing) <= 0; - next = rawFixes.higher(next); - } - } - isValid = (!atLeastOnePreviousFixInRange || foundValidPreviousFixInRange) && (!atLeastOneNextFixInRange || foundValidNextFixInRange); - e.cacheValidity(isValid); - } - } - return isValid; - } - - /** - * After gpsFix was added to this track, invalidate the {@link WithValidityCache validity caches} - * of the fixes whose validity may be affected. If subclasses redefine {@link #isValid(PartialNavigableSetView, GPSFix)}, - * they must make sure that this method is redefined accordingly. Here, {@link #getMillisecondsOverWhichToAverageSpeed()} - * before and after the fix all fixes' validity caches are reset.

- * - * Distance cache invalidation is a bit tricky. Usually, the distance cache is invalidated starting with the time point - * of the gpsFix "upwards." However, if the adjacent earlier fixes have changed their validity by the addition - * of gpsFix, the distance cache must be invalidated starting with the first fix whose validity changed. - */ - protected void invalidateValidityAndDistanceCaches(FixType gpsFix) { - assertWriteLock(); - TimePoint distanceCacheInvalidationStart = gpsFix.getTimePoint(); - // see also bug 968: cache entries for intervals ending after the last fix need to be removed because they are - // based on the last fix's position and don't extrapolate; now, with the new gpsFix, the positions between the last - // and the new fix are defined by interpolation and hence differ from the previous assumption the competitor would have - // stopped moving at the last fix. - FixType last = getInternalFixes().lower(gpsFix); - if (last != null) { - distanceCacheInvalidationStart = last.getTimePoint().plus(1); // add one millisecond to invalidate *after* the last fix only - } - gpsFix.invalidateCache(); - Iterable lowers = getEarlierFixesWhoseValidityMayBeAffected(gpsFix); - for (FixType lower : lowers) { - boolean lowerWasValid = isValid(getRawFixes(), lower); - lower.invalidateCache(); - boolean lowerIsValid = isValid(getRawFixes(), lower); - if (lowerIsValid != lowerWasValid && lower.getTimePoint().before(distanceCacheInvalidationStart)) { - distanceCacheInvalidationStart = lower.getTimePoint(); - } - } - getDistanceCache().invalidateAllAtOrLaterThan(distanceCacheInvalidationStart); - Iterable highers = getLaterFixesWhoseValidityMayBeAffected(gpsFix); - for (FixType higher : highers) { - higher.invalidateCache(); - } - } - - protected Iterable getLaterFixesWhoseValidityMayBeAffected(FixType gpsFix) { - FixType higher = getInternalRawFixes().higher(gpsFix); - if (higher == null) { - return Collections.emptySet(); - } else { - Collection result = new ArrayList(); - while (higher != null && higher.getTimePoint().asMillis() - gpsFix.getTimePoint().asMillis() <= getMillisecondsOverWhichToAverageSpeed()) { - result.add(higher); - higher = getInternalRawFixes().higher(higher); - } - return result; - } - } - - protected Iterable getEarlierFixesWhoseValidityMayBeAffected(FixType gpsFix) { - FixType lower = getInternalRawFixes().lower(gpsFix); - if (lower == null) { - return Collections.emptySet(); - } else { - Collection result = new ArrayList(); - while (lower != null && gpsFix.getTimePoint().asMillis() - lower.getTimePoint().asMillis() <= getMillisecondsOverWhichToAverageSpeed()) { - result.add(lower); - lower = getInternalRawFixes().lower(lower); - } - return result; - } - } - - @Override - public boolean hasDirectionChange(TimePoint at, double minimumDegreeDifference) { - lockForRead(); - try { - boolean result = false; - TimePoint start = new MillisecondsTimePoint(at.asMillis() - getMillisecondsOverWhichToAverageSpeed()); - TimePoint end = new MillisecondsTimePoint(at.asMillis() + getMillisecondsOverWhichToAverageSpeed()); - SpeedWithBearing estimatedSpeedAtStart = getEstimatedSpeed(start); - if (estimatedSpeedAtStart != null) { - Bearing bearingAtStart = estimatedSpeedAtStart.getBearing(); - TimePoint next = new MillisecondsTimePoint(start.asMillis() - + Math.max(1000l, getMillisecondsOverWhichToAverageSpeed() / 2)); - while (!result && next.compareTo(end) <= 0) { - SpeedWithBearing estimatedSpeedAtNext = getEstimatedSpeed(next); - if (estimatedSpeedAtNext != null) { - Bearing bearingAtEnd = estimatedSpeedAtNext.getBearing(); - result = Math.abs(bearingAtStart.getDifferenceTo(bearingAtEnd).getDegrees()) > minimumDegreeDifference; - } - next = new MillisecondsTimePoint(next.asMillis() - + Math.max(1000l, getMillisecondsOverWhichToAverageSpeed() / 2)); - } - } - return result; - } finally { - unlockAfterRead(); - } - } - - protected DistanceCache getDistanceCache() { - return distanceCache; - } - - @Override - protected boolean add(FixType fix) { - final boolean result; - lockForWrite(); - try { - result = super.add(fix); - invalidateValidityAndDistanceCaches(fix); - } finally { - unlockAfterWrite(); - } - if (logger.isLoggable(Level.FINEST)) { - FixType last; - try { - lockForRead(); - if (logger.isLoggable(Level.FINEST)) { - logger.finest("GPS fix "+fix+" for "+getTrackedItem()+", isValid="+isValid(getInternalRawFixes(), fix)+ - ", time/distance/speed from last: "+ - ((last=getInternalRawFixes().lower(fix))==null - ? "null" - : (fix.getTimePoint().asMillis()-last.getTimePoint().asMillis()+"ms/"+ - fix.getPosition().getDistance(last.getPosition())) + "/"+ - fix.getPosition().getDistance(last.getPosition()).inTime(fix.getTimePoint().asMillis()-last.getTimePoint().asMillis()))); - } - } finally { - unlockAfterRead(); - } - } - for (GPSTrackListener listener : getListeners()) { - listener.gpsFixReceived(fix, getTrackedItem()); - } - return result; - } - - protected void setMillisecondsOverWhichToAverage(long millisecondsOverWhichToAverage) { - long oldMillis = getMillisecondsOverWhichToAverage(); - this.millisecondsOverWhichToAverage = millisecondsOverWhichToAverage; - for (GPSTrackListener listener : getListeners()) { - listener.speedAveragingChanged(oldMillis, millisecondsOverWhichToAverage); - } - } -} +package com.sap.sailing.domain.tracking.impl; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.ConcurrentModificationException; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.NavigableSet; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.sap.sailing.domain.base.BearingWithConfidence; +import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence; +import com.sap.sailing.domain.base.SpeedWithConfidence; +import com.sap.sailing.domain.base.Timed; +import com.sap.sailing.domain.base.impl.BearingWithConfidenceImpl; +import com.sap.sailing.domain.base.impl.SpeedWithBearingWithConfidenceImpl; +import com.sap.sailing.domain.base.impl.SpeedWithConfidenceImpl; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.impl.KnotSpeedImpl; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.impl.NauticalMileDistance; +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.confidence.ConfidenceBasedAverager; +import com.sap.sailing.domain.confidence.ConfidenceFactory; +import com.sap.sailing.domain.confidence.HasConfidence; +import com.sap.sailing.domain.confidence.Weigher; +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.GPSTrackListener; +import com.sap.sailing.domain.tracking.WithValidityCache; +import com.sap.sailing.util.impl.ArrayListNavigableSet; + +public class GPSFixTrackImpl extends TrackImpl implements GPSFixTrack { + private static final Logger logger = Logger.getLogger(GPSFixTrackImpl.class.getName()); + private static final long serialVersionUID = -7282869695818293745L; + private static final Speed DEFAULT_MAX_SPEED_FOR_SMOOTHING = new KnotSpeedImpl(50); + protected final Speed maxSpeedForSmoothing; + + private final ItemType trackedItem; + private long millisecondsOverWhichToAverage; + + private final GPSTrackListeners listeners; + + + private static class GPSTrackListeners implements Serializable { + private static final long serialVersionUID = -7117842092078781722L; + private Set> listeners; + + public GPSTrackListeners() { + listeners = new HashSet<>(); + } + + @SuppressWarnings("unchecked") // need typed generic cast + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + listeners = (Set>) ois.readObject(); + } + + private void writeObject(ObjectOutputStream oos) throws IOException { + final Set> listenersToSerialize; + synchronized (listeners) { + listenersToSerialize = new HashSet<>(); + for (GPSTrackListener listener : listeners) { + if (!listener.isTransient()) { + listenersToSerialize.add(listener); + } + } + } + oos.writeObject(listenersToSerialize); + } + + public void addListener(GPSTrackListener listener) { + synchronized (listeners) { + listeners.add(listener); + } + } + + public void removeListener(GPSTrackListener listener) { + synchronized (listeners) { + listeners.remove(listener); + } + } + + /** + * To iterate over the resulting listener list, synchronize on the iterable returned. Only this will avoid + * {@link ConcurrentModificationException}s because listeners may be added on the fly, and this object will + * synchronize on the listeners collection before adding on. + */ + public Iterable> getListeners() { + synchronized (listeners) { + return new HashSet>(listeners); + } + } + } + + /** + * Computing {@link #getDistanceTraveled(TimePoint, TimePoint)} is more expensive the longer the track is and the + * higher the tracking frequency. It is called regularly, usually for the same start time. Keeping to re-evaluate + * all distances between all GPS fixes, even if they remain constant, is a waste of resources, particularly when we + * assume that usually GPS fixes are appended to a track and not usually randomly inserted, even though this can + * happen. + *

+ * + * This cache looks "backwards." It contains pairs whose first component represents a to parameter used + * in {@link #getDistanceTraveled(TimePoint, TimePoint)}. It is ordered by this component. The second component is a + * navigable, ordered set of pairs where the first pair component represents a from parameter used in + * {@link #getDistanceTraveled(TimePoint, TimePoint)} and the second pair component represents the result of + * {@link #getDistanceTraveled(TimePoint, TimePoint)} for this parameter combination. Note that the cache does only + * store distances for from time points actually passed to + * {@link #getDistanceTraveled(TimePoint, TimePoint)} and does not cache all interim combinations collected while + * computing a result. As such, it differs from a typical "dynamic programming" algorithm which would store all + * in-between values too. Assuming that from values are sparse (leg start, race start, ...), there + * doesn't seem to be much use in storing all the other in-between values too. + *

+ * + * For implementation efficiency in combination with using a {@link ArrayListNavigableSet} for the values and in + * order to be able to efficiently extend a cache entry for a single to fix, the navigable sets + * containing the from fixes and distances are ordered such that earlier fixes come later in the set. + * This way, extending the cache entry for a to fix to an earlier from fix only requires + * appending to the set. + *

+ * + * Invalidation: When a new fix is added to this track, all distance cache entries for fixes at or later than + * the new fix's time point are removed from this cache. Additionally, the fix insertion may have an impact on the + * {@link #getEarlierFixesWhoseValidityMayBeAffected(GPSFix) previous fix's} validity (track smoothing) and + * therefore on its selection for distance aggregation. Therefore, if fix addition turned the previous fix invalid, + * the cache entries for the time points at or after the previous fix also need to be removed. + *

+ * + * Cache use: When {@link #getDistanceTraveled(TimePoint, TimePoint)} is called, it first looks for a cache + * entry for the to parameter. If one is found, the earliest entry in the navigable set for the + * navigable set of from and distance values that is at or after the requested from time + * point is determined. If such an entry exists, the distance is remembered and the algorithm is repeated + * recursively, using the from value found in the cache as the new to value, and the + * from value originally passed to {@link #getDistanceTraveled(TimePoint, TimePoint)} as + * from again. If no entry is found in the cache entry for to that is at or after the + * requested from time, the distance is computed by iterating the smoothened fixes. + *

+ * + * If a cache entry for to is not found, the latest cache entry before it is looked up. If one is + * found, the distance between the to time point requested and the to time point found in + * the cache is computed by iterating the smoothened fixes for this interval. If none is found, the distance is + * computed by iterating backwards all the way to from. + *

+ * + * Once the {@link #getDistanceTraveled(TimePoint, TimePoint)} has computed its value, it adds the result to the + * cache. + */ + private transient DistanceCache distanceCache; + + private transient MaxSpeedCache maxSpeedCache; + + public GPSFixTrackImpl(ItemType trackedItem, long millisecondsOverWhichToAverage) { + this(trackedItem, millisecondsOverWhichToAverage, DEFAULT_MAX_SPEED_FOR_SMOOTHING); + } + + /** + * @param maxSpeedForSmoothening + * if null, any fix speed will be accepted as long as fix-provided speed (if any) and + * inferred speed can be matched. + */ + public GPSFixTrackImpl(ItemType trackedItem, long millisecondsOverWhichToAverage, Speed maxSpeedForSmoothening) { + super(/* nameForReadWriteLock */ GPSFixTrackImpl.class.getSimpleName()+(trackedItem==null?"":(" for "+trackedItem.toString()))); + this.trackedItem = trackedItem; + this.millisecondsOverWhichToAverage = millisecondsOverWhichToAverage; + this.maxSpeedForSmoothing = maxSpeedForSmoothening; + this.listeners = new GPSTrackListeners(); + this.distanceCache = new DistanceCache(trackedItem==null?"null":trackedItem.toString()); + this.maxSpeedCache = new MaxSpeedCache(this); + } + + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + distanceCache = new DistanceCache(trackedItem.toString()); + maxSpeedCache = new MaxSpeedCache(this); + } + + @Override + public String toString() { + return super.toString()+" for "+getTrackedItem(); + } + + @Override + public void addListener(GPSTrackListener listener) { + listeners.addListener(listener); + } + + @Override + public void removeListener(GPSTrackListener listener) { + listeners.removeListener(listener); + } + + protected Iterable> getListeners() { + return listeners.getListeners(); + } + + private class DummyGPSFix extends DummyTimed implements GPSFix { + private static final long serialVersionUID = -6258506654181816698L; + + public DummyGPSFix(TimePoint timePoint) { + super(timePoint); + } + @Override + public Position getPosition() { + return null; + } + @Override + public SpeedWithBearing getSpeedAndBearingRequiredToReach(GPSFix to) { + return null; + } + @Override + public boolean isValidityCached() { + return false; + } + + @Override + public boolean isValid() { + return false; + } + + @Override + public void invalidateCache() { + } + + @Override + public void cacheValidity(boolean isValid) { + } + } + + + @Override + protected FixType getDummyFix(TimePoint timePoint) { + @SuppressWarnings("unchecked") + FixType result = (FixType) new DummyGPSFix(timePoint); + return result; + } + + @Override + public ItemType getTrackedItem() { + return trackedItem; + } + + @Override + public long getMillisecondsOverWhichToAverageSpeed() { + return millisecondsOverWhichToAverage; + } + + private Pair getFixesForPositionEstimation(TimePoint timePoint, boolean inclusive) { + lockForRead(); + try { + FixType lastFixBefore = inclusive ? getLastFixAtOrBefore(timePoint) : getLastFixBefore(timePoint); + FixType firstFixAfter = inclusive ? getFirstFixAtOrAfter(timePoint) : getFirstFixAfter(timePoint); + return new Pair(lastFixBefore, firstFixAfter); + } finally { + unlockAfterRead(); + } + } + + private class EstimatedPositionIterator implements Iterator { + private final Iterator timedsIter; + private final boolean extrapolate; + private final NavigableSet fixes; + private Iterator subSetIterator; + private FixType earlierFix; + private FixType laterFix; // if this is null, earlierFix is also null + + public EstimatedPositionIterator(Iterable timeds, boolean extrapolate) { + this.timedsIter = timeds.iterator(); + this.extrapolate = extrapolate; + this.fixes = getFixes(); + } + + @Override + public boolean hasNext() { + return timedsIter.hasNext(); + } + + @Override + public Position next() { + TimePoint nextTimePoint = timedsIter.next().getTimePoint(); + if (subSetIterator == null) { + earlierFix = getLastFixAtOrBefore(nextTimePoint); + subSetIterator = fixes.subSet(createDummyGPSFix(nextTimePoint), /* fromInclusive */true, fixes.last(), /* toInclusive */ + true).iterator(); + laterFix = subSetIterator.hasNext() ? subSetIterator.next() : null; + } else { + while (laterFix != null && laterFix.getTimePoint().before(nextTimePoint)) { + earlierFix = laterFix; + laterFix = subSetIterator.hasNext() ? subSetIterator.next() : null; + } + } + return getEstimatedPosition(nextTimePoint, extrapolate, earlierFix, laterFix); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + @Override + public Iterator getEstimatedPositions(Iterable timeds, boolean extrapolate) { + return new EstimatedPositionIterator(timeds, extrapolate); + } + + @Override + public Position getEstimatedPosition(TimePoint timePoint, boolean extrapolate) { + lockForRead(); + try { + Pair fixesForPositionEstimation = getFixesForPositionEstimation(timePoint, /* inclusive */ true); + return getEstimatedPosition(timePoint, extrapolate, fixesForPositionEstimation.getA(), fixesForPositionEstimation.getB()); + } finally { + unlockAfterRead(); + } + } + + @Override + public Pair getEstimatedPositionTimePeriodAffectedBy(GPSFix fix) { + if (fix == null) { + throw new IllegalArgumentException("fix must not be null"); + } + lockForRead(); + try { + Pair fixesForPositionEstimation = getFixesForPositionEstimation(fix.getTimePoint(), /* inclusive */ true); + final TimePoint start; + if (fix.equals(fixesForPositionEstimation.getA())) { + if (getLastFixBefore(fix.getTimePoint()) == null) { + // fix is the first fix in the track; all position estimation before this point may be affected + start = new MillisecondsTimePoint(0); + } else { + start = fix.getTimePoint(); + } + } else { + if (fixesForPositionEstimation.getA() == null) { + start = new MillisecondsTimePoint(0); + } else { + start = fixesForPositionEstimation.getA().getTimePoint(); + } + } + final TimePoint end; + if (fix.equals(fixesForPositionEstimation.getB())) { + if (getFirstFixAfter(fix.getTimePoint()) == null) { + // fix is the first fix in the track; all position estimation before this point may be affected + end = new MillisecondsTimePoint(Long.MAX_VALUE); + } else { + end = fix.getTimePoint(); + } + } else { + if (fixesForPositionEstimation.getB() == null) { + end = new MillisecondsTimePoint(Long.MAX_VALUE); + } else { + end = fixesForPositionEstimation.getB().getTimePoint(); + } + } + return new Pair(start, end); + } finally { + unlockAfterRead(); + } + } + + @Override + public Position getEstimatedRawPosition(TimePoint timePoint, boolean extrapolate) { + lockForRead(); + try { + FixType lastFixAtOrBefore = getLastRawFixAtOrBefore(timePoint); + FixType firstFixAtOrAfter = getFirstRawFixAtOrAfter(timePoint); + return getEstimatedPosition(timePoint, extrapolate, lastFixAtOrBefore, firstFixAtOrAfter); + } finally { + unlockAfterRead(); + } + } + + private Position getEstimatedPosition(TimePoint timePoint, boolean extrapolate, FixType lastFixAtOrBefore, + FixType firstFixAtOrAfter) { + lockForRead(); + try { + // TODO bug #346: compute a confidence value for the position returned based on time difference between fix(es) and timePoint; consider using Taylor approximation of more fixes around timePoint to predict and weigh position + if (lastFixAtOrBefore != null && lastFixAtOrBefore == firstFixAtOrAfter) { + return lastFixAtOrBefore.getPosition(); // exact match; how unlikely is that? + } else { + if (lastFixAtOrBefore == null && firstFixAtOrAfter != null) { + // TODO shouldn't this extrapolate into the past if extrapolate==true? + return firstFixAtOrAfter.getPosition(); // asking for time point before first fix: return first fix's position + } + if (firstFixAtOrAfter == null && !extrapolate) { + return lastFixAtOrBefore == null ? null : lastFixAtOrBefore.getPosition(); + } else { + SpeedWithBearing estimatedSpeed = estimateSpeed(lastFixAtOrBefore, firstFixAtOrAfter); + if (estimatedSpeed == null) { + // TODO even if extrapolation is requested, if there is no firstFixAtOrAfter, and the GPSFixes are without speed, shouldn't this at least return the position at the lastFixAtOrBefore instead of null? + return null; + } else { + if (lastFixAtOrBefore != null) { + Distance distance = estimatedSpeed.travel(lastFixAtOrBefore.getTimePoint(), timePoint); + Position result = lastFixAtOrBefore.getPosition().translateGreatCircle( + estimatedSpeed.getBearing(), distance); + return result; + } else { + // firstFixAtOrAfter can't be null because otherwise no speed could have been estimated + // TODO shouldn't this extrapolate into the past if extrapolate==true? + return firstFixAtOrAfter.getPosition(); + } + } + } + } + } finally { + unlockAfterRead(); + } + } + + @Override + public Pair getMaximumSpeedOverGround(TimePoint from, TimePoint to) { + return maxSpeedCache.getMaxSpeed(from, to); + } + + private SpeedWithBearing estimateSpeed(FixType fix1, FixType fix2) { + if (fix1 == null) { + if (fix2 instanceof GPSFixMoving) { + return ((GPSFixMoving) fix2).getSpeed(); + } else { + return null; + } + } else if (fix2 == null) { + FixType lastBeforeFix1 = getLastFixBefore(fix1.getTimePoint()); + if (lastBeforeFix1 != null) { + fix2 = fix1; + fix1 = lastBeforeFix1; // compute speed based on the last two fixes and assume constant speed + } else { + if (fix1 instanceof GPSFixMoving) { + return ((GPSFixMoving) fix1).getSpeed(); + } else { + return null; + } + } + } + Distance distance = fix1.getPosition().getDistance(fix2.getPosition()); + long millis = Math.abs(fix1.getTimePoint().asMillis() - fix2.getTimePoint().asMillis()); + SpeedWithBearing speed = new KnotSpeedWithBearingImpl(distance.getNauticalMiles() / (millis / 1000. / 3600.), + fix1.getPosition().getBearingGreatCircle(fix2.getPosition())); + return speed; + } + + + /** + * Returns the smoothened fixes (see {@link #getInternalFixes()}), type-cast such that it's a set of {@link GPSFix} + * objects + */ + private NavigableSet getGPSFixes() { + @SuppressWarnings("unchecked") + NavigableSet result = (NavigableSet) getInternalFixes(); + return result; + } + + @Override + public Distance getDistanceTraveled(TimePoint from, TimePoint to) { + return getDistanceTraveledRecursively(from, to, 0); + } + + private Distance getDistanceTraveledRecursively(TimePoint from, TimePoint to, int recursionDepth) { + Distance result; + if (!from.before(to)) { + result = Distance.NULL; + } else { + lockForRead(); + try { + Pair> bestCacheEntry = getDistanceCache() + .getEarliestFromAndDistanceAtOrAfterFrom(from, to); + if (bestCacheEntry != null) { + // compute the missing stretches between best cache entry's "from" and our "from" and the cache + // entry's "to" and our "to" + Distance distanceFromFromToBeginningOfCacheEntry = Distance.NULL; + Distance distanceFromEndOfCacheEntryToTo = Distance.NULL; + if (!bestCacheEntry.getB().getA().equals(from)) { + assert bestCacheEntry.getB().getA().after(from); + distanceFromFromToBeginningOfCacheEntry = getDistanceTraveledRecursively(from, bestCacheEntry + .getB().getA(), recursionDepth + 1); + } + if (!bestCacheEntry.getA().equals(to)) { + assert bestCacheEntry.getA().before(to); + distanceFromEndOfCacheEntryToTo = getDistanceTraveledRecursively(bestCacheEntry.getA(), to, + recursionDepth + 1); + } + result = distanceFromFromToBeginningOfCacheEntry.add(bestCacheEntry.getB().getB()).add( + distanceFromEndOfCacheEntryToTo); + } else { + double distanceInNauticalMiles = 0; + if (from.compareTo(to) < 0) { + // getEstimatedPosition's current implementation returns a position equal to that of a fix at + // "from" if there is one with exactly that time stamp + Position fromPos = getEstimatedPosition(from, /* extrapolate */false); + if (fromPos == null) { + result = Distance.NULL; + } else { + lockForRead(); + try { + NavigableSet subset = getGPSFixes().subSet(new DummyGPSFix(from), + /* fromInclusive */false, new DummyGPSFix(to), + /* toInclusive */false); + for (GPSFix fix : subset) { + double distanceBetweenAdjacentFixesInNauticalMiles = fromPos.getDistance( + fix.getPosition()).getNauticalMiles(); + distanceInNauticalMiles += distanceBetweenAdjacentFixesInNauticalMiles; + fromPos = fix.getPosition(); + } + } finally { + unlockAfterRead(); + } + Position toPos = getEstimatedPosition(to, false); + distanceInNauticalMiles += fromPos.getDistance(toPos).getNauticalMiles(); + result = new NauticalMileDistance(distanceInNauticalMiles); + } + } else { + result = Distance.NULL; + } + } + } finally { + unlockAfterRead(); + } + getDistanceCache().cache(from, to, result); + } + return result; + } + + @Override + public Distance getRawDistanceTraveled(TimePoint from, TimePoint to) { + lockForRead(); + try { + double distanceInNauticalMiles = 0; + if (from.compareTo(to) < 0) { + Position fromPos = getEstimatedRawPosition(from, false); + if (fromPos == null) { + return Distance.NULL; + } + @SuppressWarnings("unchecked") + NavigableSet subset = (NavigableSet) getInternalRawFixes().subSet((FixType) new DummyGPSFix(from), + /* fromInclusive */false, (FixType) new DummyGPSFix(to), + /* toInclusive */false); + for (GPSFix fix : subset) { + distanceInNauticalMiles += fromPos.getDistance(fix.getPosition()).getNauticalMiles(); + fromPos = fix.getPosition(); + } + Position toPos = getEstimatedRawPosition(to, false); + distanceInNauticalMiles += fromPos.getDistance(toPos).getNauticalMiles(); + return new NauticalMileDistance(distanceInNauticalMiles); + } else { + return Distance.NULL; + } + } finally { + unlockAfterRead(); + } + } + + /** + * Here we know for sure that the GPS fixes are {@link GPSFixMoving} instances, + * so we can use their {@link GPSFixMoving#getSpeed() speed} in averaging. We're still + * using an interval of {@link #getMillisecondsOverWhichToAverage()} around at, + * but this time we add the speeds and bearings provided by the fix onto the values for + * averaging, so the result considers both, the GPS-provided speeds and bearings as well as + * the speeds/bearings determined by distance/time difference of the fixes themselves. + */ + @Override + public SpeedWithBearing getEstimatedSpeed(TimePoint at) { + lockForRead(); + try { + SpeedWithBearingWithConfidence estimatedSpeed = getEstimatedSpeed(at, getInternalFixes(), + ConfidenceFactory.INSTANCE.createExponentialTimeDifferenceWeigher( + // use a minimum confidence to avoid the bearing to flip to 270deg in case all is zero + getMillisecondsOverWhichToAverageSpeed()/2, /* minimumConfidence */ 0.00000001)); // half confidence if half averaging interval apart + return estimatedSpeed == null ? null : estimatedSpeed.getObject(); + } finally { + unlockAfterRead(); + } + } + + @Override + public SpeedWithBearing getRawEstimatedSpeed(TimePoint at) { + lockForRead(); + try { + return getEstimatedSpeed(at, getRawFixes(), + ConfidenceFactory.INSTANCE.createExponentialTimeDifferenceWeigher( + // use a minimum confidence to avoid the bearing to flip to 270deg in case all is zero + getMillisecondsOverWhichToAverageSpeed(), /* minimumConfidence */ 0.00000001)).getObject(); + } finally { + unlockAfterRead(); + } + } + + @Override + public SpeedWithBearingWithConfidence getEstimatedSpeed(TimePoint at, Weigher weigher) { + lockForRead(); + try { + return getEstimatedSpeed(at, getInternalFixes(), weigher); + } finally { + unlockAfterRead(); + } + } + + @Override + public SpeedWithBearingWithConfidence getRawEstimatedSpeed(TimePoint at, Weigher weigher) { + lockForRead(); + try { + return getEstimatedSpeed(at, getRawFixes(), weigher); + } finally { + unlockAfterRead(); + } + } + + /** + * Since we don't know for sure whether the GPS fixes are {@link GPSFixMoving} instances, here we only estimate + * speed based on the distance and time between the fixes, averaged over an interval of + * {@link #millisecondsOverWhichToAverage} milliseconds around at. Subclasses that know about the + * particular fix type may redefine this to exploit a {@link SpeedWithBearing} attached, e.g., to a + * {@link GPSFixMoving}. + * + * @param weigher + * If null, a confidence of 1.0 is assumed for all fixes used in the speed/bearing + * estimation. Otherwise, the confidence of each fix contributing to the estimation is computed using the + * weigher. + * @return a speed/bearing with the average confidence attributed to the fixes that contributed to the estimation + */ + protected SpeedWithBearingWithConfidence getEstimatedSpeed(TimePoint at, + NavigableSet fixesToUseForSpeedEstimation, Weigher weigher) { + lockForRead(); + try { + List relevantFixes = getFixesRelevantForSpeedEstimation(at, fixesToUseForSpeedEstimation); + List> speeds = new ArrayList>(); + BearingWithConfidenceCluster bearingCluster = new BearingWithConfidenceCluster(weigher); + FixType last = null; + for (FixType next : relevantFixes) { + if (last != null) { + // TODO bug #346: consider time difference between next.getTimepoint() and at to compute a confidence + aggregateSpeedAndBearingFromLastToNext(speeds, bearingCluster, last, next); + } + last = next; + } + ConfidenceBasedAverager speedAverager = ConfidenceFactory.INSTANCE.createAverager(weigher); + HasConfidence speedWithConfidence = speedAverager.getAverage(speeds, at); + BearingWithConfidence bearingAverage = bearingCluster.getAverage(at); + Bearing bearing = bearingAverage == null ? null : bearingAverage.getObject(); + SpeedWithBearing avgSpeed = (speedWithConfidence == null || bearing == null) ? null : + new KnotSpeedWithBearingImpl(speedWithConfidence.getObject().getKnots(), bearing); + SpeedWithBearingWithConfidence result = avgSpeed == null ? null : + new SpeedWithBearingWithConfidenceImpl(avgSpeed, (bearingAverage.getConfidence() + speedWithConfidence.getConfidence())/2., at); + return result; + } finally { + unlockAfterRead(); + } + } + + protected void aggregateSpeedAndBearingFromLastToNext(List> speeds, + BearingWithConfidenceCluster bearingCluster, GPSFix last, GPSFix next) { + MillisecondsTimePoint relativeTo = new MillisecondsTimePoint((last.getTimePoint().asMillis() + next.getTimePoint().asMillis())/2); + Speed speed = last.getPosition().getDistance(next.getPosition()) + .inTime(next.getTimePoint().asMillis() - last.getTimePoint().asMillis()); + SpeedWithConfidenceImpl speedWithConfidence = new SpeedWithConfidenceImpl(speed, /* original confidence */ + 0.9, relativeTo); + speeds.add(speedWithConfidence); + double bearingConfidence = 0.9; + if (speed.getKnots() < 0.01) { + bearingConfidence = 0; + } + bearingCluster.add(new BearingWithConfidenceImpl(last.getPosition().getBearingGreatCircle(next.getPosition()), + bearingConfidence, // TODO use number of tracked satellites to determine confidence of single fix + relativeTo)); + } + + /** + * Computes the time interval such that {@link #getFixesRelevantForSpeedEstimation(TimePoint, NavigableSet)}, when + * passed any time point from that interval, produces "fix" as part of its result array. The algorithm for + * determining this interval co-varies with the implementation of + * {@link #getFixesRelevantForSpeedEstimation(TimePoint, NavigableSet)}.

+ * + * This implementation looks for fixes {@link #getMillisecondsOverWhichToAverage()}/2 before and after fix. + * If no fix is found in a direction within half the averaging interval but there is a fix in that direction which is further + * apart, the yet next fix is checked. If there is none, or that next-next fix is further apart from the next fix than fix, + * then the next fix is added to the result interval. This is because for that next fix, fix in that case will be relevant + * for speed estimation because it's the closest fix. + */ + protected Pair getTimeIntervalWhoseEstimatedSpeedMayHaveChangedAfterAddingFix(FixType fix) { + TimePoint intervalStart = null; + TimePoint intervalEnd = null; + lockForRead(); + try { + NavigableSet beforeSet = getInternalRawFixes().headSet(fix, /* inclusive */false); + NavigableSet afterSet = getInternalRawFixes().tailSet(fix, /* inclusive */true); + FixType beforeFix = null; + Iterator beforeFixIter = beforeSet.descendingIterator(); + while (beforeFixIter.hasNext() + && fix.getTimePoint().asMillis() - (beforeFix = beforeFixIter.next()).getTimePoint().asMillis() < getMillisecondsOverWhichToAverage() / 2) { + intervalStart = beforeFix.getTimePoint(); + } + if (intervalStart == null) { + if (beforeFixIter.hasNext()) { + // No before fix within half averaging interval, but there is one further away; is its next neighbour even further away? + // If so, or no more neighbours are found, it's affected and marks the invalidation interval start; otherwise, fix' + // time point is the invalidation interval start + TimePoint intervalStartCandidate = beforeFixIter.next().getTimePoint(); + if (beforeFixIter.hasNext()) { + TimePoint nextNeighboursTimePoint = beforeFixIter.next().getTimePoint(); + if (intervalStartCandidate.asMillis()-nextNeighboursTimePoint.asMillis() > fix.getTimePoint().asMillis()-intervalStartCandidate.asMillis()) { + intervalStart = intervalStartCandidate; + } + } else { + intervalStart = intervalStartCandidate; + } + } + } + if (intervalStart == null) { + intervalStart = fix.getTimePoint(); + } + FixType afterFix = null; + Iterator afterFixIter = afterSet.iterator(); + while (afterFixIter.hasNext() + && (afterFix = afterFixIter.next()).getTimePoint().asMillis() - fix.getTimePoint().asMillis() < getMillisecondsOverWhichToAverage() / 2) { + intervalEnd = afterFix.getTimePoint(); + } + if (intervalEnd == null) { + if (afterFixIter.hasNext()) { + // No after fix within half averaging interval, but there is one further away; is its next neighbour even further away? + // If so, or no more neighbours are found, it's affected and marks the invalidation interval start; otherwise, fix's + // time point is the invalidation interval end + TimePoint intervalEndCandidate = afterFixIter.next().getTimePoint(); + if (afterFixIter.hasNext()) { + TimePoint nextNeighboursTimePoint = afterFixIter.next().getTimePoint(); + if (nextNeighboursTimePoint.asMillis()-intervalEndCandidate.asMillis() > intervalEndCandidate.asMillis()-fix.getTimePoint().asMillis()) { + intervalEnd = intervalEndCandidate; + } + } else { + intervalEnd = intervalEndCandidate; + } + } + } + if (intervalEnd == null) { + intervalEnd = fix.getTimePoint(); + } + } finally { + unlockAfterRead(); + } + return new Pair(intervalStart, intervalEnd); + } + + protected FixType createDummyGPSFix(TimePoint at) { + @SuppressWarnings("unchecked") + FixType result = (FixType) new DummyGPSFix(at); + return result; + } + + /** + * A track that doesn't have {@link GPSFixMoving} fixes and therefore needs to compute the speed using the fix time + * and position differences. It therefore needs at least two fixes to compute a result. The algorithm always uses + * at least the closest existing fix later and the closet existing fix earlier than at, if available. + *

+ * + * But even for a track with {@link GPSFixMoving} fixes this is a good algorithm because in case the speed changes + * significantly between fixes, it is important to know the next fix to understand and consider the trend. + */ + protected List getFixesRelevantForSpeedEstimation(TimePoint at, NavigableSet fixesToUseForSpeedEstimation) { + lockForRead(); + try { + FixType atTimed = createDummyGPSFix(at); + List relevantFixes = new LinkedList(); + NavigableSet beforeSet = fixesToUseForSpeedEstimation.headSet(atTimed, /* inclusive */false); + NavigableSet afterSet = fixesToUseForSpeedEstimation.tailSet(atTimed, /* inclusive */true); + FixType beforeFix = null; + Iterator beforeFixIter = beforeSet.descendingIterator(); + boolean noBeforeFixUsedYet = true; + while (beforeFixIter.hasNext() && + (at.asMillis() - (beforeFix=beforeFixIter.next()).getTimePoint().asMillis() < getMillisecondsOverWhichToAverage() / 2 || noBeforeFixUsedYet)) { + relevantFixes.add(0, beforeFix); + noBeforeFixUsedYet = false; + beforeFix = null; // mark the fix as used + } + FixType afterFix = null; + Iterator afterFixIter = afterSet.iterator(); + boolean noAfterFixUsedYet = true; + while (afterFixIter.hasNext() && + ((afterFix=afterFixIter.next()).getTimePoint().asMillis() - at.asMillis() < getMillisecondsOverWhichToAverage() / 2 || noAfterFixUsedYet)) { + relevantFixes.add(afterFix); + noAfterFixUsedYet = false; + afterFix = null; // mark the fix as used + } + // now fill up relevantFixes until we have at least two fixes or we run out of fixes entirely (can't estimate speed + // with this type of fix on a track with less than two fixes) + while (relevantFixes.size() < 2 && (beforeFix != null || afterFix != null)) { + if (afterFix == null) { + if (beforeFix != null) { + relevantFixes.add(0, beforeFix); // add the last beforeFix to have at least two fixes, although outside of averaging interval + if (beforeFixIter.hasNext()) { + beforeFix = beforeFixIter.next(); + } else { + beforeFix = null; + } + } + } else if (beforeFix == null) { + relevantFixes.add(afterFix); + if (afterFixIter.hasNext()) { + afterFix = afterFixIter.next(); + } else { + afterFix = null; + } + } else { + // both, beforeFix and afterFix are available; choose the one closest to "at" + if (afterFix.getTimePoint().asMillis()-at.asMillis() < beforeFix.getTimePoint().asMillis()-at.asMillis()) { + relevantFixes.add(afterFix); + if (afterFixIter.hasNext()) { + afterFix = afterFixIter.next(); + } else { + afterFix = null; + } + } else { + relevantFixes.add(0, beforeFix); + if (beforeFixIter.hasNext()) { + beforeFix = beforeFixIter.next(); + } else { + beforeFix = null; + } + } + } + } + return relevantFixes; + } finally { + unlockAfterRead(); + } + } + + protected long getMillisecondsOverWhichToAverage() { + return millisecondsOverWhichToAverage; + } + + /** + * Smoothens the track based on a max-speed assumption. + */ + @Override + protected NavigableSet getInternalFixes() { + assertReadLock(); + return new PartialNavigableSetView(super.getInternalFixes()) { + @Override + protected boolean isValid(FixType e) { + return GPSFixTrackImpl.this.isValid(getRawFixes(), e); + } + }; + } + + /** + * When redefining this method, make sure to redefine {@link #invalidateValidityAndDistanceCaches(GPSFix)} + * accordingly. This implementation checks the immediate previous and next fix for e. Therefore, when + * adding a fix, only immediately adjacent fix's validity caches need to be invalidated. + *

+ * + * The fix e is considered valid if at least one of its (not necessarily immediate) neighbors that is + * within the range of {@link #getMillisecondsOverWhichToAverageSpeed()} is in reach with less than + * {@link #maxSpeedForSmoothing}, or if there are no neighbors with the time range defined by + * {@link #getMillisecondsOverWhichToAverageSpeed()}. + */ + protected boolean isValid(NavigableSet rawFixes, FixType e) { + assertReadLock(); + boolean isValid; + if (maxSpeedForSmoothing == null) { + isValid = true; + } else { + if (e.isValidityCached()) { + isValid = e.isValid(); + } else { + FixType previous = rawFixes.lower(e); + final boolean atLeastOnePreviousFixInRange = previous != null && e.getTimePoint().asMillis() - previous.getTimePoint().asMillis() <= getMillisecondsOverWhichToAverageSpeed(); + Speed speedToPrevious = null; + boolean foundValidPreviousFixInRange = false; + while (previous != null && !foundValidPreviousFixInRange && e.getTimePoint().asMillis() - previous.getTimePoint().asMillis() <= getMillisecondsOverWhichToAverageSpeed()) { + speedToPrevious = previous.getPosition().getDistance(e.getPosition()) + .inTime(e.getTimePoint().asMillis() - previous.getTimePoint().asMillis()); + foundValidPreviousFixInRange = speedToPrevious.compareTo(maxSpeedForSmoothing) <= 0; + previous = rawFixes.lower(previous); + } + boolean foundValidNextFixInRange = false; + boolean atLeastOneNextFixInRange = false; + // only spend the effort to calculate the "next"-related predicate if the "previous"-related part of the disjunction below isn't already false + if (!atLeastOnePreviousFixInRange || foundValidPreviousFixInRange) { + FixType next = rawFixes.higher(e); + atLeastOneNextFixInRange = next != null && next.getTimePoint().asMillis() - e.getTimePoint().asMillis() <= getMillisecondsOverWhichToAverageSpeed(); + Speed speedToNext = null; + while (next != null && !foundValidNextFixInRange && next.getTimePoint().asMillis() - e.getTimePoint().asMillis() <= getMillisecondsOverWhichToAverageSpeed()) { + speedToNext = e.getPosition().getDistance(next.getPosition()) + .inTime(next.getTimePoint().asMillis() - e.getTimePoint().asMillis()); + foundValidNextFixInRange = speedToNext.compareTo(maxSpeedForSmoothing) <= 0; + next = rawFixes.higher(next); + } + } + isValid = (!atLeastOnePreviousFixInRange || foundValidPreviousFixInRange) && (!atLeastOneNextFixInRange || foundValidNextFixInRange); + e.cacheValidity(isValid); + } + } + return isValid; + } + + /** + * After gpsFix was added to this track, invalidate the {@link WithValidityCache validity caches} + * of the fixes whose validity may be affected. If subclasses redefine {@link #isValid(PartialNavigableSetView, GPSFix)}, + * they must make sure that this method is redefined accordingly. Here, {@link #getMillisecondsOverWhichToAverageSpeed()} + * before and after the fix all fixes' validity caches are reset.

+ * + * Distance cache invalidation is a bit tricky. Usually, the distance cache is invalidated starting with the time point + * of the gpsFix "upwards." However, if the adjacent earlier fixes have changed their validity by the addition + * of gpsFix, the distance cache must be invalidated starting with the first fix whose validity changed. + */ + protected void invalidateValidityAndDistanceCaches(FixType gpsFix) { + assertWriteLock(); + TimePoint distanceCacheInvalidationStart = gpsFix.getTimePoint(); + // see also bug 968: cache entries for intervals ending after the last fix need to be removed because they are + // based on the last fix's position and don't extrapolate; now, with the new gpsFix, the positions between the last + // and the new fix are defined by interpolation and hence differ from the previous assumption the competitor would have + // stopped moving at the last fix. + FixType last = getInternalFixes().lower(gpsFix); + if (last != null) { + distanceCacheInvalidationStart = last.getTimePoint().plus(1); // add one millisecond to invalidate *after* the last fix only + } + gpsFix.invalidateCache(); + Iterable lowers = getEarlierFixesWhoseValidityMayBeAffected(gpsFix); + for (FixType lower : lowers) { + boolean lowerWasValid = isValid(getRawFixes(), lower); + lower.invalidateCache(); + boolean lowerIsValid = isValid(getRawFixes(), lower); + if (lowerIsValid != lowerWasValid && lower.getTimePoint().before(distanceCacheInvalidationStart)) { + distanceCacheInvalidationStart = lower.getTimePoint(); + } + } + getDistanceCache().invalidateAllAtOrLaterThan(distanceCacheInvalidationStart); + Iterable highers = getLaterFixesWhoseValidityMayBeAffected(gpsFix); + for (FixType higher : highers) { + higher.invalidateCache(); + } + } + + protected Iterable getLaterFixesWhoseValidityMayBeAffected(FixType gpsFix) { + FixType higher = getInternalRawFixes().higher(gpsFix); + if (higher == null) { + return Collections.emptySet(); + } else { + Collection result = new ArrayList(); + while (higher != null && higher.getTimePoint().asMillis() - gpsFix.getTimePoint().asMillis() <= getMillisecondsOverWhichToAverageSpeed()) { + result.add(higher); + higher = getInternalRawFixes().higher(higher); + } + return result; + } + } + + protected Iterable getEarlierFixesWhoseValidityMayBeAffected(FixType gpsFix) { + FixType lower = getInternalRawFixes().lower(gpsFix); + if (lower == null) { + return Collections.emptySet(); + } else { + Collection result = new ArrayList(); + while (lower != null && gpsFix.getTimePoint().asMillis() - lower.getTimePoint().asMillis() <= getMillisecondsOverWhichToAverageSpeed()) { + result.add(lower); + lower = getInternalRawFixes().lower(lower); + } + return result; + } + } + + @Override + public boolean hasDirectionChange(TimePoint at, double minimumDegreeDifference) { + lockForRead(); + try { + boolean result = false; + TimePoint start = new MillisecondsTimePoint(at.asMillis() - getMillisecondsOverWhichToAverageSpeed()); + TimePoint end = new MillisecondsTimePoint(at.asMillis() + getMillisecondsOverWhichToAverageSpeed()); + SpeedWithBearing estimatedSpeedAtStart = getEstimatedSpeed(start); + if (estimatedSpeedAtStart != null) { + Bearing bearingAtStart = estimatedSpeedAtStart.getBearing(); + TimePoint next = new MillisecondsTimePoint(start.asMillis() + + Math.max(1000l, getMillisecondsOverWhichToAverageSpeed() / 2)); + while (!result && next.compareTo(end) <= 0) { + SpeedWithBearing estimatedSpeedAtNext = getEstimatedSpeed(next); + if (estimatedSpeedAtNext != null) { + Bearing bearingAtEnd = estimatedSpeedAtNext.getBearing(); + result = Math.abs(bearingAtStart.getDifferenceTo(bearingAtEnd).getDegrees()) > minimumDegreeDifference; + } + next = new MillisecondsTimePoint(next.asMillis() + + Math.max(1000l, getMillisecondsOverWhichToAverageSpeed() / 2)); + } + } + return result; + } finally { + unlockAfterRead(); + } + } + + protected DistanceCache getDistanceCache() { + return distanceCache; + } + + @Override + protected boolean add(FixType fix) { + final boolean result; + lockForWrite(); + try { + result = addWithoutLocking(fix); + invalidateValidityAndDistanceCaches(fix); + } finally { + unlockAfterWrite(); + } + if (logger.isLoggable(Level.FINEST)) { + FixType last; + try { + lockForRead(); + if (logger.isLoggable(Level.FINEST)) { + logger.finest("GPS fix "+fix+" for "+getTrackedItem()+", isValid="+isValid(getInternalRawFixes(), fix)+ + ", time/distance/speed from last: "+ + ((last=getInternalRawFixes().lower(fix))==null + ? "null" + : (fix.getTimePoint().asMillis()-last.getTimePoint().asMillis()+"ms/"+ + fix.getPosition().getDistance(last.getPosition())) + "/"+ + fix.getPosition().getDistance(last.getPosition()).inTime(fix.getTimePoint().asMillis()-last.getTimePoint().asMillis()))); + } + } finally { + unlockAfterRead(); + } + } + for (GPSTrackListener listener : getListeners()) { + listener.gpsFixReceived(fix, getTrackedItem()); + } + return result; + } + + protected void setMillisecondsOverWhichToAverage(long millisecondsOverWhichToAverage) { + long oldMillis = getMillisecondsOverWhichToAverage(); + this.millisecondsOverWhichToAverage = millisecondsOverWhichToAverage; + for (GPSTrackListener listener : getListeners()) { + listener.speedAveragingChanged(oldMillis, millisecondsOverWhichToAverage); + } + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/MaxSpeedCache.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/MaxSpeedCache.java index d3f8dfa1619..ac6352853e9 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/MaxSpeedCache.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/MaxSpeedCache.java @@ -1,276 +1,276 @@ -package com.sap.sailing.domain.tracking.impl; - -import java.util.Comparator; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.Map.Entry; -import java.util.NavigableSet; - -import com.sap.sailing.domain.common.Speed; -import com.sap.sailing.domain.common.TimePoint; -import com.sap.sailing.domain.common.impl.Util.Pair; -import com.sap.sailing.domain.tracking.GPSFix; -import com.sap.sailing.domain.tracking.GPSFixTrack; -import com.sap.sailing.domain.tracking.GPSTrackListener; -import com.sap.sailing.util.impl.ArrayListNavigableSet; -import com.sap.sailing.util.impl.LockUtil; -import com.sap.sailing.util.impl.NamedReentrantReadWriteLock; - -/** - * Re-calculating the maximum speed over a {@link GPSFixTrack} is time consuming. When the track grows the way it - * usually grows (extending at the end), caching can help to reduce the computational effort. The API for computing the - * maximum speed ( - * {@link GPSFixTrack#getMaximumSpeedOverGround(com.sap.sailing.domain.common.TimePoint, com.sap.sailing.domain.common.TimePoint)} - * ) allows clients to specify an interval for which the top speed is to be computed. Therefore, this cache offers a - * similar API which supports interval-based queries. - *

- * - * The cache assumes that queries are usually posed with one of a small set of "from" time points, such as a leg's start - * or the race starting time. The "to" time points are expected to vary, particularly to grow in case a "live" query is - * made, or to represent one of a few more or less fixed time points such as a competitor's leg finishing times. With - * this assumption it seems reasonable to structure the cache such that the "from" time point is a key into a map that - * stores results for this "from" value. The various results for the same "from" entry are stored in the navigable set - * with ascending "to" times. When a query comes in, the best fit is determined by looking up the latest "to" that is - * earlier than or equal to the requested "to." If such an entry is found, the search for a maximum speed can be - * restricted to the interval between the cache entry's "to" and the "to" time point requested. - *

- * - * When a new GPS fix is recorded for the track for which this is a max-speed cache, invalidation takes place. For this, - * the cache {@link GPSTrackListener listens} for GPS fixes being added to the track. For the fix added, we'd like to - * find the time interval within which a - * {@link GPSFixTrackImpl#getFixesRelevantForSpeedEstimation(TimePoint, NavigableSet)} with a {@link TimePoint} from - * that interval delivers the fix added in its result. All overlapping cached intervals will then be cropped to the - * remaining valid interval if their maximum speed time point was in the remaining interval, or removed from the cache - * otherwise. - * - * @author Axel Uhl (d043530) - * - */ -public class MaxSpeedCache implements GPSTrackListener { - private static final long serialVersionUID = 8825205750854940612L; - - private final GPSFixTrackImpl track; - - /** - * Keys are the "from" time points as passed to {@link #getMaxSpeed(TimePoint, TimePoint)}. Values are navigable - * sets of pairs whose {@link Pair#getA() a} component is the "to" parameter as passed to - * {@link #getMaxSpeed(TimePoint, TimePoint)}, and whose {@link Pair#getB() b} component is the track's fix where - * the averaged maximum speed for that interval was achieved together with the averaged speed at that point. The - * navigable set is ordered according to ascending to time points, yielding shorter cache intervals - * before longer intervals. - */ - private final Map>>> cache; - - private final NamedReentrantReadWriteLock lock; - - public MaxSpeedCache(GPSFixTrackImpl track) { - this.track = track; - track.addListener(this); - cache = new HashMap>>>(); - lock = new NamedReentrantReadWriteLock(MaxSpeedCache.class.getSimpleName()+" for track of "+track.getTrackedItem(), /* fair */ false); - } - - /** - * Find the invalidation interval such that getFixesRelevantForSpeedEstimation, when passed any time point from that - * interval, produces "fix", then find all cache entries that overlap with this interval and if the max fix is - * outside of the invalidation interval, crop cache entry's interval such that it's overlap-free, otherwise remove. - */ - @Override - public void gpsFixReceived(FixType fix, ItemType item) { - // find the invalidation interval such that getFixesRelevantForSpeedEstimation, when passed any time point from that interval, produces "fix" - Pair invalidationInterval = track.getTimeIntervalWhoseEstimatedSpeedMayHaveChangedAfterAddingFix(fix); - LockUtil.lockForWrite(lock); - HashMap>>> additionalCacheEntries = - new HashMap>>>(); - try { - // find all cache entries that overlap with this interval - for (Iterator>>>> i = cache.entrySet().iterator(); i.hasNext(); ) { - Entry>>> next = i.next(); - additionalCacheEntries.putAll(invalidateEntriesAndReturnAdditionalCacheEntries(next, invalidationInterval)); - if (next.getValue().isEmpty()) { - i.remove(); - } - } - mergeAdditionalCacheEntries(additionalCacheEntries); - } finally { - LockUtil.unlockAfterWrite(lock); - } - } - - /** - * Returns true in order to skip serialization of the cache as a listener. The cache itself shall not be serialized. - * It is re-established during the deserialization of the owning track. - */ - @Override - public boolean isTransient() { - return true; - } - - private void mergeAdditionalCacheEntries( - Map>>> additionalCacheEntries) { - assert lock.writeLock().isHeldByCurrentThread(); - for (Map.Entry>>> additionalCacheEntry : additionalCacheEntries.entrySet()) { - NavigableSet>> existingEntry = cache.get(additionalCacheEntry.getKey()); - if (existingEntry == null) { - cache.put(additionalCacheEntry.getKey(), additionalCacheEntry.getValue()); - } else { - existingEntry.addAll(additionalCacheEntry.getValue()); - } - } - } - - private Map>>> invalidateEntriesAndReturnAdditionalCacheEntries( - Entry>>> cacheEntry, - Pair invalidationInterval) { - assert lock.writeLock().isHeldByCurrentThread(); - // cannot modify cache here because caller is in an iteration over cache's entry set; request additions by returning them - Map>>> result = new HashMap<>(); - if (!cacheEntry.getKey().after(invalidationInterval.getB())) { - // invalidation can only become necessary if the cache entry doesn't start after the end of the invalidation interval - TimePoint croppedFrom; // start of the remaining valid part of the cache entries - if (cacheEntry.getKey().before(invalidationInterval.getA())) { - croppedFrom = cacheEntry.getKey(); - } else { - croppedFrom = invalidationInterval.getB(); // cache entry starts in the invalidationInterval; earliest valid point is end of invalidation interval - } - // now scan all entries whose "to" is at or after the invalidationInterval's start: - for (Iterator>> toAndResultIter = cacheEntry.getValue().tailSet( - new Pair>(invalidationInterval.getA(), null), /* inclusive */ true).iterator(); - toAndResultIter.hasNext(); ) { - Pair> toAndResult = toAndResultIter.next(); - // cacheEntry's from is before or in the invalidation interval; the current "to" in this loop iteration is in or after the interval - // and at or after "from"; in any case the record needs to be deleted: - toAndResultIter.remove(); - // if the current "to" is at or after croppedFrom, check if the old max fix is in the cropped interval; if so, request - // creation of a new cache entry: - if (toAndResult.getB() != null) { - final TimePoint maxFixTimePoint = toAndResult.getB().getA().getTimePoint(); - if (!toAndResult.getA().before(croppedFrom) && !maxFixTimePoint.before(croppedFrom) - && !maxFixTimePoint.after(toAndResult.getA())) { - addEntryToMap(croppedFrom, toAndResult.getA(), toAndResult.getB(), result); - } - } - } - } - return result; - } - - @Override - public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) { - LockUtil.lockForWrite(lock); - try { - cache.clear(); - } finally { - LockUtil.unlockAfterWrite(lock); - } - } - - public Pair getMaxSpeed(TimePoint from, TimePoint to) { - Pair result = null; - if (!to.before(from)) { - result = cacheLookup(from, to); - if (result == null) { - result = computeMaxSpeed(from, to); - cache(from, to, result); - } - } - return result; - } - - /** - * If a cache entry exists for "from" whose interval ends at or before to, use it and if necessary compute the - * maximum in the missing tail, producing another cache entry for both, the tail and the extended interval. Otherwise, null - * is returned. - */ - private Pair cacheLookup(TimePoint from, TimePoint to) { - assert !from.after(to); - Pair result = null; - NavigableSet>> entry; - Pair> entryForLongestSubseries = null; - LockUtil.lockForRead(lock); - try { - entry = cache.get(from); - if (entry != null) { - entryForLongestSubseries = entry.floor(new Pair>(to, null)); - } - } finally { - LockUtil.unlockAfterRead(lock); - } - if (entryForLongestSubseries != null && entryForLongestSubseries.getB() != null) { - TimePoint entryTo = entryForLongestSubseries.getA(); - // avoid endless recursion in case entryTo is not after from: - if (entryTo.equals(to) || entryTo.after(from)) { - if (entryTo.before(to)) { - Pair maxInMissingTail = getMaxSpeed(entryTo, to); - if (maxInMissingTail != null && maxInMissingTail.getB().compareTo(entryForLongestSubseries.getB().getB()) > 0) { - // the maximum speed is in the tail that was not part of the interval retrieved from the cache - result = maxInMissingTail; - } else { - result = entryForLongestSubseries.getB(); // the interval from the cache also holds the maximum - // for the extended interval - } - cache(from, to, result); // produce new - } else { - result = entryForLongestSubseries.getB(); - } - } - } - return result; - } - - private void cache(TimePoint from, TimePoint to, Pair fixAtMaxSpeed) { - LockUtil.lockForWrite(lock); - try { - addEntryToMap(from, to, fixAtMaxSpeed, cache); - } finally { - LockUtil.unlockAfterWrite(lock); - } - } - - private void addEntryToMap(TimePoint from, TimePoint to, Pair fixAtMaxSpeed, - Map>>> map) { - NavigableSet>> setForFrom = map.get(from); - if (setForFrom == null) { - setForFrom = new ArrayListNavigableSet>>( - new Comparator>>() { - @Override - public int compare(Pair> o1, - Pair> o2) { - return o1.getA().compareTo(o2.getA()); - } - }); - map.put(from, setForFrom); - } - setForFrom.add(new Pair>(to, fixAtMaxSpeed)); - } - - /** - * @return null, if no fix exists in the interval specified - */ - private Pair computeMaxSpeed(TimePoint from, TimePoint to) { - track.lockForRead(); - try { - // fetch all fixes on this leg so far and determine their maximum speed - Iterator iter = track.getFixesIterator(from, /* inclusive */ true); - Speed max = Speed.NULL; - FixType maxSpeedFix = null; - if (iter.hasNext()) { - while (iter.hasNext()) { - FixType fix = iter.next(); - if (fix.getTimePoint().after(to)) { - break; - } - Speed averagedSpeedAtFixTime = track.getEstimatedSpeed(fix.getTimePoint()); - if (averagedSpeedAtFixTime != null && averagedSpeedAtFixTime.compareTo(max) > 0) { - max = averagedSpeedAtFixTime; - maxSpeedFix = fix; - } - } - } - return maxSpeedFix == null ? null : new Pair(maxSpeedFix, max); - } finally { - track.unlockAfterRead(); - } - } -} +package com.sap.sailing.domain.tracking.impl; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NavigableSet; + +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.domain.tracking.GPSFixTrack; +import com.sap.sailing.domain.tracking.GPSTrackListener; +import com.sap.sailing.util.impl.ArrayListNavigableSet; +import com.sap.sailing.util.impl.LockUtil; +import com.sap.sailing.util.impl.NamedReentrantReadWriteLock; + +/** + * Re-calculating the maximum speed over a {@link GPSFixTrack} is time consuming. When the track grows the way it + * usually grows (extending at the end), caching can help to reduce the computational effort. The API for computing the + * maximum speed ( + * {@link GPSFixTrack#getMaximumSpeedOverGround(com.sap.sailing.domain.common.TimePoint, com.sap.sailing.domain.common.TimePoint)} + * ) allows clients to specify an interval for which the top speed is to be computed. Therefore, this cache offers a + * similar API which supports interval-based queries. + *

+ * + * The cache assumes that queries are usually posed with one of a small set of "from" time points, such as a leg's start + * or the race starting time. The "to" time points are expected to vary, particularly to grow in case a "live" query is + * made, or to represent one of a few more or less fixed time points such as a competitor's leg finishing times. With + * this assumption it seems reasonable to structure the cache such that the "from" time point is a key into a map that + * stores results for this "from" value. The various results for the same "from" entry are stored in the navigable set + * with ascending "to" times. When a query comes in, the best fit is determined by looking up the latest "to" that is + * earlier than or equal to the requested "to." If such an entry is found, the search for a maximum speed can be + * restricted to the interval between the cache entry's "to" and the "to" time point requested. + *

+ * + * When a new GPS fix is recorded for the track for which this is a max-speed cache, invalidation takes place. For this, + * the cache {@link GPSTrackListener listens} for GPS fixes being added to the track. For the fix added, we'd like to + * find the time interval within which a + * {@link GPSFixTrackImpl#getFixesRelevantForSpeedEstimation(TimePoint, NavigableSet)} with a {@link TimePoint} from + * that interval delivers the fix added in its result. All overlapping cached intervals will then be cropped to the + * remaining valid interval if their maximum speed time point was in the remaining interval, or removed from the cache + * otherwise. + * + * @author Axel Uhl (d043530) + * + */ +public class MaxSpeedCache implements GPSTrackListener { + private static final long serialVersionUID = 8825205750854940612L; + + private final GPSFixTrackImpl track; + + /** + * Keys are the "from" time points as passed to {@link #getMaxSpeed(TimePoint, TimePoint)}. Values are navigable + * sets of pairs whose {@link Pair#getA() a} component is the "to" parameter as passed to + * {@link #getMaxSpeed(TimePoint, TimePoint)}, and whose {@link Pair#getB() b} component is the track's fix where + * the averaged maximum speed for that interval was achieved together with the averaged speed at that point. The + * navigable set is ordered according to ascending to time points, yielding shorter cache intervals + * before longer intervals. + */ + private final Map>>> cache; + + private final NamedReentrantReadWriteLock lock; + + public MaxSpeedCache(GPSFixTrackImpl track) { + this.track = track; + track.addListener(this); + cache = new HashMap>>>(); + lock = new NamedReentrantReadWriteLock(MaxSpeedCache.class.getSimpleName()+" for track of "+track.getTrackedItem(), /* fair */ false); + } + + /** + * Find the invalidation interval such that getFixesRelevantForSpeedEstimation, when passed any time point from that + * interval, produces "fix", then find all cache entries that overlap with this interval and if the max fix is + * outside of the invalidation interval, crop cache entry's interval such that it's overlap-free, otherwise remove. + */ + @Override + public void gpsFixReceived(FixType fix, ItemType item) { + // find the invalidation interval such that getFixesRelevantForSpeedEstimation, when passed any time point from that interval, produces "fix" + Pair invalidationInterval = track.getTimeIntervalWhoseEstimatedSpeedMayHaveChangedAfterAddingFix(fix); + LockUtil.lockForWrite(lock); + HashMap>>> additionalCacheEntries = + new HashMap>>>(); + try { + // find all cache entries that overlap with this interval + for (Iterator>>>> i = cache.entrySet().iterator(); i.hasNext(); ) { + Entry>>> next = i.next(); + additionalCacheEntries.putAll(invalidateEntriesAndReturnAdditionalCacheEntries(next, invalidationInterval)); + if (next.getValue().isEmpty()) { + i.remove(); + } + } + mergeAdditionalCacheEntries(additionalCacheEntries); + } finally { + LockUtil.unlockAfterWrite(lock); + } + } + + /** + * Returns true in order to skip serialization of the cache as a listener. The cache itself shall not be serialized. + * It is re-established during the deserialization of the owning track. + */ + @Override + public boolean isTransient() { + return true; + } + + private void mergeAdditionalCacheEntries( + Map>>> additionalCacheEntries) { + assert lock.writeLock().isHeldByCurrentThread(); + for (Map.Entry>>> additionalCacheEntry : additionalCacheEntries.entrySet()) { + NavigableSet>> existingEntry = cache.get(additionalCacheEntry.getKey()); + if (existingEntry == null) { + cache.put(additionalCacheEntry.getKey(), additionalCacheEntry.getValue()); + } else { + existingEntry.addAll(additionalCacheEntry.getValue()); + } + } + } + + private Map>>> invalidateEntriesAndReturnAdditionalCacheEntries( + Entry>>> cacheEntry, + Pair invalidationInterval) { + assert lock.writeLock().isHeldByCurrentThread(); + // cannot modify cache here because caller is in an iteration over cache's entry set; request additions by returning them + Map>>> result = new HashMap<>(); + if (!cacheEntry.getKey().after(invalidationInterval.getB())) { + // invalidation can only become necessary if the cache entry doesn't start after the end of the invalidation interval + TimePoint croppedFrom; // start of the remaining valid part of the cache entries + if (cacheEntry.getKey().before(invalidationInterval.getA())) { + croppedFrom = cacheEntry.getKey(); + } else { + croppedFrom = invalidationInterval.getB(); // cache entry starts in the invalidationInterval; earliest valid point is end of invalidation interval + } + // now scan all entries whose "to" is at or after the invalidationInterval's start: + for (Iterator>> toAndResultIter = cacheEntry.getValue().tailSet( + new Pair>(invalidationInterval.getA(), null), /* inclusive */ true).iterator(); + toAndResultIter.hasNext(); ) { + Pair> toAndResult = toAndResultIter.next(); + // cacheEntry's from is before or in the invalidation interval; the current "to" in this loop iteration is in or after the interval + // and at or after "from"; in any case the record needs to be deleted: + toAndResultIter.remove(); + // if the current "to" is at or after croppedFrom, check if the old max fix is in the cropped interval; if so, request + // creation of a new cache entry: + if (toAndResult.getB() != null) { + final TimePoint maxFixTimePoint = toAndResult.getB().getA().getTimePoint(); + if (!toAndResult.getA().before(croppedFrom) && !maxFixTimePoint.before(croppedFrom) + && !maxFixTimePoint.after(toAndResult.getA())) { + addEntryToMap(croppedFrom, toAndResult.getA(), toAndResult.getB(), result); + } + } + } + } + return result; + } + + @Override + public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) { + LockUtil.lockForWrite(lock); + try { + cache.clear(); + } finally { + LockUtil.unlockAfterWrite(lock); + } + } + + public Pair getMaxSpeed(TimePoint from, TimePoint to) { + Pair result = null; + if (!to.before(from)) { + result = cacheLookup(from, to); + if (result == null) { + result = computeMaxSpeed(from, to); + cache(from, to, result); + } + } + return result; + } + + /** + * If a cache entry exists for "from" whose interval ends at or before to, use it and if necessary compute the + * maximum in the missing tail, producing another cache entry for both, the tail and the extended interval. Otherwise, null + * is returned. + */ + private Pair cacheLookup(TimePoint from, TimePoint to) { + assert !from.after(to); + Pair result = null; + NavigableSet>> entry; + Pair> entryForLongestSubseries = null; + LockUtil.lockForRead(lock); + try { + entry = cache.get(from); + if (entry != null) { + entryForLongestSubseries = entry.floor(new Pair>(to, null)); + } + } finally { + LockUtil.unlockAfterRead(lock); + } + if (entryForLongestSubseries != null && entryForLongestSubseries.getB() != null) { + TimePoint entryTo = entryForLongestSubseries.getA(); + // avoid endless recursion in case entryTo is not after from: + if (entryTo.equals(to) || entryTo.after(from)) { + if (entryTo.before(to)) { + Pair maxInMissingTail = getMaxSpeed(entryTo, to); + if (maxInMissingTail != null && maxInMissingTail.getB().compareTo(entryForLongestSubseries.getB().getB()) > 0) { + // the maximum speed is in the tail that was not part of the interval retrieved from the cache + result = maxInMissingTail; + } else { + result = entryForLongestSubseries.getB(); // the interval from the cache also holds the maximum + // for the extended interval + } + cache(from, to, result); // produce new + } else { + result = entryForLongestSubseries.getB(); + } + } + } + return result; + } + + private void cache(TimePoint from, TimePoint to, Pair fixAtMaxSpeed) { + LockUtil.lockForWrite(lock); + try { + addEntryToMap(from, to, fixAtMaxSpeed, cache); + } finally { + LockUtil.unlockAfterWrite(lock); + } + } + + private void addEntryToMap(TimePoint from, TimePoint to, Pair fixAtMaxSpeed, + Map>>> map) { + NavigableSet>> setForFrom = map.get(from); + if (setForFrom == null) { + setForFrom = new ArrayListNavigableSet>>( + new Comparator>>() { + @Override + public int compare(Pair> o1, + Pair> o2) { + return o1.getA().compareTo(o2.getA()); + } + }); + map.put(from, setForFrom); + } + setForFrom.add(new Pair>(to, fixAtMaxSpeed)); + } + + /** + * @return null, if no fix exists in the interval specified + */ + private Pair computeMaxSpeed(TimePoint from, TimePoint to) { + track.lockForRead(); + try { + // fetch all fixes on this leg so far and determine their maximum speed + Iterator iter = track.getFixesIterator(from, /* inclusive */ true); + Speed max = Speed.NULL; + FixType maxSpeedFix = null; + if (iter.hasNext()) { + while (iter.hasNext()) { + FixType fix = iter.next(); + if (fix.getTimePoint().after(to)) { + break; + } + Speed averagedSpeedAtFixTime = track.getEstimatedSpeed(fix.getTimePoint()); + if (averagedSpeedAtFixTime != null && averagedSpeedAtFixTime.compareTo(max) > 0) { + max = averagedSpeedAtFixTime; + maxSpeedFix = fix; + } + } + } + return maxSpeedFix == null ? null : new Pair(maxSpeedFix, max); + } finally { + track.unlockAfterRead(); + } + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/ScalableWind.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/ScalableWind.java index 58aa7572869..c9611e5c0c3 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/ScalableWind.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/ScalableWind.java @@ -1,11 +1,11 @@ package com.sap.sailing.domain.tracking.impl; -import com.sap.sailing.domain.base.impl.ScalablePosition; -import com.sap.sailing.domain.base.impl.ScalableSpeedWithBearing; import com.sap.sailing.domain.common.AbstractPosition; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; -import com.sap.sailing.domain.confidence.ScalableValue; +import com.sap.sailing.domain.common.scalablevalue.ScalableValue; +import com.sap.sailing.domain.common.scalablevalue.impl.ScalablePosition; +import com.sap.sailing.domain.common.scalablevalue.impl.ScalableSpeedWithBearing; import com.sap.sailing.domain.tracking.Wind; /** diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackBasedEstimationWindTrackImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackBasedEstimationWindTrackImpl.java index 5682a7efeeb..3f895051bfb 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackBasedEstimationWindTrackImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackBasedEstimationWindTrackImpl.java @@ -82,13 +82,21 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl private final NavigableSet timePointsWithCachedNullResult; - /** - * Access to this cache is guarded by {@link #cacheLock}. - */ private final NavigableSet> cache; + /** + * Access to {@link #cache} and {@link #timePointsWithCachedNullResult} and + * {@link #timePointsWithCachedNullResultFastContains} is guarded by this lock. + */ private final NamedReentrantReadWriteLock cacheLock; + /** + * Access to {@link #scheduledRefreshInterval} is guarded by this lock. In particular, when a client + * holds this lock the client can be sure that {@link InvalidationInterval#isSet()} implies that a + * cache update is scheduled or already running. + */ + private final NamedReentrantReadWriteLock scheduledRefreshIntervalLock; + private final Weigher weigher; /** @@ -106,7 +114,13 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl private static class InvalidationInterval implements Serializable { private static final long serialVersionUID = -6406690520919193690L; + /** + * Either both {@link #start} and {@link #end} are null, or both are non-null + */ private WindWithConfidence start; + /** + * Either both {@link #start} and {@link #end} are null, or both are non-null + */ private TimePoint end; public InvalidationInterval() { super(); @@ -117,18 +131,38 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl public TimePoint getEnd() { return end; } - public synchronized void clear() { + public void clear() { start = null; end = null; } - public synchronized boolean isSet() { + public boolean isSet() { return start != null && end != null; } - public synchronized void set(WindWithConfidence startOfInvalidation, TimePoint endOfInvalidation) { + + /** + * Checks whether the interval starting at {@link #start} and ending at {@link #end} is fully contained in this + * interval. Neither of the two arguments is allowed to be null. The interval is contained in this + * interval if this interval {@link #isSet() is set} and this interval's {@link #getStart() start} is at or + * before start and this interval's {@link #getEnd() end} is at or after end. + */ + public boolean contains(TimePoint start, TimePoint end) { + return isSet() && !getStart().getObject().getTimePoint().after(start) && !end.after(getEnd()); + } + /** + * Neither of the two arguments is allowed to be null. + */ + public void set(WindWithConfidence startOfInvalidation, TimePoint endOfInvalidation) { + assert startOfInvalidation != null; + assert endOfInvalidation != null; this.start = startOfInvalidation; this.end = endOfInvalidation; } - public synchronized void extend(WindWithConfidence startOfInvalidation, TimePoint endOfInvalidation) { + /** + * Neither of the two arguments is allowed to be null. + */ + public void extend(WindWithConfidence startOfInvalidation, TimePoint endOfInvalidation) { + assert startOfInvalidation != null; + assert endOfInvalidation != null; if (startOfInvalidation.getObject().getTimePoint().compareTo(start.getObject().getTimePoint()) < 0) { this.start = startOfInvalidation; } @@ -136,11 +170,54 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl end = endOfInvalidation; } } + + /** + * Leaves this object unchanged. Returns a new {@link InvalidationInterval} object. If the interval defined by + * start and end does not exceed this interval, the resulting interval will have + * {@link #isSet()}==false. Otherwise, the resulting interval will contain all time ranges from + * start to end (inclusive) that are not in this interval. In particular, if + * start..end exceeds this interval on both ends, the resulting interval will start at + * start and end at end. If this interval has {@link #isSet()}==false, + * the resulting interval will range from start to end. + * + * @param start must not be null + * @param end must not be null + */ + public InvalidationInterval subtract(WindWithConfidence start, TimePoint end) { + assert start != null; + assert end != null; + final InvalidationInterval result = new InvalidationInterval(); + if (!isSet()) { + result.set(start, end); + } else { + final WindWithConfidence newStart; + final TimePoint startTimePoint = getStart().getObject().getTimePoint(); + if (start.getObject().getTimePoint().before(startTimePoint)) { + newStart = start; + } else { + // don't go beyond the end of time, avoiding overflow + newStart = getDummyFixWithConfidence(getEnd().asMillis()==Long.MAX_VALUE?getEnd():getEnd().plus(1)); + } + final TimePoint newEnd; + if (end.after(getEnd())) { + newEnd = end; + } else { + // avoid underflow + newEnd = startTimePoint.asMillis()==0?startTimePoint:startTimePoint.minus(1); + } + if (!newStart.getObject().getTimePoint().after(newEnd)) { + result.set(newStart, newEnd); + } // else, the resulting interval remains unset + } + return result; + } } /** - * {@link #scheduleCacheRefresh(WindWithConfidence, TimePoint)} synchronizes on this object before changing it - * and when actually invalidating the cache. + * {@link #scheduleCacheRefresh(WindWithConfidence, TimePoint)} synchronizes on this object before changing it and + * when actually invalidating the cache. When a querying thread holds at least the read lock of {@link #cacheLock} + * and the refresh interval it {@link InvalidationInterval#isSet() is set}, an invalidation is currently running or + * has been scheduled. */ private final InvalidationInterval scheduledRefreshInterval; @@ -171,8 +248,11 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl return o1.getObject().getTimePoint().compareTo(o2.getObject().getTimePoint()); } }); - cacheLock = new NamedReentrantReadWriteLock(TrackBasedEstimationWindTrackImpl.class.getSimpleName()+" for race "+ - trackedRace.getRace().getName(), /* fair */ false); + cacheLock = new NamedReentrantReadWriteLock(TrackBasedEstimationWindTrackImpl.class.getSimpleName() + + " cacheLock for race " + trackedRace.getRace().getName(), /* fair */false); + scheduledRefreshIntervalLock = new NamedReentrantReadWriteLock( + TrackBasedEstimationWindTrackImpl.class.getSimpleName() + " scheduledRefreshIntervalLock for race " + + trackedRace.getRace().getName(), /* fair */false); virtualInternalRawFixes = new EstimatedWindFixesAsNavigableSet(trackedRace); weigher = ConfidenceFactory.INSTANCE .createHyperbolicTimeDifferenceWeigher(getMillisecondsOverWhichToAverageWind()); @@ -183,9 +263,11 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl } /** - * Synchronizes serialization on this object to avoid the cache being updated while being written. + * Uses the {@link #cacheLock} and {@link #scheduledRefreshIntervalLock} to make serialization on this object thread + * safe by avoiding the cache being updated while being written. */ - private synchronized void writeObject(ObjectOutputStream s) throws IOException { + private void writeObject(ObjectOutputStream s) throws IOException { + LockUtil.lockForRead(scheduledRefreshIntervalLock); LockUtil.lockForRead(cacheLock); lockForRead(); try { @@ -193,6 +275,7 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl } finally { unlockAfterRead(); LockUtil.unlockAfterRead(cacheLock); + LockUtil.unlockAfterRead(scheduledRefreshIntervalLock); } } @@ -227,29 +310,22 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl return cache; } + /** + * The caller must not hold the read lock of {@link #cacheLock} when calling this method (unless the caller + * also holds the corresponding write lock) because this method will acquire the corresponding write lock, and + * upgrading from a read lock to a write lock is not possible. + */ protected void cache(TimePoint timePoint, WindWithConfidence fix) { // can't use lockForWrite() here because caching can happen while holding the read lock, and the lock can't be // upgraded. But lockForRead() and synchronization will do the job because all invalidations lock the write lock, // and all contains() checks and get() calls use synchronization too. LockUtil.lockForWrite(cacheLock); try { - // synchronization necessary to protect writeObject from ConcurrentModificationException - synchronized (this) { - if (fix == null) { - synchronized (timePointsWithCachedNullResult) { - timePointsWithCachedNullResult.add(timePoint); - } - synchronized (timePointsWithCachedNullResultFastContains) { - timePointsWithCachedNullResultFastContains.add(timePoint); - } - } else { - LockUtil.lockForWrite(cacheLock); - try { - cache.add(fix); - } finally { - LockUtil.unlockAfterWrite(cacheLock); - } - } + if (fix == null) { + timePointsWithCachedNullResult.add(timePoint); + timePointsWithCachedNullResultFastContains.add(timePoint); + } else { + cache.add(fix); } } finally { LockUtil.unlockAfterWrite(cacheLock); @@ -263,19 +339,29 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl * will not be massively delayed by having to re-calculate the estimation over and over again. */ private void scheduleCacheRefresh(WindWithConfidence startOfInvalidation, TimePoint endOfInvalidation) { - LockUtil.lockForWrite(cacheLock); + final boolean alreadyContained; + LockUtil.lockForRead(scheduledRefreshIntervalLock); try { - if (!scheduledRefreshInterval.isSet()) { - // according to the invariant this implies [1]==null - scheduledRefreshInterval.set(startOfInvalidation, endOfInvalidation); - startSchedulerForCacheRefresh(); - } else { - // this means that an invalidation is already scheduled; as long as we're synchronized on scheduledInvalidationInterval - // we can safely extend the interval; the invalidation won't start before we release the lock - scheduledRefreshInterval.extend(startOfInvalidation, endOfInvalidation); - } + alreadyContained = scheduledRefreshInterval.contains(startOfInvalidation.getObject().getTimePoint(), endOfInvalidation); } finally { - LockUtil.unlockAfterWrite(cacheLock); + LockUtil.unlockAfterRead(scheduledRefreshIntervalLock); + } + if (!alreadyContained) { + LockUtil.lockForWrite(scheduledRefreshIntervalLock); + try { + if (!scheduledRefreshInterval.isSet()) { + // according to the invariant this implies [1]==null + scheduledRefreshInterval.set(startOfInvalidation, endOfInvalidation); + startSchedulerForCacheRefresh(); + } else { + // this means that an invalidation or incremental refresh is already scheduled; as long as we're + // synchronized on scheduledInvalidationInterval we can safely extend the interval; the invalidation + // won't start before we release the lock + scheduledRefreshInterval.extend(startOfInvalidation, endOfInvalidation); + } + } finally { + LockUtil.unlockAfterWrite(scheduledRefreshIntervalLock); + } } } @@ -285,6 +371,7 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl * running. */ private void invalidateCache() { + LockUtil.lockForWrite(scheduledRefreshIntervalLock); LockUtil.lockForWrite(cacheLock); try { Iterator> iter = (scheduledRefreshInterval.getStart() == null ? getCachedFixes() @@ -312,6 +399,7 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl scheduledRefreshInterval.clear(); } finally { LockUtil.unlockAfterWrite(cacheLock); + LockUtil.unlockAfterWrite(scheduledRefreshIntervalLock); } } @@ -321,27 +409,33 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl private void refreshCacheIncrementally() { Set> windFixesToRecalculate = new HashSet>(); Set cachedNullResultsToRecalculate = new HashSet(); + final WindWithConfidence refreshIntervalStart; + final TimePoint refreshIntervalEnd; + LockUtil.lockForRead(scheduledRefreshIntervalLock); LockUtil.lockForRead(cacheLock); try { - Iterator> iter = (scheduledRefreshInterval.getStart() == null ? getCachedFixes() - : getCachedFixes().tailSet(scheduledRefreshInterval.getStart(), /* inclusive */true)).iterator(); - Iterator nullIter = (scheduledRefreshInterval.getStart() == null ? timePointsWithCachedNullResult - : timePointsWithCachedNullResult.tailSet(scheduledRefreshInterval.getStart().getObject().getTimePoint(), /* inclusive */ + refreshIntervalStart = scheduledRefreshInterval.getStart(); + Iterator> iter = (refreshIntervalStart == null ? getCachedFixes() + : getCachedFixes().tailSet(refreshIntervalStart, /* inclusive */true)).iterator(); + Iterator nullIter = (refreshIntervalStart == null ? timePointsWithCachedNullResult + : timePointsWithCachedNullResult.tailSet(refreshIntervalStart.getObject().getTimePoint(), /* inclusive */ true)).iterator(); WindWithConfidence nextFixToRecalculate = null; + refreshIntervalEnd = scheduledRefreshInterval.getEnd(); while (iter.hasNext() && - ((nextFixToRecalculate = iter.next()).getObject().getTimePoint().compareTo(scheduledRefreshInterval.getEnd()) < 0) || - scheduledRefreshInterval.getEnd() == null) { + ((nextFixToRecalculate = iter.next()).getObject().getTimePoint().compareTo(refreshIntervalEnd) < 0) || + refreshIntervalEnd == null) { windFixesToRecalculate.add(nextFixToRecalculate); } TimePoint nextNullResultToRecalculate = null; while (nullIter.hasNext() && - ((nextNullResultToRecalculate = nullIter.next()).compareTo(scheduledRefreshInterval.getEnd()) < 0) || - scheduledRefreshInterval.getEnd() == null) { + ((nextNullResultToRecalculate = nullIter.next()).compareTo(refreshIntervalEnd) < 0) || + refreshIntervalEnd == null) { cachedNullResultsToRecalculate.add(nextNullResultToRecalculate); } } finally { LockUtil.unlockAfterRead(cacheLock); + LockUtil.unlockAfterRead(scheduledRefreshIntervalLock); } Set nullRemovals = new HashSet(); Set nullInsertions = new HashSet(); @@ -381,10 +475,22 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl for (Map.Entry> cacheInsertion : cacheInsertions.entrySet()) { cache(cacheInsertion.getKey(), cacheInsertion.getValue()); } - scheduledRefreshInterval.clear(); } finally { LockUtil.unlockAfterWrite(cacheLock); } + LockUtil.lockForWrite(scheduledRefreshIntervalLock); + try { + // now remove the interval for which the cache was refreshed; note that in between the interval may have been extended; + // for the extended part of the interval that hasn't been refreshed during this run, a new refresh needs to be + // requested + InvalidationInterval remainingRefreshInterval = scheduledRefreshInterval.subtract(refreshIntervalStart, refreshIntervalEnd); + scheduledRefreshInterval.clear(); + if (remainingRefreshInterval.isSet()) { + scheduleCacheRefresh(remainingRefreshInterval.getStart(), remainingRefreshInterval.getEnd()); + } + } finally { + LockUtil.unlockAfterWrite(scheduledRefreshIntervalLock); + } } private void startSchedulerForCacheRefresh() { @@ -452,15 +558,18 @@ public class TrackBasedEstimationWindTrackImpl extends VirtualWindTrackImpl impl return result; } - private WindWithConfidence getDummyFixWithConfidence(TimePoint timePoint) { + private static WindWithConfidence getDummyFixWithConfidence(TimePoint timePoint) { return new WindWithConfidenceImpl(new WindImpl(null, timePoint, defaultSpeedWithBearing), 0, timePoint, /* useSpeed */false); } private boolean nullResultCacheContains(TimePoint timePoint) { assertReadLock(); - synchronized (timePointsWithCachedNullResultFastContains) { + LockUtil.lockForRead(cacheLock); + try { return timePointsWithCachedNullResultFastContains.contains(timePoint); + } finally { + LockUtil.unlockAfterRead(cacheLock); } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegImpl.java index 13ad89e02f4..02147bc4a85 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegImpl.java @@ -258,12 +258,19 @@ public class TrackedLegImpl implements TrackedLeg, RaceChangeListener { } @Override - public Distance getCrossTrackError(Position p, TimePoint timePoint) { + public Distance getAbsoluteCrossTrackError(Position p, TimePoint timePoint) { final Position approximatePosition = getTrackedRace().getApproximatePosition(getLeg().getFrom(), timePoint); final Bearing legBearing = getLegBearing(timePoint); return approximatePosition==null || legBearing==null ? null : p.absoluteCrossTrackError(approximatePosition, legBearing); } + @Override + public Distance getSignedCrossTrackError(Position p, TimePoint timePoint) { + final Position approximatePosition = getTrackedRace().getApproximatePosition(getLeg().getFrom(), timePoint); + final Bearing legBearing = getLegBearing(timePoint); + return approximatePosition==null || legBearing==null ? null : p.crossTrackError(approximatePosition, legBearing); + } + @Override public Distance getGreatCircleDistance(TimePoint timePoint) { final Distance result; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegOfCompetitorImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegOfCompetitorImpl.java index 32703b3fc78..b35fc25f05d 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegOfCompetitorImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedLegOfCompetitorImpl.java @@ -1,768 +1,788 @@ -package com.sap.sailing.domain.tracking.impl; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; -import java.util.NavigableSet; - -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.Leg; -import com.sap.sailing.domain.base.Mark; -import com.sap.sailing.domain.common.Bearing; -import com.sap.sailing.domain.common.Distance; -import com.sap.sailing.domain.common.ManeuverType; -import com.sap.sailing.domain.common.NoWindException; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.Speed; -import com.sap.sailing.domain.common.SpeedWithBearing; -import com.sap.sailing.domain.common.TimePoint; -import com.sap.sailing.domain.common.impl.KnotSpeedImpl; -import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; -import com.sap.sailing.domain.common.impl.MeterDistance; -import com.sap.sailing.domain.common.impl.Util.Pair; -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.TrackedLegOfCompetitor; -import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sailing.domain.tracking.Wind; -import com.sap.sailing.util.impl.ArrayListNavigableSet; - -/** - * Provides a convenient view on the tracked leg, projecting to a single competitor's performance. - * - * @author Axel Uhl (d043530) - * - */ -public class TrackedLegOfCompetitorImpl implements TrackedLegOfCompetitor { - private static final long serialVersionUID = -7060076837717432808L; - private final TrackedLegImpl trackedLeg; - private final Competitor competitor; - - public TrackedLegOfCompetitorImpl(TrackedLegImpl trackedLeg, Competitor competitor) { - this.trackedLeg = trackedLeg; - this.competitor = competitor; - } - - @Override - public TrackedLegImpl getTrackedLeg() { - return trackedLeg; - } - - @Override - public Competitor getCompetitor() { - return competitor; - } - - @Override - public Leg getLeg() { - return trackedLeg.getLeg(); - } - - private TrackedRace getTrackedRace() { - return getTrackedLeg().getTrackedRace(); - } - - @Override - public Long getTimeInMilliSeconds(TimePoint timePoint) { - Long result; - MarkPassing passedStartWaypoint = getTrackedRace().getMarkPassing(getCompetitor(), - getTrackedLeg().getLeg().getFrom()); - if (passedStartWaypoint != null) { - MarkPassing passedEndWaypoint = getTrackedRace().getMarkPassing(getCompetitor(), - getTrackedLeg().getLeg().getTo()); - if (passedEndWaypoint != null) { - result = passedEndWaypoint.getTimePoint().asMillis() - passedStartWaypoint.getTimePoint().asMillis(); - } else { - if (getTrackedRace().getEndOfTracking() != null && timePoint.after(getTrackedRace().getEndOfTracking())) { - result = null; - } else { - result = timePoint.asMillis() - passedStartWaypoint.getTimePoint().asMillis(); - } - } - } else { - result = null; - } - return result; - } - - @Override - public Distance getDistanceTraveled(TimePoint timePoint) { - MarkPassing legStart = getMarkPassingForLegStart(); - if (legStart == null) { - return null; - } else { - MarkPassing legEnd = getMarkPassingForLegEnd(); - TimePoint end = timePoint; - if (legEnd != null && timePoint.compareTo(legEnd.getTimePoint()) > 0) { - // timePoint is after leg finish; take leg end and end time point - end = legEnd.getTimePoint(); - } - return getTrackedRace().getTrack(getCompetitor()).getDistanceTraveled(legStart.getTimePoint(), end); - } - } - - private MarkPassing getMarkPassingForLegStart() { - MarkPassing legStart = getTrackedRace().getMarkPassing(getCompetitor(), getLeg().getFrom()); - return legStart; - } - - private MarkPassing getMarkPassingForLegEnd() { - MarkPassing legEnd = getTrackedRace().getMarkPassing(getCompetitor(), getLeg().getTo()); - return legEnd; - } - - @Override - public Speed getAverageSpeedOverGround(TimePoint timePoint) { - Speed result; - MarkPassing legStart = getMarkPassingForLegStart(); - if (legStart == null) { - result = null; - } else { - TimePoint timePointToUse; - if (hasFinishedLeg(timePoint)) { - timePointToUse = getMarkPassingForLegEnd().getTimePoint(); - } else { - // use time point of latest fix if before timePoint, otherwise timePoint - GPSFixMoving lastFix = getTrackedRace().getTrack(getCompetitor()).getLastRawFix(); - if (lastFix == null) { - // No fix at all? Then we can't determine any speed - timePointToUse = null; - } else if (lastFix.getTimePoint().compareTo(timePoint) < 0) { - timePointToUse = lastFix.getTimePoint(); - } else { - timePointToUse = timePoint; - } - } - if (timePointToUse != null) { - Distance d = getDistanceTraveled(timePointToUse); - long millis = timePointToUse.asMillis() - legStart.getTimePoint().asMillis(); - result = d.inTime(millis); - } else { - result = null; - } - } - return result; - } - - @Override - public Pair getMaximumSpeedOverGround(TimePoint timePoint) { - // fetch all fixes on this leg so far and determine their maximum speed - MarkPassing legStart = getMarkPassingForLegStart(); - if (legStart == null) { - return null; - } - MarkPassing legEnd = getMarkPassingForLegEnd(); - TimePoint to; - if (legEnd == null || legEnd.getTimePoint().compareTo(timePoint) >= 0) { - to = timePoint; - } else { - to = legEnd.getTimePoint(); - } - GPSFixTrack track = getTrackedRace().getTrack(getCompetitor()); - return track.getMaximumSpeedOverGround(legStart.getTimePoint(), to); - } - - @Override - public Distance getWindwardDistanceToGo(TimePoint timePoint) throws NoWindException { - if (hasFinishedLeg(timePoint)) { - return Distance.NULL; - } else { - Distance result = null; - for (Mark mark : getLeg().getTo().getMarks()) { - Distance d = getWindwardDistanceTo(mark, timePoint); - if (result == null || d != null && d.compareTo(result) < 0) { - result = d; - } - } - return result; - } - } - - /** - * If the current {@link #getLeg() leg} is +/- {@link #UPWIND_DOWNWIND_TOLERANCE_IN_DEG} degrees collinear with the - * wind's bearing, the competitor's position is projected onto the line crossing mark in the wind's - * bearing, and the distance from the projection to the mark is returned. Otherwise, it is assumed that - * the leg is neither an upwind nor a downwind leg, and hence the true distance to mark is returned. - */ - private Distance getWindwardDistanceTo(Mark mark, TimePoint at) throws NoWindException { - Position estimatedPosition = getTrackedRace().getTrack(getCompetitor()).getEstimatedPosition(at, false); - if (!hasStartedLeg(at) || estimatedPosition == null) { - // covers the case with no fixes for this leg yet, also if the mark passing has already been received - estimatedPosition = getTrackedRace().getOrCreateTrack(getLeg().getFrom().getMarks().iterator().next()) - .getEstimatedPosition(at, false); - } - if (estimatedPosition == null) { // may happen if mark positions haven't been received yet - return null; - } - final Position estimatedMarkPosition = getTrackedRace().getOrCreateTrack(mark).getEstimatedPosition(at, false); - if (estimatedMarkPosition == null) { - return null; - } - return getTrackedLeg().getAbsoluteWindwardDistance(estimatedPosition, estimatedMarkPosition, at); - } - - /** - * Projects speed onto the wind direction for upwind/downwind legs to see how fast a boat travels - * "along the wind's direction." For reaching legs (neither upwind nor downwind), the speed is projected onto - * the leg's direction. - * - * @throws NoWindException in case the wind direction is not known - */ - private SpeedWithBearing getWindwardSpeed(SpeedWithBearing speed, TimePoint at) throws NoWindException { - SpeedWithBearing result = null; - if (speed != null) { - Bearing projectToBearing; - if (getTrackedLeg().isUpOrDownwindLeg(at)) { - Wind wind = getWind(getTrackedRace().getTrack(getCompetitor()).getEstimatedPosition(at, false), at); - if (wind == null) { - throw new NoWindException("Need at least wind direction to determine windward speed"); - } - projectToBearing = wind.getBearing(); - } else { - projectToBearing = getTrackedLeg().getLegBearing(at); - } - double cos = Math.cos(speed.getBearing().getRadians() - projectToBearing.getRadians()); - if (cos < 0) { - projectToBearing = projectToBearing.reverse(); - } - result = new KnotSpeedWithBearingImpl(Math.abs(speed.getKnots() * cos), projectToBearing); - } - return result; - } - - /** - * For now, we have an incredibly simple wind "model" which assigns a single common wind force and bearing - * to all positions on the course, only variable over time. - */ - Wind getWind(Position p, TimePoint at) { - return getTrackedRace().getWind(p, at); - } - - @Override - public int getRank(TimePoint timePoint) { - int result = 0; - if (hasStartedLeg(timePoint)) { - List competitorTracksByRank = getTrackedLeg().getCompetitorTracksOrderedByRank(timePoint); - result = competitorTracksByRank.indexOf(this)+1; - } - return result; - } - - @Override - public Speed getAverageVelocityMadeGood(TimePoint timePoint) throws NoWindException { - Speed result = null; - MarkPassing start = getMarkPassingForLegStart(); - if (start != null && start.getTimePoint().compareTo(timePoint) <= 0) { - MarkPassing end = getMarkPassingForLegEnd(); - if (end != null) { - TimePoint to; - if (timePoint.compareTo(end.getTimePoint()) >= 0) { - to = end.getTimePoint(); - } else { - to = timePoint; - } - Position endPos = getTrackedRace().getTrack(getCompetitor()).getEstimatedPosition(to, /* extrapolate */ false); - if (endPos != null) { - Distance d = getTrackedLeg().getAbsoluteWindwardDistance( - getTrackedRace().getTrack(getCompetitor()) - .getEstimatedPosition(start.getTimePoint(), false), endPos, to); - result = d.inTime(to.asMillis() - start.getTimePoint().asMillis()); - } - } - } - return result; - } - - - @Override - public Integer getNumberOfTacks(TimePoint timePoint) throws NoWindException { - Integer result = null; - if (hasStartedLeg(timePoint)) { - List maneuvers = getManeuvers(timePoint, /* waitForLatest */ true); - result = 0; - for (Maneuver maneuver : maneuvers) { - if (maneuver.getType() == ManeuverType.TACK) { - result++; - } - } - } - return result; - } - - @Override - public List getManeuvers(TimePoint timePoint, boolean waitForLatest) throws NoWindException { - MarkPassing legEnd = getMarkPassingForLegEnd(); - TimePoint end = timePoint; - if (legEnd != null && timePoint.compareTo(legEnd.getTimePoint()) > 0) { - // timePoint is after leg finish; take leg end and end time point - end = legEnd.getTimePoint(); - } - List maneuvers = getTrackedRace().getManeuvers(getCompetitor(), - getMarkPassingForLegStart().getTimePoint(), end, waitForLatest); - return maneuvers; - } - - @Override - public Integer getNumberOfJibes(TimePoint timePoint) throws NoWindException { - Integer result = null; - if (hasStartedLeg(timePoint)) { - List maneuvers = getManeuvers(timePoint, /* waitForLatest */ true); - result = 0; - for (Maneuver maneuver : maneuvers) { - if (maneuver.getType() == ManeuverType.JIBE) { - result++; - } - } - } - return result; - } - - @Override - public Integer getNumberOfPenaltyCircles(TimePoint timePoint) throws NoWindException { - Integer result = null; - if (hasStartedLeg(timePoint)) { - List maneuvers = getManeuvers(timePoint, /* waitForLatest */ true); - result = 0; - for (Maneuver maneuver : maneuvers) { - if (maneuver.getType() == ManeuverType.PENALTY_CIRCLE) { - result++; - } - } - } - return result; - } - - @Override - public Distance getWindwardDistanceToOverallLeader(TimePoint timePoint) throws NoWindException { - // FIXME bug 607 it seems the following fetches the leader of this leg, not the overall leader; validate!!! Use getTrackedRace().getRanks() instead - Competitor leader = getTrackedRace().getOverallLeader(timePoint); - TrackedLegOfCompetitor leaderLeg = getTrackedRace().getCurrentLeg(leader, timePoint); - Distance result = null; - Position leaderPosition = getTrackedRace().getTrack(leader).getEstimatedPosition(timePoint, /* extrapolate */ false); - Position currentPosition = getTrackedRace().getTrack(getCompetitor()).getEstimatedPosition(timePoint, /* extrapolate */ false); - if (leaderPosition != null && currentPosition != null) { - result = Distance.NULL; - boolean foundCompetitorsLeg = false; - getTrackedRace().getRace().getCourse().lockForRead(); - try { - for (Leg leg : getTrackedRace().getRace().getCourse().getLegs()) { - if (leg == getLeg()) { - foundCompetitorsLeg = true; - } - if (foundCompetitorsLeg) { - // if the leaderLeg is null, the leader has already arrived - if (leaderLeg == null || leg != leaderLeg.getLeg()) { - // add distance to next mark - Position nextMarkPosition = getTrackedRace().getApproximatePosition(leg.getTo(), timePoint); - Distance distanceToNextMark = getTrackedRace().getTrackedLeg(leg) - .getAbsoluteWindwardDistance(currentPosition, nextMarkPosition, timePoint); - result = new MeterDistance(result.getMeters() + distanceToNextMark.getMeters()); - currentPosition = nextMarkPosition; - } else { - // we're now in the same leg with leader; compute windward distance to leader - result = new MeterDistance(result.getMeters() - + getTrackedRace().getTrackedLeg(leg) - .getAbsoluteWindwardDistance(currentPosition, leaderPosition, timePoint) - .getMeters()); - break; - } - } - } - } finally { - getTrackedRace().getRace().getCourse().unlockAfterRead(); - } - } - return result; - } - - @Override - public Distance getAverageCrossTrackError(TimePoint timePoint, boolean waitForLatestAnalysis) throws NoWindException { - Distance result = null; - final MarkPassing legStartMarkPassing = getTrackedRace().getMarkPassing(competitor, getLeg().getFrom()); - if (legStartMarkPassing != null) { - TimePoint legStart = legStartMarkPassing.getTimePoint(); - final MarkPassing legEndMarkPassing = getTrackedRace().getMarkPassing(competitor, getLeg().getTo()); - TimePoint to; - if (legEndMarkPassing == null || legEndMarkPassing.getTimePoint().compareTo(timePoint) > 0) { - to = timePoint; - } else { - to = legEndMarkPassing.getTimePoint(); - } - result = getTrackedRace().getAverageCrossTrackError(competitor, legStart, to, /* upwindOnly */ false, waitForLatestAnalysis); - } - return result; - } - - @Override - public Double getGapToLeaderInSeconds(TimePoint timePoint, final Competitor leaderInLegAtTimePoint) - throws NoWindException { - return getGapToLeaderInSeconds(timePoint, new LeaderGetter() { - @Override - public Competitor getLeader() { - return leaderInLegAtTimePoint; - } - }); - } - - private static interface LeaderGetter { - Competitor getLeader(); - } - - @Override - public Double getGapToLeaderInSeconds(final TimePoint timePoint) throws NoWindException { - return getGapToLeaderInSeconds(timePoint, new LeaderGetter() { - @Override - public Competitor getLeader() { - return getTrackedLeg().getLeader(hasFinishedLeg(timePoint) ? getFinishTime() : timePoint); - } - }); - } - - private Double getGapToLeaderInSeconds(TimePoint timePoint, LeaderGetter leaderGetter) throws NoWindException { - // If the leader already completed this leg, compute the estimated arrival time at the - // end of this leg; if this leg's competitor also already finished the leg, return the - // difference between this competitor's leg completion time point and the leader's completion - // time point; else, calculate the windward distance to the leader and divide by - // the windward speed - Speed windwardSpeed = getWindwardSpeed(getTrackedRace().getTrack(getCompetitor()).getEstimatedSpeed(timePoint), timePoint); - Double result = null; - // Has our competitor started the leg already? If not, we won't be able to compute a gap - if (hasStartedLeg(timePoint)) { - Iterable markPassingsInOrder = getTrackedRace().getMarkPassingsInOrder(getLeg().getTo()); - if (markPassingsInOrder != null) { - MarkPassing firstMarkPassing = null; - getTrackedRace().lockForRead(markPassingsInOrder); - try { - Iterator markPassingsForLegEnd = markPassingsInOrder.iterator(); - if (markPassingsForLegEnd.hasNext()) { - firstMarkPassing = markPassingsForLegEnd.next(); - } - } finally { - getTrackedRace().unlockAfterRead(markPassingsInOrder); - } - if (firstMarkPassing != null) { - // someone has already finished the leg - TimePoint whenLeaderFinishedLeg = firstMarkPassing.getTimePoint(); - // Was it before the requested timePoint? - if (whenLeaderFinishedLeg.compareTo(timePoint) <= 0) { - // Has our competitor also already finished this leg? - if (hasFinishedLeg(timePoint)) { - // Yes, so the gap is the time period between the time points at which the leader and - // our competitor finished this leg. - return (getMarkPassingForLegEnd().getTimePoint().asMillis() - whenLeaderFinishedLeg - .asMillis()) / 1000.; - } else { - if (windwardSpeed == null) { - return null; - } else { - // leader has finished already; our competitor hasn't - Distance windwardDistanceToGo = getWindwardDistanceToGo(timePoint); - long millisSinceLeaderPassedMarkToTimePoint = timePoint.asMillis() - - whenLeaderFinishedLeg.asMillis(); - return windwardDistanceToGo.getMeters() / windwardSpeed.getMetersPerSecond() - + millisSinceLeaderPassedMarkToTimePoint / 1000.; - } - } - } - } - // no-one has finished this leg yet at timePoint - Competitor leader = leaderGetter.getLeader(); - // Maybe our competitor is the leader. Check: - if (leader == getCompetitor()) { - return 0.0; // the leader's gap to the leader - } else { - if (windwardSpeed == null) { - return null; - } else { - // no, we're not the leader, so compute our windward distance and divide by our current VMG - Position ourEstimatedPosition = getTrackedRace().getTrack(getCompetitor()) - .getEstimatedPosition(timePoint, false); - Position leaderEstimatedPosition = getTrackedRace().getTrack(leader).getEstimatedPosition( - timePoint, false); - if (ourEstimatedPosition == null || leaderEstimatedPosition == null) { - return null; - } else { - Distance windwardDistanceToGo = getTrackedLeg().getAbsoluteWindwardDistance(ourEstimatedPosition, - leaderEstimatedPosition, timePoint); - return windwardDistanceToGo.getMeters() / windwardSpeed.getMetersPerSecond(); - } - } - } - } - } - // else our competitor hasn't started the leg yet, so we can't compute a gap since we don't - // have a speed estimate; leave result == null - return result; - } - - @Override - public boolean hasStartedLeg(TimePoint timePoint) { - MarkPassing markPassingForLegStart = getMarkPassingForLegStart(); - return markPassingForLegStart != null && markPassingForLegStart.getTimePoint().compareTo(timePoint) <= 0; - } - - @Override - public boolean hasFinishedLeg(TimePoint timePoint) { - MarkPassing markPassingForLegEnd = getMarkPassingForLegEnd(); - return markPassingForLegEnd != null && markPassingForLegEnd.getTimePoint().compareTo(timePoint) <= 0; - } - - @Override - public TimePoint getStartTime() { - MarkPassing markPassingForLegStart = getMarkPassingForLegStart(); - return markPassingForLegStart == null ? null : markPassingForLegStart.getTimePoint(); - } - - @Override - public TimePoint getFinishTime() { - MarkPassing markPassingForLegEnd = getMarkPassingForLegEnd(); - return markPassingForLegEnd == null ? null : markPassingForLegEnd.getTimePoint(); - } - - @Override - public Speed getVelocityMadeGood(TimePoint at) throws NoWindException { - if (hasStartedLeg(at)) { - TimePoint timePoint; - if (hasFinishedLeg(at)) { - // use the leg finishing time point - timePoint = getMarkPassingForLegEnd().getTimePoint(); - } else { - timePoint = at; - } - SpeedWithBearing speedOverGround = getSpeedOverGround(timePoint); - return speedOverGround == null ? null : getWindwardSpeed(speedOverGround, timePoint); - } else { - return null; - } - } - - @Override - public SpeedWithBearing getSpeedOverGround(TimePoint at) { - if (hasStartedLeg(at)) { - TimePoint timePoint; - if (hasFinishedLeg(at)) { - // use the leg finishing time point - timePoint = getMarkPassingForLegEnd().getTimePoint(); - } else { - timePoint = at; - } - return getTrackedRace().getTrack(getCompetitor()).getEstimatedSpeed(timePoint); - } else { - return null; - } - } - - @Override - public Double getEstimatedTimeToNextMarkInSeconds(TimePoint timePoint) throws NoWindException { - Double result; - if (hasFinishedLeg(timePoint)) { - result = 0.0; - } else { - if (hasStartedLeg(timePoint)) { - Distance windwardDistanceToGo = getWindwardDistanceToGo(timePoint); - Speed vmg = getVelocityMadeGood(timePoint); - result = vmg == null ? null : windwardDistanceToGo.getMeters() / vmg.getMetersPerSecond(); - } else { - result = null; - } - } - return result; - } - - @Override - public Distance getManeuverLoss(TimePoint timePointBeforeManeuver, - TimePoint maneuverTimePoint, TimePoint timePointAfterManeuver) throws NoWindException { - assert timePointBeforeManeuver != null; - assert timePointAfterManeuver != null; - Distance result; - final GPSFixTrack track = getTrackedRace().getTrack(getCompetitor()); - List fixes = getFixesToConsiderForManeuverLossAnalysis(timePointBeforeManeuver, - maneuverTimePoint, timePointAfterManeuver); - TimePoint timePointWhenSpeedStartedToDrop = fixes.get(0).getTimePoint(); - SpeedWithBearing speedWhenSpeedStartedToDrop = track.getEstimatedSpeed(timePointWhenSpeedStartedToDrop); - if (speedWhenSpeedStartedToDrop != null) { - TimePoint timePointWhenSpeedLevelledOffAfterManeuver = fixes.get(fixes.size()-1).getTimePoint(); - SpeedWithBearing speedAfterManeuver = track.getEstimatedSpeed(timePointWhenSpeedLevelledOffAfterManeuver); - if (speedAfterManeuver != null) { - // For upwind/downwind legs, find the mean course between inbound and outbound course and project actual and - // extrapolated positions onto it: - Bearing middleManeuverAngle = speedWhenSpeedStartedToDrop.getBearing().middle(speedAfterManeuver.getBearing()); - // extrapolate maximum speed before maneuver to time point of maximum speed after maneuver and project resulting position - // onto the average maneuver course; compare to the projected position actually reached at the time point of maximum speed after - // maneuver: - Position positionWhenSpeedStartedToDrop = track.getEstimatedPosition(timePointWhenSpeedStartedToDrop, /* extrapolate */ false); - Position extrapolatedPositionAtTimePointOfMaxSpeedAfterManeuver = - speedWhenSpeedStartedToDrop.travelTo(positionWhenSpeedStartedToDrop, timePointWhenSpeedStartedToDrop, timePointWhenSpeedLevelledOffAfterManeuver); - Position actualPositionAtTimePointOfMaxSpeedAfterManeuver = track.getEstimatedPosition(timePointWhenSpeedLevelledOffAfterManeuver, /* extrapolate */ false); - Position projectedExtrapolatedPositionAtTimePointOfMaxSpeedAfterManeuver = - extrapolatedPositionAtTimePointOfMaxSpeedAfterManeuver.projectToLineThrough(positionWhenSpeedStartedToDrop, middleManeuverAngle); - Position projectedActualPositionAtTimePointOfMaxSpeedAfterManeuver = - actualPositionAtTimePointOfMaxSpeedAfterManeuver.projectToLineThrough(positionWhenSpeedStartedToDrop, middleManeuverAngle); - result = projectedActualPositionAtTimePointOfMaxSpeedAfterManeuver.getDistance(projectedExtrapolatedPositionAtTimePointOfMaxSpeedAfterManeuver); - } else { - result = null; - } - } else { - result = null; - } - return result; - } - - /** - * Fetches the set of fixes that encompass the maneuver. Usually, during a maneuver a boat loses speed over ground - * before it has reached the new tack and starts accelerating again, until the speed levels off. We assume that the - * timePointBeforeManeuver minus some excess time based on the approximate maneuver duration is the - * earliest time point to analyze. Starting there, we look for speed over ground minima between then and - * timePointAfterManeuver plus three times the approximate maneuver time as excess time. There may be - * multiple minima. We choose the one that has the best "fit" in terms of being close to the - * maneuverTimePoint and being low in terms of speed over ground. From that time point, the nearest - * maximum speeds over ground before and after are determined, and the fixes between them are returned. - */ - private List getFixesToConsiderForManeuverLossAnalysis(TimePoint timePointBeforeManeuver, - TimePoint maneuverTimePoint, TimePoint timePointAfterManeuver) { - final long EXCESS_TIME_BEFORE_MANEUVER_END_TO_SCAN_IN_MILLIS = getCompetitor().getBoat().getBoatClass().getApproximateManeuverDurationInMilliseconds(); - final long EXCESS_TIME_AFTER_MANEUVER_END_TO_SCAN_IN_MILLIS = 3*EXCESS_TIME_BEFORE_MANEUVER_END_TO_SCAN_IN_MILLIS; - List fixes = new ArrayList<>(); - NavigableSet maxima = new ArrayListNavigableSet(new TimedComparator()); - NavigableSet minima = new ArrayListNavigableSet(new TimedComparator()); - GPSFixTrack track = getTrackedRace().getTrack(getCompetitor()); - Speed lastSpeed = Speed.NULL; - Speed lastLastSpeed = Speed.NULL; - GPSFixMoving lastFix = null; - Speed minimumSpeed = new KnotSpeedImpl(Double.MAX_VALUE); - track.lockForRead(); - try { - Iterator fixIter = track.getFixesIterator( - timePointBeforeManeuver.minus(EXCESS_TIME_BEFORE_MANEUVER_END_TO_SCAN_IN_MILLIS), /* inclusive */true); - GPSFixMoving fix; - // The timePointAfterManeuver is determined based on the geometric shape of the boat's trajectory, not on - // the speed development. To understand the full maneuver loss, we need to follow the boat speed until it levels off, - // but no further than some reasonable threshold because the wind may continue to pick up, letting the boat accelerate - // over a time much longer than accounted for by the maneuver. - while (fixIter.hasNext()) { - fix = fixIter.next(); - final SpeedWithBearing estimatedSpeedAtFix = track.getEstimatedSpeed(fix.getTimePoint()); - if (lastFix != null && lastSpeed != null && lastLastSpeed != null && estimatedSpeedAtFix != null) { - if (lastSpeed.compareTo(lastLastSpeed) > 0 && lastSpeed.compareTo(estimatedSpeedAtFix) > 0) { - maxima.add(lastFix); - } else if (lastSpeed.compareTo(lastLastSpeed) < 0 && lastSpeed.compareTo(estimatedSpeedAtFix) < 0) { - minima.add(lastFix); - if (lastSpeed.compareTo(minimumSpeed) < 0) { - minimumSpeed = lastSpeed; - } - } - } - if (fix.getTimePoint().after(timePointAfterManeuver.plus(EXCESS_TIME_AFTER_MANEUVER_END_TO_SCAN_IN_MILLIS))) { - break; - } - fixes.add(fix); - lastLastSpeed = lastSpeed; - lastSpeed = estimatedSpeedAtFix; - lastFix = fix; - } - } finally { - track.unlockAfterRead(); - } - GPSFixMoving fixWithLowestSpeedOverGround = getBestFittingSpeedMinimumInManeuver(minima, maxima, minimumSpeed, maneuverTimePoint); - if (fixWithLowestSpeedOverGround == null) { - fixWithLowestSpeedOverGround = fixes.get(0); - } - // now remove all fixes before the last maximum before the fix with the lowest speed over ground during the maneuver if - // there was such a maximum; otherwise, leave all fixes from the maneuver start on in place. - final long MAX_SMOOTHENING_INTERVAL_MILLIS = getCompetitor().getBoat().getBoatClass().getApproximateManeuverDurationInMilliseconds(); - GPSFixMoving lastMaxSpeedFixBeforeLowSpeed = maxima.lower(fixWithLowestSpeedOverGround); - // now check if there's a greater one that's only a little bit earlier - if (lastMaxSpeedFixBeforeLowSpeed != null) { - GPSFixMoving stillGreater; - while ((stillGreater=maxima.lower(lastMaxSpeedFixBeforeLowSpeed)) != null && - track.getEstimatedSpeed(stillGreater.getTimePoint()).compareTo( - track.getEstimatedSpeed(lastMaxSpeedFixBeforeLowSpeed.getTimePoint())) > 0 && - lastMaxSpeedFixBeforeLowSpeed.getTimePoint().asMillis()- - stillGreater.getTimePoint().asMillis() < MAX_SMOOTHENING_INTERVAL_MILLIS) { - lastMaxSpeedFixBeforeLowSpeed = stillGreater; - } - Iterator i = fixes.iterator(); - while (i.hasNext() && i.next().getTimePoint().before(lastMaxSpeedFixBeforeLowSpeed.getTimePoint())) { - i.remove(); - } - } - // now remove all fixes after the first maximum after the global minimum: - GPSFixMoving firstMaxSpeedFixAfterLowSpeed = maxima.higher(fixWithLowestSpeedOverGround); - if (firstMaxSpeedFixAfterLowSpeed != null) { - GPSFixMoving stillGreater; - while ((stillGreater=maxima.higher(firstMaxSpeedFixAfterLowSpeed)) != null && - track.getEstimatedSpeed(stillGreater.getTimePoint()).compareTo( - track.getEstimatedSpeed(firstMaxSpeedFixAfterLowSpeed.getTimePoint())) > 0 && - stillGreater.getTimePoint().asMillis() - firstMaxSpeedFixAfterLowSpeed.getTimePoint().asMillis() - < MAX_SMOOTHENING_INTERVAL_MILLIS) { - firstMaxSpeedFixAfterLowSpeed = stillGreater; - } - ListIterator i = fixes.listIterator(fixes.size()); - while (i.hasPrevious() && i.previous().getTimePoint().after(firstMaxSpeedFixAfterLowSpeed.getTimePoint())) { - i.remove(); - } - } - return fixes; - } - - private GPSFixMoving getBestFittingSpeedMinimumInManeuver(NavigableSet minima, - NavigableSet maxima, Speed minimumSpeed, TimePoint maneuverTimePoint) { - // Idea: being 10x the approximate maneuver duration away from maneuverTimePoint is as bad as having twice the percentage between min - // and max speed (min = 0%; max=100%). - GPSFixMoving bestSpeedMinimum = null; - Speed maxSpeed = null; - for (GPSFixMoving maximum : maxima) { - if (maxSpeed == null || maximum.getSpeed().getKnots() > maxSpeed.getKnots()) { - maxSpeed = maximum.getSpeed(); - } - } - if (maxSpeed != null) { - double speedDifferenceBetweenMaxAndMinInKnots = maxSpeed.getKnots() - minimumSpeed.getKnots(); - double lowestBadness = Double.MAX_VALUE; - final long approximateManeuverTimeInMillis = getCompetitor().getBoat().getBoatClass() - .getApproximateManeuverDurationInMilliseconds(); - final GPSFixTrack track = getTrackedRace().getTrack(getCompetitor()); - for (GPSFixMoving minimum : minima) { - // best speedBadness can be 1 which represents the absolute speed minimum in the interval considered - double speedBadness = 1 + (track.getEstimatedSpeed(minimum.getTimePoint()).getKnots() - minimumSpeed.getKnots()) / - speedDifferenceBetweenMaxAndMinInKnots; - // best timePointBadness can be 1 which is a minimum exactly at the maneuver time point - double timePointBadness = 1. - + (double) Math.abs(minimum.getTimePoint().asMillis() - maneuverTimePoint.asMillis()) - / (double) approximateManeuverTimeInMillis / 10.; - final double totalBadness = speedBadness * timePointBadness; - if (totalBadness < lowestBadness) { - bestSpeedMinimum = minimum; - lowestBadness = totalBadness; - } - } - } - return bestSpeedMinimum; - } - - @Override - public Bearing getBeatAngle(TimePoint at) throws NoWindException { - Bearing beatAngle = null; - - Bearing projectToBearing; - if (getTrackedLeg().isUpOrDownwindLeg(at)) { - Wind wind = getWind(getTrackedRace().getTrack(getCompetitor()).getEstimatedPosition(at, false), at); - if (wind == null) { - throw new NoWindException("Need at least wind direction to determine windward speed"); - } - projectToBearing = wind.getBearing(); - } else { - projectToBearing = getTrackedLeg().getLegBearing(at); - } - - SpeedWithBearing speed = getSpeedOverGround(at); - if (speed != null) { - beatAngle = projectToBearing.getDifferenceTo(speed.getBearing()); - } - - return beatAngle; - } -} +package com.sap.sailing.domain.tracking.impl; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NavigableSet; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Leg; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.Duration; +import com.sap.sailing.domain.common.ManeuverType; +import com.sap.sailing.domain.common.NoWindException; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.impl.KnotSpeedImpl; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.impl.MeterDistance; +import com.sap.sailing.domain.common.impl.Util.Pair; +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.TrackedLegOfCompetitor; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.domain.tracking.Wind; +import com.sap.sailing.util.impl.ArrayListNavigableSet; + +/** + * Provides a convenient view on the tracked leg, projecting to a single competitor's performance. + * + * @author Axel Uhl (d043530) + * + */ +public class TrackedLegOfCompetitorImpl implements TrackedLegOfCompetitor { + private static final long serialVersionUID = -7060076837717432808L; + private final TrackedLegImpl trackedLeg; + private final Competitor competitor; + + public TrackedLegOfCompetitorImpl(TrackedLegImpl trackedLeg, Competitor competitor) { + this.trackedLeg = trackedLeg; + this.competitor = competitor; + } + + @Override + public TrackedLegImpl getTrackedLeg() { + return trackedLeg; + } + + @Override + public Competitor getCompetitor() { + return competitor; + } + + @Override + public Leg getLeg() { + return trackedLeg.getLeg(); + } + + private TrackedRace getTrackedRace() { + return getTrackedLeg().getTrackedRace(); + } + + @Override + public Duration getTime(TimePoint timePoint) { + Duration result; + MarkPassing passedStartWaypoint = getTrackedRace().getMarkPassing(getCompetitor(), + getTrackedLeg().getLeg().getFrom()); + if (passedStartWaypoint != null && !passedStartWaypoint.getTimePoint().after(timePoint)) { + MarkPassing passedEndWaypoint = getTrackedRace().getMarkPassing(getCompetitor(), + getTrackedLeg().getLeg().getTo()); + if (passedEndWaypoint != null && timePoint.after(passedEndWaypoint.getTimePoint())) { + // the query asks for a time point after the competitor has finished the leg; return the total leg time + result = passedStartWaypoint.getTimePoint().until(passedEndWaypoint.getTimePoint()); + } else { + if (getTrackedRace().getEndOfTracking() != null && timePoint.after(getTrackedRace().getEndOfTracking())) { + result = null; + } else { + result = passedStartWaypoint.getTimePoint().until(timePoint); + } + } + } else { + result = null; + } + return result; + } + + @Override + public Distance getDistanceTraveled(TimePoint timePoint) { + MarkPassing legStart = getMarkPassingForLegStart(); + if (legStart == null) { + return null; + } else { + MarkPassing legEnd = getMarkPassingForLegEnd(); + TimePoint end = timePoint; + if (legEnd != null && timePoint.compareTo(legEnd.getTimePoint()) > 0) { + // timePoint is after leg finish; take leg end and end time point + end = legEnd.getTimePoint(); + } + return getTrackedRace().getTrack(getCompetitor()).getDistanceTraveled(legStart.getTimePoint(), end); + } + } + + private MarkPassing getMarkPassingForLegStart() { + MarkPassing legStart = getTrackedRace().getMarkPassing(getCompetitor(), getLeg().getFrom()); + return legStart; + } + + private MarkPassing getMarkPassingForLegEnd() { + MarkPassing legEnd = getTrackedRace().getMarkPassing(getCompetitor(), getLeg().getTo()); + return legEnd; + } + + @Override + public Speed getAverageSpeedOverGround(TimePoint timePoint) { + Speed result; + MarkPassing legStart = getMarkPassingForLegStart(); + if (legStart == null) { + result = null; + } else { + TimePoint timePointToUse; + if (hasFinishedLeg(timePoint)) { + timePointToUse = getMarkPassingForLegEnd().getTimePoint(); + } else { + // use time point of latest fix if before timePoint, otherwise timePoint + GPSFixMoving lastFix = getTrackedRace().getTrack(getCompetitor()).getLastRawFix(); + if (lastFix == null) { + // No fix at all? Then we can't determine any speed + timePointToUse = null; + } else if (lastFix.getTimePoint().compareTo(timePoint) < 0) { + timePointToUse = lastFix.getTimePoint(); + } else { + timePointToUse = timePoint; + } + } + if (timePointToUse != null) { + Distance d = getDistanceTraveled(timePointToUse); + long millis = timePointToUse.asMillis() - legStart.getTimePoint().asMillis(); + result = d.inTime(millis); + } else { + result = null; + } + } + return result; + } + + @Override + public Pair getMaximumSpeedOverGround(TimePoint timePoint) { + // fetch all fixes on this leg so far and determine their maximum speed + MarkPassing legStart = getMarkPassingForLegStart(); + if (legStart == null) { + return null; + } + MarkPassing legEnd = getMarkPassingForLegEnd(); + TimePoint to; + if (legEnd == null || legEnd.getTimePoint().compareTo(timePoint) >= 0) { + to = timePoint; + } else { + to = legEnd.getTimePoint(); + } + GPSFixTrack track = getTrackedRace().getTrack(getCompetitor()); + return track.getMaximumSpeedOverGround(legStart.getTimePoint(), to); + } + + @Override + public Distance getWindwardDistanceToGo(TimePoint timePoint) throws NoWindException { + if (hasFinishedLeg(timePoint)) { + return Distance.NULL; + } else { + Distance result = null; + for (Mark mark : getLeg().getTo().getMarks()) { + Distance d = getWindwardDistanceTo(mark, timePoint); + if (result == null || d != null && d.compareTo(result) < 0) { + result = d; + } + } + return result; + } + } + + /** + * If the current {@link #getLeg() leg} is +/- {@link #UPWIND_DOWNWIND_TOLERANCE_IN_DEG} degrees collinear with the + * wind's bearing, the competitor's position is projected onto the line crossing mark in the wind's + * bearing, and the distance from the projection to the mark is returned. Otherwise, it is assumed that + * the leg is neither an upwind nor a downwind leg, and hence the true distance to mark is returned. + */ + private Distance getWindwardDistanceTo(Mark mark, TimePoint at) throws NoWindException { + Position estimatedPosition = getTrackedRace().getTrack(getCompetitor()).getEstimatedPosition(at, false); + if (!hasStartedLeg(at) || estimatedPosition == null) { + // covers the case with no fixes for this leg yet, also if the mark passing has already been received + estimatedPosition = getTrackedRace().getOrCreateTrack(getLeg().getFrom().getMarks().iterator().next()) + .getEstimatedPosition(at, false); + } + if (estimatedPosition == null) { // may happen if mark positions haven't been received yet + return null; + } + final Position estimatedMarkPosition = getTrackedRace().getOrCreateTrack(mark).getEstimatedPosition(at, false); + if (estimatedMarkPosition == null) { + return null; + } + return getTrackedLeg().getAbsoluteWindwardDistance(estimatedPosition, estimatedMarkPosition, at); + } + + /** + * Projects speed onto the wind direction for upwind/downwind legs to see how fast a boat travels + * "along the wind's direction." For reaching legs (neither upwind nor downwind), the speed is projected onto + * the leg's direction. + * + * @throws NoWindException in case the wind direction is not known + */ + private SpeedWithBearing getWindwardSpeed(SpeedWithBearing speed, TimePoint at) throws NoWindException { + SpeedWithBearing result = null; + if (speed != null) { + Bearing projectToBearing; + if (getTrackedLeg().isUpOrDownwindLeg(at)) { + Wind wind = getWind(getTrackedRace().getTrack(getCompetitor()).getEstimatedPosition(at, false), at); + if (wind == null) { + throw new NoWindException("Need at least wind direction to determine windward speed"); + } + projectToBearing = wind.getBearing(); + } else { + projectToBearing = getTrackedLeg().getLegBearing(at); + } + double cos = Math.cos(speed.getBearing().getRadians() - projectToBearing.getRadians()); + if (cos < 0) { + projectToBearing = projectToBearing.reverse(); + } + result = new KnotSpeedWithBearingImpl(Math.abs(speed.getKnots() * cos), projectToBearing); + } + return result; + } + + /** + * For now, we have an incredibly simple wind "model" which assigns a single common wind force and bearing + * to all positions on the course, only variable over time. + */ + Wind getWind(Position p, TimePoint at) { + return getTrackedRace().getWind(p, at); + } + + @Override + public int getRank(TimePoint timePoint) { + int result = 0; + if (hasStartedLeg(timePoint)) { + List competitorTracksByRank = getTrackedLeg().getCompetitorTracksOrderedByRank(timePoint); + result = competitorTracksByRank.indexOf(this)+1; + } + return result; + } + + @Override + public Speed getAverageVelocityMadeGood(TimePoint timePoint) throws NoWindException { + Speed result = null; + MarkPassing start = getMarkPassingForLegStart(); + if (start != null && start.getTimePoint().compareTo(timePoint) <= 0) { + MarkPassing end = getMarkPassingForLegEnd(); + if (end != null) { + TimePoint to; + if (timePoint.compareTo(end.getTimePoint()) >= 0) { + to = end.getTimePoint(); + } else { + to = timePoint; + } + Position endPos = getTrackedRace().getTrack(getCompetitor()).getEstimatedPosition(to, /* extrapolate */ false); + if (endPos != null) { + Distance d = getTrackedLeg().getAbsoluteWindwardDistance( + getTrackedRace().getTrack(getCompetitor()) + .getEstimatedPosition(start.getTimePoint(), false), endPos, to); + result = d.inTime(to.asMillis() - start.getTimePoint().asMillis()); + } + } + } + return result; + } + + + @Override + public Integer getNumberOfTacks(TimePoint timePoint) throws NoWindException { + Integer result = null; + if (hasStartedLeg(timePoint)) { + List maneuvers = getManeuvers(timePoint, /* waitForLatest */ true); + result = 0; + for (Maneuver maneuver : maneuvers) { + if (maneuver.getType() == ManeuverType.TACK) { + result++; + } + } + } + return result; + } + + @Override + public List getManeuvers(TimePoint timePoint, boolean waitForLatest) throws NoWindException { + MarkPassing legEnd = getMarkPassingForLegEnd(); + TimePoint end = timePoint; + if (legEnd != null && timePoint.compareTo(legEnd.getTimePoint()) > 0) { + // timePoint is after leg finish; take leg end and end time point + end = legEnd.getTimePoint(); + } + List maneuvers = getTrackedRace().getManeuvers(getCompetitor(), + getMarkPassingForLegStart().getTimePoint(), end, waitForLatest); + return maneuvers; + } + + @Override + public Integer getNumberOfJibes(TimePoint timePoint) throws NoWindException { + Integer result = null; + if (hasStartedLeg(timePoint)) { + List maneuvers = getManeuvers(timePoint, /* waitForLatest */ true); + result = 0; + for (Maneuver maneuver : maneuvers) { + if (maneuver.getType() == ManeuverType.JIBE) { + result++; + } + } + } + return result; + } + + @Override + public Integer getNumberOfPenaltyCircles(TimePoint timePoint) throws NoWindException { + Integer result = null; + if (hasStartedLeg(timePoint)) { + List maneuvers = getManeuvers(timePoint, /* waitForLatest */ true); + result = 0; + for (Maneuver maneuver : maneuvers) { + if (maneuver.getType() == ManeuverType.PENALTY_CIRCLE) { + result++; + } + } + } + return result; + } + + @Override + public Distance getWindwardDistanceToOverallLeader(TimePoint timePoint) throws NoWindException { + // FIXME bug 607 it seems the following fetches the leader of this leg, not the overall leader; validate!!! Use getTrackedRace().getRanks() instead + Competitor leader = getTrackedRace().getOverallLeader(timePoint); + TrackedLegOfCompetitor leaderLeg = getTrackedRace().getCurrentLeg(leader, timePoint); + Distance result = null; + Position leaderPosition = getTrackedRace().getTrack(leader).getEstimatedPosition(timePoint, /* extrapolate */ false); + Position currentPosition = getTrackedRace().getTrack(getCompetitor()).getEstimatedPosition(timePoint, /* extrapolate */ false); + if (leaderPosition != null && currentPosition != null) { + result = Distance.NULL; + boolean foundCompetitorsLeg = false; + getTrackedRace().getRace().getCourse().lockForRead(); + try { + for (Leg leg : getTrackedRace().getRace().getCourse().getLegs()) { + if (leg == getLeg()) { + foundCompetitorsLeg = true; + } + if (foundCompetitorsLeg) { + // if the leaderLeg is null, the leader has already arrived + if (leaderLeg == null || leg != leaderLeg.getLeg()) { + // add distance to next mark + Position nextMarkPosition = getTrackedRace().getApproximatePosition(leg.getTo(), timePoint); + Distance distanceToNextMark = getTrackedRace().getTrackedLeg(leg) + .getAbsoluteWindwardDistance(currentPosition, nextMarkPosition, timePoint); + result = new MeterDistance(result.getMeters() + distanceToNextMark.getMeters()); + currentPosition = nextMarkPosition; + } else { + // we're now in the same leg with leader; compute windward distance to leader + result = new MeterDistance(result.getMeters() + + getTrackedRace().getTrackedLeg(leg) + .getAbsoluteWindwardDistance(currentPosition, leaderPosition, timePoint) + .getMeters()); + break; + } + } + } + } finally { + getTrackedRace().getRace().getCourse().unlockAfterRead(); + } + } + return result; + } + + @Override + public Distance getAverageAbsoluteCrossTrackError(TimePoint timePoint, boolean waitForLatestAnalysis) throws NoWindException { + Distance result = null; + final MarkPassing legStartMarkPassing = getTrackedRace().getMarkPassing(competitor, getLeg().getFrom()); + if (legStartMarkPassing != null) { + TimePoint legStart = legStartMarkPassing.getTimePoint(); + final MarkPassing legEndMarkPassing = getTrackedRace().getMarkPassing(competitor, getLeg().getTo()); + TimePoint to; + if (legEndMarkPassing == null || legEndMarkPassing.getTimePoint().compareTo(timePoint) > 0) { + to = timePoint; + } else { + to = legEndMarkPassing.getTimePoint(); + } + result = getTrackedRace().getAverageAbsoluteCrossTrackError(competitor, legStart, to, /* upwindOnly */ false, waitForLatestAnalysis); + } + return result; + } + + @Override + public Distance getAverageSignedCrossTrackError(TimePoint timePoint, boolean waitForLatestAnalysis) throws NoWindException { + Distance result = null; + final MarkPassing legStartMarkPassing = getTrackedRace().getMarkPassing(competitor, getLeg().getFrom()); + if (legStartMarkPassing != null) { + TimePoint legStart = legStartMarkPassing.getTimePoint(); + final MarkPassing legEndMarkPassing = getTrackedRace().getMarkPassing(competitor, getLeg().getTo()); + TimePoint to; + if (legEndMarkPassing == null || legEndMarkPassing.getTimePoint().compareTo(timePoint) > 0) { + to = timePoint; + } else { + to = legEndMarkPassing.getTimePoint(); + } + result = getTrackedRace().getAverageSignedCrossTrackError(competitor, legStart, to, /* upwindOnly */ false, waitForLatestAnalysis); + } + return result; + } + + @Override + public Double getGapToLeaderInSeconds(TimePoint timePoint, final Competitor leaderInLegAtTimePoint) + throws NoWindException { + return getGapToLeaderInSeconds(timePoint, new LeaderGetter() { + @Override + public Competitor getLeader() { + return leaderInLegAtTimePoint; + } + }); + } + + private static interface LeaderGetter { + Competitor getLeader(); + } + + @Override + public Double getGapToLeaderInSeconds(final TimePoint timePoint) throws NoWindException { + return getGapToLeaderInSeconds(timePoint, new LeaderGetter() { + @Override + public Competitor getLeader() { + return getTrackedLeg().getLeader(hasFinishedLeg(timePoint) ? getFinishTime() : timePoint); + } + }); + } + + private Double getGapToLeaderInSeconds(TimePoint timePoint, LeaderGetter leaderGetter) throws NoWindException { + // If the leader already completed this leg, compute the estimated arrival time at the + // end of this leg; if this leg's competitor also already finished the leg, return the + // difference between this competitor's leg completion time point and the leader's completion + // time point; else, calculate the windward distance to the leader and divide by + // the windward speed + Speed windwardSpeed = getWindwardSpeed(getTrackedRace().getTrack(getCompetitor()).getEstimatedSpeed(timePoint), timePoint); + Double result = null; + // Has our competitor started the leg already? If not, we won't be able to compute a gap + if (hasStartedLeg(timePoint)) { + Iterable markPassingsInOrder = getTrackedRace().getMarkPassingsInOrder(getLeg().getTo()); + if (markPassingsInOrder != null) { + MarkPassing firstMarkPassing = null; + getTrackedRace().lockForRead(markPassingsInOrder); + try { + Iterator markPassingsForLegEnd = markPassingsInOrder.iterator(); + if (markPassingsForLegEnd.hasNext()) { + firstMarkPassing = markPassingsForLegEnd.next(); + } + } finally { + getTrackedRace().unlockAfterRead(markPassingsInOrder); + } + if (firstMarkPassing != null) { + // someone has already finished the leg + TimePoint whenLeaderFinishedLeg = firstMarkPassing.getTimePoint(); + // Was it before the requested timePoint? + if (whenLeaderFinishedLeg.compareTo(timePoint) <= 0) { + // Has our competitor also already finished this leg? + if (hasFinishedLeg(timePoint)) { + // Yes, so the gap is the time period between the time points at which the leader and + // our competitor finished this leg. + return (getMarkPassingForLegEnd().getTimePoint().asMillis() - whenLeaderFinishedLeg + .asMillis()) / 1000.; + } else { + if (windwardSpeed == null) { + return null; + } else { + // leader has finished already; our competitor hasn't + Distance windwardDistanceToGo = getWindwardDistanceToGo(timePoint); + long millisSinceLeaderPassedMarkToTimePoint = timePoint.asMillis() + - whenLeaderFinishedLeg.asMillis(); + return windwardDistanceToGo.getMeters() / windwardSpeed.getMetersPerSecond() + + millisSinceLeaderPassedMarkToTimePoint / 1000.; + } + } + } + } + // no-one has finished this leg yet at timePoint + Competitor leader = leaderGetter.getLeader(); + // Maybe our competitor is the leader. Check: + if (leader == getCompetitor()) { + return 0.0; // the leader's gap to the leader + } else { + if (windwardSpeed == null) { + return null; + } else { + // no, we're not the leader, so compute our windward distance and divide by our current VMG + Position ourEstimatedPosition = getTrackedRace().getTrack(getCompetitor()) + .getEstimatedPosition(timePoint, false); + Position leaderEstimatedPosition = getTrackedRace().getTrack(leader).getEstimatedPosition( + timePoint, false); + if (ourEstimatedPosition == null || leaderEstimatedPosition == null) { + return null; + } else { + Distance windwardDistanceToGo = getTrackedLeg().getAbsoluteWindwardDistance(ourEstimatedPosition, + leaderEstimatedPosition, timePoint); + return windwardDistanceToGo.getMeters() / windwardSpeed.getMetersPerSecond(); + } + } + } + } + } + // else our competitor hasn't started the leg yet, so we can't compute a gap since we don't + // have a speed estimate; leave result == null + return result; + } + + @Override + public boolean hasStartedLeg(TimePoint timePoint) { + MarkPassing markPassingForLegStart = getMarkPassingForLegStart(); + return markPassingForLegStart != null && markPassingForLegStart.getTimePoint().compareTo(timePoint) <= 0; + } + + @Override + public boolean hasFinishedLeg(TimePoint timePoint) { + MarkPassing markPassingForLegEnd = getMarkPassingForLegEnd(); + return markPassingForLegEnd != null && markPassingForLegEnd.getTimePoint().compareTo(timePoint) <= 0; + } + + @Override + public TimePoint getStartTime() { + MarkPassing markPassingForLegStart = getMarkPassingForLegStart(); + return markPassingForLegStart == null ? null : markPassingForLegStart.getTimePoint(); + } + + @Override + public TimePoint getFinishTime() { + MarkPassing markPassingForLegEnd = getMarkPassingForLegEnd(); + return markPassingForLegEnd == null ? null : markPassingForLegEnd.getTimePoint(); + } + + @Override + public Speed getVelocityMadeGood(TimePoint at) throws NoWindException { + if (hasStartedLeg(at)) { + TimePoint timePoint; + if (hasFinishedLeg(at)) { + // use the leg finishing time point + timePoint = getMarkPassingForLegEnd().getTimePoint(); + } else { + timePoint = at; + } + SpeedWithBearing speedOverGround = getSpeedOverGround(timePoint); + return speedOverGround == null ? null : getWindwardSpeed(speedOverGround, timePoint); + } else { + return null; + } + } + + @Override + public SpeedWithBearing getSpeedOverGround(TimePoint at) { + if (hasStartedLeg(at)) { + TimePoint timePoint; + if (hasFinishedLeg(at)) { + // use the leg finishing time point + timePoint = getMarkPassingForLegEnd().getTimePoint(); + } else { + timePoint = at; + } + return getTrackedRace().getTrack(getCompetitor()).getEstimatedSpeed(timePoint); + } else { + return null; + } + } + + @Override + public Double getEstimatedTimeToNextMarkInSeconds(TimePoint timePoint) throws NoWindException { + Double result; + if (hasFinishedLeg(timePoint)) { + result = 0.0; + } else { + if (hasStartedLeg(timePoint)) { + Distance windwardDistanceToGo = getWindwardDistanceToGo(timePoint); + Speed vmg = getVelocityMadeGood(timePoint); + result = vmg == null ? null : windwardDistanceToGo.getMeters() / vmg.getMetersPerSecond(); + } else { + result = null; + } + } + return result; + } + + @Override + public Distance getManeuverLoss(TimePoint timePointBeforeManeuver, + TimePoint maneuverTimePoint, TimePoint timePointAfterManeuver) throws NoWindException { + assert timePointBeforeManeuver != null; + assert timePointAfterManeuver != null; + Distance result; + final GPSFixTrack track = getTrackedRace().getTrack(getCompetitor()); + List fixes = getFixesToConsiderForManeuverLossAnalysis(timePointBeforeManeuver, + maneuverTimePoint, timePointAfterManeuver); + TimePoint timePointWhenSpeedStartedToDrop = fixes.get(0).getTimePoint(); + SpeedWithBearing speedWhenSpeedStartedToDrop = track.getEstimatedSpeed(timePointWhenSpeedStartedToDrop); + if (speedWhenSpeedStartedToDrop != null) { + TimePoint timePointWhenSpeedLevelledOffAfterManeuver = fixes.get(fixes.size()-1).getTimePoint(); + SpeedWithBearing speedAfterManeuver = track.getEstimatedSpeed(timePointWhenSpeedLevelledOffAfterManeuver); + if (speedAfterManeuver != null) { + // For upwind/downwind legs, find the mean course between inbound and outbound course and project actual and + // extrapolated positions onto it: + Bearing middleManeuverAngle = speedWhenSpeedStartedToDrop.getBearing().middle(speedAfterManeuver.getBearing()); + // extrapolate maximum speed before maneuver to time point of maximum speed after maneuver and project resulting position + // onto the average maneuver course; compare to the projected position actually reached at the time point of maximum speed after + // maneuver: + Position positionWhenSpeedStartedToDrop = track.getEstimatedPosition(timePointWhenSpeedStartedToDrop, /* extrapolate */ false); + Position extrapolatedPositionAtTimePointOfMaxSpeedAfterManeuver = + speedWhenSpeedStartedToDrop.travelTo(positionWhenSpeedStartedToDrop, timePointWhenSpeedStartedToDrop, timePointWhenSpeedLevelledOffAfterManeuver); + Position actualPositionAtTimePointOfMaxSpeedAfterManeuver = track.getEstimatedPosition(timePointWhenSpeedLevelledOffAfterManeuver, /* extrapolate */ false); + Position projectedExtrapolatedPositionAtTimePointOfMaxSpeedAfterManeuver = + extrapolatedPositionAtTimePointOfMaxSpeedAfterManeuver.projectToLineThrough(positionWhenSpeedStartedToDrop, middleManeuverAngle); + Position projectedActualPositionAtTimePointOfMaxSpeedAfterManeuver = + actualPositionAtTimePointOfMaxSpeedAfterManeuver.projectToLineThrough(positionWhenSpeedStartedToDrop, middleManeuverAngle); + result = projectedActualPositionAtTimePointOfMaxSpeedAfterManeuver.getDistance(projectedExtrapolatedPositionAtTimePointOfMaxSpeedAfterManeuver); + } else { + result = null; + } + } else { + result = null; + } + return result; + } + + /** + * Fetches the set of fixes that encompass the maneuver. Usually, during a maneuver a boat loses speed over ground + * before it has reached the new tack and starts accelerating again, until the speed levels off. We assume that the + * timePointBeforeManeuver minus some excess time based on the approximate maneuver duration is the + * earliest time point to analyze. Starting there, we look for speed over ground minima between then and + * timePointAfterManeuver plus three times the approximate maneuver time as excess time. There may be + * multiple minima. We choose the one that has the best "fit" in terms of being close to the + * maneuverTimePoint and being low in terms of speed over ground. From that time point, the nearest + * maximum speeds over ground before and after are determined, and the fixes between them are returned. + */ + private List getFixesToConsiderForManeuverLossAnalysis(TimePoint timePointBeforeManeuver, + TimePoint maneuverTimePoint, TimePoint timePointAfterManeuver) { + final long EXCESS_TIME_BEFORE_MANEUVER_END_TO_SCAN_IN_MILLIS = getCompetitor().getBoat().getBoatClass().getApproximateManeuverDurationInMilliseconds(); + final long EXCESS_TIME_AFTER_MANEUVER_END_TO_SCAN_IN_MILLIS = 3*EXCESS_TIME_BEFORE_MANEUVER_END_TO_SCAN_IN_MILLIS; + List fixes = new ArrayList<>(); + NavigableSet maxima = new ArrayListNavigableSet(new TimedComparator()); + NavigableSet minima = new ArrayListNavigableSet(new TimedComparator()); + GPSFixTrack track = getTrackedRace().getTrack(getCompetitor()); + Speed lastSpeed = Speed.NULL; + Speed lastLastSpeed = Speed.NULL; + GPSFixMoving lastFix = null; + Speed minimumSpeed = new KnotSpeedImpl(Double.MAX_VALUE); + track.lockForRead(); + try { + Iterator fixIter = track.getFixesIterator( + timePointBeforeManeuver.minus(EXCESS_TIME_BEFORE_MANEUVER_END_TO_SCAN_IN_MILLIS), /* inclusive */true); + GPSFixMoving fix; + // The timePointAfterManeuver is determined based on the geometric shape of the boat's trajectory, not on + // the speed development. To understand the full maneuver loss, we need to follow the boat speed until it levels off, + // but no further than some reasonable threshold because the wind may continue to pick up, letting the boat accelerate + // over a time much longer than accounted for by the maneuver. + while (fixIter.hasNext()) { + fix = fixIter.next(); + final SpeedWithBearing estimatedSpeedAtFix = track.getEstimatedSpeed(fix.getTimePoint()); + if (lastFix != null && lastSpeed != null && lastLastSpeed != null && estimatedSpeedAtFix != null) { + if (lastSpeed.compareTo(lastLastSpeed) > 0 && lastSpeed.compareTo(estimatedSpeedAtFix) > 0) { + maxima.add(lastFix); + } else if (lastSpeed.compareTo(lastLastSpeed) < 0 && lastSpeed.compareTo(estimatedSpeedAtFix) < 0) { + minima.add(lastFix); + if (lastSpeed.compareTo(minimumSpeed) < 0) { + minimumSpeed = lastSpeed; + } + } + } + if (fix.getTimePoint().after(timePointAfterManeuver.plus(EXCESS_TIME_AFTER_MANEUVER_END_TO_SCAN_IN_MILLIS))) { + break; + } + fixes.add(fix); + lastLastSpeed = lastSpeed; + lastSpeed = estimatedSpeedAtFix; + lastFix = fix; + } + } finally { + track.unlockAfterRead(); + } + GPSFixMoving fixWithLowestSpeedOverGround = getBestFittingSpeedMinimumInManeuver(minima, maxima, minimumSpeed, maneuverTimePoint); + if (fixWithLowestSpeedOverGround == null) { + fixWithLowestSpeedOverGround = fixes.get(0); + } + // now remove all fixes before the last maximum before the fix with the lowest speed over ground during the maneuver if + // there was such a maximum; otherwise, leave all fixes from the maneuver start on in place. + final long MAX_SMOOTHENING_INTERVAL_MILLIS = getCompetitor().getBoat().getBoatClass().getApproximateManeuverDurationInMilliseconds(); + GPSFixMoving lastMaxSpeedFixBeforeLowSpeed = maxima.lower(fixWithLowestSpeedOverGround); + // now check if there's a greater one that's only a little bit earlier + if (lastMaxSpeedFixBeforeLowSpeed != null) { + GPSFixMoving stillGreater; + while ((stillGreater=maxima.lower(lastMaxSpeedFixBeforeLowSpeed)) != null && + track.getEstimatedSpeed(stillGreater.getTimePoint()).compareTo( + track.getEstimatedSpeed(lastMaxSpeedFixBeforeLowSpeed.getTimePoint())) > 0 && + lastMaxSpeedFixBeforeLowSpeed.getTimePoint().asMillis()- + stillGreater.getTimePoint().asMillis() < MAX_SMOOTHENING_INTERVAL_MILLIS) { + lastMaxSpeedFixBeforeLowSpeed = stillGreater; + } + Iterator i = fixes.iterator(); + while (i.hasNext() && i.next().getTimePoint().before(lastMaxSpeedFixBeforeLowSpeed.getTimePoint())) { + i.remove(); + } + } + // now remove all fixes after the first maximum after the global minimum: + GPSFixMoving firstMaxSpeedFixAfterLowSpeed = maxima.higher(fixWithLowestSpeedOverGround); + if (firstMaxSpeedFixAfterLowSpeed != null) { + GPSFixMoving stillGreater; + while ((stillGreater=maxima.higher(firstMaxSpeedFixAfterLowSpeed)) != null && + track.getEstimatedSpeed(stillGreater.getTimePoint()).compareTo( + track.getEstimatedSpeed(firstMaxSpeedFixAfterLowSpeed.getTimePoint())) > 0 && + stillGreater.getTimePoint().asMillis() - firstMaxSpeedFixAfterLowSpeed.getTimePoint().asMillis() + < MAX_SMOOTHENING_INTERVAL_MILLIS) { + firstMaxSpeedFixAfterLowSpeed = stillGreater; + } + ListIterator i = fixes.listIterator(fixes.size()); + while (i.hasPrevious() && i.previous().getTimePoint().after(firstMaxSpeedFixAfterLowSpeed.getTimePoint())) { + i.remove(); + } + } + return fixes; + } + + private GPSFixMoving getBestFittingSpeedMinimumInManeuver(NavigableSet minima, + NavigableSet maxima, Speed minimumSpeed, TimePoint maneuverTimePoint) { + // Idea: being 10x the approximate maneuver duration away from maneuverTimePoint is as bad as having twice the percentage between min + // and max speed (min = 0%; max=100%). + GPSFixMoving bestSpeedMinimum = null; + Speed maxSpeed = null; + for (GPSFixMoving maximum : maxima) { + if (maxSpeed == null || maximum.getSpeed().getKnots() > maxSpeed.getKnots()) { + maxSpeed = maximum.getSpeed(); + } + } + if (maxSpeed != null) { + double speedDifferenceBetweenMaxAndMinInKnots = maxSpeed.getKnots() - minimumSpeed.getKnots(); + double lowestBadness = Double.MAX_VALUE; + final long approximateManeuverTimeInMillis = getCompetitor().getBoat().getBoatClass() + .getApproximateManeuverDurationInMilliseconds(); + final GPSFixTrack track = getTrackedRace().getTrack(getCompetitor()); + for (GPSFixMoving minimum : minima) { + // best speedBadness can be 1 which represents the absolute speed minimum in the interval considered + double speedBadness = 1 + (track.getEstimatedSpeed(minimum.getTimePoint()).getKnots() - minimumSpeed.getKnots()) / + speedDifferenceBetweenMaxAndMinInKnots; + // best timePointBadness can be 1 which is a minimum exactly at the maneuver time point + double timePointBadness = 1. + + (double) Math.abs(minimum.getTimePoint().asMillis() - maneuverTimePoint.asMillis()) + / (double) approximateManeuverTimeInMillis / 10.; + final double totalBadness = speedBadness * timePointBadness; + if (totalBadness < lowestBadness) { + bestSpeedMinimum = minimum; + lowestBadness = totalBadness; + } + } + } + return bestSpeedMinimum; + } + + @Override + public Bearing getBeatAngle(TimePoint at) throws NoWindException { + Bearing beatAngle = null; + + Bearing projectToBearing; + if (getTrackedLeg().isUpOrDownwindLeg(at)) { + Wind wind = getWind(getTrackedRace().getTrack(getCompetitor()).getEstimatedPosition(at, false), at); + if (wind == null) { + throw new NoWindException("Need at least wind direction to determine windward speed"); + } + projectToBearing = wind.getBearing(); + } else { + projectToBearing = getTrackedLeg().getLegBearing(at); + } + + SpeedWithBearing speed = getSpeedOverGround(at); + if (speed != null) { + beatAngle = projectToBearing.getDifferenceTo(speed.getBearing()); + } + + return beatAngle; + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java index 7d5d9d6d6c0..1cbddc3bfa6 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java @@ -82,6 +82,8 @@ import com.sap.sailing.domain.confidence.impl.HyperbolicTimeDifferenceWeigher; import com.sap.sailing.domain.confidence.impl.PositionAndTimePointWeigher; import com.sap.sailing.domain.racelog.RaceLog; import com.sap.sailing.domain.racelog.analyzing.impl.StartTimeFinder; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; +import com.sap.sailing.domain.tracking.DynamicGPSFixTrack; import com.sap.sailing.domain.tracking.GPSFix; import com.sap.sailing.domain.tracking.GPSFixMoving; import com.sap.sailing.domain.tracking.GPSFixTrack; @@ -89,6 +91,7 @@ import com.sap.sailing.domain.tracking.GPSTrackListener; import com.sap.sailing.domain.tracking.LineDetails; import com.sap.sailing.domain.tracking.Maneuver; import com.sap.sailing.domain.tracking.MarkPassing; +import com.sap.sailing.domain.tracking.Track; import com.sap.sailing.domain.tracking.TrackedLeg; import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor; import com.sap.sailing.domain.tracking.TrackedRaceStatus; @@ -224,25 +227,38 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl */ private long delayToLiveInMillis; - private enum WindLoadingState { NOT_STARTED, RUNNING, FINISHED }; + private enum LoadingFromStoresState { NOT_STARTED, RUNNING, FINISHED }; + /** - * The constructor loads wind fixes from the {@link #windStore} asynchronously. When completed, this flag is set to - * true, and all threads currently waiting on this object are notified. + * The constructor loads wind fixes from the {@link #windStore} asynchronously. + * When completed all threads currently waiting on this object are notified. */ - private WindLoadingState windLoadingCompleted; + private LoadingFromStoresState loadingFromWindStoreState = LoadingFromStoresState.NOT_STARTED; + + /** + * @see #loadingFromWindStoreState but for GPSFixStore + */ + private LoadingFromStoresState loadingFromGPSFixStoreState = LoadingFromStoresState.NOT_STARTED; private transient CrossTrackErrorCache crossTrackErrorCache; + private final GPSFixStore gpsFixStore; + /** - * Wind loading is started in a background thread during object construction. If a client needs to ensure that wind loading - * either has terminated or has not yet begun, it can obtain the read lock of this lock. The wind loading procedure will obtain - * the write lock before it starts loading wind fixes. + * Wind and loading is started in a background thread during object construction. If a client needs to + * ensure that wind loading either has terminated or has not yet begun, it can obtain the read lock of + * this lock. The wind loading procedure will obtain the write lock before it starts loading wind fixes. */ - private final NamedReentrantReadWriteLock windLoadingLock; + private final NamedReentrantReadWriteLock loadingFromWindStoreLock; + + /** + * @see #loadingFromWindStoreLock but for GPSFixStore + */ + private final NamedReentrantReadWriteLock loadingFromGPSFixStoreLock; private final Map, NamedReentrantReadWriteLock> locksForMarkPassings; - public TrackedRaceImpl(final TrackedRegatta trackedRegatta, RaceDefinition race, final Iterable sidelines, final WindStore windStore, + public TrackedRaceImpl(final TrackedRegatta trackedRegatta, RaceDefinition race, final Iterable sidelines, final WindStore windStore, final GPSFixStore gpsFixStore, long delayToLiveInMillis, final long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed, long delayForWindEstimationCacheInvalidation) { super(race, trackedRegatta, windStore, millisecondsOverWhichToAverageWind); @@ -250,7 +266,9 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl attachedRaceLogs = new HashMap<>(); this.status = new TrackedRaceStatusImpl(TrackedRaceStatusEnum.PREPARED, 0.0); this.statusNotifier = new Object[0]; - this.windLoadingLock = new NamedReentrantReadWriteLock("Wind loading lock for tracked race " + this.loadingFromWindStoreLock = new NamedReentrantReadWriteLock("Loading from wind store lock for tracked race " + + race.getName(), /* fair */ false); + this.loadingFromGPSFixStoreLock = new NamedReentrantReadWriteLock("Loading from GPSFix store lock for tracked race " + race.getName(), /* fair */ false); this.cacheInvalidationTimerLock = new Object(); this.updateCount = 0; @@ -262,6 +280,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl this.maneuverCache = createManeuverCache(); this.markTracks = new ConcurrentHashMap>(); this.crossTrackErrorCache = new CrossTrackErrorCache(this); + this.gpsFixStore = gpsFixStore; int i = 0; for (Waypoint waypoint : race.getCourse().getWaypoints()) { for (Mark mark : waypoint.getMarks()) { @@ -306,15 +325,15 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl MarkPassingByTimeComparator.INSTANCE)); } markPassingsTimes = new ArrayList>>(); - windLoadingCompleted = WindLoadingState.NOT_STARTED; - // When this tracked race is to be serialized, wait for the loading of the wind tracks to complete. - new Thread("Wind loader for tracked race " + getRace().getName()) { + loadingFromWindStoreState = LoadingFromStoresState.NOT_STARTED; + // When this tracked race is to be serialized, wait for the loading from stores to complete. + new Thread("Mongo wind loader for tracked race " + getRace().getName()) { @Override public void run() { LockUtil.lockForRead(getSerializationLock()); - LockUtil.lockForWrite(getWindLoadingLock()); + LockUtil.lockForWrite(getLoadingFromWindStoreLock()); synchronized (TrackedRaceImpl.this) { - windLoadingCompleted = WindLoadingState.RUNNING; // indicates that the serialization lock is now safely held + loadingFromWindStoreState = LoadingFromStoresState.RUNNING; // indicates that the serialization lock is now safely held TrackedRaceImpl.this.notifyAll(); } try { @@ -322,14 +341,18 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl final Map loadedWindTracks = windStore.loadWindTracks( trackedRegatta.getRegatta().getName(), TrackedRaceImpl.this, millisecondsOverWhichToAverageWind); windTracks.putAll(loadedWindTracks); - updateEventTimePoints(loadedWindTracks); + updateEventTimePoints(loadedWindTracks.values()); logger.info("Finished loading wind tracks for " + getRace().getName() + "! Found " + windTracks.size() + " wind tracks for this race!"); + } finally { synchronized (TrackedRaceImpl.this) { - windLoadingCompleted = WindLoadingState.FINISHED; + loadingFromWindStoreState = LoadingFromStoresState.FINISHED; TrackedRaceImpl.this.notifyAll(); } - LockUtil.unlockAfterWrite(getWindLoadingLock()); + synchronized (loadingFromWindStoreState) { + loadingFromWindStoreState.notifyAll(); + } + LockUtil.unlockAfterWrite(getLoadingFromWindStoreLock()); LockUtil.unlockAfterRead(getSerializationLock()); } } @@ -346,31 +369,28 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl competitorRankings = new HashMap>(); competitorRankingsLocks = new HashMap(); // now wait until wind loading has at least started; then we know that the serialization lock is safely held by the loader - synchronized (this) { - while (windLoadingCompleted != WindLoadingState.FINISHED) { - try { - this.wait(); - } catch (InterruptedException e) { - logger.log(Level.SEVERE, "Waiting for wind loading to start was interrupted", e); - } - } + try { + waitUntilLoadingFromWindStoreComplete(); + } catch (InterruptedException e) { + logger.log(Level.SEVERE, "Waiting for loading from stores to finish was interrupted", e); } } /** - * Assuming that the wind tracks loadedWindTracks were loaded from the persistent store, this method updates + * Assuming that the tracks were loaded from the persistent store, this method updates * the time stamps that frame the data held by this tracked race. See {@link #timePointOfLastEvent}, {@link #timePointOfNewestEvent} * and {@link #timePointOfOldestEvent}. */ - private void updateEventTimePoints(Map loadedWindTracks) { - for (WindTrack windTrack : loadedWindTracks.values()) { - windTrack.lockForRead(); + private void updateEventTimePoints(Iterable> tracks) { + for (Track track : tracks) { + track.lockForRead(); + try { - for (Wind wind : windTrack.getRawFixes()) { - updated(wind.getTimePoint()); + for (Timed fix : track.getRawFixes()) { + updated(fix.getTimePoint()); } } finally { - windTrack.unlockAfterRead(); + track.unlockAfterRead(); } } } @@ -407,8 +427,15 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl } @Override - public synchronized void waitUntilWindLoadingComplete() throws InterruptedException { - while (windLoadingCompleted != WindLoadingState.FINISHED) { + public synchronized void waitUntilLoadingFromWindStoreComplete() throws InterruptedException { + while (loadingFromWindStoreState != LoadingFromStoresState.FINISHED) { + wait(); + } + } + + @Override + public synchronized void waitUntilLoadingFromGPSFixStoreComplete() throws InterruptedException { + while (loadingFromGPSFixStoreState != LoadingFromStoresState.FINISHED) { wait(); } } @@ -765,23 +792,35 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl @Override public Distance getDistanceTraveled(Competitor competitor, TimePoint timePoint) { + final Distance result; NavigableSet markPassings = getMarkPassings(competitor); - if (markPassings.isEmpty()) { - return null; - } else { - TimePoint end = timePoint; - if (markPassings.last().getWaypoint() == getRace().getCourse().getLastWaypoint() - && timePoint.compareTo(markPassings.last().getTimePoint()) > 0) { - // competitor has finished race; use time point of crossing the finish line - end = markPassings.last().getTimePoint(); - } else if (markPassings.last().getWaypoint() != getRace().getCourse().getLastWaypoint() && - timePoint.after(markPassings.last().getTimePoint())) { - // if competitor has not finished the race (and the requested timepoint - // is after the last mark passing) then we can not tell anything about - // the distance traveled - this is in line with AbstractSimpleLeaderboardImpl#getTotalTimeSailedInMilliseconds - return null; + try { + lockForRead(markPassings); + if (markPassings.isEmpty()) { + result = null; + } else { + TimePoint end = timePoint; + if (markPassings.last().getWaypoint() == getRace().getCourse().getLastWaypoint() + && timePoint.compareTo(markPassings.last().getTimePoint()) > 0) { + // competitor has finished race; use time point of crossing the finish line + end = markPassings.last().getTimePoint(); + } else if (markPassings.last().getWaypoint() != getRace().getCourse().getLastWaypoint() + && ((getEndOfTracking() != null && timePoint.after(getEndOfTracking())) + || getStatus().getStatus() == TrackedRaceStatusEnum.FINISHED)) { + // If the race is no longer tracking and hence no more data can be expected, and the competitor + // hasn't finished the race, no valid distance traveled can be determined + // for the competitor in this race. + end = null; + } + if (end == null) { + result = null; + } else { + result = getTrack(competitor).getDistanceTraveled(markPassings.first().getTimePoint(), end); + } } - return getTrack(competitor).getDistanceTraveled(markPassings.first().getTimePoint(), end); + return result; + } finally { + unlockAfterRead(markPassings); } } @@ -989,7 +1028,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl } @Override - public Distance getAverageCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) + public Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) throws NoWindException { NavigableSet markPassings = getMarkPassings(competitor); TimePoint from = null; @@ -1003,7 +1042,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl } Distance result; if (from != null) { - result = getAverageCrossTrackError(competitor, from, timePoint, /* upwindOnly */true, waitForLatestAnalysis); + result = getAverageAbsoluteCrossTrackError(competitor, from, timePoint, /* upwindOnly */true, waitForLatestAnalysis); } else { result = null; } @@ -1011,11 +1050,42 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl } @Override - public Distance getAverageCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, + public Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) + throws NoWindException { + NavigableSet markPassings = getMarkPassings(competitor); + TimePoint from = null; + lockForRead(markPassings); + try { + if (markPassings != null && !markPassings.isEmpty()) { + from = markPassings.iterator().next().getTimePoint(); + } + } finally { + unlockAfterRead(markPassings); + } + Distance result; + if (from != null) { + result = getAverageSignedCrossTrackError(competitor, from, timePoint, /* upwindOnly */true, waitForLatestAnalysis); + } else { + result = null; + } + return result; + } + + @Override + public Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, boolean waitForLatestAnalysis) throws NoWindException { Distance result; result = crossTrackErrorCache - .getAverageCrossTrackError(competitor, from, to, upwindOnly, waitForLatestAnalysis); + .getAverageAbsoluteCrossTrackError(competitor, from, to, upwindOnly, waitForLatestAnalysis); + return result; + } + + @Override + public Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, + boolean waitForLatestAnalysis) throws NoWindException { + Distance result; + result = crossTrackErrorCache + .getAverageSignedCrossTrackError(competitor, from, to, upwindOnly, waitForLatestAnalysis); return result; } @@ -1216,8 +1286,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl // TODO consider parallelizing and consider caching if (!Util.contains(windSourcesToExclude, windSource)) { WindTrack track = getOrCreateWindTrack(windSource); - WindWithConfidence> windWithConfidence = track.getAveragedWindWithConfidence( - p, at); + WindWithConfidence> windWithConfidence = track.getAveragedWindWithConfidence(p, at); if (windWithConfidence != null) { windFixesWithConfidences.add(windWithConfidence); canUseSpeedOfAtLeastOneWindSource = canUseSpeedOfAtLeastOneWindSource @@ -2323,9 +2392,50 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl } @Override - public void attachRaceLog(RaceLog raceLog) { + public void attachRaceLog(final RaceLog raceLog) { if (raceLog != null) { this.attachedRaceLogs.put(raceLog.getId(), raceLog); + // Use the new race log, that possibly contains device mappings, to load GPSFix tracks from the DB + loadingFromGPSFixStoreState = LoadingFromStoresState.NOT_STARTED; + // When this tracked race is to be serialized, wait for the loading from stores to complete. + new Thread("Mongo mark and competitor track loader for tracked race " + getRace().getName()) { + @Override + public void run() { + LockUtil.lockForRead(getSerializationLock()); + LockUtil.lockForWrite(getLoadingFromGPSFixStoreLock()); + synchronized (TrackedRaceImpl.this) { + loadingFromGPSFixStoreState = LoadingFromStoresState.RUNNING; // indicates that the serialization + // lock is now safely held + TrackedRaceImpl.this.notifyAll(); + } + try { + logger.info("Started loading competitor tracks for " + getRace().getName()); + for (Competitor competitor : race.getCompetitors()) { + gpsFixStore.loadCompetitorTrack( + (DynamicGPSFixTrack) tracks.get(competitor), raceLog, + competitor); + } + logger.info("Finished loading competitor tracks for " + getRace().getName()); + logger.info("Started loading mark tracks for " + getRace().getName()); + for (Mark mark : getMarks()) { + gpsFixStore.loadMarkTrack((DynamicGPSFixTrack) markTracks.get(mark), raceLog, + mark); + } + logger.info("Finished loading mark tracks for " + getRace().getName()); + + } finally { + synchronized (TrackedRaceImpl.this) { + loadingFromGPSFixStoreState = LoadingFromStoresState.FINISHED; + TrackedRaceImpl.this.notifyAll(); + } + synchronized (loadingFromGPSFixStoreState) { + loadingFromGPSFixStoreState.notifyAll(); + } + LockUtil.unlockAfterWrite(getLoadingFromGPSFixStoreLock()); + LockUtil.unlockAfterRead(getSerializationLock()); + } + } + }.start(); } else { logger.severe("Got a request to attach race log for an empty race log!"); } @@ -2484,8 +2594,12 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl return null; } - protected NamedReentrantReadWriteLock getWindLoadingLock() { - return windLoadingLock; + protected NamedReentrantReadWriteLock getLoadingFromWindStoreLock() { + return loadingFromWindStoreLock; + } + + protected NamedReentrantReadWriteLock getLoadingFromGPSFixStoreLock() { + return loadingFromGPSFixStoreLock; } /** @@ -2680,4 +2794,9 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl } return competitorSpeedWhenPassingStart; } + + @Override + public GPSFixStore getGPSFixStore() { + return gpsFixStore; + } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRegattaImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRegattaImpl.java index c127fb84ebd..37d2c02afbc 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRegattaImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRegattaImpl.java @@ -19,6 +19,7 @@ import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.base.Sideline; import com.sap.sailing.domain.common.NoWindException; import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; import com.sap.sailing.domain.tracking.DynamicRaceDefinitionSet; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.RaceListener; @@ -168,12 +169,12 @@ public class TrackedRegattaImpl implements TrackedRegatta { } @Override - public DynamicTrackedRace createTrackedRace(RaceDefinition raceDefinition, Iterable sidelines, WindStore windStore, long delayToLiveInMillis, + public DynamicTrackedRace createTrackedRace(RaceDefinition raceDefinition, Iterable sidelines, WindStore windStore, GPSFixStore gpsFixStore, long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed, DynamicRaceDefinitionSet raceDefinitionSetToUpdate) { logger.log(Level.INFO, "Creating DynamicTrackedRaceImpl for RaceDefinition " + raceDefinition.getName()); DynamicTrackedRaceImpl result = new DynamicTrackedRaceImpl(this, raceDefinition, sidelines, - windStore, delayToLiveInMillis, millisecondsOverWhichToAverageWind, millisecondsOverWhichToAverageSpeed); + windStore, gpsFixStore, delayToLiveInMillis, millisecondsOverWhichToAverageWind, millisecondsOverWhichToAverageSpeed); // adding the raceDefinition to the raceDefinitionSetToUpdate BEFORE calling addTrackedRace helps those who // are called back by RaceListener.raceAdded(TrackedRace) and who then expect the update to have happened if (raceDefinitionSetToUpdate != null) { diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/VirtualWindFixesAsNavigableSet.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/VirtualWindFixesAsNavigableSet.java index 70cc4c93634..fb385c29c87 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/VirtualWindFixesAsNavigableSet.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/VirtualWindFixesAsNavigableSet.java @@ -47,9 +47,10 @@ public abstract class VirtualWindFixesAsNavigableSet extends AbstractUnmodifiabl this(track, trackedRace, null, null, resolutionInMilliseconds); } - public long getResolutionInMilliseconds() { - return resolutionInMilliseconds; - } + /** + * Compute the {@link Wind} fix for the specified position and time point to deliver in this virtual track. + */ + abstract protected Wind getWind(Position p, TimePoint timePoint); /** * @param from expected to be an integer multiple of {@link #resolutionInMilliseconds} or null @@ -66,6 +67,10 @@ public abstract class VirtualWindFixesAsNavigableSet extends AbstractUnmodifiabl this.resolutionInMilliseconds = resolutionInMilliseconds; } + public long getResolutionInMilliseconds() { + return resolutionInMilliseconds; + } + protected WindTrack getTrack() { return track; } @@ -153,11 +158,6 @@ public abstract class VirtualWindFixesAsNavigableSet extends AbstractUnmodifiabl return timePoint.compareTo(getFrom()) < 0 ? null : getWind(w.getPosition(), timePoint); } - /** - * Compute the {@link Wind} fix for the specified position and time point to deliver in this virtual track. - */ - abstract protected Wind getWind(Position p, TimePoint timePoint); - @Override public Wind floor(Wind w) { TimePoint timePoint = floorToResolution(w.getTimePoint()); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/VirtualWindTrackImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/VirtualWindTrackImpl.java index f4c6dbd2dd2..2d606a27eec 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/VirtualWindTrackImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/VirtualWindTrackImpl.java @@ -42,10 +42,7 @@ public abstract class VirtualWindTrackImpl extends WindTrackImpl { } /** - * This redefinition avoids very long searches in case at is before the race start or after the race's - * newest event. Should at be out of this range, it is set to the closest border of this range before - * calling the base class's implementation. If either race start or time of newest event are not known, the known - * time point is used instead. If both time points are not known, null is returned immediately. + * Delegates to {@link #getAveragedWindUnsynchronized(Position, TimePoint)} */ @Override public Wind getAveragedWind(Position p, TimePoint at) { @@ -53,6 +50,12 @@ public abstract class VirtualWindTrackImpl extends WindTrackImpl { return windWithConfidence == null ? null : windWithConfidence.getObject(); } + /** + * This redefinition avoids very long searches in case at is before the race start or after the race's + * newest event. Should at be out of this range, it is set to the closest border of this range before + * calling the base class's implementation. If either race start or time of newest event are not known, the known + * time point is used instead. If both time points are not known, null is returned immediately. + */ @Override public WindWithConfidence> getAveragedWindWithConfidence(Position p, TimePoint at) { WindWithConfidence> result = null; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/WindTrackImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/WindTrackImpl.java index 5aa0d2c8379..5d7a0621cbd 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/WindTrackImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/WindTrackImpl.java @@ -165,6 +165,12 @@ public class WindTrackImpl extends TrackImpl implements WindTrack { /** * This method implements the functionality of the {@link #getAveragedWind(Position, TimePoint)} interface method. + * It does so by collecting (smoothened, outliers removed)) wind fixes around the at time point up to + * an interval length as specified by {@link #getMillisecondsOverWhichToAverageWind()}. At least one fix that is + * closest to at will be picked up. If the track is empty, null is returned. Otherwise, + * the wind fixes in the interval constructed are averaged using a {@link ConfidenceBasedWindAverager}. + * + *

* However, not being synchronized, it does not obtain this object's monitor. Subclasses may use this * carefully if they can guarantee there are no concurrency issues with the internal fixes while iterating over the * result of {@link #getInternalFixes()}. @@ -238,8 +244,7 @@ public class WindTrackImpl extends TrackImpl implements WindTrack { if (windFixesToAverage.isEmpty()) { return null; } else { - WindWithConfidence> average = windAverager.getAverage(windFixesToAverage, - relativeTo); + WindWithConfidence> average = windAverager.getAverage(windFixesToAverage, relativeTo); return average; } } finally { diff --git a/java/com.sap.sailing.feature.p2build/raceanalysis.product b/java/com.sap.sailing.feature.p2build/raceanalysis.product index c2f37c5c3a5..11f3dc83b1d 100755 --- a/java/com.sap.sailing.feature.p2build/raceanalysis.product +++ b/java/com.sap.sailing.feature.p2build/raceanalysis.product @@ -1,7 +1,7 @@ - + @@ -33,6 +33,7 @@ + diff --git a/java/com.sap.sailing.feature/feature.xml b/java/com.sap.sailing.feature/feature.xml index d596429a6f6..d6acea67f41 100755 --- a/java/com.sap.sailing.feature/feature.xml +++ b/java/com.sap.sailing.feature/feature.xml @@ -290,4 +290,11 @@ Computes leaderboard information for sailing races and offers RESTful APIs to al version="0.0.0" unpack="false"/> + + diff --git a/java/com.sap.sailing.gwt.ui.test/.classpath b/java/com.sap.sailing.gwt.ui.test/.classpath index 2d7fd14bc00..5cc60e2c543 100755 --- a/java/com.sap.sailing.gwt.ui.test/.classpath +++ b/java/com.sap.sailing.gwt.ui.test/.classpath @@ -1,10 +1,10 @@ - + diff --git a/java/com.sap.sailing.gwt.ui.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.gwt.ui.test/META-INF/MANIFEST.MF index cc2929cfa54..bad1c3b6fe0 100755 --- a/java/com.sap.sailing.gwt.ui.test/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.gwt.ui.test/META-INF/MANIFEST.MF @@ -4,15 +4,18 @@ Bundle-Name: Test Bundle-SymbolicName: com.sap.sailing.gwt.ui.test Bundle-Version: 1.0.0.qualifier Bundle-Vendor: SAP -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 +Bundle-RequiredExecutionEnvironment: JavaSE-1.7 Fragment-Host: com.sap.sailing.gwt.ui Require-Bundle: com.sap.sailing.server, - org.hamcrest.core;bundle-version="1.1.0", com.sap.sailing.expeditionconnector, org.mockito.mockito-core;bundle-version="1.9.5", - org.objenesis;bundle-version="1.3.0" + org.objenesis;bundle-version="1.3.0", + org.hamcrest;bundle-version="1.1.0", + com.sap.sailing.domain.igtimiadapter.persistence;bundle-version="1.0.0", + com.sap.sailing.domain.racelogtrackingadapter.testsupport;bundle-version="1.0.0" Bundle-ClassPath: . -Import-Package: javax.jms;version="1.1.0", +Import-Package: com.sap.sailing.domain.racelogtracking.impl, + javax.jms;version="1.1.0", javax.servlet;version="2.6.0", javax.servlet.http;version="2.6.0", junit.framework;version="4.8.1", diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/GwtTestCaseColumnToggling.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/GwtTestCaseColumnToggling.java index 697c347bc3e..1009a7a4ff2 100644 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/GwtTestCaseColumnToggling.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/GwtTestCaseColumnToggling.java @@ -178,7 +178,7 @@ public class GwtTestCaseColumnToggling extends GWTTestCase { private void getLeaderboardByNameAndDeleteColumn(){ ArrayList al = new ArrayList(); al.add(COLUMN1_NAME); - service.getLeaderboardByName(LEADERBOARD_NAME, new Date(), al, /* previousLeaderboardId */ null, new AsyncCallback() { + service.getLeaderboardByName(LEADERBOARD_NAME, new Date(), al, /* addOverallDetails */ false, /* previousLeaderboardId */ null, new AsyncCallback() { @Override public void onFailure(Throwable caught) { fail("Failed to get leaderboard."); diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/MockedTrackedRace.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/MockedTrackedRace.java index 4e55a75dbd4..b9e9afdc60f 100755 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/MockedTrackedRace.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/MockedTrackedRace.java @@ -34,6 +34,7 @@ import com.sap.sailing.domain.common.WindSourceType; import com.sap.sailing.domain.common.impl.Util.Pair; import com.sap.sailing.domain.leaderboard.ScoringScheme; import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; import com.sap.sailing.domain.tracking.CourseDesignChangedListener; import com.sap.sailing.domain.tracking.DynamicGPSFixTrack; import com.sap.sailing.domain.tracking.DynamicRaceDefinitionSet; @@ -220,7 +221,8 @@ public class MockedTrackedRace implements DynamicTrackedRace { } @Override - public void addListener(RaceChangeListener listener, boolean notifyAboutWindFixesAlreadyLoaded) { + public void addListener(RaceChangeListener listener, boolean notifyAboutWindFixesAlreadyLoaded, + boolean notifyAboutGPSFixesAlreadyLoaded) { // TODO Auto-generated method stub } @@ -518,7 +520,7 @@ public class MockedTrackedRace implements DynamicTrackedRace { @Override public DynamicTrackedRace createTrackedRace(RaceDefinition raceDefinition, Iterable sidelines, WindStore windStore, - long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed, + GPSFixStore gpsFixStore, long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed, DynamicRaceDefinitionSet raceDefinitionSetToUpdate) { // TODO Auto-generated method stub return null; @@ -672,7 +674,7 @@ public class MockedTrackedRace implements DynamicTrackedRace { } @Override - public Distance getAverageCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) throws NoWindException { + public Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint timePoint, boolean waitForLatestAnalysis) throws NoWindException { // TODO Auto-generated method stub return null; } @@ -737,14 +739,14 @@ public class MockedTrackedRace implements DynamicTrackedRace { } @Override - public Distance getAverageCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, boolean waitForLatestAnalyses) + public Distance getAverageAbsoluteCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, boolean upwindOnly, boolean waitForLatestAnalyses) throws NoWindException { // TODO Auto-generated method stub return null; } @Override - public void waitUntilWindLoadingComplete() throws InterruptedException { + public void waitUntilLoadingFromWindStoreComplete() throws InterruptedException { // TODO Auto-generated method stub } @@ -931,6 +933,12 @@ public class MockedTrackedRace implements DynamicTrackedRace { return null; } + @Override + public GPSFixStore getGPSFixStore() { + // TODO Auto-generated method stub + return null; + } + @Override public Bearing getCrossingBearing(Waypoint w, TimePoint t) { // TODO Auto-generated method stub @@ -943,4 +951,24 @@ public class MockedTrackedRace implements DynamicTrackedRace { return null; } + @Override + public Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint timePoint, + boolean waitForLatestAnalysis) throws NoWindException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Distance getAverageSignedCrossTrackError(Competitor competitor, TimePoint from, TimePoint to, + boolean upwindOnly, boolean waitForLatestAnalysis) throws NoWindException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void waitUntilLoadingFromGPSFixStoreComplete() throws InterruptedException { + // TODO Auto-generated method stub + + } + } diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/PingMarkViaRaceLogTest.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/PingMarkViaRaceLogTest.java new file mode 100644 index 00000000000..0cd398f06cf --- /dev/null +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/PingMarkViaRaceLogTest.java @@ -0,0 +1,46 @@ +package com.sap.sailing.gwt.ui.test; + +import java.util.Collections; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.base.impl.SeriesImpl; +import com.sap.sailing.domain.common.dto.PositionDTO; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.leaderboard.impl.HighPoint; +import com.sap.sailing.gwt.ui.shared.MarkDTO; +import com.sap.sailing.server.RacingEventService; + +public class PingMarkViaRaceLogTest { + private SailingServiceImplMock sailingService; + private RacingEventService service; + private final String columnName = "column"; + private final Fleet fleet = new FleetImpl("fleet"); + + @Before + public void prepare() { + sailingService = new SailingServiceImplMock(); + service = sailingService.getRacingEventService(); + } + + @Test + public void testPinging() { + service.getMongoObjectFactory().getDatabase().dropDatabase(); + Series series = new SeriesImpl("series", false, Collections.singletonList(fleet), + Collections.singletonList(columnName), service); + Regatta regatta = service.createRegatta("regatta", "Laser", UUID.randomUUID(), Collections.singletonList(series), + false, new HighPoint(), UUID.randomUUID()); + RegattaLeaderboard leaderboard = service.addRegattaLeaderboard(regatta.getRegattaIdentifier(), "RegattaLeaderboard", new int[] {}); + + MarkDTO mark = new MarkDTO("mark", "mark"); + PositionDTO position = new PositionDTO(30, 40); + + sailingService.pingMarkViaRaceLogTracking(leaderboard.getName(), columnName, fleet.getName(), mark, position); + } +} diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SailingServiceImplMock.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SailingServiceImplMock.java index 5f66b11461a..6859c85452d 100755 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SailingServiceImplMock.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SailingServiceImplMock.java @@ -1,158 +1,192 @@ -package com.sap.sailing.gwt.ui.test; - -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.net.UnknownHostException; -import java.text.ParseException; -import java.util.List; - -import org.osgi.framework.BundleContext; -import org.osgi.util.tracker.ServiceTracker; - -import com.sap.sailing.domain.base.BoatClass; -import com.sap.sailing.domain.base.DomainFactory; -import com.sap.sailing.domain.common.RegattaIdentifier; -import com.sap.sailing.domain.common.ScoreCorrectionProvider; -import com.sap.sailing.domain.racelog.RaceLogStore; -import com.sap.sailing.domain.swisstimingadapter.SwissTimingAdapter; -import com.sap.sailing.domain.swisstimingadapter.SwissTimingAdapterFactory; -import com.sap.sailing.domain.swisstimingadapter.SwissTimingFactory; -import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayService; -import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayServiceFactory; -import com.sap.sailing.domain.tracking.RacesHandle; -import com.sap.sailing.domain.tracking.TrackerManager; -import com.sap.sailing.domain.tractracadapter.TracTracAdapterFactory; -import com.sap.sailing.gwt.ui.server.SailingServiceImpl; -import com.sap.sailing.server.RacingEventService; -import com.sap.sailing.server.impl.RacingEventServiceImpl; -import com.sap.sailing.server.replication.ReplicationService; -import com.sap.sailing.server.replication.impl.ReplicationInstancesManager; -import com.sap.sailing.server.replication.impl.ReplicationServiceImpl; - -public class SailingServiceImplMock extends SailingServiceImpl { - private static final long serialVersionUID = 8564037671550730455L; - private RacingEventService service; - - public SailingServiceImplMock() { - super(); - service = new RacingEventServiceImpl(); - } - - @Override - protected ServiceTracker createAndOpenRacingEventServiceTracker( - BundleContext context) { - @SuppressWarnings("unchecked") - ServiceTracker result = mock(ServiceTracker.class); - when(result.getService()).thenReturn(new RacingEventServiceImpl()); - return result; - } - - @Override - protected ServiceTracker createAndOpenReplicationServiceTracker( - BundleContext context) { - @SuppressWarnings("unchecked") - ServiceTracker result = mock(ServiceTracker.class); - try { - final ReplicationServiceImpl replicationService = new ReplicationServiceImpl("test exchange", "localhost", new ReplicationInstancesManager()) { - @Override - protected ServiceTracker getRacingEventServiceTracker() { - @SuppressWarnings("unchecked") - ServiceTracker result = (ServiceTracker) mock(ServiceTracker.class); - doReturn("Humba Humba").when(result).toString(); - when(result.getService()).thenReturn(new RacingEventServiceImpl()); - return result; - } - }; - when(result.getService()).thenReturn(replicationService); - } catch (IOException e) { - throw new RuntimeException(); - } - return result; - } - - @Override - protected ServiceTracker createAndOpenScoreCorrectionProviderServiceTracker( - BundleContext bundleContext) { - return null; - } - - @Override - protected ServiceTracker createAndOpenTracTracAdapterTracker( - BundleContext context) { - @SuppressWarnings("unchecked") - ServiceTracker result = mock(ServiceTracker.class); - when(result.getService()).thenReturn(TracTracAdapterFactory.INSTANCE); - return result; - } - - @Override - protected ServiceTracker createAndOpenSwissTimingAdapterTracker( - BundleContext context) { - @SuppressWarnings("unchecked") - ServiceTracker result = mock(ServiceTracker.class); - when(result.getService()).thenReturn(new SwissTimingAdapterFactory() { - @Override - public SwissTimingAdapter getOrCreateSwissTimingAdapter(DomainFactory baseDomainFactory) { - return new SwissTimingAdapter() { - @Override - public List getSwissTimingRaceRecords( - String hostname, int port) throws InterruptedException, - UnknownHostException, IOException, ParseException { - // TODO Auto-generated method stub - return null; - } - - @Override - public RacesHandle addSwissTimingRace(TrackerManager trackerManager, - RegattaIdentifier regattaToAddTo, String raceID, String raceDescription, BoatClass boatClass, String hostname, - int port, RaceLogStore logStore, long timeoutInMilliseconds) - throws InterruptedException, UnknownHostException, IOException, ParseException, - Exception { - // TODO Auto-generated method stub - return null; - } - - @Override - public SwissTimingFactory getSwissTimingFactory() { - // TODO Auto-generated method stub - return null; - } - - @Override - public com.sap.sailing.domain.swisstimingadapter.DomainFactory getSwissTimingDomainFactory() { - return com.sap.sailing.domain.swisstimingadapter.DomainFactory.INSTANCE; - } - }; - } - }); - return result; - } - - @Override - protected ServiceTracker createAndOpenSwissTimingReplayServiceTracker( - BundleContext context) { - @SuppressWarnings("unchecked") - ServiceTracker result = mock(ServiceTracker.class); - final SwissTimingReplayServiceFactory swissTimingReplayService = new SwissTimingReplayServiceFactory() { - @Override - public SwissTimingReplayService createSwissTimingReplayService( - com.sap.sailing.domain.swisstimingadapter.DomainFactory domainFactory) { - // TODO Auto-generated method stub - return null; - } - }; - when(result.getService()).thenReturn(swissTimingReplayService); - return result; - } - - @Override - protected RacingEventService getService() { - if (service == null) { - service = new RacingEventServiceImpl(); - } - return service; - } -} +package com.sap.sailing.gwt.ui.test; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.text.ParseException; +import java.util.List; + +import javax.xml.bind.JAXBException; + +import org.osgi.framework.BundleContext; +import org.osgi.util.tracker.ServiceTracker; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.common.RegattaIdentifier; +import com.sap.sailing.domain.common.ScoreCorrectionProvider; +import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.racelog.tracking.test.mock.MockSmartphoneImeiServiceFinderFactory; +import com.sap.sailing.domain.swisstimingadapter.StartList; +import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapterFactory; +import com.sap.sailing.domain.racelogtracking.impl.RaceLogTrackingAdapterFactoryImpl; +import com.sap.sailing.domain.swisstimingadapter.SwissTimingAdapter; +import com.sap.sailing.domain.swisstimingadapter.SwissTimingAdapterFactory; +import com.sap.sailing.domain.swisstimingadapter.SwissTimingFactory; +import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayService; +import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayServiceFactory; +import com.sap.sailing.domain.tracking.RacesHandle; +import com.sap.sailing.domain.tracking.TrackerManager; +import com.sap.sailing.domain.tractracadapter.TracTracAdapterFactory; +import com.sap.sailing.gwt.ui.server.SailingServiceImpl; +import com.sap.sailing.server.RacingEventService; +import com.sap.sailing.server.impl.RacingEventServiceImpl; +import com.sap.sailing.server.replication.ReplicationService; +import com.sap.sailing.server.replication.impl.ReplicationInstancesManager; +import com.sap.sailing.server.replication.impl.ReplicationServiceImpl; +import com.sap.sailing.xrr.resultimport.schema.RegattaResults; + +public class SailingServiceImplMock extends SailingServiceImpl { + private static final long serialVersionUID = 8564037671550730455L; + private RacingEventService service; + + public SailingServiceImplMock() { + super(); + service = new RacingEventServiceImpl(true, new MockSmartphoneImeiServiceFinderFactory()); + } + + @Override + protected ServiceTracker createAndOpenRacingEventServiceTracker( + BundleContext context) { + @SuppressWarnings("unchecked") + ServiceTracker result = mock(ServiceTracker.class); + when(result.getService()).thenReturn(new RacingEventServiceImpl()); + return result; + } + + @Override + protected ServiceTracker createAndOpenReplicationServiceTracker( + BundleContext context) { + @SuppressWarnings("unchecked") + ServiceTracker result = mock(ServiceTracker.class); + try { + final ReplicationServiceImpl replicationService = new ReplicationServiceImpl("test exchange", "localhost", new ReplicationInstancesManager()) { + @Override + protected ServiceTracker getRacingEventServiceTracker() { + @SuppressWarnings("unchecked") + ServiceTracker result = (ServiceTracker) mock(ServiceTracker.class); + doReturn("Humba Humba").when(result).toString(); + when(result.getService()).thenReturn(new RacingEventServiceImpl()); + return result; + } + }; + when(result.getService()).thenReturn(replicationService); + } catch (IOException e) { + throw new RuntimeException(); + } + return result; + } + + @Override + protected ServiceTracker createAndOpenScoreCorrectionProviderServiceTracker( + BundleContext bundleContext) { + return null; + } + + @Override + protected ServiceTracker createAndOpenTracTracAdapterTracker( + BundleContext context) { + @SuppressWarnings("unchecked") + ServiceTracker result = mock(ServiceTracker.class); + when(result.getService()).thenReturn(TracTracAdapterFactory.INSTANCE); + return result; + } + + @Override + protected ServiceTracker createAndOpenSwissTimingAdapterTracker( + BundleContext context) { + @SuppressWarnings("unchecked") + ServiceTracker result = mock(ServiceTracker.class); + when(result.getService()).thenReturn(new SwissTimingAdapterFactory() { + @Override + public SwissTimingAdapter getOrCreateSwissTimingAdapter(DomainFactory baseDomainFactory) { + return new SwissTimingAdapter() { + @Override + public List getSwissTimingRaceRecords( + String hostname, int port) throws InterruptedException, + UnknownHostException, IOException, ParseException { + // TODO Auto-generated method stub + return null; + } + + @Override + public RacesHandle addSwissTimingRace(TrackerManager trackerManager, + RegattaIdentifier regattaToAddTo, String raceID, String raceName, String raceDescription, BoatClass boatClass, String hostname, + int port, StartList startList, RaceLogStore logStore, long timeoutInMilliseconds) + throws InterruptedException, UnknownHostException, IOException, ParseException, + Exception { + // TODO Auto-generated method stub + return null; + } + + @Override + public SwissTimingFactory getSwissTimingFactory() { + // TODO Auto-generated method stub + return null; + } + + @Override + public com.sap.sailing.domain.swisstimingadapter.DomainFactory getSwissTimingDomainFactory() { + return com.sap.sailing.domain.swisstimingadapter.DomainFactory.INSTANCE; + } + + @Override + public StartList readStartListForRace(String raceId, RegattaResults regattaResults) { + // TODO Auto-generated method stub + return null; + } + + @Override + public RegattaResults readRegattaEntryListFromXrrUrl(String xrrEntryListUrl) throws IOException, + JAXBException { + // TODO Auto-generated method stub + return null; + } + }; + } + }); + return result; + } + + @Override + protected ServiceTracker createAndOpenSwissTimingReplayServiceTracker( + BundleContext context) { + @SuppressWarnings("unchecked") + ServiceTracker result = mock(ServiceTracker.class); + final SwissTimingReplayServiceFactory swissTimingReplayService = new SwissTimingReplayServiceFactory() { + @Override + public SwissTimingReplayService createSwissTimingReplayService( + com.sap.sailing.domain.swisstimingadapter.DomainFactory domainFactory) { + // TODO Auto-generated method stub + return null; + } + }; + when(result.getService()).thenReturn(swissTimingReplayService); + return result; + } + + @Override + protected ServiceTracker createAndOpenRaceLogTrackingAdapterTracker( + BundleContext context) { + @SuppressWarnings("unchecked") + ServiceTracker result = mock(ServiceTracker.class); + RaceLogTrackingAdapterFactory factory = RaceLogTrackingAdapterFactoryImpl.INSTANCE; + when(result.getService()).thenReturn(factory); + return result; + } + + @Override + protected RacingEventService getService() { + if (service == null) { + service = new RacingEventServiceImpl(); + } + return service; + } + + public RacingEventService getRacingEventService() { + return getService(); + } +} diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/TestColumnSwapping.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/TestColumnSwapping.java index 0294a67fd0b..e82d972522b 100755 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/TestColumnSwapping.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/TestColumnSwapping.java @@ -98,7 +98,7 @@ public class TestColumnSwapping { sailingService.moveLeaderboardColumnUp(TEST_LEADERBOARD_NAME, races[2]); sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[0], true); sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[2], false); - lb = sailingService.getLeaderboardByName(TEST_LEADERBOARD_NAME, new Date(), /* races to load */ null, /* previous leaderboard ID */ null).getLeaderboardDTO(/* previousVersion */ null); + lb = sailingService.getLeaderboardByName(TEST_LEADERBOARD_NAME, new Date(), /* races to load */ null, /* addOverallDetails */ true, /* previous leaderboard ID */ null).getLeaderboardDTO(/* previousVersion */ null); } catch (Exception e) { // e.printStackTrace(); fail(e.getLocalizedMessage()); @@ -139,7 +139,7 @@ public class TestColumnSwapping { public void testColumnSwappingFabian() { service.moveLeaderboardColumnUp(LEADERBOARDNAME, "Race3"); try { - leaderboardDTO = service.getLeaderboardByName(LEADERBOARDNAME, leaderboardCreationDate, leglist, /* previous leaderboard ID */ null).getLeaderboardDTO(/* previousVersion */ null); + leaderboardDTO = service.getLeaderboardByName(LEADERBOARDNAME, leaderboardCreationDate, leglist, /* addOverallDetails */ true, /* previous leaderboard ID */ null).getLeaderboardDTO(/* previousVersion */ null); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/TestWindChartPointRecalculation.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/TestWindChartPointRecalculation.java index c39d660cbbd..10de1427378 100644 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/TestWindChartPointRecalculation.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/TestWindChartPointRecalculation.java @@ -1,66 +1,119 @@ package com.sap.sailing.gwt.ui.test; -import static org.junit.Assert.*; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import static org.hamcrest.core.IsEqual.*; - import org.junit.Test; import org.moxieapps.gwt.highcharts.client.Point; -import com.sap.sailing.gwt.ui.client.shared.charts.WindChartPointRecalculator; +import com.sap.sailing.gwt.ui.client.shared.charts.ChartPointRecalculator; public class TestWindChartPointRecalculation { + + private static final double POINT_Y_EQUALITY_EPSILON = 0.005; + + private static final List MULTIPLE_WRAP_AROUNDS = Arrays.asList(356.74, 10.54, 30.192, 20.625, 5.647, 350.526); + private static final List EXPECTED_RECALCULATED_VALUES_FOR_MULTIPLE_WRAP_AROUNDS = Arrays.asList(356.74, 10.54 + 360, 30.192 + 360, 20.625 + 360, 5.647 + 360, 350.526); + + private static final List CONTINOUS_WRAP_AROUNDS = Arrays.asList(5.34, 358.97, 10.42, 50.0, 110.65, 140.54, + 210.85, 280.38, 330.5, 358.97, 7.24, 48.0, 112.65, 135.54, 215.85, 278.38, 333.5, 356.97, 8.24, 48.245, + 112.57, 133.54, 215.637, 278.38, 333.5, 356.97); + private static final List EXPECTED_RECALCULATED_VALUES_FOR_CONTINOUS_WRAP_AROUNDS_OVERALL_DELTA_MINIMUM = Arrays.asList( + 5.34, 358.97 - 360, 10.42, 50.0, 110.65, 140.54, 210.85 - 360, 280.38 - 360, 330.5 - 360, + 358.97 - 360, 7.24, 48.0, 112.65, 135.54, 215.85 - 360, 278.38 - 360, 333.5 - 360, 356.97 - 360, + 8.24, 48.245, 112.57, 133.54, 215.637 - 360, 278.38 - 360, 333.5 - 360, 356.97 - 360); + private static final List EXPECTED_RECALCULATED_VALUES_FOR_CONTINOUS_WRAP_AROUNDS_CLOSEST_TO_PREVIOUS_POINT = Arrays.asList( + 5.34, 358.97 - 360, 10.42, 50.0, 110.65, 140.54, 210.85, 280.38, 330.5, 358.97, 367.24, 408.0, 472.65, + 135.54 + 360, 575.85, 638.38, 693.5, 716.97, 728.24, 768.245, 112.57 + 720, 853.54, 935.637, 998.38, 1053.5, + 1076.97); + + private static void pointYEquals(Point actual, double expectedY) { + assertThat(actual.getY().doubleValue(), closeTo(expectedY, POINT_Y_EQUALITY_EPSILON)); + } @Test - public void testRecalculaion() { + public void testClosestToPreviousPointRecalculation() { + Point previousPoint = new Point(9, 20); + Point notToBeRecalculated = new Point(10, 100); - assertThat(WindChartPointRecalculator.recalculateDirectionPoint(90.0, 110.0, notToBeRecalculated).getY(), equalTo(notToBeRecalculated.getY())); - assertThat(WindChartPointRecalculator.recalculateDirectionPoint(null, 110.0, notToBeRecalculated).getY(), equalTo(notToBeRecalculated.getY())); - assertThat(WindChartPointRecalculator.recalculateDirectionPoint(90.0, null, notToBeRecalculated).getY(), equalTo(notToBeRecalculated.getY())); - assertThat(WindChartPointRecalculator.recalculateDirectionPoint(100.27458, 110.0, notToBeRecalculated).getY(), equalTo(notToBeRecalculated.getY())); - assertThat(WindChartPointRecalculator.recalculateDirectionPoint(90.0, 100.75639, notToBeRecalculated).getY(), equalTo(notToBeRecalculated.getY())); + pointYEquals(ChartPointRecalculator.stayClosestToPreviousPoint(previousPoint, notToBeRecalculated), notToBeRecalculated.getY().doubleValue()); + + notToBeRecalculated = new Point(10, 5); + pointYEquals(ChartPointRecalculator.stayClosestToPreviousPoint(previousPoint, notToBeRecalculated), notToBeRecalculated.getY().doubleValue()); + + Point toBeMovedDown = new Point(10, 358); + pointYEquals(ChartPointRecalculator.stayClosestToPreviousPoint(previousPoint, toBeMovedDown), -2); + + previousPoint = new Point(9, 350); + Point toBeMovedUp = new Point(10, 2); + pointYEquals(ChartPointRecalculator.stayClosestToPreviousPoint(previousPoint, toBeMovedUp), 362); + } + + @Test + public void testOverallDeltaMinimalRecalculaion() { + Point notToBeRecalculated = new Point(10, 100); + assertThat(ChartPointRecalculator.keepOverallDeltaMinimal(90.0, 110.0, notToBeRecalculated).getY(), equalTo(notToBeRecalculated.getY())); + assertThat(ChartPointRecalculator.keepOverallDeltaMinimal(null, 110.0, notToBeRecalculated).getY(), equalTo(notToBeRecalculated.getY())); + assertThat(ChartPointRecalculator.keepOverallDeltaMinimal(90.0, null, notToBeRecalculated).getY(), equalTo(notToBeRecalculated.getY())); + assertThat(ChartPointRecalculator.keepOverallDeltaMinimal(100.27458, 110.0, notToBeRecalculated).getY(), equalTo(notToBeRecalculated.getY())); + assertThat(ChartPointRecalculator.keepOverallDeltaMinimal(90.0, 100.75639, notToBeRecalculated).getY(), equalTo(notToBeRecalculated.getY())); //Test for special case, which caused a wrong recalculation, because of the wrong use of Math.abs() notToBeRecalculated = new Point(10, 179.8537); - assertThat(WindChartPointRecalculator.recalculateDirectionPoint(180.12356, 190.0, notToBeRecalculated).getY(), equalTo(notToBeRecalculated.getY())); - assertThat(WindChartPointRecalculator.recalculateDirectionPoint(170.0, 179.5372, notToBeRecalculated).getY(), equalTo(notToBeRecalculated.getY())); + assertThat(ChartPointRecalculator.keepOverallDeltaMinimal(180.12356, 190.0, notToBeRecalculated).getY(), equalTo(notToBeRecalculated.getY())); + assertThat(ChartPointRecalculator.keepOverallDeltaMinimal(170.0, 179.5372, notToBeRecalculated).getY(), equalTo(notToBeRecalculated.getY())); Point toBeMovedDown = new Point(10, 356); - assertThat(WindChartPointRecalculator.recalculateDirectionPoint(4.0, 23.0, toBeMovedDown).getY(), equalTo(new Point(10, 356.0 - 360.0).getY())); + assertThat(ChartPointRecalculator.keepOverallDeltaMinimal(4.0, 23.0, toBeMovedDown).getY(), equalTo(new Point(10, 356.0 - 360.0).getY())); Point toBeMovedUp = new Point(10, 5); - assertThat(WindChartPointRecalculator.recalculateDirectionPoint(356.0, 359.83364, toBeMovedUp).getY(), equalTo(new Point(10, 5.0 + 360.0).getY())); + assertThat(ChartPointRecalculator.keepOverallDeltaMinimal(356.0, 359.83364, toBeMovedUp).getY(), equalTo(new Point(10, 5.0 + 360.0).getY())); } @Test - public void testContinuousWrapAround() { - //Test multiple wrap around between 350� and 30� - List pointYValues = Arrays.asList(356.74, 10.54, 30.192, 20.625, 5.647, 350.526); - List expectedPointYValues = Arrays.asList(356.74, 10.54 + 360, 30.192 + 360, 20.625 + 360, 5.647 + 360, 350.526); - assertThat(recalculatePoints(pointYValues), equalTo(expectedPointYValues)); + public void testClosestToPreviousPointWithMultipleAndContiniusWrapArounds() { + //Test multiple wrap around between 350 and 30 + assertThat(recalculatePointsClosestToPreviousPoint(MULTIPLE_WRAP_AROUNDS), equalTo(EXPECTED_RECALCULATED_VALUES_FOR_MULTIPLE_WRAP_AROUNDS)); - //Test multiple round trips from 0� to 360� - pointYValues = Arrays.asList(5.34, 358.97, 10.42, 50.0, 110.65, 140.54, 210.85, 280.38, 330.5, 358.97, - 7.24, 48.0, 112.65, 135.54, 215.85, 278.38, 333.5, 356.97, - 8.24, 48.245, 112.57, 133.54, 215.637, 278.38, 333.5, 356.97); - expectedPointYValues = Arrays.asList(5.34, 358.97 - 360, 10.42, 50.0, 110.65, 140.54, 210.85 - 360, 280.38 - 360, 330.5 - 360, 358.97 - 360, - 7.24, 48.0, 112.65, 135.54, 215.85 - 360, 278.38 - 360, 333.5 - 360, 356.97 - 360, - 8.24, 48.245, 112.57, 133.54, 215.637 - 360, 278.38 - 360, 333.5 - 360, 356.97 - 360); - - assertThat(recalculatePoints(pointYValues), equalTo(expectedPointYValues)); + //Test multiple round trips from 0 to 360 + assertThat(recalculatePointsClosestToPreviousPoint(CONTINOUS_WRAP_AROUNDS), equalTo(EXPECTED_RECALCULATED_VALUES_FOR_CONTINOUS_WRAP_AROUNDS_CLOSEST_TO_PREVIOUS_POINT)); + } + + private List recalculatePointsClosestToPreviousPoint(List points) { + List racalculatedPoints = new ArrayList(); + Point previousPoint = null; + for (Double yValue : points) { + Point newPoint = new Point(10, yValue); + if (previousPoint != null) { + newPoint = ChartPointRecalculator.stayClosestToPreviousPoint(previousPoint, newPoint); + } + racalculatedPoints.add(newPoint.getY().doubleValue()); + previousPoint = newPoint; + } + return racalculatedPoints; } - private List recalculatePoints(List pointYValues) { + @Test + public void testOverallDeltaMinimalWithMultipleAndContinuousWrapArounds() { + //Test multiple wrap around between 350 and 30 + assertThat(recalculatePointsWithOverallDeltaMinimal(MULTIPLE_WRAP_AROUNDS), equalTo(EXPECTED_RECALCULATED_VALUES_FOR_MULTIPLE_WRAP_AROUNDS)); + + //Test multiple round trips from 0 to 360 + assertThat(recalculatePointsWithOverallDeltaMinimal(CONTINOUS_WRAP_AROUNDS), equalTo(EXPECTED_RECALCULATED_VALUES_FOR_CONTINOUS_WRAP_AROUNDS_OVERALL_DELTA_MINIMUM)); + } + + private List recalculatePointsWithOverallDeltaMinimal(List pointYValues) { List recalculatedPointYValues = new ArrayList(); Double min = null; Double max = null; for (Double yValue : pointYValues) { Point p = new Point(10, yValue); - p = WindChartPointRecalculator.recalculateDirectionPoint(min, max, p); + p = ChartPointRecalculator.keepOverallDeltaMinimal(min, max, p); Double newYValue = p.getY().doubleValue(); recalculatedPointYValues.add(newYValue); diff --git a/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs b/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs index 3d1e6b08933..548f1163d5a 100755 --- a/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs +++ b/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs @@ -1,3 +1,3 @@ -#Fri Jan 13 22:25:34 CET 2012 -eclipse.preferences.version=1 -encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties=UTF-8 +eclipse.preferences.version=1 +encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties=UTF-8 +encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties=UTF-8 diff --git a/java/com.sap.sailing.gwt.ui/AdminConsole.css b/java/com.sap.sailing.gwt.ui/AdminConsole.css index 2fcaf5f94d3..c5597a11b54 100644 --- a/java/com.sap.sailing.gwt.ui/AdminConsole.css +++ b/java/com.sap.sailing.gwt.ui/AdminConsole.css @@ -206,3 +206,7 @@ select { .gwt-SuggestBoxPopup .item-selected { background-color: #ffc; } + +.wrap-cols td div { + white-space: normal; +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/AdminConsole.html b/java/com.sap.sailing.gwt.ui/AdminConsole.html index 4169d2fb226..e8d613d1750 100644 --- a/java/com.sap.sailing.gwt.ui/AdminConsole.html +++ b/java/com.sap.sailing.gwt.ui/AdminConsole.html @@ -1,59 +1,60 @@ - - - - - - - - - - - - - - - - - - - - - SAP Sailing Analytics Administration Console - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- Your web browser must have JavaScript enabled - in order for this application to display correctly. -
- - - - + + + + + + + + + + + + + + + + + + + + + SAP Sailing Analytics Administration Console + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.gwt.ui/Leaderboard.css b/java/com.sap.sailing.gwt.ui/Leaderboard.css index 28080806749..270d817cdc7 100755 --- a/java/com.sap.sailing.gwt.ui/Leaderboard.css +++ b/java/com.sap.sailing.gwt.ui/Leaderboard.css @@ -158,3 +158,10 @@ body, table td, select, button { .circleRed { background: #f01010; } + +.busyIndicator-Simple { + margin-top: 7px; +} + +.busyIndicator-Circle { +} diff --git a/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF b/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF index 4384c4d83aa..99f195755f7 100755 --- a/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF @@ -26,9 +26,11 @@ Require-Bundle: com.sap.sailing.domain, com.sap.sailing.simulator;bundle-version="1.0.0", com.google.gwt.osgi;bundle-version="2.6.0", com.sap.sailing.domain.igtimiadapter, + com.sap.sailing.xrr.resultimport, com.sap.sailing.datamining.shared;bundle-version="1.0.0", com.sap.sailing.datamining;bundle-version="1.0.0", com.sap.sse.gwt;bundle-version="1.0.0", + com.sap.sailing.domain.racelogtrackingadapter;bundle-version="1.0.0", com.sap.sailing.manage2sail;bundle-version="1.0.0", com.sap.sse.common;bundle-version="1.0.0", com.sap.sse.datamining.shared;bundle-version="1.0.0", diff --git a/java/com.sap.sailing.gwt.ui/RaceBoard.css b/java/com.sap.sailing.gwt.ui/RaceBoard.css index 889f9dba932..f3daea67dce 100644 --- a/java/com.sap.sailing.gwt.ui/RaceBoard.css +++ b/java/com.sap.sailing.gwt.ui/RaceBoard.css @@ -371,3 +371,9 @@ img.openColumn { background: #f01010; } +.busyIndicator-Simple { + margin-top: 7px; +} + +.busyIndicator-Circle { +} diff --git a/java/com.sap.sailing.gwt.ui/SailingGWT Remote Profiling.launch b/java/com.sap.sailing.gwt.ui/SailingGWT Remote Profiling.launch index 0c012601c94..e19f996d629 100755 --- a/java/com.sap.sailing.gwt.ui/SailingGWT Remote Profiling.launch +++ b/java/com.sap.sailing.gwt.ui/SailingGWT Remote Profiling.launch @@ -27,7 +27,6 @@ - @@ -46,7 +45,6 @@ - diff --git a/java/com.sap.sailing.gwt.ui/SailingGWT.launch b/java/com.sap.sailing.gwt.ui/SailingGWT.launch index 972faa69fac..6cf37e1ef58 100755 --- a/java/com.sap.sailing.gwt.ui/SailingGWT.launch +++ b/java/com.sap.sailing.gwt.ui/SailingGWT.launch @@ -1,55 +1,59 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.gwt.ui/pom.xml b/java/com.sap.sailing.gwt.ui/pom.xml index 79a1e2f5b8e..3293b97fe0a 100644 --- a/java/com.sap.sailing.gwt.ui/pom.xml +++ b/java/com.sap.sailing.gwt.ui/pom.xml @@ -85,7 +85,9 @@ -Dgwt.persistentunitcache=false + com.sap.sailing.gwt.ui.EmulatedJRE com.sap.sailing.gwt.ui.AdminConsole diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/java/util/UUID_CustomFieldSerializer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/java/util/UUID_CustomFieldSerializer.java index baf58bfff0c..8a609c76b27 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/java/util/UUID_CustomFieldSerializer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/java/util/UUID_CustomFieldSerializer.java @@ -1,10 +1,11 @@ package com.google.gwt.user.client.rpc.core.java.util; +import java.util.UUID; + import com.google.gwt.user.client.rpc.CustomFieldSerializer; import com.google.gwt.user.client.rpc.SerializationException; import com.google.gwt.user.client.rpc.SerializationStreamReader; import com.google.gwt.user.client.rpc.SerializationStreamWriter; -import java.util.UUID; /** * Serializer for emulated {@link UUID}. diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetLeaderboardByNameAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetLeaderboardByNameAction.java index a7e989f3fd3..4d6b53f6121 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetLeaderboardByNameAction.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetLeaderboardByNameAction.java @@ -17,7 +17,7 @@ import com.sap.sse.gwt.client.player.Timer; * the logic how differential, incremental leaderboard fetching and transmission works. This helps guarantee the * requirement of {@link IncrementalOrFullLeaderboardDTO#getLeaderboardDTO(LeaderboardDTO)} (only pass null * as the previous leaderboard if null was passed as identifier of the previous leaderboard to - * {@link SailingServiceAsync#getLeaderboardByName(String, Date, Collection, String, AsyncCallback)} too. + * {@link SailingServiceAsync#getLeaderboardByName(String, Date, Collection, boolean, String, AsyncCallback)} too. *

* * When a valid {@link IncrementalOrFullLeaderboardDTO} object is received, the @@ -32,20 +32,22 @@ public class GetLeaderboardByNameAction implements AsyncAction { private final String leaderboardName; private final Date date; private final Collection namesOfRacesForWhichToLoadLegDetails; + private final boolean addOverallDetails; private final LeaderboardDTO previousLeaderboard; private final StringMessages stringMessages; private final ErrorReporter errorReporter; private final Timer timerToAdjustOffetIn; public GetLeaderboardByNameAction(SailingServiceAsync sailingService, String leaderboardName, Date date, - final Collection namesOfRacesForWhichToLoadLegDetails, LeaderboardDTO previousLeaderboard, - Timer timerToAdjustOffsetIn, ErrorReporter errorReporter, StringMessages stringMessages) { + final Collection namesOfRacesForWhichToLoadLegDetails, boolean addOverallDetails, + LeaderboardDTO previousLeaderboard, Timer timerToAdjustOffsetIn, ErrorReporter errorReporter, StringMessages stringMessages) { this.sailingService = sailingService; this.errorReporter = errorReporter; this.stringMessages = stringMessages; this.leaderboardName = leaderboardName; this.date = date; this.namesOfRacesForWhichToLoadLegDetails = namesOfRacesForWhichToLoadLegDetails; + this.addOverallDetails = addOverallDetails; this.previousLeaderboard = previousLeaderboard; this.timerToAdjustOffetIn = timerToAdjustOffsetIn; } @@ -53,9 +55,8 @@ public class GetLeaderboardByNameAction implements AsyncAction { @Override public void execute(final AsyncCallback callback) { final long clientTimeWhenRequestWasSent = System.currentTimeMillis(); - sailingService.getLeaderboardByName(leaderboardName, date, namesOfRacesForWhichToLoadLegDetails, - previousLeaderboard==null?null:previousLeaderboard.getId(), - new AsyncCallback() { + sailingService.getLeaderboardByName(leaderboardName, date, namesOfRacesForWhichToLoadLegDetails, addOverallDetails, + previousLeaderboard==null?null:previousLeaderboard.getId(), new AsyncCallback() { @Override public void onFailure(Throwable caught) { callback.onFailure(caught); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetWindInfoAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetWindInfoAction.java index 21592ec5d1d..c7a061c99f6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetWindInfoAction.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetWindInfoAction.java @@ -12,23 +12,28 @@ import com.sap.sse.gwt.client.async.AsyncAction; public class GetWindInfoAction implements AsyncAction { private final SailingServiceAsync sailingService; private final RegattaAndRaceIdentifier raceIdentifier; - private Date from; - private long millisecondsStepWidth; - private int numberOfFixes; - private Collection windSourceTypeNames; + private final Date from; + private final long millisecondsStepWidth; + private final int numberOfFixes; + private final Collection windSourceTypeNames; - private long resolutionInMilliseconds; - private Date fromDate; - private Date toDate; + private final long resolutionInMilliseconds; + private final Date fromDate; + private final Date toDate; + private final boolean onlyUpToNewestEvent; private enum CallVariants { Variant1, Variant2 }; private final CallVariants callVariant; public GetWindInfoAction(SailingServiceAsync sailingService, RegattaAndRaceIdentifier raceIdentifier, Date from, - long millisecondsStepWidth, int numberOfFixes, Collection windSourceTypeNames) { + long millisecondsStepWidth, int numberOfFixes, Collection windSourceTypeNames, boolean onlyUpToNewestEvent) { this.sailingService = sailingService; + this.onlyUpToNewestEvent = onlyUpToNewestEvent; this.raceIdentifier = raceIdentifier; this.from = from; + this.toDate = null; // not needed in this variant + this.fromDate = null; // not needed in this variant + this.resolutionInMilliseconds = 0; // not needed in this variant this.millisecondsStepWidth = millisecondsStepWidth; this.numberOfFixes = numberOfFixes; this.windSourceTypeNames = windSourceTypeNames; @@ -36,11 +41,15 @@ public class GetWindInfoAction implements AsyncAction { } public GetWindInfoAction(SailingServiceAsync sailingService, RegattaAndRaceIdentifier raceIdentifier, Date fromDate, - Date toDate, long resolutionInMilliseconds, Collection windSourceTypeNames) { + Date toDate, long resolutionInMilliseconds, Collection windSourceTypeNames, boolean onlyUpToNewestEvent) { this.sailingService = sailingService; + this.onlyUpToNewestEvent = onlyUpToNewestEvent; this.raceIdentifier = raceIdentifier; this.fromDate = fromDate; this.toDate = toDate; + this.numberOfFixes = 0; // not needed in this variant + this.millisecondsStepWidth = 0; // not needed in this variant + this.from = null; // not needed in this variant this.resolutionInMilliseconds = resolutionInMilliseconds; this.windSourceTypeNames = windSourceTypeNames; callVariant = CallVariants.Variant2; @@ -50,10 +59,12 @@ public class GetWindInfoAction implements AsyncAction { public void execute(AsyncCallback callback) { switch (callVariant) { case Variant1: - sailingService.getAveragedWindInfo(raceIdentifier, from, millisecondsStepWidth, numberOfFixes, windSourceTypeNames, callback); + sailingService.getAveragedWindInfo(raceIdentifier, from, millisecondsStepWidth, numberOfFixes, windSourceTypeNames, + onlyUpToNewestEvent, callback); break; case Variant2: - sailingService.getAveragedWindInfo(raceIdentifier, fromDate, toDate, resolutionInMilliseconds, windSourceTypeNames, callback); + sailingService.getAveragedWindInfo(raceIdentifier, fromDate, toDate, resolutionInMilliseconds, windSourceTypeNames, + onlyUpToNewestEvent, callback); break; } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractEventManagementPanel.java index 82222b38d8d..3b43de1b9bc 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractEventManagementPanel.java @@ -8,8 +8,8 @@ import java.util.List; import com.google.gwt.user.client.ui.ListBox; import com.sap.sailing.gwt.ui.client.AbstractRegattaPanel; import com.sap.sailing.gwt.ui.client.ErrorReporter; -import com.sap.sailing.gwt.ui.client.RegattaRefresher; import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; +import com.sap.sailing.gwt.ui.client.RegattaRefresher; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.RegattaDTO; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java new file mode 100644 index 00000000000..38a6478d771 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java @@ -0,0 +1,497 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.cellview.client.CellTable; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.CaptionPanel; +import com.google.gwt.user.client.ui.FormPanel; +import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.view.client.ListDataProvider; +import com.google.gwt.view.client.MultiSelectionModel; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SetSelectionModel; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.RegattaNameAndRaceName; +import com.sap.sailing.domain.common.dto.FleetDTO; +import com.sap.sailing.domain.common.dto.NamedDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.domain.common.impl.Util; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; +import com.sap.sailing.gwt.ui.client.RaceSelectionModel; +import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; +import com.sap.sailing.gwt.ui.client.RegattaDisplayer; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.panels.LabeledAbstractFilterablePanel; +import com.sap.sailing.gwt.ui.shared.RegattaDTO; +import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; +import com.sap.sse.common.Pair; +import com.sap.sse.gwt.client.async.MarkedAsyncCallback; + +public abstract class AbstractLeaderboardConfigPanel extends FormPanel implements SelectedLeaderboardProvider, RegattaDisplayer, RaceSelectionChangeListener, +TrackedRaceChangedListener { + protected final VerticalPanel mainPanel; + + protected final TrackedRacesListComposite trackedRacesListComposite; + + protected final StringMessages stringMessages; + + protected final SailingServiceAsync sailingService; + + protected final ListDataProvider leaderboardList; + + protected final ErrorReporter errorReporter; + + protected final CellTable leaderboardTable; + + protected final RaceTableWrapper> raceColumnTable; + protected final SetSelectionModel raceColumnTableSelectionModel; + + protected RaceColumnDTOAndFleetDTOWithNameBasedEquality selectedRaceInLeaderboard; + + protected final CaptionPanel selectedLeaderBoardPanel; + protected final CaptionPanel trackedRacesCaptionPanel; + protected final List allRegattas; + + protected LabeledAbstractFilterablePanel filterLeaderboardPanel; + + protected List availableLeaderboardList; + + protected final MultiSelectionModel leaderboardSelectionModel; + + protected final RaceSelectionProvider raceSelectionProvider; + + public static class RaceColumnDTOAndFleetDTOWithNameBasedEquality extends Pair { + private static final long serialVersionUID = -8742476113296862662L; + + public RaceColumnDTOAndFleetDTOWithNameBasedEquality(RaceColumnDTO a, FleetDTO b) { + super(a, b); + } + + @Override + public int hashCode() { + return getA().getName().hashCode() ^ getB().getName().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else { + if (obj == null) { + return false; + } else { + return equalNamesOrBothNull(getA(), ((RaceColumnDTOAndFleetDTOWithNameBasedEquality) obj).getA()) + && equalNamesOrBothNull(getB(), ((RaceColumnDTOAndFleetDTOWithNameBasedEquality) obj).getB()); + } + } + } + + private boolean equalNamesOrBothNull(NamedDTO a, NamedDTO b) { + if (a == null) { + return b == null; + } else { + if (b == null) { + return false; + } else { + return Util.equalsWithNull(a.getName(), b.getName()); + } + } + } + } + + public AbstractLeaderboardConfigPanel(final SailingServiceAsync sailingService, AdminConsoleEntryPoint adminConsole, + final ErrorReporter errorReporter, StringMessages theStringConstants, + SetSelectionModel raceColumnTableSelectionModel) { + this.stringMessages = theStringConstants; + this.sailingService = sailingService; + leaderboardList = new ListDataProvider(); + allRegattas = new ArrayList(); + this.errorReporter = errorReporter; + this.availableLeaderboardList = new ArrayList(); + mainPanel = new VerticalPanel(); + mainPanel.setWidth("100%"); + this.setWidget(mainPanel); + + //Create leaderboards list and functionality + CaptionPanel leaderboardsCaptionPanel = new CaptionPanel(stringMessages.leaderboards()); + leaderboardsCaptionPanel.setStyleName("bold"); + leaderboardsCaptionPanel.setWidth("75%"); + mainPanel.add(leaderboardsCaptionPanel); + + VerticalPanel leaderboardsPanel = new VerticalPanel(); + leaderboardsCaptionPanel.add(leaderboardsPanel); + + HorizontalPanel leaderboardConfigControlsPanel = new HorizontalPanel(); + Label lblFilterEvents = new Label(stringMessages.filterLeaderboardsByName() + ": "); + leaderboardConfigControlsPanel.setSpacing(5); + + AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); + leaderboardTable = new CellTable(/* pageSize */10000, tableRes); + filterLeaderboardPanel = new LabeledAbstractFilterablePanel(lblFilterEvents, availableLeaderboardList, leaderboardTable, leaderboardList) { + @Override + public List getSearchableStrings(StrippedLeaderboardDTO t) { + List strings = new ArrayList(); + strings.add(t.name); + strings.add(t.displayName); + return strings; + } + }; + leaderboardConfigControlsPanel.add(filterLeaderboardPanel); + + addLeaderboardConfigControls(leaderboardConfigControlsPanel); + + leaderboardsPanel.add(leaderboardConfigControlsPanel); + leaderboardTable.ensureDebugId("AvailableLeaderboardsTable"); + + addColumnsToLeaderboardTable(leaderboardTable); + + leaderboardTable.setWidth("100%"); + leaderboardSelectionModel = new MultiSelectionModel(); + leaderboardTable.setSelectionModel(leaderboardSelectionModel); + leaderboardSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { + public void onSelectionChange(SelectionChangeEvent event) { + leaderboardSelectionChanged(); + raceColumnTable.setSelectedLeaderboardName(getSelectedLeaderboardName()); + } + }); + leaderboardList.addDataDisplay(leaderboardTable); + leaderboardsPanel.add(leaderboardTable); + HorizontalPanel leaderboardButtonPanel = new HorizontalPanel(); + leaderboardButtonPanel.setSpacing(5); + leaderboardsPanel.add(leaderboardButtonPanel); + + addLeaderboardCreateControls(leaderboardButtonPanel); + + + mainPanel.add(new Grid(1, 1)); + + // caption panels for the selected leaderboard and tracked races + HorizontalPanel splitPanel = new HorizontalPanel(); + splitPanel.ensureDebugId("LeaderboardDetailsPanel"); + mainPanel.add(splitPanel); + + selectedLeaderBoardPanel = new CaptionPanel(stringMessages.leaderboard()); + selectedLeaderBoardPanel.setWidth("50%"); + splitPanel.add(selectedLeaderBoardPanel); + + VerticalPanel vPanel = new VerticalPanel(); + vPanel.setWidth("100%"); + selectedLeaderBoardPanel.setContentWidget(vPanel); + + trackedRacesCaptionPanel = new CaptionPanel(stringMessages.trackedRaces()); + trackedRacesCaptionPanel.setWidth("50%"); + splitPanel.add(trackedRacesCaptionPanel); + + VerticalPanel trackedRacesPanel = new VerticalPanel(); + trackedRacesPanel.setWidth("100%"); + trackedRacesCaptionPanel.setContentWidget(trackedRacesPanel); + trackedRacesCaptionPanel.setStyleName("bold"); + + raceSelectionProvider = new RaceSelectionModel(); + trackedRacesListComposite = new TrackedRacesListComposite(sailingService, errorReporter, adminConsole, + raceSelectionProvider, stringMessages, /* multiselection */false); + trackedRacesListComposite.ensureDebugId("TrackedRacesListComposite"); + trackedRacesPanel.add(trackedRacesListComposite); + trackedRacesListComposite.addTrackedRaceChangeListener(this); + raceSelectionProvider.addRaceSelectionChangeListener(this); + + Button reloadAllRaceLogs = new Button(stringMessages.reloadAllRaceLogs()); + reloadAllRaceLogs.ensureDebugId("ReloadAllRaceLogsButton"); + reloadAllRaceLogs.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + StrippedLeaderboardDTO leaderboard = getSelectedLeaderboard(); + for (RaceColumnDTO column : leaderboard.getRaceList()) { + for (FleetDTO fleet : column.getFleets()) { + refreshRaceLog(column, fleet, false); + } + } + Window.alert(stringMessages.raceLogReloaded()); + } + }); + vPanel.add(reloadAllRaceLogs); + + + Label lblRaceNamesIn = new Label(stringMessages.races()); + vPanel.add(lblRaceNamesIn); + + raceColumnTable = new RaceTableWrapper>( + sailingService, stringMessages, errorReporter, raceColumnTableSelectionModel); + raceColumnTable.getTable().setWidth("500px"); + addColumnsToRacesTable(raceColumnTable.getTable()); + this.raceColumnTableSelectionModel = raceColumnTableSelectionModel; + raceColumnTableSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { + public void onSelectionChange(SelectionChangeEvent event) { + leaderboardRaceColumnSelectionChanged(); + } + }); + vPanel.add(raceColumnTable); + + HorizontalPanel selectedLeaderboardRaceButtonPanel = new HorizontalPanel(); + selectedLeaderboardRaceButtonPanel.setSpacing(5); + vPanel.add(selectedLeaderboardRaceButtonPanel); + + addSelectedLeaderboardRacesControls(selectedLeaderboardRaceButtonPanel); + + loadLeaderboards(); + } + + protected abstract void addLeaderboardConfigControls(Panel configPanel); + protected abstract void addLeaderboardCreateControls(Panel createPanel); + protected abstract void addSelectedLeaderboardRacesControls(Panel racesPanel); + protected abstract void addColumnsToLeaderboardTable(CellTable leaderboardTable); + protected abstract void addColumnsToRacesTable(CellTable racesTable); + + + public void loadLeaderboards() { + sailingService.getLeaderboards(new MarkedAsyncCallback>( + new AsyncCallback>() { + @Override + public void onSuccess(List leaderboards) { + leaderboardList.getList().clear(); + availableLeaderboardList.clear(); + leaderboardList.getList().addAll(leaderboards); + availableLeaderboardList.addAll(leaderboards); + filterLeaderboardPanel.updateAll(availableLeaderboardList); + leaderboardSelectionChanged(); + leaderboardRaceColumnSelectionChanged(); + } + + @Override + public void onFailure(Throwable t) { + AbstractLeaderboardConfigPanel.this.errorReporter.reportError("Error trying to obtain list of leaderboards: " + + t.getMessage()); + } + })); + } + + /** + * @param nameOfRaceColumnToSelect + * if not null, selects the first race column name with this name found in the leaderboard + * after the refresh has successfully completed. See {@link #selectRaceColumn(String)}. + */ + public void loadAndRefreshLeaderboard(final String leaderboardName, final String nameOfRaceColumnToSelect) { + sailingService.getLeaderboard(leaderboardName, new MarkedAsyncCallback( + new AsyncCallback() { + @Override + public void onSuccess(StrippedLeaderboardDTO leaderboard) { + for (StrippedLeaderboardDTO leaderboardDTO : leaderboardSelectionModel.getSelectedSet()) { + if (leaderboardDTO.name.equals(leaderboardName)) { + leaderboardSelectionModel.setSelected(leaderboardDTO, false); + break; + } + } + + replaceLeaderboardInList(leaderboardList.getList(), leaderboardName, leaderboard); + replaceLeaderboardInList(availableLeaderboardList, leaderboardName, leaderboard); + leaderboardSelectionModel.setSelected(leaderboard, true); + if (nameOfRaceColumnToSelect != null) { + selectRaceColumn(nameOfRaceColumnToSelect); + } + leaderboardSelectionChanged(); + } + + @Override + public void onFailure(Throwable t) { + AbstractLeaderboardConfigPanel.this.errorReporter.reportError("Error trying to update leaderboard with name " + leaderboardName + " : " + + t.getMessage()); + } + })); + } + + private void replaceLeaderboardInList(List leaderboardList, String leaderboardToReplace, StrippedLeaderboardDTO newLeaderboard) { + int index = -1; + for (StrippedLeaderboardDTO existingLeaderboard : leaderboardList) { + index++; + if (existingLeaderboard.name.equals(leaderboardToReplace)) { + break; + } + } + if (index >= 0) { + leaderboardList.set(index, newLeaderboard); + } + } + + protected void unlinkRaceColumnFromTrackedRace(final String raceColumnName, final FleetDTO fleet) { + final String selectedLeaderboardName = getSelectedLeaderboardName(); + sailingService.disconnectLeaderboardColumnFromTrackedRace(selectedLeaderboardName, raceColumnName, fleet.getName(), + new AsyncCallback() { + @Override + public void onFailure(Throwable t) { + errorReporter.reportError("Error trying to unlink tracked race from column " + raceColumnName + + " from leaderboard " + selectedLeaderboardName + ": " + t.getMessage()); + } + + @Override + public void onSuccess(Void arg0) { + trackedRacesListComposite.clearSelection(); + getSelectedRaceColumnWithFleet().getA().setRaceIdentifier(fleet, null); + raceColumnTable.getDataProvider().refresh(); + } + }); + } + + protected void refreshRaceLog(final RaceColumnDTO raceColumnDTO, final FleetDTO fleet, final boolean showAlerts) { + final String selectedLeaderboardName = getSelectedLeaderboardName(); + sailingService.reloadRaceLog(selectedLeaderboardName, raceColumnDTO, fleet, new MarkedAsyncCallback( + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + if (showAlerts) { + errorReporter.reportError(caught.getMessage()); + } + } + @Override + public void onSuccess(Void result) { + if (showAlerts) { + Window.alert(stringMessages.raceLogReloaded()); + } + } + })); + } + + protected abstract void leaderboardRaceColumnSelectionChanged(); + + protected void selectRaceColumn(String raceColumnName) { + List list = raceColumnTable.getDataProvider().getList(); + for (RaceColumnDTOAndFleetDTOWithNameBasedEquality pair : list) { + if (pair.getA().getName().equals(raceColumnName)) { + raceColumnTable.getSelectionModel().setSelected(pair, true); + break; + } + } + } + + protected void selectTrackedRaceInRaceList() { + final String selectedLeaderboardName = getSelectedLeaderboardName(); + if (selectedLeaderboardName != null) { + final RaceColumnDTOAndFleetDTOWithNameBasedEquality selectedRaceColumnAndFleetNameInLeaderboard = getSelectedRaceColumnWithFleet(); + final String selectedRaceColumnName = selectedRaceColumnAndFleetNameInLeaderboard.getA().getRaceColumnName(); + final String selectedFleetName = selectedRaceColumnAndFleetNameInLeaderboard.getB().getName(); + sailingService.getRegattaAndRaceNameOfTrackedRaceConnectedToLeaderboardColumn(selectedLeaderboardName, + selectedRaceColumnName, new MarkedAsyncCallback>( + new AsyncCallback>() { + @Override + public void onFailure(Throwable t) { + errorReporter.reportError("Error trying to determine tracked race linked to race column " + + selectedRaceColumnName + " in leaderboard " + selectedLeaderboardName + ": " + + t.getMessage()); + } + + @Override + public void onSuccess(Map regattaAndRaceNamesPerFleet) { + if (regattaAndRaceNamesPerFleet != null && !regattaAndRaceNamesPerFleet.isEmpty()) { + RegattaAndRaceIdentifier raceIdentifier = regattaAndRaceNamesPerFleet.get(selectedFleetName); + + if (raceIdentifier != null) { + selectRaceInList(raceIdentifier.getRegattaName(), raceIdentifier.getRaceName()); + } else { + trackedRacesListComposite.clearSelection(); + } + } else { + trackedRacesListComposite.clearSelection(); + } + } + })); + } + } + + protected void selectRaceInList(String regattaName, String raceName) { + RegattaNameAndRaceName raceIdentifier = new RegattaNameAndRaceName(regattaName, raceName); + trackedRacesListComposite.selectRaceByIdentifier(raceIdentifier); + } + + protected RaceColumnDTOAndFleetDTOWithNameBasedEquality getSelectedRaceColumnWithFleet() { + if (raceColumnTable.getSelectionModel().getSelectedSet().isEmpty()) { + return null; + } + return raceColumnTable.getSelectionModel().getSelectedSet().iterator().next(); + } + + protected String getSelectedLeaderboardName() { + return getSelectedLeaderboard() != null ? getSelectedLeaderboard().name : null; + } + + protected abstract void leaderboardSelectionChanged(); + + @Override + public void fillRegattas(List regattas) { + trackedRacesListComposite.fillRegattas(regattas); + + allRegattas.clear(); + allRegattas.addAll(regattas); + } + + @Override + public void changeTrackingRace(Iterable regattaAndRaceIdentifiers, boolean isTracked) { + for (RegattaAndRaceIdentifier regattaAndRaceIdentifier : regattaAndRaceIdentifiers) { + for (RaceColumnDTOAndFleetDTOWithNameBasedEquality raceColumnAndFleetName : raceColumnTable.getDataProvider().getList()) { + if (raceColumnAndFleetName.getA().getRaceColumnName().equals(regattaAndRaceIdentifier.getRaceName())) { + raceColumnAndFleetName.getA().setRaceIdentifier(raceColumnAndFleetName.getB(), + regattaAndRaceIdentifier); + } + } + raceColumnTable.getDataProvider().refresh(); + } + } + + @Override + public void onRaceSelectionChange(List selectedRaces) { + // if no leaderboard column is selected, ignore the race selection change + RaceColumnDTOAndFleetDTOWithNameBasedEquality selectedRaceColumnAndFleetName = getSelectedRaceColumnWithFleet(); + if (selectedRaceColumnAndFleetName != null) { + if (selectedRaces.isEmpty()) { + if (selectedRaceColumnAndFleetName.getA().getRaceIdentifier(selectedRaceColumnAndFleetName.getB()) != null) { + unlinkRaceColumnFromTrackedRace(selectedRaceColumnAndFleetName.getA().getRaceColumnName(), selectedRaceColumnAndFleetName.getB()); + } + } else { + linkTrackedRaceToSelectedRaceColumn(selectedRaceColumnAndFleetName.getA(), selectedRaceColumnAndFleetName.getB(), + selectedRaces.iterator().next()); + } + } + } + + private void linkTrackedRaceToSelectedRaceColumn(final RaceColumnDTO selectedRaceInLeaderboard, + final FleetDTO fleet, final RegattaAndRaceIdentifier selectedRace) { + sailingService.connectTrackedRaceToLeaderboardColumn(getSelectedLeaderboardName(), selectedRaceInLeaderboard + .getRaceColumnName(), fleet.getName(), selectedRace, + new MarkedAsyncCallback( + new AsyncCallback() { + @Override + public void onFailure(Throwable t) { + errorReporter.reportError("Error trying to link tracked race " + selectedRace + " to race column named " + + selectedRaceInLeaderboard.getRaceColumnName() + " of leaderboard " + + getSelectedLeaderboardName() + ": " + t.getMessage()); + trackedRacesListComposite.clearSelection(); + } + + @Override + public void onSuccess(Boolean success) { + if (success) { + // TODO consider enabling the Unlink button + selectedRaceInLeaderboard.setRaceIdentifier(fleet, selectedRace); + raceColumnTable.getDataProvider().refresh(); + } + } + })); + } + + @Override + public StrippedLeaderboardDTO getSelectedLeaderboard() { + return leaderboardSelectionModel.getSelectedSet().isEmpty() ? null : leaderboardSelectionModel.getSelectedSet().iterator().next(); + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AddDeviceMappingDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AddDeviceMappingDialog.java new file mode 100644 index 00000000000..f2c30146be5 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AddDeviceMappingDialog.java @@ -0,0 +1,244 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +import com.github.gwtbootstrap.datetimepicker.client.ui.base.HasViewMode.ViewMode; +import com.google.gwt.event.logical.shared.AttachEvent; +import com.google.gwt.event.logical.shared.AttachEvent.Handler; +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.event.logical.shared.ValueChangeHandler; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.CaptionPanel; +import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.ListBox; +import com.google.gwt.user.client.ui.TextBox; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SelectionModel; +import com.google.gwt.view.client.SingleSelectionModel; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.BetterDateTimeBox; +import com.sap.sailing.gwt.ui.shared.DeviceIdentifierDTO; +import com.sap.sailing.gwt.ui.shared.DeviceMappingDTO; +import com.sap.sailing.gwt.ui.shared.MarkDTO; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; + +public class AddDeviceMappingDialog extends DataEntryDialog { + private final BetterDateTimeBox from; + private final BetterDateTimeBox to; + private final ListBox deviceType; + private final TextBox deviceId; + private final CompetitorTableWrapper competitorTable; + private final MarkTableWrapper> markTable; + private final StringMessages stringMessages; + + private Serializable selectedItem; + + public AddDeviceMappingDialog(SailingServiceAsync sailingService, final ErrorReporter errorReporter, + final StringMessages stringMessages, String leaderboardName, String raceColumnName, String fleetName, + DialogCallback callback, final DeviceMappingDTO mapping) { + super(stringMessages.add(stringMessages.deviceMappings()), stringMessages.add(stringMessages.deviceMappings()), + stringMessages.add(), stringMessages.cancel(), new DataEntryDialog.Validator() { + @Override + public String getErrorMessage(DeviceMappingDTO valueToValidate) { + String deviceType = valueToValidate.deviceIdentifier.deviceType; + String deviceId = valueToValidate.deviceIdentifier.deviceId; + if (deviceType == null || deviceType.isEmpty()) { + return stringMessages.pleaseEnterA(stringMessages.deviceType()); + } + if (deviceId == null || deviceId.isEmpty()) { + return stringMessages.pleaseEnterA(stringMessages.deviceId()); + } + if ((valueToValidate.from == null || valueToValidate.from.compareTo(new Date(Long.MIN_VALUE)) == 0) + && (valueToValidate.to == null || valueToValidate.to.compareTo(new Date(Long.MAX_VALUE)) == 0)) { + return stringMessages.atMostOneEndOfTheTimeRangeMayBeOpen(); + } + if (valueToValidate.from != null && valueToValidate.to != null && + valueToValidate.to.before(valueToValidate.from)) { + return stringMessages.startOfTimeRangeMustLieBeforeEnd(); + } + if (valueToValidate.mappedTo == null) { + return stringMessages.pleaseSelectAnItemToMapTo(); + } + return null; + } + }, true, callback); + + this.stringMessages = stringMessages; + + from = initTimeBox(); + to = initTimeBox(); + + deviceType = createListBox(false); + sailingService.getDeserializableDeviceIdentifierTypes(new AsyncCallback>() { + @Override + public void onSuccess(List result) { + String typeToPreselect = mapping != null ? mapping.deviceIdentifier.deviceType : null; + int i = 0; + for (String type : result) { + deviceType.addItem(type); + if (type.equals(typeToPreselect)) { + deviceType.setSelectedIndex(i); + } + i++; + } + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could not load deserializable device identifier types: " + caught.getMessage()); + } + }); + + deviceId = createTextBox(""); + + final SingleSelectionModel markSelectionModel = new SingleSelectionModel(); + competitorTable = new CompetitorTableWrapper(sailingService, stringMessages, errorReporter); + markTable = new MarkTableWrapper>( + markSelectionModel, sailingService, stringMessages, errorReporter); + + competitorTable.getSelectionModel().addSelectionChangeHandler(new SelectionChangeEvent.Handler() { + @Override + public void onSelectionChange(SelectionChangeEvent event) { + if (competitorTable.getSelectionModel().getSelectedSet().size() == 1) { + selectedItem = competitorTable.getSelectionModel().getSelectedSet().iterator().next(); + deselectAll(markTable.getSelectionModel(), markTable.getDataProvider().getList()); + validate(); + } + } + }); + + markTable.getSelectionModel().addSelectionChangeHandler(new SelectionChangeEvent.Handler() { + @Override + public void onSelectionChange(SelectionChangeEvent event) { + if (markSelectionModel.getSelectedSet().size() == 1) { + selectedItem = markSelectionModel.getSelectedSet().iterator().next(); + deselectAll(competitorTable.getSelectionModel(), competitorTable.getAllCompetitors()); + validate(); + } + } + }); + + sailingService.getCompetitorRegistrations(leaderboardName, raceColumnName, fleetName, new AsyncCallback>() { + @Override + public void onSuccess(Collection result) { + competitorTable.refreshCompetitorList(result); + if (mapping != null && mapping.mappedTo instanceof CompetitorDTO) { + //got new DTOs with new object identities, so have to go through them one by one + CompetitorDTO old = (CompetitorDTO) mapping.mappedTo; + for (CompetitorDTO inList : competitorTable.getAllCompetitors()) { + if (old.getIdAsString().equals(inList.getIdAsString())) { + competitorTable.getSelectionModel().setSelected(inList, true); + } + } + } + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could not load competitors: " + caught.getMessage()); + } + }); + + sailingService.getMarksInRaceLog(leaderboardName, raceColumnName, fleetName, new AsyncCallback>() { + @Override + public void onSuccess(Collection result) { + markTable.refresh(result); + if (mapping != null && mapping.mappedTo instanceof MarkDTO) { + //got new DTOs with new object identities, so have to go through them one by one + MarkDTO old = (MarkDTO) mapping.mappedTo; + for (MarkDTO inList : markTable.getDataProvider().getList()) { + if (old.getIdAsString().equals(inList.getIdAsString())) { + markTable.getSelectionModel().setSelected(inList, true); + } + } + } + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could not load marks: " + caught.getMessage()); + } + }); + + if (mapping != null) { + deviceId.setValue(mapping.deviceIdentifier.deviceId); + from.setValue(mapping.from); + to.setValue(mapping.to); + } + } + + @Override + protected Widget getAdditionalWidget() { + HorizontalPanel panel = new HorizontalPanel(); + VerticalPanel tablesPanel = new VerticalPanel(); + Grid entryGrid = new Grid(4, 2); + CaptionPanel marksPanel = new CaptionPanel(stringMessages.mark()); + CaptionPanel competitorsPanel = new CaptionPanel(stringMessages.competitor()); + + entryGrid.setWidget(0, 0, new Label(stringMessages.deviceType())); + entryGrid.setWidget(0, 1, deviceType); + entryGrid.setWidget(1, 0, new Label(stringMessages.deviceId())); + entryGrid.setWidget(1, 1, deviceId); + entryGrid.setWidget(2, 0, new Label(stringMessages.from())); + entryGrid.setWidget(2, 1, from); + entryGrid.setWidget(3, 0, new Label(stringMessages.to())); + entryGrid.setWidget(3, 1, to); + + panel.add(entryGrid); + panel.add(tablesPanel); + tablesPanel.add(marksPanel); + tablesPanel.add(competitorsPanel); + + marksPanel.setContentWidget(markTable.getTable()); + competitorsPanel.setContentWidget(competitorTable.getTable()); + + return panel; + } + + private static void deselectAll(SelectionModel selectionModel, Collection list) { + for (T t : list) { + selectionModel.setSelected(t, false); + } + } + + private BetterDateTimeBox initTimeBox() { + final BetterDateTimeBox timeBox = new BetterDateTimeBox(); + timeBox.addValueChangeHandler(new ValueChangeHandler() { + @Override + public void onValueChange(ValueChangeEvent event) { + validate(); + } + }); + timeBox.addAttachHandler(new Handler() { + @Override + public void onAttachOrDetach(AttachEvent event) { + if (event.isAttached()) { + addAutoHidePartner(timeBox.getPicker()); + } + } + }); + timeBox.setAutoClose(true); + timeBox.setStartView(ViewMode.HOUR); + timeBox.setFormat("dd/mm/yyyy hh:ii"); + return timeBox; + } + + @Override + protected DeviceMappingDTO getResult() { + String deviceTypeS = deviceType.getSelectedIndex() < 0 ? null : deviceType.getValue(deviceType.getSelectedIndex()); + DeviceIdentifierDTO deviceIdentifier = new DeviceIdentifierDTO(deviceTypeS, deviceId.getValue()); + return new DeviceMappingDTO(deviceIdentifier, from.getValue(), to.getValue(), + selectedItem, null); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java index f9b82ec355e..b83bbcb0c2a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java @@ -1,224 +1,235 @@ -package com.sap.sailing.gwt.ui.adminconsole; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import com.google.gwt.core.client.GWT; -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.event.logical.shared.SelectionEvent; -import com.google.gwt.event.logical.shared.SelectionHandler; -import com.google.gwt.safehtml.shared.SafeHtmlBuilder; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.rpc.ServiceDefTarget; -import com.google.gwt.user.client.ui.Anchor; -import com.google.gwt.user.client.ui.DockLayoutPanel; -import com.google.gwt.user.client.ui.DockPanel; -import com.google.gwt.user.client.ui.HasHorizontalAlignment; -import com.google.gwt.user.client.ui.RootLayoutPanel; -import com.google.gwt.user.client.ui.ScrollPanel; -import com.google.gwt.user.client.ui.TabLayoutPanel; -import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.gwt.ui.client.AbstractEntryPoint; -import com.sap.sailing.gwt.ui.client.MediaService; -import com.sap.sailing.gwt.ui.client.MediaServiceAsync; -import com.sap.sailing.gwt.ui.client.RegattaDisplayer; -import com.sap.sailing.gwt.ui.client.RegattaRefresher; -import com.sap.sailing.gwt.ui.client.RemoteServiceMappingConstants; -import com.sap.sailing.gwt.ui.client.SailingService; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.UserManagementService; -import com.sap.sailing.gwt.ui.client.UserManagementServiceAsync; -import com.sap.sailing.gwt.ui.client.shared.controls.ScrolledTabLayoutPanel; -import com.sap.sailing.gwt.ui.client.shared.panels.SystemInformationPanel; -import com.sap.sailing.gwt.ui.client.shared.panels.UserStatusPanel; -import com.sap.sailing.gwt.ui.masterdataimport.MasterDataImportPanel; -import com.sap.sailing.gwt.ui.shared.BetterDateTimeBox; -import com.sap.sailing.gwt.ui.shared.RegattaDTO; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; - -public class AdminConsoleEntryPoint extends AbstractEntryPoint implements RegattaRefresher { - private Set regattaDisplayers; - - private final AdminConsoleResources resources = GWT.create(AdminConsoleResources.class); - - private final SailingServiceAsync sailingService = GWT.create(SailingService.class); - private final MediaServiceAsync mediaService = GWT.create(MediaService.class); - private final UserManagementServiceAsync userManagementService = GWT.create(UserManagementService.class); - - @Override - protected void doOnModuleLoad() { - super.doOnModuleLoad(); - - registerASyncService((ServiceDefTarget) userManagementService, RemoteServiceMappingConstants.userManagementServiceRemotePath); - registerASyncService((ServiceDefTarget) sailingService, RemoteServiceMappingConstants.sailingServiceRemotePath); - registerASyncService((ServiceDefTarget) mediaService, RemoteServiceMappingConstants.mediaServiceRemotePath); - - BetterDateTimeBox.initialize(); - - RootLayoutPanel rootPanel = RootLayoutPanel.get(); - DockLayoutPanel dockPanel = new DockLayoutPanel(Unit.EM); - rootPanel.add(dockPanel); - - DockPanel topInformationPanel = new DockPanel(); - topInformationPanel.setSize("100%", "95%"); - UserStatusPanel userStatusPanel = new UserStatusPanel(userManagementService, this); - userStatusPanel.ensureDebugId("UserStatus"); - topInformationPanel.add(userStatusPanel, DockPanel.WEST); - topInformationPanel.add(persistentAlertLabel, DockPanel.CENTER); - final Anchor releaseNotesLink = new Anchor(new SafeHtmlBuilder().appendEscaped(stringMessages.releaseNotes()).toSafeHtml(), "/release_notes_admin.html"); - topInformationPanel.add(releaseNotesLink, DockPanel.EAST); - topInformationPanel.setCellHorizontalAlignment(releaseNotesLink, HasHorizontalAlignment.ALIGN_RIGHT); - - dockPanel.addNorth(topInformationPanel, 2.5); - - TabLayoutPanel tabPanel = new ScrolledTabLayoutPanel(2.5, Unit.EM, resources.arrowLeft(), resources.arrowRight(), 200); - tabPanel.ensureDebugId("AdministrationTabs"); - regattaDisplayers = new HashSet(); - - SailingEventManagementPanel sailingEventManagementPanel = new SailingEventManagementPanel(sailingService, this, - stringMessages); - sailingEventManagementPanel.setSize("90%", "90%"); - addScrollableTab(tabPanel, sailingEventManagementPanel, stringMessages.events()); - - RegattaStructureManagementPanel eventStructureManagementPanel = new RegattaStructureManagementPanel( - sailingService, this, stringMessages, this); - eventStructureManagementPanel.ensureDebugId("RegattaStructureManagement"); - eventStructureManagementPanel.setSize("90%", "90%"); - addScrollableTab(tabPanel, eventStructureManagementPanel, stringMessages.regattas()); - regattaDisplayers.add(eventStructureManagementPanel); - - TracTracEventManagementPanel tractracEventManagementPanel = new TracTracEventManagementPanel(sailingService, - this, this, stringMessages); - tractracEventManagementPanel.ensureDebugId("TracTracEventManagement"); - tractracEventManagementPanel.setSize("90%", "90%"); - addScrollableTab(tabPanel, tractracEventManagementPanel, stringMessages.tracTracEvents()); - regattaDisplayers.add(tractracEventManagementPanel); - - SwissTimingReplayConnectorPanel swissTimingReplayConnectorPanel = new SwissTimingReplayConnectorPanel( - sailingService, this, this, stringMessages); - swissTimingReplayConnectorPanel.setSize("90%", "90%"); - addScrollableTab(tabPanel, swissTimingReplayConnectorPanel, stringMessages.swissTimingArchiveConnector()); - regattaDisplayers.add(swissTimingReplayConnectorPanel); - - SwissTimingEventManagementPanel swisstimingEventManagementPanel = new SwissTimingEventManagementPanel( - sailingService, this, this, stringMessages); - // swisstimingEventManagementPanel.ensureDebugId("SwissTimingEventManagement"); - swisstimingEventManagementPanel.setSize("90%", "90%"); - addScrollableTab(tabPanel, swisstimingEventManagementPanel, stringMessages.swissTimingEvents()); - regattaDisplayers.add(swisstimingEventManagementPanel); - - IgtimiAccountsPanel igtimiAccountsPanel = new IgtimiAccountsPanel(sailingService, this, stringMessages); - igtimiAccountsPanel.ensureDebugId("IgtimiAccounts"); - igtimiAccountsPanel.setSize("90%", "90%"); - addScrollableTab(tabPanel, igtimiAccountsPanel, stringMessages.igtimiAccounts()); - - TrackedRacesManagementPanel trackedRacesManagementPanel = new TrackedRacesManagementPanel(sailingService, this, - this, stringMessages); - trackedRacesManagementPanel.ensureDebugId("TrackedRacesManagement"); - trackedRacesManagementPanel.setSize("90%", "90%"); - addScrollableTab(tabPanel, trackedRacesManagementPanel, stringMessages.trackedRaces()); - regattaDisplayers.add(trackedRacesManagementPanel); - - final CompetitorPanel competitorPanel = new CompetitorPanel(sailingService, stringMessages, this); - // trackedRacesManagementPanel.ensureDebugId("TrackedRacesManagement"); - trackedRacesManagementPanel.setSize("90%", "90%"); - addScrollableTab(tabPanel, competitorPanel, stringMessages.competitors()); - - RaceCourseManagementPanel raceCourseManagementPanel = new RaceCourseManagementPanel(sailingService, this, this, - stringMessages); - // raceCourseManagementPanel.ensureDebugId("RaceCourseManagement"); - raceCourseManagementPanel.setSize("90%", "90%"); - addScrollableTab(tabPanel, raceCourseManagementPanel, stringMessages.courseLayout()); - regattaDisplayers.add(raceCourseManagementPanel); - - final AsyncActionsExecutor asyncActionsExecutor = new AsyncActionsExecutor(); - - WindPanel windPanel = new WindPanel(sailingService, asyncActionsExecutor, this, this, stringMessages); - // windPanel.ensureDebugId("WindManagement"); - regattaDisplayers.add(windPanel); - windPanel.setSize("90%", "90%"); - addScrollableTab(tabPanel, windPanel, stringMessages.wind()); - - final LeaderboardGroupConfigPanel leaderboardGroupConfigPanel = new LeaderboardGroupConfigPanel(sailingService, - this, this, stringMessages); - leaderboardGroupConfigPanel.ensureDebugId("LeaderboardGroupConfiguration"); - leaderboardGroupConfigPanel.setSize("90%", "90%"); - addScrollableTab(tabPanel, leaderboardGroupConfigPanel, stringMessages.leaderboardGroupConfiguration()); - regattaDisplayers.add(leaderboardGroupConfigPanel); - - final LeaderboardConfigPanel leaderboardConfigPanel = new LeaderboardConfigPanel(sailingService, this, this, - stringMessages, - /* showRaceDetails */true); - leaderboardConfigPanel.ensureDebugId("LeaderboardConfiguration"); - leaderboardConfigPanel.setSize("90%", "90%"); - addScrollableTab(tabPanel, leaderboardConfigPanel, stringMessages.leaderboardConfiguration()); - regattaDisplayers.add(leaderboardConfigPanel); - - addScrollableTab(tabPanel, new ResultImportUrlsManagementPanel(sailingService, this, stringMessages), - stringMessages.resultImportUrls()); - - ReplicationPanel replicationPanel = new ReplicationPanel(sailingService, this, stringMessages); - // replicationPanel.ensureDebugId("ReplicationManagement"); - addScrollableTab(tabPanel, replicationPanel, stringMessages.replication()); - final MediaPanel mediaPanel = new MediaPanel(mediaService, this, stringMessages); - addScrollableTab(tabPanel, mediaPanel, stringMessages.mediaPanel()); - - final MasterDataImportPanel masterDataImportPanel = new MasterDataImportPanel(stringMessages, sailingService, - this, sailingEventManagementPanel, leaderboardGroupConfigPanel); - addScrollableTab(tabPanel, masterDataImportPanel, stringMessages.masterDataImportPanel()); - - /*final DeviceConfigurationPanel deviceConfigurationAdminPanel = new DeviceConfigurationPanel(sailingService, - stringMessages, this); - addScrollableTab(tabPanel, deviceConfigurationAdminPanel, stringMessages.deviceConfiguration() + " (admin)");*/ - - final DeviceConfigurationPanel deviceConfigurationUserPanel = new DeviceConfigurationUserPanel(sailingService, - stringMessages, this); - addScrollableTab(tabPanel, deviceConfigurationUserPanel, stringMessages.deviceConfiguration()); - - tabPanel.selectTab(0); - tabPanel.addSelectionHandler(new SelectionHandler() { - @Override - public void onSelection(SelectionEvent event) { - if (leaderboardConfigPanel.isVisible()) { - leaderboardConfigPanel.loadLeaderboards(); - } - if (mediaPanel.isVisible()) { - mediaPanel.onShow(); - } - if (competitorPanel.isVisible()) { - competitorPanel.refreshCompetitorList(); - } - } - }); - fillRegattas(); - - SystemInformationPanel sysinfoPanel = new SystemInformationPanel(sailingService, this); - sysinfoPanel.ensureDebugId("SystemInformation"); - dockPanel.addSouth(sysinfoPanel, 2.0); - dockPanel.add(tabPanel); - } - - private void addScrollableTab(TabLayoutPanel tabPanel, Widget widget, String tabTitle) { - ScrollPanel widgetScroller = new ScrollPanel(); - widgetScroller.add(widget); - tabPanel.add(widgetScroller, tabTitle, false); - } - - @Override - public void fillRegattas() { - sailingService.getRegattas(new AsyncCallback>() { - @Override - public void onSuccess(List result) { - for (RegattaDisplayer regattaDisplayer : regattaDisplayers) { - regattaDisplayer.fillRegattas(result); - } - } - - @Override - public void onFailure(Throwable caught) { - reportError("Remote Procedure Call getRegattas() - Failure"); - } - }); - } -} +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.logical.shared.SelectionEvent; +import com.google.gwt.event.logical.shared.SelectionHandler; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.rpc.ServiceDefTarget; +import com.google.gwt.user.client.ui.Anchor; +import com.google.gwt.user.client.ui.DockLayoutPanel; +import com.google.gwt.user.client.ui.DockPanel; +import com.google.gwt.user.client.ui.HasHorizontalAlignment; +import com.google.gwt.user.client.ui.RootLayoutPanel; +import com.google.gwt.user.client.ui.ScrollPanel; +import com.google.gwt.user.client.ui.TabLayoutPanel; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.gwt.ui.client.AbstractEntryPoint; +import com.sap.sailing.gwt.ui.client.MediaService; +import com.sap.sailing.gwt.ui.client.MediaServiceAsync; +import com.sap.sailing.gwt.ui.client.RegattaDisplayer; +import com.sap.sailing.gwt.ui.client.RegattaRefresher; +import com.sap.sailing.gwt.ui.client.RemoteServiceMappingConstants; +import com.sap.sailing.gwt.ui.client.SailingService; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.UserManagementService; +import com.sap.sailing.gwt.ui.client.UserManagementServiceAsync; +import com.sap.sailing.gwt.ui.client.shared.controls.ScrolledTabLayoutPanel; +import com.sap.sailing.gwt.ui.client.shared.panels.SystemInformationPanel; +import com.sap.sailing.gwt.ui.client.shared.panels.UserStatusPanel; +import com.sap.sailing.gwt.ui.masterdataimport.MasterDataImportPanel; +import com.sap.sailing.gwt.ui.shared.BetterDateTimeBox; +import com.sap.sailing.gwt.ui.shared.RegattaDTO; +import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.async.MarkedAsyncCallback; + +public class AdminConsoleEntryPoint extends AbstractEntryPoint implements RegattaRefresher { + private Set regattaDisplayers; + + private final AdminConsoleResources resources = GWT.create(AdminConsoleResources.class); + + private final SailingServiceAsync sailingService = GWT.create(SailingService.class); + private final MediaServiceAsync mediaService = GWT.create(MediaService.class); + private final UserManagementServiceAsync userManagementService = GWT.create(UserManagementService.class); + + @Override + protected void doOnModuleLoad() { + super.doOnModuleLoad(); + + registerASyncService((ServiceDefTarget) userManagementService, RemoteServiceMappingConstants.userManagementServiceRemotePath); + registerASyncService((ServiceDefTarget) sailingService, RemoteServiceMappingConstants.sailingServiceRemotePath); + registerASyncService((ServiceDefTarget) mediaService, RemoteServiceMappingConstants.mediaServiceRemotePath); + + BetterDateTimeBox.initialize(); + + RootLayoutPanel rootPanel = RootLayoutPanel.get(); + DockLayoutPanel dockPanel = new DockLayoutPanel(Unit.EM); + rootPanel.add(dockPanel); + + DockPanel topInformationPanel = new DockPanel(); + topInformationPanel.setSize("100%", "95%"); + UserStatusPanel userStatusPanel = new UserStatusPanel(userManagementService, this); + userStatusPanel.ensureDebugId("UserStatus"); + topInformationPanel.add(userStatusPanel, DockPanel.WEST); + topInformationPanel.add(persistentAlertLabel, DockPanel.CENTER); + final Anchor releaseNotesLink = new Anchor(new SafeHtmlBuilder().appendEscaped(stringMessages.releaseNotes()).toSafeHtml(), "/release_notes_admin.html"); + topInformationPanel.add(releaseNotesLink, DockPanel.EAST); + topInformationPanel.setCellHorizontalAlignment(releaseNotesLink, HasHorizontalAlignment.ALIGN_RIGHT); + + dockPanel.addNorth(topInformationPanel, 2.5); + + TabLayoutPanel tabPanel = new ScrolledTabLayoutPanel(2.5, Unit.EM, resources.arrowLeft(), resources.arrowRight(), 200); + tabPanel.ensureDebugId("AdministrationTabs"); + regattaDisplayers = new HashSet(); + + SailingEventManagementPanel sailingEventManagementPanel = new SailingEventManagementPanel(sailingService, this, + stringMessages); + sailingEventManagementPanel.setSize("90%", "90%"); + addScrollableTab(tabPanel, sailingEventManagementPanel, stringMessages.events()); + + RegattaStructureManagementPanel eventStructureManagementPanel = new RegattaStructureManagementPanel( + sailingService, this, stringMessages, this); + eventStructureManagementPanel.ensureDebugId("RegattaStructureManagement"); + eventStructureManagementPanel.setSize("90%", "90%"); + addScrollableTab(tabPanel, eventStructureManagementPanel, stringMessages.regattas()); + regattaDisplayers.add(eventStructureManagementPanel); + + TracTracEventManagementPanel tractracEventManagementPanel = new TracTracEventManagementPanel(sailingService, + this, this, stringMessages); + tractracEventManagementPanel.ensureDebugId("TracTracEventManagement"); + tractracEventManagementPanel.setSize("90%", "90%"); + addScrollableTab(tabPanel, tractracEventManagementPanel, stringMessages.tracTracEvents()); + regattaDisplayers.add(tractracEventManagementPanel); + + SwissTimingReplayConnectorPanel swissTimingReplayConnectorPanel = new SwissTimingReplayConnectorPanel( + sailingService, this, this, stringMessages); + swissTimingReplayConnectorPanel.setSize("90%", "90%"); + addScrollableTab(tabPanel, swissTimingReplayConnectorPanel, stringMessages.swissTimingArchiveConnector()); + regattaDisplayers.add(swissTimingReplayConnectorPanel); + + SwissTimingEventManagementPanel swisstimingEventManagementPanel = new SwissTimingEventManagementPanel( + sailingService, this, this, stringMessages); + // swisstimingEventManagementPanel.ensureDebugId("SwissTimingEventManagement"); + swisstimingEventManagementPanel.setSize("90%", "90%"); + addScrollableTab(tabPanel, swisstimingEventManagementPanel, stringMessages.swissTimingEvents()); + regattaDisplayers.add(swisstimingEventManagementPanel); + + final RaceLogTrackingEventManagementPanel raceLogTrackingEventManagementPanel = new RaceLogTrackingEventManagementPanel( + sailingService, this, this, this, stringMessages); + raceLogTrackingEventManagementPanel.setSize("90%", "90%"); + addScrollableTab(tabPanel, raceLogTrackingEventManagementPanel, stringMessages.raceLogTracking()); + regattaDisplayers.add(raceLogTrackingEventManagementPanel); + + IgtimiAccountsPanel igtimiAccountsPanel = new IgtimiAccountsPanel(sailingService, this, stringMessages); + igtimiAccountsPanel.ensureDebugId("IgtimiAccounts"); + igtimiAccountsPanel.setSize("90%", "90%"); + addScrollableTab(tabPanel, igtimiAccountsPanel, stringMessages.igtimiAccounts()); + + TrackedRacesManagementPanel trackedRacesManagementPanel = new TrackedRacesManagementPanel(sailingService, this, + this, stringMessages); + trackedRacesManagementPanel.ensureDebugId("TrackedRacesManagement"); + trackedRacesManagementPanel.setSize("90%", "90%"); + addScrollableTab(tabPanel, trackedRacesManagementPanel, stringMessages.trackedRaces()); + regattaDisplayers.add(trackedRacesManagementPanel); + + final CompetitorPanel competitorPanel = new CompetitorPanel(sailingService, stringMessages, this); + // trackedRacesManagementPanel.ensureDebugId("TrackedRacesManagement"); + trackedRacesManagementPanel.setSize("90%", "90%"); + addScrollableTab(tabPanel, competitorPanel, stringMessages.competitors()); + + RaceCourseManagementPanel raceCourseManagementPanel = new RaceCourseManagementPanel(sailingService, this, this, + stringMessages); + // raceCourseManagementPanel.ensureDebugId("RaceCourseManagement"); + raceCourseManagementPanel.setSize("90%", "90%"); + addScrollableTab(tabPanel, raceCourseManagementPanel, stringMessages.courseLayout()); + regattaDisplayers.add(raceCourseManagementPanel); + + final AsyncActionsExecutor asyncActionsExecutor = new AsyncActionsExecutor(); + + WindPanel windPanel = new WindPanel(sailingService, asyncActionsExecutor, this, this, stringMessages); + // windPanel.ensureDebugId("WindManagement"); + regattaDisplayers.add(windPanel); + windPanel.setSize("90%", "90%"); + addScrollableTab(tabPanel, windPanel, stringMessages.wind()); + + final LeaderboardGroupConfigPanel leaderboardGroupConfigPanel = new LeaderboardGroupConfigPanel(sailingService, + this, this, stringMessages); + leaderboardGroupConfigPanel.ensureDebugId("LeaderboardGroupConfiguration"); + leaderboardGroupConfigPanel.setSize("90%", "90%"); + addScrollableTab(tabPanel, leaderboardGroupConfigPanel, stringMessages.leaderboardGroupConfiguration()); + regattaDisplayers.add(leaderboardGroupConfigPanel); + + final LeaderboardConfigPanel leaderboardConfigPanel = new LeaderboardConfigPanel(sailingService, this, this, + stringMessages, + /* showRaceDetails */true); + leaderboardConfigPanel.ensureDebugId("LeaderboardConfiguration"); + leaderboardConfigPanel.setSize("90%", "90%"); + addScrollableTab(tabPanel, leaderboardConfigPanel, stringMessages.leaderboardConfiguration()); + regattaDisplayers.add(leaderboardConfigPanel); + + addScrollableTab(tabPanel, new ResultImportUrlsManagementPanel(sailingService, this, stringMessages), + stringMessages.resultImportUrls()); + + ReplicationPanel replicationPanel = new ReplicationPanel(sailingService, this, stringMessages); + // replicationPanel.ensureDebugId("ReplicationManagement"); + addScrollableTab(tabPanel, replicationPanel, stringMessages.replication()); + final MediaPanel mediaPanel = new MediaPanel(mediaService, this, stringMessages); + addScrollableTab(tabPanel, mediaPanel, stringMessages.mediaPanel()); + + final MasterDataImportPanel masterDataImportPanel = new MasterDataImportPanel(stringMessages, sailingService, + this, sailingEventManagementPanel, leaderboardGroupConfigPanel); + addScrollableTab(tabPanel, masterDataImportPanel, stringMessages.masterDataImportPanel()); + + /*final DeviceConfigurationPanel deviceConfigurationAdminPanel = new DeviceConfigurationPanel(sailingService, + stringMessages, this); + addScrollableTab(tabPanel, deviceConfigurationAdminPanel, stringMessages.deviceConfiguration() + " (admin)");*/ + + final DeviceConfigurationPanel deviceConfigurationUserPanel = new DeviceConfigurationUserPanel(sailingService, + stringMessages, this); + addScrollableTab(tabPanel, deviceConfigurationUserPanel, stringMessages.deviceConfiguration()); + + tabPanel.selectTab(0); + tabPanel.addSelectionHandler(new SelectionHandler() { + @Override + public void onSelection(SelectionEvent event) { + if (leaderboardConfigPanel.isVisible()) { + leaderboardConfigPanel.loadLeaderboards(); + } + if (mediaPanel.isVisible()) { + mediaPanel.onShow(); + } + if (competitorPanel.isVisible()) { + competitorPanel.refreshCompetitorList(); + } + if (raceLogTrackingEventManagementPanel.isVisible()) { + raceLogTrackingEventManagementPanel.loadLeaderboards(); + } + } + }); + fillRegattas(); + + SystemInformationPanel sysinfoPanel = new SystemInformationPanel(sailingService, this); + sysinfoPanel.ensureDebugId("SystemInformation"); + dockPanel.addSouth(sysinfoPanel, 2.0); + dockPanel.add(tabPanel); + } + + private void addScrollableTab(TabLayoutPanel tabPanel, Widget widget, String tabTitle) { + ScrollPanel widgetScroller = new ScrollPanel(); + widgetScroller.add(widget); + tabPanel.add(widgetScroller, tabTitle, false); + } + + @Override + public void fillRegattas() { + sailingService.getRegattas(new MarkedAsyncCallback>( + new AsyncCallback>() { + @Override + public void onSuccess(List result) { + for (RegattaDisplayer regattaDisplayer : regattaDisplayers) { + regattaDisplayer.fillRegattas(result); + } + } + + @Override + public void onFailure(Throwable caught) { + reportError("Remote Procedure Call getRegattas() - Failure"); + } + })); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleResources.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleResources.java index 1a486f4eba0..5a8d8debba0 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleResources.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleResources.java @@ -61,4 +61,37 @@ interface AdminConsoleResources extends ClientBundle { @Source("com/sap/sailing/gwt/ui/client/images/xml.png") ImageResource exportXMLIcon(); + + @Source("com/sap/sailing/gwt/ui/client/images/add_racelog_tracker.png") + ImageResource addRaceLogTracker(); + + @Source("com/sap/sailing/gwt/ui/client/images/denote_for_racelog_tracking.png") + ImageResource denoteForRaceLogTracking(); + + @Source("com/sap/sailing/gwt/ui/client/images/undenote_for_racelog_tracking.png") + ImageResource unDenoteForRaceLogTracking(); + + @Source("com/sap/sailing/gwt/ui/client/images/start_racelog_tracking.png") + ImageResource startRaceLogTracking(); + + @Source("com/sap/sailing/gwt/ui/client/images/competitor_registrations.png") + ImageResource competitorRegistrations(); + + @Source("com/sap/sailing/gwt/ui/client/images/define_course.png") + ImageResource defineCourse(); + + @Source("com/sap/sailing/gwt/ui/client/images/map_devices.png") + ImageResource mapDevices(); + + @Source("com/sap/sailing/gwt/ui/client/images/ping.png") + ImageResource ping(); + + @Source("com/sap/sailing/gwt/ui/client/images/copy_course.png") + ImageResource copyCourse(); + + @Source("com/sap/sailing/gwt/ui/client/images/copy.png") + ImageResource copy(); + + @Source("com/sap/sailing/gwt/ui/client/images/close_time_range.png") + ImageResource closeTimeRange(); } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ColorColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ColorColumn.java new file mode 100644 index 00000000000..495f9183565 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ColorColumn.java @@ -0,0 +1,34 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import com.google.gwt.cell.client.SafeHtmlCell; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; +import com.google.gwt.user.cellview.client.Column; +import com.sap.sailing.domain.common.Color; + +public class ColorColumn extends Column { + public interface ColorRetriever { + Color getColor(T t); + } + + private final ColorRetriever colorRetriever; + + public ColorColumn(ColorRetriever colorRetriever) { + super(new SafeHtmlCell()); + this.colorRetriever = colorRetriever; + } + + @Override + public SafeHtml getValue(T t) { + SafeHtmlBuilder sb = new SafeHtmlBuilder(); + if (colorRetriever.getColor(t) != null) { + String colorString = colorRetriever.getColor(t).getAsHtml(); + sb.appendHtmlConstant(""); + sb.appendHtmlConstant(colorString); + sb.appendHtmlConstant(""); + } else { + sb.appendHtmlConstant(" "); + } + return sb.toSafeHtml(); + } +}; \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorEditDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorEditDialog.java index 184c2746ac1..0a1c26f5339 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorEditDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorEditDialog.java @@ -13,6 +13,7 @@ import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.domain.common.Color; import com.sap.sailing.domain.common.CountryCode; import com.sap.sailing.domain.common.CountryCodeFactory; +import com.sap.sailing.domain.common.dto.BoatClassDTO; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.domain.common.dto.CompetitorDTOImpl; import com.sap.sailing.domain.common.impl.RGBColor; @@ -33,6 +34,7 @@ import com.sap.sse.gwt.client.dialog.DataEntryDialog; public class CompetitorEditDialog extends DataEntryDialog { private final CompetitorDTO competitorToEdit; private final TextBox name; + private final ListBox boatClassName; private final TextBox displayColorTextBox; private final ListBox threeLetterIocCountryCode; private final TextBox sailId; @@ -54,17 +56,43 @@ public class CompetitorEditDialog extends DataEntryDialog { if (displayColor instanceof InvalidColor) { result = displayColor.getAsHtml(); } + } else if (valueToValidate.getBoatClass().getName() == null || valueToValidate.getBoatClass().getName().isEmpty()) { + result = stringMessages.pleaseEnterABoatClass(); } return result; } }, /* animationEnabled */true, callback); this.stringMessages = stringMessages; this.competitorToEdit = competitorToEdit; + + this.boatClassName = createListBox(/* isMultipleSelect */ false); + int i=0; + List boatClassNamesList = new ArrayList(); + for (BoatTypes t : BoatTypes.values()) { + boatClassNamesList.add(t.getName()); + } + String competitorsBoatClassName = competitorToEdit.getBoatClass() != null ? competitorToEdit.getBoatClass().getName() : null; + Collections.sort(boatClassNamesList); + for (String name : boatClassNamesList) { + boatClassName.addItem(name); + if (name.equals(competitorsBoatClassName)) { + boatClassName.setSelectedIndex(i); + } + i++; + } + if (boatClassName.getSelectedIndex() == -1 && competitorsBoatClassName != null) { + boatClassName.addItem(competitorsBoatClassName); + boatClassName.setSelectedIndex(i); + } + if (competitorToEdit.getBoatClass() != null) { + boatClassName.setEnabled(false); + } + this.name = createTextBox(competitorToEdit.getName()); this.displayColorTextBox = createTextBox(competitorToEdit.getColor() == null ? "" : competitorToEdit.getColor().getAsHtml()); this.threeLetterIocCountryCode = createListBox(/* isMultipleSelect */ false); CountryCodeFactory ccf = CountryCodeFactory.INSTANCE; - int i=0; + i=0; List ccs = new ArrayList(); Util.addAll(ccf.getAll(), ccs); ccs.add(null); // representing no nationality (NONE / white flag) @@ -132,22 +160,27 @@ public class CompetitorEditDialog extends DataEntryDialog { @Override protected CompetitorDTO getResult() { Color color; - try { - color = new RGBColor(displayColorTextBox.getText()); - } catch (IllegalArgumentException iae) { - color = new InvalidColor(iae); + if (displayColorTextBox.getText() == null || displayColorTextBox.getText().isEmpty()) { + color = null; + } else { + try { + color = new RGBColor(displayColorTextBox.getText()); + } catch (IllegalArgumentException iae) { + color = new InvalidColor(iae); + } } + BoatClassDTO boatClass = new BoatClassDTO(boatClassName.getValue(boatClassName.getSelectedIndex()), 0); CompetitorDTO result = new CompetitorDTOImpl(name.getText(), color, /* twoLetterIsoCountryCode */ null, threeLetterIocCountryCode.getValue(threeLetterIocCountryCode.getSelectedIndex()), /* countryName */ null, sailId.getText(), competitorToEdit.getIdAsString(), - competitorToEdit.getBoatClass()); + boatClass); return result; } @Override protected Widget getAdditionalWidget() { - Grid result = new Grid(4, 2); + Grid result = new Grid(5, 2); result.setWidget(0, 0, new Label(stringMessages.name())); result.setWidget(0, 1, name); result.setWidget(1, 0, new Label(stringMessages.sailNumber())); @@ -156,6 +189,8 @@ public class CompetitorEditDialog extends DataEntryDialog { result.setWidget(2, 1, threeLetterIocCountryCode); result.setWidget(3, 0, new Label(stringMessages.color())); result.setWidget(3, 1, displayColorTextBox); + result.setWidget(4, 0, new Label(stringMessages.boatClass())); + result.setWidget(4, 1, boatClassName); return result; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorPanel.java index 3a243f8d61f..6ee5aa00b0f 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorPanel.java @@ -2,41 +2,26 @@ package com.sap.sailing.gwt.ui.adminconsole; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.List; import com.google.gwt.cell.client.FieldUpdater; -import com.google.gwt.cell.client.SafeHtmlCell; -import com.google.gwt.core.client.GWT; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.resources.client.ImageResource; -import com.google.gwt.safehtml.shared.SafeHtml; -import com.google.gwt.safehtml.shared.SafeHtmlBuilder; -import com.google.gwt.user.cellview.client.CellTable; -import com.google.gwt.user.cellview.client.Column; -import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler; -import com.google.gwt.user.cellview.client.TextColumn; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.HorizontalPanel; -import com.google.gwt.user.client.ui.ImageResourceRenderer; -import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.VerticalPanel; -import com.google.gwt.view.client.ListDataProvider; import com.google.gwt.view.client.MultiSelectionModel; import com.google.gwt.view.client.SelectionChangeEvent; import com.google.gwt.view.client.SelectionChangeEvent.Handler; import com.sap.sailing.domain.common.dto.CompetitorDTO; -import com.sap.sailing.domain.common.impl.NaturalComparator; +import com.sap.sailing.domain.common.dto.CompetitorDTOImpl; import com.sap.sailing.domain.common.impl.Util; import com.sap.sailing.gwt.ui.client.ErrorReporter; -import com.sap.sailing.gwt.ui.client.FlagImageResolver; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.shared.panels.LabeledAbstractFilterablePanel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; /** @@ -49,14 +34,9 @@ public class CompetitorPanel extends SimplePanel { private final SailingServiceAsync sailingService; private final StringMessages stringMessages; private final ErrorReporter errorReporter; - private CellTable competitorTable; - private MultiSelectionModel competitorSelectionModel; - private ListDataProvider competitorProvider; - private List allCompetitors; + private final CompetitorTableWrapper competitorTable; + private final MultiSelectionModel competitorSelectionModel; private final String leaderboardName; - private LabeledAbstractFilterablePanel filterField; - - private final AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); public CompetitorPanel(final SailingServiceAsync sailingService, final StringMessages stringMessages, final ErrorReporter errorReporter) { @@ -70,8 +50,8 @@ public class CompetitorPanel extends SimplePanel { this.stringMessages = stringMessages; this.errorReporter = errorReporter; this.leaderboardName = leaderboardName; - competitorProvider = new ListDataProvider(); - ListHandler competitorColumnListHandler = new ListHandler(competitorProvider.getList()); + this.competitorTable = new CompetitorTableWrapper(sailingService, stringMessages, errorReporter); + this.competitorSelectionModel = competitorTable.getSelectionModel(); VerticalPanel mainPanel = new VerticalPanel(); this.setWidget(mainPanel); mainPanel.setWidth("100%"); @@ -96,80 +76,18 @@ public class CompetitorPanel extends SimplePanel { } }); buttonPanel.add(allowReloadButton); + Button addCompetitorButton = new Button(stringMessages.add()); + addCompetitorButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + openAddCompetitorDialog(); + } + }); + buttonPanel.add(addCompetitorButton); competitorsPanel.add(buttonPanel); - // competitors table - TextColumn competitorNameColumn = new TextColumn() { - @Override - public String getValue(CompetitorDTO competitor) { - return competitor.getName(); - } - }; - competitorNameColumn.setSortable(true); - competitorColumnListHandler.setComparator(competitorNameColumn, new Comparator() { - @Override - public int compare(CompetitorDTO o1, CompetitorDTO o2) { - return o1.getName().compareTo(o2.getName()); - } - }); - - TextColumn boatClassColumn = new TextColumn() { - @Override - public String getValue(CompetitorDTO competitor) { - return competitor.getBoatClass() != null ? competitor.getBoatClass().getName() : ""; - } - }; - boatClassColumn.setSortable(true); - competitorColumnListHandler.setComparator(boatClassColumn, new Comparator() { - @Override - public int compare(CompetitorDTO o1, CompetitorDTO o2) { - return new NaturalComparator(false).compare(o1.getBoatClass().getName(), o2.getBoatClass().getName()); - } - }); - - Column sailIdColumn = new Column(new SafeHtmlCell()) { - @Override - public SafeHtml getValue(CompetitorDTO competitor) { - SafeHtmlBuilder sb = new SafeHtmlBuilder(); - ImageResourceRenderer renderer = new ImageResourceRenderer(); - final String twoLetterIsoCountryCode = competitor.getTwoLetterIsoCountryCode(); - final ImageResource flagImageResource; - if (twoLetterIsoCountryCode==null || twoLetterIsoCountryCode.isEmpty()) { - flagImageResource = FlagImageResolver.getEmptyFlagImageResource(); - } else { - flagImageResource = FlagImageResolver.getFlagImageResource(twoLetterIsoCountryCode); - } - if (flagImageResource != null) { - sb.append(renderer.render(flagImageResource)); - sb.appendHtmlConstant(" "); - } - sb.appendEscaped(competitor.getSailID()); - return sb.toSafeHtml(); - } - }; - sailIdColumn.setSortable(true); - competitorColumnListHandler.setComparator(sailIdColumn, new Comparator() { - @Override - public int compare(CompetitorDTO o1, CompetitorDTO o2) { - return new NaturalComparator(false).compare(o1.getSailID(), o2.getSailID()); - } - }); - - Column displayColorColumn = new Column(new SafeHtmlCell()) { - @Override - public SafeHtml getValue(CompetitorDTO competitor) { - SafeHtmlBuilder sb = new SafeHtmlBuilder(); - if (competitor.getColor() != null) { - sb.appendHtmlConstant(""); - sb.appendHtmlConstant(competitor.getColor().getAsHtml()); - sb.appendHtmlConstant(""); - } else { - sb.appendHtmlConstant(" "); - } - return sb.toSafeHtml(); - } - }; + //competitor table ImagesBarColumn competitorActionColumn = new ImagesBarColumn( new CompetitorConfigImagesBarCell(stringMessages)); competitorActionColumn.setFieldUpdater(new FieldUpdater() { @@ -184,46 +102,7 @@ public class CompetitorPanel extends SimplePanel { }); - TextColumn competitorIdColumn = new TextColumn() { - @Override - public String getValue(CompetitorDTO competitor) { - return competitor.getIdAsString(); - } - - }; - - competitorIdColumn.setSortable(true); - competitorColumnListHandler.setComparator(competitorIdColumn, new Comparator() { - @Override - public int compare(CompetitorDTO o1, CompetitorDTO o2) { - return new NaturalComparator(false).compare(o1.getIdAsString(), o2.getIdAsString()); - } - }); - competitorTable = new CellTable(10000, tableRes); - filterField = new LabeledAbstractFilterablePanel(new Label(stringMessages.filterCompetitors()), - allCompetitors, competitorTable, competitorProvider) { - - @Override - public Iterable getSearchableStrings(CompetitorDTO t) { - List string = new ArrayList(); - string.add(t.getName()); - string.add(t.getSailID()); - string.add(t.getBoatClass().getName()); - return string; - } - }; - - mainPanel.add(filterField); - competitorProvider.addDataDisplay(competitorTable); - competitorTable.addColumnSortHandler(competitorColumnListHandler); - competitorTable.addColumn(sailIdColumn, stringMessages.sailNumber()); - competitorTable.addColumn(competitorNameColumn, stringMessages.name()); - competitorTable.addColumn(boatClassColumn, stringMessages.boatClass()); - competitorTable.addColumn(displayColorColumn, stringMessages.color()); - competitorTable.addColumn(competitorIdColumn, stringMessages.id()); - competitorTable.addColumn(competitorActionColumn, stringMessages.actions()); - competitorSelectionModel = new MultiSelectionModel(); - competitorTable.setSelectionModel(competitorSelectionModel); + competitorTable.getTable().addColumn(competitorActionColumn, stringMessages.actions()); mainPanel.add(competitorTable); competitorSelectionModel.addSelectionChangeHandler(new Handler() { @Override @@ -252,11 +131,15 @@ public class CompetitorPanel extends SimplePanel { }); } + private void openAddCompetitorDialog() { + openEditCompetitorDialog(new CompetitorDTOImpl()); + } + private void openEditCompetitorDialog(CompetitorDTO competitor) { new CompetitorEditDialog(stringMessages, competitor, new DialogCallback() { @Override public void ok(CompetitorDTO competitor) { - sailingService.updateCompetitor(competitor, new AsyncCallback() { + sailingService.addOrUpdateCompetitor(competitor, new AsyncCallback() { @Override public void onFailure(Throwable caught) { errorReporter.reportError("Error trying to update competitor: "+caught.getMessage()); @@ -264,14 +147,7 @@ public class CompetitorPanel extends SimplePanel { @Override public void onSuccess(CompetitorDTO updatedCompetitor) { - // replace updated competitor in competitor list - for (int i=0; i>() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError("Remote Procedure Call getCompetitors() - Failure: " + caught.getMessage()); - } - - @Override - public void onSuccess(Iterable result) { - getFilteredCompetitors(result); - } - }); - } else { - sailingService.getCompetitors(new AsyncCallback>() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError("Remote Procedure Call getCompetitors() - Failure: " + caught.getMessage()); - } - - @Override - public void onSuccess(Iterable result) { - getFilteredCompetitors(result); - } - }); - } - } - - private void getFilteredCompetitors(Iterable result) { - allCompetitors = new ArrayList(); - for (CompetitorDTO c : result) { - allCompetitors.add(c); - } - filterField.updateAll(allCompetitors); + + public void refreshCompetitorList() { + competitorTable.refreshCompetitorList(leaderboardName); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java new file mode 100644 index 00000000000..38fee020d78 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java @@ -0,0 +1,201 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +import com.google.gwt.cell.client.SafeHtmlCell; +import com.google.gwt.core.client.Callback; +import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; +import com.google.gwt.user.cellview.client.Column; +import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler; +import com.google.gwt.user.cellview.client.TextColumn; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.ImageResourceRenderer; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.view.client.MultiSelectionModel; +import com.sap.sailing.domain.common.Color; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.domain.common.impl.NaturalComparator; +import com.sap.sailing.gwt.ui.adminconsole.ColorColumn.ColorRetriever; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.FlagImageResolver; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.panels.LabeledAbstractFilterablePanel; + +public class CompetitorTableWrapper extends TableWrapper> { + private final LabeledAbstractFilterablePanel filterField; + + public CompetitorTableWrapper(SailingServiceAsync sailingService, StringMessages stringMessages,ErrorReporter errorReporter) { + super(sailingService, stringMessages, errorReporter, new MultiSelectionModel()); + + ListHandler competitorColumnListHandler = new ListHandler(dataProvider.getList()); + + // competitors table + TextColumn competitorNameColumn = new TextColumn() { + @Override + public String getValue(CompetitorDTO competitor) { + return competitor.getName(); + } + }; + competitorNameColumn.setSortable(true); + competitorColumnListHandler.setComparator(competitorNameColumn, new Comparator() { + @Override + public int compare(CompetitorDTO o1, CompetitorDTO o2) { + return o1.getName().compareTo(o2.getName()); + } + }); + + TextColumn boatClassColumn = new TextColumn() { + @Override + public String getValue(CompetitorDTO competitor) { + return competitor.getBoatClass() != null ? competitor.getBoatClass().getName() : ""; + } + }; + boatClassColumn.setSortable(true); + competitorColumnListHandler.setComparator(boatClassColumn, new Comparator() { + @Override + public int compare(CompetitorDTO o1, CompetitorDTO o2) { + return new NaturalComparator(false).compare(o1.getBoatClass().getName(), o2.getBoatClass().getName()); + } + }); + + Column sailIdColumn = new Column(new SafeHtmlCell()) { + @Override + public SafeHtml getValue(CompetitorDTO competitor) { + SafeHtmlBuilder sb = new SafeHtmlBuilder(); + ImageResourceRenderer renderer = new ImageResourceRenderer(); + final String twoLetterIsoCountryCode = competitor.getTwoLetterIsoCountryCode(); + final ImageResource flagImageResource; + if (twoLetterIsoCountryCode==null || twoLetterIsoCountryCode.isEmpty()) { + flagImageResource = FlagImageResolver.getEmptyFlagImageResource(); + } else { + flagImageResource = FlagImageResolver.getFlagImageResource(twoLetterIsoCountryCode); + } + if (flagImageResource != null) { + sb.append(renderer.render(flagImageResource)); + sb.appendHtmlConstant(" "); + } + sb.appendEscaped(competitor.getSailID()); + return sb.toSafeHtml(); + } + }; + sailIdColumn.setSortable(true); + competitorColumnListHandler.setComparator(sailIdColumn, new Comparator() { + @Override + public int compare(CompetitorDTO o1, CompetitorDTO o2) { + return new NaturalComparator(false).compare(o1.getSailID(), o2.getSailID()); + } + }); + + Column displayColorColumn = new ColorColumn<>(new ColorRetriever() { + @Override + public Color getColor(CompetitorDTO t) { + return t.getColor(); + } + }); + + TextColumn competitorIdColumn = new TextColumn() { + @Override + public String getValue(CompetitorDTO competitor) { + return competitor.getIdAsString(); + } + + }; + + competitorIdColumn.setSortable(true); + competitorColumnListHandler.setComparator(competitorIdColumn, new Comparator() { + @Override + public int compare(CompetitorDTO o1, CompetitorDTO o2) { + return new NaturalComparator(false).compare(o1.getIdAsString(), o2.getIdAsString()); + } + }); + + filterField = new LabeledAbstractFilterablePanel(new Label(stringMessages.filterCompetitors()), + new ArrayList(), table, dataProvider) { + + @Override + public Iterable getSearchableStrings(CompetitorDTO t) { + List string = new ArrayList(); + string.add(t.getName()); + string.add(t.getSailID()); + string.add(t.getBoatClass().getName()); + return string; + } + }; + + mainPanel.add(filterField); + table.addColumnSortHandler(competitorColumnListHandler); + table.addColumn(sailIdColumn, stringMessages.sailNumber()); + table.addColumn(competitorNameColumn, stringMessages.name()); + table.addColumn(boatClassColumn, stringMessages.boatClass()); + table.addColumn(displayColorColumn, stringMessages.color()); + table.addColumn(competitorIdColumn, stringMessages.id()); + mainPanel.add(table); + } + + public Collection getAllCompetitors() { + return filterField.getAll(); + } + + public LabeledAbstractFilterablePanel getFilterField() { + return filterField; + } + + public void refreshCompetitorList(Iterable competitors) { + getFilteredCompetitors(competitors); + } + + public void refreshCompetitorList(String leaderboardName) { + refreshCompetitorList(leaderboardName, false, null); + } + + public void refreshCompetitorList(String leaderboardName, boolean lookInRaceLogs) { + refreshCompetitorList(leaderboardName, lookInRaceLogs, null); + } + + /** + * @param leaderboardName If null, all existing competitors are loaded + */ + public void refreshCompetitorList(String leaderboardName, boolean lookInRaceLogs, final Callback, + Throwable> callback) { + if(leaderboardName != null) { + sailingService.getCompetitorsOfLeaderboard(leaderboardName, lookInRaceLogs, new AsyncCallback>() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Remote Procedure Call getCompetitors() - Failure: " + caught.getMessage()); + if (callback != null) callback.onFailure(caught); + } + + @Override + public void onSuccess(Iterable result) { + refreshCompetitorList(result); + if (callback != null) callback.onSuccess(result); + } + }); + } else { + sailingService.getCompetitors(new AsyncCallback>() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Remote Procedure Call getCompetitors() - Failure: " + caught.getMessage()); + if (callback != null) callback.onFailure(caught); + } + + @Override + public void onSuccess(Iterable result) { + getFilteredCompetitors(result); + refreshCompetitorList(result); + if (callback != null) callback.onSuccess(result); + } + }); + } + } + + private void getFilteredCompetitors(Iterable result) { + filterField.updateAll(result); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java new file mode 100644 index 00000000000..1002b056cbc --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java @@ -0,0 +1,41 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import com.google.gwt.user.cellview.client.TextColumn; +import com.google.gwt.view.client.SelectionModel; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.ControlPointDTO; +import com.sap.sailing.gwt.ui.shared.GateDTO; +import com.sap.sailing.gwt.ui.shared.MarkDTO; + +public class ControlPointTableWrapper> extends TableWrapper { + public ControlPointTableWrapper(S selectionModel, SailingServiceAsync sailingService, final StringMessages stringMessages, + ErrorReporter errorReporter) { + super(sailingService, stringMessages, errorReporter, selectionModel); + + mainPanel.add(table); + + TextColumn nameColumn = new TextColumn() { + @Override + public String getValue(ControlPointDTO d) { + return d.getName(); + } + }; + table.addColumn(nameColumn, stringMessages.name()); + + TextColumn typeColumn = new TextColumn() { + @Override + public String getValue(ControlPointDTO d) { + if (d instanceof GateDTO) { + return "2x " + stringMessages.mark(); + } else if (d instanceof MarkDTO) { + return stringMessages.mark(); + } else { + return stringMessages.unknown(); + } + } + }; + table.addColumn(typeColumn, stringMessages.type()); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CourseManagementWidget.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CourseManagementWidget.java new file mode 100644 index 00000000000..c3c1725292c --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CourseManagementWidget.java @@ -0,0 +1,284 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gwt.cell.client.FieldUpdater; +import com.google.gwt.core.shared.GWT; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.CaptionPanel; +import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.HasVerticalAlignment; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.IsWidget; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.view.client.MultiSelectionModel; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SelectionChangeEvent.Handler; +import com.google.gwt.view.client.SetSelectionModel; +import com.google.gwt.view.client.SingleSelectionModel; +import com.sap.sailing.domain.common.PassingInstruction; +import com.sap.sailing.domain.common.impl.Util; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.ControlPointDTO; +import com.sap.sailing.gwt.ui.shared.GateDTO; +import com.sap.sailing.gwt.ui.shared.MarkDTO; +import com.sap.sailing.gwt.ui.shared.RaceCourseDTO; +import com.sap.sailing.gwt.ui.shared.WaypointDTO; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; + +public abstract class CourseManagementWidget implements IsWidget { + protected final MarkTableWrapper> marks; + protected final ControlPointTableWrapper> multiMarkControlPoints; + protected final WaypointTableWrapper> waypoints; + + protected final Grid mainPanel; + + protected final SailingServiceAsync sailingService; + protected final ErrorReporter errorReporter; + protected final StringMessages stringMessages; + + protected final HorizontalPanel waypointsBtnsPanel; + protected final HorizontalPanel controlPointsBtnsPanel; + protected final HorizontalPanel marksBtnsPanel; + protected final HorizontalPanel buttonsPanel; + + protected final Button insertWaypointBefore; + protected final Button insertWaypointAfter; + + protected final AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); + + @Override + public Widget asWidget() { + return mainPanel; + } + + public CourseManagementWidget(final SailingServiceAsync sailingService, ErrorReporter errorReporter, + final StringMessages stringMessages) { + this.sailingService = sailingService; + this.errorReporter = errorReporter; + this.stringMessages = stringMessages; + + mainPanel = new Grid(3, 3); + mainPanel.setCellPadding(5); + mainPanel.getRowFormatter().setVerticalAlign(0, HasVerticalAlignment.ALIGN_TOP); + + waypoints = new WaypointTableWrapper>( + new SingleSelectionModel(), sailingService, stringMessages, errorReporter); + multiMarkControlPoints = new ControlPointTableWrapper>( + new MultiSelectionModel(), sailingService, stringMessages, errorReporter); + marks = new MarkTableWrapper>( + new MultiSelectionModel(), sailingService, stringMessages, errorReporter); + + CaptionPanel waypointsPanel = new CaptionPanel(stringMessages.waypoints()); + CaptionPanel controlPointsPanel = new CaptionPanel(stringMessages.twoMarkControlPoint()); + CaptionPanel marksPanel = new CaptionPanel(stringMessages.mark()); + waypointsPanel.add(waypoints); + controlPointsPanel.add(multiMarkControlPoints); + marksPanel.add(marks); + mainPanel.setWidget(0, 0, waypointsPanel); + mainPanel.setWidget(0, 1, controlPointsPanel); + mainPanel.setWidget(0, 2, marksPanel); + + waypointsBtnsPanel = new HorizontalPanel(); + controlPointsBtnsPanel = new HorizontalPanel(); + marksBtnsPanel = new HorizontalPanel(); + mainPanel.setWidget(1, 0, waypointsBtnsPanel); + mainPanel.setWidget(1, 1, controlPointsBtnsPanel); + mainPanel.setWidget(1, 2, marksBtnsPanel); + + ImagesBarColumn waypointsActionColumn = + new ImagesBarColumn( + new CourseManagementWidgetWaypointsImagesBarCell(stringMessages)); + waypointsActionColumn.setFieldUpdater(new FieldUpdater() { + @Override + public void update(int index, WaypointDTO waypoint, String value) { + if (CourseManagementWidgetWaypointsImagesBarCell.ACTION_DELETE.equals(value)) { + removeWaypoint(waypoint); + } + } + }); + waypoints.getTable().addColumn(waypointsActionColumn); + + waypoints.getSelectionModel().addSelectionChangeHandler(new Handler() { + @Override + public void onSelectionChange(SelectionChangeEvent event) { + WaypointDTO waypoint = waypoints.getSelectionModel().getSelectedObject(); + if (waypoint != null) { + selectControlPoints(waypoint); + selectMarks(waypoint.controlPoint.getMarks()); + } + updateWaypointButtons(); + } + }); + + multiMarkControlPoints.getSelectionModel().addSelectionChangeHandler(new Handler() { + @Override + public void onSelectionChange(SelectionChangeEvent event) { + if (multiMarkControlPoints.getSelectionModel().getSelectedSet().size() > 0) { + ControlPointDTO first = multiMarkControlPoints.getSelectionModel().getSelectedSet().iterator().next(); + selectMarks(first.getMarks()); + } + } + }); + + insertWaypointBefore = new Button(stringMessages.insertWaypointBeforeSelected()); + insertWaypointBefore.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + insertWaypoint(/* before */ true); + } + }); + insertWaypointBefore.setEnabled(false); + waypointsBtnsPanel.add(insertWaypointBefore); + insertWaypointAfter = new Button(stringMessages.insertWaypointAfterSelected()); + insertWaypointAfter.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + insertWaypoint(/* before */ false); + } + }); + insertWaypointAfter.setEnabled(false); + waypointsBtnsPanel.add(insertWaypointAfter); + + final Button addControlPoint = new Button(stringMessages.add(stringMessages.twoMarkControlPoint())); + addControlPoint.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + addMultiMarkControlPoint(); + } + }); + controlPointsBtnsPanel.add(addControlPoint); + + buttonsPanel = new HorizontalPanel(); + mainPanel.setWidget(2, 2, buttonsPanel); + Button refreshBtn = new Button(stringMessages.refresh()); + refreshBtn.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + refresh(); + } + }); + buttonsPanel.add(refreshBtn); + Button saveBtn = new Button(stringMessages.save()); + saveBtn.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + save(); + } + }); + buttonsPanel.add(saveBtn); + } + + private void removeWaypoint(WaypointDTO waypoint) { + waypoints.getDataProvider().getList().remove(waypoint); + } + + private void selectControlPoints(WaypointDTO waypoint) { + for (ControlPointDTO controlPoint : multiMarkControlPoints.getDataProvider().getList()) { + multiMarkControlPoints.getSelectionModel().setSelected( + controlPoint, waypoint.controlPoint == controlPoint); + } + } + + private void selectMarks(Iterable newMarks) { + marks.getSelectionModel().clear(); + for (MarkDTO toSelect : newMarks) { + marks.getSelectionModel().setSelected(toSelect, true); + } + } + + protected abstract void save(); + + private T getFirstSelected(SetSelectionModel selectionModel) { + if (selectionModel.getSelectedSet().isEmpty()) { + return null; + } + return selectionModel.getSelectedSet().iterator().next(); + } + + private void insert(TableWrapper> tableWrapper, T toInsert, boolean beforeSelection) { + T selected = getFirstSelected(tableWrapper.getSelectionModel()); + int index = -1; + if (tableWrapper.getDataProvider().getList().isEmpty()) { + index = 0; + } else if (selected != null) { + index = tableWrapper.getDataProvider().getList().indexOf(selected) + (beforeSelection?0:1); + } + if (index != -1) { + tableWrapper.getDataProvider().getList().add(index, toInsert); + } + } + + private void addMultiMarkControlPoint() { + new GateCreationDialog(sailingService, errorReporter, stringMessages, tableRes, + marks.getDataProvider().getList(), new DataEntryDialog.DialogCallback() { + @Override + public void cancel() {} + + @Override + public void ok(GateDTO result) { + multiMarkControlPoints.dataProvider.getList().add(result); + } + }).show(); + } + + private void insertWaypoint(final boolean beforeSelection) { + List allControlPoints = new ArrayList<>(); + allControlPoints.addAll(multiMarkControlPoints.getDataProvider().getList()); + allControlPoints.addAll(marks.getDataProvider().getList()); + new WaypointCreationDialog(sailingService, errorReporter, stringMessages, tableRes, + allControlPoints, new DataEntryDialog.DialogCallback() { + @Override + public void cancel() {} + + @Override + public void ok(WaypointDTO result) { + insert(waypoints, result, beforeSelection); + } + }).show(); + } + + public abstract void refresh(); + + protected void updateWaypointsAndControlPoints(RaceCourseDTO raceCourseDTO) { + waypoints.getDataProvider().getList().clear(); + multiMarkControlPoints.getDataProvider().getList().clear(); + waypoints.getDataProvider().getList().addAll(raceCourseDTO.waypoints); + + Map noDuplicateCPs = new HashMap<>(); + for (ControlPointDTO controlPoint : raceCourseDTO.getControlPoints()) { + if (controlPoint instanceof GateDTO) { + noDuplicateCPs.put(controlPoint.getIdAsString(), controlPoint); + } + } + multiMarkControlPoints.getDataProvider().getList().addAll(noDuplicateCPs.values()); + + updateWaypointButtons(); + } + + protected List> createWaypointPairs() { + List> result = new ArrayList<>(); + for (WaypointDTO waypoint : waypoints.getDataProvider().getList()) { + result.add(new Util.Pair<>(waypoint.controlPoint, waypoint.passingInstructions)); + } + return result; + } + + protected void updateWaypointButtons() { + if (waypoints.getDataProvider().getList().isEmpty() || + waypoints.getSelectionModel().getSelectedObject() != null) { + insertWaypointAfter.setEnabled(true); + insertWaypointBefore.setEnabled(true); + } else { + insertWaypointBefore.setEnabled(false); + insertWaypointAfter.setEnabled(false); + } + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CourseManagementWidgetWaypointsImagesBarCell.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CourseManagementWidgetWaypointsImagesBarCell.java new file mode 100644 index 00000000000..6f7e9c87be9 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CourseManagementWidgetWaypointsImagesBarCell.java @@ -0,0 +1,25 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; + +import com.google.gwt.core.client.GWT; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.components.ImagesBarCell; + +public class CourseManagementWidgetWaypointsImagesBarCell extends ImagesBarCell { + public static final String ACTION_DELETE = "ACTION_DELETE"; + private static AdminConsoleResources resources = GWT.create(AdminConsoleResources.class); + private final StringMessages stringMessages; + + public CourseManagementWidgetWaypointsImagesBarCell(StringMessages stringMessages) { + super(); + this.stringMessages = stringMessages; + } + + @Override + protected Iterable getImageSpecs() { + ArrayList result = new ArrayList(); + result.add(new ImageSpec(ACTION_DELETE, stringMessages.remove(), makeImagePrototype(resources.removeIcon()))); + return result; + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceIdentifierTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceIdentifierTableWrapper.java new file mode 100644 index 00000000000..375b4482043 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceIdentifierTableWrapper.java @@ -0,0 +1,34 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import com.google.gwt.user.cellview.client.TextColumn; +import com.google.gwt.view.client.MultiSelectionModel; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.DeviceIdentifierDTO; + +public class DeviceIdentifierTableWrapper extends TableWrapper> { + + public DeviceIdentifierTableWrapper(SailingServiceAsync sailingService, StringMessages stringMessages, + ErrorReporter errorReporter) { + super(sailingService, stringMessages, errorReporter, new MultiSelectionModel()); + + TextColumn typeColumn = new TextColumn() { + @Override + public String getValue(DeviceIdentifierDTO object) { + return object.deviceType; + } + }; + + TextColumn idColumn = new TextColumn() { + @Override + public String getValue(DeviceIdentifierDTO object) { + return object.deviceId; + } + }; + + table.addColumn(typeColumn, stringMessages.deviceType()); + table.addColumn(idColumn, stringMessages.deviceId()); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceMappingTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceMappingTableWrapper.java new file mode 100644 index 00000000000..3920dd093bd --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceMappingTableWrapper.java @@ -0,0 +1,127 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.Comparator; +import java.util.List; + +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler; +import com.google.gwt.user.cellview.client.TextColumn; +import com.google.gwt.view.client.SingleSelectionModel; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.DeviceMappingDTO; + +public class DeviceMappingTableWrapper extends TableWrapper> { + + public DeviceMappingTableWrapper(SailingServiceAsync sailingService, final StringMessages stringMessages, + ErrorReporter errorReporter) { + super(sailingService, stringMessages, errorReporter, new SingleSelectionModel()); + + table.setWidth("1000px", true); + table.addStyleName("wrap-cols"); + + ListHandler listHandler = new ListHandler(dataProvider.getList()); + + TextColumn itemTypeCol = new TextColumn() { + @Override + public String getValue(DeviceMappingDTO mapping) { + if (mapping.mappedTo instanceof CompetitorDTO) return stringMessages.competitor(); + else return stringMessages.mark(); + } + }; + itemTypeCol.setSortable(true); + listHandler.setComparator(itemTypeCol, new Comparator() { + @Override + public int compare(DeviceMappingDTO o1, DeviceMappingDTO o2) { + return o1.mappedTo.getClass().getName().compareTo(o2.mappedTo.getClass().getName()); + } + }); + table.addColumn(itemTypeCol, stringMessages.mappedToType()); + + TextColumn itemCol = new TextColumn() { + @Override + public String getValue(DeviceMappingDTO mapping) { + return mapping.mappedTo.toString(); + } + }; + itemCol.setSortable(true); + listHandler.setComparator(itemCol, new Comparator() { + @Override + public int compare(DeviceMappingDTO o1, DeviceMappingDTO o2) { + return o1.mappedTo.toString().compareTo(o2.mappedTo.toString()); + } + }); + table.addColumn(itemCol, stringMessages.mappedTo()); + + TextColumn deviceTypeCol = new TextColumn() { + @Override + public String getValue(DeviceMappingDTO mapping) { + return mapping.deviceIdentifier.deviceType; + } + }; + deviceTypeCol.setSortable(true); + listHandler.setComparator(deviceTypeCol, new Comparator() { + @Override + public int compare(DeviceMappingDTO o1, DeviceMappingDTO o2) { + return o1.deviceIdentifier.deviceType.compareTo(o2.deviceIdentifier.deviceType); + } + }); + table.addColumn(deviceTypeCol, stringMessages.deviceType()); + + TextColumn deviceIdCol = new TextColumn() { + @Override + public String getValue(DeviceMappingDTO mapping) { + return mapping.deviceIdentifier.deviceId; + } + }; + deviceIdCol.setSortable(true); + listHandler.setComparator(deviceIdCol, new Comparator() { + @Override + public int compare(DeviceMappingDTO o1, DeviceMappingDTO o2) { + return o1.deviceIdentifier.deviceId.compareTo(o2.deviceIdentifier.deviceId); + } + }); + table.addColumn(deviceIdCol, stringMessages.deviceId()); + table.setColumnWidth(deviceIdCol, 400, Unit.PX); + + TextColumn fromCol = new TextColumn() { + @Override + public String getValue(DeviceMappingDTO mapping) { + return DateAndTimeFormatterUtil.formatDateAndTime(mapping.from); + } + }; + fromCol.setSortable(true); + listHandler.setComparator(fromCol, new Comparator() { + @Override + public int compare(DeviceMappingDTO o1, DeviceMappingDTO o2) { + return o1.from.compareTo(o2.from); + } + }); + table.addColumn(fromCol, stringMessages.from()); + + TextColumn toCol = new TextColumn() { + @Override + public String getValue(DeviceMappingDTO mapping) { + return DateAndTimeFormatterUtil.formatDateAndTime(mapping.to); + } + }; + toCol.setSortable(true); + listHandler.setComparator(toCol, new Comparator() { + @Override + public int compare(DeviceMappingDTO o1, DeviceMappingDTO o2) { + return o1.to.compareTo(o2.to); + } + }); + table.addColumn(toCol, stringMessages.to()); + + table.addColumnSortHandler(listHandler); + mainPanel.add(table); + } + + public void refresh(List mappings) { + dataProvider.getList().clear(); + dataProvider.getList().addAll(mappings); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ExportPopup.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ExportPopup.java index cd6f989b9d9..918e1c629b1 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ExportPopup.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ExportPopup.java @@ -38,7 +38,7 @@ public class ExportPopup extends DialogBox { getCaption().setText(stringMessages.export()); formPanel = new FormPanel("_blank"); - formPanel.setAction("/sailingserver/trackfiles"); + formPanel.setAction("/sailingserver/trackfiles/export"); formPanel.setMethod(FormPanel.METHOD_POST); mainPanel = new VerticalPanel(); formPanel.setWidget(mainPanel); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/GateCreationDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/GateCreationDialog.java new file mode 100644 index 00000000000..21093935d9b --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/GateCreationDialog.java @@ -0,0 +1,88 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.TextBox; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.view.client.MultiSelectionModel; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SelectionChangeEvent.Handler; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.GateDTO; +import com.sap.sailing.gwt.ui.shared.MarkDTO; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; + +public class GateCreationDialog extends DataEntryDialog { + private final MarkTableWrapper> marksWrapper; + private final TextBox name; + private final StringMessages stringMessages; + + public GateCreationDialog(SailingServiceAsync sailingService, ErrorReporter errorReporter, + final StringMessages stringMessages, AdminConsoleTableResources tableRes, + List marks, DialogCallback callback) { + super(stringMessages.gate(), stringMessages.gate(), + stringMessages.ok(), stringMessages.cancel(), new DataEntryDialog.Validator() { + @Override + public String getErrorMessage(GateDTO valueToValidate) { + if (valueToValidate == null) { + return stringMessages.selectTwoMarksForGate(); + } else if (valueToValidate.getName() == null || valueToValidate.getName().isEmpty()) { + return stringMessages.pleaseEnterAName(); + } + return null; + } + + }, /* animationEnabled */ false, callback); + + this.stringMessages = stringMessages; + + name = createTextBox(""); + + marksWrapper = new MarkTableWrapper>( + new MultiSelectionModel(), sailingService, stringMessages, errorReporter); + marksWrapper.getDataProvider().getList().addAll(marks); + + marksWrapper.getSelectionModel().addSelectionChangeHandler(new Handler() { + @Override + public void onSelectionChange(SelectionChangeEvent event) { + validate(); + } + }); + } + + @Override + protected GateDTO getResult() { + GateDTO controlPoint = null; + Set selection = marksWrapper.getSelectionModel().getSelectedSet(); + if (selection.size() == 2) { + name.setEnabled(true); + Iterator i = marksWrapper.getSelectionModel().getSelectedSet().iterator(); + MarkDTO first = i.next(); + MarkDTO second = i.next(); + controlPoint = new GateDTO(/* generate UUID on the server */ null, name.getText(), first, second); + } + + return controlPoint; + } + + @Override + protected Widget getAdditionalWidget() { + Grid grid = new Grid(2,1); + + HorizontalPanel nameRow = new HorizontalPanel(); + nameRow.add(new Label(stringMessages.name())); + nameRow.add(name); + grid.setWidget(0, 0, nameRow); + + grid.setWidget(1, 0, marksWrapper); + + return grid; + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigImagesBarCell.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigImagesBarCell.java index 613d36e7142..afbe55e69ab 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigImagesBarCell.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigImagesBarCell.java @@ -29,7 +29,8 @@ public class LeaderboardConfigImagesBarCell extends ImagesBarCell { @Override protected Iterable getImageSpecs() { - return Arrays.asList(new ImageSpec(ACTION_EDIT, stringMessages.actionEdit(), makeImagePrototype(resources.editIcon())), + return Arrays.asList( + new ImageSpec(ACTION_EDIT, stringMessages.actionEdit(), makeImagePrototype(resources.editIcon())), new ImageSpec(ACTION_EDIT_SCORES, stringMessages.actionEditScores(), makeImagePrototype(resources.scoresIcon())), new ImageSpec(ACTION_EDIT_COMPETITORS, stringMessages.actionEditCompetitors(), makeImagePrototype(resources.competitorsIcon())), new ImageSpec(ACTION_CONFIGURE_URL, stringMessages.actionConfigureUrl(), makeImagePrototype(resources.settingsActionIcon())), diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java index d268f4508c5..401ac26b54e 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java @@ -1,1341 +1,899 @@ -package com.sap.sailing.gwt.ui.adminconsole; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import com.google.gwt.cell.client.AbstractCell; -import com.google.gwt.cell.client.FieldUpdater; -import com.google.gwt.core.client.GWT; -import com.google.gwt.core.client.Scheduler; -import com.google.gwt.core.client.Scheduler.ScheduledCommand; -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.safehtml.client.SafeHtmlTemplates; -import com.google.gwt.safehtml.shared.SafeHtml; -import com.google.gwt.safehtml.shared.SafeHtmlBuilder; -import com.google.gwt.safehtml.shared.SafeHtmlUtils; -import com.google.gwt.user.cellview.client.CellTable; -import com.google.gwt.user.cellview.client.Column; -import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler; -import com.google.gwt.user.cellview.client.ColumnSortList; -import com.google.gwt.user.cellview.client.TextColumn; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.CaptionPanel; -import com.google.gwt.user.client.ui.FormPanel; -import com.google.gwt.user.client.ui.Grid; -import com.google.gwt.user.client.ui.HasHorizontalAlignment; -import com.google.gwt.user.client.ui.HorizontalPanel; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.VerticalPanel; -import com.google.gwt.view.client.ListDataProvider; -import com.google.gwt.view.client.MultiSelectionModel; -import com.google.gwt.view.client.SelectionChangeEvent; -import com.google.gwt.view.client.SingleSelectionModel; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.RegattaIdentifier; -import com.sap.sailing.domain.common.RegattaName; -import com.sap.sailing.domain.common.RegattaNameAndRaceName; -import com.sap.sailing.domain.common.dto.AbstractLeaderboardDTO; -import com.sap.sailing.domain.common.dto.CompetitorDTO; -import com.sap.sailing.domain.common.dto.FleetDTO; -import com.sap.sailing.domain.common.dto.NamedDTO; -import com.sap.sailing.domain.common.dto.RaceColumnDTO; -import com.sap.sailing.domain.common.impl.Util; -import com.sap.sailing.domain.common.impl.Util.Pair; -import com.sap.sailing.gwt.ui.adminconsole.DisablableCheckboxCell.IsEnabled; -import com.sap.sailing.gwt.ui.adminconsole.RaceColumnInLeaderboardDialog.RaceColumnDescriptor; -import com.sap.sailing.gwt.ui.client.ErrorReporter; -import com.sap.sailing.gwt.ui.client.ParallelExecutionCallback; -import com.sap.sailing.gwt.ui.client.ParallelExecutionHolder; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.RaceSelectionModel; -import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; -import com.sap.sailing.gwt.ui.client.RegattaDisplayer; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.URLEncoder; -import com.sap.sailing.gwt.ui.client.shared.panels.LabeledAbstractFilterablePanel; -import com.sap.sailing.gwt.ui.leaderboard.LeaderboardEntryPoint; -import com.sap.sailing.gwt.ui.leaderboard.ScoringSchemeTypeFormatter; -import com.sap.sailing.gwt.ui.raceboard.RaceBoardViewConfiguration; -import com.sap.sailing.gwt.ui.shared.EventDTO; -import com.sap.sailing.gwt.ui.shared.RaceLogDTO; -import com.sap.sailing.gwt.ui.shared.RaceLogSetStartTimeAndProcedureDTO; -import com.sap.sailing.gwt.ui.shared.RegattaDTO; -import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; -import com.sap.sse.gwt.client.async.MarkedAsyncCallback; -import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; - -public class LeaderboardConfigPanel extends FormPanel implements SelectedLeaderboardProvider, RegattaDisplayer, RaceSelectionChangeListener, - TrackedRaceChangedListener { - - private final TrackedRacesListComposite trackedRacesListComposite; - - private final StringMessages stringMessages; - - private final SailingServiceAsync sailingService; - - private final ListDataProvider leaderboardList; - - private final ListDataProvider raceColumnAndFleetList; - - private final ErrorReporter errorReporter; - - private final CellTable leaderboardTable; - - private Button leaderboardRemoveButton; - - private final CellTable raceColumnTable; - - private RaceColumnDTOAndFleetDTOWithNameBasedEquality selectedRaceInLeaderboard; - - private final Button addRaceColumnsButton; - - private final Button columnMoveUpButton; - private final Button columnMoveDownButton; - - private final CaptionPanel selectedLeaderBoardPanel; - private final CaptionPanel trackedRacesCaptionPanel; - private final List allRegattas; - - private LabeledAbstractFilterablePanel filterLeaderboardPanel; - - final SingleSelectionModel raceColumnTableSelectionModel; - - private List availableLeaderboardList; - - private final MultiSelectionModel leaderboardSelectionModel; - - private final RaceSelectionProvider raceSelectionProvider; - - public static class AnchorCell extends AbstractCell { - - @Override - public void render(com.google.gwt.cell.client.Cell.Context context, SafeHtml safeHtml, SafeHtmlBuilder sb) { - sb.append(safeHtml); - } - } - - interface AnchorTemplates extends SafeHtmlTemplates { - @SafeHtmlTemplates.Template("{1}") - SafeHtml cell(String url, String displayName); - } - - private static AnchorTemplates ANCHORTEMPLATE = GWT.create(AnchorTemplates.class); - - private static class RaceColumnDTOAndFleetDTOWithNameBasedEquality extends Pair { - private static final long serialVersionUID = -8742476113296862662L; - - public RaceColumnDTOAndFleetDTOWithNameBasedEquality(RaceColumnDTO a, FleetDTO b) { - super(a, b); - } - - @Override - public int hashCode() { - return getA().getName().hashCode() ^ getB().getName().hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } else { - if (obj == null) { - return false; - } else { - return equalNamesOrBothNull(getA(), ((RaceColumnDTOAndFleetDTOWithNameBasedEquality) obj).getA()) - && equalNamesOrBothNull(getB(), ((RaceColumnDTOAndFleetDTOWithNameBasedEquality) obj).getB()); - } - } - } - - private boolean equalNamesOrBothNull(NamedDTO a, NamedDTO b) { - if (a == null) { - return b == null; - } else { - if (b == null) { - return false; - } else { - return Util.equalsWithNull(a.getName(), b.getName()); - } - } - } - } - - public LeaderboardConfigPanel(final SailingServiceAsync sailingService, AdminConsoleEntryPoint adminConsole, - final ErrorReporter errorReporter, StringMessages theStringConstants, final boolean showRaceDetails) { - this.stringMessages = theStringConstants; - this.sailingService = sailingService; - leaderboardList = new ListDataProvider(); - allRegattas = new ArrayList(); - raceColumnAndFleetList = new ListDataProvider(); - this.errorReporter = errorReporter; - this.availableLeaderboardList = new ArrayList(); - VerticalPanel mainPanel = new VerticalPanel(); - mainPanel.setWidth("100%"); - this.setWidget(mainPanel); - - //Create leaderboards list and functionality - CaptionPanel leaderboardsCaptionPanel = new CaptionPanel(stringMessages.leaderboards()); - leaderboardsCaptionPanel.setStyleName("bold"); - leaderboardsCaptionPanel.setWidth("75%"); - mainPanel.add(leaderboardsCaptionPanel); - - VerticalPanel leaderboardsPanel = new VerticalPanel(); - leaderboardsCaptionPanel.add(leaderboardsPanel); - - HorizontalPanel leaderboardConfigControlsPanel = new HorizontalPanel(); - Label lblFilterEvents = new Label(stringMessages.filterLeaderboardsByName() + ": "); - leaderboardConfigControlsPanel.setSpacing(5); - - AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); - leaderboardTable = new CellTable(/* pageSize */10000, tableRes); - leaderboardTable.ensureDebugId("LeaderboardsCellTable"); - filterLeaderboardPanel = new LabeledAbstractFilterablePanel(lblFilterEvents, availableLeaderboardList, leaderboardTable, leaderboardList) { - @Override - public List getSearchableStrings(StrippedLeaderboardDTO t) { - List strings = new ArrayList(); - strings.add(t.name); - strings.add(t.displayName); - return strings; - } - }; - filterLeaderboardPanel.getTextBox().ensureDebugId("LeaderboardsFilterTextBox"); - - leaderboardRemoveButton = new Button(stringMessages.remove()); - leaderboardRemoveButton.ensureDebugId("LeaderboardsRemoveButton"); - leaderboardRemoveButton.setEnabled(false); - leaderboardRemoveButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - if (Window.confirm(stringMessages.doYouReallyWantToRemoveLeaderboards())) { - removeLeaderboards(leaderboardSelectionModel.getSelectedSet()); - } - } - }); - leaderboardConfigControlsPanel.add(filterLeaderboardPanel); - leaderboardConfigControlsPanel.add(leaderboardRemoveButton); - leaderboardsPanel.add(leaderboardConfigControlsPanel); - ListHandler leaderboardColumnListHandler = new ListHandler( - leaderboardList.getList()); - - AnchorCell anchorCell = new AnchorCell(); - Column linkColumn = new Column(anchorCell) { - @Override - public SafeHtml getValue(StrippedLeaderboardDTO object) { - String debugParam = Window.Location.getParameter("gwt.codesvr"); - String link = URLEncoder.encode("/gwt/Leaderboard.html?name=" + object.name - + (showRaceDetails ? "&showRaceDetails=true" : "") - + (object.displayName != null ? "&displayName="+object.displayName : "") - + (debugParam != null && !debugParam.isEmpty() ? "&gwt.codesvr=" + debugParam : "")); - return ANCHORTEMPLATE.cell(link, object.name); - } - - }; - linkColumn.setSortable(true); - leaderboardColumnListHandler.setComparator(linkColumn, new Comparator() { - - @Override - public int compare(StrippedLeaderboardDTO o1, StrippedLeaderboardDTO o2) { - boolean ascending = isSortedAscending(); - if (o1.name.equals(o2.name)) { - return 0; - } - int val = -1; - val = (o1 != null && o2 != null && ascending) ? (o1.name.compareTo(o2.name)) : -(o2.name - .compareTo(o1.name)); - - return val; - } - - private boolean isSortedAscending() { - ColumnSortList sortList = leaderboardTable.getColumnSortList(); - return sortList.size() > 0 & sortList.get(0).isAscending(); - } - }); - - TextColumn leaderboardDisplayNameColumn = new TextColumn() { - @Override - public String getValue(StrippedLeaderboardDTO leaderboard) { - return leaderboard.getDisplayName() != null ? leaderboard.getDisplayName() : ""; - } - }; - - TextColumn discardingOptionsColumn = new TextColumn() { - @Override - public String getValue(StrippedLeaderboardDTO leaderboard) { - String result = ""; - if (leaderboard.discardThresholds != null) { - for (int discardThreshold : leaderboard.discardThresholds) { - result += discardThreshold + " "; - } - } - return result; - } - }; - - TextColumn leaderboardTypeColumn = new TextColumn() { - @Override - public String getValue(StrippedLeaderboardDTO leaderboard) { - String result = leaderboard.type.isRegattaLeaderboard() ? "Regatta" : "Flexible"; - if (leaderboard.type.isMetaLeaderboard()) { - result += " , Meta"; - } - return result; - } - }; - - TextColumn scoringSystemColumn = new TextColumn() { - @Override - public String getValue(StrippedLeaderboardDTO leaderboard) { - return leaderboard.scoringScheme == null ? "" : ScoringSchemeTypeFormatter.format(leaderboard.scoringScheme, stringMessages); - } - }; - - TextColumn courseAreaColumn = new TextColumn() { - @Override - public String getValue(StrippedLeaderboardDTO leaderboard) { - return leaderboard.defaultCourseAreaId == null ? "" : leaderboard.defaultCourseAreaName; - } - }; - - ImagesBarColumn leaderboardActionColumn = new ImagesBarColumn( - new LeaderboardConfigImagesBarCell(stringMessages)); - leaderboardActionColumn.setFieldUpdater(new FieldUpdater() { - @Override - public void update(int index, StrippedLeaderboardDTO leaderboardDTO, String value) { - if (LeaderboardConfigImagesBarCell.ACTION_REMOVE.equals(value)) { - if (Window.confirm(stringMessages.doYouReallyWantToRemoveLeaderboard(leaderboardDTO.name))) { - removeLeaderboard(leaderboardDTO); - } - } else if (LeaderboardConfigImagesBarCell.ACTION_EDIT.equals(value)) { - final String oldLeaderboardName = leaderboardDTO.name; - List otherExistingLeaderboard = new ArrayList(); - otherExistingLeaderboard.addAll(availableLeaderboardList); - otherExistingLeaderboard.remove(leaderboardDTO); - if (leaderboardDTO.type.isMetaLeaderboard()) { - Window.alert(stringMessages.metaLeaderboardCannotBeChanged()); - } else { - if (leaderboardDTO.type.isRegattaLeaderboard()) { - LeaderboardDescriptor descriptor = new LeaderboardDescriptor(leaderboardDTO.name, - leaderboardDTO.displayName, /* scoring scheme provided by regatta */ null, - leaderboardDTO.discardThresholds, leaderboardDTO.regattaName, - leaderboardDTO.defaultCourseAreaId); - AbstractLeaderboardDialog dialog = new RegattaLeaderboardEditDialog(Collections - .unmodifiableCollection(otherExistingLeaderboard), Collections.unmodifiableCollection(allRegattas), - descriptor, stringMessages, errorReporter, - new DialogCallback() { - @Override - public void cancel() { - } - - @Override - public void ok(LeaderboardDescriptor result) { - updateLeaderboard(oldLeaderboardName, result); - } - }); - dialog.show(); - } else { - LeaderboardDescriptor descriptor = new LeaderboardDescriptor(leaderboardDTO.name, leaderboardDTO.displayName, leaderboardDTO.scoringScheme, leaderboardDTO.discardThresholds, leaderboardDTO.defaultCourseAreaId); - openUpdateFlexibleLeaderboardDialog(leaderboardDTO, otherExistingLeaderboard, leaderboardDTO.name, descriptor); - } - } - } else if (LeaderboardConfigImagesBarCell.ACTION_EDIT_SCORES.equals(value)) { - String debugParam = Window.Location.getParameter("gwt.codesvr"); - Window.open("/gwt/LeaderboardEditing.html?name=" + leaderboardDTO.name - + (debugParam != null && !debugParam.isEmpty() ? "&gwt.codesvr=" + debugParam : ""), "_blank", null); - } else if (LeaderboardConfigImagesBarCell.ACTION_EDIT_COMPETITORS.equals(value)) { - EditCompetitorsDialog editCompetitorsDialog = new EditCompetitorsDialog(sailingService, leaderboardDTO.name, stringMessages, - errorReporter, new DialogCallback>() { - @Override - public void cancel() { - } - - @Override - public void ok(final List result) { - } - }); - editCompetitorsDialog.show(); - - } else if (LeaderboardConfigImagesBarCell.ACTION_CONFIGURE_URL.equals(value)) { - openLeaderboardUrlConfigDialog(leaderboardDTO, stringMessages); - } else if (LeaderboardConfigImagesBarCell.ACTION_EXPORT_XML.equals(value)) { - Window.open("/export/xml?domain=leaderboard&name=" + leaderboardDTO.name, "", null); - } - } - }); - leaderboardTable.addColumn(linkColumn, stringMessages.name()); - leaderboardTable.addColumn(leaderboardDisplayNameColumn, stringMessages.displayName()); - leaderboardTable.addColumn(discardingOptionsColumn, stringMessages.discarding()); - leaderboardTable.addColumn(leaderboardTypeColumn, stringMessages.type()); - leaderboardTable.addColumn(scoringSystemColumn, stringMessages.scoringSystem()); - leaderboardTable.addColumn(courseAreaColumn, stringMessages.courseArea()); - leaderboardTable.addColumn(leaderboardActionColumn, stringMessages.actions()); - leaderboardTable.addColumnSortHandler(leaderboardColumnListHandler); - leaderboardTable.setWidth("100%"); - leaderboardSelectionModel = new MultiSelectionModel(); - leaderboardTable.setSelectionModel(leaderboardSelectionModel); - leaderboardSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { - public void onSelectionChange(SelectionChangeEvent event) { - leaderboardSelectionChanged(); - } - }); - leaderboardList.addDataDisplay(leaderboardTable); - leaderboardsPanel.add(leaderboardTable); - - HorizontalPanel leaderboardButtonPanel = new HorizontalPanel(); - leaderboardButtonPanel.setSpacing(5); - leaderboardsPanel.add(leaderboardButtonPanel); - Button createFlexibleLeaderboardBtn = new Button(stringMessages.createFlexibleLeaderboard() + "..."); - createFlexibleLeaderboardBtn.ensureDebugId("CreateFlexibleLeaderboardButton"); - leaderboardButtonPanel.add(createFlexibleLeaderboardBtn); - createFlexibleLeaderboardBtn.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent clickEvent) { - createFlexibleLeaderboard(); - } - }); - - Button createRegattaLeaderboardBtn = new Button(stringMessages.createRegattaLeaderboard() + "..."); - createRegattaLeaderboardBtn.ensureDebugId("CreateRegattaLeaderboardButton"); - leaderboardButtonPanel.add(createRegattaLeaderboardBtn); - createRegattaLeaderboardBtn.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent clickEvent) { - createRegattaLeaderboard(); - } - }); - - mainPanel.add(new Grid(1, 1)); - - // caption panels for the selected leaderboard and tracked races - HorizontalPanel splitPanel = new HorizontalPanel(); - splitPanel.ensureDebugId("LeaderboardDetailsPanel"); - mainPanel.add(splitPanel); - - selectedLeaderBoardPanel = new CaptionPanel(stringMessages.leaderboard()); - selectedLeaderBoardPanel.setWidth("50%"); - splitPanel.add(selectedLeaderBoardPanel); - - VerticalPanel vPanel = new VerticalPanel(); - vPanel.setWidth("100%"); - selectedLeaderBoardPanel.setContentWidget(vPanel); - - trackedRacesCaptionPanel = new CaptionPanel(stringMessages.trackedRaces()); - trackedRacesCaptionPanel.setWidth("50%"); - splitPanel.add(trackedRacesCaptionPanel); - - VerticalPanel trackedRacesPanel = new VerticalPanel(); - trackedRacesPanel.setWidth("100%"); - trackedRacesCaptionPanel.setContentWidget(trackedRacesPanel); - trackedRacesCaptionPanel.setStyleName("bold"); - - raceSelectionProvider = new RaceSelectionModel(); - trackedRacesListComposite = new TrackedRacesListComposite(sailingService, errorReporter, adminConsole, - raceSelectionProvider, stringMessages, /* multiselection */false); - trackedRacesListComposite.ensureDebugId("TrackedRacesListComposite"); - trackedRacesPanel.add(trackedRacesListComposite); - trackedRacesListComposite.addTrackedRaceChangeListener(this); - raceSelectionProvider.addRaceSelectionChangeListener(this); - - Button reloadAllRaceLogs = new Button(stringMessages.reloadAllRaceLogs()); - reloadAllRaceLogs.ensureDebugId("ReloadAllRaceLogsButton"); - reloadAllRaceLogs.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - StrippedLeaderboardDTO leaderboard = getSelectedLeaderboard(); - for (RaceColumnDTO column : leaderboard.getRaceList()) { - for (FleetDTO fleet : column.getFleets()) { - refreshRaceLog(column, fleet, false); - } - } - Window.alert(stringMessages.raceLogReloaded()); - } - }); - vPanel.add(reloadAllRaceLogs); - - // ------------ races of the selected leaderboard ---------------- - AnchorCell raceAnchorCell = new AnchorCell(); - Column raceLinkColumn = new Column(raceAnchorCell) { - @Override - public SafeHtml getValue(RaceColumnDTOAndFleetDTOWithNameBasedEquality raceInLeaderboardDTOAndFleetName) { - if (raceInLeaderboardDTOAndFleetName.getA().getRaceIdentifier(raceInLeaderboardDTOAndFleetName.getB()) != null) { - RegattaNameAndRaceName raceIdentifier = (RegattaNameAndRaceName) raceInLeaderboardDTOAndFleetName - .getA().getRaceIdentifier(raceInLeaderboardDTOAndFleetName.getB()); - String debugParam = Window.Location.getParameter("gwt.codesvr"); - String link = URLEncoder.encode("/gwt/RaceBoard.html?leaderboardName=" - + getSelectedLeaderboard().name + "&raceName=" + raceIdentifier.getRaceName() + "®attaName=" - + raceIdentifier.getRegattaName() - + "&"+RaceBoardViewConfiguration.PARAM_CAN_REPLAY_DURING_LIVE_RACES + "=true" - + (debugParam != null && !debugParam.isEmpty() ? "&gwt.codesvr=" + debugParam : "")); - return ANCHORTEMPLATE.cell(link, raceInLeaderboardDTOAndFleetName.getA().getRaceColumnName()); - } else { - return SafeHtmlUtils.fromString(raceInLeaderboardDTOAndFleetName.getA().getRaceColumnName()); - } - } - }; - TextColumn fleetNameColumn = new TextColumn() { - @Override - public String getValue(RaceColumnDTOAndFleetDTOWithNameBasedEquality object) { - return object.getB().getName(); - } - }; - TextColumn explicitFactorColumn = new TextColumn() { - @Override - public String getValue(RaceColumnDTOAndFleetDTOWithNameBasedEquality object) { - return object.getA().getExplicitFactor() == null ? "" : object.getA().getExplicitFactor().toString(); - } - }; - - Column isMedalRaceCheckboxColumn = new Column( - new DisablableCheckboxCell(new IsEnabled() { - @Override - public boolean isEnabled() { - return getSelectedLeaderboard() != null && !getSelectedLeaderboard().type.isRegattaLeaderboard(); - - } - })) { - @Override - public Boolean getValue(RaceColumnDTOAndFleetDTOWithNameBasedEquality race) { - return race.getA().isMedalRace(); - } - }; - isMedalRaceCheckboxColumn.setFieldUpdater(new FieldUpdater() { - @Override - public void update(int index, RaceColumnDTOAndFleetDTOWithNameBasedEquality object, Boolean value) { - setIsMedalRace(getSelectedLeaderboard().name, object.getA(), value); - } - }); - isMedalRaceCheckboxColumn.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_CENTER); - - TextColumn isLinkedRaceColumn = new TextColumn() { - @Override - public String getValue(RaceColumnDTOAndFleetDTOWithNameBasedEquality raceColumnAndFleetName) { - boolean isTrackedRace = raceColumnAndFleetName.getA().isTrackedRace(raceColumnAndFleetName.getB()); - return isTrackedRace ? stringMessages.yes() : stringMessages.no(); - } - }; - ImagesBarColumn raceActionColumn = - new ImagesBarColumn( - new LeaderboardRaceConfigImagesBarCell(this, stringMessages)); - raceActionColumn.setFieldUpdater(new FieldUpdater() { - @Override - public void update(int index, RaceColumnDTOAndFleetDTOWithNameBasedEquality object, String value) { - if (LeaderboardRaceConfigImagesBarCell.ACTION_REMOVE.equals(value)) { - if (Window.confirm(stringMessages.reallyRemoveRace(object.getA().getRaceColumnName()))) { - removeRaceColumn(object.getA()); - } - } else if (LeaderboardRaceConfigImagesBarCell.ACTION_EDIT.equals(value)) { - editRaceColumnOfLeaderboard(object); - } else if (LeaderboardRaceConfigImagesBarCell.ACTION_UNLINK.equals(value)) { - unlinkRaceColumnFromTrackedRace(object.getA().getRaceColumnName(), object.getB()); - } else if (LeaderboardRaceConfigImagesBarCell.ACTION_REFRESH_RACELOG.equals(value)) { - refreshRaceLog(object.getA(), object.getB(), true); - } else if(LeaderboardRaceConfigImagesBarCell.ACTION_SET_STARTTIME.equals(value)) { - setStartTime(object.getA(), object.getB()); - } else if(LeaderboardRaceConfigImagesBarCell.ACTION_SHOW_RACELOG.equals(value)) { - showRaceLog(object.getA(), object.getB()); - } - } - }); - Label lblRaceNamesIn = new Label(stringMessages.races()); - vPanel.add(lblRaceNamesIn); - raceColumnTable = new CellTable(/* pageSize */200, tableRes); - raceColumnTable.ensureDebugId("RacesCellTable"); - raceColumnTable.addColumn(raceLinkColumn, stringMessages.name()); - raceColumnTable.addColumn(fleetNameColumn, stringMessages.fleet()); - raceColumnTable.addColumn(isMedalRaceCheckboxColumn, stringMessages.medalRace()); - raceColumnTable.addColumn(isLinkedRaceColumn, stringMessages.islinked()); - raceColumnTable.addColumn(explicitFactorColumn, stringMessages.factor()); - raceColumnTable.addColumn(raceActionColumn, stringMessages.actions()); - raceColumnAndFleetList.addDataDisplay(raceColumnTable); - raceColumnTable.setWidth("500px"); - raceColumnTableSelectionModel = new SingleSelectionModel(); - raceColumnTable.setSelectionModel(raceColumnTableSelectionModel); - raceColumnTableSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { - public void onSelectionChange(SelectionChangeEvent event) { - leaderboardRaceColumnSelectionChanged(); - } - }); - vPanel.add(raceColumnTable); - - HorizontalPanel selectedLeaderboardRaceButtonPanel = new HorizontalPanel(); - selectedLeaderboardRaceButtonPanel.setSpacing(5); - vPanel.add(selectedLeaderboardRaceButtonPanel); - - addRaceColumnsButton = new Button(stringMessages.actionAddRaces() + "..."); - addRaceColumnsButton.ensureDebugId("AddRacesButton"); - addRaceColumnsButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - if (getSelectedLeaderboard().type.isRegattaLeaderboard()) { - Window.alert(stringMessages.cannotAddRacesToRegattaLeaderboardButOnlyToRegatta()); - } else { - addRaceColumnsToLeaderboard(); - } - } - }); - selectedLeaderboardRaceButtonPanel.add(addRaceColumnsButton); - - columnMoveUpButton = new Button(stringMessages.columnMoveUp()); - columnMoveUpButton.ensureDebugId("MoveRaceUpButton"); - columnMoveUpButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - moveSelectedRaceColumnUp(); - } - }); - selectedLeaderboardRaceButtonPanel.add(columnMoveUpButton); - - columnMoveDownButton = new Button(stringMessages.columnMoveDown()); - columnMoveDownButton.ensureDebugId("MoveRaceDownButton"); - columnMoveDownButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - moveSelectedRaceColumnDown(); - } - }); - selectedLeaderboardRaceButtonPanel.add(columnMoveDownButton); - - loadLeaderboards(); - } - - protected void openUpdateFlexibleLeaderboardDialog(final StrippedLeaderboardDTO leaderboardDTO, final List otherExistingLeaderboard, - final String oldLeaderboardName, final LeaderboardDescriptor descriptor) { - sailingService.getEvents(new MarkedAsyncCallback>( - new AsyncCallback>() { - @Override - public void onSuccess(List result) { - openUpdateFlexibleLeaderboardDialog(leaderboardDTO, otherExistingLeaderboard, oldLeaderboardName, - descriptor, result); - } - - @Override - public void onFailure(Throwable caught) { - openUpdateFlexibleLeaderboardDialog(leaderboardDTO, otherExistingLeaderboard, oldLeaderboardName, - descriptor, new ArrayList()); - } - })); - } - - protected void openUpdateFlexibleLeaderboardDialog(StrippedLeaderboardDTO leaderboardDTO, List otherExistingLeaderboard, - final String oldLeaderboardName, LeaderboardDescriptor descriptor, List existingEvents) { - FlexibleLeaderboardEditDialog dialog = new FlexibleLeaderboardEditDialog( - Collections.unmodifiableCollection(otherExistingLeaderboard), descriptor, stringMessages, - Collections.unmodifiableList(existingEvents), errorReporter, - new DialogCallback() { - @Override - public void cancel() { - } - - @Override - public void ok(LeaderboardDescriptor result) { - updateLeaderboard(oldLeaderboardName, result); - } - }); - dialog.show(); - } - - /** - * Allow the user to combine the various URL parameters that exist for the {@link LeaderboardEntryPoint} and obtain the - * resulting URL in a link. The link's reference target is updated dynamically as the user adjusts the settings. Therefore, - * the link can be clicked, bookmarked or copied to the clipboard at any time. The OK / Cancel actions for the dialog shown - * are no-ops. - */ - private void openLeaderboardUrlConfigDialog(AbstractLeaderboardDTO leaderboard, StringMessages stringMessages) { - LeaderboardEntryPoint.getUrlConfigurationDialog(leaderboard, stringMessages).show(); - } - - public void loadLeaderboards() { - sailingService.getLeaderboards(new MarkedAsyncCallback>( - new AsyncCallback>() { - @Override - public void onSuccess(List leaderboards) { - leaderboardList.getList().clear(); - availableLeaderboardList.clear(); - leaderboardList.getList().addAll(leaderboards); - availableLeaderboardList.addAll(leaderboards); - filterLeaderboardPanel.updateAll(availableLeaderboardList); - leaderboardSelectionChanged(); - leaderboardRaceColumnSelectionChanged(); - } - - @Override - public void onFailure(Throwable t) { - LeaderboardConfigPanel.this.errorReporter.reportError("Error trying to obtain list of leaderboards: " - + t.getMessage()); - } - })); - } - - /** - * @param nameOfRaceColumnToSelect - * if not null, selects the first race column name with this name found in the leaderboard - * after the refresh has successfully completed. See {@link #selectRaceColumn(String)}. - */ - public void loadAndRefreshLeaderboard(final String leaderboardName, final String nameOfRaceColumnToSelect) { - sailingService.getLeaderboard(leaderboardName, new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onSuccess(StrippedLeaderboardDTO leaderboard) { - for (StrippedLeaderboardDTO leaderboardDTO : leaderboardSelectionModel.getSelectedSet()) { - if (leaderboardDTO.name.equals(leaderboardName)) { - leaderboardSelectionModel.setSelected(leaderboardDTO, false); - break; - } - } - - replaceLeaderboardInList(leaderboardList.getList(), leaderboardName, leaderboard); - replaceLeaderboardInList(availableLeaderboardList, leaderboardName, leaderboard); - leaderboardSelectionModel.setSelected(leaderboard, true); - if (nameOfRaceColumnToSelect != null) { - selectRaceColumn(nameOfRaceColumnToSelect); - } - leaderboardSelectionChanged(); - } - - @Override - public void onFailure(Throwable t) { - LeaderboardConfigPanel.this.errorReporter.reportError("Error trying to update leaderboard with name " + leaderboardName + " : " - + t.getMessage()); - } - })); - } - - private void replaceLeaderboardInList(List leaderboardList, String leaderboardToReplace, StrippedLeaderboardDTO newLeaderboard) { - int index = -1; - for (StrippedLeaderboardDTO existingLeaderboard : leaderboardList) { - index++; - if (existingLeaderboard.name.equals(leaderboardToReplace)) { - break; - } - } - if (index >= 0) { - leaderboardList.set(index, newLeaderboard); - } - } - - private void unlinkRaceColumnFromTrackedRace(final String raceColumnName, final FleetDTO fleet) { - final String selectedLeaderboardName = getSelectedLeaderboardName(); - sailingService.disconnectLeaderboardColumnFromTrackedRace(selectedLeaderboardName, raceColumnName, fleet.getName(), - new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onFailure(Throwable t) { - errorReporter.reportError("Error trying to unlink tracked race from column " + raceColumnName - + " from leaderboard " + selectedLeaderboardName + ": " + t.getMessage()); - } - - @Override - public void onSuccess(Void arg0) { - trackedRacesListComposite.clearSelection(); - getSelectedRaceColumnWithFleet().getA().setRaceIdentifier(fleet, null); - raceColumnAndFleetList.refresh(); - } - })); - } - - private void setStartTime(RaceColumnDTO raceColumnDTO, FleetDTO fleetDTO) { - new SetStartTimeDialog(sailingService, errorReporter, getSelectedLeaderboardName(), raceColumnDTO.getName(), - fleetDTO.getName(), stringMessages, new DialogCallback() { - @Override - public void ok(RaceLogSetStartTimeAndProcedureDTO editedObject) { - sailingService.setStartTimeAndProcedure(editedObject, new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(caught.getMessage()); - } - - @Override - public void onSuccess(Boolean result) { - if (!result) { - Window.alert(stringMessages.failedToSetNewStartTime()); - } - } - }); - } - - @Override - public void cancel() { } - }).show(); - } - - private void showRaceLog(final RaceColumnDTO raceColumnDTO, final FleetDTO fleetDTO) { - final String selectedLeaderboardName = getSelectedLeaderboardName(); - sailingService.getRaceLog(selectedLeaderboardName, raceColumnDTO, fleetDTO, - new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(caught.getMessage(), true); - } - @Override - public void onSuccess(RaceLogDTO result) { - openRaceLogDialog(result); - } - })); - } - - private void openRaceLogDialog(RaceLogDTO raceLogDTO) { - RaceLogDialog dialog = new RaceLogDialog(raceLogDTO, stringMessages, new DialogCallback() { - @Override - public void cancel() { - } - - @Override - public void ok(RaceLogDTO result) { - } - }); - dialog.show(); - } - - private void refreshRaceLog(final RaceColumnDTO raceColumnDTO, final FleetDTO fleet, final boolean showAlerts) { - final String selectedLeaderboardName = getSelectedLeaderboardName(); - sailingService.reloadRaceLog(selectedLeaderboardName, raceColumnDTO, fleet, new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - if (showAlerts) { - errorReporter.reportError(caught.getMessage()); - } - } - @Override - public void onSuccess(Void result) { - if (showAlerts) { - Window.alert(stringMessages.raceLogReloaded()); - } - } - })); - } - - private void removeRaceColumn(final RaceColumnDTO raceColumnDTO) { - final String selectedLeaderboardName = getSelectedLeaderboardName(); - final String raceColumnString = raceColumnDTO.getRaceColumnName(); - sailingService.removeLeaderboardColumn(getSelectedLeaderboardName(), raceColumnString, - new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onFailure(Throwable t) { - errorReporter.reportError("Error trying to remove leaderboard race column " + raceColumnDTO - + " in leaderboard " + getSelectedLeaderboardName() + ": " + t.getMessage()); - } - - @Override - public void onSuccess(Void arg0) { - loadAndRefreshLeaderboard(selectedLeaderboardName, /* raceColumnNameToSelect */ null); - } - })); - } - - /** - * The selected row is potentially only one of several fleet-based rows of the same RaceColumn. In this case, - * move all fleet-based rows of the same RaceColumn down. - */ - private void moveSelectedRaceColumnDown() { - final String selectedLeaderboardName = getSelectedLeaderboardName(); - final String selectedRaceColumnName = raceColumnTableSelectionModel.getSelectedObject().getA().getRaceColumnName(); - sailingService.moveLeaderboardColumnDown(getSelectedLeaderboardName(), selectedRaceColumnName, - new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError("Error trying to move leaderboard race column " - + selectedRaceColumnName + " in leaderboard " + getSelectedLeaderboardName() - + " down: " + caught.getMessage()); - } - - @Override - public void onSuccess(Void result) { - loadAndRefreshLeaderboard(selectedLeaderboardName, selectedRaceColumnName); - } - })); - } - - /** - * The selected row is potentially only one of several fleet-based rows of the same RaceColumn. In this case, - * move all fleet-based rows of the same RaceColumn down. - */ - private void moveSelectedRaceColumnUp() { - final String selectedLeaderboardName = getSelectedLeaderboardName(); - final String selectedRaceColumnName = raceColumnTableSelectionModel.getSelectedObject().getA().getRaceColumnName(); - sailingService.moveLeaderboardColumnUp(getSelectedLeaderboardName(), selectedRaceColumnName, - new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError("Error trying to move leaderboard race column " - + selectedRaceColumnName + " in leaderboard " + getSelectedLeaderboardName() + " up: " - + caught.getMessage()); - } - - @Override - public void onSuccess(Void result) { - loadAndRefreshLeaderboard(selectedLeaderboardName, selectedRaceColumnName); - } - })); - } - - private void leaderboardRaceColumnSelectionChanged() { - selectedRaceInLeaderboard = getSelectedRaceColumnWithFleet(); - if (selectedRaceInLeaderboard != null) { - columnMoveUpButton.setEnabled(true); - columnMoveDownButton.setEnabled(true); - selectTrackedRaceInRaceList(); - } else { - columnMoveUpButton.setEnabled(false); - columnMoveDownButton.setEnabled(false); - trackedRacesListComposite.clearSelection(); - } - } - - private void selectRaceColumn(String raceCoumnName) { - List list = raceColumnAndFleetList.getList(); - for (RaceColumnDTOAndFleetDTOWithNameBasedEquality pair : list) { - if (pair.getA().getName().equals(raceCoumnName)) { - raceColumnTableSelectionModel.setSelected(pair, true); - break; - } - } - } - - private void selectTrackedRaceInRaceList() { - final String selectedLeaderboardName = getSelectedLeaderboardName(); - if (selectedLeaderboardName != null) { - final RaceColumnDTOAndFleetDTOWithNameBasedEquality selectedRaceColumnAndFleetNameInLeaderboard = getSelectedRaceColumnWithFleet(); - final String selectedRaceColumnName = selectedRaceColumnAndFleetNameInLeaderboard.getA().getRaceColumnName(); - final String selectedFleetName = selectedRaceColumnAndFleetNameInLeaderboard.getB().getName(); - sailingService.getRegattaAndRaceNameOfTrackedRaceConnectedToLeaderboardColumn(selectedLeaderboardName, - selectedRaceColumnName, new MarkedAsyncCallback>( - new AsyncCallback>() { - @Override - public void onFailure(Throwable t) { - errorReporter.reportError("Error trying to determine tracked race linked to race column " - + selectedRaceColumnName + " in leaderboard " + selectedLeaderboardName + ": " - + t.getMessage()); - } - - @Override - public void onSuccess(Map regattaAndRaceNamesPerFleet) { - if (regattaAndRaceNamesPerFleet != null && !regattaAndRaceNamesPerFleet.isEmpty()) { - RegattaAndRaceIdentifier raceIdentifier = regattaAndRaceNamesPerFleet.get(selectedFleetName); - - if (raceIdentifier != null) { - selectRaceInList(raceIdentifier.getRegattaName(), raceIdentifier.getRaceName()); - } else { - trackedRacesListComposite.clearSelection(); - } - } else { - trackedRacesListComposite.clearSelection(); - } - } - })); - } - } - - private void selectRaceInList(String regattaName, String raceName) { - RegattaNameAndRaceName raceIdentifier = new RegattaNameAndRaceName(regattaName, raceName); - trackedRacesListComposite.selectRaceByIdentifier(raceIdentifier); - } - - private RaceColumnDTOAndFleetDTOWithNameBasedEquality getSelectedRaceColumnWithFleet() { - RaceColumnDTOAndFleetDTOWithNameBasedEquality raceInLeaderboardAndFleetName = raceColumnTableSelectionModel.getSelectedObject(); - return raceInLeaderboardAndFleetName; - } - - private void editRaceColumnOfLeaderboard(final RaceColumnDTOAndFleetDTOWithNameBasedEquality raceColumnWithFleet) { - final String selectedLeaderboardName = getSelectedLeaderboardName(); - final boolean oldIsMedalRace = raceColumnWithFleet.getA().isMedalRace(); - final String oldRaceColumnName = raceColumnWithFleet.getA().getRaceColumnName(); - final Double oldExplicitFactor = raceColumnWithFleet.getA().getExplicitFactor(); - // use a set to avoid duplicates in the case of regatta leaderboards with multiple fleets per column - Set existingRacesWithoutThisRace = new HashSet(); - for (RaceColumnDTOAndFleetDTOWithNameBasedEquality pair : raceColumnAndFleetList.getList()) { - existingRacesWithoutThisRace.add(pair.getA()); - } - existingRacesWithoutThisRace.remove(raceColumnWithFleet.getA()); - final RaceColumnInLeaderboardDialog raceDialog = new RaceColumnInLeaderboardDialog(existingRacesWithoutThisRace, - raceColumnWithFleet.getA(), getSelectedLeaderboard().type.isRegattaLeaderboard(), stringMessages, new DialogCallback() { - @Override - public void cancel() { - } - - @Override - public void ok(final RaceColumnDescriptor result) { - boolean rename = !oldRaceColumnName.equals(result.getName()); - boolean updateIsMedalRace = oldIsMedalRace != result.isMedalRace(); - boolean updateFactor = oldExplicitFactor != result.getExplicitFactor(); - List> callbacks = new ArrayList>(); - final ParallelExecutionCallback renameLeaderboardColumnCallback = new ParallelExecutionCallback(); - if (rename) { - callbacks.add(renameLeaderboardColumnCallback); - } - final ParallelExecutionCallback updateIsMedalRaceCallback = new ParallelExecutionCallback(); - if (updateIsMedalRace) { - callbacks.add(updateIsMedalRaceCallback); - } - final ParallelExecutionCallback updateLeaderboardColumnFactorCallback = new ParallelExecutionCallback(); - if (updateFactor) { - callbacks.add(updateLeaderboardColumnFactorCallback); - } - new ParallelExecutionHolder(callbacks.toArray(new ParallelExecutionCallback[0])) { - @Override - public void handleSuccess() { - loadAndRefreshLeaderboard(selectedLeaderboardName, result.getName()); - } - @Override - public void handleFailure(Throwable t) { - errorReporter.reportError("Error trying to update data of race column " - + oldRaceColumnName + " in leaderboard " + selectedLeaderboardName + ": " - + t.getMessage()); - } - }; - if (rename) { - sailingService.renameLeaderboardColumn(selectedLeaderboardName, oldRaceColumnName, - result.getName(), renameLeaderboardColumnCallback); - } - if (updateIsMedalRace) { - sailingService.updateIsMedalRace(selectedLeaderboardName, result.getName(), - result.isMedalRace(), updateIsMedalRaceCallback); - } - if (updateFactor) { - sailingService.updateLeaderboardColumnFactor(selectedLeaderboardName, result.getName(), - result.getExplicitFactor(), updateLeaderboardColumnFactorCallback); - } - } - }); - raceDialog.show(); - } - - private void setIsMedalRace(String leaderboardName, final RaceColumnDTO raceInLeaderboard, - final boolean isMedalRace) { - sailingService.updateIsMedalRace(leaderboardName, raceInLeaderboard.getRaceColumnName(), isMedalRace, - new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorUpdatingIsMedalRace(caught.getMessage())); - } - - @Override - public void onSuccess(Void result) { - getSelectedLeaderboard().setIsMedalRace(raceInLeaderboard.getRaceColumnName(), isMedalRace); - } - })); - } - - private void addRaceColumnsToLeaderboard() { - final String leaderboardName = getSelectedLeaderboardName(); - final List existingRaceColumns = new ArrayList(); - for (RaceColumnDTOAndFleetDTOWithNameBasedEquality pair : raceColumnAndFleetList.getList()) { - existingRaceColumns.add(pair.getA()); - } - final RaceColumnsInLeaderboardDialog raceDialog = new RaceColumnsInLeaderboardDialog(existingRaceColumns, - stringMessages, new DialogCallback>() { - @Override - public void cancel() { - } - - @Override - public void ok(final List result) { - updateRaceColumnsOfLeaderboard(leaderboardName, existingRaceColumns, result); - } - }); - raceDialog.ensureDebugId("RaceColumnsInLeaderboardDialog"); - raceDialog.show(); - } - - private void updateRaceColumnsOfLeaderboard(final String leaderboardName, List existingRaceColumns, List newRaceColumns) { - final List> raceColumnsToAdd = new ArrayList>(); - - for (RaceColumnDTO newRaceColumn : newRaceColumns) { - if (!existingRaceColumns.contains(newRaceColumn)) { - raceColumnsToAdd.add(new Pair(newRaceColumn.getName(), newRaceColumn.isMedalRace())); - } - } - - sailingService.addColumnsToLeaderboard(leaderboardName, raceColumnsToAdd, new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError("Error trying to add race columns to leaderboard " + leaderboardName - + ": " + caught.getMessage()); - } - - @Override - public void onSuccess(Void v) { - loadAndRefreshLeaderboard(leaderboardName, /* nameOfRaceColumnToSelect */ null); - } - })); - } - - private String getSelectedLeaderboardName() { - return getSelectedLeaderboard() != null ? getSelectedLeaderboard().name : null; - } - - private void leaderboardSelectionChanged() { - // make sure that clearing the selection doesn't cause an unlinking of the selected tracked race - raceSelectionProvider.removeRaceSelectionChangeListener(this); - trackedRacesListComposite.clearSelection(); - // add listener again using a scheduled command which is executed when the browser's event loop re-gains - // control; we assume that at that point in time the selection updates have already been performed - Scheduler.get().scheduleFinally(new ScheduledCommand() { - @Override - public void execute() { - raceSelectionProvider.addRaceSelectionChangeListener(LeaderboardConfigPanel.this); - } - }); - leaderboardRemoveButton.setEnabled(!leaderboardSelectionModel.getSelectedSet().isEmpty()); - StrippedLeaderboardDTO selectedLeaderboard = getSelectedLeaderboard(); - if (leaderboardSelectionModel.getSelectedSet().size() == 1 && selectedLeaderboard != null) { - raceColumnAndFleetList.getList().clear(); - for (RaceColumnDTO raceColumn : selectedLeaderboard.getRaceList()) { - for (FleetDTO fleet : raceColumn.getFleets()) { - raceColumnAndFleetList.getList().add(new RaceColumnDTOAndFleetDTOWithNameBasedEquality(raceColumn, fleet)); - } - } - selectedLeaderBoardPanel.setVisible(true); - selectedLeaderBoardPanel.setCaptionText("Details of leaderboard '" + selectedLeaderboard.name + "'"); - if (!selectedLeaderboard.type.isMetaLeaderboard()) { - trackedRacesCaptionPanel.setVisible(true); - } - addRaceColumnsButton.setVisible(!selectedLeaderboard.type.isRegattaLeaderboard()); - columnMoveUpButton.setVisible(!selectedLeaderboard.type.isRegattaLeaderboard()); - columnMoveDownButton.setVisible(!selectedLeaderboard.type.isRegattaLeaderboard()); - } else { - selectedLeaderBoardPanel.setVisible(false); - trackedRacesCaptionPanel.setVisible(false); - selectedRaceInLeaderboard = null; - } - } - - @Override - public void fillRegattas(List regattas) { - trackedRacesListComposite.fillRegattas(regattas); - - allRegattas.clear(); - allRegattas.addAll(regattas); - } - - @Override - public void changeTrackingRace(Iterable regattaAndRaceIdentifiers, boolean isTracked) { - for (RegattaAndRaceIdentifier regattaAndRaceIdentifier : regattaAndRaceIdentifiers) { - for (RaceColumnDTOAndFleetDTOWithNameBasedEquality raceColumnAndFleetName : raceColumnAndFleetList.getList()) { - if (raceColumnAndFleetName.getA().getRaceColumnName().equals(regattaAndRaceIdentifier.getRaceName())) { - raceColumnAndFleetName.getA().setRaceIdentifier(raceColumnAndFleetName.getB(), - regattaAndRaceIdentifier); - } - } - raceColumnAndFleetList.refresh(); - } - } - - private void createFlexibleLeaderboard() { - sailingService.getEvents(new MarkedAsyncCallback>( - new AsyncCallback>() { - @Override - public void onSuccess(List result) { - createFlexibleLeaderboard(result); - } - - @Override - public void onFailure(Throwable caught) { - createFlexibleLeaderboard(new ArrayList()); - } - })); - } - - private void createFlexibleLeaderboard(List existingEvents) { - - AbstractLeaderboardDialog dialog = new FlexibleLeaderboardCreateDialog(Collections.unmodifiableCollection(availableLeaderboardList), - stringMessages, Collections.unmodifiableCollection(existingEvents), errorReporter, new DialogCallback() { - @Override - public void cancel() { - } - - @Override - public void ok(final LeaderboardDescriptor newLeaderboard) { - sailingService.createFlexibleLeaderboard(newLeaderboard.getName(), newLeaderboard.getDisplayName(), - newLeaderboard.getDiscardThresholds(), newLeaderboard.getScoringScheme(), newLeaderboard.getCourseAreaId(), - new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onFailure(Throwable t) { - errorReporter.reportError("Error trying to create the new flexible leaderboard " + newLeaderboard.getName() - + ": " + t.getMessage()); - } - - @Override - public void onSuccess(StrippedLeaderboardDTO result) { - addLeaderboard(result); - } - })); - } - }); - dialog.ensureDebugId("FlexibleLeaderboardCreateDialog"); - dialog.show(); - } - - private void createRegattaLeaderboard() { - RegattaLeaderboardCreateDialog dialog = new RegattaLeaderboardCreateDialog(Collections.unmodifiableCollection(availableLeaderboardList), - Collections.unmodifiableCollection(allRegattas), stringMessages, errorReporter, new DialogCallback() { - @Override - public void cancel() { - } - - @Override - public void ok(final LeaderboardDescriptor newLeaderboard) { - RegattaIdentifier regattaIdentifier = new RegattaName(newLeaderboard.getRegattaName()); - sailingService.createRegattaLeaderboard(regattaIdentifier, newLeaderboard.getDisplayName(), newLeaderboard.getDiscardThresholds(), - new AsyncCallback() { - @Override - public void onFailure(Throwable t) { - errorReporter.reportError("Error trying to create the new regatta leaderboard " + newLeaderboard.getName() - + ": " + t.getMessage()); - } - - @Override - public void onSuccess(StrippedLeaderboardDTO result) { - addLeaderboard(result); - } - }); - } - }); - dialog.ensureDebugId("RegattaLeaderboardCreateDialog"); - dialog.show(); - } - - private void addLeaderboard(StrippedLeaderboardDTO result) { - leaderboardList.getList().add(result); - availableLeaderboardList.add(result); - leaderboardSelectionModel.clear(); - leaderboardSelectionModel.setSelected(result, true); - } - - private void updateLeaderboard(final String oldLeaderboardName, final LeaderboardDescriptor leaderboardToUpdate) { - sailingService.updateLeaderboard(oldLeaderboardName, leaderboardToUpdate.getName(), leaderboardToUpdate.getDisplayName(), - leaderboardToUpdate.getDiscardThresholds(), leaderboardToUpdate.getCourseAreaId(), - new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onFailure(Throwable t) { - errorReporter.reportError("Error trying to update leaderboard " + oldLeaderboardName + ": " - + t.getMessage()); - } - - @Override - public void onSuccess(StrippedLeaderboardDTO updatedLeaderboard) { - int indexOfLeaderboard = 0; - for (int i = 0; i < leaderboardList.getList().size(); i++) { - StrippedLeaderboardDTO dao = leaderboardList.getList().get(i); - if (dao.name.equals(oldLeaderboardName)) { - indexOfLeaderboard = i; - break; - } - } - leaderboardList.getList().set(indexOfLeaderboard, updatedLeaderboard); - leaderboardList.refresh(); - } - })); - } - - private void removeLeaderboards(final Collection leaderboards) { - if (!leaderboards.isEmpty()) { - Set leaderboardNames = new HashSet(); - for (StrippedLeaderboardDTO leaderboard : leaderboards) { - leaderboardNames.add(leaderboard.name); - } - sailingService.removeLeaderboards(leaderboardNames, new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError("Error trying to remove the leaderboards:" + caught.getMessage()); - - - } - - @Override - public void onSuccess(Void result) { - for (StrippedLeaderboardDTO leaderboard : leaderboards) { - removeLeaderboardFromTable(leaderboard); - } - } - })); - } - } - - private void removeLeaderboard(final StrippedLeaderboardDTO leaderBoard) { - sailingService.removeLeaderboard(leaderBoard.name, new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError("Error trying to remove leaderboard " + leaderBoard.name + ": " - + caught.getMessage()); - } - - @Override - public void onSuccess(Void result) { - removeLeaderboardFromTable(leaderBoard); - } - })); - } - - private void removeLeaderboardFromTable(final StrippedLeaderboardDTO leaderBoard) { - leaderboardList.getList().remove(leaderBoard); - availableLeaderboardList.remove(leaderBoard); - leaderboardSelectionModel.setSelected(leaderBoard, false); - } - - @Override - public void onRaceSelectionChange(List selectedRaces) { - // if no leaderboard column is selected, ignore the race selection change - RaceColumnDTOAndFleetDTOWithNameBasedEquality selectedRaceColumnAndFleetName = getSelectedRaceColumnWithFleet(); - if (selectedRaceColumnAndFleetName != null) { - if (selectedRaces.isEmpty()) { - if (selectedRaceColumnAndFleetName.getA().getRaceIdentifier(selectedRaceColumnAndFleetName.getB()) != null) { - unlinkRaceColumnFromTrackedRace(selectedRaceColumnAndFleetName.getA().getRaceColumnName(), selectedRaceColumnAndFleetName.getB()); - } - } else { - linkTrackedRaceToSelectedRaceColumn(selectedRaceColumnAndFleetName.getA(), selectedRaceColumnAndFleetName.getB(), - selectedRaces.iterator().next()); - } - } - } - - private void linkTrackedRaceToSelectedRaceColumn(final RaceColumnDTO selectedRaceInLeaderboard, - final FleetDTO fleet, final RegattaAndRaceIdentifier selectedRace) { - sailingService.connectTrackedRaceToLeaderboardColumn(getSelectedLeaderboardName(), selectedRaceInLeaderboard - .getRaceColumnName(), fleet.getName(), selectedRace, - new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onFailure(Throwable t) { - errorReporter.reportError("Error trying to link tracked race " + selectedRace + " to race column named " - + selectedRaceInLeaderboard.getRaceColumnName() + " of leaderboard " - + getSelectedLeaderboardName() + ": " + t.getMessage()); - trackedRacesListComposite.clearSelection(); - } - - @Override - public void onSuccess(Boolean success) { - if (success) { - // TODO consider enabling the Unlink button - selectedRaceInLeaderboard.setRaceIdentifier(fleet, selectedRace); - raceColumnAndFleetList.refresh(); - } - } - })); - } - - @Override - public StrippedLeaderboardDTO getSelectedLeaderboard() { - return leaderboardSelectionModel.getSelectedSet().isEmpty() ? null : leaderboardSelectionModel.getSelectedSet().iterator().next(); - } +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.gwt.cell.client.AbstractCell; +import com.google.gwt.cell.client.FieldUpdater; +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.safehtml.client.SafeHtmlTemplates; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; +import com.google.gwt.user.cellview.client.CellTable; +import com.google.gwt.user.cellview.client.Column; +import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler; +import com.google.gwt.user.cellview.client.ColumnSortList; +import com.google.gwt.user.cellview.client.TextColumn; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.HasHorizontalAlignment; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.view.client.SingleSelectionModel; +import com.sap.sailing.domain.common.RegattaIdentifier; +import com.sap.sailing.domain.common.RegattaName; +import com.sap.sailing.domain.common.dto.AbstractLeaderboardDTO; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.domain.common.dto.FleetDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.gwt.ui.adminconsole.DisablableCheckboxCell.IsEnabled; +import com.sap.sailing.gwt.ui.adminconsole.RaceColumnInLeaderboardDialog.RaceColumnDescriptor; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.ParallelExecutionCallback; +import com.sap.sailing.gwt.ui.client.ParallelExecutionHolder; +import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; +import com.sap.sailing.gwt.ui.client.RegattaDisplayer; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.URLEncoder; +import com.sap.sailing.gwt.ui.leaderboard.LeaderboardEntryPoint; +import com.sap.sailing.gwt.ui.leaderboard.ScoringSchemeTypeFormatter; +import com.sap.sailing.gwt.ui.shared.EventDTO; +import com.sap.sailing.gwt.ui.shared.RaceLogDTO; +import com.sap.sailing.gwt.ui.shared.RaceLogSetStartTimeAndProcedureDTO; +import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; +import com.sap.sse.gwt.client.async.MarkedAsyncCallback; +import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; + +public class LeaderboardConfigPanel extends AbstractLeaderboardConfigPanel implements SelectedLeaderboardProvider, RegattaDisplayer, RaceSelectionChangeListener, +TrackedRaceChangedListener { + private final AnchorTemplates ANCHORTEMPLATE = GWT.create(AnchorTemplates.class); + + private final boolean showRaceDetails; + + private Button leaderboardRemoveButton; + + private Button addRaceColumnsButton; + + private Button columnMoveUpButton; + private Button columnMoveDownButton; + + public static class AnchorCell extends AbstractCell { + + @Override + public void render(com.google.gwt.cell.client.Cell.Context context, SafeHtml safeHtml, SafeHtmlBuilder sb) { + sb.append(safeHtml); + } + } + + interface AnchorTemplates extends SafeHtmlTemplates { + @SafeHtmlTemplates.Template("{1}") + SafeHtml cell(String url, String displayName); + } + + + public LeaderboardConfigPanel(final SailingServiceAsync sailingService, AdminConsoleEntryPoint adminConsole, + final ErrorReporter errorReporter, StringMessages theStringConstants, final boolean showRaceDetails) { + super(sailingService, adminConsole, errorReporter, theStringConstants, new SingleSelectionModel()); + this.showRaceDetails = showRaceDetails; + + leaderboardTable.ensureDebugId("LeaderboardsCellTable"); + } + + @Override + protected void addLeaderboardConfigControls(Panel configPanel) { + filterLeaderboardPanel.getTextBox().ensureDebugId("LeaderboardsFilterTextBox"); + + leaderboardRemoveButton = new Button(stringMessages.remove()); + leaderboardRemoveButton.ensureDebugId("LeaderboardsRemoveButton"); + leaderboardRemoveButton.setEnabled(false); + leaderboardRemoveButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + if (Window.confirm(stringMessages.doYouReallyWantToRemoveLeaderboards())) { + removeLeaderboards(leaderboardSelectionModel.getSelectedSet()); + } + } + }); + configPanel.add(leaderboardRemoveButton); + } + + @Override + protected void addColumnsToLeaderboardTable(final CellTable leaderboardTable) { + ListHandler leaderboardColumnListHandler = new ListHandler( + leaderboardList.getList()); + + AnchorCell anchorCell = new AnchorCell(); + Column linkColumn = new Column(anchorCell) { + @Override + public SafeHtml getValue(StrippedLeaderboardDTO object) { + String debugParam = Window.Location.getParameter("gwt.codesvr"); + String link = URLEncoder.encode("/gwt/Leaderboard.html?name=" + object.name + + (showRaceDetails ? "&showRaceDetails=true" : "") + + (object.displayName != null ? "&displayName="+object.displayName : "") + + (debugParam != null && !debugParam.isEmpty() ? "&gwt.codesvr=" + debugParam : "")); + return ANCHORTEMPLATE.cell(link, object.name); + } + + }; + linkColumn.setSortable(true); + leaderboardColumnListHandler.setComparator(linkColumn, new Comparator() { + + @Override + public int compare(StrippedLeaderboardDTO o1, StrippedLeaderboardDTO o2) { + boolean ascending = isSortedAscending(); + if (o1.name.equals(o2.name)) { + return 0; + } + int val = -1; + val = (o1 != null && o2 != null && ascending) ? (o1.name.compareTo(o2.name)) : -(o2.name + .compareTo(o1.name)); + + return val; + } + + private boolean isSortedAscending() { + ColumnSortList sortList = leaderboardTable.getColumnSortList(); + return sortList.size() > 0 & sortList.get(0).isAscending(); + } + }); + + TextColumn leaderboardDisplayNameColumn = new TextColumn() { + @Override + public String getValue(StrippedLeaderboardDTO leaderboard) { + return leaderboard.getDisplayName() != null ? leaderboard.getDisplayName() : ""; + } + }; + + TextColumn discardingOptionsColumn = new TextColumn() { + @Override + public String getValue(StrippedLeaderboardDTO leaderboard) { + String result = ""; + if (leaderboard.discardThresholds != null) { + for (int discardThreshold : leaderboard.discardThresholds) { + result += discardThreshold + " "; + } + } + return result; + } + }; + + TextColumn leaderboardTypeColumn = new TextColumn() { + @Override + public String getValue(StrippedLeaderboardDTO leaderboard) { + String result = leaderboard.type.isRegattaLeaderboard() ? "Regatta" : "Flexible"; + if (leaderboard.type.isMetaLeaderboard()) { + result += " , Meta"; + } + return result; + } + }; + + TextColumn scoringSystemColumn = new TextColumn() { + @Override + public String getValue(StrippedLeaderboardDTO leaderboard) { + return leaderboard.scoringScheme == null ? "" : ScoringSchemeTypeFormatter.format(leaderboard.scoringScheme, stringMessages); + } + }; + + TextColumn courseAreaColumn = new TextColumn() { + @Override + public String getValue(StrippedLeaderboardDTO leaderboard) { + return leaderboard.defaultCourseAreaId == null ? "" : leaderboard.defaultCourseAreaName; + } + }; + + ImagesBarColumn leaderboardActionColumn = new ImagesBarColumn( + new LeaderboardConfigImagesBarCell(stringMessages)); + leaderboardActionColumn.setFieldUpdater(new FieldUpdater() { + @Override + public void update(int index, StrippedLeaderboardDTO leaderboardDTO, String value) { + if (LeaderboardConfigImagesBarCell.ACTION_REMOVE.equals(value)) { + if (Window.confirm(stringMessages.doYouReallyWantToRemoveLeaderboard(leaderboardDTO.name))) { + removeLeaderboard(leaderboardDTO); + } + } else if (LeaderboardConfigImagesBarCell.ACTION_EDIT.equals(value)) { + final String oldLeaderboardName = leaderboardDTO.name; + List otherExistingLeaderboard = new ArrayList(); + otherExistingLeaderboard.addAll(availableLeaderboardList); + otherExistingLeaderboard.remove(leaderboardDTO); + if (leaderboardDTO.type.isMetaLeaderboard()) { + Window.alert(stringMessages.metaLeaderboardCannotBeChanged()); + } else { + if (leaderboardDTO.type.isRegattaLeaderboard()) { + LeaderboardDescriptor descriptor = new LeaderboardDescriptor(leaderboardDTO.name, + leaderboardDTO.displayName, /* scoring scheme provided by regatta */ null, + leaderboardDTO.discardThresholds, leaderboardDTO.regattaName, + leaderboardDTO.defaultCourseAreaId); + AbstractLeaderboardDialog dialog = new RegattaLeaderboardEditDialog(Collections + .unmodifiableCollection(otherExistingLeaderboard), Collections.unmodifiableCollection(allRegattas), + descriptor, stringMessages, errorReporter, + new DialogCallback() { + @Override + public void cancel() { + } + + @Override + public void ok(LeaderboardDescriptor result) { + updateLeaderboard(oldLeaderboardName, result); + } + }); + dialog.show(); + } else { + LeaderboardDescriptor descriptor = new LeaderboardDescriptor(leaderboardDTO.name, leaderboardDTO.displayName, leaderboardDTO.scoringScheme, leaderboardDTO.discardThresholds, leaderboardDTO.defaultCourseAreaId); + openUpdateFlexibleLeaderboardDialog(leaderboardDTO, otherExistingLeaderboard, leaderboardDTO.name, descriptor); + } + } + } else if (LeaderboardConfigImagesBarCell.ACTION_EDIT_SCORES.equals(value)) { + String debugParam = Window.Location.getParameter("gwt.codesvr"); + Window.open("/gwt/LeaderboardEditing.html?name=" + leaderboardDTO.name + + (debugParam != null && !debugParam.isEmpty() ? "&gwt.codesvr=" + debugParam : ""), "_blank", null); + } else if (LeaderboardConfigImagesBarCell.ACTION_EDIT_COMPETITORS.equals(value)) { + EditCompetitorsDialog editCompetitorsDialog = new EditCompetitorsDialog(sailingService, leaderboardDTO.name, stringMessages, + errorReporter, new DialogCallback>() { + @Override + public void cancel() { + } + + @Override + public void ok(final List result) { + } + }); + editCompetitorsDialog.show(); + + } else if (LeaderboardConfigImagesBarCell.ACTION_CONFIGURE_URL.equals(value)) { + openLeaderboardUrlConfigDialog(leaderboardDTO, stringMessages); + } else if (LeaderboardConfigImagesBarCell.ACTION_EXPORT_XML.equals(value)) { + Window.open("/export/xml?domain=leaderboard&name=" + leaderboardDTO.name, "", null); + } + } + }); + leaderboardTable.addColumn(linkColumn, stringMessages.name()); + leaderboardTable.addColumn(leaderboardDisplayNameColumn, stringMessages.displayName()); + leaderboardTable.addColumn(discardingOptionsColumn, stringMessages.discarding()); + leaderboardTable.addColumn(leaderboardTypeColumn, stringMessages.type()); + leaderboardTable.addColumn(scoringSystemColumn, stringMessages.scoringSystem()); + leaderboardTable.addColumn(courseAreaColumn, stringMessages.courseArea()); + leaderboardTable.addColumn(leaderboardActionColumn, stringMessages.actions()); + leaderboardTable.addColumnSortHandler(leaderboardColumnListHandler); + } + + protected void addLeaderboardCreateControls(Panel createPanel) { + Button createFlexibleLeaderboardBtn = new Button(stringMessages.createFlexibleLeaderboard() + "..."); + createFlexibleLeaderboardBtn.ensureDebugId("CreateFlexibleLeaderboardButton"); + createPanel.add(createFlexibleLeaderboardBtn); + createFlexibleLeaderboardBtn.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent clickEvent) { + createFlexibleLeaderboard(); + } + }); + + Button createRegattaLeaderboardBtn = new Button(stringMessages.createRegattaLeaderboard() + "..."); + createRegattaLeaderboardBtn.ensureDebugId("CreateRegattaLeaderboardButton"); + createPanel.add(createRegattaLeaderboardBtn); + createRegattaLeaderboardBtn.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent clickEvent) { + createRegattaLeaderboard(); + } + }); + } + + @Override + protected void addColumnsToRacesTable(CellTable racesTable) { + TextColumn explicitFactorColumn = new TextColumn() { + @Override + public String getValue(RaceColumnDTOAndFleetDTOWithNameBasedEquality object) { + return object.getA().getExplicitFactor() == null ? "" : object.getA().getExplicitFactor().toString(); + } + }; + + Column isMedalRaceCheckboxColumn = new Column( + new DisablableCheckboxCell(new IsEnabled() { + @Override + public boolean isEnabled() { + return getSelectedLeaderboard() != null && !getSelectedLeaderboard().type.isRegattaLeaderboard(); + + } + })) { + @Override + public Boolean getValue(RaceColumnDTOAndFleetDTOWithNameBasedEquality race) { + return race.getA().isMedalRace(); + } + }; + isMedalRaceCheckboxColumn.setFieldUpdater(new FieldUpdater() { + @Override + public void update(int index, RaceColumnDTOAndFleetDTOWithNameBasedEquality object, Boolean value) { + setIsMedalRace(getSelectedLeaderboard().name, object.getA(), value); + } + }); + isMedalRaceCheckboxColumn.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_CENTER); + + TextColumn isLinkedRaceColumn = new TextColumn() { + @Override + public String getValue(RaceColumnDTOAndFleetDTOWithNameBasedEquality raceColumnAndFleetName) { + boolean isTrackedRace = raceColumnAndFleetName.getA().isTrackedRace(raceColumnAndFleetName.getB()); + return isTrackedRace ? stringMessages.yes() : stringMessages.no(); + } + }; + + ImagesBarColumn raceActionColumn = + new ImagesBarColumn( + new LeaderboardRaceConfigImagesBarCell(this, stringMessages)); + raceActionColumn.setFieldUpdater(new FieldUpdater() { + @Override + public void update(int index, RaceColumnDTOAndFleetDTOWithNameBasedEquality object, String value) { + if (LeaderboardRaceConfigImagesBarCell.ACTION_REMOVE.equals(value)) { + if (Window.confirm(stringMessages.reallyRemoveRace(object.getA().getRaceColumnName()))) { + removeRaceColumn(object.getA()); + } + } else if (LeaderboardRaceConfigImagesBarCell.ACTION_EDIT.equals(value)) { + editRaceColumnOfLeaderboard(object); + } else if (LeaderboardRaceConfigImagesBarCell.ACTION_UNLINK.equals(value)) { + unlinkRaceColumnFromTrackedRace(object.getA().getRaceColumnName(), object.getB()); + } else if (LeaderboardRaceConfigImagesBarCell.ACTION_REFRESH_RACELOG.equals(value)) { + refreshRaceLog(object.getA(), object.getB(), true); + } else if(LeaderboardRaceConfigImagesBarCell.ACTION_SET_STARTTIME.equals(value)) { + setStartTime(object.getA(), object.getB()); + } else if(LeaderboardRaceConfigImagesBarCell.ACTION_SHOW_RACELOG.equals(value)) { + showRaceLog(object.getA(), object.getB()); + } + } + }); + + racesTable.addColumn(isMedalRaceCheckboxColumn, stringMessages.medalRace()); + racesTable.addColumn(isLinkedRaceColumn, stringMessages.islinked()); + racesTable.addColumn(explicitFactorColumn, stringMessages.factor()); + racesTable.addColumn(raceActionColumn, stringMessages.actions()); + + racesTable.ensureDebugId("RacesCellTable"); + } + + @Override + protected void addSelectedLeaderboardRacesControls(Panel racesPanel) { + addRaceColumnsButton = new Button(stringMessages.actionAddRaces() + "..."); + addRaceColumnsButton.ensureDebugId("AddRacesButton"); + racesPanel.add(addRaceColumnsButton); + addRaceColumnsButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + if (getSelectedLeaderboard().type.isRegattaLeaderboard()) { + Window.alert(stringMessages.cannotAddRacesToRegattaLeaderboardButOnlyToRegatta()); + } else { + addRaceColumnsToLeaderboard(); + } + } + }); + racesPanel.add(addRaceColumnsButton); + + columnMoveUpButton = new Button(stringMessages.columnMoveUp()); + racesPanel.add(columnMoveUpButton); + columnMoveUpButton.ensureDebugId("MoveRaceUpButton"); + columnMoveUpButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + moveSelectedRaceColumnUp(); + } + }); + racesPanel.add(columnMoveUpButton); + + columnMoveDownButton = new Button(stringMessages.columnMoveDown()); + racesPanel.add(columnMoveDownButton); + columnMoveDownButton.ensureDebugId("MoveRaceDownButton"); + columnMoveDownButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + moveSelectedRaceColumnDown(); + } + }); + racesPanel.add(columnMoveDownButton); + } + + protected void openUpdateFlexibleLeaderboardDialog(final StrippedLeaderboardDTO leaderboardDTO, final List otherExistingLeaderboard, + final String oldLeaderboardName, final LeaderboardDescriptor descriptor) { + sailingService.getEvents(new MarkedAsyncCallback>( + new AsyncCallback>() { + @Override + public void onSuccess(List result) { + openUpdateFlexibleLeaderboardDialog(leaderboardDTO, otherExistingLeaderboard, oldLeaderboardName, + descriptor, result); + } + + @Override + public void onFailure(Throwable caught) { + openUpdateFlexibleLeaderboardDialog(leaderboardDTO, otherExistingLeaderboard, oldLeaderboardName, + descriptor, new ArrayList()); + } + })); + } + + protected void openUpdateFlexibleLeaderboardDialog(StrippedLeaderboardDTO leaderboardDTO, List otherExistingLeaderboard, + final String oldLeaderboardName, LeaderboardDescriptor descriptor, List existingEvents) { + FlexibleLeaderboardEditDialog dialog = new FlexibleLeaderboardEditDialog( + Collections.unmodifiableCollection(otherExistingLeaderboard), descriptor, stringMessages, + Collections.unmodifiableList(existingEvents), errorReporter, + new DialogCallback() { + @Override + public void cancel() { + } + + @Override + public void ok(LeaderboardDescriptor result) { + updateLeaderboard(oldLeaderboardName, result); + } + }); + dialog.show(); + } + + /** + * Allow the user to combine the various URL parameters that exist for the {@link LeaderboardEntryPoint} and obtain the + * resulting URL in a link. The link's reference target is updated dynamically as the user adjusts the settings. Therefore, + * the link can be clicked, bookmarked or copied to the clipboard at any time. The OK / Cancel actions for the dialog shown + * are no-ops. + */ + private void openLeaderboardUrlConfigDialog(AbstractLeaderboardDTO leaderboard, StringMessages stringMessages) { + LeaderboardEntryPoint.getUrlConfigurationDialog(leaderboard, stringMessages).show(); + } + + private void setStartTime(RaceColumnDTO raceColumnDTO, FleetDTO fleetDTO) { + new SetStartTimeDialog(sailingService, errorReporter, getSelectedLeaderboardName(), raceColumnDTO.getName(), + fleetDTO.getName(), stringMessages, new DialogCallback() { + @Override + public void ok(RaceLogSetStartTimeAndProcedureDTO editedObject) { + sailingService.setStartTimeAndProcedure(editedObject, new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(caught.getMessage()); + } + + @Override + public void onSuccess(Boolean result) { + if (!result) { + Window.alert(stringMessages.failedToSetNewStartTime()); + } + } + }); + } + + @Override + public void cancel() { } + }).show(); + } + + private void showRaceLog(final RaceColumnDTO raceColumnDTO, final FleetDTO fleetDTO) { + final String selectedLeaderboardName = getSelectedLeaderboardName(); + sailingService.getRaceLog(selectedLeaderboardName, raceColumnDTO, fleetDTO, + new MarkedAsyncCallback( + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(caught.getMessage(), true); + } + @Override + public void onSuccess(RaceLogDTO result) { + openRaceLogDialog(result); + } + })); + } + + private void openRaceLogDialog(RaceLogDTO raceLogDTO) { + RaceLogDialog dialog = new RaceLogDialog(raceLogDTO, stringMessages, new DialogCallback() { + @Override + public void cancel() { + } + + @Override + public void ok(RaceLogDTO result) { + } + }); + dialog.show(); + } + + private void removeRaceColumn(final RaceColumnDTO raceColumnDTO) { + final String selectedLeaderboardName = getSelectedLeaderboardName(); + final String raceColumnString = raceColumnDTO.getRaceColumnName(); + sailingService.removeLeaderboardColumn(getSelectedLeaderboardName(), raceColumnString, + new MarkedAsyncCallback( + new AsyncCallback() { + @Override + public void onFailure(Throwable t) { + errorReporter.reportError("Error trying to remove leaderboard race column " + raceColumnDTO + + " in leaderboard " + getSelectedLeaderboardName() + ": " + t.getMessage()); + } + + @Override + public void onSuccess(Void arg0) { + loadAndRefreshLeaderboard(selectedLeaderboardName, /* raceColumnNameToSelect */ null); + } + })); + } + + /** + * The selected row is potentially only one of several fleet-based rows of the same RaceColumn. In this case, + * move all fleet-based rows of the same RaceColumn down. + */ + private void moveSelectedRaceColumnDown() { + final String selectedLeaderboardName = getSelectedLeaderboardName(); + final String selectedRaceColumnName = getSelectedRaceColumnWithFleet().getA().getRaceColumnName(); + sailingService.moveLeaderboardColumnDown(getSelectedLeaderboardName(), selectedRaceColumnName, + new MarkedAsyncCallback( + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Error trying to move leaderboard race column " + + selectedRaceColumnName + " in leaderboard " + getSelectedLeaderboardName() + + " down: " + caught.getMessage()); + } + + @Override + public void onSuccess(Void result) { + loadAndRefreshLeaderboard(selectedLeaderboardName, selectedRaceColumnName); + } + })); + } + + /** + * The selected row is potentially only one of several fleet-based rows of the same RaceColumn. In this case, + * move all fleet-based rows of the same RaceColumn down. + */ + private void moveSelectedRaceColumnUp() { + final String selectedLeaderboardName = getSelectedLeaderboardName(); + final String selectedRaceColumnName = getSelectedRaceColumnWithFleet().getA().getRaceColumnName(); + sailingService.moveLeaderboardColumnUp(getSelectedLeaderboardName(), selectedRaceColumnName, + new MarkedAsyncCallback( + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Error trying to move leaderboard race column " + + selectedRaceColumnName + " in leaderboard " + getSelectedLeaderboardName() + " up: " + + caught.getMessage()); + } + + @Override + public void onSuccess(Void result) { + loadAndRefreshLeaderboard(selectedLeaderboardName, selectedRaceColumnName); + } + })); + } + + @Override + protected void leaderboardRaceColumnSelectionChanged() { + selectedRaceInLeaderboard = getSelectedRaceColumnWithFleet(); + if (selectedRaceInLeaderboard != null) { + columnMoveUpButton.setEnabled(true); + columnMoveDownButton.setEnabled(true); + selectTrackedRaceInRaceList(); + } else { + columnMoveUpButton.setEnabled(false); + columnMoveDownButton.setEnabled(false); + trackedRacesListComposite.clearSelection(); + } + } + + private void editRaceColumnOfLeaderboard(final RaceColumnDTOAndFleetDTOWithNameBasedEquality raceColumnWithFleet) { + final String selectedLeaderboardName = getSelectedLeaderboardName(); + final boolean oldIsMedalRace = raceColumnWithFleet.getA().isMedalRace(); + final String oldRaceColumnName = raceColumnWithFleet.getA().getRaceColumnName(); + final Double oldExplicitFactor = raceColumnWithFleet.getA().getExplicitFactor(); + // use a set to avoid duplicates in the case of regatta leaderboards with multiple fleets per column + Set existingRacesWithoutThisRace = new HashSet(); + for (RaceColumnDTOAndFleetDTOWithNameBasedEquality pair : raceColumnTable.getDataProvider().getList()) { + existingRacesWithoutThisRace.add(pair.getA()); + } + existingRacesWithoutThisRace.remove(raceColumnWithFleet.getA()); + final RaceColumnInLeaderboardDialog raceDialog = new RaceColumnInLeaderboardDialog(existingRacesWithoutThisRace, + raceColumnWithFleet.getA(), getSelectedLeaderboard().type.isRegattaLeaderboard(), stringMessages, new DialogCallback() { + @Override + public void cancel() { + } + + @Override + public void ok(final RaceColumnDescriptor result) { + boolean rename = !oldRaceColumnName.equals(result.getName()); + boolean updateIsMedalRace = oldIsMedalRace != result.isMedalRace(); + boolean updateFactor = oldExplicitFactor != result.getExplicitFactor(); + List> callbacks = new ArrayList>(); + final ParallelExecutionCallback renameLeaderboardColumnCallback = new ParallelExecutionCallback(); + if (rename) { + callbacks.add(renameLeaderboardColumnCallback); + } + final ParallelExecutionCallback updateIsMedalRaceCallback = new ParallelExecutionCallback(); + if (updateIsMedalRace) { + callbacks.add(updateIsMedalRaceCallback); + } + final ParallelExecutionCallback updateLeaderboardColumnFactorCallback = new ParallelExecutionCallback(); + if (updateFactor) { + callbacks.add(updateLeaderboardColumnFactorCallback); + } + new ParallelExecutionHolder(callbacks.toArray(new ParallelExecutionCallback[0])) { + @Override + public void handleSuccess() { + loadAndRefreshLeaderboard(selectedLeaderboardName, result.getName()); + } + @Override + public void handleFailure(Throwable t) { + errorReporter.reportError("Error trying to update data of race column " + + oldRaceColumnName + " in leaderboard " + selectedLeaderboardName + ": " + + t.getMessage()); + } + }; + if (rename) { + sailingService.renameLeaderboardColumn(selectedLeaderboardName, oldRaceColumnName, + result.getName(), renameLeaderboardColumnCallback); + } + if (updateIsMedalRace) { + sailingService.updateIsMedalRace(selectedLeaderboardName, result.getName(), + result.isMedalRace(), updateIsMedalRaceCallback); + } + if (updateFactor) { + sailingService.updateLeaderboardColumnFactor(selectedLeaderboardName, result.getName(), + result.getExplicitFactor(), updateLeaderboardColumnFactorCallback); + } + } + }); + raceDialog.show(); + } + + private void setIsMedalRace(String leaderboardName, final RaceColumnDTO raceInLeaderboard, + final boolean isMedalRace) { + sailingService.updateIsMedalRace(leaderboardName, raceInLeaderboard.getRaceColumnName(), isMedalRace, + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorUpdatingIsMedalRace(caught.getMessage())); + } + + @Override + public void onSuccess(Void result) { + getSelectedLeaderboard().setIsMedalRace(raceInLeaderboard.getRaceColumnName(), isMedalRace); + } + }); + } + + private void addRaceColumnsToLeaderboard() { + final String leaderboardName = getSelectedLeaderboardName(); + final List existingRaceColumns = new ArrayList(); + for (RaceColumnDTOAndFleetDTOWithNameBasedEquality pair : raceColumnTable.getDataProvider().getList()) { + existingRaceColumns.add(pair.getA()); + } + final RaceColumnsInLeaderboardDialog raceDialog = new RaceColumnsInLeaderboardDialog(existingRaceColumns, + stringMessages, new DialogCallback>() { + @Override + public void cancel() { + } + + @Override + public void ok(final List result) { + updateRaceColumnsOfLeaderboard(leaderboardName, existingRaceColumns, result); + } + }); + raceDialog.ensureDebugId("RaceColumnsInLeaderboardDialog"); + raceDialog.show(); + } + + private void updateRaceColumnsOfLeaderboard(final String leaderboardName, List existingRaceColumns, List newRaceColumns) { + final List> raceColumnsToAdd = new ArrayList>(); + + for (RaceColumnDTO newRaceColumn : newRaceColumns) { + if (!existingRaceColumns.contains(newRaceColumn)) { + raceColumnsToAdd.add(new Pair(newRaceColumn.getName(), newRaceColumn.isMedalRace())); + } + } + + sailingService.addColumnsToLeaderboard(leaderboardName, raceColumnsToAdd, new MarkedAsyncCallback( + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Error trying to add race columns to leaderboard " + leaderboardName + + ": " + caught.getMessage()); + } + + @Override + public void onSuccess(Void v) { + loadAndRefreshLeaderboard(leaderboardName, /* nameOfRaceColumnToSelect */ null); + } + })); + } + + @Override + protected void leaderboardSelectionChanged() { + // make sure that clearing the selection doesn't cause an unlinking of the selected tracked race + raceSelectionProvider.removeRaceSelectionChangeListener(this); + trackedRacesListComposite.clearSelection(); + // add listener again using a scheduled command which is executed when the browser's event loop re-gains + // control; we assume that at that point in time the selection updates have already been performed + Scheduler.get().scheduleFinally(new ScheduledCommand() { + @Override + public void execute() { + raceSelectionProvider.addRaceSelectionChangeListener(LeaderboardConfigPanel.this); + } + }); + leaderboardRemoveButton.setEnabled(!leaderboardSelectionModel.getSelectedSet().isEmpty()); + StrippedLeaderboardDTO selectedLeaderboard = getSelectedLeaderboard(); + if (leaderboardSelectionModel.getSelectedSet().size() == 1 && selectedLeaderboard != null) { + raceColumnTable.getDataProvider().getList().clear(); + for (RaceColumnDTO raceColumn : selectedLeaderboard.getRaceList()) { + for (FleetDTO fleet : raceColumn.getFleets()) { + raceColumnTable.getDataProvider().getList().add(new RaceColumnDTOAndFleetDTOWithNameBasedEquality(raceColumn, fleet)); + } + } + selectedLeaderBoardPanel.setVisible(true); + selectedLeaderBoardPanel.setCaptionText("Details of leaderboard '" + selectedLeaderboard.name + "'"); + if (!selectedLeaderboard.type.isMetaLeaderboard()) { + trackedRacesCaptionPanel.setVisible(true); + } + addRaceColumnsButton.setVisible(!selectedLeaderboard.type.isRegattaLeaderboard()); + columnMoveUpButton.setVisible(!selectedLeaderboard.type.isRegattaLeaderboard()); + columnMoveDownButton.setVisible(!selectedLeaderboard.type.isRegattaLeaderboard()); + } else { + selectedLeaderBoardPanel.setVisible(false); + trackedRacesCaptionPanel.setVisible(false); + selectedRaceInLeaderboard = null; + } + } + + private void createFlexibleLeaderboard() { + sailingService.getEvents(new MarkedAsyncCallback>( + new AsyncCallback>() { + @Override + public void onSuccess(List result) { + createFlexibleLeaderboard(result); + } + + @Override + public void onFailure(Throwable caught) { + createFlexibleLeaderboard(new ArrayList()); + } + })); + } + + private void createFlexibleLeaderboard(List existingEvents) { + + AbstractLeaderboardDialog dialog = new FlexibleLeaderboardCreateDialog(Collections.unmodifiableCollection(availableLeaderboardList), + stringMessages, Collections.unmodifiableCollection(existingEvents), errorReporter, new DialogCallback() { + @Override + public void cancel() { + } + + @Override + public void ok(final LeaderboardDescriptor newLeaderboard) { + sailingService.createFlexibleLeaderboard(newLeaderboard.getName(), newLeaderboard.getDisplayName(), + newLeaderboard.getDiscardThresholds(), newLeaderboard.getScoringScheme(), newLeaderboard.getCourseAreaId(), + new MarkedAsyncCallback( + new AsyncCallback() { + @Override + public void onFailure(Throwable t) { + errorReporter.reportError("Error trying to create the new flexible leaderboard " + newLeaderboard.getName() + + ": " + t.getMessage()); + } + + @Override + public void onSuccess(StrippedLeaderboardDTO result) { + addLeaderboard(result); + } + })); + } + }); + dialog.ensureDebugId("FlexibleLeaderboardCreateDialog"); + dialog.show(); + } + + private void createRegattaLeaderboard() { + RegattaLeaderboardCreateDialog dialog = new RegattaLeaderboardCreateDialog(Collections.unmodifiableCollection(availableLeaderboardList), + Collections.unmodifiableCollection(allRegattas), stringMessages, errorReporter, new DialogCallback() { + @Override + public void cancel() { + } + + @Override + public void ok(final LeaderboardDescriptor newLeaderboard) { + RegattaIdentifier regattaIdentifier = new RegattaName(newLeaderboard.getRegattaName()); + sailingService.createRegattaLeaderboard(regattaIdentifier, newLeaderboard.getDisplayName(), newLeaderboard.getDiscardThresholds(), + new AsyncCallback() { + @Override + public void onFailure(Throwable t) { + errorReporter.reportError("Error trying to create the new regatta leaderboard " + newLeaderboard.getName() + + ": " + t.getMessage()); + } + + @Override + public void onSuccess(StrippedLeaderboardDTO result) { + addLeaderboard(result); + } + }); + } + }); + dialog.ensureDebugId("RegattaLeaderboardCreateDialog"); + dialog.show(); + } + + private void addLeaderboard(StrippedLeaderboardDTO result) { + leaderboardList.getList().add(result); + availableLeaderboardList.add(result); + leaderboardSelectionModel.clear(); + leaderboardSelectionModel.setSelected(result, true); + } + + private void updateLeaderboard(final String oldLeaderboardName, final LeaderboardDescriptor leaderboardToUpdate) { + sailingService.updateLeaderboard(oldLeaderboardName, leaderboardToUpdate.getName(), leaderboardToUpdate.getDisplayName(), + leaderboardToUpdate.getDiscardThresholds(), leaderboardToUpdate.getCourseAreaId(), new AsyncCallback() { + @Override + public void onFailure(Throwable t) { + errorReporter.reportError("Error trying to update leaderboard " + oldLeaderboardName + ": " + + t.getMessage()); + } + + @Override + public void onSuccess(StrippedLeaderboardDTO updatedLeaderboard) { + int indexOfLeaderboard = 0; + for (int i = 0; i < leaderboardList.getList().size(); i++) { + StrippedLeaderboardDTO dao = leaderboardList.getList().get(i); + if (dao.name.equals(oldLeaderboardName)) { + indexOfLeaderboard = i; + break; + } + } + leaderboardList.getList().set(indexOfLeaderboard, updatedLeaderboard); + leaderboardList.refresh(); + } + }); + } + + private void removeLeaderboards(final Collection leaderboards) { + if (!leaderboards.isEmpty()) { + Set leaderboardNames = new HashSet(); + for (StrippedLeaderboardDTO leaderboard : leaderboards) { + leaderboardNames.add(leaderboard.name); + } + sailingService.removeLeaderboards(leaderboardNames, new AsyncCallback() { + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Error trying to remove the leaderboards:" + caught.getMessage()); + + + } + + @Override + public void onSuccess(Void result) { + for (StrippedLeaderboardDTO leaderboard : leaderboards) { + removeLeaderboardFromTable(leaderboard); + } + } + }); + } + } + + private void removeLeaderboard(final StrippedLeaderboardDTO leaderBoard) { + sailingService.removeLeaderboard(leaderBoard.name, new MarkedAsyncCallback( + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Error trying to remove leaderboard " + leaderBoard.name + ": " + + caught.getMessage()); + } + + @Override + public void onSuccess(Void result) { + removeLeaderboardFromTable(leaderBoard); + } + })); + } + + private void removeLeaderboardFromTable(final StrippedLeaderboardDTO leaderBoard) { + leaderboardList.getList().remove(leaderBoard); + availableLeaderboardList.remove(leaderBoard); + leaderboardSelectionModel.setSelected(leaderBoard, false); + } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkEditDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkEditDialog.java new file mode 100644 index 00000000000..ca7d454bbb1 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkEditDialog.java @@ -0,0 +1,113 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.ListBox; +import com.google.gwt.user.client.ui.TextBox; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.MarkType; +import com.sap.sailing.domain.common.impl.RGBColor; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.MarkDTO; +import com.sap.sailing.gwt.ui.shared.racemap.Pattern; +import com.sap.sailing.gwt.ui.shared.racemap.Shape; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; + +public class MarkEditDialog extends DataEntryDialog { + private final MarkDTO markToEdit; //color, shape, pattern, type + private final TextBox name; + private final ListBox shape; + private final ListBox pattern; + private final ListBox type; + private final TextBox color; + private final StringMessages stringMessages; + + public MarkEditDialog(final StringMessages stringMessages, MarkDTO markToEdit, boolean isNewMark, + DialogCallback callback) { + super(isNewMark ? stringMessages.add(stringMessages.mark()) : stringMessages.edit(stringMessages.mark()), + isNewMark ? stringMessages.add(stringMessages.mark()) : stringMessages.edit(stringMessages.mark()), + stringMessages.ok(), stringMessages.cancel(), new DataEntryDialog.Validator() { + @Override + public String getErrorMessage(MarkDTO valueToValidate) { + String result = null; + if (valueToValidate.getName() == null || valueToValidate.getName().isEmpty()) { + result = stringMessages.pleaseEnterAName(); + } else if (valueToValidate.color != null && ! valueToValidate.color.isEmpty()) { + try { + new RGBColor(valueToValidate.color); + } catch (IllegalArgumentException iae) { + result = valueToValidate.color; + } + } + return result; + } + }, /* animationEnabled */true, callback); + this.stringMessages = stringMessages; + this.markToEdit = markToEdit; + + this.name = createTextBox(markToEdit.getName()); + this.shape = createAndSelectListBox(Shape.values(), markToEdit.shape, true); + this.type = createAndSelectListBox(MarkType.values(), markToEdit.type == null ? null : markToEdit.type.name(), false); + this.color = createTextBox(markToEdit.color); + this.pattern = createAndSelectListBox(Pattern.values(), markToEdit.pattern, true); + } + + private > ListBox createAndSelectListBox(T[] values, String toSelect, boolean emptySelectionPossible) { + ListBox result = createListBox(/* isMultipleSelect */ false); + int i=0; + List list = new ArrayList(); + for (T t : values) { + list.add(t.name()); + } + if (emptySelectionPossible) { + list.add(""); + } + Collections.sort(list); + for (String name : list) { + result.addItem(name); + if (name.equals(toSelect)) { + result.setSelectedIndex(i); + } + i++; + } + return result; + } + + + @Override + public void show() { + super.show(); + name.setFocus(true); + } + + @Override + protected MarkDTO getResult() { + MarkDTO result = new MarkDTO(markToEdit.getIdAsString(), name.getText()); + result.shape = shape.getItemText(shape.getSelectedIndex()); + result.type = MarkType.valueOf(type.getItemText(type.getSelectedIndex())); + result.color = color.getText(); + result.pattern = pattern.getItemText(pattern.getSelectedIndex()); + return result; + } + + @Override + protected Widget getAdditionalWidget() { + Grid result = new Grid(5, 2); + result.setWidget(0, 0, new Label(stringMessages.name())); + result.setWidget(0, 1, name); + result.setWidget(1, 0, new Label(stringMessages.shape())); + result.setWidget(1, 1, shape); + result.setWidget(2, 0, new Label(stringMessages.type())); + result.setWidget(2, 1, type); + result.setWidget(3, 0, new Label(stringMessages.color())); + result.setWidget(3, 1, color); + result.setWidget(4, 0, new Label(stringMessages.pattern())); + result.setWidget(4, 1, pattern); + return result; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java new file mode 100644 index 00000000000..badf06600c3 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java @@ -0,0 +1,110 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; + +import com.google.gwt.cell.client.SafeHtmlCell; +import com.google.gwt.i18n.client.NumberFormat; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; +import com.google.gwt.user.cellview.client.Column; +import com.google.gwt.user.cellview.client.TextColumn; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.view.client.SelectionModel; +import com.sap.sailing.domain.common.Color; +import com.sap.sailing.domain.common.impl.RGBColor; +import com.sap.sailing.gwt.ui.adminconsole.ColorColumn.ColorRetriever; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.MarkDTO; + +public class MarkTableWrapper> extends TableWrapper { + public MarkTableWrapper(S selectionModel, SailingServiceAsync sailingService, StringMessages stringMessages, + ErrorReporter errorReporter) { + super(sailingService, stringMessages, errorReporter, selectionModel); + + mainPanel.add(table); + + TextColumn markNameColumn = new TextColumn() { + @Override + public String getValue(MarkDTO markDTO) { + return markDTO.getName(); + } + }; + table.addColumn(markNameColumn, stringMessages.mark()); + + final SafeHtmlCell markPositionCell = new SafeHtmlCell(); + Column markPositionColumn = new Column(markPositionCell) { + @Override + public SafeHtml getValue(MarkDTO mark) { + SafeHtmlBuilder builder = new SafeHtmlBuilder(); + if(mark.position != null) { + NumberFormat fmt = NumberFormat.getFormat("#.###"); + builder.appendEscaped(fmt.format(mark.position.latDeg)+", "+fmt.format(mark.position.lngDeg)); + } + return builder.toSafeHtml(); + } + }; + table.addColumn(markPositionColumn, stringMessages.position()); + + Column markColorColumn = new ColorColumn<>(new ColorRetriever() { + @Override + public Color getColor(MarkDTO t) { + return t.color != null && ! t.color.isEmpty() ? new RGBColor(t.color) : null; + } + }); + table.addColumn(markColorColumn, stringMessages.color()); + + TextColumn markShapeColumn = new TextColumn() { + @Override + public String getValue(MarkDTO markDTO) { + return markDTO.shape != null ? markDTO.shape : ""; + } + }; + table.addColumn(markShapeColumn, stringMessages.shape()); + + TextColumn markPatternColumn = new TextColumn() { + @Override + public String getValue(MarkDTO markDTO) { + return markDTO.pattern != null ? markDTO.pattern : ""; + } + }; + table.addColumn(markPatternColumn, stringMessages.pattern()); + + TextColumn markUUIDColumn = new TextColumn() { + @Override + public String getValue(MarkDTO markDTO) { + return markDTO.getIdAsString(); + } + }; + table.addColumn(markUUIDColumn, "UUID"); + } + + public void refresh(Collection marks) { + dataProvider.getList().clear(); + dataProvider.getList().addAll(marks); + dataProvider.flush(); + Collections.sort(dataProvider.getList(), new Comparator() { + @Override + public int compare(MarkDTO o1, MarkDTO o2) { + return o1.getName().compareTo(o2.getName()); + } + }); + } + + public void refresh(String leaderboardName, String raceColumnName, String fleetName) { + sailingService.getMarksInRaceLog(leaderboardName, raceColumnName, fleetName, new AsyncCallback>() { + @Override + public void onSuccess(Collection result) { + refresh(result); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could not load marks: " + caught.getMessage()); + } + }); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java index 7dddbeb9e95..5de53e1016d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java @@ -24,11 +24,11 @@ import com.google.gwt.view.client.SelectionModel; import com.google.gwt.view.client.SingleSelectionModel; import com.sap.sailing.domain.common.media.MediaTrack; import com.sap.sailing.domain.common.media.MediaUtil; -import com.sap.sailing.gwt.ui.client.media.NewMediaDialog; -import com.sap.sailing.gwt.ui.client.media.TimeFormatUtil; import com.sap.sailing.gwt.ui.client.ErrorReporter; import com.sap.sailing.gwt.ui.client.MediaServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.media.NewMediaDialog; +import com.sap.sailing.gwt.ui.client.media.TimeFormatUtil; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; /** diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/PositionEntryDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/PositionEntryDialog.java new file mode 100644 index 00000000000..79401bc92bf --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/PositionEntryDialog.java @@ -0,0 +1,64 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import com.google.gwt.user.client.ui.DoubleBox; +import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.dto.PositionDTO; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; + +public class PositionEntryDialog extends DataEntryDialog { + private final DoubleBox lat; + private final DoubleBox lng; + private final StringMessages stringMessages; + + private static final double ERROR_VAL = Double.MIN_VALUE; + + public PositionEntryDialog(String title, final StringMessages stringMessages, DialogCallback callback) { + super(title, title, stringMessages.save(), stringMessages.cancel(), + new DataEntryDialog.Validator() { + @Override + public String getErrorMessage(PositionDTO valueToValidate) { + if (valueToValidate.latDeg == ERROR_VAL) { + return stringMessages.pleaseEnterA(stringMessages.latitude()); + } + if (valueToValidate.latDeg > 90 || valueToValidate.latDeg < -90) { + return stringMessages.pleaseEnterAValidValueFor(stringMessages.latitude(), "-90.0 - 90.0"); + } + if (valueToValidate.lngDeg == ERROR_VAL) { + return stringMessages.pleaseEnterA(stringMessages.longitude()); + } + if (valueToValidate.lngDeg > 180 || valueToValidate.lngDeg < -180) { + return stringMessages.pleaseEnterAValidValueFor(stringMessages.latitude(), "-180.0 - 180.0"); + } + return null; + } + }, true, callback); + + this.stringMessages = stringMessages; + + lat = createDoubleBox(10); + lng = createDoubleBox(10); + } + + @Override + protected PositionDTO getResult() { + Double latDeg = lat.getValue(); + Double lngDeg = lng.getValue(); + return new PositionDTO(latDeg == null ? ERROR_VAL : latDeg, lngDeg == null ? ERROR_VAL : lngDeg); + + } + + @Override + protected Widget getAdditionalWidget() { + Grid grid = new Grid(2, 2); + grid.setWidget(0, 0, new Label(stringMessages.latitude() + " (" + stringMessages.degreesShort() + ")")); + grid.setWidget(0, 1, lat); + grid.setWidget(1, 0, new Label(stringMessages.longitude() + " (" + stringMessages.degreesShort() + ")")); + grid.setWidget(1, 1, lng); + + return grid; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceCourseManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceCourseManagementPanel.java index fc47167b585..01e1e53c308 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceCourseManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceCourseManagementPanel.java @@ -1,583 +1,76 @@ -package com.sap.sailing.gwt.ui.adminconsole; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; - -import com.google.gwt.cell.client.SafeHtmlCell; -import com.google.gwt.core.client.GWT; -import com.google.gwt.dom.client.Style.FontWeight; -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.i18n.client.NumberFormat; -import com.google.gwt.safehtml.shared.SafeHtml; -import com.google.gwt.safehtml.shared.SafeHtmlBuilder; -import com.google.gwt.user.cellview.client.CellTable; -import com.google.gwt.user.cellview.client.Column; -import com.google.gwt.user.cellview.client.TextColumn; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.Grid; -import com.google.gwt.user.client.ui.HasVerticalAlignment; -import com.google.gwt.user.client.ui.HorizontalPanel; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.Widget; -import com.google.gwt.view.client.ListDataProvider; -import com.google.gwt.view.client.MultiSelectionModel; -import com.google.gwt.view.client.SelectionChangeEvent; -import com.google.gwt.view.client.SelectionChangeEvent.Handler; -import com.google.gwt.view.client.SelectionModel; -import com.google.gwt.view.client.SingleSelectionModel; -import com.sap.sailing.domain.common.PassingInstruction; -import com.sap.sailing.domain.common.impl.Util.Pair; -import com.sap.sailing.gwt.ui.client.ErrorReporter; -import com.sap.sailing.gwt.ui.client.RegattaRefresher; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.shared.ControlPointDTO; -import com.sap.sailing.gwt.ui.shared.GateDTO; -import com.sap.sailing.gwt.ui.shared.MarkDTO; -import com.sap.sailing.gwt.ui.shared.RaceCourseDTO; -import com.sap.sailing.gwt.ui.shared.WaypointDTO; -import com.sap.sse.gwt.client.dialog.DataEntryDialog; -import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; - -/** - * A panel that has a race selection (inherited from {@link AbstractRaceManagementPanel}) and which adds a table - * for a selected race showing the race's waypoints together with the number of mark passings already received for that - * waypoint. Also, the control can be used to send course updates into the tracked race, mostly to simulate these types - * of events. Conceivably, this may in the future also become a way to set up and edit courses for a tracked race. - * - * @author Frank Mittag (C5163874), Axel Uhl (D043530) - */ -public class RaceCourseManagementPanel extends AbstractRaceManagementPanel { - /** - * Represents one mark assignment for a control point. Gates have multiple such records, one for each of their marks. - * - * @author Axel Uhl (D043530) - */ - private static class ControlPointAndOldAndNewMark { - private final ControlPointDTO controlPoint; - private final MarkDTO oldMark; - private MarkDTO newMark; - private PassingInstruction passingInstructions; - public ControlPointAndOldAndNewMark(ControlPointDTO controlPoint, PassingInstruction passingInstructions, MarkDTO oldMark) { - super(); - this.controlPoint = controlPoint; - this.passingInstructions = passingInstructions; - this.oldMark = oldMark; - this.newMark = oldMark; - } - public PassingInstruction getPassingInstructions() { - return passingInstructions; - } - public MarkDTO getNewMark() { - return newMark; - } - public void setNewMark(MarkDTO newMark) { - this.newMark = newMark; - } - public ControlPointDTO getControlPoint() { - return controlPoint; - } - public MarkDTO getOldMark() { - return oldMark; - } - } - - private class ControlPointCreationDialog extends DataEntryDialog { - private final CellTable marksTable; - private final MultiSelectionModel selectionModel; - - public ControlPointCreationDialog(final StringMessages stringMessages, AdminConsoleTableResources tableRes, - List marks, DialogCallback callback) { - super(stringMessages.controlPoint(), stringMessages.selectOneMarkOrTwoMarksForGate(), - stringMessages.ok(), stringMessages.cancel(), new Validator() { - @Override - public String getErrorMessage(ControlPointDTO valueToValidate) { - if (valueToValidate == null) { - return stringMessages.selectOneMarkOrTwoMarksForGate(); - } else { - return null; - } - } - - }, /* animationEnabled */ false, callback); - selectionModel = new MultiSelectionModel(); - selectionModel.addSelectionChangeHandler(new Handler() { - @Override - public void onSelectionChange(SelectionChangeEvent event) { - validate(); - } - }); - marksTable = createAvailableMarksTable(stringMessages, tableRes, selectionModel); - ListDataProvider markDataProvider = new ListDataProvider(); - markDataProvider.getList().addAll(marks); - markDataProvider.addDataDisplay(marksTable); - } - - @Override - protected ControlPointDTO getResult() { - ControlPointDTO result = null; - Set selection = selectionModel.getSelectedSet(); - if (selection.size() == 1) { - result = selectionModel.getSelectedSet().iterator().next(); - } else if (selection.size() == 2) { - Iterator i = selectionModel.getSelectedSet().iterator(); - MarkDTO first = i.next(); - MarkDTO second = i.next(); - MarkDTO left; - MarkDTO right; - String gateName; - if (first.getName().matches("^.*"+REGEX_FOR_LEFT+".*$")) { - left = first; - right = second; - } else { - left = second; - right = first; - } - gateName = left.getName().replaceFirst(REGEX_FOR_LEFT, ""); - result = new GateDTO(/* generate UUID on the server */ null, gateName, left, right); - } - return result; - } - - @Override - protected Widget getAdditionalWidget() { - return marksTable; - } - } - - private static final String REGEX_FOR_LEFT = "( \\()?(([lL][eE][fF][tT])|(1))\\)?"; - - /** - * A table that lists the marks for which events have been received for the race selected. Note that this list may - * be longer than the list of marks actually used by the control points backing the course's waypoints because of - * the possibility of spare marks. - */ - private final CellTable marksTable; - private final ListDataProvider markDataProvider; - private final SingleSelectionModel markSelectionModel; - - /** - * A table that lists the product of Waypoint x ControlPoint x Mark plus a hint as to the number of mark passings. - * The (multi-)selection on this table can be used as either a selection of waypoints or a selection of control points - * or a selection of marks. - */ - private final CellTable controlPointsTable; - private final MultiSelectionModel controlPointsSelectionModel; - private final ListDataProvider controlPointDataProvider; - - /** - * When for a control point's mark a replacement mark is defined (see {@link #updateNewMark(Set, MarkDTO)}), - * the control point needs to be replaced before {@link #saveCourse(SailingServiceAsync, StringMessages) saving}. - * Those control points are added to this set. When the mark is reset to the original mark for all the control - * point's marks, the control point is removed from this set again. {@link #saveCourse(SailingServiceAsync, StringMessages)} - * then is responsible for creating replacement {@link ControlPointDTO}s before sending the new control point list to the - * server. - */ - private final Set controlPointsNeedingReplacement; - - private final HorizontalPanel courseActionsPanel; - - private final Handler markSelectionChangeHandler; - private final Button insertWaypointBefore; - private final Button insertWaypointAfter; - private final Button removeWaypointBtn; - private boolean ignoreWaypointAndOldAndNewMarkSelectionChange; - - public RaceCourseManagementPanel(final SailingServiceAsync sailingService, ErrorReporter errorReporter, - RegattaRefresher regattaRefresher, final StringMessages stringMessages) { - super(sailingService, errorReporter, regattaRefresher, stringMessages); - controlPointsNeedingReplacement = new HashSet(); - Grid grid = new Grid(2, 2); - grid.setCellPadding(5); - selectedRaceContentPanel.add(grid); - - Label currentRaceCourseLabel = new Label("Current race course"); - currentRaceCourseLabel.getElement().getStyle().setFontWeight(FontWeight.BOLD); - grid.setWidget(0, 0, currentRaceCourseLabel); - Label availableMarksLabel = new Label("Available marks"); - availableMarksLabel.getElement().getStyle().setFontWeight(FontWeight.BOLD); - grid.setWidget(0, 1, availableMarksLabel); - grid.getRowFormatter().setVerticalAlign(1, HasVerticalAlignment.ALIGN_TOP); - - final AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); - controlPointsTable = new CellTable(/* pageSize */10000, tableRes); - grid.setWidget(1, 0, controlPointsTable); - controlPointsSelectionModel = new MultiSelectionModel(); - controlPointsTable.setSelectionModel(controlPointsSelectionModel); - controlPointsSelectionModel.addSelectionChangeHandler(new Handler() { - @Override - public void onSelectionChange(SelectionChangeEvent event) { - handleControlPointSelectionChange(); - } - }); - TextColumn nameColumn = new TextColumn() { - @Override - public String getValue(ControlPointAndOldAndNewMark cpaoanm) { - return cpaoanm.getControlPoint().getName(); - } - }; - controlPointsTable.addColumn(nameColumn, stringMessages.controlPoint()); - TextColumn passingInstructionsColumn = new TextColumn() { - @Override - public String getValue(ControlPointAndOldAndNewMark cpaoanm) { - String result = ""; - if(cpaoanm.getPassingInstructions() != null) { - result = cpaoanm.getPassingInstructions().name(); - } - return result; - } - }; - controlPointsTable.addColumn(passingInstructionsColumn, "Passing instructions"); - TextColumn oldMarkColumn = new TextColumn() { - @Override - public String getValue(ControlPointAndOldAndNewMark cpaoanm) { - return "" + cpaoanm.getOldMark().getName(); - } - }; - controlPointsTable.addColumn(oldMarkColumn, stringMessages.mark()); - TextColumn newMarkColumn = new TextColumn(){ - - @Override - public String getValue(ControlPointAndOldAndNewMark cpaoanm) { - return "" + cpaoanm.getNewMark().getName(); - } - - }; - controlPointsTable.addColumn(newMarkColumn, stringMessages.newMark()); - controlPointDataProvider = new ListDataProvider(); - controlPointDataProvider.addDataDisplay(controlPointsTable); - - // race course marks table - markDataProvider = new ListDataProvider(); - markSelectionChangeHandler = new Handler() { - @Override - public void onSelectionChange(SelectionChangeEvent event) { - if (!ignoreWaypointAndOldAndNewMarkSelectionChange) { - updateNewMark(controlPointsSelectionModel.getSelectedSet(), markSelectionModel.getSelectedObject()); - } - } - }; - markSelectionModel = new SingleSelectionModel(); - marksTable = createAvailableMarksTable(stringMessages, tableRes, markSelectionModel); - marksTable.getSelectionModel().addSelectionChangeHandler(markSelectionChangeHandler); - markDataProvider.addDataDisplay(marksTable); - grid.setWidget(1, 1, marksTable); - - courseActionsPanel = new HorizontalPanel(); - courseActionsPanel.setSpacing(10); - insertWaypointBefore = new Button(stringMessages.insertWaypointBeforeSelected()); - insertWaypointBefore.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - insertWaypoint(sailingService, stringMessages, tableRes, /* before */ true); - } - - }); - insertWaypointBefore.setEnabled(false); - courseActionsPanel.add(insertWaypointBefore); - insertWaypointAfter = new Button(stringMessages.insertWaypointAfterSelected()); - insertWaypointAfter.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - insertWaypoint(sailingService, stringMessages, tableRes, /* before */ false); - } - - }); - insertWaypointAfter.setEnabled(false); - courseActionsPanel.add(insertWaypointAfter); - removeWaypointBtn = new Button(stringMessages.remove()); - removeWaypointBtn.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - removeSelectedWaypoints(sailingService); - } - }); - removeWaypointBtn.setEnabled(false); - courseActionsPanel.add(removeWaypointBtn); - Button refreshBtn = new Button(stringMessages.refresh()); - refreshBtn.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - refreshSelectedRaceData (); - } - }); - courseActionsPanel.add(refreshBtn); - Button saveBtn = new Button(stringMessages.save()); - saveBtn.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - saveCourse(sailingService, stringMessages); - } - }); courseActionsPanel.add(saveBtn); - courseActionsPanel.setVisible(false); - this.selectedRaceContentPanel.add(courseActionsPanel); - } - - private void handleControlPointSelectionChange() { - ignoreWaypointAndOldAndNewMarkSelectionChange = true; - try { - markSelectionModel.setSelected(markSelectionModel.getSelectedObject(), false); - final int selectionSize = controlPointsSelectionModel.getSelectedSet().size(); - insertWaypointAfter.setEnabled(selectionSize==1); - insertWaypointBefore.setEnabled(selectionSize==1); - removeWaypointBtn.setEnabled(selectionSize>=1); - if (selectionSize == 1) { - MarkDTO newMark = controlPointsSelectionModel.getSelectedSet().iterator().next().getNewMark(); - if (newMark != null) { - for (MarkDTO markDTO : markDataProvider.getList()) { - if (markDTO.getName().equals(newMark.getName())) { - markSelectionModel.setSelected(markDTO, true); - } - } - } - } - } finally { - ignoreWaypointAndOldAndNewMarkSelectionChange = false; - } - } - - private void saveCourse(SailingServiceAsync sailingService, final StringMessages stringMessages) { - Set oldControlPointsFromTableAlreadyHandled = new HashSet(); - List> controlPoints = new ArrayList>(); - for (ControlPointAndOldAndNewMark cpaoanb : controlPointDataProvider.getList()) { - if (!oldControlPointsFromTableAlreadyHandled.contains(cpaoanb.getControlPoint())) { - oldControlPointsFromTableAlreadyHandled.add(cpaoanb.getControlPoint()); - ControlPointDTO controlPointToAdd; - if (controlPointsNeedingReplacement.contains(cpaoanb.getControlPoint())) { - if (cpaoanb.getControlPoint() instanceof GateDTO) { - controlPointToAdd = createGate((GateDTO) cpaoanb.getControlPoint()); - } else { - controlPointToAdd = cpaoanb.getNewMark(); - } - } else { - controlPointToAdd = cpaoanb.getControlPoint(); - } - controlPoints.add(new Pair(controlPointToAdd, cpaoanb.passingInstructions)); - } - } - sailingService.updateRaceCourse(singleSelectedRace, controlPoints, new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorUpdatingRaceCourse(caught.getMessage())); - } - - @Override - public void onSuccess(Void result) { - Window.setStatus(stringMessages.successfullyUpdatedCourse()); - refreshSelectedRaceData(); - } - }); - } - - /** - * When a gate needs replacement, its entries in {@link #controlPointDataProvider} are looked up, and a new - * {@link GateDTO} is created having the same name as the old gate, but using the new marks as the gate's marks. - */ - private ControlPointDTO createGate(GateDTO oldGate) { - MarkDTO newLeft = null; - MarkDTO newRight = null; - for (ControlPointAndOldAndNewMark cpaoanb : controlPointDataProvider.getList()) { - if (cpaoanb.getControlPoint() == oldGate) { - MarkDTO newMark = cpaoanb.getNewMark(); - if (newRight != null || newMark.getName().matches("^.*"+REGEX_FOR_LEFT+".*$")) { - newLeft = newMark; - } else { - newRight = newMark; - } - } - } - assert newLeft != null && newRight != null; - // if old gate had null ID, the new gate will have a null ID too, causing the server to generate one - return new GateDTO(oldGate.getIdAsString(), oldGate.getName(), newLeft, newRight); - } - - private CellTable createAvailableMarksTable(final StringMessages stringMessages, AdminConsoleTableResources tableRes, - SelectionModel selectionModel) { - CellTable result = new CellTable(/* pageSize */10000, tableRes); - result.setSelectionModel(selectionModel); - TextColumn markNameColumn = new TextColumn() { - @Override - public String getValue(MarkDTO markDTO) { - return markDTO.getName(); - } - }; - result.addColumn(markNameColumn, stringMessages.mark()); - - final SafeHtmlCell markPositionCell = new SafeHtmlCell(); - Column markPositionColumn = new Column(markPositionCell) { - @Override - public SafeHtml getValue(MarkDTO mark) { - SafeHtmlBuilder builder = new SafeHtmlBuilder(); - if(mark.position != null) { - NumberFormat fmt = NumberFormat.getFormat("#.###"); - builder.appendEscaped(fmt.format(mark.position.latDeg)+", "+fmt.format(mark.position.lngDeg)); - } - return builder.toSafeHtml(); - } - }; - result.addColumn(markPositionColumn, stringMessages.position()); - - TextColumn markColorColumn = new TextColumn() { - @Override - public String getValue(MarkDTO markDTO) { - return markDTO.color != null ? markDTO.color : ""; - } - }; - result.addColumn(markColorColumn, stringMessages.color()); - - TextColumn markShapeColumn = new TextColumn() { - @Override - public String getValue(MarkDTO markDTO) { - return markDTO.shape != null ? markDTO.shape : ""; - } - }; - result.addColumn(markShapeColumn, stringMessages.shape()); - - TextColumn markPatternColumn = new TextColumn() { - @Override - public String getValue(MarkDTO markDTO) { - return markDTO.pattern != null ? markDTO.pattern : ""; - } - }; - result.addColumn(markPatternColumn, stringMessages.pattern()); - - TextColumn markUUIDColumn = new TextColumn() { - @Override - public String getValue(MarkDTO markDTO) { - return markDTO.getIdAsString(); - } - }; - result.addColumn(markUUIDColumn, "UUID"); - - return result; - } - - private void updateNewMark(Set selectedWaypointsAndOldAndNewMarks, MarkDTO selectedNewMark) { - if (selectedWaypointsAndOldAndNewMarks != null) { - for (ControlPointAndOldAndNewMark w : selectedWaypointsAndOldAndNewMarks) { - if (selectedNewMark == null) { - w.setNewMark(w.getOldMark()); - } else { - w.setNewMark(selectedNewMark); - } - if (w.getOldMark().getName().equals(w.getNewMark().getName())) { - checkIfAllMarksOfControlPointAreUnchangedAndIfSoRememberThis(w.getControlPoint()); - } else { - controlPointsNeedingReplacement.add(w.getControlPoint()); - } - final int indexOf = controlPointDataProvider.getList().indexOf(w); - if (indexOf != -1) { - controlPointDataProvider.getList().set(indexOf, w); - } - } - } - } - - private void checkIfAllMarksOfControlPointAreUnchangedAndIfSoRememberThis(ControlPointDTO controlPoint) { - boolean allMarksUnchanged = true; - for (ControlPointAndOldAndNewMark cpaoanb : controlPointDataProvider.getList()) { - if (cpaoanb.getControlPoint() == controlPoint) { - if (!cpaoanb.getOldMark().getName().equals(cpaoanb.getNewMark().getName())) { - allMarksUnchanged = false; - break; - } - } - } - if (allMarksUnchanged) { - controlPointsNeedingReplacement.remove(controlPoint); - } - } - - private void insertWaypoint(final SailingServiceAsync sailingService, StringMessages stringMessages, - AdminConsoleTableResources tableRes, final boolean beforeSelection) { - new ControlPointCreationDialog(stringMessages, tableRes, markDataProvider.getList(), new DialogCallback() { - @Override - public void cancel() { - // dialog cancelled, do nothing - } - - @Override - public void ok(ControlPointDTO result) { - Set selectedElements = controlPointsSelectionModel.getSelectedSet(); - if (!selectedElements.isEmpty()) { - ControlPointAndOldAndNewMark selectedElement = selectedElements.iterator().next(); - int insertPos = controlPointDataProvider.getList().indexOf(selectedElement) + (beforeSelection?0:1); - for (MarkDTO markDTO : result.getMarks()) { - controlPointDataProvider.getList().add(insertPos++, new ControlPointAndOldAndNewMark(result, /* passing instructions */ null, markDTO)); - } - } - } - }).show(); - } - - @Override - void refreshSelectedRaceData() { - if (singleSelectedRace != null && selectedRaceDTO != null) { - courseActionsPanel.setVisible(true); - // TODO bug 1351: never use System.currentTimeMillis() on the client when trying to compare anything with "server time"; this one is not so urgent as it is reached only in the AdminConsole and we expect administrators to have proper client-side time settings - sailingService.getRaceCourse(singleSelectedRace, new Date(), new AsyncCallback() { - @Override - public void onSuccess(RaceCourseDTO raceCourseDTO) { - updateCourseAndMarksInfo(raceCourseDTO); - } - - @Override - public void onFailure(Throwable caught) { - RaceCourseManagementPanel.this.errorReporter.reportError( - RaceCourseManagementPanel.this.stringMessages.errorTryingToObtainTheMarksOfTheRace( - caught.getMessage())); - } - }); - } else { - courseActionsPanel.setVisible(false); - } - } - - private void updateCourseAndMarksInfo(RaceCourseDTO raceCourseDTO) { - List waypointsAndOldAndNewMarks = new ArrayList(); - for (WaypointDTO waypointDTO : raceCourseDTO.waypoints) { - ControlPointDTO controlPointDTO = waypointDTO.controlPoint; - for (MarkDTO mark : controlPointDTO.getMarks()) { - ControlPointAndOldAndNewMark waypointAndOldAndNewMark = new ControlPointAndOldAndNewMark(controlPointDTO, waypointDTO.passingInstructions, mark); - waypointsAndOldAndNewMarks.add(waypointAndOldAndNewMark); - } - } - controlPointDataProvider.getList().clear(); - controlPointDataProvider.getList().addAll(waypointsAndOldAndNewMarks); - controlPointsNeedingReplacement.clear(); - markDataProvider.getList().clear(); - markDataProvider.getList().addAll(raceCourseDTO.getMarks()); - Collections.sort(markDataProvider.getList(), new Comparator() { - @Override - public int compare(MarkDTO o1, MarkDTO o2) { - return o1.getName().compareTo(o2.getName()); - } - }); - for (ControlPointAndOldAndNewMark w : controlPointsSelectionModel.getSelectedSet()) { - controlPointsSelectionModel.setSelected(w, false); - } - } - - private void removeSelectedWaypoints(final SailingServiceAsync sailingService) { - final Set selectedControlPoints = new HashSet(); - for (ControlPointAndOldAndNewMark cpaoanb : controlPointsSelectionModel.getSelectedSet()) { - selectedControlPoints.add(cpaoanb.getControlPoint()); - } - for (Iterator i=controlPointDataProvider.getList().iterator(); i.hasNext(); ) { - ControlPointAndOldAndNewMark next = i.next(); - if (selectedControlPoints.contains(next.getControlPoint())) { - i.remove(); - } - } - handleControlPointSelectionChange(); - } -} +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.Date; + +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.RegattaRefresher; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.RaceCourseDTO; + +/** + * A panel that has a race selection (inherited from {@link AbstractRaceManagementPanel}) and which adds a table + * for a selected race showing the race's waypoints together with the number of mark passings already received for that + * waypoint. Also, the control can be used to send course updates into the tracked race, mostly to simulate these types + * of events. Conceivably, this may in the future also become a way to set up and edit courses for a tracked race. + * + * @author Frank Mittag (C5163874), Axel Uhl (D043530) + */ +public class RaceCourseManagementPanel extends AbstractRaceManagementPanel { + private final CourseManagementWidget courseManagementWidget; + + public RaceCourseManagementPanel(final SailingServiceAsync sailingService, final ErrorReporter errorReporter, + RegattaRefresher regattaRefresher, final StringMessages stringMessages) { + super(sailingService, errorReporter, regattaRefresher, stringMessages); + + courseManagementWidget = new CourseManagementWidget(sailingService, errorReporter, stringMessages) { + @Override + protected void save() { + sailingService.updateRaceCourse(singleSelectedRace, createWaypointPairs(), new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorUpdatingRaceCourse(caught.getMessage())); + } + + @Override + public void onSuccess(Void result) { + Window.setStatus(stringMessages.successfullyUpdatedCourse()); + refreshSelectedRaceData(); + } + }); + } + + @Override + public void refresh() { + if (singleSelectedRace != null && selectedRaceDTO != null) { + mainPanel.setVisible(true); + // TODO bug 1351: never use System.currentTimeMillis() on the client when trying to compare anything with "server time"; this one is not so urgent as it is reached only in the AdminConsole and we expect administrators to have proper client-side time settings + sailingService.getRaceCourse(singleSelectedRace, new Date(), new AsyncCallback() { + @Override + public void onSuccess(RaceCourseDTO raceCourseDTO) { + updateWaypointsAndControlPoints(raceCourseDTO); + marks.refresh(raceCourseDTO.getMarks()); + } + + @Override + public void onFailure(Throwable caught) { + RaceCourseManagementPanel.this.errorReporter.reportError( + RaceCourseManagementPanel.this.stringMessages.errorTryingToObtainTheMarksOfTheRace( + caught.getMessage())); + } + }); + } else { + mainPanel.setVisible(false); + } + } + }; + this.selectedRaceContentPanel.add(courseManagementWidget); + } + + @Override + void refreshSelectedRaceData() { + courseManagementWidget.refresh(); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingCompetitorRegistrationsDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingCompetitorRegistrationsDialog.java new file mode 100644 index 00000000000..645b95d36f0 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingCompetitorRegistrationsDialog.java @@ -0,0 +1,178 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.gwt.core.client.Callback; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.CaptionPanel; +import com.google.gwt.user.client.ui.HasVerticalAlignment; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.domain.common.dto.CompetitorDTOImpl; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; + +public class RaceLogTrackingCompetitorRegistrationsDialog extends RaceLogTrackingDialog { + private CompetitorTableWrapper allCompetitorsTable; + private CompetitorTableWrapper registeredCompetitorsTable; + private final boolean filterByLeaderBoardInitially = false; + private final Callback competitorsRegistered; + + public RaceLogTrackingCompetitorRegistrationsDialog(final SailingServiceAsync sailingService, final StringMessages stringMessages, + final ErrorReporter errorReporter, final String leaderboardName, final String raceColumnName, final String fleetName, boolean editable, + Callback competitorsRegistered) { + super(sailingService, stringMessages, errorReporter, leaderboardName, raceColumnName, fleetName, editable); + this.competitorsRegistered = competitorsRegistered; + + refreshCompetitors(); + } + + @Override + protected void addButtons(Panel buttonPanel) { + Button addCompetitorButton = new Button(stringMessages.add(stringMessages.competitor())); + addCompetitorButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + openAddCompetitorDialog(); + } + }); + buttonPanel.add(addCompetitorButton); + + super.addButtons(buttonPanel); + } + + private void move(CompetitorTableWrapper from, CompetitorTableWrapper to, Collection toMove) { + if (toMove.isEmpty()) { + return; + } + List newFromList = new ArrayList<>(from.getFilterField().getAll()); + newFromList.removeAll(toMove); + from.getFilterField().updateAll(newFromList); + List newToList = new ArrayList<>(to.getFilterField().getAll()); + newToList.addAll(toMove); + to.getFilterField().updateAll(newToList); + } + + private void moveSelected(CompetitorTableWrapper from, CompetitorTableWrapper to) { + move(from, to, from.getSelectionModel().getSelectedSet()); + } + + @Override + protected void addMainContent(Panel mainPanel) { + super.addMainContent(mainPanel); + + HorizontalPanel panel = new HorizontalPanel(); + mainPanel.add(panel); + CaptionPanel allCompetitorsPanel = new CaptionPanel(stringMessages.competitorPool()); + CaptionPanel registeredCompetitorsPanel = new CaptionPanel(stringMessages.registeredCompetitors()); + allCompetitorsTable = new CompetitorTableWrapper(sailingService, stringMessages, errorReporter); + registeredCompetitorsTable = new CompetitorTableWrapper(sailingService, stringMessages, errorReporter); + allCompetitorsPanel.add(allCompetitorsTable); + registeredCompetitorsPanel.add(registeredCompetitorsTable); + VerticalPanel movePanel = new VerticalPanel(); + Button registerBtn = new Button("<"); + Button unregisterBtn = new Button(">"); + registerBtn.setEnabled(editable); + unregisterBtn.setEnabled(editable); + movePanel.add(registerBtn); + movePanel.add(unregisterBtn); + registerBtn.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + moveSelected(allCompetitorsTable, registeredCompetitorsTable); + } + }); + unregisterBtn.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + moveSelected(registeredCompetitorsTable, allCompetitorsTable); + } + }); + panel.add(registeredCompetitorsPanel); + panel.add(movePanel); + panel.setCellVerticalAlignment(movePanel, HasVerticalAlignment.ALIGN_MIDDLE); + panel.add(allCompetitorsPanel); + } + + @Override + protected void save() { + final Set registeredCompetitors = new HashSet<>( + registeredCompetitorsTable.getAllCompetitors()); + sailingService.setCompetitorRegistrations(leaderboardName, raceColumnName, fleetName, + registeredCompetitors, new AsyncCallback() { + @Override + public void onSuccess(Void result) { + hide(); + competitorsRegistered.onSuccess(! registeredCompetitors.isEmpty()); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could not save competitor registrations: " + caught.getMessage()); + competitorsRegistered.onFailure(caught); + } + }); + } + + private void openAddCompetitorDialog() { + new CompetitorEditDialog(stringMessages, new CompetitorDTOImpl(), new DataEntryDialog.DialogCallback() { + @Override + public void ok(CompetitorDTO competitor) { + sailingService.addOrUpdateCompetitor(competitor, new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Error trying to add competitor: "+caught.getMessage()); + } + + @Override + public void onSuccess(CompetitorDTO updatedCompetitor) { + allCompetitorsTable.getFilterField().add(updatedCompetitor); + } + }); + } + + @Override + public void cancel() { + } + }).show(); + } + + private void refreshCompetitors() { + registeredCompetitorsTable.getDataProvider().getList().clear(); + + allCompetitorsTable.refreshCompetitorList(filterByLeaderBoardInitially ? leaderboardName : null, + true, new Callback, Throwable>() { + @Override + public void onSuccess(Iterable result) { + center(); + + //add competitors that are already registered + sailingService.getCompetitorRegistrations(leaderboardName, raceColumnName, fleetName, new AsyncCallback>() { + @Override + public void onSuccess(Collection result) { + move(allCompetitorsTable, registeredCompetitorsTable, result); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could not load already registered competitors: " + caught.getMessage()); + } + }); + } + + @Override + public void onFailure(Throwable reason) {} + }); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingCourseDefinitionDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingCourseDefinitionDialog.java new file mode 100644 index 00000000000..ca1e07ac26e --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingCourseDefinitionDialog.java @@ -0,0 +1,168 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.Collection; + +import com.google.gwt.cell.client.FieldUpdater; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.Panel; +import com.sap.sailing.domain.common.dto.PositionDTO; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.MarkDTO; +import com.sap.sailing.gwt.ui.shared.RaceCourseDTO; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; + +public class RaceLogTrackingCourseDefinitionDialog extends RaceLogTrackingDialog { + private class RaceLogCourseManagementWidget extends CourseManagementWidget { + public RaceLogCourseManagementWidget(final SailingServiceAsync sailingService, final ErrorReporter errorReporter, + final StringMessages stringMessages) { + super(sailingService, errorReporter, stringMessages); + + Button addMark = new Button(stringMessages.add(stringMessages.mark())); + addMark.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + new MarkEditDialog(stringMessages, new MarkDTO(null, null), true, new DataEntryDialog.DialogCallback() { + @Override + public void ok(MarkDTO mark) { + sailingService.addMarkToRaceLog(leaderboardName, raceColumnName, fleetName, mark, new AsyncCallback() { + @Override + public void onSuccess(Void result) { + courseManagementWidget.refresh(); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could not add mark: " + caught.getMessage()); + } + }); + } + + @Override + public void cancel() {} + }).show(); + } + }); + marksBtnsPanel.add(addMark); + + + Button cancel = new Button(stringMessages.cancel()); + cancel.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + hide(); + } + }); + buttonsPanel.add(cancel); + + ImagesBarColumn actionColumn = + new ImagesBarColumn( + new RaceLogTrackingCourseDefinitionDialogMarksImagesBarCell(stringMessages)); + actionColumn.setFieldUpdater(new FieldUpdater() { + @Override + public void update(int index, final MarkDTO markDTO, String value) { + if (RaceLogTrackingCourseDefinitionDialogMarksImagesBarCell.ACTION_PING.equals(value)) { + new PositionEntryDialog(stringMessages.pingPosition(stringMessages.mark()), + stringMessages, new DataEntryDialog.DialogCallback() { + + @Override + public void ok(PositionDTO position) { + sailingService.pingMarkViaRaceLogTracking(leaderboardName, raceColumnName, fleetName, + markDTO, position, new AsyncCallback() { + + @Override + public void onSuccess(Void result) { + refresh(); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could not ping mark: " + caught.getMessage()); + } + }); + } + + @Override + public void cancel() {} + }).show(); + } + } + }); + marks.getTable().addColumn(actionColumn); + } + + @Override + protected void save() { + sailingService.addCourseDefinitionToRaceLog(leaderboardName, raceColumnName, fleetName, + createWaypointPairs(), new AsyncCallback() { + @Override + public void onSuccess(Void result) { + hide(); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could note save course: " + caught.getMessage()); + } + }); + } + + @Override + public void refresh() { + sailingService.getLastCourseDefinitionInRaceLog(leaderboardName, raceColumnName, fleetName, new AsyncCallback() { + @Override + public void onSuccess(RaceCourseDTO result) { + updateWaypointsAndControlPoints(result); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could not load course: " + caught.getMessage()); + } + }); + + sailingService.getMarksInRaceLog(leaderboardName, raceColumnName, fleetName, new AsyncCallback>() { + @Override + public void onSuccess(Collection result) { + marks.refresh(result); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could not load marks: " + caught.getMessage()); + } + }); + } + }; + + private CourseManagementWidget courseManagementWidget; + + public RaceLogTrackingCourseDefinitionDialog(final SailingServiceAsync sailingService, final StringMessages stringMessages, + final ErrorReporter errorReporter, final String leaderboardName, final String raceColumnName, final String fleetName) { + super(sailingService, stringMessages, errorReporter, leaderboardName, raceColumnName, fleetName); + + courseManagementWidget.refresh(); + } + + @Override + protected void addMainContent(Panel mainPanel) { + super.addMainContent(mainPanel); + + courseManagementWidget = new RaceLogCourseManagementWidget(sailingService, errorReporter, stringMessages); + mainPanel.add(courseManagementWidget); + } + + @Override + protected void addButtons(Panel buttonPanel) { + //reuse buttons of widget + } + + @Override + protected void save() { + //won't be called, as we are reusing widget buttons + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingCourseDefinitionDialogMarksImagesBarCell.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingCourseDefinitionDialogMarksImagesBarCell.java new file mode 100644 index 00000000000..a33ffe1c9f9 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingCourseDefinitionDialogMarksImagesBarCell.java @@ -0,0 +1,26 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; + +import com.google.gwt.core.client.GWT; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.components.ImagesBarCell; + +public class RaceLogTrackingCourseDefinitionDialogMarksImagesBarCell extends ImagesBarCell { + public static final String ACTION_PING = "ACTION_PING"; + private static AdminConsoleResources resources = GWT.create(AdminConsoleResources.class); + private final StringMessages stringMessages; + + public RaceLogTrackingCourseDefinitionDialogMarksImagesBarCell(StringMessages stringMessages) { + super(); + this.stringMessages = stringMessages; + } + + @Override + protected Iterable getImageSpecs() { + ArrayList result = new ArrayList(); + result.add(new ImageSpec(ACTION_PING, stringMessages.pingPosition(stringMessages.mark()), makeImagePrototype(resources.ping()))); + + return result; + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingDeviceMappingsDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingDeviceMappingsDialog.java new file mode 100644 index 00000000000..14f1d45dd51 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingDeviceMappingsDialog.java @@ -0,0 +1,282 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.moxieapps.gwt.highcharts.client.Axis; +import org.moxieapps.gwt.highcharts.client.AxisTitle; +import org.moxieapps.gwt.highcharts.client.Chart; +import org.moxieapps.gwt.highcharts.client.Global; +import org.moxieapps.gwt.highcharts.client.Highcharts; +import org.moxieapps.gwt.highcharts.client.Legend; +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.PointMouseOverEvent; +import org.moxieapps.gwt.highcharts.client.events.PointMouseOverEventHandler; +import org.moxieapps.gwt.highcharts.client.plotOptions.SeriesPlotOptions; + +import com.google.gwt.cell.client.FieldUpdater; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.view.client.CellPreviewEvent; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.DeviceMappingDTO; +import com.sap.sailing.gwt.ui.shared.MarkDTO; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; + +public class RaceLogTrackingDeviceMappingsDialog extends RaceLogTrackingDialog { + public static final double PERCENTAGE_OF_TIMESPAN_TO_EXTEND_OPEN_ENDS = 0.1; + public static final String FIELD_INDEX = "index"; + public static final int CHART_WIDTH = 500; + public static final String SERIES_COLOR = "#fcb913"; + + private List allMappings = new ArrayList(); + + private DeviceMappingTableWrapper deviceMappingTable; + + private Point[] data; + + private Date latest; + private Date earliest; + + private Chart chart; + + public RaceLogTrackingDeviceMappingsDialog(final SailingServiceAsync sailingService, final StringMessages stringMessages, + final ErrorReporter errorReporter, final String leaderboardName, + final String raceColumnName, final String fleetName) { + super(sailingService, stringMessages, errorReporter, leaderboardName, raceColumnName, fleetName, false); + + deviceMappingTable = new DeviceMappingTableWrapper(sailingService, stringMessages, errorReporter); + deviceMappingTable.getTable().addCellPreviewHandler(new CellPreviewEvent.Handler() { + @Override + public void onCellPreview(CellPreviewEvent event) { + int i = allMappings.indexOf(event.getValue()); + chart.getSeries()[0].getPoints()[i].select(true, false); + } + }); + ImagesBarColumn actionCol = + new ImagesBarColumn( + new RaceLogTrackingDeviceMappingsImagesBarCell(stringMessages)); + actionCol.setFieldUpdater(new FieldUpdater() { + @Override + public void update(int index, final DeviceMappingDTO dto, String value) { + if (RaceLogTrackingDeviceMappingsImagesBarCell.ACTION_CLOSE.equals(value)) { + new SetTimePointDialog(stringMessages, stringMessages.setClosingTimePoint(), new DataEntryDialog.DialogCallback() { + @Override + public void ok(Date editedObject) { + sailingService.closeOpenEndedDeviceMapping(leaderboardName, raceColumnName, fleetName, + dto, editedObject, new AsyncCallback() { + @Override + public void onSuccess(Void result) { + refresh(); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could not close open ended mapping: " + caught.getMessage()); + } + }); + } + + @Override + public void cancel() {} + }).show(); + } else if (RaceLogTrackingDeviceMappingsImagesBarCell.ACTION_REMOVE.equals(value)) { + sailingService.revokeRaceLogEvents(leaderboardName, raceColumnName, fleetName, dto.originalRaceLogEventIds, + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could not remove mappings: " + caught.getMessage()); + } + + @Override + public void onSuccess(Void result) { + refresh(); + } + + }); + } + } + }); + deviceMappingTable.getTable().addColumn(actionCol, stringMessages.actions()); + + HorizontalPanel panel = new HorizontalPanel(); + mainPanel.add(panel); + + chart = new Chart() + .setType(Series.Type.COLUMN_RANGE) + .setChartTitleText(stringMessages.deviceMappings()) + .setLegend(new Legend().setEnabled(false)) + .setInverted(true); + + chart.setSeriesPlotOptions(new SeriesPlotOptions().setPointMouseOverEventHandler(new PointMouseOverEventHandler() { + @Override + public boolean onMouseOver(PointMouseOverEvent pointMouseOverEvent) { + int i = (int) pointMouseOverEvent.getXAsLong(); + deviceMappingTable.getSelectionModel().setSelected(allMappings.get(i), true); + chart.getSeries()[0].getPoints()[i].select(true, false); + return true; + } + }).setColor(SERIES_COLOR)); + + Highcharts.setOptions( + new Highcharts.Options().setGlobal( + new Global() + .setUseUTC(false) + )); + + chart.getXAxis().setOption("labels/enabled", false) + .setGridLineWidth(0) + .setMinorGridLineWidth(0); + + chart.getYAxis() + .setAxisTitle(new AxisTitle().setText(stringMessages.time())) + .setType(Axis.Type.DATE_TIME) + .setGridLineWidth(0) + .setMinorGridLineWidth(0); + + chart.setWidth(CHART_WIDTH + "px"); + chart.setHeight("400px"); + + chart.setToolTip(new ToolTip().setFormatter(new ToolTipFormatter() { + @Override + public String format(ToolTipData toolTipData) { + int index = (int) toolTipData.getXAsLong(); + DeviceMappingDTO mapping = allMappings.get(index); + String itemType = mapping.mappedTo instanceof MarkDTO ? stringMessages.mark() : stringMessages.competitor(); + + return "" + stringMessages.device() + ": " + mapping.deviceIdentifier.deviceType + " - " + mapping.deviceIdentifier.deviceId + "
" + + "" + stringMessages.mappedTo() + ": " + itemType + " - " + mapping.mappedTo + "
" + + "" + stringMessages.from() + ": " + DateAndTimeFormatterUtil.formatDateAndTime(mapping.from) + "
" + + "" + stringMessages.to() + ": " + DateAndTimeFormatterUtil.formatDateAndTime(mapping.to); + } + })); + + + panel.add(chart); + panel.add(deviceMappingTable); + + refresh(); + center(); + } + + @Override + protected void addButtons(Panel buttonPanel) { + Button addCompetitorButton = new Button(stringMessages.add()); + addCompetitorButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + addMapping(); + } + }); + buttonPanel.add(addCompetitorButton); + + super.addButtons(buttonPanel); + } + + @Override + protected void save() { + // TODO Auto-generated method stub + + } + + private void updateChart() { + data = new Point[allMappings.size()]; + long earliestMillis = earliest.getTime(); + long latestMillis = latest.getTime(); + long range = latestMillis - earliestMillis; + long extension = (long) (range * PERCENTAGE_OF_TIMESPAN_TO_EXTEND_OPEN_ENDS); + long yMin = earliestMillis - extension; + long yMax = latestMillis + extension; + + int i = 0; + for (DeviceMappingDTO mapping : allMappings) { + JSONObject userData = new JSONObject(); + userData.put(FIELD_INDEX, userData); + + long from = mapping.from == null ? yMin - range : mapping.from.getTime(); + long to = mapping.to == null ? yMax + range : mapping.to.getTime(); + + data[i] = new Point(i, from, to); + + i++; + } + + chart.removeAllSeries(false); + + chart.addSeries(chart.createSeries() + .setName(stringMessages.deviceMappings()) + .setPoints(data)); + + chart.getYAxis().setExtremes(yMin, yMax); + } + + private void updateExtremes(DeviceMappingDTO mapping) { + if (mapping.from != null && earliest.after(mapping.from)) earliest = mapping.from; + if (mapping.to != null && latest.before(mapping.to)) latest = mapping.to; + if (mapping.to != null && earliest.after(mapping.to)) earliest = mapping.to; + if (mapping.from != null && latest.before(mapping.from)) latest = mapping.from; + } + + private void refresh() { + earliest = new Date(Long.MAX_VALUE); + latest = new Date(Long.MIN_VALUE); + + sailingService.getDeviceMappingsFromRaceLog(leaderboardName, raceColumnName, fleetName, new AsyncCallback>() { + @Override + public void onSuccess(List result) { + for (DeviceMappingDTO mapping : result) { + updateExtremes(mapping); + } + allMappings = result; + + updateChart(); + deviceMappingTable.refresh(allMappings); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could not load mappings for marks: " + caught.getMessage()); + } + }); + } + + private void showAddMappingDialog(DeviceMappingDTO mapping) { + new AddDeviceMappingDialog(sailingService, errorReporter, stringMessages, + leaderboardName, raceColumnName, fleetName, new DataEntryDialog.DialogCallback() { + @Override + public void ok(final DeviceMappingDTO mapping) { + sailingService.addDeviceMappingToRaceLog(leaderboardName, raceColumnName, fleetName, mapping, new AsyncCallback() { + @Override + public void onSuccess(Void result) { + refresh(); + } + + @Override + public void onFailure(Throwable caught) { + showAddMappingDialog(mapping); + errorReporter.reportError("Could not add mapping: " + caught.getMessage()); + } + }); + } + + @Override + public void cancel() {} + }, mapping).show(); + } + + private void addMapping() { + showAddMappingDialog(null); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingDeviceMappingsImagesBarCell.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingDeviceMappingsImagesBarCell.java new file mode 100644 index 00000000000..d31582aa965 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingDeviceMappingsImagesBarCell.java @@ -0,0 +1,35 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; + +import com.google.gwt.core.client.GWT; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.components.ImagesBarCell; +import com.sap.sailing.gwt.ui.shared.DeviceMappingDTO; + +public class RaceLogTrackingDeviceMappingsImagesBarCell extends ImagesBarCell { + public static final String ACTION_CLOSE = "CLOSE"; + public static final String ACTION_REMOVE = "REMOVE"; + private static AdminConsoleResources resources = GWT.create(AdminConsoleResources.class); + private final StringMessages stringMessages; + + public RaceLogTrackingDeviceMappingsImagesBarCell(StringMessages stringMessages) { + super(); + this.stringMessages = stringMessages; + } + + @Override + protected Iterable getImageSpecs() { + ArrayList result = new ArrayList(); + DeviceMappingDTO mapping = (DeviceMappingDTO) getContext().getKey(); + + result.add(new ImageSpec(ACTION_REMOVE, stringMessages.remove(), makeImagePrototype(resources.removeIcon()))); + + if (mapping.from == null || mapping.to == null) { + result.add(new ImageSpec(ACTION_CLOSE, stringMessages.closeTimeRange(), + makeImagePrototype(resources.closeTimeRange()))); + } + + return result; + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingDialog.java new file mode 100644 index 00000000000..aaa8baddae4 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingDialog.java @@ -0,0 +1,77 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.DialogBox; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; + +public abstract class RaceLogTrackingDialog extends DialogBox { + protected final SailingServiceAsync sailingService; + protected final StringMessages stringMessages; + protected final ErrorReporter errorReporter; + protected final String leaderboardName; + protected final String raceColumnName; + protected final String fleetName; + protected Button saveButton; + protected final boolean editable; + protected final VerticalPanel mainPanel; + + public RaceLogTrackingDialog(SailingServiceAsync sailingService, StringMessages stringMessages, + ErrorReporter errorReporter, String leaderboardName, String raceColumnName, String fleetName) { + this(sailingService, stringMessages, errorReporter, leaderboardName, raceColumnName, fleetName, true); + } + + public RaceLogTrackingDialog(SailingServiceAsync sailingService, StringMessages stringMessages, + ErrorReporter errorReporter, String leaderboardName, String raceColumnName, String fleetName, boolean editable) { + this.sailingService = sailingService; + this.stringMessages = stringMessages; + this.errorReporter = errorReporter; + this.leaderboardName = leaderboardName; + this.raceColumnName = raceColumnName; + this.fleetName = fleetName; + this.editable = editable; + + mainPanel = new VerticalPanel(); + setWidget(mainPanel); + + addMainContent(mainPanel); + center(); + } + + protected void addMainContent(Panel mainPanel) { + HorizontalPanel buttonPanel = new HorizontalPanel(); + mainPanel.add(buttonPanel); + + addButtons(buttonPanel); + } + + protected void addButtons(Panel buttonPanel) { + Button cancel = new Button(stringMessages.cancel()); + cancel.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + hide(); + } + }); + buttonPanel.add(cancel); + if (editable) { + saveButton = new Button(stringMessages.save()); + saveButton.setTitle(stringMessages.canOnlyBeEditedBeforeStartingTracking()); + saveButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + save(); + } + }); + buttonPanel.add(saveButton); + } + } + + protected abstract void save(); +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingEventManagementImagesBarCell.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingEventManagementImagesBarCell.java new file mode 100644 index 00000000000..56b0d917dcc --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingEventManagementImagesBarCell.java @@ -0,0 +1,34 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.text.shared.SafeHtmlRenderer; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.components.ImagesBarCell; + +public class RaceLogTrackingEventManagementImagesBarCell extends ImagesBarCell { + public static final String ACTION_DENOTE_FOR_RACELOG_TRACKING = "ACTION_DENOTE_FOR_RACELOG_TRACKING"; + private static AdminConsoleResources resources = GWT.create(AdminConsoleResources.class); + private final StringMessages stringMessages; + + public RaceLogTrackingEventManagementImagesBarCell(StringMessages stringMessages) { + super(); + this.stringMessages = stringMessages; + } + + public RaceLogTrackingEventManagementImagesBarCell(SafeHtmlRenderer renderer, StringMessages stringConstants) { + super(); + this.stringMessages = stringConstants; + } + + @Override + protected Iterable getImageSpecs() { + ArrayList result = new ArrayList(); + + result.add(new ImageSpec(ACTION_DENOTE_FOR_RACELOG_TRACKING, stringMessages.denoteForRaceLogTracking(), + makeImagePrototype(resources.denoteForRaceLogTracking()))); + + return result; + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingEventManagementPanel.java new file mode 100644 index 00000000000..07516b606c6 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingEventManagementPanel.java @@ -0,0 +1,423 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.gwt.cell.client.FieldUpdater; +import com.google.gwt.core.client.Callback; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.json.client.JSONArray; +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONParser; +import com.google.gwt.user.cellview.client.CellTable; +import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler; +import com.google.gwt.user.cellview.client.TextColumn; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.CaptionPanel; +import com.google.gwt.user.client.ui.FileUpload; +import com.google.gwt.user.client.ui.FormPanel; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.view.client.MultiSelectionModel; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SelectionChangeEvent.Handler; +import com.sap.sailing.domain.common.dto.FleetDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.domain.common.impl.Util.Triple; +import com.sap.sailing.domain.common.racelog.tracking.RaceLogTrackingState; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.RegattaRefresher; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.DeviceIdentifierDTO; +import com.sap.sailing.gwt.ui.shared.RaceLogSetStartTimeAndProcedureDTO; +import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; +import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; + +/** + * Allows the user to start and stop tracking of races using the RaceLog-tracking connector. + */ +public class RaceLogTrackingEventManagementPanel extends AbstractLeaderboardConfigPanel { + private Button startTrackingButton; + private DeviceIdentifierTableWrapper deviceIdentifierTable; + + public RaceLogTrackingEventManagementPanel(SailingServiceAsync sailingService, AdminConsoleEntryPoint adminConsole, + RegattaRefresher regattaRefresher, + ErrorReporter errorReporter, StringMessages stringMessages) { + super(sailingService, adminConsole, errorReporter, stringMessages, + new MultiSelectionModel()); + + //add upload panel + CaptionPanel importPanel = new CaptionPanel(stringMessages.importFixes()); + VerticalPanel importContent = new VerticalPanel(); + mainPanel.add(importPanel); + importPanel.add(importContent); + final FormPanel formPanel = new FormPanel(); + importContent.add(formPanel); + deviceIdentifierTable = new DeviceIdentifierTableWrapper(sailingService, stringMessages, errorReporter); + importContent.add(deviceIdentifierTable); + + HorizontalPanel inFormPanel = new HorizontalPanel(); + formPanel.add(inFormPanel); + formPanel.setAction("/sailingserver/trackfiles/import"); + formPanel.setEncoding(FormPanel.ENCODING_MULTIPART); + formPanel.setMethod(FormPanel.METHOD_POST); + formPanel.addSubmitCompleteHandler(new SubmitCompleteHandler() { + @Override + public void onSubmitComplete(SubmitCompleteEvent arg0) { + JSONArray json = JSONParser.parseLenient(arg0.getResults()).isArray(); + deviceIdentifierTable.getDataProvider().getList().clear(); + for (int i=0; i leaderboardTable) { + ListHandler leaderboardColumnListHandler = new ListHandler( + leaderboardList.getList()); + + TextColumn leaderboardNameColumn = new TextColumn() { + @Override + public String getValue(StrippedLeaderboardDTO leaderboard) { + return leaderboard.name; + } + }; + + TextColumn leaderboardDisplayNameColumn = new TextColumn() { + @Override + public String getValue(StrippedLeaderboardDTO leaderboard) { + return leaderboard.getDisplayName() != null ? leaderboard.getDisplayName() : ""; + } + }; + + ImagesBarColumn leaderboardActionColumn = + new ImagesBarColumn( + new RaceLogTrackingEventManagementImagesBarCell(stringMessages)); + leaderboardActionColumn.setFieldUpdater(new FieldUpdater() { + @Override + public void update(int index, StrippedLeaderboardDTO leaderboardDTO, String value) { + if (RaceLogTrackingEventManagementImagesBarCell.ACTION_DENOTE_FOR_RACELOG_TRACKING.equals(value)) { + denoteForRaceLogTracking(leaderboardDTO); + } + } + }); + + leaderboardTable.addColumn(leaderboardNameColumn, stringMessages.name()); + leaderboardTable.addColumn(leaderboardDisplayNameColumn, stringMessages.displayName()); + leaderboardTable.addColumn(leaderboardActionColumn, stringMessages.actions()); + leaderboardTable.addColumnSortHandler(leaderboardColumnListHandler); + } + + private RaceLogTrackingState getTrackingState(RaceColumnDTOAndFleetDTOWithNameBasedEquality race) { + return race.getA().getRaceLogTrackingInfo(race.getB()).raceLogTrackingState; + } + + private boolean doesTrackerExist(RaceColumnDTOAndFleetDTOWithNameBasedEquality race) { + return race.getA().getRaceLogTrackingInfo(race.getB()).raceLogTrackerExists; + } + + private boolean doCompetitorResgistrationsExist(RaceColumnDTOAndFleetDTOWithNameBasedEquality race) { + return race.getA().getRaceLogTrackingInfo(race.getB()).competitorRegistrationsExists; + } + + @Override + protected void addColumnsToRacesTable(CellTable racesTable) { + TextColumn raceLogTrackingStateColumn = new TextColumn() { + @Override + public String getValue(RaceColumnDTOAndFleetDTOWithNameBasedEquality raceColumnAndFleetName) { + RaceLogTrackingState state = getTrackingState(raceColumnAndFleetName); + return state.name(); + } + }; + + TextColumn trackerStateColumn = new TextColumn() { + @Override + public String getValue(RaceColumnDTOAndFleetDTOWithNameBasedEquality raceColumnAndFleetName) { + return doesTrackerExist(raceColumnAndFleetName) ? "Active" : "None"; + } + }; + + ImagesBarColumn raceActionColumn = + new ImagesBarColumn( + new RaceLogTrackingEventManagementRaceImagesBarCell(stringMessages)); + raceActionColumn.setFieldUpdater(new FieldUpdater() { + @Override + public void update(int index, final RaceColumnDTOAndFleetDTOWithNameBasedEquality object, String value) { + final String leaderboardName = getSelectedLeaderboardName(); + final String raceColumnName = object.getA().getName(); + final String fleetName = object.getB().getName(); + boolean editable = ! (doesTrackerExist(object) && + getTrackingState(object) == RaceLogTrackingState.TRACKING); + if (RaceLogTrackingEventManagementRaceImagesBarCell.ACTION_DENOTE_FOR_RACELOG_TRACKING.equals(value)) { + denoteForRaceLogTracking(object.getA(), object.getB()); + } else if (RaceLogTrackingEventManagementRaceImagesBarCell.ACTION_REMOVE_DENOTATION.equals(value)) { + removeDenotation(object.getA(), object.getB()); + } else if (RaceLogTrackingEventManagementRaceImagesBarCell.ACTION_COMPETITOR_REGISTRATIONS.equals(value)) { + new RaceLogTrackingCompetitorRegistrationsDialog(sailingService, stringMessages, errorReporter, + getSelectedLeaderboardName(), object.getA().getName(), object.getB().getName(), editable, + new Callback() { + @Override + public void onSuccess(Boolean result) { + object.getA().getRaceLogTrackingInfo(object.getB()).competitorRegistrationsExists = result; + } + + @Override + public void onFailure(Throwable reason) {} + }).show(); + } else if (RaceLogTrackingEventManagementRaceImagesBarCell.ACTION_DEFINE_COURSE.equals(value)) { + new RaceLogTrackingCourseDefinitionDialog(sailingService, stringMessages, errorReporter, leaderboardName, raceColumnName, fleetName).show(); + } else if (RaceLogTrackingEventManagementRaceImagesBarCell.ACTION_MAP_DEVICES.equals(value)) { + new RaceLogTrackingDeviceMappingsDialog(sailingService, stringMessages, errorReporter, leaderboardName, raceColumnName, fleetName).show(); + } else if (RaceLogTrackingEventManagementRaceImagesBarCell.ACTION_COPY.equals(value)) { + List races = + new ArrayList<>(raceColumnTable.getDataProvider().getList()); + races.remove(object); + new SelectRacesDialog(sailingService, errorReporter, stringMessages, races, + new DialogCallback>() { + @Override + public void ok(Set editedObject) { + Set> toRaceLogs = new HashSet<>(); + for (RaceColumnDTOAndFleetDTOWithNameBasedEquality race : editedObject) { + toRaceLogs.add(toTriple(leaderboardName, race)); + } + sailingService.copyCourseAndCompetitorsToOtherRaceLogs( + toTriple(leaderboardName, object), toRaceLogs, new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could not copy course and competitors: " + caught.getMessage()); + } + + @Override + public void onSuccess(Void result) { + loadAndRefreshLeaderboard(leaderboardName, object.getA().getName()); + } + }); + + } + + @Override + public void cancel() {} + }).show(); + } else if (RaceLogTrackingEventManagementRaceImagesBarCell.ACTION_SET_START_TIME.equals(value)) { + setStartTime(getSelectedRaceColumnWithFleet().getA(), getSelectedRaceColumnWithFleet().getB()); + } + } + }); + + racesTable.addColumn(raceLogTrackingStateColumn, stringMessages.raceStatusColumn()); + racesTable.addColumn(trackerStateColumn, stringMessages.trackerStatus()); + racesTable.addColumn(raceActionColumn, stringMessages.actions()); + racesTable.setWidth("600px"); + } + + private Triple toTriple(String leaderboardName, + RaceColumnDTOAndFleetDTOWithNameBasedEquality race) { + return new Triple(leaderboardName, race.getA().getName(), race.getB().getName()); + } + + @Override + protected void addLeaderboardConfigControls(Panel configPanel) {} + + @Override + protected void addLeaderboardCreateControls(Panel createPanel) {} + + @Override + protected void addSelectedLeaderboardRacesControls(Panel racesPanel) { + startTrackingButton = new Button(stringMessages.startTracking()); + startTrackingButton.setEnabled(false); + racesPanel.add(startTrackingButton); + startTrackingButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + startTracking(raceColumnTableSelectionModel.getSelectedSet()); + } + }); + + raceColumnTableSelectionModel.addSelectionChangeHandler(new Handler() { + @Override + public void onSelectionChange(SelectionChangeEvent event) { + enableStartTrackingButtonIfAppropriateRacesSelected(); + } + }); + } + + private void enableStartTrackingButtonIfAppropriateRacesSelected() { + boolean enable = raceColumnTableSelectionModel.getSelectedSet().size() > 0; + for (RaceColumnDTOAndFleetDTOWithNameBasedEquality race : raceColumnTableSelectionModel.getSelectedSet()) { + if (! getTrackingState(race).isForTracking() || doesTrackerExist(race)) { + enable = false; + } + } + startTrackingButton.setEnabled(enable); + } + + @Override + protected void leaderboardRaceColumnSelectionChanged() { + RaceColumnDTOAndFleetDTOWithNameBasedEquality selectedRaceInLeaderboard = getSelectedRaceColumnWithFleet(); + if (selectedRaceInLeaderboard != null) { + selectTrackedRaceInRaceList(); + } else { + trackedRacesListComposite.clearSelection(); + } + enableStartTrackingButtonIfAppropriateRacesSelected(); + } + + @Override + protected void leaderboardSelectionChanged() { + StrippedLeaderboardDTO selectedLeaderboard = getSelectedLeaderboard(); + if (leaderboardSelectionModel.getSelectedSet().size() == 1 && selectedLeaderboard != null) { + raceColumnTable.getDataProvider().getList().clear(); + for (RaceColumnDTO raceColumn : selectedLeaderboard.getRaceList()) { + for (FleetDTO fleet : raceColumn.getFleets()) { + raceColumnTable.getDataProvider().getList().add(new RaceColumnDTOAndFleetDTOWithNameBasedEquality(raceColumn, fleet)); + } + } + selectedLeaderBoardPanel.setVisible(true); + selectedLeaderBoardPanel.setCaptionText("Details of leaderboard '" + selectedLeaderboard.name + "'"); + if (!selectedLeaderboard.type.isMetaLeaderboard()) { + trackedRacesCaptionPanel.setVisible(true); + } + } else { + selectedLeaderBoardPanel.setVisible(false); + trackedRacesCaptionPanel.setVisible(false); + } + raceColumnTableSelectionModel.clear(); + } + + private void denoteForRaceLogTracking(final StrippedLeaderboardDTO leaderboard) { + sailingService.denoteForRaceLogTracking(leaderboard.name, new AsyncCallback() { + @Override + public void onSuccess(Void result) { + loadAndRefreshLeaderboard(leaderboard.name, null); + raceColumnTableSelectionModel.clear(); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could not denote for RaceLog tracking: " + caught.getMessage()); + } + }); + } + + private void denoteForRaceLogTracking(final RaceColumnDTO raceColumn, final FleetDTO fleet) { + final StrippedLeaderboardDTO leaderboard = getSelectedLeaderboard(); + sailingService.denoteForRaceLogTracking(leaderboard.name, raceColumn.getName(), fleet.getName(), new AsyncCallback() { + @Override + public void onSuccess(Void result) { + loadAndRefreshLeaderboard(leaderboard.name, raceColumn.getName()); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could not denote for RaceLog tracking: " + caught.getMessage()); + } + }); + } + + private void removeDenotation(final RaceColumnDTO raceColumn, final FleetDTO fleet) { + final StrippedLeaderboardDTO leaderboard = getSelectedLeaderboard(); + sailingService.removeDenotationForRaceLogTracking(leaderboard.name, raceColumn.getName(), fleet.getName(), new AsyncCallback() { + @Override + public void onSuccess(Void result) { + loadAndRefreshLeaderboard(leaderboard.name, raceColumn.getName()); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Could not remove denotation: " + caught.getMessage()); + } + }); + } + + private void startTracking(Set races) { + final StrippedLeaderboardDTO leaderboard = getSelectedLeaderboard(); + + //prompt user if competitor registrations are missing for same races + String namesOfRacesMissingRegistrations = ""; + for (RaceColumnDTOAndFleetDTOWithNameBasedEquality race : races) { + if (! doCompetitorResgistrationsExist(race)) { + namesOfRacesMissingRegistrations += race.getA().getName() + "/" + race.getB().getName() + " "; + } + } + if (! namesOfRacesMissingRegistrations.isEmpty()) { + boolean proceed = Window.confirm(stringMessages.competitorRegistrationsMissingProceed( + namesOfRacesMissingRegistrations)); + if (! proceed) { + return; + } + } + + for (RaceColumnDTOAndFleetDTOWithNameBasedEquality race : races) { + final RaceColumnDTO raceColumn = race.getA(); + final FleetDTO fleet = race.getB(); + + sailingService.startRaceLogTracking(leaderboard.name, raceColumn.getName(), fleet.getName(), + new AsyncCallback() { + @Override + public void onSuccess(Void result) { + loadAndRefreshLeaderboard(leaderboard.name, raceColumn.getName()); + trackedRacesListComposite.regattaRefresher.fillRegattas(); + } + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Failed to start tracking " + raceColumn.getName() + " - " + + fleet.getName() + ": " + caught.getMessage()); + } + }); + } + } + + private void setStartTime(RaceColumnDTO raceColumnDTO, FleetDTO fleetDTO) { + new SetStartTimeDialog(sailingService, errorReporter, getSelectedLeaderboardName(), raceColumnDTO.getName(), + fleetDTO.getName(), stringMessages, new DataEntryDialog.DialogCallback() { + @Override + public void ok(RaceLogSetStartTimeAndProcedureDTO editedObject) { + sailingService.setStartTimeAndProcedure(editedObject, new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(caught.getMessage()); + } + + @Override + public void onSuccess(Boolean result) { + if (!result) { + Window.alert(stringMessages.failedToSetNewStartTime()); + } else { + trackedRacesListComposite.regattaRefresher.fillRegattas(); + } + } + }); + } + + @Override + public void cancel() { } + }).show(); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingEventManagementRaceImagesBarCell.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingEventManagementRaceImagesBarCell.java new file mode 100644 index 00000000000..52459167fc4 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogTrackingEventManagementRaceImagesBarCell.java @@ -0,0 +1,45 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gwt.core.client.GWT; +import com.sap.sailing.gwt.ui.adminconsole.AbstractLeaderboardConfigPanel.RaceColumnDTOAndFleetDTOWithNameBasedEquality; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.components.ImagesBarCell; + +public class RaceLogTrackingEventManagementRaceImagesBarCell extends ImagesBarCell { + public final static String ACTION_DENOTE_FOR_RACELOG_TRACKING = "ACTION_DENOTE_FOR_RACELOG_TRACKING"; + public final static String ACTION_REMOVE_DENOTATION = "ACTION_REMOVE_DENOTATION"; + public final static String ACTION_COMPETITOR_REGISTRATIONS = "ACTION_COMPETITOR_REGISTRATIONS"; + public final static String ACTION_DEFINE_COURSE = "ACTION_DEFINE_COURSE"; + public final static String ACTION_MAP_DEVICES = "ACTION_MAP_DEVICES"; + public final static String ACTION_COPY = "ACTION_COPY"; + public final static String ACTION_SET_START_TIME = "ACTION_SET_START_TIME"; + + private final StringMessages stringMessages; + private static AdminConsoleResources resources = GWT.create(AdminConsoleResources.class); + + public RaceLogTrackingEventManagementRaceImagesBarCell(StringMessages stringConstants) { + super(); + this.stringMessages = stringConstants; + } + + @Override + protected Iterable getImageSpecs() { + List result = new ArrayList(); + RaceColumnDTOAndFleetDTOWithNameBasedEquality object = (RaceColumnDTOAndFleetDTOWithNameBasedEquality) getContext().getKey(); + if (! object.getA().getRaceLogTrackingInfo(object.getB()).raceLogTrackingState.isForTracking()) { + result.add(new ImageSpec(ACTION_DENOTE_FOR_RACELOG_TRACKING, stringMessages.denoteForRaceLogTracking(), makeImagePrototype(resources.denoteForRaceLogTracking()))); + } else { + result.add(new ImageSpec(ACTION_REMOVE_DENOTATION, stringMessages.removeDenotation(), makeImagePrototype(resources.unDenoteForRaceLogTracking()))); + result.add(new ImageSpec(ACTION_COMPETITOR_REGISTRATIONS, stringMessages.competitorRegistrations(), makeImagePrototype(resources.competitorRegistrations()))); + result.add(new ImageSpec(ACTION_DEFINE_COURSE, stringMessages.defineCourse(), makeImagePrototype(resources.defineCourse()))); + result.add(new ImageSpec(ACTION_MAP_DEVICES, stringMessages.mapDevices(), makeImagePrototype(resources.mapDevices()))); + result.add(new ImageSpec(ACTION_SET_START_TIME, stringMessages.setStartTime(), makeImagePrototype(resources.clockIcon()))); + result.add(new ImageSpec(ACTION_COPY, stringMessages.copyCourseAndCompetitors(), makeImagePrototype(resources.copy()))); + } + + return result; + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceTableWrapper.java new file mode 100644 index 00000000000..865a14fbed0 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceTableWrapper.java @@ -0,0 +1,64 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.HashMap; +import java.util.Map; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlUtils; +import com.google.gwt.user.cellview.client.Column; +import com.google.gwt.user.cellview.client.TextColumn; +import com.google.gwt.view.client.SelectionModel; +import com.sap.sailing.domain.common.RegattaNameAndRaceName; +import com.sap.sailing.gwt.ui.adminconsole.AbstractLeaderboardConfigPanel.RaceColumnDTOAndFleetDTOWithNameBasedEquality; +import com.sap.sailing.gwt.ui.adminconsole.LeaderboardConfigPanel.AnchorCell; +import com.sap.sailing.gwt.ui.adminconsole.LeaderboardConfigPanel.AnchorTemplates; +import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; + +public class RaceTableWrapper> +extends TableWrapper { + private final AnchorTemplates ANCHORTEMPLATE = GWT.create(AnchorTemplates.class); + private String selectedLeaderboardName; + + public RaceTableWrapper(SailingServiceAsync sailingService, StringMessages stringMessages, + ErrorReporter errorReporter, S selectionModel) { + super(sailingService, stringMessages, errorReporter, selectionModel); + + Column raceNameColumn = + new Column(new AnchorCell()) { + @Override + public SafeHtml getValue(RaceColumnDTOAndFleetDTOWithNameBasedEquality raceInLeaderboardDTOAndFleetName) { + if (raceInLeaderboardDTOAndFleetName.getA().getRaceIdentifier(raceInLeaderboardDTOAndFleetName.getB()) != null) { + RegattaNameAndRaceName raceIdentifier = (RegattaNameAndRaceName) raceInLeaderboardDTOAndFleetName + .getA().getRaceIdentifier(raceInLeaderboardDTOAndFleetName.getB()); + Map params = new HashMap<>(); + params.put("leaderboardName", selectedLeaderboardName); + params.put("regattaName", raceIdentifier.getRegattaName()); + params.put("raceName", raceIdentifier.getRaceName()); + params.put("canReplayDuringLiveRaces", "true"); + String link = EntryPointLinkFactory.createRaceBoardLink(params); + return ANCHORTEMPLATE.cell(link, raceInLeaderboardDTOAndFleetName.getA().getRaceColumnName()); + } else { + return SafeHtmlUtils.fromString(raceInLeaderboardDTOAndFleetName.getA().getRaceColumnName()); + } + } + }; + + TextColumn fleetNameColumn = new TextColumn() { + @Override + public String getValue(RaceColumnDTOAndFleetDTOWithNameBasedEquality object) { + return object.getB().getName(); + } + }; + + table.addColumn(raceNameColumn, stringMessages.race()); + table.addColumn(fleetNameColumn, stringMessages.fleet()); + } + + public void setSelectedLeaderboardName(String name) { + this.selectedLeaderboardName = name; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SelectRacesDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SelectRacesDialog.java new file mode 100644 index 00000000000..ee49e8cee09 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SelectRacesDialog.java @@ -0,0 +1,58 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.Collection; +import java.util.Set; + +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.view.client.MultiSelectionModel; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SelectionChangeEvent.Handler; +import com.sap.sailing.gwt.ui.adminconsole.AbstractLeaderboardConfigPanel.RaceColumnDTOAndFleetDTOWithNameBasedEquality; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; + +public class SelectRacesDialog extends DataEntryDialog> { + private final RaceTableWrapper> + racesTable; + + public SelectRacesDialog(SailingServiceAsync sailingService, ErrorReporter errorReporter, final StringMessages stringMessages, + Collection races, + DialogCallback> callback) { + super(stringMessages.selectRaces(), stringMessages.selectRaces(), stringMessages.ok(), stringMessages.cancel(), + new Validator>() { + @Override + public String getErrorMessage(Set valueToValidate) { + if (valueToValidate.isEmpty()) { + return stringMessages.selectAtLeastOne(); + } + return null; + } + + }, true, callback); + + racesTable = new RaceTableWrapper>( + sailingService, stringMessages, errorReporter, new MultiSelectionModel()); + + racesTable.getDataProvider().getList().addAll(races); + + racesTable.getSelectionModel().addSelectionChangeHandler(new Handler() { + @Override + public void onSelectionChange(SelectionChangeEvent event) { + validate(); + } + }); + } + + @Override + protected Widget getAdditionalWidget() { + return racesTable.getTable(); + } + + @Override + protected Set getResult() { + return racesTable.getSelectionModel().getSelectedSet(); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SetTimePointDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SetTimePointDialog.java new file mode 100644 index 00000000000..a0415d4f82f --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SetTimePointDialog.java @@ -0,0 +1,70 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.Date; + +import com.github.gwtbootstrap.datetimepicker.client.ui.base.HasViewMode.ViewMode; +import com.google.gwt.event.logical.shared.AttachEvent; +import com.google.gwt.event.logical.shared.AttachEvent.Handler; +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.event.logical.shared.ValueChangeHandler; +import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.BetterDateTimeBox; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; + +public class SetTimePointDialog extends DataEntryDialog { + private final BetterDateTimeBox time; + private final StringMessages stringMessages; + + public SetTimePointDialog(final StringMessages stringMessages, String title, DialogCallback callback) { + super(title, title, stringMessages.ok(), stringMessages.cancel(), new DataEntryDialog.Validator() { + @Override + public String getErrorMessage(Date valueToValidate) { + if (valueToValidate == null) return stringMessages.pleaseEnterA(stringMessages.time()); + return null; + } + }, callback); + + this.stringMessages = stringMessages; + + time = createTimeBox(); + } + + private BetterDateTimeBox createTimeBox() { + final BetterDateTimeBox timeBox = new BetterDateTimeBox(); + timeBox.addValueChangeHandler(new ValueChangeHandler() { + @Override + public void onValueChange(ValueChangeEvent event) { + validate(); + } + }); + timeBox.addAttachHandler(new Handler() { + @Override + public void onAttachOrDetach(AttachEvent event) { + if (event.isAttached()) { + addAutoHidePartner(timeBox.getPicker()); + } + } + }); + timeBox.setAutoClose(true); + timeBox.setStartView(ViewMode.HOUR); + timeBox.setFormat("dd/mm/yyyy hh:ii"); + return timeBox; + } + + @Override + protected Widget getAdditionalWidget() { + Grid grid = new Grid(1, 2); + grid.setWidget(0, 0, new Label(stringMessages.time())); + grid.setWidget(0, 1, time); + return grid; + } + + @Override + protected Date getResult() { + return time.getValue(); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java index 5b92481b772..2356f5ceb3b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java @@ -102,7 +102,6 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane fillConfigurations(); jsonUrlBox = new TextBox(); - jsonUrlBox.setText("test"); // by using 'test' as the JSON URL you will get some useful content from a static json file jsonUrlBox.getElement().getStyle().setWidth(50, Unit.EM); connectionsGrid.setWidget(0, 0, new Label(stringMessages.swissTimingEvents() + ":")); @@ -135,6 +134,20 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane } }; + TextColumn regattaNameColumn = new TextColumn() { + @Override + public String getValue(SwissTimingRaceRecordDTO object) { + return object.regattaName; + } + }; + + TextColumn seriesNameColumn = new TextColumn() { + @Override + public String getValue(SwissTimingRaceRecordDTO object) { + return object.seriesName; + } + }; + TextColumn raceIdColumn = new TextColumn() { @Override public String getValue(SwissTimingRaceRecordDTO object) { @@ -156,15 +169,12 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane } }; - // AXEL: DON'T DELETE -// TextColumn stateColumn = new TextColumn() { -// @Override -// public String getValue(SwissTimingRaceRecordDTO object) { -// String result = object.hasCourse ? "C✓ " : ""; -// result += object.hasStartlist ? "S✓" : ""; -// return result; -// } -// }; + TextColumn raceStatusColumn = new TextColumn() { + @Override + public String getValue(SwissTimingRaceRecordDTO object) { + return object.raceStatus != null ? object.raceStatus : ""; + } + }; TextColumn raceStartTimeColumn = new TextColumn() { @Override @@ -229,11 +239,15 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane boatClassColumn.setSortable(true); raceIdColumn.setSortable(true); genderColumn.setSortable(true); + raceStatusColumn.setSortable(true); AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); raceTable = new CellTable(/* pageSize */ 10000, tableRes); + raceTable.addColumn(regattaNameColumn, stringConstants.regatta()); + raceTable.addColumn(seriesNameColumn, stringConstants.series()); raceTable.addColumn(raceNameColumn, stringConstants.name()); - raceTable.addColumn(raceIdColumn, stringConstants.id()); + //raceTable.addColumn(raceIdColumn, stringConstants.id()); + raceTable.addColumn(raceStatusColumn, stringConstants.status()); raceTable.addColumn(boatClassColumn, stringConstants.boatClass()); raceTable.addColumn(genderColumn, "Gender"); raceTable.addColumn(raceStartTimeColumn, stringConstants.startTime()); @@ -244,8 +258,8 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane raceList = new ListDataProvider(); raceList.addDataDisplay(raceTable); - Handler columnSortHandler = getRaceTableColumnSortHandler(raceList.getList(), raceNameColumn, raceStartTimeColumn, - raceIdColumn, boatClassColumn, genderColumn); + Handler columnSortHandler = getRaceTableColumnSortHandler(raceList.getList(), regattaNameColumn, seriesNameColumn, + raceNameColumn, raceStartTimeColumn, raceIdColumn, boatClassColumn, genderColumn, raceStatusColumn); raceTable.addColumnSortHandler(columnSortHandler); trackedRacesPanel.add(trackedRacesListComposite); @@ -254,10 +268,16 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane availableSwissTimingRaces, raceTable, raceList) { @Override public Iterable getSearchableStrings(SwissTimingRaceRecordDTO t) { - List strings = new ArrayList(); - strings.add(t.raceId); + List strings = new ArrayList<>(); + strings.add(t.regattaName); + strings.add(t.seriesName); strings.add(t.raceName); + strings.add(t.raceStatus); strings.add(t.boatClass); + strings.add(t.gender); + if (t.raceStartTime != null) { + strings.add(dateFormatter.render(t.raceStartTime)); + } return strings; } }; @@ -277,11 +297,24 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane } private ListHandler getRaceTableColumnSortHandler(List raceRecords, - Column nameColumn, Column trackingStartColumn, + Column regattaNameColumn, Column seriesNameColumn, + Column nameColumn, Column trackingStartColumn, Column raceIdColumn, Column boatClassColumn, - final Column genderColumn) { + Column genderColumn, Column statusColumn) { ListHandler result = new ListHandler(raceRecords); + result.setComparator(regattaNameColumn, new Comparator() { + @Override + public int compare(SwissTimingRaceRecordDTO o1, SwissTimingRaceRecordDTO o2) { + return new NaturalComparator().compare(o1.regattaName, o2.regattaName); + } + }); + result.setComparator(seriesNameColumn, new Comparator() { + @Override + public int compare(SwissTimingRaceRecordDTO o1, SwissTimingRaceRecordDTO o2) { + return new NaturalComparator().compare(o1.seriesName, o2.seriesName); + } + }); result.setComparator(nameColumn, new Comparator() { @Override public int compare(SwissTimingRaceRecordDTO o1, SwissTimingRaceRecordDTO o2) { @@ -314,6 +347,12 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane return o1.gender == null ? -1 : o2.gender == null ? 1 : new NaturalComparator(false).compare(o1.gender, o2.gender); } }); + result.setComparator(statusColumn, new Comparator() { + @Override + public int compare(SwissTimingRaceRecordDTO o1, SwissTimingRaceRecordDTO o2) { + return new NaturalComparator().compare(o1.raceStatus, o2.raceStatus); + } + }); return result; } @@ -427,6 +466,7 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane if (stConfig != null) { hostnameTextbox.setValue(stConfig.getHostname()); portIntegerbox.setValue(stConfig.getPort()); + jsonUrlBox.setValue(stConfig.getJsonURL()); } } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java new file mode 100644 index 00000000000..b6a633b09d9 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java @@ -0,0 +1,68 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.Collection; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.user.cellview.client.CellTable; +import com.google.gwt.user.client.ui.IsWidget; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.view.client.ListDataProvider; +import com.google.gwt.view.client.SelectionModel; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; + +/** + * Implementing classes still have to add the table to the main panel. + */ +public abstract class TableWrapper> implements IsWidget { + protected final CellTable table; + protected final S selectionModel; + protected final ListDataProvider dataProvider; + protected Panel mainPanel; + protected final SailingServiceAsync sailingService; + protected final ErrorReporter errorReporter; + + private final AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); + + @Override + public Widget asWidget() { + return mainPanel; + } + + public TableWrapper(SailingServiceAsync sailingService, StringMessages stringMessages, ErrorReporter errorReporter, + S selectionModel) { + this.sailingService = sailingService; + this.errorReporter = errorReporter; + this.selectionModel = selectionModel; + this.dataProvider = new ListDataProvider(); + mainPanel = new VerticalPanel(); + + table = new CellTable(10000, tableRes); + + dataProvider.addDataDisplay(table); + table.setSelectionModel(selectionModel); + + mainPanel.add(table); + } + + public CellTable getTable() { + return table; + } + + public S getSelectionModel() { + return selectionModel; + } + + public ListDataProvider getDataProvider() { + return dataProvider; + } + + public void refresh(Collection newItems) { + dataProvider.getList().clear(); + dataProvider.getList().addAll(newItems); + dataProvider.flush(); + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackedRacesListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackedRacesListComposite.java index 87246cca965..db75d83ea9e 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackedRacesListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackedRacesListComposite.java @@ -4,9 +4,10 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; -import com.google.gwt.user.client.rpc.AsyncCallback; + import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.HorizontalPanel; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackedRacesSettingsDialogComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackedRacesSettingsDialogComponent.java index 2f28415cfe2..e2c09837b0f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackedRacesSettingsDialogComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackedRacesSettingsDialogComponent.java @@ -5,8 +5,8 @@ import com.google.gwt.user.client.ui.FocusWidget; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.LongBox; import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.gwt.ui.client.shared.components.SettingsDialogComponent; import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.components.SettingsDialogComponent; import com.sap.sse.gwt.client.dialog.DataEntryDialog; import com.sap.sse.gwt.client.dialog.DataEntryDialog.Validator; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointCreationDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointCreationDialog.java new file mode 100644 index 00000000000..1e3a783ace2 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointCreationDialog.java @@ -0,0 +1,104 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import java.util.List; + +import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.ListBox; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SelectionChangeEvent.Handler; +import com.google.gwt.view.client.SingleSelectionModel; +import com.sap.sailing.domain.common.PassingInstruction; +import com.sap.sailing.domain.common.impl.Util; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.ControlPointDTO; +import com.sap.sailing.gwt.ui.shared.WaypointDTO; +import com.sap.sse.gwt.client.dialog.DataEntryDialog; + +public class WaypointCreationDialog extends DataEntryDialog { + private final ControlPointTableWrapper> controlPointsWrapper; + private final ListBox passingInstructions; + private final StringMessages stringMessages; + + public WaypointCreationDialog(SailingServiceAsync sailingService, ErrorReporter errorReporter, + final StringMessages stringMessages, AdminConsoleTableResources tableRes, + List controlPoints, DialogCallback callback) { + super(stringMessages.waypoint(), stringMessages.waypoint(), + stringMessages.ok(), stringMessages.cancel(), new DataEntryDialog.Validator() { + @Override + public String getErrorMessage(WaypointDTO valueToValidate) { + if (valueToValidate.controlPoint == null) { + return stringMessages.pleaseSelectAControlPoint(); + } + return null; + } + + }, /* animationEnabled */ false, callback); + + this.stringMessages = stringMessages; + + controlPointsWrapper = new ControlPointTableWrapper>( + new SingleSelectionModel(), sailingService, stringMessages, errorReporter); + controlPointsWrapper.getDataProvider().getList().addAll(controlPoints); + + passingInstructions = createListBox(false); + updatePassingInstructions(); + + controlPointsWrapper.getSelectionModel().addSelectionChangeHandler(new Handler() { + @Override + public void onSelectionChange(SelectionChangeEvent event) { + updatePassingInstructions(); + validate(); + } + }); + } + + private void updatePassingInstructions() { + passingInstructions.clear(); + + int numMarks = controlPointsWrapper.getSelectionModel().getSelectedObject() != null ? + Util.size(controlPointsWrapper.getSelectionModel().getSelectedObject().getMarks()) : 0; + + int i = 0; + passingInstructions.insertItem(PassingInstruction.None.name(), i++); + for (PassingInstruction pi : PassingInstruction.relevantValues()) { + for (int numApplicableMarks : pi.applicability) { + if (numApplicableMarks == numMarks) { + passingInstructions.insertItem(pi.name(), i++); + } + } + } + } + + @Override + protected WaypointDTO getResult() { + PassingInstruction pi = passingInstructions.getSelectedIndex() > 0 ? + PassingInstruction.valueOf(passingInstructions.getItemText(passingInstructions.getSelectedIndex())) + : PassingInstruction.None; + + ControlPointDTO controlPoint = controlPointsWrapper.getSelectionModel().getSelectedObject(); + String name = null; + if (controlPoint != null) { + name = controlPoint.getName(); + } + return new WaypointDTO(name, controlPoint, pi); + } + + @Override + protected Widget getAdditionalWidget() { + Grid grid = new Grid(2,1); + + grid.setWidget(0, 0, controlPointsWrapper); + + HorizontalPanel passingInstructionsRow = new HorizontalPanel(); + grid.setWidget(1, 0, passingInstructionsRow); + passingInstructionsRow.add(new Label(stringMessages.passingInstructions() + ":")); + passingInstructionsRow.add(passingInstructions); + + return grid; + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java new file mode 100644 index 00000000000..ea04002dfe2 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java @@ -0,0 +1,51 @@ +package com.sap.sailing.gwt.ui.adminconsole; + +import com.google.gwt.user.cellview.client.TextColumn; +import com.google.gwt.view.client.SelectionModel; +import com.sap.sailing.gwt.ui.client.ErrorReporter; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.ControlPointDTO; +import com.sap.sailing.gwt.ui.shared.GateDTO; +import com.sap.sailing.gwt.ui.shared.MarkDTO; +import com.sap.sailing.gwt.ui.shared.WaypointDTO; + +public class WaypointTableWrapper> extends TableWrapper { + public WaypointTableWrapper(S selectionModel, SailingServiceAsync sailingService, final StringMessages stringMessages, + ErrorReporter errorReporter) { + super(sailingService, stringMessages, errorReporter, selectionModel); + + mainPanel.add(table); + + TextColumn nameColumn = new TextColumn() { + @Override + public String getValue(WaypointDTO d) { + return d.controlPoint.getName(); + } + }; + table.addColumn(nameColumn, stringMessages.controlPoint() + " " + stringMessages.name()); + + TextColumn typeColumn = new TextColumn() { + @Override + public String getValue(WaypointDTO w) { + ControlPointDTO d = w.controlPoint; + if (d instanceof GateDTO) { + return "2x " + stringMessages.mark(); + } else if (d instanceof MarkDTO) { + return stringMessages.mark(); + } else { + return stringMessages.unknown(); + } + } + }; + table.addColumn(typeColumn, stringMessages.type()); + + TextColumn passingInstructionsColumn = new TextColumn() { + @Override + public String getValue(WaypointDTO waypoint) { + return waypoint.passingInstructions != null ? waypoint.passingInstructions.name() : ""; + } + }; + table.addColumn(passingInstructionsColumn, stringMessages.passingInstructions()); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java index e4108e2d379..0778a9d0cff 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java @@ -571,7 +571,8 @@ public class WindPanel extends FormPanel implements RegattaDisplayer, WindShower private void showWindFixesList(RegattaAndRaceIdentifier selectedRace, RaceDTO raceDTO) { List windSourceTypeNames = new ArrayList(); windSourceTypeNames.add(WindSourceType.COMBINED.name()); - sailingService.getAveragedWindInfo(selectedRace, raceDTO.startOfRace, 30000L, 100, windSourceTypeNames, new AsyncCallback() { + sailingService.getAveragedWindInfo(selectedRace, raceDTO.startOfRace, 30000L, 100, windSourceTypeNames, + /* onlyUpToNewestEvent==true means to only use data "based on facts" */ true, new AsyncCallback() { @Override public void onFailure(Throwable caught) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DebugIdHelper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DebugIdHelper.java new file mode 100644 index 00000000000..8bc293b508e --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DebugIdHelper.java @@ -0,0 +1,45 @@ +package com.sap.sailing.gwt.ui.client; + +/** + *

Helper class for generating debug identifiers.

+ * + * @author + * Riccardo Nimser + */ +public class DebugIdHelper { + /** + *

Creates a debug identifier for the given enum value. The identifier is the camel cased name of the enum value + * as declared in its enum declaration. The name is split into single words using the underscore.

+ * + * @param value + * The enum value for which the identfier should be created. + * @return + * A debug identifier for the given enum value. + */ + public static > String createDebugId(T value) { + return createDebugId(value.name(), "_"); + } + + /** + *

Creates a debug identifier for the given name. The name is splited into single words using all whitespace + * characters.

+ * + * @param value + * The name for which the identifier should be created. + * @return + * A debug identifier for the given enum value. + */ + public static String createDebugId(String name) { + return createDebugId(name, "\\s"); + } + + private static String createDebugId(String name, String splitRegex) { + StringBuilder builder = new StringBuilder(); + + for (String token : name.split(splitRegex)) { + builder.append(token.substring(0, 1).toUpperCase()); + builder.append(token.substring(1, token.length()).toLowerCase()); + } + return builder.toString(); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DetailTypeFormatter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DetailTypeFormatter.java index 481828356ac..8f04b1f0212 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DetailTypeFormatter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DetailTypeFormatter.java @@ -53,8 +53,10 @@ public class DetailTypeFormatter { return stringMessages.gapToLeaderInSeconds(); case RACE_DISTANCE_TO_LEADER_IN_METERS: return stringMessages.windwardDistanceToLeaderInMeters(); - case RACE_AVERAGE_CROSS_TRACK_ERROR_IN_METERS: - return stringMessages.averageCrossTrackErrorInMeters(); + case RACE_AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS: + return stringMessages.averageAbsoluteCrossTrackErrorInMeters(); + case RACE_AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS: + return stringMessages.averageSignedCrossTrackErrorInMeters(); case START_TACK: return stringMessages.startTack(); case WINDWARD_DISTANCE_TO_OVERALL_LEADER: @@ -79,8 +81,10 @@ public class DetailTypeFormatter { return stringMessages.currentLeg(); case TIME_TRAVELED: return stringMessages.time(); - case AVERAGE_CROSS_TRACK_ERROR_IN_METERS: - return stringMessages.averageCrossTrackErrorInMeters(); + case AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS: + return stringMessages.averageAbsoluteCrossTrackErrorInMeters(); + case AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS: + return stringMessages.averageSignedCrossTrackErrorInMeters(); case MAXIMUM_SPEED_OVER_GROUND_IN_KNOTS: return stringMessages.maximumSpeedOverGroundInKnots(); case TOTAL_DISTANCE_TRAVELED: @@ -153,8 +157,10 @@ public class DetailTypeFormatter { case AVERAGE_TACK_LOSS_IN_METERS: case AVERAGE_JIBE_LOSS_IN_METERS: case AVERAGE_MANEUVER_LOSS_IN_METERS: - case AVERAGE_CROSS_TRACK_ERROR_IN_METERS: - case RACE_AVERAGE_CROSS_TRACK_ERROR_IN_METERS: + case AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS: + case AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS: + case RACE_AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS: + case RACE_AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS: case DISTANCE_TO_START_AT_RACE_START: case TOTAL_DISTANCE_TRAVELED: case DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_PASSING_START_IN_METERS: @@ -213,12 +219,16 @@ public class DetailTypeFormatter { */ public static String getTooltip(DetailType detailType) { switch (detailType) { - case AVERAGE_CROSS_TRACK_ERROR_IN_METERS: - return stringMessages.averageCrossTrackErrorInMetersTooltip(); + case AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS: + return stringMessages.averageAbsoluteCrossTrackErrorInMetersTooltip(); + case AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS: + return stringMessages.averageSignedCrossTrackErrorInMetersTooltip(); case GAP_CHANGE_SINCE_LEG_START_IN_SECONDS: return stringMessages.gapChangeSinceLegStartInSecondsTooltip(); - case RACE_AVERAGE_CROSS_TRACK_ERROR_IN_METERS: - return stringMessages.raceAverageCrossTrackErrorInMetersTooltip(); + case RACE_AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS: + return stringMessages.raceAverageAbsoluteCrossTrackErrorInMetersTooltip(); + case RACE_AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS: + return stringMessages.raceAverageSignedCrossTrackErrorInMetersTooltip(); case SIDE_TO_WHICH_MARK_AT_LEG_START_WAS_ROUNDED: return stringMessages.sideToWhichMarkAtLegStartWasRoundedTooltip(); case VELOCITY_MADE_GOOD_IN_KNOTS: diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceSelectionModel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceSelectionModel.java index 42315b4407e..556bbf02538 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceSelectionModel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceSelectionModel.java @@ -8,8 +8,8 @@ import java.util.Iterator; import java.util.List; import java.util.Set; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; public class RaceSelectionModel implements RaceSelectionProvider { private final List selection; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceTimePanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceTimePanel.java index 751ea204167..40475ac7e14 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceTimePanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceTimePanel.java @@ -193,7 +193,7 @@ public class RaceTimePanel extends TimePanel implements R * {@link RaceTimesInfoDTO#timePointOfNewestEvent timePointOfNewestEvent} received from the tracking infrastructure * so far and puts the timer into {@link PlayStates#Playing play mode}. */ - private boolean initTimerPosition(RaceTimesInfoDTO newRaceTimesInfo) { + private void initTimerPosition(RaceTimesInfoDTO newRaceTimesInfo) { // initialize timer position switch (timer.getPlayMode()) { case Live: @@ -209,7 +209,6 @@ public class RaceTimePanel extends TimePanel implements R } break; } - return timer.getTime() != null; } private void updateLegMarkers(RaceTimesInfoDTO newRaceTimesInfo) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java index 189a7b876bb..ea2b86f5b23 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java @@ -1,381 +1,467 @@ -package com.sap.sailing.gwt.ui.client; - -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -import com.google.gwt.user.client.rpc.RemoteService; -import com.sap.sailing.domain.common.DataImportProgress; -import com.sap.sailing.domain.common.DetailType; -import com.sap.sailing.domain.common.LeaderboardType; -import com.sap.sailing.domain.common.MaxPointsReason; -import com.sap.sailing.domain.common.NoWindException; -import com.sap.sailing.domain.common.PassingInstruction; -import com.sap.sailing.domain.common.PolarSheetGenerationResponse; -import com.sap.sailing.domain.common.PolarSheetGenerationSettings; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.RegattaIdentifier; -import com.sap.sailing.domain.common.ScoringSchemeType; -import com.sap.sailing.domain.common.WindSource; -import com.sap.sailing.domain.common.configuration.DeviceConfigurationMatcherType; -import com.sap.sailing.domain.common.dto.CompetitorDTO; -import com.sap.sailing.domain.common.dto.FleetDTO; -import com.sap.sailing.domain.common.dto.IncrementalOrFullLeaderboardDTO; -import com.sap.sailing.domain.common.dto.RaceColumnDTO; -import com.sap.sailing.domain.common.dto.RaceColumnInSeriesDTO; -import com.sap.sailing.domain.common.dto.RaceDTO; -import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; -import com.sap.sailing.domain.common.impl.Util.Pair; -import com.sap.sailing.domain.common.impl.Util.Triple; -import com.sap.sailing.domain.common.racelog.RacingProcedureType; -import com.sap.sailing.gwt.ui.shared.BulkScoreCorrectionDTO; -import com.sap.sailing.gwt.ui.shared.CompactRaceMapDataDTO; -import com.sap.sailing.gwt.ui.shared.CompetitorsRaceDataDTO; -import com.sap.sailing.gwt.ui.shared.ControlPointDTO; -import com.sap.sailing.gwt.ui.shared.CourseAreaDTO; -import com.sap.sailing.gwt.ui.shared.CoursePositionsDTO; -import com.sap.sailing.gwt.ui.shared.DeviceConfigurationDTO; -import com.sap.sailing.gwt.ui.shared.DeviceConfigurationDTO.RegattaConfigurationDTO; -import com.sap.sailing.gwt.ui.shared.DeviceConfigurationMatcherDTO; -import com.sap.sailing.gwt.ui.shared.EventDTO; -import com.sap.sailing.gwt.ui.shared.GPSFixDTO; -import com.sap.sailing.gwt.ui.shared.LeaderboardGroupDTO; -import com.sap.sailing.gwt.ui.shared.ManeuverDTO; -import com.sap.sailing.gwt.ui.shared.RaceCourseDTO; -import com.sap.sailing.gwt.ui.shared.RaceGroupDTO; -import com.sap.sailing.gwt.ui.shared.RaceLogDTO; -import com.sap.sailing.gwt.ui.shared.RaceLogSetStartTimeAndProcedureDTO; -import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; -import com.sap.sailing.gwt.ui.shared.RegattaDTO; -import com.sap.sailing.gwt.ui.shared.RegattaOverviewEntryDTO; -import com.sap.sailing.gwt.ui.shared.RegattaScoreCorrectionDTO; -import com.sap.sailing.gwt.ui.shared.ReplicationStateDTO; -import com.sap.sailing.gwt.ui.shared.ScoreCorrectionProviderDTO; -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.SwissTimingEventRecordDTO; -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.VenueDTO; -import com.sap.sailing.gwt.ui.shared.WindDTO; -import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; - -/** - * The client side stub for the RPC service. Usually, when a null date is passed to - * the time-dependent service methods, an empty (non-null) result is returned. - */ -public interface SailingService extends RemoteService { - List getPreviousTracTracConfigurations() throws Exception; - - List getRegattas(); - - List getEvents(); - - Pair> listTracTracRacesInEvent(String eventJsonURL, boolean listHiddenRaces) throws Exception; - - void trackWithTracTrac(RegattaIdentifier regattaToAddTo, Iterable rrs, String liveURI, - String storedURI, String courseDesignUpdateURI, boolean trackWind, boolean correctWindByDeclination, boolean simulateWithStartTimeNow, String tracTracUsername, String tracTracPassword) throws Exception; - - void trackWithSwissTiming(RegattaIdentifier regattaToAddTo, Iterable rrs, String hostname, int port, - boolean trackWind, boolean correctWindByDeclination) throws Exception; - - void replaySwissTimingRace(RegattaIdentifier regattaIdentifier, Iterable replayRaces, - boolean trackWind, boolean correctWindByDeclination, boolean simulateWithStartTimeNow); - - void storeTracTracConfiguration(String name, String jsonURL, String liveDataURI, String storedDataURI, String courseDesignUpdateURI, String tracTracUsername, String tracTracPassword) throws Exception; - - void stopTrackingEvent(RegattaIdentifier eventIdentifier) throws Exception; - - void stopTrackingRaces(Iterable racesToStopTracking) throws Exception; - - void removeAndUntrackRaces(Iterable regattaNamesAndRaceNames); - - WindInfoForRaceDTO getRawWindFixes(RegattaAndRaceIdentifier raceIdentifier, Collection windSources); - - void setWind(RegattaAndRaceIdentifier raceIdentifier, WindDTO wind); - - WindInfoForRaceDTO getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, long millisecondsStepWidth, - int numberOfFixes, Collection windSourceTypeNames) throws NoWindException; - - WindInfoForRaceDTO getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, Date to, long resolutionInMilliseconds, - Collection windSourceTypeNames); - - WindInfoForRaceDTO getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, long millisecondsStepWidth, - int numberOfFixes, double latDeg, double lngDeg, Collection windSources) throws NoWindException; - - CompactRaceMapDataDTO getRaceMapData(RegattaAndRaceIdentifier raceIdentifier, Date date, Map fromPerCompetitorIdAsString, - Map toPerCompetitorIdAsString, boolean extrapolate) throws NoWindException; - - RaceTimesInfoDTO getRaceTimesInfo(RegattaAndRaceIdentifier raceIdentifier); - - List getRaceTimesInfos(Collection raceIdentifiers); - - CoursePositionsDTO getCoursePositions(RegattaAndRaceIdentifier raceIdentifier, Date date); - - RaceCourseDTO getRaceCourse(RegattaAndRaceIdentifier raceIdentifier, Date date); - - void removeWind(RegattaAndRaceIdentifier raceIdentifier, WindDTO windDTO); - - public List getLeaderboardNames(); - - IncrementalOrFullLeaderboardDTO getLeaderboardByName(String leaderboardName, Date date, - Collection namesOfRaceColumnsForWhichToLoadLegDetails, String previousLeaderboardId) throws Exception; - - List getLeaderboards(); - - List getLeaderboardsByRegatta(RegattaDTO regatta); - - List getLeaderboardsByEvent(EventDTO event); - - StrippedLeaderboardDTO updateLeaderboard(String leaderboardName, String newLeaderboardName, String newLeaderboardDisplayName, int[] newDiscardingThreasholds, UUID newCourseAreaId); - - StrippedLeaderboardDTO createFlexibleLeaderboard(String leaderboardName, String leaderboardDisplayName, int[] discardThresholds, ScoringSchemeType scoringSchemeType, UUID courseAreaId); - - StrippedLeaderboardDTO createRegattaLeaderboard(RegattaIdentifier regattaIdentifier, String leaderboardDisplayName, int[] discardThresholds); - - void removeLeaderboard(String leaderboardName); - - void removeLeaderboards(Collection leaderboardNames); - - void renameLeaderboard(String leaderboardName, String newLeaderboardName); - - void renameLeaderboardColumn(String leaderboardName, String oldColumnName, String newColumnName); - - void removeLeaderboardColumn(String leaderboardName, String columnName); - - void addColumnToLeaderboard(String columnName, String leaderboardName, boolean medalRace); - - void moveLeaderboardColumnUp(String leaderboardName, String columnName); - - void moveLeaderboardColumnDown(String leaderboardName, String columnName); - - RegattaDTO createRegatta(String regattaName, String boatClassName, - RegattaCreationParametersDTO seriesNamesWithFleetNamesAndFleetOrderingAndMedal, boolean persistent, - ScoringSchemeType scoringSchemeType, UUID defaultCourseAreaId); - - void removeRegatta(RegattaIdentifier regattaIdentifier); - - void removeSeries(RegattaIdentifier regattaIdentifier, String seriesName); - - void removeRegattas(Collection regattas); - - void updateRegatta(RegattaIdentifier regattaIdentifier, UUID defaultCourseAreaUuid, RegattaConfigurationDTO regattaConfiguration); - - List addRaceColumnsToSeries(RegattaIdentifier regattaIdentifier, String seriesName, List columnNames); - - void updateSeries(RegattaIdentifier regattaIdentifier, String seriesName, String newSeriesName, boolean isMedal, - int[] resultDiscardingThresholds, boolean startsWithZeroScore, - boolean firstRaceIsNonDiscardableCarryForward, boolean hasSplitFleetScore, List fleets); - - RaceColumnInSeriesDTO addRaceColumnToSeries(RegattaIdentifier regattaIdentifier, String seriesName, String columnName); - - void removeRaceColumnsFromSeries(RegattaIdentifier regattaIdentifier, String seriesName, List columnNames); - - void removeRaceColumnFromSeries(RegattaIdentifier regattaIdentifier, String seriesName, String columnName); - - void moveRaceColumnInSeriesUp(RegattaIdentifier regattaIdentifier, String seriesName, String columnName); - - void moveRaceColumnInSeriesDown(RegattaIdentifier regattaIdentifier, String seriesName, String columnName); - - boolean connectTrackedRaceToLeaderboardColumn(String leaderboardName, String raceColumnName, - String fleetName, RegattaAndRaceIdentifier raceIdentifier); - - void disconnectLeaderboardColumnFromTrackedRace(String leaderboardName, String raceColumnName, String fleetName); - - Map getRegattaAndRaceNameOfTrackedRaceConnectedToLeaderboardColumn(String leaderboardName, String raceColumnName); - - void updateLeaderboardCarryValue(String leaderboardName, String competitorIdAsString, Double carriedPoints); - - /** - * @return the new net points in {@link Pair#getA()} and the new total points in {@link Pair#getB()} for time point - * date after the max points reason has been updated to maxPointsReasonAsString. - */ - Triple updateLeaderboardMaxPointsReason(String leaderboardName, String competitorIdAsString, - String raceColumnName, MaxPointsReason maxPointsReason, Date date) throws NoWindException; - - Triple updateLeaderboardScoreCorrection(String leaderboardName, String competitorIdAsString, - String columnName, Double correctedScore, Date date) throws NoWindException; - - void updateCompetitorDisplayNameInLeaderboard(String leaderboardName, String competitorIdAsString, String displayName); - - void updateIsMedalRace(String leaderboardName, String columnName, boolean isMedalRace); - - List getPreviousSwissTimingConfigurations(); - - SwissTimingEventRecordDTO getRacesOfSwissTimingEvent(String eventJsonURL) throws Exception; - - void storeSwissTimingConfiguration(String configName, String jsonURL, String hostname, int port); - - String[] getCountryCodes(); - - Map> getDouglasPoints(RegattaAndRaceIdentifier raceIdentifier, - Map from, Map to, double meters) throws NoWindException; - - Map> getManeuvers(RegattaAndRaceIdentifier raceIdentifier, - Map from, Map to) throws NoWindException; - - List getLeaderboardsByRaceAndRegatta(RaceDTO race, RegattaIdentifier regattaIdentifier); - - List getLeaderboardGroups(boolean withGeoLocationData); - - LeaderboardGroupDTO getLeaderboardGroupByName(String groupName, boolean withGeoLocationData); - - void renameLeaderboardGroup(String oldName, String newName); - - void removeLeaderboardGroups(Set groupNames); - - LeaderboardGroupDTO createLeaderboardGroup(String groupName, String description, - boolean displayGroupsInReverseOrder, int[] overallLeaderboardDiscardThresholds, - ScoringSchemeType overallLeaderboardScoringSchemeType); - - void updateLeaderboardGroup(String oldName, String newName, String description, List leaderboardNames, - int[] overallLeaderboardDiscardThresholds, ScoringSchemeType overallLeaderboardScoringSchemeType); - - CompetitorsRaceDataDTO getCompetitorsRaceData(RegattaAndRaceIdentifier race, List competitors, Date from, Date to, - long stepSizeInMs, DetailType detailType, String leaderboardGroupName, String leaderboardName) throws NoWindException; - - void setRaceIsKnownToStartUpwind(RegattaAndRaceIdentifier raceIdentifier, boolean raceIsKnownToStartUpwind); - - void setWindSourcesToExclude(RegattaAndRaceIdentifier raceIdentifier, Iterable windSourcesToExclude); - - ReplicationStateDTO getReplicaInfo(); - - void startReplicatingFromMaster(String messagingHost, String masterHost, String exchangeName, int servletPort, int messagingPort) throws Exception; - - void updateRaceDelayToLive(RegattaAndRaceIdentifier regattaAndRaceIdentifier, long delayToLiveInMs); - - void updateRacesDelayToLive(List regattaAndRaceIdentifiers, long delayToLiveInMs); - - void updateEvent(UUID eventId, String eventName, Date startDate, Date endDate, VenueDTO venue, boolean isPublic, - List regattaNames); - - EventDTO createEvent(String eventName, Date startDate, Date endDate, String venueName, boolean isPublic, List courseAreaNames); - - void removeEvent(UUID eventId); - - void removeEvents(Collection eventIds); - - void renameEvent(UUID eventId, String newName); - - EventDTO getEventByName(String eventName); - - EventDTO getEventById(UUID id); - - Iterable getScoreCorrectionProviderNames(); - - ScoreCorrectionProviderDTO getScoreCorrectionsOfProvider(String providerName) throws Exception; - - RegattaScoreCorrectionDTO getScoreCorrections(String scoreCorrectionProviderName, String eventName, String boatClassName, - Date timePointWhenResultPublished) throws Exception; - - void updateLeaderboardScoreCorrectionsAndMaxPointsReasons(BulkScoreCorrectionDTO updates) throws NoWindException; - - WindInfoForRaceDTO getWindSourcesInfo(RegattaAndRaceIdentifier raceIdentifier); - - List getResultImportUrls(String resultProviderName); - - void removeResultImportURLs(String resultProviderName, Set toRemove) throws Exception; - - void addResultImportUrl(String resultProviderName, String url) throws Exception; - - Void updateLeaderboardScoreCorrectionMetadata(String leaderboardName, Date timePointOfLastCorrectionValidity, - String comment); - - List getUrlResultProviderNames(); - - void updateRaceCourse(RegattaAndRaceIdentifier raceIdentifier, List> controlPoints); - - void addColumnsToLeaderboard(String leaderboardName, List> columnsToAdd); - - void removeLeaderboardColumns(String leaderboardName, List columnsToRemove); - - StrippedLeaderboardDTO getLeaderboard(String leaderboardName); - - void suppressCompetitorInLeaderboard(String leaderboardName, String competitorIdAsString, boolean suppressed); - - void updateLeaderboardColumnFactor(String leaderboardName, String columnName, Double newFactor); - - List listSwissTiminigReplayRaces(String swissTimingUrl); - - List, List>> getLeaderboardDataEntriesForAllRaceColumns(String leaderboardName, - Date date, DetailType detailType) throws Exception; - - List getOverallLeaderboardNamesContaining(String leaderboardName); - - List getPreviousSwissTimingArchiveConfigurations(); - - void storeSwissTimingArchiveConfiguration(String swissTimingUrl); - - PolarSheetGenerationResponse generatePolarSheetForRaces(List selectedRaces, - PolarSheetGenerationSettings settings, String name) throws Exception; - - CourseAreaDTO createCourseArea(UUID eventId, String courseAreaName); - - List> getLeaderboardsNamesOfMetaleaderboard(String metaLeaderboardName); - - Pair checkLeaderboardName(String leaderboardName); - - List getRegattaStructureForEvent(UUID eventId); - - List getRaceStateEntriesForRaceGroup(UUID eventId, List visibleCourseAreas, - List visibleRegattas, boolean showOnlyCurrentlyRunningRaces, boolean showOnlyRacesOfSameDay) - throws Exception; - - List getRaceStateEntriesForLeaderboard(String leaderboardName, - boolean showOnlyCurrentlyRunningRaces, boolean showOnlyRacesOfSameDay, List visibleRegattas) - throws Exception; - - String getBuildVersion(); - - void stopReplicatingFromMaster(); - - void stopAllReplicas(); - - void stopSingleReplicaInstance(String identifier); - - void reloadRaceLog(String leaderboardName, RaceColumnDTO raceColumnDTO, FleetDTO fleet); - - RaceLogDTO getRaceLog(String leaderboardName, RaceColumnDTO raceColumnDTO, FleetDTO fleet); - - List getLeaderboardGroupNamesFromRemoteServer(String host); - - UUID importMasterData(String host, String[] groupNames, boolean override, boolean compress, boolean exportWind); - - DataImportProgress getImportOperationProgress(UUID id); - - Iterable getCompetitors(); - - Iterable getCompetitorsOfLeaderboard(String leaderboardName); - - CompetitorDTO updateCompetitor(CompetitorDTO competitor); - - void allowCompetitorResetToDefaults(Iterable competitors); - - List getDeviceConfigurationMatchers(); - - DeviceConfigurationDTO getDeviceConfiguration(DeviceConfigurationMatcherDTO matcher); - - DeviceConfigurationMatcherDTO createOrUpdateDeviceConfiguration(DeviceConfigurationMatcherDTO matcherDTO, DeviceConfigurationDTO configurationDTO); - - boolean removeDeviceConfiguration(DeviceConfigurationMatcherType type, List clientIds); - - boolean setStartTimeAndProcedure(RaceLogSetStartTimeAndProcedureDTO dto); - - Triple getStartTimeAndProcedure(String leaderboardName, String raceColumnName, String fleetName); - - Iterable getAllIgtimiAccountEmailAddresses(); - - String getIgtimiAuthorizationUrl(); - - boolean authorizeAccessToIgtimiUser(String eMailAddress, String password) throws Exception; - - void removeIgtimiAccount(String eMailOfAccountToRemove); - - Map importWindFromIgtimi(List selectedRaces) throws Exception; -} +package com.sap.sailing.gwt.ui.client; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import com.google.gwt.user.client.rpc.RemoteService; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.common.DataImportProgress; +import com.sap.sailing.domain.common.DetailType; +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.common.MaxPointsReason; +import com.sap.sailing.domain.common.NoWindException; +import com.sap.sailing.domain.common.PassingInstruction; +import com.sap.sailing.domain.common.PolarSheetGenerationResponse; +import com.sap.sailing.domain.common.PolarSheetGenerationSettings; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.RegattaIdentifier; +import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.configuration.DeviceConfigurationMatcherType; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.domain.common.dto.FleetDTO; +import com.sap.sailing.domain.common.dto.IncrementalOrFullLeaderboardDTO; +import com.sap.sailing.domain.common.dto.PositionDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.domain.common.dto.RaceColumnInSeriesDTO; +import com.sap.sailing.domain.common.dto.RaceDTO; +import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.common.impl.Util.Triple; +import com.sap.sailing.domain.common.racelog.RacingProcedureType; +import com.sap.sailing.domain.common.racelog.tracking.NoCorrespondingServiceRegisteredException; +import com.sap.sailing.domain.common.racelog.tracking.NotDenotedForRaceLogTrackingException; +import com.sap.sailing.domain.common.racelog.tracking.NotRevokableException; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.Revokable; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; +import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapter; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.gwt.ui.shared.BulkScoreCorrectionDTO; +import com.sap.sailing.gwt.ui.shared.CompactRaceMapDataDTO; +import com.sap.sailing.gwt.ui.shared.CompetitorsRaceDataDTO; +import com.sap.sailing.gwt.ui.shared.ControlPointDTO; +import com.sap.sailing.gwt.ui.shared.CourseAreaDTO; +import com.sap.sailing.gwt.ui.shared.CoursePositionsDTO; +import com.sap.sailing.gwt.ui.shared.DeviceConfigurationDTO; +import com.sap.sailing.gwt.ui.shared.DeviceConfigurationDTO.RegattaConfigurationDTO; +import com.sap.sailing.gwt.ui.shared.DeviceConfigurationMatcherDTO; +import com.sap.sailing.gwt.ui.shared.DeviceMappingDTO; +import com.sap.sailing.gwt.ui.shared.EventDTO; +import com.sap.sailing.gwt.ui.shared.GPSFixDTO; +import com.sap.sailing.gwt.ui.shared.LeaderboardGroupDTO; +import com.sap.sailing.gwt.ui.shared.ManeuverDTO; +import com.sap.sailing.gwt.ui.shared.MarkDTO; +import com.sap.sailing.gwt.ui.shared.RaceCourseDTO; +import com.sap.sailing.gwt.ui.shared.RaceGroupDTO; +import com.sap.sailing.gwt.ui.shared.RaceLogDTO; +import com.sap.sailing.gwt.ui.shared.RaceLogSetStartTimeAndProcedureDTO; +import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; +import com.sap.sailing.gwt.ui.shared.RegattaDTO; +import com.sap.sailing.gwt.ui.shared.RegattaOverviewEntryDTO; +import com.sap.sailing.gwt.ui.shared.RegattaScoreCorrectionDTO; +import com.sap.sailing.gwt.ui.shared.ReplicationStateDTO; +import com.sap.sailing.gwt.ui.shared.ScoreCorrectionProviderDTO; +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.SwissTimingEventRecordDTO; +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.VenueDTO; +import com.sap.sailing.gwt.ui.shared.WindDTO; +import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; + +/** + * The client side stub for the RPC service. Usually, when a null date is passed to + * the time-dependent service methods, an empty (non-null) result is returned. + */ +public interface SailingService extends RemoteService { + List getPreviousTracTracConfigurations() throws Exception; + + List getRegattas(); + + List getEvents(); + + Pair> listTracTracRacesInEvent(String eventJsonURL, boolean listHiddenRaces) throws Exception; + + void trackWithTracTrac(RegattaIdentifier regattaToAddTo, Iterable rrs, String liveURI, + String storedURI, String courseDesignUpdateURI, boolean trackWind, boolean correctWindByDeclination, boolean simulateWithStartTimeNow, String tracTracUsername, String tracTracPassword) throws Exception; + + void trackWithSwissTiming(RegattaIdentifier regattaToAddTo, Iterable rrs, String hostname, int port, + boolean trackWind, boolean correctWindByDeclination) throws Exception; + + void replaySwissTimingRace(RegattaIdentifier regattaIdentifier, Iterable replayRaces, + boolean trackWind, boolean correctWindByDeclination, boolean simulateWithStartTimeNow); + + void storeTracTracConfiguration(String name, String jsonURL, String liveDataURI, String storedDataURI, String courseDesignUpdateURI, String tracTracUsername, String tracTracPassword) throws Exception; + + void stopTrackingEvent(RegattaIdentifier eventIdentifier) throws Exception; + + void stopTrackingRaces(Iterable racesToStopTracking) throws Exception; + + void removeAndUntrackRaces(Iterable regattaNamesAndRaceNames); + + WindInfoForRaceDTO getRawWindFixes(RegattaAndRaceIdentifier raceIdentifier, Collection windSources); + + void setWind(RegattaAndRaceIdentifier raceIdentifier, WindDTO wind); + + WindInfoForRaceDTO getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, long millisecondsStepWidth, + int numberOfFixes, Collection windSourceTypeNames, boolean onlyUpToNewestEvent) throws NoWindException; + + WindInfoForRaceDTO getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, Date to, long resolutionInMilliseconds, + Collection windSourceTypeNames, boolean onlyUpToNewestEvent); + + WindInfoForRaceDTO getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, long millisecondsStepWidth, + int numberOfFixes, double latDeg, double lngDeg, Collection windSources) throws NoWindException; + + CompactRaceMapDataDTO getRaceMapData(RegattaAndRaceIdentifier raceIdentifier, Date date, Map fromPerCompetitorIdAsString, + Map toPerCompetitorIdAsString, boolean extrapolate) throws NoWindException; + + RaceTimesInfoDTO getRaceTimesInfo(RegattaAndRaceIdentifier raceIdentifier); + + List getRaceTimesInfos(Collection raceIdentifiers); + + CoursePositionsDTO getCoursePositions(RegattaAndRaceIdentifier raceIdentifier, Date date); + + RaceCourseDTO getRaceCourse(RegattaAndRaceIdentifier raceIdentifier, Date date); + + void removeWind(RegattaAndRaceIdentifier raceIdentifier, WindDTO windDTO); + + public List getLeaderboardNames(); + + IncrementalOrFullLeaderboardDTO getLeaderboardByName(String leaderboardName, Date date, + Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, + String previousLeaderboardId) throws Exception; + + List getLeaderboards(); + + List getLeaderboardsByRegatta(RegattaDTO regatta); + + List getLeaderboardsByEvent(EventDTO event); + + StrippedLeaderboardDTO updateLeaderboard(String leaderboardName, String newLeaderboardName, String newLeaderboardDisplayName, int[] newDiscardingThreasholds, UUID newCourseAreaId); + + StrippedLeaderboardDTO createFlexibleLeaderboard(String leaderboardName, String leaderboardDisplayName, int[] discardThresholds, ScoringSchemeType scoringSchemeType, UUID courseAreaId); + + StrippedLeaderboardDTO createRegattaLeaderboard(RegattaIdentifier regattaIdentifier, String leaderboardDisplayName, int[] discardThresholds); + + void removeLeaderboard(String leaderboardName); + + void removeLeaderboards(Collection leaderboardNames); + + void renameLeaderboard(String leaderboardName, String newLeaderboardName); + + void renameLeaderboardColumn(String leaderboardName, String oldColumnName, String newColumnName); + + void removeLeaderboardColumn(String leaderboardName, String columnName); + + void addColumnToLeaderboard(String columnName, String leaderboardName, boolean medalRace); + + void moveLeaderboardColumnUp(String leaderboardName, String columnName); + + void moveLeaderboardColumnDown(String leaderboardName, String columnName); + + RegattaDTO createRegatta(String regattaName, String boatClassName, + RegattaCreationParametersDTO seriesNamesWithFleetNamesAndFleetOrderingAndMedal, boolean persistent, + ScoringSchemeType scoringSchemeType, UUID defaultCourseAreaId); + + void removeRegatta(RegattaIdentifier regattaIdentifier); + + void removeSeries(RegattaIdentifier regattaIdentifier, String seriesName); + + void removeRegattas(Collection regattas); + + void updateRegatta(RegattaIdentifier regattaIdentifier, UUID defaultCourseAreaUuid, RegattaConfigurationDTO regattaConfiguration); + + List addRaceColumnsToSeries(RegattaIdentifier regattaIdentifier, String seriesName, List columnNames); + + void updateSeries(RegattaIdentifier regattaIdentifier, String seriesName, String newSeriesName, boolean isMedal, + int[] resultDiscardingThresholds, boolean startsWithZeroScore, + boolean firstRaceIsNonDiscardableCarryForward, boolean hasSplitFleetScore, List fleets); + + RaceColumnInSeriesDTO addRaceColumnToSeries(RegattaIdentifier regattaIdentifier, String seriesName, String columnName); + + void removeRaceColumnsFromSeries(RegattaIdentifier regattaIdentifier, String seriesName, List columnNames); + + void removeRaceColumnFromSeries(RegattaIdentifier regattaIdentifier, String seriesName, String columnName); + + void moveRaceColumnInSeriesUp(RegattaIdentifier regattaIdentifier, String seriesName, String columnName); + + void moveRaceColumnInSeriesDown(RegattaIdentifier regattaIdentifier, String seriesName, String columnName); + + boolean connectTrackedRaceToLeaderboardColumn(String leaderboardName, String raceColumnName, + String fleetName, RegattaAndRaceIdentifier raceIdentifier); + + void disconnectLeaderboardColumnFromTrackedRace(String leaderboardName, String raceColumnName, String fleetName); + + Map getRegattaAndRaceNameOfTrackedRaceConnectedToLeaderboardColumn(String leaderboardName, String raceColumnName); + + void updateLeaderboardCarryValue(String leaderboardName, String competitorIdAsString, Double carriedPoints); + + /** + * @return the new net points in {@link Pair#getA()} and the new total points in {@link Pair#getB()} for time point + * date after the max points reason has been updated to maxPointsReasonAsString. + */ + Triple updateLeaderboardMaxPointsReason(String leaderboardName, String competitorIdAsString, + String raceColumnName, MaxPointsReason maxPointsReason, Date date) throws NoWindException; + + Triple updateLeaderboardScoreCorrection(String leaderboardName, String competitorIdAsString, + String columnName, Double correctedScore, Date date) throws NoWindException; + + void updateCompetitorDisplayNameInLeaderboard(String leaderboardName, String competitorIdAsString, String displayName); + + void updateIsMedalRace(String leaderboardName, String columnName, boolean isMedalRace); + + List getPreviousSwissTimingConfigurations(); + + SwissTimingEventRecordDTO getRacesOfSwissTimingEvent(String eventJsonURL) throws Exception; + + void storeSwissTimingConfiguration(String configName, String jsonURL, String hostname, int port); + + String[] getCountryCodes(); + + Map> getDouglasPoints(RegattaAndRaceIdentifier raceIdentifier, + Map from, Map to, double meters) throws NoWindException; + + Map> getManeuvers(RegattaAndRaceIdentifier raceIdentifier, + Map from, Map to) throws NoWindException; + + List getLeaderboardsByRaceAndRegatta(RaceDTO race, RegattaIdentifier regattaIdentifier); + + List getLeaderboardGroups(boolean withGeoLocationData); + + LeaderboardGroupDTO getLeaderboardGroupByName(String groupName, boolean withGeoLocationData); + + void renameLeaderboardGroup(String oldName, String newName); + + void removeLeaderboardGroups(Set groupNames); + + LeaderboardGroupDTO createLeaderboardGroup(String groupName, String description, + boolean displayGroupsInReverseOrder, int[] overallLeaderboardDiscardThresholds, + ScoringSchemeType overallLeaderboardScoringSchemeType); + + void updateLeaderboardGroup(String oldName, String newName, String description, List leaderboardNames, + int[] overallLeaderboardDiscardThresholds, ScoringSchemeType overallLeaderboardScoringSchemeType); + + CompetitorsRaceDataDTO getCompetitorsRaceData(RegattaAndRaceIdentifier race, List competitors, Date from, Date to, + long stepSizeInMs, DetailType detailType, String leaderboardGroupName, String leaderboardName) throws NoWindException; + + void setRaceIsKnownToStartUpwind(RegattaAndRaceIdentifier raceIdentifier, boolean raceIsKnownToStartUpwind); + + void setWindSourcesToExclude(RegattaAndRaceIdentifier raceIdentifier, Iterable windSourcesToExclude); + + ReplicationStateDTO getReplicaInfo(); + + void startReplicatingFromMaster(String messagingHost, String masterHost, String exchangeName, int servletPort, int messagingPort) throws Exception; + + void updateRaceDelayToLive(RegattaAndRaceIdentifier regattaAndRaceIdentifier, long delayToLiveInMs); + + void updateRacesDelayToLive(List regattaAndRaceIdentifiers, long delayToLiveInMs); + + void updateEvent(UUID eventId, String eventName, Date startDate, Date endDate, VenueDTO venue, boolean isPublic, + List regattaNames); + + EventDTO createEvent(String eventName, Date startDate, Date endDate, String venueName, boolean isPublic, List courseAreaNames); + + void removeEvent(UUID eventId); + + void removeEvents(Collection eventIds); + + void renameEvent(UUID eventId, String newName); + + EventDTO getEventByName(String eventName); + + EventDTO getEventById(UUID id); + + Iterable getScoreCorrectionProviderNames(); + + ScoreCorrectionProviderDTO getScoreCorrectionsOfProvider(String providerName) throws Exception; + + RegattaScoreCorrectionDTO getScoreCorrections(String scoreCorrectionProviderName, String eventName, String boatClassName, + Date timePointWhenResultPublished) throws Exception; + + void updateLeaderboardScoreCorrectionsAndMaxPointsReasons(BulkScoreCorrectionDTO updates) throws NoWindException; + + WindInfoForRaceDTO getWindSourcesInfo(RegattaAndRaceIdentifier raceIdentifier); + + List getResultImportUrls(String resultProviderName); + + void removeResultImportURLs(String resultProviderName, Set toRemove) throws Exception; + + void addResultImportUrl(String resultProviderName, String url) throws Exception; + + Void updateLeaderboardScoreCorrectionMetadata(String leaderboardName, Date timePointOfLastCorrectionValidity, + String comment); + + List getUrlResultProviderNames(); + + void updateRaceCourse(RegattaAndRaceIdentifier raceIdentifier, List> controlPoints); + + void addColumnsToLeaderboard(String leaderboardName, List> columnsToAdd); + + void removeLeaderboardColumns(String leaderboardName, List columnsToRemove); + + StrippedLeaderboardDTO getLeaderboard(String leaderboardName); + + void suppressCompetitorInLeaderboard(String leaderboardName, String competitorIdAsString, boolean suppressed); + + void updateLeaderboardColumnFactor(String leaderboardName, String columnName, Double newFactor); + + List listSwissTiminigReplayRaces(String swissTimingUrl); + + List, List>> getLeaderboardDataEntriesForAllRaceColumns(String leaderboardName, + Date date, DetailType detailType) throws Exception; + + List getOverallLeaderboardNamesContaining(String leaderboardName); + + List getPreviousSwissTimingArchiveConfigurations(); + + void storeSwissTimingArchiveConfiguration(String swissTimingUrl); + + PolarSheetGenerationResponse generatePolarSheetForRaces(List selectedRaces, + PolarSheetGenerationSettings settings, String name) throws Exception; + + CourseAreaDTO createCourseArea(UUID eventId, String courseAreaName); + + List> getLeaderboardsNamesOfMetaleaderboard(String metaLeaderboardName); + + Pair checkLeaderboardName(String leaderboardName); + + List getRegattaStructureForEvent(UUID eventId); + + List getRaceStateEntriesForRaceGroup(UUID eventId, List visibleCourseAreas, + List visibleRegattas, boolean showOnlyCurrentlyRunningRaces, boolean showOnlyRacesOfSameDay) + throws Exception; + + List getRaceStateEntriesForLeaderboard(String leaderboardName, + boolean showOnlyCurrentlyRunningRaces, boolean showOnlyRacesOfSameDay, List visibleRegattas) + throws Exception; + + String getBuildVersion(); + + void stopReplicatingFromMaster(); + + void stopAllReplicas(); + + void stopSingleReplicaInstance(String identifier); + + void reloadRaceLog(String leaderboardName, RaceColumnDTO raceColumnDTO, FleetDTO fleet); + + RaceLogDTO getRaceLog(String leaderboardName, RaceColumnDTO raceColumnDTO, FleetDTO fleet); + + List getLeaderboardGroupNamesFromRemoteServer(String host); + + UUID importMasterData(String host, String[] groupNames, boolean override, boolean compress, boolean exportWind); + + DataImportProgress getImportOperationProgress(UUID id); + + Iterable getCompetitors(); + + /** + * + * @param leaderboardName + * @param lookInRaceLogs If set to {@code true}, the {@link RaceLog}s are checked for the competitor registrations. + * If set to {@code false}, the {@link RaceDefinition}s are checked instead. + * @return + */ + Iterable getCompetitorsOfLeaderboard(String leaderboardName, boolean lookInRaceLogs); + + CompetitorDTO addOrUpdateCompetitor(CompetitorDTO competitor); + + void allowCompetitorResetToDefaults(Iterable competitors); + + List getDeviceConfigurationMatchers(); + + DeviceConfigurationDTO getDeviceConfiguration(DeviceConfigurationMatcherDTO matcher); + + DeviceConfigurationMatcherDTO createOrUpdateDeviceConfiguration(DeviceConfigurationMatcherDTO matcherDTO, DeviceConfigurationDTO configurationDTO); + + boolean removeDeviceConfiguration(DeviceConfigurationMatcherType type, List clientIds); + + boolean setStartTimeAndProcedure(RaceLogSetStartTimeAndProcedureDTO dto); + + Triple getStartTimeAndProcedure(String leaderboardName, String raceColumnName, String fleetName); + + Iterable getAllIgtimiAccountEmailAddresses(); + + String getIgtimiAuthorizationUrl(); + + boolean authorizeAccessToIgtimiUser(String eMailAddress, String password) throws Exception; + + void removeIgtimiAccount(String eMailOfAccountToRemove); + + Map importWindFromIgtimi(List selectedRaces) throws Exception; + + void denoteForRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName) throws Exception; + + /** + * Revoke the {@link DenoteForTrackingEvent}. This does not affect an existing {@code RaceLogRaceTracker} + * or {@link TrackedRace} for this {@code RaceLog}. + * + * @see RaceLogTrackingAdapter#removeDenotationForRaceLogTracking + */ + void removeDenotationForRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName); + + void denoteForRaceLogTracking(String leaderboardName) throws Exception; + + /** + * Performs all the necessary steps to start tracking the race. + * The {@code RaceLog} needs to be denoted for racelog-tracking beforehand. + * + * @see RaceLogTrackingAdapter#startTracking + */ + void startRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName) + throws NotDenotedForRaceLogTrackingException, Exception; + + void setCompetitorRegistrations(String leaderboardName, String raceColumnName, String fleetName, Set competitors); + + Collection getCompetitorRegistrations(String leaderboardName, String raceColumnName, String fleetName); + + void addMarkToRaceLog(String leaderboardName, String raceColumnName, String fleetName, MarkDTO markDTO); + + Collection getMarksInRaceLog(String leaderboardName, String raceColumnName, String fleetName); + + /** + * Adds the course definition to the racelog, while trying to reuse existing marks, controlpoints and waypoints + * from the previous course definition in the racelog. + */ + void addCourseDefinitionToRaceLog(String leaderboardName, String raceColumnName, String fleetName, List> course); + + RaceCourseDTO getLastCourseDefinitionInRaceLog(String leaderboardName, String raceColumnName, String fleetName); + + /** + * Adds a fix to the {@link GPSFixStore}, and creates a mapping with a virtual device for exactly the current timepoint. + */ + void pingMarkViaRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName, MarkDTO mark, PositionDTO position); + + void copyCourseAndCompetitorsToOtherRaceLogs(Triple raceLogFrom, + Set> raceLogsTo); + + void addDeviceMappingToRaceLog(String leaderboardName, String raceColumnName, String fleetName, DeviceMappingDTO mapping) + throws TransformationException; + + List getDeviceMappingsFromRaceLog(String leaderboardName, String raceColumnName, String fleetName) + throws TransformationException; + + List getDeserializableDeviceIdentifierTypes(); + + void closeOpenEndedDeviceMapping(String leaderboardName, String raceColumnName, String fleetName, DeviceMappingDTO mapping, + Date closingTimePoint) throws NoCorrespondingServiceRegisteredException, TransformationException; + + /** + * Revoke the events in the {@code RaceLog} that are identified by the {@code eventIds}. + * This only affects such events that implement {@link Revokable}. + */ + void revokeRaceLogEvents(String leaderboardName, String raceColumnName, String fleetName, List eventIds) + throws NotRevokableException; +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java index c4ae433c7c7..51b59a4726f 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java @@ -1,526 +1,584 @@ -package com.sap.sailing.gwt.ui.client; - -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.sap.sailing.domain.common.DataImportProgress; -import com.sap.sailing.domain.common.DetailType; -import com.sap.sailing.domain.common.LeaderboardType; -import com.sap.sailing.domain.common.MaxPointsReason; -import com.sap.sailing.domain.common.PassingInstruction; -import com.sap.sailing.domain.common.PolarSheetGenerationResponse; -import com.sap.sailing.domain.common.PolarSheetGenerationSettings; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.RegattaIdentifier; -import com.sap.sailing.domain.common.RegattaNameAndRaceName; -import com.sap.sailing.domain.common.ScoringSchemeType; -import com.sap.sailing.domain.common.WindSource; -import com.sap.sailing.domain.common.configuration.DeviceConfigurationMatcherType; -import com.sap.sailing.domain.common.dto.CompetitorDTO; -import com.sap.sailing.domain.common.dto.FleetDTO; -import com.sap.sailing.domain.common.dto.IncrementalOrFullLeaderboardDTO; -import com.sap.sailing.domain.common.dto.RaceColumnDTO; -import com.sap.sailing.domain.common.dto.RaceColumnInSeriesDTO; -import com.sap.sailing.domain.common.dto.RaceDTO; -import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; -import com.sap.sailing.domain.common.impl.Util.Pair; -import com.sap.sailing.domain.common.impl.Util.Triple; -import com.sap.sailing.domain.common.racelog.RacingProcedureType; -import com.sap.sailing.gwt.ui.shared.BulkScoreCorrectionDTO; -import com.sap.sailing.gwt.ui.shared.CompactRaceMapDataDTO; -import com.sap.sailing.gwt.ui.shared.CompetitorsRaceDataDTO; -import com.sap.sailing.gwt.ui.shared.ControlPointDTO; -import com.sap.sailing.gwt.ui.shared.CourseAreaDTO; -import com.sap.sailing.gwt.ui.shared.CoursePositionsDTO; -import com.sap.sailing.gwt.ui.shared.DeviceConfigurationDTO; -import com.sap.sailing.gwt.ui.shared.DeviceConfigurationDTO.RegattaConfigurationDTO; -import com.sap.sailing.gwt.ui.shared.DeviceConfigurationMatcherDTO; -import com.sap.sailing.gwt.ui.shared.EventDTO; -import com.sap.sailing.gwt.ui.shared.GPSFixDTO; -import com.sap.sailing.gwt.ui.shared.LeaderboardGroupDTO; -import com.sap.sailing.gwt.ui.shared.ManeuverDTO; -import com.sap.sailing.gwt.ui.shared.RaceCourseDTO; -import com.sap.sailing.gwt.ui.shared.RaceGroupDTO; -import com.sap.sailing.gwt.ui.shared.RaceLogDTO; -import com.sap.sailing.gwt.ui.shared.RaceLogSetStartTimeAndProcedureDTO; -import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; -import com.sap.sailing.gwt.ui.shared.RegattaDTO; -import com.sap.sailing.gwt.ui.shared.RegattaOverviewEntryDTO; -import com.sap.sailing.gwt.ui.shared.RegattaScoreCorrectionDTO; -import com.sap.sailing.gwt.ui.shared.ReplicationStateDTO; -import com.sap.sailing.gwt.ui.shared.ScoreCorrectionProviderDTO; -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.SwissTimingEventRecordDTO; -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.VenueDTO; -import com.sap.sailing.gwt.ui.shared.WindDTO; -import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; - -/** - * The async counterpart of {@link SailingService} - */ -public interface SailingServiceAsync { - - void getRegattas(AsyncCallback> callback); - - /** - * The string returned in the callback's pair is the common event name - * @param listHiddenRaces - */ - void listTracTracRacesInEvent(String eventJsonURL, boolean listHiddenRaces, AsyncCallback>> callback); - - /** - * @param regattaToAddTo - * if null, an existing regatta by the name of the TracTrac event with the boat class name - * appended in parentheses will be looked up; if not found, a default regatta with that name will be - * created, with a single default series and a single default fleet. If a valid {@link RegattaIdentifier} - * is specified, a regatta lookup is performed with that identifier; if the regatta is found, it is used - * to add the races to. Otherwise, a default regatta as described above will be created and used. - * @param liveURI - * may be null or the empty string in which case the server will use the - * {@link TracTracRaceRecordDTO#liveURI} from the rr race record. - * @param simulateWithStartTimeNow - * if true, the connector will adjust the time stamps of all events received such that the - * first mark passing for the first waypoint will be set to "now." It will delay the forwarding of all - * events received such that they seem to be sent in "real-time." So, more or less the time points attached - * to the events sent to the receivers will again approximate the wall time. - * @param storedURImay - * be null or the empty string in which case the server will use the - * {@link TracTracRaceRecordDTO#storedURI} from the rr race record. - */ - void trackWithTracTrac(RegattaIdentifier regattaToAddTo, - Iterable rrs, String liveURI, String storedURI, String courseDesignUpdateURI, boolean trackWind, boolean correctWindByDeclination, - boolean simulateWithStartTimeNow, String tracTracUsername, String tracTracPassword, AsyncCallback callback); - - void trackWithSwissTiming(RegattaIdentifier regattaToAddTo, Iterable rrs, - String hostname, int port, boolean trackWind, boolean correctWindByDeclination, - AsyncCallback asyncCallback); - - void replaySwissTimingRace(RegattaIdentifier regattaIdentifier, Iterable replayRaces, - boolean trackWind, boolean correctWindByDeclination, boolean simulateWithStartTimeNow, - AsyncCallback asyncCallback); - - void getPreviousTracTracConfigurations(AsyncCallback> callback); - - void storeTracTracConfiguration(String name, String jsonURL, String liveDataURI, String storedDataURI, String courseDesignUpdateURI, String tracTracUsername, String tracTracPassword, - AsyncCallback callback); - - void stopTrackingEvent(RegattaIdentifier eventIdentifier, AsyncCallback callback); - - void stopTrackingRaces(Iterable racesToStopTracking, AsyncCallback asyncCallback); - - /** - * Untracks the race and removes it from the regatta. It will also be removed in all leaderboards - * @param regattaNamesAndRaceNames The identifier for the regatta name, and the race name to remove - */ - void removeAndUntrackRaces(Iterable regattaNamesAndRaceNames, AsyncCallback callback); - - void getRawWindFixes(RegattaAndRaceIdentifier raceIdentifier, Collection windSources, AsyncCallback callback); - - /** - * @param from if null, the tracked race's start of tracking is used - * @param to if null, the tracked race's time point of newest event is used - */ - void getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, Date to, long resolutionInMilliseconds, - Collection windSourceTypeNames, AsyncCallback callback); - - /** - * @param windSourceTypeNames - * if null, data from all available wind sources will be returned, otherwise only from those - * whose {@link WindSource} name is contained in the windSources collection. - */ - void getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, long millisecondsStepWidth, int numberOfFixes, - double latDeg, double lngDeg, Collection windSourceTypeNames, - AsyncCallback callback); - - /** - * Same as {@link #getWindInfo(RegattaAndRaceIdentifier, Date, long, int, double, double, Collection, AsyncCallback)}, only - * that the wind is not requested for a specific position, but instead the wind sources associated with the tracked - * race identified by raceIdentifier are requested to deliver their original position. This will in - * particular preserve the positions of actual measurements and will deliver the averaged positions for averaged / - * combined wind read-outs. - * - * @param from - * must not be null - * @param numberOfFixes - * no matter how great this value is chosen, never returns data beyond the newest event recorded in the - * race - */ - void getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, long millisecondsStepWidth, int numberOfFixes, - Collection windSourceTypeNames, AsyncCallback callback); - - void setWind(RegattaAndRaceIdentifier raceIdentifier, WindDTO wind, AsyncCallback callback); - - void removeWind(RegattaAndRaceIdentifier raceIdentifier, WindDTO windDTO, AsyncCallback callback); - - void getRaceTimesInfo(RegattaAndRaceIdentifier raceIdentifier, AsyncCallback callback); - - void getRaceTimesInfos(Collection raceIdentifiers, AsyncCallback> callback); - - void getCoursePositions(RegattaAndRaceIdentifier raceIdentifier, Date date, AsyncCallback asyncCallback); - - void getLeaderboardByName(String leaderboardName, Date date, - Collection namesOfRaceColumnsForWhichToLoadLegDetails, - String previousLeaderboardId, AsyncCallback callback); - - void getLeaderboardNames(AsyncCallback> callback); - - void getLeaderboards(AsyncCallback> callback); - - void getLeaderboardsByRegatta(RegattaDTO regatta, AsyncCallback> callback); - - void getLeaderboardsByRaceAndRegatta(RaceDTO race, RegattaIdentifier regattaIdentifier, - AsyncCallback> callback); - - void updateLeaderboard(String leaderboardName, String newLeaderboardName, String newLeaderboardDisplayName, - int[] newDiscardingThreasholds, UUID newCourseAreaId, AsyncCallback callback); - - void createFlexibleLeaderboard(String leaderboardName, String leaderboardDisplayName, int[] discardThresholds, ScoringSchemeType scoringSchemeType, UUID courseAreaId, - AsyncCallback asyncCallback); - - void createRegattaLeaderboard(RegattaIdentifier regattaIdentifier, String leaderboardDisplayName, int[] discardThresholds, - AsyncCallback asyncCallback); - - void removeLeaderboard(String leaderboardName, AsyncCallback asyncCallback); - - void removeLeaderboards(Collection leaderboardNames, AsyncCallback asyncCallback); - - void renameLeaderboard(String leaderboardName, String newLeaderboardName, AsyncCallback asyncCallback); - - void addColumnToLeaderboard(String columnName, String leaderboardName, boolean medalRace, - AsyncCallback callback); - - void renameLeaderboardColumn(String leaderboardName, String oldColumnName, String newColumnName, AsyncCallback callback); - - void removeLeaderboardColumn(String leaderboardName, String columnName, AsyncCallback callback); - - void connectTrackedRaceToLeaderboardColumn(String leaderboardName, String raceColumnName, String fleetName, - RegattaAndRaceIdentifier raceIdentifier, AsyncCallback asyncCallback); - - /** - * The key set of the map returned contains all fleets of the race column identified by the combination of - * leaderboardName and raceColumnName. If a value is null, there is no - * tracked race currently linked to the fleet in the race column; otherwise, the value is the {@link RaceIdentifier} - * of the tracked race currently connected for the fleet whose name is the key. The map returned is never null. - */ - void getRegattaAndRaceNameOfTrackedRaceConnectedToLeaderboardColumn(String leaderboardName, String raceColumnName, - AsyncCallback> callback); - - void disconnectLeaderboardColumnFromTrackedRace(String leaderboardName, String raceColumnName, String fleetName, - AsyncCallback callback); - - void updateLeaderboardCarryValue(String leaderboardName, String competitorIdAsString, Double carriedPoints, AsyncCallback callback); - - void updateLeaderboardMaxPointsReason(String leaderboardName, String competitorIdAsString, String raceColumnName, - MaxPointsReason maxPointsReason, Date date, AsyncCallback> asyncCallback); - - void updateLeaderboardScoreCorrection(String leaderboardName, String competitorIdAsString, String columnName, - Double correctedScore, Date date, AsyncCallback> asyncCallback); - - void updateLeaderboardScoreCorrectionMetadata(String leaderboardName, Date timePointOfLastCorrectionValidity, - String comment, AsyncCallback callback); - - void updateLeaderboardScoreCorrectionsAndMaxPointsReasons(BulkScoreCorrectionDTO updates, - AsyncCallback callback); - - void updateCompetitorDisplayNameInLeaderboard(String leaderboardName, String competitorID, String displayName, - AsyncCallback callback); - - void moveLeaderboardColumnUp(String leaderboardName, String columnName, AsyncCallback callback); - - void moveLeaderboardColumnDown(String leaderboardName, String columnName, AsyncCallback callback); - - void updateIsMedalRace(String leaderboardName, String columnName, boolean isMedalRace, AsyncCallback callback); - - void updateRaceDelayToLive(RegattaAndRaceIdentifier regattaAndRaceIdentifier, long delayToLiveInMs, AsyncCallback callback); - - void updateRacesDelayToLive(List regattaAndRaceIdentifiers, long delayToLiveInMs, AsyncCallback callback); - - void getPreviousSwissTimingConfigurations(AsyncCallback> asyncCallback); - - void getRacesOfSwissTimingEvent(String eventJsonUrl, AsyncCallback asyncCallback); - - void storeSwissTimingConfiguration(String configName, String jsonURL, String hostname, int port, AsyncCallback asyncCallback); - - void getCountryCodes(AsyncCallback callback); - - void getDouglasPoints(RegattaAndRaceIdentifier raceIdentifier, Map from, Map to, - double meters, AsyncCallback>> callback); - - void getManeuvers(RegattaAndRaceIdentifier raceIdentifier, Map from, Map to, - AsyncCallback>> callback); - - void getLeaderboardGroups(boolean withGeoLocationData, AsyncCallback> callback); - - void getLeaderboardGroupByName(String groupName, boolean withGeoLocationData, - AsyncCallback callback); - - /** - * Renames the group with the name oldName to the newName.
- * If there's no group with the name oldName or there's already a group with the name - * newName a {@link IllegalArgumentException} is thrown. - */ - void renameLeaderboardGroup(String oldName, String newName, AsyncCallback callback); - - /** - * Removes the leaderboard groups with the given names from the service and the persistant store. - */ - void removeLeaderboardGroups(Set groupNames, AsyncCallback asyncCallback); - - /** - * Creates a new group with the name groupname, the description description and an empty list of leaderboards.
- * @param displayGroupsInReverseOrder TODO - */ - void createLeaderboardGroup(String groupName, String description, - boolean displayGroupsInReverseOrder, int[] overallLeaderboardDiscardThresholds, - ScoringSchemeType overallLeaderboardScoringSchemeType, AsyncCallback callback); - - /** - * Updates the data of the group with the name oldName. - * - * @param oldName The old name of the group - * @param newName The new name of the group - * @param description The new description of the group - * @param leaderboardNames The list of names of the new leaderboards of the group - */ - void updateLeaderboardGroup(String oldName, String newName, String description, - List leaderboardNames, int[] overallLeaderboardDiscardThresholds, ScoringSchemeType overallLeaderboardScoringSchemeType, AsyncCallback callback); - - - void setRaceIsKnownToStartUpwind(RegattaAndRaceIdentifier raceIdentifier, boolean raceIsKnownToStartUpwind, - AsyncCallback callback); - - void setWindSourcesToExclude(RegattaAndRaceIdentifier raceIdentifier, Iterable windSourcesToExclude, - AsyncCallback callback); - - void getRaceMapData(RegattaAndRaceIdentifier raceIdentifier, Date date, Map fromPerCompetitorIdAsString, - Map toPerCompetitorIdAsString, boolean extrapolate, AsyncCallback callback); - - void getReplicaInfo(AsyncCallback callback); - - void startReplicatingFromMaster(String messagingHost, String masterName, String exchangeName, int servletPort, int messagingPort, - AsyncCallback callback); - - void getEvents(AsyncCallback> callback); - - /** - * Creates a {@link EventDTO} for the {@link com.sap.sailing.domain.base.Event} with the name eventName, which contains the - * name, the description and a list with {@link RegattaDTO RegattaDTOs} contained in the event.
- * If no event with the name eventName is known, an {@link IllegalArgumentException} is thrown. - */ - void getEventByName(String eventName, AsyncCallback callback); - - /** - * Renames the event with the name oldName to the newName.
- * If there's no event with the name oldName or there's already a event with the name - * newName a {@link IllegalArgumentException} is thrown. - */ - void renameEvent(UUID eventId, String newName, AsyncCallback callback); - - /** - * Removes the event with the id id from the service and the persistence store. - */ - void removeEvent(UUID eventId, AsyncCallback callback); - - void removeEvents(Collection eventIds, AsyncCallback asyncCallback); - - void createEvent(String eventName, Date startDate, Date endDate, String description, boolean isPublic, List courseAreaNames, AsyncCallback callback); - - void updateEvent(UUID eventId, String eventName, Date startDate, Date endDate, VenueDTO venue, boolean isPublic, - List regattaNames, AsyncCallback callback); - - void createCourseArea(UUID eventId, String courseAreaName, AsyncCallback callback); - - void removeRegatta(RegattaIdentifier regattaIdentifier, AsyncCallback callback); - - void removeRegattas(Collection regattas, AsyncCallback asyncCallback); - - void addRaceColumnToSeries(RegattaIdentifier regattaIdentifier, String seriesName, String columnName, - AsyncCallback callback); - - void removeRaceColumnFromSeries(RegattaIdentifier regattaIdentifier, String seriesName, String columnName, - AsyncCallback callback); - - void moveRaceColumnInSeriesUp(RegattaIdentifier regattaIdentifier, String seriesName, String columnName, - AsyncCallback callback); - - void moveRaceColumnInSeriesDown(RegattaIdentifier regattaIdentifier, String seriesName, String columnName, - AsyncCallback callback); - - void createRegatta(String regattaName, String boatClassName, - RegattaCreationParametersDTO seriesNamesWithFleetNamesAndFleetOrderingAndMedal, boolean persistent, - ScoringSchemeType scoringSchemeType, UUID defaultCourseAreaId, AsyncCallback callback); - - void addRaceColumnsToSeries(RegattaIdentifier regattaIdentifier, String seriesName, List columnNames, - AsyncCallback> callback); - - void updateSeries(RegattaIdentifier regattaIdentifier, String seriesName, String newSeriesName, boolean isMedal, - int[] resultDiscardingThresholds, boolean startsWithZeroScore, - boolean firstRaceIsNonDiscardableCarryForward, boolean hasSplitFleetScore, List fleets, - AsyncCallback callback); - - void removeRaceColumnsFromSeries(RegattaIdentifier regattaIdentifier, String seriesName, List columnNames, - AsyncCallback callback); - - void getScoreCorrectionProviderNames(AsyncCallback> callback); - - void getScoreCorrectionsOfProvider(String providerName, AsyncCallback callback); - - void getScoreCorrections(String scoreCorrectionProviderName, String eventName, String boatClassName, - Date timePointWhenResultPublished, AsyncCallback asyncCallback); - - void getWindSourcesInfo(RegattaAndRaceIdentifier raceIdentifier, AsyncCallback callback); - - void getRaceCourse(RegattaAndRaceIdentifier raceIdentifier, Date date, AsyncCallback callback); - - void updateRaceCourse(RegattaAndRaceIdentifier raceIdentifier, List> controlPoints, AsyncCallback callback); - - void getResultImportUrls(String resultProviderName, AsyncCallback> callback); - - void removeResultImportURLs(String resultProviderName, Set toRemove, AsyncCallback callback); - - void addResultImportUrl(String resultProviderName, String url, AsyncCallback callback); - - void getUrlResultProviderNames(AsyncCallback> callback); - - void addColumnsToLeaderboard(String leaderboardName, List> columnsToAdd, - AsyncCallback callback); - - void removeLeaderboardColumns(String leaderboardName, List columnsToRemove, AsyncCallback callback); - - void getLeaderboard(String leaderboardName, AsyncCallback callback); - - void suppressCompetitorInLeaderboard(String leaderboardName, String competitorIdAsString, boolean suppressed, AsyncCallback asyncCallback); - - void updateLeaderboardColumnFactor(String leaderboardName, String columnName, Double newFactor, - AsyncCallback callback); - - void listSwissTiminigReplayRaces(String swissTimingUrl, AsyncCallback> asyncCallback); - - void getCompetitorsRaceData(RegattaAndRaceIdentifier race, List competitors, Date from, Date to, - long stepSize, DetailType detailType, String leaderboarGroupName, String leaderboardName, AsyncCallback callback); - - /** - * Finds out the names of all {@link com.sap.sailing.domain.leaderboard.MetaLeaderboard}s managed by this server that - * {@link com.sap.sailing.domain.leaderboard.MetaLeaderboard#getLeaderboards() contain} the leaderboard identified by leaderboardName. The - * names of those meta-leaderboards are returned. The list returned is never null but may be empty if no such - * leaderboard is found. - */ - void getOverallLeaderboardNamesContaining(String leaderboardName, AsyncCallback> asyncCallback); - - void getPreviousSwissTimingArchiveConfigurations( - AsyncCallback> asyncCallback); - - void storeSwissTimingArchiveConfiguration(String swissTimingUrl, AsyncCallback asyncCallback); - - void generatePolarSheetForRaces(List selectedRaces, - PolarSheetGenerationSettings settings, String name, AsyncCallback asyncCallback); - - - void updateRegatta(RegattaIdentifier regattaIdentifier, UUID defaultCourseAreaUuid, RegattaConfigurationDTO regattaConfiguration, AsyncCallback callback); - - /** - * @param detailType - * supports {@link DetailType#REGATTA_RANK}, {@link DetailType#REGATTA_TOTAL_POINTS} and - * {@link DetailType#OVERALL_RANK}. - * - * @return the first triple element is the race column name; then follows the list of competitors, and finally the - * list of values whose indices correspond with the elements in the {@link CompetitorDTO} list. - */ - void getLeaderboardDataEntriesForAllRaceColumns(String leaderboardName, Date date, DetailType detailType, - AsyncCallback, List>>> callback); - - void getLeaderboardsNamesOfMetaleaderboard(String metaLeaderboardName, - AsyncCallback>> callback); - - void checkLeaderboardName(String leaderboardName, AsyncCallback> callback); - - void getBuildVersion(AsyncCallback callback); - - void stopReplicatingFromMaster(AsyncCallback asyncCallback); - - void getRegattaStructureForEvent(UUID eventId, AsyncCallback> asyncCallback); - - void getRaceStateEntriesForRaceGroup(UUID eventId, List visibleCourseAreas, - List visibleRegattas, boolean showOnlyCurrentlyRunningRaces, boolean showOnlyRacesOfSameDay, - AsyncCallback> markedAsyncCallback); - - void getRaceStateEntriesForLeaderboard(String leaderboardName, boolean showOnlyCurrentlyRunningRaces, - boolean showOnlyRacesOfSameDay, List visibleRegattas, - AsyncCallback> callback); - - void stopAllReplicas(AsyncCallback asyncCallback); - - void stopSingleReplicaInstance(String identifier, AsyncCallback asyncCallback); - - void reloadRaceLog(String leaderboardName, RaceColumnDTO raceColumnDTO, FleetDTO fleet, - AsyncCallback asyncCallback); - - void getRaceLog(String leaderboardName, RaceColumnDTO raceColumnDTO, FleetDTO fleet, - AsyncCallback callback); - - void importMasterData(String host, String[] names, boolean override, boolean compress, - boolean exportWind, - AsyncCallback asyncCallback); - - void getImportOperationProgress(UUID id, AsyncCallback asyncCallback); - - void getLeaderboardGroupNamesFromRemoteServer(String host, AsyncCallback> leaderboardGroupNames); - - void getCompetitors(AsyncCallback> asyncCallback); - - void getCompetitorsOfLeaderboard(String leaderboardName, AsyncCallback> asyncCallback); - - void updateCompetitor(CompetitorDTO competitor, AsyncCallback asyncCallback); - - void allowCompetitorResetToDefaults(Iterable competitors, AsyncCallback asyncCallback); - - void getDeviceConfigurationMatchers(AsyncCallback> asyncCallback); - - void getDeviceConfiguration(DeviceConfigurationMatcherDTO matcher, AsyncCallback callback); - - void createOrUpdateDeviceConfiguration(DeviceConfigurationMatcherDTO matcherDTO, DeviceConfigurationDTO configurationDTO, AsyncCallback callback); - - void removeDeviceConfiguration(DeviceConfigurationMatcherType type, List clientIds, AsyncCallback asyncCallback); - - /** - * Sets the a new start time. - * @param dto {@link RaceLogSetStartTimeAndProcedureDTO} identifying the race to set the start time on and the new start time. - */ - void setStartTimeAndProcedure(RaceLogSetStartTimeAndProcedureDTO dto, AsyncCallback callback); - - /** - * Gets the race's current start time, current pass identifier and racing procedure. If no start time is set, the - * pass identifier will still be returned, but the start time field will be null. - */ - void getStartTimeAndProcedure(String leaderboardName, String raceColumnName, String fleetName, - AsyncCallback> callback); - - void getAllIgtimiAccountEmailAddresses(AsyncCallback> callback); - - void getIgtimiAuthorizationUrl(AsyncCallback callback); - - void authorizeAccessToIgtimiUser(String eMailAddress, String password, AsyncCallback callback); - - void removeIgtimiAccount(String eMailOfAccountToRemove, AsyncCallback asyncCallback); - - void importWindFromIgtimi(List selectedRaces, AsyncCallback> asyncCallback); - - void getEventById(UUID id, AsyncCallback callback); - - void getLeaderboardsByEvent(EventDTO event, AsyncCallback> callback); - - void removeSeries(RegattaIdentifier regattaIdentifier, String seriesName, AsyncCallback callback); -} - +package com.sap.sailing.gwt.ui.client; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.sap.sailing.domain.common.DataImportProgress; +import com.sap.sailing.domain.common.DetailType; +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.common.MaxPointsReason; +import com.sap.sailing.domain.common.PassingInstruction; +import com.sap.sailing.domain.common.PolarSheetGenerationResponse; +import com.sap.sailing.domain.common.PolarSheetGenerationSettings; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.RegattaIdentifier; +import com.sap.sailing.domain.common.RegattaNameAndRaceName; +import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.configuration.DeviceConfigurationMatcherType; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.domain.common.dto.FleetDTO; +import com.sap.sailing.domain.common.dto.IncrementalOrFullLeaderboardDTO; +import com.sap.sailing.domain.common.dto.PositionDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.domain.common.dto.RaceColumnInSeriesDTO; +import com.sap.sailing.domain.common.dto.RaceDTO; +import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; +import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.domain.common.impl.Util.Triple; +import com.sap.sailing.domain.common.racelog.RacingProcedureType; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.gwt.ui.shared.BulkScoreCorrectionDTO; +import com.sap.sailing.gwt.ui.shared.CompactRaceMapDataDTO; +import com.sap.sailing.gwt.ui.shared.CompetitorsRaceDataDTO; +import com.sap.sailing.gwt.ui.shared.ControlPointDTO; +import com.sap.sailing.gwt.ui.shared.CourseAreaDTO; +import com.sap.sailing.gwt.ui.shared.CoursePositionsDTO; +import com.sap.sailing.gwt.ui.shared.DeviceConfigurationDTO; +import com.sap.sailing.gwt.ui.shared.DeviceConfigurationDTO.RegattaConfigurationDTO; +import com.sap.sailing.gwt.ui.shared.DeviceConfigurationMatcherDTO; +import com.sap.sailing.gwt.ui.shared.DeviceMappingDTO; +import com.sap.sailing.gwt.ui.shared.EventDTO; +import com.sap.sailing.gwt.ui.shared.GPSFixDTO; +import com.sap.sailing.gwt.ui.shared.LeaderboardGroupDTO; +import com.sap.sailing.gwt.ui.shared.ManeuverDTO; +import com.sap.sailing.gwt.ui.shared.MarkDTO; +import com.sap.sailing.gwt.ui.shared.RaceCourseDTO; +import com.sap.sailing.gwt.ui.shared.RaceGroupDTO; +import com.sap.sailing.gwt.ui.shared.RaceLogDTO; +import com.sap.sailing.gwt.ui.shared.RaceLogSetStartTimeAndProcedureDTO; +import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; +import com.sap.sailing.gwt.ui.shared.RegattaDTO; +import com.sap.sailing.gwt.ui.shared.RegattaOverviewEntryDTO; +import com.sap.sailing.gwt.ui.shared.RegattaScoreCorrectionDTO; +import com.sap.sailing.gwt.ui.shared.ReplicationStateDTO; +import com.sap.sailing.gwt.ui.shared.ScoreCorrectionProviderDTO; +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.SwissTimingEventRecordDTO; +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.VenueDTO; +import com.sap.sailing.gwt.ui.shared.WindDTO; +import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; + +/** + * The async counterpart of {@link SailingService} + */ +public interface SailingServiceAsync { + + void getRegattas(AsyncCallback> callback); + + /** + * The string returned in the callback's pair is the common event name + * @param listHiddenRaces + */ + void listTracTracRacesInEvent(String eventJsonURL, boolean listHiddenRaces, AsyncCallback>> callback); + + /** + * @param regattaToAddTo + * if null, an existing regatta by the name of the TracTrac event with the boat class name + * appended in parentheses will be looked up; if not found, a default regatta with that name will be + * created, with a single default series and a single default fleet. If a valid {@link RegattaIdentifier} + * is specified, a regatta lookup is performed with that identifier; if the regatta is found, it is used + * to add the races to. Otherwise, a default regatta as described above will be created and used. + * @param liveURI + * may be null or the empty string in which case the server will use the + * {@link TracTracRaceRecordDTO#liveURI} from the rr race record. + * @param simulateWithStartTimeNow + * if true, the connector will adjust the time stamps of all events received such that the + * first mark passing for the first waypoint will be set to "now." It will delay the forwarding of all + * events received such that they seem to be sent in "real-time." So, more or less the time points attached + * to the events sent to the receivers will again approximate the wall time. + * @param storedURImay + * be null or the empty string in which case the server will use the + * {@link TracTracRaceRecordDTO#storedURI} from the rr race record. + */ + void trackWithTracTrac(RegattaIdentifier regattaToAddTo, + Iterable rrs, String liveURI, String storedURI, String courseDesignUpdateURI, boolean trackWind, boolean correctWindByDeclination, + boolean simulateWithStartTimeNow, String tracTracUsername, String tracTracPassword, AsyncCallback callback); + + void trackWithSwissTiming(RegattaIdentifier regattaToAddTo, Iterable rrs, + String hostname, int port, boolean trackWind, boolean correctWindByDeclination, + AsyncCallback asyncCallback); + + void replaySwissTimingRace(RegattaIdentifier regattaIdentifier, Iterable replayRaces, + boolean trackWind, boolean correctWindByDeclination, boolean simulateWithStartTimeNow, + AsyncCallback asyncCallback); + + void getPreviousTracTracConfigurations(AsyncCallback> callback); + + void storeTracTracConfiguration(String name, String jsonURL, String liveDataURI, String storedDataURI, String courseDesignUpdateURI, String tracTracUsername, String tracTracPassword, + AsyncCallback callback); + + void stopTrackingEvent(RegattaIdentifier eventIdentifier, AsyncCallback callback); + + void stopTrackingRaces(Iterable racesToStopTracking, AsyncCallback asyncCallback); + + /** + * Untracks the race and removes it from the regatta. It will also be removed in all leaderboards + * @param regattaNamesAndRaceNames The identifier for the regatta name, and the race name to remove + */ + void removeAndUntrackRaces(Iterable regattaNamesAndRaceNames, AsyncCallback callback); + + void getRawWindFixes(RegattaAndRaceIdentifier raceIdentifier, Collection windSources, AsyncCallback callback); + + /** + * @param onlyUpToNewestEvent + * if true, no wind data will be returned for time points later than + * {@link TrackedRace#getTimePointOfNewestEvent() trackedRace.getTimePointOfNewestEvent()}. This is + * helpful in case the client wants to populate a chart during live mode. If false, the + * "best effort" readings are provided for the time interval requested, no matter if based on any sensor + * evidence or not, regardless of {@link TrackedRace#getTimePointOfNewestEvent() + * trackedRace.getTimePointOfNewestEvent()}. + */ + void getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, Date to, + long resolutionInMilliseconds, Collection windSourceTypeNames, boolean onlyUpToNewestEvent, + AsyncCallback callback); + + /** + * @param windSourceTypeNames + * if null, data from all available wind sources will be returned, otherwise only from those + * whose {@link WindSource} name is contained in the windSources collection. + */ + void getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, long millisecondsStepWidth, int numberOfFixes, + double latDeg, double lngDeg, Collection windSourceTypeNames, + AsyncCallback callback); + + /** + * @param onlyUpToNewestEvent + * if true, no wind data will be returned for time points later than + * {@link TrackedRace#getTimePointOfNewestEvent() trackedRace.getTimePointOfNewestEvent()}. This is + * helpful in case the client wants to populate a chart during live mode. If false, the + * "best effort" readings are provided for the time interval requested, no matter if based on any sensor + * evidence or not, regardless of {@link TrackedRace#getTimePointOfNewestEvent() + * trackedRace.getTimePointOfNewestEvent()}. + */ + void getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, long millisecondsStepWidth, + int numberOfFixes, Collection windSourceTypeNames, boolean onlyUpToNewestEvent, + AsyncCallback callback); + + void setWind(RegattaAndRaceIdentifier raceIdentifier, WindDTO wind, AsyncCallback callback); + + void removeWind(RegattaAndRaceIdentifier raceIdentifier, WindDTO windDTO, AsyncCallback callback); + + void getRaceTimesInfo(RegattaAndRaceIdentifier raceIdentifier, AsyncCallback callback); + + void getRaceTimesInfos(Collection raceIdentifiers, AsyncCallback> callback); + + void getCoursePositions(RegattaAndRaceIdentifier raceIdentifier, Date date, AsyncCallback asyncCallback); + + void getLeaderboardByName(String leaderboardName, Date date, + Collection namesOfRaceColumnsForWhichToLoadLegDetails, + boolean addOverallDetails, String previousLeaderboardId, AsyncCallback callback); + + void getLeaderboardNames(AsyncCallback> callback); + + void getLeaderboards(AsyncCallback> callback); + + void getLeaderboardsByRegatta(RegattaDTO regatta, AsyncCallback> callback); + + void getLeaderboardsByRaceAndRegatta(RaceDTO race, RegattaIdentifier regattaIdentifier, + AsyncCallback> callback); + + void updateLeaderboard(String leaderboardName, String newLeaderboardName, String newLeaderboardDisplayName, + int[] newDiscardingThreasholds, UUID newCourseAreaId, AsyncCallback callback); + + void createFlexibleLeaderboard(String leaderboardName, String leaderboardDisplayName, int[] discardThresholds, ScoringSchemeType scoringSchemeType, UUID courseAreaId, + AsyncCallback asyncCallback); + + void createRegattaLeaderboard(RegattaIdentifier regattaIdentifier, String leaderboardDisplayName, int[] discardThresholds, + AsyncCallback asyncCallback); + + void removeLeaderboard(String leaderboardName, AsyncCallback asyncCallback); + + void removeLeaderboards(Collection leaderboardNames, AsyncCallback asyncCallback); + + void renameLeaderboard(String leaderboardName, String newLeaderboardName, AsyncCallback asyncCallback); + + void addColumnToLeaderboard(String columnName, String leaderboardName, boolean medalRace, + AsyncCallback callback); + + void renameLeaderboardColumn(String leaderboardName, String oldColumnName, String newColumnName, AsyncCallback callback); + + void removeLeaderboardColumn(String leaderboardName, String columnName, AsyncCallback callback); + + void connectTrackedRaceToLeaderboardColumn(String leaderboardName, String raceColumnName, String fleetName, + RegattaAndRaceIdentifier raceIdentifier, AsyncCallback asyncCallback); + + /** + * The key set of the map returned contains all fleets of the race column identified by the combination of + * leaderboardName and raceColumnName. If a value is null, there is no + * tracked race currently linked to the fleet in the race column; otherwise, the value is the {@link RaceIdentifier} + * of the tracked race currently connected for the fleet whose name is the key. The map returned is never null. + */ + void getRegattaAndRaceNameOfTrackedRaceConnectedToLeaderboardColumn(String leaderboardName, String raceColumnName, + AsyncCallback> callback); + + void disconnectLeaderboardColumnFromTrackedRace(String leaderboardName, String raceColumnName, String fleetName, + AsyncCallback callback); + + void updateLeaderboardCarryValue(String leaderboardName, String competitorIdAsString, Double carriedPoints, AsyncCallback callback); + + void updateLeaderboardMaxPointsReason(String leaderboardName, String competitorIdAsString, String raceColumnName, + MaxPointsReason maxPointsReason, Date date, AsyncCallback> asyncCallback); + + void updateLeaderboardScoreCorrection(String leaderboardName, String competitorIdAsString, String columnName, + Double correctedScore, Date date, AsyncCallback> asyncCallback); + + void updateLeaderboardScoreCorrectionMetadata(String leaderboardName, Date timePointOfLastCorrectionValidity, + String comment, AsyncCallback callback); + + void updateLeaderboardScoreCorrectionsAndMaxPointsReasons(BulkScoreCorrectionDTO updates, + AsyncCallback callback); + + void updateCompetitorDisplayNameInLeaderboard(String leaderboardName, String competitorID, String displayName, + AsyncCallback callback); + + void moveLeaderboardColumnUp(String leaderboardName, String columnName, AsyncCallback callback); + + void moveLeaderboardColumnDown(String leaderboardName, String columnName, AsyncCallback callback); + + void updateIsMedalRace(String leaderboardName, String columnName, boolean isMedalRace, AsyncCallback callback); + + void updateRaceDelayToLive(RegattaAndRaceIdentifier regattaAndRaceIdentifier, long delayToLiveInMs, AsyncCallback callback); + + void updateRacesDelayToLive(List regattaAndRaceIdentifiers, long delayToLiveInMs, AsyncCallback callback); + + void getPreviousSwissTimingConfigurations(AsyncCallback> asyncCallback); + + void getRacesOfSwissTimingEvent(String eventJsonUrl, AsyncCallback asyncCallback); + + void storeSwissTimingConfiguration(String configName, String jsonURL, String hostname, int port, AsyncCallback asyncCallback); + + void getCountryCodes(AsyncCallback callback); + + void getDouglasPoints(RegattaAndRaceIdentifier raceIdentifier, Map from, Map to, + double meters, AsyncCallback>> callback); + + void getManeuvers(RegattaAndRaceIdentifier raceIdentifier, Map from, Map to, + AsyncCallback>> callback); + + void getLeaderboardGroups(boolean withGeoLocationData, AsyncCallback> callback); + + void getLeaderboardGroupByName(String groupName, boolean withGeoLocationData, + AsyncCallback callback); + + /** + * Renames the group with the name oldName to the newName.
+ * If there's no group with the name oldName or there's already a group with the name + * newName a {@link IllegalArgumentException} is thrown. + */ + void renameLeaderboardGroup(String oldName, String newName, AsyncCallback callback); + + /** + * Removes the leaderboard groups with the given names from the service and the persistant store. + */ + void removeLeaderboardGroups(Set groupNames, AsyncCallback asyncCallback); + + /** + * Creates a new group with the name groupname, the description description and an empty list of leaderboards.
+ * @param displayGroupsInReverseOrder TODO + */ + void createLeaderboardGroup(String groupName, String description, + boolean displayGroupsInReverseOrder, int[] overallLeaderboardDiscardThresholds, + ScoringSchemeType overallLeaderboardScoringSchemeType, AsyncCallback callback); + + /** + * Updates the data of the group with the name oldName. + * + * @param oldName The old name of the group + * @param newName The new name of the group + * @param description The new description of the group + * @param leaderboardNames The list of names of the new leaderboards of the group + */ + void updateLeaderboardGroup(String oldName, String newName, String description, + List leaderboardNames, int[] overallLeaderboardDiscardThresholds, ScoringSchemeType overallLeaderboardScoringSchemeType, AsyncCallback callback); + + + void setRaceIsKnownToStartUpwind(RegattaAndRaceIdentifier raceIdentifier, boolean raceIsKnownToStartUpwind, + AsyncCallback callback); + + void setWindSourcesToExclude(RegattaAndRaceIdentifier raceIdentifier, Iterable windSourcesToExclude, + AsyncCallback callback); + + void getRaceMapData(RegattaAndRaceIdentifier raceIdentifier, Date date, Map fromPerCompetitorIdAsString, + Map toPerCompetitorIdAsString, boolean extrapolate, AsyncCallback callback); + + void getReplicaInfo(AsyncCallback callback); + + void startReplicatingFromMaster(String messagingHost, String masterName, String exchangeName, int servletPort, int messagingPort, + AsyncCallback callback); + + void getEvents(AsyncCallback> callback); + + /** + * Creates a {@link EventDTO} for the {@link com.sap.sailing.domain.base.Event} with the name eventName, which contains the + * name, the description and a list with {@link RegattaDTO RegattaDTOs} contained in the event.
+ * If no event with the name eventName is known, an {@link IllegalArgumentException} is thrown. + */ + void getEventByName(String eventName, AsyncCallback callback); + + /** + * Renames the event with the name oldName to the newName.
+ * If there's no event with the name oldName or there's already a event with the name + * newName a {@link IllegalArgumentException} is thrown. + */ + void renameEvent(UUID eventId, String newName, AsyncCallback callback); + + /** + * Removes the event with the id id from the service and the persistence store. + */ + void removeEvent(UUID eventId, AsyncCallback callback); + + void removeEvents(Collection eventIds, AsyncCallback asyncCallback); + + void createEvent(String eventName, Date startDate, Date endDate, String description, boolean isPublic, List courseAreaNames, AsyncCallback callback); + + void updateEvent(UUID eventId, String eventName, Date startDate, Date endDate, VenueDTO venue, boolean isPublic, + List regattaNames, AsyncCallback callback); + + void createCourseArea(UUID eventId, String courseAreaName, AsyncCallback callback); + + void removeRegatta(RegattaIdentifier regattaIdentifier, AsyncCallback callback); + + void removeRegattas(Collection regattas, AsyncCallback asyncCallback); + + void addRaceColumnToSeries(RegattaIdentifier regattaIdentifier, String seriesName, String columnName, + AsyncCallback callback); + + void removeRaceColumnFromSeries(RegattaIdentifier regattaIdentifier, String seriesName, String columnName, + AsyncCallback callback); + + void moveRaceColumnInSeriesUp(RegattaIdentifier regattaIdentifier, String seriesName, String columnName, + AsyncCallback callback); + + void moveRaceColumnInSeriesDown(RegattaIdentifier regattaIdentifier, String seriesName, String columnName, + AsyncCallback callback); + + void createRegatta(String regattaName, String boatClassName, + RegattaCreationParametersDTO seriesNamesWithFleetNamesAndFleetOrderingAndMedal, boolean persistent, + ScoringSchemeType scoringSchemeType, UUID defaultCourseAreaId, AsyncCallback callback); + + void addRaceColumnsToSeries(RegattaIdentifier regattaIdentifier, String seriesName, List columnNames, + AsyncCallback> callback); + + void updateSeries(RegattaIdentifier regattaIdentifier, String seriesName, String newSeriesName, boolean isMedal, + int[] resultDiscardingThresholds, boolean startsWithZeroScore, + boolean firstRaceIsNonDiscardableCarryForward, boolean hasSplitFleetScore, List fleets, + AsyncCallback callback); + + void removeRaceColumnsFromSeries(RegattaIdentifier regattaIdentifier, String seriesName, List columnNames, + AsyncCallback callback); + + void getScoreCorrectionProviderNames(AsyncCallback> callback); + + void getScoreCorrectionsOfProvider(String providerName, AsyncCallback callback); + + void getScoreCorrections(String scoreCorrectionProviderName, String eventName, String boatClassName, + Date timePointWhenResultPublished, AsyncCallback asyncCallback); + + void getWindSourcesInfo(RegattaAndRaceIdentifier raceIdentifier, AsyncCallback callback); + + void getRaceCourse(RegattaAndRaceIdentifier raceIdentifier, Date date, AsyncCallback callback); + + void updateRaceCourse(RegattaAndRaceIdentifier raceIdentifier, List> controlPoints, AsyncCallback callback); + + void getResultImportUrls(String resultProviderName, AsyncCallback> callback); + + void removeResultImportURLs(String resultProviderName, Set toRemove, AsyncCallback callback); + + void addResultImportUrl(String resultProviderName, String url, AsyncCallback callback); + + void getUrlResultProviderNames(AsyncCallback> callback); + + void addColumnsToLeaderboard(String leaderboardName, List> columnsToAdd, + AsyncCallback callback); + + void removeLeaderboardColumns(String leaderboardName, List columnsToRemove, AsyncCallback callback); + + void getLeaderboard(String leaderboardName, AsyncCallback callback); + + void suppressCompetitorInLeaderboard(String leaderboardName, String competitorIdAsString, boolean suppressed, AsyncCallback asyncCallback); + + void updateLeaderboardColumnFactor(String leaderboardName, String columnName, Double newFactor, + AsyncCallback callback); + + void listSwissTiminigReplayRaces(String swissTimingUrl, AsyncCallback> asyncCallback); + + void getCompetitorsRaceData(RegattaAndRaceIdentifier race, List competitors, Date from, Date to, + long stepSize, DetailType detailType, String leaderboarGroupName, String leaderboardName, AsyncCallback callback); + + /** + * Finds out the names of all {@link com.sap.sailing.domain.leaderboard.MetaLeaderboard}s managed by this server that + * {@link com.sap.sailing.domain.leaderboard.MetaLeaderboard#getLeaderboards() contain} the leaderboard identified by leaderboardName. The + * names of those meta-leaderboards are returned. The list returned is never null but may be empty if no such + * leaderboard is found. + */ + void getOverallLeaderboardNamesContaining(String leaderboardName, AsyncCallback> asyncCallback); + + void getPreviousSwissTimingArchiveConfigurations( + AsyncCallback> asyncCallback); + + void storeSwissTimingArchiveConfiguration(String swissTimingUrl, AsyncCallback asyncCallback); + + void generatePolarSheetForRaces(List selectedRaces, + PolarSheetGenerationSettings settings, String name, AsyncCallback asyncCallback); + + + void updateRegatta(RegattaIdentifier regattaIdentifier, UUID defaultCourseAreaUuid, RegattaConfigurationDTO regattaConfiguration, AsyncCallback callback); + + /** + * @param detailType + * supports {@link DetailType#REGATTA_RANK}, {@link DetailType#REGATTA_TOTAL_POINTS} and + * {@link DetailType#OVERALL_RANK}. + * + * @return the first triple element is the race column name; then follows the list of competitors, and finally the + * list of values whose indices correspond with the elements in the {@link CompetitorDTO} list. + */ + void getLeaderboardDataEntriesForAllRaceColumns(String leaderboardName, Date date, DetailType detailType, + AsyncCallback, List>>> callback); + + void getLeaderboardsNamesOfMetaleaderboard(String metaLeaderboardName, + AsyncCallback>> callback); + + void checkLeaderboardName(String leaderboardName, AsyncCallback> callback); + + void getBuildVersion(AsyncCallback callback); + + void stopReplicatingFromMaster(AsyncCallback asyncCallback); + + void getRegattaStructureForEvent(UUID eventId, AsyncCallback> asyncCallback); + + void getRaceStateEntriesForRaceGroup(UUID eventId, List visibleCourseAreas, + List visibleRegattas, boolean showOnlyCurrentlyRunningRaces, boolean showOnlyRacesOfSameDay, + AsyncCallback> markedAsyncCallback); + + void getRaceStateEntriesForLeaderboard(String leaderboardName, boolean showOnlyCurrentlyRunningRaces, + boolean showOnlyRacesOfSameDay, List visibleRegattas, + AsyncCallback> callback); + + void stopAllReplicas(AsyncCallback asyncCallback); + + void stopSingleReplicaInstance(String identifier, AsyncCallback asyncCallback); + + void reloadRaceLog(String leaderboardName, RaceColumnDTO raceColumnDTO, FleetDTO fleet, + AsyncCallback asyncCallback); + + void getRaceLog(String leaderboardName, RaceColumnDTO raceColumnDTO, FleetDTO fleet, + AsyncCallback callback); + + void importMasterData(String host, String[] names, boolean override, boolean compress, + boolean exportWind, + AsyncCallback asyncCallback); + + void getImportOperationProgress(UUID id, AsyncCallback asyncCallback); + + void getLeaderboardGroupNamesFromRemoteServer(String host, AsyncCallback> leaderboardGroupNames); + + void getCompetitors(AsyncCallback> asyncCallback); + + void getCompetitorsOfLeaderboard(String leaderboardName, boolean lookInRaceLogs, + AsyncCallback> asyncCallback); + + void addOrUpdateCompetitor(CompetitorDTO competitor, AsyncCallback asyncCallback); + + void allowCompetitorResetToDefaults(Iterable competitors, AsyncCallback asyncCallback); + + void getDeviceConfigurationMatchers(AsyncCallback> asyncCallback); + + void getDeviceConfiguration(DeviceConfigurationMatcherDTO matcher, AsyncCallback callback); + + void createOrUpdateDeviceConfiguration(DeviceConfigurationMatcherDTO matcherDTO, DeviceConfigurationDTO configurationDTO, AsyncCallback callback); + + void removeDeviceConfiguration(DeviceConfigurationMatcherType type, List clientIds, AsyncCallback asyncCallback); + + /** + * Sets the a new start time. + * @param dto {@link RaceLogSetStartTimeAndProcedureDTO} identifying the race to set the start time on and the new start time. + */ + void setStartTimeAndProcedure(RaceLogSetStartTimeAndProcedureDTO dto, AsyncCallback callback); + + /** + * Gets the race's current start time, current pass identifier and racing procedure. If no start time is set, the + * pass identifier will still be returned, but the start time field will be null. + */ + void getStartTimeAndProcedure(String leaderboardName, String raceColumnName, String fleetName, + AsyncCallback> callback); + + void getAllIgtimiAccountEmailAddresses(AsyncCallback> callback); + + void getIgtimiAuthorizationUrl(AsyncCallback callback); + + void authorizeAccessToIgtimiUser(String eMailAddress, String password, AsyncCallback callback); + + void removeIgtimiAccount(String eMailOfAccountToRemove, AsyncCallback asyncCallback); + + void importWindFromIgtimi(List selectedRaces, AsyncCallback> asyncCallback); + + void getEventById(UUID id, AsyncCallback callback); + + void getLeaderboardsByEvent(EventDTO event, AsyncCallback> callback); + + void denoteForRaceLogTracking(String leaderboardName, + String raceColumnName, String fleetName, + AsyncCallback callback); + + void denoteForRaceLogTracking(String leaderboardName, AsyncCallback callback); + + void startRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName, + AsyncCallback callback); + + void setCompetitorRegistrations(String leaderboardName, String raceColumnName, String fleetName, + Set competitors, AsyncCallback callback); + + void getCompetitorRegistrations(String leaderboardName, String raceColumnName, String fleetName, + AsyncCallback> callback); + + void addMarkToRaceLog(String leaderboardName, String raceColumnName, String fleetName, MarkDTO markDTO, + AsyncCallback callback); + + void getMarksInRaceLog(String leaderboardName, String raceColumnName, String fleetName, + AsyncCallback> callback); + + void addCourseDefinitionToRaceLog(String leaderboardName, String raceColumnName, String fleetName, + List> course, AsyncCallback callback); + + void getLastCourseDefinitionInRaceLog(String leaderboardName, String raceColumnName, String fleetName, + AsyncCallback callback); + + void pingMarkViaRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName, MarkDTO mark, + PositionDTO position, AsyncCallback callback); + + void getDeserializableDeviceIdentifierTypes(AsyncCallback> callback); + + void getDeviceMappingsFromRaceLog(String leaderboardName, String raceColumnName, String fleetName, + AsyncCallback> callback); + + void addDeviceMappingToRaceLog(String leaderboardName, String raceColumnName, String fleetName, + DeviceMappingDTO mapping, AsyncCallback callback); + + void closeOpenEndedDeviceMapping(String leaderboardName, String raceColumnName, String fleetName, + DeviceMappingDTO mapping, Date closingTimePoint, AsyncCallback callback); + + void revokeRaceLogEvents(String leaderboardName, String raceColumnName, String fleetName, List eventIds, + AsyncCallback callback); + + void removeSeries(RegattaIdentifier regattaIdentifier, String seriesName, AsyncCallback callback); + + void removeDenotationForRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName, + AsyncCallback callback); + + void copyCourseAndCompetitorsToOtherRaceLogs(Triple raceLogFrom, + Set> raceLogsTo, AsyncCallback callback); +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index 831dc8a70fa..41865209368 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -289,7 +289,8 @@ public interface StringMessages extends Messages { String leaderboardsInGroup(); String replication(); String errorFetchingReplicaData(String message); - String averageCrossTrackErrorInMeters(); + String averageAbsoluteCrossTrackErrorInMeters(); + String averageSignedCrossTrackErrorInMeters(); String enterMaster(); String errorStartingReplication(String hostname, String exchangeName, String message); String helpLines(); @@ -364,14 +365,16 @@ public interface StringMessages extends Messages { String controlPoint(); String errorUpdatingRaceCourse(String message); String successfullyUpdatedCourse(); - String selectOneMarkOrTwoMarksForGate(); + String selectTwoMarksForGate(); String errorTryingToObtainTheMarksOfTheRace(String message); String errorTryingToObtainRaceCourse(String message); String insertWaypointBeforeSelected(); String insertWaypointAfterSelected(); + String addGate(); String scoringSchemeLowPointSystem(); String scoringSchemeHighPointSystem(); String scoringSchemeLowPointWinnerGetsZero(); + String scoringSchemeWinnerGetsSix(); String scoringSystem(); String createFlexibleLeaderboard(); String createRegattaLeaderboard(); @@ -485,8 +488,10 @@ public interface StringMessages extends Messages { String totalTimeSailedReachingInSecondsTooltip(); String totalTimeSailedUpwindInSecondsTooltip(); String windwardDistanceToGoInMetersTooltip(); - String averageCrossTrackErrorInMetersTooltip(); - String raceAverageCrossTrackErrorInMetersTooltip(); + String averageAbsoluteCrossTrackErrorInMetersTooltip(); + String averageSignedCrossTrackErrorInMetersTooltip(); + String raceAverageAbsoluteCrossTrackErrorInMetersTooltip(); + String raceAverageSignedCrossTrackErrorInMetersTooltip(); String gapChangeSinceLegStartInSecondsTooltip(); String velocityMadeGoodInKnotsTooltip(); String sideToWhichMarkAtLegStartWasRoundedTooltip(); @@ -961,6 +966,42 @@ public interface StringMessages extends Messages { String releaseNotes(); String hasSplitFleetContiguousScoring(); String eventOverview(); + String addRaceLogTracker(); + String addRaceLogTrackers(); + String denoteForRaceLogTracking(); + String raceLogTracking(); + String startRaceLogTracking(); + String filterByLeaderboard(); + String removeLeaderboardFilter(); + String competitorRegistrations(); + String trackerStatus(); + String canOnlyBeEditedBeforeStartingTracking(); + String defineCourse(); + String mapDevices(); + @Key("edit_what") + String edit(String what); + @Key("add_what") + String add(String what); + @Key("remove_what") + String remove(String what); + String pleaseEnterA(String what); + String pingPosition(String whose); + String pleaseEnterAValidValueFor(String what, String validTemplate); + String copyCourseAndCompetitors(); + String nowSelectRaceColumnAndFleetToCopyTo(); + String selectRowInTableToCopyCourse(); + String deviceId(); + String deviceType(); + String mappedToType(); + String mappedTo(); + String deviceMappings(); + String atMostOneEndOfTheTimeRangeMayBeOpen(); + String pleaseSelectAnItemToMapTo(); + String device(); + String timeRange(); + String closeTimeRange(); + String setClosingTimePoint(); + String startOfTimeRangeMustLieBeforeEnd(); String start(); String overallProgress(); String showAddedScores(); @@ -969,4 +1010,13 @@ public interface StringMessages extends Messages { String importWindTooltip(); String authorName(); String authorPriority(); + String removeDenotation(); + String competitorRegistrationsMissingProceed(String namesOfRaces); + String pleaseSelectAControlPoint(); + String twoMarkControlPoint(); + String registeredCompetitors(); + String competitorPool(); + String selectAtLeastOne(); + String selectRaces(); + String importFixes(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index afecbda8ab1..68c37dd0ed6 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -311,9 +311,12 @@ racesInLeaderboard=Races in leaderboard leaderboardsInGroup=Leaderboards in group replication=Replication errorFetchingReplicaData=Error fetching replica data: {0} -averageCrossTrackErrorInMeters=\u2205 XTE -averageCrossTrackErrorInMetersTooltip=The average cross track error to the great circle segment connecting leg start with leg end -raceAverageCrossTrackErrorInMetersTooltip=The average cross track error to the great circle segments connecting leg start with leg end during this race. +averageAbsoluteCrossTrackErrorInMeters=\u2205 XTE +averageAbsoluteCrossTrackErrorInMetersTooltip=The average cross track error to the great circle segment connecting leg start with leg end +averageSignedCrossTrackErrorInMeters=\u2205 XTE +/- +averageSignedCrossTrackErrorInMetersTooltip=The average position on the course as a signed distance from the course middle line; 0 meaning on the course middle line; negative values mean left-hand side, positive values mean right-hand side. +raceAverageAbsoluteCrossTrackErrorInMetersTooltip=The average absolute cross track error to the great circle segments connecting leg start with leg end for all upwind legs of this race, ignoring to which side of the course middle line the competitor is +raceAverageSignedCrossTrackErrorInMetersTooltip=The signed average cross track error to the great circle segments connecting leg start with leg end for all upwind legs of this race; negative values mean left of the course middle line, positive values mean right. enterMaster=Hostname of Master Instance errorStartingReplication=Error starting replication from host {0} / exchange {1}: {2} helpLines=Help lines @@ -389,16 +392,18 @@ newMark=New Mark controlPoint=Mark/Gate errorUpdatingRaceCourse=Error updating race course: {0} successfullyUpdatedCourse=Successfully updated race course -selectOneMarkOrTwoMarksForGate=Select a single mark or two marks to create a gate +selectTwoMarksForGate=Select two marks to create a gate errorTryingToObtainTheMarksOfTheRace=Error trying to obtain the marks of the race course: {0} errorTryingToObtainRaceCourse=Error trying to obtain the race course: {0} insertWaypointBeforeSelected=Insert waypoint before selected insertWaypointAfterSelected=Insert waypoint after selected +addGate=Add Gate scoringSchemeLowPointSystem=Low Point System scoringSchemeHighPointSystem=High Point System scoringSchemeHighPointEssOverall=High Point System ESS Overall scoringSchemeHighPointLastBreaksTie=High Point System, Last Race Breaks Tie scoringSchemeLowPointWinnerGetsZero=Low Point System, Winner Gets Zero +scoringSchemeWinnerGetsSix=High Point System, Winner Gets 6 Points scoringSystem=Scoring system createFlexibleLeaderboard=Create flexible leaderboard createRegattaLeaderboard=Create regatta leaderboard @@ -419,7 +424,7 @@ totalTimeSailedUpwindInSecondsTooltip=The total time the competitor sailed upwin totalTimeSailedReachingInSeconds=Total time reaching totalTimeSailedReachingInSecondsTooltip=The total time the competitor sailed on reaching legs in the regatta. totalTimeSailedInSeconds=Total time -totalTimeSailedInSecondsTooltip=The total time the competitor sailed in the regatta. +totalTimeSailedInSecondsTooltip=The total time the competitor sailed in the regatta, counted from race start time. totalDistanceTraveled=Total distance totalDistanceTraveledTooltip=The total distance sailed in the regatta. totalAverageSpeedOverGround=\u2205 SOG @@ -948,6 +953,40 @@ use=Use groupingProvider=Grouping Provider releaseNotes=News and Release History hasSplitFleetContiguousScoring=Split fleets scored contiguously +eventOverview=Event Overview +addRaceLogTracker=Add RaceLog Tracker +addRaceLogTrackers=Add RaceLog Trackers +denoteForRaceLogTracking=Denote for RaceLog tracking +raceLogTracking=RaceLog Tracking +startRaceLogTracking=Start RaceLog Tracking +filterByLeaderboard=Filter by Leaderboard +removeLeaderboardFilter=Remove Leaderboard filter +competitorRegistrations=Competitor Registrations +trackerStatus=Tracker Status +canOnlyBeEditedBeforeStartingTracking=Can only be edited before starting tracking +defineCourse=Define Course Layout +mapDevices=Map Devices to Competitors and Marks +edit_what=Edit {0} +add_what=Add {0} +remove_what=Remove {0} +pleaseEnterA=Please enter a {0} +pingPosition=Ping {0} position +pleaseEnterAValidValueFor=Please enter a valid value for {0} ({1}) +copyCourseAndCompetitors=Copy course and competitors to other races +nowSelectRaceColumnAndFleetToCopyTo=Now select the race column and fleet to copy the course to. +selectRowInTableToCopyCourse=Select a single row in the table, to which to copy the course. +deviceId=Device ID +deviceType=Device Type +mappedToType=Mapped to Type +mappedTo=Mapped to +deviceMappings=Device Mappings +atMostOneEndOfTheTimeRangeMayBeOpen=At most one end of the mapping''s time range may be left open (left unset) +pleaseSelectAnItemToMapTo=Please select an item to map the device to +device=Device +timeRange=Time Range +closeTimeRange=Close open end of Time Range +setClosingTimePoint=Set Timepoint that closes open time range +startOfTimeRangeMustLieBeforeEnd=The start of the time range must lie before its end overallProgress=Overall Progress eventOverview=Event Overview start=Start @@ -956,4 +995,13 @@ ratio=ratio importWind=Import Wind importWindTooltip=Unchecking this option will drastically lower the amount of data transfered. authorName=Author name -authorPriority=Author priority \ No newline at end of file +authorPriority=Author priority +removeDenotation=Remove Denotation +competitorRegistrationsMissingProceed=At least one of the selected races ({0}) is missing competitor registrations. Proceed anyway? +pleaseSelectAControlPoint=Please select a control point +twoMarkControlPoint=2-mark-controlpoint +registeredCompetitors=Registered Competitors +competitorPool=Competitor Pool +selectAtLeastOne=Select at least one +selectRaces=Select the Races +importFixes=Import Fixes diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index c9700668778..511eb97acf9 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -312,9 +312,12 @@ racesInLeaderboard=Rennen im Leaderboard leaderboardsInGroup=Leaderboards in der Gruppe replication=Replikation errorFetchingReplicaData=Fehler beim Beschaffen der Replikationsdaten: {0} -averageCrossTrackErrorInMeters=\u2205 XTE -averageCrossTrackErrorInMetersTooltip=Die durchschnittliche Querabweichung zur Kursmittellinie in diesem Schenkel -raceAverageCrossTrackErrorInMetersTooltip=Die durchschnittliche Querabweichung zur Kursmittellinie in diesem Rennen +averageAbsoluteCrossTrackErrorInMeters=\u2205 XTE +averageAbsoluteCrossTrackErrorInMetersTooltip=Die durchschnittliche Querabweichung zur Kursmittellinie in diesem Schenkel; die Seite, zu der der Teilnehmer abweicht, spielt dabei keine Rolle. +raceAverageAbsoluteCrossTrackErrorInMetersTooltip=Die durchschnittliche Querabweichung zur Kursmittellinie für alle Kreuzen in diesem Rennen; die Seite, zu der der Teilnehmer abweicht, spielt dabei keine Rolle. +averageSignedCrossTrackErrorInMeters=\u2205 XTE +/- +averageSignedCrossTrackErrorInMetersTooltip=Die durchschnittliche Querabweichung zur Kursmittellinie in diesem Schenkel, wobei negative Werte Abweichungen nach links, positive Werte Abweichungen nach rechts bedeuten. +raceAverageSignedCrossTrackErrorInMetersTooltip=Die durchschnittliche Querabweichung zur Kursmittellinie für alle Kreuzen diesem Rennen, wobei negative Werte Abweichungen nach links, positive Werte Abweichungen nach rechts bedeuten. enterMaster=Hostname der Master-Instanz errorStartingReplication=Fehler beim Starten der Replikation von Host {0} / Exchange {1}: {2} helpLines=Hilfslinien @@ -390,16 +393,18 @@ newMark=Neue Marke controlPoint=Einzelmarke/Gate errorUpdatingRaceCourse=Fehler beim Aktualisieren des neuen Kurses: {0} successfullyUpdatedCourse=Kurs erfolgreich aktualisiert -selectOneMarkOrTwoMarksForGate=Eine einzelne Marke auswählen oder zwei Marken, um ein Gate zu erstellen +selectTwoMarksForGate=Zwei Marken auswählen, um ein Gate zu erstellen errorTryingToObtainTheMarksOfTheRace=Fehler beim Ermitteln der Marken des Regattakurses: {0} errorTryingToObtainRaceCourse=Fehler beim Ermitteln des Rennkurses: {0} insertWaypointBeforeSelected=Wegpunkt nach dem selektierten einfügen insertWaypointAfterSelected=Wegpunkt vor dem selektierten einfügen +addGate=Gate hinzufügen scoringSchemeLowPointSystem=Low Point System scoringSchemeHighPointSystem=High Point System scoringSchemeHighPointEssOverall=High Point System ESS Gesamtwertung scoringSchemeHighPointLastBreaksTie=High Point System, letztes Rennen entscheidet bei Gleichstand scoringSchemeLowPointWinnerGetsZero=Low Point System, Gewinner erhält 0 Punkte +scoringSchemeWinnerGetsSix=High Point System, Gewinner erhält 6 Punkte scoringSystem=Bepunktungssystem createFlexibleLeaderboard=Flexible Rangliste anlegen createRegattaLeaderboard=Rangliste für Regatta anlegen @@ -420,7 +425,7 @@ totalTimeSailedUpwindInSecondsTooltip=Gesamtzeit, die der Teilnehmer in der Rega totalTimeSailedReachingInSeconds=Gesamtzeit Holeschlag totalTimeSailedReachingInSecondsTooltip=Gesamtzeit, die der Teilnehmer in der Regatta auf Holeschlägen gesegelt ist. totalTimeSailedInSeconds=Gesamtzeit -totalTimeSailedInSecondsTooltip=Gesamtzeit, die der Teilnehmer für die Regatta gebraucht hat. +totalTimeSailedInSecondsTooltip=Gesamtzeit, die der Teilnehmer für die Regatta gebraucht hat, gezählt ab der jeweiligen Startzeit jeder Wettfahrt. totalDistanceTraveled=Gesamtdistanz totalDistanceTraveledTooltip=Gesamtdistanz, die der Teilnehmer in der Regatta zurückgelegt hat. totalAverageSpeedOverGround=\u2205 FüG @@ -944,6 +949,40 @@ use=Verwende groupingProvider=Gruppierungs-Versorger releaseNotes=Neuigkeiten und Änderungshistorie hasSplitFleetContiguousScoring=Gruppen durchgängig bepunkten +eventOverview=Event Übersicht +addRaceLogTracker=RaceLog Tracker hinzufügen +addRaceLogTrackers=RaceLog Tracker hinzufügen +denoteForRaceLogTracking=Für RaceLog Tracking markieren +raceLogTracking=RaceLog Tracking +startRaceLogTracking=RaceLog Tracking starten +filterByLeaderboard=Nach Rangliste filtern +removeLeaderboardFilter=Nicht nach Rangliste filtern +competitorRegistrations=Team Anmeldungen +trackerStatus=Tracker-Status +canOnlyBeEditedBeforeStartingTracking=Kann nur vor Beginn des Trackings bearbeitet werden +defineCourse=Kurs definieren +mapDevices=Gerätezuordnung zu Teams und Tonnen +edit_what={0} bearbeiten +add_what={0} hinzufügen +remove_what={0} entfernen +pleaseEnterA=Bitte {0} eingeben. +pingPosition={0}-Position pingen +pleaseEnterAValidValueFor=Bitte geben Sie für {0} einen gültigen Wert ein ({1}) +copyCourseAndCompetitors=Kurs und Teams in andere Rennen kopieren +nowSelectRaceColumnAndFleetToCopyTo=Bitte wählen Sie nun die Renn-Spalte und Flotte zum Kopieren des Kurses aus. +selectRowInTableToCopyCourse=Bitte selektieren Sie eine einzelne Zeile der Tabelle, um den Kurs dorthin zu kopieren. +deviceId=Geräte ID +deviceType=Gerätetyp +mappedToType=Zuordnungs-typ +mappedTo=Zugeordnet zu +deviceMappings=Geräte Zuordnungen +atMostOneEndOfTheTimeRangeMayBeOpen=Es darf höchstens ein Ende der zeitlichen Zuordnung offen bleiben (nicht gesetzt sein) +pleaseSelectAnItemToMapTo=Bitte wählen Sie ein Objekt aus, dem das Gerät zugeordnet werden soll +device=Gerät +timeRange=Zeitspanne +closeTimeRange=Offenes Ende schließen +setClosingTimePoint=Zeitpunkt setzen um offene Zeitspanne zu schließen +startOfTimeRangeMustLieBeforeEnd=Der Anfang der Zeitspanne muss vor deren Ende liegen overallProgress=Gesamtfortschritt eventOverview=Veranstaltungsübersicht start=Start @@ -952,4 +991,11 @@ ratio=Verhältnis importWind=Wind importieren importWindTooltip=Diese Option zu deselektieren führt zu einer wesentlich kleineren übertragenen Datenmenge. authorName=Benutzername -authorPriority=Benutzer-Priorität \ No newline at end of file +authorPriority=Benutzer-Priorität +removeDenotation=Markierung entfernen +competitorRegistrationsMissingProceed=Für mindestens eins der Rennen ({0}) sind keine Teams registriert. Trotzdem fortfahren? +pleaseSelectAControlPoint=Bitte eine Einzelmark/ein Gate auswählen +twoMarkControlPoint=2-Marken-Kontrollpunkt +registeredCompetitors=Registrierte Teams +competitorPool=Team Vorrat +importFixes=Fixes importieren diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties new file mode 100755 index 00000000000..b87f4eed600 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties @@ -0,0 +1,967 @@ +helloWorld=Привет Мир +kielWeel2011=Кильская Неделя 2011 +stgAccount=Счет STG +tracTracEvents=События Трак Трак +swissTimingEvents=События Swiss Timing +trackedBefore=Ранее Отслеженные +listRaces=Перечень Гонок +wind=Ветер +windPanelLabel=Это окно информации о ветре, которое до сих пор абсолютно пустое. +refresh=Обновить +remove=Удалить +windSource=Источники информации о ветре +dampeningInterval=Интервал ослабления +map=Карта +race=Гонка +races=Гонки +tracked=Отслежена +time=Время +timeTooltip=Учет времени соперника, выступавшего на этом этапе. На первом этапе учет времени ведется с начала гонки, а не при пересечении соперником линии старта. +playSpeed=Скорость воспроизведения +playSpeedHelp=Используйте "1" для обычной скорости воспроизведения, "2" для удвоенной скорости воспроизведения и т. д. +playModeLive=В прямом эфире +playModeReplay=Режим повтора +timeDelay=Задержка +timeDelayHelp=Не указывайте, для воспроизведения повтора в любой момент времени; введите секунды (например, "60" для одной минуты или "0" без указания) задержки перед загрузкой данных в режиме реального времени +speedInKnots=Скорость (узлы) +fromDeg=Начиная с (градус) +noRacesYet=Гонок пока нет. +total=\u03A3 +carry=Суммарная +competitor=Соперник +competitors=Соперники +defaultLeaderboard=Таблица лидеров по умолчанию +noSuchLeaderboard=Такая таблица лидеров отсутствует +leaderboardConfiguration=Форма таблицы лидеров +newDotDotDot=Новый... +editScores=Изменить баллы... +columnNamesInSelectedLeaderboard=Названия гонок в выбранной таблице лидеров +addDotDotDot=Добавить... +renameDotDotDot=Переименовать... +trackedRaceConnectedToSelectedRaceName=Отслеженная гонка, соединенная с выбранным названием гонки +linkToColumn=Соединить со столбцом +unlink=Отсоединить +leaderboardName=Имя таблицы лидеров +cancel=Отмена +pleaseEnterAName=Пожалуйста, введите имя +pleaseEnterABoatClass=Пожалуйста, введите класс судна +discardRacesFromHowManyStartedRacesOn=Не учитывать еще одну начинающуюся гонку в количестве начавшихся гонок +leaderboardWithThisNameAlreadyExists=Таблица лидеров с таким именем уже существует +discardThresholdsMustBeNumeric=Предельные значения не учитываемых должны быть выражены числом +discardThresholdsMustBeAscending=Предельные значения не учитываемых должны быть возрастающими +discarding=Неучет +startingFromNumberOfRaces=Начиная с количества гонок +renameLeaderboard=Переименовать таблицу лидеров +addColumnToLeaderboard=Добавить столбец в таблицу лидеров +pleaseEnterNameForNewRaceColumn=Введите, пожалуйста, имя для столбца новой гонки +ok=OK +medalRace=Медальная гонка +renameRace=Переименовать гонку +openSelectedLeaderboard=Открыть выбранную таблицу лидеров +rank=Занимаемое место +rankTooltip=Место, занимаемое участником в гонке +regattaRank=Место, занимаемое в регате +regattaRankTooltip=Место, занимаемое участником в регате +overallRank=Итоговое место +overallRankTooltip=Место, занимаемое участником на этапах +totalRegattaRank=Итоговое место в регате +collapse=- +expand=+ +leg=L +distanceInMeters=Расстояние +averageSpeedInKnots=\u2205 Скорость +averageSpeedInKnotsTooltip=Средняя скорость относительно земли в ходе этого этапа,\nрассчитывается путем деления пройденного расстояния на затраченное на это время. +raceAverageSpeedInKnotsTooltip=Средняя скорость относительно земли в ходе этой гонки,\nрассчитывается путем деления пройденного расстояния на затраченное на это время. +rankGain=Улучшение места +rankGainTooltip=Разность между местом, занятым на текущем и предыдущем этапах. +sapSailingAnalytics=Аналитическая информация парусного спорта +leaderboard=Таблица лидеров +leaderboards=Таблицы лидеров +leaderboardSettings=Параметры таблицы лидеров +settings=Параметры +selectAtLeastOneLegDetail=Выберите хотя бы одну подробность об этапе +currentSpeedOverGroundInKnots=Скорость относительно земли (SOG) +currentSpeedOverGroundInKnotsTooltip=Текущая скорость относительно земли. +estimatedTimeToNextWaypointInSeconds=Расчетное время прибытия (ETA). +estimatedTimeToNextWaypointInSecondsTooltip= Расчетное время, необходимое участнику для прибытия в следующую промежуточную точку маршрута, исходя из скорости лавирования участника и с учетом истинного направление ветра. +gapToLeaderInSeconds=Отставание от лидера +gapToLeaderInSecondsTooltip=Время, необходимое участнику для достижения текущего положения лидера, исходя из скорости лавирования. +velocityMadeGoodInKnots=Скорость лавирования (VMG) +velocityMadeGoodInKnotsTooltip=Скорость лавирования, представляет собой скорость продвижения на ветер/ по ветру для этапов движения против ветра/ по ветру соответственно, или скорость в направлении движения при лавировании по ветру +windwardDistanceToGoInMeters=Расстояние, которое необходимо пройти на ветер +windwardDistanceToGoInMetersTooltip=Расстояние движения на ветер/ по ветру от текущего положения участника до конца этапа для этапов движения против ветра/ по ветру соответственно, или скорость в направлении движения при лавировании по ветру. +windwardDistanceToLeaderInMeters=Расстояние на ветер до лидера +windwardDistanceToLeaderInMetersTooltip=Расстояние на ветер/ под ветер от участника до лидера для этапов движения против ветра/ по ветру соответственно или скорость в направлении движения при лавировании по ветру. +name=Имя +leaveEmptyForDefault=Оставьте пустым по умолчанию. +trackNewEvent=Отследить новое событие +trackableRaces=Отслеживаемые гонки +trackWind=Отслеживать ветер +regattaName=Название регаты +eventName=Название события +event=Событие +startTime=Время старта +regatta=Регата +regattas=Регаты +startTracking=Начать отслеживание +stopTracking=Остановить отслеживание +connections=Подключения +historyOfConnections=История подключений +defineNewConnection=Определить новое подключение +trackedRaces=Отслеженные гонки +canSendRequests=Можно отправить запрос +connectAndReadRaces=Подключиться и прочитать данные о гонке +ports=Порты +liveData=Данные в реальном времени +storedData=Сохраненные данные +uris=URI-адреса +jsonUrl=JSON URL +livePort=Порт данных в реальном времени +storedPort=Порт сохраненных данных +liveUri=Данные пользователя в реальном времени +storedUri=Сохраненные данные пользователя +eventsConnectedTo=Текущие отслеживаемые события +hostname=Имя узла +declinationCheckbox=Корректировка направления ветра с помощью отклонения +raceStartTrackingColumn=Отслеживание начато +pauseAutomaticRefresh=Приостановка автоматического обновления +autoRefresh=Обновлять автоматически +delayInSeconds=Задержка (и) +delayBetweenAutoAdvances=Задержка между автоматическими обновлениями в секундах: +delayMustBeNonNegative=Значение задержки должно быть больше нуля +chooseUpdateIntervalOfAtLeastOneSecond=Выберите интервал обновления, он должен быть не менее одной секунды +errorSettingWindForRace=Ошибка при настройке параметра ветер для гонки +errorFetchingWindInformationForRace=Ошибка при получении данных о ветре для гонки +errorWhileTryingToSetWindSourceForRace=Ошибка при попытке установить флажок «начинается встречный ветер» +inEvent=В случае +to=к +showEstimatedWind=Показать оценочный ветер +timing=Хронометраж +raceDetailsToShow=Подробные сведения о гонке +overallDetailsToShow=Общие сведения +legDetailsToShow=Сведения об этапе +columnMoveUp=Вверх +columnMoveDown=Вниз +port=Порт +raceStartTimeColumn=Гонка началась +showOnlySelectedCompetitors=Показать только выбранных участников +showSelectedCompetitorsInfo=Показать окно краткого описания только для выбранных участников +tailLength=Длина (с) хвоста +selectedRaces=Какие гонки вы хотели бы отобразить? +lengthInSeconds=Длина (с) +stressTest=Стресс тест +numberOfManeuvers=Маневры +numberOfManeuversTooltip=Количество маневров, выполненных во время этого этапа. +numberOfManeuversInRaceTooltip=Количество маневров, выполненных во время этой гонки. +tacks=Галсы +jibes=Перекладывания паруса +penaltyCircles=Штрафные круги +medalRaceIsNull=Значение медальной гонки недопустимо +configuration=Конфигурация +maneuverTypes=Маневры +chooseChart=Выбрать график +distanceTraveled=Пройденное расстояние +distanceTraveledTooltip=Расстояние, пройденное от старта до финиша этапа\а не до текущего момента времени, если этап не завершен. +raceDistanceTraveledTooltip=Расстояние, пройденное от старта до финиша гонки\а не до текущего момента времени, если гонка не завершена. +velocityMadeGoodLong=Скорость лавирования +speedOverGroundLong=Скорость относительно земли +gapToLeaderLong=Отставание от лидера +pointsToLoad=Баллов для нагрузки +speed=Скорость +distance=Расстояние +in=в +secondsUnit=с +metersUnit=м +close=Закрыть +compareCompetitors=Сравнить участников +description=Описание +sailNumber=Номер на парусе +country=Страна +no3LetterCodes=Не удалось найти 3-х буквенные коды стран МОК. +add=Добавить +delete=Удалить +showCharts=Показать графики +raceWithThisNameAlreadyExists=Гонка с таким именем уже существует. +raceWithThisNameAlreadyExistsInRegatta=Гонка с таким именем уже существует в этой регате. +headUp=Заголовок +bearAway=Поворачивать под ветер +tack=Галс +tackTooltip=Количество галсов совершенных во время этой гонки. +jibe=Перебрасывать парус +jibeTooltip=Количество перебрасываний паруса во время этой гонки. +penaltyCircle=Штрафной круг +penaltyCircleTooltip=Количество штрафных кругов, пройденных в ходе этой гонки. +markPassing=Проход знака +mark=Знак +otherManeuver=Неизвестный маневр +douglasPeuckerPoints=Точки алгоритма Дугласа-Пойкера +filterRacesByName=Сортировать гонки по названию +trackSettings=Отслеживать параметры +actionEdit=Изменить +actionEditCompetitors=Изменить участников +actionEditScores=Изменить баллы +actionOpenBrowser=Открыть в браузере +actionRemove=Удалить +actionEditSeries=Изменить серии +actionRaceEdit=Изменить или переименовать гонку +actionRaceUnlink=Отсоединить гонку +actionRaceRemove=Удалить гонку +actionExportXML=Экспорт в формате XML +filterLeaderboardsByName=Сортировать таблицы лидеров по названию +actions=Действия +islinked=Соединен +yes=Да +no=Нет +hoverOverAPoint=Наведите курсор на точку для просмотра подробных сведений +noSelection=Ничего не выбрано +raceIsKnownToStartUpwind=Гонка начинается с этапа с наветренной стороны +events=События +settingsForComponent=Параметры для {0} +noEventsFound=События не обнаружены +noEventSelected=События не выбраны +noLeaderboardsFound=Талицы лидеров не обнаружены +searchEvents=Поиск событий +location=Местонахождение +from=С +until=По +onlyLiveEvents=Только события в прямом эфире +locationNotAvailable=Местонахождение недоступно +startDateNotAvailable=Дата начала недоступна +startDate=Дата начала +endDate=Дата окончания +unknownManeuver=Неизвестный маневр +tailLengthMustBePositive=Число длины хвоста должно быть больше нуля. +failedToLoadRaceInformation=Сбой при загрузке сведений о гонке: {0} +competitorPassedMarkAtDate={0} прошли {1} в {2} +valueForCompetitorAt={0} в {1}: {2} +reallyRemoveRace=Вы действительно хотите удалить гонку ''{0}''? Примечание: все записи о гонке будут также удалены без возможности восстановления! +showDetails=Показать подробности +hideLeaderboard=Скрыть таблицу лидеров +hideLeaderboards=Скрыть таблицы лидеров +showLeaderboard=Показать таблицу лидеров +hideRaces=Скрыть гонки +showMap=Показать карту +hideMap=Скрыть карту +noLeaderboardSelected=Таблица лидеров не выбрана +leaderboardGroups=Группы таблицы лидеров +filterLeaderboardGroupsByName=Сортировать группы таблицы лидеров по имени +createNewLeaderboardGroup=Создать группу таблицы лидеров +leaderboardGroupConfiguration=Конфигурация группы таблицы лидеров +legs=Этапы +currentLeg=На этапе +currentLegTooltip=Номер этапа, на котором находится участник в данный момент, начиная с 1 +leaderboardGroup=Группа таблицы лидеров +pleaseEnterNonEmptyDescription=Пожалуйста, введите непустое описание +groupWithThisNameAlreadyExists=Группа таблицы лидеров с этим именем уже существует. +detailsOfLeaderboardGroup=Сведения о группе таблицы лидеров +edit=Изменить +save=Сохранить +abort=Отмена +noLeaderboardGroupWithNameFound=Группа таблицы лидеров с именем {0} не обнаружена +overview=Обзор +welcomeToSailingAnalytics=Добро пожаловать в Аналитическую информацию парусного спорта SAP +welcomeToSailingAnalyticsBody=SAP приближает любителей парусного спорта к происходящим на воде событиям как никогда раньше.\ +\ Таблица лидеров в прямом эфире расширяет возможности для любителей, отображая в режиме реального времени место, занимаемое участником, скорости судов, а также победителей с указанием отрыва. Следите за каждой гонкой и благодаря SAP не пропускайте ни одного совершенного маневра.\ +\ Это самый простой способ наблюдать за парусной гонкой в прямом эфире, где бы вы ни находились. +home=Главная +clickAndDragToZoomIn=Щелкните и перетащите область графика для увеличения +windSpeed=Скорость ветра (узлы) +allTimesInUTC=Время указано в универсальном глобальном времени по Гринвичу (UTC) +autoZoomTo=Автоматическое масштабирование до +autoZoomToBoats=Суда +autoZoomToBuoys=Буи +autoZoomToWindSensors=Датчики ветра +autoZoomOff=Выкл +autoZoomToTails=Хвосты +autoZoomSelectedCompetitors=Масштабировать только выбранных участников +bearing=Направление (ветра) +zoom=Масштабирование +stepSizeMustBeGreaterThanNull=Размер шага должен быть больше 0 +stepSizeInSeconds=Размер шага в секундах +refreshInterval=Обновить интервал (ы) +refreshIntervalMustBeGreaterThanXSeconds=Обновление интервала должно быть больше, чем {0} секунд +competitorCharts=Графики участника +selectAtLeastOneCompetitor=Для отображения графиков участников, выберите хотя бы одного участника +directionChange=Изменение направления +speedChange=Изменение скорости +degreesShort=градус +untracked=Неотслеженная +delayForLiveMode=Задержка для режима прямого эфира: +notAvailable=Недоступно +details=Подробности +noGroupSelected=Группа не выбрана +combinedWindSourceTypeName=Комбинированная +courseBasedWindSourceTypeName=Проложенный курс +trackBasedEstimationWindSourceTypeName=Расчетный +expeditionWindSourceTypeName=Измеренный +webWindSourceTypeName=Вручную +raceCommitteeWindSourceTypeName=Гоночный комитет +clickChartToSetTime=Щелкните по графику для установки времени +position=Положение +windSourcesUsed=Использованные источники информации о ветре +errorTryingToUpdateWindSourcesToExclude=Ошибка при попытке обновления источников информации о ветре для исключения для гонки {0}: {1} +feedback=Комментарии +startStopPlaying=Начать/Остановить воспроизведение +backToLive=Вернуться к моменту времени в прямом эфире +slowPlaySpeedDown=Замедлить скорость воспроизведения или даже воспроизводить назад. +speedPlaySpeedUp=Увеличить скорость воспроизведения или снова воспроизводить вперед. +showHideComponent=Показать/ Скрыть {0} +degreesBoatToTheWind=Угол к ветру +racesInLeaderboard=Гонки в таблице лидеров +leaderboardsInGroup=Таблицы лидеров в группе +replication=Дублирование +errorFetchingReplicaData=Ошибка получения данных дублирования: {0} +averageCrossTrackErrorInMeters=\u2205 XTE +averageCrossTrackErrorInMetersTooltip=Среднее боковое уклонение от заданного курса по большой дуге окружности, соединяющей начало и конец этапа +raceAverageCrossTrackErrorInMetersTooltip=Среднее боковое уклонение от заданного курса по большой дуге окружности, соединяющей начало и конец этапа в течение этой гонки. +enterMaster=Имя узла ведущего экземпляра +errorStartingReplication=Ошибка начала повтора с узла {0} / обмена {1}: {2} +helpLines=Вспомогательные линии +startLine=Линия старта +finishLine=Линия финиша +advantageLine=Линия преимущества +courseMiddleLine=Средняя линия курса +messagingPortNumber=Номер порта обмена сообщениями ( по умолчанию 0) +servletPortNumber=Номер порта сервлета +registeredAt=Зарегистрирован в {0} +replicatingFromMaster=Дублирование с основного {0}, порт передачи обмена сообщениями {1}, порт сервлета {2} +showWindSpeedSeries=Показать данные о силе ветра +showWindDirectionSeries=Показать данные о направлении ветра +fleet=Флот +boatClass=Класс судна +setDelayToLive=Установить задержку от прямого эфира +pleaseEnterNonEmptyVenue=Введите, пожалуйста, место проведения соревнования. +pleaseEnterStartAndEndDate=Пожалуйста, введите дату начала и дату окончания. +startDateMustBeforeEndDate=Дата начала должна предшествовать дате окончания. +eventWithThisNameAlreadyExists=Событие с этим именем уже существует. +venue=Место проведения +pleaseSelectABoatClass=Выберите, пожалуйста, класс судна. +regattaWithThisNameAlreadyExists=Регата с этим именем уже существует. +seriesWithThisNameAlreadyExists=Серия с этим именем уже существует. +fleetWithThisNameAlreadyExists=Флот с этим именем уже существует. +fleets=Флоты +series=Серии +splitFleet=Разделить флот +color=Цвет +medalSeries=Медальные серии +noColor=Цвет не указан +pleaseSelectARegatta=Пожалуйста, выберите регату. +addFleet=Добавить флот +doYouReallyWantToRemoveRegatta=Вы действительно хотите удалить регату "{0}"? Это приведет к удалению без возможности восстановления всех таблиц лидеров для этой регаты! +errorLoadingScoreCorrectionProviders=Ошибка при загрузке провайдеров для корректировки очков: {0} +selectResultListToImportFrom=Выберите список результатов для импорта из +of=из +errorObtainingScoreCorrections=Ошибка получения корректировки очков от провайдера {0} и события {1} для класса судна {2} опубликованного {3}: {4} +assignRaceNumbersToRaceColumns=Назначение номеров гонок из официальных результатов столбам гонок таблицы лидеров +importOfficialResults=Импорт официальных результатов... +selectAll=Выбрать все +apply=Применить +successfullyUpdatedScores=Очки обновлены успешно +errorUpdatingScoresForLeaderboard=Ошибка обновления очков для таблицы лидеров {0}: {1} +maneuverTypesToShowWhenCompetitorIsClicked=Типы маневров для отображения при нажатии на участника +simulateWithStartTimeNow=Моделирования с "сейчас" в качестве времени начала +boatClassDoesNotMatchSelectedRegatta=Выбранные гонки содержат классы судов не соответствующие классу судов ''{0}'' выбранной регаты.\\ Вы действительно хотите использовать регату ''{1}''? +regattaExistForSelectedBoatClass=Есть только одна регата для выбранных классов судов.\\ Вы действительно хотите использовать выбор ''ни одной регаты''? +reload=Перезагрузить +addRegatta=Добавить регату... +exchangeName=Заменить имя +course=Курс +courseLayout=Проложенный курс +buoy=Буй +gate=Ворота +buoys=Буи +waypoint=Точка маршрута +showAllCompetitors=Показать всех участников +boatTails=Лодочные хвосты +resultImportUrls=URL-адреса импорта результатов +errorRefreshingResultImportUrlList=Ошибка обновления списка URL-адресов импорта результатов: {0} +successfullyUpdatedResultImportUrls=Список URL-адресов импорта результатов успешно обновлен +errorRemovingResultImportUrls=Ошибка удаления URL-адресов импорта результатов: {0} +pleaseEnterNonEmptyUrl=Пожалуйста, введите URL-адрес +addResultImportUrl=Добавить URL-адрес импорта результатов +errorAddingResultImportUrl=Ошибка добавления URL-адреса импорта результатов: {0} +errorTryingToObtainLeaderboardContents=Ошибка попытки получения содержимого таблицы лидеров: {0} +lastScoreUpdate=Обновление последних очков +warningBrowserUnsupported=Этот веб-сайт не оптимизирован для вашего браузера. Пожалуйста, выберите браузер новой версии, такой как Firefox или Chrome. +newMark=Новый знак +controlPoint=Знак/Ворота +errorUpdatingRaceCourse=Ошибка обновления курса гонки: {0} +successfullyUpdatedCourse=Курс гонки успешно обновлен +selectOneMarkOrTwoMarksForGate=Выберите одну отметку или две отметки для создания ворот +errorTryingToObtainTheMarksOfTheRace=Ошибка попытки получения отметок дистанции гонки: {0} +errorTryingToObtainRaceCourse=Ошибка попытки получения курса гонки: {0} +insertWaypointBeforeSelected=Вставьте ранее выбранную точку маршрута +insertWaypointAfterSelected=Вставьте точку маршрута, выбранную позднее +scoringSchemeLowPointSystem=Низкобальная (линейная) система подсчета очков +scoringSchemeHighPointSystem=Высокобальная система подсчета очков +scoringSchemeHighPointEssOverall=Высокобальная система подсчета, итоговые баллы экстремальной серии ESS +scoringSchemeHighPointLastBreaksTie=Высокобальная система подсчета, тай-брейк на последней гонке +scoringSchemeLowPointWinnerGetsZero=Низкобальная система подсчета очков, победитель получает ноль очков +scoringSystem=Система подсчета очков +createFlexibleLeaderboard=Создание адаптивной таблицы лидеров +createRegattaLeaderboard=Создание таблицы лидеров регаты +editFlexibleLeaderboard=Изменение адаптивной таблицы лидеров +editRegattaLeaderboard=Изменение таблицы лидеров регаты +defaultRegatta=Регата по умолчанию +useOverallLeaderboard=Использование общей таблицы лидеров +overallStandings=Общий зачет +type=Тип +actionAddRaces=Добавить гонки +racesWithNamePrefix=Гонки с именем-префиксом +maximumSpeedOverGroundInKnots=Максимальная скорость относительно земли +maximumSpeedOverGroundInKnotsTooltip=Самая большая скорость относительно земли участника, измеренная в ходе регаты. +totalTimeSailedDownwindInSeconds=Общее время под ветером +totalTimeSailedDownwindInSecondsTooltip=Общее время, которое в течение регаты участник шел под ветер. +totalTimeSailedUpwindInSeconds=Общее время против ветер +totalTimeSailedUpwindInSecondsTooltip=Общее время, которое в течение регаты участник шел против ветер. +totalTimeSailedReachingInSeconds=Общее время лавирования по ветру +totalTimeSailedReachingInSecondsTooltip=Общее время этапов лавирования по ветру участником в ходе регаты. +totalTimeSailedInSeconds=Общее время +totalTimeSailedInSecondsTooltip=Общее время, которое участник проплыл в регате. +totalDistanceTraveled=Общее расстояние +totalDistanceTraveledTooltip=Общее расстояние, пройденное во время регаты. +totalAverageSpeedOverGround=\u2205 Скорость относительно земли +totalAverageSpeedOverGroundTooltip=Средняя скорость относительно земли на протяжении регаты. +hhmmssUnit=чч:мм:сс +actionAddWindData=Добавить данные о ветре +valueMustBeBetweenMinMax=Значение ''{0}''должно быть в пределах от {1} до {2}. +optional=Дополнительно +pleaseEnterAValue=Введите, пожалуйста, значение. +latitude=Широта +longitude=Долгота +actionAddEvent=Добавить событие +publicationUrl=URL-адрес для публикации +suppress=скрыть +isPublic=является открытым +suppress=скрыть +unsuppress=Отобразить +suppressedCompetitors=Скрытые участники +mediaPanel=Аудио и Видео +url=URL +mimeType=MIME-тип +title=Заголовок +duration=Продолжительность (ч:мм:сс.мс) +addMediaTrack=Добавить аудио/видео +addYoutubeTrack=Добавить Youtube +youtubeId=Идентификатор Youtube +reallyRemoveMediaTrack=Вы действительно хотите удалить медиа трек''{0}''? +pleaseSelectASeries=Пожалуйста, выберите серию +pleaseSelectARace=Пожалуйста, выберите гонку +displayGroupsInReverseOrder=Отобразить группы в обратном порядке +actionConfigureUrl=Настроить URL-адрес +configureLeaderboardUrlDescription=Настроить таблицу лидеров и получить результирующий URL-адрес по ссылке +embedded=Внедренный +additionalUrlSettings=Дополнительные параметры URL-адреса +restrictToSingleSelectedRace=Ограничить одной выбранной гонкой +expandSelectedRace=Расширить выбранную гонку +showRaceDetails=Разрешить подробности для гонки +numberOfRaces=Количество гонок +numberOfRacesMustBeNonNegativeNumber=Пожалуйста, введите количество последних гонок, которые должны отображаться +chooseTheWayYouSelectRaces=Выберите способ выбора гонок +selectFromAllRaces=Выбрать из всех гонок +selectANumberOfRaces=Выбрать несколько гонок +numberOfLastNRaces=Количество самых последних гонок для отображения +expandLastRace=Всегда раскрывать последнюю отображенную гонку +factor=Фактор +errorUpdatingIsMedalRace=Ошибка обновления параметров медальной гонки: {0} +maneuverLoss=Потеря при выполнении маневра +averageManeuverLossInMeters=\u2205 Потеря при выполнение маневра +averageManeuverLossInMetersTooltip=Усредненная потеря расстояния при выполнении маневра. +averageTackLossInMeters=\u2205 Потеря при смене галса +averageTackLossInMetersTooltip=Усредненная потеря расстояния при смене галса. +averageJibeLossInMeters=\u2205 Потеря при перекладывании паруса +averageJibeLossInMetersTooltip=Усредненная потеря расстояния при перекладывании паруса. +shape=Форма +pattern=Модель +legend=Легенда +live=прямой эфир +swissTimingArchiveConnector=события SwissTiming, помещенные в архив +errorFetchingChartData=Ошибка получения данных графика: {0} +loadingCompetitorData=Загрузка данных участника... +competitorRegattaDataAfterRaceN={0} после гонки {1} +rankChart=Рейтинг +totalPoints=Итоговое количество очков +raceTotalPointsTooltip=Итоговое количество очков участника в гонке +regattaTotalPointsTooltip=Итоговое количество очков участника в регате +showRankChart=Показать таблицу рейтинга +overallLeaderboardSelection=Итоговая таблица лидеров +errorTryingToObtainOverallLeaderboards=Ошибка получения итоговых таблиц лидеров: {0} +finished=Завершившиеся +passedTo=Переданные в {0} +gapChangeSinceLegStartInSeconds=Изменение отставания +gapChangeSinceLegStartInSecondsTooltip=Изменение отставания от лидера с начала последнего этапа.\nОтрицательное число означает, что отставание уменьшилось;\nположительное число означает, что отставание увеличилось. +sideToWhichMarkAtLegStartWasRounded=Отметка округленная до +sideToWhichMarkAtLegStartWasRoundedTooltip=Сторона в которую округляется отметка на старте этапа. +raceIsLive=Гонка {0} в прямом эфире +racesAreLive=Гонки {0} в прямом эфире +knotsUnit=Узлы +scoringSchemeHighPointFirstGetsOne=Самый лучший результат, победитель получает 1 очко +scoringSchemeHighPointFirstGetsTen=Самый лучший результат, победитель получает 10 очков +knotsUnit=Узлы +generatePolarSheet=Создать полярную электронную таблицу +polars=Полярная диаграмма +polarSheetChart=Полярная электронная таблица +polarSheetMinimumDataSizePerGraph=Минимальное количество данных на графике +polarSheetMinimumDataSizePerGraphTooltip=Все графики с меньшим количеством точек данных визуально не отображаются.\nТаким образом, не отображаются отвлекающие внимание диаграммы для \nскоростей ветра, в которых отсутствует достаточный объем данных. +polarSheetMinimumDataSizePerAngle=Минимальное количество точек данных в расчете на угол +polarSheetMinimumDataSizePerAngleTooltip=На каждом графике будут отображаться только данные об углах с указанным числом\nбазовых точек данных. Не имеющие важного значения\nчасти графика могут быть скрыты с помощью задания этой пороговой величины. +polarSheetMinimumWindConfidence=Минимальное значение достоверных данных о ветре +polarSheetMinimumWindConfidenceTooltip=Только фиксированные значения ветра с достоверностью, превышающей указанное пороговое значение\nприменяются. Эта достоверность от 0 до 1. +polarSheetMinimumConfidenceMeasure=Минимально допустимое измерение достоверности +polarSheetMinimumConfidenceMeasureTooltip=Минимальная степень достоверности для точки, указанной на графике.\nЕсли она ниже установленного порогового значения, то точка не отображается.\nЗначение лежит от 0 до 1 и детерминируется \nс использованием статистических мер. +polarSheetUseOnlyWindGaugeData=Для определения скорости ветра используйте только данные, полученные с помощью анемометра +polarSheetUseOnlyWindGaugeDataTooltip=Благодаря учету данных о скорости ветра, полученных с помощью анемометра\nмы исключаем примерные значения скорости ветра, введенные вручную, которые могли бы исказить данные, отображенные на полярной диаграмме. +polarSheetUseOnlyEstimationData=Используйте, определенные на глаз значения только для направления ветра +polarSheetUseOnlyEstimationDataTooltip=Использование точных расчетных значений о направлении ветра,\nпозволяет нам устранить ошибки полярных диаграмм, которые вращаются из-за неправильных данных, полученных с анемометра\n. +polarSheetRemoveOutliers=Удалите сильно отличающиеся значения (Подход основан на расстоянии) +polarSheetRemoveOutliersTooltip=Сильно отличающиеся значения удаляются с помощью применения подхода, основанного на расстоянии.\nПараметры могут быть отрегулированы ниже. +polarSheetOutlierDetectionRadius=Соседний радиус, определяющий сильно отличающееся значение +polarSheetOutlierDetectionRadiusTooltip=Радиус, используемый в алгоритме определения сильно отличающегося значения\nна основании расстояния. +polarSheetOutlierDetectionMinimumPerc=Минимальный процент соседних точек, определяющий сильно отличающееся значение +polarSheetOutlierDetectionMinimumPercTooltip=Процент соседних точек данных /всего точек данных\nдолжен быть ниже указанного значения для того, чтобы считаться сильно отличающимся значением. +polarSheetNumberOfHistogramColumns=Количество столбцов гистограммы +polarSheetWindSteppingInKnots=Шаг значения ветра в узлах +polarSheetWindSteppingMaxDistance=Максимальное расстояние до уровня ветра +polarSheetWindSteppingMaxDistanceTooltip=Устанавливает максимальный радиус, для которого уровни ветра будут фиксированными. +polarSheetSplitByWindGauges=Разделить источники данных о ветре +polarSheetSplitByWindGaugesTooltip=Разделение источников данных о ветре позволяет найти неисправные анемометры.\nСтатистическая правильность не гарантируется. Такое исправление может \nбыть учтено в полярной диаграмме результатов несколько раз (для разных анемомтеров). +pleaseSeeToolTips=Пожалуйста, наведите курсор на свойства, чтобы ознакомиться с всплывающими подсказками, при их наличии. +competitorColumnTooltip=Имена членов команды этого участника. +sailIdColumnTooltip=Отображает флаг страны и номер на парусе участника. +rankColumnTooltip=Итоговое место этого участника в регате. +carryColumnTooltip=Очки, перенесенные из предыдущих гонок. +totalsColumnTooltip=Итоговое количество очков участника в регате. +windData=Данные о ветре +gpsData=GPS-данные +status=Статус +noDataFound=Данные не обнаружены +displayName=Отобразить имя +histogram=Гистограмма +numberOfDataPoints=Количество точек данных +angleAndTotalNumberOfDataPointsAndCovAndCm=Угол: {0}; Общее количество точек данных: {1}; Коэффициент отклонения: {2}; Степень достоверности: {3} +buoyZone=Зона буя +radiusInMeters=Радиус (м) +filterRegattasByName=Сортировка регат по имени +noRegattasYet=Пока регат нет. +addCourseArea=Добавить зону курса +courseAreas=Зоны курса +courseArea=Зона курса +pleaseEnterNonEmptyCourseArea=Пожалуйста, выведите зону курса. +pleaseSelectACourseArea=Пожалуйста, выберите зону курса. +radiusInMeters=Радиус (м) +lastUpperFlag=Верхний флажок +lastLowerFlag=Нижний флажок +cannotAddRacesToRegattaLeaderboardButOnlyToRegatta=Это таблица лидеров регаты. Вы можете добавлять гонки в эту таблицу лидеров только непосредственно в определении регаты. +correctScore=Исправить очки +correctScoreFor=Исправить очки для участника {0} в гонке {1} +penaltyOrRedress=Штраф/Корректировка +netScore=Чистые очки +timePointMustBeAfterStartOfTracking=Момент времени должен быть после начала отслеживания. +timePointMustBeBeforeEndOfTracking=Момент времени должен быть до окончания отслеживания. +regattaUsedForTheTrackedRace=Регата, используемая для отслеживаемой гонки +tracTracUpdateUrl=Обновление URl-адреса сервера ТракТрак +passingInstructions=Инструкции по передаче +refreshNow=Обновить сейчас +stopUpdating=Остановить обновление +startUpdating=Начать обновление +currentTime=Текущее время: +tractracUsername=Имя пользователя ТракТрак +tractracPassword=Пароль ТракТрак +operatorEquals=Равно +operatorNotEqualTo=Не равно +operatorLessThan=Меньше +operatorLessThanEquals=Меньше или равно +operatorGreaterThan=Больше чем +operatorGreaterThanEquals=Больше или равно +operatorContains=Содержит +operatorNotContains=Не содержит +operatorStartsWith=Начинается с +operatorEndsWith=Заканчивается на +nationality=Гражданство +numberMustBePositive=Цифра должна быть больше нуля. +distanceToLineAtRaceStart=Расстояние до линии «Старт» +distanceToLineFiveSecondsBeforeStart=Расстояние до линии 5с до старта +speedOverGroundAtRaceStart=Скорость относительно земли на линии «Старт» +speedOverGroundFiveSecondsBeforeStart=Скорость относительно земли на линии 5с до старта +speedOverGroundWhenPassingStart=Скорость относительно земли во время старта +distanceToLineAtRaceStartTooltip=Расстояние до стартовой линии во время начала гонки +distanceToLineFiveSecondsBeforeStartTooltip=Расстояние до стартовой линии за пять секунд до начала гонки +speedOverGroundAtRaceStartTooltip=Скорость относительно земли во время начала гонки +speedOverGroundFiveSecondsBeforeStartTooltip=Скорость относительно земли за пять секунд до начала гонки +speedOverGroundWhenPassingStartTooltip=Скорость относительно земли при пересечении участником линии старта +startTack=Сменить галс +startTackTooltip=галс, на котором участник начал +portTack=Левый галс +starboardTack=Правый галс +portSide=Левый борт +starboardSide=Правый борт +pleaseEnterANumber=Пожалуйста, введите номер. +competitorsFilter=Сортировать участников +flagStatus=Флажок состояния +additionalInformation=Информация +windFixListingDescription=Исправления для ветра (первые 3, последние 3, фактические значения) +noWindFixesAvailable=Исправления для ветра недоступны. +distanceToStarboardEndOfStartlineWhenPassingStart=Расстояние от линии до правого борта во время прохождения старта +distanceToStarboardEndOfStartlineWhenPassingStartTooltip=Расстояние до правого борта от стартовой линии или одной отметки начала, когда участник пересек линию старта +actionAddFilter=Добавить фильтр +actionEditFilter=Изменить фильтр +availableFilters=Доступные фильтры +createFilterHint=Создать набор фильтров для сортировки списка участников. +filterThisNameAlreadyExists=Фильтр с таким именем уже существует. +addAtLeastOneFilterCriteria=Добавьте хотя бы один критерий сортировки. +filterCriteria=Критерий сортировки +selectAFilterCriteria=Выбрать критерий сортировки +filterName=Имя фильтра +operator=Оператор +value=Значение +nationalityMustBeISOorIOCcode=Гражданство должно содержать двухбуквенный код МОК или трехбуквенный код МОК. +raceRank=Рейтинг гонки +waypoints=Точки маршрута +disableRaceFilter=Отключить фильтр +enableRaceFilter=Включить фильтр +raceStatusColumn=Статус гонки +loading=Загрузка (при медленном соединении с Интернетом процесс может занять несколько минут)... +racesWithHiddenState=Гонки со статусом «СКРЫТЫ» +mediaDateFormatError=Недействительный формат даты. Использовать: {0} +selectedCompetitors=Выбранные участники +filterNothing=Нечего сортировать +topNCompetitorsByRaceRank=Лидеры гонки {0} +topNCompetitorsByTotalRank=Лидеры в общем зачете {0} +simulator=Модель +optionsBar=Панель опций +simulateButton=Прогнозировать +setup=Установка +strength=Сила (в узлах) +oscillation=Колебание +oscillating=Колеблющийся +amplitude=Амплитуда(в градусах) +frequency=Частота (в 1/ч) +noise=Шум +sailing=Плавание под парусом +optimality=Оптимальность +maximumTurns=Максимальное количество галсов +raceDirection=Направление гонки +upWind=Против ветра +downWind=По ветру +strategies=Стратегии +omniscient=Всезнающий +opportunistic=Оппортунист +mixed=Смешанный +maps=Карты +startEnd=Старт-Финиш +racecourse=Курс +summary=Сводная информация +replay=Повтор +display=Отобразить +simulatorPolarHeader = Полярная диаграмма +show=Показать +hide=скрыть +leftWind=Ветер слева +rightWind=Ветер справа +strategyOption1=стратегияВариант1 +strategyOption2=стратегияВариант2 +strategyOption3=стратегияВариант3 +legLabel=Этап +raceLabel=Гонка +competitorLabel=Участник +freemode=Сохранять направления +whatIfCourse=Что если курс +editRegatta=Изменить регату... +selectSailingEvent=Пожалуйста, выберите событие парусного спорта... +selectCourseArea=Пожалуйста, выберите зону курса.. +courseAreaOverview=Обзор зоны +courseDesignOverview=Обзор создания курса +regattaOverview=Обзор регаты +additionalSettings=Дополнительные параметры +showFollowingCourseAreas=Показать следующие зоны курса +showFollowingRegattas=Показать следующие регаты +showOnlyRacesOfSameDay=Показать только гонки, проходившие в один и тот же день +showOnlyCurrentlyRunningRaces=Показать только проходящие гонки +seriesDefinesResultDiscardingRule=Серии определяют правила исключения результатов +addSeries=Все серии +regattaDefinesResultDiscardingRules=Регата определяет правила исключения результатов +startsWithZeroScore=Серия начинается с нуля очков +masterDataImportPanel=Импорт основных данных +importSuccess=Успех\nСозданная {0} группа таблиц (ы) лидеров и {1} таблиц (ы) и {2} события (й) и {3} регат (ы). +importSuccessOverwriteInfo=Пришлось перезаписать следующие регаты, необходимо повторно проложить их гонки: +regattaOverviewConfiguration=Обзор конфигурации регаты +firstRaceIsNonDiscardableCarryForward=Начиная с не исключаемых переносов в другую графу +addReplicationMaster=Подключение к главному серверу дублирования +connect=Подключиться +connectToMaster=Подключиться к Главному +exchangeHost=Имя узла обмена сообщениями +explainExchangeHostName=Имя узла, где сообщения ставятся в очередь ожидания. +explainReplicationHostname=Имя узла, где работает главный сервер дублирования. +explainReplicationExchangeName=Имя очереди ожидания обмена сообщениями. Она должна быть настроена как главный сервер. +explainReplicationExchangePort=Порт очереди ожидания обмена сообщениями. Он используется после первоначальной загрузки для всех данных. +explainReplicationServletPort=Номер порта сервлета, используемого для подключения для регистрации этой копии и получения первоначальной загрузки. +explainReplicasRegistered=Копии зарегистрированы (этот сервер главный ): +explainConnectionsToMaster=Подключения к главному серверу (это сервер копий): +explainNoConnectionsToMaster=Не настроенных подключений к главному серверу.Для настройки одного соединения щелкните «Подключиться к главному серверу» . +explainNoConnectionsFromReplicas=Подключений к серверу копий не обнаружено. Копии можно подключить используя их Консоль администрирования. +stopConnectionToMaster=Остановить копирование +asLink=Как ссылка +noStarttimeAnnouncedYet=Ожидание +raceIsRunning=Проходящая +raceIsFinishing=Завершающаяся +raceIsFinished=Завершенная +raceIsScheduled=Запланированная +raceIsInStartphase=Этап старта +generalRecall=Общая отмена +startPostponed=Отложенная +raceAbandoned=Прекращенная +stopAllReplicas=Разъединить соединение со всеми копиями (Опасно!) +warningServerIsReplica=ВНИМАНИЕ: Этот сервер настроек может быть сервером копий. НЕ ВНОСИТЕ никаких изменений или это приведет к нежелательным последствиям! +dropReplicaConnection=Разъединить соединение +timePanelName=Контроль времени +competitorFilterExplanation=Фильтр отображает всех участников, соответствующих критериям: +dropReplicaConnection=Разъединить соединения +protestTimeFinishesAt=Срок подачи протеста истекает в +protestTimeFinishedAt=Срок подачи протеста истек в +flags=Флаги +lastUpdate=Последние обновления +noMoreRacingToday=Сегодня больше нет гонок +furtherSignalsAshore=Дальнейшие сигналы на берегу +earlyStarters=Совершивших фальшстар +showingDetailsOfRace=Отобразить подробности для гонки {0} +startAt=Старт в +finishAt=Финиш в +protestEndsAt=Срок подачи протеста завершается в +unknown=Неизвестно +earlyStarters=Совершивших фальшстарт +raceLog=Запись о гонке +refreshRaceLog=Обновить запись о гонке +raceLogReloaded=Запись о гонке успешно повторно загружена из базы данных +reloadAllRaceLogs=Повторно загрузить все записи о гонке +export=Эскпорт +sideline=Боковая линия +classAndGolfUp=Флаг поднят – восемь минут до старта – процедура старта – через стартовые ворота +classDisplayed=Флаг поднят – пять минут до старта +papaDisplayed=Флаг режима старта четыре минуты до старта +blackDisplayed=Флаг режима старта – дисквалификация совершивших фальшстарт – четыре минуты до старта +zuluIndiaDisplayed=Флаг режима старта – штрафы для совершивших фальшстарт – четыре минуты до старта +papaRemoved=Флаг режима старта - одна минута до старта +blackRemoved=Флаг режима старта – дисквалификация для совершивших фальшстарт - одна минута до старта +zuluIndiaRemoved=Флаг режима старта – старт со штрафами совершившим фальшстарт – одна минута до старта +classFlagRemoved=Флаг убран – сигнал начала гонки +xray=Индивидуальная отмена – есть совершившие фальшстарт +firstSubstitute=Общая отмена – слишком много совершивших фальшстарт – процедура старта будет повторена +blueFlagDisplayed=Флаг отображается – первый участник пересек линию финиша +blueFlagRemoved=Флаг убран – последний участник пересек линию финиша или предельное время финиша прошло +golfRemoved=Флаг убран –ворота закрыты +answeringPennantOverAlphaDisplayed=Флаг отображается - старт отложен - сегодня больше нет гонок +answeringPennantOverHotelDisplayed=Флаг отображается – старт отложен - дальнейшие сигналы на берегу +answeringPennantDisplayed=Флаг отображается – старт отложен +answeringPennantRemoved=Флаг убран – процедура старта начинается через одну минуту +novemberOverAlphaDisplayed=Флаг поднят - гонка отменена – сегодня гонок больше нет +novemberOverHotelDisplayed=Флаг поднят – гонка отменена - дальнейшие сигналы на берегу +novemberDisplayed=Флаг поднят – гонка отменена +bravoDisplayed=Срок подачи протеста начался +bravoRemoved=Срок подачи протеста закончился +essThreeDisplayed=Три минуты до старта +essTwoDisplayed=Две минуты до старта +essOneDisplayed=Одна минуты до старта +essOneRemoved=Сигнал начала гонки +dataBeforeAfter=Данные до/после гонки +rawFixes=Все значения (включая сильно отличающиеся значения) +importRemoteHost=Удаленный узел: +importFetchRemoteLgs=Получить список группы таблицы лидеров +importUrlInvalid=Недействительный URL-адрес для получения групп таблицы лидеров: {0} +importGetLeaderboardsFailed=Запрос ПОЛУЧИТЬ группы таблиц лидеров выполнить не удалось для узла {0} с ошибкой {1} +importNoDataReturned=Данные сервером не возвращены +importServerError=Запрос ПОЛУЧИТЬ группы таблиц лидеров выполнить не удалось с ошибкой Сервера. Дополнительные сведения, см. в журнале сервера. +importLeaderboardGroups=Группы таблиц лидеров: +importOverrideSwitchLabel=Перезаписать существующие данные, если имена и идентификационные данные совпадают +importSelectedLeaderboardGroups=Импорт выбранных групп таблиц лидеров +importSelectAtLeastOne=Нужно выбрать хотя бы одну группу таблиц лидеров для импорта +deselectAll=Отменить выбор всех +polarDiagramButton=Полярная диаграмма +ratioBetweenTimeSinceLastPositionFixAndAverageSamplingInterval=Запаздывание GPS +ratioBetweenTimeSinceLastPositionFixAndAverageSamplingIntervalTooltip=Соотношение между последними полученными с помощью GPS данными о положении и усредненным интервалом выборки +trackingQuality=Качество отслеживания +legType=Тип этапа +sailID=Номер на парусе +seriesLeaderboard=Серия таблиц лидеров +regattaLeaderboards=Таблица лидеров регаты +clearSelection=Отменить выбор +running=Проходящая в настоящее время +runAsSubstantive=Начать +done=Готово +lastFinished=Последняя завершенная +run=Начать +times=раз +numberOfGPSFixes=Количество точек фиксации GPS +averageCleanedServerTime=\u2205 Чистое время на сервере +averageCleanedOverallTime=\u2205 Чистое итоговое время +dataMiningBenchmarkResults=Результаты сравнительного теста сбора данных +serverTime=Время на сервере +confidenceShouldBeBetween=Достоверность данных должна быть от 0 до 1. +minDataValuesNeedToBeAtLeastZero=Минимальные значения объема данных не могут быть отрицательными. +numberOfColumnsAtLeast2=Количество столбцов гистограммы должно быть больше одного. +outlierRadiusNeedsToBePositiveString=Сильно отличающееся значение радиуса должно быть положительным числом. +outlierMinimumNeighborhoodPctString=Сильно отличающееся значение в процентах соседней точки должно быть положительным и максимально равно 1. +oneOrMoreParametersAreEmpty=Поле параметра не может быть пустым. +errorWhileAddingSeriesToChart=При добавлении \nновой серии в график возникла ошибка +generatingPolarSheet=Создание полярной таблицы... +dataCount=Объем данных +generationFinished=Создание завершено! +generating=Создание +removeAllSheets=Удалить все листы +removeAll=Удалить все +selectSheet=Выбрать лист +cleanedServerTime=Чистое время на сервере +overallTime=Общее время +cleanedOverallTime=Чистое итоговое время +dataMiningResult=Результат сбора данных +groupBy=Группировать по +statisticToCalculate=Рассчитать статистику +queryResultsChartSubtitle=Обработанные {1}/{0} элементы данных через {2} секунд +noQuerySelected=Запросы не выбраны +runAutomatically=Запустить автоматически +windImport_Upload=Загрузить +windImport_Title=Импортировать данные о ветре от экспедиции +windImport_BoatId=Идентификационный номер судна: +windImport_SelectedRacesWarning=Вы действительно хотите импортировать данные о ветре в выбранные {0} гонки?\nЭто в любом случае повлияет только на гонки с применимым временным окном. +windImport_AllRacesWarning=Вы действительно хотите импортировать данные о ветре во все гонки?\nЭто в любом случае повлияет только на гонки с применимым временным окном. +windImport_ResultHeader=Импортированные данные с: {0} по: {1}. {2} повлияли на гонки. +windImport_ResultEntry=Гонка: {0} ({1}) - {2} корректировка с: {3} по: {4} +windImport_ResultError=Ошибка: {0} +showWindSpeedSeriesTooltip=Переключает видимость скорости ветра для всех источников данных +showWindDirectionSeriesTooltip=Переключает видимость направления ветра для всех источников данных +combinedWindSourceTypeTooltip=Объединяет данные из всех других источников данных о ветре в один набор данных +courseBasedWindSourceTypeTooltip=Значение основано на прокладке курса гонки (с учетом предположения, что гонка начнется с этапа против ветра) +trackBasedEstimationWindSourceTypeTooltip=Расчетные данные из маршрута участника +expeditionWindSourceTypeTooltip=Данные, полученные из мобильных систем измерения +webWindSourceTypeTooltip=Значения данных, установленные вручную +raceCommitteeWindSourceTypeTooltip=Значения данных, установленные вручную, полученные от официальных лиц гонки +sheetName=Имя листа +sheetNameTooltip=Вы можете присвоить имя своему полярному листу используя текстовое поле.\nИмя будет создано с использованием класса судна\nесли имя не предоставлено. +timeDownwind=Время по ветру +timeReaching=Время лавирования по ветру +timeUpwind=Время против ветра +raceTimeTooltip=Общее время затраченное в этой гонке +raceTimeDownwindTooltip=Общее время движения по ветру в этой гонке +raceTimeReachingTooltip=Общее время лавирования по ветру в этой гонке +raceTimeUpwindTooltip=Общее время движения против ветра в этой гонке +noStatisticSelectedError=Статистических данных для расчета не выбрано +noCustomGrouperScriptTextError=Скрипт блока группирования пустой +noDimensionToGroupBySelectedError=Размер для группировки по не выбран +noGrouperSelectedError=Тип блока группирования не выбран +queryNotValidBecause=Запрос не возможен, т.к. +dataMining=Сбор данных +errorRunningDataMiningQuery=Ошибка при выполнении запроса +hideToolbar=Скрыть панель инструментов +showSeriesLeaderboards=Показать таблицу лидеров серии +showOverallLeaderboard=Показать общую таблицу лидеров +lineAngleToWindAndAdvantage=длина: {0}м; угол к ветру: {1}градус; {2} борт {3}м имеет преимущество +doYouReallyWantToRemoveEvents=Вы действительно хотите удалить все события? +doYouReallyWantToRemoveEvent=Вы действительно хотите удалить событие {0}? +line=Строка +offset=Смещение +doYouReallyWantToRemoveCompetitors=Вы действительно хотите удалить всех выбранных участников? +doYouReallyWantToRemoveCompetitor=Вы действительно хотите удалить выбранного участника {0}? +filterCompetitors=Сортировать по имени, классу судна или номеру на парусе +editCompetitor=Изменить участника +pleaseEnterASailNumber=Пожалуйста, введите номер на парусе +successfullyAllowedCompetitorReset=Перезагрузка участника (ов) {0} разрешена. +id=Идентификатор +allowReload=Разрешить перезагрузку +compress=Сжать +compressTooltip=Используйте только, если на экземпляре сервера, с которого выполняется экспорт, хотя бы\nзапущена операция подтверждения 0fbf6071dea125bec4a56dee55d61c99def4a62e. +queryRunner=Отправка запросов +rerunQueryAfterRefresh=Повторно выполнить запрос после обновления +refreshIntervalMustntBeEmpty=Интервал обновления не может быть пустым +selectionTables=Выбор таблиц +addConfiguration=Добавить конфигурацию +noConfigurations=Конфигурации отсутствуют. +errorRetrievingConfiguration=Ошибка получения данных о конфигурации с сервера. +allowedCourseAreas=Разрешить зоны курса +minimumRoundsForCourse=Минимальное количество кругов для курса гонки +maximumRoundsForCourse=Максимальное количество кругов для курса гонки +resultsMailRecipient=Результаты получателя почты +deviceConfiguration=Конфигурация устройства +none=Ни одного +courseDesignerMode=Создатель курсов +racingProcedure=Процедура гонки +courseNames=Имена курсов гонок +generate=Создать +dontoverwrite=Не перезаписывать +setRacingProcedureConfiguration=Настроить конфигурацию регаты +racingProcedureConfiguration=Конфигурация регаты +activateIndividualRecall=Активировать индивидуальную отмену +rrs26Start=RRS26 старт +setConfiguration=Активировать конфигурацию +classFlag=Флаг класса +classFlagHelpText=Устанавливает флаг класса, используемый для {0}. Выберите ''Не перезаписывать''для активации флага по умолчанию для процедуры. +individualRecallHelpText=После активации можно обратиться к официальному лицу гонки для индивидуальной отмены. +startmodeFlags=Флаги режима старта +startmodeFlagsHelpText=Устанавливает доступные флаги для режима старта. Выберите «ни один» из предложенных, для включения флагов по умолчанию для этой процедуры. +gateStart=Ворота старта +activatePathfinder=Активировать «Следопыт» +pathfinderHelpText=После активации официальное лицо гонки может настроить судно «Следопыт». +essStart=Старт ESS +basicStart=Основной старт +configured=Настроенный +overwriteRacingProceduresConfiguration=Активировать конфигурацию регаты +overwriteRacingProceduresConfigurationHelpText=Включение этой опции предоставляет базовую конфигурацию регаты для всех опций, не настроенных в специальной конфигурации регаты. +setStartTime=Установить время старта +currentPass=Текущий пропуск +setStartTimeDescription=Выбрать новое время старта для гонки +hasAdditionalGolfDownTime=Активировать дополнительное время отсчета +additionalGolfDownTimeHelpText=Активировать дополнительное время отсчета +invalidColor=Недействительный цвет: {0} +igtimiAccounts=Учетные записи Igtimi +errorFetchingIgtimiAccounts=Ошибка получения всех Igtimi учетных записей: {0} +addIgtimiAccount=Добавить учетную запись Igtimi +errorAuthorizingAccessToIgtimiUser=Ошибка авторизации доступа к Igtimi пользователю {0}: {1} +successfullyAuthorizedAccessToIgtimiUser=Успешная авторизация доступа к Igtimi пользователю {0} +couldNotAuthorizedAccessToIgtimiUser=Невозможно авторизовать пользователя {0}; недействительное имя пользователя или пароль +errorGettingIgtimiAuthorizationUrl=Ошибка получения URL-адреса для авторизации Igtimi: {0} +addIgtimiUser=Добавить пользователя Igtimi +doYouReallyWantToRemoveLeaderboards=Вы действительно хотите удалить таблицы лидеров? +errorTryingToRemoveIgtimiAccount=Ошибка попытки удаления учетной записи Igtimi для пользователя {0} +successfullyRemoveIgtimiAccount=Учетная запись Igtimi для пользователя {0} удалена успешно +eMailMustNotBeEmpty=Адрес электронной почты не должен быть пустым +emailAddress=адрес электронной почты +password=Пароль +doYouReallyWantToRemoveLeaderboard=Вы действительно хотите удалить таблицу лидеров: ''{0}''? +metaLeaderboardCannotBeChanged=Это мета таблица лидеров. Ее нельзя изменить здесь. +failedToSetNewStartTime=Ошибка при установке нового времени старта. +doYouReallyWantToRemoveIgtimiAccount=Вы действительно хотите удалить учетную запись Igtimi, предоставляющую доступ к данным пользователя {0}? +igtimiWindImport=Импорт данных о ветре из Igtimi +seeIgtimiTabForAccountSettings=См. вкладку ''Igtimi'' для параметров учетной записи, определяющих какие данные из Igtimi будут учитываться. +importWindFromIgtimi=Импорт данных о ветре из Igtimi +errorImportingIgtimiWind=Ошибка импорта данных о ветре из Igtimi: {0} +resultFromIgtimiWindImport=Следующее количество фиксированных точек было импортировано для перечисленных гонок: {0} +timeToStart={0} до старта +timeSinceStart={0} со старта +distanceToLine=Растояние до линии старта +beatAngleTooltip=Угол между направлением движения участника и ветром +beatAngle=Угол лавирования +runAutomaticallyTooltip=Запустить запрос автоматически, например, после изменения статистических данных или группировки. +rerunQueryAfterRefreshTooltip=Повторно запускает запрос после обновления таблиц. +queryDefinitionProvider=Поставщик определения запроса +statisticProvider=Поставщик статистических данных +toCalculateThe=для расчета +use=Использовать +groupingProvider=Поставщик группировки +releaseNotes=Новости и история выпусков +hasSplitFleetContiguousScoring=Разделить флоты, имеющие близкие количества очков +overallProgress=Итоговое улучшение +eventOverview=Обзор события +start=Старт +showAddedScores=Показать добавленные очки для каждой записи таблицы лидеров +ratio=коэффициент +importWind=Имопрт данных о ветре +importWindTooltip=Снятие флажка с этой опции значительно снизит объем передаваемых данных. +authorName=Имя автора +authorPriority=Приоритет автора +removeDenotation=todo +competitorRegistrationsMissingProceed=todo +pleaseSelectAControlPoint=todo +twoMarkControlPoint=todo +registeredCompetitors=todo +competitorPool=todo +selectAtLeastOne=todo +selectRaces=todo +importFixes=todo diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractChartSettingsComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractChartSettingsComponent.java index e1e7eca5cd9..86e38762ff6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractChartSettingsComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractChartSettingsComponent.java @@ -5,8 +5,8 @@ import com.google.gwt.user.client.ui.FocusWidget; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.gwt.ui.client.shared.components.SettingsDialogComponent; import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.components.SettingsDialogComponent; import com.sap.sse.gwt.client.dialog.DataEntryDialog; import com.sap.sse.gwt.client.dialog.DataEntryDialog.Validator; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorLeaderboardChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorLeaderboardChart.java index 0123c90f005..da1b2b0303f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorLeaderboardChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorLeaderboardChart.java @@ -159,6 +159,7 @@ public abstract class AbstractCompetitorLeaderboardChart extends A break; } chart.setTitle(new ChartTitle().setText(DetailTypeFormatter.format(selectedDetailType)), null); + chart.ensureDebugId("CompetitorChart"); final String unit = DetailTypeFormatter.getUnit(getSelectedDetailType()); final String label = unit.isEmpty() ? "" : "[" + unit + "]"; chart.getYAxis().setAxisTitleText(label); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java index 4a5b8f03ac1..7766b06852a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractCompetitorRaceChart.java @@ -13,12 +13,12 @@ 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.PlotLine.DashStyle; 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.PlotLine.DashStyle; import org.moxieapps.gwt.highcharts.client.events.ChartClickEvent; import org.moxieapps.gwt.highcharts.client.events.ChartClickEventHandler; import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartPointRecalculator.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartPointRecalculator.java new file mode 100644 index 00000000000..6566d0ffec0 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/ChartPointRecalculator.java @@ -0,0 +1,58 @@ +package com.sap.sailing.gwt.ui.client.shared.charts; + +import org.moxieapps.gwt.highcharts.client.Point; + +public final class ChartPointRecalculator { + + private ChartPointRecalculator() { } + + public static Point stayClosestToPreviousPoint(Point previousPoint, Point newPoint) { + double previousY = previousPoint.getY().doubleValue(); + double newY = newPoint.getY().doubleValue(); + + double bestNewY = newY; + boolean hasBeenMoved; + do { + hasBeenMoved = false; + double deltaPreviousYToNewY = Math.abs(previousY - bestNewY); + double deltaPreviousYToNewYUp = Math.abs(previousY - (bestNewY + 360)); + double deltaPreviousYToNewYDown = Math.abs(previousY - (bestNewY - 360)); + + if (deltaPreviousYToNewYUp < deltaPreviousYToNewY || + deltaPreviousYToNewYDown < deltaPreviousYToNewY) { + bestNewY = deltaPreviousYToNewYUp <= deltaPreviousYToNewYDown ? + bestNewY + 360 : bestNewY - 360; + hasBeenMoved = true; + } + } while (hasBeenMoved); + + return new Point(newPoint.getX(), bestNewY); + } + + public static Point keepOverallDeltaMinimal(Double yMin, Double yMax, Point newPoint) { + double y = newPoint.getY().doubleValue(); + + if (yMax != null && yMin != null && (y < yMin || y > yMax)) { + double deltaMin = Math.abs(yMin - y); + double deltaMax = Math.abs(yMax - y); + + double yDown = y - 360; + double deltaMinDown = Math.abs(yMin - yDown); + + double yUp = y + 360; + double deltaMaxUp = Math.abs(yMax - yUp); + + if (isRecalculationNeeded(deltaMin, deltaMax, deltaMinDown, deltaMaxUp)) { + y = deltaMaxUp <= deltaMinDown ? yUp : yDown; + return new Point(newPoint.getX(), y); + } + } + + return newPoint; + } + + private static boolean isRecalculationNeeded(double deltaMin, double deltaMax, double deltaMinDown, double deltaMaxUp) { + return (deltaMinDown < deltaMin || deltaMinDown < deltaMax) || (deltaMaxUp < deltaMin || deltaMaxUp < deltaMax); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorLeaderboardChartSettingsDialogComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorLeaderboardChartSettingsDialogComponent.java index 327fbbb90bf..8b81686a151 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorLeaderboardChartSettingsDialogComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/MultiCompetitorLeaderboardChartSettingsDialogComponent.java @@ -49,6 +49,7 @@ public class MultiCompetitorLeaderboardChartSettingsDialogComponent implements S Label chartSelectionLabel = new Label(stringMessages.chooseChart()); mainPanel.add(chartSelectionLabel); chartTypeSelectionListBox = dialog.createListBox(/* isMultiSelect */false); + chartTypeSelectionListBox.ensureDebugId("ChartTypeListBox"); int i = 0; for (DetailType detailType : availableDetailsTypes) { chartTypeSelectionListBox.addItem(DetailTypeFormatter.format(detailType), detailType.name()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java index 33dd2ed3a9b..c2ee6e565ce 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChart.java @@ -15,12 +15,12 @@ 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.PlotLine; +import org.moxieapps.gwt.highcharts.client.PlotLine.DashStyle; 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.PlotLine.DashStyle; import org.moxieapps.gwt.highcharts.client.events.ChartClickEvent; import org.moxieapps.gwt.highcharts.client.events.ChartClickEventHandler; import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; @@ -42,15 +42,14 @@ import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.impl.ColorMapImpl; import com.sap.sailing.domain.common.impl.Util; -import com.sap.sailing.domain.common.impl.Util.Pair; import com.sap.sailing.gwt.ui.actions.GetWindInfoAction; 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.WindSourceTypeFormatter; import com.sap.sailing.gwt.ui.client.shared.components.Component; import com.sap.sailing.gwt.ui.client.shared.components.SettingsDialogComponent; -import com.sap.sailing.gwt.ui.client.WindSourceTypeFormatter; import com.sap.sailing.gwt.ui.shared.WindDTO; import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; import com.sap.sailing.gwt.ui.shared.WindTrackInfoDTO; @@ -73,7 +72,6 @@ public class WindChart extends AbstractRaceChart implements Component windSourceSpeedSeries; private final Map windSourceDirectionPoints; private final Map windSourceSpeedPoints; - private Pair overallDirectionMinMax; private Long timeOfEarliestRequestInMillis; private Long timeOfLatestRequestInMillis; @@ -96,7 +94,6 @@ public class WindChart extends AbstractRaceChart implements Component(); windSourceDirectionPoints = new HashMap(); windSourceSpeedPoints = new HashMap(); - overallDirectionMinMax = new Pair(null, null); colorMap = new ColorMapImpl(); chart = new Chart() .setPersistent(true) @@ -327,8 +324,8 @@ public class WindChart extends AbstractRaceChart implements Component directionMax) { - directionMax = direction; - } - if (directionMin == null || direction < directionMin) { - directionMin = direction; - } - Point newSpeedPoint = new Point(wind.requestTimepoint, wind.dampenedTrueWindSpeedInKnots); speedPoints[currentPointIndex++] = newSpeedPoint; } } - overallDirectionMinMax = new Pair(directionMin, directionMax); Point[] newDirectionPoints; Point[] newSpeedPoints = null; @@ -430,7 +422,6 @@ public class WindChart extends AbstractRaceChart implements Component(null, null); windSourceDirectionPoints.clear(); windSourceSpeedPoints.clear(); @@ -451,7 +442,8 @@ public class WindChart extends AbstractRaceChart implements Component() { @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChartPointRecalculator.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChartPointRecalculator.java deleted file mode 100644 index 13e0ea69965..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChartPointRecalculator.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.sap.sailing.gwt.ui.client.shared.charts; - -import org.moxieapps.gwt.highcharts.client.Point; - -public final class WindChartPointRecalculator { - - private WindChartPointRecalculator() { } - - public static Point recalculateDirectionPoint(Double yMin, Double yMax, Point directionPoint) { - double y = directionPoint.getY().doubleValue(); - - if (yMax != null && yMin != null && (y < yMin || y > yMax)) { - double deltaMin = Math.abs(yMin - y); - double deltaMax = Math.abs(yMax - y); - - double yDown = y - 360; - double deltaMinDown = Math.abs(yMin - yDown); - - double yUp = y + 360; - double deltaMaxUp = Math.abs(yMax - yUp); - - if (isRecalculationNeeded(deltaMin, deltaMax, deltaMinDown, deltaMaxUp)) { - y = deltaMaxUp <= deltaMinDown ? yUp : yDown; - return new Point(directionPoint.getX(), y); - } - } - - return directionPoint; - } - - private static boolean isRecalculationNeeded(double deltaMin, double deltaMax, double deltaMinDown, double deltaMaxUp) { - return (deltaMinDown < deltaMin || deltaMinDown < deltaMax) || (deltaMaxUp < deltaMin || deltaMaxUp < deltaMax); - } - -} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/components/CollapsablePanelHeader.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/components/CollapsablePanelHeader.java index b4bae949958..4dfd4b30fa5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/components/CollapsablePanelHeader.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/components/CollapsablePanelHeader.java @@ -1,12 +1,12 @@ package com.sap.sailing.gwt.ui.client.shared.components; +import com.google.gwt.dom.client.Element; import com.google.gwt.event.logical.shared.CloseEvent; import com.google.gwt.event.logical.shared.CloseHandler; import com.google.gwt.event.logical.shared.OpenEvent; import com.google.gwt.event.logical.shared.OpenHandler; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.user.client.DOM; -import com.google.gwt.dom.client.Element; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HasText; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/components/ImagesBarCell.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/components/ImagesBarCell.java index 1485b968509..86f6d744521 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/components/ImagesBarCell.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/components/ImagesBarCell.java @@ -31,6 +31,8 @@ public abstract class ImagesBarCell extends AbstractSafeHtmlCell { */ private final static ImagesBarTemplates imageTemplate = GWT.create(ImagesBarTemplates.class); + private Context context; + protected class ImageSpec { private final AbstractImagePrototype imagePrototype; private final String actionName; @@ -138,9 +140,15 @@ public abstract class ImagesBarCell extends AbstractSafeHtmlCell { } protected abstract Iterable getImageSpecs(); + + protected Context getContext() { + return context; + } @Override protected void render(com.google.gwt.cell.client.Cell.Context context, SafeHtml data, SafeHtmlBuilder sb) { + this.context = context; + /* * Always do a null check on the value. Cell widgets can pass null to * cells if the underlying data contains a null, or if the data arrives diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/listedit/ExpandedListEditorUi.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/listedit/ExpandedListEditorUi.java index 63885cc8b52..0a449e282a0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/listedit/ExpandedListEditorUi.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/listedit/ExpandedListEditorUi.java @@ -30,7 +30,8 @@ public abstract class ExpandedListEditorUi extends ListEditorUi extends ListEditorUi extends ListEditorUi extends HorizontalPanel { - - protected Iterable all; - protected final AbstractCellTable display; - protected final ListDataProvider filtered; - protected final TextBox textBox; + protected Collection all; + protected final AbstractCellTable display; + protected final ListDataProvider filtered; + protected final TextBox textBox; + private final AbstractListFilter filterer = new AbstractListFilter(){ - @Override public Iterable getStrings(T t) { return getSearchableStrings(t); } }; - - public AbstractFilterablePanel(Iterable all, AbstractCellTable display, final ListDataProvider filtered) { + + public AbstractFilterablePanel(Iterable all, AbstractCellTable display, final ListDataProvider filtered) { setSpacing(5); this.display = display; this.filtered = filtered; this.textBox = new TextBox(); this.textBox.ensureDebugId("FilterTextBox"); - this.all = all; + setAll(all); add(getTextBox()); getTextBox().addKeyUpHandler(new KeyUpHandler() { @Override @@ -53,6 +55,19 @@ public abstract class AbstractFilterablePanel extends HorizontalPanel { } }); } + + private void setAll(Iterable all) { + if (all instanceof Collection) { + this.all = (Collection) all; + } else { + this.all = new ArrayList(); + if (all != null) { + for (T t : all) { + this.all.add(t); + } + } + } + } /** * Subclasses must implement this to extract the strings from an object of type T based on which the @@ -70,7 +85,15 @@ public abstract class AbstractFilterablePanel extends HorizontalPanel { */ public void updateAll(Iterable all){ - this.all = all; + setAll(all); + filter(); + } + + /** + * Adds an object and applies the search filter. + */ + public void add(T object){ + all.add(object); filter(); } @@ -87,4 +110,8 @@ public abstract class AbstractFilterablePanel extends HorizontalPanel { public TextBox getTextBox() { return textBox; } + + public Collection getAll() { + return all; + } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/CombinedWindPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/CombinedWindPanel.java index f3b9dcbab55..396c9733d20 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/CombinedWindPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/CombinedWindPanel.java @@ -53,7 +53,7 @@ public class CombinedWindPanel extends FlowPanel { protected void redraw() { if (windTrackInfoDTO != null) { - if (windTrackInfoDTO.windFixes.size() > 0) { + if (!windTrackInfoDTO.windFixes.isEmpty()) { WindDTO windDTO = windTrackInfoDTO.windFixes.get(0); double speedInKnots = windDTO.dampenedTrueWindSpeedInKnots; double windFromDeg = windDTO.dampenedTrueWindFromDeg; @@ -73,10 +73,6 @@ public class CombinedWindPanel extends FlowPanel { } } - public WindTrackInfoDTO getWindTrackInfoDTO() { - return windTrackInfoDTO; - } - public void setWindInfo(WindTrackInfoDTO windTrackInfoDTO, WindSource windSource) { this.windTrackInfoDTO = windTrackInfoDTO; this.windSource = windSource; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index f63795a06b6..9b6280e388a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -69,6 +69,7 @@ import com.sap.sailing.domain.common.impl.RGBColor; 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.scalablevalue.impl.ScalableBearing; import com.sap.sailing.gwt.ui.actions.GetRaceMapDataAction; import com.sap.sailing.gwt.ui.actions.GetWindInfoAction; import com.sap.sailing.gwt.ui.adminconsole.DateAndTimeFormatterUtil; @@ -491,7 +492,8 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe windSourceTypeNames.add(WindSourceType.EXPEDITION.name()); windSourceTypeNames.add(WindSourceType.COMBINED.name()); - GetWindInfoAction getWindInfoAction = new GetWindInfoAction(sailingService, race, newTime, 1000L, 1, windSourceTypeNames); + GetWindInfoAction getWindInfoAction = new GetWindInfoAction(sailingService, race, newTime, 1000L, 1, windSourceTypeNames, + /* onlyUpToNewestEvent==false means get us any data we can get by a best effort */ false); asyncActionsExecutor.execute(getWindInfoAction, GET_WIND_DATA_CATEGORY, new AsyncCallback() { @Override public void onFailure(Throwable caught) { @@ -778,7 +780,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe private void showAdvantageLine(Iterable competitorsToShow, Date date) { if (map != null && lastRaceTimesInfo != null && quickRanks != null && lastCombinedWindTrackInfoDTO != null - && lastCombinedWindTrackInfoDTO.windFixes.size() > 0) { + && !lastCombinedWindTrackInfoDTO.windFixes.isEmpty()) { boolean drawAdvantageLine = false; if (settings.getHelpLinesSettings().isVisible(HelpLineTypes.ADVANTAGELINE)) { // find competitor with highest rank @@ -1480,16 +1482,18 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe } else { closer = fixAfter; } - Date betweenDate = new Date((fixBefore.timepoint.getTime() + fixAfter.timepoint.getTime()) / 2); - PositionDTO betweenPosition = new PositionDTO((fixBefore.position.latDeg + fixAfter.position.latDeg)/2, - (fixBefore.position.lngDeg + fixAfter.position.lngDeg)/2); - double betweenBearing = new DegreeBearingImpl(fixBefore.speedWithBearing.bearingInDegrees).middle( - new DegreeBearingImpl(fixAfter.speedWithBearing.bearingInDegrees)).getDegrees(); + // now compute a weighted average depending on the time difference to "date" (see also bug 1924) + double factorForAfter = (double) (date.getTime()-fixBefore.timepoint.getTime()) / (double) (fixAfter.timepoint.getTime() - fixBefore.timepoint.getTime()); + double factorForBefore = 1-factorForAfter; + PositionDTO betweenPosition = new PositionDTO(factorForBefore*fixBefore.position.latDeg + factorForAfter*fixAfter.position.latDeg, + factorForBefore*fixBefore.position.lngDeg + factorForAfter*fixAfter.position.lngDeg); + double betweenBearing = new ScalableBearing(new DegreeBearingImpl(fixBefore.speedWithBearing.bearingInDegrees)). + multiply(factorForBefore).add(new ScalableBearing(new DegreeBearingImpl(fixAfter.speedWithBearing.bearingInDegrees)). + multiply(factorForAfter)).divide(1).getDegrees(); SpeedWithBearingDTO betweenSpeed = new SpeedWithBearingDTO( - (fixBefore.speedWithBearing.speedInKnots + fixAfter.speedWithBearing.speedInKnots)/2, + factorForBefore*fixBefore.speedWithBearing.speedInKnots + factorForAfter*fixAfter.speedWithBearing.speedInKnots, betweenBearing); - // TODO move SpeedWithBearing and GPSFix with implementation classes to com.sap.sailing.domain.common and share in GWT bundle - result = new GPSFixDTO(betweenDate, betweenPosition, betweenSpeed, closer.degreesBoatToTheWind, + result = new GPSFixDTO(date, betweenPosition, betweenSpeed, closer.degreesBoatToTheWind, closer.tack, closer.legType, fixBefore.extrapolated || fixAfter.extrapolated); } } else { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapSettingsDialogComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapSettingsDialogComponent.java index 3b9df175ac6..14f5e8eb0b9 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapSettingsDialogComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapSettingsDialogComponent.java @@ -19,11 +19,11 @@ import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.domain.common.ManeuverType; import com.sap.sailing.domain.common.impl.Util.Pair; +import com.sap.sailing.gwt.ui.client.ManeuverTypeFormatter; +import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.shared.components.SettingsDialogComponent; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapHelpLinesSettings.HelpLineTypes; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapZoomSettings.ZoomTypes; -import com.sap.sailing.gwt.ui.client.ManeuverTypeFormatter; -import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sse.gwt.client.dialog.DataEntryDialog; import com.sap.sse.gwt.client.dialog.DataEntryDialog.Validator; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/DataMiningEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/DataMiningEntryPoint.java index a439f87d4de..0370c159fa5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/DataMiningEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/DataMiningEntryPoint.java @@ -15,10 +15,10 @@ import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.RootLayoutPanel; import com.google.gwt.user.client.ui.SplitLayoutPanel; import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.datamining.shared.impl.SimpleQueryDefinition; import com.sap.sailing.datamining.shared.DataTypes; import com.sap.sailing.datamining.shared.DimensionIdentifier; import com.sap.sailing.datamining.shared.StatisticType; +import com.sap.sailing.datamining.shared.impl.SimpleQueryDefinition; import com.sap.sailing.gwt.ui.client.AbstractEntryPoint; import com.sap.sailing.gwt.ui.client.GlobalNavigationPanel; import com.sap.sailing.gwt.ui.client.LogoAndTitlePanel; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/GroupingProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/GroupingProvider.java index 419bf891334..88ff9ef88c2 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/GroupingProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/GroupingProvider.java @@ -2,8 +2,8 @@ package com.sap.sailing.gwt.ui.datamining.client; import java.util.Collection; -import com.sap.sailing.datamining.shared.QueryDefinition; import com.sap.sailing.datamining.shared.DimensionIdentifier; +import com.sap.sailing.datamining.shared.QueryDefinition; import com.sap.sailing.gwt.ui.client.shared.components.Component; import com.sap.sse.datamining.shared.components.GrouperType; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/SelectionProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/SelectionProvider.java index 96feba89d13..1acaf20a9c1 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/SelectionProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/SelectionProvider.java @@ -3,8 +3,8 @@ package com.sap.sailing.gwt.ui.datamining.client; import java.util.Collection; import java.util.Map; -import com.sap.sailing.datamining.shared.QueryDefinition; import com.sap.sailing.datamining.shared.DimensionIdentifier; +import com.sap.sailing.datamining.shared.QueryDefinition; import com.sap.sailing.gwt.ui.client.shared.components.Component; public interface SelectionProvider extends Component { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/selection/AbstractQueryDefinitionProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/selection/AbstractQueryDefinitionProvider.java index ec8f1617bf3..3d39d762474 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/selection/AbstractQueryDefinitionProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/selection/AbstractQueryDefinitionProvider.java @@ -5,8 +5,8 @@ import java.util.Collection; import java.util.HashSet; import java.util.Set; -import com.sap.sailing.datamining.shared.QueryDefinition; import com.sap.sailing.datamining.shared.DimensionIdentifier; +import com.sap.sailing.datamining.shared.QueryDefinition; import com.sap.sailing.gwt.ui.client.ErrorReporter; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/selection/RestrictedGroupingProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/selection/RestrictedGroupingProvider.java index 9d51d5ebbed..ef6bbd69f42 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/selection/RestrictedGroupingProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/selection/RestrictedGroupingProvider.java @@ -14,9 +14,9 @@ import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.ValueListBox; import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.datamining.shared.QueryDefinition; import com.sap.sailing.datamining.shared.DimensionIdentifier; import com.sap.sailing.datamining.shared.DimensionIdentifier.OrdinalComparator; +import com.sap.sailing.datamining.shared.QueryDefinition; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.shared.components.SettingsDialogComponent; import com.sap.sailing.gwt.ui.client.shared.components.SimpleValueListBox; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/AbstractLeaderboardViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/AbstractLeaderboardViewer.java index 7efacc69f49..9dd56e6f9ca 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/AbstractLeaderboardViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/AbstractLeaderboardViewer.java @@ -11,6 +11,7 @@ import com.google.gwt.user.client.ui.CheckBox; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.SimplePanel; import com.sap.sailing.gwt.ui.client.CompetitorSelectionModel; +import com.sap.sailing.gwt.ui.client.DebugIdHelper; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.shared.components.Component; import com.sap.sailing.gwt.ui.client.shared.components.SettingsDialog; @@ -20,8 +21,8 @@ import com.sap.sse.gwt.client.player.Timer; /** * A base class for a leaderboard viewer. + * * @author Frank Mittag (c163874) - * */ public abstract class AbstractLeaderboardViewer extends SimplePanel { protected final StringMessages stringMessages; @@ -49,69 +50,62 @@ public abstract class AbstractLeaderboardViewer extends SimplePanel { protected FlowPanel createViewerPanel() { FlowPanel mainPanel = new FlowPanel(); mainPanel.setSize("100%", "100%"); - getElement().getStyle().setMarginLeft(12, Unit.PX); getElement().getStyle().setMarginRight(12, Unit.PX); - - if(!hideToolbar) { + if (!hideToolbar) { componentsNavigationPanel = new FlowPanel(); componentsNavigationPanel.addStyleName(STYLE_VIEWER_TOOLBAR); mainPanel.add(componentsNavigationPanel); } - return mainPanel; } protected void addComponentToNavigationMenu(final Component component, boolean isCheckboxEnabled, String componentDisplayName, final boolean hasSettingsWhenComponentIsInvisible) { - if(hideToolbar) { - return; - } - - final String componentName = componentDisplayName != null ? componentDisplayName : component.getLocalizedShortName(); - final CheckBox checkBox= new CheckBox(componentName); - final Button settingsButton = new Button(""); - - checkBox.getElement().getStyle().setFloat(Style.Float.LEFT); - - checkBox.setEnabled(isCheckboxEnabled); - checkBox.setValue(component.isVisible()); - checkBox.setTitle(stringMessages.showHideComponent(componentName)); - checkBox.addStyleName(STYLE_VIEWER_TOOLBAR_INNERELEMENT); - - checkBox.addValueChangeHandler(new ValueChangeHandler() { - @Override - public void onValueChange(ValueChangeEvent newValue) { - boolean visible = checkBox.getValue(); - component.setVisible(visible); - - if (visible && component instanceof TimeListener) { - // trigger the component to update its data - ((TimeListener) component).timeChanged(timer.getTime(), null); - } - if(component.hasSettings() && !hasSettingsWhenComponentIsInvisible) { - settingsButton.setEnabled(visible); - } - } - }); - - componentsNavigationPanel.add(checkBox); - - if(component.hasSettings()) { - settingsButton.addClickHandler(new ClickHandler() { + if (!hideToolbar) { + final String componentName = componentDisplayName != null ? componentDisplayName : component.getLocalizedShortName(); + final String debugIdPrefix = DebugIdHelper.createDebugId(componentName); + final CheckBox checkBox = new CheckBox(componentName); + checkBox.ensureDebugId(debugIdPrefix + "DisplayCheckBox"); + final Button settingsButton = new Button(""); + settingsButton.ensureDebugId(debugIdPrefix + "SettingsButton"); + checkBox.getElement().getStyle().setFloat(Style.Float.LEFT); + checkBox.setEnabled(isCheckboxEnabled); + checkBox.setValue(component.isVisible()); + checkBox.setTitle(stringMessages.showHideComponent(componentName)); + checkBox.addStyleName(STYLE_VIEWER_TOOLBAR_INNERELEMENT); + checkBox.addValueChangeHandler(new ValueChangeHandler() { @Override - public void onClick(ClickEvent event) { - new SettingsDialog(component, stringMessages).show(); - } + public void onValueChange(ValueChangeEvent newValue) { + boolean visible = checkBox.getValue(); + component.setVisible(visible); + if (visible && component instanceof TimeListener) { + // trigger the component to update its data + ((TimeListener) component).timeChanged(timer.getTime(), null); + } + if (component.hasSettings() && !hasSettingsWhenComponentIsInvisible) { + settingsButton.setEnabled(visible); + } + } }); - } - - settingsButton.setEnabled(component.hasSettings() && hasSettingsWhenComponentIsInvisible); - settingsButton.addStyleName(STYLE_VIEWER_TOOLBAR_SETTINGS_BUTTON); - settingsButton.getElement().getStyle().setFloat(Style.Float.LEFT); - settingsButton.setTitle(stringMessages.settingsForComponent(componentName)); - - componentsNavigationPanel.add(settingsButton); + componentsNavigationPanel.add(checkBox); + if (component.hasSettings()) { + settingsButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + SettingsDialog dialog = new SettingsDialog(component, + stringMessages); + dialog.ensureDebugId(debugIdPrefix + "SettingsDialog"); + dialog.show(); + } + }); + } + settingsButton.setEnabled(component.hasSettings() && hasSettingsWhenComponentIsInvisible); + settingsButton.addStyleName(STYLE_VIEWER_TOOLBAR_SETTINGS_BUTTON); + settingsButton.getElement().getStyle().setFloat(Style.Float.LEFT); + settingsButton.setTitle(stringMessages.settingsForComponent(componentName)); + componentsNavigationPanel.add(settingsButton); + } } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/DialogBoxExt.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/DialogBoxExt.java index 4abd972eb15..80c191d8e72 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/DialogBoxExt.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/DialogBoxExt.java @@ -1,9 +1,9 @@ package com.sap.sailing.gwt.ui.leaderboard; +import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.EventTarget; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.safehtml.shared.SafeHtml; -import com.google.gwt.dom.client.Element; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.DialogBox; import com.google.gwt.user.client.ui.HTML; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardEntryPoint.java index 224608c70a1..ff29983de26 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardEntryPoint.java @@ -33,6 +33,7 @@ import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.URLEncoder; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettings.RaceColumnSelectionStrategies; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.async.MarkedAsyncCallback; import com.sap.sse.gwt.client.dialog.DataEntryDialog; import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.player.Timer.PlayModes; @@ -95,23 +96,24 @@ public class LeaderboardEntryPoint extends AbstractEntryPoint { leaderboardGroupName = Window.Location.getParameter(PARAM_LEADERBOARD_GROUP_NAME); if (leaderboardName != null) { - sailingService.checkLeaderboardName(leaderboardName, new AsyncCallback>() { - @Override - public void onSuccess(Pair leaderboardNameAndType) { - if (leaderboardNameAndType != null && leaderboardName.equals(leaderboardNameAndType.getA())) { - Window.setTitle(leaderboardName); - leaderboardType = leaderboardNameAndType.getB(); - createUI(showRaceDetails, embedded, hideToolbar); - } else { - RootPanel.get().add(new Label(stringMessages.noSuchLeaderboard())); - } - } - - @Override - public void onFailure(Throwable t) { - reportError("Error trying to obtain list of leaderboard names: " + t.getMessage()); - } - }); + sailingService.checkLeaderboardName(leaderboardName, new MarkedAsyncCallback>( + new AsyncCallback>() { + @Override + public void onSuccess(Pair leaderboardNameAndType) { + if (leaderboardNameAndType != null && leaderboardName.equals(leaderboardNameAndType.getA())) { + Window.setTitle(leaderboardName); + leaderboardType = leaderboardNameAndType.getB(); + createUI(showRaceDetails, embedded, hideToolbar); + } else { + RootPanel.get().add(new Label(stringMessages.noSuchLeaderboard())); + } + } + + @Override + public void onFailure(Throwable t) { + reportError("Error trying to obtain list of leaderboard names: " + t.getMessage()); + } + })); } else { RootPanel.get().add(new Label(stringMessages.noSuchLeaderboard())); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java index 81102f1937b..9ee28f2e040 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java @@ -86,6 +86,7 @@ import com.sap.sailing.gwt.ui.leaderboard.DetailTypeColumn.LegDetailField; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettings.RaceColumnSelectionStrategies; import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.async.MarkedAsyncCallback; import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicator; import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; import com.sap.sse.gwt.client.player.PlayStateListener; @@ -134,7 +135,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS /** * The leaderboard name is used to - * {@link SailingServiceAsync#getLeaderboardByName(String, java.util.Date, String[], String, com.google.gwt.user.client.rpc.AsyncCallback) + * {@link SailingServiceAsync#getLeaderboardByName(String, java.util.Date, String[], boolean, String, com.google.gwt.user.client.rpc.AsyncCallback) * obtain the leaderboard contents} from the server. It may change in case the leaderboard is renamed. */ private String leaderboardName; @@ -745,7 +746,8 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS DetailType.RACE_TIME_TRAVELED, DetailType.RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS, DetailType.RACE_DISTANCE_TO_LEADER_IN_METERS, DetailType.NUMBER_OF_MANEUVERS, DetailType.DISPLAY_LEGS, DetailType.CURRENT_LEG, - DetailType.RACE_AVERAGE_CROSS_TRACK_ERROR_IN_METERS, + DetailType.RACE_AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS, + DetailType.RACE_AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS, DetailType.RACE_DISTANCE_TO_START_FIVE_SECONDS_BEFORE_RACE_START, DetailType.RACE_SPEED_OVER_GROUND_FIVE_SECONDS_BEFORE_START, DetailType.DISTANCE_TO_START_AT_RACE_START, DetailType.SPEED_OVER_GROUND_AT_RACE_START, DetailType.SPEED_OVER_GROUND_WHEN_PASSING_START, @@ -790,20 +792,21 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS getSailingService().getLeaderboardByName(getLeaderboardName(), timer.getPlayMode() == PlayModes.Live ? null : getLeaderboardDisplayDate(), /* namesOfRacesForWhichToLoadLegDetails */getNamesOfExpandedRaces(), - previousLeaderboard.getId(), new AsyncCallback() { - @Override - public void onSuccess(IncrementalOrFullLeaderboardDTO result) { - updateLeaderboard(result.getLeaderboardDTO(previousLeaderboard)); - callWhenExpansionDataIsLoaded.run(); - } - - @Override - public void onFailure(Throwable caught) { - getErrorReporter().reportError( - stringMessages.errorTryingToObtainLeaderboardContents(caught.getMessage()), - true /* silentMode */); - } - }); + shallAddOverallDetails(), previousLeaderboard.getId(), new MarkedAsyncCallback( + new AsyncCallback() { + @Override + public void onSuccess(IncrementalOrFullLeaderboardDTO result) { + updateLeaderboard(result.getLeaderboardDTO(previousLeaderboard)); + callWhenExpansionDataIsLoaded.run(); + } + + @Override + public void onFailure(Throwable caught) { + getErrorReporter().reportError( + stringMessages.errorTryingToObtainLeaderboardContents(caught.getMessage()), + true /* silentMode */); + } + })); } } @@ -830,8 +833,11 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS result.put(DetailType.RACE_DISTANCE_TO_LEADER_IN_METERS, new FormattedDoubleDetailTypeColumn(DetailType.RACE_DISTANCE_TO_LEADER_IN_METERS, new RaceDistanceToLeaderInMeters(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE)); - result.put(DetailType.RACE_AVERAGE_CROSS_TRACK_ERROR_IN_METERS, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_AVERAGE_CROSS_TRACK_ERROR_IN_METERS, new RaceAverageCrossTrackErrorInMeters(), + result.put(DetailType.RACE_AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS, new RaceAverageAbsoluteCrossTrackErrorInMeters(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE)); + result.put(DetailType.RACE_AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS, new RaceAverageSignedCrossTrackErrorInMeters(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE)); result.put(DetailType.RACE_DISTANCE_TO_START_FIVE_SECONDS_BEFORE_RACE_START, new FormattedDoubleDetailTypeColumn(DetailType.RACE_DISTANCE_TO_START_FIVE_SECONDS_BEFORE_RACE_START, new DistanceToStartFiveSecondsBeforeStartInMeters(), @@ -970,17 +976,34 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS } /** - * Fetches the average cross-track error for the race + * Fetches the average absolute (distance always counted as positive, no matter whether left or right) cross-track error for the race * * @author Axel Uhl (D043530) */ - private class RaceAverageCrossTrackErrorInMeters implements LegDetailField { + private class RaceAverageAbsoluteCrossTrackErrorInMeters implements LegDetailField { @Override public Double get(LeaderboardRowDTO row) { Double result = null; LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); if (fieldsForRace != null) { - result = fieldsForRace.averageCrossTrackErrorInMeters; + result = fieldsForRace.averageAbsoluteCrossTrackErrorInMeters; + } + return result; + } + } + + /** + * Fetches the average signed (right course side is positive, left course side is negative) cross-track error for the race + * + * @author Axel Uhl (D043530) + */ + private class RaceAverageSignedCrossTrackErrorInMeters implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.averageSignedCrossTrackErrorInMeters; } return result; } @@ -1365,7 +1388,8 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS String leaderboardName, ErrorReporter errorReporter, final StringMessages stringMessages, final UserAgentDetails userAgent, boolean showRaceDetails) { this(sailingService, asyncActionsExecutor, settings, preSelectedRace, competitorSelectionProvider, new Timer( - PlayModes.Replay, /* delayBetweenAutoAdvancesInMilliseconds */3000l), leaderboardGroupName, + // perform the first request as "live" but don't by default auto-play + PlayModes.Live, PlayStates.Paused, /* delayBetweenAutoAdvancesInMilliseconds */3000l), leaderboardGroupName, leaderboardName, errorReporter, stringMessages, userAgent, showRaceDetails, /* optionalRaceTimesInfoProvider */ null, /* autoExpandLastRaceColumn */ false, /* adjustTimerDelay */ true); } @@ -1435,7 +1459,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS TOTAL_COLUMN_STYLE = tableResources.cellTableStyle().cellTableTotalColumn(); leaderboardTable = new SortedCellTableWithStylableHeaders( /* pageSize */10000, tableResources); - leaderboardTable.ensureDebugId("LeaderboardTable"); + leaderboardTable.ensureDebugId("LeaderboardCellTable"); getLeaderboardTable().setWidth("100%"); leaderboardSelectionModel = new MultiSelectionModel(); leaderboardSelectionModel.addSelectionChangeHandler(new Handler() { @@ -1458,7 +1482,9 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS leaderboardTable.setSelectionModel(leaderboardSelectionModel); } setShowAddedScores(settings.isShowAddedScores()); - loadCompleteLeaderboard(getLeaderboardDisplayDate()); + if (timer.isInitialized()) { + loadCompleteLeaderboard(getLeaderboardDisplayDate()); + } if (this.preSelectedRace == null) { isEmbedded = false; @@ -1470,7 +1496,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS // the information panel informationPanel = new FlowPanel(); - informationPanel.setStyleName(STYLE_LEADERBOARD_INFO); + informationPanel.setStyleName(STYLE_LEADERBOARD_INFO); scoreCorrectionLastUpdateTimeLabel = new Label(""); scoreCorrectionCommentLabel = new Label(""); informationPanel.add(scoreCorrectionCommentLabel); @@ -1485,8 +1511,10 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS // the toolbar panel DockPanel toolbarPanel = new DockPanel(); - toolbarPanel.setStyleName(STYLE_LEADERBOARD_TOOLBAR); + toolbarPanel.ensureDebugId("ToolbarPanel"); + toolbarPanel.setStyleName(STYLE_LEADERBOARD_TOOLBAR); busyIndicator = new SimpleBusyIndicator(false, 0.8f); + busyIndicator.ensureDebugId("BusyIndicator"); if (!isEmbedded) { toolbarPanel.add(informationPanel, DockPanel.WEST); toolbarPanel.add(busyIndicator, DockPanel.WEST); @@ -1507,12 +1535,14 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS pauseIcon = resources.autoRefreshEnabledIcon(); playIcon = resources.autoRefreshDisabledIcon(); refreshAndSettingsPanel = new HorizontalPanel(); + refreshAndSettingsPanel.ensureDebugId("RefreshAndSettingsPanel"); refreshAndSettingsPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE); FlowPanel refreshPanel = new FlowPanel(); refreshPanel.addStyleName("refreshPanel"); toolbarPanel.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_RIGHT); toolbarPanel.addStyleName("refreshAndSettings"); playPause = new Anchor(getPlayPauseImgHtml(timer.getPlayState())); + playPause.ensureDebugId("PlayAndPauseAnchor"); playPause.addClickHandler(playPauseHandler); playStateChanged(timer.getPlayState(), timer.getPlayMode()); refreshPanel.add(playPause); @@ -1638,7 +1668,16 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS } private void setDelayInMilliseconds(long delayInMilliseconds) { + // If the timer is currently in a mode that causes null to be used as the leaderboard request time stamp, + // don't let a live play delay change cause another load request by unregistering this panel as timer + // listener before and re-registering it after adjusting the live play delay. + if (useNullAsTimePoint()) { + timer.removeTimeListener(this); + } timer.setLivePlayDelayInMillis(delayInMilliseconds); + if (useNullAsTimePoint()) { + timer.addTimeListener(this); + } } private boolean isAutoExpandPreSelectedRace() { @@ -1747,10 +1786,10 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS private void loadCompleteLeaderboard(Date date) { if (needsDataLoading()) { GetLeaderboardByNameAction getLeaderboardByNameAction = new GetLeaderboardByNameAction(sailingService, - getLeaderboardName(), timer.getPlayMode() == PlayModes.Live ? null : date, + getLeaderboardName(), useNullAsTimePoint() ? null : date, /* namesOfRacesForWhichToLoadLegDetails */getNamesOfExpandedRaces(), - /* previousLeaderboard */ getLeaderboard(), timer, errorReporter, stringMessages); - asyncActionsExecutor.execute(getLeaderboardByNameAction, LOAD_LEADERBOARD_DATA_CATEGORY, + shallAddOverallDetails(), /* previousLeaderboard */ getLeaderboard(), timer, errorReporter, stringMessages); + this.asyncActionsExecutor.execute(getLeaderboardByNameAction, LOAD_LEADERBOARD_DATA_CATEGORY, new AsyncCallback() { @Override public void onSuccess(LeaderboardDTO result) { @@ -1771,6 +1810,16 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS } } + /** + * In {@link PlayModes#Live live mode}, when {@link #loadCompleteLeaderboard(Date) loading the leaderboard contents}, null + * is used as time point. The condition for this is encapsulated in this method so others can find out. For example, when a time change + * is signaled due to local offset / delay adjustments, no additional call to {@link #loadCompleteLeaderboard(Date)} would be required + * as null will be passed in any case, not being affected by local time offsets. + */ + private boolean useNullAsTimePoint() { + return timer.getPlayMode() == PlayModes.Live; + } + private boolean needsDataLoading() { return isVisible(); } @@ -1792,6 +1841,10 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS return namesOfExpandedRaces; } + private boolean shallAddOverallDetails() { + return !selectedOverallDetailColumns.isEmpty(); + } + /** * Assigns leaderboard to {@link #leaderboard} and updates the UI accordingly. Also updates the min/max * values on the columns. @@ -2491,7 +2544,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS public boolean hasBusyIndicator() { return true; } - + private Iterable getSelectedRows() { ArrayList selectedRows = new ArrayList(); for (LeaderboardRowDTO row : getData().getList()) { @@ -2504,7 +2557,9 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS @Override public void competitorsListChanged(Iterable competitors) { - timeChanged(timer.getTime(), null); + if (timer.isInitialized()) { + timeChanged(timer.getTime(), null); + } } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardSettingsDialogComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardSettingsDialogComponent.java index fdcceeb5170..b94c5f7d061 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardSettingsDialogComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardSettingsDialogComponent.java @@ -22,6 +22,7 @@ import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.domain.common.DetailType; import com.sap.sailing.domain.common.dto.RaceColumnDTO; import com.sap.sailing.domain.common.impl.Util; +import com.sap.sailing.gwt.ui.client.DebugIdHelper; import com.sap.sailing.gwt.ui.client.DetailTypeFormatter; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.shared.components.SettingsDialogComponent; @@ -90,19 +91,19 @@ public class LeaderboardSettingsDialogComponent implements SettingsDialogCompone dialogPanel.add(createTimingDetailsPanel(dialog)); return dialogPanel; } - + private FlowPanel createManeuverDetailsPanel(DataEntryDialog dialog) { FlowPanel meneuverPanel = new FlowPanel(); + meneuverPanel.ensureDebugId("ManeuverSettingsPanel"); meneuverPanel.add(dialog.createHeadline(stringMessages.maneuverTypes(), true)); meneuverPanel.addStyleName("SettingsDialogComponent"); FlowPanel meneuverContent = new FlowPanel(); meneuverContent.addStyleName("dialogInnerContent"); List currentMeneuverDetailSelection = maneuverDetailSelection; for (DetailType detailType : ManeuverCountRaceColumn.getAvailableManeuverDetailColumnTypes()) { - CheckBox checkbox = dialog.createCheckbox(DetailTypeFormatter.format(detailType)); - dialog.addTooltip(checkbox, DetailTypeFormatter.getTooltip(detailType)); - checkbox.setValue(currentMeneuverDetailSelection.contains(detailType)); - maneuverDetailCheckboxes.put(detailType, checkbox); + CheckBox checkbox = createAndRegisterCheckbox(dialog, detailType, + currentMeneuverDetailSelection.contains(detailType), maneuverDetailCheckboxes); + meneuverContent.add(checkbox); } meneuverPanel.add(meneuverContent); @@ -111,7 +112,9 @@ public class LeaderboardSettingsDialogComponent implements SettingsDialogCompone private FlowPanel createTimingDetailsPanel(DataEntryDialog dialog) { FlowPanel timingPanel = new FlowPanel(); + timingPanel.ensureDebugId("TimingSettingsPanel"); refreshIntervalInSecondsBox = dialog.createLongBox(delayBetweenAutoAdvancesInMilliseconds / 1000l, 4); + refreshIntervalInSecondsBox.ensureDebugId("RefreshIntervalLongBox"); timingPanel.add(dialog.createHeadline(stringMessages.timing(), true)); timingPanel.addStyleName("SettingsDialogComponent"); @@ -132,6 +135,7 @@ public class LeaderboardSettingsDialogComponent implements SettingsDialogCompone private FlowPanel createRaceDetailPanel(DataEntryDialog dialog) { FlowPanel raceDetailDialog = new FlowPanel(); + raceDetailDialog.ensureDebugId("RaceDetailsSettingsPanel"); raceDetailDialog.add(dialog.createHeadline(stringMessages.raceDetailsToShow(), true)); raceDetailDialog.addStyleName("SettingsDialogComponent"); int detailCountInCurrentFlowPanel = 0; @@ -143,10 +147,8 @@ public class LeaderboardSettingsDialogComponent implements SettingsDialogCompone raceDetailDialogContent.addStyleName("dialogInnerContent"); raceDetailDialog.add(raceDetailDialogContent); } - CheckBox checkbox = dialog.createCheckbox(DetailTypeFormatter.format(type)); - checkbox.setValue(currentRaceDetailSelection.contains(type)); - dialog.addTooltip(checkbox, DetailTypeFormatter.getTooltip(type)); - raceDetailCheckboxes.put(type, checkbox); + CheckBox checkbox = createAndRegisterCheckbox(dialog, type, currentRaceDetailSelection.contains(type), + raceDetailCheckboxes); raceDetailDialogContent.add(checkbox); detailCountInCurrentFlowPanel++; } @@ -163,16 +165,15 @@ public class LeaderboardSettingsDialogComponent implements SettingsDialogCompone private FlowPanel createOverallDetailPanel(DataEntryDialog dialog) { FlowPanel overallDetailDialog = new FlowPanel(); + overallDetailDialog.ensureDebugId("OverallDetailsSettingsPanel"); overallDetailDialog.add(dialog.createHeadline(stringMessages.overallDetailsToShow(), true)); overallDetailDialog.addStyleName("SettingsDialogComponent overallDetailSettings"); FlowPanel overallDetailDialogContent = new FlowPanel(); overallDetailDialogContent.addStyleName("dialogInnerContent"); List currentOverallDetailSelection = overallDetailSelection; for (DetailType type : LeaderboardPanel.getAvailableOverallDetailColumnTypes()) { - CheckBox checkbox = dialog.createCheckbox(DetailTypeFormatter.format(type)); - dialog.addTooltip(checkbox, DetailTypeFormatter.getTooltip(type)); - checkbox.setValue(currentOverallDetailSelection.contains(type)); - overallDetailCheckboxes.put(type, checkbox); + CheckBox checkbox = createAndRegisterCheckbox(dialog, type, currentOverallDetailSelection.contains(type), + overallDetailCheckboxes); overallDetailDialogContent.add(checkbox); } overallDetailDialog.add(overallDetailDialogContent); @@ -181,6 +182,7 @@ public class LeaderboardSettingsDialogComponent implements SettingsDialogCompone private FlowPanel createLegDetailsPanel(DataEntryDialog dialog) { FlowPanel legDetailsToShow = new FlowPanel(); + legDetailsToShow.ensureDebugId("LegDetailsSettingsPanel"); legDetailsToShow.add(dialog.createHeadline(stringMessages.legDetailsToShow(), true)); legDetailsToShow.addStyleName("SettingsDialogComponent"); FlowPanel legDetailsContent = null; @@ -192,10 +194,8 @@ public class LeaderboardSettingsDialogComponent implements SettingsDialogCompone legDetailsContent.addStyleName("dialogInnerContent"); legDetailsToShow.add(legDetailsContent); } - CheckBox checkbox = dialog.createCheckbox(DetailTypeFormatter.format(type)); - dialog.addTooltip(checkbox, DetailTypeFormatter.getTooltip(type)); - checkbox.setValue(currentLegDetailSelection.contains(type)); - legDetailCheckboxes.put(type, checkbox); + CheckBox checkbox = createAndRegisterCheckbox(dialog, type, currentLegDetailSelection.contains(type), + legDetailCheckboxes); legDetailsContent.add(checkbox); detailCountInCurrentFlowPanel++; } @@ -204,6 +204,7 @@ public class LeaderboardSettingsDialogComponent implements SettingsDialogCompone private FlowPanel createSelectedRacesPanel(DataEntryDialog dialog) { FlowPanel selectedRacesPanel = new FlowPanel(); + selectedRacesPanel.ensureDebugId("RaceSelectionSettingsPanel"); selectedRacesPanel.addStyleName("SettingsDialogComponent"); selectedRacesPanel.add(dialog.createHeadline(stringMessages.selectedRaces(), true)); // race selection strategy elements @@ -218,12 +219,16 @@ public class LeaderboardSettingsDialogComponent implements SettingsDialogCompone int racesCount = raceAllRaceColumns.size(); if (racesCount > 0) { final FlowPanel explicitRaceSelectionContent = new FlowPanel(); + explicitRaceSelectionContent.ensureDebugId("ExplicitRaceSelectionPanel"); final FlowPanel lastNRacesSelectionContent = new FlowPanel(); + lastNRacesSelectionContent.ensureDebugId("MostCurrentRacesSelectionPanel"); + String radioButtonGroupName = "raceSelectionStrategyGroup"; Label raceSelectionWayLabel = new Label(stringMessages.chooseTheWayYouSelectRaces() + ":"); raceSelectionWayLabel.getElement().getStyle().setPaddingRight(5, Unit.PX); racesSelectionStrategyPanel.add(raceSelectionWayLabel); explicitRaceColumnSelectionRadioBtn = dialog.createRadioButton(radioButtonGroupName, stringMessages.selectFromAllRaces()); + explicitRaceColumnSelectionRadioBtn.ensureDebugId("ExplicitRaceSelectionRadioButton"); racesSelectionStrategyPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE); explicitRaceColumnSelectionRadioBtn.setValue(activeRaceColumnSelectionStrategy == RaceColumnSelectionStrategies.EXPLICIT); explicitRaceColumnSelectionRadioBtn.addClickHandler(new ClickHandler() { @@ -246,8 +251,8 @@ public class LeaderboardSettingsDialogComponent implements SettingsDialogCompone } Grid grid = new Grid(rowCount, maxRacesPerRow); for (RaceColumnDTO expandableSortableColumn : raceAllRaceColumns) { - CheckBox checkbox = dialog.createCheckbox(expandableSortableColumn.getRaceColumnName()); - checkbox.setValue(Util.contains(raceColumnSelection, expandableSortableColumn)); + CheckBox checkbox = createCheckbox(dialog, expandableSortableColumn.getRaceColumnName(), + Util.contains(raceColumnSelection, expandableSortableColumn), null); raceColumnCheckboxes.put(expandableSortableColumn, checkbox); grid.setWidget(rowIndex, columnIndex++, checkbox); if(columnIndex == maxRacesPerRow) { @@ -260,6 +265,7 @@ public class LeaderboardSettingsDialogComponent implements SettingsDialogCompone selectedRacesContent.add(explicitRaceSelectionContent); lastNRacesColumnSelectionRadioBtn = dialog.createRadioButton(radioButtonGroupName, stringMessages.selectANumberOfRaces()); + lastNRacesColumnSelectionRadioBtn.ensureDebugId("MostCurrentRacesSelectionRadioButton"); lastNRacesColumnSelectionRadioBtn.setValue(activeRaceColumnSelectionStrategy == RaceColumnSelectionStrategies.LAST_N); lastNRacesColumnSelectionRadioBtn.addClickHandler(new ClickHandler() { @Override @@ -279,6 +285,7 @@ public class LeaderboardSettingsDialogComponent implements SettingsDialogCompone numberOfLastRacesLabel.getElement().getStyle().setPaddingRight(10, Unit.PX); hPanel.add(numberOfLastRacesLabel); numberOfLastRacesToShowBox = dialog.createIntegerBox(numberOfLastRacesToShow != null ? numberOfLastRacesToShow : racesCount, 3); + numberOfLastRacesToShowBox.ensureDebugId("NumberOfMostCurrentRacesIntegerBox"); hPanel.add(numberOfLastRacesToShowBox); dialog.alignAllPanelWidgetsVertically(hPanel, HasVerticalAlignment.ALIGN_MIDDLE); lastNRacesSelectionContent.setVisible(activeRaceColumnSelectionStrategy == RaceColumnSelectionStrategies.LAST_N); @@ -288,6 +295,28 @@ public class LeaderboardSettingsDialogComponent implements SettingsDialogCompone } return selectedRacesPanel; } + + private CheckBox createAndRegisterCheckbox(DataEntryDialog dialog, DetailType detailType, boolean selected, + Map registry) { + CheckBox checkbox = createCheckbox(dialog, detailType, selected); + registry.put(detailType, checkbox); + return checkbox; + } + + private CheckBox createCheckbox(DataEntryDialog dialog, DetailType detailType, boolean selected) { + CheckBox checkbox = createCheckbox(dialog, DetailTypeFormatter.format(detailType), selected, + DetailTypeFormatter.getTooltip(detailType)); + checkbox.ensureDebugId(DebugIdHelper.createDebugId(detailType) + "CheckBox"); + return checkbox; + } + + private CheckBox createCheckbox(DataEntryDialog dialog, String label, boolean selected, String tooltip) { + CheckBox checkbox = dialog.createCheckbox(label); + checkbox.ensureDebugId(DebugIdHelper.createDebugId(label) + "CheckBox"); + checkbox.setValue(selected); + dialog.addTooltip(checkbox, tooltip); + return checkbox; + } @Override public LeaderboardSettings getResult() { @@ -341,16 +370,18 @@ public class LeaderboardSettingsDialogComponent implements SettingsDialogCompone return new Validator() { @Override public String getErrorMessage(LeaderboardSettings valueToValidate) { + final String result; if (valueToValidate.getLegDetailsToShow().isEmpty()) { - return stringMessages.selectAtLeastOneLegDetail(); + result = stringMessages.selectAtLeastOneLegDetail(); } else if (valueToValidate.getDelayBetweenAutoAdvancesInMilliseconds() < 1000) { - return stringMessages.chooseUpdateIntervalOfAtLeastOneSecond(); + result = stringMessages.chooseUpdateIntervalOfAtLeastOneSecond(); } else if (valueToValidate.getActiveRaceColumnSelectionStrategy() == RaceColumnSelectionStrategies.LAST_N && (numberOfLastRacesToShowBox.getValue() == null || numberOfLastRacesToShowBox.getValue() < 0)) { - return stringMessages.numberOfRacesMustBeNonNegativeNumber(); + result = stringMessages.numberOfRacesMustBeNonNegativeNumber(); } else { - return null; + result = null; } + return result; } }; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardViewer.java old mode 100644 new mode 100755 index a0f9fbc0ba5..a7b5b4502e8 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardViewer.java @@ -13,8 +13,10 @@ import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.shared.charts.MultiCompetitorLeaderboardChart; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.async.MarkedAsyncCallback; import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.player.Timer.PlayModes; +import com.sap.sse.gwt.client.player.Timer.PlayStates; import com.sap.sse.gwt.client.useragent.UserAgentDetails; /** @@ -34,7 +36,8 @@ public class LeaderboardViewer extends AbstractLeaderboardViewer { final UserAgentDetails userAgent, boolean showRaceDetails, boolean hideToolbar, boolean autoExpandLastRaceColumn, boolean showCharts, DetailType chartDetailType, boolean showOverallLeaderboard) { super(new CompetitorSelectionModel(/* hasMultiSelection */true), asyncActionsExecutor, - new Timer(PlayModes.Replay, /*delayBetweenAutoAdvancesInMilliseconds*/ 3000l), stringMessages, hideToolbar); + // perform the first request as live request but don't auto-play + new Timer(PlayModes.Live, PlayStates.Paused, /* delayBetweenAutoAdvancesInMilliseconds */ 3000l), stringMessages, hideToolbar); final FlowPanel mainPanel = createViewerPanel(); setWidget(mainPanel); @@ -60,29 +63,29 @@ public class LeaderboardViewer extends AbstractLeaderboardViewer { if(showCharts) { multiCompetitorChart.timeChanged(timer.getTime(), null); } - overallLeaderboardPanel = null; if(showOverallLeaderboard) { - sailingService.getOverallLeaderboardNamesContaining(leaderboardName, new AsyncCallback>() { - @Override - public void onSuccess(List result) { - if(result.size() == 1) { - String overallLeaderboardName = result.get(0); - overallLeaderboardPanel = new LeaderboardPanel(sailingService, asyncActionsExecutor, - leaderboardSettings, preselectedRace, competitorSelectionProvider, timer, - leaderboardGroupName, overallLeaderboardName, errorReporter, stringMessages, userAgent, - false, /* raceTimesInfoProvider */null, false, /* adjustTimerDelay */ true); - mainPanel.add(overallLeaderboardPanel); - addComponentToNavigationMenu(overallLeaderboardPanel, true, stringMessages.seriesLeaderboard(), - /* hasSettingsWhenComponentIsInvisible*/ true); - } - } - - @Override - public void onFailure(Throwable caught) { - // DO NOTHING - } - }); + sailingService.getOverallLeaderboardNamesContaining(leaderboardName, new MarkedAsyncCallback>( + new AsyncCallback>() { + @Override + public void onSuccess(List result) { + if(result.size() == 1) { + String overallLeaderboardName = result.get(0); + overallLeaderboardPanel = new LeaderboardPanel(sailingService, asyncActionsExecutor, + leaderboardSettings, preselectedRace, competitorSelectionProvider, timer, + leaderboardGroupName, overallLeaderboardName, errorReporter, stringMessages, userAgent, + false, /* raceTimesInfoProvider */null, false, /* adjustTimerDelay */ true); + mainPanel.add(overallLeaderboardPanel); + addComponentToNavigationMenu(overallLeaderboardPanel, true, stringMessages.seriesLeaderboard(), + /* hasSettingsWhenComponentIsInvisible*/ true); + } + } + + @Override + public void onFailure(Throwable caught) { + // DO NOTHING + } + })); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LegColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LegColumn.java index 28829a0168e..838a44dd19d 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LegColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LegColumn.java @@ -67,10 +67,17 @@ public class LegColumn extends ExpandableSortableColumn { } } - private class AverageCrossTrackErrorInMeters extends AbstractLegDetailField { + private class AverageAbsoluteCrossTrackErrorInMeters extends AbstractLegDetailField { @Override protected Double getFromNonNullEntry(LegEntryDTO entry) { - return entry.averageCrossTrackErrorInMeters; + return entry.averageAbsoluteCrossTrackErrorInMeters; + } + } + + private class AverageSignedCrossTrackErrorInMeters extends AbstractLegDetailField { + @Override + protected Double getFromNonNullEntry(LegEntryDTO entry) { + return entry.averageSignedCrossTrackErrorInMeters; } } @@ -230,7 +237,8 @@ public class LegColumn extends ExpandableSortableColumn { DetailType.SIDE_TO_WHICH_MARK_AT_LEG_START_WAS_ROUNDED, DetailType.CURRENT_SPEED_OVER_GROUND_IN_KNOTS, DetailType.WINDWARD_DISTANCE_TO_GO_IN_METERS, DetailType.NUMBER_OF_MANEUVERS, DetailType.ESTIMATED_TIME_TO_NEXT_WAYPOINT_IN_SECONDS, DetailType.VELOCITY_MADE_GOOD_IN_KNOTS, - DetailType.TIME_TRAVELED, DetailType.AVERAGE_CROSS_TRACK_ERROR_IN_METERS, DetailType.RANK_GAIN }; + DetailType.TIME_TRAVELED, DetailType.AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS, + DetailType.AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS, DetailType.RANK_GAIN }; } @Override @@ -276,8 +284,11 @@ public class LegColumn extends ExpandableSortableColumn { result.put(DetailType.TIME_TRAVELED, new FormattedDoubleDetailTypeColumn(DetailType.TIME_TRAVELED, new TimeTraveledInSeconds(), detailHeaderStyle, detailColumnStyle)); - result.put(DetailType.AVERAGE_CROSS_TRACK_ERROR_IN_METERS, - new FormattedDoubleDetailTypeColumn(DetailType.AVERAGE_CROSS_TRACK_ERROR_IN_METERS, new AverageCrossTrackErrorInMeters(), detailHeaderStyle, detailColumnStyle)); + result.put(DetailType.AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS, + new FormattedDoubleDetailTypeColumn(DetailType.AVERAGE_ABSOLUTE_CROSS_TRACK_ERROR_IN_METERS, new AverageAbsoluteCrossTrackErrorInMeters(), detailHeaderStyle, detailColumnStyle)); + + result.put(DetailType.AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS, + new FormattedDoubleDetailTypeColumn(DetailType.AVERAGE_SIGNED_CROSS_TRACK_ERROR_IN_METERS, new AverageSignedCrossTrackErrorInMeters(), detailHeaderStyle, detailColumnStyle)); return result; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MetaLeaderboardViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MetaLeaderboardViewer.java index b9b900c9ce9..81c9aac2bce 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MetaLeaderboardViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MetaLeaderboardViewer.java @@ -13,6 +13,7 @@ import com.sap.sailing.gwt.ui.client.shared.charts.MultiCompetitorLeaderboardCha import com.sap.sse.gwt.client.async.AsyncActionsExecutor; import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.player.Timer.PlayModes; +import com.sap.sse.gwt.client.player.Timer.PlayStates; import com.sap.sse.gwt.client.useragent.UserAgentDetails; /** @@ -32,34 +33,28 @@ public class MetaLeaderboardViewer extends AbstractLeaderboardViewer { UserAgentDetails userAgent, boolean showRaceDetails, boolean hideToolbar, boolean autoExpandLastRaceColumn, boolean showCharts, DetailType chartDetailType, boolean showSeriesLeaderboards) { super(new CompetitorSelectionModel(/* hasMultiSelection */true), asyncActionsExecutor, - new Timer(PlayModes.Replay, /*delayBetweenAutoAdvancesInMilliseconds*/ 3000l), stringMessages, hideToolbar); - + // perform the first request as live request but don't auto-play + new Timer(PlayModes.Live, PlayStates.Paused, /*delayBetweenAutoAdvancesInMilliseconds*/ 3000l), stringMessages, hideToolbar); FlowPanel mainPanel = createViewerPanel(); setWidget(mainPanel); - final Label overallStandingsLabel = new Label(stringMessages.overallStandings()); overallStandingsLabel.setStyleName("leaderboardHeading"); - metaLeaderboardPanel = new LeaderboardPanel(sailingService, asyncActionsExecutor, leaderboardSettings, preselectedRace, competitorSelectionProvider, timer, leaderboardGroupName, metaLeaderboardName, errorReporter, stringMessages, userAgent, showRaceDetails, /* raceTimesInfoProvider */null, autoExpandLastRaceColumn, /* adjustTimerDelay */ true); - multiCompetitorChart = new MultiCompetitorLeaderboardChart(sailingService, asyncActionsExecutor, metaLeaderboardName, chartDetailType, competitorSelectionProvider, timer, stringMessages, errorReporter); multiCompetitorChart.setVisible(showCharts); multiCompetitorChart.getElement().getStyle().setMarginTop(10, Unit.PX); multiCompetitorChart.getElement().getStyle().setMarginBottom(10, Unit.PX); - multiLeaderboardPanel = new MultiLeaderboardPanel(sailingService, metaLeaderboardName, asyncActionsExecutor, timer, preselectedLeaderboardName, preselectedRace, errorReporter, stringMessages, userAgent, showRaceDetails, autoExpandLastRaceColumn); multiLeaderboardPanel.setVisible(showSeriesLeaderboards); - mainPanel.add(metaLeaderboardPanel); mainPanel.add(multiCompetitorChart); mainPanel.add(multiLeaderboardPanel); - addComponentToNavigationMenu(metaLeaderboardPanel, false, stringMessages.seriesLeaderboard(), /* hasSettingsWhenComponentIsInvisible*/ true); addComponentToNavigationMenu(multiCompetitorChart, true, null, /* hasSettingsWhenComponentIsInvisible*/ true); addComponentToNavigationMenu(multiLeaderboardPanel, true , stringMessages.regattaLeaderboards(), /* hasSettingsWhenComponentIsInvisible*/ false); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ScoringSchemeTypeFormatter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ScoringSchemeTypeFormatter.java index d3a2d0fc004..9c9f225bd67 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ScoringSchemeTypeFormatter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ScoringSchemeTypeFormatter.java @@ -20,6 +20,8 @@ public class ScoringSchemeTypeFormatter { return stringMessages.scoringSchemeHighPointFirstGetsTen(); case LOW_POINT_WINNER_GETS_ZERO: return stringMessages.scoringSchemeLowPointWinnerGetsZero(); + case HIGH_POINT_WINNER_GETS_SIX: + return stringMessages.scoringSchemeWinnerGetsSix(); } return null; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/LeaderboardEditPage.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/LeaderboardEditPage.java index 1d5bb8aad83..0faf325e516 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/LeaderboardEditPage.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/LeaderboardEditPage.java @@ -4,8 +4,8 @@ import java.util.List; import com.google.gwt.core.client.GWT; import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.rpc.ServiceDefTarget; import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.rpc.ServiceDefTarget; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.RootPanel; import com.sap.sailing.gwt.ui.client.AbstractEntryPoint; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java index a11507698f5..94e90871dc9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java @@ -8,7 +8,6 @@ import java.util.Map; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.dom.client.Style.VerticalAlign; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.logical.shared.ValueChangeEvent; @@ -39,12 +38,12 @@ import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; import com.sap.sailing.gwt.ui.client.RegattaDisplayer; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.media.MediaSelector; import com.sap.sailing.gwt.ui.client.shared.charts.MultiCompetitorRaceChart; import com.sap.sailing.gwt.ui.client.shared.charts.WindChart; import com.sap.sailing.gwt.ui.client.shared.charts.WindChartSettings; import com.sap.sailing.gwt.ui.client.shared.components.Component; import com.sap.sailing.gwt.ui.client.shared.components.ComponentViewer; -import com.sap.sailing.gwt.ui.client.media.MediaSelector; import com.sap.sailing.gwt.ui.client.shared.components.SettingsDialog; import com.sap.sailing.gwt.ui.client.shared.filter.CompetitorRaceRankFilter; import com.sap.sailing.gwt.ui.client.shared.filter.CompetitorSelectionProviderFilterContext; @@ -175,7 +174,6 @@ public class RaceBoardPanel extends SimplePanel implements RegattaDisplayer, Rac // add busy indicator to toolbar panel because the embedded leaderboard has no space for it toolbarPanel.add(leaderboardPanel.getBusyIndicator()); - leaderboardPanel.getBusyIndicator().getElement().getStyle().setVerticalAlign(VerticalAlign.MIDDLE); CompetitorsFilterSets loadedCompetitorsFilterSets = loadCompetitorsFilterSets(); if (loadedCompetitorsFilterSets != null) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/regattaoverview/RegattaRaceStatesSettingsDialogComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/regattaoverview/RegattaRaceStatesSettingsDialogComponent.java index 3eaf988b039..3eb2840abd6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/regattaoverview/RegattaRaceStatesSettingsDialogComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/regattaoverview/RegattaRaceStatesSettingsDialogComponent.java @@ -4,8 +4,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.UUID; import java.util.Map.Entry; +import java.util.UUID; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/PlaceHolderDeviceIdentifierStringSerializationHandler.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/PlaceHolderDeviceIdentifierStringSerializationHandler.java new file mode 100644 index 00000000000..dd95376bcfe --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/PlaceHolderDeviceIdentifierStringSerializationHandler.java @@ -0,0 +1,8 @@ +package com.sap.sailing.gwt.ui.server; + +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifierStringSerializationHandler; +import com.sap.sailing.domain.racelog.tracking.impl.PlaceHolderDeviceIdentifierSerializationHandler; + +public class PlaceHolderDeviceIdentifierStringSerializationHandler extends PlaceHolderDeviceIdentifierSerializationHandler implements + DeviceIdentifierStringSerializationHandler { +} 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 6946d37e95c..ac00244f393 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 @@ -50,6 +50,7 @@ import javax.servlet.ServletContext; import org.apache.http.client.ClientProtocolException; import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; import org.osgi.util.tracker.ServiceTracker; import com.sap.sailing.domain.base.BoatClass; @@ -62,6 +63,7 @@ import com.sap.sailing.domain.base.CourseBase; import com.sap.sailing.domain.base.Event; import com.sap.sailing.domain.base.Fleet; import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.Nationality; import com.sap.sailing.domain.base.RaceColumn; import com.sap.sailing.domain.base.RaceColumnInSeries; import com.sap.sailing.domain.base.RaceDefinition; @@ -80,9 +82,17 @@ import com.sap.sailing.domain.base.configuration.impl.GateStartConfigurationImpl import com.sap.sailing.domain.base.configuration.impl.RRS26ConfigurationImpl; import com.sap.sailing.domain.base.configuration.impl.RacingProcedureConfigurationImpl; import com.sap.sailing.domain.base.configuration.impl.RegattaConfigurationImpl; +import com.sap.sailing.domain.base.impl.BoatImpl; +import com.sap.sailing.domain.base.impl.CourseDataImpl; +import com.sap.sailing.domain.base.impl.CourseImpl; +import com.sap.sailing.domain.base.impl.DynamicBoat; +import com.sap.sailing.domain.base.impl.DynamicPerson; +import com.sap.sailing.domain.base.impl.DynamicTeam; import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.base.impl.PersonImpl; import com.sap.sailing.domain.base.impl.RegattaImpl; import com.sap.sailing.domain.base.impl.SeriesImpl; +import com.sap.sailing.domain.base.impl.TeamImpl; import com.sap.sailing.domain.common.Bearing; import com.sap.sailing.domain.common.CountryCode; import com.sap.sailing.domain.common.DataImportProgress; @@ -115,6 +125,7 @@ import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.SpeedWithBearing; import com.sap.sailing.domain.common.Tack; import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.TimeRange; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.WindSourceType; import com.sap.sailing.domain.common.configuration.DeviceConfigurationMatcherType; @@ -131,6 +142,7 @@ import com.sap.sailing.domain.common.dto.PositionDTO; import com.sap.sailing.domain.common.dto.RaceColumnDTO; import com.sap.sailing.domain.common.dto.RaceColumnInSeriesDTO; import com.sap.sailing.domain.common.dto.RaceDTO; +import com.sap.sailing.domain.common.dto.RaceLogTrackingInfoDTO; import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; import com.sap.sailing.domain.common.dto.TrackedRaceDTO; import com.sap.sailing.domain.common.impl.DegreeBearingImpl; @@ -141,6 +153,7 @@ import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.common.impl.MeterDistance; import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; import com.sap.sailing.domain.common.impl.PolarSheetGenerationResponseImpl; +import com.sap.sailing.domain.common.impl.TimeRangeImpl; 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; @@ -149,6 +162,13 @@ import com.sap.sailing.domain.common.racelog.FlagPole; import com.sap.sailing.domain.common.racelog.Flags; import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; import com.sap.sailing.domain.common.racelog.RacingProcedureType; +import com.sap.sailing.domain.common.racelog.tracking.NoCorrespondingServiceRegisteredException; +import com.sap.sailing.domain.common.racelog.tracking.NotDenotedForRaceLogTrackingException; +import com.sap.sailing.domain.common.racelog.tracking.NotRevokableException; +import com.sap.sailing.domain.common.racelog.tracking.RaceLogTrackingState; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinder; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinderFactory; import com.sap.sailing.domain.igtimiadapter.Account; import com.sap.sailing.domain.igtimiadapter.IgtimiConnection; import com.sap.sailing.domain.igtimiadapter.IgtimiConnectionFactory; @@ -165,14 +185,33 @@ import com.sap.sailing.domain.persistence.MongoRaceLogStoreFactory; import com.sap.sailing.domain.polarsheets.PolarSheetGenerationWorker; import com.sap.sailing.domain.racelog.RaceLog; import com.sap.sailing.domain.racelog.RaceLogEvent; +import com.sap.sailing.domain.racelog.RaceLogEventFactory; import com.sap.sailing.domain.racelog.RaceLogFlagEvent; import com.sap.sailing.domain.racelog.RaceStateOfSameDayHelper; +import com.sap.sailing.domain.racelog.Revokable; import com.sap.sailing.domain.racelog.analyzing.impl.AbortingFlagFinder; +import com.sap.sailing.domain.racelog.analyzing.impl.LastPublishedCourseDesignFinder; import com.sap.sailing.domain.racelog.state.ReadonlyRaceState; import com.sap.sailing.domain.racelog.state.impl.ReadonlyRaceStateImpl; import com.sap.sailing.domain.racelog.state.racingprocedure.FlagPoleState; import com.sap.sailing.domain.racelog.state.racingprocedure.gate.ReadonlyGateStartRacingProcedure; import com.sap.sailing.domain.racelog.state.racingprocedure.rrs26.ReadonlyRRS26RacingProcedure; +import com.sap.sailing.domain.racelog.tracking.CloseOpenEndedDeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifierStringSerializationHandler; +import com.sap.sailing.domain.racelog.tracking.DeviceMapping; +import com.sap.sailing.domain.racelog.tracking.DeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.DefinedMarkFinder; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.DeviceCompetitorMappingFinder; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.DeviceMarkMappingFinder; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.LastPingMappingEventFinder; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.OpenEndedDeviceMappingCloser; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.RaceLogTrackingStateAnalyzer; +import com.sap.sailing.domain.racelog.tracking.analyzing.impl.RegisteredCompetitorsAnalyzer; +import com.sap.sailing.domain.racelog.tracking.impl.DeviceMappingImpl; +import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapter; +import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapterFactory; +import com.sap.sailing.domain.swisstimingadapter.StartList; import com.sap.sailing.domain.swisstimingadapter.SwissTimingAdapter; import com.sap.sailing.domain.swisstimingadapter.SwissTimingAdapterFactory; import com.sap.sailing.domain.swisstimingadapter.SwissTimingArchiveConfiguration; @@ -199,6 +238,8 @@ 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.DynamicGPSFixTrackImpl; +import com.sap.sailing.domain.tracking.impl.GPSFixImpl; import com.sap.sailing.domain.tracking.impl.GPSFixMovingImpl; import com.sap.sailing.domain.tracking.impl.WindImpl; import com.sap.sailing.domain.tractracadapter.RaceRecord; @@ -217,6 +258,8 @@ import com.sap.sailing.gwt.ui.shared.CoursePositionsDTO; import com.sap.sailing.gwt.ui.shared.DeviceConfigurationDTO; import com.sap.sailing.gwt.ui.shared.DeviceConfigurationDTO.RegattaConfigurationDTO; import com.sap.sailing.gwt.ui.shared.DeviceConfigurationMatcherDTO; +import com.sap.sailing.gwt.ui.shared.DeviceIdentifierDTO; +import com.sap.sailing.gwt.ui.shared.DeviceMappingDTO; import com.sap.sailing.gwt.ui.shared.EventDTO; import com.sap.sailing.gwt.ui.shared.GPSFixDTO; import com.sap.sailing.gwt.ui.shared.GateDTO; @@ -267,7 +310,6 @@ import com.sap.sailing.manage2sail.EventResultDescriptor; import com.sap.sailing.manage2sail.Manage2SailEventResultsParserImpl; import com.sap.sailing.manage2sail.RaceResultDescriptor; import com.sap.sailing.manage2sail.RegattaResultDescriptor; -import com.sap.sailing.manage2sail.TestManage2SailEventResultsParser; import com.sap.sailing.resultimport.ResultUrlProvider; import com.sap.sailing.resultimport.ResultUrlRegistry; import com.sap.sailing.server.RacingEventService; @@ -324,6 +366,7 @@ import com.sap.sailing.server.replication.ReplicationMasterDescriptor; import com.sap.sailing.server.replication.ReplicationService; import com.sap.sailing.server.replication.impl.ReplicaDescriptor; import com.sap.sailing.util.BuildVersion; +import com.sap.sailing.xrr.resultimport.schema.RegattaResults; /** * The server side implementation of the RPC service. @@ -348,6 +391,11 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S private final ServiceTracker tractracAdapterTracker; private final ServiceTracker igtimiAdapterTracker; + + private final ServiceTracker raceLogTrackingAdapterTracker; + + private final ServiceTracker + deviceIdentifierStringSerializationHandlerTracker; private final com.sap.sailing.domain.tractracadapter.persistence.MongoObjectFactory tractracMongoObjectFactory; @@ -388,6 +436,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S replicationServiceTracker = createAndOpenReplicationServiceTracker(context); swissTimingAdapterTracker = createAndOpenSwissTimingAdapterTracker(context); tractracAdapterTracker = createAndOpenTracTracAdapterTracker(context); + raceLogTrackingAdapterTracker = createAndOpenRaceLogTrackingAdapterTracker(context); + deviceIdentifierStringSerializationHandlerTracker = createAndOpenDeviceIdentifierStringSerializationHandlerTracker(context); igtimiAdapterTracker = createAndOpenIgtimiTracker(context); baseDomainFactory = getService().getBaseDomainFactory(); mongoObjectFactory = getService().getMongoObjectFactory(); @@ -468,6 +518,15 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S return result; } + protected ServiceTracker createAndOpenRaceLogTrackingAdapterTracker( + BundleContext context) { + ServiceTracker result = + new ServiceTracker( + context, RaceLogTrackingAdapterFactory.class.getName(), null); + result.open(); + return result; + } + private void writeObject(ObjectOutputStream oos) throws IOException { oos.defaultWriteObject(); } @@ -556,7 +615,6 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S *

* * Otherwise, the leaderboard is computed synchronously on the fly. - * * @param previousLeaderboardId * if null or no leaderboard with that {@link LeaderboardDTO#getId() ID} is known, a * {@link FullLeaderboardDTO} will be computed; otherwise, an {@link IncrementalLeaderboardDTO} will be @@ -564,8 +622,9 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S */ @Override public IncrementalOrFullLeaderboardDTO getLeaderboardByName(final String leaderboardName, final Date date, - final Collection namesOfRaceColumnsForWhichToLoadLegDetails, String previousLeaderboardId) - throws NoWindException, InterruptedException, ExecutionException, IllegalArgumentException { + final Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, + String previousLeaderboardId) throws NoWindException, InterruptedException, ExecutionException, + IllegalArgumentException { try { long startOfRequestHandling = System.currentTimeMillis(); IncrementalOrFullLeaderboardDTO result = null; @@ -578,7 +637,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S timePoint = new MillisecondsTimePoint(date); } LeaderboardDTO leaderboardDTO = leaderboard.getLeaderboardDTO(timePoint, - namesOfRaceColumnsForWhichToLoadLegDetails, getService(), baseDomainFactory); + namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, getService(), baseDomainFactory); LeaderboardDTO previousLeaderboardDTO = null; synchronized (leaderboardByNameResultsCacheById) { leaderboardByNameResultsCacheById.put(leaderboardDTO.getId(), leaderboardDTO); @@ -624,9 +683,10 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S result = incrementalResult; } logger.fine("getLeaderboardByName(" + leaderboardName + ", " + date + ", " - + namesOfRaceColumnsForWhichToLoadLegDetails + ") took " - + (System.currentTimeMillis() - startOfRequestHandling) + "ms; diff cache hits/misses " - + leaderboardDifferenceCacheByIdPairHits+"/"+leaderboardDifferenceCacheByIdPairMisses); + + namesOfRaceColumnsForWhichToLoadLegDetails + ", addOverallDetails=" + addOverallDetails + + ") took " + (System.currentTimeMillis() - startOfRequestHandling) + + "ms; diff cache hits/misses " + leaderboardDifferenceCacheByIdPairHits + "/" + + leaderboardDifferenceCacheByIdPairMisses); } return result; } catch (NoWindException e) { @@ -822,11 +882,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S List waypointDTOs = new ArrayList(); for (Waypoint waypoint : lastCourseDesign.getWaypoints()) { ControlPointDTO controlPointDTO = convertToControlPointDTO(waypoint.getControlPoint()); - List marks = new ArrayList(); - for (MarkDTO markDTO : controlPointDTO.getMarks()) { - marks.add(markDTO); - } - WaypointDTO waypointDTO = new WaypointDTO(waypoint.getName(), controlPointDTO, marks, waypoint.getPassingInstructions()); + WaypointDTO waypointDTO = new WaypointDTO(waypoint.getName(), controlPointDTO, waypoint.getPassingInstructions()); waypointDTOs.add(waypointDTO); } result = new RaceCourseDTO(waypointDTOs); @@ -1115,7 +1171,6 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S 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())) { @@ -1131,16 +1186,15 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S 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(), timePoint); double confidence = averagedWindWithConfidence.getConfidence(); windDTO.confidence = confidence; windTrackInfoDTO.windFixes.add(windDTO); - if(confidence < minWindConfidence) { + if (confidence < minWindConfidence) { minWindConfidence = confidence; } - if(confidence > maxWindConfidence) { + if (confidence > maxWindConfidence) { maxWindConfidence = confidence; } } @@ -1154,20 +1208,37 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S return result; } + /** + * @param onlyUpToNewestEvent + * if true, no wind data will be returned for time points later than + * {@link TrackedRace#getTimePointOfNewestEvent() trackedRace.getTimePointOfNewestEvent()}. This is + * helpful in case the client wants to populate a chart during live mode. If false, the + * "best effort" readings are provided for the time interval requested, no matter if based on any sensor + * evidence or not, regardless of {@link TrackedRace#getTimePointOfNewestEvent() + * trackedRace.getTimePointOfNewestEvent()}. + */ @Override public WindInfoForRaceDTO getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, long millisecondsStepWidth, - int numberOfFixes, Collection windSourceTypeNames) + int numberOfFixes, Collection windSourceTypeNames, boolean onlyUpToNewestEvent) throws NoWindException { assert from != null; TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); - WindInfoForRaceDTO result = getAveragedWindInfo(new MillisecondsTimePoint(from), millisecondsStepWidth, numberOfFixes, - windSourceTypeNames, trackedRace); + windSourceTypeNames, trackedRace, /* onlyUpToNewestEvent */ true); return result; } + /** + * @param onlyUpToNewestEvent + * if true, no wind data will be returned for time points later than + * {@link TrackedRace#getTimePointOfNewestEvent() trackedRace.getTimePointOfNewestEvent()}. This is + * helpful in case the client wants to populate a chart during live mode. If false, the + * "best effort" readings are provided for the time interval requested, no matter if based on any sensor + * evidence or not, regardless of {@link TrackedRace#getTimePointOfNewestEvent() + * trackedRace.getTimePointOfNewestEvent()}. + */ private WindInfoForRaceDTO getAveragedWindInfo(TimePoint from, long millisecondsStepWidth, int numberOfFixes, - Collection windSourceTypeNames, TrackedRace trackedRace) { + Collection windSourceTypeNames, TrackedRace trackedRace, boolean onlyUpToNewestEvent) { WindInfoForRaceDTO result = null; if (trackedRace != null) { TimePoint newestEvent = trackedRace.getTimePointOfNewestEvent(); @@ -1195,19 +1266,27 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S TimePoint timePoint = from; Double minWindConfidence = 2.0; Double maxWindConfidence = -1.0; - for (int i = 0; i < numberOfFixes && newestEvent != null && timePoint.compareTo(newestEvent) < 0; i++) { + for (int i = 0; i < numberOfFixes && (!onlyUpToNewestEvent || + (newestEvent != null && timePoint.before(newestEvent))); i++) { WindWithConfidence> averagedWindWithConfidence = windTrack.getAveragedWindWithConfidence(null, timePoint); if (averagedWindWithConfidence != null) { + if (logger.getLevel() != null && logger.getLevel().equals(Level.FINEST)) { + logger.finest("Found averaged wind: " + averagedWindWithConfidence); + } double confidence = averagedWindWithConfidence.getConfidence(); WindDTO windDTO = createWindDTOFromAlreadyAveraged(averagedWindWithConfidence.getObject(), timePoint); windDTO.confidence = confidence; windTrackInfoDTO.windFixes.add(windDTO); - if(confidence < minWindConfidence) { + if (confidence < minWindConfidence) { minWindConfidence = confidence; } if (confidence > maxWindConfidence) { maxWindConfidence = confidence; } + } else { + if (logger.getLevel() != null && logger.getLevel().equals(Level.FINEST)) { + logger.finest("Did NOT find any averaged wind for timepoint " + timePoint + " and tracked race " + trackedRace.getRaceIdentifier().getRaceName()); + } } timePoint = new MillisecondsTimePoint(timePoint.asMillis() + millisecondsStepWidth); } @@ -1219,9 +1298,18 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S return result; } + /** + * @param onlyUpToNewestEvent + * if true, no wind data will be returned for time points later than + * {@link TrackedRace#getTimePointOfNewestEvent() trackedRace.getTimePointOfNewestEvent()}. This is + * helpful in case the client wants to populate a chart during live mode. If false, the + * "best effort" readings are provided for the time interval requested, no matter if based on any sensor + * evidence or not, regardless of {@link TrackedRace#getTimePointOfNewestEvent() + * trackedRace.getTimePointOfNewestEvent()}. + */ @Override public WindInfoForRaceDTO getAveragedWindInfo(RegattaAndRaceIdentifier raceIdentifier, Date from, Date to, - long resolutionInMilliseconds, Collection windSourceTypeNames) { + long resolutionInMilliseconds, Collection windSourceTypeNames, boolean onlyUpToNewestEvent) { TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); WindInfoForRaceDTO result = null; if (trackedRace != null) { @@ -1229,7 +1317,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S 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); + result = getAveragedWindInfo(fromTimePoint, resolutionInMilliseconds, numberOfFixes, + windSourceTypeNames, trackedRace, onlyUpToNewestEvent); } } return result; @@ -1325,6 +1414,9 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S while (fixIter.hasNext()) { GPSFixMoving fix = fixIter.next(); if (fix.getTimePoint().compareTo(toTimePointExcluding) < 0) { + if (logger.getLevel() != null && logger.getLevel().equals(Level.FINEST)) { + logger.finest(""+competitor.getName()+": " + fix); + } fixes.add(fix); } else { break; @@ -1339,7 +1431,11 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S Position estimatedPosition = track.getEstimatedPosition(middle, extrapolate); SpeedWithBearing estimatedSpeed = track.getEstimatedSpeed(middle); if (estimatedPosition != null && estimatedSpeed != null) { - fixes.add(new GPSFixMovingImpl(estimatedPosition, middle, estimatedSpeed)); + GPSFixMoving estimatedFix = new GPSFixMovingImpl(estimatedPosition, middle, estimatedSpeed); + if (logger.getLevel() != null && logger.getLevel().equals(Level.FINEST)) { + logger.finest(""+competitor.getName()+": " + estimatedFix+" (estimated)"); + } + fixes.add(estimatedFix); } } Iterator fixIter = fixes.iterator(); @@ -1576,26 +1672,28 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public RaceCourseDTO getRaceCourse(RegattaAndRaceIdentifier raceIdentifier, Date date) { List waypointDTOs = new ArrayList(); - RaceCourseDTO result = new RaceCourseDTO(waypointDTOs); - 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()) { - List markDTOs = new ArrayList(); - for (Mark mark : trackedRace.getMarks()) { - GPSFixTrack track = trackedRace.getOrCreateTrack(mark); - Position positionAtDate = track.getEstimatedPosition(dateAsTimePoint, /* extrapolate */false); - markDTOs.add(convertToMarkDTO(mark, positionAtDate)); - } - ControlPointDTO controlPointDTO = convertToControlPointDTO(waypoint.getControlPoint(), trackedRace, dateAsTimePoint); - WaypointDTO waypointDTO = new WaypointDTO(waypoint.getName(), controlPointDTO, markDTOs, waypoint.getPassingInstructions()); - waypointDTOs.add(waypointDTO); + Map controlPointCache = new HashMap<>(); + TimePoint dateAsTimePoint = new MillisecondsTimePoint(date); + TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); + List allMarks = new ArrayList<>(); + if (trackedRace != null) { + for (Mark mark : trackedRace.getMarks()) { + Position pos = trackedRace.getOrCreateTrack(mark).getEstimatedPosition(dateAsTimePoint, false); + allMarks.add(convertToMarkDTO(mark, pos)); + } + Course course = trackedRace.getRace().getCourse(); + for (Waypoint waypoint : course.getWaypoints()) { + ControlPointDTO controlPointDTO = controlPointCache.get(waypoint.getControlPoint().getId()); + if (controlPointDTO == null) { + controlPointDTO = convertToControlPointDTO(waypoint.getControlPoint(), trackedRace, dateAsTimePoint); + controlPointCache.put(waypoint.getControlPoint().getId(), controlPointDTO); } + WaypointDTO waypointDTO = new WaypointDTO(waypoint.getName(), controlPointDTO, + waypoint.getPassingInstructions()); + waypointDTOs.add(waypointDTO); } } - return result; + return new RaceCourseDTO(waypointDTOs, allMarks); } private ControlPointDTO convertToControlPointDTO(ControlPoint controlPoint, TrackedRace trackedRace, TimePoint timePoint) { @@ -1609,7 +1707,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } else { final Position posOfFirst = trackedRace.getOrCreateTrack(controlPoint.getMarks().iterator().next()). getEstimatedPosition(timePoint, /* extrapolate */ false); - result = new MarkDTO(controlPoint.getId().toString(), controlPoint.getName(), posOfFirst.getLatDeg(), posOfFirst.getLngDeg()); + result = new MarkDTO(controlPoint.getId().toString(), controlPoint.getName(), posOfFirst == null ? -1 : posOfFirst.getLatDeg(), posOfFirst == null ? -1 : posOfFirst.getLngDeg()); } return result; } @@ -1626,6 +1724,61 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S return result; } + /** + * For each {@link ControlPointDTO} in controlPoints tries to find the best-matching waypoint + * from the {@code old} {@code CourseBase}, and also from the newly created additional waypoints. + * 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. + */ + private List> resolveControlPoints( + CourseBase old, List> controlPoints) { + Map newlyCreatedCache = new HashMap(); + Iterable waypoints = old.getWaypoints(); + List> newControlPoints = new ArrayList>(); + for (Pair controlPointAndPassingInstruction : controlPoints) { + ControlPointDTO controlPointDTO = controlPointAndPassingInstruction.getA(); + ControlPoint match = null; + if (newlyCreatedCache.containsKey(controlPointDTO)) { + match = newlyCreatedCache.get(controlPointDTO); + } else { + for (Waypoint oldWP : waypoints) { + ControlPoint oldControlPoint = oldWP.getControlPoint(); + if (oldControlPoint.getId().toString().equals(controlPointDTO.getIdAsString()) + && markIDsMatch(oldControlPoint.getMarks(), controlPointDTO.getMarks())) { + match = oldControlPoint; + break; + } + } + } + if (match != null) { + newControlPoints.add(new Pair(match, controlPointAndPassingInstruction.getB())); + } else { + // no match found; create new control point: + ControlPoint newControlPoint; + if (controlPointDTO instanceof GateDTO) { + GateDTO gateDTO = (GateDTO) controlPointDTO; + final Serializable id; + if (gateDTO.getIdAsString() == null) { + id = UUID.randomUUID(); + } else { + id = gateDTO.getIdAsString(); + } + Mark left = baseDomainFactory.getOrCreateMark(gateDTO.getLeft().getIdAsString(), gateDTO.getLeft().getName()); + Mark right = baseDomainFactory.getOrCreateMark(gateDTO.getRight().getIdAsString(), gateDTO.getRight().getName()); + newControlPoint = baseDomainFactory.createControlPointWithTwoMarks(id, left, right, gateDTO.getName()); + newControlPoints.add(new Pair(newControlPoint, null)); + } else { + newControlPoint = baseDomainFactory.getOrCreateMark(controlPointDTO.getIdAsString(), controlPointDTO.getName()); + PassingInstruction passingInstructions = controlPointAndPassingInstruction.getB(); + newControlPoints.add(new Pair(newControlPoint, passingInstructions)); + } + newlyCreatedCache.put(controlPointDTO, newControlPoint); + } + } + return newControlPoints; + } + /** * 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 @@ -1639,43 +1792,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); if (trackedRace != null) { Course course = trackedRace.getRace().getCourse(); - Iterable waypoints = course.getWaypoints(); - List> newControlPoints = new ArrayList>(); - int lastMatchPosition = -1; - for (Pair controlPointAndPassingInstruction : controlPoints) { - ControlPointDTO controlPointDTO = controlPointAndPassingInstruction.getA(); - ControlPoint matchFromOldCourse = null; - for (int i=lastMatchPosition+1; matchFromOldCourse == null && i(matchFromOldCourse, null)); - lastMatchPosition = i; - } - } - if (matchFromOldCourse == null) { - // no match found; create new control point: - ControlPoint newControlPoint; - if (controlPointDTO instanceof GateDTO) { - GateDTO gateDTO = (GateDTO) controlPointDTO; - final Serializable id; - if (gateDTO.getIdAsString() == null) { - id = UUID.randomUUID(); - } else { - id = gateDTO.getIdAsString(); - } - Mark left = baseDomainFactory.getOrCreateMark(gateDTO.getLeft().getIdAsString(), gateDTO.getLeft().getName()); - Mark right = baseDomainFactory.getOrCreateMark(gateDTO.getRight().getIdAsString(), gateDTO.getRight().getName()); - newControlPoint = baseDomainFactory.createControlPointWithTwoMarks(id, left, right, gateDTO.getName()); - newControlPoints.add(new Pair(newControlPoint, null)); - } else { - newControlPoint = baseDomainFactory.getOrCreateMark(controlPointDTO.getIdAsString(), controlPointDTO.getName()); - PassingInstruction passingInstructions = controlPointAndPassingInstruction.getB(); - newControlPoints.add(new Pair(newControlPoint, passingInstructions)); - } - } - } + List> newControlPoints = resolveControlPoints(course, controlPoints); try { course.update(newControlPoints, baseDomainFactory); } catch (Exception e) { @@ -1978,8 +2095,17 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S raceDTO = baseDomainFactory.createRaceDTO(getService(), withGeoLocationData, raceIdentifier, trackedRace); } final FleetDTO fleetDTO = baseDomainFactory.convertToFleetDTO(fleet); - leaderboardDTO.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), raceColumn.getFactor(), - fleetDTO, raceColumn.isMedalRace(), raceIdentifier, raceDTO); + RaceColumnDTO raceColumnDTO = leaderboardDTO.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), + raceColumn.getFactor(), fleetDTO, raceColumn.isMedalRace(), raceIdentifier, raceDTO); + + RaceLog raceLog = raceColumn.getRaceLog(fleet); + RaceLogTrackingState raceLogTrackingState = raceLog == null ? RaceLogTrackingState.NOT_A_RACELOG_TRACKED_RACE : + new RaceLogTrackingStateAnalyzer(raceLog).analyze(); + boolean raceLogTrackerExists = raceLog == null ? false : getService().getRaceTrackerById(raceLog.getId()) != null; + boolean competitorRegistrationsExist = raceLog == null ? false : !new RegisteredCompetitorsAnalyzer(raceLog).analyze().isEmpty(); + RaceLogTrackingInfoDTO raceLogTrackingInfo = new RaceLogTrackingInfoDTO(raceLogTrackerExists, + competitorRegistrationsExist, raceLogTrackingState); + raceColumnDTO.setRaceLogTrackingInfo(fleetDTO, raceLogTrackingInfo); } } return leaderboardDTO; @@ -2174,24 +2300,22 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S // TODO: delete getSwissTimingAdapter().getSwissTimingRaceRecords() method // TODO: delete SwissTimingDomainFactory.getRaceTypeFromRaceID(String raceID) - EventResultDescriptor eventResult = null; - if("test".equalsIgnoreCase(eventJsonURL)) { - eventResult = TestManage2SailEventResultsParser.getTestEventResult(); - } else { - URL url = new URL(eventJsonURL); - URLConnection eventResultConn = url.openConnection(); - Manage2SailEventResultsParserImpl parser = new Manage2SailEventResultsParserImpl(); - eventResult = parser.getEventResult((InputStream) eventResultConn.getContent()); - } + URL url = new URL(eventJsonURL); + URLConnection eventResultConn = url.openConnection(); + Manage2SailEventResultsParserImpl parser = new Manage2SailEventResultsParserImpl(); + EventResultDescriptor eventResult = parser.getEventResult((InputStream) eventResultConn.getContent()); if (eventResult != null) { for (RegattaResultDescriptor regattaResult : eventResult.getRegattaResults()) { for(RaceResultDescriptor race: regattaResult.getRaceResults()) { - SwissTimingRaceRecordDTO swissTimingRaceRecordDTO = new SwissTimingRaceRecordDTO(race.getId(), race.getName(), race.getStartTimeUTC()); - swissTimingRaceRecordDTO.boatClass = regattaResult.getIsafId() != null && !regattaResult.getIsafId().isEmpty() ? regattaResult.getIsafId() : regattaResult.getClassName(); - swissTimingRaceRecordDTO.gender = regattaResult.getCompetitorGenderType().name(); - swissTimingRaceRecordDTO.hasCourse = false; - swissTimingRaceRecordDTO.hasStartlist = false; - swissTimingRaces.add(swissTimingRaceRecordDTO); + // add only the tracked races + if(race.isTracked() != null && race.isTracked() == true) { + SwissTimingRaceRecordDTO swissTimingRaceRecordDTO = new SwissTimingRaceRecordDTO(race.getId(), race.getName(), + regattaResult.getName(), race.getSeriesName(), race.getFleetName(), race.getStatus(), race.getStartTime(), + regattaResult.getXrrEntriesUrl() != null ? regattaResult.getXrrEntriesUrl().toExternalForm() : null); + swissTimingRaceRecordDTO.boatClass = regattaResult.getIsafId() != null && !regattaResult.getIsafId().isEmpty() ? regattaResult.getIsafId() : regattaResult.getClassName(); + swissTimingRaceRecordDTO.gender = regattaResult.getCompetitorGenderType().name(); + swissTimingRaces.add(swissTimingRaceRecordDTO); + } } } result = new SwissTimingEventRecordDTO(eventResult.getId(), eventResult.getName(), eventResult.getTrackingDataHost(), @@ -2212,10 +2336,27 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S boolean trackWind, final boolean correctWindByDeclination) throws Exception { logger.info("tracWithSwissTiming for regatta " + regattaToAddTo + " for race records " + rrs + " with hostname " + hostname + " and port " + port); + Map cachedRegattaEntriesLists = new HashMap(); for (SwissTimingRaceRecordDTO rr : rrs) { BoatClass boatClass = getBaseDomainFactory().getOrCreateBoatClass(rr.boatClass); - final RacesHandle raceHandle = getSwissTimingAdapter().addSwissTimingRace(getService(), regattaToAddTo, rr.raceId, rr.raceName, - boatClass, hostname, port, + String raceDescription = rr.regattaName != null ? rr.regattaName : ""; + raceDescription += rr.seriesName != null ? "/" + rr.seriesName : ""; + raceDescription += raceDescription.length() > 0 ? "/" + rr.raceName : rr.raceName; + // try to find a cached entry list for the regatta + RegattaResults regattaResults = cachedRegattaEntriesLists.get(rr.xrrEntriesUrl); + if(regattaResults == null) { + regattaResults = getSwissTimingAdapter().readRegattaEntryListFromXrrUrl(rr.xrrEntriesUrl); + if(regattaResults != null) { + cachedRegattaEntriesLists.put(rr.xrrEntriesUrl, regattaResults); + } + } + StartList startList = null; + if(regattaResults != null) { + startList = getSwissTimingAdapter().readStartListForRace(rr.raceId, regattaResults); + } + // now read the entry list for the race from the result + final RacesHandle raceHandle = getSwissTimingAdapter().addSwissTimingRace(getService(), regattaToAddTo, rr.raceId, rr.raceName, raceDescription, + boatClass, hostname, port, startList, MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(mongoObjectFactory, domainObjectFactory), RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS); if (trackWind) { new Thread("Wind tracking starter for race " + rr.raceId + "/" + rr.raceName) { @@ -2486,7 +2627,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S switch (detailType) { case REGATTA_TOTAL_POINTS: LeaderboardDTO leaderboardDTO = leaderboard.getLeaderboardDTO(timePoint, Collections. emptyList(), - getService(), baseDomainFactory); + /* addOverallDetails */ false, getService(), baseDomainFactory); for (RaceColumnDTO raceColumnDTO : leaderboardDTO.getRaceList()) { result.add(getTotalPointsForRaceColumn(leaderboardDTO, raceColumnDTO)); } @@ -2810,13 +2951,17 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void updateEvent(UUID eventId, String eventName, Date startDate, Date endDate, VenueDTO venue, boolean isPublic, List regattaNames) { - getService().apply(new UpdateEvent(eventId, eventName, new MillisecondsTimePoint(startDate), new MillisecondsTimePoint(endDate), venue.getName(), isPublic, regattaNames)); + TimePoint startTimePoint = startDate != null ? new MillisecondsTimePoint(startDate) : null; + TimePoint endTimePoint = endDate != null ? new MillisecondsTimePoint(endDate) : null; + getService().apply(new UpdateEvent(eventId, eventName, startTimePoint, endTimePoint, venue.getName(), isPublic, regattaNames)); } @Override public EventDTO createEvent(String eventName, Date startDate, Date endDate, String venue, boolean isPublic, List courseAreaNames) { UUID eventUuid = UUID.randomUUID(); - getService().apply(new CreateEvent(eventName, new MillisecondsTimePoint(startDate), new MillisecondsTimePoint(endDate), venue, isPublic, eventUuid)); + TimePoint startTimePoint = startDate != null ? new MillisecondsTimePoint(startDate) : null; + TimePoint endTimePoint = endDate != null ? new MillisecondsTimePoint(endDate) : null; + getService().apply(new CreateEvent(eventName, startTimePoint, endTimePoint, venue, isPublic, eventUuid)); for (String courseAreaName : courseAreaNames) { createCourseArea(eventUuid, courseAreaName); } @@ -3620,18 +3765,40 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } @Override - public Iterable getCompetitorsOfLeaderboard(String leaderboardName) { - Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName); - return convertToCompetitorDTOs(leaderboard.getAllCompetitors()); + public Iterable getCompetitorsOfLeaderboard(String leaderboardName, boolean lookInRaceLogs) { + if (lookInRaceLogs) { + Set result = new HashSet(); + Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName); + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + for (Fleet fleet : raceColumn.getFleets()) { + RaceLog raceLog = raceColumn.getRaceLog(fleet); + result.addAll(new RegisteredCompetitorsAnalyzer(raceLog).analyze()); + } + } + return convertToCompetitorDTOs(result); + } else { + Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName); + return convertToCompetitorDTOs(leaderboard.getAllCompetitors()); + } } - + @Override - public CompetitorDTO updateCompetitor(CompetitorDTO competitor) { + public CompetitorDTO addOrUpdateCompetitor(CompetitorDTO competitor) { + Nationality nationality = (competitor.getThreeLetterIocCountryCode() == null || competitor.getThreeLetterIocCountryCode().isEmpty()) ? null : + getBaseDomainFactory().getOrCreateNationality(competitor.getThreeLetterIocCountryCode()); + //new competitor + if (competitor.getIdAsString() == null || competitor.getIdAsString().isEmpty()) { + BoatClass boatClass = getBaseDomainFactory().getOrCreateBoatClass(competitor.getBoatClass().getName()); + DynamicPerson sailor = new PersonImpl(competitor.getName(), nationality, null, null); + DynamicTeam team = new TeamImpl(competitor.getName() + " team", Collections.singleton(sailor), null); + DynamicBoat boat = new BoatImpl(competitor.getName() + " boat", boatClass, competitor.getSailID()); + return getBaseDomainFactory().convertToCompetitorDTO(getBaseDomainFactory().getOrCreateCompetitor(UUID.randomUUID(), competitor.getName(), competitor.getColor(), + team, boat)); + } + return getBaseDomainFactory().convertToCompetitorDTO( getService().apply(new UpdateCompetitor(competitor.getIdAsString(), competitor.getName(), - competitor.getColor(), competitor.getSailID(), - (competitor.getThreeLetterIocCountryCode() == null || competitor.getThreeLetterIocCountryCode().isEmpty()) ? null : - getBaseDomainFactory().getOrCreateNationality(competitor.getThreeLetterIocCountryCode())))); + competitor.getColor(), competitor.getSailID(), nationality))); } @Override @@ -3823,6 +3990,10 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S return igtimiAdapterTracker.getService(); } + protected RaceLogTrackingAdapter getRaceLogTrackingAdapter() { + return raceLogTrackingAdapterTracker.getService().getAdapter(getBaseDomainFactory()); + } + @Override public String getIgtimiAuthorizationUrl() { return getIgtimiConnectionFactory().getAuthorizationUrl(); @@ -3913,5 +4084,351 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } } + + @Override + public void denoteForRaceLogTracking(String leaderboardName, + String raceColumnName, String fleetName) throws Exception { + Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName); + RaceColumn raceColumn = getService().getLeaderboardByName(leaderboardName).getRaceColumnByName(raceColumnName); + Fleet fleet = raceColumn.getFleetByName(fleetName); + + getRaceLogTrackingAdapter().denoteRaceForRaceLogTracking(getService(), leaderboard, raceColumn, fleet, null); + } + + @Override + public void removeDenotationForRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName) { + RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName); + getRaceLogTrackingAdapter().removeDenotationForRaceLogTracking(getService(), raceLog); + } + + @Override + public void denoteForRaceLogTracking(String leaderboardName) throws Exception { + Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName); + getRaceLogTrackingAdapter().denoteAllRacesForRaceLogTracking(getService(), leaderboard); + } + + /** + * @param triple leaderboard and racecolumn and fleet names + * @return + */ + private RaceLog getRaceLog(Triple triple) { + return getRaceLog(triple.getA(), triple.getB(), triple.getC()); + } + + private RaceLog getRaceLog(String leaderboardName, String raceColumnName, String fleetName) { + RaceColumn raceColumn = getService().getLeaderboardByName(leaderboardName).getRaceColumnByName(raceColumnName); + Fleet fleet = raceColumn.getFleetByName(fleetName); + return raceColumn.getRaceLog(fleet); + } + + @Override + public Collection getCompetitorRegistrations(String leaderboardName, String raceColumnName, String fleetName) { + return convertToCompetitorDTOs( + new RegisteredCompetitorsAnalyzer( + getRaceLog(leaderboardName, raceColumnName, fleetName)).analyze()); + } + + private Competitor getCompetitor(CompetitorDTO dto) { + return getService().getCompetitorStore().getExistingCompetitorByIdAsString(dto.getIdAsString()); + } + + @Override + public void setCompetitorRegistrations(String leaderboardName, String raceColumnName, String fleetName, + Set competitorDTOs) { + RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName); + Set competitors = new HashSet(); + for (CompetitorDTO dto : competitorDTOs) { + competitors.add(getCompetitor(dto)); + } + + getRaceLogTrackingAdapter().registerCompetitors(getService(), raceLog, competitors); + } + + private Mark convertToMark(MarkDTO dto, boolean resolve) { + if (resolve) { + Mark existing = baseDomainFactory.getExistingMarkByIdAsString(dto.getIdAsString()); + if (existing != null) { + return existing; + } + } + + Serializable id = UUID.randomUUID(); + + return baseDomainFactory.getOrCreateMark(id, dto.getName(), dto.type, dto.color, dto.shape, dto.pattern); + } + + /** + * Also finds the last position of the marks, if set by pinging them + */ + private Collection convertToMarkDTOs(RaceLog raceLog, Iterable marks) { + List dtos = new ArrayList(); + for (Mark mark : marks) { + DynamicGPSFixTrackImpl track = new DynamicGPSFixTrackImpl(mark, 0); + DeviceMappingEvent lastPingMappingEvent = new LastPingMappingEventFinder(raceLog, mark).analyze(); + if (lastPingMappingEvent != null) { + getService().getGPSFixStore().loadMarkTrack(track, DeviceMappingImpl.convertToDeviceMapping(lastPingMappingEvent)); + } + Position lastPos = track.getLastRawFix() == null ? null : track.getLastRawFix().getPosition(); + dtos.add(convertToMarkDTO(mark, lastPos)); + } + return dtos; + } + + @Override + public void addMarkToRaceLog(String leaderboardName, String raceColumnName, String fleetName, MarkDTO markDTO) { + Mark mark = convertToMark(markDTO, false); + RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName); + RaceLogEvent event = RaceLogEventFactory.INSTANCE.createDefineMarkEvent(MillisecondsTimePoint.now(), + getService().getServerAuthor(), raceLog.getCurrentPassId(), mark); + raceLog.add(event); + } + + @Override + public Collection getMarksInRaceLog(String leaderboardName, String raceColumnName, String fleetName) { + RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName); + Collection marks = new DefinedMarkFinder(raceLog).analyze(); + return convertToMarkDTOs(raceLog, marks); + } + + @Override + public void addCourseDefinitionToRaceLog(String leaderboardName, String raceColumnName, String fleetName, + List> courseDTO) { + RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName); + String name = String.format("Course for %s - %s - %s", leaderboardName, raceColumnName, fleetName); + + CourseBase lastPublishedCourse = new LastPublishedCourseDesignFinder(raceLog).analyze(); + if (lastPublishedCourse == null) { + lastPublishedCourse = new CourseDataImpl(name); + } + + List> waypoints = resolveControlPoints(lastPublishedCourse, courseDTO); + Course course = new CourseImpl(name, lastPublishedCourse.getWaypoints()); + + try { + course.update(waypoints, baseDomainFactory); + } catch (Exception e) { + throw new RuntimeException(e); + } + + RaceLogEvent event = RaceLogEventFactory.INSTANCE.createCourseDesignChangedEvent(MillisecondsTimePoint.now(), + getService().getServerAuthor(), raceLog.getCurrentPassId(), course); + raceLog.add(event); + } + + private WaypointDTO convertToWaypointDTO(Waypoint waypoint, Map controlPointCache) { + ControlPointDTO cp = controlPointCache.get(waypoint.getControlPoint().getId()); + if (cp == null) { + cp = convertToControlPointDTO(waypoint.getControlPoint()); + controlPointCache.put(waypoint.getControlPoint().getId(), cp); + } + return new WaypointDTO(waypoint.getName(), cp, waypoint.getPassingInstructions()); + } + + private RaceCourseDTO convertToRaceCourseDTO(CourseBase course) { + List waypointDTOs = new ArrayList(); + Map controlPointCache = new HashMap<>(); + RaceCourseDTO result = new RaceCourseDTO(waypointDTOs); + for (Waypoint waypoint : course.getWaypoints()) { + waypointDTOs.add(convertToWaypointDTO(waypoint, controlPointCache)); + } + return result; + } + + @Override + public RaceCourseDTO getLastCourseDefinitionInRaceLog(String leaderboardName, String raceColumnName, + String fleetName) { + RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName); + + CourseBase lastPublishedCourse = new LastPublishedCourseDesignFinder(raceLog).analyze(); + if (lastPublishedCourse == null) { + lastPublishedCourse = new CourseDataImpl(""); + } + + return convertToRaceCourseDTO(lastPublishedCourse); + } + + private Position convertToPosition(PositionDTO dto) { + return new DegreePosition(dto.latDeg, dto.lngDeg); + } + + @Override + public void pingMarkViaRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName, + MarkDTO markDTO, PositionDTO positionDTO) { + RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName); + Mark mark = convertToMark(markDTO, true); + TimePoint time = MillisecondsTimePoint.now(); + Position position = convertToPosition(positionDTO); + GPSFix fix = new GPSFixImpl(position, time); + + getRaceLogTrackingAdapter().pingMark(raceLog, mark, fix, getService()); + } + + @Override + public void copyCourseAndCompetitorsToOtherRaceLogs(Triple fromTriple, + Set> toTriples) { + RaceLog fromRaceLog = getRaceLog(fromTriple); + Set toRaceLogs = new HashSet<>(); + for (Triple toTriple : toTriples) { + toRaceLogs.add(getRaceLog(toTriple)); + } -} \ No newline at end of file + getRaceLogTrackingAdapter().copyCourseAndCompetitors(fromRaceLog, toRaceLogs, baseDomainFactory, getService()); + } + + private TypeBasedServiceFinder getDeviceIdentifierStringSerializerHandlerFinder( + boolean withFallback) { + TypeBasedServiceFinderFactory factory = getService().getTypeBasedServiceFinderFactory(); + TypeBasedServiceFinder finder = factory.createServiceFinder(DeviceIdentifierStringSerializationHandler.class); + if (withFallback) { + finder.setFallbackService(new PlaceHolderDeviceIdentifierStringSerializationHandler()); + } + return finder; + } + + private DeviceIdentifier deserializeDeviceIdentifier(String type, String deviceId) throws NoCorrespondingServiceRegisteredException, + TransformationException { + DeviceIdentifierStringSerializationHandler handler = + getDeviceIdentifierStringSerializerHandlerFinder(false).findService(type); + return handler.deserialize(deviceId, type, null); + } + + private String serializeDeviceIdentifier(DeviceIdentifier deviceId) throws TransformationException { + return getDeviceIdentifierStringSerializerHandlerFinder(true).findService( + deviceId.getIdentifierType()).serialize(deviceId).getB(); + } + + private DeviceMappingDTO convertToDeviceMappingDTO(DeviceMapping mapping) throws TransformationException { + String deviceId = serializeDeviceIdentifier(mapping.getDevice()); + Date from = mapping.getTimeRange().from() == null || mapping.getTimeRange().from().asMillis() == Long.MIN_VALUE ? + null : mapping.getTimeRange().from().asDate(); + Date to = mapping.getTimeRange().to() == null || mapping.getTimeRange().to().asMillis() == Long.MAX_VALUE ? + null : mapping.getTimeRange().to().asDate(); + Serializable item = null; + if (mapping.getMappedTo() instanceof Competitor) { + item = baseDomainFactory.convertToCompetitorDTO((Competitor) mapping.getMappedTo()); + } else if (mapping.getMappedTo() instanceof Mark) { + item = convertToMarkDTO((Mark) mapping.getMappedTo(), null); + } else { + throw new RuntimeException("Can only handle Competitor or Mark as mapped item type"); + } + return new DeviceMappingDTO(new DeviceIdentifierDTO(mapping.getDevice().getIdentifierType(), + deviceId), from, to, item, mapping.getOriginalRaceLogEventIds()); + } + + @Override + public List getDeviceMappingsFromRaceLog(String leaderboardName, String raceColumnName, + String fleetName) throws TransformationException { + RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName); + List result = new ArrayList(); + for (List> list : new DeviceCompetitorMappingFinder(raceLog).analyze().values()) { + for (DeviceMapping mapping : list) { + result.add(convertToDeviceMappingDTO(mapping)); + } + } + for (List> list : new DeviceMarkMappingFinder(raceLog).analyze().values()) { + for (DeviceMapping mapping : list) { + result.add(convertToDeviceMappingDTO(mapping)); + } + } + return result; + } + + @Override + public void addDeviceMappingToRaceLog(String leaderboardName, String raceColumnName, String fleetName, + DeviceMappingDTO dto) throws NoCorrespondingServiceRegisteredException, TransformationException { + RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName); + DeviceMapping mapping = convertToDeviceMapping(dto); + TimePoint now = MillisecondsTimePoint.now(); + DeviceMappingEvent event = null; + TimePoint from = mapping.getTimeRange().openBeginning() ? null : mapping.getTimeRange().from(); + TimePoint to = mapping.getTimeRange().openEnd() ? null : mapping.getTimeRange().to(); + if (dto.mappedTo instanceof MarkDTO) { + Mark mark = convertToMark(((MarkDTO) dto.mappedTo), true); + event = RaceLogEventFactory.INSTANCE.createDeviceMarkMappingEvent(now, getService().getServerAuthor(), + mapping.getDevice(), mark, raceLog.getCurrentPassId(), from, to); + } else if (dto.mappedTo instanceof CompetitorDTO) { + Competitor competitor = getService().getCompetitorStore().getExistingCompetitorByIdAsString( + ((CompetitorDTO) dto.mappedTo).getIdAsString()); + event = RaceLogEventFactory.INSTANCE.createDeviceCompetitorMappingEvent(now, getService().getServerAuthor(), + mapping.getDevice(), competitor, raceLog.getCurrentPassId(), from, to); + } else { + throw new RuntimeException("Can only map devices to competitors or marks"); + } + raceLog.add(event); + } + + private ServiceTracker + createAndOpenDeviceIdentifierStringSerializationHandlerTracker(BundleContext context) { + ServiceTracker tracker = + new ServiceTracker( + context, DeviceIdentifierStringSerializationHandler.class, null); + + tracker.open(); + return tracker; + } + + @Override + public List getDeserializableDeviceIdentifierTypes() { + List result = new ArrayList(); + for (ServiceReference reference : + deviceIdentifierStringSerializationHandlerTracker.getServiceReferences()) { + result.add((String) reference.getProperty(TypeBasedServiceFinder.TYPE)); + } + return result; + } + + DeviceMapping convertToDeviceMapping(DeviceMappingDTO dto) throws NoCorrespondingServiceRegisteredException, TransformationException { + DeviceIdentifier device = deserializeDeviceIdentifier(dto.deviceIdentifier.deviceType, dto.deviceIdentifier.deviceId); + TimePoint from = dto.from == null ? null : new MillisecondsTimePoint(dto.from); + TimePoint to = dto.to == null ? null : new MillisecondsTimePoint(dto.to); + TimeRange timeRange = new TimeRangeImpl(from, to); + if (dto.mappedTo instanceof MarkDTO) { + Mark mark = convertToMark(((MarkDTO) dto.mappedTo), true); + return new DeviceMappingImpl(mark, device, timeRange,dto.originalRaceLogEventIds); + } else if (dto.mappedTo instanceof CompetitorDTO) { + Competitor competitor = getService().getCompetitorStore().getExistingCompetitorByIdAsString( + ((CompetitorDTO) dto.mappedTo).getIdAsString()); + return new DeviceMappingImpl(competitor, device, timeRange, dto.originalRaceLogEventIds); + } else { + throw new RuntimeException("Can only map devices to competitors or marks"); + } + } + + @Override + public void closeOpenEndedDeviceMapping(String leaderboardName, String raceColumnName, String fleetName, + DeviceMappingDTO mappingDTO, Date closingTimePoint) throws NoCorrespondingServiceRegisteredException, TransformationException { + RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName); + DeviceMapping mapping = convertToDeviceMapping(mappingDTO); + List closingEvents = + new OpenEndedDeviceMappingCloser(raceLog, mapping, getService().getServerAuthor(), + new MillisecondsTimePoint(closingTimePoint)).analyze(); + + for (RaceLogEvent event : closingEvents) { + raceLog.add(event); + } + } + + @Override + public void revokeRaceLogEvents(String leaderboardName, String raceColumnName, String fleetName, + List eventIds) throws NotRevokableException { + RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName); + for (Serializable idToRevoke : eventIds) { + raceLog.lockForRead(); + RaceLogEvent event = raceLog.getEventById(idToRevoke); + raceLog.unlockAfterRead(); + if (event instanceof Revokable) { + raceLog.revokeEvent(getService().getServerAuthor(), (Revokable) event); + } + } + } + + @Override + public void startRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName) + throws NotDenotedForRaceLogTrackingException, Exception { + Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName); + RaceColumn raceColumn = leaderboard.getRaceColumnByName(raceColumnName); + Fleet fleet = raceColumn.getFleetByName(fleetName); + + getRaceLogTrackingAdapter().startTracking(getService(), leaderboard, raceColumn, fleet); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/DeviceIdentifierDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/DeviceIdentifierDTO.java new file mode 100644 index 00000000000..12d8789a1b1 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/DeviceIdentifierDTO.java @@ -0,0 +1,20 @@ +package com.sap.sailing.gwt.ui.shared; + +import java.io.Serializable; + +public class DeviceIdentifierDTO implements Serializable { + private static final long serialVersionUID = 1171014272760543228L; + public String deviceType; + public String deviceId; + + protected DeviceIdentifierDTO() {} + + public DeviceIdentifierDTO(String deviceType, String deviceId) { + this.deviceType = deviceType; + this.deviceId = deviceId; + } + + public String toString() { + return deviceType + " - " + deviceId; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/DeviceMappingDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/DeviceMappingDTO.java new file mode 100644 index 00000000000..233c0b076de --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/DeviceMappingDTO.java @@ -0,0 +1,25 @@ +package com.sap.sailing.gwt.ui.shared; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +public class DeviceMappingDTO implements Serializable { + private static final long serialVersionUID = -3272980620254526040L; + public DeviceIdentifierDTO deviceIdentifier; + public Date from; + public Date to; + public Serializable mappedTo; + public List originalRaceLogEventIds; + + protected DeviceMappingDTO() {} + + public DeviceMappingDTO(DeviceIdentifierDTO deviceId, Date from, Date to, Serializable mappedTo, + List originalRaceLogEventIds) { + this.deviceIdentifier = deviceId; + this.from = from; + this.to = to; + this.mappedTo = mappedTo; + this.originalRaceLogEventIds = originalRaceLogEventIds; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/MarkDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/MarkDTO.java index 0e2720bc656..02752f0e306 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/MarkDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/MarkDTO.java @@ -28,4 +28,18 @@ public class MarkDTO extends ControlPointDTO { public Iterable getMarks() { return Collections.singleton(this); } + + @Override + public boolean equals(Object o) { + if (o instanceof MarkDTO) { + MarkDTO other = (MarkDTO) o; + return getIdAsString() != null && getIdAsString().equals(other.getIdAsString()); + } + return false; + } + + @Override + public int hashCode() { + return getIdAsString().hashCode(); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/RaceCourseDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/RaceCourseDTO.java index 935d72762ac..9ddb68d97b0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/RaceCourseDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/RaceCourseDTO.java @@ -10,30 +10,48 @@ import com.google.gwt.user.client.rpc.IsSerializable; public class RaceCourseDTO implements IsSerializable { public List waypoints; + + /** + * List of all marks in race, that can include such that are not actually part of + * the race course definition. + */ + private List allMarks; - RaceCourseDTO() {} + RaceCourseDTO() { + } public RaceCourseDTO(List waypoints) { + this(waypoints, null); + } + + public RaceCourseDTO(List waypoints, List allMarks) { this.waypoints = waypoints; + this.allMarks = allMarks; } public List getControlPoints() { List controlPoints = new ArrayList(); - for(WaypointDTO waypoint: waypoints) { + for (WaypointDTO waypoint : waypoints) { controlPoints.add(waypoint.controlPoint); } return controlPoints; } public Collection getMarks() { - Map marks = new HashMap(); - for(WaypointDTO waypoint: waypoints) { - for(MarkDTO mark: waypoint.marks) { - if(!marks.containsKey(mark.getName())) { - marks.put(mark.getName(), mark); + Collection result; + if (allMarks != null) { + result = allMarks; + } else { + Map marks = new HashMap(); + for (WaypointDTO waypoint : waypoints) { + for (MarkDTO mark : waypoint.controlPoint.getMarks()) { + if (!marks.containsKey(mark.getName())) { + marks.put(mark.getName(), mark); + } } } + result = marks.values(); } - return marks.values(); + return result; } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SwissTimingRaceRecordDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SwissTimingRaceRecordDTO.java index aa998e6c928..78d1eeb7e83 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SwissTimingRaceRecordDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/SwissTimingRaceRecordDTO.java @@ -8,24 +8,37 @@ public class SwissTimingRaceRecordDTO implements IsSerializable { public String raceId; public String raceName; public Date raceStartTime; + public String regattaName; + public String seriesName; + public String fleetName; + public String raceStatus; + public String xrrEntriesUrl; public String boatClass; public String gender; - - public boolean hasCourse; - public boolean hasStartlist; public SwissTimingRaceRecordDTO() {} - public SwissTimingRaceRecordDTO(String raceId, String raceName, Date raceStartTime) { + public SwissTimingRaceRecordDTO(String raceId, String raceName, String regattaName, String seriesName, String fleetName, + String raceStatus, Date raceStartTime, String xrrEntriesUrl) { super(); this.raceId = raceId; this.raceName = raceName; + this.regattaName = regattaName; + this.seriesName = seriesName; + this.fleetName = fleetName; + this.raceStatus = raceStatus; this.raceStartTime = raceStartTime; + this.xrrEntriesUrl = xrrEntriesUrl; } - - @Override - public String toString() { - return "Race "+raceId+" ("+raceStartTime+")"; - } + + @Override + public String toString() { + return "SwissTimingRaceRecordDTO [raceId=" + raceId + ", raceName=" + + raceName + ", raceStartTime=" + raceStartTime + + ", regattaName=" + regattaName + ", seriesName=" + seriesName + + ", fleetName=" + fleetName + ", raceStatus=" + raceStatus + + ", boatClass=" + boatClass + ", gender=" + gender + + "]"; + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/WaypointDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/WaypointDTO.java index 5a775cee274..29836d30408 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/WaypointDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/WaypointDTO.java @@ -1,14 +1,14 @@ package com.sap.sailing.gwt.ui.shared; -import java.util.List; - import com.sap.sailing.domain.common.PassingInstruction; import com.sap.sailing.domain.common.dto.NamedDTO; +/** + * Equality and hashcode based on object identity + * + */ public class WaypointDTO extends NamedDTO { private static final long serialVersionUID = 7439553659782967746L; - - public List marks; public ControlPointDTO controlPoint; @@ -16,11 +16,20 @@ public class WaypointDTO extends NamedDTO { WaypointDTO() {} - public WaypointDTO(String name, ControlPointDTO controlPoint, List marks, PassingInstruction passingInstructions) { + public WaypointDTO(String name, ControlPointDTO controlPoint, PassingInstruction passingInstructions) { super(name); this.setName(name); - this.marks = marks; this.controlPoint = controlPoint; this.passingInstructions = passingInstructions; } + + @Override + public boolean equals(Object obj) { + return this == obj; + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/MarkVectorGraphics.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/MarkVectorGraphics.java index 38f7845a90b..f7ca991f49e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/MarkVectorGraphics.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/MarkVectorGraphics.java @@ -57,9 +57,9 @@ public class MarkVectorGraphics { switch(type) { case BUOY: if(shape != null) { - if("Cylinder".equalsIgnoreCase(shape) && pattern != null && "Checkered".equalsIgnoreCase(pattern)) { + if(Shape.CYLINDER.name().equalsIgnoreCase(shape) && pattern != null && Pattern.CHECKERED.name().equalsIgnoreCase(pattern)) { drawBuoyWithFinishFlag(ctx, isSelected, color); - } else if ("Conical".equalsIgnoreCase(shape)) { + } else if (Shape.CONICAL.name().equalsIgnoreCase(shape)) { drawConicalBuoy(ctx, isSelected, color); } else { drawSimpleBuoy(ctx, isSelected, color); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/Pattern.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/Pattern.java new file mode 100644 index 00000000000..4e662d66e85 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/Pattern.java @@ -0,0 +1,5 @@ +package com.sap.sailing.gwt.ui.shared.racemap; + +public enum Pattern { + CHECKERED +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/Shape.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/Shape.java new file mode 100644 index 00000000000..5da42c05bff --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/Shape.java @@ -0,0 +1,5 @@ +package com.sap.sailing.gwt.ui.shared.racemap; + +public enum Shape { + CYLINDER, CONICAL +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/spectator/LeaderboardGroupPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/spectator/LeaderboardGroupPanel.java index b407f7fb492..926b4f0a0d5 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/spectator/LeaderboardGroupPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/spectator/LeaderboardGroupPanel.java @@ -384,12 +384,11 @@ public class LeaderboardGroupPanel extends SimplePanel implements HasWelcomeWidg private List getRacesOfFleet(StrippedLeaderboardDTO leaderboard, SeriesDTO series, FleetDTO fleet) { List racesColumnsOfFleet = new ArrayList(); - for (RaceColumnDTO raceColumn : series.getRaceColumns()) { for (FleetDTO fleetOfRaceColumn : series.getFleets()) { - if(fleet.equals(fleetOfRaceColumn)) { - //We have to get the race column from the leaderboard, because the race column of the series - //have no tracked race and would be displayed as inactive race. + if (fleet.equals(fleetOfRaceColumn)) { + // We have to get the race column from the leaderboard, because the race column of the series + // have no tracked race and would be displayed as inactive race. racesColumnsOfFleet.add(leaderboard.getRaceColumnByName(raceColumn.getName())); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/AdminConsole.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/AdminConsole.gwt.xml index 04e0c5ceeef..ba975c19318 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/AdminConsole.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/AdminConsole.gwt.xml @@ -55,8 +55,9 @@ - - + + + diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Leaderboard.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Leaderboard.gwt.xml index ca8dfe51451..8b6b3088382 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Leaderboard.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Leaderboard.gwt.xml @@ -6,7 +6,9 @@ - + + + @@ -27,8 +29,11 @@ + - + + + diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/RegattaOverview.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/RegattaOverview.gwt.xml index e67cdf024bb..0aab3682dce 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/RegattaOverview.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/RegattaOverview.gwt.xml @@ -42,6 +42,8 @@ + + diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Spectator.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Spectator.gwt.xml index a371794cfbc..7b83424fac7 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Spectator.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Spectator.gwt.xml @@ -29,6 +29,8 @@ + + diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/add_racelog_tracker.png b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/add_racelog_tracker.png new file mode 100644 index 00000000000..fc34656a556 Binary files /dev/null and b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/add_racelog_tracker.png differ diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/close_time_range.png b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/close_time_range.png new file mode 100644 index 00000000000..265fad660d1 Binary files /dev/null and b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/close_time_range.png differ diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/competitor_registrations.png b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/competitor_registrations.png new file mode 100644 index 00000000000..ac81bb566cd Binary files /dev/null and b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/competitor_registrations.png differ diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/copy.png b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/copy.png new file mode 100644 index 00000000000..039e6ee863a Binary files /dev/null and b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/copy.png differ diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/copy_course.png b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/copy_course.png new file mode 100644 index 00000000000..7247b54fece Binary files /dev/null and b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/copy_course.png differ diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/define_course.png b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/define_course.png new file mode 100644 index 00000000000..fc201d2882e Binary files /dev/null and b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/define_course.png differ diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/denote_for_racelog_tracking.png b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/denote_for_racelog_tracking.png new file mode 100644 index 00000000000..7fcfe5674d2 Binary files /dev/null and b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/denote_for_racelog_tracking.png differ diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/map_devices.png b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/map_devices.png new file mode 100644 index 00000000000..1fcb041b765 Binary files /dev/null and b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/map_devices.png differ diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/ping.png b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/ping.png new file mode 100644 index 00000000000..e8860cdc014 Binary files /dev/null and b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/ping.png differ diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/start_racelog_tracking.png b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/start_racelog_tracking.png new file mode 100644 index 00000000000..7db79a1807a Binary files /dev/null and b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/start_racelog_tracking.png differ diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/undenote_for_racelog_tracking.png b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/undenote_for_racelog_tracking.png new file mode 100644 index 00000000000..5cc9e328861 Binary files /dev/null and b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/client/images/undenote_for_racelog_tracking.png differ diff --git a/java/com.sap.sailing.manage2sail/resources/worldcupMallorca2013_Races.json b/java/com.sap.sailing.manage2sail/resources/worldcupMallorca2013_Races.json new file mode 100644 index 00000000000..3755422b3e1 --- /dev/null +++ b/java/com.sap.sailing.manage2sail/resources/worldcupMallorca2013_Races.json @@ -0,0 +1 @@ +{"Id":"ec19d09b-7e65-4e12-9fc4-8cafe449a4dc","Name":"WC 2013 Mallorca","XrrUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta","Tracking":{"Host":"host_test","Port":"99999"},"Regattas":[{"Id":"7587a739-c4de-4e49-b5cc-202b49f8bf62","ExternalId":"SAM102000","Name":"RS:X M","Gender":"M","ClassName":"RS:X","PdfUrl":"","XrrEntriesUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/7587a739-c4de-4e49-b5cc-202b49f8bf62?xrrType=Entries","XrrPreliminaryUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/7587a739-c4de-4e49-b5cc-202b49f8bf62?xrrType=PreliminaryResults","XrrFinalUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/7587a739-c4de-4e49-b5cc-202b49f8bf62?xrrType=FinalResults","HtmlUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/ResultsFragment/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/7587a739-c4de-4e49-b5cc-202b49f8bf62"},{"Id":"75ee1797-be2c-45cb-8052-5d317f40272d","ExternalId":"SAM009000","Name":"49er","Gender":"M","ClassName":"49er","PdfUrl":"","XrrEntriesUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/75ee1797-be2c-45cb-8052-5d317f40272d?xrrType=Entries","XrrPreliminaryUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/75ee1797-be2c-45cb-8052-5d317f40272d?xrrType=PreliminaryResults","XrrFinalUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/75ee1797-be2c-45cb-8052-5d317f40272d?xrrType=FinalResults","HtmlUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/ResultsFragment/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/75ee1797-be2c-45cb-8052-5d317f40272d"},{"Id":"42c72455-a7f8-4446-9116-84db41693a7d","ExternalId":"SAM004000","Name":"Laser","Gender":"M","ClassName":"Laser","PdfUrl":"","XrrEntriesUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/42c72455-a7f8-4446-9116-84db41693a7d?xrrType=Entries","XrrPreliminaryUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/42c72455-a7f8-4446-9116-84db41693a7d?xrrType=PreliminaryResults","XrrFinalUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/42c72455-a7f8-4446-9116-84db41693a7d?xrrType=FinalResults","HtmlUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/ResultsFragment/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/42c72455-a7f8-4446-9116-84db41693a7d"},{"Id":"12be791a-a8a1-47fc-9537-d34958d7e759","ExternalId":"SAM005000","Name":"470 M","Gender":"M","ClassName":"470","PdfUrl":"","XrrEntriesUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/12be791a-a8a1-47fc-9537-d34958d7e759?xrrType=Entries","XrrPreliminaryUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/12be791a-a8a1-47fc-9537-d34958d7e759?xrrType=PreliminaryResults","XrrFinalUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/12be791a-a8a1-47fc-9537-d34958d7e759?xrrType=FinalResults","HtmlUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/ResultsFragment/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/12be791a-a8a1-47fc-9537-d34958d7e759"},{"Id":"48ade9ae-ce27-4cad-a7a8-5be9bc316b1d","ExternalId":"SAW103000","Name":"Radial","Gender":"W","ClassName":"Laser Radial","PdfUrl":"","XrrEntriesUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/48ade9ae-ce27-4cad-a7a8-5be9bc316b1d?xrrType=Entries","XrrPreliminaryUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/48ade9ae-ce27-4cad-a7a8-5be9bc316b1d?xrrType=PreliminaryResults","XrrFinalUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/48ade9ae-ce27-4cad-a7a8-5be9bc316b1d?xrrType=FinalResults","HtmlUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/ResultsFragment/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/48ade9ae-ce27-4cad-a7a8-5be9bc316b1d"},{"Id":"b5230bda-ed13-4a1b-9eff-b80227964bb2","ExternalId":"SAW005000","Name":"470 W","Gender":"W","ClassName":"470","PdfUrl":"","XrrEntriesUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/b5230bda-ed13-4a1b-9eff-b80227964bb2?xrrType=Entries","XrrPreliminaryUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/b5230bda-ed13-4a1b-9eff-b80227964bb2?xrrType=PreliminaryResults","XrrFinalUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/b5230bda-ed13-4a1b-9eff-b80227964bb2?xrrType=FinalResults","HtmlUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/ResultsFragment/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/b5230bda-ed13-4a1b-9eff-b80227964bb2"},{"Id":"b87afb52-4974-4b68-acac-bb790d419e55","ExternalId":"SAW009000","Name":"49er FX","Gender":"W","ClassName":"49erFX","PdfUrl":"","XrrEntriesUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/b87afb52-4974-4b68-acac-bb790d419e55?xrrType=Entries","XrrPreliminaryUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/b87afb52-4974-4b68-acac-bb790d419e55?xrrType=PreliminaryResults","XrrFinalUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/b87afb52-4974-4b68-acac-bb790d419e55?xrrType=FinalResults","HtmlUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/ResultsFragment/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/b87afb52-4974-4b68-acac-bb790d419e55"},{"Id":"3cd63bca-229c-4552-860d-fd9fe4898f22","ExternalId":"SAW102000","Name":"RS:X W","Gender":"W","ClassName":"RS:X","PdfUrl":"","XrrEntriesUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/3cd63bca-229c-4552-860d-fd9fe4898f22?xrrType=Entries","XrrPreliminaryUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/3cd63bca-229c-4552-860d-fd9fe4898f22?xrrType=PreliminaryResults","XrrFinalUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/3cd63bca-229c-4552-860d-fd9fe4898f22?xrrType=FinalResults","HtmlUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/ResultsFragment/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/3cd63bca-229c-4552-860d-fd9fe4898f22"},{"Id":"1d4d189d-6860-4f52-a30e-67b84f3a8388","ExternalId":"SAX001000","Name":"2.4 mR","Gender":"X","PdfUrl":"","XrrEntriesUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/1d4d189d-6860-4f52-a30e-67b84f3a8388?xrrType=Entries","XrrPreliminaryUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/1d4d189d-6860-4f52-a30e-67b84f3a8388?xrrType=PreliminaryResults","XrrFinalUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/1d4d189d-6860-4f52-a30e-67b84f3a8388?xrrType=FinalResults","HtmlUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/ResultsFragment/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/1d4d189d-6860-4f52-a30e-67b84f3a8388"},{"Id":"fb50f519-069f-4ec5-8168-ff2f5874b11f","ExternalId":"SAX017000","Name":"Nacra 17","Gender":"X","ClassName":"Nacra 17","PdfUrl":"","XrrEntriesUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/fb50f519-069f-4ec5-8168-ff2f5874b11f?xrrType=Entries","XrrPreliminaryUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/fb50f519-069f-4ec5-8168-ff2f5874b11f?xrrType=PreliminaryResults","XrrFinalUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/fb50f519-069f-4ec5-8168-ff2f5874b11f?xrrType=FinalResults","HtmlUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/ResultsFragment/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/fb50f519-069f-4ec5-8168-ff2f5874b11f"},{"Id":"a10ffdab-8717-4d30-b746-3f3aa56f2f00","IsafId":"FINN","ExternalId":"SAM002000","Name":"Finn","Gender":"M","ClassName":"Finn","PdfUrl":"","XrrEntriesUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/a10ffdab-8717-4d30-b746-3f3aa56f2f00?xrrType=Entries","XrrPreliminaryUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/a10ffdab-8717-4d30-b746-3f3aa56f2f00?xrrType=PreliminaryResults","XrrFinalUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/DownloadXrr/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/a10ffdab-8717-4d30-b746-3f3aa56f2f00?xrrType=FinalResults","HtmlUrl":"http://sailing.st-software.com/SailingInfo/en-US/Home/ResultsFragment/event/ec19d09b-7e65-4e12-9fc4-8cafe449a4dc/regatta/a10ffdab-8717-4d30-b746-3f3aa56f2f00","Published":"2014-04-01T14:57:17+02:00","LastPublishedRoundName":"R5"}]} \ No newline at end of file diff --git a/java/com.sap.sailing.manage2sail/resources/yes2014_Races.json b/java/com.sap.sailing.manage2sail/resources/yes2014_Races.json deleted file mode 100644 index d2a95f6ea4f..00000000000 --- a/java/com.sap.sailing.manage2sail/resources/yes2014_Races.json +++ /dev/null @@ -1 +0,0 @@ -{"Id":"c3436c78-28af-4eaa-9559-d134e3e9053e","Name":"YES - Young Europeans Sailing 2014","TrackingDataHost":"localhost","TrackingDataPort":6699,"XrrUrl":"http://manage2sail.com/en-US/Home/DownloadXrr/event/c3436c78-28af-4eaa-9559-d134e3e9053e/regatta","Regattas":[{"Id":"f5eefaf3-a2e6-420f-bf5c-1c4f3754ee7c","Name":"29er","Gender":"Open","ClassName":"29er","PdfUrl":"","XrrEntriesUrl":"http://manage2sail.com/en-US/Home/DownloadXrr/event/c3436c78-28af-4eaa-9559-d134e3e9053e/regatta/f5eefaf3-a2e6-420f-bf5c-1c4f3754ee7c?xrrType=Entries","XrrPreliminaryUrl":"http://manage2sail.com/en-US/Home/DownloadXrr/event/c3436c78-28af-4eaa-9559-d134e3e9053e/regatta/f5eefaf3-a2e6-420f-bf5c-1c4f3754ee7c?xrrType=PreliminaryResults","XrrFinalUrl":"http://manage2sail.com/en-US/Home/DownloadXrr/event/c3436c78-28af-4eaa-9559-d134e3e9053e/regatta/f5eefaf3-a2e6-420f-bf5c-1c4f3754ee7c?xrrType=FinalResults","Races":[{"Id":"c34c423c-0295-4fe5-91ca-33668162aa3f","Name":"R1","StartTimeUTC":"","EndTimeUTC":"","State":"","IsTracked":true},{"Id":"deec423c-0295-4fe5-91ca-33668162aa3f","Name":"R2","StartTimeUTC":"","EndTimeUTC":"","State":"","IsTracked":true}],"HtmlUrl":"http://manage2sail.com/en-US/Home/ResultsFragment/event/c3436c78-28af-4eaa-9559-d134e3e9053e/regatta/f5eefaf3-a2e6-420f-bf5c-1c4f3754ee7c"}]} \ No newline at end of file diff --git a/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/Manage2SailEventResultsParserImpl.java b/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/Manage2SailEventResultsParserImpl.java index 95710a397dc..2b0df2bf4b8 100644 --- a/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/Manage2SailEventResultsParserImpl.java +++ b/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/Manage2SailEventResultsParserImpl.java @@ -31,8 +31,11 @@ public class Manage2SailEventResultsParserImpl implements Manage2SailEventResult result.setIsafId((String) jsonRoot.get("IsafId")); result.setName((String) jsonRoot.get("Name")); result.setXrrUrl(parseURL(jsonRoot, "XrrUrl")); - result.setTrackingDataHost((String) jsonRoot.get("TrackingDataHost")); - result.setTrackingDataPort(parseInteger(jsonRoot, "TrackingDataPort")); + JSONObject jsonTracking = (JSONObject) jsonRoot.get("Tracking"); + if(jsonTracking != null) { + result.setTrackingDataHost((String) jsonTracking.get("Host")); + result.setTrackingDataPort(parseInteger(jsonTracking, "Port")); + } JSONArray jsonRegattas = (JSONArray) jsonRoot.get("Regattas"); for (Object regattaObject: jsonRegattas) { @@ -47,6 +50,7 @@ public class Manage2SailEventResultsParserImpl implements Manage2SailEventResult regattaResult.setPdfUrl(parseURL(jsonRegatta, "PdfUrl")); regattaResult.setXrrPreliminaryUrl(parseURL(jsonRegatta, "XrrPreliminaryUrl")); regattaResult.setXrrFinalUrl(parseURL(jsonRegatta, "XrrFinalUrl")); + regattaResult.setXrrEntriesUrl(parseURL(jsonRegatta, "XrrEntriesUrl")); regattaResult.setHtmlUrl(parseURL(jsonRegatta, "HtmlUrl")); regattaResult.setPublishedAt(parseDate(jsonRegatta, "Published")); regattaResult.setIsFinal((Boolean) jsonRegatta.get("Final")); @@ -58,10 +62,12 @@ public class Manage2SailEventResultsParserImpl implements Manage2SailEventResult JSONObject jsonRace = (JSONObject) raceObject; raceResult.setId((String) jsonRace.get("Id")); raceResult.setName((String) jsonRace.get("Name")); - raceResult.setStartTimeUTC(parseDate(jsonRace, "StartTimeUTC")); - raceResult.setEndTimeUTC(parseDate(jsonRace, "EndTimeUTC")); - raceResult.setState((String) jsonRace.get("State")); + raceResult.setRaceColumnNumber(parseInteger(jsonRace, "RaceIndex")); + raceResult.setStatus((String) jsonRace.get("Status")); + raceResult.setSeriesName((String) jsonRace.get("Series")); + raceResult.setFleetName((String) jsonRace.get("Fleet")); raceResult.setTracked((Boolean) jsonRace.get("IsTracked")); + raceResult.setStartTime(parseDate(jsonRace, "StartTime")); regattaResult.getRaceResults().add(raceResult); } } @@ -92,9 +98,19 @@ public class Manage2SailEventResultsParserImpl implements Manage2SailEventResult private Integer parseInteger(JSONObject jsonNumber, String attributeName) { Integer result = null; - Number asNumber = (Number) jsonNumber.get(attributeName); - if(asNumber != null) { - result = asNumber.intValue(); + Object object = jsonNumber.get(attributeName); + if(object != null) { + if(object instanceof Number) { + result = ((Number) object).intValue(); + } else if (object instanceof String) { + String numberAsString = (String) object; + if(!numberAsString.isEmpty()) { + try { + result = Integer.parseInt(numberAsString); + } catch (NumberFormatException e) { + } + } + } } return result; } diff --git a/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/RaceResultDescriptor.java b/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/RaceResultDescriptor.java index 06a4ef3c27b..a98d3eabf3f 100644 --- a/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/RaceResultDescriptor.java +++ b/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/RaceResultDescriptor.java @@ -5,45 +5,76 @@ import java.util.Date; public class RaceResultDescriptor { private String id; private String name; - private Date startTimeUTC; - private Date endTimeUTC; - private String state; + private Integer raceColumnNumber; + private String seriesName; + private String fleetName; private Boolean isTracked; - + private Date startTime; + + /** Possible states: Scheduled, StartSequence, Racing, Finished, GeneralRecall, Postponed, Abandoned */ + private String status; + + public Integer getRaceColumnNumber() { + return raceColumnNumber; + } + + public void setRaceColumnNumber(Integer raceColumnNumber) { + this.raceColumnNumber = raceColumnNumber; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getSeriesName() { + return seriesName; + } + + public void setSeriesName(String seriesName) { + this.seriesName = seriesName; + } + + public String getFleetName() { + return fleetName; + } + + public void setFleetName(String fleetName) { + this.fleetName = fleetName; + } + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } - public Date getStartTimeUTC() { - return startTimeUTC; - } - public void setStartTimeUTC(Date startTimeUTC) { - this.startTimeUTC = startTimeUTC; - } - public Date getEndTimeUTC() { - return endTimeUTC; - } - public void setEndTimeUTC(Date endTimeUTC) { - this.endTimeUTC = endTimeUTC; - } - public String getState() { - return state; - } - public void setState(String state) { - this.state = state; - } + public Boolean isTracked() { return isTracked; } + public void setTracked(Boolean isTracked) { this.isTracked = isTracked; } + + public Date getStartTime() { + return startTime; + } + + public void setStartTime(Date startTime) { + this.startTime = startTime; + } } diff --git a/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/RegattaResultDescriptor.java b/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/RegattaResultDescriptor.java index 628bfee12d2..0d9ded55c56 100644 --- a/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/RegattaResultDescriptor.java +++ b/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/RegattaResultDescriptor.java @@ -17,6 +17,7 @@ public class RegattaResultDescriptor { private URL pdfUrl; private URL xrrPreliminaryUrl; private URL xrrFinalUrl; + private URL xrrEntriesUrl; private URL htmlUrl; private Date publishedAt; private Boolean isFinal; @@ -106,4 +107,12 @@ public class RegattaResultDescriptor { public void setRaceResults(List raceResults) { this.raceResults = raceResults; } + + public URL getXrrEntriesUrl() { + return xrrEntriesUrl; + } + + public void setXrrEntriesUrl(URL xrrEntriesUrl) { + this.xrrEntriesUrl = xrrEntriesUrl; + } } diff --git a/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/TestManage2SailEventResultsParser.java b/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/TestManage2SailEventResultsParser.java index 639769026e3..6a31b123c09 100644 --- a/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/TestManage2SailEventResultsParser.java +++ b/java/com.sap.sailing.manage2sail/src/com/sap/sailing/manage2sail/TestManage2SailEventResultsParser.java @@ -8,7 +8,7 @@ public class TestManage2SailEventResultsParser { public static EventResultDescriptor getTestEventResult() throws IOException { Manage2SailEventResultsParser parser = new Manage2SailEventResultsParserImpl(); - InputStream is = TestManage2SailEventResultsParser.class.getClassLoader().getResourceAsStream("yes2014_Races.json"); + InputStream is = TestManage2SailEventResultsParser.class.getClassLoader().getResourceAsStream("worldcupMallorca2013_Races.json"); return parser.getEventResult(is); } } diff --git a/java/com.sap.sailing.mongodb.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.mongodb.test/META-INF/MANIFEST.MF index d5eb223095b..c2ae4e6911f 100755 --- a/java/com.sap.sailing.mongodb.test/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.mongodb.test/META-INF/MANIFEST.MF @@ -17,8 +17,11 @@ Require-Bundle: com.sap.sailing.domain, org.json.simple;bundle-version="1.1.0", com.sap.sailing.domain.swisstimingadapter, org.objenesis;bundle-version="1.3.0", - org.junit4;bundle-version="4.8.2", + org.junit4;bundle-version="4.8.2", org.hamcrest;bundle-version="1.1.0", - org.mockito.mockito-core;bundle-version="1.9.5" -Import-Package: com.sap.sailing.domain.common.impl + org.mockito.mockito-core;bundle-version="1.9.5", + com.sap.sailing.domain.racelogtrackingadapter.testsupport, + com.sap.sailing.server.gateway;bundle-version="1.0.0" +Import-Package: com.sap.sailing.domain.common.impl, + com.sap.sailing.domain.common.racelog.tracking diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/CollectionNameServiceTest.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/CollectionNameServiceTest.java index 2cc5c4394a9..589954b3e9b 100644 --- a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/CollectionNameServiceTest.java +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/CollectionNameServiceTest.java @@ -6,20 +6,20 @@ import com.sap.sailing.mongodb.AlreadyRegisteredException; import com.sap.sailing.mongodb.MongoDBService; public class CollectionNameServiceTest { - - @Test - public void shouldWork() throws AlreadyRegisteredException { - MongoDBService service = MongoDBService.INSTANCE; - service.registerExclusively(TestInterface.class, "a"); - service.registerExclusively(TestInterface.class, "b"); - service.registerExclusively(TestInterface.class, "B"); - service.registerExclusively(TestInterface.class, "B"); - } - - @Test(expected=AlreadyRegisteredException.class) - public void shouldNotWork() throws AlreadyRegisteredException { - MongoDBService service = MongoDBService.INSTANCE; - service.registerExclusively(TestInterface.class, "b"); - service.registerExclusively(com.sap.sailing.mongodb.test.needadifferentpackage.TestInterface.class, "b"); - } + + @Test + public void shouldWork() throws AlreadyRegisteredException { + MongoDBService service = MongoDBService.INSTANCE; + service.registerExclusively(TestInterface.class, "a"); + service.registerExclusively(TestInterface.class, "b"); + service.registerExclusively(TestInterface.class, "B"); + service.registerExclusively(TestInterface.class, "B"); + } + + @Test(expected=AlreadyRegisteredException.class) + public void shouldNotWork() throws AlreadyRegisteredException { + MongoDBService service = MongoDBService.INSTANCE; + service.registerExclusively(TestInterface.class, "b"); + service.registerExclusively(com.sap.sailing.mongodb.test.needadifferentpackage.TestInterface.class, "b"); + } } diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/StoreAndLoadDeviceIdentifierTest.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/StoreAndLoadDeviceIdentifierTest.java new file mode 100644 index 00000000000..c1f70fb1b2b --- /dev/null +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/StoreAndLoadDeviceIdentifierTest.java @@ -0,0 +1,151 @@ +package com.sap.sailing.mongodb.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.net.UnknownHostException; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; + +import com.mongodb.MongoException; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.impl.CompetitorImpl; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.impl.TimeRangeImpl; +import com.sap.sailing.domain.common.impl.Util; +import com.sap.sailing.domain.common.impl.Util.Triple; +import com.sap.sailing.domain.common.racelog.tracking.NoCorrespondingServiceRegisteredException; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinderFactory; +import com.sap.sailing.domain.persistence.PersistenceFactory; +import com.sap.sailing.domain.persistence.impl.DomainObjectFactoryImpl; +import com.sap.sailing.domain.persistence.impl.MongoObjectFactoryImpl; +import com.sap.sailing.domain.persistence.impl.MongoRaceLogStoreImpl; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventFactory; +import com.sap.sailing.domain.racelog.RaceLogIdentifier; +import com.sap.sailing.domain.racelog.impl.RaceLogEventAuthorImpl; +import com.sap.sailing.domain.racelog.tracking.DeviceCompetitorMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.PlaceHolderDeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.test.mock.MockEmptyServiceFinderFactory; +import com.sap.sailing.domain.racelog.tracking.test.mock.MockSmartphoneImeiServiceFinderFactory; +import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifier; +import com.sap.sailing.server.gateway.trackfiles.TrackFileImportDeviceIdentifierImpl; + +public class StoreAndLoadDeviceIdentifierTest extends AbstractMongoDBTest { + public StoreAndLoadDeviceIdentifierTest() throws UnknownHostException, MongoException { + super(); + } + + protected static final String group = "group"; + protected static final String column = "column"; + protected static final String fleet = "fleet"; + protected final RaceLogEventAuthor author = new RaceLogEventAuthorImpl("author", 0); + protected RaceLogIdentifier logIdentifier; + + protected MongoObjectFactoryImpl mongoFactory; + protected DomainObjectFactoryImpl domainFactory; + protected RaceLog raceLog; + + private TimePoint now() { + return MillisecondsTimePoint.now(); + } + + @Before + public void setup() throws UnknownHostException, MongoException, InterruptedException { + dropTestDB(); + + logIdentifier = mock(RaceLogIdentifier.class); + Triple triple = new Triple("a", "b", UUID.randomUUID().toString()); + when(logIdentifier.getIdentifier()).thenReturn(triple); + when(logIdentifier.getDeprecatedIdentifier()).thenReturn(""); + } + + private RaceLog loadRaceLog() { + return new MongoRaceLogStoreImpl(mongoFactory, domainFactory).getRaceLog(logIdentifier, true); + } + + private void createFactories(TypeBasedServiceFinderFactory factory) { + mongoFactory = (MongoObjectFactoryImpl) PersistenceFactory.INSTANCE + .getMongoObjectFactory(getMongoService(), factory); + domainFactory = (DomainObjectFactoryImpl) PersistenceFactory.INSTANCE + .getDomainObjectFactory(getMongoService(), DomainFactory.INSTANCE, factory); + + raceLog = loadRaceLog(); + } + + private DeviceIdentifier storeAndLoad(DeviceIdentifier device, TypeBasedServiceFinderFactory forStoring, TypeBasedServiceFinderFactory forLoading) { + createFactories(forStoring); + Competitor c = new CompetitorImpl("a", "a", null, null, null); + + raceLog.add(RaceLogEventFactory.INSTANCE.createDeviceCompetitorMappingEvent(now(), author, device, + c, 0, now(), now())); + + createFactories(forLoading); + + RaceLog loadedRaceLog = loadRaceLog(); + loadedRaceLog.lockForRead(); + assertEquals(1, Util.size(loadedRaceLog.getRawFixes())); + DeviceCompetitorMappingEvent mapping = (DeviceCompetitorMappingEvent) loadedRaceLog.getRawFixes().iterator().next(); + loadedRaceLog.unlockAfterRead(); + + return mapping.getDevice(); + } + + @Test + public void testNormal() throws TransformationException, NoCorrespondingServiceRegisteredException { + TypeBasedServiceFinderFactory factory = new MockSmartphoneImeiServiceFinderFactory(); + + DeviceIdentifier smartphone = new SmartphoneImeiIdentifier("abc"); + DeviceIdentifier loaded = storeAndLoad(smartphone, factory, factory); + + assertEquals(smartphone, loaded); + } + + @Test + public void testNoAppropriateDeserializer() throws TransformationException, NoCorrespondingServiceRegisteredException { + TypeBasedServiceFinderFactory forStoring = new MockSmartphoneImeiServiceFinderFactory(); + TypeBasedServiceFinderFactory forLoading = new MockEmptyServiceFinderFactory(); + + DeviceIdentifier smartphone = new SmartphoneImeiIdentifier("abc"); + DeviceIdentifier loaded = storeAndLoad(smartphone, forStoring, forLoading); + + assertTrue(loaded instanceof PlaceHolderDeviceIdentifier); + assertEquals(smartphone.getIdentifierType(), loaded.getIdentifierType()); + assertEquals(smartphone.getStringRepresentation(), loaded.getStringRepresentation()); + } + + @Test + public void testUseStringRepForDeserializing() throws TransformationException, NoCorrespondingServiceRegisteredException { + TypeBasedServiceFinderFactory forLoading = new MockSmartphoneImeiServiceFinderFactory(); + TypeBasedServiceFinderFactory forStoring = new MockEmptyServiceFinderFactory(); + + DeviceIdentifier smartphone = new SmartphoneImeiIdentifier("abc"); + DeviceIdentifier loaded = storeAndLoad(smartphone, forStoring, forLoading); + + //smartphone imei identifier can be restored from string rep + assertTrue(loaded instanceof SmartphoneImeiIdentifier); + assertEquals(smartphone.getIdentifierType(), loaded.getIdentifierType()); + assertEquals(smartphone.getStringRepresentation(), loaded.getStringRepresentation()); + } + + @Test + public void testNoAppropriateSerializer() throws TransformationException, NoCorrespondingServiceRegisteredException { + TypeBasedServiceFinderFactory forLoading = new MockSmartphoneImeiServiceFinderFactory(); + TypeBasedServiceFinderFactory forStoring = new MockEmptyServiceFinderFactory(); + + DeviceIdentifier trackFile = new TrackFileImportDeviceIdentifierImpl("FILE", "track", TimeRangeImpl.create(100, 200), 30); + DeviceIdentifier loaded = storeAndLoad(trackFile, forStoring, forLoading); + assertTrue(loaded instanceof PlaceHolderDeviceIdentifier); + assertEquals(trackFile.getIdentifierType(), loaded.getIdentifierType()); + assertEquals(trackFile.getStringRepresentation(), loaded.getStringRepresentation()); + } +} diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/StoreAndLoadRaceLogEventsTest.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/StoreAndLoadRaceLogEventsTest.java index 141fad10644..c12802eead7 100644 --- a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/StoreAndLoadRaceLogEventsTest.java +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/StoreAndLoadRaceLogEventsTest.java @@ -1,6 +1,9 @@ package com.sap.sailing.mongodb.test; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -15,8 +18,10 @@ import org.junit.Test; import com.mongodb.DBObject; import com.mongodb.MongoException; +import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Mark; import com.sap.sailing.domain.common.TimePoint; import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; import com.sap.sailing.domain.common.impl.Util; @@ -30,21 +35,32 @@ import com.sap.sailing.domain.persistence.impl.MongoObjectFactoryImpl; import com.sap.sailing.domain.racelog.RaceLogCourseAreaChangedEvent; import com.sap.sailing.domain.racelog.RaceLogEvent; import com.sap.sailing.domain.racelog.RaceLogEventAuthor; -import com.sap.sailing.domain.racelog.RaceLogEventFactory; +import com.sap.sailing.domain.racelog.RaceLogEventRestoreFactory; import com.sap.sailing.domain.racelog.RaceLogFlagEvent; import com.sap.sailing.domain.racelog.RaceLogIdentifier; import com.sap.sailing.domain.racelog.RaceLogPassChangeEvent; import com.sap.sailing.domain.racelog.RaceLogRaceStatusEvent; import com.sap.sailing.domain.racelog.RaceLogStartTimeEvent; +import com.sap.sailing.domain.racelog.RevokeEvent; import com.sap.sailing.domain.racelog.impl.RaceLogEventAuthorImpl; +import com.sap.sailing.domain.racelog.tracking.CloseOpenEndedDeviceMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DefineMarkEvent; +import com.sap.sailing.domain.racelog.tracking.DenoteForTrackingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceCompetitorMappingEvent; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.DeviceMarkMappingEvent; +import com.sap.sailing.domain.racelog.tracking.RegisterCompetitorEvent; +import com.sap.sailing.domain.racelog.tracking.test.mock.MockSmartphoneImeiServiceFinderFactory; +import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifier; public class StoreAndLoadRaceLogEventsTest extends AbstractMongoDBTest { protected MongoObjectFactoryImpl mongoFactory = (MongoObjectFactoryImpl) PersistenceFactory.INSTANCE - .getMongoObjectFactory(getMongoService()); + .getMongoObjectFactory(getMongoService(), new MockSmartphoneImeiServiceFinderFactory()); protected DomainObjectFactoryImpl domainFactory = (DomainObjectFactoryImpl) PersistenceFactory.INSTANCE - .getDomainObjectFactory(getMongoService(), DomainFactory.INSTANCE); - protected RaceLogEventFactory eventFactory = RaceLogEventFactory.INSTANCE; + .getDomainObjectFactory(getMongoService(), DomainFactory.INSTANCE, + new MockSmartphoneImeiServiceFinderFactory()); + protected RaceLogEventRestoreFactory eventFactory = RaceLogEventRestoreFactory.INSTANCE; protected RaceLogIdentifier logIdentifier; @@ -156,6 +172,115 @@ public class StoreAndLoadRaceLogEventsTest extends AbstractMongoDBTest { assertEquals(startTime, actualEvent.getStartTime()); } + @Test + public void testStoreAndLoadDeviceCompetitorMappingEvent() { + DeviceIdentifier device = new SmartphoneImeiIdentifier("a"); + Competitor mappedTo = DomainFactory.INSTANCE.getOrCreateCompetitor("abc", "abc", null, null, null); + TimePoint from = new MillisecondsTimePoint(20); + TimePoint to = new MillisecondsTimePoint(30); + DeviceCompetitorMappingEvent expectedEvent = eventFactory.createDeviceCompetitorMappingEvent( + expectedEventTime, author, expectedEventTime, expectedId, device, mappedTo, expectedPassId, from, to); + + DBObject dbObject = mongoFactory.storeRaceLogEntry(logIdentifier, expectedEvent); + DeviceCompetitorMappingEvent actualEvent = loadEvent(dbObject); + + assertBaseFields(expectedEvent, actualEvent); + assertEquals(device, actualEvent.getDevice()); + assertEquals(from, actualEvent.getFrom()); + assertEquals(to, actualEvent.getTo()); + assertEquals(mappedTo, actualEvent.getMappedTo()); + } + + @Test + public void testStoreAndLoadDeviceMarkMappingEvent() { + DeviceIdentifier device = new SmartphoneImeiIdentifier("a"); + Mark mappedTo = DomainFactory.INSTANCE.getOrCreateMark("abc", "abc"); + TimePoint from = new MillisecondsTimePoint(20); + TimePoint to = new MillisecondsTimePoint(30); + DeviceMarkMappingEvent expectedEvent = eventFactory.createDeviceMarkMappingEvent( + expectedEventTime, author, expectedEventTime, expectedId, device, mappedTo, expectedPassId, from, to); + + DBObject dbObject = mongoFactory.storeRaceLogEntry(logIdentifier, expectedEvent); + DeviceMarkMappingEvent actualEvent = loadEvent(dbObject); + + assertBaseFields(expectedEvent, actualEvent); + assertEquals(device, actualEvent.getDevice()); + assertEquals(from, actualEvent.getFrom()); + assertEquals(to, actualEvent.getTo()); + assertEquals(mappedTo, actualEvent.getMappedTo()); + } + + @Test + public void testStoreAndLoadDenoteForTrackingEvent() { + String raceName = "race"; + BoatClass boatClass = DomainFactory.INSTANCE.getOrCreateBoatClass("49er"); + UUID raceId = UUID.randomUUID(); + DenoteForTrackingEvent expectedEvent = eventFactory.createDenoteForTrackingEvent( + expectedEventTime, author, expectedEventTime, expectedId, expectedPassId, raceName, boatClass, + raceId); + + DBObject dbObject = mongoFactory.storeRaceLogEntry(logIdentifier, expectedEvent); + DenoteForTrackingEvent actualEvent = loadEvent(dbObject); + + assertBaseFields(expectedEvent, actualEvent); + assertEquals(boatClass, actualEvent.getBoatClass()); + assertEquals(raceName, actualEvent.getRaceName()); + assertEquals(raceId, actualEvent.getRaceId()); + } + + @Test + public void testStoreAndLoadRevokeEvent() { + UUID revokedEventId = UUID.randomUUID(); + RevokeEvent expectedEvent = eventFactory.createRevokeEvent( + expectedEventTime, author, expectedEventTime, expectedId, expectedPassId, revokedEventId); + + DBObject dbObject = mongoFactory.storeRaceLogEntry(logIdentifier, expectedEvent); + RevokeEvent actualEvent = loadEvent(dbObject); + + assertBaseFields(expectedEvent, actualEvent); + assertEquals(revokedEventId, actualEvent.getRevokedEventId()); + } + + @Test + public void testStoreAndLoadRegisterCompetitorEvent() { + RegisterCompetitorEvent expectedEvent = eventFactory.createRegisterCompetitorEvent( + expectedEventTime, author, expectedEventTime, expectedId, expectedPassId, + DomainFactory.INSTANCE.getOrCreateCompetitor("comp", "comp", null, null, null)); + + DBObject dbObject = mongoFactory.storeRaceLogEntry(logIdentifier, expectedEvent); + RegisterCompetitorEvent actualEvent = loadEvent(dbObject); + + assertBaseFields(expectedEvent, actualEvent); + assertEquals(expectedEvent.getCompetitor(), actualEvent.getCompetitor()); + } + + @Test + public void testStoreAndLoadDefineMarkEvent() { + DefineMarkEvent expectedEvent = eventFactory.createDefineMarkEvent( + expectedEventTime, author, expectedEventTime, expectedId, expectedPassId, + DomainFactory.INSTANCE.getOrCreateMark("mytestmark")); + + DBObject dbObject = mongoFactory.storeRaceLogEntry(logIdentifier, expectedEvent); + DefineMarkEvent actualEvent = loadEvent(dbObject); + + assertBaseFields(expectedEvent, actualEvent); + assertEquals(expectedEvent.getMark(), actualEvent.getMark()); + } + + @Test + public void testStoreAndLoadCloseOpenDeviceMappingEvent() { + CloseOpenEndedDeviceMappingEvent expectedEvent = eventFactory.createCloseOpenEndedDeviceMappingEvent( + expectedEventTime, author, expectedEventTime, expectedId, expectedPassId, + UUID.randomUUID(), new MillisecondsTimePoint(49)); + + DBObject dbObject = mongoFactory.storeRaceLogEntry(logIdentifier, expectedEvent); + CloseOpenEndedDeviceMappingEvent actualEvent = loadEvent(dbObject); + + assertBaseFields(expectedEvent, actualEvent); + assertEquals(expectedEvent.getDeviceMappingEventId(), actualEvent.getDeviceMappingEventId()); + assertEquals(expectedEvent.getClosingTimePoint(), actualEvent.getClosingTimePoint()); + } + /** * Will always wait a couple of milliseconds to ensure that {@link RaceLogEvent#getCreatedAt()} has passed. */ diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/StoreGPSFixTrackTest.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/StoreGPSFixTrackTest.java new file mode 100644 index 00000000000..f3a9bda27f5 --- /dev/null +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/StoreGPSFixTrackTest.java @@ -0,0 +1,103 @@ +package com.sap.sailing.mongodb.test; + +import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertEquals; + +import java.net.UnknownHostException; + +import org.junit.After; +import org.junit.Test; + +import com.mongodb.DB; +import com.mongodb.MongoException; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.common.TimePoint; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.impl.MillisecondsTimePoint; +import com.sap.sailing.domain.common.impl.Util; +import com.sap.sailing.domain.common.racelog.tracking.NoCorrespondingServiceRegisteredException; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.common.racelog.tracking.TypeBasedServiceFinderFactory; +import com.sap.sailing.domain.persistence.DomainObjectFactory; +import com.sap.sailing.domain.persistence.MongoObjectFactory; +import com.sap.sailing.domain.persistence.PersistenceFactory; +import com.sap.sailing.domain.persistence.impl.CollectionNames; +import com.sap.sailing.domain.persistence.racelog.tracking.impl.MongoGPSFixStoreImpl; +import com.sap.sailing.domain.racelog.RaceLog; +import com.sap.sailing.domain.racelog.RaceLogEventAuthor; +import com.sap.sailing.domain.racelog.RaceLogEventFactory; +import com.sap.sailing.domain.racelog.impl.RaceLogEventAuthorImpl; +import com.sap.sailing.domain.racelog.impl.RaceLogImpl; +import com.sap.sailing.domain.racelog.tracking.DeviceIdentifier; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; +import com.sap.sailing.domain.racelog.tracking.test.mock.MockSmartphoneImeiServiceFinderFactory; +import com.sap.sailing.domain.racelog.tracking.test.mock.SmartphoneImeiIdentifier; +import com.sap.sailing.domain.tracking.GPSFix; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.impl.DynamicGPSFixMovingTrackImpl; +import com.sap.sailing.domain.tracking.impl.DynamicGPSFixTrackImpl; +import com.sap.sailing.domain.tracking.impl.GPSFixImpl; +import com.sap.sailing.domain.tracking.impl.GPSFixMovingImpl; + +public class StoreGPSFixTrackTest extends AbstractMongoDBTest { + public StoreGPSFixTrackTest() throws UnknownHostException, MongoException { + super(); + } + + @After + public void dropCollection() { + DB db = getMongoService().getDB(); + db.getCollection(CollectionNames.GPS_FIXES.name()).drop(); + } + + @Test + public void testStoreAndLoadFixes() throws TransformationException, NoCorrespondingServiceRegisteredException { + TypeBasedServiceFinderFactory factory = new MockSmartphoneImeiServiceFinderFactory(); + RaceLogEventAuthor author = new RaceLogEventAuthorImpl("author", 0); + Competitor comp = DomainFactory.INSTANCE.getOrCreateCompetitor("comp", "comp", null, null, null); + Mark mark = DomainFactory.INSTANCE.getOrCreateMark("mark"); + MongoObjectFactory mongoObjectFactory = PersistenceFactory.INSTANCE.getMongoObjectFactory(getMongoService(), factory); + DomainObjectFactory domainObjectFactory = PersistenceFactory.INSTANCE.getDomainObjectFactory(getMongoService(), DomainFactory.INSTANCE, factory); + GPSFixStore store = new MongoGPSFixStoreImpl(mongoObjectFactory, domainObjectFactory, factory); + + TimePoint time0 = new MillisecondsTimePoint(0); + TimePoint time1 = new MillisecondsTimePoint(1); + TimePoint time2 = new MillisecondsTimePoint(2); + + GPSFixMoving fix1 = new GPSFixMovingImpl(new DegreePosition(30, 40), time0, + new KnotSpeedWithBearingImpl(0, new DegreeBearingImpl(0))); + GPSFixMoving fix2 = new GPSFixMovingImpl(new DegreePosition(30, 40), time1, + new KnotSpeedWithBearingImpl(0, new DegreeBearingImpl(0))); + //lies outside of mapping range + GPSFixMoving fix3 = new GPSFixMovingImpl(new DegreePosition(30, 40), time2, + new KnotSpeedWithBearingImpl(0, new DegreeBearingImpl(0))); + DeviceIdentifier device1 = new SmartphoneImeiIdentifier("a"); + store.storeFix(device1, fix1); + store.storeFix(device1, fix2); + store.storeFix(device1, fix3); + + GPSFix fix4 = new GPSFixImpl(new DegreePosition(40, 50), time0); + DeviceIdentifier device2 = new SmartphoneImeiIdentifier("b"); + store.storeFix(device2, fix4); + + RaceLog raceLog = new RaceLogImpl("racelog"); + raceLog.add(RaceLogEventFactory.INSTANCE.createDeviceCompetitorMappingEvent(time0, author, device1, comp, 0, time0, time1)); + raceLog.add(RaceLogEventFactory.INSTANCE.createDeviceMarkMappingEvent(time0, author, device2, mark, 0, time0, time1)); + + DynamicGPSFixMovingTrackImpl track1 = new DynamicGPSFixMovingTrackImpl<>(comp, 0); + store.loadCompetitorTrack(track1, raceLog, comp); + DynamicGPSFixTrackImpl track2 = new DynamicGPSFixTrackImpl<>(mark, 0); + store.loadMarkTrack(track2, raceLog, mark); + track1.lockForRead(); + assertEquals(2, Util.size(track1.getRawFixes())); + assertTrue(track1.getFirstRawFix() instanceof GPSFixMoving); + track1.unlockAfterRead(); + track2.lockForRead(); + assertEquals(1, Util.size(track2.getRawFixes())); + track2.unlockAfterRead(); + } +} diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndLoadingEventsAndRegattas.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndLoadingEventsAndRegattas.java index 2527360d9b0..ba990fb1ef6 100755 --- a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndLoadingEventsAndRegattas.java +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndLoadingEventsAndRegattas.java @@ -67,6 +67,7 @@ import com.sap.sailing.domain.persistence.MongoObjectFactory; import com.sap.sailing.domain.persistence.PersistenceFactory; import com.sap.sailing.domain.persistence.impl.CollectionNames; import com.sap.sailing.domain.persistence.media.MediaDBFactory; +import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; import com.sap.sailing.domain.test.AbstractLeaderboardTest; import com.sap.sailing.domain.test.mock.MockedTrackedRaceWithFixedRank; import com.sap.sailing.domain.tracking.DynamicTrackedRace; @@ -177,7 +178,7 @@ public class TestStoringAndLoadingEventsAndRegattas extends AbstractMongoDBTest @Test public void testLoadStoreSimpleRegattaLeaderboard() { RacingEventService res = new RacingEventServiceImpl(PersistenceFactory.INSTANCE.getDomainObjectFactory(getMongoService(), DomainFactory.INSTANCE), PersistenceFactory.INSTANCE - .getMongoObjectFactory(getMongoService()), MediaDBFactory.INSTANCE.getMediaDB(getMongoService()), EmptyWindStore.INSTANCE); + .getMongoObjectFactory(getMongoService()), MediaDBFactory.INSTANCE.getMediaDB(getMongoService()), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE); final int numberOfQualifyingRaces = 5; final int numberOfFinalRaces = 7; final String regattaBaseName = "Kieler Woche"; @@ -291,7 +292,7 @@ public class TestStoringAndLoadingEventsAndRegattas extends AbstractMongoDBTest private RacingEventServiceImpl createRacingEventServiceWithOneMockedTrackedRace(final DynamicTrackedRace q2YellowTrackedRace) { return new RacingEventServiceImpl(PersistenceFactory.INSTANCE.getDomainObjectFactory(getMongoService(), DomainFactory.INSTANCE), PersistenceFactory.INSTANCE - .getMongoObjectFactory(getMongoService()), MediaDBFactory.INSTANCE.getMediaDB(getMongoService()), EmptyWindStore.INSTANCE) { + .getMongoObjectFactory(getMongoService()), MediaDBFactory.INSTANCE.getMediaDB(getMongoService()), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE) { @Override public DynamicTrackedRace getExistingTrackedRace(RegattaAndRaceIdentifier raceIdentifier) { return q2YellowTrackedRace; @@ -513,7 +514,7 @@ public class TestStoringAndLoadingEventsAndRegattas extends AbstractMongoDBTest regatta.addRace(racedef); RacingEventServiceImpl evs = new RacingEventServiceImpl(PersistenceFactory.INSTANCE.getDomainObjectFactory(getMongoService(), DomainFactory.INSTANCE), PersistenceFactory.INSTANCE - .getMongoObjectFactory(getMongoService()), MediaDBFactory.INSTANCE.getMediaDB(getMongoService()), EmptyWindStore.INSTANCE); + .getMongoObjectFactory(getMongoService()), MediaDBFactory.INSTANCE.getMediaDB(getMongoService()), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE); assertNull(evs.getRememberedRegattaForRace(racedef.getId())); evs.raceAdded(regatta, racedef); assertNotNull(evs.getRememberedRegattaForRace(racedef.getId())); diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingLeaderboardGroups.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingLeaderboardGroups.java index e1c464181b6..e50d08e7749 100755 --- a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingLeaderboardGroups.java +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingLeaderboardGroups.java @@ -49,6 +49,7 @@ import com.sap.sailing.domain.persistence.PersistenceFactory; import com.sap.sailing.domain.persistence.impl.DomainObjectFactoryImpl; import com.sap.sailing.domain.persistence.impl.MongoObjectFactoryImpl; import com.sap.sailing.domain.persistence.media.MediaDBFactory; +import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; import com.sap.sailing.domain.test.mock.MockedTrackedRaceWithFixedRankAndManyCompetitors; import com.sap.sailing.domain.tracking.impl.EmptyWindStore; import com.sap.sailing.mongodb.MongoDBService; @@ -317,7 +318,7 @@ public class TestStoringAndRetrievingLeaderboardGroups extends AbstractMongoDBTe // the leaderboard named leaderboardNames[2] occurs in both groups RacingEventService racingEventService = new RacingEventServiceImpl(PersistenceFactory.INSTANCE.getDomainObjectFactory(MongoDBService.INSTANCE, DomainFactory.INSTANCE), PersistenceFactory.INSTANCE - .getMongoObjectFactory(MongoDBService.INSTANCE), MediaDBFactory.INSTANCE.getMediaDB(MongoDBService.INSTANCE), EmptyWindStore.INSTANCE); // expected to load leaderboard groups + .getMongoObjectFactory(MongoDBService.INSTANCE), MediaDBFactory.INSTANCE.getMediaDB(MongoDBService.INSTANCE), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE); // expected to load leaderboard groups final LeaderboardGroup loadedLeaderboardGroup1 = racingEventService.getLeaderboardGroupByName(groupName1); final LeaderboardGroup loadedLeaderboardGroup2 = racingEventService.getLeaderboardGroupByName(groupName2); diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingWindTracksTest.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingWindTracksTest.java index 10440215b4c..12a211f324b 100755 --- a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingWindTracksTest.java +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingWindTracksTest.java @@ -30,6 +30,7 @@ import com.sap.sailing.domain.common.impl.WindSourceImpl; import com.sap.sailing.domain.persistence.impl.DomainObjectFactoryImpl; import com.sap.sailing.domain.persistence.impl.MongoObjectFactoryImpl; import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; +import com.sap.sailing.domain.racelog.tracking.EmptyGPSFixStore; import com.sap.sailing.domain.test.AbstractTracTracLiveTest; import com.sap.sailing.domain.tracking.DynamicRaceDefinitionSet; import com.sap.sailing.domain.tracking.DynamicTrackedRace; @@ -85,7 +86,7 @@ public class TestStoringAndRetrievingWindTracksTest extends AbstractTracTracLive }, /* trackedRegattaRegistry */ null, /*courseDesignUpdateURI*/ null, /*tracTracUsername*/ null, /*tracTracPassword*/ null, ReceiverType.RACECOURSE); addListenersForStoredDataAndStartController(typeControllers); RaceDefinition race = domainFactory.getAndWaitForRaceDefinition(getTracTracEvent().getRaceList().iterator().next().getId()); - DynamicTrackedRace trackedRace = trackedRegatta.createTrackedRace(race, Collections. emptyList(), EmptyWindStore.INSTANCE, + DynamicTrackedRace trackedRace = trackedRegatta.createTrackedRace(race, Collections. emptyList(), EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, /* delayToLiveInMillis */ 0l, /* millisecondsOverWhichToAverageWind */ 30000, /* millisecondsOverWhichToAverageSpeed */ 10000, new DynamicRaceDefinitionSet() { @Override public void addRaceDefinition(RaceDefinition race, DynamicTrackedRace trackedRace) { diff --git a/java/com.sap.sailing.mongodb/src/com/sap/sailing/mongodb/AlreadyRegisteredException.java b/java/com.sap.sailing.mongodb/src/com/sap/sailing/mongodb/AlreadyRegisteredException.java index 808ae9ffb9c..0990fa1d676 100644 --- a/java/com.sap.sailing.mongodb/src/com/sap/sailing/mongodb/AlreadyRegisteredException.java +++ b/java/com.sap.sailing.mongodb/src/com/sap/sailing/mongodb/AlreadyRegisteredException.java @@ -2,8 +2,8 @@ package com.sap.sailing.mongodb; public class AlreadyRegisteredException extends Exception { - /** - * - */ - private static final long serialVersionUID = -9052060438723088594L; + /** + * + */ + private static final long serialVersionUID = -9052060438723088594L; } diff --git a/java/com.sap.sailing.selenium.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.selenium.test/META-INF/MANIFEST.MF index 15bf89d47eb..eb259fcb76f 100644 --- a/java/com.sap.sailing.selenium.test/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.selenium.test/META-INF/MANIFEST.MF @@ -5,8 +5,9 @@ Bundle-SymbolicName: com.sap.sailing.selenium.test Bundle-Version: 1.0.0.qualifier Bundle-Vendor: SAP Bundle-RequiredExecutionEnvironment: JavaSE-1.7 -Require-Bundle: org.junit4;bundle-version="4.8.2", - org.openqa.selenium.osgi;bundle-version="2.39.0", +Require-Bundle: org.hamcrest;bundle-version="1.1.0", + org.junit4;bundle-version="4.8.2", + org.openqa.selenium.osgi;bundle-version="2.40.0", com.sap.sailing.mongodb;bundle-version="1.0.0", com.mongodb.driver;bundle-version="2.6.2", com.sap.sailing.domain.common diff --git a/java/com.sap.sailing.selenium.test/ci-test-environment.xml b/java/com.sap.sailing.selenium.test/ci-test-environment.xml index d99fc2c0958..8dc260e081a 100755 --- a/java/com.sap.sailing.selenium.test/ci-test-environment.xml +++ b/java/com.sap.sailing.selenium.test/ci-test-environment.xml @@ -19,7 +19,7 @@ The folder where screenshots (attachments) are stored. --> - file:///home/hudson/repo/jobs/SAPSailingAnalytics-selenium/workspace/java/com.sap.sailing.selenium.test/bin/surefire-reports/ + ./bin/surefire-reports/ http://riccardo:riccardo@localhost:8888/ - + ./bin/surefire-reports/