diff --git a/configuration/syncEC2ElbLogs b/configuration/syncEC2ElbLogs new file mode 100755 index 00000000000..25affdba085 --- /dev/null +++ b/configuration/syncEC2ElbLogs @@ -0,0 +1,2 @@ +#!/bin/sh +/usr/bin/s3cmd -c /root/.s3cfg sync s3://sapsailing-access-logs/elb-access-logs/ /var/log/old/elb-access-logs/ >/var/log/last-elb-access-logs-sync.out 2>/var/log/last-elb-access-logs-sync.err diff --git a/configuration/unique_ips_per_referrer b/configuration/unique_ips_per_referrer index d06f2201d0a..518f7f27509 100755 --- a/configuration/unique_ips_per_referrer +++ b/configuration/unique_ips_per_referrer @@ -16,6 +16,8 @@ mkdir -p "$STATS" # A sample line: # 505Worlds2012.sapsailing.com 66.249.73.53 - - [12/Jan/2014:03:33:11 +0000] "GET /robots.txt HTTP/1.1" 404 238 "-" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" +echo Starting "$0" at `date` on file set "$*" + for i in $*; do grep -q "^$i\$" $VISITED_FILES if [ "$?" = "0" ]; then @@ -50,4 +52,4 @@ for month in `cat $MONTHS`; do done | tee $STATS/unique-ips-days-useragents-per-event-$month echo "Wrote per-month results to $STATS/unique-ips-days-useragents-per-event-$month" done -echo "Done." +echo "Done at `date`." diff --git a/configuration/unique_ips_per_referrer_generate_month_results_only b/configuration/unique_ips_per_referrer_generate_month_results_only new file mode 100755 index 00000000000..dcea8f54211 --- /dev/null +++ b/configuration/unique_ips_per_referrer_generate_month_results_only @@ -0,0 +1,16 @@ +#!/bin/bash + +CACHE=/var/log/old/cache/unique-ips-per-referrer +VISITED_FILES=$CACHE/visited +STATS=$CACHE/stats +MONTHS=$STATS/months + +for month in `cat $MONTHS`; do + for i in ${STATS}/*.ips; do + echo -n "`basename $i .ips` " + cat $i | grep "^[^ ]* [0-9]*/$month .*" | sort -u | wc | awk '{ print $1; }' + # TODO consider producing an AWStats configuration file for each virtual host name and splitting all new logs into virtual host name-specific log files + done | tee $STATS/unique-ips-days-useragents-per-event-`echo $month | tr / -` + echo "Wrote per-month results to $STATS/unique-ips-days-useragents-per-event-$month" +done +echo "Done." diff --git a/configuration/unique_ips_per_referrer_generate_results_only b/configuration/unique_ips_per_referrer_generate_results_only new file mode 100755 index 00000000000..23e316484ce --- /dev/null +++ b/configuration/unique_ips_per_referrer_generate_results_only @@ -0,0 +1,26 @@ +#!/bin/bash + +CACHE=/var/log/old/cache/unique-ips-per-referrer +VISITED_FILES=$CACHE/visited +STATS=$CACHE/stats +MONTHS=$STATS/months + +for i in ${STATS}/*.ips; do + echo -n "`basename $i .ips` " + cat $i | sort -u | wc | awk '{ print $1; }' + # TODO consider producing an AWStats configuration file for each virtual host name and splitting all new logs into virtual host name-specific log files +done | tee $STATS/unique-ips-days-useragents-per-event +echo "Wrote total results to $STATS/unique-ips-days-useragents-per-event" + + +# Now filter and group by month +cat ${STATS}/*.ips | awk '{ print $2; }' | sed -e 's/^[0-9]*\///' | sort -u >$MONTHS +for month in `cat $MONTHS`; do + for i in ${STATS}/*.ips; do + echo -n "`basename $i .ips` " + cat $i | grep "^[^ ]* [0-9]*/$month .*" | sort -u | wc | awk '{ print $1; }' + # TODO consider producing an AWStats configuration file for each virtual host name and splitting all new logs into virtual host name-specific log files + done | tee $STATS/unique-ips-days-useragents-per-event-`echo $month | tr / -` + echo "Wrote per-month results to $STATS/unique-ips-days-useragents-per-event-$month" +done +echo "Done." diff --git a/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/TracTrac.java b/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/TracTrac.java index 3ecf17a7c01..1df916f6e48 100644 --- a/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/TracTrac.java +++ b/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/TracTrac.java @@ -94,8 +94,8 @@ public class TracTrac { new MillisecondsTimePoint(record.getTrackingStartTime().asMillis()), new MillisecondsTimePoint(record.getTrackingEndTime().asMillis()), raceLogStore, regattaLogStore, - RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS, /* simulateWithStartTimeNow */ - false, /*useTracTracMarkPassings*/ false, + RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS, /* offsetToStartTimeOfSimulatedRace */ + null, /*useTracTracMarkPassings*/ false, /* tracTracUsername */"", /* tracTracPassword */"", record.getRaceStatus(), record.getRaceVisibility()); } else { diff --git a/java/com.sap.sailing.domain.common/pom.xml b/java/com.sap.sailing.domain.common/pom.xml index 031a6daf0f6..a1ae29ff16f 100755 --- a/java/com.sap.sailing.domain.common/pom.xml +++ b/java/com.sap.sailing.domain.common/pom.xml @@ -1,40 +1,40 @@ - - - 4.0.0 - - root - com.sap.sailing - 1.0.0-SNAPSHOT - - com.sap.sailing.domain.common - eclipse-plugin - - - - - org.eclipse.tycho - tycho-compiler-plugin - ${tycho-version} - - 1.7 - 1.7 - - - - org.eclipse.tycho - tycho-source-plugin - ${tycho-version} - - - plugin-source - generate-sources - - plugin-source - - - - - - - + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sailing.domain.common + eclipse-plugin + + + + + org.eclipse.tycho + tycho-compiler-plugin + ${tycho-version} + + 1.7 + 1.7 + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + + + diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractSpeedImpl.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractSpeedImpl.java index 90b3c7e5116..4b38e380d31 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractSpeedImpl.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractSpeedImpl.java @@ -21,7 +21,7 @@ public abstract class AbstractSpeedImpl implements Speed { @Override public Duration getDuration(Distance distance) { - return new MillisecondsDurationImpl((long) (1000 * distance.getMeters() / getMetersPerSecond())); + return distance == null ? null : new MillisecondsDurationImpl((long) (1000 * distance.getMeters() / getMetersPerSecond())); } @Override 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 8a9fa9017a9..ff197288789 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 @@ -1,430 +1,433 @@ -package com.sap.sailing.domain.common.dto; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -import com.sap.sailing.domain.common.LeaderboardType; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.ScoringSchemeType; -import com.sap.sse.common.Util; - -public abstract class AbstractLeaderboardDTO implements Serializable { - private static final long serialVersionUID = -205106531931903527L; - - public String name; - - private List races; - public Map competitorDisplayNames; - public Map rows; - public boolean hasCarriedPoints; - public int[] discardThresholds; - - /** - * Set to the non-null regatta name if this DTO represents a RegattaLeaderboard. - */ - public String regattaName; - public String displayName; - public UUID defaultCourseAreaId; - public String defaultCourseAreaName; - public ScoringSchemeType scoringScheme; - public LeaderboardType type; - - private Long delayToLiveInMillisForLatestRace; - - public AbstractLeaderboardDTO() { - races = new ArrayList(); - } - - public Set getBoatClasses() { - Set result = new HashSet(); - if(rows != null) { - for (CompetitorDTO competitor : rows.keySet()) { - result.add(competitor.getBoatClass()); - } - } - return result; - } - - public String getDisplayName() { - return displayName; - } - - public String getDisplayName(CompetitorDTO competitor) { - if (competitorDisplayNames == null || competitorDisplayNames.get(competitor) == null) { - return competitor.getName(); - } else { - return competitorDisplayNames.get(competitor); - } - } - - /** - * If the race column whose name is specified in raceColumnName has at least one competitor who has valid - * {@link LeaderboardEntryDTO#legDetails} for that race column, the maximum number of entries of all such competitors in - * the leg details is returned, telling the number of legs that the race column shall display. Otherwise, -1 is returned. - * If you specify an non null preselectedRace the leg count is calculated only for that race for the column - * holding that race. All other columns will be unaffected by preselectedRace and will return their regular - * leg count. Note that this may lead to a situation where some competitors may have sailed more legs than others because - * their fleet association may have varied across races.

- * - * See also bug 2604 and bug 2035. - */ - public int getLegCount(String raceColumnName, RaceIdentifier preselectedRace) { - int result = -1; - final boolean preselectedRaceIsInRaceColumn = isRaceInColumn(preselectedRace, raceColumnName); - for (LeaderboardRowDTO row : rows.values()) { - LeaderboardEntryDTO leaderboardEntryDTO = row.fieldsByRaceColumnName.get(raceColumnName); - if (leaderboardEntryDTO != null && leaderboardEntryDTO.legDetails != null - // when no race is pre-selected, always use the actual leg count; - // otherwise, if the pre-selected race is in the column identified by - // raceColumnName, use the entry only if it's in the pre-selected race; - // otherwise (a race is pre-selected but a different column's leg count - // is requested), use the regular leg count as is. - && (preselectedRace == null || !preselectedRaceIsInRaceColumn || preselectedRace.equals(leaderboardEntryDTO.race))) { - result = Math.max(result, leaderboardEntryDTO.legDetails.size()); - } - } - return result; - } - - private boolean isRaceInColumn(RaceIdentifier preselectedRace, String raceColumnName) { - RaceColumnDTO raceColumn = getRaceColumnByName(raceColumnName); - return raceColumn.containsRace(preselectedRace); - } - - /** - * Tells if the competitor scored (and therefore presumably participated) in a medal race represented - * in this leaderboard. - */ - public boolean scoredInMedalRace(CompetitorDTO competitor) { - LeaderboardRowDTO row = rows.get(competitor); - for (RaceColumnDTO race : races) { - if (race.isMedalRace() && row.fieldsByRaceColumnName.get(race.getRaceColumnName()).totalPoints > 0) { - return true; - } - } - return false; - } - - public Double getNetPoints(CompetitorDTO competitor, String nameOfLastRaceSoFar) { - Double netPoints = null; - LeaderboardRowDTO row = rows.get(competitor); - if (row != null) { - LeaderboardEntryDTO field = row.fieldsByRaceColumnName.get(nameOfLastRaceSoFar); - if (field != null && field.netPoints != null) { - netPoints = field.netPoints; - } - } - return netPoints; - } - - public boolean raceIsTracked(String raceColumnName) { - for (RaceColumnDTO race : races) { - if (race.getRaceColumnName().equals(raceColumnName)) { - for (FleetDTO fleet : race.getFleets()) { - if (race.isTrackedRace(fleet)) { - return true; - } - } - } - } - return false; - - } - - public boolean raceIsMedalRace(String raceColumnName) { - return getRaceColumnByName(raceColumnName).isMedalRace(); - } - - /** - * If the {@link RaceColumnDTO} by the name raceColumnName doesn't exist yet within this leaderboard - * DTO, it is created, setting is {@link RaceColumnDTO#isValidInTotalScore()} to true. This method - * ensures that a fleet named fleetName is present. If it's not present yet, it's added to the race - * column's fleet name list. The trackedRaceIdentifier and race are associated with the - * column for the fleet identified by fleetName. - * - * @param explicitFactor - * factor by which to multiply the race column's points for the overall score; if null, the - * default will be determined by whether or not the column is marked as medal race - * @param effectiveFactor - * is what you get when you call {@link RaceColumn#getFactor()} on the race column that the resulting - * {@link RaceColumnDTO} represents - * @param regattaName - * must not be null if seriesName is not null; specified - * separately from trackedRaceIdentifier because a column may belong to regatta/series - * despite not having a tracked race associated - * @param seriesName - * if null, this method will produce a {@link RaceColumnDTO}, otherwise a - * {@link RaceColumnInSeriesDTO} - * @param fleetDTO - * must not be null - */ - public RaceColumnDTO addRace(String raceColumnName, Double explicitFactor, double effectiveFactor, - String regattaName, String seriesName, FleetDTO fleetDTO, boolean medalRace, RegattaAndRaceIdentifier trackedRaceIdentifier, RaceDTO race) { - assert fleetDTO != null; - RaceColumnDTO raceColumnDTO = getRaceColumnByName(raceColumnName); - if (raceColumnDTO == null) { - raceColumnDTO = RaceColumnDTOFactory.INSTANCE.createRaceColumnDTO(raceColumnName, medalRace, - explicitFactor, regattaName, seriesName); - races.add(raceColumnDTO); - } - raceColumnDTO.setEffectiveFactor(effectiveFactor); - boolean contains = false; - for (FleetDTO fleet : raceColumnDTO.getFleets()) { - if (fleet.getName().equals(fleetDTO.getName())) { - contains = true; - break; - } - } - if (!contains) { - raceColumnDTO.addFleet(fleetDTO); - } - raceColumnDTO.setRaceIdentifier(fleetDTO, trackedRaceIdentifier); - raceColumnDTO.setRace(fleetDTO, race); - return raceColumnDTO; - } - - public RaceColumnDTO createEmptyRaceColumn(String raceColumnName, boolean medalRace, String regattaName, String seriesName) { - final RaceColumnDTO raceColumn = RaceColumnDTOFactory.INSTANCE.createRaceColumnDTO(raceColumnName, - medalRace, /* explicit factor */ null, regattaName, seriesName); - races.add(raceColumn); - return raceColumn; - } - - public RaceColumnDTO getRaceColumnByName(String raceColumnName) { - for (RaceColumnDTO race : races) { - if (race.getRaceColumnName().equals(raceColumnName)) { - return race; - } - } - return null; - } - - public int getRaceColumnsCount() { - return races.size(); - } - - public int getRacesCount() { - int result = 0; - for (RaceColumnDTO race : getRaceList()) { - result += race.getFleets().size(); - } - return result; - } - - public int getTrackedRacesCount() { - int result = 0; - for (RaceColumnDTO raceColumn : getRaceList()) { - for (FleetDTO fleet : raceColumn.getFleets()) { - RaceDTO race = raceColumn.getRace(fleet); - if(race != null && race.trackedRace != null && race.trackedRace.hasGPSData && race.trackedRace.hasWindData) { - result++; - } - } - } - return result; - } - - public List getRaceList() { - return races; - } - - public void setRaceList(List raceList) { - this.races = raceList; - } - - public boolean raceListContains(String raceColumnName) { - return getRaceColumnByName(raceColumnName) != null; - } - - public void moveRaceUp(String raceColumnName) { - RaceColumnDTO race = getRaceColumnByName(raceColumnName); - int index = races.indexOf(race); - index--; - if (index >= 0) { - races.remove(index + 1); - races.add(index, race); - } - } - - public void moveRaceDown(String raceColumnName) { - RaceColumnDTO race = getRaceColumnByName(raceColumnName); - int index = races.indexOf(race); - if (index != -1) { - index++; - if (index < races.size()) { - races.remove(index - 1); - races.add(index, race); - } - } - } - - public void setIsMedalRace(String raceColumnName, boolean medalRace) { - getRaceColumnByName(raceColumnName).setMedalRace(medalRace); - } - - /** - * @return The earliest start date of the races, or null if no start dates of the races are available. - */ - public Date getStartDate() { - Date leaderboardStart = null; - for (RaceColumnDTO race : getRaceList()) { - for (FleetDTO fleet: race.getFleets()) { - Date raceStart = race.getStartDate(fleet); - if (raceStart != null) { - if (leaderboardStart == null) { - leaderboardStart = new Date(); - } else { - leaderboardStart = leaderboardStart.before(raceStart) ? leaderboardStart : raceStart; - } - } - } - } - return leaderboardStart; - } - - /** - * Takes the {@link PlacemarkOrderDTO} of all races in this leaderboard, if the PlacemarkOrderDTO for the race is - * available, and fills all {@link PlacemarkDTO} in a new PlacemarkOrderDTO.
- * The order of the races in this leaderboard determine the order of the PlacemarkDTOs in the PlacemarkOrderDTO. - * - * @return The places of this leaderboard in form of a {@link PlacemarkOrderDTO}, or null if the - * {@link PlacemarkOrderDTO places} of no race are available - */ - public PlacemarkOrderDTO getPlaces() { - PlacemarkOrderDTO leaderboardPlaces = null; - for (RaceColumnDTO race : getRaceList()) { - PlacemarkOrderDTO racePlaces = race.getPlaces(); - if (racePlaces != null) { - if (leaderboardPlaces == null) { - leaderboardPlaces = new PlacemarkOrderDTO(); - } - leaderboardPlaces.getPlacemarks().addAll(racePlaces.getPlacemarks()); - } - } - return leaderboardPlaces; - } - - /** - * @return true if the leaderboard contains a race which is live - */ - public boolean hasLiveRace(long serverTimePointAsMillis) { - for (RaceColumnDTO race : getRaceList()) { - for (FleetDTO fleet : race.getFleets()) { - if (race.isLive(fleet, serverTimePointAsMillis)) { - return true; - } - } - } - return false; - } - - public List> getLiveRaces(long serverTimePointAsMillis) { - List> result = new ArrayList>(); - for (RaceColumnDTO race : getRaceList()) { - for (FleetDTO fleet : race.getFleets()) { - if (race.isLive(fleet, serverTimePointAsMillis)) { - result.add(new Util.Pair(race, fleet)); - } - } - } - return result; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((competitorDisplayNames == null) ? 0 : competitorDisplayNames.hashCode()); - result = prime * result + Arrays.hashCode(discardThresholds); - result = prime * result + (hasCarriedPoints ? 1231 : 1237); - result = prime * result + ((name == null) ? 0 : name.hashCode()); - result = prime * result + ((scoringScheme == null) ? 0 : scoringScheme.hashCode()); - if (races == null) { - result = prime * result; - } else { - List raceNames = new ArrayList(); - for (RaceColumnDTO race : races) { - raceNames.add(race.getName()); - } - result = prime * result + raceNames.hashCode(); - } - result = prime * result + ((rows == null) ? 0 : rows.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; - AbstractLeaderboardDTO other = (AbstractLeaderboardDTO) obj; - if (competitorDisplayNames == null) { - if (other.competitorDisplayNames != null) - return false; - } else if (!competitorDisplayNames.equals(other.competitorDisplayNames)) - return false; - if (!Arrays.equals(discardThresholds, other.discardThresholds)) - return false; - if (hasCarriedPoints != other.hasCarriedPoints) - return false; - if (type != other.type) - return false; - if (scoringScheme != other.scoringScheme) - return false; - if (name == null) { - if (other.name != null) - return false; - } else if (!name.equals(other.name)) - return false; - if (races == null) { - if (other.races != null) - return false; - } else { - // compare race column names only, not the contents - if (races.size() != (other.races==null?0:other.races.size())) { - return false; - } - List raceColumnNames = new ArrayList(races.size()); - List otherRaceColumnNames = new ArrayList(races.size()); - for (RaceColumnDTO race : races) { - raceColumnNames.add(race.getName()); - } - if (other.races != null) { - for (RaceColumnDTO otherRace : other.races) { - otherRaceColumnNames.add(otherRace.getName()); - } - } - if (!raceColumnNames.equals(otherRaceColumnNames)) - return false; - if (rows == null) { - if (other.rows != null) - return false; - } else if (!rows.equals(other.rows)) - return false; - } - return true; - } - - public boolean isDisplayNameSet(CompetitorDTO competitor) { - return competitorDisplayNames.get(competitor) != null; - } - - public Long getDelayToLiveInMillisForLatestRace() { - return delayToLiveInMillisForLatestRace; - } - - public void setDelayToLiveInMillisForLatestRace(Long delayToLiveInMillisForLatestRace) { - this.delayToLiveInMillisForLatestRace = delayToLiveInMillisForLatestRace; - } -} +package com.sap.sailing.domain.common.dto; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sse.common.Util; + +public abstract class AbstractLeaderboardDTO implements Serializable { + private static final long serialVersionUID = -205106531931903527L; + + public String name; + + private List races; + public Map competitorDisplayNames; + public Map rows; + public boolean hasCarriedPoints; + public int[] discardThresholds; + + /** + * Set to the non-null regatta name if this DTO represents a RegattaLeaderboard. + */ + public String regattaName; + public String displayName; + public UUID defaultCourseAreaId; + public String defaultCourseAreaName; + public ScoringSchemeType scoringScheme; + public LeaderboardType type; + + private Long delayToLiveInMillisForLatestRace; + + public AbstractLeaderboardDTO() { + races = new ArrayList(); + } + + public Set getBoatClasses() { + Set result = new HashSet(); + if(rows != null) { + for (CompetitorDTO competitor : rows.keySet()) { + result.add(competitor.getBoatClass()); + } + } + return result; + } + + public String getDisplayName() { + return displayName; + } + + public String getDisplayName(CompetitorDTO competitor) { + if (competitorDisplayNames == null || competitorDisplayNames.get(competitor) == null) { + return competitor.getName(); + } else { + return competitorDisplayNames.get(competitor); + } + } + + /** + * If the race column whose name is specified in raceColumnName has at least one competitor who has valid + * {@link LeaderboardEntryDTO#legDetails} for that race column, the maximum number of entries of all such competitors in + * the leg details is returned, telling the number of legs that the race column shall display. Otherwise, -1 is returned. + * If you specify an non null preselectedRace the leg count is calculated only for that race for the column + * holding that race. All other columns will be unaffected by preselectedRace and will return their regular + * leg count. Note that this may lead to a situation where some competitors may have sailed more legs than others because + * their fleet association may have varied across races.

+ * + * See also bug 2604 and bug 2035. + */ + public int getLegCount(String raceColumnName, RaceIdentifier preselectedRace) { + int result = -1; + final boolean preselectedRaceIsInRaceColumn = isRaceInColumn(preselectedRace, raceColumnName); + for (LeaderboardRowDTO row : rows.values()) { + LeaderboardEntryDTO leaderboardEntryDTO = row.fieldsByRaceColumnName.get(raceColumnName); + if (leaderboardEntryDTO != null && leaderboardEntryDTO.legDetails != null + // when no race is pre-selected, always use the actual leg count; + // otherwise, if the pre-selected race is in the column identified by + // raceColumnName, use the entry only if it's in the pre-selected race; + // otherwise (a race is pre-selected but a different column's leg count + // is requested), use the regular leg count as is. + && (preselectedRace == null || !preselectedRaceIsInRaceColumn || preselectedRace.equals(leaderboardEntryDTO.race))) { + result = Math.max(result, leaderboardEntryDTO.legDetails.size()); + } + } + return result; + } + + private boolean isRaceInColumn(RaceIdentifier preselectedRace, String raceColumnName) { + RaceColumnDTO raceColumn = getRaceColumnByName(raceColumnName); + return raceColumn.containsRace(preselectedRace); + } + + /** + * Tells if the competitor scored (and therefore presumably participated) in a medal race represented + * in this leaderboard. + */ + public boolean scoredInMedalRace(CompetitorDTO competitor) { + LeaderboardRowDTO row = rows.get(competitor); + for (RaceColumnDTO race : races) { + if (race.isMedalRace() && row.fieldsByRaceColumnName.get(race.getRaceColumnName()).totalPoints > 0) { + return true; + } + } + return false; + } + + public Double getNetPoints(CompetitorDTO competitor, String nameOfLastRaceSoFar) { + Double netPoints = null; + LeaderboardRowDTO row = rows.get(competitor); + if (row != null) { + LeaderboardEntryDTO field = row.fieldsByRaceColumnName.get(nameOfLastRaceSoFar); + if (field != null && field.netPoints != null) { + netPoints = field.netPoints; + } + } + return netPoints; + } + + public boolean raceIsTracked(String raceColumnName) { + for (RaceColumnDTO race : races) { + if (race.getRaceColumnName().equals(raceColumnName)) { + for (FleetDTO fleet : race.getFleets()) { + if (race.isTrackedRace(fleet)) { + return true; + } + } + } + } + return false; + + } + + public boolean raceIsMedalRace(String raceColumnName) { + return getRaceColumnByName(raceColumnName).isMedalRace(); + } + + /** + * If the {@link RaceColumnDTO} by the name raceColumnName doesn't exist yet within this leaderboard + * DTO, it is created, setting is {@link RaceColumnDTO#isValidInTotalScore()} to true. This method + * ensures that a fleet named fleetName is present. If it's not present yet, it's added to the race + * column's fleet name list. The trackedRaceIdentifier and race are associated with the + * column for the fleet identified by fleetName. + * + * @param explicitFactor + * factor by which to multiply the race column's points for the overall score; if null, the + * default will be determined by whether or not the column is marked as medal race + * @param effectiveFactor + * is what you get when you call {@link RaceColumn#getFactor()} on the race column that the resulting + * {@link RaceColumnDTO} represents + * @param regattaName + * must not be null if seriesName is not null; specified + * separately from trackedRaceIdentifier because a column may belong to regatta/series + * despite not having a tracked race associated + * @param seriesName + * if null, this method will produce a {@link RaceColumnDTO}, otherwise a + * {@link RaceColumnInSeriesDTO} + * @param fleetDTO + * must not be null + */ + public RaceColumnDTO addRace(String raceColumnName, Double explicitFactor, double effectiveFactor, + String regattaName, String seriesName, FleetDTO fleetDTO, boolean medalRace, + RegattaAndRaceIdentifier trackedRaceIdentifier, RaceDTO race, boolean isMetaLeaderboardColumn) { + assert fleetDTO != null; + RaceColumnDTO raceColumnDTO = getRaceColumnByName(raceColumnName); + if (raceColumnDTO == null) { + raceColumnDTO = RaceColumnDTOFactory.INSTANCE.createRaceColumnDTO(raceColumnName, medalRace, + explicitFactor, regattaName, seriesName, isMetaLeaderboardColumn); + races.add(raceColumnDTO); + } + raceColumnDTO.setEffectiveFactor(effectiveFactor); + boolean contains = false; + for (FleetDTO fleet : raceColumnDTO.getFleets()) { + if (fleet.getName().equals(fleetDTO.getName())) { + contains = true; + break; + } + } + if (!contains) { + raceColumnDTO.addFleet(fleetDTO); + } + raceColumnDTO.setRaceIdentifier(fleetDTO, trackedRaceIdentifier); + raceColumnDTO.setRace(fleetDTO, race); + return raceColumnDTO; + } + + public RaceColumnDTO createEmptyRaceColumn(String raceColumnName, boolean medalRace, String regattaName, + String seriesName, boolean isMetaLeaderboardColumn) { + final RaceColumnDTO raceColumn = RaceColumnDTOFactory.INSTANCE.createRaceColumnDTO(raceColumnName, + medalRace, /* explicit factor */ null, regattaName, seriesName, isMetaLeaderboardColumn); + races.add(raceColumn); + return raceColumn; + } + + public RaceColumnDTO getRaceColumnByName(String raceColumnName) { + for (RaceColumnDTO race : races) { + if (race.getRaceColumnName().equals(raceColumnName)) { + return race; + } + } + return null; + } + + public int getRaceColumnsCount() { + return races.size(); + } + + public int getRacesCount() { + int result = 0; + for (RaceColumnDTO race : getRaceList()) { + result += race.getFleets().size(); + } + return result; + } + + public int getTrackedRacesCount() { + int result = 0; + for (RaceColumnDTO raceColumn : getRaceList()) { + for (FleetDTO fleet : raceColumn.getFleets()) { + RaceDTO race = raceColumn.getRace(fleet); + if(race != null && race.trackedRace != null && race.trackedRace.hasGPSData && race.trackedRace.hasWindData) { + result++; + } + } + } + return result; + } + + public List getRaceList() { + return races; + } + + public void setRaceList(List raceList) { + this.races = raceList; + } + + public boolean raceListContains(String raceColumnName) { + return getRaceColumnByName(raceColumnName) != null; + } + + public void moveRaceUp(String raceColumnName) { + RaceColumnDTO race = getRaceColumnByName(raceColumnName); + int index = races.indexOf(race); + index--; + if (index >= 0) { + races.remove(index + 1); + races.add(index, race); + } + } + + public void moveRaceDown(String raceColumnName) { + RaceColumnDTO race = getRaceColumnByName(raceColumnName); + int index = races.indexOf(race); + if (index != -1) { + index++; + if (index < races.size()) { + races.remove(index - 1); + races.add(index, race); + } + } + } + + public void setIsMedalRace(String raceColumnName, boolean medalRace) { + getRaceColumnByName(raceColumnName).setMedalRace(medalRace); + } + + /** + * @return The earliest start date of the races, or null if no start dates of the races are available. + */ + public Date getStartDate() { + Date leaderboardStart = null; + for (RaceColumnDTO race : getRaceList()) { + for (FleetDTO fleet: race.getFleets()) { + Date raceStart = race.getStartDate(fleet); + if (raceStart != null) { + if (leaderboardStart == null) { + leaderboardStart = new Date(); + } else { + leaderboardStart = leaderboardStart.before(raceStart) ? leaderboardStart : raceStart; + } + } + } + } + return leaderboardStart; + } + + /** + * Takes the {@link PlacemarkOrderDTO} of all races in this leaderboard, if the PlacemarkOrderDTO for the race is + * available, and fills all {@link PlacemarkDTO} in a new PlacemarkOrderDTO.
+ * The order of the races in this leaderboard determine the order of the PlacemarkDTOs in the PlacemarkOrderDTO. + * + * @return The places of this leaderboard in form of a {@link PlacemarkOrderDTO}, or null if the + * {@link PlacemarkOrderDTO places} of no race are available + */ + public PlacemarkOrderDTO getPlaces() { + PlacemarkOrderDTO leaderboardPlaces = null; + for (RaceColumnDTO race : getRaceList()) { + PlacemarkOrderDTO racePlaces = race.getPlaces(); + if (racePlaces != null) { + if (leaderboardPlaces == null) { + leaderboardPlaces = new PlacemarkOrderDTO(); + } + leaderboardPlaces.getPlacemarks().addAll(racePlaces.getPlacemarks()); + } + } + return leaderboardPlaces; + } + + /** + * @return true if the leaderboard contains a race which is live + */ + public boolean hasLiveRace(long serverTimePointAsMillis) { + for (RaceColumnDTO race : getRaceList()) { + for (FleetDTO fleet : race.getFleets()) { + if (race.isLive(fleet, serverTimePointAsMillis)) { + return true; + } + } + } + return false; + } + + public List> getLiveRaces(long serverTimePointAsMillis) { + List> result = new ArrayList>(); + for (RaceColumnDTO race : getRaceList()) { + for (FleetDTO fleet : race.getFleets()) { + if (race.isLive(fleet, serverTimePointAsMillis)) { + result.add(new Util.Pair(race, fleet)); + } + } + } + return result; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((competitorDisplayNames == null) ? 0 : competitorDisplayNames.hashCode()); + result = prime * result + Arrays.hashCode(discardThresholds); + result = prime * result + (hasCarriedPoints ? 1231 : 1237); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((scoringScheme == null) ? 0 : scoringScheme.hashCode()); + if (races == null) { + result = prime * result; + } else { + List raceNames = new ArrayList(); + for (RaceColumnDTO race : races) { + raceNames.add(race.getName()); + } + result = prime * result + raceNames.hashCode(); + } + result = prime * result + ((rows == null) ? 0 : rows.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; + AbstractLeaderboardDTO other = (AbstractLeaderboardDTO) obj; + if (competitorDisplayNames == null) { + if (other.competitorDisplayNames != null) + return false; + } else if (!competitorDisplayNames.equals(other.competitorDisplayNames)) + return false; + if (!Arrays.equals(discardThresholds, other.discardThresholds)) + return false; + if (hasCarriedPoints != other.hasCarriedPoints) + return false; + if (type != other.type) + return false; + if (scoringScheme != other.scoringScheme) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (races == null) { + if (other.races != null) + return false; + } else { + // compare race column names only, not the contents + if (races.size() != (other.races==null?0:other.races.size())) { + return false; + } + List raceColumnNames = new ArrayList(races.size()); + List otherRaceColumnNames = new ArrayList(races.size()); + for (RaceColumnDTO race : races) { + raceColumnNames.add(race.getName()); + } + if (other.races != null) { + for (RaceColumnDTO otherRace : other.races) { + otherRaceColumnNames.add(otherRace.getName()); + } + } + if (!raceColumnNames.equals(otherRaceColumnNames)) + return false; + if (rows == null) { + if (other.rows != null) + return false; + } else if (!rows.equals(other.rows)) + return false; + } + return true; + } + + public boolean isDisplayNameSet(CompetitorDTO competitor) { + return competitorDisplayNames.get(competitor) != null; + } + + public Long getDelayToLiveInMillisForLatestRace() { + return delayToLiveInMillisForLatestRace; + } + + public void setDelayToLiveInMillisForLatestRace(Long delayToLiveInMillisForLatestRace) { + this.delayToLiveInMillisForLatestRace = delayToLiveInMillisForLatestRace; + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java new file mode 100644 index 00000000000..8938136783c --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java @@ -0,0 +1,112 @@ +package com.sap.sailing.domain.common.dto; + +import java.util.Date; + +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.TimingConstants; + +/** + * Master data about a single race that is to be transferred to the client. Holds only timing and a bit + * of information about the tracked race, if any. See also {@link RaceDTO} for a more comprehensive set + * of data about a race to be serialized to a client.

+ * + * @author Axel Uhl (d043530) + * + */ +public class BasicRaceDTO extends NamedDTO { + private static final long serialVersionUID = -7884808503795229609L; + + public Date startOfRace; + public Date endOfRace; + public TrackedRaceDTO trackedRace; + + public BasicRaceDTO() {} // for GWT serialization only + + public BasicRaceDTO(RegattaAndRaceIdentifier raceIdentifier, TrackedRaceDTO trackedRace) { + super(raceIdentifier.getRaceName()); + this.trackedRace = trackedRace; + } + + /** + * @see {@link TrackedRace#isLive} for further explanation. + * @param serverTimePointAsMillis + * the time point (in server clock time) at which to determine whether the race is/was live + * @return true if serverTimePointAsMillis is between (inclusively) the start and end time + * point of the "live" interval as defined above. + */ + public boolean isLive(long serverTimePointAsMillis) { + final Date startOfLivePeriod; + final Date endOfLivePeriod; + if (trackedRace == null || !trackedRace.hasGPSData || !trackedRace.hasWindData) { + startOfLivePeriod = null; + endOfLivePeriod = null; + } else { + if (startOfRace == null) { + startOfLivePeriod = trackedRace.startOfTracking; + } else { + startOfLivePeriod = new Date(startOfRace.getTime() - TimingConstants.PRE_START_PHASE_DURATION_IN_MILLIS); + } + if (endOfRace == null) { + if (trackedRace.timePointOfNewestEvent != null) { + endOfLivePeriod = new Date(trackedRace.timePointOfNewestEvent.getTime() + + TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS); + } else { + endOfLivePeriod = null; + } + } else { + endOfLivePeriod = new Date(endOfRace.getTime() + TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS); + } + } + + // if an empty timepoint is given then take the start of the race + if (serverTimePointAsMillis == 0) { + serverTimePointAsMillis = startOfLivePeriod.getTime()+1; + } + + // whenLastTrackedRaceWasLive is null if there is no tracked race for fleet, or the tracked race hasn't started yet at the server time + // when this DTO was assembled, or there were no GPS or wind data + final boolean result = + startOfLivePeriod != null && + endOfLivePeriod != null && + startOfLivePeriod.getTime() <= serverTimePointAsMillis && + serverTimePointAsMillis <= endOfLivePeriod.getTime(); + return result; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((endOfRace == null) ? 0 : endOfRace.hashCode()); + result = prime * result + ((startOfRace == null) ? 0 : startOfRace.hashCode()); + result = prime * result + ((trackedRace == null) ? 0 : trackedRace.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + BasicRaceDTO other = (BasicRaceDTO) obj; + if (endOfRace == null) { + if (other.endOfRace != null) + return false; + } else if (!endOfRace.equals(other.endOfRace)) + return false; + if (startOfRace == null) { + if (other.startOfRace != null) + return false; + } else if (!startOfRace.equals(other.startOfRace)) + return false; + if (trackedRace == null) { + if (other.trackedRace != null) + return false; + } else if (!trackedRace.equals(other.trackedRace)) + return false; + return true; + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardDTO.java index 64012fec26c..dd9b5e94981 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardDTO.java @@ -62,7 +62,6 @@ public class LeaderboardDTO extends AbstractLeaderboardDTO implements Serializab /** * @param uuidGenerator used to provide the {@link #id ID} for this object (see also {@link #getId()}) and for any clones produced * from it by the {@link #clone()} operation. - * @param hasOverallDetails TODO */ public LeaderboardDTO(Date timePointOfLastCorrectionsValidity, String comment, ScoringSchemeType scoringScheme, boolean higherScoreIsBetter, UUIDGenerator uuidGenerator, boolean hasOverallDetails) { initCollections(); @@ -79,7 +78,7 @@ public class LeaderboardDTO extends AbstractLeaderboardDTO implements Serializab this.suppressedCompetitors = new ArrayList(); } - public LeaderboardDTO(String id) { + protected LeaderboardDTO(String id) { initCollections(); this.id = id; } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java new file mode 100644 index 00000000000..475718b447b --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java @@ -0,0 +1,49 @@ +package com.sap.sailing.domain.common.dto; + +import java.util.ArrayList; + +public class MetaLeaderboardRaceColumnDTO extends RaceColumnDTO { + + private static final long serialVersionUID = 6152752963316150432L; + + private ArrayList raceList = new ArrayList(); + + public boolean isLive(FleetDTO fleet, long serverTimePointAsMillis) { + for (BasicRaceDTO race : raceList) { + if (race.isLive(serverTimePointAsMillis)) { + return true; + } + } + return false; + } + + public void addRace(BasicRaceDTO race) { + raceList.add(race); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((raceList == null) ? 0 : raceList.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + MetaLeaderboardRaceColumnDTO other = (MetaLeaderboardRaceColumnDTO) obj; + if (raceList == null) { + if (other.raceList != null) + return false; + } else if (!raceList.equals(other.raceList)) + return false; + return true; + } + +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceColumnDTOFactory.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceColumnDTOFactory.java index 7d8a0efbb67..46083549e93 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceColumnDTOFactory.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceColumnDTOFactory.java @@ -7,15 +7,17 @@ public class RaceColumnDTOFactory { * @param regattaName * must not be null if seriesName is not null * @param seriesName - * when null, a regular {@link RaceColumnDTO} will be produced and initialized from the - * properties passed; otherwise, a {@link RaceColumnInSeriesDTO} will be created. + * when null, a regular {@link RaceColumnDTO} or {@link MetaLeaderboardRaceColumnDTO} will + * be produced (depending on isMetaLeaderboardColumn) and initialized from the properties + * passed; otherwise, a {@link RaceColumnInSeriesDTO} will be created. */ - public RaceColumnDTO createRaceColumnDTO(String columnName, boolean isMedal, Double explicitFactor, String regattaName, String seriesName) { + public RaceColumnDTO createRaceColumnDTO(String columnName, boolean isMedal, Double explicitFactor, + String regattaName, String seriesName, boolean isMetaLeaderboardColumn) { final RaceColumnDTO raceColumnDTO; if (seriesName != null) { raceColumnDTO = new RaceColumnInSeriesDTO(seriesName, regattaName); } else { - raceColumnDTO = new RaceColumnDTO(); + raceColumnDTO = isMetaLeaderboardColumn ? new MetaLeaderboardRaceColumnDTO() : new RaceColumnDTO(); } fillRaceColumnDTO(raceColumnDTO, columnName, isMedal, explicitFactor); return raceColumnDTO; diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceDTO.java index eba9caf39b7..6a8e90c617f 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceDTO.java @@ -1,10 +1,7 @@ package com.sap.sailing.domain.common.dto; -import java.util.Date; - import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.RegattaNameAndRaceName; -import com.sap.sailing.domain.common.TimingConstants; /** * Master data about a single race that is to be transferred to the client.

@@ -12,7 +9,7 @@ import com.sap.sailing.domain.common.TimingConstants; * @author Axel Uhl (d043530) * */ -public class RaceDTO extends NamedDTO { +public class RaceDTO extends BasicRaceDTO { private static final long serialVersionUID = 2613189982608149975L; public enum RaceLiveState { NOT_TRACKED, TRACKED, TRACKED_AND_LIVE, TRACKED_BUT_NOT_SCHEDULED }; @@ -24,13 +21,10 @@ public class RaceDTO extends NamedDTO { */ public boolean isTracked; - public Date startOfRace; - public Date endOfRace; public RaceStatusDTO status; public PlacemarkOrderDTO places; - public TrackedRaceDTO trackedRace; public TrackedRaceStatisticsDTO trackedRaceStatistics; private String regattaName; @@ -43,21 +37,20 @@ public class RaceDTO extends NamedDTO { } public RaceDTO(RegattaAndRaceIdentifier raceIdentifier, TrackedRaceDTO trackedRace, boolean isCurrentlyTracked) { - super(raceIdentifier.getRaceName()); + super(raceIdentifier, trackedRace); this.regattaName = raceIdentifier.getRegattaName(); - this.trackedRace = trackedRace; this.isTracked = isCurrentlyTracked; } public RaceLiveState getLiveState(long serverTimePointAsMillis) { - RaceLiveState result = RaceLiveState.NOT_TRACKED; - if(trackedRace != null && trackedRace.hasGPSData && trackedRace.hasWindData) { + RaceLiveState result = RaceLiveState.NOT_TRACKED; + if (trackedRace != null && trackedRace.hasGPSData && trackedRace.hasWindData) { result = RaceLiveState.TRACKED; - if(isLive(serverTimePointAsMillis)) { + if (isLive(serverTimePointAsMillis)) { result = RaceLiveState.TRACKED_AND_LIVE; - if(startOfRace == null) { + if (startOfRace == null) { result = RaceLiveState.TRACKED_BUT_NOT_SCHEDULED; - } + } } } return result; @@ -71,64 +64,15 @@ public class RaceDTO extends NamedDTO { return regattaName; } - /** - * @see {@link TrackedRace#isLive} for further explanation. - * @param serverTimePointAsMillis - * the time point (in server clock time) at which to determine whether the race is/was live - * @return true if serverTimePointAsMillis is between (inclusively) the start and end time - * point of the "live" interval as defined above. - */ - public boolean isLive(long serverTimePointAsMillis) { - final Date startOfLivePeriod; - final Date endOfLivePeriod; - if (trackedRace == null || !trackedRace.hasGPSData || !trackedRace.hasWindData) { - startOfLivePeriod = null; - endOfLivePeriod = null; - } else { - if (startOfRace == null) { - startOfLivePeriod = trackedRace.startOfTracking; - } else { - startOfLivePeriod = new Date(startOfRace.getTime() - TimingConstants.PRE_START_PHASE_DURATION_IN_MILLIS); - } - if (endOfRace == null) { - if (trackedRace.timePointOfNewestEvent != null) { - endOfLivePeriod = new Date(trackedRace.timePointOfNewestEvent.getTime() - + TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS); - } else { - endOfLivePeriod = null; - } - } else { - endOfLivePeriod = new Date(endOfRace.getTime() + TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS); - } - } - - // if an empty timepoint is given then take the start of the race - if (serverTimePointAsMillis == 0) { - serverTimePointAsMillis = startOfLivePeriod.getTime()+1; - } - - // whenLastTrackedRaceWasLive is null if there is no tracked race for fleet, or the tracked race hasn't started yet at the server time - // when this DTO was assembled, or there were no GPS or wind data - final boolean result = - startOfLivePeriod != null && - endOfLivePeriod != null && - startOfLivePeriod.getTime() <= serverTimePointAsMillis && - serverTimePointAsMillis <= endOfLivePeriod.getTime(); - return result; - } - @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); result = prime * result + ((boatClass == null) ? 0 : boatClass.hashCode()); - result = prime * result + ((endOfRace == null) ? 0 : endOfRace.hashCode()); result = prime * result + (isTracked ? 1231 : 1237); result = prime * result + ((places == null) ? 0 : places.hashCode()); result = prime * result + ((regattaName == null) ? 0 : regattaName.hashCode()); - result = prime * result + ((startOfRace == null) ? 0 : startOfRace.hashCode()); result = prime * result + ((status == null) ? 0 : status.hashCode()); - result = prime * result + ((trackedRace == null) ? 0 : trackedRace.hashCode()); return result; } @@ -146,11 +90,6 @@ public class RaceDTO extends NamedDTO { return false; } else if (!boatClass.equals(other.boatClass)) return false; - if (endOfRace == null) { - if (other.endOfRace != null) - return false; - } else if (!endOfRace.equals(other.endOfRace)) - return false; if (isTracked != other.isTracked) return false; if (places == null) { @@ -163,21 +102,11 @@ public class RaceDTO extends NamedDTO { return false; } else if (!regattaName.equals(other.regattaName)) return false; - if (startOfRace == null) { - if (other.startOfRace != null) - return false; - } else if (!startOfRace.equals(other.startOfRace)) - return false; if (status == null) { if (other.status != null) return false; } else if (!status.equals(other.status)) return false; - if (trackedRace == null) { - if (other.trackedRace != null) - return false; - } else if (!trackedRace.equals(other.trackedRace)) - return false; return true; } } diff --git a/java/com.sap.sailing.domain.shared.android/pom.xml b/java/com.sap.sailing.domain.shared.android/pom.xml index b6b4b65e730..af7117f708a 100644 --- a/java/com.sap.sailing.domain.shared.android/pom.xml +++ b/java/com.sap.sailing.domain.shared.android/pom.xml @@ -1,39 +1,39 @@ - - - 4.0.0 - - root - com.sap.sailing - 1.0.0-SNAPSHOT - - com.sap.sailing.domain.shared.android - eclipse-plugin - - - - org.eclipse.tycho - tycho-compiler-plugin - ${tycho-version} - - 1.7 - 1.7 - - - - org.eclipse.tycho - tycho-source-plugin - ${tycho-version} - - - plugin-source - generate-sources - - plugin-source - - - - - - - + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sailing.domain.shared.android + eclipse-plugin + + + + org.eclipse.tycho + tycho-compiler-plugin + ${tycho-version} + + 1.7 + 1.7 + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + + + diff --git a/java/com.sap.sailing.domain.test/resources/IncrementalLeaderboardDTO.ser b/java/com.sap.sailing.domain.test/resources/IncrementalLeaderboardDTO.ser index 1fd2fb452b3..8b0225928aa 100755 Binary files a/java/com.sap.sailing.domain.test/resources/IncrementalLeaderboardDTO.ser and b/java/com.sap.sailing.domain.test/resources/IncrementalLeaderboardDTO.ser differ 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 7e7a7e29630..4b91a74d1aa 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 @@ -60,7 +60,7 @@ 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, /* ignoreTracTracMarkPassings*/ + /* startOfTracking */null, /* endOfTracking */null, /* delayToLiveInMillis */0l, /* offsetToStartTimeOfSimulatedRace */ null, /* ignoreTracTracMarkPassings*/ false, EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, TracTracConnectionConstants.ONLINE_VISIBILITY, @@ -71,7 +71,7 @@ public class MultipleClassesInRegattaTest { 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, /* ignoreTracTracMarkPassings*/ + /* startOfTracking */null, /* endOfTracking */null, /* delayToLiveInMillis */0l, /* offsetToStartTimeOfSimulatedRace */ null, /* ignoreTracTracMarkPassings*/ false, EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, TracTracConnectionConstants.ONLINE_VISIBILITY, @@ -82,7 +82,7 @@ public class MultipleClassesInRegattaTest { 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, /* ignoreTracTracMarkPassings*/ + /* startOfTracking */null, /* endOfTracking */null, /* delayToLiveInMillis */0l, /* offsetToStartTimeOfSimulatedRace */ null, /* ignoreTracTracMarkPassings*/ false, EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, TracTracConnectionConstants.ONLINE_VISIBILITY, @@ -93,7 +93,7 @@ public class MultipleClassesInRegattaTest { 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, /* ignoreTracTracMarkPassings*/ + /* startOfTracking */null, /* endOfTracking */null, /* delayToLiveInMillis */0l, /* offsetToStartTimeOfSimulatedRace */ null, /* ignoreTracTracMarkPassings*/ false, EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, TracTracConnectionConstants.ONLINE_VISIBILITY, 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 eefd9495d9c..3b907f3cf30 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 @@ -67,7 +67,7 @@ public class UnicodeCharactersInCompetitorNamesTest { + TracTracConnectionConstants.PORT_STORED), new URI( "http://tracms.traclive.dk/update_course"), /* startOfTracking */null, /* endOfTracking */null, /* delayToLiveInMillis */0l, - /* simulateWithStartTimeNow */ false, /* ignoreTracTracMarkPassings*/ + /* offsetToStartTimeOfSimulatedRace */ null, /* ignoreTracTracMarkPassings*/ false, EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, "tracTest", "tracTest", "", "", new DummyTrackedRegattaRegistry(), mock(RaceLogResolver.class)); 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 2ee226e8b63..eca3d7cebdc 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 @@ -44,6 +44,7 @@ import com.sap.sailing.domain.tracking.impl.EmptyWindStore; import com.sap.sailing.domain.tractracadapter.impl.DomainFactoryImpl; import com.sap.sailing.domain.tractracadapter.impl.RaceCourseReceiver; import com.sap.sailing.domain.tractracadapter.impl.Simulator; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.tractrac.model.lib.api.data.IPosition; @@ -95,7 +96,6 @@ public interface DomainFactory { * Fetch a race definition previously created by a call to {@link #getOrCreateRaceDefinitionAndTrackedRace}. If no such * race definition was created so far, the call blocks until such a definition is provided by a call to * {@link #getOrCreateRaceDefinitionAndTrackedRace}. - * @param raceId TODO */ RaceDefinition getAndWaitForRaceDefinition(UUID raceId); @@ -105,7 +105,6 @@ public interface DomainFactory { * A new {@link com.sap.sailing.domain.base.Regatta} is created if no event by * an equal name with a boat class with an equal name as the event's * boat class exists yet. - * @param trackedRegattaRegistry TODO */ com.sap.sailing.domain.base.Regatta getOrCreateDefaultRegatta(RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, IRace race, TrackedRegattaRegistry trackedRegattaRegistry); @@ -134,27 +133,30 @@ public interface DomainFactory { * if null, all stored data until the "end of time" will be loaded that the event has * to provide, particularly for the mark positions which are stored per event, not per race; otherwise, * particularly the mark position loading will be constrained to this end time. + * @param offsetToStartTimeOfSimulatedRace + * if non-null, the {@link Simulator} will be used with this duration as start offset * @param windStore * Provides the capability to obtain the {@link WindTrack}s for the different wind sources. A trivial * implementation is {@link EmptyWindStore} which simply provides new, empty tracks. This is always * available but loses track of the wind, e.g., during server restarts. - * @param raceLogResolver TODO */ TracTracRaceTracker createRaceTracker(URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, - boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws MalformedURLException, FileNotFoundException, URISyntaxException, CreateModelException, SubscriberInitializationException; /** - * Same as {@link #createRaceTracker(URL, URI, URI, URI, TimePoint, TimePoint, WindStore, TrackedRegattaRegistry)}, only that - * a predefined {@link Regatta} is used to hold the resulting races. - * @param raceLogResolver TODO + * Same as {@link #createRaceTracker(URL, URI, URI, URI, TimePoint, TimePoint, WindStore, TrackedRegattaRegistry)}, + * only that a predefined {@link Regatta} is used to hold the resulting races. + * + * @param offsetToStartTimeOfSimulatedRace + * if non-null, the {@link Simulator} will be used with this duration as start offset */ RaceTracker createRaceTracker(Regatta regatta, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, - boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws MalformedURLException, FileNotFoundException, URISyntaxException, CreateModelException, @@ -262,9 +264,13 @@ public interface DomainFactory { Util.Pair, BoatClass> getCompetitorsAndDominantBoatClass(IRace race); + /** + * @param offsetToStartTimeOfSimulatedRace + * if non-null, the {@link Simulator} will be used with this duration as start offset + */ RaceTrackingConnectivityParameters createTrackingConnectivityParameters(URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, - boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility); /** * Removes all knowledge about tractracRace which includes removing it from the race cache, from the diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/TracTracAdapter.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/TracTracAdapter.java index 72fcb93662e..ca159e2ca8e 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/TracTracAdapter.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/TracTracAdapter.java @@ -17,6 +17,8 @@ import com.sap.sailing.domain.regattalog.RegattaLogStore; import com.sap.sailing.domain.tracking.RaceHandle; import com.sap.sailing.domain.tracking.TrackerManager; import com.sap.sailing.domain.tracking.WindStore; +import com.sap.sailing.domain.tractracadapter.impl.Simulator; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; @@ -65,12 +67,13 @@ public interface TracTracAdapter { * 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 raceStatus + * @param offsetToStartTimeOfSimulatedRace + * if non-null, the {@link Simulator} will be used with this duration as start offset */ RaceHandle addTracTracRace(TrackerManager trackerManager, RegattaIdentifier regattaToAddTo, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint trackingStartTime, TimePoint trackingEndTime, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, - long timeoutForReceivingRaceDefinitionInMilliseconds, boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, + long timeoutForReceivingRaceDefinitionInMilliseconds, Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility) throws MalformedURLException, FileNotFoundException, URISyntaxException, Exception; 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 ec6c5959d06..7df01e515ff 100755 --- 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 @@ -74,6 +74,7 @@ import com.sap.sailing.domain.tractracadapter.ReceiverType; import com.sap.sailing.domain.tractracadapter.TracTracConfiguration; import com.sap.sailing.domain.tractracadapter.TracTracControlPoint; import com.sap.sailing.domain.tractracadapter.TracTracRaceTracker; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; @@ -652,27 +653,27 @@ 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, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws MalformedURLException, FileNotFoundException, URISyntaxException, CreateModelException, SubscriberInitializationException { return new TracTracRaceTrackerImpl(this, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, - endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, + endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver); } @Override public RaceTracker createRaceTracker(Regatta regatta, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, - boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws MalformedURLException, FileNotFoundException, URISyntaxException, CreateModelException, SubscriberInitializationException { return new TracTracRaceTrackerImpl(regatta, this, paramURL, liveURI, storedURI, courseDesignUpdateURI, - startOfTracking, endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, useInternalMarkPassingAlgorithm, raceLogStore, + startOfTracking, endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver); } @@ -690,11 +691,11 @@ public class DomainFactoryImpl implements DomainFactory { @Override public RaceTrackingConnectivityParameters createTrackingConnectivityParameters(URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, - long delayToLiveInMillis, boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, + long delayToLiveInMillis, Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility) { return new RaceTrackingConnectivityParametersImpl(paramURL, liveURI, storedURI, courseDesignUpdateURI, - startOfTracking, endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, useInternalMarkPassingAlgorithm, raceLogStore, + startOfTracking, endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, this, tracTracUsername, tracTracPassword, raceStatus, raceVisibility); } 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 a438f5b0450..17c00105762 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 @@ -16,6 +16,7 @@ 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.domain.tractracadapter.DomainFactory; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.tractrac.model.lib.api.event.CreateModelException; import com.tractrac.subscription.lib.api.SubscriberInitializationException; @@ -31,7 +32,7 @@ public class RaceTrackingConnectivityParametersImpl implements RaceTrackingConne private final RegattaLogStore regattaLogStore; private final DomainFactory domainFactory; private final long delayToLiveInMillis; - private final boolean simulateWithStartTimeNow; + private final Duration offsetToStartTimeOfSimulatedRace; private final String tracTracUsername; private final String tracTracPassword; private final String raceStatus; @@ -40,7 +41,7 @@ public class RaceTrackingConnectivityParametersImpl implements RaceTrackingConne public RaceTrackingConnectivityParametersImpl(URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, - boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, DomainFactory domainFactory, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility) { super(); @@ -52,7 +53,7 @@ public class RaceTrackingConnectivityParametersImpl implements RaceTrackingConne this.endOfTracking = endOfTracking; this.delayToLiveInMillis = delayToLiveInMillis; this.domainFactory = domainFactory; - this.simulateWithStartTimeNow = simulateWithStartTimeNow; + this.offsetToStartTimeOfSimulatedRace = offsetToStartTimeOfSimulatedRace; this.raceLogStore = raceLogStore; this.regattaLogStore = regattaLogStore; this.tracTracUsername = tracTracUsername; @@ -67,7 +68,7 @@ public class RaceTrackingConnectivityParametersImpl implements RaceTrackingConne GPSFixStore gpsFixStore, RaceLogResolver raceLogResolver) throws MalformedURLException, FileNotFoundException, URISyntaxException, CreateModelException, SubscriberInitializationException { RaceTracker tracker = domainFactory.createRaceTracker(paramURL, liveURI, storedURI, courseDesignUpdateURI, - startOfTracking, endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, useInternalMarkPassingAlgorithm, raceLogStore, + startOfTracking, endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver); return tracker; @@ -77,7 +78,7 @@ public class RaceTrackingConnectivityParametersImpl implements RaceTrackingConne public RaceTracker createRaceTracker(Regatta regatta, TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore, GPSFixStore gpsFixStore, RaceLogResolver raceLogResolver) throws Exception { RaceTracker tracker = domainFactory.createRaceTracker(regatta, paramURL, liveURI, storedURI, - courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, useInternalMarkPassingAlgorithm, + courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver); return tracker; diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RawPositionReceiver.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RawPositionReceiver.java index 2e5cfea7755..7e5049f9cf4 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RawPositionReceiver.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RawPositionReceiver.java @@ -4,7 +4,6 @@ import java.util.logging.Logger; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.common.tracking.GPSFixMoving; -import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; import com.sap.sailing.domain.tractracadapter.DomainFactory; @@ -57,13 +56,13 @@ public class RawPositionReceiver extends AbstractReceiverWithQueuetimed's time point according to this simulator's delay and waits roughly until this time @@ -157,12 +171,12 @@ public class Simulator { * Like {@link #delay}, only that it doesn't wait until timePoint is reached in wall time. */ public TimePoint advance(TimePoint timePoint) { - return new MillisecondsTimePoint(timePoint.asMillis()+getAdvanceInMillis()); + return timePoint.plus(getAdvance()); } /** * If {@link #advanceInMillis} is already set to a non-negative value, it is left alone, and - * {@link #delay(TimePoint)} is called. Otherwise, time is taken to be the original start time of the + * {@link #advance(TimePoint)} is called. Otherwise, time is taken to be the original start time of the * race which is then used to compute {@link #advanceInMillis} such that * time.asMillis() + advanceInMillis == System.currentTimeMillis(). */ @@ -183,13 +197,13 @@ public class Simulator { if (isAdvanceInMillisSet()) { return advance(time); } else { - setAdvanceInMillis(System.currentTimeMillis() - time.asMillis()); + setAdvanceInMillis(new MillisecondsDurationImpl(MillisecondsTimePoint.now().minus(time.asMillis()).plus(getOffsetToStart()).asMillis())); return advance(time); } } private boolean isAdvanceInMillisSet() { - return advanceInMillis != -1; + return !advanceInMillis.equals(Duration.NULL.minus(1)); } /** @@ -238,21 +252,35 @@ public class Simulator { // deliver an empty list now trackedRace.updateMarkPassings(competitor, markPassings); } - + } + + public void scheduleCompetitorPosition(final Competitor competitor, GPSFixMoving competitorFix) { + final RecordGPSFix recorder = (c, f)->trackedRace.recordFix(c, f); + scheduleFixRecording(competitor, competitorFix, recorder); + } + + @FunctionalInterface + private static interface RecordGPSFix { + void recordFix(T objectForFix, GPSFixMoving fix); } public void scheduleMarkPosition(final Mark mark, GPSFixMoving markFix) { - final TimePoint transformedTimepoint = advance(markFix.getTimePoint()); - final GPSFixMoving transformedMarkFix = new GPSFixMovingImpl(markFix.getPosition(), transformedTimepoint, markFix.getSpeed()); + final RecordGPSFix recorder = (m, f)->trackedRace.recordFix(mark, f); + scheduleFixRecording(mark, markFix, recorder); + } + + private void scheduleFixRecording(final T object, GPSFixMoving fix, final RecordGPSFix recorder) { + final TimePoint transformedTimepoint = advance(fix.getTimePoint()); + final GPSFixMoving transformedMarkFix = new GPSFixMovingImpl(fix.getPosition(), transformedTimepoint, fix.getSpeed()); long waitTime = getWaitTimeInMillisUntil(transformedMarkFix.getTimePoint()); if (waitTime <= 0) { - trackedRace.recordFix(mark, transformedMarkFix); + recorder.recordFix(object, transformedMarkFix); } else { timer.schedule(new TimerTask() { @Override public void run() { try { - trackedRace.recordFix(mark, transformedMarkFix); + recorder.recordFix(object, transformedMarkFix); } catch (Exception e) { logger.throwing(Simulator.class.getName(), "scheduleMarkPosition", e); } diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracAdapterImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracAdapterImpl.java index 8264853e299..0d7fd26a6ec 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracAdapterImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracAdapterImpl.java @@ -21,6 +21,7 @@ import com.sap.sailing.domain.tractracadapter.JSONService; import com.sap.sailing.domain.tractracadapter.RaceRecord; import com.sap.sailing.domain.tractracadapter.TracTracAdapter; import com.sap.sailing.domain.tractracadapter.TracTracConfiguration; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; @@ -56,7 +57,7 @@ public class TracTracAdapterImpl implements TracTracAdapter { getTracTracDomainFactory().createTrackingConnectivityParameters(paramURL, liveURI, storedURI, courseDesignUpdateURI, /* startOfTracking */null, - /* endOfTracking */null, delayToLiveInMillis, /* simulateWithStartTimeNow */false, /* ignoreTracTracMarkPassings */ false, + /* endOfTracking */null, delayToLiveInMillis, /* offsetToStartTimeOfSimulatedRace */null, /* ignoreTracTracMarkPassings */ false, raceLogStore, regattaLogStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility), timeoutInMilliseconds); } @@ -65,13 +66,13 @@ public class TracTracAdapterImpl implements TracTracAdapter { public RaceHandle addTracTracRace(TrackerManager trackerManager, RegattaIdentifier regattaToAddTo, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, long timeoutInMilliseconds, - boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, String tracTracUsername, String tracTracPassword, String raceStatus, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility) throws Exception { return trackerManager.addRace( regattaToAddTo, getTracTracDomainFactory().createTrackingConnectivityParameters(paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, - simulateWithStartTimeNow, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, tracTracUsername, tracTracPassword, + offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility), timeoutInMilliseconds); } 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 4dac78b6701..50c376d419c 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 @@ -49,6 +49,7 @@ import com.sap.sailing.domain.tractracadapter.DomainFactory; import com.sap.sailing.domain.tractracadapter.Receiver; import com.sap.sailing.domain.tractracadapter.TracTracConnectionConstants; import com.sap.sailing.domain.tractracadapter.TracTracRaceTracker; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.tractrac.model.lib.api.ModelLocator; @@ -253,26 +254,26 @@ 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, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws URISyntaxException, MalformedURLException, FileNotFoundException, CreateModelException, SubscriberInitializationException { this(ModelLocator.getEventFactory().createRace(new URI(paramURL.toString())), domainFactory, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, - simulateWithStartTimeNow, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, + offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver); } private TracTracRaceTrackerImpl(IRace tractracRace, DomainFactory domainFactory, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, - long delayToLiveInMillis, boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, + long delayToLiveInMillis, Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws URISyntaxException, MalformedURLException, FileNotFoundException, SubscriberInitializationException { this(tractracRace, null, domainFactory, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, - endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, + endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver); } @@ -284,14 +285,14 @@ 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, boolean ignoreTracTracMarkPassings, RaceLogStore raceLogStore, + long delayToLiveInMillis, Duration offsetToStartTimeOfSimulatedRace, boolean ignoreTracTracMarkPassings, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws URISyntaxException, MalformedURLException, FileNotFoundException, CreateModelException, SubscriberInitializationException { this(ModelLocator.getEventFactory().createRace(new URI(paramURL.toString())), regatta, domainFactory, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, - simulateWithStartTimeNow, ignoreTracTracMarkPassings, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, + offsetToStartTimeOfSimulatedRace, ignoreTracTracMarkPassings, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver); } @@ -300,15 +301,15 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements * @param regatta * if null, then domainFactory.getOrCreateRegatta(tractracEvent) will be used * to obtain a default regatta - * @param simulateWithStartTimeNow - * if true, the connector will adjust the time stamps of all events received such that the + * @param offsetToStartTimeOfSimulatedRace + * if not null, 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 + * events received such that they seem to be sent in "real-time" + offsetToStartTimeOfSimulatedRace So, more or less the time points * attached to the events sent to the receivers will again approximate the wall time. */ private TracTracRaceTrackerImpl(IRace tractracRace, final Regatta regatta, DomainFactory domainFactory, URL paramURL, URI liveURI, URI storedURI, URI tracTracUpdateURI, TimePoint startOfTracking, - TimePoint endOfTracking, long delayToLiveInMillis, boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, + TimePoint endOfTracking, long delayToLiveInMillis, Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws URISyntaxException, MalformedURLException, @@ -323,8 +324,8 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements this.gpsFixStore = gpsFixStore; this.domainFactory = domainFactory; this.lastProgressPerID = new HashMap<>(); - if (simulateWithStartTimeNow) { - simulator = new Simulator(windStore); + if (offsetToStartTimeOfSimulatedRace != null) { + simulator = new Simulator(windStore, offsetToStartTimeOfSimulatedRace); // don't write the transformed wind fixes into the DB again... see also bug 1974 this.windStore = EmptyWindStore.INSTANCE; } else { diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Course.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Course.java index fb1dadb2f61..b86693c71cf 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Course.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Course.java @@ -43,4 +43,6 @@ public interface Course extends CourseBase { void update(Iterable> newControlPoints, DomainFactory baseDomainFactory) throws PatchFailedException; int getNumberOfWaypoints(); + + Leg getLeg(int zeroBasedIndexOfWaypoint); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/CourseImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/CourseImpl.java index b68ea6ac7ff..e3e39e010c1 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/CourseImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/CourseImpl.java @@ -246,6 +246,11 @@ public class CourseImpl extends NamedImpl implements Course { return waypoints.size(); } + @Override + public Leg getLeg(int zeroBasedIndexOfWaypoint) { + return legs.get(zeroBasedIndexOfWaypoint); + } + @Override public String toString() { lockForRead(); 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 de92f254836..454843ba3e2 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 @@ -1,1907 +1,1937 @@ -package com.sap.sailing.domain.leaderboard.impl; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.NavigableSet; -import java.util.NoSuchElementException; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.RunnableFuture; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -import com.sap.sailing.domain.abstractlog.race.InvalidatesLeaderboardCache; -import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; -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.Fleet; -import com.sap.sailing.domain.base.Leg; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceColumnInSeries; -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.LegType; -import com.sap.sailing.domain.common.ManeuverType; -import com.sap.sailing.domain.common.MaxPointsReason; -import com.sap.sailing.domain.common.NoWindError; -import com.sap.sailing.domain.common.NoWindException; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.RegattaNameAndRaceName; -import com.sap.sailing.domain.common.Speed; -import com.sap.sailing.domain.common.SpeedWithBearing; -import com.sap.sailing.domain.common.dto.CompetitorDTO; -import com.sap.sailing.domain.common.dto.FleetDTO; -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.RaceDTO; -import com.sap.sailing.domain.common.tracking.GPSFixMoving; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.leaderboard.NumberOfCompetitorsInLeaderboardFetcher; -import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; -import com.sap.sailing.domain.leaderboard.ScoreCorrection.Result; -import com.sap.sailing.domain.leaderboard.SettableScoreCorrection; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCache; -import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCalculationReuseCache; -import com.sap.sailing.domain.leaderboard.caching.LiveLeaderboardUpdater; -import com.sap.sailing.domain.racelog.RaceLogIdentifier; -import com.sap.sailing.domain.ranking.RankingMetric.CompetitorRankingInfo; -import com.sap.sailing.domain.ranking.RankingMetric.RankingInfo; -import com.sap.sailing.domain.tracking.GPSFixTrack; -import com.sap.sailing.domain.tracking.Maneuver; -import com.sap.sailing.domain.tracking.MarkPassing; -import com.sap.sailing.domain.tracking.MarkPassingManeuver; -import com.sap.sailing.domain.tracking.RaceChangeListener; -import com.sap.sailing.domain.tracking.TrackedLeg; -import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor; -import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; -import com.sap.sailing.domain.tracking.WindLegTypeAndLegBearingCache; -import com.sap.sailing.domain.tracking.WindPositionMode; -import com.sap.sailing.domain.tracking.impl.AbstractRaceChangeListener; -import com.sap.sailing.util.impl.RaceColumnListeners; -import com.sap.sse.common.Duration; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.Util; -import com.sap.sse.common.impl.MillisecondsTimePoint; -import com.sap.sse.concurrent.LockUtil; -import com.sap.sse.concurrent.NamedReentrantReadWriteLock; -import com.sap.sse.util.impl.ThreadFactoryWithPriority; - -/** - * Base implementation for various types of leaderboards. The {@link RaceColumnListener} implementation forwards events - * received to all {@link RaceColumnListener} subscribed with this leaderboard. To which objects this leaderboard subscribes - * as {@link RaceColumnListener} is left to the concrete subclasses to implement, but the race columns seem like useful - * candidates. - * - * @author Axel Uhl (d043530) - * - */ -public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, RaceColumnListener { - private static final long serialVersionUID = 330156778603279333L; - - private static final Logger logger = Logger.getLogger(AbstractSimpleLeaderboardImpl.class.getName()); - - static final Double DOUBLE_0 = new Double(0); - - private final SettableScoreCorrection scoreCorrection; - - private ThresholdBasedResultDiscardingRule crossLeaderboardResultDiscardingRule; - - /** - * The optional display name mappings for competitors. This allows a user to override the tracking-provided - * competitor names for display in a leaderboard. - */ - private final Map displayNames; - - /** the display name of the leaderboard */ - private String displayName; - - /** - * Backs the {@link #getCarriedPoints(Competitor)} API with data. Can be used to prime this leaderboard - * with aggregated results of races not tracked / displayed by this leaderboard in detail. The points - * provided by this map are considered by {@link #getTotalPoints(Competitor, TimePoint)}. - */ - private final Map carriedPoints; - - private final RaceColumnListeners raceColumnListeners; - - /** - * A set that manages the difference between {@link #getCompetitors()} and {@link #getAllCompetitors()}. Access - * is controlled by the {@link #suppressedCompetitorsLock} lock. - */ - private final Set suppressedCompetitors; - private final NamedReentrantReadWriteLock suppressedCompetitorsLock; - - private transient Map, RunnableFuture> raceDetailsAtEndOfTrackingCache; - - /** - * This executor needs to be a different one than {@link #executor} because the tasks run by {@link #executor} - * can depend on the results of the tasks run by {@link #raceDetailsExecutor}, and an {@link Executor} doesn't - * move a task that is blocked by waiting for another {@link FutureTask} to the side but blocks permanently, - * ending in a deadlock (one that cannot easily be detected by the Eclipse debugger either). - */ - private transient Executor raceDetailsExecutor; - - /** - * Used to remove all these listeners from their tracked races when this servlet is {@link #destroy() destroyed}. - */ - private transient Set cacheInvalidationListeners; - - private transient ThreadPoolExecutor executor; - - private transient LiveLeaderboardUpdater liveLeaderboardUpdater; - - private transient LeaderboardDTOCache leaderboardDTOCache; - - /** - * A leaderboard entry representing a snapshot of a cell at a given time point for a single race/competitor. - * - * @author Axel Uhl (d043530) - * - */ - class EntryImpl implements Entry { - private final Callable trackedRankProvider; - private final Double netPoints; - private final Callable netPointsUncorrectedProvider; - private final boolean isNetPointsCorrected; - private final Double totalPoints; - private final MaxPointsReason maxPointsReason; - private final boolean discarded; - private final Fleet fleet; - - private EntryImpl(Callable trackedRankProvider, Double netPoints, - Callable netPointsUncorrectedProvider, boolean isNetPointsCorrected, - Double totalPoints, MaxPointsReason maxPointsReason, boolean discarded, Fleet fleet) { - super(); - this.trackedRankProvider = trackedRankProvider; - this.netPoints = netPoints; - this.netPointsUncorrectedProvider = netPointsUncorrectedProvider; - this.isNetPointsCorrected = isNetPointsCorrected; - this.totalPoints = totalPoints; - this.maxPointsReason = maxPointsReason; - this.discarded = discarded; - this.fleet = fleet; - } - @Override - public int getTrackedRank() { - try { - return trackedRankProvider.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - @Override - public Double getNetPoints() { - return netPoints; - } - @Override - public boolean isNetPointsCorrected() { - return isNetPointsCorrected; - } - @Override - public Double getTotalPoints() { - return totalPoints; - } - @Override - public MaxPointsReason getMaxPointsReason() { - return maxPointsReason; - } - @Override - public boolean isDiscarded() { - return discarded; - } - @Override - public Fleet getFleet() { - return fleet; - } - @Override - public Double getNetPointsUncorrected() { - try { - return netPointsUncorrectedProvider.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - - /** - * Computing the competitors can be a bit expensive, particularly if the fleet is large and there may be suppressed - * competitors, and the leaderboard may be a meta-leaderboard that refers to other leaderboards which each have - * several tracked races attached from where the competitors need to be retrieved. Ideally, the competitors list - * would be cached, but that is again difficult because we would have to monitor all changes in all dependent - * leaderboards and columns and tracked races properly. - *

- * - * As it turns out, one of the most frequent uses of the {@link AbstractSimpleLeaderboardImpl#getCompetitors} - * competitors list is to determine their number which in turn is only required for high-point scoring systems and - * for computing the default score for penalties. Again, the most frequently used low-point family of scoring schemes - * does not require this number. Yet, the scoring scheme requires an argument for polymorphic use by those that - * need it. Instead of computing it for each call, this interface lets us defer the actual calculation until the - * point when it's really needed. Once asked, this object will cache the result. Therefore, a new one should be - * constructed each time the number shall be computed. - * - * @author Axel Uhl (D043530) - * - */ - public class NumberOfCompetitorsFetcherImpl implements NumberOfCompetitorsInLeaderboardFetcher { - private int numberOfCompetitors = -1; - - @Override - public int getNumberOfCompetitorsInLeaderboard() { - if (numberOfCompetitors == -1) { - numberOfCompetitors = Util.size(getCompetitors()); - } - return numberOfCompetitors; - } - } - - /** - * Handles the invalidation of the {@link SailingServiceImpl#raceDetailsAtEndOfTrackingCache} entries if the tracked - * race changes in any way. In particular, for {@link #statusChanged}, when the status changes away from LOADING, - * calculations may start or resume, making it necessary to clear the cache. - * - * @author Axel Uhl (D043530) - * - */ - private class CacheInvalidationListener extends AbstractRaceChangeListener { - private final TrackedRace trackedRace; - private final Competitor competitor; - - public CacheInvalidationListener(TrackedRace trackedRace, Competitor competitor) { - this.trackedRace = trackedRace; - this.competitor = competitor; - } - - public TrackedRace getTrackedRace() { - return trackedRace; - } - - public void removeFromTrackedRace() { - trackedRace.removeListener(this); - } - - private void invalidateCacheAndRemoveThisListenerFromTrackedRace() { - synchronized (raceDetailsAtEndOfTrackingCache) { - raceDetailsAtEndOfTrackingCache.remove(new com.sap.sse.common.Util.Pair(trackedRace, competitor)); - removeFromTrackedRace(); - } - } - - @Override - protected void defaultAction() { - invalidateCacheAndRemoveThisListenerFromTrackedRace(); - } - } - - private static class UUIDGenerator implements LeaderboardDTO.UUIDGenerator { - @Override - public String generateRandomUUID() { - return UUID.randomUUID().toString(); - } - } - - public AbstractSimpleLeaderboardImpl(ThresholdBasedResultDiscardingRule resultDiscardingRule) { - this.carriedPoints = new HashMap(); - this.scoreCorrection = createScoreCorrection(); - this.displayNames = new HashMap(); - this.crossLeaderboardResultDiscardingRule = resultDiscardingRule; - this.suppressedCompetitors = new HashSet(); - this.suppressedCompetitorsLock = new NamedReentrantReadWriteLock("suppressedCompetitorsLock", /* fair */ false); - this.raceColumnListeners = new RaceColumnListeners(); - this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); - initTransientFields(); - } - - /** - * Produces the score correction object to use in this leaderboard. Used by the constructor. Subclasses may override - * this method to create a more specific type of score correction. This implementation produces an object of type - * {@link ScoreCorrectionImpl}. - */ - protected SettableScoreCorrection createScoreCorrection() { - return new ScoreCorrectionImpl(this); - } - - private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { - ois.defaultReadObject(); - initTransientFields(); - } - - private void initTransientFields() { - this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); - this.raceDetailsExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); - this.cacheInvalidationListeners = new HashSet(); - // When many updates are triggered in a short period of time by a single thread, ensure that the single thread - // providing the updates is not outperformed by all the re-calculations happening here. Leave at least one - // core to other things, but by using at least three threads ensure that no simplistic deadlocks may occur. - final int THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors(), 3); - executor = new ThreadPoolExecutor(/* corePoolSize */ THREAD_POOL_SIZE, - /* maximumPoolSize */ THREAD_POOL_SIZE, - /* keepAliveTime */ 60, TimeUnit.SECONDS, - /* workQueue */ new LinkedBlockingQueue(), new ThreadFactoryWithPriority(Thread.NORM_PRIORITY-1, /* daemon */ true)); - } - - @Override - public void destroy() { - for (CacheInvalidationListener cacheInvalidationListener : cacheInvalidationListeners) { - cacheInvalidationListener.removeFromTrackedRace(); - } - } - - @Override - public SettableScoreCorrection getScoreCorrection() { - return scoreCorrection; - } - - @Override - public String getDisplayName(Competitor competitor) { - return displayNames.get(competitor); - } - - @Override - public String getDisplayName() { - return displayName; - } - - @Override - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - - @Override - public ResultDiscardingRule getResultDiscardingRule() { - return crossLeaderboardResultDiscardingRule; - } - - @Override - public void setCarriedPoints(Competitor competitor, double carriedPoints) { - Double oldCarriedPoints = this.carriedPoints.put(competitor, carriedPoints); - if (!Util.equalsWithNull(oldCarriedPoints, carriedPoints)) { - getScoreCorrection().notifyListenersAboutCarriedPointsChange(competitor, oldCarriedPoints, carriedPoints); - } - } - - @Override - public double getCarriedPoints(Competitor competitor) { - Double result = carriedPoints.get(competitor); - return result == null ? 0 : result; - } - - @Override - public Map getCompetitorsForWhichThereAreCarriedPoints() { - return Collections.unmodifiableMap(carriedPoints); - } - - @Override - public void unsetCarriedPoints(Competitor competitor) { - Double oldCarriedPoints = carriedPoints.remove(competitor); - if (oldCarriedPoints != null) { - getScoreCorrection().notifyListenersAboutCarriedPointsChange(competitor, oldCarriedPoints, null); - } - } - - @Override - public boolean hasCarriedPoints() { - return !carriedPoints.isEmpty(); - } - - @Override - public boolean hasCarriedPoints(Competitor competitor) { - return carriedPoints.containsKey(competitor); - } - - @Override - public void setDisplayName(Competitor competitor, String displayName) { - String oldDisplayName = displayNames.get(competitor); - displayNames.put(competitor, displayName); - if (!Util.equalsWithNull(oldDisplayName, displayName)) { - getRaceColumnListeners().notifyListenersAboutCompetitorDisplayNameChanged(competitor, oldDisplayName, displayName); - } - } - - @Override - public void setCrossLeaderboardResultDiscardingRule(ThresholdBasedResultDiscardingRule discardingRule) { - ResultDiscardingRule oldDiscardingRule = getResultDiscardingRule(); - this.crossLeaderboardResultDiscardingRule = discardingRule; - getRaceColumnListeners().notifyListenersAboutResultDiscardingRuleChanged(oldDiscardingRule, discardingRule); - } - - @Override - public Double getNetPoints(final Competitor competitor, final RaceColumn raceColumn, final TimePoint timePoint) { - return getScoreCorrection().getCorrectedScore( - new Callable() { - public Integer call() throws NoWindException { - return getTrackedRank(competitor, raceColumn, timePoint); - } - }, competitor, - raceColumn, timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()).getCorrectedScore(); - } - - @Override - public MaxPointsReason getMaxPointsReason(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { - return getScoreCorrection().getMaxPointsReason(competitor, raceColumn, timePoint); - } - - @Override - public boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { - return isDiscarded(competitor, raceColumn, getRaceColumns(), timePoint); - } - - private boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, - Iterable raceColumnsToConsider, TimePoint timePoint) { - final Set discardedRaceColumns = getResultDiscardingRule() - .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); - return isDiscarded(competitor, raceColumn, timePoint, discardedRaceColumns); - } - - /** - * Same as {@link #isDiscarded(Competitor, RaceColumn, TimePoint)}, only that the set of discarded race columns can - * be specified which is useful when total points are to be computed for more than one column for the same - * competitor because then the calculation of discards (which requires looking at all columns) only needs to be done - * once and not again for each column (which would lead to quadratic effort). - * - * @param discardedRaceColumns - * expected to be the result of what we would get if we called {@link #getResultDiscardingRule()}. - * {@link ResultDiscardingRule#getDiscardedRaceColumns(Competitor, Leaderboard, Iterable, TimePoint) - * getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint)}. - */ - private boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint, - final Set discardedRaceColumns) { - return !raceColumn.isMedalRace() - && getMaxPointsReason(competitor, raceColumn, timePoint).isDiscardable() - && discardedRaceColumns.contains(raceColumn); - } - - @Override - public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) throws NoWindException { - return getTotalPoints(competitor, raceColumn, getRaceColumns(), timePoint); - } - - @Override - public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, - Iterable raceColumnsToConsider, TimePoint timePoint) throws NoWindException { - final Set discardedRaceColumns = getResultDiscardingRule() - .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); - return getTotalPoints(competitor, raceColumn, timePoint, discardedRaceColumns); - } - - /** - * Same as {@link #getTotalPoints(Competitor, RaceColumn, Iterable, TimePoint)}, only that the set of discarded race columns can - * be specified which is useful when total points are to be computed for more than one column for the same - * competitor because then the calculation of discards (which requires looking at all columns) only needs to be done - * once and not again for each column (which would lead to quadratic effort). - * - * @param discardedRaceColumns - * expected to be the result of what we would get if we called {@link #getResultDiscardingRule()}. - * {@link ResultDiscardingRule#getDiscardedRaceColumns(Competitor, Leaderboard, Iterable, TimePoint) - * getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint)}. - */ - @Override - public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint, Set discardedRaceColumns) { - Double result; - if (isDiscarded(competitor, raceColumn, timePoint, discardedRaceColumns)) { - result = 0.0; - } else { - final Double netPoints = getNetPoints(competitor, raceColumn, timePoint); - if (netPoints == null) { - result = null; - } else { - result = raceColumn.getFactor() * netPoints; - } - } - return result; - } - - @Override - public Double getTotalPoints(Competitor competitor, TimePoint timePoint) { - return getTotalPoints(competitor, getRaceColumns(), timePoint); - } - - @Override - public Double getTotalPoints(Competitor competitor, final Iterable raceColumnsToConsider, - TimePoint timePoint) { - // when a column with isStartsWithZeroScore() is found, only reset score if the competitor scored in any race from there on - boolean needToResetScoreUponNextNonEmptyEntry = false; - double result = getCarriedPoints(competitor); - final Set discardedRaceColumns = getResultDiscardingRule() - .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); - for (RaceColumn raceColumn : raceColumnsToConsider) { - if (raceColumn.isStartsWithZeroScore()) { - needToResetScoreUponNextNonEmptyEntry = true; - } - if (getScoringScheme().isValidInTotalScore(this, raceColumn, competitor, timePoint)) { - final Double totalPoints = getTotalPoints(competitor, raceColumn, timePoint, discardedRaceColumns); - if (totalPoints != null) { - if (needToResetScoreUponNextNonEmptyEntry) { - result = 0; - needToResetScoreUponNextNonEmptyEntry = false; - } - result += totalPoints; - } - } - } - return result; - } - - /** - * All competitors with non-null net points are added to the result which is then sorted by net points in ascending - * order. The fleet, if ordered, is the primary ordering criterion, followed by the net points. - */ - @Override - public List getCompetitorsFromBestToWorst(final RaceColumn raceColumn, TimePoint timePoint) throws NoWindException { - final Map> netPointsAndFleet = new HashMap>(); - for (Competitor competitor : getCompetitors()) { - Double netPoints = getNetPoints(competitor, raceColumn, timePoint); - if (netPoints != null) { - netPointsAndFleet.put(competitor, new com.sap.sse.common.Util.Pair(netPoints, raceColumn.getFleetOfCompetitor(competitor))); - } - } - List result = new ArrayList(netPointsAndFleet.keySet()); - Collections.sort(result, new Comparator() { - @Override - public int compare(Competitor o1, Competitor o2) { - int comparisonResult; - if (o1 == o2) { - comparisonResult = 0; - } else { - if (raceColumn.hasSplitFleets() && !raceColumn.hasSplitFleetContiguousScoring()) { - // only check fleets if there are more than one and the column is not to be contiguously scored even in case - // of split fleets - final Fleet o1Fleet = netPointsAndFleet.get(o1).getB(); - final Fleet o2Fleet = netPointsAndFleet.get(o2).getB(); - if (o1Fleet == null) { - if (o2Fleet == null) { - comparisonResult = 0; - } else { - comparisonResult = 1; // o1 ranks "worse" because it doesn't have a fleet set while o2 - // has - } - } else { - if (o2Fleet == null) { - comparisonResult = -1; // o1 ranks "better" because it has a fleet set while o2 hasn't - } else { - comparisonResult = o1Fleet.compareTo(o2Fleet); - } - } - } else { - // either there are no split fleets or the split isn't relevant for scoring as for ordered fleets - // the scoring runs contiguously from top to bottom - comparisonResult = 0; - } - if (comparisonResult == 0) { - comparisonResult = getScoringScheme().getScoreComparator(/* nullScoresAreBetter */ false).compare( - netPointsAndFleet.get(o1).getA(), netPointsAndFleet.get(o2).getA()); - } - } - return comparisonResult; - } - }); - return result; - } - - @Override - public List getCompetitorsFromBestToWorst(TimePoint timePoint) { - return getCompetitorsFromBestToWorst(getRaceColumns(), timePoint); - } - - private List getCompetitorsFromBestToWorst(Iterable raceColumnsToConsider, TimePoint timePoint) { - List result = new ArrayList(); - for (Competitor competitor : getCompetitors()) { - result.add(competitor); - } - Collections.sort(result, getTotalRankComparator(raceColumnsToConsider, timePoint)); - return result; - } - - @Override - public int getTotalRankOfCompetitor(Competitor competitor, TimePoint timePoint) throws NoWindException { - List competitorsFromBestToWorst = getCompetitorsFromBestToWorst(timePoint); - return competitorsFromBestToWorst.indexOf(competitor) + 1; - } - - protected Comparator getTotalRankComparator(Iterable raceColumnsToConsider, TimePoint timePoint) { - return new LeaderboardTotalRankComparator(this, timePoint, getScoringScheme(), /* nullScoresAreBetter */ false, raceColumnsToConsider); - } - - @Override - public RaceColumn getRaceColumnByName(String columnName) { - RaceColumn result = null; - for (RaceColumn r : getRaceColumns()) { - if (r.getName().equals(columnName)) { - result = r; - break; - } - } - return result; - } - - @Override - public Competitor getCompetitorByName(String competitorName) { - for (Competitor competitor : getAllCompetitors()) { - if (competitor.getName().equals(competitorName)) { - return competitor; - } - } - return null; - } - - @Override - public boolean countRaceForComparisonWithDiscardingThresholds(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { - TrackedRace trackedRaceForCompetitorInColumn; - return getScoringScheme().isValidInTotalScore(this, raceColumn, competitor, timePoint) && - (getScoreCorrection().isScoreCorrected(competitor, raceColumn, timePoint) || - ((trackedRaceForCompetitorInColumn=raceColumn.getTrackedRace(competitor)) != null && - trackedRaceForCompetitorInColumn.hasStarted(timePoint) && - trackedRaceForCompetitorInColumn.getRank(competitor, timePoint) != 0)); - } - - @Override - public void addRaceColumnListener(RaceColumnListener listener) { - getRaceColumnListeners().addRaceColumnListener(listener); - } - - @Override - public void removeRaceColumnListener(RaceColumnListener listener) { - getRaceColumnListeners().removeRaceColumnListener(listener); - } - - @Override - public Entry getEntry(final Competitor competitor, final RaceColumn race, final TimePoint timePoint) throws NoWindException { - final Set discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); - return getEntry(competitor, race, timePoint, discardedRaceColumns); - } - - @Override - public Entry getEntry(final Competitor competitor, final RaceColumn race, final TimePoint timePoint, - Set discardedRaceColumns) throws NoWindException { - Callable trackedRankProvider = new Callable() { - public Integer call() throws NoWindException { - return getTrackedRank(competitor, race, timePoint); - } - }; - final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, race, - timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); - boolean discarded = isDiscarded(competitor, race, timePoint, discardedRaceColumns); - final Double correctedScore = correctedResults.getCorrectedScore(); - return new EntryImpl(trackedRankProvider, correctedScore, new Callable() { - @Override - public Double call() { - return correctedResults.getUncorrectedScore(); - } - }, correctedResults.isCorrected(), discarded ? DOUBLE_0 : correctedScore == null ? null - : Double.valueOf(correctedScore * race.getFactor()), correctedResults.getMaxPointsReason(), discarded, - race.getFleetOfCompetitor(competitor)); - } - - @Override - public Map> getRankedCompetitorsFromBestToWorstAfterEachRaceColumn(TimePoint timePoint) throws NoWindException { - Map> result = new LinkedHashMap<>(); - List raceColumnsToConsider = new ArrayList<>(); - for (RaceColumn raceColumn : getRaceColumns()) { - raceColumnsToConsider.add(raceColumn); - result.put(raceColumn, getCompetitorsFromBestToWorst(raceColumnsToConsider, timePoint)); - } - return result; - } - - @Override - public Map> getTotalPointsSumAfterRaceColumn(final TimePoint timePoint) - throws NoWindException { - final Map> result = new LinkedHashMap<>(); - List raceColumnsToConsider = new ArrayList<>(); - Map>> futures = new HashMap<>(); - for (final RaceColumn raceColumn : getRaceColumns()) { - raceColumnsToConsider.add(raceColumn); - final Iterable finalRaceColumnsToConsider = new ArrayList<>(raceColumnsToConsider); - futures.put(raceColumn, executor.submit(new Callable>() { - @Override - public Map call() { - Map totalPointsSumPerCompetitorInColumn = new HashMap<>(); - for (Competitor competitor : getCompetitors()) { - totalPointsSumPerCompetitorInColumn.put(competitor, getTotalPoints(competitor, finalRaceColumnsToConsider, timePoint)); - } - synchronized (result) { - return totalPointsSumPerCompetitorInColumn; - } - } - })); - } - for (RaceColumn raceColumn : getRaceColumns()) { - try { - result.put(raceColumn, futures.get(raceColumn).get()); - } catch (InterruptedException | ExecutionException e) { - if (e.getCause() instanceof NoWindError) { - throw ((NoWindError) e.getCause()).getCause(); - } else { - throw new RuntimeException(e); // no caught exceptions occur in the futures executed - } - } - } - return result; - } - - @Override - public Map, Entry> getContent(final TimePoint timePoint) throws NoWindException { - Map, Entry> result = new HashMap, Entry>(); - Map> discardedRaces = new HashMap>(); - for (final RaceColumn raceColumn : getRaceColumns()) { - for (final Competitor competitor : getCompetitors()) { - Callable trackedRankProvider = new Callable() { - @Override - public Integer call() throws Exception { - return getTrackedRank(competitor, raceColumn, timePoint); - } - }; - final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, raceColumn, - timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); - Set discardedRacesForCompetitor = discardedRaces.get(competitor); - if (discardedRacesForCompetitor == null) { - discardedRacesForCompetitor = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); - discardedRaces.put(competitor, discardedRacesForCompetitor); - } - boolean discarded = discardedRacesForCompetitor.contains(raceColumn); - final Double correctedScore = correctedResults.getCorrectedScore(); - Entry entry = new EntryImpl(trackedRankProvider, correctedScore, - new Callable() { @Override public Double call() { return correctedResults.getUncorrectedScore(); } }, - correctedResults.isCorrected(), - discarded ? DOUBLE_0 : (correctedScore==null?null: - Double.valueOf((correctedScore * raceColumn.getFactor()))), correctedResults.getMaxPointsReason(), - discarded, raceColumn.getFleetOfCompetitor(competitor)); - result.put(new com.sap.sse.common.Util.Pair(competitor, raceColumn), entry); - } - } - return result; - } - - @Override - public void trackedRaceLinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { - getRaceColumnListeners().notifyListenersAboutTrackedRaceLinked(raceColumn, fleet, trackedRace); - } - - @Override - public void trackedRaceUnlinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { - getRaceColumnListeners().notifyListenersAboutTrackedRaceUnlinked(raceColumn, fleet, trackedRace); - // It's generally possible that a leaderboard links to the same tracked race in multiple columns / fleets; - // only if it no longer references the trackedRace currently unlinked from one column/fleet, also unlink - // all cache invalidation listeners for said trackedRace - if (!Util.contains(getTrackedRaces(), trackedRace)) { - synchronized (cacheInvalidationListeners) { - for (Iterator cacheInvalidationListenerIter=cacheInvalidationListeners.iterator(); - cacheInvalidationListenerIter.hasNext(); ) { - CacheInvalidationListener cacheInvalidationListener = cacheInvalidationListenerIter.next(); - if (cacheInvalidationListener.getTrackedRace() == trackedRace) { - cacheInvalidationListener.removeFromTrackedRace(); - cacheInvalidationListenerIter.remove(); - } - } - } - } - } - - @Override - public void isMedalRaceChanged(RaceColumn raceColumn, boolean newIsMedalRace) { - getRaceColumnListeners().notifyListenersAboutIsMedalRaceChanged(raceColumn, newIsMedalRace); - } - - @Override - public void isStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore) { - getRaceColumnListeners().notifyListenersAboutIsStartsWithZeroScoreChanged(raceColumn, newIsStartsWithZeroScore); - } - - @Override - public void isFirstColumnIsNonDiscardableCarryForwardChanged(RaceColumn raceColumn, boolean firstColumnIsNonDiscardableCarryForward) { - getRaceColumnListeners().notifyListenersAboutIsFirstColumnIsNonDiscardableCarryForwardChanged(raceColumn, firstColumnIsNonDiscardableCarryForward); - } - - @Override - public void hasSplitFleetContiguousScoringChanged(RaceColumn raceColumn, boolean hasSplitFleetContiguousScoring) { - getRaceColumnListeners().notifyListenersAboutHasSplitFleetContiguousScoringChanged(raceColumn, hasSplitFleetContiguousScoring); - } - - @Override - public void raceColumnMoved(RaceColumn raceColumn, int newIndex) { - getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(raceColumn, newIndex); - } - - @Override - public void factorChanged(RaceColumn raceColumn, Double oldFactor, Double newFactor) { - getRaceColumnListeners().notifyListenersAboutFactorChanged(raceColumn, oldFactor, newFactor); - } - - /** - * A leaderboard will only accept the addition of a race column if the column's name is unique across the leaderboard. - */ - @Override - public boolean canAddRaceColumnToContainer(RaceColumn newRaceColumn) { - boolean result = true; - for (RaceColumn raceColumn : getRaceColumns()) { - if (raceColumn.getName().equals(newRaceColumn.getName())) { - result = false; - break; - } - } - return result; - } - - @Override - public void raceColumnAddedToContainer(RaceColumn raceColumn) { - getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(raceColumn); - } - - @Override - public void raceColumnRemovedFromContainer(RaceColumn raceColumn) { - getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(raceColumn); - } - - @Override - public void competitorDisplayNameChanged(Competitor competitor, String oldDisplayName, String displayName) { - getRaceColumnListeners().notifyListenersAboutCompetitorDisplayNameChanged(competitor, oldDisplayName, displayName); - } - - @Override - public void resultDiscardingRuleChanged(ResultDiscardingRule oldDiscardingRule, ResultDiscardingRule newDiscardingRule) { - getRaceColumnListeners().notifyListenersAboutResultDiscardingRuleChanged(oldDiscardingRule, newDiscardingRule); - } - - @Override - public boolean isTransient() { - return false; - } - - /** - * Finds out the time point when any of the {@link Leaderboard#getTrackedRaces() tracked races currently attached to - * the leaderboard} and the {@link Leaderboard#getScoreCorrection() score corrections} have last been - * modified. If no tracked race is attached and no time-stamped score corrections have been applied to the leaderboard, - * null is returned. The time point computed this way is a good choice for normalizing queries for later time - * points in an attempt to achieve more cache hits.

- * - * Note, however, that the result does not tell about structural changes to the leaderboard and therefore cannot be used - * to determine the need for cache invalidation. For example, if a column is added to a leaderboard after the time point - * returned by this method but that column's attached tracked race has finished before the time point returned by this method, - * the result of this method won't change. Still, the contents of the leaderboard will change by a change in column structure. - * A different means to determine the possibility of changes that happened to this leaderboard must be used for cache - * management. Such a facility has to listen for score correction changes, tracked races being attached or detached and - * the column structure changing. - * - * @see TrackedRace#getTimePointOfNewestEvent() - * @see SettableScoreCorrection#getTimePointOfLastCorrectionsValidity() - */ - @Override - public TimePoint getTimePointOfLatestModification() { - TimePoint result = null; - for (TrackedRace trackedRace : getTrackedRaces()) { - if (result == null || (trackedRace.getTimePointOfNewestEvent() != null && trackedRace.getTimePointOfNewestEvent().after(result))) { - result = trackedRace.getTimePointOfNewestEvent(); - } - } - TimePoint timePointOfLastScoreCorrection = getScoreCorrection().getTimePointOfLastCorrectionsValidity(); - if (timePointOfLastScoreCorrection != null && (result == null || timePointOfLastScoreCorrection.after(result))) { - result = timePointOfLastScoreCorrection; - } - return result; - } - - @Override - public com.sap.sse.common.Util.Pair getMaximumSpeedOverGround(Competitor competitor, TimePoint timePoint) { - com.sap.sse.common.Util.Pair result = null; - // TODO should we ensure that competitor participated in all race columns? - for (TrackedRace trackedRace : getTrackedRaces()) { - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { - NavigableSet markPassings = trackedRace.getMarkPassings(competitor); - if (!markPassings.isEmpty()) { - TimePoint from = markPassings.first().getTimePoint(); - TimePoint to; - if (timePoint.after(markPassings.last().getTimePoint()) && - markPassings.last().getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint()) { - // stop counting when competitor finished the race - to = markPassings.last().getTimePoint(); - } else { - to = timePoint; - } - com.sap.sse.common.Util.Pair maxSpeed = trackedRace.getTrack(competitor).getMaximumSpeedOverGround(from, to); - if (result == null || result.getB() == null || - (maxSpeed != null && maxSpeed.getB() != null && maxSpeed.getB().compareTo(result.getB()) > 0)) { - result = maxSpeed; - } - } - } - } - return result; - } - - @Override - public Speed getAverageSpeedOverGround(Competitor competitor, TimePoint timePoint) { - Speed result = null; - for (TrackedRace trackedRace : getTrackedRaces()) { - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { - NavigableSet markPassings = trackedRace.getMarkPassings(competitor); - if (!markPassings.isEmpty()) { - TimePoint from = markPassings.first().getTimePoint(); - TimePoint to; - if (timePoint.after(markPassings.last().getTimePoint()) && - markPassings.last().getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint()) { - // stop counting when competitor finished the race - to = markPassings.last().getTimePoint(); - } else { - if (markPassings.last().getWaypoint() != trackedRace.getRace().getCourse().getLastWaypoint() && - timePoint.after(markPassings.last().getTimePoint())) { - result = null; - break; - } - to = timePoint; - } - Distance distanceTraveled = trackedRace.getDistanceTraveled(competitor, timePoint); - if (distanceTraveled != null) { - result = distanceTraveled.inTime(to.asMillis()-from.asMillis()); - } - } - } - } - return result; - } - - @Override - 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()) { - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { - trackedRace.getRace().getCourse().lockForRead(); - try { - for (Leg leg : trackedRace.getRace().getCourse().getLegs()) { - TrackedLegOfCompetitor trackedLegOfCompetitor = trackedRace.getTrackedLeg(competitor, leg); - if (trackedLegOfCompetitor.hasStartedLeg(timePoint)) { - // find out leg type at the time the competitor started the leg - try { - 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) { - Duration timeSpentOnDownwind = trackedLegOfCompetitor.getTime(timePoint); - if (timeSpentOnDownwind != null) { - if (result == null) { - result = timeSpentOnDownwind; - } else { - 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. - result = null; - break outerLoop; - } - } - } catch (NoWindException nwe) { - // without wind there is no leg type and hence there is no reasonable value for this: - result = null; - break outerLoop; - } - } - } - } finally { - trackedRace.getRace().getCourse().unlockAfterRead(); - } - } - } - return result; - } - - @Override - 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); - if (!markPassings.isEmpty()) { - TimePoint from = trackedRace.getStartOfRace(); // start counting at race start, not when the competitor passed the line - if (from != null && !timePoint.before(from)) { // but only if the race started after timePoint - TimePoint to; - if (timePoint.after(markPassings.last().getTimePoint()) - && markPassings.last().getWaypoint() == trackedRace.getRace().getCourse() - .getLastWaypoint()) { - // stop counting when competitor finished the race - to = markPassings.last().getTimePoint(); - } else { - if (trackedRace.getEndOfTracking() != null - && timePoint.after(trackedRace.getEndOfTracking())) { - result = null; // race not finished until end of tracking; no reasonable value can be - // computed for competitor - break; - } else { - to = timePoint; - } - } - Duration timeSpent = from.until(to); - if (result == null) { - result = timeSpent; - } else { - result=result.plus(timeSpent); - } - } - } - } - } - return result; - } - - @Override - public Distance getTotalDistanceTraveled(Competitor competitor, TimePoint timePoint) { - Distance result = null; - for (TrackedRace trackedRace : getTrackedRaces()) { - TimePoint startOfRace; - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor) && - (startOfRace=trackedRace.getStartOfRace()) != null && - !startOfRace.after(timePoint)) { - Distance distanceSailedInRace = trackedRace.getDistanceTraveled(competitor, timePoint); - if (distanceSailedInRace != null) { - if (result == null) { - result = distanceSailedInRace; - } else { - result = result.add(distanceSailedInRace); - } - } else { - // if competitor has not finished one single race in the whole - // series then we can not return a meaningful value for all - // all races - return null; - } - } - } - return result; - } - - protected RaceColumnListeners getRaceColumnListeners() { - return raceColumnListeners; - } - - @Override - public Iterable getCompetitors() { - final Iterable result; - // mostly the set of suppressed competitors is empty; in this case, avoid having to loop over the - // potentially large set of competitors - if (Util.isEmpty(getSuppressedCompetitors())) { - result = getAllCompetitors(); - } else { - final Iterable allCompetitors = getAllCompetitors(); - final Set suppressed = new HashSet<>(); - Util.addAll(getSuppressedCompetitors(), suppressed); - result = getCompetitorIterableSkippingSuppressed(allCompetitors, suppressed); - } - return result; - } - - @Override - public Iterable getCompetitors(RaceColumn raceColumn, Fleet fleet) { - return getCompetitorIterableSkippingSuppressed(getAllCompetitors(raceColumn, fleet), getSuppressedCompetitors()); - } - - /** - * return an iterable with a smart iterator that filters out the suppressed elements on demand - */ - protected Iterable getCompetitorIterableSkippingSuppressed(final Iterable allCompetitors, - final Iterable suppressed) { - final Iterable result; - result = new Iterable() { - @Override - public Iterator iterator() { - return new Iterator() { - private final Iterator allIter = allCompetitors.iterator(); - private Competitor next = advance(); - - private Competitor advance() { - next = null; - while (allIter.hasNext() && next == null) { - next = allIter.next(); - if (Util.contains(suppressed, next)) { - next = null; - } - } - return next; - } - - @Override - public boolean hasNext() { - return next != null; - } - - @Override - public Competitor next() { - if (next == null) { - throw new NoSuchElementException(); - } - final Competitor result = next; - advance(); - return result; - } - }; - } - }; - return result; - } - - @Override - public Iterable getSuppressedCompetitors() { - LockUtil.lockForRead(suppressedCompetitorsLock); - try { - return new HashSet(suppressedCompetitors); - } finally { - LockUtil.unlockAfterRead(suppressedCompetitorsLock); - } - } - - @Override - public void setSuppressed(Competitor competitor, boolean suppressed) { - LockUtil.lockForWrite(suppressedCompetitorsLock); - try { - if (suppressed) { - suppressedCompetitors.add(competitor); - } else { - suppressedCompetitors.remove(competitor); - } - } finally { - LockUtil.unlockAfterWrite(suppressedCompetitorsLock); - } - getScoreCorrection().notifyListenersAboutIsSuppressedChange(competitor, suppressed); - } - - @Override - public TimePoint getNowMinusDelay() { - final TimePoint now = MillisecondsTimePoint.now(); - final Long delayToLiveInMillis = getDelayToLiveInMillis(); - TimePoint timePoint = delayToLiveInMillis == null ? now : now.minus(delayToLiveInMillis); - return timePoint; - } - - @Override - public void raceLogEventAdded(RaceColumn raceColumn, RaceLogIdentifier raceLogIdentifier, RaceLogEvent event) { - getRaceColumnListeners().notifyListenersAboutRaceLogEventAdded(raceColumn, raceLogIdentifier, event); - if (event instanceof InvalidatesLeaderboardCache) { - // make sure to invalidate the cache as this event indicates that - // it changes values the cache could still hold - if (leaderboardDTOCache != null) { - leaderboardDTOCache.invalidate(this); - } - } - } - - @Override - public LeaderboardDTO computeDTO(final TimePoint timePoint, - final Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, - final boolean waitForLatestAnalyses, TrackedRegattaRegistry trackedRegattaRegistry, final DomainFactory baseDomainFactory, - final boolean fillNetPointsUncorrected) - throws NoWindException { - long startOfRequestHandling = System.currentTimeMillis(); - final LeaderboardDTOCalculationReuseCache cache = new LeaderboardDTOCalculationReuseCache(timePoint); - final LeaderboardDTO result = new LeaderboardDTO(this.getScoreCorrection().getTimePointOfLastCorrectionsValidity() == null ? null - : this.getScoreCorrection().getTimePointOfLastCorrectionsValidity().asDate(), - this.getScoreCorrection() == null ? null : this.getScoreCorrection().getComment(), - this.getScoringScheme() == null ? null : this.getScoringScheme().getType(), this - .getScoringScheme().isHigherBetter(), new UUIDGenerator(), addOverallDetails); - result.competitors = new ArrayList(); - result.name = this.getName(); - result.competitorDisplayNames = new HashMap(); - for (Competitor suppressedCompetitor : this.getSuppressedCompetitors()) { - result.setSuppressed(baseDomainFactory.convertToCompetitorDTO(suppressedCompetitor), true); - } - // 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(), - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null); - 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(), - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, - fleetDTO, raceColumn.isMedalRace(), raceIdentifier, race); - } - FutureTask> task = new FutureTask>( - () -> 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(); - result.hasCarriedPoints = this.hasCarriedPoints(); - if (this.getResultDiscardingRule() instanceof ThresholdBasedResultDiscardingRule) { - result.discardThresholds = ((ThresholdBasedResultDiscardingRule) this.getResultDiscardingRule()) - .getDiscardIndexResultsStartingWithHowManyRaces(); - } else { - result.discardThresholds = null; - } - // Computing the competitor leg ranks is expensive, especially in live mode, in case new events keep - // invalidating the ranks cache in TrackedLegImpl. The problem then is that the sorting based on wind data is repeated for - // each competitor, leading to square effort. We therefore need to compute the leg ranks for those races where leg - // 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 - if (namesOfRaceColumnsForWhichToLoadLegDetails != null - && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn.getName())) { - for (Fleet fleet : raceColumn.getFleets()) { - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - if (trackedRace != null) { - trackedRace.getRace().getCourse().lockForRead(); - try { - for (TrackedLeg trackedLeg : trackedRace.getTrackedLegs()) { - legRanksCache.put(trackedLeg.getLeg(), trackedLeg.getRanks(timePoint, cache)); - } - } finally { - trackedRace.getRace().getCourse().unlockAfterRead(); - } - } - } - } - } - for (final Competitor competitor : this.getCompetitorsFromBestToWorst(timePoint)) { - CompetitorDTO competitorDTO = baseDomainFactory.convertToCompetitorDTO(competitor); - LeaderboardRowDTO row = new LeaderboardRowDTO(); - row.competitor = competitorDTO; - row.fieldsByRaceColumnName = new HashMap(); - row.carriedPoints = this.hasCarriedPoints(competitor) ? this.getCarriedPoints(competitor) : null; - row.totalPoints = this.getTotalPoints(competitor, timePoint); - if (addOverallDetails) { - addOverallDetailsToRow(timePoint, competitor, row); - } - result.competitors.add(competitorDTO); - Map> futuresForColumnName = new HashMap>(); - final Set discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); - for (final RaceColumn raceColumn : this.getRaceColumns()) { - RunnableFuture future = new FutureTask(() -> { - Entry entry = AbstractSimpleLeaderboardImpl.this.getEntry(competitor, raceColumn, timePoint, discardedRaceColumns); - return getLeaderboardEntryDTO(entry, raceColumn, competitor, timePoint, - namesOfRaceColumnsForWhichToLoadLegDetails != null - && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn - .getName()), waitForLatestAnalyses, legRanksCache, baseDomainFactory, - fillNetPointsUncorrected, cache); - }); - executor.execute(future); - futuresForColumnName.put(raceColumn.getName(), future); - } - for (Map.Entry> raceColumnNameAndFuture : futuresForColumnName.entrySet()) { - try { - row.fieldsByRaceColumnName.put(raceColumnNameAndFuture.getKey(), raceColumnNameAndFuture.getValue().get()); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - // 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 leaderboard entry for competitor " - + competitor.getName() + " in race column " + raceColumnNameAndFuture.getKey() - + ". Leaving empty.", e); - } - } - result.rows.put(competitorDTO, row); - String displayName = this.getDisplayName(competitor); - if (displayName != null) { - result.competitorDisplayNames.put(competitorDTO, displayName); - } - } - logger.info("computeLeaderboardByName(" + this.getName() + ", " + timePoint + ", " - + namesOfRaceColumnsForWhichToLoadLegDetails + ", addOverallDetails=" + addOverallDetails + ") took " - + (System.currentTimeMillis() - startOfRequestHandling) + "ms"); - return result; - } - - private void addOverallDetailsToRow(final TimePoint timePoint, - final Competitor competitor, LeaderboardRowDTO row) throws NoWindException { - final com.sap.sse.common.Util.Pair maximumSpeedOverGround = this.getMaximumSpeedOverGround(competitor, timePoint); - if (maximumSpeedOverGround != null && maximumSpeedOverGround.getB() != null) { - row.maximumSpeedOverGroundInKnots = maximumSpeedOverGround.getB().getKnots(); - row.whenMaximumSpeedOverGroundWasAchieved = maximumSpeedOverGround.getA().getTimePoint().asDate(); - } - 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(); - } - - /** - * @param waitForLatestAnalyses - * if false, this method is allowed to read the maneuver analysis results from a cache that - * may not reflect all data already received; otherwise, the method will always block for the latest - * cache updates to have happened before returning. - * @param fillNetPointsUncorrected - * tells if {@link LeaderboardEntryDTO#netPointsUncorrected} shall be filled; filling it is rather - * expensive, especially when compared to simply retrieving a score correction, and particularly if in a - * larger fleet a number of competitors haven't properly finished the race. This should only be used for - * leaderboard editing where a user needs to see what the uncorrected score was that would be used when - * the correction was removed. - */ - private LeaderboardEntryDTO getLeaderboardEntryDTO(Entry entry, RaceColumn raceColumn, Competitor competitor, - TimePoint timePoint, boolean addLegDetails, boolean waitForLatestAnalyses, - Map> legRanksCache, DomainFactory baseDomainFactory, - boolean fillNetPointsUncorrected, WindLegTypeAndLegBearingCache cache) { - LeaderboardEntryDTO entryDTO = new LeaderboardEntryDTO(); - TrackedRace trackedRace = raceColumn.getTrackedRace(competitor); - entryDTO.race = trackedRace == null ? null : trackedRace.getRaceIdentifier(); - entryDTO.netPoints = entry.getNetPoints(); - if (fillNetPointsUncorrected) { - entryDTO.netPointsUncorrected = entry.getNetPointsUncorrected(); - } - entryDTO.netPointsCorrected = entry.isNetPointsCorrected(); - entryDTO.totalPoints = entry.getTotalPoints(); - entryDTO.reasonForMaxPoints = entry.getMaxPointsReason(); - entryDTO.discarded = entry.isDiscarded(); - final GPSFixTrack track = trackedRace == null ? null : trackedRace.getTrack(competitor); - if (trackedRace != null) { - Date timePointOfLastPositionFixAtOrBeforeQueryTimePoint = getTimePointOfLastFixAtOrBefore(competitor, trackedRace, timePoint); - if (track != null) { - entryDTO.averageSamplingInterval = track.getAverageIntervalBetweenRawFixes(); - } - if (timePointOfLastPositionFixAtOrBeforeQueryTimePoint != null) { - long timeDifferenceInMs = timePoint.asMillis() - timePointOfLastPositionFixAtOrBeforeQueryTimePoint.getTime(); - entryDTO.timeSinceLastPositionFixInSeconds = timeDifferenceInMs == 0 ? 0.0 : timeDifferenceInMs / 1000.0; - } else { - entryDTO.timeSinceLastPositionFixInSeconds = null; - } - } - if (addLegDetails && trackedRace != null) { - try { - final RankingInfo rankingInfo = trackedRace.getRankingMetric().getRankingInfo(timePoint, cache); - RaceDetails raceDetails = getRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, - legRanksCache, rankingInfo, cache); - entryDTO.legDetails = raceDetails.getLegDetails(); - entryDTO.windwardDistanceToCompetitorFarthestAheadInMeters = raceDetails.getWindwardDistanceToCompetitorFarthestAhead() == null ? null - : raceDetails.getWindwardDistanceToCompetitorFarthestAhead().getMeters(); - entryDTO.gapToLeaderInOwnTime = trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache); - entryDTO.averageAbsoluteCrossTrackErrorInMeters = raceDetails.getAverageAbsoluteCrossTrackError() == null ? null - : raceDetails.getAverageAbsoluteCrossTrackError().getMeters(); - entryDTO.averageSignedCrossTrackErrorInMeters = raceDetails.getAverageSignedCrossTrackError() == null ? null - : raceDetails.getAverageSignedCrossTrackError().getMeters(); - entryDTO.calculatedTime = raceDetails.getCorrectedTime(); - entryDTO.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = raceDetails.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead(); - entryDTO.gapToLeaderInOwnTime = raceDetails.getGapToLeaderInOwnTime(); - final TimePoint startOfRace = trackedRace.getStartOfRace(); - if (startOfRace != null) { - Waypoint startWaypoint = trackedRace.getRace().getCourse().getFirstWaypoint(); - NavigableSet competitorMarkPassings = trackedRace.getMarkPassings(competitor); - trackedRace.lockForRead(competitorMarkPassings); - try { - if (!competitorMarkPassings.isEmpty()) { - final MarkPassing firstMarkPassing = competitorMarkPassings.iterator().next(); - if (firstMarkPassing.getWaypoint() == startWaypoint) { - Distance distanceToStartLineFiveSecondsBeforeStartOfRace = trackedRace.getDistanceToStartLine(competitor, /*milliseconds before start*/ 5000); - entryDTO.distanceToStartLineFiveSecondsBeforeStartInMeters = distanceToStartLineFiveSecondsBeforeStartOfRace == null ? null - : distanceToStartLineFiveSecondsBeforeStartOfRace.getMeters(); - Speed speedFiveSecondsBeforeStartOfRace = trackedRace.getSpeed(competitor, /*milliseconds before start*/ 5000); - entryDTO.speedOverGroundFiveSecondsBeforeStartInKnots = speedFiveSecondsBeforeStartOfRace == null ? null - : speedFiveSecondsBeforeStartOfRace.getKnots(); - Distance distanceToStartLineAtStartOfRace = trackedRace.getDistanceToStartLine( - competitor, startOfRace); - entryDTO.distanceToStartLineAtStartOfRaceInMeters = distanceToStartLineAtStartOfRace == null ? null - : distanceToStartLineAtStartOfRace.getMeters(); - Speed speedAtStartTime = track == null ? null : track.getEstimatedSpeed(startOfRace); - entryDTO.speedOverGroundAtStartOfRaceInKnots = speedAtStartTime == null ? null - : speedAtStartTime.getKnots(); - TimePoint competitorStartTime = firstMarkPassing.getTimePoint(); - entryDTO.timeBetweenRaceStartAndCompetitorStartInSeconds = startOfRace.until(competitorStartTime).asSeconds(); - Speed competitorSpeedWhenPassingStart = track == null ? null : track - .getEstimatedSpeed(competitorStartTime); - entryDTO.speedOverGroundAtPassingStartWaypointInKnots = competitorSpeedWhenPassingStart == null ? null - : competitorSpeedWhenPassingStart.getKnots(); - try { - entryDTO.startTack = trackedRace.getTack(competitor, competitorStartTime); - } catch (NoWindException nwe) { - entryDTO.startTack = null; // leave empty in case no wind information is available - } - Distance distanceFromStarboardSideOfStartLineWhenPassingStart = trackedRace - .getDistanceFromStarboardSideOfStartLineWhenPassingStart(competitor); - entryDTO.distanceToStarboardSideOfStartLineInMeters = distanceFromStarboardSideOfStartLineWhenPassingStart == null ? null - : distanceFromStarboardSideOfStartLineWhenPassingStart.getMeters(); - } - } - } finally { - trackedRace.unlockAfterRead(competitorMarkPassings); - } - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - throw new RuntimeException(e); // the future used to calculate the leg details was interrupted; escalate as runtime exception - } - } - final Fleet fleet = entry.getFleet(); - entryDTO.fleet = fleet == null ? null : baseDomainFactory.convertToFleetDTO(fleet); - return entryDTO; - } - - /** - * Determines the time point of the last raw fix (with outliers not removed) for competitor in - * trackedRace. If the competitor's track is null or empty, null is returned. - * @param trackedRace must not be null - * @param atOrBefore find the last fix at or before the time point specified - */ - private Date getTimePointOfLastFixAtOrBefore(Competitor competitor, TrackedRace trackedRace, TimePoint atOrBefore) { - assert trackedRace != null; - final Date timePointOfLastPositionFix; - GPSFixTrack track = trackedRace.getTrack(competitor); - if (track == null) { - timePointOfLastPositionFix = null; - } else { - GPSFixMoving lastFix = track.getLastFixAtOrBefore(atOrBefore); - if (lastFix == null) { - timePointOfLastPositionFix = null; - } else { - timePointOfLastPositionFix = lastFix.getTimePoint().asDate(); - } - } - return timePointOfLastPositionFix; - } - - private static class RaceDetails { - private final List legDetails; - private final Distance windwardDistanceToCompetitorFarthestAhead; - private final Distance averageAbsoluteCrossTrackError; - private final Distance averageSignedCrossTrackError; - private final Duration gapToLeaderInOwnTime; - private final Duration correctedTime; - private final Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; - - public RaceDetails(List legDetails, Distance windwardDistanceToCompetitorFarthestAhead, - Distance averageAbsoluteCrossTrackError, Distance averageSignedCrossTrackError, - Duration gapToLeaderInOwnTime, Duration correctedTime, - Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead) { - super(); - this.legDetails = legDetails; - this.windwardDistanceToCompetitorFarthestAhead = windwardDistanceToCompetitorFarthestAhead; - this.averageAbsoluteCrossTrackError = averageAbsoluteCrossTrackError; - this.averageSignedCrossTrackError = averageSignedCrossTrackError; - this.gapToLeaderInOwnTime = gapToLeaderInOwnTime; - this.correctedTime = correctedTime; - this.correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; - } - public List getLegDetails() { - return legDetails; - } - public Distance getWindwardDistanceToCompetitorFarthestAhead() { - return windwardDistanceToCompetitorFarthestAhead; - } - public Distance getAverageAbsoluteCrossTrackError() { - return averageAbsoluteCrossTrackError; - } - public Distance getAverageSignedCrossTrackError() { - return averageSignedCrossTrackError; - } - public Duration getGapToLeaderInOwnTime() { - return gapToLeaderInOwnTime; - } - public Duration getCorrectedTime() { - return correctedTime; - } - public Duration getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead() { - return correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; - } - } - - /** - * If timePoint is after the end of the race's tracking the query will be adjusted to obtain the values - * at the end of the {@link TrackedRace#getEndOfTracking() race's tracking time}. If the time point adjusted this - * way equals the end of the tracking time, the query results will be looked up in a cache first and if not found, - * they will be stored to the cache after calculating them. A cache invalidation {@link RaceChangeListener listener} - * will be registered with the race which will be triggered for any event received by the race. - * @param waitForLatestAnalyses - * if false, this method is allowed to read the maneuver analysis results from a cache that - * may not reflect all data already received; otherwise, the method will always block for the latest - * cache updates to have happened before returning. - */ - private RaceDetails getRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, - boolean waitForLatestAnalyses, Map> legRanksCache, - RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) throws InterruptedException, ExecutionException { - final RaceDetails raceDetails; - if (trackedRace.getEndOfTracking() != null && trackedRace.getEndOfTracking().compareTo(timePoint) < 0) { - raceDetails = getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(trackedRace, competitor, legRanksCache, rankingInfo, cache); - } else { - raceDetails = calculateRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, legRanksCache, cache, rankingInfo); - } - return raceDetails; - } - - private RaceDetails getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(final TrackedRace trackedRace, - final Competitor competitor, final Map> legRanksCache, - RankingInfo rankingInfo, final WindLegTypeAndLegBearingCache cache) throws InterruptedException, ExecutionException { - final com.sap.sse.common.Util.Pair key = new com.sap.sse.common.Util.Pair(trackedRace, competitor); - RunnableFuture raceDetails; - synchronized (raceDetailsAtEndOfTrackingCache) { - raceDetails = raceDetailsAtEndOfTrackingCache.get(key); - if (raceDetails == null) { - raceDetails = new FutureTask(new Callable() { - @Override - public RaceDetails call() throws Exception { - TimePoint end = trackedRace.getEndOfRace(); - if (end == null) { - end = trackedRace.getEndOfTracking(); - } - return calculateRaceDetails(trackedRace, competitor, end, - // TODO see bug 1358: for now, use waitForLatest==false until we've switched to optimistic locking for the course read lock - /* TODO old comment when it was still true: "because this is done only once after end of tracking" */ - /* waitForLatestAnalyses (maneuver and cross track error) */ false, - legRanksCache, cache, rankingInfo); - } - }); - raceDetailsExecutor.execute(raceDetails); - raceDetailsAtEndOfTrackingCache.put(key, raceDetails); - final CacheInvalidationListener cacheInvalidationListener = new CacheInvalidationListener(trackedRace, competitor); - trackedRace.addListener(cacheInvalidationListener); - cacheInvalidationListeners.add(cacheInvalidationListener); - } - } - return raceDetails.get(); - } - - private RaceDetails calculateRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, - boolean waitForLatestAnalyses, Map> legRanksCache, - WindLegTypeAndLegBearingCache cache, final RankingInfo rankingInfo) { - final List legDetails = new ArrayList(); - final Course course = trackedRace.getRace().getCourse(); - course.lockForRead(); // hold back any course re-configurations while looping over the legs - try { - for (Leg leg : course.getLegs()) { - LegEntryDTO legEntry; - // We loop over a copy of the course's legs; during a course change, legs may become "stale," even with - // regard to the leg/trackedLeg structures inside the tracked race which is updated by the course change - // immediately. That's why we've acquired a read lock for the course above. - TrackedLegOfCompetitor trackedLeg = trackedRace.getTrackedLeg(competitor, leg); - if (trackedLeg != null && trackedLeg.hasStartedLeg(timePoint)) { - legEntry = createLegEntry(trackedLeg, timePoint, waitForLatestAnalyses, legRanksCache, rankingInfo, cache); - } else { - legEntry = null; - } - legDetails.add(legEntry); - } - final Distance windwardDistanceToCompetitorFarthestAhead = trackedRace == null ? null : trackedRace - .getWindwardDistanceToCompetitorFarthestAhead(competitor, timePoint, WindPositionMode.LEG_MIDDLE, rankingInfo, cache); - Distance averageAbsoluteCrossTrackError; - try { - averageAbsoluteCrossTrackError = trackedRace == null ? null : trackedRace.getAverageAbsoluteCrossTrackError( - competitor, timePoint, waitForLatestAnalyses, cache); - } catch (NoWindException nwe) { - // without wind information, use null meaning "unknown" - averageAbsoluteCrossTrackError = null; - } - Distance averageSignedCrossTrackError; - try { - averageSignedCrossTrackError = trackedRace == null ? null : trackedRace.getAverageSignedCrossTrackError( - competitor, timePoint, waitForLatestAnalyses, cache); - } catch (NoWindException nwe) { - // without wind information, use null meaning "unknown" - averageSignedCrossTrackError = null; - } - final CompetitorRankingInfo competitorRankingInfo = rankingInfo.getCompetitorRankingInfo().apply(competitor); - return new RaceDetails(legDetails, windwardDistanceToCompetitorFarthestAhead, averageAbsoluteCrossTrackError, averageSignedCrossTrackError, - trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache), - trackedRace.getRankingMetric().getCorrectedTime(competitor, timePoint), - competitorRankingInfo == null ? null : competitorRankingInfo.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead()); - } finally { - course.unlockAfterRead(); - } - } - - private LegEntryDTO createLegEntry(TrackedLegOfCompetitor trackedLeg, TimePoint timePoint, - boolean waitForLatestAnalyses, Map> legRanksCache, - RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) { - LegEntryDTO result; - final Duration time = trackedLeg.getTime(timePoint); - if (trackedLeg == null || time == null) { - result = null; - } else { - result = new LegEntryDTO(); - try { - result.legType = trackedLeg.getTrackedLeg().getLegType(timePoint); - } catch (NoWindException nwe) { - result.legType = null; // can't determine leg type without wind data - } - final Speed averageSpeedOverGround = trackedLeg.getAverageSpeedOverGround(timePoint); - result.averageSpeedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); - Distance averageAbsoluteCrossTrackError; - try { - averageAbsoluteCrossTrackError = trackedLeg.getAverageAbsoluteCrossTrackError(timePoint, waitForLatestAnalyses); - } catch (NoWindException nwe) { - // leave averageAbsoluteCrossTrackError as null, meaning "unknown" - averageAbsoluteCrossTrackError = null; - } - result.averageAbsoluteCrossTrackErrorInMeters = averageAbsoluteCrossTrackError == null ? null : averageAbsoluteCrossTrackError.getMeters(); - Distance averageSignedCrossTrackError; - try { - averageSignedCrossTrackError = trackedLeg.getAverageSignedCrossTrackError(timePoint, waitForLatestAnalyses); - } catch (NoWindException nwe) { - // leave averageSignedCrossTrackError as null, meaning "unknown" - averageSignedCrossTrackError = null; - } - result.averageSignedCrossTrackErrorInMeters = averageSignedCrossTrackError == null ? null : averageSignedCrossTrackError.getMeters(); - Double speedOverGroundInKnots; - if (trackedLeg.hasFinishedLeg(timePoint)) { - speedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); - } else { - final SpeedWithBearing speedOverGround = trackedLeg.getSpeedOverGround(timePoint); - speedOverGroundInKnots = speedOverGround == null ? null : speedOverGround.getKnots(); - } - result.currentSpeedOverGroundInKnots = speedOverGroundInKnots == null ? null : speedOverGroundInKnots; - Distance distanceTraveled = trackedLeg.getDistanceTraveled(timePoint); - result.distanceTraveledInMeters = distanceTraveled == null ? null : distanceTraveled.getMeters(); - Distance distanceTraveledConsideringGateStart = trackedLeg.getDistanceTraveledConsideringGateStart(timePoint); - result.distanceTraveledIncludingGateStartInMeters = distanceTraveledConsideringGateStart == null ? null : distanceTraveledConsideringGateStart.getMeters(); - final Duration estimatedTimeToNextMarkInSeconds = trackedLeg.getEstimatedTimeToNextMark(timePoint, WindPositionMode.EXACT, cache); - result.estimatedTimeToNextWaypointInSeconds = estimatedTimeToNextMarkInSeconds==null?null:estimatedTimeToNextMarkInSeconds.asSeconds(); - result.timeInMilliseconds = time.asMillis(); - result.finished = trackedLeg.hasFinishedLeg(timePoint); - final TimePoint legFinishTime = trackedLeg.getFinishTime(); - result.correctedTotalTime = trackedLeg.hasStartedLeg(timePoint) ? trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric().getCorrectedTime(trackedLeg.getCompetitor(), - trackedLeg.hasFinishedLeg(timePoint) ? legFinishTime : timePoint, cache) : null; - // fetch the leg gap in own corrected time from the ranking metric - final Duration gapToLeaderInOwnTime = trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric(). - getLegGapToLegLeaderInOwnTime(trackedLeg, timePoint, rankingInfo, cache); - result.gapToLeaderInSeconds = gapToLeaderInOwnTime == null ? null : gapToLeaderInOwnTime.asSeconds(); - if (result.gapToLeaderInSeconds != null) { - final Duration gapAtEndOfPreviousLeg = getGapAtEndOfPreviousLeg(trackedLeg, rankingInfo, cache); - if (gapAtEndOfPreviousLeg != null) { - result.gapChangeSinceLegStartInSeconds = result.gapToLeaderInSeconds - gapAtEndOfPreviousLeg.asSeconds(); - } - } - LinkedHashMap legRanks = legRanksCache.get(trackedLeg.getLeg()); - if (legRanks != null) { - result.rank = legRanks.get(trackedLeg.getCompetitor()); - } else { - result.rank = trackedLeg.getRank(timePoint, cache); - } - result.started = trackedLeg.hasStartedLeg(timePoint); - Speed velocityMadeGood; - if (trackedLeg.hasFinishedLeg(timePoint)) { - velocityMadeGood = trackedLeg.getAverageVelocityMadeGood(timePoint); - } else { - velocityMadeGood = trackedLeg.getVelocityMadeGood(timePoint, WindPositionMode.EXACT); - } - result.velocityMadeGoodInKnots = velocityMadeGood == null ? null : velocityMadeGood.getKnots(); - Distance windwardDistanceToGo = trackedLeg.getWindwardDistanceToGo(timePoint, WindPositionMode.LEG_MIDDLE); - result.windwardDistanceToGoInMeters = windwardDistanceToGo == null ? null : windwardDistanceToGo - .getMeters(); - final TimePoint startOfRace = trackedLeg.getTrackedLeg().getTrackedRace().getStartOfRace(); - if (startOfRace != null && trackedLeg.hasStartedLeg(timePoint)) { - // not using trackedLeg.getManeuvers(...) because it may not catch the mark passing maneuver starting this leg - // because that may have been detected as slightly before the mark passing time, hence associated with the previous leg - List maneuvers = trackedLeg.getTrackedLeg().getTrackedRace() - .getManeuvers(trackedLeg.getCompetitor(), startOfRace, timePoint, waitForLatestAnalyses); - if (maneuvers != null) { - result.numberOfManeuvers = new HashMap(); - result.numberOfManeuvers.put(ManeuverType.TACK, 0); - result.numberOfManeuvers.put(ManeuverType.JIBE, 0); - result.numberOfManeuvers.put(ManeuverType.PENALTY_CIRCLE, 0); - Map totalManeuverLossInMeters = new HashMap(); - totalManeuverLossInMeters.put(ManeuverType.TACK, 0.0); - totalManeuverLossInMeters.put(ManeuverType.JIBE, 0.0); - totalManeuverLossInMeters.put(ManeuverType.PENALTY_CIRCLE, 0.0); - TimePoint startOfLeg = trackedLeg.getStartTime(); - for (Maneuver maneuver : maneuvers) { - // don't count maneuvers that were in previous legs - switch (maneuver.getType()) { - case TACK: - case JIBE: - case PENALTY_CIRCLE: - if (!maneuver.getTimePoint().before(startOfLeg) && (legFinishTime == null || legFinishTime.after(timePoint) || - maneuver.getTimePoint().before(legFinishTime))) { - if (maneuver.getManeuverLoss() != null) { - result.numberOfManeuvers.put(maneuver.getType(), - result.numberOfManeuvers.get(maneuver.getType()) + 1); - totalManeuverLossInMeters.put(maneuver.getType(), - totalManeuverLossInMeters.get(maneuver.getType()) - + maneuver.getManeuverLoss().getMeters()); - } - } - break; - case MARK_PASSING: - // analyze all mark passings, not only those after this leg's start, to catch the mark passing - // maneuver starting this leg, even if its time point is slightly before the mark passing starting this leg - MarkPassingManeuver mpm = (MarkPassingManeuver) maneuver; - if (mpm.getWaypointPassed() == trackedLeg.getLeg().getFrom()) { - result.sideToWhichMarkAtLegStartWasRounded = mpm.getSide(); - } - break; - default: - /* Do nothing here. - * Throwing an exception destroys the toggling (and maybe other behaviour) of the leaderboard. - */ - } - } - result.averageManeuverLossInMeters = new HashMap(); - for (ManeuverType maneuverType : new ManeuverType[] { ManeuverType.TACK, ManeuverType.JIBE, - ManeuverType.PENALTY_CIRCLE }) { - if (result.numberOfManeuvers.get(maneuverType) != 0) { - result.averageManeuverLossInMeters.put( - maneuverType, - totalManeuverLossInMeters.get(maneuverType) - / result.numberOfManeuvers.get(maneuverType)); - } - } - } - } - } - return result; - } - - private Duration getGapAtEndOfPreviousLeg(TrackedLegOfCompetitor trackedLeg, final RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) { - final Duration result; - final Course course = trackedLeg.getTrackedLeg().getTrackedRace().getRace().getCourse(); - // if trackedLeg is the first leg, compute the gap at the start of this leg; otherwise, compute gap - // at the end of the previous leg - final TimePoint timePoint = trackedLeg.getStartTime(); - final TrackedLegOfCompetitor tloc; - if (course.getFirstWaypoint() == trackedLeg.getLeg().getFrom()) { - tloc = trackedLeg; - } else { - tloc = trackedLeg.getTrackedLeg().getTrackedRace().getTrackedLegFinishingAt(trackedLeg.getLeg().getFrom()) - .getTrackedLeg(trackedLeg.getCompetitor()); - } - result = trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric().getLegGapToLegLeaderInOwnTime(tloc, timePoint, rankingInfo, cache); - return result; - } - - private LeaderboardDTO getLiveLeaderboard(Collection namesOfRaceColumnsForWhichToLoadLegDetails, - boolean addOverallDetails, TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory) throws NoWindException, ExecutionException { - LiveLeaderboardUpdater liveLeaderboardUpdater = getLiveLeaderboardUpdater(trackedRegattaRegistry, - baseDomainFactory); - return liveLeaderboardUpdater.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails); - } - - private LiveLeaderboardUpdater getLiveLeaderboardUpdater(TrackedRegattaRegistry trackedRegattaRegistry, - DomainFactory baseDomainFactory) { - LiveLeaderboardUpdater result = this.liveLeaderboardUpdater; - if (result == null) { - synchronized (this) { - result = this.liveLeaderboardUpdater; - if (result == null) { - this.liveLeaderboardUpdater = new LiveLeaderboardUpdater(this, trackedRegattaRegistry, baseDomainFactory); - result = this.liveLeaderboardUpdater; - } - } - } - return result; - } - - private LeaderboardDTOCache getLeaderboardDTOCache() { - LeaderboardDTOCache result = this.leaderboardDTOCache; - if (result == null) { - synchronized (this) { - result = this.leaderboardDTOCache; - if (result == null) { - // The leaderboard cache is invalidated upon all competitor and mark position changes; some analyzes - // are pretty expensive, such as the maneuver re-calculation. Waiting for the latest analysis after only a - // single fix was updated is too expensive if users use the replay feature while a race is still running. - // Therefore, using waitForLatestAnalyses==false seems appropriate here. - this.leaderboardDTOCache = new LeaderboardDTOCache(/* waitForLatestAnalyses */false, this); - result = this.leaderboardDTOCache; - } - } - } - return result; - } - - @Override - public LeaderboardDTO getLeaderboardDTO(TimePoint timePoint, - Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, - TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory, boolean fillNetPointsUncorrected) throws NoWindException, - InterruptedException, ExecutionException { - LeaderboardDTO result = null; - if (timePoint == null) { - // timePoint==null means live mode; however, if we're after the end of all races and after all score - // corrections, don't use the live leaderboard updater which would keep re-calculating over and over again, but map - // this to a usual non-live call which uses the regular LeaderboardDTOCache which is invalidated properly - // when the tracked race associations or score corrections or tracked race contents changes: - final TimePoint nowMinusDelay = this.getNowMinusDelay(); - final TimePoint timePointOfLatestModification = this.getTimePointOfLatestModification(); - if (fillNetPointsUncorrected || (timePointOfLatestModification != null && !nowMinusDelay.before(timePointOfLatestModification))) { - // if there hasn't been any modification to the leaderboard since nowMinusDelay, use non-live mode - // and pull the result from the regular leaderboard cache: - timePoint = timePointOfLatestModification; - } 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, addOverallDetails, trackedRegattaRegistry, baseDomainFactory); - } - } - if (timePoint != null) { - if (fillNetPointsUncorrected) { - // explicitly filling the uncorrected net points requires uncached recalculation - result = computeDTO(timePoint, namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, /* waitForLatestAnalyses */ true, - trackedRegattaRegistry, baseDomainFactory, fillNetPointsUncorrected); - } else { - // in replay we'd like up-to-date results; they are still cached - // which is OK because the cache is invalidated whenever any of the tracked races attached to the - // leaderboard changes. - result = getLeaderboardDTOCache().getLeaderboardByName(timePoint, - namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, baseDomainFactory, - trackedRegattaRegistry); - } - } - return result; - } - - public String toString() { - return getName() + " " + (getDefaultCourseArea() != null ? getDefaultCourseArea().getName() : "") + " " + (getScoringScheme() != null ? getScoringScheme().getType().name() : ""); - } - - @Override - public NumberOfCompetitorsInLeaderboardFetcher getNumberOfCompetitorsInLeaderboardFetcher() { - return new NumberOfCompetitorsFetcherImpl(); - } -} +package com.sap.sailing.domain.leaderboard.impl; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NavigableSet; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RunnableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.sap.sailing.domain.abstractlog.race.InvalidatesLeaderboardCache; +import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; +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.Fleet; +import com.sap.sailing.domain.base.Leg; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnInSeries; +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.LeaderboardType; +import com.sap.sailing.domain.common.LegType; +import com.sap.sailing.domain.common.ManeuverType; +import com.sap.sailing.domain.common.MaxPointsReason; +import com.sap.sailing.domain.common.NoWindError; +import com.sap.sailing.domain.common.NoWindException; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.RegattaNameAndRaceName; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.dto.BasicRaceDTO; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.domain.common.dto.FleetDTO; +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.MetaLeaderboardRaceColumnDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.domain.common.dto.RaceDTO; +import com.sap.sailing.domain.common.tracking.GPSFixMoving; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.NumberOfCompetitorsInLeaderboardFetcher; +import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; +import com.sap.sailing.domain.leaderboard.ScoreCorrection.Result; +import com.sap.sailing.domain.leaderboard.SettableScoreCorrection; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCache; +import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCalculationReuseCache; +import com.sap.sailing.domain.leaderboard.caching.LiveLeaderboardUpdater; +import com.sap.sailing.domain.leaderboard.meta.MetaLeaderboardColumn; +import com.sap.sailing.domain.racelog.RaceLogIdentifier; +import com.sap.sailing.domain.ranking.RankingMetric.CompetitorRankingInfo; +import com.sap.sailing.domain.ranking.RankingMetric.RankingInfo; +import com.sap.sailing.domain.tracking.GPSFixTrack; +import com.sap.sailing.domain.tracking.Maneuver; +import com.sap.sailing.domain.tracking.MarkPassing; +import com.sap.sailing.domain.tracking.MarkPassingManeuver; +import com.sap.sailing.domain.tracking.RaceChangeListener; +import com.sap.sailing.domain.tracking.TrackedLeg; +import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; +import com.sap.sailing.domain.tracking.WindLegTypeAndLegBearingCache; +import com.sap.sailing.domain.tracking.WindPositionMode; +import com.sap.sailing.domain.tracking.impl.AbstractRaceChangeListener; +import com.sap.sailing.util.impl.RaceColumnListeners; +import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; +import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.concurrent.LockUtil; +import com.sap.sse.concurrent.NamedReentrantReadWriteLock; +import com.sap.sse.util.impl.ThreadFactoryWithPriority; + +/** + * Base implementation for various types of leaderboards. The {@link RaceColumnListener} implementation forwards events + * received to all {@link RaceColumnListener} subscribed with this leaderboard. To which objects this leaderboard subscribes + * as {@link RaceColumnListener} is left to the concrete subclasses to implement, but the race columns seem like useful + * candidates. + * + * @author Axel Uhl (d043530) + * + */ +public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, RaceColumnListener { + private static final long serialVersionUID = 330156778603279333L; + + private static final Logger logger = Logger.getLogger(AbstractSimpleLeaderboardImpl.class.getName()); + + static final Double DOUBLE_0 = new Double(0); + + private final SettableScoreCorrection scoreCorrection; + + private ThresholdBasedResultDiscardingRule crossLeaderboardResultDiscardingRule; + + /** + * The optional display name mappings for competitors. This allows a user to override the tracking-provided + * competitor names for display in a leaderboard. + */ + private final Map displayNames; + + /** the display name of the leaderboard */ + private String displayName; + + /** + * Backs the {@link #getCarriedPoints(Competitor)} API with data. Can be used to prime this leaderboard + * with aggregated results of races not tracked / displayed by this leaderboard in detail. The points + * provided by this map are considered by {@link #getTotalPoints(Competitor, TimePoint)}. + */ + private final Map carriedPoints; + + private final RaceColumnListeners raceColumnListeners; + + /** + * A set that manages the difference between {@link #getCompetitors()} and {@link #getAllCompetitors()}. Access + * is controlled by the {@link #suppressedCompetitorsLock} lock. + */ + private final Set suppressedCompetitors; + private final NamedReentrantReadWriteLock suppressedCompetitorsLock; + + private transient Map, RunnableFuture> raceDetailsAtEndOfTrackingCache; + + /** + * This executor needs to be a different one than {@link #executor} because the tasks run by {@link #executor} + * can depend on the results of the tasks run by {@link #raceDetailsExecutor}, and an {@link Executor} doesn't + * move a task that is blocked by waiting for another {@link FutureTask} to the side but blocks permanently, + * ending in a deadlock (one that cannot easily be detected by the Eclipse debugger either). + */ + private transient Executor raceDetailsExecutor; + + /** + * Used to remove all these listeners from their tracked races when this servlet is {@link #destroy() destroyed}. + */ + private transient Set cacheInvalidationListeners; + + private transient ThreadPoolExecutor executor; + + private transient LiveLeaderboardUpdater liveLeaderboardUpdater; + + private transient LeaderboardDTOCache leaderboardDTOCache; + + /** + * A leaderboard entry representing a snapshot of a cell at a given time point for a single race/competitor. + * + * @author Axel Uhl (d043530) + * + */ + class EntryImpl implements Entry { + private final Callable trackedRankProvider; + private final Double netPoints; + private final Callable netPointsUncorrectedProvider; + private final boolean isNetPointsCorrected; + private final Double totalPoints; + private final MaxPointsReason maxPointsReason; + private final boolean discarded; + private final Fleet fleet; + + private EntryImpl(Callable trackedRankProvider, Double netPoints, + Callable netPointsUncorrectedProvider, boolean isNetPointsCorrected, + Double totalPoints, MaxPointsReason maxPointsReason, boolean discarded, Fleet fleet) { + super(); + this.trackedRankProvider = trackedRankProvider; + this.netPoints = netPoints; + this.netPointsUncorrectedProvider = netPointsUncorrectedProvider; + this.isNetPointsCorrected = isNetPointsCorrected; + this.totalPoints = totalPoints; + this.maxPointsReason = maxPointsReason; + this.discarded = discarded; + this.fleet = fleet; + } + @Override + public int getTrackedRank() { + try { + return trackedRankProvider.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + @Override + public Double getNetPoints() { + return netPoints; + } + @Override + public boolean isNetPointsCorrected() { + return isNetPointsCorrected; + } + @Override + public Double getTotalPoints() { + return totalPoints; + } + @Override + public MaxPointsReason getMaxPointsReason() { + return maxPointsReason; + } + @Override + public boolean isDiscarded() { + return discarded; + } + @Override + public Fleet getFleet() { + return fleet; + } + @Override + public Double getNetPointsUncorrected() { + try { + return netPointsUncorrectedProvider.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + /** + * Computing the competitors can be a bit expensive, particularly if the fleet is large and there may be suppressed + * competitors, and the leaderboard may be a meta-leaderboard that refers to other leaderboards which each have + * several tracked races attached from where the competitors need to be retrieved. Ideally, the competitors list + * would be cached, but that is again difficult because we would have to monitor all changes in all dependent + * leaderboards and columns and tracked races properly. + *

+ * + * As it turns out, one of the most frequent uses of the {@link AbstractSimpleLeaderboardImpl#getCompetitors} + * competitors list is to determine their number which in turn is only required for high-point scoring systems and + * for computing the default score for penalties. Again, the most frequently used low-point family of scoring schemes + * does not require this number. Yet, the scoring scheme requires an argument for polymorphic use by those that + * need it. Instead of computing it for each call, this interface lets us defer the actual calculation until the + * point when it's really needed. Once asked, this object will cache the result. Therefore, a new one should be + * constructed each time the number shall be computed. + * + * @author Axel Uhl (D043530) + * + */ + public class NumberOfCompetitorsFetcherImpl implements NumberOfCompetitorsInLeaderboardFetcher { + private int numberOfCompetitors = -1; + + @Override + public int getNumberOfCompetitorsInLeaderboard() { + if (numberOfCompetitors == -1) { + numberOfCompetitors = Util.size(getCompetitors()); + } + return numberOfCompetitors; + } + } + + /** + * Handles the invalidation of the {@link SailingServiceImpl#raceDetailsAtEndOfTrackingCache} entries if the tracked + * race changes in any way. In particular, for {@link #statusChanged}, when the status changes away from LOADING, + * calculations may start or resume, making it necessary to clear the cache. + * + * @author Axel Uhl (D043530) + * + */ + private class CacheInvalidationListener extends AbstractRaceChangeListener { + private final TrackedRace trackedRace; + private final Competitor competitor; + + public CacheInvalidationListener(TrackedRace trackedRace, Competitor competitor) { + this.trackedRace = trackedRace; + this.competitor = competitor; + } + + public TrackedRace getTrackedRace() { + return trackedRace; + } + + public void removeFromTrackedRace() { + trackedRace.removeListener(this); + } + + private void invalidateCacheAndRemoveThisListenerFromTrackedRace() { + synchronized (raceDetailsAtEndOfTrackingCache) { + raceDetailsAtEndOfTrackingCache.remove(new com.sap.sse.common.Util.Pair(trackedRace, competitor)); + removeFromTrackedRace(); + } + } + + @Override + protected void defaultAction() { + invalidateCacheAndRemoveThisListenerFromTrackedRace(); + } + } + + private static class UUIDGenerator implements LeaderboardDTO.UUIDGenerator { + @Override + public String generateRandomUUID() { + return UUID.randomUUID().toString(); + } + } + + public AbstractSimpleLeaderboardImpl(ThresholdBasedResultDiscardingRule resultDiscardingRule) { + this.carriedPoints = new HashMap(); + this.scoreCorrection = createScoreCorrection(); + this.displayNames = new HashMap(); + this.crossLeaderboardResultDiscardingRule = resultDiscardingRule; + this.suppressedCompetitors = new HashSet(); + this.suppressedCompetitorsLock = new NamedReentrantReadWriteLock("suppressedCompetitorsLock", /* fair */ false); + this.raceColumnListeners = new RaceColumnListeners(); + this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); + initTransientFields(); + } + + /** + * Produces the score correction object to use in this leaderboard. Used by the constructor. Subclasses may override + * this method to create a more specific type of score correction. This implementation produces an object of type + * {@link ScoreCorrectionImpl}. + */ + protected SettableScoreCorrection createScoreCorrection() { + return new ScoreCorrectionImpl(this); + } + + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + initTransientFields(); + } + + private void initTransientFields() { + this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); + this.raceDetailsExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + this.cacheInvalidationListeners = new HashSet(); + // When many updates are triggered in a short period of time by a single thread, ensure that the single thread + // providing the updates is not outperformed by all the re-calculations happening here. Leave at least one + // core to other things, but by using at least three threads ensure that no simplistic deadlocks may occur. + final int THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors(), 3); + executor = new ThreadPoolExecutor(/* corePoolSize */ THREAD_POOL_SIZE, + /* maximumPoolSize */ THREAD_POOL_SIZE, + /* keepAliveTime */ 60, TimeUnit.SECONDS, + /* workQueue */ new LinkedBlockingQueue(), new ThreadFactoryWithPriority(Thread.NORM_PRIORITY-1, /* daemon */ true)); + } + + @Override + public void destroy() { + for (CacheInvalidationListener cacheInvalidationListener : cacheInvalidationListeners) { + cacheInvalidationListener.removeFromTrackedRace(); + } + } + + @Override + public SettableScoreCorrection getScoreCorrection() { + return scoreCorrection; + } + + @Override + public String getDisplayName(Competitor competitor) { + return displayNames.get(competitor); + } + + @Override + public String getDisplayName() { + return displayName; + } + + @Override + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + @Override + public ResultDiscardingRule getResultDiscardingRule() { + return crossLeaderboardResultDiscardingRule; + } + + @Override + public void setCarriedPoints(Competitor competitor, double carriedPoints) { + Double oldCarriedPoints = this.carriedPoints.put(competitor, carriedPoints); + if (!Util.equalsWithNull(oldCarriedPoints, carriedPoints)) { + getScoreCorrection().notifyListenersAboutCarriedPointsChange(competitor, oldCarriedPoints, carriedPoints); + } + } + + @Override + public double getCarriedPoints(Competitor competitor) { + Double result = carriedPoints.get(competitor); + return result == null ? 0 : result; + } + + @Override + public Map getCompetitorsForWhichThereAreCarriedPoints() { + return Collections.unmodifiableMap(carriedPoints); + } + + @Override + public void unsetCarriedPoints(Competitor competitor) { + Double oldCarriedPoints = carriedPoints.remove(competitor); + if (oldCarriedPoints != null) { + getScoreCorrection().notifyListenersAboutCarriedPointsChange(competitor, oldCarriedPoints, null); + } + } + + @Override + public boolean hasCarriedPoints() { + return !carriedPoints.isEmpty(); + } + + @Override + public boolean hasCarriedPoints(Competitor competitor) { + return carriedPoints.containsKey(competitor); + } + + @Override + public void setDisplayName(Competitor competitor, String displayName) { + String oldDisplayName = displayNames.get(competitor); + displayNames.put(competitor, displayName); + if (!Util.equalsWithNull(oldDisplayName, displayName)) { + getRaceColumnListeners().notifyListenersAboutCompetitorDisplayNameChanged(competitor, oldDisplayName, displayName); + } + } + + @Override + public void setCrossLeaderboardResultDiscardingRule(ThresholdBasedResultDiscardingRule discardingRule) { + ResultDiscardingRule oldDiscardingRule = getResultDiscardingRule(); + this.crossLeaderboardResultDiscardingRule = discardingRule; + getRaceColumnListeners().notifyListenersAboutResultDiscardingRuleChanged(oldDiscardingRule, discardingRule); + } + + @Override + public Double getNetPoints(final Competitor competitor, final RaceColumn raceColumn, final TimePoint timePoint) { + return getScoreCorrection().getCorrectedScore( + new Callable() { + public Integer call() throws NoWindException { + return getTrackedRank(competitor, raceColumn, timePoint); + } + }, competitor, + raceColumn, timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()).getCorrectedScore(); + } + + @Override + public MaxPointsReason getMaxPointsReason(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { + return getScoreCorrection().getMaxPointsReason(competitor, raceColumn, timePoint); + } + + @Override + public boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { + return isDiscarded(competitor, raceColumn, getRaceColumns(), timePoint); + } + + private boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, + Iterable raceColumnsToConsider, TimePoint timePoint) { + final Set discardedRaceColumns = getResultDiscardingRule() + .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); + return isDiscarded(competitor, raceColumn, timePoint, discardedRaceColumns); + } + + /** + * Same as {@link #isDiscarded(Competitor, RaceColumn, TimePoint)}, only that the set of discarded race columns can + * be specified which is useful when total points are to be computed for more than one column for the same + * competitor because then the calculation of discards (which requires looking at all columns) only needs to be done + * once and not again for each column (which would lead to quadratic effort). + * + * @param discardedRaceColumns + * expected to be the result of what we would get if we called {@link #getResultDiscardingRule()}. + * {@link ResultDiscardingRule#getDiscardedRaceColumns(Competitor, Leaderboard, Iterable, TimePoint) + * getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint)}. + */ + private boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint, + final Set discardedRaceColumns) { + return !raceColumn.isMedalRace() + && getMaxPointsReason(competitor, raceColumn, timePoint).isDiscardable() + && discardedRaceColumns.contains(raceColumn); + } + + @Override + public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) throws NoWindException { + return getTotalPoints(competitor, raceColumn, getRaceColumns(), timePoint); + } + + @Override + public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, + Iterable raceColumnsToConsider, TimePoint timePoint) throws NoWindException { + final Set discardedRaceColumns = getResultDiscardingRule() + .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); + return getTotalPoints(competitor, raceColumn, timePoint, discardedRaceColumns); + } + + /** + * Same as {@link #getTotalPoints(Competitor, RaceColumn, Iterable, TimePoint)}, only that the set of discarded race columns can + * be specified which is useful when total points are to be computed for more than one column for the same + * competitor because then the calculation of discards (which requires looking at all columns) only needs to be done + * once and not again for each column (which would lead to quadratic effort). + * + * @param discardedRaceColumns + * expected to be the result of what we would get if we called {@link #getResultDiscardingRule()}. + * {@link ResultDiscardingRule#getDiscardedRaceColumns(Competitor, Leaderboard, Iterable, TimePoint) + * getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint)}. + */ + @Override + public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint, Set discardedRaceColumns) { + Double result; + if (isDiscarded(competitor, raceColumn, timePoint, discardedRaceColumns)) { + result = 0.0; + } else { + final Double netPoints = getNetPoints(competitor, raceColumn, timePoint); + if (netPoints == null) { + result = null; + } else { + result = raceColumn.getFactor() * netPoints; + } + } + return result; + } + + @Override + public Double getTotalPoints(Competitor competitor, TimePoint timePoint) { + return getTotalPoints(competitor, getRaceColumns(), timePoint); + } + + @Override + public Double getTotalPoints(Competitor competitor, final Iterable raceColumnsToConsider, + TimePoint timePoint) { + // when a column with isStartsWithZeroScore() is found, only reset score if the competitor scored in any race from there on + boolean needToResetScoreUponNextNonEmptyEntry = false; + double result = getCarriedPoints(competitor); + final Set discardedRaceColumns = getResultDiscardingRule() + .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); + for (RaceColumn raceColumn : raceColumnsToConsider) { + if (raceColumn.isStartsWithZeroScore()) { + needToResetScoreUponNextNonEmptyEntry = true; + } + if (getScoringScheme().isValidInTotalScore(this, raceColumn, competitor, timePoint)) { + final Double totalPoints = getTotalPoints(competitor, raceColumn, timePoint, discardedRaceColumns); + if (totalPoints != null) { + if (needToResetScoreUponNextNonEmptyEntry) { + result = 0; + needToResetScoreUponNextNonEmptyEntry = false; + } + result += totalPoints; + } + } + } + return result; + } + + /** + * All competitors with non-null net points are added to the result which is then sorted by net points in ascending + * order. The fleet, if ordered, is the primary ordering criterion, followed by the net points. + */ + @Override + public List getCompetitorsFromBestToWorst(final RaceColumn raceColumn, TimePoint timePoint) throws NoWindException { + final Map> netPointsAndFleet = new HashMap>(); + for (Competitor competitor : getCompetitors()) { + Double netPoints = getNetPoints(competitor, raceColumn, timePoint); + if (netPoints != null) { + netPointsAndFleet.put(competitor, new com.sap.sse.common.Util.Pair(netPoints, raceColumn.getFleetOfCompetitor(competitor))); + } + } + List result = new ArrayList(netPointsAndFleet.keySet()); + Collections.sort(result, new Comparator() { + @Override + public int compare(Competitor o1, Competitor o2) { + int comparisonResult; + if (o1 == o2) { + comparisonResult = 0; + } else { + if (raceColumn.hasSplitFleets() && !raceColumn.hasSplitFleetContiguousScoring()) { + // only check fleets if there are more than one and the column is not to be contiguously scored even in case + // of split fleets + final Fleet o1Fleet = netPointsAndFleet.get(o1).getB(); + final Fleet o2Fleet = netPointsAndFleet.get(o2).getB(); + if (o1Fleet == null) { + if (o2Fleet == null) { + comparisonResult = 0; + } else { + comparisonResult = 1; // o1 ranks "worse" because it doesn't have a fleet set while o2 + // has + } + } else { + if (o2Fleet == null) { + comparisonResult = -1; // o1 ranks "better" because it has a fleet set while o2 hasn't + } else { + comparisonResult = o1Fleet.compareTo(o2Fleet); + } + } + } else { + // either there are no split fleets or the split isn't relevant for scoring as for ordered fleets + // the scoring runs contiguously from top to bottom + comparisonResult = 0; + } + if (comparisonResult == 0) { + comparisonResult = getScoringScheme().getScoreComparator(/* nullScoresAreBetter */ false).compare( + netPointsAndFleet.get(o1).getA(), netPointsAndFleet.get(o2).getA()); + } + } + return comparisonResult; + } + }); + return result; + } + + @Override + public List getCompetitorsFromBestToWorst(TimePoint timePoint) { + return getCompetitorsFromBestToWorst(getRaceColumns(), timePoint); + } + + private List getCompetitorsFromBestToWorst(Iterable raceColumnsToConsider, TimePoint timePoint) { + List result = new ArrayList(); + for (Competitor competitor : getCompetitors()) { + result.add(competitor); + } + Collections.sort(result, getTotalRankComparator(raceColumnsToConsider, timePoint)); + return result; + } + + @Override + public int getTotalRankOfCompetitor(Competitor competitor, TimePoint timePoint) throws NoWindException { + List competitorsFromBestToWorst = getCompetitorsFromBestToWorst(timePoint); + return competitorsFromBestToWorst.indexOf(competitor) + 1; + } + + protected Comparator getTotalRankComparator(Iterable raceColumnsToConsider, TimePoint timePoint) { + return new LeaderboardTotalRankComparator(this, timePoint, getScoringScheme(), /* nullScoresAreBetter */ false, raceColumnsToConsider); + } + + @Override + public RaceColumn getRaceColumnByName(String columnName) { + RaceColumn result = null; + for (RaceColumn r : getRaceColumns()) { + if (r.getName().equals(columnName)) { + result = r; + break; + } + } + return result; + } + + @Override + public Competitor getCompetitorByName(String competitorName) { + for (Competitor competitor : getAllCompetitors()) { + if (competitor.getName().equals(competitorName)) { + return competitor; + } + } + return null; + } + + @Override + public boolean countRaceForComparisonWithDiscardingThresholds(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { + TrackedRace trackedRaceForCompetitorInColumn; + return getScoringScheme().isValidInTotalScore(this, raceColumn, competitor, timePoint) && + (getScoreCorrection().isScoreCorrected(competitor, raceColumn, timePoint) || + ((trackedRaceForCompetitorInColumn=raceColumn.getTrackedRace(competitor)) != null && + trackedRaceForCompetitorInColumn.hasStarted(timePoint) && + trackedRaceForCompetitorInColumn.getRank(competitor, timePoint) != 0)); + } + + @Override + public void addRaceColumnListener(RaceColumnListener listener) { + getRaceColumnListeners().addRaceColumnListener(listener); + } + + @Override + public void removeRaceColumnListener(RaceColumnListener listener) { + getRaceColumnListeners().removeRaceColumnListener(listener); + } + + @Override + public Entry getEntry(final Competitor competitor, final RaceColumn race, final TimePoint timePoint) throws NoWindException { + final Set discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); + return getEntry(competitor, race, timePoint, discardedRaceColumns); + } + + @Override + public Entry getEntry(final Competitor competitor, final RaceColumn race, final TimePoint timePoint, + Set discardedRaceColumns) throws NoWindException { + Callable trackedRankProvider = new Callable() { + public Integer call() throws NoWindException { + return getTrackedRank(competitor, race, timePoint); + } + }; + final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, race, + timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); + boolean discarded = isDiscarded(competitor, race, timePoint, discardedRaceColumns); + final Double correctedScore = correctedResults.getCorrectedScore(); + return new EntryImpl(trackedRankProvider, correctedScore, new Callable() { + @Override + public Double call() { + return correctedResults.getUncorrectedScore(); + } + }, correctedResults.isCorrected(), discarded ? DOUBLE_0 : correctedScore == null ? null + : Double.valueOf(correctedScore * race.getFactor()), correctedResults.getMaxPointsReason(), discarded, + race.getFleetOfCompetitor(competitor)); + } + + @Override + public Map> getRankedCompetitorsFromBestToWorstAfterEachRaceColumn(TimePoint timePoint) throws NoWindException { + Map> result = new LinkedHashMap<>(); + List raceColumnsToConsider = new ArrayList<>(); + for (RaceColumn raceColumn : getRaceColumns()) { + raceColumnsToConsider.add(raceColumn); + result.put(raceColumn, getCompetitorsFromBestToWorst(raceColumnsToConsider, timePoint)); + } + return result; + } + + @Override + public Map> getTotalPointsSumAfterRaceColumn(final TimePoint timePoint) + throws NoWindException { + final Map> result = new LinkedHashMap<>(); + List raceColumnsToConsider = new ArrayList<>(); + Map>> futures = new HashMap<>(); + for (final RaceColumn raceColumn : getRaceColumns()) { + raceColumnsToConsider.add(raceColumn); + final Iterable finalRaceColumnsToConsider = new ArrayList<>(raceColumnsToConsider); + futures.put(raceColumn, executor.submit(new Callable>() { + @Override + public Map call() { + Map totalPointsSumPerCompetitorInColumn = new HashMap<>(); + for (Competitor competitor : getCompetitors()) { + totalPointsSumPerCompetitorInColumn.put(competitor, getTotalPoints(competitor, finalRaceColumnsToConsider, timePoint)); + } + synchronized (result) { + return totalPointsSumPerCompetitorInColumn; + } + } + })); + } + for (RaceColumn raceColumn : getRaceColumns()) { + try { + result.put(raceColumn, futures.get(raceColumn).get()); + } catch (InterruptedException | ExecutionException e) { + if (e.getCause() instanceof NoWindError) { + throw ((NoWindError) e.getCause()).getCause(); + } else { + throw new RuntimeException(e); // no caught exceptions occur in the futures executed + } + } + } + return result; + } + + @Override + public Map, Entry> getContent(final TimePoint timePoint) throws NoWindException { + Map, Entry> result = new HashMap, Entry>(); + Map> discardedRaces = new HashMap>(); + for (final RaceColumn raceColumn : getRaceColumns()) { + for (final Competitor competitor : getCompetitors()) { + Callable trackedRankProvider = new Callable() { + @Override + public Integer call() throws Exception { + return getTrackedRank(competitor, raceColumn, timePoint); + } + }; + final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, raceColumn, + timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); + Set discardedRacesForCompetitor = discardedRaces.get(competitor); + if (discardedRacesForCompetitor == null) { + discardedRacesForCompetitor = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); + discardedRaces.put(competitor, discardedRacesForCompetitor); + } + boolean discarded = discardedRacesForCompetitor.contains(raceColumn); + final Double correctedScore = correctedResults.getCorrectedScore(); + Entry entry = new EntryImpl(trackedRankProvider, correctedScore, + new Callable() { @Override public Double call() { return correctedResults.getUncorrectedScore(); } }, + correctedResults.isCorrected(), + discarded ? DOUBLE_0 : (correctedScore==null?null: + Double.valueOf((correctedScore * raceColumn.getFactor()))), correctedResults.getMaxPointsReason(), + discarded, raceColumn.getFleetOfCompetitor(competitor)); + result.put(new com.sap.sse.common.Util.Pair(competitor, raceColumn), entry); + } + } + return result; + } + + @Override + public void trackedRaceLinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { + getRaceColumnListeners().notifyListenersAboutTrackedRaceLinked(raceColumn, fleet, trackedRace); + } + + @Override + public void trackedRaceUnlinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { + getRaceColumnListeners().notifyListenersAboutTrackedRaceUnlinked(raceColumn, fleet, trackedRace); + // It's generally possible that a leaderboard links to the same tracked race in multiple columns / fleets; + // only if it no longer references the trackedRace currently unlinked from one column/fleet, also unlink + // all cache invalidation listeners for said trackedRace + if (!Util.contains(getTrackedRaces(), trackedRace)) { + synchronized (cacheInvalidationListeners) { + for (Iterator cacheInvalidationListenerIter=cacheInvalidationListeners.iterator(); + cacheInvalidationListenerIter.hasNext(); ) { + CacheInvalidationListener cacheInvalidationListener = cacheInvalidationListenerIter.next(); + if (cacheInvalidationListener.getTrackedRace() == trackedRace) { + cacheInvalidationListener.removeFromTrackedRace(); + cacheInvalidationListenerIter.remove(); + } + } + } + } + } + + @Override + public void isMedalRaceChanged(RaceColumn raceColumn, boolean newIsMedalRace) { + getRaceColumnListeners().notifyListenersAboutIsMedalRaceChanged(raceColumn, newIsMedalRace); + } + + @Override + public void isStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore) { + getRaceColumnListeners().notifyListenersAboutIsStartsWithZeroScoreChanged(raceColumn, newIsStartsWithZeroScore); + } + + @Override + public void isFirstColumnIsNonDiscardableCarryForwardChanged(RaceColumn raceColumn, boolean firstColumnIsNonDiscardableCarryForward) { + getRaceColumnListeners().notifyListenersAboutIsFirstColumnIsNonDiscardableCarryForwardChanged(raceColumn, firstColumnIsNonDiscardableCarryForward); + } + + @Override + public void hasSplitFleetContiguousScoringChanged(RaceColumn raceColumn, boolean hasSplitFleetContiguousScoring) { + getRaceColumnListeners().notifyListenersAboutHasSplitFleetContiguousScoringChanged(raceColumn, hasSplitFleetContiguousScoring); + } + + @Override + public void raceColumnMoved(RaceColumn raceColumn, int newIndex) { + getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(raceColumn, newIndex); + } + + @Override + public void factorChanged(RaceColumn raceColumn, Double oldFactor, Double newFactor) { + getRaceColumnListeners().notifyListenersAboutFactorChanged(raceColumn, oldFactor, newFactor); + } + + /** + * A leaderboard will only accept the addition of a race column if the column's name is unique across the leaderboard. + */ + @Override + public boolean canAddRaceColumnToContainer(RaceColumn newRaceColumn) { + boolean result = true; + for (RaceColumn raceColumn : getRaceColumns()) { + if (raceColumn.getName().equals(newRaceColumn.getName())) { + result = false; + break; + } + } + return result; + } + + @Override + public void raceColumnAddedToContainer(RaceColumn raceColumn) { + getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(raceColumn); + } + + @Override + public void raceColumnRemovedFromContainer(RaceColumn raceColumn) { + getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(raceColumn); + } + + @Override + public void competitorDisplayNameChanged(Competitor competitor, String oldDisplayName, String displayName) { + getRaceColumnListeners().notifyListenersAboutCompetitorDisplayNameChanged(competitor, oldDisplayName, displayName); + } + + @Override + public void resultDiscardingRuleChanged(ResultDiscardingRule oldDiscardingRule, ResultDiscardingRule newDiscardingRule) { + getRaceColumnListeners().notifyListenersAboutResultDiscardingRuleChanged(oldDiscardingRule, newDiscardingRule); + } + + @Override + public boolean isTransient() { + return false; + } + + /** + * Finds out the time point when any of the {@link Leaderboard#getTrackedRaces() tracked races currently attached to + * the leaderboard} and the {@link Leaderboard#getScoreCorrection() score corrections} have last been + * modified. If no tracked race is attached and no time-stamped score corrections have been applied to the leaderboard, + * null is returned. The time point computed this way is a good choice for normalizing queries for later time + * points in an attempt to achieve more cache hits.

+ * + * Note, however, that the result does not tell about structural changes to the leaderboard and therefore cannot be used + * to determine the need for cache invalidation. For example, if a column is added to a leaderboard after the time point + * returned by this method but that column's attached tracked race has finished before the time point returned by this method, + * the result of this method won't change. Still, the contents of the leaderboard will change by a change in column structure. + * A different means to determine the possibility of changes that happened to this leaderboard must be used for cache + * management. Such a facility has to listen for score correction changes, tracked races being attached or detached and + * the column structure changing. + * + * @see TrackedRace#getTimePointOfNewestEvent() + * @see SettableScoreCorrection#getTimePointOfLastCorrectionsValidity() + */ + @Override + public TimePoint getTimePointOfLatestModification() { + TimePoint result = null; + for (TrackedRace trackedRace : getTrackedRaces()) { + if (result == null || (trackedRace.getTimePointOfNewestEvent() != null && trackedRace.getTimePointOfNewestEvent().after(result))) { + result = trackedRace.getTimePointOfNewestEvent(); + } + } + TimePoint timePointOfLastScoreCorrection = getScoreCorrection().getTimePointOfLastCorrectionsValidity(); + if (timePointOfLastScoreCorrection != null && (result == null || timePointOfLastScoreCorrection.after(result))) { + result = timePointOfLastScoreCorrection; + } + return result; + } + + @Override + public com.sap.sse.common.Util.Pair getMaximumSpeedOverGround(Competitor competitor, TimePoint timePoint) { + com.sap.sse.common.Util.Pair result = null; + // TODO should we ensure that competitor participated in all race columns? + for (TrackedRace trackedRace : getTrackedRaces()) { + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { + NavigableSet markPassings = trackedRace.getMarkPassings(competitor); + if (!markPassings.isEmpty()) { + TimePoint from = markPassings.first().getTimePoint(); + TimePoint to; + if (timePoint.after(markPassings.last().getTimePoint()) && + markPassings.last().getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint()) { + // stop counting when competitor finished the race + to = markPassings.last().getTimePoint(); + } else { + to = timePoint; + } + com.sap.sse.common.Util.Pair maxSpeed = trackedRace.getTrack(competitor).getMaximumSpeedOverGround(from, to); + if (result == null || result.getB() == null || + (maxSpeed != null && maxSpeed.getB() != null && maxSpeed.getB().compareTo(result.getB()) > 0)) { + result = maxSpeed; + } + } + } + } + return result; + } + + @Override + public Speed getAverageSpeedOverGround(Competitor competitor, TimePoint timePoint) { + Speed result = null; + for (TrackedRace trackedRace : getTrackedRaces()) { + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { + NavigableSet markPassings = trackedRace.getMarkPassings(competitor); + if (!markPassings.isEmpty()) { + TimePoint from = markPassings.first().getTimePoint(); + TimePoint to; + if (timePoint.after(markPassings.last().getTimePoint()) && + markPassings.last().getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint()) { + // stop counting when competitor finished the race + to = markPassings.last().getTimePoint(); + } else { + if (markPassings.last().getWaypoint() != trackedRace.getRace().getCourse().getLastWaypoint() && + timePoint.after(markPassings.last().getTimePoint())) { + result = null; + break; + } + to = timePoint; + } + Distance distanceTraveled = trackedRace.getDistanceTraveled(competitor, timePoint); + if (distanceTraveled != null) { + result = distanceTraveled.inTime(to.asMillis()-from.asMillis()); + } + } + } + } + return result; + } + + @Override + 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()) { + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { + trackedRace.getRace().getCourse().lockForRead(); + try { + for (Leg leg : trackedRace.getRace().getCourse().getLegs()) { + TrackedLegOfCompetitor trackedLegOfCompetitor = trackedRace.getTrackedLeg(competitor, leg); + if (trackedLegOfCompetitor.hasStartedLeg(timePoint)) { + // find out leg type at the time the competitor started the leg + try { + 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) { + Duration timeSpentOnDownwind = trackedLegOfCompetitor.getTime(timePoint); + if (timeSpentOnDownwind != null) { + if (result == null) { + result = timeSpentOnDownwind; + } else { + 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. + result = null; + break outerLoop; + } + } + } catch (NoWindException nwe) { + // without wind there is no leg type and hence there is no reasonable value for this: + result = null; + break outerLoop; + } + } + } + } finally { + trackedRace.getRace().getCourse().unlockAfterRead(); + } + } + } + return result; + } + + @Override + 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); + if (!markPassings.isEmpty()) { + TimePoint from = trackedRace.getStartOfRace(); // start counting at race start, not when the competitor passed the line + if (from != null && !timePoint.before(from)) { // but only if the race started after timePoint + TimePoint to; + if (timePoint.after(markPassings.last().getTimePoint()) + && markPassings.last().getWaypoint() == trackedRace.getRace().getCourse() + .getLastWaypoint()) { + // stop counting when competitor finished the race + to = markPassings.last().getTimePoint(); + } else { + if (trackedRace.getEndOfTracking() != null + && timePoint.after(trackedRace.getEndOfTracking())) { + result = null; // race not finished until end of tracking; no reasonable value can be + // computed for competitor + break; + } else { + to = timePoint; + } + } + Duration timeSpent = from.until(to); + if (result == null) { + result = timeSpent; + } else { + result=result.plus(timeSpent); + } + } + } + } + } + return result; + } + + @Override + public Distance getTotalDistanceTraveled(Competitor competitor, TimePoint timePoint) { + Distance result = null; + for (TrackedRace trackedRace : getTrackedRaces()) { + TimePoint startOfRace; + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor) && + (startOfRace=trackedRace.getStartOfRace()) != null && + !startOfRace.after(timePoint)) { + Distance distanceSailedInRace = trackedRace.getDistanceTraveled(competitor, timePoint); + if (distanceSailedInRace != null) { + if (result == null) { + result = distanceSailedInRace; + } else { + result = result.add(distanceSailedInRace); + } + } else { + // if competitor has not finished one single race in the whole + // series then we can not return a meaningful value for all + // all races + return null; + } + } + } + return result; + } + + protected RaceColumnListeners getRaceColumnListeners() { + return raceColumnListeners; + } + + @Override + public Iterable getCompetitors() { + final Iterable result; + // mostly the set of suppressed competitors is empty; in this case, avoid having to loop over the + // potentially large set of competitors + if (Util.isEmpty(getSuppressedCompetitors())) { + result = getAllCompetitors(); + } else { + final Iterable allCompetitors = getAllCompetitors(); + final Set suppressed = new HashSet<>(); + Util.addAll(getSuppressedCompetitors(), suppressed); + result = getCompetitorIterableSkippingSuppressed(allCompetitors, suppressed); + } + return result; + } + + @Override + public Iterable getCompetitors(RaceColumn raceColumn, Fleet fleet) { + return getCompetitorIterableSkippingSuppressed(getAllCompetitors(raceColumn, fleet), getSuppressedCompetitors()); + } + + /** + * return an iterable with a smart iterator that filters out the suppressed elements on demand + */ + protected Iterable getCompetitorIterableSkippingSuppressed(final Iterable allCompetitors, + final Iterable suppressed) { + final Iterable result; + result = new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + private final Iterator allIter = allCompetitors.iterator(); + private Competitor next = advance(); + + private Competitor advance() { + next = null; + while (allIter.hasNext() && next == null) { + next = allIter.next(); + if (Util.contains(suppressed, next)) { + next = null; + } + } + return next; + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public Competitor next() { + if (next == null) { + throw new NoSuchElementException(); + } + final Competitor result = next; + advance(); + return result; + } + }; + } + }; + return result; + } + + @Override + public Iterable getSuppressedCompetitors() { + LockUtil.lockForRead(suppressedCompetitorsLock); + try { + return new HashSet(suppressedCompetitors); + } finally { + LockUtil.unlockAfterRead(suppressedCompetitorsLock); + } + } + + @Override + public void setSuppressed(Competitor competitor, boolean suppressed) { + LockUtil.lockForWrite(suppressedCompetitorsLock); + try { + if (suppressed) { + suppressedCompetitors.add(competitor); + } else { + suppressedCompetitors.remove(competitor); + } + } finally { + LockUtil.unlockAfterWrite(suppressedCompetitorsLock); + } + getScoreCorrection().notifyListenersAboutIsSuppressedChange(competitor, suppressed); + } + + @Override + public TimePoint getNowMinusDelay() { + final TimePoint now = MillisecondsTimePoint.now(); + final Long delayToLiveInMillis = getDelayToLiveInMillis(); + TimePoint timePoint = delayToLiveInMillis == null ? now : now.minus(delayToLiveInMillis); + return timePoint; + } + + @Override + public void raceLogEventAdded(RaceColumn raceColumn, RaceLogIdentifier raceLogIdentifier, RaceLogEvent event) { + getRaceColumnListeners().notifyListenersAboutRaceLogEventAdded(raceColumn, raceLogIdentifier, event); + if (event instanceof InvalidatesLeaderboardCache) { + // make sure to invalidate the cache as this event indicates that + // it changes values the cache could still hold + if (leaderboardDTOCache != null) { + leaderboardDTOCache.invalidate(this); + } + } + } + + @Override + public LeaderboardDTO computeDTO(final TimePoint timePoint, + final Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, + final boolean waitForLatestAnalyses, TrackedRegattaRegistry trackedRegattaRegistry, final DomainFactory baseDomainFactory, + final boolean fillNetPointsUncorrected) + throws NoWindException { + long startOfRequestHandling = System.currentTimeMillis(); + final LeaderboardDTOCalculationReuseCache cache = new LeaderboardDTOCalculationReuseCache(timePoint); + final LeaderboardDTO result = new LeaderboardDTO(this.getScoreCorrection().getTimePointOfLastCorrectionsValidity() == null ? null + : this.getScoreCorrection().getTimePointOfLastCorrectionsValidity().asDate(), + this.getScoreCorrection() == null ? null : this.getScoreCorrection().getComment(), + this.getScoringScheme() == null ? null : this.getScoringScheme().getType(), this + .getScoringScheme().isHigherBetter(), new UUIDGenerator(), addOverallDetails); + result.type = getLeaderboardType(); + result.competitors = new ArrayList(); + result.name = this.getName(); + result.competitorDisplayNames = new HashMap(); + for (Competitor suppressedCompetitor : this.getSuppressedCompetitors()) { + result.setSuppressed(baseDomainFactory.convertToCompetitorDTO(suppressedCompetitor), true); + } + // 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()) { + boolean isMetaLeaderboardColumn = raceColumn instanceof MetaLeaderboardColumn; + RaceColumnDTO raceColumnDTO = result.createEmptyRaceColumn(raceColumn.getName(), raceColumn.isMedalRace(), + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, + isMetaLeaderboardColumn); + if (isMetaLeaderboardColumn && raceColumnDTO instanceof MetaLeaderboardRaceColumnDTO) { + calculateRacesMetadata((MetaLeaderboardColumn) raceColumn, (MetaLeaderboardRaceColumnDTO) raceColumnDTO, + trackedRegattaRegistry, baseDomainFactory); + } + 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(), + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, + fleetDTO, raceColumn.isMedalRace(), raceIdentifier, race, isMetaLeaderboardColumn); + } + FutureTask> task = new FutureTask>( + () -> 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(); + result.hasCarriedPoints = this.hasCarriedPoints(); + if (this.getResultDiscardingRule() instanceof ThresholdBasedResultDiscardingRule) { + result.discardThresholds = ((ThresholdBasedResultDiscardingRule) this.getResultDiscardingRule()) + .getDiscardIndexResultsStartingWithHowManyRaces(); + } else { + result.discardThresholds = null; + } + // Computing the competitor leg ranks is expensive, especially in live mode, in case new events keep + // invalidating the ranks cache in TrackedLegImpl. The problem then is that the sorting based on wind data is repeated for + // each competitor, leading to square effort. We therefore need to compute the leg ranks for those races where leg + // 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 + if (namesOfRaceColumnsForWhichToLoadLegDetails != null + && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn.getName())) { + for (Fleet fleet : raceColumn.getFleets()) { + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + trackedRace.getRace().getCourse().lockForRead(); + try { + for (TrackedLeg trackedLeg : trackedRace.getTrackedLegs()) { + legRanksCache.put(trackedLeg.getLeg(), trackedLeg.getRanks(timePoint, cache)); + } + } finally { + trackedRace.getRace().getCourse().unlockAfterRead(); + } + } + } + } + } + for (final Competitor competitor : this.getCompetitorsFromBestToWorst(timePoint)) { + CompetitorDTO competitorDTO = baseDomainFactory.convertToCompetitorDTO(competitor); + LeaderboardRowDTO row = new LeaderboardRowDTO(); + row.competitor = competitorDTO; + row.fieldsByRaceColumnName = new HashMap(); + row.carriedPoints = this.hasCarriedPoints(competitor) ? this.getCarriedPoints(competitor) : null; + row.totalPoints = this.getTotalPoints(competitor, timePoint); + if (addOverallDetails) { + addOverallDetailsToRow(timePoint, competitor, row); + } + result.competitors.add(competitorDTO); + Map> futuresForColumnName = new HashMap>(); + final Set discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); + for (final RaceColumn raceColumn : this.getRaceColumns()) { + RunnableFuture future = new FutureTask(() -> { + Entry entry = AbstractSimpleLeaderboardImpl.this.getEntry(competitor, raceColumn, timePoint, discardedRaceColumns); + return getLeaderboardEntryDTO(entry, raceColumn, competitor, timePoint, + namesOfRaceColumnsForWhichToLoadLegDetails != null + && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn + .getName()), waitForLatestAnalyses, legRanksCache, baseDomainFactory, + fillNetPointsUncorrected, cache); + }); + executor.execute(future); + futuresForColumnName.put(raceColumn.getName(), future); + } + for (Map.Entry> raceColumnNameAndFuture : futuresForColumnName.entrySet()) { + try { + row.fieldsByRaceColumnName.put(raceColumnNameAndFuture.getKey(), raceColumnNameAndFuture.getValue().get()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + // 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 leaderboard entry for competitor " + + competitor.getName() + " in race column " + raceColumnNameAndFuture.getKey() + + ". Leaving empty.", e); + } + } + result.rows.put(competitorDTO, row); + String displayName = this.getDisplayName(competitor); + if (displayName != null) { + result.competitorDisplayNames.put(competitorDTO, displayName); + } + } + logger.info("computeLeaderboardByName(" + this.getName() + ", " + timePoint + ", " + + namesOfRaceColumnsForWhichToLoadLegDetails + ", addOverallDetails=" + addOverallDetails + ") took " + + (System.currentTimeMillis() - startOfRequestHandling) + "ms"); + return result; + } + + private void calculateRacesMetadata(MetaLeaderboardColumn metaLeaderboardColumn, MetaLeaderboardRaceColumnDTO columnDTO, + TrackedRegattaRegistry trackedRegattaRegistry, final DomainFactory baseDomainFactory) { + for (final RaceColumn raceColumn : metaLeaderboardColumn.getLeaderboard().getRaceColumns()) { + for (Fleet fleet : raceColumn.getFleets()) { + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + String regattaName = trackedRace.getTrackedRegatta().getRegatta().getName(); + String raceName = trackedRace.getRace().getName(); + RegattaAndRaceIdentifier raceIdentifier = new RegattaNameAndRaceName(regattaName, raceName); + columnDTO.addRace(new BasicRaceDTO(raceIdentifier, baseDomainFactory.createTrackedRaceDTO(trackedRace))); + } + } + } + } + + private void addOverallDetailsToRow(final TimePoint timePoint, + final Competitor competitor, LeaderboardRowDTO row) throws NoWindException { + final com.sap.sse.common.Util.Pair maximumSpeedOverGround = this.getMaximumSpeedOverGround(competitor, timePoint); + if (maximumSpeedOverGround != null && maximumSpeedOverGround.getB() != null) { + row.maximumSpeedOverGroundInKnots = maximumSpeedOverGround.getB().getKnots(); + row.whenMaximumSpeedOverGroundWasAchieved = maximumSpeedOverGround.getA().getTimePoint().asDate(); + } + 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(); + } + + /** + * @param waitForLatestAnalyses + * if false, this method is allowed to read the maneuver analysis results from a cache that + * may not reflect all data already received; otherwise, the method will always block for the latest + * cache updates to have happened before returning. + * @param fillNetPointsUncorrected + * tells if {@link LeaderboardEntryDTO#netPointsUncorrected} shall be filled; filling it is rather + * expensive, especially when compared to simply retrieving a score correction, and particularly if in a + * larger fleet a number of competitors haven't properly finished the race. This should only be used for + * leaderboard editing where a user needs to see what the uncorrected score was that would be used when + * the correction was removed. + */ + private LeaderboardEntryDTO getLeaderboardEntryDTO(Entry entry, RaceColumn raceColumn, Competitor competitor, + TimePoint timePoint, boolean addLegDetails, boolean waitForLatestAnalyses, + Map> legRanksCache, DomainFactory baseDomainFactory, + boolean fillNetPointsUncorrected, WindLegTypeAndLegBearingCache cache) { + LeaderboardEntryDTO entryDTO = new LeaderboardEntryDTO(); + TrackedRace trackedRace = raceColumn.getTrackedRace(competitor); + entryDTO.race = trackedRace == null ? null : trackedRace.getRaceIdentifier(); + entryDTO.netPoints = entry.getNetPoints(); + if (fillNetPointsUncorrected) { + entryDTO.netPointsUncorrected = entry.getNetPointsUncorrected(); + } + entryDTO.netPointsCorrected = entry.isNetPointsCorrected(); + entryDTO.totalPoints = entry.getTotalPoints(); + entryDTO.reasonForMaxPoints = entry.getMaxPointsReason(); + entryDTO.discarded = entry.isDiscarded(); + final GPSFixTrack track = trackedRace == null ? null : trackedRace.getTrack(competitor); + if (trackedRace != null) { + Date timePointOfLastPositionFixAtOrBeforeQueryTimePoint = getTimePointOfLastFixAtOrBefore(competitor, trackedRace, timePoint); + if (track != null) { + entryDTO.averageSamplingInterval = track.getAverageIntervalBetweenRawFixes(); + } + if (timePointOfLastPositionFixAtOrBeforeQueryTimePoint != null) { + long timeDifferenceInMs = timePoint.asMillis() - timePointOfLastPositionFixAtOrBeforeQueryTimePoint.getTime(); + entryDTO.timeSinceLastPositionFixInSeconds = timeDifferenceInMs == 0 ? 0.0 : timeDifferenceInMs / 1000.0; + } else { + entryDTO.timeSinceLastPositionFixInSeconds = null; + } + } + if (addLegDetails && trackedRace != null) { + try { + final RankingInfo rankingInfo = trackedRace.getRankingMetric().getRankingInfo(timePoint, cache); + RaceDetails raceDetails = getRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, + legRanksCache, rankingInfo, cache); + entryDTO.legDetails = raceDetails.getLegDetails(); + entryDTO.windwardDistanceToCompetitorFarthestAheadInMeters = raceDetails.getWindwardDistanceToCompetitorFarthestAhead() == null ? null + : raceDetails.getWindwardDistanceToCompetitorFarthestAhead().getMeters(); + entryDTO.gapToLeaderInOwnTime = trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache); + entryDTO.averageAbsoluteCrossTrackErrorInMeters = raceDetails.getAverageAbsoluteCrossTrackError() == null ? null + : raceDetails.getAverageAbsoluteCrossTrackError().getMeters(); + entryDTO.averageSignedCrossTrackErrorInMeters = raceDetails.getAverageSignedCrossTrackError() == null ? null + : raceDetails.getAverageSignedCrossTrackError().getMeters(); + entryDTO.calculatedTime = raceDetails.getCorrectedTime(); + entryDTO.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = raceDetails.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead(); + entryDTO.gapToLeaderInOwnTime = raceDetails.getGapToLeaderInOwnTime(); + final TimePoint startOfRace = trackedRace.getStartOfRace(); + if (startOfRace != null) { + Waypoint startWaypoint = trackedRace.getRace().getCourse().getFirstWaypoint(); + NavigableSet competitorMarkPassings = trackedRace.getMarkPassings(competitor); + trackedRace.lockForRead(competitorMarkPassings); + try { + if (!competitorMarkPassings.isEmpty()) { + final MarkPassing firstMarkPassing = competitorMarkPassings.iterator().next(); + if (firstMarkPassing.getWaypoint() == startWaypoint) { + Distance distanceToStartLineFiveSecondsBeforeStartOfRace = trackedRace.getDistanceToStartLine(competitor, /*milliseconds before start*/ 5000); + entryDTO.distanceToStartLineFiveSecondsBeforeStartInMeters = distanceToStartLineFiveSecondsBeforeStartOfRace == null ? null + : distanceToStartLineFiveSecondsBeforeStartOfRace.getMeters(); + Speed speedFiveSecondsBeforeStartOfRace = trackedRace.getSpeed(competitor, /*milliseconds before start*/ 5000); + entryDTO.speedOverGroundFiveSecondsBeforeStartInKnots = speedFiveSecondsBeforeStartOfRace == null ? null + : speedFiveSecondsBeforeStartOfRace.getKnots(); + Distance distanceToStartLineAtStartOfRace = trackedRace.getDistanceToStartLine( + competitor, startOfRace); + entryDTO.distanceToStartLineAtStartOfRaceInMeters = distanceToStartLineAtStartOfRace == null ? null + : distanceToStartLineAtStartOfRace.getMeters(); + Speed speedAtStartTime = track == null ? null : track.getEstimatedSpeed(startOfRace); + entryDTO.speedOverGroundAtStartOfRaceInKnots = speedAtStartTime == null ? null + : speedAtStartTime.getKnots(); + TimePoint competitorStartTime = firstMarkPassing.getTimePoint(); + entryDTO.timeBetweenRaceStartAndCompetitorStartInSeconds = startOfRace.until(competitorStartTime).asSeconds(); + Speed competitorSpeedWhenPassingStart = track == null ? null : track + .getEstimatedSpeed(competitorStartTime); + entryDTO.speedOverGroundAtPassingStartWaypointInKnots = competitorSpeedWhenPassingStart == null ? null + : competitorSpeedWhenPassingStart.getKnots(); + try { + entryDTO.startTack = trackedRace.getTack(competitor, competitorStartTime); + } catch (NoWindException nwe) { + entryDTO.startTack = null; // leave empty in case no wind information is available + } + Distance distanceFromStarboardSideOfStartLineWhenPassingStart = trackedRace + .getDistanceFromStarboardSideOfStartLineWhenPassingStart(competitor); + entryDTO.distanceToStarboardSideOfStartLineInMeters = distanceFromStarboardSideOfStartLineWhenPassingStart == null ? null + : distanceFromStarboardSideOfStartLineWhenPassingStart.getMeters(); + } + } + } finally { + trackedRace.unlockAfterRead(competitorMarkPassings); + } + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e); // the future used to calculate the leg details was interrupted; escalate as runtime exception + } + } + final Fleet fleet = entry.getFleet(); + entryDTO.fleet = fleet == null ? null : baseDomainFactory.convertToFleetDTO(fleet); + return entryDTO; + } + + /** + * Determines the time point of the last raw fix (with outliers not removed) for competitor in + * trackedRace. If the competitor's track is null or empty, null is returned. + * @param trackedRace must not be null + * @param atOrBefore find the last fix at or before the time point specified + */ + private Date getTimePointOfLastFixAtOrBefore(Competitor competitor, TrackedRace trackedRace, TimePoint atOrBefore) { + assert trackedRace != null; + final Date timePointOfLastPositionFix; + GPSFixTrack track = trackedRace.getTrack(competitor); + if (track == null) { + timePointOfLastPositionFix = null; + } else { + GPSFixMoving lastFix = track.getLastFixAtOrBefore(atOrBefore); + if (lastFix == null) { + timePointOfLastPositionFix = null; + } else { + timePointOfLastPositionFix = lastFix.getTimePoint().asDate(); + } + } + return timePointOfLastPositionFix; + } + + private static class RaceDetails { + private final List legDetails; + private final Distance windwardDistanceToCompetitorFarthestAhead; + private final Distance averageAbsoluteCrossTrackError; + private final Distance averageSignedCrossTrackError; + private final Duration gapToLeaderInOwnTime; + private final Duration correctedTime; + private final Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; + + public RaceDetails(List legDetails, Distance windwardDistanceToCompetitorFarthestAhead, + Distance averageAbsoluteCrossTrackError, Distance averageSignedCrossTrackError, + Duration gapToLeaderInOwnTime, Duration correctedTime, + Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead) { + super(); + this.legDetails = legDetails; + this.windwardDistanceToCompetitorFarthestAhead = windwardDistanceToCompetitorFarthestAhead; + this.averageAbsoluteCrossTrackError = averageAbsoluteCrossTrackError; + this.averageSignedCrossTrackError = averageSignedCrossTrackError; + this.gapToLeaderInOwnTime = gapToLeaderInOwnTime; + this.correctedTime = correctedTime; + this.correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; + } + public List getLegDetails() { + return legDetails; + } + public Distance getWindwardDistanceToCompetitorFarthestAhead() { + return windwardDistanceToCompetitorFarthestAhead; + } + public Distance getAverageAbsoluteCrossTrackError() { + return averageAbsoluteCrossTrackError; + } + public Distance getAverageSignedCrossTrackError() { + return averageSignedCrossTrackError; + } + public Duration getGapToLeaderInOwnTime() { + return gapToLeaderInOwnTime; + } + public Duration getCorrectedTime() { + return correctedTime; + } + public Duration getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead() { + return correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; + } + } + + /** + * If timePoint is after the end of the race's tracking the query will be adjusted to obtain the values + * at the end of the {@link TrackedRace#getEndOfTracking() race's tracking time}. If the time point adjusted this + * way equals the end of the tracking time, the query results will be looked up in a cache first and if not found, + * they will be stored to the cache after calculating them. A cache invalidation {@link RaceChangeListener listener} + * will be registered with the race which will be triggered for any event received by the race. + * @param waitForLatestAnalyses + * if false, this method is allowed to read the maneuver analysis results from a cache that + * may not reflect all data already received; otherwise, the method will always block for the latest + * cache updates to have happened before returning. + */ + private RaceDetails getRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, + boolean waitForLatestAnalyses, Map> legRanksCache, + RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) throws InterruptedException, ExecutionException { + final RaceDetails raceDetails; + if (trackedRace.getEndOfTracking() != null && trackedRace.getEndOfTracking().compareTo(timePoint) < 0) { + raceDetails = getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(trackedRace, competitor, legRanksCache, rankingInfo, cache); + } else { + raceDetails = calculateRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, legRanksCache, cache, rankingInfo); + } + return raceDetails; + } + + private RaceDetails getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(final TrackedRace trackedRace, + final Competitor competitor, final Map> legRanksCache, + RankingInfo rankingInfo, final WindLegTypeAndLegBearingCache cache) throws InterruptedException, ExecutionException { + final com.sap.sse.common.Util.Pair key = new com.sap.sse.common.Util.Pair(trackedRace, competitor); + RunnableFuture raceDetails; + synchronized (raceDetailsAtEndOfTrackingCache) { + raceDetails = raceDetailsAtEndOfTrackingCache.get(key); + if (raceDetails == null) { + raceDetails = new FutureTask(new Callable() { + @Override + public RaceDetails call() throws Exception { + TimePoint end = trackedRace.getEndOfRace(); + if (end == null) { + end = trackedRace.getEndOfTracking(); + } + return calculateRaceDetails(trackedRace, competitor, end, + // TODO see bug 1358: for now, use waitForLatest==false until we've switched to optimistic locking for the course read lock + /* TODO old comment when it was still true: "because this is done only once after end of tracking" */ + /* waitForLatestAnalyses (maneuver and cross track error) */ false, + legRanksCache, cache, rankingInfo); + } + }); + raceDetailsExecutor.execute(raceDetails); + raceDetailsAtEndOfTrackingCache.put(key, raceDetails); + final CacheInvalidationListener cacheInvalidationListener = new CacheInvalidationListener(trackedRace, competitor); + trackedRace.addListener(cacheInvalidationListener); + cacheInvalidationListeners.add(cacheInvalidationListener); + } + } + return raceDetails.get(); + } + + private RaceDetails calculateRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, + boolean waitForLatestAnalyses, Map> legRanksCache, + WindLegTypeAndLegBearingCache cache, final RankingInfo rankingInfo) { + final List legDetails = new ArrayList(); + final Course course = trackedRace.getRace().getCourse(); + course.lockForRead(); // hold back any course re-configurations while looping over the legs + try { + for (Leg leg : course.getLegs()) { + LegEntryDTO legEntry; + // We loop over a copy of the course's legs; during a course change, legs may become "stale," even with + // regard to the leg/trackedLeg structures inside the tracked race which is updated by the course change + // immediately. That's why we've acquired a read lock for the course above. + TrackedLegOfCompetitor trackedLeg = trackedRace.getTrackedLeg(competitor, leg); + if (trackedLeg != null && trackedLeg.hasStartedLeg(timePoint)) { + legEntry = createLegEntry(trackedLeg, timePoint, waitForLatestAnalyses, legRanksCache, rankingInfo, cache); + } else { + legEntry = null; + } + legDetails.add(legEntry); + } + final Distance windwardDistanceToCompetitorFarthestAhead = trackedRace == null ? null : trackedRace + .getWindwardDistanceToCompetitorFarthestAhead(competitor, timePoint, WindPositionMode.LEG_MIDDLE, rankingInfo, cache); + Distance averageAbsoluteCrossTrackError; + try { + averageAbsoluteCrossTrackError = trackedRace == null ? null : trackedRace.getAverageAbsoluteCrossTrackError( + competitor, timePoint, waitForLatestAnalyses, cache); + } catch (NoWindException nwe) { + // without wind information, use null meaning "unknown" + averageAbsoluteCrossTrackError = null; + } + Distance averageSignedCrossTrackError; + try { + averageSignedCrossTrackError = trackedRace == null ? null : trackedRace.getAverageSignedCrossTrackError( + competitor, timePoint, waitForLatestAnalyses, cache); + } catch (NoWindException nwe) { + // without wind information, use null meaning "unknown" + averageSignedCrossTrackError = null; + } + final CompetitorRankingInfo competitorRankingInfo = rankingInfo.getCompetitorRankingInfo().apply(competitor); + return new RaceDetails(legDetails, windwardDistanceToCompetitorFarthestAhead, averageAbsoluteCrossTrackError, averageSignedCrossTrackError, + trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache), + trackedRace.getRankingMetric().getCorrectedTime(competitor, timePoint), + competitorRankingInfo == null ? null : competitorRankingInfo.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead()); + } finally { + course.unlockAfterRead(); + } + } + + private LegEntryDTO createLegEntry(TrackedLegOfCompetitor trackedLeg, TimePoint timePoint, + boolean waitForLatestAnalyses, Map> legRanksCache, + RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) { + LegEntryDTO result; + final Duration time = trackedLeg.getTime(timePoint); + if (trackedLeg == null || time == null) { + result = null; + } else { + result = new LegEntryDTO(); + try { + result.legType = trackedLeg.getTrackedLeg().getLegType(timePoint); + } catch (NoWindException nwe) { + result.legType = null; // can't determine leg type without wind data + } + final Speed averageSpeedOverGround = trackedLeg.getAverageSpeedOverGround(timePoint); + result.averageSpeedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); + Distance averageAbsoluteCrossTrackError; + try { + averageAbsoluteCrossTrackError = trackedLeg.getAverageAbsoluteCrossTrackError(timePoint, waitForLatestAnalyses); + } catch (NoWindException nwe) { + // leave averageAbsoluteCrossTrackError as null, meaning "unknown" + averageAbsoluteCrossTrackError = null; + } + result.averageAbsoluteCrossTrackErrorInMeters = averageAbsoluteCrossTrackError == null ? null : averageAbsoluteCrossTrackError.getMeters(); + Distance averageSignedCrossTrackError; + try { + averageSignedCrossTrackError = trackedLeg.getAverageSignedCrossTrackError(timePoint, waitForLatestAnalyses); + } catch (NoWindException nwe) { + // leave averageSignedCrossTrackError as null, meaning "unknown" + averageSignedCrossTrackError = null; + } + result.averageSignedCrossTrackErrorInMeters = averageSignedCrossTrackError == null ? null : averageSignedCrossTrackError.getMeters(); + Double speedOverGroundInKnots; + if (trackedLeg.hasFinishedLeg(timePoint)) { + speedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); + } else { + final SpeedWithBearing speedOverGround = trackedLeg.getSpeedOverGround(timePoint); + speedOverGroundInKnots = speedOverGround == null ? null : speedOverGround.getKnots(); + } + result.currentSpeedOverGroundInKnots = speedOverGroundInKnots == null ? null : speedOverGroundInKnots; + Distance distanceTraveled = trackedLeg.getDistanceTraveled(timePoint); + result.distanceTraveledInMeters = distanceTraveled == null ? null : distanceTraveled.getMeters(); + Distance distanceTraveledConsideringGateStart = trackedLeg.getDistanceTraveledConsideringGateStart(timePoint); + result.distanceTraveledIncludingGateStartInMeters = distanceTraveledConsideringGateStart == null ? null : distanceTraveledConsideringGateStart.getMeters(); + final Duration estimatedTimeToNextMarkInSeconds = trackedLeg.getEstimatedTimeToNextMark(timePoint, WindPositionMode.EXACT, cache); + result.estimatedTimeToNextWaypointInSeconds = estimatedTimeToNextMarkInSeconds==null?null:estimatedTimeToNextMarkInSeconds.asSeconds(); + result.timeInMilliseconds = time.asMillis(); + result.finished = trackedLeg.hasFinishedLeg(timePoint); + final TimePoint legFinishTime = trackedLeg.getFinishTime(); + result.correctedTotalTime = trackedLeg.hasStartedLeg(timePoint) ? trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric().getCorrectedTime(trackedLeg.getCompetitor(), + trackedLeg.hasFinishedLeg(timePoint) ? legFinishTime : timePoint, cache) : null; + // fetch the leg gap in own corrected time from the ranking metric + final Duration gapToLeaderInOwnTime = trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric(). + getLegGapToLegLeaderInOwnTime(trackedLeg, timePoint, rankingInfo, cache); + result.gapToLeaderInSeconds = gapToLeaderInOwnTime == null ? null : gapToLeaderInOwnTime.asSeconds(); + if (result.gapToLeaderInSeconds != null) { + final Duration gapAtEndOfPreviousLeg = getGapAtEndOfPreviousLeg(trackedLeg, rankingInfo, cache); + if (gapAtEndOfPreviousLeg != null) { + result.gapChangeSinceLegStartInSeconds = result.gapToLeaderInSeconds - gapAtEndOfPreviousLeg.asSeconds(); + } + } + LinkedHashMap legRanks = legRanksCache.get(trackedLeg.getLeg()); + if (legRanks != null) { + result.rank = legRanks.get(trackedLeg.getCompetitor()); + } else { + result.rank = trackedLeg.getRank(timePoint, cache); + } + result.started = trackedLeg.hasStartedLeg(timePoint); + Speed velocityMadeGood; + if (trackedLeg.hasFinishedLeg(timePoint)) { + velocityMadeGood = trackedLeg.getAverageVelocityMadeGood(timePoint); + } else { + velocityMadeGood = trackedLeg.getVelocityMadeGood(timePoint, WindPositionMode.EXACT); + } + result.velocityMadeGoodInKnots = velocityMadeGood == null ? null : velocityMadeGood.getKnots(); + Distance windwardDistanceToGo = trackedLeg.getWindwardDistanceToGo(timePoint, WindPositionMode.LEG_MIDDLE); + result.windwardDistanceToGoInMeters = windwardDistanceToGo == null ? null : windwardDistanceToGo + .getMeters(); + final TimePoint startOfRace = trackedLeg.getTrackedLeg().getTrackedRace().getStartOfRace(); + if (startOfRace != null && trackedLeg.hasStartedLeg(timePoint)) { + // not using trackedLeg.getManeuvers(...) because it may not catch the mark passing maneuver starting this leg + // because that may have been detected as slightly before the mark passing time, hence associated with the previous leg + List maneuvers = trackedLeg.getTrackedLeg().getTrackedRace() + .getManeuvers(trackedLeg.getCompetitor(), startOfRace, timePoint, waitForLatestAnalyses); + if (maneuvers != null) { + result.numberOfManeuvers = new HashMap(); + result.numberOfManeuvers.put(ManeuverType.TACK, 0); + result.numberOfManeuvers.put(ManeuverType.JIBE, 0); + result.numberOfManeuvers.put(ManeuverType.PENALTY_CIRCLE, 0); + Map totalManeuverLossInMeters = new HashMap(); + totalManeuverLossInMeters.put(ManeuverType.TACK, 0.0); + totalManeuverLossInMeters.put(ManeuverType.JIBE, 0.0); + totalManeuverLossInMeters.put(ManeuverType.PENALTY_CIRCLE, 0.0); + TimePoint startOfLeg = trackedLeg.getStartTime(); + for (Maneuver maneuver : maneuvers) { + // don't count maneuvers that were in previous legs + switch (maneuver.getType()) { + case TACK: + case JIBE: + case PENALTY_CIRCLE: + if (!maneuver.getTimePoint().before(startOfLeg) && (legFinishTime == null || legFinishTime.after(timePoint) || + maneuver.getTimePoint().before(legFinishTime))) { + if (maneuver.getManeuverLoss() != null) { + result.numberOfManeuvers.put(maneuver.getType(), + result.numberOfManeuvers.get(maneuver.getType()) + 1); + totalManeuverLossInMeters.put(maneuver.getType(), + totalManeuverLossInMeters.get(maneuver.getType()) + + maneuver.getManeuverLoss().getMeters()); + } + } + break; + case MARK_PASSING: + // analyze all mark passings, not only those after this leg's start, to catch the mark passing + // maneuver starting this leg, even if its time point is slightly before the mark passing starting this leg + MarkPassingManeuver mpm = (MarkPassingManeuver) maneuver; + if (mpm.getWaypointPassed() == trackedLeg.getLeg().getFrom()) { + result.sideToWhichMarkAtLegStartWasRounded = mpm.getSide(); + } + break; + default: + /* Do nothing here. + * Throwing an exception destroys the toggling (and maybe other behaviour) of the leaderboard. + */ + } + } + result.averageManeuverLossInMeters = new HashMap(); + for (ManeuverType maneuverType : new ManeuverType[] { ManeuverType.TACK, ManeuverType.JIBE, + ManeuverType.PENALTY_CIRCLE }) { + if (result.numberOfManeuvers.get(maneuverType) != 0) { + result.averageManeuverLossInMeters.put( + maneuverType, + totalManeuverLossInMeters.get(maneuverType) + / result.numberOfManeuvers.get(maneuverType)); + } + } + } + } + } + return result; + } + + private Duration getGapAtEndOfPreviousLeg(TrackedLegOfCompetitor trackedLeg, final RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) { + final Duration result; + final Course course = trackedLeg.getTrackedLeg().getTrackedRace().getRace().getCourse(); + // if trackedLeg is the first leg, compute the gap at the start of this leg; otherwise, compute gap + // at the end of the previous leg + final TimePoint timePoint = trackedLeg.getStartTime(); + final TrackedLegOfCompetitor tloc; + if (course.getFirstWaypoint() == trackedLeg.getLeg().getFrom()) { + tloc = trackedLeg; + } else { + tloc = trackedLeg.getTrackedLeg().getTrackedRace().getTrackedLegFinishingAt(trackedLeg.getLeg().getFrom()) + .getTrackedLeg(trackedLeg.getCompetitor()); + } + result = trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric().getLegGapToLegLeaderInOwnTime(tloc, timePoint, rankingInfo, cache); + return result; + } + + private LeaderboardDTO getLiveLeaderboard(Collection namesOfRaceColumnsForWhichToLoadLegDetails, + boolean addOverallDetails, TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory) throws NoWindException, ExecutionException { + LiveLeaderboardUpdater liveLeaderboardUpdater = getLiveLeaderboardUpdater(trackedRegattaRegistry, + baseDomainFactory); + return liveLeaderboardUpdater.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails); + } + + private LiveLeaderboardUpdater getLiveLeaderboardUpdater(TrackedRegattaRegistry trackedRegattaRegistry, + DomainFactory baseDomainFactory) { + LiveLeaderboardUpdater result = this.liveLeaderboardUpdater; + if (result == null) { + synchronized (this) { + result = this.liveLeaderboardUpdater; + if (result == null) { + this.liveLeaderboardUpdater = new LiveLeaderboardUpdater(this, trackedRegattaRegistry, baseDomainFactory); + result = this.liveLeaderboardUpdater; + } + } + } + return result; + } + + private LeaderboardDTOCache getLeaderboardDTOCache() { + LeaderboardDTOCache result = this.leaderboardDTOCache; + if (result == null) { + synchronized (this) { + result = this.leaderboardDTOCache; + if (result == null) { + // The leaderboard cache is invalidated upon all competitor and mark position changes; some analyzes + // are pretty expensive, such as the maneuver re-calculation. Waiting for the latest analysis after only a + // single fix was updated is too expensive if users use the replay feature while a race is still running. + // Therefore, using waitForLatestAnalyses==false seems appropriate here. + this.leaderboardDTOCache = new LeaderboardDTOCache(/* waitForLatestAnalyses */false, this); + result = this.leaderboardDTOCache; + } + } + } + return result; + } + + @Override + public LeaderboardDTO getLeaderboardDTO(TimePoint timePoint, + Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, + TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory, boolean fillNetPointsUncorrected) throws NoWindException, + InterruptedException, ExecutionException { + LeaderboardDTO result = null; + if (timePoint == null) { + // timePoint==null means live mode; however, if we're after the end of all races and after all score + // corrections, don't use the live leaderboard updater which would keep re-calculating over and over again, but map + // this to a usual non-live call which uses the regular LeaderboardDTOCache which is invalidated properly + // when the tracked race associations or score corrections or tracked race contents changes: + final TimePoint nowMinusDelay = this.getNowMinusDelay(); + final TimePoint timePointOfLatestModification = this.getTimePointOfLatestModification(); + if (fillNetPointsUncorrected || (timePointOfLatestModification != null && !nowMinusDelay.before(timePointOfLatestModification))) { + // if there hasn't been any modification to the leaderboard since nowMinusDelay, use non-live mode + // and pull the result from the regular leaderboard cache: + timePoint = timePointOfLatestModification; + } 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, addOverallDetails, trackedRegattaRegistry, baseDomainFactory); + } + } + if (timePoint != null) { + if (fillNetPointsUncorrected) { + // explicitly filling the uncorrected net points requires uncached recalculation + result = computeDTO(timePoint, namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, /* waitForLatestAnalyses */ true, + trackedRegattaRegistry, baseDomainFactory, fillNetPointsUncorrected); + } else { + // in replay we'd like up-to-date results; they are still cached + // which is OK because the cache is invalidated whenever any of the tracked races attached to the + // leaderboard changes. + result = getLeaderboardDTOCache().getLeaderboardByName(timePoint, + namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, baseDomainFactory, + trackedRegattaRegistry); + } + } + return result; + } + + public String toString() { + return getName() + " " + (getDefaultCourseArea() != null ? getDefaultCourseArea().getName() : "") + " " + (getScoringScheme() != null ? getScoringScheme().getType().name() : ""); + } + + @Override + public NumberOfCompetitorsInLeaderboardFetcher getNumberOfCompetitorsInLeaderboardFetcher() { + return new NumberOfCompetitorsFetcherImpl(); + } + + protected abstract LeaderboardType getLeaderboardType(); + +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/FlexibleLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/FlexibleLeaderboardImpl.java index ab37c03d57d..62d50860e6a 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/FlexibleLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/FlexibleLeaderboardImpl.java @@ -1,336 +1,342 @@ -package com.sap.sailing.domain.leaderboard.impl; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.ObjectStreamException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.logging.Logger; - -import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.CourseArea; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceColumnListener; -import com.sap.sailing.domain.base.impl.AbstractRaceExecutionOrderProvider; -import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; -import com.sap.sailing.domain.leaderboard.FlexibleRaceColumn; -import com.sap.sailing.domain.leaderboard.ScoringScheme; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sailing.domain.racelog.RaceLogStore; -import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; -import com.sap.sailing.domain.regattalike.BaseRegattaLikeImpl; -import com.sap.sailing.domain.regattalike.FlexibleLeaderboardAsRegattaLikeIdentifier; -import com.sap.sailing.domain.regattalike.IsRegattaLike; -import com.sap.sailing.domain.regattalike.RegattaLikeIdentifier; -import com.sap.sailing.domain.regattalike.RegattaLikeListener; -import com.sap.sailing.domain.regattalog.RegattaLogStore; -import com.sap.sailing.domain.regattalog.impl.EmptyRegattaLogStore; -import com.sap.sailing.domain.tracking.RaceExecutionOrderProvider; -import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sse.common.Duration; - -/** - * A leaderboard implementation that allows users to flexibly configure which columns exist. No constraints need to be observed regarding - * the columns belonging to the same regatta or even boat class.

- * - * The flexible leaderboard listens as {@link RaceColumnListener} on all its {@link RaceColumn}s and forwards all events to - * all {@link RaceColumnListener}s subscribed with this leaderboard. - * - * @author Axel Uhl (D043530) - * - */ -public class FlexibleLeaderboardImpl extends AbstractLeaderboardImpl implements FlexibleLeaderboard { - private static Logger logger = Logger.getLogger(FlexibleLeaderboardImpl.class.getName()); - - protected static final DefaultFleetImpl defaultFleet = new DefaultFleetImpl(); - private static final long serialVersionUID = -5708971849158747846L; - private final List races; - private final ScoringScheme scoringScheme; - private String name; - private transient RaceLogStore raceLogStore; - private CourseArea courseArea; - private RaceExecutionOrderProvider raceExecutionOrderProvider; - - /** - * @see RegattaLog for the reason why the leaderboard manages a {@code RegattaLog} - */ - private final IsRegattaLike regattaLikeHelper; - - public FlexibleLeaderboardImpl(String name, ThresholdBasedResultDiscardingRule resultDiscardingRule, - ScoringScheme scoringScheme, CourseArea courseArea) { - this(EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, - name, resultDiscardingRule, scoringScheme, courseArea); - } - - public FlexibleLeaderboardImpl(RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, - String name, ThresholdBasedResultDiscardingRule resultDiscardingRule, - ScoringScheme scoringScheme, CourseArea courseArea) { - super(resultDiscardingRule); - this.scoringScheme = scoringScheme; - if (name == null) { - throw new IllegalArgumentException("A leaderboard's name must not be null"); - } - this.name = name; - this.races = new ArrayList<>(); - this.raceLogStore = raceLogStore; - this.courseArea = courseArea; - this.regattaLikeHelper = new BaseRegattaLikeImpl(new FlexibleLeaderboardAsRegattaLikeIdentifier(this), regattaLogStore) { - private static final long serialVersionUID = 4082392360832548953L; - - @Override - public RaceColumn getRaceColumnByName(String raceColumnName) { - return getRaceColumnByName(raceColumnName); - } - }; - this.raceExecutionOrderProvider = new RaceExecutionOrderCache(); - } - - /** - * Deserialization has to be maintained in lock-step with {@link #writeObject(ObjectOutputStream) serialization}. - * When de-serializing, a possibly remote {@link #raceLogStore} is ignored because it is transient. Instead, an - * {@link EmptyRaceLogStore} is used for the de-serialized instance. A new {@link RaceLogInformation} is - * assembled for this empty race log and applied to all columns. - */ - private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { - ois.defaultReadObject(); - raceLogStore = EmptyRaceLogStore.INSTANCE; - for (RaceColumn column : getRaceColumns()) { - column.setRaceLogInformation(raceLogStore, new FlexibleLeaderboardAsRegattaLikeIdentifier(this)); - } - } - - protected Object readResolve() throws ObjectStreamException { - raceExecutionOrderProvider.triggerUpdate(); - return this; - } - - @Override - public String getName() { - return name; - } - - /** - * @param newName must not be null - */ - public void setName(String newName) { - if (newName == null) { - throw new IllegalArgumentException("A leaderboard's name must not be null"); - } - this.name = newName; - } - - @Override - public RaceColumn addRace(TrackedRace race, String columnName, boolean medalRace) { - FlexibleRaceColumn column = addRaceColumn(columnName, medalRace, /* logAlreadyExistingColumn */false); - column.setTrackedRace(defaultFleet, race); // triggers listeners because this object was registered above as - // race column listener on the column - return column; - } - - @Override - public FlexibleRaceColumn addRaceColumn(String name, boolean medalRace) { - return addRaceColumn(name, medalRace, /* logAlreadyExistingColumn */true); - } - - private FlexibleRaceColumn addRaceColumn(String name, boolean medalRace, boolean logAlreadyExistingColumn) { - FlexibleRaceColumn column = getRaceColumnByName(name); - if (column != null) { - if (logAlreadyExistingColumn) { - final String msg = "Trying to create race column with duplicate name " + name + " in leaderboard " + getName(); - logger.severe(msg); - } - } else { - column = createRaceColumn(name, medalRace); - column.addRaceColumnListener(this); - races.add(column); - column.setRaceLogInformation(raceLogStore, new FlexibleLeaderboardAsRegattaLikeIdentifier(this)); - getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(column); - } - return column; - } - - @Override - public FlexibleRaceColumn getRaceColumnByName(String columnName) { - return (FlexibleRaceColumn) super.getRaceColumnByName(columnName); - } - - @Override - public Fleet getFleet(String fleetName) { - Fleet result; - if (fleetName == null) { - result = defaultFleet; - } else { - result = super.getFleet(fleetName); - } - return result; - } - - @Override - public void removeRaceColumn(String columnName) { - final FlexibleRaceColumn raceColumn = getRaceColumnByName(columnName); - if (raceColumn != null) { - for (Fleet fleet : raceColumn.getFleets()) { - raceLogStore.removeRaceLog(raceColumn.getRaceLogIdentifier(fleet)); - if (raceColumn.getTrackedRace(fleet) != null) { - raceColumn.getTrackedRace(fleet).detachRaceExecutionOrderProvider(raceExecutionOrderProvider); - } - } - races.remove(raceColumn); - getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(raceColumn); - raceColumn.removeRaceColumnListener(this); - } - } - - @Override - public Iterable getRaceColumns() { - final Iterable result; - if (races != null) { - result = Collections.unmodifiableCollection(new ArrayList(races)); - } else { - result = null; - } - return result; - } - - protected RaceColumnImpl createRaceColumn(String column, boolean medalRace) { - return new RaceColumnImpl(column, medalRace, raceExecutionOrderProvider); - } - - protected Iterable turnNullOrEmptyFleetsIntoDefaultFleet(Fleet... fleets) { - Iterable theFleets; - if (fleets == null || fleets.length == 0) { - Fleet defaultfleetCasted = defaultFleet; - theFleets = Collections.singleton(defaultfleetCasted); - } else { - theFleets = Arrays.asList(fleets); - } - return theFleets; - } - - @Override - public void moveRaceColumnUp(String name) { - FlexibleRaceColumn race = null; - for (FlexibleRaceColumn r : races) { - if (r.getName().equals(name)) { - race = r; - } - } - if (race != null) { - int index = 0; - index = races.lastIndexOf(race); - index--; - if (index >= 0) { - races.remove(race); - races.add(index, race); - getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(race, index); - } - } - } - - @Override - public void moveRaceColumnDown(String name) { - FlexibleRaceColumn race = null; - for (FlexibleRaceColumn r : races) { - if (r.getName().equals(name)) { - race = r; - } - } - if (race != null) { - int index = 0; - index = races.lastIndexOf(race); - if (index != -1) { - index++; - if (index < races.size()) { - races.remove(race); - races.add(index, race); - getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(race, index); - } - } - } - } - - @Override - public void updateIsMedalRace(String raceName, boolean isMedalRace) { - FlexibleRaceColumn race = null; - for (FlexibleRaceColumn r : races) { - if (r.getName().equals(raceName)) - race = r; - } - if (race != null) { - race.setIsMedalRace(isMedalRace); - } - } - - @Override - public ScoringScheme getScoringScheme() { - return scoringScheme; - } - - @Override - public CourseArea getDefaultCourseArea() { - return courseArea; - } - - @Override - public void setDefaultCourseArea(CourseArea newCourseArea) { - this.courseArea = newCourseArea; - } - - @Override - public IsRegattaLike getRegattaLike() { - return regattaLikeHelper; - } - - @Override - public RegattaLog getRegattaLog() { - return regattaLikeHelper.getRegattaLog(); - } - - @Override - public RegattaLikeIdentifier getRegattaLikeIdentifier() { - return regattaLikeHelper.getRegattaLikeIdentifier(); - } - - @Override - public void addListener(RegattaLikeListener listener) { - regattaLikeHelper.addListener(listener); - } - - @Override - public void removeListener(RegattaLikeListener listener) { - regattaLikeHelper.removeListener(listener); - } - - @Override - public Double getTimeOnTimeFactor(Competitor competitor) { - return regattaLikeHelper.getTimeOnTimeFactor(competitor); - } - - @Override - public Duration getTimeOnDistanceAllowancePerNauticalMile(Competitor competitor) { - return regattaLikeHelper.getTimeOnDistanceAllowancePerNauticalMile(competitor); - } - - private class RaceExecutionOrderCache extends AbstractRaceExecutionOrderProvider { - private static final long serialVersionUID = 652833386555762661L; - - public RaceExecutionOrderCache() { - super(); - addRaceColumnListener(this); - } - - @Override - protected Map> getRaceColumnsOfSeries() { - final Map> result = new HashMap<>(); - result.put(FlexibleLeaderboardImpl.defaultFleet, getRaceColumns()); - return result; - } - } -} +package com.sap.sailing.domain.leaderboard.impl; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnListener; +import com.sap.sailing.domain.base.impl.AbstractRaceExecutionOrderProvider; +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; +import com.sap.sailing.domain.leaderboard.FlexibleRaceColumn; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; +import com.sap.sailing.domain.regattalike.BaseRegattaLikeImpl; +import com.sap.sailing.domain.regattalike.FlexibleLeaderboardAsRegattaLikeIdentifier; +import com.sap.sailing.domain.regattalike.IsRegattaLike; +import com.sap.sailing.domain.regattalike.RegattaLikeIdentifier; +import com.sap.sailing.domain.regattalike.RegattaLikeListener; +import com.sap.sailing.domain.regattalog.RegattaLogStore; +import com.sap.sailing.domain.regattalog.impl.EmptyRegattaLogStore; +import com.sap.sailing.domain.tracking.RaceExecutionOrderProvider; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sse.common.Duration; + +/** + * A leaderboard implementation that allows users to flexibly configure which columns exist. No constraints need to be observed regarding + * the columns belonging to the same regatta or even boat class.

+ * + * The flexible leaderboard listens as {@link RaceColumnListener} on all its {@link RaceColumn}s and forwards all events to + * all {@link RaceColumnListener}s subscribed with this leaderboard. + * + * @author Axel Uhl (D043530) + * + */ +public class FlexibleLeaderboardImpl extends AbstractLeaderboardImpl implements FlexibleLeaderboard { + private static Logger logger = Logger.getLogger(FlexibleLeaderboardImpl.class.getName()); + + protected static final DefaultFleetImpl defaultFleet = new DefaultFleetImpl(); + private static final long serialVersionUID = -5708971849158747846L; + private final List races; + private final ScoringScheme scoringScheme; + private String name; + private transient RaceLogStore raceLogStore; + private CourseArea courseArea; + private RaceExecutionOrderProvider raceExecutionOrderProvider; + + /** + * @see RegattaLog for the reason why the leaderboard manages a {@code RegattaLog} + */ + private final IsRegattaLike regattaLikeHelper; + + public FlexibleLeaderboardImpl(String name, ThresholdBasedResultDiscardingRule resultDiscardingRule, + ScoringScheme scoringScheme, CourseArea courseArea) { + this(EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, + name, resultDiscardingRule, scoringScheme, courseArea); + } + + public FlexibleLeaderboardImpl(RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, + String name, ThresholdBasedResultDiscardingRule resultDiscardingRule, + ScoringScheme scoringScheme, CourseArea courseArea) { + super(resultDiscardingRule); + this.scoringScheme = scoringScheme; + if (name == null) { + throw new IllegalArgumentException("A leaderboard's name must not be null"); + } + this.name = name; + this.races = new ArrayList<>(); + this.raceLogStore = raceLogStore; + this.courseArea = courseArea; + this.regattaLikeHelper = new BaseRegattaLikeImpl(new FlexibleLeaderboardAsRegattaLikeIdentifier(this), regattaLogStore) { + private static final long serialVersionUID = 4082392360832548953L; + + @Override + public RaceColumn getRaceColumnByName(String raceColumnName) { + return getRaceColumnByName(raceColumnName); + } + }; + this.raceExecutionOrderProvider = new RaceExecutionOrderCache(); + } + + /** + * Deserialization has to be maintained in lock-step with {@link #writeObject(ObjectOutputStream) serialization}. + * When de-serializing, a possibly remote {@link #raceLogStore} is ignored because it is transient. Instead, an + * {@link EmptyRaceLogStore} is used for the de-serialized instance. A new {@link RaceLogInformation} is + * assembled for this empty race log and applied to all columns. + */ + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + raceLogStore = EmptyRaceLogStore.INSTANCE; + for (RaceColumn column : getRaceColumns()) { + column.setRaceLogInformation(raceLogStore, new FlexibleLeaderboardAsRegattaLikeIdentifier(this)); + } + } + + protected Object readResolve() throws ObjectStreamException { + raceExecutionOrderProvider.triggerUpdate(); + return this; + } + + @Override + public String getName() { + return name; + } + + /** + * @param newName must not be null + */ + public void setName(String newName) { + if (newName == null) { + throw new IllegalArgumentException("A leaderboard's name must not be null"); + } + this.name = newName; + } + + @Override + public RaceColumn addRace(TrackedRace race, String columnName, boolean medalRace) { + FlexibleRaceColumn column = addRaceColumn(columnName, medalRace, /* logAlreadyExistingColumn */false); + column.setTrackedRace(defaultFleet, race); // triggers listeners because this object was registered above as + // race column listener on the column + return column; + } + + @Override + public FlexibleRaceColumn addRaceColumn(String name, boolean medalRace) { + return addRaceColumn(name, medalRace, /* logAlreadyExistingColumn */true); + } + + private FlexibleRaceColumn addRaceColumn(String name, boolean medalRace, boolean logAlreadyExistingColumn) { + FlexibleRaceColumn column = getRaceColumnByName(name); + if (column != null) { + if (logAlreadyExistingColumn) { + final String msg = "Trying to create race column with duplicate name " + name + " in leaderboard " + getName(); + logger.severe(msg); + } + } else { + column = createRaceColumn(name, medalRace); + column.addRaceColumnListener(this); + races.add(column); + column.setRaceLogInformation(raceLogStore, new FlexibleLeaderboardAsRegattaLikeIdentifier(this)); + getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(column); + } + return column; + } + + @Override + public FlexibleRaceColumn getRaceColumnByName(String columnName) { + return (FlexibleRaceColumn) super.getRaceColumnByName(columnName); + } + + @Override + public Fleet getFleet(String fleetName) { + Fleet result; + if (fleetName == null) { + result = defaultFleet; + } else { + result = super.getFleet(fleetName); + } + return result; + } + + @Override + public void removeRaceColumn(String columnName) { + final FlexibleRaceColumn raceColumn = getRaceColumnByName(columnName); + if (raceColumn != null) { + for (Fleet fleet : raceColumn.getFleets()) { + raceLogStore.removeRaceLog(raceColumn.getRaceLogIdentifier(fleet)); + if (raceColumn.getTrackedRace(fleet) != null) { + raceColumn.getTrackedRace(fleet).detachRaceExecutionOrderProvider(raceExecutionOrderProvider); + } + } + races.remove(raceColumn); + getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(raceColumn); + raceColumn.removeRaceColumnListener(this); + } + } + + @Override + public Iterable getRaceColumns() { + final Iterable result; + if (races != null) { + result = Collections.unmodifiableCollection(new ArrayList(races)); + } else { + result = null; + } + return result; + } + + protected RaceColumnImpl createRaceColumn(String column, boolean medalRace) { + return new RaceColumnImpl(column, medalRace, raceExecutionOrderProvider); + } + + protected Iterable turnNullOrEmptyFleetsIntoDefaultFleet(Fleet... fleets) { + Iterable theFleets; + if (fleets == null || fleets.length == 0) { + Fleet defaultfleetCasted = defaultFleet; + theFleets = Collections.singleton(defaultfleetCasted); + } else { + theFleets = Arrays.asList(fleets); + } + return theFleets; + } + + @Override + public void moveRaceColumnUp(String name) { + FlexibleRaceColumn race = null; + for (FlexibleRaceColumn r : races) { + if (r.getName().equals(name)) { + race = r; + } + } + if (race != null) { + int index = 0; + index = races.lastIndexOf(race); + index--; + if (index >= 0) { + races.remove(race); + races.add(index, race); + getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(race, index); + } + } + } + + @Override + public void moveRaceColumnDown(String name) { + FlexibleRaceColumn race = null; + for (FlexibleRaceColumn r : races) { + if (r.getName().equals(name)) { + race = r; + } + } + if (race != null) { + int index = 0; + index = races.lastIndexOf(race); + if (index != -1) { + index++; + if (index < races.size()) { + races.remove(race); + races.add(index, race); + getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(race, index); + } + } + } + } + + @Override + public void updateIsMedalRace(String raceName, boolean isMedalRace) { + FlexibleRaceColumn race = null; + for (FlexibleRaceColumn r : races) { + if (r.getName().equals(raceName)) + race = r; + } + if (race != null) { + race.setIsMedalRace(isMedalRace); + } + } + + @Override + public ScoringScheme getScoringScheme() { + return scoringScheme; + } + + @Override + public CourseArea getDefaultCourseArea() { + return courseArea; + } + + @Override + public void setDefaultCourseArea(CourseArea newCourseArea) { + this.courseArea = newCourseArea; + } + + @Override + public IsRegattaLike getRegattaLike() { + return regattaLikeHelper; + } + + @Override + public RegattaLog getRegattaLog() { + return regattaLikeHelper.getRegattaLog(); + } + + @Override + public RegattaLikeIdentifier getRegattaLikeIdentifier() { + return regattaLikeHelper.getRegattaLikeIdentifier(); + } + + @Override + public void addListener(RegattaLikeListener listener) { + regattaLikeHelper.addListener(listener); + } + + @Override + public void removeListener(RegattaLikeListener listener) { + regattaLikeHelper.removeListener(listener); + } + + @Override + public Double getTimeOnTimeFactor(Competitor competitor) { + return regattaLikeHelper.getTimeOnTimeFactor(competitor); + } + + @Override + public Duration getTimeOnDistanceAllowancePerNauticalMile(Competitor competitor) { + return regattaLikeHelper.getTimeOnDistanceAllowancePerNauticalMile(competitor); + } + + private class RaceExecutionOrderCache extends AbstractRaceExecutionOrderProvider { + private static final long serialVersionUID = 652833386555762661L; + + public RaceExecutionOrderCache() { + super(); + addRaceColumnListener(this); + } + + @Override + protected Map> getRaceColumnsOfSeries() { + final Map> result = new HashMap<>(); + result.put(FlexibleLeaderboardImpl.defaultFleet, getRaceColumns()); + return result; + } + } + + @Override + protected LeaderboardType getLeaderboardType() { + return LeaderboardType.FlexibleLeaderboard; + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java index a6df854049f..d5af239cf6a 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java @@ -1,117 +1,123 @@ -package com.sap.sailing.domain.leaderboard.impl; - -import java.util.ArrayList; -import java.util.List; - -import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.CourseArea; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceColumnInSeries; -import com.sap.sailing.domain.base.RaceColumnListener; -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.impl.RaceColumnInSeriesImpl; -import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; -import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; -import com.sap.sailing.domain.leaderboard.ScoringScheme; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sailing.domain.regattalike.IsRegattaLike; -import com.sap.sailing.domain.tracking.TrackedRace; - -/** - * A leaderboard that is based on the definition of a {@link Regatta} with its {@link Series} and {@link Fleet}. The regatta - * leaderboard listens to its {@link Regatta} as a {@link RaceColumnListener} and forwards all link/unlink events for - * {@link TrackedRace}s being linked to / unlinked from race columns to all {@link RaceColumnListener}s subscribed with - * this leaderboard. - * - * @author Axel Uhl (D043530) - * - */ -public class RegattaLeaderboardImpl extends AbstractLeaderboardImpl implements RegattaLeaderboard { - private static final long serialVersionUID = 2370461218294770084L; - private final Regatta regatta; - - public RegattaLeaderboardImpl(Regatta regatta, ThresholdBasedResultDiscardingRule resultDiscardingRule) { - super(resultDiscardingRule); - this.regatta = regatta; - regatta.addRaceColumnListener(this); - } - - /** - * Updates the display name of this regatta leaderboard so that the regatta's name is no longer used as the default name. - */ - @Override - public void setName(String newName) { - setDisplayName(newName); - } - - @Override - public Regatta getRegatta() { - return regatta; - } - - @Override - public String getName() { - return getRegatta().getName(); - } - - @Override - public Iterable getRaceColumns() { - List result = new ArrayList(); - for (Series series : getRegatta().getSeries()) { - for (RaceColumn raceColumn : series.getRaceColumns()) { - result.add(raceColumn); - } - } - return result; - } - - @Override - public RaceColumnInSeries getRaceColumnByName(String columnName) { - return (RaceColumnInSeriesImpl) super.getRaceColumnByName(columnName); - } - - @Override - public ScoringScheme getScoringScheme() { - return regatta.getScoringScheme(); - } - - @Override - public CourseArea getDefaultCourseArea() { - return regatta.getDefaultCourseArea(); - } - - /** - * If the regatta' series {@link Regatta#definesSeriesDiscardThresholds() define} their own result discarding rules, this leaderboard uses - * a composite result discarding rule of type {@link PerSeriesResultDiscardingRuleImpl} that contains discards within each series according - * to the series' discarding rules. Otherwise, the default cross-leaderboard result discarding rule is obtained from the super class - * implementation. - */ - @Override - public ResultDiscardingRule getResultDiscardingRule() { - if (regatta.definesSeriesDiscardThresholds()) { - return new PerSeriesResultDiscardingRuleImpl(regatta); - } else { - return super.getResultDiscardingRule(); - } - } - - /** - * Delegates to {@link Regatta#getAllCompetitors()} which is expected to deliver all competitors from this - * leaderboard's regatta which includes those belonging to {@link RaceDefinition races} - * {@link Regatta#getAllRaces() belonging to the regatta} as well as competitors listed on the regatta's - * {@link RegattaLog}. - */ - @Override - public Iterable getAllCompetitors() { - return regatta.getAllCompetitors(); - } - - @Override - public IsRegattaLike getRegattaLike() { - return regatta; - } -} +package com.sap.sailing.domain.leaderboard.impl; + +import java.util.ArrayList; +import java.util.List; + +import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnInSeries; +import com.sap.sailing.domain.base.RaceColumnListener; +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.impl.RaceColumnInSeriesImpl; +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sailing.domain.regattalike.IsRegattaLike; +import com.sap.sailing.domain.tracking.TrackedRace; + +/** + * A leaderboard that is based on the definition of a {@link Regatta} with its {@link Series} and {@link Fleet}. The regatta + * leaderboard listens to its {@link Regatta} as a {@link RaceColumnListener} and forwards all link/unlink events for + * {@link TrackedRace}s being linked to / unlinked from race columns to all {@link RaceColumnListener}s subscribed with + * this leaderboard. + * + * @author Axel Uhl (D043530) + * + */ +public class RegattaLeaderboardImpl extends AbstractLeaderboardImpl implements RegattaLeaderboard { + private static final long serialVersionUID = 2370461218294770084L; + private final Regatta regatta; + + public RegattaLeaderboardImpl(Regatta regatta, ThresholdBasedResultDiscardingRule resultDiscardingRule) { + super(resultDiscardingRule); + this.regatta = regatta; + regatta.addRaceColumnListener(this); + } + + /** + * Updates the display name of this regatta leaderboard so that the regatta's name is no longer used as the default name. + */ + @Override + public void setName(String newName) { + setDisplayName(newName); + } + + @Override + public Regatta getRegatta() { + return regatta; + } + + @Override + public String getName() { + return getRegatta().getName(); + } + + @Override + public Iterable getRaceColumns() { + List result = new ArrayList(); + for (Series series : getRegatta().getSeries()) { + for (RaceColumn raceColumn : series.getRaceColumns()) { + result.add(raceColumn); + } + } + return result; + } + + @Override + public RaceColumnInSeries getRaceColumnByName(String columnName) { + return (RaceColumnInSeriesImpl) super.getRaceColumnByName(columnName); + } + + @Override + public ScoringScheme getScoringScheme() { + return regatta.getScoringScheme(); + } + + @Override + public CourseArea getDefaultCourseArea() { + return regatta.getDefaultCourseArea(); + } + + /** + * If the regatta' series {@link Regatta#definesSeriesDiscardThresholds() define} their own result discarding rules, this leaderboard uses + * a composite result discarding rule of type {@link PerSeriesResultDiscardingRuleImpl} that contains discards within each series according + * to the series' discarding rules. Otherwise, the default cross-leaderboard result discarding rule is obtained from the super class + * implementation. + */ + @Override + public ResultDiscardingRule getResultDiscardingRule() { + if (regatta.definesSeriesDiscardThresholds()) { + return new PerSeriesResultDiscardingRuleImpl(regatta); + } else { + return super.getResultDiscardingRule(); + } + } + + /** + * Delegates to {@link Regatta#getAllCompetitors()} which is expected to deliver all competitors from this + * leaderboard's regatta which includes those belonging to {@link RaceDefinition races} + * {@link Regatta#getAllRaces() belonging to the regatta} as well as competitors listed on the regatta's + * {@link RegattaLog}. + */ + @Override + public Iterable getAllCompetitors() { + return regatta.getAllCompetitors(); + } + + @Override + public IsRegattaLike getRegattaLike() { + return regatta; + } + + @Override + protected LeaderboardType getLeaderboardType() { + return LeaderboardType.RegattaLeaderboard; + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/FlexibleMetaLeaderboard.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/FlexibleMetaLeaderboard.java index a7de5eb4c40..e7f9c04de2d 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/FlexibleMetaLeaderboard.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/FlexibleMetaLeaderboard.java @@ -1,64 +1,70 @@ -package com.sap.sailing.domain.leaderboard.meta; - -import java.util.ArrayList; -import java.util.List; - -import com.sap.sailing.domain.base.CourseArea; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.leaderboard.ScoringScheme; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sse.concurrent.LockUtil; -import com.sap.sse.concurrent.NamedReentrantReadWriteLock; - -public class FlexibleMetaLeaderboard extends AbstractMetaLeaderboard { - private static final long serialVersionUID = 789076326144062944L; - - private final List leaderboards; - - private final NamedReentrantReadWriteLock leaderboardsLock; - - public FlexibleMetaLeaderboard(String name, ScoringScheme scoringScheme, - ThresholdBasedResultDiscardingRule resultDiscardingRule) { - super(name, scoringScheme, resultDiscardingRule); - leaderboards = new ArrayList(); - leaderboardsLock = new NamedReentrantReadWriteLock("leaderboards collection of "+FlexibleMetaLeaderboard.class.getSimpleName()+" "+getName(), - /* fair */ false); - } - - @Override - public Iterable getLeaderboards() { - LockUtil.lockForRead(leaderboardsLock); - try { - return new ArrayList(leaderboards); - } finally { - LockUtil.unlockAfterRead(leaderboardsLock); - } - } - - public void addLeaderboard(Leaderboard leaderboard) { - LockUtil.lockForWrite(leaderboardsLock); - try { - leaderboards.add(leaderboard); - registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); - getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(getColumnForLeaderboard(leaderboard)); - } finally { - LockUtil.unlockAfterWrite(leaderboardsLock); - } - } - - public void removeLeaderboard(Leaderboard leaderboard) { - LockUtil.lockForWrite(leaderboardsLock); - try { - leaderboards.remove(leaderboard); - getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(getColumnForLeaderboard(leaderboard)); - unregisterScoreCorrectionChangeForwarder(leaderboard); - } finally { - LockUtil.unlockAfterWrite(leaderboardsLock); - } - } - - @Override - public CourseArea getDefaultCourseArea() { - return null; - } -} +package com.sap.sailing.domain.leaderboard.meta; + +import java.util.ArrayList; +import java.util.List; + +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sse.concurrent.LockUtil; +import com.sap.sse.concurrent.NamedReentrantReadWriteLock; + +public class FlexibleMetaLeaderboard extends AbstractMetaLeaderboard { + private static final long serialVersionUID = 789076326144062944L; + + private final List leaderboards; + + private final NamedReentrantReadWriteLock leaderboardsLock; + + public FlexibleMetaLeaderboard(String name, ScoringScheme scoringScheme, + ThresholdBasedResultDiscardingRule resultDiscardingRule) { + super(name, scoringScheme, resultDiscardingRule); + leaderboards = new ArrayList(); + leaderboardsLock = new NamedReentrantReadWriteLock("leaderboards collection of "+FlexibleMetaLeaderboard.class.getSimpleName()+" "+getName(), + /* fair */ false); + } + + @Override + public Iterable getLeaderboards() { + LockUtil.lockForRead(leaderboardsLock); + try { + return new ArrayList(leaderboards); + } finally { + LockUtil.unlockAfterRead(leaderboardsLock); + } + } + + public void addLeaderboard(Leaderboard leaderboard) { + LockUtil.lockForWrite(leaderboardsLock); + try { + leaderboards.add(leaderboard); + registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); + getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(getColumnForLeaderboard(leaderboard)); + } finally { + LockUtil.unlockAfterWrite(leaderboardsLock); + } + } + + public void removeLeaderboard(Leaderboard leaderboard) { + LockUtil.lockForWrite(leaderboardsLock); + try { + leaderboards.remove(leaderboard); + getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(getColumnForLeaderboard(leaderboard)); + unregisterScoreCorrectionChangeForwarder(leaderboard); + } finally { + LockUtil.unlockAfterWrite(leaderboardsLock); + } + } + + @Override + public CourseArea getDefaultCourseArea() { + return null; + } + + @Override + protected LeaderboardType getLeaderboardType() { + return LeaderboardType.FlexibleMetaLeaderboard; + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/LeaderboardGroupMetaLeaderboard.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/LeaderboardGroupMetaLeaderboard.java index 73860657959..673e1ee5120 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/LeaderboardGroupMetaLeaderboard.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/LeaderboardGroupMetaLeaderboard.java @@ -1,92 +1,98 @@ -package com.sap.sailing.domain.leaderboard.meta; - -import com.sap.sailing.domain.base.CourseArea; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceColumnListener; -import com.sap.sailing.domain.base.impl.TrackedRaces; -import com.sap.sailing.domain.common.LeaderboardNameConstants; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.leaderboard.LeaderboardGroup; -import com.sap.sailing.domain.leaderboard.LeaderboardGroupListener; -import com.sap.sailing.domain.leaderboard.ScoringScheme; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sailing.domain.tracking.TrackedRace; - -/** - * A meta leaderboard which considers all regular leaderboards of a {@link LeaderboardGroup}. To stay up to date, this - * meta leaderboard registers itself as a {@link LeaderboardGroupListener} on the leaderboard group it represents. This - * way, whenever a leaderboard is added to or removed from the group, this meta leaderboard can in turn inform its - * {@link RaceColumnListener}s about the impact this change has on the set of {@link TrackedRaces} and therefore the - * competitors attached to this meta leaderboard. - *

- * - * After an object of this type has been de-serialized, and after all objects referenced by it (leaderboard, leaderboard - * group) have been initialized, {@link #registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards()} must be called to - * ensure that score corrections are propagated as desired. This is because score correction listeners are "transient" - * and are as such not serialized. Note: calling {@link #registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards()} - * during readObject or readResolve is not possible because the object graph hasn't been fully - * initialized yet when those methods are called. Hence, the structures for registering the score correction listeners - * are not yet in place at that time. - * - * @author Axel Uhl (d043530) - * - */ -public class LeaderboardGroupMetaLeaderboard extends AbstractMetaLeaderboard implements LeaderboardGroupListener, RaceColumnListener { - private static final long serialVersionUID = 8087872002175528002L; - - private final LeaderboardGroup leaderboardGroup; - - public LeaderboardGroupMetaLeaderboard(LeaderboardGroup leaderboardGroup, ScoringScheme scoringScheme, - ThresholdBasedResultDiscardingRule resultDiscardingRule) { - super(leaderboardGroup.getName() + " " + LeaderboardNameConstants.OVERALL, scoringScheme, resultDiscardingRule); - this.leaderboardGroup = leaderboardGroup; - leaderboardGroup.addLeaderboardGroupListener(this); - registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards(); - } - - public void registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards() { - for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { - registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); - } - } - - @Override - public Iterable getLeaderboards() { - return leaderboardGroup.getLeaderboards(); - } - - @Override - public void leaderboardAdded(LeaderboardGroup group, Leaderboard leaderboard) { - registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); - for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - for (Fleet fleet : raceColumn.getFleets()) { - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - if (trackedRace != null) { - getRaceColumnListeners().notifyListenersAboutTrackedRaceLinked(raceColumn, fleet, trackedRace); - } - } - } - getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(getColumnForLeaderboard(leaderboard)); - } - - @Override - public void leaderboardRemoved(LeaderboardGroup group, Leaderboard leaderboard) { - leaderboard.removeRaceColumnListener(this); - unregisterScoreCorrectionChangeForwarder(leaderboard); - for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - for (Fleet fleet : raceColumn.getFleets()) { - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - if (trackedRace != null) { - getRaceColumnListeners().notifyListenersAboutTrackedRaceUnlinked(raceColumn, fleet, trackedRace); - } - } - } - getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(getColumnForLeaderboard(leaderboard)); - } - - @Override - public CourseArea getDefaultCourseArea() { - return null; - } -} +package com.sap.sailing.domain.leaderboard.meta; + +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnListener; +import com.sap.sailing.domain.base.impl.TrackedRaces; +import com.sap.sailing.domain.common.LeaderboardNameConstants; +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.LeaderboardGroup; +import com.sap.sailing.domain.leaderboard.LeaderboardGroupListener; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sailing.domain.tracking.TrackedRace; + +/** + * A meta leaderboard which considers all regular leaderboards of a {@link LeaderboardGroup}. To stay up to date, this + * meta leaderboard registers itself as a {@link LeaderboardGroupListener} on the leaderboard group it represents. This + * way, whenever a leaderboard is added to or removed from the group, this meta leaderboard can in turn inform its + * {@link RaceColumnListener}s about the impact this change has on the set of {@link TrackedRaces} and therefore the + * competitors attached to this meta leaderboard. + *

+ * + * After an object of this type has been de-serialized, and after all objects referenced by it (leaderboard, leaderboard + * group) have been initialized, {@link #registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards()} must be called to + * ensure that score corrections are propagated as desired. This is because score correction listeners are "transient" + * and are as such not serialized. Note: calling {@link #registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards()} + * during readObject or readResolve is not possible because the object graph hasn't been fully + * initialized yet when those methods are called. Hence, the structures for registering the score correction listeners + * are not yet in place at that time. + * + * @author Axel Uhl (d043530) + * + */ +public class LeaderboardGroupMetaLeaderboard extends AbstractMetaLeaderboard implements LeaderboardGroupListener, RaceColumnListener { + private static final long serialVersionUID = 8087872002175528002L; + + private final LeaderboardGroup leaderboardGroup; + + public LeaderboardGroupMetaLeaderboard(LeaderboardGroup leaderboardGroup, ScoringScheme scoringScheme, + ThresholdBasedResultDiscardingRule resultDiscardingRule) { + super(leaderboardGroup.getName() + " " + LeaderboardNameConstants.OVERALL, scoringScheme, resultDiscardingRule); + this.leaderboardGroup = leaderboardGroup; + leaderboardGroup.addLeaderboardGroupListener(this); + registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards(); + } + + public void registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards() { + for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { + registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); + } + } + + @Override + public Iterable getLeaderboards() { + return leaderboardGroup.getLeaderboards(); + } + + @Override + public void leaderboardAdded(LeaderboardGroup group, Leaderboard leaderboard) { + registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + for (Fleet fleet : raceColumn.getFleets()) { + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + getRaceColumnListeners().notifyListenersAboutTrackedRaceLinked(raceColumn, fleet, trackedRace); + } + } + } + getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(getColumnForLeaderboard(leaderboard)); + } + + @Override + public void leaderboardRemoved(LeaderboardGroup group, Leaderboard leaderboard) { + leaderboard.removeRaceColumnListener(this); + unregisterScoreCorrectionChangeForwarder(leaderboard); + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + for (Fleet fleet : raceColumn.getFleets()) { + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + getRaceColumnListeners().notifyListenersAboutTrackedRaceUnlinked(raceColumn, fleet, trackedRace); + } + } + } + getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(getColumnForLeaderboard(leaderboard)); + } + + @Override + public CourseArea getDefaultCourseArea() { + return null; + } + + @Override + protected LeaderboardType getLeaderboardType() { + return LeaderboardType.RegattaMetaLeaderboard; + } +} 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 db9cbbe03cc..b9d964fc98e 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 @@ -13,7 +13,6 @@ import java.util.ConcurrentModificationException; import java.util.Date; import java.util.HashMap; import java.util.HashSet; -import java.util.IdentityHashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; @@ -154,6 +153,7 @@ import com.sap.sse.common.Util.Pair; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.concurrent.LockUtil; import com.sap.sse.concurrent.NamedReentrantReadWriteLock; +import com.sap.sse.util.IdentityWrapper; import com.sap.sse.util.SmartFutureCache; import com.sap.sse.util.SmartFutureCache.AbstractCacheUpdater; import com.sap.sse.util.SmartFutureCache.EmptyUpdateInterval; @@ -183,7 +183,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl * By default, all wind sources are used, none are excluded. However, e.g., for performance reasons, particular wind * sources such as the track-based estimation wind source, may be excluded by adding them to this set. */ - private final Set windSourcesToExclude; + private final ConcurrentHashMap windSourcesToExclude; /** * Keeps the oldest timestamp that is fed into this tracked race, either from a boat fix, a mark fix, a race @@ -269,7 +269,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl */ private transient SmartFutureCache>, EmptyUpdateInterval> maneuverCache; - private transient Map> directionFromStartToNextMarkCache; + private transient ConcurrentHashMap> directionFromStartToNextMarkCache; protected final MarkPassingCalculator markPassingCalculator; @@ -354,7 +354,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl */ private final NamedReentrantReadWriteLock loadingFromGPSFixStoreLock; - private final Map, NamedReentrantReadWriteLock> locksForMarkPassings; + private final ConcurrentHashMap>, NamedReentrantReadWriteLock> locksForMarkPassings; /** * Caches wind requests for a few seconds to accelerate access in live mode @@ -412,7 +412,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl rankingMetric = rankingMetricConstructor.apply(this); raceStates = new WeakHashMap<>(); shortTimeWindCache = new ShortTimeWindCache(this, millisecondsOverWhichToAverageWind / 2); - locksForMarkPassings = new IdentityHashMap<>(); + locksForMarkPassings = new ConcurrentHashMap>, NamedReentrantReadWriteLock>(); attachedRaceLogs = new ConcurrentHashMap<>(); attachedRegattaLogs = new ConcurrentHashMap<>(); attachedRaceExecutionOrderProviders = new ConcurrentHashMap<>(); @@ -424,8 +424,8 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl + race.getName(), /* fair */ false); this.cacheInvalidationTimerLock = new Object(); this.updateCount = 0; - this.windSourcesToExclude = new HashSet(); - this.directionFromStartToNextMarkCache = new HashMap>(); + this.windSourcesToExclude = new ConcurrentHashMap<>(); + this.directionFromStartToNextMarkCache = new ConcurrentHashMap>(); this.millisecondsOverWhichToAverageSpeed = millisecondsOverWhichToAverageSpeed; this.delayToLiveInMillis = delayToLiveInMillis; this.startToNextMarkCacheInvalidationListeners = new ConcurrentHashMap(); @@ -624,7 +624,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl gpsFixStore = EmptyGPSFixStore.INSTANCE; competitorRankings = createCompetitorRankingsCache(); competitorRankingsLocks = createCompetitorRankingsLockMap(); - directionFromStartToNextMarkCache = new HashMap>(); + directionFromStartToNextMarkCache = new ConcurrentHashMap<>(); crossTrackErrorCache = new CrossTrackErrorCache(this); crossTrackErrorCache.invalidate(); maneuverCache = createManeuverCache(); @@ -1146,11 +1146,11 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl int indexOfWaypoint = getRace().getCourse().getIndexOfWaypoint(startOfLeg); if (indexOfWaypoint == -1) { throw new IllegalArgumentException("Waypoint " + startOfLeg + " not found in " + getRace().getCourse()); - } else if (indexOfWaypoint == Util.size(getRace().getCourse().getWaypoints()) - 1) { + } else if (indexOfWaypoint == getRace().getCourse().getNumberOfWaypoints() - 1) { throw new IllegalArgumentException("Waypoint " + startOfLeg + " isn't start of any leg in " + getRace().getCourse()); } - return trackedLegs.get(race.getCourse().getLegs().get(indexOfWaypoint)); + return trackedLegs.get(race.getCourse().getLeg(indexOfWaypoint)); } finally { getRace().getCourse().unlockAfterRead(); } @@ -1679,24 +1679,20 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl @Override public Set getWindSourcesToExclude() { - synchronized (windSourcesToExclude) { - return Collections.unmodifiableSet(windSourcesToExclude); - } + return Collections.unmodifiableSet(windSourcesToExclude.keySet()); } @Override public void setWindSourcesToExclude(Iterable windSourcesToExclude) { - Set old = new HashSet<>(this.windSourcesToExclude); - synchronized (this.windSourcesToExclude) { - LockUtil.lockForRead(getSerializationLock()); - try { - this.windSourcesToExclude.clear(); - for (WindSource windSourceToExclude : windSourcesToExclude) { - this.windSourcesToExclude.add(windSourceToExclude); - } - } finally { - LockUtil.unlockAfterRead(getSerializationLock()); + Set old = new HashSet<>(getWindSourcesToExclude()); + LockUtil.lockForRead(getSerializationLock()); + try { + this.windSourcesToExclude.clear(); + for (WindSource windSourceToExclude : windSourcesToExclude) { + this.windSourcesToExclude.put(windSourceToExclude, this); } + } finally { + LockUtil.unlockAfterRead(getSerializationLock()); } if (!old.equals(this.windSourcesToExclude)) { clearAllCachesExceptManeuvers(); @@ -1742,30 +1738,33 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl public Wind getDirectionFromStartToNextMark(final TimePoint at) { Future future; FutureTask newFuture = null; - synchronized (directionFromStartToNextMarkCache) { - future = directionFromStartToNextMarkCache.get(at); - if (future == null) { - newFuture = new FutureTask(new Callable() { - @Override - public Wind call() { - Wind result; - Leg firstLeg = getRace().getCourse().getFirstLeg(); - if (firstLeg != null) { - Position firstLegEnd = getApproximatePosition(firstLeg.getTo(), at); - Position firstLegStart = getApproximatePosition(firstLeg.getFrom(), at); - if (firstLegStart != null && firstLegEnd != null) { - result = new WindImpl(firstLegStart, at, new KnotSpeedWithBearingImpl(0.0, - firstLegEnd.getBearingGreatCircle(firstLegStart))); + future = directionFromStartToNextMarkCache.get(at); + if (future == null) { + synchronized (directionFromStartToNextMarkCache) { + future = directionFromStartToNextMarkCache.get(at); + if (future == null) { + newFuture = new FutureTask(new Callable() { + @Override + public Wind call() { + Wind result; + Leg firstLeg = getRace().getCourse().getFirstLeg(); + if (firstLeg != null) { + Position firstLegEnd = getApproximatePosition(firstLeg.getTo(), at); + Position firstLegStart = getApproximatePosition(firstLeg.getFrom(), at); + if (firstLegStart != null && firstLegEnd != null) { + result = new WindImpl(firstLegStart, at, new KnotSpeedWithBearingImpl(0.0, + firstLegEnd.getBearingGreatCircle(firstLegStart))); + } else { + result = null; + } } else { result = null; } - } else { - result = null; + return result; } - return result; - } - }); - directionFromStartToNextMarkCache.put(at, newFuture); + }); + directionFromStartToNextMarkCache.put(at, newFuture); + } } } if (newFuture != null) { @@ -2062,15 +2061,19 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl } protected NamedReentrantReadWriteLock getMarkPassingsLock(Iterable markPassings) { - synchronized (locksForMarkPassings) { - NamedReentrantReadWriteLock lock = locksForMarkPassings.get(markPassings); - if (lock == null) { - lock = new NamedReentrantReadWriteLock("mark passings lock for tracked race " + getRace().getName(), /* fair */ - false); - locksForMarkPassings.put(markPassings, lock); + final IdentityWrapper> markPassingsIdentity = new IdentityWrapper<>(markPassings); + NamedReentrantReadWriteLock lock = locksForMarkPassings.get(markPassingsIdentity); + if (lock == null) { + synchronized (locksForMarkPassings) { + lock = locksForMarkPassings.get(markPassingsIdentity); + if (lock == null) { + lock = new NamedReentrantReadWriteLock( + "mark passings lock for tracked race " + getRace().getName(), /* fair */false); + locksForMarkPassings.put(markPassingsIdentity, lock); + } } - return lock; } + return lock; } private void updateStartToNextMarkCacheInvalidationCacheListenersAfterWaypointRemoved(int zeroBasedIndex, 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 f9c0928e151..c6ef63bfb48 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 @@ -95,7 +95,7 @@ public class GwtTestCaseColumnToggling extends GWTTestCase { + TracTracConnectionConstants.PORT_TUNNEL_STORED : "tcp://" + TracTracConnectionConstants.HOST_NAME + ":" + TracTracConnectionConstants.PORT_STORED, COURSE_DESIGN_UPDATE_URI, - false, false, /* simulateWithStartTimeNow */ false, /* ignoreTracTracMarkPassings */ false, TRACTRAC_USERNAME, TRACTRAC_PASSWORD, new AsyncCallback() { + false, false, /* offsetToStartTimeOfSimulatedRace */ null, /* ignoreTracTracMarkPassings */ false, TRACTRAC_USERNAME, TRACTRAC_PASSWORD, new AsyncCallback() { @Override public void onFailure(Throwable caught) { diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/LeaderboardPanelMock.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/LeaderboardPanelMock.java index 4999b3d2c9f..38c0fbdb061 100644 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/LeaderboardPanelMock.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/LeaderboardPanelMock.java @@ -20,7 +20,7 @@ public class LeaderboardPanelMock extends LeaderboardPanel { String leaderboardName, ErrorReporter errorReporter, StringMessages stringMessages) { super(sailingService, new AsyncActionsExecutor(), LeaderboardSettingsFactory.getInstance().createNewDefaultSettings( - /* racesToShow */null, /* namesOfRacesToShow */ null, null, /* autoExpandFirstRace */ false, /* showRegattaRank */ true), new CompetitorSelectionModel( + /* racesToShow */null, /* namesOfRacesToShow */ null, null, /* autoExpandFirstRace */ false, /* showRegattaRank */ true, /* showCompetitorSailIdColumn */ true, /* showCompetitorFullNameColumn */ true), new CompetitorSelectionModel( /* hasMultiSelection */true), leaderboardName, errorReporter, stringMessages, new UserAgentDetails("gecko1_8"), /* showRaceDetails */ true); } 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 38860207a43..a6123d06096 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 @@ -1,162 +1,162 @@ -package com.sap.sailing.gwt.ui.test; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.UUID; - -import org.junit.Before; -import org.junit.Test; - -import com.sap.sailing.domain.common.ScoringSchemeType; -import com.sap.sailing.domain.common.dto.FleetDTO; -import com.sap.sailing.domain.common.dto.LeaderboardDTO; -import com.sap.sailing.domain.common.dto.RaceColumnDTO; -import com.sap.sailing.gwt.ui.server.SailingServiceImpl; - -public class TestColumnSwapping { - - private LeaderboardDTO lb = null; - private static final String LEADERBOARDNAME = "test"; - private static final String DEFAULT_FLEET_NAME = "Default"; - private static final FleetDTO DEFAULT_FLEET = new FleetDTO(DEFAULT_FLEET_NAME, /* ordering */ 0, /* color */ null); - private SailingServiceImpl service; - private LeaderboardDTO leaderboardOriginalDTO; - private LeaderboardDTO leaderboardDTO; - private Collection leglist; - private Date leaderboardCreationDate; - private SailingServiceImpl sailingService = null; - - // Test-Data - private final String TEST_LEADERBOARD_NAME = "test_board"; - private final String[] races = { "1", "2", "3" }; - private final boolean[] isMedalRace = { false, false, true }; - - @Before - public void prepareColumnSwapping() { - service = new SailingServiceImplMock(); - int[] disc = { 5, 8, 9, 0, 7, 5, 43 }; - service.removeLeaderboard(LEADERBOARDNAME); - service.createFlexibleLeaderboard(LEADERBOARDNAME, null, disc, ScoringSchemeType.LOW_POINT, null); - service.addColumnToLeaderboard("Race1", LEADERBOARDNAME, true); - service.addColumnToLeaderboard("Race2", LEADERBOARDNAME, true); - service.addColumnToLeaderboard("Race3", LEADERBOARDNAME, true); - leglist = new ArrayList(); - leglist.add("Race1"); - leglist.add("Race2"); - leglist.add("Race3"); - leaderboardCreationDate = new Date(); - try { - // get Leaderboard with name and current date - leaderboardOriginalDTO = new LeaderboardDTO(null, null, ScoringSchemeType.LOW_POINT, /* higherScoreIsBetter */ false, new LeaderboardDTO.UUIDGenerator() { - @Override - public String generateRandomUUID() { - return UUID.randomUUID().toString(); - } - }, /* hasOverallDetails */ false); - leaderboardOriginalDTO.addRace("Race1", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null); - leaderboardOriginalDTO.addRace("Race3", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null); - leaderboardOriginalDTO.addRace("Race2", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null); - } catch (Exception e) { - e.printStackTrace(); - } - } - - @Test - public void testColumnSwapping() { - testColumnSwappingFabian(); - testLeaderBoardDTOMethods(); - testSailingService(); - } - - @Test - public void testSailingService() { - sailingService = new SailingServiceImplMock(); - assertNotNull("Sailingservice != NULL", sailingService); - int td[] = { 5, 8 }; - sailingService.removeLeaderboard(TEST_LEADERBOARD_NAME); - sailingService.createFlexibleLeaderboard(TEST_LEADERBOARD_NAME, null, td, ScoringSchemeType.LOW_POINT, null); - for (int i = 0; i < races.length; i++) - sailingService.addColumnToLeaderboard(races[i], TEST_LEADERBOARD_NAME, isMedalRace[i]); - sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); - sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); - sailingService.moveLeaderboardColumnUp(TEST_LEADERBOARD_NAME, races[2]); - sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[0], true); - sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[2], false); - try { - for (int i = 0; i < races.length; i++) - sailingService.addColumnToLeaderboard(races[i], TEST_LEADERBOARD_NAME, isMedalRace[i]); - sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); - sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); - 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, /* addOverallDetails */ true, /* previous leaderboard ID */ null, - /* fillNetPointsUncorrected */ false).getLeaderboardDTO(/* previousVersion */ null); - } catch (Exception e) { - // e.printStackTrace(); - fail(e.getLocalizedMessage()); - } - assertNotNull("LeaderboardDTO != NULL", lb); - - // asserted data - String[] raceNames = new String[] { "3", "2", "1" }; - boolean[] medalRace = { true, false, false }; - - for (int i = 0; i < lb.getRaceList().size(); i++) { - assertEquals("Race[" + i + "] == " + raceNames[i], raceNames[i], lb.getRaceList().get(i).getName()); - assertTrue("Race[" + i + "] is " + (medalRace[i] ? "" : "no ") + "medalrace.", - lb.raceIsMedalRace(races[i]) == medalRace[i]); - } - } - - @Test - public void testLeaderBoardDTOMethods() { - lb = new LeaderboardDTO(null, null, ScoringSchemeType.LOW_POINT, /* higherScoreIsBetter */ false, new LeaderboardDTO.UUIDGenerator() { - @Override - public String generateRandomUUID() { - return UUID.randomUUID().toString(); - } - }, /* hasOverallDetails */ false); - assertNotNull("Leaderboard != NULL", lb); - lb.addRace("1", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, false, null, null); - lb.addRace("2", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, false, null, null); - lb.addRace("3", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null); - lb.moveRaceDown("1"); - String[] s = new String[] { "2", "1", "3" }; - for (int i = 0; i < lb.getRaceList().size(); i++) { - assertEquals("Race[" + i + "] = " + s[i], s[i], lb.getRaceList().get(i).getName()); - } - } - - @Test - public void testColumnSwappingFabian() { - service.moveLeaderboardColumnUp(LEADERBOARDNAME, "Race3"); - try { - leaderboardDTO = service.getLeaderboardByName(LEADERBOARDNAME, leaderboardCreationDate, leglist, /* addOverallDetails */ - true, /* previous leaderboard ID */null, /* fillNetPointsUncorrected */false).getLeaderboardDTO(/* previousVersion */null); - } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - // check if leaderboardDTO an dleaderboardOriginalDTO same - List leaderboardList = leaderboardDTO.getRaceList(); - List leaderboardOriginalList = leaderboardOriginalDTO.getRaceList(); - - // ??????? assert races in list - assertArrayEquals(leaderboardList.toArray(), leaderboardOriginalList.toArray()); - - for (RaceColumnDTO raceDTO : leaderboardOriginalList) { - assert leaderboardDTO.raceIsMedalRace(raceDTO.getRaceColumnName()) == leaderboardOriginalDTO.raceIsMedalRace(raceDTO.getRaceColumnName()); - assert leaderboardDTO.raceIsTracked(raceDTO.getRaceColumnName()) == leaderboardOriginalDTO.raceIsTracked(raceDTO.getRaceColumnName()); - } - } -} +package com.sap.sailing.gwt.ui.test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sailing.domain.common.dto.FleetDTO; +import com.sap.sailing.domain.common.dto.LeaderboardDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.gwt.ui.server.SailingServiceImpl; + +public class TestColumnSwapping { + + private LeaderboardDTO lb = null; + private static final String LEADERBOARDNAME = "test"; + private static final String DEFAULT_FLEET_NAME = "Default"; + private static final FleetDTO DEFAULT_FLEET = new FleetDTO(DEFAULT_FLEET_NAME, /* ordering */ 0, /* color */ null); + private SailingServiceImpl service; + private LeaderboardDTO leaderboardOriginalDTO; + private LeaderboardDTO leaderboardDTO; + private Collection leglist; + private Date leaderboardCreationDate; + private SailingServiceImpl sailingService = null; + + // Test-Data + private final String TEST_LEADERBOARD_NAME = "test_board"; + private final String[] races = { "1", "2", "3" }; + private final boolean[] isMedalRace = { false, false, true }; + + @Before + public void prepareColumnSwapping() { + service = new SailingServiceImplMock(); + int[] disc = { 5, 8, 9, 0, 7, 5, 43 }; + service.removeLeaderboard(LEADERBOARDNAME); + service.createFlexibleLeaderboard(LEADERBOARDNAME, null, disc, ScoringSchemeType.LOW_POINT, null); + service.addColumnToLeaderboard("Race1", LEADERBOARDNAME, true); + service.addColumnToLeaderboard("Race2", LEADERBOARDNAME, true); + service.addColumnToLeaderboard("Race3", LEADERBOARDNAME, true); + leglist = new ArrayList(); + leglist.add("Race1"); + leglist.add("Race2"); + leglist.add("Race3"); + leaderboardCreationDate = new Date(); + try { + // get Leaderboard with name and current date + leaderboardOriginalDTO = new LeaderboardDTO(null, null, ScoringSchemeType.LOW_POINT, /* higherScoreIsBetter */ false, new LeaderboardDTO.UUIDGenerator() { + @Override + public String generateRandomUUID() { + return UUID.randomUUID().toString(); + } + }, /* hasOverallDetails */ false); + leaderboardOriginalDTO.addRace("Race1", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null, false); + leaderboardOriginalDTO.addRace("Race3", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null, false); + leaderboardOriginalDTO.addRace("Race2", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null, false); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Test + public void testColumnSwapping() { + testColumnSwappingFabian(); + testLeaderBoardDTOMethods(); + testSailingService(); + } + + @Test + public void testSailingService() { + sailingService = new SailingServiceImplMock(); + assertNotNull("Sailingservice != NULL", sailingService); + int td[] = { 5, 8 }; + sailingService.removeLeaderboard(TEST_LEADERBOARD_NAME); + sailingService.createFlexibleLeaderboard(TEST_LEADERBOARD_NAME, null, td, ScoringSchemeType.LOW_POINT, null); + for (int i = 0; i < races.length; i++) + sailingService.addColumnToLeaderboard(races[i], TEST_LEADERBOARD_NAME, isMedalRace[i]); + sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); + sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); + sailingService.moveLeaderboardColumnUp(TEST_LEADERBOARD_NAME, races[2]); + sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[0], true); + sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[2], false); + try { + for (int i = 0; i < races.length; i++) + sailingService.addColumnToLeaderboard(races[i], TEST_LEADERBOARD_NAME, isMedalRace[i]); + sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); + sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); + 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, /* addOverallDetails */ true, /* previous leaderboard ID */ null, + /* fillNetPointsUncorrected */ false).getLeaderboardDTO(/* previousVersion */ null); + } catch (Exception e) { + // e.printStackTrace(); + fail(e.getLocalizedMessage()); + } + assertNotNull("LeaderboardDTO != NULL", lb); + + // asserted data + String[] raceNames = new String[] { "3", "2", "1" }; + boolean[] medalRace = { true, false, false }; + + for (int i = 0; i < lb.getRaceList().size(); i++) { + assertEquals("Race[" + i + "] == " + raceNames[i], raceNames[i], lb.getRaceList().get(i).getName()); + assertTrue("Race[" + i + "] is " + (medalRace[i] ? "" : "no ") + "medalrace.", + lb.raceIsMedalRace(races[i]) == medalRace[i]); + } + } + + @Test + public void testLeaderBoardDTOMethods() { + lb = new LeaderboardDTO(null, null, ScoringSchemeType.LOW_POINT, /* higherScoreIsBetter */ false, new LeaderboardDTO.UUIDGenerator() { + @Override + public String generateRandomUUID() { + return UUID.randomUUID().toString(); + } + }, /* hasOverallDetails */ false); + assertNotNull("Leaderboard != NULL", lb); + lb.addRace("1", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, false, null, null, false); + lb.addRace("2", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, false, null, null, false); + lb.addRace("3", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null, false); + lb.moveRaceDown("1"); + String[] s = new String[] { "2", "1", "3" }; + for (int i = 0; i < lb.getRaceList().size(); i++) { + assertEquals("Race[" + i + "] = " + s[i], s[i], lb.getRaceList().get(i).getName()); + } + } + + @Test + public void testColumnSwappingFabian() { + service.moveLeaderboardColumnUp(LEADERBOARDNAME, "Race3"); + try { + leaderboardDTO = service.getLeaderboardByName(LEADERBOARDNAME, leaderboardCreationDate, leglist, /* addOverallDetails */ + true, /* previous leaderboard ID */null, /* fillNetPointsUncorrected */false).getLeaderboardDTO(/* previousVersion */null); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + // check if leaderboardDTO an dleaderboardOriginalDTO same + List leaderboardList = leaderboardDTO.getRaceList(); + List leaderboardOriginalList = leaderboardOriginalDTO.getRaceList(); + + // ??????? assert races in list + assertArrayEquals(leaderboardList.toArray(), leaderboardOriginalList.toArray()); + + for (RaceColumnDTO raceDTO : leaderboardOriginalList) { + assert leaderboardDTO.raceIsMedalRace(raceDTO.getRaceColumnName()) == leaderboardOriginalDTO.raceIsMedalRace(raceDTO.getRaceColumnName()); + assert leaderboardDTO.raceIsTracked(raceDTO.getRaceColumnName()) == leaderboardOriginalDTO.raceIsTracked(raceDTO.getRaceColumnName()); + } + } +} 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 d6863f1498a..0f7d005f58f 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 @@ -3,7 +3,9 @@ encoding//src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages.prop encoding//src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_de.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_ru.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_zh.properties=UTF-8 +encoding//src/main/java/com/sap/sailing/gwt/server/HomeServiceUtil.java=utf-8 encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties=UTF-8 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 encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties=UTF-8 +encoding//src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/AbstractResultsPresenter.java=utf-8 diff --git a/java/com.sap.sailing.gwt.ui/Leaderboard.css b/java/com.sap.sailing.gwt.ui/Leaderboard.css index 8ab68acf9b9..ca7ab51f790 100755 --- a/java/com.sap.sailing.gwt.ui/Leaderboard.css +++ b/java/com.sap.sailing.gwt.ui/Leaderboard.css @@ -98,13 +98,13 @@ body, table td, select, button { padding: 49px 0 0 80px; width: 300px; color: #7c7c7c; - background: url(images/important-message.png) left center no-repeat; + background: url(images/important-message.png) left center no-repeat; } .LeaderboardHeader { position: relative; margin: 0 auto; - width: 400px; + width: 100%; text-align: center; font-family: 'Open Sans', Arial, Verdana, sans-serif; font-size: 18px; diff --git a/java/com.sap.sailing.gwt.ui/RaceBoard.css b/java/com.sap.sailing.gwt.ui/RaceBoard.css index 0e7cd4f163c..0175c2a97dd 100644 --- a/java/com.sap.sailing.gwt.ui/RaceBoard.css +++ b/java/com.sap.sailing.gwt.ui/RaceBoard.css @@ -6,6 +6,13 @@ height: 0; clear: both; visibility: hidden; + + RegattaRaceInformation-Header + RaceName-Label + RaceSeriesAndFleet-Label + + RegattaAndRaceTime-Header??? + RegattaName-Anchor */ @@ -87,6 +94,14 @@ table { padding-top: 10px; padding-left: 10px; } +.CombinedWindPanelParentDiv.indentsmall, +.TrueNorthIndicatorPanelParentDiv.indentsmall { + padding-left: 30px; +} +.CombinedWindPanelParentDiv.indentbig, +.TrueNorthIndicatorPanelParentDiv.indentbig { + padding-left: 45px; +} .MapSimulationLegendParentDiv { padding-bottom: 10px; @@ -573,7 +588,8 @@ img.openColumn { url(images/viewicons/@1x/img/SAP_RV_Leaderboard_GHOSTED.png), url(images/arrows_top_black_small.png); background-repeat: no-repeat, no-repeat; - background-position: 4% 50%, 96% 50% + background-position: 4% 50%, 96% 50%; + background-position: 5px center, calc(100% - 6px) center; } .gwt-SplitLayoutPanel-EastToggleButton-Closed-leaderboard { @@ -582,7 +598,8 @@ img.openColumn { url(images/viewicons/@1x/img/SAP_RV_Leaderboard_INACTIVE.png), url(images/arrows_bottom_black_small.png); background-repeat: no-repeat, no-repeat; - background-position: 4% 50%, 96% 50% + background-position: 4% 50%, 96% 50%; + background-position: 5px center, calc(100% - 6px) center; } .gwt-SplitLayoutPanel-NorthSouthToggleButton-Closed-media, @@ -664,6 +681,32 @@ img.openColumn { margin-top: 5px; } +.compactHeader .RegattaRaceInformation-Header { + left: 110px; +} +.compactHeader .RegattaRaceInformation-Header .RaceName-Label { + display: block; + font-size: 16px; + padding: 2px 0; +} +.compactHeader .RegattaRaceInformation-Header .RaceSeriesAndFleet-Label { + display: block; + margin-top: 5px; + white-space: nowrap; +} +.compactHeader .RegattaAndRaceTime-Header { + top: 11px; +} +.compactHeader .RegattaAndRaceTime-Header .RegattaName-Anchor { + width: 115px; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + white-space: nowrap; + padding: 2px 0 2px 20px; + background-position: 4px -1px; +} + .VideoPopup-Close-Button { border: none; } diff --git a/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch b/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch index af074b8e973..5356d07a245 100644 --- a/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch +++ b/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch @@ -17,7 +17,7 @@ - + @@ -73,7 +73,7 @@ - + diff --git a/java/com.sap.sailing.gwt.ui/SailingGWT.launch b/java/com.sap.sailing.gwt.ui/SailingGWT.launch index 4f94e217a34..b677fe5c939 100755 --- a/java/com.sap.sailing.gwt.ui/SailingGWT.launch +++ b/java/com.sap.sailing.gwt.ui/SailingGWT.launch @@ -16,7 +16,7 @@ - + @@ -70,7 +70,7 @@ - + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/player/AutoPlayController.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/player/AutoPlayController.java index 5cfe4fe6697..1b9a18e2bd7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/player/AutoPlayController.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/player/AutoPlayController.java @@ -107,7 +107,7 @@ public class AutoPlayController implements RaceTimesInfoProviderListener { asyncActionsExecutor = new AsyncActionsExecutor(); showWindChart = false; leaderboard = null; - leaderboardSettings = LeaderboardSettingsFactory.getInstance().createNewDefaultSettings(null, null, null, /* autoExpandFirstRace */ false, /* showRegattaRank */ true); + leaderboardSettings = LeaderboardSettingsFactory.getInstance().createNewDefaultSettings(null, null, null, /* autoExpandFirstRace */ false, /* showRegattaRank */ true, /* showCompetitorSailIdColumn */ true, /* showCompetitorFullNameColumn */ true); leaderboardTimer = new Timer(PlayModes.Live, /* delayBetweenAutoAdvancesInMilliseconds */1000l); leaderboardTimer.setLivePlayDelayInMillis(delayToLiveInMillis); @@ -170,10 +170,10 @@ public class AutoPlayController implements RaceTimesInfoProviderListener { RaceSelectionModel raceSelectionModel = new RaceSelectionModel(); List singletonList = Collections.singletonList(raceToShow); raceSelectionModel.setSelection(singletonList); - RaceBoardPanel raceBoardPanel = new RaceBoardPanel(sailingService, mediaService, userService, asyncActionsExecutor, - competitorsAndTheirBoats, raceboardTimer, raceSelectionModel, leaderboardName, null, /* event */null, raceboardViewConfig, - errorReporter, StringMessages.INSTANCE, userAgent, raceTimesInfoProvider, /* showMapControls */false); + competitorsAndTheirBoats, raceboardTimer, raceSelectionModel, leaderboardName, null, /* event */ null, raceboardViewConfig, + errorReporter, StringMessages.INSTANCE, userAgent, raceTimesInfoProvider, /* showMapControls */ false, + /* isScreenLargeEnoughToOfferChartSupport */ true); return raceBoardPanel; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/AbstractLeaderboardFullscreenViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/AbstractLeaderboardFullscreenViewer.java index 6e1baf957dd..16ce11cff0f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/AbstractLeaderboardFullscreenViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/AbstractLeaderboardFullscreenViewer.java @@ -1,5 +1,6 @@ package com.sap.sailing.gwt.home.desktop.partials.old; +import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.user.client.ui.FlowPanel; @@ -8,6 +9,9 @@ import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.gwt.home.shared.partials.fullscreen.FullscreenContainer; import com.sap.sse.common.Color; +import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicator; +import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicatorResources; +import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; public abstract class AbstractLeaderboardFullscreenViewer extends FullscreenContainer implements LeaderboardDelegate { @@ -15,14 +19,18 @@ public abstract class AbstractLeaderboardFullscreenViewer exte private final Image autoRefreshControl = new Image("images/home/reload.svg"); private final Image settingsControl = new Image("images/home/settings.svg"); + private final static BusyIndicatorResources RESOURCES = GWT.create(BusyIndicatorResources.class); + private final Label lastScoringUpdateTime = new Label(); private final Label lastScoringUpdateText = new Label(); protected final Label lastScoringComment = new Label(); protected final Label scoringScheme = new Label(); + protected final BusyIndicator busyIndicator = new SimpleBusyIndicator(false, 0.9f, RESOURCES.busyIndicatorCircleInverted()); public AbstractLeaderboardFullscreenViewer() { showLogo(); showBorder(); + addToolbarBusyIndicator(busyIndicator); addToolbarInfo(createPanel(lastScoringUpdateText, lastScoringUpdateTime)); addToolbarAction(autoRefreshControl); addToolbarAction(settingsControl); @@ -88,4 +96,13 @@ public abstract class AbstractLeaderboardFullscreenViewer exte return scoringScheme.getElement(); } + @Override + public Element getBusyIndicatorElement() { + return busyIndicator.getElement(); + } + + @Override + public void setBusyState(boolean isBusy) { + busyIndicator.setBusy(isBusy); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboard.gss b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboard.gss index 7aee44185c8..0eaa3b013d7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboard.gss +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboard.gss @@ -106,6 +106,11 @@ background-color: #f0ab00; } +.regattaleaderboard_meta_busyindicator { + display: inline-block; + margin: 0 0.666666666666667em; +} + .regattaleaderboard_meta_liverace { color: red; font-weight: 600; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboardResources.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboardResources.java index fee597cac95..90bef62da36 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboardResources.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboardResources.java @@ -30,6 +30,7 @@ public interface EventRegattaLeaderboardResources extends ClientBundle { String regattaleaderboard_meta_update_type(); String regattaleaderboard_meta_scoring_timestamp(); String regattaleaderboard_meta_update_timestamp(); + String regattaleaderboard_meta_busyindicator(); String regattaleaderboard_meta_scoring_text(); String regattaleaderboard_meta_update_text(); String regattaleaderboard_meta_reload(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/LeaderboardDelegate.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/LeaderboardDelegate.java index c1523e63036..fd6ee999728 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/LeaderboardDelegate.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/LeaderboardDelegate.java @@ -10,6 +10,8 @@ public interface LeaderboardDelegate { void setLeaderboardPanel(T leaderboardPanel); + void setBusyState(boolean isBusy); + Widget getAutoRefreshControl(); Widget getSettingsControl(); @@ -24,4 +26,5 @@ public interface LeaderboardDelegate { Element getScoringSchemeElement(); + Element getBusyIndicatorElement(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java index cc8fecd8c30..2fb55ac6429 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java @@ -31,12 +31,14 @@ import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettings; import com.sap.sailing.gwt.ui.leaderboard.ScoringSchemeTypeFormatter; +import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicator; +import com.sap.sse.gwt.client.controls.busyindicator.BusyStateChangeListener; 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.shared.components.SettingsDialog; -public class OldLeaderboard extends Composite { +public class OldLeaderboard extends Composite implements BusyStateChangeListener { private static OldLeaderboardUiBinder uiBinder = GWT.create(OldLeaderboardUiBinder.class); interface OldLeaderboardUiBinder extends UiBinder { @@ -52,6 +54,7 @@ public class OldLeaderboard extends Composite { @UiField DivElement lastScoringCommentDiv; @UiField DivElement scoringSchemeDiv; @UiField DivElement hasLiveRaceDiv; + @UiField BusyIndicator busyIndicator; @UiField EventRegattaLeaderboardResources local_res; private LeaderboardPanel leaderboardPanel; @@ -166,6 +169,7 @@ public class OldLeaderboard extends Composite { this.autoRefreshTimer = timer; this.leaderboardPanel = leaderboardPanel; oldLeaderboardPanel.add(leaderboardPanel); + leaderboardPanel.addBusyStateChangeListener(this); } public void updatedLeaderboard(LeaderboardDTO leaderboard) { @@ -217,8 +221,17 @@ public class OldLeaderboard extends Composite { } lastScoringUpdateTimeDiv.getStyle().setVisibility(Visibility.HIDDEN); } - + + @Override + public void onBusyStateChange(boolean busyState) { + busyIndicator.setBusy(busyState); + if(delegate != null) { + delegate.setBusyState(busyState); + } + } + public interface OldLeaderboardDelegate extends LeaderboardDelegate { Element getHasLiveRaceElement(); } + } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.ui.xml index 593f26846a6..a65cf15c133 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.ui.xml @@ -1,16 +1,18 @@ - +

-
+
-
+
+
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java index 0d822d22293..0cb578288a0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java @@ -3,7 +3,7 @@ package com.sap.sailing.gwt.home.desktop.partials.old.multileaderboard; import java.util.Date; import com.google.gwt.core.client.GWT; -import com.google.gwt.dom.client.ParagraphElement; +import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.Style.Overflow; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.Style.Visibility; @@ -27,15 +27,19 @@ import com.sap.sailing.gwt.home.desktop.partials.old.LeaderboardDelegate; import com.sap.sailing.gwt.ui.client.DebugIdHelper; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; +import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettings; import com.sap.sailing.gwt.ui.leaderboard.MultiLeaderboardPanel; import com.sap.sailing.gwt.ui.leaderboard.ScoringSchemeTypeFormatter; +import com.sap.sailing.gwt.ui.leaderboard.SelectedLeaderboardChangeListener; +import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicator; +import com.sap.sse.gwt.client.controls.busyindicator.BusyStateChangeListener; 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.shared.components.SettingsDialog; -public class OldMultiLeaderboard extends Composite { +public class OldMultiLeaderboard extends Composite implements SelectedLeaderboardChangeListener, BusyStateChangeListener { private static OldMultiLeaderboardUiBinder uiBinder = GWT.create(OldMultiLeaderboardUiBinder.class); interface OldMultiLeaderboardUiBinder extends UiBinder { @@ -46,16 +50,18 @@ public class OldMultiLeaderboard extends Composite { @UiField Anchor settingsAnchor; @UiField Anchor autoRefreshAnchor; @UiField Anchor fullscreenAnchor; - @UiField ParagraphElement lastScoringUpdateTimeDiv; - @UiField ParagraphElement lastScoringUpdateTextDiv; - @UiField ParagraphElement lastScoringCommentDiv; - @UiField ParagraphElement scoringSchemeDiv; + @UiField DivElement lastScoringUpdateTimeDiv; + @UiField DivElement lastScoringUpdateTextDiv; + @UiField DivElement lastScoringCommentDiv; + @UiField DivElement scoringSchemeDiv; + @UiField BusyIndicator busyIndicator; @UiField EventRegattaLeaderboardResources local_res; private MultiLeaderboardPanel multiLeaderboardPanel; private Timer autoRefreshTimer; private final OldMultiLeaderboardDelegate delegate; - + private LeaderboardPanel lastSelectedLeaderboardPanel; + public OldMultiLeaderboard() { this(null); } @@ -69,6 +75,7 @@ public class OldMultiLeaderboard extends Composite { fullscreenAnchor.setTitle(StringMessages.INSTANCE.openFullscreenView()); this.delegate = delegate; this.setupFullscreenDelegate(); + lastSelectedLeaderboardPanel = null; } private void setupFullscreenDelegate() { @@ -163,6 +170,7 @@ public class OldMultiLeaderboard extends Composite { public void setMultiLeaderboard(MultiLeaderboardPanel multiLeaderboardPanel, final Timer timer) { this.autoRefreshTimer = timer; this.multiLeaderboardPanel = multiLeaderboardPanel; + this.multiLeaderboardPanel.addSelectedLeaderboardChangeListener(this); oldMultiLeaderboardPanel.add(multiLeaderboardPanel); } @@ -198,6 +206,23 @@ public class OldMultiLeaderboard extends Composite { } } } + + @Override + public void onBusyStateChange(boolean busyState) { + busyIndicator.setBusy(busyState); + if(delegate != null) { + delegate.setBusyState(busyState); + } + } + + @Override + public void onSelectedLeaderboardChanged(LeaderboardPanel selectedLeaderboard) { + if(lastSelectedLeaderboardPanel != null) { + lastSelectedLeaderboardPanel.removeBusyStateChangeListener(this); + } + selectedLeaderboard.addBusyStateChangeListener(this); + lastSelectedLeaderboardPanel = selectedLeaderboard; + } public interface OldMultiLeaderboardDelegate extends LeaderboardDelegate{ } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.ui.xml index 737bef4a01d..55a4cf060f9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.ui.xml @@ -1,23 +1,25 @@ - +
-

-

+
+
+
-

-

+
+
-
+
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/utils/EventParamUtils.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/utils/EventParamUtils.java index d10d5917c86..e2c3f612111 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/utils/EventParamUtils.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/utils/EventParamUtils.java @@ -108,7 +108,7 @@ public class EventParamUtils { .createNewDefaultSettings(null, null, /* overallDetails */overallDetails, null, /* autoExpandFirstRace */false, refreshIntervalMillis, numberOfLastRacesToShow, - raceColumnSelectionStrategy); + raceColumnSelectionStrategy, /* showCompetitorSailIdColumns */ true, /*showCompetitorFullNameColumn*/ true); } return result; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventActivity.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventActivity.java index d0867a28588..4edafe69a6b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventActivity.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventActivity.java @@ -44,7 +44,6 @@ import com.sap.sailing.gwt.home.shared.places.fakeseries.SeriesDefaultPlace; import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.refresh.ErrorAndBusyClientFactory; -import com.sap.sailing.gwt.ui.raceboard.RaceBoardViewConfiguration; import com.sap.sailing.gwt.ui.shared.util.NullSafeComparableComparator; public abstract class AbstractEventActivity extends AbstractActivity implements Presenter { @@ -248,7 +247,6 @@ public abstract class AbstractEventActivity ex } linkParams.put("raceName", trackedRaceName); linkParams.put("regattaName", regattaName); - linkParams.put(RaceBoardViewConfiguration.PARAM_VIEW_MODE, "simple"); // TODO this must only be forwarded if there is a logged-on user // linkParams.put(RaceBoardViewConfiguration.PARAM_CAN_REPLAY_DURING_LIVE_RACES, "true"); return linkParams; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/ExperimentalFeatures.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/ExperimentalFeatures.java index dcfbd043171..53366e8b6d0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/ExperimentalFeatures.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/ExperimentalFeatures.java @@ -54,7 +54,7 @@ public class ExperimentalFeatures { /** * Enables/disables the link to the race board on mobile races view (competition format), in case of a tracked race */ - public static final boolean ENABLE_RACE_VIEWER_LINK_ON_MOBILE = false; + public static final boolean ENABLE_RACE_VIEWER_LINK_ON_MOBILE = true; /** * Provide selection to filter regattas by boat category on mobile multiregatta event overview */ diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/busy/BusyViewImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/busy/BusyViewImpl.java index 27c28089e2c..fb3434000bb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/busy/BusyViewImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/busy/BusyViewImpl.java @@ -8,7 +8,8 @@ public class BusyViewImpl extends Composite implements BusyView { public BusyViewImpl() { BusyViewResources.INSTANCE.css().ensureInjected(); - initWidget(new SimpleBusyIndicator(true, 1.0f, "", BusyViewResources.INSTANCE.css().busy())); + SimpleBusyIndicator busyIndicator = new SimpleBusyIndicator(true, 1.0f); + busyIndicator.setImageStyleClass(BusyViewResources.INSTANCE.css().busy()); + initWidget(busyIndicator); } - } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.java index 998b211f053..8315428bfd4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.java @@ -37,6 +37,7 @@ public class FullscreenContainer { String popup(); String toolbarInfo(); String toolbarAction(); + String toolbarBusyIndicator(); String content(); String contentBorder(); } @@ -88,7 +89,12 @@ public class FullscreenContainer { widget.addStyleName(style.toolbarAction()); toolbarUi.add(widget); } - + + public void addToolbarBusyIndicator(Widget widget) { + widget.addStyleName(style.toolbarBusyIndicator()); + toolbarUi.add(widget); + } + public void addToolbarInfo(Widget widget) { widget.addStyleName(style.toolbarInfo()); toolbarUi.add(widget); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.ui.xml index bc1f63ed53d..2fcfd38df77 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.ui.xml @@ -50,6 +50,13 @@ cursor: pointer; padding: 10px; } + .toolbarBusyIndicator { + display: inline-block; + height: 50px; + width: auto; + padding: 10px; + vertical-align: top; + } .toolbarAction:hover { background-color: #d79900; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewPanel.java index 4d4de5230a4..fc2180d3170 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/regattaoverview/client/RegattaOverviewPanel.java @@ -237,7 +237,7 @@ public class RegattaOverviewPanel extends SimplePanel { if (leaderboardsTabPanel != null) { if (showLeaderboard) { final CompetitorSelectionModel competitorSelectionProvider = new CompetitorSelectionModel(/* hasMultiSelection */ true); - final LeaderboardSettings leaderboardSettings = LeaderboardSettingsFactory.getInstance().createNewDefaultSettings(null, null, null, /* autoExpandFirstRace */ false, /* showRegattaRank */ true); + final LeaderboardSettings leaderboardSettings = LeaderboardSettingsFactory.getInstance().createNewDefaultSettings(null, null, null, /* autoExpandFirstRace */ false, /* showRegattaRank */ true, /* showCompetitorSailIdColumn */ true, /* showCompetitorFullNameColumn */ true); sailingService.getLeaderboardsByEvent(eventDTO, new MarkedAsyncCallback>( new AsyncCallback>() { @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AddWindFixDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AddWindFixDialog.java index 3416428eaba..8f26b6d6801 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AddWindFixDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AddWindFixDialog.java @@ -40,18 +40,18 @@ public class AddWindFixDialog extends DataEntryDialog { if(windDTO.trueWindSpeedInKnots == null) { errorMessage = stringMessages.pleaseEnterAValue(); } else if(windDTO.trueWindSpeedInKnots != null && (windDTO.trueWindSpeedInKnots < 0.0 || windDTO.trueWindSpeedInKnots > 100.0)) { - errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.speedInKnots(), "0", "100"); + errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.speedInKnots(), 0, 100); } else if(windDTO.trueWindFromDeg == null){ errorMessage = stringMessages.pleaseEnterAValue(); } else if(windDTO.trueWindFromDeg != null && (windDTO.trueWindFromDeg < 0.0 || windDTO.trueWindFromDeg > 360.0)){ - errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.fromDeg(), "0", "360"); + errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.fromDeg(), 0, 360); } else if(windDTO.measureTimepoint == null) { errorMessage = stringMessages.pleaseEnterAValue(); } else if(windDTO.position != null) { if(windDTO.position.getLatDeg() < -90.0 || windDTO.position.getLatDeg() > 90.0){ - errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.latitude(), "-90", "90"); + errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.latitude(), -90, 90); } else if(windDTO.position.getLngDeg() < -180.0 || windDTO.position.getLngDeg() > 180.0){ - errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.longitude(), "-180", "180"); + errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.longitude(), -180, 180); } } 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 06507532f71..a4405fcaf29 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 @@ -225,7 +225,7 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane declinationCheckbox.setValue(true); trackableRacesPanel.add(declinationCheckbox); - final CheckBox simulateWithStartTimeNow = new CheckBox(stringMessages.simulateWithStartTimeNow()); + final CheckBox simulateWithStartTimeNow = new CheckBox(stringMessages.simulateAsLiveRace()); simulateWithStartTimeNow.setWordWrap(false); simulateWithStartTimeNow.setValue(false); trackableRacesPanel.add(simulateWithStartTimeNow); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java index 040c531102c..b9f26f50610 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java @@ -24,6 +24,7 @@ import com.google.gwt.user.client.ui.CaptionPanel; import com.google.gwt.user.client.ui.CheckBox; import com.google.gwt.user.client.ui.FlexTable; import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter; +import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Grid; import com.google.gwt.user.client.ui.HTMLTable.ColumnFormatter; import com.google.gwt.user.client.ui.HorizontalPanel; @@ -44,6 +45,7 @@ import com.sap.sailing.gwt.ui.client.shared.controls.SelectionCheckboxColumn; import com.sap.sailing.gwt.ui.shared.RegattaDTO; import com.sap.sailing.gwt.ui.shared.TracTracConfigurationDTO; import com.sap.sailing.gwt.ui.shared.TracTracRaceRecordDTO; +import com.sap.sse.common.Duration; import com.sap.sse.common.util.NaturalComparator; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.async.MarkedAsyncCallback; @@ -83,7 +85,8 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { private LabeledAbstractFilterablePanel racesFilterablePanel; private CellTable racesTable; - + private static final String ZERO_AS_STRING = "0"; + public TracTracEventManagementPanel(final SailingServiceAsync sailingService, ErrorReporter errorReporter, RegattaRefresher regattaRefresher, StringMessages stringMessages) { super(sailingService, regattaRefresher, errorReporter, new RaceSelectionModel(), true, stringMessages); @@ -229,17 +232,12 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { protected HorizontalPanel createRacesPanel() { HorizontalPanel racesPanel = new HorizontalPanel(); - CaptionPanel trackableRacesPanel = createTrackableRacesPanel(); - racesPanel.add(trackableRacesPanel); racesPanel.setCellWidth(trackableRacesPanel, "50%"); - CaptionPanel trackedRacesPanel = createTrackedRacesPanel(); - racesPanel.add(trackedRacesPanel); racesPanel.setCellWidth(trackedRacesPanel, "50%"); - return racesPanel; } @@ -276,10 +274,32 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { correctWindCheckBox.setWordWrap(false); correctWindCheckBox.setValue(Boolean.TRUE); - final CheckBox simulateWithStartTimeNowCheckBox = new CheckBox(stringMessages.simulateWithStartTimeNow()); + final TextBox offsetToStartTimeOfSimulatedRaceTextBox = new TextBox(); + offsetToStartTimeOfSimulatedRaceTextBox.setWidth("40px"); + offsetToStartTimeOfSimulatedRaceTextBox.setEnabled(false); + offsetToStartTimeOfSimulatedRaceTextBox.setValue(ZERO_AS_STRING); + + final CheckBox simulateWithStartTimeNowCheckBox = new CheckBox(stringMessages.simulateAsLiveRace()); simulateWithStartTimeNowCheckBox.ensureDebugId("SimulateWithStartTimeNowCheckBox"); simulateWithStartTimeNowCheckBox.setWordWrap(false); simulateWithStartTimeNowCheckBox.setValue(Boolean.FALSE); + simulateWithStartTimeNowCheckBox.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + offsetToStartTimeOfSimulatedRaceTextBox.setEnabled(simulateWithStartTimeNowCheckBox.getValue()); + offsetToStartTimeOfSimulatedRaceTextBox.setFocus(simulateWithStartTimeNowCheckBox.getValue()); + } + }); + + final FlowPanel simulateAsLiveRacePanel = new FlowPanel(); + simulateAsLiveRacePanel.add(simulateWithStartTimeNowCheckBox); + + final Label offsetToStartLabel = new Label(stringMessages.simulateWithOffset()); + + final HorizontalPanel simulateWithOffsetPanel = new HorizontalPanel(); + simulateWithOffsetPanel.add(offsetToStartLabel); + simulateWithOffsetPanel.add(offsetToStartTimeOfSimulatedRaceTextBox); + final CheckBox ignoreTracTracMarkPassingsCheckbox = new CheckBox(stringMessages.useInternalAlgorithm()); ignoreTracTracMarkPassingsCheckbox.setWordWrap(false); ignoreTracTracMarkPassingsCheckbox.setValue(Boolean.FALSE); @@ -287,8 +307,9 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { layoutTable.setWidget(1, 0, trackSettingsLabel); layoutTable.setWidget(1, 1, trackWindCheckBox); layoutTable.setWidget(2, 1, correctWindCheckBox); - layoutTable.setWidget(3, 1, simulateWithStartTimeNowCheckBox); - layoutTable.setWidget(4, 1, ignoreTracTracMarkPassingsCheckbox); + layoutTable.setWidget(3, 1, simulateAsLiveRacePanel); + layoutTable.setWidget(4, 1, simulateWithOffsetPanel); + layoutTable.setWidget(5, 1, ignoreTracTracMarkPassingsCheckbox); // Filter Label racesFilterLabel = new Label(stringMessages.filterRacesByName() + ":"); @@ -310,8 +331,8 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { } }; racesFilterablePanel.getTextBox().ensureDebugId("TrackableRacesFilterTextBox"); - layoutTable.setWidget(5, 0, racesFilterLabel); - layoutTable.setWidget(5, 1, racesFilterablePanel); + layoutTable.setWidget(6, 0, racesFilterLabel); + layoutTable.setWidget(6, 1, racesFilterablePanel); // Races TextColumn regattaNameColumn = new TextColumn() { @@ -357,7 +378,6 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { } }; raceVisibilityColumn.setSortable(true); - SelectionCheckboxColumn selectionCheckboxColumn = new SelectionCheckboxColumn(tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell()) { @Override @@ -381,26 +401,24 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { raceNameColumn, boatClassColumn, raceStartTrackingColumn, raceStatusColumn)); racesTable.setSelectionModel(selectionCheckboxColumn.getSelectionModel(), selectionCheckboxColumn.getSelectionManager()); racesTable.setWidth("100%"); - raceList.addDataDisplay(racesTable); - layoutTable.setWidget(6, 0, racesTable); cellFormatter.setColSpan(6, 0, 2); - Button startTrackingButton = new Button(stringMessages.startTracking()); startTrackingButton.ensureDebugId("StartTrackingButton"); startTrackingButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { + Duration offsetToStartTimeOfSimulatedRace = null; + if(simulateWithStartTimeNowCheckBox.getValue().booleanValue()) { + offsetToStartTimeOfSimulatedRace = getMillisecondsDurationFromMinutesAsString(offsetToStartTimeOfSimulatedRaceTextBox.getValue()); + } trackSelectedRaces(trackWindCheckBox.getValue(), correctWindCheckBox.getValue(), - simulateWithStartTimeNowCheckBox.getValue(), ignoreTracTracMarkPassingsCheckbox.getValue()); + offsetToStartTimeOfSimulatedRace, ignoreTracTracMarkPassingsCheckbox.getValue()); } }); - layoutTable.setWidget(7, 1, startTrackingButton); - trackableRacesPanel.setContentWidget(layoutTable); - return trackableRacesPanel; } @@ -462,6 +480,17 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { return boatClassNames.substring(0, boatClassNames.length() - 2); } + + private Duration getMillisecondsDurationFromMinutesAsString(String minutesAsString) { + Duration result = null; + if (minutesAsString != null) { + Double minutesAsDouble = Double.parseDouble(minutesAsString); + if (minutesAsDouble != null) { + result = Duration.ONE_MINUTE.times(minutesAsDouble.longValue()); + } + } + return result; + } private void fillConfigurations() { this.sailingService.getPreviousTracTracConfigurations(new MarkedAsyncCallback>( @@ -551,7 +580,7 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { })); } - private void trackSelectedRaces(boolean trackWind, boolean correctWind, final boolean simulateWithStartTimeNow, boolean ignoreTracTracMarkPassings) { + private void trackSelectedRaces(boolean trackWind, boolean correctWind, final Duration offsetToStartTimeOfSimulatedRace, boolean ignoreTracTracMarkPassings) { String liveURI = liveURITextBox.getValue(); String storedURI = storedURITextBox.getValue(); String courseDesignUpdateURI = tracTracUpdateURITextBox.getValue(); @@ -577,7 +606,7 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { } if (checkBoatClassOK(selectedRegatta, selectedRaces)) { sailingService.trackWithTracTrac(regattaIdentifier, selectedRaces, liveURI, storedURI, - courseDesignUpdateURI, trackWind, correctWind, simulateWithStartTimeNow, ignoreTracTracMarkPassings, tractracUsername, + courseDesignUpdateURI, trackWind, correctWind, offsetToStartTimeOfSimulatedRace, ignoreTracTracMarkPassings, tractracUsername, tractracPassword, new MarkedAsyncCallback(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/RaceTimePanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceTimePanel.java index ef6d167bb08..45e0b3b8de5 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 @@ -30,8 +30,8 @@ public class RaceTimePanel extends TimePanel implements R private boolean redrawAllMarkersPendingForMinMaxBeingInitialized; public RaceTimePanel(Timer timer, TimeRangeWithZoomProvider timeRangeProvider, StringMessages stringMessages, - RaceTimesInfoProvider raceTimesInfoProvider, boolean canReplayWhileLiveIsPossible) { - super(timer, timeRangeProvider, stringMessages, canReplayWhileLiveIsPossible); + RaceTimesInfoProvider raceTimesInfoProvider, boolean canReplayWhileLiveIsPossible, boolean isScreenLargeEnoughToOfferChartSupport) { + super(timer, timeRangeProvider, stringMessages, canReplayWhileLiveIsPossible, isScreenLargeEnoughToOfferChartSupport); this.raceTimesInfoProvider = raceTimesInfoProvider; selectedRace = null; autoAdjustPlayMode = true; 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 e8c206f0285..594417853c8 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 @@ -86,6 +86,7 @@ import com.sap.sailing.gwt.ui.shared.TrackFileImportDeviceIdentifierDTO; import com.sap.sailing.gwt.ui.shared.VenueDTO; import com.sap.sailing.gwt.ui.shared.WindDTO; import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; +import com.sap.sse.common.Duration; import com.sap.sse.common.NoCorrespondingServiceRegisteredException; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; @@ -116,7 +117,7 @@ public interface SailingService extends RemoteService, FileStorageManagementGwtS void trackWithTracTrac(RegattaIdentifier regattaToAddTo, Iterable rrs, String liveURI, String storedURI, String courseDesignUpdateURI, boolean trackWind, boolean correctWindByDeclination, - boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, String tracTracUsername, String tracTracPassword) throws Exception; + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, String tracTracUsername, String tracTracPassword) throws Exception; void trackWithSwissTiming(RegattaIdentifier regattaToAddTo, Iterable rrs, String hostname, int port, boolean trackWind, boolean correctWindByDeclination, boolean useInternalMarkPassingAlgorithm) throws Exception; 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 dc673186e14..ebcfba02b56 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 @@ -78,6 +78,7 @@ import com.sap.sailing.gwt.ui.shared.TrackFileImportDeviceIdentifierDTO; import com.sap.sailing.gwt.ui.shared.VenueDTO; import com.sap.sailing.gwt.ui.shared.WindDTO; import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; @@ -114,10 +115,10 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan * @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 + * @param offsetToStartTimeOfSimulatedRace + * if not null, 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 + * events received such that they seem to be sent in "real-time" plus the offsetToStartTimeOfSimulatedRace. So, more or less the time points * attached to the events sent to the receivers will again approximate the wall time. * @param useInternalMarkPassingAlgorithm * whether or not to ignore the TracTrac-provided mark passings; if true, a separate mark @@ -128,7 +129,7 @@ public interface SailingServiceAsync extends ServerInfoRetriever, FileStorageMan */ void trackWithTracTrac(RegattaIdentifier regattaToAddTo, Iterable rrs, String liveURI, String storedURI, String courseDesignUpdateURI, boolean trackWind, boolean correctWindByDeclination, - boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, String tracTracUsername, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, String tracTracUsername, String tracTracPassword, AsyncCallback callback); void trackWithSwissTiming(RegattaIdentifier regattaToAddTo, Iterable rrs, 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 5c82ce097aa..251c04e89b0 100644 --- 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 @@ -361,7 +361,8 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, c String successfullyUpdatedScores(); String errorUpdatingScoresForLeaderboard(String leaderboardName, String message); String maneuverTypesToShowWhenCompetitorIsClicked(); - String simulateWithStartTimeNow(); + String simulateAsLiveRace(); + String simulateWithOffset(); String boatClassDoesNotMatchSelectedRegatta(String boatClass); String regattaExistForSelectedBoatClass(); String reload(); @@ -436,7 +437,8 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, c String overallDetailsToShow(); String hhmmssUnit(); String actionAddWindData(); - String valueMustBeBetweenMinMax(String name, String minValue, String maxValue); + String valueMustBeBetweenMinMax(String name, int minValue, int maxValue); + String valueMustBeGreaterThan(String name, String minValue); String optional(); String pleaseEnterAValue(); String latitude(); @@ -503,6 +505,8 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, c String sideToWhichMarkAtLegStartWasRounded(); String raceIsLive(String raceName); String racesAreLive(String raceNames); + String regattaIsLive(String regattaName); + String regattasAreLive(String regattaNames); String scoringSchemeHighPointFirstGetsOne(); String scoringSchemeHighPointFirstGetsTen(); String knotsUnit(); @@ -1444,4 +1448,6 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, c String requiresValidRegatta(); String couldNotObtainRace(String regattaLikeName, String raceColumnName, String fleetName, String technicalErrorMessage); String errorTryingToCreateEmbeddedMap(String message); + String transparentBufferLineOnHover(); + String bufferLineStrokeWeight(); } 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 7c9b921b440..3361c95bf25 100644 --- 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 @@ -371,7 +371,8 @@ apply=Apply successfullyUpdatedScores=Successfully updated scores errorUpdatingScoresForLeaderboard=Error updating scores for leaderboard {0}: {1} maneuverTypesToShowWhenCompetitorIsClicked=Maneuver types to show when competitor is clicked -simulateWithStartTimeNow=Simulate with "now" as start time +simulateAsLiveRace=Simulate as live race +simulateWithOffset=Offset before start in minutes: boatClassDoesNotMatchSelectedRegatta=The selected races contain boat classes which are not the same as the boat class ''{0}'' of the selected regatta. No races will be loaded. regattaExistForSelectedBoatClass=There is at least one regatta for the selected boat classes. Really create default regatta(s) for those race(s)? reload=Reload @@ -456,6 +457,7 @@ totalAverageSpeedOverGroundTooltip=Average speed over ground across the regatta. hhmmssUnit=hh:mm:ss actionAddWindData=Add wind data valueMustBeBetweenMinMax=The value of ''{0}'' must be between {1} and {2}. +valueMustBeGreaterThan=The value of ''{0}'' must be greater than {1}. optional=optional pleaseEnterAValue=Please enter a value. latitude=Latitude @@ -527,6 +529,8 @@ sideToWhichMarkAtLegStartWasRounded=Mark rounded to sideToWhichMarkAtLegStartWasRoundedTooltip=Side to which the mark at leg start was rounded. raceIsLive=Race {0} is live racesAreLive=Races {0} are live +regattaIsLive=Regatta {0} is live +regattasAreLive=Regattas {0} are live knotsUnit=kts knotsValue={0,number,#0.0} kts knotsRange={0,number,#0.0} - {1,number,#0.0} kts @@ -1410,4 +1414,6 @@ selectALeaderboardGroup=Select a leaderboard group... pleaseSelect=Please select requiresValidRegatta=This page requires a valid regatta, race column and fleet name to identify the race to show. couldNotObtainRace=Could not obtain a race with name {1} for fleet {2} for a regatta with name {0}: {3} -errorTryingToCreateEmbeddedMap=Error trying to create the embedded map: {0} \ No newline at end of file +errorTryingToCreateEmbeddedMap=Error trying to create the embedded map: {0} +transparentBufferLineOnHover=Transparent buffer lines on hover of helplines +bufferLineStrokeWeight=Buffer line stroke weight 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 189a589b9f1..04cd149154d 100644 --- 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 @@ -372,7 +372,8 @@ apply=Übernehmen successfullyUpdatedScores=Ergebnisse erfolgreich aktualisiert errorUpdatingScoresForLeaderboard=Fehler beim Aktualisieren der Ergebnisse für {0}: {1} maneuverTypesToShowWhenCompetitorIsClicked=Anzuzeigende Manövertypen für angeklickte Teilnehmer -simulateWithStartTimeNow=Simulieren mit "jetzt" als Startzeit +simulateAsLiveRace=Simuliere als Live Rennen +simulateWithOffset=Zeit bis zum Start in Minuten: boatClassDoesNotMatchSelectedRegatta=Die ausgewählten Rennen enthalten eine Bootsklasse, die nicht der Bootsklasse der ausgewählten Regatta ''{0}'' entspricht. Es werden keine Rennen geladen. regattaExistForSelectedBoatClass=Es gibt mindestens eine Regatta zu den ausgewählten Bootsklassen. Dennoch Default-Regatta für diese(s) Rennen anlegen? reload=Neu laden @@ -456,7 +457,8 @@ totalAverageSpeedOverGround=\u2205 FüG totalAverageSpeedOverGroundTooltip=Durchschnittliche Fahrt über Grund über die gesamte Regatta. hhmmssUnit=hh:mm:ss actionAddWindData=Winddaten hinzufügen -valueMustBeBetweenMinMax=Der Wert von ''{0}'' muß zwischen {1} und {2} sein. +valueMustBeBetweenMinMax=Der Wert von ''{0}'' muss zwischen {1} und {2} sein. +valueMustBeGreaterThan=Der Wert von ''{0}'' muss größer als {1} sein. optional=optional pleaseEnterAValue=Bitte geben Sie einen Wert ein. latitude=Breite @@ -528,6 +530,8 @@ sideToWhichMarkAtLegStartWasRounded=Tonne gerundet an sideToWhichMarkAtLegStartWasRoundedTooltip=Seite, an der die Bahnmarke am Schenkelstart gerundet wurde raceIsLive=Rennen {0} ist live racesAreLive=Rennen {0} sind live +regattaIsLive=Regatta {0} ist live +regattasAreLive=Regatten {0} sind live scoringSchemeHighPointFirstGetsOne=High Point System, Gewinner erhält 1 Punkt scoringSchemeHighPointFirstGetsTen=High Point System, Gewinner erhält 10 Punkte knotsUnit=kn @@ -1401,4 +1405,6 @@ selectALeaderboardGroup=Bitte eine Ranglisten-Gruppe auswählen... pleaseSelect=Bitte auswählen requiresValidRegatta=Diese Seite benötigt eine gültige Regatta, Rennspalte und einen gültigen Gruppennamen um die anzuzeigende Wettfahrt zu identifizieren. couldNotObtainRace=Wettfahrt namens {1} für Gruppe {2} in einer Regatta names {0} konnte nicht gefunden werden: {3} -errorTryingToCreateEmbeddedMap=Fehler beim Erstellen der eingebetteten Karte: {0} \ No newline at end of file +errorTryingToCreateEmbeddedMap=Fehler beim Erstellen der eingebetteten Karte: {0} +transparentBufferLineOnHover=Transparente Pufferlinien bei Maus über Hilfslinien +bufferLineStrokeWeight=Linienbreite der Pufferlinien 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 index c1a452bacc9..32f66fb642a 100644 --- 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 @@ -364,7 +364,8 @@ apply=Применить successfullyUpdatedScores=Очки обновлены успешно errorUpdatingScoresForLeaderboard=Ошибка обновления очков для таблицы лидеров {0}: {1} maneuverTypesToShowWhenCompetitorIsClicked=Типы маневров для отображения при нажатии на участника -simulateWithStartTimeNow=Моделирования с "сейчас" в качестве времени начала +simulateAsLiveRace=Simulate as live race +simulateWithOffset=Offset before start in minutes: boatClassDoesNotMatchSelectedRegatta=Выбранные гонки содержат классы судов не соответствующие классу судов ''{0}'' выбранной регаты. regattaExistForSelectedBoatClass=Есть только одна регата для выбранных классов судов. reload=Перезагрузить @@ -440,6 +441,7 @@ totalAverageSpeedOverGroundTooltip=Средняя скорость относи hhmmssUnit=чч:мм:сс actionAddWindData=Добавить данные о ветре valueMustBeBetweenMinMax=Значение ''{0}''должно быть в пределах от {1} до {2}. +valueMustBeGreaterThan=The value of ''{0}'' must be greater than {1}. optional=Дополнительно pleaseEnterAValue=Введите, пожалуйста, значение. latitude=Широта @@ -511,6 +513,8 @@ sideToWhichMarkAtLegStartWasRounded=Отметка округленная до sideToWhichMarkAtLegStartWasRoundedTooltip=Сторона в которую округляется отметка на старте этапа. raceIsLive=Гонка {0} в прямом эфире racesAreLive=Гонки {0} в прямом эфире +regattaIsLive=Регата {0} в прямом эфире +regattasAreLive=Регаты {0} в прямом эфире knotsUnit=Узлы knotsValue={0,number,#0.0} Узлы knotsRange={0,number,#0.0} - {1,number,#0.0} Узлы @@ -1340,4 +1344,6 @@ selectALeaderboardGroup=Select a leaderboard group... pleaseSelect=Please select requiresValidRegatta=This page requires a valid regatta, race column and fleet name to identify the race to show. couldNotObtainRace=Could not obtain a race with name {1} for fleet {2} for a regatta with name {0}: {3} -errorTryingToCreateEmbeddedMap=Error trying to create the embedded map: {0} \ No newline at end of file +errorTryingToCreateEmbeddedMap=Error trying to create the embedded map: {0} +transparentBufferLineOnHover=Transparent buffer lines on hover of helplines +bufferLineStrokeWeight=Buffer line stroke weight diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties index c8aa38e7f9c..88bec57c775 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties @@ -538,6 +538,7 @@ connectors=Connectors dampeningInterval=Dampening Interval trackBasedEstimationWindSourceTypeTooltip=Calculated data from the competitor tracks valueMustBeBetweenMinMax=The value of ''{0}'' must be between {1} and {2}. +valueMustBeGreaterThan=The value of ''{0}'' must be greater than {1}. orMultipleEmails=or multiple E-Mails separated by comma searchEvents=Search for events explainNoConnectionsFromReplicas=No connections from a replica found. You can connect replicas by using their AdminConsole. @@ -844,7 +845,10 @@ explainReplicationHostname=Hostname where the replication master is running on. igtimiAccounts=Igtimi Accounts windChart=Wind Chart events=活动 +raceIsLive=Race {0} is live racesAreLive=Races {0} are live +regattaIsLive=Regatta {0} is live +regattasAreLive=Regattas {0} are live operatorNotContains=Not Contains windwardDistanceToCompetitorFarthestAheadInMetersTooltip=迎/顺风航段时离开上风/下风第一名的平行距离或横风时的实际距离name 名字 errorAddingResultImportUrl=Error adding result import URL: {0} @@ -1066,13 +1070,13 @@ polarSheetMinimumConfidenceMeasure=Minimum confidence measure operator=operator tractracUsername=TracTrac username webWindSourceTypeName=Manual -raceIsLive=Race {0} is live sapSailingAnalytics=航行分析 trackWind=Track Wind rawFixes=All fixes (including outliers) showCompetitorFullNameColumn=Show Competitor Full Name disableRaceFilter=Disable filter -simulateWithStartTimeNow=Simulate with "now" as start time +simulateAsLiveRace=Simulate as live race +simulateWithOffset=Offset before start in minutes: polarSheetOutlierDetectionRadiusTooltip=The radius used in the distance based outlierndetection algorithm. noWindFixesAvailable=No Wind-Fixes available. correctScoreFor=Correct score for competitor {0} in race {1} @@ -1355,4 +1359,6 @@ selectALeaderboardGroup=Select a leaderboard group... pleaseSelect=Please select requiresValidRegatta=This page requires a valid regatta, race column and fleet name to identify the race to show. couldNotObtainRace=Could not obtain a race with name {1} for fleet {2} for a regatta with name {0}: {3} -errorTryingToCreateEmbeddedMap=Error trying to create the embedded map: {0} \ No newline at end of file +errorTryingToCreateEmbeddedMap=Error trying to create the embedded map: {0} +transparentBufferLineOnHover=Transparent buffer lines on hover of helplines +bufferLineStrokeWeight=Buffer line stroke weight \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/TimePanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/TimePanel.java index b87c3903017..22f8ebbc100 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/TimePanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/TimePanel.java @@ -89,7 +89,13 @@ public class TimePanel extends SimplePanel implemen private static ClientResources resources = GWT.create(ClientResources.class); protected static TimePanelCss timePanelCss = TimePanelCssResources.INSTANCE.css(); - public TimePanel(Timer timer, TimeRangeWithZoomProvider timeRangeProvider, StringMessages stringMessages, boolean canReplayWhileLiveIsPossible) { + /** + * @param isScreenLargeEnoughToOfferChartSupport + * if true, the right padding will be set such that the time panel lines up with charts such + * as the competitor chart or the wind chart shown above it + */ + public TimePanel(Timer timer, TimeRangeWithZoomProvider timeRangeProvider, StringMessages stringMessages, + boolean canReplayWhileLiveIsPossible, boolean isScreenLargeEnoughToOfferChartSupport) { this.timer = timer; this.timeRangeProvider = timeRangeProvider; this.stringMessages = stringMessages; @@ -105,7 +111,9 @@ public class TimePanel extends SimplePanel implemen timePanelSliderFlowWrapper = new FlowPanel(); timePanelSlider.setStyleName("timePanelSlider"); timePanelSlider.getElement().getStyle().setPaddingLeft(66, Unit.PX); - timePanelSlider.getElement().getStyle().setPaddingRight(66, Unit.PX); + if (isScreenLargeEnoughToOfferChartSupport) { + timePanelSlider.getElement().getStyle().setPaddingRight(66, Unit.PX); + } timePanelSliderFlowWrapper.add(timePanelSlider); playSpeedImg = resources.timesliderPlaySpeedIcon(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java index 64e2ddfae51..49aa6bfbc08 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/AbstractRaceChart.java @@ -83,7 +83,9 @@ public abstract class AbstractRaceChart extends AbsolutePanel implements RaceSel timeRangeWithZoomProvider.addTimeZoomChangeListener(this); timeRangeWithZoomProvider.addTimeRangeChangeListener(this); chartsCss.ensureInjected(); - busyIndicator = new SimpleBusyIndicator(/* busy */ true, 2.0f, chartsCss.busyIndicatorStyle(), chartsCss.busyIndicatorImageStyle()); + busyIndicator = new SimpleBusyIndicator(/* busy */ true, 2.0f); + busyIndicator.setPanelStyleClass(chartsCss.busyIndicatorStyle()); + busyIndicator.setImageStyleClass(chartsCss.busyIndicatorImageStyle()); settingsButton = createSettingsButton(); settingsButton.setStyleName(chartsCss.settingsButtonStyle()); settingsButton.addStyleName(chartsCss.settingsButtonBackgroundImage()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/Hoverline.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/Hoverline.java new file mode 100644 index 00000000000..ffb6f83106f --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/Hoverline.java @@ -0,0 +1,57 @@ +package com.sap.sailing.gwt.ui.client.shared.racemap; + +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.maps.client.events.click.ClickMapHandler; +import com.google.gwt.maps.client.events.mouseout.MouseOutMapEvent; +import com.google.gwt.maps.client.events.mouseout.MouseOutMapHandler; +import com.google.gwt.maps.client.events.mouseover.MouseOverMapEvent; +import com.google.gwt.maps.client.events.mouseover.MouseOverMapHandler; +import com.google.gwt.maps.client.overlays.Polyline; +import com.google.gwt.maps.client.overlays.PolylineOptions; + +public class Hoverline { + private static final double TRANSPARENT = 0; + private static final double VISIBLE = 0.2d; + + private final Polyline hoverline; + private final PolylineOptions options; + + public Hoverline(final Polyline polyline, PolylineOptions polylineOptions, final RaceMap map) { + this.options = PolylineOptions.newInstance(); + this.options.setClickable(polylineOptions.getClickable()); + this.options.setGeodesic(polylineOptions.getGeodesic()); + this.options.setMap(polyline.getMap()); + this.options.setPath(polyline.getPath()); + this.options.setStrokeColor(polylineOptions.getStrokeColor()); + this.options.setZindex(polylineOptions.getZindex()); + this.hoverline = Polyline.newInstance(this.options); + this.hoverline.setVisible(false); + polyline.addMouseOverHandler(new MouseOverMapHandler() { + @Override + public void onEvent(MouseOverMapEvent event) { + options.setStrokeOpacity(map.getSettings().getTransparentHoverlines() ? TRANSPARENT : VISIBLE); + options.setStrokeWeight(map.getSettings().getHoverlineStrokeWeight()); + options.setVisible(true); + hoverline.setOptions(options); + } + }); + hoverline.addMouseOutMoveHandler(new MouseOutMapHandler() { + @Override + public void onEvent(MouseOutMapEvent event) { + hoverline.setVisible(false); + } + }); + } + + public HandlerRegistration addClickHandler(ClickMapHandler handler) { + return this.hoverline.addClickHandler(handler); + } + + public HandlerRegistration addMouseOutMoveHandler(MouseOutMapHandler handler) { + return this.hoverline.addMouseOutMoveHandler(handler); + } + + public HandlerRegistration addMouseOverHandler(MouseOverMapHandler handler) { + return this.hoverline.addMouseOverHandler(handler); + } +} \ 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/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index b4657f0d566..a6d094c52a8 100644 --- 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 @@ -1,2601 +1,2636 @@ -package com.sap.sailing.gwt.ui.client.shared.racemap; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import com.google.gwt.canvas.dom.client.CssColor; -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.dom.client.Style; -import com.google.gwt.dom.client.Style.Unit; -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.maps.client.LoadApi; -import com.google.gwt.maps.client.LoadApi.LoadLibrary; -import com.google.gwt.maps.client.MapOptions; -import com.google.gwt.maps.client.MapWidget; -import com.google.gwt.maps.client.base.LatLng; -import com.google.gwt.maps.client.base.LatLngBounds; -import com.google.gwt.maps.client.controls.ControlPosition; -import com.google.gwt.maps.client.controls.MapTypeControlOptions; -import com.google.gwt.maps.client.controls.MapTypeStyle; -import com.google.gwt.maps.client.controls.PanControlOptions; -import com.google.gwt.maps.client.controls.ZoomControlOptions; -import com.google.gwt.maps.client.events.bounds.BoundsChangeMapEvent; -import com.google.gwt.maps.client.events.bounds.BoundsChangeMapHandler; -import com.google.gwt.maps.client.events.click.ClickMapEvent; -import com.google.gwt.maps.client.events.click.ClickMapHandler; -import com.google.gwt.maps.client.events.dragend.DragEndMapEvent; -import com.google.gwt.maps.client.events.dragend.DragEndMapHandler; -import com.google.gwt.maps.client.events.idle.IdleMapEvent; -import com.google.gwt.maps.client.events.idle.IdleMapHandler; -import com.google.gwt.maps.client.events.mouseout.MouseOutMapEvent; -import com.google.gwt.maps.client.events.mouseout.MouseOutMapHandler; -import com.google.gwt.maps.client.events.mouseover.MouseOverMapEvent; -import com.google.gwt.maps.client.events.mouseover.MouseOverMapHandler; -import com.google.gwt.maps.client.events.zoom.ZoomChangeMapEvent; -import com.google.gwt.maps.client.events.zoom.ZoomChangeMapHandler; -import com.google.gwt.maps.client.maptypes.MapTypeStyleFeatureType; -import com.google.gwt.maps.client.mvc.MVCArray; -import com.google.gwt.maps.client.overlays.InfoWindow; -import com.google.gwt.maps.client.overlays.InfoWindowOptions; -import com.google.gwt.maps.client.overlays.Marker; -import com.google.gwt.maps.client.overlays.MarkerOptions; -import com.google.gwt.maps.client.overlays.Polygon; -import com.google.gwt.maps.client.overlays.PolygonOptions; -import com.google.gwt.maps.client.overlays.Polyline; -import com.google.gwt.maps.client.overlays.PolylineOptions; -import com.google.gwt.resources.client.ImageResource; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.AbsolutePanel; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.FlowPanel; -import com.google.gwt.user.client.ui.Image; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.RequiresResize; -import com.google.gwt.user.client.ui.VerticalPanel; -import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.domain.common.Bearing; -import com.sap.sailing.domain.common.Bounds; -import com.sap.sailing.domain.common.ManeuverType; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.Tack; -import com.sap.sailing.domain.common.WindSource; -import com.sap.sailing.domain.common.WindSourceType; -import com.sap.sailing.domain.common.dto.CompetitorDTO; -import com.sap.sailing.domain.common.impl.DegreeBearingImpl; -import com.sap.sailing.domain.common.impl.DegreePosition; -import com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing; -import com.sap.sailing.domain.common.scalablevalue.impl.ScalablePosition; -import com.sap.sailing.gwt.ui.actions.GetPolarAction; -import com.sap.sailing.gwt.ui.actions.GetRaceMapDataAction; -import com.sap.sailing.gwt.ui.actions.GetWindInfoAction; -import com.sap.sailing.gwt.ui.client.ClientResources; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; -import com.sap.sailing.gwt.ui.client.NumberFormatterFactory; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.RaceTimesInfoProviderListener; -import com.sap.sailing.gwt.ui.client.RequiresDataInitialization; -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.filter.QuickRankProvider; -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.common.client.DateAndTimeFormatterUtil; -import com.sap.sailing.gwt.ui.shared.ControlPointDTO; -import com.sap.sailing.gwt.ui.shared.CoursePositionsDTO; -import com.sap.sailing.gwt.ui.shared.GPSFixDTO; -import com.sap.sailing.gwt.ui.shared.LegInfoDTO; -import com.sap.sailing.gwt.ui.shared.ManeuverDTO; -import com.sap.sailing.gwt.ui.shared.MarkDTO; -import com.sap.sailing.gwt.ui.shared.QuickRankDTO; -import com.sap.sailing.gwt.ui.shared.RaceCourseDTO; -import com.sap.sailing.gwt.ui.shared.RaceMapDataDTO; -import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; -import com.sap.sailing.gwt.ui.shared.SidelineDTO; -import com.sap.sailing.gwt.ui.shared.SpeedWithBearingDTO; -import com.sap.sailing.gwt.ui.shared.WaypointDTO; -import com.sap.sailing.gwt.ui.shared.WindDTO; -import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; -import com.sap.sailing.gwt.ui.shared.WindTrackInfoDTO; -import com.sap.sailing.gwt.ui.shared.racemap.GoogleMapAPIKey; -import com.sap.sailing.gwt.ui.shared.racemap.GoogleMapStyleHelper; -import com.sap.sailing.gwt.ui.shared.racemap.RaceSimulationOverlay; -import com.sap.sailing.gwt.ui.shared.racemap.WindStreamletsRaceboardOverlay; -import com.sap.sse.common.Util; -import com.sap.sse.common.Util.Pair; -import com.sap.sse.common.Util.Triple; -import com.sap.sse.common.filter.Filter; -import com.sap.sse.common.filter.FilterSet; -import com.sap.sse.common.impl.RGBColor; -import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.async.MarkedAsyncCallback; -import com.sap.sse.gwt.client.player.TimeListener; -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.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; - -public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSelectionChangeListener, RaceSelectionChangeListener, - RaceTimesInfoProviderListener, TailFactory, Component, RequiresDataInitialization, RequiresResize, QuickRankProvider { - public static final String GET_RACE_MAP_DATA_CATEGORY = "getRaceMapData"; - public static final String GET_WIND_DATA_CATEGORY = "getWindData"; - - private MapWidget map; - - /** - * Always valid, non-null. Must be used to map all coordinates, headings, bearings, and directions - * displayed on the map, including the orientations of any canvases such as boat icons, wind displays etc. that are - * embedded in the map. The coordinate systems facilitates the possibility of transformed displays such as - * rotated and translated versions of the map, implementing the "wind-up" view. - */ - private DelegateCoordinateSystem coordinateSystem; - - private FlowPanel headerPanel; - private AbsolutePanel panelForLeftHeaderLabels; - private AbsolutePanel panelForRightHeaderLabels; - - private final SailingServiceAsync sailingService; - private final ErrorReporter errorReporter; - - private final static ClientResources resources = GWT.create(ClientResources.class); - - /** - * Polyline for the start line (connecting two marks representing the start gate). - */ - private Polyline startLine; - - /** - * Polyline for the finish line (connecting two marks representing the finish gate). - */ - private Polyline finishLine; - - /** - * Polyline for the advantage line (the leading line for the boats, orthogonal to the wind direction; touching the leading boat). - */ - private Polyline advantageLine; - - /** - * The windward of two Polylines representing a triangle between startline and first mark. - */ - private Polyline windwardStartLineMarkToFirstMarkLine; - - /** - * The leeward of two Polylines representing a triangle between startline and first mark. - */ - private Polyline leewardStartLineMarkToFirstMarkLine; - - private class AdvantageLineMouseOverMapHandler implements MouseOverMapHandler { - private double trueWindAngle; - private Date date; - - public AdvantageLineMouseOverMapHandler(double trueWindAngle, Date date) { - this.trueWindAngle = trueWindAngle; - this.date = date; - } - - public void setTrueWindBearing(double trueWindAngle) { - this.trueWindAngle = trueWindAngle; - } - - public void setDate(Date date) { - this.date = date; - } - - @Override - public void onEvent(MouseOverMapEvent event) { - map.setTitle(stringMessages.advantageLine()+" (from "+new DegreeBearingImpl(Math.round(trueWindAngle)).reverse().getDegrees()+"deg"+ - (date == null ? ")" : ", "+ date) + ")"); - } - }; - - private AdvantageLineMouseOverMapHandler advantageLineMouseOverHandler; - - /** - * Polylines for the course middle lines; keys are the two control points delimiting the leg for which the - * {@link Polyline} value shows the course middle line. As only one course middle line is shown even if there - * are multiple legs using the same control points in different directions, using a {@link Set} makes this - * independent of the order of the two control points. If no course middle line is currently being shown for - * a pair of control points, the map will not contain a value for this pair. - */ - private final Map, Polyline> courseMiddleLines; - - private final Map courseSidelines; - - /** - * When the {@link HelpLineTypes#COURSEGEOMETRY} option is selected, little markers will be displayed on the - * lines that show the tooltip text in a little info box linked to the line. When the line is removed by - * {@link #showOrRemoveOrUpdateLine(Polyline, boolean, Position, Position, LineInfoProvider, String)}, these - * overlays need to be removed as well. Also, when the {@link HelpLineTypes#COURSEGEOMETRY} setting is - * deactivated, all these overlays need to go. - */ - private final Map infoOverlaysForLinesForCourseGeometry; - - /** - * Wind data used to display the advantage line. Retrieved by a {@link GetWindInfoAction} execution and used in - * {@link #showAdvantageLine(Iterable, Date)}. - */ - private WindInfoForRaceDTO lastCombinedWindTrackInfoDTO; - - /** - * Manages the cached set of {@link GPSFixDTO}s for the boat positions as well as their graphical counterpart in the - * form of {@link Polyline}s. - */ - private final FixesAndTails fixesAndTails; - - /** - * html5 canvases used as boat display on the map - */ - private final Map boatOverlays; - - /** - * html5 canvases used for competitor info display on the map - */ - private final Map competitorInfoOverlays; - - private SmallTransparentInfoOverlay countDownOverlay; - - /** - * Map overlays with html5 canvas used to display wind sensors - */ - private final Map windSensorOverlays; - - /** - * Map overlays with html5 canvas used to display course marks including buoy zones - */ - private final Map courseMarkOverlays; - - private final Map markDTOs; - - /** - * markers displayed in response to - * {@link SailingServiceAsync#getDouglasPoints(String, String, Map, Map, double, AsyncCallback)} - */ - protected Set douglasMarkers; - - /** - * markers displayed in response to - * {@link SailingServiceAsync#getDouglasPoints(String, String, Map, Map, double, AsyncCallback)} - */ - private Set maneuverMarkers; - - private Map> lastManeuverResult; - - private Map> lastDouglasPeuckerResult; - - private final CompetitorSelectionProvider competitorSelection; - - private List selectedRaces; - - /** - * Used to check if the first initial zoom to the mark markers was already done. - */ - private boolean mapFirstZoomDone = false; - - private final Timer timer; - - private RaceTimesInfoDTO lastRaceTimesInfo; - - private InfoWindow lastInfoWindow = null; - - /** - * RPC calls may receive responses out of order if there are multiple calls in-flight at the same time. If the time - * slider is moved quickly it generates many requests for boat positions quickly after each other. Sometimes, - * responses for requests send later may return before the responses to all earlier requests have been received and - * processed. This counter is used to number the requests. When processing of a response for a later request has - * already begun, responses to earlier requests will be ignored. - */ - private int boatPositionRequestIDCounter; - - /** - * Corresponds to {@link #boatPositionRequestIDCounter}. As soon as the processing of a response for a request ID - * begins, this attribute is set to the ID. A response won't be processed if a later response is already being - * processed. - */ - private int startedProcessingRequestID; - - private RaceMapImageManager raceMapImageManager; - - private final RaceMapSettings settings; - - private final StringMessages stringMessages; - - private boolean isMapInitialized; - - private Date lastTimeChangeBeforeInitialization; - - private int lastLegNumber; - - /** - * The last quick ranks received from a call to {@link SailingServiceAsync#getQuickRanks(RaceIdentifier, Date, AsyncCallback)} upon - * the last {@link #timeChanged(Date, Date)} event. Therefore, the ranks listed here correspond to the {@link #timer}'s time. - */ - private LinkedHashMap quickRanks; - - /** - * Taken from {@link RaceMapDataDTO#competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber}; tells the - * windward distances traveled and the leg numbers in which the respective competitor is. - */ - private LinkedHashMap competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber; - - - private final CombinedWindPanel combinedWindPanel; - - private final TrueNorthIndicatorPanel trueNorthIndicatorPanel; - - private final AsyncActionsExecutor asyncActionsExecutor; - - /** - * The map bounds as last received by map callbacks; used to determine whether to suppress the boat animation during zoom/pan - */ - private LatLngBounds currentMapBounds; // bounds to which bounds-changed-handler compares - private int currentZoomLevel; // zoom-level to which bounds-changed-handler compares - - private boolean autoZoomIn = false; // flags auto-zoom-in in progress - private boolean autoZoomOut = false; // flags auto-zoom-out in progress - private int autoZoomLevel; // zoom-level to which auto-zoom-in/-out is zooming - LatLngBounds autoZoomLatLngBounds; // bounds to which auto-zoom-in/-out is panning&zooming - - private RaceSimulationOverlay simulationOverlay; - private WindStreamletsRaceboardOverlay streamletOverlay; - private final boolean showViewStreamlets; - private final boolean showViewStreamletColors; - private final boolean showViewSimulation; - - private static final String GET_POLAR_CATEGORY = "getPolar"; - - /** - * Tells about the availability of polar / VPP data for this race. If available, the simulation feature can be - * offered to the user. - */ - private boolean hasPolar; - - private final RegattaAndRaceIdentifier raceIdentifier; - - /** - * When the user requests wind-up display this may happen at a point where no mark positions are known or when - * no wind direction is known yet. In this case, this flag will be set, and when wind information or course mark - * positions are received later, this flag is checked, and if set, a {@link #updateCoordinateSystemFromSettings()} - * call is issued to make sure that the user's request for a new coordinate system is honored. - */ - private boolean requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown; - - private final boolean showMapControls; - - /** - * Tells whether currently an auto-zoom is in progress; this is used particularly to keep the smooth CSS boat transitions - * active while auto-zooming whereas stopping them seems the better option for manual zooms. - */ - private boolean autoZoomInProgress; - - /** - * Tells whether currently an orientation change is in progress; this is required handle map events during the configuration of the map - * during an orientation change. - */ - private boolean orientationChangeInProgress; - - private final NumberFormat numberFormatOneDecimal = NumberFormat.getFormat("0.0"); - - public RaceMap(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, - ErrorReporter errorReporter, Timer timer, CompetitorSelectionProvider competitorSelection, StringMessages stringMessages, - boolean showMapControls, boolean showViewStreamlets, boolean showViewStreamletColors, boolean showViewSimulation, - RegattaAndRaceIdentifier raceIdentifier, CombinedWindPanelStyle combinedWindPanelStyle, boolean showHeaderPanel) { - this.setSize("100%", "100%"); - this.showMapControls = showMapControls; - this.stringMessages = stringMessages; - this.sailingService = sailingService; - this.raceIdentifier = raceIdentifier; - this.asyncActionsExecutor = asyncActionsExecutor; - this.errorReporter = errorReporter; - this.timer = timer; - timer.addTimeListener(this); - raceMapImageManager = new RaceMapImageManager(); - markDTOs = new HashMap(); - courseSidelines = new HashMap<>(); - courseMiddleLines = new HashMap<>(); - infoOverlaysForLinesForCourseGeometry = new HashMap<>(); - boatOverlays = new HashMap(); - competitorInfoOverlays = new HashMap(); - windSensorOverlays = new HashMap(); - courseMarkOverlays = new HashMap(); - this.competitorSelection = competitorSelection; - competitorSelection.addCompetitorSelectionChangeListener(this); - settings = new RaceMapSettings(); - coordinateSystem = new DelegateCoordinateSystem(new IdentityCoordinateSystem()); - fixesAndTails = new FixesAndTails(coordinateSystem); - updateCoordinateSystemFromSettings(); - lastTimeChangeBeforeInitialization = null; - isMapInitialized = false; - this.showViewStreamlets = showViewStreamlets; - this.showViewStreamletColors = showViewStreamletColors; - this.showViewSimulation = showViewSimulation; - this.hasPolar = false; - headerPanel = new FlowPanel(); - headerPanel.setStyleName("RaceMap-HeaderPanel"); - panelForLeftHeaderLabels = new AbsolutePanel(); - panelForRightHeaderLabels = new AbsolutePanel(); - initializeData(showMapControls, showHeaderPanel); - combinedWindPanel = new CombinedWindPanel(raceMapImageManager, combinedWindPanelStyle, stringMessages, coordinateSystem); - combinedWindPanel.setVisible(false); - trueNorthIndicatorPanel = new TrueNorthIndicatorPanel(this, raceMapImageManager, combinedWindPanelStyle, stringMessages, coordinateSystem); - trueNorthIndicatorPanel.setVisible(true); - orientationChangeInProgress = false; - } - - /** - * The {@link WindDTO#dampenedTrueWindFromDeg} direction if {@link #lastCombinedWindTrackInfoDTO} has a - * {@link WindSourceType#COMBINED} source which has at least one fix recorded; null otherwise. - */ - private Bearing getLastCombinedTrueWindFromDirection() { - if (lastCombinedWindTrackInfoDTO != null) { - for (Entry e : lastCombinedWindTrackInfoDTO.windTrackInfoByWindSource.entrySet()) { - if (e.getKey().getType() == WindSourceType.COMBINED) { - final List windFixes = e.getValue().windFixes; - if (!windFixes.isEmpty()) { - return new DegreeBearingImpl(windFixes.get(0).dampenedTrueWindFromDeg); - } - } - } - } - return null; - } - - private void updateCoordinateSystemFromSettings() { - final MapOptions mapOptions; - orientationChangeInProgress = true; - if (getSettings().isWindUp()) { - final Position centerOfCourse = getCenterOfCourse(); - if (centerOfCourse != null) { - final Bearing lastCombinedTrueWindFromDirection = getLastCombinedTrueWindFromDirection(); - if (lastCombinedTrueWindFromDirection != null) { - // new equator shall point 90deg right of the "from" wind direction to make wind come from top of map - coordinateSystem.setCoordinateSystem(new RotateAndTranslateCoordinateSystem(centerOfCourse, - lastCombinedTrueWindFromDirection.add(new DegreeBearingImpl(90)))); - if (map != null) { - mapOptions = getMapOptions(showMapControls, /* wind-up */ true); - } else { - mapOptions = null; - } - requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown = false; - } else { - // register callback in case center of course and wind info becomes known - requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown = true; - mapOptions = null; - } - } else { - // register callback in case center of course and wind info becomes known - requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown = true; - mapOptions = null; - } - } else { - if (map != null) { - mapOptions = getMapOptions(showMapControls, /* wind-up */ false); - } else { - mapOptions = null; - } - coordinateSystem.setCoordinateSystem(new IdentityCoordinateSystem()); - } - if (mapOptions != null) { // if no coordinate system change happened that affects an existing map, don't redraw - fixesAndTails.clearTails(); - redraw(); - // zooming and setting options while the event loop is still working doesn't work reliably; defer until event loop returns - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - @Override - public void execute() { - if (map != null) { - map.setOptions(mapOptions); - // ensure zooming to what the settings tell, or defaults if what the settings tell isn't possible right now - mapFirstZoomDone = false; - trueNorthIndicatorPanel.redraw(); - orientationChangeInProgress = false; - } - } - }); - } - } - - private void loadMapsAPIV3(final boolean showMapControls, final boolean showHeaderPanel) { - boolean sensor = true; - - // load all the libs for use in the maps - ArrayList loadLibraries = new ArrayList(); - loadLibraries.add(LoadLibrary.DRAWING); - loadLibraries.add(LoadLibrary.GEOMETRY); - - Runnable onLoad = new Runnable() { - @Override - public void run() { - MapOptions mapOptions = getMapOptions(showMapControls, /* wind up */ false); - map = new MapWidget(mapOptions); - RaceMap.this.add(map, 0, 0); - if (showHeaderPanel) { - Image sapLogo = createSAPLogo(); - RaceMap.this.add(sapLogo); - } - map.setControls(ControlPosition.LEFT_TOP, combinedWindPanel); - combinedWindPanel.getParent().addStyleName("CombinedWindPanelParentDiv"); - map.setControls(ControlPosition.LEFT_TOP, trueNorthIndicatorPanel); - trueNorthIndicatorPanel.getParent().addStyleName("TrueNorthIndicatorPanelParentDiv"); - - RaceMap.this.raceMapImageManager.loadMapIcons(map); - map.setSize("100%", "100%"); - map.addZoomChangeHandler(new ZoomChangeMapHandler() { - @Override - public void onEvent(ZoomChangeMapEvent event) { - if (!autoZoomIn && !autoZoomOut && !orientationChangeInProgress) { - // stop automatic zoom after a manual zoom event; automatic zoom in zoomMapToNewBounds will restore old settings - final List emptyList = Collections.emptyList(); - settings.getZoomSettings().setTypesToConsiderOnZoom(emptyList); - } - // TODO bug489 when in wind-up mode, avoid zooming out too far; perhaps zoom back in if zoomed out too far - } - }); - map.addDragEndHandler(new DragEndMapHandler() { - @Override - public void onEvent(DragEndMapEvent event) { - // stop automatic zoom after a manual drag event - autoZoomIn = false; - autoZoomOut = false; - final List emptyList = Collections.emptyList(); - settings.getZoomSettings().setTypesToConsiderOnZoom(emptyList); - } - }); - map.addIdleHandler(new IdleMapHandler() { - @Override - public void onEvent(IdleMapEvent event) { - // the "idle"-event is raised at the end of map-animations - if (autoZoomIn) { - // finalize zoom-in that was started with panTo() in zoomMapToNewBounds() - map.setZoom(autoZoomLevel); - autoZoomIn = false; - } - if (autoZoomOut) { - // finalize zoom-out that was started with setZoom() in zoomMapToNewBounds() - map.panTo(autoZoomLatLngBounds.getCenter()); - autoZoomOut = false; - } - } - }); - map.addBoundsChangeHandler(new BoundsChangeMapHandler() { - @Override - public void onEvent(BoundsChangeMapEvent event) { - int newZoomLevel = map.getZoom(); - if (!isAutoZoomInProgress() && (newZoomLevel != currentZoomLevel)) { - removeTransitions(); - } - if ((streamletOverlay != null) && !map.getBounds().equals(currentMapBounds)) { - streamletOverlay.onBoundsChanged(newZoomLevel != currentZoomLevel); - } - if ((simulationOverlay != null) && !map.getBounds().equals(currentMapBounds)) { - simulationOverlay.onBoundsChanged(newZoomLevel != currentZoomLevel); - } - currentMapBounds = map.getBounds(); - currentZoomLevel = newZoomLevel; - headerPanel.getElement().getStyle().setWidth(map.getOffsetWidth(), Unit.PX); - } - }); - - // If there was a time change before the API was loaded, reset the time - if (lastTimeChangeBeforeInitialization != null) { - timeChanged(lastTimeChangeBeforeInitialization, null); - lastTimeChangeBeforeInitialization = null; - } - // Initialize streamlet canvas for wind visualization; it shouldn't be doing anything unless it's visible - streamletOverlay = new WindStreamletsRaceboardOverlay(getMap(), /* zIndex */ 0, - timer, raceIdentifier, sailingService, asyncActionsExecutor, stringMessages, coordinateSystem); - streamletOverlay.addToMap(); - if (showViewStreamlets) { - streamletOverlay.setColors(showViewStreamletColors); - streamletOverlay.setVisible(true); - } - - if (showViewSimulation) { - // determine availability of polar diagram - setHasPolar(); - // initialize simulation canvas - simulationOverlay = new RaceSimulationOverlay(getMap(), /* zIndex */ 0, raceIdentifier, sailingService, stringMessages, asyncActionsExecutor, coordinateSystem); - simulationOverlay.addToMap(); - simulationOverlay.setVisible(false); - } - if (showHeaderPanel) { - createHeaderPanel(map); - } - createSettingsButton(map); - - // Data has been initialized - RaceMap.this.isMapInitialized = true; - RaceMap.this.redraw(); - trueNorthIndicatorPanel.redraw(); - showAdditionalControls(map); - } - }; - LoadApi.go(onLoad, loadLibraries, sensor, "key="+GoogleMapAPIKey.V3_APIKey); - } - - /** - * Subclasses may define additional stuff to be shown on the map. - */ - protected void showAdditionalControls(MapWidget map) { - } - - private void setHasPolar() { - GetPolarAction getPolar = new GetPolarAction(sailingService, raceIdentifier); - asyncActionsExecutor.execute(getPolar, GET_POLAR_CATEGORY, - new MarkedAsyncCallback<>(new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError(stringMessages.errorDeterminingPolarAvailability( - raceIdentifier.getRaceName(), caught.getMessage()), /* silent */ true); - } - - @Override - public void onSuccess(Boolean result) { - // store results - hasPolar = result.booleanValue(); - } - })); - - } - - /** - * Creates a header panel where additional information can be displayed by using - * {@link #getLeftHeaderPanel()} or {@link #getRightHeaderPanel()}. - * - * This panel is transparent and configured in such a way that it moves other controls - * down by its height. To achieve the goal of not having added widgets transparent - * this widget consists of two parts: First one is the transparent panel and the - * second one is the panel for the controls. The controls then need to moved onto - * the panel by using CSS. - */ - private void createHeaderPanel(MapWidget map) { - // we need a panel that does not have any transparency to have the - // labels shown in the right color. This panel also needs to have - // a higher z-index than other elements on the map - map.setControls(ControlPosition.TOP_LEFT, panelForLeftHeaderLabels); - panelForLeftHeaderLabels.getElement().getParentElement().getStyle().setProperty("zIndex", "1"); - panelForLeftHeaderLabels.getElement().getStyle().setProperty("overflow", "visible"); - add(panelForRightHeaderLabels); - panelForRightHeaderLabels.getElement().getStyle().setProperty("zIndex", "1"); - panelForRightHeaderLabels.getElement().getStyle().setProperty("overflow", "visible"); - // need to initialize size before css kicks in to make sure - // that controls get positioned right - headerPanel.getElement().getStyle().setHeight(60, Unit.PX); - headerPanel.getElement().getStyle().setWidth(map.getOffsetWidth(), Unit.PX); - // some sort of hack: not positioning TOP_LEFT because then the - // controls at RIGHT would not get the correct top setting - map.setControls(ControlPosition.TOP_RIGHT, headerPanel); - } - - private void createSettingsButton(MapWidget map) { - final Component component = this; - Button settingsButton = new Button(); - settingsButton.setStyleName("gwt-MapSettingsButton"); - settingsButton.setTitle(stringMessages.settings()); - settingsButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - new SettingsDialog(component, stringMessages).show(); - } - }); - map.setControls(ControlPosition.RIGHT_TOP, settingsButton); - } - - private void removeTransitions() { - // remove the canvas animations for boats - for (BoatOverlay boatOverlay : RaceMap.this.getBoatOverlays().values()) { - boatOverlay.removeCanvasPositionAndRotationTransition(); - } - // remove the canvas animations for the info overlays of the selected boats - for (CompetitorInfoOverlay infoOverlay : competitorInfoOverlays.values()) { - infoOverlay.removeCanvasPositionAndRotationTransition(); - } - } - - public void redraw() { - timeChanged(timer.getTime(), null); - } - - Map getBoatOverlays() { - return Collections.unmodifiableMap(boatOverlays); - } - - public MapWidget getMap() { - return map; - } - - /** - * @return the Panel where labels or other controls for the header can be positioned - */ - public AbsolutePanel getLeftHeaderPanel() { - return panelForLeftHeaderLabels; - } - - public AbsolutePanel getRightHeaderPanel() { - return panelForRightHeaderLabels; - } - - @Override - public void onRaceSelectionChange(List selectedRaces) { - mapFirstZoomDone = false; - // TODO bug 494: reset zoom settings to user preferences - this.selectedRaces = selectedRaces; - } - - @Override - public void raceTimesInfosReceived(Map raceTimesInfos, long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) { - timer.adjustClientServerOffset(clientTimeWhenRequestWasSent, serverTimeDuringRequest, clientTimeWhenResponseWasReceived); - this.lastRaceTimesInfo = raceTimesInfos.get(selectedRaces.get(0)); - } - - /** - * 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; - } - - @Override - public void timeChanged(final Date newTime, final Date oldTime) { - if (newTime != null && isMapInitialized) { - if (selectedRaces != null && !selectedRaces.isEmpty()) { - RegattaAndRaceIdentifier race = selectedRaces.get(selectedRaces.size() - 1); - final Iterable competitorsToShow = getCompetitorsToShow(); - - if (race != null) { - final com.sap.sse.common.Util.Triple, Map, Map> fromAndToAndOverlap = - fixesAndTails.computeFromAndTo(newTime, competitorsToShow, settings.getEffectiveTailLengthInMilliseconds()); - int requestID = ++boatPositionRequestIDCounter; - // For those competitors for which the tails don't overlap (and therefore will be replaced by the new tail coming from the server) - // we expect some potential delay in computing the full tail. Therefore, in those cases we fire two requests: one fetching only the - // boat positions at newTime with zero tail length; and another one fetching everything else. - GetRaceMapDataAction getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping = getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping(fromAndToAndOverlap, race, newTime); - if (getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping != null) { - asyncActionsExecutor.execute(getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping, GET_RACE_MAP_DATA_CATEGORY, - getRaceMapDataCallback(oldTime, newTime, fromAndToAndOverlap.getC(), competitorsToShow, requestID)); - requestID = ++boatPositionRequestIDCounter; - } - // next, do the full thing; being the later call, if request throttling kicks in, the later call - // supersedes the earlier call which may get dropped then - GetRaceMapDataAction getRaceMapDataAction = new GetRaceMapDataAction(sailingService, competitorSelection.getAllCompetitors(), race, - useNullAsTimePoint() ? null : newTime, fromAndToAndOverlap.getA(), fromAndToAndOverlap.getB(), /* extrapolate */ true, (settings.isShowSimulationOverlay() ? simulationOverlay.getLegIdentifier() : null)); - asyncActionsExecutor.execute(getRaceMapDataAction, GET_RACE_MAP_DATA_CATEGORY, - getRaceMapDataCallback(oldTime, newTime, fromAndToAndOverlap.getC(), competitorsToShow, requestID)); - - // draw the wind into the map, get the combined wind - List windSourceTypeNames = new ArrayList(); - windSourceTypeNames.add(WindSourceType.EXPEDITION.name()); - windSourceTypeNames.add(WindSourceType.COMBINED.name()); - 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) { - errorReporter.reportError("Error obtaining wind information: " + caught.getMessage(), true /*silentMode */); - } - - @Override - public void onSuccess(WindInfoForRaceDTO windInfo) { - List> windSourcesToShow = new ArrayList>(); - if (windInfo != null) { - lastCombinedWindTrackInfoDTO = windInfo; - showAdvantageLine(competitorsToShow, newTime); - for (WindSource windSource : windInfo.windTrackInfoByWindSource.keySet()) { - WindTrackInfoDTO windTrackInfoDTO = windInfo.windTrackInfoByWindSource.get(windSource); - switch (windSource.getType()) { - case EXPEDITION: - // we filter out measured wind sources with vary low confidence - if (windTrackInfoDTO.minWindConfidence > 0.0001) { - windSourcesToShow.add(new com.sap.sse.common.Util.Pair(windSource, windTrackInfoDTO)); - } - break; - case COMBINED: - showCombinedWindOnMap(windSource, windTrackInfoDTO); - if (requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown) { - updateCoordinateSystemFromSettings(); - } - break; - default: - // Which wind sources are requested is defined in a list above this - // action. So we throw here an exception to notice a missing source. - throw new UnsupportedOperationException( - "There is currently no support for the enum value '" - + windSource.getType() + "' in this method."); - } - } - } - showWindSensorsOnMap(windSourcesToShow); - } - }); - } - } - } - } - - /** - * We assume that overlapping segments usually don't require a lot of loading time as the most typical case will be to update a longer - * tail with a few new fixes that were received since the last time tick. Non-overlapping position requests typically occur for the - * first request when no fix at all is known for the competitor yet, and when the user has radically moved the time slider to some - * other time such that given the current tail length setting the new tail segment does not overlap with the old one, requiring a full - * load of the entire tail data for that competitor.

- * - * For the non-overlapping requests, this method creates a separate request which only loads boat positions, quick ranks, sidelines and - * mark positions for the zero-length interval at newTime, assuming that this will work fairly fast and in particular in - * O(1) time regardless of tail length, compared to fetching the entire tail for all competitors. - */ - private GetRaceMapDataAction getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping( - Triple, Map, Map> fromAndToAndOverlap, - RegattaAndRaceIdentifier race, Date newTime) { - Map fromTimes = new HashMap<>(); - Map toTimes = new HashMap<>(); - for (Map.Entry e : fromAndToAndOverlap.getC().entrySet()) { - if (!e.getValue()) { - // no overlap; add competitor to request - fromTimes.put(e.getKey(), newTime); - toTimes.put(e.getKey(), newTime); - } - } - final GetRaceMapDataAction result; - if (!fromTimes.isEmpty()) { - result = new GetRaceMapDataAction(sailingService, competitorSelection.getAllCompetitors(), - race, useNullAsTimePoint() ? null : newTime, fromTimes, toTimes, /* extrapolate */true, (settings.isShowSimulationOverlay() ? simulationOverlay.getLegIdentifier() : null)); - } else { - result = null; - } - return result; - } - - private AsyncCallback getRaceMapDataCallback( - final Date oldTime, - final Date newTime, - final Map hasTailOverlapForCompetitor, - final Iterable competitorsToShow, final int requestID) { - return new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError("Error obtaining racemap data: " + caught.getMessage(), true /*silentMode */); - } - - @Override - public void onSuccess(RaceMapDataDTO raceMapDataDTO) { - if (map != null && raceMapDataDTO != null) { - quickRanks = raceMapDataDTO.quickRanks; - competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber = - raceMapDataDTO.competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber; - if (showViewSimulation && settings.isShowSimulationOverlay()) { - lastLegNumber = raceMapDataDTO.coursePositions.currentLegNumber; - simulationOverlay.updateLeg(Math.max(lastLegNumber,1), /* clearCanvas */ false, raceMapDataDTO.simulationResultVersion); - } - // process response only if not received out of order - if (startedProcessingRequestID < requestID) { - startedProcessingRequestID = requestID; - // Do boat specific actions - Map> boatData = raceMapDataDTO.boatPositions; - long timeForPositionTransitionMillis = calculateTimeForPositionTransition(newTime, oldTime); - fixesAndTails.updateFixes(boatData, hasTailOverlapForCompetitor, RaceMap.this, timeForPositionTransitionMillis); - showBoatsOnMap(newTime, timeForPositionTransitionMillis, getCompetitorsToShow()); - showCompetitorInfoOnMap(newTime, timeForPositionTransitionMillis, competitorSelection.getSelectedFilteredCompetitors()); - if (douglasMarkers != null) { - removeAllMarkDouglasPeuckerpoints(); - } - if (maneuverMarkers != null) { - removeAllManeuverMarkers(); - } - - // Do mark specific actions - showCourseMarksOnMap(raceMapDataDTO.coursePositions); - if (requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown) { - updateCoordinateSystemFromSettings(); - } - showCourseSidelinesOnMap(raceMapDataDTO.courseSidelines); - showStartAndFinishAndCourseMiddleLines(raceMapDataDTO.coursePositions); - showStartLineToFirstMarkTriangle(raceMapDataDTO.coursePositions); - // even though the wind data is retrieved by a separate call, re-draw the advantage line because it needs to - // adjust to new boat positions - showAdvantageLine(competitorsToShow, newTime); - - // Rezoom the map - LatLngBounds zoomToBounds = null; - if (!settings.getZoomSettings().containsZoomType(ZoomTypes.NONE)) { // Auto zoom if setting is not manual - zoomToBounds = settings.getZoomSettings().getNewBounds(RaceMap.this); - if (zoomToBounds == null && !mapFirstZoomDone) { - zoomToBounds = getDefaultZoomBounds(); // the user-specified zoom couldn't find what it was looking for; try defaults once - } - } else if (!mapFirstZoomDone) { // Zoom once to the marks if marks exist - zoomToBounds = new CourseMarksBoundsCalculator().calculateNewBounds(RaceMap.this); - if (zoomToBounds == null) { - zoomToBounds = getDefaultZoomBounds(); // use default zoom, e.g., - } - /* - * Reset the mapZoomedOrPannedSinceLastRaceSelection: In spite of the fact that - * the map was just zoomed to the bounds of the marks, it was not a zoom or pan - * triggered by the user. As a consequence the - * mapZoomedOrPannedSinceLastRaceSelection option has to reset again. - */ - } - zoomMapToNewBounds(zoomToBounds); - mapFirstZoomDone = true; - } - } else { - lastTimeChangeBeforeInitialization = newTime; - } - } - }; - } - - private void showCourseSidelinesOnMap(List sidelinesDTOs) { - if (map != null && sidelinesDTOs != null ) { - Map toRemoveSidelines = new HashMap(courseSidelines); - for (SidelineDTO sidelineDTO : sidelinesDTOs) { - if (sidelineDTO.getMarks().size() == 2) { // right now we only support sidelines with 2 marks - Polygon sideline = courseSidelines.get(sidelineDTO); - LatLng[] sidelinePoints = new LatLng[sidelineDTO.getMarks().size()]; - int i=0; - for (MarkDTO sidelineMark : sidelineDTO.getMarks()) { - sidelinePoints[i] = coordinateSystem.toLatLng(sidelineMark.position); - i++; - } - if (sideline == null) { - PolygonOptions options = PolygonOptions.newInstance(); - options.setClickable(true); - options.setStrokeColor("#0000FF"); - options.setStrokeWeight(1); - options.setStrokeOpacity(1.0); - options.setFillColor(null); - options.setFillOpacity(1.0); - - sideline = Polygon.newInstance(options); - MVCArray pointsAsArray = MVCArray.newInstance(sidelinePoints); - sideline.setPath(pointsAsArray); - - sideline.addMouseOverHandler(new MouseOverMapHandler() { - @Override - public void onEvent(MouseOverMapEvent event) { - map.setTitle(stringMessages.sideline()); - } - }); - sideline.addMouseOutMoveHandler(new MouseOutMapHandler() { - @Override - public void onEvent(MouseOutMapEvent event) { - map.setTitle(""); - } - }); - courseSidelines.put(sidelineDTO, sideline); - sideline.setMap(map); - } else { - sideline.getPath().removeAt(1); - sideline.getPath().removeAt(0); - sideline.getPath().insertAt(0, sidelinePoints[0]); - sideline.getPath().insertAt(1, sidelinePoints[1]); - toRemoveSidelines.remove(sidelineDTO); - } - } - } - for (SidelineDTO toRemoveSideline : toRemoveSidelines.keySet()) { - Polygon sideline = courseSidelines.remove(toRemoveSideline); - sideline.setMap(null); - } - } - } - - private void showCourseMarksOnMap(CoursePositionsDTO courseDTO) { - if (map != null && courseDTO != null) { - WaypointDTO endWaypointForCurrentLegNumber = null; - if(courseDTO.currentLegNumber > 0 && courseDTO.currentLegNumber <= courseDTO.totalLegsCount) { - endWaypointForCurrentLegNumber = courseDTO.getEndWaypointForLegNumber(courseDTO.currentLegNumber); - } - - Map toRemoveCourseMarks = new HashMap(courseMarkOverlays); - if (courseDTO.marks != null) { - for (MarkDTO markDTO : courseDTO.marks) { - boolean isSelected = false; - if (endWaypointForCurrentLegNumber != null && Util.contains(endWaypointForCurrentLegNumber.controlPoint.getMarks(), markDTO)) { - isSelected = true; - } - CourseMarkOverlay courseMarkOverlay = courseMarkOverlays.get(markDTO.getName()); - if (courseMarkOverlay == null) { - courseMarkOverlay = createCourseMarkOverlay(RaceMapOverlaysZIndexes.COURSEMARK_ZINDEX, markDTO); - courseMarkOverlay.setShowBuoyZone(settings.getHelpLinesSettings().isVisible(HelpLineTypes.BUOYZONE)); - courseMarkOverlay.setBuoyZoneRadiusInMeter(settings.getBuoyZoneRadiusInMeters()); - courseMarkOverlay.setSelected(isSelected); - courseMarkOverlays.put(markDTO.getName(), courseMarkOverlay); - markDTOs.put(markDTO.getName(), markDTO); - courseMarkOverlay.addToMap(); - } else { - courseMarkOverlay.setMarkPosition(markDTO.position); - courseMarkOverlay.setShowBuoyZone(settings.getHelpLinesSettings().isVisible(HelpLineTypes.BUOYZONE)); - courseMarkOverlay.setBuoyZoneRadiusInMeter(settings.getBuoyZoneRadiusInMeters()); - courseMarkOverlay.setSelected(isSelected); - courseMarkOverlay.draw(); - toRemoveCourseMarks.remove(markDTO.getName()); - } - } - } - for (String toRemoveMarkName : toRemoveCourseMarks.keySet()) { - CourseMarkOverlay removedOverlay = courseMarkOverlays.remove(toRemoveMarkName); - if(removedOverlay != null) { - removedOverlay.removeFromMap(); - } - } - } - } - - /** - * Based on the mark positions in {@link #courseMarkOverlays}' values this method determines the center of gravity of these marks' - * {@link CourseMarkOverlay#getPosition() positions}. - */ - private Position getCenterOfCourse() { - ScalablePosition center = null; - int count = 0; - for (CourseMarkOverlay markOverlay : courseMarkOverlays.values()) { - ScalablePosition markPosition = new ScalablePosition(markOverlay.getPosition()); - if (center == null) { - center = markPosition; - } else { - center.add(markPosition); - } - count++; - } - return center == null ? null : center.divide(count); - } - - private void showCombinedWindOnMap(WindSource windSource, WindTrackInfoDTO windTrackInfoDTO) { - if (map != null) { - combinedWindPanel.setWindInfo(windTrackInfoDTO, windSource); - combinedWindPanel.redraw(); - } - } - - private void showWindSensorsOnMap(List> windSensorsList) { - if (map != null) { - Set toRemoveWindSources = new HashSet(windSensorOverlays.keySet()); - for (com.sap.sse.common.Util.Pair windSourcePair : windSensorsList) { - WindSource windSource = windSourcePair.getA(); - WindTrackInfoDTO windTrackInfoDTO = windSourcePair.getB(); - - WindSensorOverlay windSensorOverlay = windSensorOverlays.get(windSource); - if (windSensorOverlay == null) { - windSensorOverlay = createWindSensorOverlay(RaceMapOverlaysZIndexes.WINDSENSOR_ZINDEX, windSource, windTrackInfoDTO); - windSensorOverlays.put(windSource, windSensorOverlay); - windSensorOverlay.addToMap(); - } else { - windSensorOverlay.setWindInfo(windTrackInfoDTO, windSource); - windSensorOverlay.draw(); - toRemoveWindSources.remove(windSource); - } - } - for (WindSource toRemoveWindSource : toRemoveWindSources) { - WindSensorOverlay removedWindSensorOverlay = windSensorOverlays.remove(toRemoveWindSource); - if (removedWindSensorOverlay != null) { - removedWindSensorOverlay.removeFromMap(); - } - } - } - } - - private void showCompetitorInfoOnMap(final Date newTime, final long timeForPositionTransitionMillis, final Iterable competitorsToShow) { - if (map != null) { - if (settings.isShowSelectedCompetitorsInfo()) { - Set toRemoveCompetorInfoOverlays = new HashSet( - competitorInfoOverlays.keySet()); - for (CompetitorDTO competitorDTO : competitorsToShow) { - if (fixesAndTails.hasFixesFor(competitorDTO)) { - GPSFixDTO lastBoatFix = getBoatFix(competitorDTO, newTime); - if (lastBoatFix != null) { - CompetitorInfoOverlay competitorInfoOverlay = competitorInfoOverlays.get(competitorDTO); - if (competitorInfoOverlay == null) { - competitorInfoOverlay = createCompetitorInfoOverlay(RaceMapOverlaysZIndexes.INFO_OVERLAY_ZINDEX, competitorDTO); - competitorInfoOverlays.put(competitorDTO, competitorInfoOverlay); - competitorInfoOverlay.setPosition(lastBoatFix.position, timeForPositionTransitionMillis); - competitorInfoOverlay.addToMap(); - } else { - competitorInfoOverlay.setPosition(lastBoatFix.position, timeForPositionTransitionMillis); - competitorInfoOverlay.draw(); - } - toRemoveCompetorInfoOverlays.remove(competitorDTO); - } - } - } - for (CompetitorDTO toRemoveCompetorDTO : toRemoveCompetorInfoOverlays) { - CompetitorInfoOverlay competitorInfoOverlay = competitorInfoOverlays.get(toRemoveCompetorDTO); - competitorInfoOverlay.removeFromMap(); - competitorInfoOverlays.remove(toRemoveCompetorDTO); - } - } else { - // remove all overlays - for (CompetitorInfoOverlay competitorInfoOverlay : competitorInfoOverlays.values()) { - competitorInfoOverlay.removeFromMap(); - } - competitorInfoOverlays.clear(); - } - } - } - - private long calculateTimeForPositionTransition(final Date newTime, final Date oldTime) { - final long timeForPositionTransitionMillis; - boolean hasTimeJumped = oldTime != null && Math.abs(oldTime.getTime() - newTime.getTime()) > 3*timer.getRefreshInterval(); - if (timer.getPlayState() == PlayStates.Playing && !hasTimeJumped) { - // choose 130% of the refresh interval as transition period to make it unlikely that the transition - // stops before the next update has been received - timeForPositionTransitionMillis = 1300 * timer.getRefreshInterval() / 1000; - } else { - timeForPositionTransitionMillis = -1; // -1 means 'no transition - } - return timeForPositionTransitionMillis; - } - - private void showBoatsOnMap(final Date newTime, final long timeForPositionTransitionMillis, final Iterable competitorsToShow) { - if (map != null) { - Date tailsFromTime = new Date(newTime.getTime() - settings.getEffectiveTailLengthInMilliseconds()); - Date tailsToTime = newTime; - Set competitorDTOsOfUnusedTails = new HashSet(fixesAndTails.getCompetitorsWithTails()); - Set competitorDTOsOfUnusedBoatCanvases = new HashSet(boatOverlays.keySet()); - for (CompetitorDTO competitorDTO : competitorsToShow) { - if (fixesAndTails.hasFixesFor(competitorDTO)) { - Polyline tail = fixesAndTails.getTail(competitorDTO); - if (tail == null) { - tail = fixesAndTails.createTailAndUpdateIndices(competitorDTO, tailsFromTime, tailsToTime, this); - tail.setMap(map); - } else { - fixesAndTails.updateTail(tail, competitorDTO, tailsFromTime, tailsToTime, - (int) (timeForPositionTransitionMillis==-1?-1:timeForPositionTransitionMillis/2)); - competitorDTOsOfUnusedTails.remove(competitorDTO); - PolylineOptions newOptions = createTailStyle(competitorDTO, displayHighlighted(competitorDTO)); - tail.setOptions(newOptions); - } - boolean usedExistingBoatCanvas = updateBoatCanvasForCompetitor(competitorDTO, newTime, timeForPositionTransitionMillis); - if (usedExistingBoatCanvas) { - competitorDTOsOfUnusedBoatCanvases.remove(competitorDTO); - } - } - } - for (CompetitorDTO unusedBoatCanvasCompetitorDTO : competitorDTOsOfUnusedBoatCanvases) { - BoatOverlay boatCanvas = boatOverlays.get(unusedBoatCanvasCompetitorDTO); - boatCanvas.removeFromMap(); - boatOverlays.remove(unusedBoatCanvasCompetitorDTO); - } - for (CompetitorDTO unusedTailCompetitorDTO : competitorDTOsOfUnusedTails) { - fixesAndTails.removeTail(unusedTailCompetitorDTO); - } - } - } - - /** - * This algorithm is limited to distances such that dlon < pi/2, i.e., those that extend around less than one - * quarter of the circumference of the earth in longitude. A completely general, but more complicated algorithm is - * necessary if greater distances are allowed. - */ - public LatLng calculatePositionAlongRhumbline(LatLng position, double bearingDeg, double distanceInKm) { - double distianceRad = distanceInKm / 6371.0; // r = 6371 means earth's radius in km - double lat1 = position.getLatitude() / 180. * Math.PI; - double lon1 = position.getLongitude() / 180. * Math.PI; - double bearingRad = bearingDeg / 180. * Math.PI; - double lat2 = Math.asin(Math.sin(lat1) * Math.cos(distianceRad) + - Math.cos(lat1) * Math.sin(distianceRad) * Math.cos(bearingRad)); - double lon2 = lon1 + Math.atan2(Math.sin(bearingRad)*Math.sin(distianceRad)*Math.cos(lat1), - Math.cos(distianceRad)-Math.sin(lat1)*Math.sin(lat2)); - lon2 = (lon2+3*Math.PI) % (2*Math.PI) - Math.PI; // normalize to -180..+180� - // position is already in LatLng space, so no mapping through coordinateSystem is required here - return LatLng.newInstance(lat2 / Math.PI * 180., lon2 / Math.PI * 180.); - } - - /** - * Returns a pair whose first component is the leg number (one-based) of the competitor returned as the second component. - */ - private com.sap.sse.common.Util.Pair getFarthestAheadVisibleCompetitorWithOneBasedLegNumber( - Iterable competitorsToShow) { - CompetitorDTO leadingCompetitorDTO = null; - int legOfLeaderCompetitor = -1; - // this only works because the quickRanks are sorted - for (Entry competitorsByWindwardDistanceTraveledAndOneBasedLegNumber : - competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber.entrySet()) { - if (Util.contains(competitorsToShow, competitorsByWindwardDistanceTraveledAndOneBasedLegNumber.getKey()) && - competitorsByWindwardDistanceTraveledAndOneBasedLegNumber.getValue() != null) { - leadingCompetitorDTO = competitorsByWindwardDistanceTraveledAndOneBasedLegNumber.getKey(); - legOfLeaderCompetitor = competitorsByWindwardDistanceTraveledAndOneBasedLegNumber.getValue(); - return new com.sap.sse.common.Util.Pair(legOfLeaderCompetitor, leadingCompetitorDTO); - } - } - return null; - } - - private void showAdvantageLine(Iterable competitorsToShow, Date date) { - if (map != null && lastRaceTimesInfo != null && quickRanks != null && lastCombinedWindTrackInfoDTO != null) { - boolean drawAdvantageLine = false; - if (settings.getHelpLinesSettings().isVisible(HelpLineTypes.ADVANTAGELINE)) { - // find competitor with highest rank - com.sap.sse.common.Util.Pair visibleLeaderInfo = getFarthestAheadVisibleCompetitorWithOneBasedLegNumber(competitorsToShow); - // the boat fix may be null; may mean that no positions were loaded yet for the leading visible boat; - // don't show anything - GPSFixDTO lastBoatFix = null; - boolean isVisibleLeaderInfoComplete = false; - boolean isLegTypeKnown = false; - WindTrackInfoDTO windDataForLegMiddle = null; - LegInfoDTO legInfoDTO = null; - if (visibleLeaderInfo != null - && visibleLeaderInfo.getA() > 0 - && visibleLeaderInfo.getA() <= lastRaceTimesInfo.getLegInfos().size() - // get wind at middle of leg for leading visible competitor - && (windDataForLegMiddle = lastCombinedWindTrackInfoDTO - .getCombinedWindOnLegMiddle(visibleLeaderInfo.getA() - 1)) != null - && !windDataForLegMiddle.windFixes.isEmpty()) { - isVisibleLeaderInfoComplete = true; - legInfoDTO = lastRaceTimesInfo.getLegInfos().get(visibleLeaderInfo.getA() - 1); - if (legInfoDTO.legType != null) { - isLegTypeKnown = true; - } - lastBoatFix = getBoatFix(visibleLeaderInfo.getB(), date); - } - if (isVisibleLeaderInfoComplete && isLegTypeKnown && lastBoatFix != null && lastBoatFix.speedWithBearing != null) { - double advantageLineLengthInKm = 1.0; // TODO this should probably rather scale with the visible - // area of the map; bug 616 - double distanceFromBoatPositionInKm = visibleLeaderInfo.getB().getBoatClass() - .getHullLengthInMeters() / 1000.; // one hull length - // implement and use Position.translateRhumb() - double bearingOfBoatInDeg = lastBoatFix.speedWithBearing.bearingInDegrees; - LatLng boatPosition = coordinateSystem.toLatLng(lastBoatFix.position); - LatLng posAheadOfFirstBoat = calculatePositionAlongRhumbline(boatPosition, - coordinateSystem.mapDegreeBearing(bearingOfBoatInDeg), distanceFromBoatPositionInKm); - final WindDTO windFix = windDataForLegMiddle.windFixes.get(0); - double bearingOfCombinedWindInDeg = windFix.trueWindBearingDeg; - double rotatedBearingDeg1 = 0.0; - double rotatedBearingDeg2 = 0.0; - switch (legInfoDTO.legType) { - case UPWIND: - case DOWNWIND: { - rotatedBearingDeg1 = bearingOfCombinedWindInDeg + 90.0; - if (rotatedBearingDeg1 >= 360.0) { - rotatedBearingDeg1 -= 360.0; - } - rotatedBearingDeg2 = bearingOfCombinedWindInDeg - 90.0; - if (rotatedBearingDeg2 < 0.0) { - rotatedBearingDeg2 += 360.0; - } - } - break; - case REACHING: { - rotatedBearingDeg1 = legInfoDTO.legBearingInDegrees + 90.0; - if (rotatedBearingDeg1 >= 360.0) { - rotatedBearingDeg1 -= 360.0; - } - rotatedBearingDeg2 = legInfoDTO.legBearingInDegrees - 90.0; - if (rotatedBearingDeg2 < 0.0) { - rotatedBearingDeg2 += 360.0; - } - } - break; - } - LatLng advantageLinePos1 = calculatePositionAlongRhumbline(posAheadOfFirstBoat, - coordinateSystem.mapDegreeBearing(rotatedBearingDeg1), advantageLineLengthInKm / 2.0); - LatLng advantageLinePos2 = calculatePositionAlongRhumbline(posAheadOfFirstBoat, - coordinateSystem.mapDegreeBearing(rotatedBearingDeg2), advantageLineLengthInKm / 2.0); - if (advantageLine == null) { - PolylineOptions options = PolylineOptions.newInstance(); - options.setClickable(true); - options.setGeodesic(true); - options.setStrokeColor("#000000"); - options.setStrokeWeight(1); - options.setStrokeOpacity(0.5); - - advantageLine = Polyline.newInstance(options); - MVCArray pointsAsArray = MVCArray.newInstance(); - pointsAsArray.insertAt(0, advantageLinePos1); - pointsAsArray.insertAt(1, advantageLinePos2); - advantageLine.setPath(pointsAsArray); - - advantageLineMouseOverHandler = new AdvantageLineMouseOverMapHandler( - bearingOfCombinedWindInDeg, new Date(windFix.measureTimepoint)); - advantageLine.addMouseOverHandler(advantageLineMouseOverHandler); - advantageLine.addMouseOutMoveHandler(new MouseOutMapHandler() { - @Override - public void onEvent(MouseOutMapEvent event) { - map.setTitle(""); - } - }); - advantageLine.setMap(map); - } else { - advantageLine.getPath().removeAt(1); - advantageLine.getPath().removeAt(0); - advantageLine.getPath().insertAt(0, advantageLinePos1); - advantageLine.getPath().insertAt(1, advantageLinePos2); - advantageLineMouseOverHandler.setTrueWindBearing(bearingOfCombinedWindInDeg); - advantageLineMouseOverHandler.setDate(new Date(windFix.measureTimepoint)); - } - drawAdvantageLine = true; - } - } - if (!drawAdvantageLine) { - if (advantageLine != null) { - advantageLine.setMap(null); - advantageLine = null; - } - } - } - } - - private final StringBuilder windwardStartLineMarkToFirstMarkLineText = new StringBuilder(); - private final StringBuilder leewardStartLineMarkToFirstMarkLineText = new StringBuilder(); - - private void showStartLineToFirstMarkTriangle(final CoursePositionsDTO courseDTO){ - final List startMarkPositions = courseDTO.getStartMarkPositions(); - final Position windwardStartLinePosition = startMarkPositions.get(0); - final Position leewardStartLinePosition = startMarkPositions.get(1); - final Position firstMarkPosition = courseDTO.waypointPositions.get(1); - windwardStartLineMarkToFirstMarkLineText.replace(0, windwardStartLineMarkToFirstMarkLineText.length(), - stringMessages.startLineToFirstMarkTriangle(numberFormatOneDecimal - .format(windwardStartLinePosition.getDistance(firstMarkPosition) - .getMeters()))); - leewardStartLineMarkToFirstMarkLineText.replace(0, leewardStartLineMarkToFirstMarkLineText.length(), - stringMessages.startLineToFirstMarkTriangle(numberFormatOneDecimal - .format(leewardStartLinePosition.getDistance(firstMarkPosition) - .getMeters()))); - final LineInfoProvider windwardStartLineMarkToFirstMarkLineInfoProvider = new LineInfoProvider() { - @Override - public String getLineInfo() { - return windwardStartLineMarkToFirstMarkLineText.toString(); - } - }; - final LineInfoProvider leewardStartLineMarkToFirstMarkLineInfoProvider = new LineInfoProvider() { - @Override - public String getLineInfo() { - return leewardStartLineMarkToFirstMarkLineText.toString(); - } - }; - windwardStartLineMarkToFirstMarkLine = showOrRemoveOrUpdateLine(windwardStartLineMarkToFirstMarkLine, /* showLine */ - (settings.getHelpLinesSettings().isVisible(HelpLineTypes.STARTLINETOFIRSTMARKTRIANGLE)) - && startMarkPositions.size() > 1 && courseDTO.waypointPositions.size() > 1, - windwardStartLinePosition, firstMarkPosition, windwardStartLineMarkToFirstMarkLineInfoProvider, - "grey"); - leewardStartLineMarkToFirstMarkLine = showOrRemoveOrUpdateLine(leewardStartLineMarkToFirstMarkLine, /* showLine */ - (settings.getHelpLinesSettings().isVisible(HelpLineTypes.STARTLINETOFIRSTMARKTRIANGLE)) - && startMarkPositions.size() > 1 && courseDTO.waypointPositions.size() > 1, - leewardStartLinePosition, firstMarkPosition, leewardStartLineMarkToFirstMarkLineInfoProvider, - "grey"); - } - - private final StringBuilder startLineAdvantageText = new StringBuilder(); - private final StringBuilder finishLineAdvantageText = new StringBuilder(); - final LineInfoProvider startLineInfoProvider = new LineInfoProvider() { - @Override - public String getLineInfo() { - return stringMessages.startLine()+startLineAdvantageText; - } - }; - final LineInfoProvider finishLineInfoProvider = new LineInfoProvider() { - @Override - public String getLineInfo() { - return stringMessages.finishLine()+finishLineAdvantageText; - } - }; - - private void showStartAndFinishAndCourseMiddleLines(final CoursePositionsDTO courseDTO) { - if (map != null && courseDTO != null && courseDTO.course != null && courseDTO.course.waypoints != null && - !courseDTO.course.waypoints.isEmpty()) { - // draw the start line - final WaypointDTO startWaypoint = courseDTO.course.waypoints.get(0); - updateCountdownCanvas(startWaypoint); - final int numberOfStartWaypointMarks = courseDTO.getStartMarkPositions() == null ? 0 : courseDTO.getStartMarkPositions().size(); - final int numberOfFinishWaypointMarks = courseDTO.getFinishMarkPositions() == null ? 0 : courseDTO.getFinishMarkPositions().size(); - final Position startLineLeftPosition = numberOfStartWaypointMarks == 0 ? null : courseDTO.getStartMarkPositions().get(0); - final Position startLineRightPosition = numberOfStartWaypointMarks < 2 ? null : courseDTO.getStartMarkPositions().get(1); - if (courseDTO.startLineAngleToCombinedWind != null) { - startLineAdvantageText.replace(0, startLineAdvantageText.length(), " "+stringMessages.lineAngleToWindAndAdvantage( - NumberFormat.getFormat("0.0").format(courseDTO.startLineLengthInMeters), - NumberFormat.getFormat("0.0").format(Math.abs(courseDTO.startLineAngleToCombinedWind)), - courseDTO.startLineAdvantageousSide.name().charAt(0)+courseDTO.startLineAdvantageousSide.name().substring(1).toLowerCase(), - NumberFormat.getFormat("0.0").format(courseDTO.startLineAdvantageInMeters))); - } else { - startLineAdvantageText.delete(0, startLineAdvantageText.length()); - } - final boolean showStartLineBasedOnCurrentLeg = numberOfStartWaypointMarks == 2 && courseDTO.currentLegNumber <= 1; - final boolean showFinishLineBasedOnCurrentLeg = numberOfFinishWaypointMarks == 2 && courseDTO.currentLegNumber == courseDTO.totalLegsCount; - // show the line when STARTLINE is selected and the current leg is around the start leg, - // or when COURSEGEOMETRY is selected and the finish line isn't equal and wouldn't be shown at the same time based on the current leg. - // With this, if COURSEGEOMETRY is selected and start and finish line are equal, the start line will not be displayed if - // based on the race progress the finish line is to be preferred, so only the finish line will be shown. - final boolean reallyShowStartLine = - (settings.getHelpLinesSettings().isVisible(HelpLineTypes.STARTLINE) && showStartLineBasedOnCurrentLeg) || - (settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY) && - (!showFinishLineBasedOnCurrentLeg || !startLineEqualsFinishLine(courseDTO))); - // show the line when FINISHLINE is selected and the current leg is the last leg, - // or when COURSEGEOMETRY is selected and the start line isn't equal or the current leg is the last leg. - // With this, if COURSEGEOMETRY is selected and start and finish line are equal, the start line will be displayed unless - // the finish line should take precedence based on race progress. - final boolean reallyShowFinishLine = showFinishLineBasedOnCurrentLeg && - (!showStartLineBasedOnCurrentLeg || !startLineEqualsFinishLine(courseDTO)) && - (settings.getHelpLinesSettings().isVisible(HelpLineTypes.FINISHLINE) && showFinishLineBasedOnCurrentLeg) || - (settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY) && - (!startLineEqualsFinishLine(courseDTO) || showFinishLineBasedOnCurrentLeg)); - startLine = showOrRemoveOrUpdateLine(startLine, reallyShowStartLine, - startLineLeftPosition, startLineRightPosition, startLineInfoProvider, "#ffffff"); - // draw the finish line - final Position finishLineLeftPosition = numberOfFinishWaypointMarks == 0 ? null : courseDTO.getFinishMarkPositions().get(0); - final Position finishLineRightPosition = numberOfFinishWaypointMarks < 2 ? null : courseDTO.getFinishMarkPositions().get(1); - if (courseDTO.finishLineAngleToCombinedWind != null) { - finishLineAdvantageText.replace(0, finishLineAdvantageText.length(), " "+stringMessages.lineAngleToWindAndAdvantage( - NumberFormat.getFormat("0.0").format(courseDTO.finishLineLengthInMeters), - NumberFormat.getFormat("0.0").format(Math.abs(courseDTO.finishLineAngleToCombinedWind)), - courseDTO.finishLineAdvantageousSide.name().charAt(0)+courseDTO.finishLineAdvantageousSide.name().substring(1).toLowerCase(), - NumberFormat.getFormat("0.0").format(courseDTO.finishLineAdvantageInMeters))); - } else { - finishLineAdvantageText.delete(0, finishLineAdvantageText.length()); - } - finishLine = showOrRemoveOrUpdateLine(finishLine, reallyShowFinishLine, - finishLineLeftPosition, finishLineRightPosition, finishLineInfoProvider, "#000000"); - // the control point pairs for which we already decided whether or not - // to show a course middle line for; values tell whether to show the line and for which zero-based - // start waypoint index to do so; when for an equal control point pair multiple decisions with different - // outcome are made, a decision to show the line overrules the decision to not show it (OR-semantics) - final Map, Pair> keysAlreadyHandled = new HashMap<>(); - for (int zeroBasedIndexOfStartWaypoint = 0; zeroBasedIndexOfStartWaypoint key = getCourseMiddleLinesKey(courseDTO, zeroBasedIndexOfStartWaypoint); - boolean showCourseMiddleLine = keysAlreadyHandled.containsKey(key) && keysAlreadyHandled.get(key).getA() || - settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY) || - (settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEMIDDLELINE) - && courseDTO.currentLegNumber > 0 - && courseDTO.currentLegNumber-1 == zeroBasedIndexOfStartWaypoint); - keysAlreadyHandled.put(key, new Pair<>(showCourseMiddleLine, zeroBasedIndexOfStartWaypoint)); - } - Set> keysToConsider = new HashSet<>(keysAlreadyHandled.keySet()); - keysToConsider.addAll(courseMiddleLines.keySet()); - for (final Set key : keysToConsider) { - final int zeroBasedIndexOfStartWaypoint = keysAlreadyHandled.containsKey(key) ? - keysAlreadyHandled.get(key).getB() : 0; // if not handled, the line will be removed, so the waypoint index doesn't matter - final Pair showLineAndZeroBasedIndexOfStartWaypoint = keysAlreadyHandled.get(key); - final boolean showCourseMiddleLine = showLineAndZeroBasedIndexOfStartWaypoint != null && showLineAndZeroBasedIndexOfStartWaypoint.getA(); - courseMiddleLines.put(key, showOrRemoveCourseMiddleLine(courseDTO, courseMiddleLines.get(key), zeroBasedIndexOfStartWaypoint, showCourseMiddleLine)); - } - } - } - - private boolean startLineEqualsFinishLine(CoursePositionsDTO courseDTO) { - final List waypoints; - return courseDTO != null && courseDTO.course != null && - (waypoints = courseDTO.course.waypoints) != null && - waypoints.get(0).controlPoint.equals(waypoints.get(waypoints.size()-1).controlPoint); - } - - /** - * Given a zero-based index into courseDTO's {@link RaceCourseDTO#waypoints waypoints list} that denotes the start - * waypoint of the leg in question, returns a key that can be used for the {@link #courseMiddleLines} map, consisting of a set - * that holds the two {@link ControlPointDTO}s representing the start and finish control point of that leg. - */ - private Set getCourseMiddleLinesKey(final CoursePositionsDTO courseDTO, - final int zeroBasedIndexOfStartWaypoint) { - ControlPointDTO startControlPoint = courseDTO.course.waypoints.get(zeroBasedIndexOfStartWaypoint).controlPoint; - ControlPointDTO endControlPoint = courseDTO.course.waypoints.get(zeroBasedIndexOfStartWaypoint+1).controlPoint; - final Set key = new HashSet<>(); - key.add(startControlPoint); - key.add(endControlPoint); - return key; - } - - /** - * @param showLine - * tells whether or not to show the line; if the lineToShowOrRemoveOrUpdate references a - * line but the line shall not be shown, the line is removed from the map; conversely, if the line is not - * yet shown but shall be, a new line is created, added to the map and returned. If the line is shown and - * shall continue to be shown, the line is returned after updating its vertex coordinates. - * @return null if the line is not shown; the polyline object representing the line being displayed - * otherwise - */ - private Polyline showOrRemoveCourseMiddleLine(final CoursePositionsDTO courseDTO, Polyline lineToShowOrRemoveOrUpdate, - final int zeroBasedIndexOfStartWaypoint, final boolean showLine) { - final Position position1DTO = courseDTO.waypointPositions.get(zeroBasedIndexOfStartWaypoint); - final Position position2DTO = courseDTO.waypointPositions.get(zeroBasedIndexOfStartWaypoint+1); - final LineInfoProvider lineInfoProvider = new LineInfoProvider() { - @Override - public String getLineInfo() { - final StringBuilder sb = new StringBuilder(); - sb.append(stringMessages.courseMiddleLine()); - sb.append('\n'); - sb.append(NumberFormat.getFormat("0").format( - Math.abs(position1DTO.getDistance(position2DTO).getMeters()))+stringMessages.metersUnit()); - if (lastCombinedWindTrackInfoDTO != null) { - final WindTrackInfoDTO windTrackAtLegMiddle = lastCombinedWindTrackInfoDTO.getCombinedWindOnLegMiddle(zeroBasedIndexOfStartWaypoint); - if (windTrackAtLegMiddle != null && windTrackAtLegMiddle.windFixes != null && !windTrackAtLegMiddle.windFixes.isEmpty()) { - WindDTO windAtLegMiddle = windTrackAtLegMiddle.windFixes.get(0); - final double legBearingDeg = position1DTO.getBearingGreatCircle(position2DTO).getDegrees(); - final String diff = NumberFormat.getFormat("0.0").format( - Math.min(Math.abs(windAtLegMiddle.dampenedTrueWindBearingDeg-legBearingDeg), - Math.abs(windAtLegMiddle.dampenedTrueWindFromDeg-legBearingDeg))); - sb.append(", "); - sb.append(stringMessages.degreesToWind(diff)); - } - } - return sb.toString(); - } - }; - return showOrRemoveOrUpdateLine(lineToShowOrRemoveOrUpdate, showLine, position1DTO, position2DTO, lineInfoProvider, "#2268a0"); - } - - private interface LineInfoProvider { - String getLineInfo(); - } - - /** - * @param showLine - * tells whether or not to show the line; if the lineToShowOrRemoveOrUpdate references a - * line but the line shall not be shown, the line is removed from the map; conversely, if the line is not - * yet shown but shall be, a new line is created, added to the map and returned. If the line is shown and - * shall continue to be shown, the line is returned after updating its vertex coordinates. - * @return null if the line is not shown; the polyline object representing the line being displayed - * otherwise - */ - private Polyline showOrRemoveOrUpdateLine(Polyline lineToShowOrRemoveOrUpdate, final boolean showLine, - final Position position1DTO, final Position position2DTO, final LineInfoProvider lineInfoProvider, String lineColorRGB) { - if (showLine) { - LatLng courseMiddleLinePoint1 = coordinateSystem.toLatLng(position1DTO); - LatLng courseMiddleLinePoint2 = coordinateSystem.toLatLng(position2DTO); - final MVCArray pointsAsArray; - if (lineToShowOrRemoveOrUpdate == null) { - PolylineOptions options = PolylineOptions.newInstance(); - options.setClickable(true); - options.setGeodesic(true); - options.setStrokeColor(lineColorRGB); - options.setStrokeWeight(1); - options.setStrokeOpacity(1.0); - pointsAsArray = MVCArray.newInstance(); - lineToShowOrRemoveOrUpdate = Polyline.newInstance(options); - lineToShowOrRemoveOrUpdate.setPath(pointsAsArray); - lineToShowOrRemoveOrUpdate.addMouseOverHandler(new MouseOverMapHandler() { - @Override - public void onEvent(MouseOverMapEvent event) { - map.setTitle(lineInfoProvider.getLineInfo()); - } - }); - lineToShowOrRemoveOrUpdate.addMouseOutMoveHandler(new MouseOutMapHandler() { - @Override - public void onEvent(MouseOutMapEvent event) { - map.setTitle(""); - } - }); - lineToShowOrRemoveOrUpdate.setMap(map); - - } else { - pointsAsArray = lineToShowOrRemoveOrUpdate.getPath(); - pointsAsArray.removeAt(1); - pointsAsArray.removeAt(0); - } - adjustInfoOverlayForVisibleLine(lineToShowOrRemoveOrUpdate, position1DTO, position2DTO, lineInfoProvider); - pointsAsArray.insertAt(0, courseMiddleLinePoint1); - pointsAsArray.insertAt(1, courseMiddleLinePoint2); - } else { - if (lineToShowOrRemoveOrUpdate != null) { - lineToShowOrRemoveOrUpdate.setMap(null); - adjustInfoOverlayForRemovedLine(lineToShowOrRemoveOrUpdate); - lineToShowOrRemoveOrUpdate = null; - } - } - return lineToShowOrRemoveOrUpdate; - } - - private void adjustInfoOverlayForRemovedLine(Polyline lineToShowOrRemoveOrUpdate) { - SmallTransparentInfoOverlay infoOverlay = infoOverlaysForLinesForCourseGeometry.remove(lineToShowOrRemoveOrUpdate); - if (infoOverlay != null) { - infoOverlay.removeFromMap(); - } - } - - private void adjustInfoOverlayForVisibleLine(Polyline lineToShowOrRemoveOrUpdate, final Position position1DTO, - final Position position2DTO, final LineInfoProvider lineInfoProvider) { - SmallTransparentInfoOverlay infoOverlay = infoOverlaysForLinesForCourseGeometry.get(lineToShowOrRemoveOrUpdate); - if (getSettings().getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY)) { - if (infoOverlay == null) { - infoOverlay = new SmallTransparentInfoOverlay(map, RaceMapOverlaysZIndexes.INFO_OVERLAY_ZINDEX, lineInfoProvider.getLineInfo(), coordinateSystem); - infoOverlaysForLinesForCourseGeometry.put(lineToShowOrRemoveOrUpdate, infoOverlay); - infoOverlay.addToMap(); - } else { - infoOverlay.setInfoText(lineInfoProvider.getLineInfo()); - } - infoOverlay.setPosition(position1DTO.translateGreatCircle(position1DTO.getBearingGreatCircle(position2DTO), - position1DTO.getDistance(position2DTO).scale(0.5)), /* transition time */ -1); - infoOverlay.draw(); - } else { - if (infoOverlay != null) { - infoOverlay.removeFromMap(); - infoOverlaysForLinesForCourseGeometry.remove(lineToShowOrRemoveOrUpdate); - } - } - } - - /** - * If, according to {@link #lastRaceTimesInfo} and {@link #timer} the race is - * still in the pre-start phase, show a {@link SmallTransparentInfoOverlay} at the - * start line that shows the count down. - */ - private void updateCountdownCanvas(WaypointDTO startWaypoint) { - if (!settings.isShowSelectedCompetitorsInfo() || startWaypoint == null || Util.isEmpty(startWaypoint.controlPoint.getMarks()) - || lastRaceTimesInfo == null || lastRaceTimesInfo.startOfRace == null || timer.getTime().after(lastRaceTimesInfo.startOfRace)) { - if (countDownOverlay != null) { - countDownOverlay.removeFromMap(); - countDownOverlay = null; - } - } else { - long timeToStartInMs = lastRaceTimesInfo.startOfRace.getTime() - timer.getTime().getTime(); - String countDownText = timeToStartInMs > 1000 ? stringMessages.timeToStart(DateAndTimeFormatterUtil - .formatElapsedTime(timeToStartInMs)) : stringMessages.start(); - if (countDownOverlay == null) { - countDownOverlay = new SmallTransparentInfoOverlay(map, RaceMapOverlaysZIndexes.INFO_OVERLAY_ZINDEX, - countDownText, coordinateSystem); - countDownOverlay.addToMap(); - } else { - countDownOverlay.setInfoText(countDownText); - } - countDownOverlay.setPosition(startWaypoint.controlPoint.getMarks().iterator().next().position, /* transition time */ -1); - countDownOverlay.draw(); - } - } - - // Google scales coordinates so that the globe-tile has mercator-latitude [-pi, +pi], i.e. tile height of 2*pi - // mercator-latitude pi corresponds to geo-latitude of approx. 85.0998 (where Google cuts off the map visualization) - // official documentation: http://developers.google.com/maps/documentation/javascript/maptypes#TileCoordinates - private double getMercatorLatitude(double lat) { - // cutting-off for latitudes close to +-90 degrees is recommended (to avoid division by zero) - double sine = Math.max(-0.9999, Math.min(0.9999, Math.sin(Math.PI * lat / 180))); - return Math.log((1 + sine) / (1 - sine)) / 2; - } - - private int getZoomLevel(LatLngBounds bounds) { - int GLOBE_PXSIZE = 256; // a constant in Google's map projection - int MAX_ZOOM = 20; // maximum zoom-level that should be automatically selected - double LOG2 = Math.log(2.0); - double deltaLng = bounds.getNorthEast().getLongitude() - bounds.getSouthWest().getLongitude(); - double deltaLat = getMercatorLatitude(bounds.getNorthEast().getLatitude()) - getMercatorLatitude(bounds.getSouthWest().getLatitude()); - if ((deltaLng == 0) && (deltaLat == 0)) { - return MAX_ZOOM; - } - if (deltaLng < 0) { - deltaLng += 360; - } - int zoomLng = (int) Math.floor(Math.log(map.getDiv().getClientWidth() * 360 / deltaLng / GLOBE_PXSIZE) / LOG2); - int zoomLat = (int) Math.floor(Math.log(map.getDiv().getClientHeight() * 2 * Math.PI / deltaLat / GLOBE_PXSIZE) / LOG2); - return Math.min(Math.min(zoomLat, zoomLng), MAX_ZOOM); - } - - private void zoomMapToNewBounds(LatLngBounds newBounds) { - if (newBounds != null) { - LatLngBounds currentMapBounds; - if (map.getBounds() == null - || !BoundsUtil.contains((currentMapBounds = map.getBounds()), newBounds) - || graticuleAreaRatio(currentMapBounds, newBounds) > 10) { - // only change bounds if the new bounds don't fit into the current map zoom - Iterable oldZoomSettings = settings.getZoomSettings().getTypesToConsiderOnZoom(); - setAutoZoomInProgress(true); - autoZoomLatLngBounds = newBounds; - int newZoomLevel = getZoomLevel(autoZoomLatLngBounds); - if (newZoomLevel != map.getZoom()) { - // distinguish between zoom-in and zoom-out, because the sequence of panTo() and setZoom() - // appears different on the screen due to map-animations - // following sequences keep the selected boats allways visible: - // zoom-in : 1. panTo(), 2. setZoom() - // zoom-out: 1. setZoom(), 2. panTo() - autoZoomIn = newZoomLevel > map.getZoom(); - autoZoomOut = !autoZoomIn; - autoZoomLevel = newZoomLevel; - removeTransitions(); - if (autoZoomIn) { - map.panTo(autoZoomLatLngBounds.getCenter()); - } else { - map.setZoom(autoZoomLevel); - } - } else { - map.panTo(autoZoomLatLngBounds.getCenter()); - } - settings.getZoomSettings().setTypesToConsiderOnZoom(oldZoomSettings); - setAutoZoomInProgress(false); - } - } - } - - private double graticuleAreaRatio(LatLngBounds containing, LatLngBounds contained) { - assert BoundsUtil.contains(containing, contained); - double containingAreaRatio = getGraticuleArea(containing) / getGraticuleArea(contained); - return containingAreaRatio; - } - - /** - * A much simplified "area" calculation for a {@link Bounds} object, multiplying the differences in latitude and longitude degrees. - * The result therefore is in the order of magnitude of 60*60 square nautical miles. - */ - private double getGraticuleArea(LatLngBounds bounds) { - return ((BoundsUtil.isCrossesDateLine(bounds) ? bounds.getNorthEast().getLongitude()+360 : bounds.getNorthEast().getLongitude())-bounds.getSouthWest().getLongitude()) * - (bounds.getNorthEast().getLatitude() - bounds.getSouthWest().getLatitude()); - } - - private void setAutoZoomInProgress(boolean autoZoomInProgress) { - this.autoZoomInProgress = autoZoomInProgress; - } - - boolean isAutoZoomInProgress() { - return autoZoomInProgress; - } - - /** - * @param timeForPositionTransitionMillis use -1 to not animate the position transition, e.g., during map zoom or non-play - */ - private boolean updateBoatCanvasForCompetitor(CompetitorDTO competitorDTO, Date date, long timeForPositionTransitionMillis) { - boolean usedExistingCanvas = false; - GPSFixDTO lastBoatFix = getBoatFix(competitorDTO, date); - if (lastBoatFix != null) { - BoatOverlay boatOverlay = boatOverlays.get(competitorDTO); - if (boatOverlay == null) { - boatOverlay = createBoatOverlay(RaceMapOverlaysZIndexes.BOATS_ZINDEX, competitorDTO, displayHighlighted(competitorDTO)); - boatOverlays.put(competitorDTO, boatOverlay); - boatOverlay.setSelected(displayHighlighted(competitorDTO)); - boatOverlay.setBoatFix(lastBoatFix, timeForPositionTransitionMillis); - boatOverlay.addToMap(); - } else { - usedExistingCanvas = true; - boatOverlay.setSelected(displayHighlighted(competitorDTO)); - boatOverlay.setBoatFix(lastBoatFix, timeForPositionTransitionMillis); - boatOverlay.draw(); - } - } - - return usedExistingCanvas; - } - - private boolean displayHighlighted(CompetitorDTO competitorDTO) { - return !settings.isShowOnlySelectedCompetitors() && competitorSelection.isSelected(competitorDTO); - } - - protected CourseMarkOverlay createCourseMarkOverlay(int zIndex, final MarkDTO markDTO) { - final CourseMarkOverlay courseMarkOverlay = new CourseMarkOverlay(map, zIndex, markDTO, coordinateSystem); - courseMarkOverlay.addClickHandler(new ClickMapHandler() { - @Override - public void onEvent(ClickMapEvent event) { - LatLng latlng = courseMarkOverlay.getMarkLatLngPosition(); - showMarkInfoWindow(markDTO, latlng); - } - }); - return courseMarkOverlay; - } - - private CompetitorInfoOverlay createCompetitorInfoOverlay(int zIndex, final CompetitorDTO competitorDTO) { - String infoText = competitorDTO.getSailID() == null || competitorDTO.getSailID().isEmpty() ? competitorDTO.getName() : competitorDTO.getSailID(); - return new CompetitorInfoOverlay(map, zIndex, competitorSelection.getColor(competitorDTO, raceIdentifier), infoText, coordinateSystem); - } - - private BoatOverlay createBoatOverlay(int zIndex, final CompetitorDTO competitorDTO, boolean highlighted) { - final BoatOverlay boatCanvas = new BoatOverlay(map, zIndex, competitorDTO, competitorSelection.getColor(competitorDTO, raceIdentifier), coordinateSystem); - boatCanvas.setSelected(highlighted); - boatCanvas.addClickHandler(new ClickMapHandler() { - @Override - public void onEvent(ClickMapEvent event) { - if (lastInfoWindow != null) { - lastInfoWindow.close(); - } - GPSFixDTO latestFixForCompetitor = getBoatFix(competitorDTO, timer.getTime()); - LatLng where = coordinateSystem.toLatLng(latestFixForCompetitor.position); - InfoWindowOptions options = InfoWindowOptions.newInstance(); - InfoWindow infoWindow = InfoWindow.newInstance(options); - infoWindow.setContent(getInfoWindowContent(competitorDTO, latestFixForCompetitor)); - infoWindow.setPosition(where); - lastInfoWindow = infoWindow; - infoWindow.open(map); - } - }); - - boatCanvas.addMouseOverHandler(new MouseOverMapHandler() { - @Override - public void onEvent(MouseOverMapEvent event) { - map.setTitle(competitorDTO.getSailID()); - } - }); - boatCanvas.addMouseOutMoveHandler(new MouseOutMapHandler() { - @Override - public void onEvent(MouseOutMapEvent event) { - map.setTitle(""); - } - }); - - return boatCanvas; - } - - protected WindSensorOverlay createWindSensorOverlay(int zIndex, final WindSource windSource, final WindTrackInfoDTO windTrackInfoDTO) { - final WindSensorOverlay windSensorOverlay = new WindSensorOverlay(map, zIndex, raceMapImageManager, stringMessages, coordinateSystem); - windSensorOverlay.setWindInfo(windTrackInfoDTO, windSource); - windSensorOverlay.addClickHandler(new ClickMapHandler() { - @Override - public void onEvent(ClickMapEvent event) { - showWindSensorInfoWindow(windSensorOverlay); - } - }); - return windSensorOverlay; - } - - private void showMarkInfoWindow(MarkDTO markDTO, LatLng position) { - if(lastInfoWindow != null) { - lastInfoWindow.close(); - } - InfoWindowOptions options = InfoWindowOptions.newInstance(); - InfoWindow infoWindow = InfoWindow.newInstance(options); - infoWindow.setContent(getInfoWindowContent(markDTO)); - infoWindow.setPosition(position); - lastInfoWindow = infoWindow; - infoWindow.open(map); - } - - private void showCompetitorInfoWindow(final CompetitorDTO competitorDTO, LatLng where) { - if(lastInfoWindow != null) { - lastInfoWindow.close(); - } - GPSFixDTO latestFixForCompetitor = getBoatFix(competitorDTO, timer.getTime()); - // TODO find close fix where the mouse was; see BUG 470 - InfoWindowOptions options = InfoWindowOptions.newInstance(); - InfoWindow infoWindow = InfoWindow.newInstance(options); - infoWindow.setContent(getInfoWindowContent(competitorDTO, latestFixForCompetitor)); - infoWindow.setPosition(where); - lastInfoWindow = infoWindow; - infoWindow.open(map); - } - - private String formatPosition(double lat, double lng) { - NumberFormat numberFormat = NumberFormat.getFormat("0.00000"); - String result = numberFormat.format(lat) + " lat, " + numberFormat.format(lng) + " lng"; - return result; - } - - private void showWindSensorInfoWindow(final WindSensorOverlay windSensorOverlay) { - WindSource windSource = windSensorOverlay.getWindSource(); - WindTrackInfoDTO windTrackInfoDTO = windSensorOverlay.getWindTrackInfoDTO(); - WindDTO windDTO = windTrackInfoDTO.windFixes.get(0); - if(windDTO != null && windDTO.position != null) { - if(lastInfoWindow != null) { - lastInfoWindow.close(); - } - LatLng where = coordinateSystem.toLatLng(windDTO.position); - InfoWindowOptions options = InfoWindowOptions.newInstance(); - InfoWindow infoWindow = InfoWindow.newInstance(options); - infoWindow.setContent(getInfoWindowContent(windSource, windTrackInfoDTO)); - infoWindow.setPosition(where); - lastInfoWindow = infoWindow; - infoWindow.open(map); - } - } - - private Widget createInfoWindowLabelAndValue(String labelName, String value) { - FlowPanel flowPanel = new FlowPanel(); - Label label = new Label(labelName + ":"); - label.setWordWrap(false); - label.getElement().getStyle().setFloat(Style.Float.LEFT); - label.getElement().getStyle().setPadding(3, Style.Unit.PX); - label.getElement().getStyle().setFontWeight(Style.FontWeight.BOLD); - flowPanel.add(label); - - Label valueLabel = new Label(value); - valueLabel.setWordWrap(false); - valueLabel.getElement().getStyle().setFloat(Style.Float.LEFT); - valueLabel.getElement().getStyle().setPadding(3, Style.Unit.PX); - flowPanel.add(valueLabel); - - return flowPanel; - } - - private Widget getInfoWindowContent(MarkDTO markDTO) { - VerticalPanel vPanel = new VerticalPanel(); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.mark(), markDTO.getName())); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), formatPosition(markDTO.position.getLatDeg(), markDTO.position.getLngDeg()))); - return vPanel; - } - - private Widget getInfoWindowContent(WindSource windSource, WindTrackInfoDTO windTrackInfoDTO) { - WindDTO windDTO = windTrackInfoDTO.windFixes.get(0); - NumberFormat numberFormat = NumberFormat.getFormat("0.0"); - VerticalPanel vPanel = new VerticalPanel(); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.windSource(), WindSourceTypeFormatter.format(windSource, stringMessages))); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.wind(), Math.round(windDTO.dampenedTrueWindFromDeg) + " " + stringMessages.degreesShort())); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.windSpeed(), numberFormat.format(windDTO.dampenedTrueWindSpeedInKnots))); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), formatPosition(windDTO.position.getLatDeg(), windDTO.position.getLngDeg()))); - return vPanel; - } - - private Widget getInfoWindowContent(CompetitorDTO competitorDTO, GPSFixDTO lastFix) { - final VerticalPanel vPanel = new VerticalPanel(); - vPanel.setWidth("350px"); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.competitor(), competitorDTO.getName())); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.sailNumber(), competitorDTO.getSailID())); - Integer rank = null; - if (quickRanks != null) { - QuickRankDTO quickRank = quickRanks.get(competitorDTO); - if (quickRank != null) { - rank = quickRank.rank; - } - } - if (rank != null) { - vPanel.add(createInfoWindowLabelAndValue(stringMessages.rank(), String.valueOf(rank))); - } - SpeedWithBearingDTO speedWithBearing = lastFix.speedWithBearing; - if (speedWithBearing == null) { - // TODO should we show the boat at all? - speedWithBearing = new SpeedWithBearingDTO(0, 0); - } - vPanel.add(createInfoWindowLabelAndValue(stringMessages.speed(), - NumberFormatterFactory.getDecimalFormat(1).format(speedWithBearing.speedInKnots) + " "+stringMessages.knotsUnit())); - vPanel.add(createInfoWindowLabelAndValue(stringMessages.bearing(), (int) speedWithBearing.bearingInDegrees + " "+stringMessages.degreesShort())); - if (lastFix.degreesBoatToTheWind != null) { - vPanel.add(createInfoWindowLabelAndValue(stringMessages.degreesBoatToTheWind(), - (int) Math.abs(lastFix.degreesBoatToTheWind) + " " + stringMessages.degreesShort())); - } - if (!selectedRaces.isEmpty()) { - RegattaAndRaceIdentifier race = selectedRaces.get(selectedRaces.size() - 1); - if (race != null) { - Map from = new HashMap(); - from.put(competitorDTO, fixesAndTails.getFixes(competitorDTO).get(fixesAndTails.getFirstShownFix(competitorDTO)).timepoint); - Map to = new HashMap(); - to.put(competitorDTO, getBoatFix(competitorDTO, timer.getTime()).timepoint); - sailingService.getDouglasPoints(race, from, to, 3, - new AsyncCallback>>() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError("Error obtaining douglas positions: " + caught.getMessage(), true /*silentMode */); - } - - @Override - public void onSuccess(Map> result) { - lastDouglasPeuckerResult = result; - if (douglasMarkers != null) { - removeAllMarkDouglasPeuckerpoints(); - } - if (!(timer.getPlayState() == PlayStates.Playing)) { - if (settings.isShowDouglasPeuckerPoints()) { - showMarkDouglasPeuckerPoints(result); - } - } - } - }); - sailingService.getManeuvers(race, from, to, - new AsyncCallback>>() { - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError("Error obtaining maneuvers: " + caught.getMessage(), true /*silentMode */); - } - - @Override - public void onSuccess(Map> result) { - lastManeuverResult = result; - if (maneuverMarkers != null) { - removeAllManeuverMarkers(); - } - if (!(timer.getPlayState() == PlayStates.Playing)) { - showManeuvers(result); - } - } - }); - - } - } - return vPanel; - } - - private Iterable getCompetitorsToShow() { - Iterable result; - Iterable selection = competitorSelection.getSelectedCompetitors(); - if (!settings.isShowOnlySelectedCompetitors() || Util.isEmpty(selection)) { - result = competitorSelection.getFilteredCompetitors(); - } else { - result = selection; - } - return result; - } - - protected void removeAllMarkDouglasPeuckerpoints() { - if (douglasMarkers != null) { - for (Marker marker : douglasMarkers) { - marker.setMap((MapWidget) null); - } - } - douglasMarkers = null; - } - - protected void removeAllManeuverMarkers() { - if (maneuverMarkers != null) { - for (Marker marker : maneuverMarkers) { - marker.setMap((MapWidget) null); - } - maneuverMarkers = null; - } - } - - private void showMarkDouglasPeuckerPoints(Map> gpsFixPointMapForCompetitors) { - douglasMarkers = new HashSet(); - if (map != null && gpsFixPointMapForCompetitors != null) { - Set keySet = gpsFixPointMapForCompetitors.keySet(); - Iterator iter = keySet.iterator(); - while (iter.hasNext()) { - CompetitorDTO competitorDTO = iter.next(); - List gpsFix = gpsFixPointMapForCompetitors.get(competitorDTO); - for (GPSFixDTO fix : gpsFix) { - LatLng latLng = coordinateSystem.toLatLng(fix.position); - MarkerOptions options = MarkerOptions.newInstance(); - options.setTitle(fix.timepoint+": "+fix.position+", "+fix.speedWithBearing.toString()); - Marker marker = Marker.newInstance(options); - marker.setPosition(latLng); - douglasMarkers.add(marker); - marker.setMap(map); - } - } - } - } - - private void showManeuvers(Map> maneuvers) { - maneuverMarkers = new HashSet(); - if (map != null && maneuvers != null) { - Set keySet = maneuvers.keySet(); - Iterator iter = keySet.iterator(); - while (iter.hasNext()) { - CompetitorDTO competitorDTO = iter.next(); - List maneuversForCompetitor = maneuvers.get(competitorDTO); - for (ManeuverDTO maneuver : maneuversForCompetitor) { - if (settings.isShowManeuverType(maneuver.type)) { - LatLng latLng = coordinateSystem.toLatLng(maneuver.position); - Marker maneuverMarker = raceMapImageManager.maneuverIconsForTypeAndTargetTack.get(new com.sap.sse.common.Util.Pair(maneuver.type, maneuver.newTack)); - MarkerOptions options = MarkerOptions.newInstance(); - options.setTitle(maneuver.toString(stringMessages)); - options.setIcon(maneuverMarker.getIcon_MarkerImage()); - Marker marker = Marker.newInstance(options); - marker.setPosition(latLng); - maneuverMarkers.add(marker); - marker.setMap(map); - } - } - } - } - } - - /** - * @param date - * the point in time for which to determine the competitor's boat position; approximated by using the fix - * from {@link #fixes} whose time point comes closest to this date - * - * @return The GPS fix for the given competitor from {@link #fixes} that is closest to date, or - * null if no fix is available - */ - private GPSFixDTO getBoatFix(CompetitorDTO competitorDTO, Date date) { - GPSFixDTO result = null; - List competitorFixes = fixesAndTails.getFixes(competitorDTO); - if (competitorFixes != null && !competitorFixes.isEmpty()) { - int i = Collections.binarySearch(competitorFixes, new GPSFixDTO(date, null, null, (WindDTO) null, null, null, false), - new Comparator() { - @Override - public int compare(GPSFixDTO o1, GPSFixDTO o2) { - return o1.timepoint.compareTo(o2.timepoint); - } - }); - if (i<0) { - i = -i-1; // no perfect match; i is now the insertion point - // if the insertion point is at the end, use last fix - if (i >= competitorFixes.size()) { - result = competitorFixes.get(competitorFixes.size()-1); - } else if (i == 0) { - // if the insertion point is at the beginning, use first fix - result = competitorFixes.get(0); - } else { - // competitorFixes must have at least two elements, and i points neither to the end nor the beginning; - // get the fix from i and i+1 whose timepoint is closer to date - final GPSFixDTO fixBefore = competitorFixes.get(i-1); - final GPSFixDTO fixAfter = competitorFixes.get(i); - final GPSFixDTO closer; - if (date.getTime() - fixBefore.timepoint.getTime() < fixAfter.timepoint.getTime() - date.getTime()) { - closer = fixBefore; - } else { - closer = fixAfter; - } - // 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; - DegreePosition betweenPosition = new DegreePosition(factorForBefore*fixBefore.position.getLatDeg() + factorForAfter*fixAfter.position.getLatDeg(), - factorForBefore*fixBefore.position.getLngDeg() + factorForAfter*fixAfter.position.getLngDeg()); - final double betweenBearing; - if (fixBefore.speedWithBearing == null) { - if (fixAfter.speedWithBearing == null) { - betweenBearing = 0; - } else { - betweenBearing = fixAfter.speedWithBearing.bearingInDegrees; - } - } else if (fixAfter.speedWithBearing == null) { - betweenBearing = fixBefore.speedWithBearing.bearingInDegrees; - } else { - 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( - factorForBefore*(fixBefore.speedWithBearing==null?0:fixBefore.speedWithBearing.speedInKnots) + - factorForAfter*(fixAfter.speedWithBearing==null?0:fixAfter.speedWithBearing.speedInKnots), - betweenBearing); - result = new GPSFixDTO(date, betweenPosition, betweenSpeed, closer.degreesBoatToTheWind, - closer.tack, closer.legType, fixBefore.extrapolated || fixAfter.extrapolated); - } - } else { - // perfect match - final GPSFixDTO fixAfter = competitorFixes.get(i); - result = fixAfter; - } - } - return result; - } - - public RaceMapSettings getSettings() { - return settings; - } - - @Override - public void addedToSelection(CompetitorDTO competitor) { - if (settings.isShowOnlySelectedCompetitors()) { - if (Util.size(competitorSelection.getSelectedCompetitors()) == 1) { - // first competitors selected; remove all others from map - Iterator> i = boatOverlays.entrySet().iterator(); - while (i.hasNext()) { - Entry next = i.next(); - if (!next.getKey().equals(competitor)) { - BoatOverlay boatOverlay = next.getValue(); - boatOverlay.removeFromMap(); - fixesAndTails.removeTail(next.getKey()); - i.remove(); // only this way a ConcurrentModificationException while looping can be avoided - } - } - showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); - } else { - // adding a single competitor; may need to re-load data, so refresh: - timeChanged(timer.getTime(), null); - } - } else { - // only change highlighting - BoatOverlay boatCanvas = boatOverlays.get(competitor); - if (boatCanvas != null) { - boatCanvas.setSelected(displayHighlighted(competitor)); - boatCanvas.draw(); - showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); - } else { - // seems like an internal error not to find the lowlighted marker; but maybe the - // competitor was added late to the race; - // data for newly selected competitor supposedly missing; refresh - timeChanged(timer.getTime(), null); - } - } - // Now update tails for all competitors because selection change may also affect all unselected competitors - for (CompetitorDTO oneOfAllCompetitors : competitorSelection.getAllCompetitors()) { - Polyline tail = fixesAndTails.getTail(oneOfAllCompetitors); - if (tail != null) { - PolylineOptions newOptions = createTailStyle(oneOfAllCompetitors, displayHighlighted(oneOfAllCompetitors)); - tail.setOptions(newOptions); - } - } - // Trigger auto-zoom if needed - RaceMapZoomSettings zoomSettings = settings.getZoomSettings(); - if (!zoomSettings.containsZoomType(ZoomTypes.NONE) && zoomSettings.isZoomToSelectedCompetitors()) { - zoomMapToNewBounds(zoomSettings.getNewBounds(this)); - } - } - - @Override - public void removedFromSelection(CompetitorDTO competitor) { - if (isShowAnyHelperLines()) { - // helper lines depend on which competitor is visible, because the *visible* leader is used for - // deciding which helper lines to show: - timeChanged(timer.getTime(), null); - } else { - // try a more incremental update otherwise - if (settings.isShowOnlySelectedCompetitors()) { - // if selection is now empty, show all competitors - if (Util.isEmpty(competitorSelection.getSelectedCompetitors())) { - timeChanged(timer.getTime(), null); - } else { - // otherwise remove only deselected competitor's boat images and tail - BoatOverlay removedBoatOverlay = boatOverlays.remove(competitor); - if (removedBoatOverlay != null) { - removedBoatOverlay.removeFromMap(); - } - fixesAndTails.removeTail(competitor); - showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); - } - } else { - // "lowlight" currently selected competitor - BoatOverlay boatCanvas = boatOverlays.get(competitor); - if (boatCanvas != null) { - boatCanvas.setSelected(displayHighlighted(competitor)); - boatCanvas.draw(); - } - showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); - } - } - //Trigger auto-zoom if needed - RaceMapZoomSettings zoomSettings = settings.getZoomSettings(); - if (!zoomSettings.containsZoomType(ZoomTypes.NONE) && zoomSettings.isZoomToSelectedCompetitors()) { - zoomMapToNewBounds(zoomSettings.getNewBounds(this)); - } - } - - private boolean isShowAnyHelperLines() { - return settings.getHelpLinesSettings().isShowAnyHelperLines(); - } - - @Override - public String getLocalizedShortName() { - return stringMessages.map(); - } - - @Override - public Widget getEntryWidget() { - return this; - } - - @Override - public boolean hasSettings() { - return true; - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return new RaceMapSettingsDialogComponent(settings, stringMessages, this.showViewSimulation && this.hasPolar); - } - - @Override - public void updateSettings(RaceMapSettings newSettings) { - boolean maneuverTypeSelectionChanged = false; - boolean requiredRedraw = false; - for (ManeuverType maneuverType : ManeuverType.values()) { - if (newSettings.isShowManeuverType(maneuverType) != settings.isShowManeuverType(maneuverType)) { - maneuverTypeSelectionChanged = true; - settings.showManeuverType(maneuverType, newSettings.isShowManeuverType(maneuverType)); - } - } - if (maneuverTypeSelectionChanged) { - if (!(timer.getPlayState() == PlayStates.Playing) && lastManeuverResult != null) { - removeAllManeuverMarkers(); - showManeuvers(lastManeuverResult); - } - } - if (newSettings.isShowDouglasPeuckerPoints() != settings.isShowDouglasPeuckerPoints()) { - if (!(timer.getPlayState() == PlayStates.Playing) && lastDouglasPeuckerResult != null && newSettings.isShowDouglasPeuckerPoints()) { - settings.setShowDouglasPeuckerPoints(true); - removeAllMarkDouglasPeuckerpoints(); - showMarkDouglasPeuckerPoints(lastDouglasPeuckerResult); - } else if (!newSettings.isShowDouglasPeuckerPoints()) { - settings.setShowDouglasPeuckerPoints(false); - removeAllMarkDouglasPeuckerpoints(); - } - } - if (newSettings.getTailLengthInMilliseconds() != settings.getTailLengthInMilliseconds()) { - settings.setTailLengthInMilliseconds(newSettings.getTailLengthInMilliseconds()); - requiredRedraw = true; - } - if (newSettings.getBuoyZoneRadiusInMeters() != settings.getBuoyZoneRadiusInMeters()) { - settings.setBuoyZoneRadiusInMeters(newSettings.getBuoyZoneRadiusInMeters()); - requiredRedraw = true; - } - if (newSettings.isShowOnlySelectedCompetitors() != settings.isShowOnlySelectedCompetitors()) { - settings.setShowOnlySelectedCompetitors(newSettings.isShowOnlySelectedCompetitors()); - requiredRedraw = true; - } - if (newSettings.isShowSelectedCompetitorsInfo() != settings.isShowSelectedCompetitorsInfo()) { - settings.setShowSelectedCompetitorsInfo(newSettings.isShowSelectedCompetitorsInfo()); - requiredRedraw = true; - } - if (!newSettings.getZoomSettings().equals(settings.getZoomSettings())) { - settings.setZoomSettings(newSettings.getZoomSettings()); - if (!settings.getZoomSettings().containsZoomType(ZoomTypes.NONE)) { - removeTransitions(); - zoomMapToNewBounds(settings.getZoomSettings().getNewBounds(this)); - } - } - if (!newSettings.getHelpLinesSettings().equals(settings.getHelpLinesSettings())) { - settings.setHelpLinesSettings(newSettings.getHelpLinesSettings()); - requiredRedraw = true; - } - if (newSettings.isShowWindStreamletOverlay() != settings.isShowWindStreamletOverlay()) { - settings.setShowWindStreamletOverlay(newSettings.isShowWindStreamletOverlay()); - streamletOverlay.setVisible(newSettings.isShowWindStreamletOverlay()); - } - if (newSettings.isShowWindStreamletColors() != settings.isShowWindStreamletColors()) { - settings.setShowWindStreamletColors(newSettings.isShowWindStreamletColors()); - streamletOverlay.setColors(newSettings.isShowWindStreamletColors()); - } - if (newSettings.isShowSimulationOverlay() != settings.isShowSimulationOverlay()) { - settings.setShowSimulationOverlay(newSettings.isShowSimulationOverlay()); - simulationOverlay.setVisible(newSettings.isShowSimulationOverlay()); - if (newSettings.isShowSimulationOverlay()) { - simulationOverlay.updateLeg(Math.max(lastLegNumber,1), true, -1 /* ensure ui-update */); - } - } - if (newSettings.isWindUp() != settings.isWindUp()) { - settings.setWindUp(newSettings.isWindUp()); - updateCoordinateSystemFromSettings(); - requiredRedraw = true; - } - if (requiredRedraw) { - redraw(); - } - } - - public static class BoatsBoundsCalculator extends LatLngBoundsCalculatorForSelected { - - @Override - public LatLngBounds calculateNewBounds(RaceMap forMap) { - LatLngBounds newBounds = null; - Iterable selectedCompetitors = forMap.competitorSelection.getSelectedCompetitors(); - Iterable competitors = new ArrayList(); - if (selectedCompetitors == null || !selectedCompetitors.iterator().hasNext()) { - competitors = forMap.getCompetitorsToShow(); - } else { - competitors = isZoomOnlyToSelectedCompetitors(forMap) ? selectedCompetitors : forMap.getCompetitorsToShow(); - } - for (CompetitorDTO competitor : competitors) { - try { - GPSFixDTO competitorFix = forMap.getBoatFix(competitor, forMap.timer.getTime()); - Position competitorPosition = competitorFix != null ? competitorFix.position : null; - if (competitorPosition != null) { - if (newBounds == null) { - newBounds = BoundsUtil.getAsBounds(forMap.coordinateSystem.toLatLng(competitorPosition)); - } else { - newBounds = newBounds.extend(forMap.coordinateSystem.toLatLng(competitorPosition)); - } - } - } catch (IndexOutOfBoundsException e) { - // TODO can't this be predicted and the exception be avoided in the first place? - // Catch this in case the competitor has no GPS fixes at the current time (e.g. in race 'Finale 2' of STG) - } - } - return newBounds; - } - - } - - public static class TailsBoundsCalculator extends LatLngBoundsCalculatorForSelected { - @Override - public LatLngBounds calculateNewBounds(RaceMap racemap) { - LatLngBounds newBounds = null; - Iterable competitors = isZoomOnlyToSelectedCompetitors(racemap) ? racemap.competitorSelection.getSelectedCompetitors() : racemap.getCompetitorsToShow(); - for (CompetitorDTO competitor : competitors) { - Polyline tail = racemap.fixesAndTails.getTail(competitor); - LatLngBounds bounds = null; - // TODO: Find a replacement for missing Polyline function getBounds() from v2 - // see also http://stackoverflow.com/questions/3284808/getting-the-bounds-of-a-polyine-in-google-maps-api-v3; - // optionally, consider providing a bounds cache with two sorted sets that organize the LatLng objects for O(1) bounds calculation and logarithmic add, ideally O(1) remove - if (tail != null && tail.getPath().getLength() >= 1) { - bounds = BoundsUtil.getAsBounds(tail.getPath().get(0)); - for (int i = 1; i < tail.getPath().getLength(); i++) { - bounds = bounds.extend(tail.getPath().get(i)); - } - } - if (bounds != null) { - if (newBounds == null) { - newBounds = bounds; - } else { - newBounds = newBounds.union(bounds); - } - } - } - return newBounds; - } - } - - public static class CourseMarksBoundsCalculator implements LatLngBoundsCalculator { - @Override - public LatLngBounds calculateNewBounds(RaceMap forMap) { - LatLngBounds newBounds = null; - Iterable marksToZoom = forMap.markDTOs.values(); - if (marksToZoom != null) { - for (MarkDTO markDTO : marksToZoom) { - if (newBounds == null) { - newBounds = BoundsUtil.getAsBounds(forMap.coordinateSystem.toLatLng(markDTO.position)); - } else { - newBounds = newBounds.extend(forMap.coordinateSystem.toLatLng(markDTO.position)); - } - } - } - return newBounds; - } - } - - public static class WindSensorsBoundsCalculator implements LatLngBoundsCalculator { - @Override - public LatLngBounds calculateNewBounds(RaceMap forMap) { - LatLngBounds newBounds = null; - Collection marksToZoom = forMap.windSensorOverlays.values(); - if (marksToZoom != null) { - for (WindSensorOverlay windSensorOverlay : marksToZoom) { - if (BoundsUtil.getAsPosition(windSensorOverlay.getLatLngPosition()) != null) { - LatLngBounds bounds = BoundsUtil.getAsBounds(windSensorOverlay.getLatLngPosition()); - if (newBounds == null) { - newBounds = bounds; - } else { - newBounds = newBounds.extend(windSensorOverlay.getLatLngPosition()); - } - } - } - } - return newBounds; - } - } - - @Override - public void initializeData(boolean showMapControls, boolean showHeaderPanel) { - loadMapsAPIV3(showMapControls, showHeaderPanel); - } - - @Override - public boolean isDataInitialized() { - return isMapInitialized; - } - - @Override - public void onResize() { - if (map != null) { - map.triggerResize(); - zoomMapToNewBounds(settings.getZoomSettings().getNewBounds(RaceMap.this)); - } - } - - @Override - public void competitorsListChanged(Iterable competitors) { - timeChanged(timer.getTime(), null); - } - - @Override - public void filteredCompetitorsListChanged(Iterable filteredCompetitors) { - timeChanged(timer.getTime(), null); - } - - @Override - public void filterChanged(FilterSet> oldFilterSet, - FilterSet> newFilterSet) { - // nothing to do; if the list of filtered competitors has changed, a separate call to filteredCompetitorsListChanged will occur - } - - @Override - public PolylineOptions createTailStyle(CompetitorDTO competitor, boolean isHighlighted) { - PolylineOptions options = PolylineOptions.newInstance(); - options.setClickable(true); - options.setGeodesic(true); - options.setStrokeOpacity(1.0); - boolean noCompetitorSelected = Util.isEmpty(competitorSelection.getSelectedCompetitors()); - if (isHighlighted || noCompetitorSelected) { - options.setStrokeColor(competitorSelection.getColor(competitor, raceIdentifier).getAsHtml()); - } else { - options.setStrokeColor(CssColor.make(200, 200, 200).toString()); - } - if (isHighlighted) { - options.setStrokeWeight(2); - } else { - options.setStrokeWeight(1); - } - options.setZindex(RaceMapOverlaysZIndexes.BOATTAILS_ZINDEX); - return options; - } - - @Override - public Polyline createTail(final CompetitorDTO competitor, List points) { - PolylineOptions options = createTailStyle(competitor, displayHighlighted(competitor)); - Polyline result = Polyline.newInstance(options); - - MVCArray pointsAsArray = MVCArray.newInstance(points.toArray(new LatLng[0])); - result.setPath(pointsAsArray); - - result.addClickHandler(new ClickMapHandler() { - @Override - public void onEvent(ClickMapEvent event) { - showCompetitorInfoWindow(competitor, event.getMouseEvent().getLatLng()); - } - }); - result.addMouseOverHandler(new MouseOverMapHandler() { - @Override - public void onEvent(MouseOverMapEvent event) { - map.setTitle(competitor.getSailID() + ", " + competitor.getName()); - } - }); - result.addMouseOutMoveHandler(new MouseOutMapHandler() { - @Override - public void onEvent(MouseOutMapEvent event) { - map.setTitle(""); - } - }); - return result; - } - - @Override - public Integer getRank(CompetitorDTO competitor) { - final Integer result; - QuickRankDTO quickRank = quickRanks.get(competitor); - if (quickRank != null) { - result = quickRank.rank; - } else { - result = null; - } - return result; - } - - private Image createSAPLogo() { - ImageResource sapLogoResource = resources.sapLogoOverlay(); - Image sapLogo = new Image(sapLogoResource); - sapLogo.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - Window.open("http://www.sap.com", "_blank", null); - } - }); - sapLogo.setStyleName("raceBoard-Logo"); - return sapLogo; - } - - @Override - public String getDependentCssClassName() { - return "raceMap"; - } - - /** - * The default zoom bounds are defined by the boats - */ - private LatLngBounds getDefaultZoomBounds() { - return new BoatsBoundsCalculator().calculateNewBounds(RaceMap.this); - } - - private MapOptions getMapOptions(final boolean showMapControls, boolean windUp) { - MapOptions mapOptions = MapOptions.newInstance(); - mapOptions.setScrollWheel(true); - mapOptions.setMapTypeControl(showMapControls && !windUp); - mapOptions.setPanControl(showMapControls); - mapOptions.setZoomControl(showMapControls); - mapOptions.setScaleControl(true); - if (windUp) { - mapOptions.setMinZoom(8); - } else { - mapOptions.setMinZoom(0); - } - MapTypeStyle[] mapTypeStyles = new MapTypeStyle[4]; - - // hide all transit lines including ferry lines - mapTypeStyles[0] = GoogleMapStyleHelper.createHiddenStyle(MapTypeStyleFeatureType.TRANSIT); - // hide points of interest - mapTypeStyles[1] = GoogleMapStyleHelper.createHiddenStyle(MapTypeStyleFeatureType.POI); - // simplify road display - mapTypeStyles[2] = GoogleMapStyleHelper.createSimplifiedStyle(MapTypeStyleFeatureType.ROAD); - // set water color - // To play with the styles, check out http://gmaps-samples-v3.googlecode.com/svn/trunk/styledmaps/wizard/index.html. - // To convert an RGB color into the strange hue/saturation/lightness model used by the Google Map use - // http://software.stadtwerk.org/google_maps_colorizr/#water/all/123456/. - mapTypeStyles[3] = GoogleMapStyleHelper.createColorStyle(MapTypeStyleFeatureType.WATER, new RGBColor(0, 136, 255), 0, -70); - - MapTypeControlOptions mapTypeControlOptions = MapTypeControlOptions.newInstance(); - mapTypeControlOptions.setPosition(ControlPosition.BOTTOM_RIGHT); - mapOptions.setMapTypeControlOptions(mapTypeControlOptions); - - mapOptions.setMapTypeStyles(mapTypeStyles); - // no need to try to position the scale control; it always ends up at the right bottom corner - mapOptions.setStreetViewControl(false); - if (showMapControls) { - ZoomControlOptions zoomControlOptions = ZoomControlOptions.newInstance(); - zoomControlOptions.setPosition(ControlPosition.RIGHT_TOP); - mapOptions.setZoomControlOptions(zoomControlOptions); - PanControlOptions panControlOptions = PanControlOptions.newInstance(); - panControlOptions.setPosition(ControlPosition.RIGHT_TOP); - mapOptions.setPanControlOptions(panControlOptions); - } - return mapOptions; - } -} +package com.sap.sailing.gwt.ui.client.shared.racemap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.google.gwt.canvas.dom.client.CssColor; +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.dom.client.Style; +import com.google.gwt.dom.client.Style.Unit; +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.maps.client.LoadApi; +import com.google.gwt.maps.client.LoadApi.LoadLibrary; +import com.google.gwt.maps.client.MapOptions; +import com.google.gwt.maps.client.MapWidget; +import com.google.gwt.maps.client.base.LatLng; +import com.google.gwt.maps.client.base.LatLngBounds; +import com.google.gwt.maps.client.controls.ControlPosition; +import com.google.gwt.maps.client.controls.MapTypeControlOptions; +import com.google.gwt.maps.client.controls.MapTypeStyle; +import com.google.gwt.maps.client.controls.PanControlOptions; +import com.google.gwt.maps.client.controls.ZoomControlOptions; +import com.google.gwt.maps.client.events.bounds.BoundsChangeMapEvent; +import com.google.gwt.maps.client.events.bounds.BoundsChangeMapHandler; +import com.google.gwt.maps.client.events.click.ClickMapEvent; +import com.google.gwt.maps.client.events.click.ClickMapHandler; +import com.google.gwt.maps.client.events.dragend.DragEndMapEvent; +import com.google.gwt.maps.client.events.dragend.DragEndMapHandler; +import com.google.gwt.maps.client.events.idle.IdleMapEvent; +import com.google.gwt.maps.client.events.idle.IdleMapHandler; +import com.google.gwt.maps.client.events.mouseout.MouseOutMapEvent; +import com.google.gwt.maps.client.events.mouseout.MouseOutMapHandler; +import com.google.gwt.maps.client.events.mouseover.MouseOverMapEvent; +import com.google.gwt.maps.client.events.mouseover.MouseOverMapHandler; +import com.google.gwt.maps.client.events.zoom.ZoomChangeMapEvent; +import com.google.gwt.maps.client.events.zoom.ZoomChangeMapHandler; +import com.google.gwt.maps.client.maptypes.MapTypeStyleFeatureType; +import com.google.gwt.maps.client.mvc.MVCArray; +import com.google.gwt.maps.client.overlays.InfoWindow; +import com.google.gwt.maps.client.overlays.InfoWindowOptions; +import com.google.gwt.maps.client.overlays.Marker; +import com.google.gwt.maps.client.overlays.MarkerOptions; +import com.google.gwt.maps.client.overlays.Polygon; +import com.google.gwt.maps.client.overlays.PolygonOptions; +import com.google.gwt.maps.client.overlays.Polyline; +import com.google.gwt.maps.client.overlays.PolylineOptions; +import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.AbsolutePanel; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Image; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.RequiresResize; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.Bounds; +import com.sap.sailing.domain.common.ManeuverType; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.Tack; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.WindSourceType; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sailing.domain.common.scalablevalue.impl.ScalableBearing; +import com.sap.sailing.domain.common.scalablevalue.impl.ScalablePosition; +import com.sap.sailing.gwt.ui.actions.GetPolarAction; +import com.sap.sailing.gwt.ui.actions.GetRaceMapDataAction; +import com.sap.sailing.gwt.ui.actions.GetWindInfoAction; +import com.sap.sailing.gwt.ui.client.ClientResources; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionChangeListener; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; +import com.sap.sailing.gwt.ui.client.NumberFormatterFactory; +import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; +import com.sap.sailing.gwt.ui.client.RaceTimesInfoProviderListener; +import com.sap.sailing.gwt.ui.client.RequiresDataInitialization; +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.filter.QuickRankProvider; +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.common.client.DateAndTimeFormatterUtil; +import com.sap.sailing.gwt.ui.shared.ControlPointDTO; +import com.sap.sailing.gwt.ui.shared.CoursePositionsDTO; +import com.sap.sailing.gwt.ui.shared.GPSFixDTO; +import com.sap.sailing.gwt.ui.shared.LegInfoDTO; +import com.sap.sailing.gwt.ui.shared.ManeuverDTO; +import com.sap.sailing.gwt.ui.shared.MarkDTO; +import com.sap.sailing.gwt.ui.shared.QuickRankDTO; +import com.sap.sailing.gwt.ui.shared.RaceCourseDTO; +import com.sap.sailing.gwt.ui.shared.RaceMapDataDTO; +import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; +import com.sap.sailing.gwt.ui.shared.SidelineDTO; +import com.sap.sailing.gwt.ui.shared.SpeedWithBearingDTO; +import com.sap.sailing.gwt.ui.shared.WaypointDTO; +import com.sap.sailing.gwt.ui.shared.WindDTO; +import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; +import com.sap.sailing.gwt.ui.shared.WindTrackInfoDTO; +import com.sap.sailing.gwt.ui.shared.racemap.GoogleMapAPIKey; +import com.sap.sailing.gwt.ui.shared.racemap.GoogleMapStyleHelper; +import com.sap.sailing.gwt.ui.shared.racemap.RaceSimulationOverlay; +import com.sap.sailing.gwt.ui.shared.racemap.WindStreamletsRaceboardOverlay; +import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.Util.Triple; +import com.sap.sse.common.filter.Filter; +import com.sap.sse.common.filter.FilterSet; +import com.sap.sse.common.impl.RGBColor; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.async.MarkedAsyncCallback; +import com.sap.sse.gwt.client.player.TimeListener; +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.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.SettingsDialog; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; + +public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSelectionChangeListener, RaceSelectionChangeListener, + RaceTimesInfoProviderListener, TailFactory, Component, RequiresDataInitialization, RequiresResize, QuickRankProvider { + public static final String GET_RACE_MAP_DATA_CATEGORY = "getRaceMapData"; + public static final String GET_WIND_DATA_CATEGORY = "getWindData"; + + private static final String COMPACT_HEADER_STYLE = "compactHeader"; + + private MapWidget map; + + /** + * Always valid, non-null. Must be used to map all coordinates, headings, bearings, and directions + * displayed on the map, including the orientations of any canvases such as boat icons, wind displays etc. that are + * embedded in the map. The coordinate systems facilitates the possibility of transformed displays such as + * rotated and translated versions of the map, implementing the "wind-up" view. + */ + private DelegateCoordinateSystem coordinateSystem; + + private FlowPanel headerPanel; + private AbsolutePanel panelForLeftHeaderLabels; + private AbsolutePanel panelForRightHeaderLabels; + + private final SailingServiceAsync sailingService; + private final ErrorReporter errorReporter; + + private final static ClientResources resources = GWT.create(ClientResources.class); + + /** + * Polyline for the start line (connecting two marks representing the start gate). + */ + private Polyline startLine; + + /** + * Polyline for the finish line (connecting two marks representing the finish gate). + */ + private Polyline finishLine; + + /** + * Polyline for the advantage line (the leading line for the boats, orthogonal to the wind direction; touching the leading boat). + */ + private Polyline advantageLine; + + /** + * The windward of two Polylines representing a triangle between startline and first mark. + */ + private Polyline windwardStartLineMarkToFirstMarkLine; + + /** + * The leeward of two Polylines representing a triangle between startline and first mark. + */ + private Polyline leewardStartLineMarkToFirstMarkLine; + + private class AdvantageLineMouseOverMapHandler implements MouseOverMapHandler { + private double trueWindAngle; + private Date date; + + public AdvantageLineMouseOverMapHandler(double trueWindAngle, Date date) { + this.trueWindAngle = trueWindAngle; + this.date = date; + } + + public void setTrueWindBearing(double trueWindAngle) { + this.trueWindAngle = trueWindAngle; + } + + public void setDate(Date date) { + this.date = date; + } + + @Override + public void onEvent(MouseOverMapEvent event) { + map.setTitle(stringMessages.advantageLine()+" (from "+new DegreeBearingImpl(Math.round(trueWindAngle)).reverse().getDegrees()+"deg"+ + (date == null ? ")" : ", "+ date) + ")"); + } + }; + + private AdvantageLineMouseOverMapHandler advantageLineMouseOverHandler; + + /** + * Polylines for the course middle lines; keys are the two control points delimiting the leg for which the + * {@link Polyline} value shows the course middle line. As only one course middle line is shown even if there + * are multiple legs using the same control points in different directions, using a {@link Set} makes this + * independent of the order of the two control points. If no course middle line is currently being shown for + * a pair of control points, the map will not contain a value for this pair. + */ + private final Map, Polyline> courseMiddleLines; + + private final Map courseSidelines; + + /** + * When the {@link HelpLineTypes#COURSEGEOMETRY} option is selected, little markers will be displayed on the + * lines that show the tooltip text in a little info box linked to the line. When the line is removed by + * {@link #showOrRemoveOrUpdateLine(Polyline, boolean, Position, Position, LineInfoProvider, String)}, these + * overlays need to be removed as well. Also, when the {@link HelpLineTypes#COURSEGEOMETRY} setting is + * deactivated, all these overlays need to go. + */ + private final Map infoOverlaysForLinesForCourseGeometry; + + /** + * Wind data used to display the advantage line. Retrieved by a {@link GetWindInfoAction} execution and used in + * {@link #showAdvantageLine(Iterable, Date)}. + */ + private WindInfoForRaceDTO lastCombinedWindTrackInfoDTO; + + /** + * Manages the cached set of {@link GPSFixDTO}s for the boat positions as well as their graphical counterpart in the + * form of {@link Polyline}s. + */ + private final FixesAndTails fixesAndTails; + + /** + * html5 canvases used as boat display on the map + */ + private final Map boatOverlays; + + /** + * html5 canvases used for competitor info display on the map + */ + private final Map competitorInfoOverlays; + + private SmallTransparentInfoOverlay countDownOverlay; + + /** + * Map overlays with html5 canvas used to display wind sensors + */ + private final Map windSensorOverlays; + + /** + * Map overlays with html5 canvas used to display course marks including buoy zones + */ + private final Map courseMarkOverlays; + + private final Map markDTOs; + + /** + * markers displayed in response to + * {@link SailingServiceAsync#getDouglasPoints(String, String, Map, Map, double, AsyncCallback)} + */ + protected Set douglasMarkers; + + /** + * markers displayed in response to + * {@link SailingServiceAsync#getDouglasPoints(String, String, Map, Map, double, AsyncCallback)} + */ + private Set maneuverMarkers; + + private Map> lastManeuverResult; + + private Map> lastDouglasPeuckerResult; + + private final CompetitorSelectionProvider competitorSelection; + + private List selectedRaces; + + /** + * Used to check if the first initial zoom to the mark markers was already done. + */ + private boolean mapFirstZoomDone = false; + + private final Timer timer; + + private RaceTimesInfoDTO lastRaceTimesInfo; + + private InfoWindow lastInfoWindow = null; + + /** + * RPC calls may receive responses out of order if there are multiple calls in-flight at the same time. If the time + * slider is moved quickly it generates many requests for boat positions quickly after each other. Sometimes, + * responses for requests send later may return before the responses to all earlier requests have been received and + * processed. This counter is used to number the requests. When processing of a response for a later request has + * already begun, responses to earlier requests will be ignored. + */ + private int boatPositionRequestIDCounter; + + /** + * Corresponds to {@link #boatPositionRequestIDCounter}. As soon as the processing of a response for a request ID + * begins, this attribute is set to the ID. A response won't be processed if a later response is already being + * processed. + */ + private int startedProcessingRequestID; + + private RaceMapImageManager raceMapImageManager; + + private final RaceMapSettings settings; + + private final StringMessages stringMessages; + + private boolean isMapInitialized; + + private Date lastTimeChangeBeforeInitialization; + + private int lastLegNumber; + + /** + * The last quick ranks received from a call to {@link SailingServiceAsync#getQuickRanks(RaceIdentifier, Date, AsyncCallback)} upon + * the last {@link #timeChanged(Date, Date)} event. Therefore, the ranks listed here correspond to the {@link #timer}'s time. + */ + private LinkedHashMap quickRanks; + + /** + * Taken from {@link RaceMapDataDTO#competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber}; tells the + * windward distances traveled and the leg numbers in which the respective competitor is. + */ + private LinkedHashMap competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber; + + + private final CombinedWindPanel combinedWindPanel; + + private final TrueNorthIndicatorPanel trueNorthIndicatorPanel; + + private final AsyncActionsExecutor asyncActionsExecutor; + + /** + * The map bounds as last received by map callbacks; used to determine whether to suppress the boat animation during zoom/pan + */ + private LatLngBounds currentMapBounds; // bounds to which bounds-changed-handler compares + private int currentZoomLevel; // zoom-level to which bounds-changed-handler compares + + private boolean autoZoomIn = false; // flags auto-zoom-in in progress + private boolean autoZoomOut = false; // flags auto-zoom-out in progress + private int autoZoomLevel; // zoom-level to which auto-zoom-in/-out is zooming + LatLngBounds autoZoomLatLngBounds; // bounds to which auto-zoom-in/-out is panning&zooming + + private RaceSimulationOverlay simulationOverlay; + private WindStreamletsRaceboardOverlay streamletOverlay; + private final boolean showViewStreamlets; + private final boolean showViewStreamletColors; + private final boolean showViewSimulation; + + private static final String GET_POLAR_CATEGORY = "getPolar"; + + /** + * Tells about the availability of polar / VPP data for this race. If available, the simulation feature can be + * offered to the user. + */ + private boolean hasPolar; + + private final RegattaAndRaceIdentifier raceIdentifier; + + /** + * When the user requests wind-up display this may happen at a point where no mark positions are known or when + * no wind direction is known yet. In this case, this flag will be set, and when wind information or course mark + * positions are received later, this flag is checked, and if set, a {@link #updateCoordinateSystemFromSettings()} + * call is issued to make sure that the user's request for a new coordinate system is honored. + */ + private boolean requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown; + + private final boolean showMapControls; + + /** + * Tells whether currently an auto-zoom is in progress; this is used particularly to keep the smooth CSS boat transitions + * active while auto-zooming whereas stopping them seems the better option for manual zooms. + */ + private boolean autoZoomInProgress; + + /** + * Tells whether currently an orientation change is in progress; this is required handle map events during the configuration of the map + * during an orientation change. + */ + private boolean orientationChangeInProgress; + + private final NumberFormat numberFormatOneDecimal = NumberFormat.getFormat("0.0"); + + public RaceMap(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + ErrorReporter errorReporter, Timer timer, CompetitorSelectionProvider competitorSelection, StringMessages stringMessages, + boolean showMapControls, boolean showViewStreamlets, boolean showViewStreamletColors, boolean showViewSimulation, + RegattaAndRaceIdentifier raceIdentifier, CombinedWindPanelStyle combinedWindPanelStyle, boolean showHeaderPanel) { + this.setSize("100%", "100%"); + this.showMapControls = showMapControls; + this.stringMessages = stringMessages; + this.sailingService = sailingService; + this.raceIdentifier = raceIdentifier; + this.asyncActionsExecutor = asyncActionsExecutor; + this.errorReporter = errorReporter; + this.timer = timer; + timer.addTimeListener(this); + raceMapImageManager = new RaceMapImageManager(); + markDTOs = new HashMap(); + courseSidelines = new HashMap<>(); + courseMiddleLines = new HashMap<>(); + infoOverlaysForLinesForCourseGeometry = new HashMap<>(); + boatOverlays = new HashMap(); + competitorInfoOverlays = new HashMap(); + windSensorOverlays = new HashMap(); + courseMarkOverlays = new HashMap(); + this.competitorSelection = competitorSelection; + competitorSelection.addCompetitorSelectionChangeListener(this); + settings = new RaceMapSettings(); + coordinateSystem = new DelegateCoordinateSystem(new IdentityCoordinateSystem()); + fixesAndTails = new FixesAndTails(coordinateSystem); + updateCoordinateSystemFromSettings(); + lastTimeChangeBeforeInitialization = null; + isMapInitialized = false; + this.showViewStreamlets = showViewStreamlets; + this.showViewStreamletColors = showViewStreamletColors; + this.showViewSimulation = showViewSimulation; + this.hasPolar = false; + headerPanel = new FlowPanel(); + headerPanel.setStyleName("RaceMap-HeaderPanel"); + panelForLeftHeaderLabels = new AbsolutePanel(); + panelForRightHeaderLabels = new AbsolutePanel(); + initializeData(showMapControls, showHeaderPanel); + combinedWindPanel = new CombinedWindPanel(raceMapImageManager, combinedWindPanelStyle, stringMessages, coordinateSystem); + combinedWindPanel.setVisible(false); + trueNorthIndicatorPanel = new TrueNorthIndicatorPanel(this, raceMapImageManager, combinedWindPanelStyle, stringMessages, coordinateSystem); + trueNorthIndicatorPanel.setVisible(true); + orientationChangeInProgress = false; + } + + /** + * The {@link WindDTO#dampenedTrueWindFromDeg} direction if {@link #lastCombinedWindTrackInfoDTO} has a + * {@link WindSourceType#COMBINED} source which has at least one fix recorded; null otherwise. + */ + private Bearing getLastCombinedTrueWindFromDirection() { + if (lastCombinedWindTrackInfoDTO != null) { + for (Entry e : lastCombinedWindTrackInfoDTO.windTrackInfoByWindSource.entrySet()) { + if (e.getKey().getType() == WindSourceType.COMBINED) { + final List windFixes = e.getValue().windFixes; + if (!windFixes.isEmpty()) { + return new DegreeBearingImpl(windFixes.get(0).dampenedTrueWindFromDeg); + } + } + } + } + return null; + } + + private void updateCoordinateSystemFromSettings() { + final MapOptions mapOptions; + orientationChangeInProgress = true; + if (getSettings().isWindUp()) { + final Position centerOfCourse = getCenterOfCourse(); + if (centerOfCourse != null) { + final Bearing lastCombinedTrueWindFromDirection = getLastCombinedTrueWindFromDirection(); + if (lastCombinedTrueWindFromDirection != null) { + // new equator shall point 90deg right of the "from" wind direction to make wind come from top of map + coordinateSystem.setCoordinateSystem(new RotateAndTranslateCoordinateSystem(centerOfCourse, + lastCombinedTrueWindFromDirection.add(new DegreeBearingImpl(90)))); + if (map != null) { + mapOptions = getMapOptions(showMapControls, /* wind-up */ true); + } else { + mapOptions = null; + } + requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown = false; + } else { + // register callback in case center of course and wind info becomes known + requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown = true; + mapOptions = null; + } + } else { + // register callback in case center of course and wind info becomes known + requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown = true; + mapOptions = null; + } + } else { + if (map != null) { + mapOptions = getMapOptions(showMapControls, /* wind-up */ false); + } else { + mapOptions = null; + } + coordinateSystem.setCoordinateSystem(new IdentityCoordinateSystem()); + } + if (mapOptions != null) { // if no coordinate system change happened that affects an existing map, don't redraw + fixesAndTails.clearTails(); + redraw(); + // zooming and setting options while the event loop is still working doesn't work reliably; defer until event loop returns + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + if (map != null) { + map.setOptions(mapOptions); + // ensure zooming to what the settings tell, or defaults if what the settings tell isn't possible right now + mapFirstZoomDone = false; + trueNorthIndicatorPanel.redraw(); + orientationChangeInProgress = false; + } + } + }); + } + } + + private void loadMapsAPIV3(final boolean showMapControls, final boolean showHeaderPanel) { + boolean sensor = true; + + // load all the libs for use in the maps + ArrayList loadLibraries = new ArrayList(); + loadLibraries.add(LoadLibrary.DRAWING); + loadLibraries.add(LoadLibrary.GEOMETRY); + + Runnable onLoad = new Runnable() { + @Override + public void run() { + MapOptions mapOptions = getMapOptions(showMapControls, /* wind up */ false); + map = new MapWidget(mapOptions); + RaceMap.this.add(map, 0, 0); + if (showHeaderPanel) { + Image sapLogo = createSAPLogo(); + RaceMap.this.add(sapLogo); + } + map.setControls(ControlPosition.LEFT_TOP, combinedWindPanel); + map.setControls(ControlPosition.LEFT_TOP, trueNorthIndicatorPanel); + adjustLeftControlsIndent(); + + RaceMap.this.raceMapImageManager.loadMapIcons(map); + map.setSize("100%", "100%"); + map.addZoomChangeHandler(new ZoomChangeMapHandler() { + @Override + public void onEvent(ZoomChangeMapEvent event) { + if (!autoZoomIn && !autoZoomOut && !orientationChangeInProgress) { + // stop automatic zoom after a manual zoom event; automatic zoom in zoomMapToNewBounds will restore old settings + final List emptyList = Collections.emptyList(); + settings.getZoomSettings().setTypesToConsiderOnZoom(emptyList); + } + // TODO bug489 when in wind-up mode, avoid zooming out too far; perhaps zoom back in if zoomed out too far + } + }); + map.addDragEndHandler(new DragEndMapHandler() { + @Override + public void onEvent(DragEndMapEvent event) { + // stop automatic zoom after a manual drag event + autoZoomIn = false; + autoZoomOut = false; + final List emptyList = Collections.emptyList(); + settings.getZoomSettings().setTypesToConsiderOnZoom(emptyList); + } + }); + map.addIdleHandler(new IdleMapHandler() { + @Override + public void onEvent(IdleMapEvent event) { + // the "idle"-event is raised at the end of map-animations + if (autoZoomIn) { + // finalize zoom-in that was started with panTo() in zoomMapToNewBounds() + map.setZoom(autoZoomLevel); + autoZoomIn = false; + } + if (autoZoomOut) { + // finalize zoom-out that was started with setZoom() in zoomMapToNewBounds() + map.panTo(autoZoomLatLngBounds.getCenter()); + autoZoomOut = false; + } + } + }); + map.addBoundsChangeHandler(new BoundsChangeMapHandler() { + @Override + public void onEvent(BoundsChangeMapEvent event) { + int newZoomLevel = map.getZoom(); + if (!isAutoZoomInProgress() && (newZoomLevel != currentZoomLevel)) { + removeTransitions(); + } + if ((streamletOverlay != null) && !map.getBounds().equals(currentMapBounds)) { + streamletOverlay.onBoundsChanged(newZoomLevel != currentZoomLevel); + } + if ((simulationOverlay != null) && !map.getBounds().equals(currentMapBounds)) { + simulationOverlay.onBoundsChanged(newZoomLevel != currentZoomLevel); + } + currentMapBounds = map.getBounds(); + currentZoomLevel = newZoomLevel; + headerPanel.getElement().getStyle().setWidth(map.getOffsetWidth(), Unit.PX); + } + }); + + // If there was a time change before the API was loaded, reset the time + if (lastTimeChangeBeforeInitialization != null) { + timeChanged(lastTimeChangeBeforeInitialization, null); + lastTimeChangeBeforeInitialization = null; + } + // Initialize streamlet canvas for wind visualization; it shouldn't be doing anything unless it's visible + streamletOverlay = new WindStreamletsRaceboardOverlay(getMap(), /* zIndex */ 0, + timer, raceIdentifier, sailingService, asyncActionsExecutor, stringMessages, coordinateSystem); + streamletOverlay.addToMap(); + if (showViewStreamlets) { + streamletOverlay.setColors(showViewStreamletColors); + streamletOverlay.setVisible(true); + } + + if (showViewSimulation) { + // determine availability of polar diagram + setHasPolar(); + // initialize simulation canvas + simulationOverlay = new RaceSimulationOverlay(getMap(), /* zIndex */ 0, raceIdentifier, sailingService, stringMessages, asyncActionsExecutor, coordinateSystem); + simulationOverlay.addToMap(); + simulationOverlay.setVisible(false); + } + if (showHeaderPanel) { + createHeaderPanel(map); + } + createSettingsButton(map); + + // Data has been initialized + RaceMap.this.isMapInitialized = true; + RaceMap.this.redraw(); + trueNorthIndicatorPanel.redraw(); + showAdditionalControls(map); + } + }; + LoadApi.go(onLoad, loadLibraries, sensor, "key="+GoogleMapAPIKey.V3_APIKey); + } + + /** + * Subclasses may define additional stuff to be shown on the map. + */ + protected void showAdditionalControls(MapWidget map) { + } + + private void setHasPolar() { + GetPolarAction getPolar = new GetPolarAction(sailingService, raceIdentifier); + asyncActionsExecutor.execute(getPolar, GET_POLAR_CATEGORY, + new MarkedAsyncCallback<>(new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(stringMessages.errorDeterminingPolarAvailability( + raceIdentifier.getRaceName(), caught.getMessage()), /* silent */ true); + } + + @Override + public void onSuccess(Boolean result) { + // store results + hasPolar = result.booleanValue(); + } + })); + + } + + /** + * Creates a header panel where additional information can be displayed by using + * {@link #getLeftHeaderPanel()} or {@link #getRightHeaderPanel()}. + * + * This panel is transparent and configured in such a way that it moves other controls + * down by its height. To achieve the goal of not having added widgets transparent + * this widget consists of two parts: First one is the transparent panel and the + * second one is the panel for the controls. The controls then need to moved onto + * the panel by using CSS. + */ + private void createHeaderPanel(MapWidget map) { + // we need a panel that does not have any transparency to have the + // labels shown in the right color. This panel also needs to have + // a higher z-index than other elements on the map + map.setControls(ControlPosition.TOP_LEFT, panelForLeftHeaderLabels); + panelForLeftHeaderLabels.getElement().getParentElement().getStyle().setProperty("zIndex", "1"); + panelForLeftHeaderLabels.getElement().getStyle().setProperty("overflow", "visible"); + add(panelForRightHeaderLabels); + panelForRightHeaderLabels.getElement().getStyle().setProperty("zIndex", "1"); + panelForRightHeaderLabels.getElement().getStyle().setProperty("overflow", "visible"); + // need to initialize size before css kicks in to make sure + // that controls get positioned right + headerPanel.getElement().getStyle().setHeight(60, Unit.PX); + headerPanel.getElement().getStyle().setWidth(map.getOffsetWidth(), Unit.PX); + // some sort of hack: not positioning TOP_LEFT because then the + // controls at RIGHT would not get the correct top setting + map.setControls(ControlPosition.TOP_RIGHT, headerPanel); + } + + private void createSettingsButton(MapWidget map) { + final Component component = this; + Button settingsButton = new Button(); + settingsButton.setStyleName("gwt-MapSettingsButton"); + settingsButton.setTitle(stringMessages.settings()); + settingsButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + new SettingsDialog(component, stringMessages).show(); + } + }); + map.setControls(ControlPosition.RIGHT_TOP, settingsButton); + } + + private void removeTransitions() { + // remove the canvas animations for boats + for (BoatOverlay boatOverlay : RaceMap.this.getBoatOverlays().values()) { + boatOverlay.removeCanvasPositionAndRotationTransition(); + } + // remove the canvas animations for the info overlays of the selected boats + for (CompetitorInfoOverlay infoOverlay : competitorInfoOverlays.values()) { + infoOverlay.removeCanvasPositionAndRotationTransition(); + } + } + + public void redraw() { + timeChanged(timer.getTime(), null); + } + + Map getBoatOverlays() { + return Collections.unmodifiableMap(boatOverlays); + } + + public MapWidget getMap() { + return map; + } + + /** + * @return the Panel where labels or other controls for the header can be positioned + */ + public AbsolutePanel getLeftHeaderPanel() { + return panelForLeftHeaderLabels; + } + + public AbsolutePanel getRightHeaderPanel() { + return panelForRightHeaderLabels; + } + + @Override + public void onRaceSelectionChange(List selectedRaces) { + mapFirstZoomDone = false; + // TODO bug 494: reset zoom settings to user preferences + this.selectedRaces = selectedRaces; + } + + @Override + public void raceTimesInfosReceived(Map raceTimesInfos, long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) { + timer.adjustClientServerOffset(clientTimeWhenRequestWasSent, serverTimeDuringRequest, clientTimeWhenResponseWasReceived); + this.lastRaceTimesInfo = raceTimesInfos.get(selectedRaces.get(0)); + } + + /** + * 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; + } + + @Override + public void timeChanged(final Date newTime, final Date oldTime) { + if (newTime != null && isMapInitialized) { + if (selectedRaces != null && !selectedRaces.isEmpty()) { + RegattaAndRaceIdentifier race = selectedRaces.get(selectedRaces.size() - 1); + final Iterable competitorsToShow = getCompetitorsToShow(); + + if (race != null) { + final com.sap.sse.common.Util.Triple, Map, Map> fromAndToAndOverlap = + fixesAndTails.computeFromAndTo(newTime, competitorsToShow, settings.getEffectiveTailLengthInMilliseconds()); + int requestID = ++boatPositionRequestIDCounter; + // For those competitors for which the tails don't overlap (and therefore will be replaced by the new tail coming from the server) + // we expect some potential delay in computing the full tail. Therefore, in those cases we fire two requests: one fetching only the + // boat positions at newTime with zero tail length; and another one fetching everything else. + GetRaceMapDataAction getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping = getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping(fromAndToAndOverlap, race, newTime); + if (getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping != null) { + asyncActionsExecutor.execute(getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping, GET_RACE_MAP_DATA_CATEGORY, + getRaceMapDataCallback(oldTime, newTime, fromAndToAndOverlap.getC(), competitorsToShow, requestID)); + requestID = ++boatPositionRequestIDCounter; + } + // next, do the full thing; being the later call, if request throttling kicks in, the later call + // supersedes the earlier call which may get dropped then + GetRaceMapDataAction getRaceMapDataAction = new GetRaceMapDataAction(sailingService, competitorSelection.getAllCompetitors(), race, + useNullAsTimePoint() ? null : newTime, fromAndToAndOverlap.getA(), fromAndToAndOverlap.getB(), /* extrapolate */ true, (settings.isShowSimulationOverlay() ? simulationOverlay.getLegIdentifier() : null)); + asyncActionsExecutor.execute(getRaceMapDataAction, GET_RACE_MAP_DATA_CATEGORY, + getRaceMapDataCallback(oldTime, newTime, fromAndToAndOverlap.getC(), competitorsToShow, requestID)); + + // draw the wind into the map, get the combined wind + List windSourceTypeNames = new ArrayList(); + windSourceTypeNames.add(WindSourceType.EXPEDITION.name()); + windSourceTypeNames.add(WindSourceType.COMBINED.name()); + 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) { + errorReporter.reportError("Error obtaining wind information: " + caught.getMessage(), true /*silentMode */); + } + + @Override + public void onSuccess(WindInfoForRaceDTO windInfo) { + List> windSourcesToShow = new ArrayList>(); + if (windInfo != null) { + lastCombinedWindTrackInfoDTO = windInfo; + showAdvantageLine(competitorsToShow, newTime); + for (WindSource windSource : windInfo.windTrackInfoByWindSource.keySet()) { + WindTrackInfoDTO windTrackInfoDTO = windInfo.windTrackInfoByWindSource.get(windSource); + switch (windSource.getType()) { + case EXPEDITION: + // we filter out measured wind sources with vary low confidence + if (windTrackInfoDTO.minWindConfidence > 0.0001) { + windSourcesToShow.add(new com.sap.sse.common.Util.Pair(windSource, windTrackInfoDTO)); + } + break; + case COMBINED: + showCombinedWindOnMap(windSource, windTrackInfoDTO); + if (requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown) { + updateCoordinateSystemFromSettings(); + } + break; + default: + // Which wind sources are requested is defined in a list above this + // action. So we throw here an exception to notice a missing source. + throw new UnsupportedOperationException( + "There is currently no support for the enum value '" + + windSource.getType() + "' in this method."); + } + } + } + showWindSensorsOnMap(windSourcesToShow); + } + }); + } + } + } + } + + /** + * We assume that overlapping segments usually don't require a lot of loading time as the most typical case will be to update a longer + * tail with a few new fixes that were received since the last time tick. Non-overlapping position requests typically occur for the + * first request when no fix at all is known for the competitor yet, and when the user has radically moved the time slider to some + * other time such that given the current tail length setting the new tail segment does not overlap with the old one, requiring a full + * load of the entire tail data for that competitor.

+ * + * For the non-overlapping requests, this method creates a separate request which only loads boat positions, quick ranks, sidelines and + * mark positions for the zero-length interval at newTime, assuming that this will work fairly fast and in particular in + * O(1) time regardless of tail length, compared to fetching the entire tail for all competitors. + */ + private GetRaceMapDataAction getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping( + Triple, Map, Map> fromAndToAndOverlap, + RegattaAndRaceIdentifier race, Date newTime) { + Map fromTimes = new HashMap<>(); + Map toTimes = new HashMap<>(); + for (Map.Entry e : fromAndToAndOverlap.getC().entrySet()) { + if (!e.getValue()) { + // no overlap; add competitor to request + fromTimes.put(e.getKey(), newTime); + toTimes.put(e.getKey(), newTime); + } + } + final GetRaceMapDataAction result; + if (!fromTimes.isEmpty()) { + result = new GetRaceMapDataAction(sailingService, competitorSelection.getAllCompetitors(), + race, useNullAsTimePoint() ? null : newTime, fromTimes, toTimes, /* extrapolate */true, (settings.isShowSimulationOverlay() ? simulationOverlay.getLegIdentifier() : null)); + } else { + result = null; + } + return result; + } + + private AsyncCallback getRaceMapDataCallback( + final Date oldTime, + final Date newTime, + final Map hasTailOverlapForCompetitor, + final Iterable competitorsToShow, final int requestID) { + return new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Error obtaining racemap data: " + caught.getMessage(), true /*silentMode */); + } + + @Override + public void onSuccess(RaceMapDataDTO raceMapDataDTO) { + if (map != null && raceMapDataDTO != null) { + quickRanks = raceMapDataDTO.quickRanks; + competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber = + raceMapDataDTO.competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber; + if (showViewSimulation && settings.isShowSimulationOverlay()) { + lastLegNumber = raceMapDataDTO.coursePositions.currentLegNumber; + simulationOverlay.updateLeg(Math.max(lastLegNumber,1), /* clearCanvas */ false, raceMapDataDTO.simulationResultVersion); + } + // process response only if not received out of order + if (startedProcessingRequestID < requestID) { + startedProcessingRequestID = requestID; + // Do boat specific actions + Map> boatData = raceMapDataDTO.boatPositions; + long timeForPositionTransitionMillis = calculateTimeForPositionTransition(newTime, oldTime); + fixesAndTails.updateFixes(boatData, hasTailOverlapForCompetitor, RaceMap.this, timeForPositionTransitionMillis); + showBoatsOnMap(newTime, timeForPositionTransitionMillis, getCompetitorsToShow()); + showCompetitorInfoOnMap(newTime, timeForPositionTransitionMillis, competitorSelection.getSelectedFilteredCompetitors()); + if (douglasMarkers != null) { + removeAllMarkDouglasPeuckerpoints(); + } + if (maneuverMarkers != null) { + removeAllManeuverMarkers(); + } + + // Do mark specific actions + showCourseMarksOnMap(raceMapDataDTO.coursePositions); + if (requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown) { + updateCoordinateSystemFromSettings(); + } + showCourseSidelinesOnMap(raceMapDataDTO.courseSidelines); + showStartAndFinishAndCourseMiddleLines(raceMapDataDTO.coursePositions); + showStartLineToFirstMarkTriangle(raceMapDataDTO.coursePositions); + // even though the wind data is retrieved by a separate call, re-draw the advantage line because it needs to + // adjust to new boat positions + showAdvantageLine(competitorsToShow, newTime); + + // Rezoom the map + LatLngBounds zoomToBounds = null; + if (!settings.getZoomSettings().containsZoomType(ZoomTypes.NONE)) { // Auto zoom if setting is not manual + zoomToBounds = settings.getZoomSettings().getNewBounds(RaceMap.this); + if (zoomToBounds == null && !mapFirstZoomDone) { + zoomToBounds = getDefaultZoomBounds(); // the user-specified zoom couldn't find what it was looking for; try defaults once + } + } else if (!mapFirstZoomDone) { // Zoom once to the marks if marks exist + zoomToBounds = new CourseMarksBoundsCalculator().calculateNewBounds(RaceMap.this); + if (zoomToBounds == null) { + zoomToBounds = getDefaultZoomBounds(); // use default zoom, e.g., + } + /* + * Reset the mapZoomedOrPannedSinceLastRaceSelection: In spite of the fact that + * the map was just zoomed to the bounds of the marks, it was not a zoom or pan + * triggered by the user. As a consequence the + * mapZoomedOrPannedSinceLastRaceSelection option has to reset again. + */ + } + zoomMapToNewBounds(zoomToBounds); + mapFirstZoomDone = true; + } + } else { + lastTimeChangeBeforeInitialization = newTime; + } + } + }; + } + + private void showCourseSidelinesOnMap(List sidelinesDTOs) { + if (map != null && sidelinesDTOs != null ) { + Map toRemoveSidelines = new HashMap(courseSidelines); + for (SidelineDTO sidelineDTO : sidelinesDTOs) { + if (sidelineDTO.getMarks().size() == 2) { // right now we only support sidelines with 2 marks + Polygon sideline = courseSidelines.get(sidelineDTO); + LatLng[] sidelinePoints = new LatLng[sidelineDTO.getMarks().size()]; + int i=0; + for (MarkDTO sidelineMark : sidelineDTO.getMarks()) { + sidelinePoints[i] = coordinateSystem.toLatLng(sidelineMark.position); + i++; + } + if (sideline == null) { + PolygonOptions options = PolygonOptions.newInstance(); + options.setClickable(true); + options.setStrokeColor("#0000FF"); + options.setStrokeWeight(1); + options.setStrokeOpacity(1.0); + options.setFillColor(null); + options.setFillOpacity(1.0); + + sideline = Polygon.newInstance(options); + MVCArray pointsAsArray = MVCArray.newInstance(sidelinePoints); + sideline.setPath(pointsAsArray); + + sideline.addMouseOverHandler(new MouseOverMapHandler() { + @Override + public void onEvent(MouseOverMapEvent event) { + map.setTitle(stringMessages.sideline()); + } + }); + sideline.addMouseOutMoveHandler(new MouseOutMapHandler() { + @Override + public void onEvent(MouseOutMapEvent event) { + map.setTitle(""); + } + }); + courseSidelines.put(sidelineDTO, sideline); + sideline.setMap(map); + } else { + sideline.getPath().removeAt(1); + sideline.getPath().removeAt(0); + sideline.getPath().insertAt(0, sidelinePoints[0]); + sideline.getPath().insertAt(1, sidelinePoints[1]); + toRemoveSidelines.remove(sidelineDTO); + } + } + } + for (SidelineDTO toRemoveSideline : toRemoveSidelines.keySet()) { + Polygon sideline = courseSidelines.remove(toRemoveSideline); + sideline.setMap(null); + } + } + } + + private void showCourseMarksOnMap(CoursePositionsDTO courseDTO) { + if (map != null && courseDTO != null) { + WaypointDTO endWaypointForCurrentLegNumber = null; + if(courseDTO.currentLegNumber > 0 && courseDTO.currentLegNumber <= courseDTO.totalLegsCount) { + endWaypointForCurrentLegNumber = courseDTO.getEndWaypointForLegNumber(courseDTO.currentLegNumber); + } + + Map toRemoveCourseMarks = new HashMap(courseMarkOverlays); + if (courseDTO.marks != null) { + for (MarkDTO markDTO : courseDTO.marks) { + boolean isSelected = false; + if (endWaypointForCurrentLegNumber != null && Util.contains(endWaypointForCurrentLegNumber.controlPoint.getMarks(), markDTO)) { + isSelected = true; + } + CourseMarkOverlay courseMarkOverlay = courseMarkOverlays.get(markDTO.getName()); + if (courseMarkOverlay == null) { + courseMarkOverlay = createCourseMarkOverlay(RaceMapOverlaysZIndexes.COURSEMARK_ZINDEX, markDTO); + courseMarkOverlay.setShowBuoyZone(settings.getHelpLinesSettings().isVisible(HelpLineTypes.BUOYZONE)); + courseMarkOverlay.setBuoyZoneRadiusInMeter(settings.getBuoyZoneRadiusInMeters()); + courseMarkOverlay.setSelected(isSelected); + courseMarkOverlays.put(markDTO.getName(), courseMarkOverlay); + markDTOs.put(markDTO.getName(), markDTO); + courseMarkOverlay.addToMap(); + } else { + courseMarkOverlay.setMarkPosition(markDTO.position); + courseMarkOverlay.setShowBuoyZone(settings.getHelpLinesSettings().isVisible(HelpLineTypes.BUOYZONE)); + courseMarkOverlay.setBuoyZoneRadiusInMeter(settings.getBuoyZoneRadiusInMeters()); + courseMarkOverlay.setSelected(isSelected); + courseMarkOverlay.draw(); + toRemoveCourseMarks.remove(markDTO.getName()); + } + } + } + for (String toRemoveMarkName : toRemoveCourseMarks.keySet()) { + CourseMarkOverlay removedOverlay = courseMarkOverlays.remove(toRemoveMarkName); + if(removedOverlay != null) { + removedOverlay.removeFromMap(); + } + } + } + } + + /** + * Based on the mark positions in {@link #courseMarkOverlays}' values this method determines the center of gravity of these marks' + * {@link CourseMarkOverlay#getPosition() positions}. + */ + private Position getCenterOfCourse() { + ScalablePosition center = null; + int count = 0; + for (CourseMarkOverlay markOverlay : courseMarkOverlays.values()) { + ScalablePosition markPosition = new ScalablePosition(markOverlay.getPosition()); + if (center == null) { + center = markPosition; + } else { + center.add(markPosition); + } + count++; + } + return center == null ? null : center.divide(count); + } + + private void showCombinedWindOnMap(WindSource windSource, WindTrackInfoDTO windTrackInfoDTO) { + if (map != null) { + combinedWindPanel.setWindInfo(windTrackInfoDTO, windSource); + combinedWindPanel.redraw(); + } + } + + private void showWindSensorsOnMap(List> windSensorsList) { + if (map != null) { + Set toRemoveWindSources = new HashSet(windSensorOverlays.keySet()); + for (com.sap.sse.common.Util.Pair windSourcePair : windSensorsList) { + WindSource windSource = windSourcePair.getA(); + WindTrackInfoDTO windTrackInfoDTO = windSourcePair.getB(); + + WindSensorOverlay windSensorOverlay = windSensorOverlays.get(windSource); + if (windSensorOverlay == null) { + windSensorOverlay = createWindSensorOverlay(RaceMapOverlaysZIndexes.WINDSENSOR_ZINDEX, windSource, windTrackInfoDTO); + windSensorOverlays.put(windSource, windSensorOverlay); + windSensorOverlay.addToMap(); + } else { + windSensorOverlay.setWindInfo(windTrackInfoDTO, windSource); + windSensorOverlay.draw(); + toRemoveWindSources.remove(windSource); + } + } + for (WindSource toRemoveWindSource : toRemoveWindSources) { + WindSensorOverlay removedWindSensorOverlay = windSensorOverlays.remove(toRemoveWindSource); + if (removedWindSensorOverlay != null) { + removedWindSensorOverlay.removeFromMap(); + } + } + } + } + + private void showCompetitorInfoOnMap(final Date newTime, final long timeForPositionTransitionMillis, final Iterable competitorsToShow) { + if (map != null) { + if (settings.isShowSelectedCompetitorsInfo()) { + Set toRemoveCompetorInfoOverlays = new HashSet( + competitorInfoOverlays.keySet()); + for (CompetitorDTO competitorDTO : competitorsToShow) { + if (fixesAndTails.hasFixesFor(competitorDTO)) { + GPSFixDTO lastBoatFix = getBoatFix(competitorDTO, newTime); + if (lastBoatFix != null) { + CompetitorInfoOverlay competitorInfoOverlay = competitorInfoOverlays.get(competitorDTO); + if (competitorInfoOverlay == null) { + competitorInfoOverlay = createCompetitorInfoOverlay(RaceMapOverlaysZIndexes.INFO_OVERLAY_ZINDEX, competitorDTO); + competitorInfoOverlays.put(competitorDTO, competitorInfoOverlay); + competitorInfoOverlay.setPosition(lastBoatFix.position, timeForPositionTransitionMillis); + competitorInfoOverlay.addToMap(); + } else { + competitorInfoOverlay.setPosition(lastBoatFix.position, timeForPositionTransitionMillis); + competitorInfoOverlay.draw(); + } + toRemoveCompetorInfoOverlays.remove(competitorDTO); + } + } + } + for (CompetitorDTO toRemoveCompetorDTO : toRemoveCompetorInfoOverlays) { + CompetitorInfoOverlay competitorInfoOverlay = competitorInfoOverlays.get(toRemoveCompetorDTO); + competitorInfoOverlay.removeFromMap(); + competitorInfoOverlays.remove(toRemoveCompetorDTO); + } + } else { + // remove all overlays + for (CompetitorInfoOverlay competitorInfoOverlay : competitorInfoOverlays.values()) { + competitorInfoOverlay.removeFromMap(); + } + competitorInfoOverlays.clear(); + } + } + } + + private long calculateTimeForPositionTransition(final Date newTime, final Date oldTime) { + final long timeForPositionTransitionMillis; + boolean hasTimeJumped = oldTime != null && Math.abs(oldTime.getTime() - newTime.getTime()) > 3*timer.getRefreshInterval(); + if (timer.getPlayState() == PlayStates.Playing && !hasTimeJumped) { + // choose 130% of the refresh interval as transition period to make it unlikely that the transition + // stops before the next update has been received + timeForPositionTransitionMillis = 1300 * timer.getRefreshInterval() / 1000; + } else { + timeForPositionTransitionMillis = -1; // -1 means 'no transition + } + return timeForPositionTransitionMillis; + } + + private void showBoatsOnMap(final Date newTime, final long timeForPositionTransitionMillis, final Iterable competitorsToShow) { + if (map != null) { + Date tailsFromTime = new Date(newTime.getTime() - settings.getEffectiveTailLengthInMilliseconds()); + Date tailsToTime = newTime; + Set competitorDTOsOfUnusedTails = new HashSet(fixesAndTails.getCompetitorsWithTails()); + Set competitorDTOsOfUnusedBoatCanvases = new HashSet(boatOverlays.keySet()); + for (CompetitorDTO competitorDTO : competitorsToShow) { + if (fixesAndTails.hasFixesFor(competitorDTO)) { + Polyline tail = fixesAndTails.getTail(competitorDTO); + if (tail == null) { + tail = fixesAndTails.createTailAndUpdateIndices(competitorDTO, tailsFromTime, tailsToTime, this); + } else { + fixesAndTails.updateTail(tail, competitorDTO, tailsFromTime, tailsToTime, + (int) (timeForPositionTransitionMillis==-1?-1:timeForPositionTransitionMillis/2)); + competitorDTOsOfUnusedTails.remove(competitorDTO); + PolylineOptions newOptions = createTailStyle(competitorDTO, displayHighlighted(competitorDTO)); + tail.setOptions(newOptions); + } + boolean usedExistingBoatCanvas = updateBoatCanvasForCompetitor(competitorDTO, newTime, timeForPositionTransitionMillis); + if (usedExistingBoatCanvas) { + competitorDTOsOfUnusedBoatCanvases.remove(competitorDTO); + } + } + } + for (CompetitorDTO unusedBoatCanvasCompetitorDTO : competitorDTOsOfUnusedBoatCanvases) { + BoatOverlay boatCanvas = boatOverlays.get(unusedBoatCanvasCompetitorDTO); + boatCanvas.removeFromMap(); + boatOverlays.remove(unusedBoatCanvasCompetitorDTO); + } + for (CompetitorDTO unusedTailCompetitorDTO : competitorDTOsOfUnusedTails) { + fixesAndTails.removeTail(unusedTailCompetitorDTO); + } + } + } + + /** + * This algorithm is limited to distances such that dlon < pi/2, i.e., those that extend around less than one + * quarter of the circumference of the earth in longitude. A completely general, but more complicated algorithm is + * necessary if greater distances are allowed. + */ + public LatLng calculatePositionAlongRhumbline(LatLng position, double bearingDeg, double distanceInKm) { + double distianceRad = distanceInKm / 6371.0; // r = 6371 means earth's radius in km + double lat1 = position.getLatitude() / 180. * Math.PI; + double lon1 = position.getLongitude() / 180. * Math.PI; + double bearingRad = bearingDeg / 180. * Math.PI; + double lat2 = Math.asin(Math.sin(lat1) * Math.cos(distianceRad) + + Math.cos(lat1) * Math.sin(distianceRad) * Math.cos(bearingRad)); + double lon2 = lon1 + Math.atan2(Math.sin(bearingRad)*Math.sin(distianceRad)*Math.cos(lat1), + Math.cos(distianceRad)-Math.sin(lat1)*Math.sin(lat2)); + lon2 = (lon2+3*Math.PI) % (2*Math.PI) - Math.PI; // normalize to -180..+180� + // position is already in LatLng space, so no mapping through coordinateSystem is required here + return LatLng.newInstance(lat2 / Math.PI * 180., lon2 / Math.PI * 180.); + } + + /** + * Returns a pair whose first component is the leg number (one-based) of the competitor returned as the second component. + */ + private com.sap.sse.common.Util.Pair getFarthestAheadVisibleCompetitorWithOneBasedLegNumber( + Iterable competitorsToShow) { + CompetitorDTO leadingCompetitorDTO = null; + int legOfLeaderCompetitor = -1; + // this only works because the quickRanks are sorted + for (Entry competitorsByWindwardDistanceTraveledAndOneBasedLegNumber : + competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber.entrySet()) { + if (Util.contains(competitorsToShow, competitorsByWindwardDistanceTraveledAndOneBasedLegNumber.getKey()) && + competitorsByWindwardDistanceTraveledAndOneBasedLegNumber.getValue() != null) { + leadingCompetitorDTO = competitorsByWindwardDistanceTraveledAndOneBasedLegNumber.getKey(); + legOfLeaderCompetitor = competitorsByWindwardDistanceTraveledAndOneBasedLegNumber.getValue(); + return new com.sap.sse.common.Util.Pair(legOfLeaderCompetitor, leadingCompetitorDTO); + } + } + return null; + } + + private void showAdvantageLine(Iterable competitorsToShow, Date date) { + if (map != null && lastRaceTimesInfo != null && quickRanks != null && lastCombinedWindTrackInfoDTO != null) { + boolean drawAdvantageLine = false; + if (settings.getHelpLinesSettings().isVisible(HelpLineTypes.ADVANTAGELINE)) { + // find competitor with highest rank + com.sap.sse.common.Util.Pair visibleLeaderInfo = getFarthestAheadVisibleCompetitorWithOneBasedLegNumber(competitorsToShow); + // the boat fix may be null; may mean that no positions were loaded yet for the leading visible boat; + // don't show anything + GPSFixDTO lastBoatFix = null; + boolean isVisibleLeaderInfoComplete = false; + boolean isLegTypeKnown = false; + WindTrackInfoDTO windDataForLegMiddle = null; + LegInfoDTO legInfoDTO = null; + if (visibleLeaderInfo != null + && visibleLeaderInfo.getA() > 0 + && visibleLeaderInfo.getA() <= lastRaceTimesInfo.getLegInfos().size() + // get wind at middle of leg for leading visible competitor + && (windDataForLegMiddle = lastCombinedWindTrackInfoDTO + .getCombinedWindOnLegMiddle(visibleLeaderInfo.getA() - 1)) != null + && !windDataForLegMiddle.windFixes.isEmpty()) { + isVisibleLeaderInfoComplete = true; + legInfoDTO = lastRaceTimesInfo.getLegInfos().get(visibleLeaderInfo.getA() - 1); + if (legInfoDTO.legType != null) { + isLegTypeKnown = true; + } + lastBoatFix = getBoatFix(visibleLeaderInfo.getB(), date); + } + if (isVisibleLeaderInfoComplete && isLegTypeKnown && lastBoatFix != null && lastBoatFix.speedWithBearing != null) { + double advantageLineLengthInKm = 1.0; // TODO this should probably rather scale with the visible + // area of the map; bug 616 + double distanceFromBoatPositionInKm = visibleLeaderInfo.getB().getBoatClass() + .getHullLengthInMeters() / 1000.; // one hull length + // implement and use Position.translateRhumb() + double bearingOfBoatInDeg = lastBoatFix.speedWithBearing.bearingInDegrees; + LatLng boatPosition = coordinateSystem.toLatLng(lastBoatFix.position); + LatLng posAheadOfFirstBoat = calculatePositionAlongRhumbline(boatPosition, + coordinateSystem.mapDegreeBearing(bearingOfBoatInDeg), distanceFromBoatPositionInKm); + final WindDTO windFix = windDataForLegMiddle.windFixes.get(0); + double bearingOfCombinedWindInDeg = windFix.trueWindBearingDeg; + double rotatedBearingDeg1 = 0.0; + double rotatedBearingDeg2 = 0.0; + switch (legInfoDTO.legType) { + case UPWIND: + case DOWNWIND: { + rotatedBearingDeg1 = bearingOfCombinedWindInDeg + 90.0; + if (rotatedBearingDeg1 >= 360.0) { + rotatedBearingDeg1 -= 360.0; + } + rotatedBearingDeg2 = bearingOfCombinedWindInDeg - 90.0; + if (rotatedBearingDeg2 < 0.0) { + rotatedBearingDeg2 += 360.0; + } + } + break; + case REACHING: { + rotatedBearingDeg1 = legInfoDTO.legBearingInDegrees + 90.0; + if (rotatedBearingDeg1 >= 360.0) { + rotatedBearingDeg1 -= 360.0; + } + rotatedBearingDeg2 = legInfoDTO.legBearingInDegrees - 90.0; + if (rotatedBearingDeg2 < 0.0) { + rotatedBearingDeg2 += 360.0; + } + } + break; + } + LatLng advantageLinePos1 = calculatePositionAlongRhumbline(posAheadOfFirstBoat, + coordinateSystem.mapDegreeBearing(rotatedBearingDeg1), advantageLineLengthInKm / 2.0); + LatLng advantageLinePos2 = calculatePositionAlongRhumbline(posAheadOfFirstBoat, + coordinateSystem.mapDegreeBearing(rotatedBearingDeg2), advantageLineLengthInKm / 2.0); + if (advantageLine == null) { + PolylineOptions options = PolylineOptions.newInstance(); + options.setClickable(true); + options.setGeodesic(true); + options.setStrokeColor("#000000"); + options.setStrokeWeight(1); + options.setStrokeOpacity(0.5); + + advantageLine = Polyline.newInstance(options); + MVCArray pointsAsArray = MVCArray.newInstance(); + pointsAsArray.insertAt(0, advantageLinePos1); + pointsAsArray.insertAt(1, advantageLinePos2); + advantageLine.setPath(pointsAsArray); + advantageLine.setMap(map); + Hoverline advantageHoverline = new Hoverline(advantageLine, options, this); + + advantageLineMouseOverHandler = new AdvantageLineMouseOverMapHandler( + bearingOfCombinedWindInDeg, new Date(windFix.measureTimepoint)); + advantageLine.addMouseOverHandler(advantageLineMouseOverHandler); + advantageHoverline.addMouseOutMoveHandler(new MouseOutMapHandler() { + @Override + public void onEvent(MouseOutMapEvent event) { + map.setTitle(""); + } + }); + } else { + advantageLine.getPath().removeAt(1); + advantageLine.getPath().removeAt(0); + advantageLine.getPath().insertAt(0, advantageLinePos1); + advantageLine.getPath().insertAt(1, advantageLinePos2); + advantageLineMouseOverHandler.setTrueWindBearing(bearingOfCombinedWindInDeg); + advantageLineMouseOverHandler.setDate(new Date(windFix.measureTimepoint)); + } + drawAdvantageLine = true; + } + } + if (!drawAdvantageLine) { + if (advantageLine != null) { + advantageLine.setMap(null); + advantageLine = null; + } + } + } + } + + private final StringBuilder windwardStartLineMarkToFirstMarkLineText = new StringBuilder(); + private final StringBuilder leewardStartLineMarkToFirstMarkLineText = new StringBuilder(); + + private void showStartLineToFirstMarkTriangle(final CoursePositionsDTO courseDTO){ + final List startMarkPositions = courseDTO.getStartMarkPositions(); + final Position windwardStartLinePosition = startMarkPositions.get(0); + final Position leewardStartLinePosition = startMarkPositions.get(1); + final Position firstMarkPosition = courseDTO.waypointPositions.get(1); + windwardStartLineMarkToFirstMarkLineText.replace(0, windwardStartLineMarkToFirstMarkLineText.length(), + stringMessages.startLineToFirstMarkTriangle(numberFormatOneDecimal + .format(windwardStartLinePosition.getDistance(firstMarkPosition) + .getMeters()))); + leewardStartLineMarkToFirstMarkLineText.replace(0, leewardStartLineMarkToFirstMarkLineText.length(), + stringMessages.startLineToFirstMarkTriangle(numberFormatOneDecimal + .format(leewardStartLinePosition.getDistance(firstMarkPosition) + .getMeters()))); + final LineInfoProvider windwardStartLineMarkToFirstMarkLineInfoProvider = new LineInfoProvider() { + @Override + public String getLineInfo() { + return windwardStartLineMarkToFirstMarkLineText.toString(); + } + }; + final LineInfoProvider leewardStartLineMarkToFirstMarkLineInfoProvider = new LineInfoProvider() { + @Override + public String getLineInfo() { + return leewardStartLineMarkToFirstMarkLineText.toString(); + } + }; + windwardStartLineMarkToFirstMarkLine = showOrRemoveOrUpdateLine(windwardStartLineMarkToFirstMarkLine, /* showLine */ + (settings.getHelpLinesSettings().isVisible(HelpLineTypes.STARTLINETOFIRSTMARKTRIANGLE)) + && startMarkPositions.size() > 1 && courseDTO.waypointPositions.size() > 1, + windwardStartLinePosition, firstMarkPosition, windwardStartLineMarkToFirstMarkLineInfoProvider, + "grey"); + leewardStartLineMarkToFirstMarkLine = showOrRemoveOrUpdateLine(leewardStartLineMarkToFirstMarkLine, /* showLine */ + (settings.getHelpLinesSettings().isVisible(HelpLineTypes.STARTLINETOFIRSTMARKTRIANGLE)) + && startMarkPositions.size() > 1 && courseDTO.waypointPositions.size() > 1, + leewardStartLinePosition, firstMarkPosition, leewardStartLineMarkToFirstMarkLineInfoProvider, + "grey"); + } + + private final StringBuilder startLineAdvantageText = new StringBuilder(); + private final StringBuilder finishLineAdvantageText = new StringBuilder(); + final LineInfoProvider startLineInfoProvider = new LineInfoProvider() { + @Override + public String getLineInfo() { + return stringMessages.startLine()+startLineAdvantageText; + } + }; + final LineInfoProvider finishLineInfoProvider = new LineInfoProvider() { + @Override + public String getLineInfo() { + return stringMessages.finishLine()+finishLineAdvantageText; + } + }; + + private void showStartAndFinishAndCourseMiddleLines(final CoursePositionsDTO courseDTO) { + if (map != null && courseDTO != null && courseDTO.course != null && courseDTO.course.waypoints != null && + !courseDTO.course.waypoints.isEmpty()) { + // draw the start line + final WaypointDTO startWaypoint = courseDTO.course.waypoints.get(0); + updateCountdownCanvas(startWaypoint); + final int numberOfStartWaypointMarks = courseDTO.getStartMarkPositions() == null ? 0 : courseDTO.getStartMarkPositions().size(); + final int numberOfFinishWaypointMarks = courseDTO.getFinishMarkPositions() == null ? 0 : courseDTO.getFinishMarkPositions().size(); + final Position startLineLeftPosition = numberOfStartWaypointMarks == 0 ? null : courseDTO.getStartMarkPositions().get(0); + final Position startLineRightPosition = numberOfStartWaypointMarks < 2 ? null : courseDTO.getStartMarkPositions().get(1); + if (courseDTO.startLineAngleToCombinedWind != null) { + startLineAdvantageText.replace(0, startLineAdvantageText.length(), " "+stringMessages.lineAngleToWindAndAdvantage( + NumberFormat.getFormat("0.0").format(courseDTO.startLineLengthInMeters), + NumberFormat.getFormat("0.0").format(Math.abs(courseDTO.startLineAngleToCombinedWind)), + courseDTO.startLineAdvantageousSide.name().charAt(0)+courseDTO.startLineAdvantageousSide.name().substring(1).toLowerCase(), + NumberFormat.getFormat("0.0").format(courseDTO.startLineAdvantageInMeters))); + } else { + startLineAdvantageText.delete(0, startLineAdvantageText.length()); + } + final boolean showStartLineBasedOnCurrentLeg = numberOfStartWaypointMarks == 2 && courseDTO.currentLegNumber <= 1; + final boolean showFinishLineBasedOnCurrentLeg = numberOfFinishWaypointMarks == 2 && courseDTO.currentLegNumber == courseDTO.totalLegsCount; + // show the line when STARTLINE is selected and the current leg is around the start leg, + // or when COURSEGEOMETRY is selected and the finish line isn't equal and wouldn't be shown at the same time based on the current leg. + // With this, if COURSEGEOMETRY is selected and start and finish line are equal, the start line will not be displayed if + // based on the race progress the finish line is to be preferred, so only the finish line will be shown. + final boolean reallyShowStartLine = + (settings.getHelpLinesSettings().isVisible(HelpLineTypes.STARTLINE) && showStartLineBasedOnCurrentLeg) || + (settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY) && + (!showFinishLineBasedOnCurrentLeg || !startLineEqualsFinishLine(courseDTO))); + // show the line when FINISHLINE is selected and the current leg is the last leg, + // or when COURSEGEOMETRY is selected and the start line isn't equal or the current leg is the last leg. + // With this, if COURSEGEOMETRY is selected and start and finish line are equal, the start line will be displayed unless + // the finish line should take precedence based on race progress. + final boolean reallyShowFinishLine = showFinishLineBasedOnCurrentLeg && + (!showStartLineBasedOnCurrentLeg || !startLineEqualsFinishLine(courseDTO)) && + (settings.getHelpLinesSettings().isVisible(HelpLineTypes.FINISHLINE) && showFinishLineBasedOnCurrentLeg) || + (settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY) && + (!startLineEqualsFinishLine(courseDTO) || showFinishLineBasedOnCurrentLeg)); + startLine = showOrRemoveOrUpdateLine(startLine, reallyShowStartLine, + startLineLeftPosition, startLineRightPosition, startLineInfoProvider, "#ffffff"); + // draw the finish line + final Position finishLineLeftPosition = numberOfFinishWaypointMarks == 0 ? null : courseDTO.getFinishMarkPositions().get(0); + final Position finishLineRightPosition = numberOfFinishWaypointMarks < 2 ? null : courseDTO.getFinishMarkPositions().get(1); + if (courseDTO.finishLineAngleToCombinedWind != null) { + finishLineAdvantageText.replace(0, finishLineAdvantageText.length(), " "+stringMessages.lineAngleToWindAndAdvantage( + NumberFormat.getFormat("0.0").format(courseDTO.finishLineLengthInMeters), + NumberFormat.getFormat("0.0").format(Math.abs(courseDTO.finishLineAngleToCombinedWind)), + courseDTO.finishLineAdvantageousSide.name().charAt(0)+courseDTO.finishLineAdvantageousSide.name().substring(1).toLowerCase(), + NumberFormat.getFormat("0.0").format(courseDTO.finishLineAdvantageInMeters))); + } else { + finishLineAdvantageText.delete(0, finishLineAdvantageText.length()); + } + finishLine = showOrRemoveOrUpdateLine(finishLine, reallyShowFinishLine, + finishLineLeftPosition, finishLineRightPosition, finishLineInfoProvider, "#000000"); + // the control point pairs for which we already decided whether or not + // to show a course middle line for; values tell whether to show the line and for which zero-based + // start waypoint index to do so; when for an equal control point pair multiple decisions with different + // outcome are made, a decision to show the line overrules the decision to not show it (OR-semantics) + final Map, Pair> keysAlreadyHandled = new HashMap<>(); + for (int zeroBasedIndexOfStartWaypoint = 0; zeroBasedIndexOfStartWaypoint key = getCourseMiddleLinesKey(courseDTO, zeroBasedIndexOfStartWaypoint); + boolean showCourseMiddleLine = keysAlreadyHandled.containsKey(key) && keysAlreadyHandled.get(key).getA() || + settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY) || + (settings.getHelpLinesSettings().isVisible(HelpLineTypes.COURSEMIDDLELINE) + && courseDTO.currentLegNumber > 0 + && courseDTO.currentLegNumber-1 == zeroBasedIndexOfStartWaypoint); + keysAlreadyHandled.put(key, new Pair<>(showCourseMiddleLine, zeroBasedIndexOfStartWaypoint)); + } + Set> keysToConsider = new HashSet<>(keysAlreadyHandled.keySet()); + keysToConsider.addAll(courseMiddleLines.keySet()); + for (final Set key : keysToConsider) { + final int zeroBasedIndexOfStartWaypoint = keysAlreadyHandled.containsKey(key) ? + keysAlreadyHandled.get(key).getB() : 0; // if not handled, the line will be removed, so the waypoint index doesn't matter + final Pair showLineAndZeroBasedIndexOfStartWaypoint = keysAlreadyHandled.get(key); + final boolean showCourseMiddleLine = showLineAndZeroBasedIndexOfStartWaypoint != null && showLineAndZeroBasedIndexOfStartWaypoint.getA(); + courseMiddleLines.put(key, showOrRemoveCourseMiddleLine(courseDTO, courseMiddleLines.get(key), zeroBasedIndexOfStartWaypoint, showCourseMiddleLine)); + } + } + } + + private boolean startLineEqualsFinishLine(CoursePositionsDTO courseDTO) { + final List waypoints; + return courseDTO != null && courseDTO.course != null && + (waypoints = courseDTO.course.waypoints) != null && + waypoints.get(0).controlPoint.equals(waypoints.get(waypoints.size()-1).controlPoint); + } + + /** + * Given a zero-based index into courseDTO's {@link RaceCourseDTO#waypoints waypoints list} that denotes the start + * waypoint of the leg in question, returns a key that can be used for the {@link #courseMiddleLines} map, consisting of a set + * that holds the two {@link ControlPointDTO}s representing the start and finish control point of that leg. + */ + private Set getCourseMiddleLinesKey(final CoursePositionsDTO courseDTO, + final int zeroBasedIndexOfStartWaypoint) { + ControlPointDTO startControlPoint = courseDTO.course.waypoints.get(zeroBasedIndexOfStartWaypoint).controlPoint; + ControlPointDTO endControlPoint = courseDTO.course.waypoints.get(zeroBasedIndexOfStartWaypoint+1).controlPoint; + final Set key = new HashSet<>(); + key.add(startControlPoint); + key.add(endControlPoint); + return key; + } + + /** + * @param showLine + * tells whether or not to show the line; if the lineToShowOrRemoveOrUpdate references a + * line but the line shall not be shown, the line is removed from the map; conversely, if the line is not + * yet shown but shall be, a new line is created, added to the map and returned. If the line is shown and + * shall continue to be shown, the line is returned after updating its vertex coordinates. + * @return null if the line is not shown; the polyline object representing the line being displayed + * otherwise + */ + private Polyline showOrRemoveCourseMiddleLine(final CoursePositionsDTO courseDTO, Polyline lineToShowOrRemoveOrUpdate, + final int zeroBasedIndexOfStartWaypoint, final boolean showLine) { + final Position position1DTO = courseDTO.waypointPositions.get(zeroBasedIndexOfStartWaypoint); + final Position position2DTO = courseDTO.waypointPositions.get(zeroBasedIndexOfStartWaypoint+1); + final LineInfoProvider lineInfoProvider = new LineInfoProvider() { + @Override + public String getLineInfo() { + final StringBuilder sb = new StringBuilder(); + sb.append(stringMessages.courseMiddleLine()); + sb.append('\n'); + sb.append(NumberFormat.getFormat("0").format( + Math.abs(position1DTO.getDistance(position2DTO).getMeters()))+stringMessages.metersUnit()); + if (lastCombinedWindTrackInfoDTO != null) { + final WindTrackInfoDTO windTrackAtLegMiddle = lastCombinedWindTrackInfoDTO.getCombinedWindOnLegMiddle(zeroBasedIndexOfStartWaypoint); + if (windTrackAtLegMiddle != null && windTrackAtLegMiddle.windFixes != null && !windTrackAtLegMiddle.windFixes.isEmpty()) { + WindDTO windAtLegMiddle = windTrackAtLegMiddle.windFixes.get(0); + final double legBearingDeg = position1DTO.getBearingGreatCircle(position2DTO).getDegrees(); + final String diff = NumberFormat.getFormat("0.0").format( + Math.min(Math.abs(windAtLegMiddle.dampenedTrueWindBearingDeg-legBearingDeg), + Math.abs(windAtLegMiddle.dampenedTrueWindFromDeg-legBearingDeg))); + sb.append(", "); + sb.append(stringMessages.degreesToWind(diff)); + } + } + return sb.toString(); + } + }; + return showOrRemoveOrUpdateLine(lineToShowOrRemoveOrUpdate, showLine, position1DTO, position2DTO, lineInfoProvider, "#2268a0"); + } + + private interface LineInfoProvider { + String getLineInfo(); + } + + /** + * @param showLine + * tells whether or not to show the line; if the lineToShowOrRemoveOrUpdate references a + * line but the line shall not be shown, the line is removed from the map; conversely, if the line is not + * yet shown but shall be, a new line is created, added to the map and returned. If the line is shown and + * shall continue to be shown, the line is returned after updating its vertex coordinates. + * @return null if the line is not shown; the polyline object representing the line being displayed + * otherwise + */ + private Polyline showOrRemoveOrUpdateLine(Polyline lineToShowOrRemoveOrUpdate, final boolean showLine, + final Position position1DTO, final Position position2DTO, final LineInfoProvider lineInfoProvider, String lineColorRGB) { + if (showLine) { + LatLng courseMiddleLinePoint1 = coordinateSystem.toLatLng(position1DTO); + LatLng courseMiddleLinePoint2 = coordinateSystem.toLatLng(position2DTO); + final MVCArray pointsAsArray; + if (lineToShowOrRemoveOrUpdate == null) { + PolylineOptions options = PolylineOptions.newInstance(); + options.setClickable(true); + options.setGeodesic(true); + options.setStrokeColor(lineColorRGB); + options.setStrokeWeight(1); + options.setStrokeOpacity(1.0); + pointsAsArray = MVCArray.newInstance(); + lineToShowOrRemoveOrUpdate = Polyline.newInstance(options); + lineToShowOrRemoveOrUpdate.setPath(pointsAsArray); + lineToShowOrRemoveOrUpdate.setMap(map); + Hoverline lineToShowOrRemoveOrUpdateHoverline = new Hoverline(lineToShowOrRemoveOrUpdate, options, this); + lineToShowOrRemoveOrUpdate.addMouseOverHandler(new MouseOverMapHandler() { + @Override + public void onEvent(MouseOverMapEvent event) { + map.setTitle(lineInfoProvider.getLineInfo()); + } + }); + lineToShowOrRemoveOrUpdateHoverline.addMouseOutMoveHandler(new MouseOutMapHandler() { + @Override + public void onEvent(MouseOutMapEvent event) { + map.setTitle(""); + } + }); + } else { + pointsAsArray = lineToShowOrRemoveOrUpdate.getPath(); + pointsAsArray.removeAt(1); + pointsAsArray.removeAt(0); + } + adjustInfoOverlayForVisibleLine(lineToShowOrRemoveOrUpdate, position1DTO, position2DTO, lineInfoProvider); + pointsAsArray.insertAt(0, courseMiddleLinePoint1); + pointsAsArray.insertAt(1, courseMiddleLinePoint2); + } else { + if (lineToShowOrRemoveOrUpdate != null) { + lineToShowOrRemoveOrUpdate.setMap(null); + adjustInfoOverlayForRemovedLine(lineToShowOrRemoveOrUpdate); + lineToShowOrRemoveOrUpdate = null; + } + } + return lineToShowOrRemoveOrUpdate; + } + + private void adjustInfoOverlayForRemovedLine(Polyline lineToShowOrRemoveOrUpdate) { + SmallTransparentInfoOverlay infoOverlay = infoOverlaysForLinesForCourseGeometry.remove(lineToShowOrRemoveOrUpdate); + if (infoOverlay != null) { + infoOverlay.removeFromMap(); + } + } + + private void adjustInfoOverlayForVisibleLine(Polyline lineToShowOrRemoveOrUpdate, final Position position1DTO, + final Position position2DTO, final LineInfoProvider lineInfoProvider) { + SmallTransparentInfoOverlay infoOverlay = infoOverlaysForLinesForCourseGeometry.get(lineToShowOrRemoveOrUpdate); + if (getSettings().getHelpLinesSettings().isVisible(HelpLineTypes.COURSEGEOMETRY)) { + if (infoOverlay == null) { + infoOverlay = new SmallTransparentInfoOverlay(map, RaceMapOverlaysZIndexes.INFO_OVERLAY_ZINDEX, lineInfoProvider.getLineInfo(), coordinateSystem); + infoOverlaysForLinesForCourseGeometry.put(lineToShowOrRemoveOrUpdate, infoOverlay); + infoOverlay.addToMap(); + } else { + infoOverlay.setInfoText(lineInfoProvider.getLineInfo()); + } + infoOverlay.setPosition(position1DTO.translateGreatCircle(position1DTO.getBearingGreatCircle(position2DTO), + position1DTO.getDistance(position2DTO).scale(0.5)), /* transition time */ -1); + infoOverlay.draw(); + } else { + if (infoOverlay != null) { + infoOverlay.removeFromMap(); + infoOverlaysForLinesForCourseGeometry.remove(lineToShowOrRemoveOrUpdate); + } + } + } + + /** + * If, according to {@link #lastRaceTimesInfo} and {@link #timer} the race is + * still in the pre-start phase, show a {@link SmallTransparentInfoOverlay} at the + * start line that shows the count down. + */ + private void updateCountdownCanvas(WaypointDTO startWaypoint) { + if (!settings.isShowSelectedCompetitorsInfo() || startWaypoint == null || Util.isEmpty(startWaypoint.controlPoint.getMarks()) + || lastRaceTimesInfo == null || lastRaceTimesInfo.startOfRace == null || timer.getTime().after(lastRaceTimesInfo.startOfRace)) { + if (countDownOverlay != null) { + countDownOverlay.removeFromMap(); + countDownOverlay = null; + } + } else { + long timeToStartInMs = lastRaceTimesInfo.startOfRace.getTime() - timer.getTime().getTime(); + String countDownText = timeToStartInMs > 1000 ? stringMessages.timeToStart(DateAndTimeFormatterUtil + .formatElapsedTime(timeToStartInMs)) : stringMessages.start(); + if (countDownOverlay == null) { + countDownOverlay = new SmallTransparentInfoOverlay(map, RaceMapOverlaysZIndexes.INFO_OVERLAY_ZINDEX, + countDownText, coordinateSystem); + countDownOverlay.addToMap(); + } else { + countDownOverlay.setInfoText(countDownText); + } + countDownOverlay.setPosition(startWaypoint.controlPoint.getMarks().iterator().next().position, /* transition time */ -1); + countDownOverlay.draw(); + } + } + + // Google scales coordinates so that the globe-tile has mercator-latitude [-pi, +pi], i.e. tile height of 2*pi + // mercator-latitude pi corresponds to geo-latitude of approx. 85.0998 (where Google cuts off the map visualization) + // official documentation: http://developers.google.com/maps/documentation/javascript/maptypes#TileCoordinates + private double getMercatorLatitude(double lat) { + // cutting-off for latitudes close to +-90 degrees is recommended (to avoid division by zero) + double sine = Math.max(-0.9999, Math.min(0.9999, Math.sin(Math.PI * lat / 180))); + return Math.log((1 + sine) / (1 - sine)) / 2; + } + + private int getZoomLevel(LatLngBounds bounds) { + int GLOBE_PXSIZE = 256; // a constant in Google's map projection + int MAX_ZOOM = 20; // maximum zoom-level that should be automatically selected + double LOG2 = Math.log(2.0); + double deltaLng = bounds.getNorthEast().getLongitude() - bounds.getSouthWest().getLongitude(); + double deltaLat = getMercatorLatitude(bounds.getNorthEast().getLatitude()) - getMercatorLatitude(bounds.getSouthWest().getLatitude()); + if ((deltaLng == 0) && (deltaLat == 0)) { + return MAX_ZOOM; + } + if (deltaLng < 0) { + deltaLng += 360; + } + int zoomLng = (int) Math.floor(Math.log(map.getDiv().getClientWidth() * 360 / deltaLng / GLOBE_PXSIZE) / LOG2); + int zoomLat = (int) Math.floor(Math.log(map.getDiv().getClientHeight() * 2 * Math.PI / deltaLat / GLOBE_PXSIZE) / LOG2); + return Math.min(Math.min(zoomLat, zoomLng), MAX_ZOOM); + } + + private void zoomMapToNewBounds(LatLngBounds newBounds) { + if (newBounds != null) { + LatLngBounds currentMapBounds; + if (map.getBounds() == null + || !BoundsUtil.contains((currentMapBounds = map.getBounds()), newBounds) + || graticuleAreaRatio(currentMapBounds, newBounds) > 10) { + // only change bounds if the new bounds don't fit into the current map zoom + Iterable oldZoomSettings = settings.getZoomSettings().getTypesToConsiderOnZoom(); + setAutoZoomInProgress(true); + autoZoomLatLngBounds = newBounds; + int newZoomLevel = getZoomLevel(autoZoomLatLngBounds); + if (newZoomLevel != map.getZoom()) { + // distinguish between zoom-in and zoom-out, because the sequence of panTo() and setZoom() + // appears different on the screen due to map-animations + // following sequences keep the selected boats allways visible: + // zoom-in : 1. panTo(), 2. setZoom() + // zoom-out: 1. setZoom(), 2. panTo() + autoZoomIn = newZoomLevel > map.getZoom(); + autoZoomOut = !autoZoomIn; + autoZoomLevel = newZoomLevel; + removeTransitions(); + if (autoZoomIn) { + map.panTo(autoZoomLatLngBounds.getCenter()); + } else { + map.setZoom(autoZoomLevel); + } + } else { + map.panTo(autoZoomLatLngBounds.getCenter()); + } + settings.getZoomSettings().setTypesToConsiderOnZoom(oldZoomSettings); + setAutoZoomInProgress(false); + } + } + } + + private double graticuleAreaRatio(LatLngBounds containing, LatLngBounds contained) { + assert BoundsUtil.contains(containing, contained); + double containingAreaRatio = getGraticuleArea(containing) / getGraticuleArea(contained); + return containingAreaRatio; + } + + /** + * A much simplified "area" calculation for a {@link Bounds} object, multiplying the differences in latitude and longitude degrees. + * The result therefore is in the order of magnitude of 60*60 square nautical miles. + */ + private double getGraticuleArea(LatLngBounds bounds) { + return ((BoundsUtil.isCrossesDateLine(bounds) ? bounds.getNorthEast().getLongitude()+360 : bounds.getNorthEast().getLongitude())-bounds.getSouthWest().getLongitude()) * + (bounds.getNorthEast().getLatitude() - bounds.getSouthWest().getLatitude()); + } + + private void setAutoZoomInProgress(boolean autoZoomInProgress) { + this.autoZoomInProgress = autoZoomInProgress; + } + + boolean isAutoZoomInProgress() { + return autoZoomInProgress; + } + + /** + * @param timeForPositionTransitionMillis use -1 to not animate the position transition, e.g., during map zoom or non-play + */ + private boolean updateBoatCanvasForCompetitor(CompetitorDTO competitorDTO, Date date, long timeForPositionTransitionMillis) { + boolean usedExistingCanvas = false; + GPSFixDTO lastBoatFix = getBoatFix(competitorDTO, date); + if (lastBoatFix != null) { + BoatOverlay boatOverlay = boatOverlays.get(competitorDTO); + if (boatOverlay == null) { + boatOverlay = createBoatOverlay(RaceMapOverlaysZIndexes.BOATS_ZINDEX, competitorDTO, displayHighlighted(competitorDTO)); + boatOverlays.put(competitorDTO, boatOverlay); + boatOverlay.setSelected(displayHighlighted(competitorDTO)); + boatOverlay.setBoatFix(lastBoatFix, timeForPositionTransitionMillis); + boatOverlay.addToMap(); + } else { + usedExistingCanvas = true; + boatOverlay.setSelected(displayHighlighted(competitorDTO)); + boatOverlay.setBoatFix(lastBoatFix, timeForPositionTransitionMillis); + boatOverlay.draw(); + } + } + + return usedExistingCanvas; + } + + private boolean displayHighlighted(CompetitorDTO competitorDTO) { + return !settings.isShowOnlySelectedCompetitors() && competitorSelection.isSelected(competitorDTO); + } + + protected CourseMarkOverlay createCourseMarkOverlay(int zIndex, final MarkDTO markDTO) { + final CourseMarkOverlay courseMarkOverlay = new CourseMarkOverlay(map, zIndex, markDTO, coordinateSystem); + courseMarkOverlay.addClickHandler(new ClickMapHandler() { + @Override + public void onEvent(ClickMapEvent event) { + LatLng latlng = courseMarkOverlay.getMarkLatLngPosition(); + showMarkInfoWindow(markDTO, latlng); + } + }); + return courseMarkOverlay; + } + + private CompetitorInfoOverlay createCompetitorInfoOverlay(int zIndex, final CompetitorDTO competitorDTO) { + String infoText = competitorDTO.getSailID() == null || competitorDTO.getSailID().isEmpty() ? competitorDTO.getName() : competitorDTO.getSailID(); + return new CompetitorInfoOverlay(map, zIndex, competitorSelection.getColor(competitorDTO, raceIdentifier), infoText, coordinateSystem); + } + + private BoatOverlay createBoatOverlay(int zIndex, final CompetitorDTO competitorDTO, boolean highlighted) { + final BoatOverlay boatCanvas = new BoatOverlay(map, zIndex, competitorDTO, competitorSelection.getColor(competitorDTO, raceIdentifier), coordinateSystem); + boatCanvas.setSelected(highlighted); + boatCanvas.addClickHandler(new ClickMapHandler() { + @Override + public void onEvent(ClickMapEvent event) { + if (lastInfoWindow != null) { + lastInfoWindow.close(); + } + GPSFixDTO latestFixForCompetitor = getBoatFix(competitorDTO, timer.getTime()); + LatLng where = coordinateSystem.toLatLng(latestFixForCompetitor.position); + InfoWindowOptions options = InfoWindowOptions.newInstance(); + InfoWindow infoWindow = InfoWindow.newInstance(options); + infoWindow.setContent(getInfoWindowContent(competitorDTO, latestFixForCompetitor)); + infoWindow.setPosition(where); + lastInfoWindow = infoWindow; + infoWindow.open(map); + } + }); + + boatCanvas.addMouseOverHandler(new MouseOverMapHandler() { + @Override + public void onEvent(MouseOverMapEvent event) { + map.setTitle(competitorDTO.getSailID()); + } + }); + boatCanvas.addMouseOutMoveHandler(new MouseOutMapHandler() { + @Override + public void onEvent(MouseOutMapEvent event) { + map.setTitle(""); + } + }); + + return boatCanvas; + } + + protected WindSensorOverlay createWindSensorOverlay(int zIndex, final WindSource windSource, final WindTrackInfoDTO windTrackInfoDTO) { + final WindSensorOverlay windSensorOverlay = new WindSensorOverlay(map, zIndex, raceMapImageManager, stringMessages, coordinateSystem); + windSensorOverlay.setWindInfo(windTrackInfoDTO, windSource); + windSensorOverlay.addClickHandler(new ClickMapHandler() { + @Override + public void onEvent(ClickMapEvent event) { + showWindSensorInfoWindow(windSensorOverlay); + } + }); + return windSensorOverlay; + } + + private void showMarkInfoWindow(MarkDTO markDTO, LatLng position) { + if(lastInfoWindow != null) { + lastInfoWindow.close(); + } + InfoWindowOptions options = InfoWindowOptions.newInstance(); + InfoWindow infoWindow = InfoWindow.newInstance(options); + infoWindow.setContent(getInfoWindowContent(markDTO)); + infoWindow.setPosition(position); + lastInfoWindow = infoWindow; + infoWindow.open(map); + } + + private void showCompetitorInfoWindow(final CompetitorDTO competitorDTO, LatLng where) { + if(lastInfoWindow != null) { + lastInfoWindow.close(); + } + GPSFixDTO latestFixForCompetitor = getBoatFix(competitorDTO, timer.getTime()); + // TODO find close fix where the mouse was; see BUG 470 + InfoWindowOptions options = InfoWindowOptions.newInstance(); + InfoWindow infoWindow = InfoWindow.newInstance(options); + infoWindow.setContent(getInfoWindowContent(competitorDTO, latestFixForCompetitor)); + infoWindow.setPosition(where); + lastInfoWindow = infoWindow; + infoWindow.open(map); + } + + private String formatPosition(double lat, double lng) { + NumberFormat numberFormat = NumberFormat.getFormat("0.00000"); + String result = numberFormat.format(lat) + " lat, " + numberFormat.format(lng) + " lng"; + return result; + } + + private void showWindSensorInfoWindow(final WindSensorOverlay windSensorOverlay) { + WindSource windSource = windSensorOverlay.getWindSource(); + WindTrackInfoDTO windTrackInfoDTO = windSensorOverlay.getWindTrackInfoDTO(); + WindDTO windDTO = windTrackInfoDTO.windFixes.get(0); + if(windDTO != null && windDTO.position != null) { + if(lastInfoWindow != null) { + lastInfoWindow.close(); + } + LatLng where = coordinateSystem.toLatLng(windDTO.position); + InfoWindowOptions options = InfoWindowOptions.newInstance(); + InfoWindow infoWindow = InfoWindow.newInstance(options); + infoWindow.setContent(getInfoWindowContent(windSource, windTrackInfoDTO)); + infoWindow.setPosition(where); + lastInfoWindow = infoWindow; + infoWindow.open(map); + } + } + + private Widget createInfoWindowLabelAndValue(String labelName, String value) { + FlowPanel flowPanel = new FlowPanel(); + Label label = new Label(labelName + ":"); + label.setWordWrap(false); + label.getElement().getStyle().setFloat(Style.Float.LEFT); + label.getElement().getStyle().setPadding(3, Style.Unit.PX); + label.getElement().getStyle().setFontWeight(Style.FontWeight.BOLD); + flowPanel.add(label); + + Label valueLabel = new Label(value); + valueLabel.setWordWrap(false); + valueLabel.getElement().getStyle().setFloat(Style.Float.LEFT); + valueLabel.getElement().getStyle().setPadding(3, Style.Unit.PX); + flowPanel.add(valueLabel); + + return flowPanel; + } + + private Widget getInfoWindowContent(MarkDTO markDTO) { + VerticalPanel vPanel = new VerticalPanel(); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.mark(), markDTO.getName())); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), formatPosition(markDTO.position.getLatDeg(), markDTO.position.getLngDeg()))); + return vPanel; + } + + private Widget getInfoWindowContent(WindSource windSource, WindTrackInfoDTO windTrackInfoDTO) { + WindDTO windDTO = windTrackInfoDTO.windFixes.get(0); + NumberFormat numberFormat = NumberFormat.getFormat("0.0"); + VerticalPanel vPanel = new VerticalPanel(); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.windSource(), WindSourceTypeFormatter.format(windSource, stringMessages))); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.wind(), Math.round(windDTO.dampenedTrueWindFromDeg) + " " + stringMessages.degreesShort())); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.windSpeed(), numberFormat.format(windDTO.dampenedTrueWindSpeedInKnots))); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.position(), formatPosition(windDTO.position.getLatDeg(), windDTO.position.getLngDeg()))); + return vPanel; + } + + private Widget getInfoWindowContent(CompetitorDTO competitorDTO, GPSFixDTO lastFix) { + final VerticalPanel vPanel = new VerticalPanel(); + vPanel.setWidth("350px"); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.competitor(), competitorDTO.getName())); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.sailNumber(), competitorDTO.getSailID())); + Integer rank = null; + if (quickRanks != null) { + QuickRankDTO quickRank = quickRanks.get(competitorDTO); + if (quickRank != null) { + rank = quickRank.rank; + } + } + if (rank != null) { + vPanel.add(createInfoWindowLabelAndValue(stringMessages.rank(), String.valueOf(rank))); + } + SpeedWithBearingDTO speedWithBearing = lastFix.speedWithBearing; + if (speedWithBearing == null) { + // TODO should we show the boat at all? + speedWithBearing = new SpeedWithBearingDTO(0, 0); + } + vPanel.add(createInfoWindowLabelAndValue(stringMessages.speed(), + NumberFormatterFactory.getDecimalFormat(1).format(speedWithBearing.speedInKnots) + " "+stringMessages.knotsUnit())); + vPanel.add(createInfoWindowLabelAndValue(stringMessages.bearing(), (int) speedWithBearing.bearingInDegrees + " "+stringMessages.degreesShort())); + if (lastFix.degreesBoatToTheWind != null) { + vPanel.add(createInfoWindowLabelAndValue(stringMessages.degreesBoatToTheWind(), + (int) Math.abs(lastFix.degreesBoatToTheWind) + " " + stringMessages.degreesShort())); + } + if (!selectedRaces.isEmpty()) { + RegattaAndRaceIdentifier race = selectedRaces.get(selectedRaces.size() - 1); + if (race != null) { + Map from = new HashMap(); + from.put(competitorDTO, fixesAndTails.getFixes(competitorDTO).get(fixesAndTails.getFirstShownFix(competitorDTO)).timepoint); + Map to = new HashMap(); + to.put(competitorDTO, getBoatFix(competitorDTO, timer.getTime()).timepoint); + sailingService.getDouglasPoints(race, from, to, 3, + new AsyncCallback>>() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Error obtaining douglas positions: " + caught.getMessage(), true /*silentMode */); + } + + @Override + public void onSuccess(Map> result) { + lastDouglasPeuckerResult = result; + if (douglasMarkers != null) { + removeAllMarkDouglasPeuckerpoints(); + } + if (!(timer.getPlayState() == PlayStates.Playing)) { + if (settings.isShowDouglasPeuckerPoints()) { + showMarkDouglasPeuckerPoints(result); + } + } + } + }); + sailingService.getManeuvers(race, from, to, + new AsyncCallback>>() { + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError("Error obtaining maneuvers: " + caught.getMessage(), true /*silentMode */); + } + + @Override + public void onSuccess(Map> result) { + lastManeuverResult = result; + if (maneuverMarkers != null) { + removeAllManeuverMarkers(); + } + if (!(timer.getPlayState() == PlayStates.Playing)) { + showManeuvers(result); + } + } + }); + + } + } + return vPanel; + } + + private Iterable getCompetitorsToShow() { + Iterable result; + Iterable selection = competitorSelection.getSelectedCompetitors(); + if (!settings.isShowOnlySelectedCompetitors() || Util.isEmpty(selection)) { + result = competitorSelection.getFilteredCompetitors(); + } else { + result = selection; + } + return result; + } + + protected void removeAllMarkDouglasPeuckerpoints() { + if (douglasMarkers != null) { + for (Marker marker : douglasMarkers) { + marker.setMap((MapWidget) null); + } + } + douglasMarkers = null; + } + + protected void removeAllManeuverMarkers() { + if (maneuverMarkers != null) { + for (Marker marker : maneuverMarkers) { + marker.setMap((MapWidget) null); + } + maneuverMarkers = null; + } + } + + private void showMarkDouglasPeuckerPoints(Map> gpsFixPointMapForCompetitors) { + douglasMarkers = new HashSet(); + if (map != null && gpsFixPointMapForCompetitors != null) { + Set keySet = gpsFixPointMapForCompetitors.keySet(); + Iterator iter = keySet.iterator(); + while (iter.hasNext()) { + CompetitorDTO competitorDTO = iter.next(); + List gpsFix = gpsFixPointMapForCompetitors.get(competitorDTO); + for (GPSFixDTO fix : gpsFix) { + LatLng latLng = coordinateSystem.toLatLng(fix.position); + MarkerOptions options = MarkerOptions.newInstance(); + options.setTitle(fix.timepoint+": "+fix.position+", "+fix.speedWithBearing.toString()); + Marker marker = Marker.newInstance(options); + marker.setPosition(latLng); + douglasMarkers.add(marker); + marker.setMap(map); + } + } + } + } + + private void showManeuvers(Map> maneuvers) { + maneuverMarkers = new HashSet(); + if (map != null && maneuvers != null) { + Set keySet = maneuvers.keySet(); + Iterator iter = keySet.iterator(); + while (iter.hasNext()) { + CompetitorDTO competitorDTO = iter.next(); + List maneuversForCompetitor = maneuvers.get(competitorDTO); + for (ManeuverDTO maneuver : maneuversForCompetitor) { + if (settings.isShowManeuverType(maneuver.type)) { + LatLng latLng = coordinateSystem.toLatLng(maneuver.position); + Marker maneuverMarker = raceMapImageManager.maneuverIconsForTypeAndTargetTack.get(new com.sap.sse.common.Util.Pair(maneuver.type, maneuver.newTack)); + MarkerOptions options = MarkerOptions.newInstance(); + options.setTitle(maneuver.toString(stringMessages)); + options.setIcon(maneuverMarker.getIcon_MarkerImage()); + Marker marker = Marker.newInstance(options); + marker.setPosition(latLng); + maneuverMarkers.add(marker); + marker.setMap(map); + } + } + } + } + } + + /** + * @param date + * the point in time for which to determine the competitor's boat position; approximated by using the fix + * from {@link #fixes} whose time point comes closest to this date + * + * @return The GPS fix for the given competitor from {@link #fixes} that is closest to date, or + * null if no fix is available + */ + private GPSFixDTO getBoatFix(CompetitorDTO competitorDTO, Date date) { + GPSFixDTO result = null; + List competitorFixes = fixesAndTails.getFixes(competitorDTO); + if (competitorFixes != null && !competitorFixes.isEmpty()) { + int i = Collections.binarySearch(competitorFixes, new GPSFixDTO(date, null, null, (WindDTO) null, null, null, false), + new Comparator() { + @Override + public int compare(GPSFixDTO o1, GPSFixDTO o2) { + return o1.timepoint.compareTo(o2.timepoint); + } + }); + if (i<0) { + i = -i-1; // no perfect match; i is now the insertion point + // if the insertion point is at the end, use last fix + if (i >= competitorFixes.size()) { + result = competitorFixes.get(competitorFixes.size()-1); + } else if (i == 0) { + // if the insertion point is at the beginning, use first fix + result = competitorFixes.get(0); + } else { + // competitorFixes must have at least two elements, and i points neither to the end nor the beginning; + // get the fix from i and i+1 whose timepoint is closer to date + final GPSFixDTO fixBefore = competitorFixes.get(i-1); + final GPSFixDTO fixAfter = competitorFixes.get(i); + final GPSFixDTO closer; + if (date.getTime() - fixBefore.timepoint.getTime() < fixAfter.timepoint.getTime() - date.getTime()) { + closer = fixBefore; + } else { + closer = fixAfter; + } + // 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; + DegreePosition betweenPosition = new DegreePosition(factorForBefore*fixBefore.position.getLatDeg() + factorForAfter*fixAfter.position.getLatDeg(), + factorForBefore*fixBefore.position.getLngDeg() + factorForAfter*fixAfter.position.getLngDeg()); + final double betweenBearing; + if (fixBefore.speedWithBearing == null) { + if (fixAfter.speedWithBearing == null) { + betweenBearing = 0; + } else { + betweenBearing = fixAfter.speedWithBearing.bearingInDegrees; + } + } else if (fixAfter.speedWithBearing == null) { + betweenBearing = fixBefore.speedWithBearing.bearingInDegrees; + } else { + 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( + factorForBefore*(fixBefore.speedWithBearing==null?0:fixBefore.speedWithBearing.speedInKnots) + + factorForAfter*(fixAfter.speedWithBearing==null?0:fixAfter.speedWithBearing.speedInKnots), + betweenBearing); + result = new GPSFixDTO(date, betweenPosition, betweenSpeed, closer.degreesBoatToTheWind, + closer.tack, closer.legType, fixBefore.extrapolated || fixAfter.extrapolated); + } + } else { + // perfect match + final GPSFixDTO fixAfter = competitorFixes.get(i); + result = fixAfter; + } + } + return result; + } + + public RaceMapSettings getSettings() { + return settings; + } + + @Override + public void addedToSelection(CompetitorDTO competitor) { + if (settings.isShowOnlySelectedCompetitors()) { + if (Util.size(competitorSelection.getSelectedCompetitors()) == 1) { + // first competitors selected; remove all others from map + Iterator> i = boatOverlays.entrySet().iterator(); + while (i.hasNext()) { + Entry next = i.next(); + if (!next.getKey().equals(competitor)) { + BoatOverlay boatOverlay = next.getValue(); + boatOverlay.removeFromMap(); + fixesAndTails.removeTail(next.getKey()); + i.remove(); // only this way a ConcurrentModificationException while looping can be avoided + } + } + showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); + } else { + // adding a single competitor; may need to re-load data, so refresh: + timeChanged(timer.getTime(), null); + } + } else { + // only change highlighting + BoatOverlay boatCanvas = boatOverlays.get(competitor); + if (boatCanvas != null) { + boatCanvas.setSelected(displayHighlighted(competitor)); + boatCanvas.draw(); + showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); + } else { + // seems like an internal error not to find the lowlighted marker; but maybe the + // competitor was added late to the race; + // data for newly selected competitor supposedly missing; refresh + timeChanged(timer.getTime(), null); + } + } + // Now update tails for all competitors because selection change may also affect all unselected competitors + for (CompetitorDTO oneOfAllCompetitors : competitorSelection.getAllCompetitors()) { + Polyline tail = fixesAndTails.getTail(oneOfAllCompetitors); + if (tail != null) { + PolylineOptions newOptions = createTailStyle(oneOfAllCompetitors, displayHighlighted(oneOfAllCompetitors)); + tail.setOptions(newOptions); + } + } + // Trigger auto-zoom if needed + RaceMapZoomSettings zoomSettings = settings.getZoomSettings(); + if (!zoomSettings.containsZoomType(ZoomTypes.NONE) && zoomSettings.isZoomToSelectedCompetitors()) { + zoomMapToNewBounds(zoomSettings.getNewBounds(this)); + } + } + + @Override + public void removedFromSelection(CompetitorDTO competitor) { + if (isShowAnyHelperLines()) { + // helper lines depend on which competitor is visible, because the *visible* leader is used for + // deciding which helper lines to show: + timeChanged(timer.getTime(), null); + } else { + // try a more incremental update otherwise + if (settings.isShowOnlySelectedCompetitors()) { + // if selection is now empty, show all competitors + if (Util.isEmpty(competitorSelection.getSelectedCompetitors())) { + timeChanged(timer.getTime(), null); + } else { + // otherwise remove only deselected competitor's boat images and tail + BoatOverlay removedBoatOverlay = boatOverlays.remove(competitor); + if (removedBoatOverlay != null) { + removedBoatOverlay.removeFromMap(); + } + fixesAndTails.removeTail(competitor); + showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); + } + } else { + // "lowlight" currently selected competitor + BoatOverlay boatCanvas = boatOverlays.get(competitor); + if (boatCanvas != null) { + boatCanvas.setSelected(displayHighlighted(competitor)); + boatCanvas.draw(); + } + showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); + } + } + //Trigger auto-zoom if needed + RaceMapZoomSettings zoomSettings = settings.getZoomSettings(); + if (!zoomSettings.containsZoomType(ZoomTypes.NONE) && zoomSettings.isZoomToSelectedCompetitors()) { + zoomMapToNewBounds(zoomSettings.getNewBounds(this)); + } + } + + private boolean isShowAnyHelperLines() { + return settings.getHelpLinesSettings().isShowAnyHelperLines(); + } + + @Override + public String getLocalizedShortName() { + return stringMessages.map(); + } + + @Override + public Widget getEntryWidget() { + return this; + } + + @Override + public boolean hasSettings() { + return true; + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return new RaceMapSettingsDialogComponent(settings, stringMessages, this.showViewSimulation && this.hasPolar); + } + + @Override + public void updateSettings(RaceMapSettings newSettings) { + boolean maneuverTypeSelectionChanged = false; + boolean requiredRedraw = false; + for (ManeuverType maneuverType : ManeuverType.values()) { + if (newSettings.isShowManeuverType(maneuverType) != settings.isShowManeuverType(maneuverType)) { + maneuverTypeSelectionChanged = true; + settings.showManeuverType(maneuverType, newSettings.isShowManeuverType(maneuverType)); + } + } + if (maneuverTypeSelectionChanged) { + if (!(timer.getPlayState() == PlayStates.Playing) && lastManeuverResult != null) { + removeAllManeuverMarkers(); + showManeuvers(lastManeuverResult); + } + } + if (newSettings.isShowDouglasPeuckerPoints() != settings.isShowDouglasPeuckerPoints()) { + if (!(timer.getPlayState() == PlayStates.Playing) && lastDouglasPeuckerResult != null && newSettings.isShowDouglasPeuckerPoints()) { + settings.setShowDouglasPeuckerPoints(true); + removeAllMarkDouglasPeuckerpoints(); + showMarkDouglasPeuckerPoints(lastDouglasPeuckerResult); + } else if (!newSettings.isShowDouglasPeuckerPoints()) { + settings.setShowDouglasPeuckerPoints(false); + removeAllMarkDouglasPeuckerpoints(); + } + } + if (newSettings.getTailLengthInMilliseconds() != settings.getTailLengthInMilliseconds()) { + settings.setTailLengthInMilliseconds(newSettings.getTailLengthInMilliseconds()); + requiredRedraw = true; + } + if (newSettings.getBuoyZoneRadiusInMeters() != settings.getBuoyZoneRadiusInMeters()) { + settings.setBuoyZoneRadiusInMeters(newSettings.getBuoyZoneRadiusInMeters()); + requiredRedraw = true; + } + if (newSettings.isShowOnlySelectedCompetitors() != settings.isShowOnlySelectedCompetitors()) { + settings.setShowOnlySelectedCompetitors(newSettings.isShowOnlySelectedCompetitors()); + requiredRedraw = true; + } + if (newSettings.isShowSelectedCompetitorsInfo() != settings.isShowSelectedCompetitorsInfo()) { + settings.setShowSelectedCompetitorsInfo(newSettings.isShowSelectedCompetitorsInfo()); + requiredRedraw = true; + } + if (!newSettings.getZoomSettings().equals(settings.getZoomSettings())) { + settings.setZoomSettings(newSettings.getZoomSettings()); + if (!settings.getZoomSettings().containsZoomType(ZoomTypes.NONE)) { + removeTransitions(); + zoomMapToNewBounds(settings.getZoomSettings().getNewBounds(this)); + } + } + if (!newSettings.getHelpLinesSettings().equals(settings.getHelpLinesSettings())) { + settings.setHelpLinesSettings(newSettings.getHelpLinesSettings()); + requiredRedraw = true; + } + if (newSettings.isShowWindStreamletOverlay() != settings.isShowWindStreamletOverlay()) { + settings.setShowWindStreamletOverlay(newSettings.isShowWindStreamletOverlay()); + streamletOverlay.setVisible(newSettings.isShowWindStreamletOverlay()); + } + if (newSettings.isShowWindStreamletColors() != settings.isShowWindStreamletColors()) { + settings.setShowWindStreamletColors(newSettings.isShowWindStreamletColors()); + streamletOverlay.setColors(newSettings.isShowWindStreamletColors()); + } + if (newSettings.isShowSimulationOverlay() != settings.isShowSimulationOverlay()) { + settings.setShowSimulationOverlay(newSettings.isShowSimulationOverlay()); + simulationOverlay.setVisible(newSettings.isShowSimulationOverlay()); + if (newSettings.isShowSimulationOverlay()) { + simulationOverlay.updateLeg(Math.max(lastLegNumber,1), true, -1 /* ensure ui-update */); + } + } + if (newSettings.isWindUp() != settings.isWindUp()) { + settings.setWindUp(newSettings.isWindUp()); + updateCoordinateSystemFromSettings(); + requiredRedraw = true; + } + if (newSettings.getTransparentHoverlines() != settings.getTransparentHoverlines()) { + settings.setTransparentHoverlines(newSettings.getTransparentHoverlines()); + } + if (newSettings.getHoverlineStrokeWeight() != settings.getHoverlineStrokeWeight()) { + settings.setHoverlineStrokeWeight(newSettings.getHoverlineStrokeWeight()); + } + if (requiredRedraw) { + redraw(); + } + } + + public static class BoatsBoundsCalculator extends LatLngBoundsCalculatorForSelected { + + @Override + public LatLngBounds calculateNewBounds(RaceMap forMap) { + LatLngBounds newBounds = null; + Iterable selectedCompetitors = forMap.competitorSelection.getSelectedCompetitors(); + Iterable competitors = new ArrayList(); + if (selectedCompetitors == null || !selectedCompetitors.iterator().hasNext()) { + competitors = forMap.getCompetitorsToShow(); + } else { + competitors = isZoomOnlyToSelectedCompetitors(forMap) ? selectedCompetitors : forMap.getCompetitorsToShow(); + } + for (CompetitorDTO competitor : competitors) { + try { + GPSFixDTO competitorFix = forMap.getBoatFix(competitor, forMap.timer.getTime()); + Position competitorPosition = competitorFix != null ? competitorFix.position : null; + if (competitorPosition != null) { + if (newBounds == null) { + newBounds = BoundsUtil.getAsBounds(forMap.coordinateSystem.toLatLng(competitorPosition)); + } else { + newBounds = newBounds.extend(forMap.coordinateSystem.toLatLng(competitorPosition)); + } + } + } catch (IndexOutOfBoundsException e) { + // TODO can't this be predicted and the exception be avoided in the first place? + // Catch this in case the competitor has no GPS fixes at the current time (e.g. in race 'Finale 2' of STG) + } + } + return newBounds; + } + + } + + public static class TailsBoundsCalculator extends LatLngBoundsCalculatorForSelected { + @Override + public LatLngBounds calculateNewBounds(RaceMap racemap) { + LatLngBounds newBounds = null; + Iterable competitors = isZoomOnlyToSelectedCompetitors(racemap) ? racemap.competitorSelection.getSelectedCompetitors() : racemap.getCompetitorsToShow(); + for (CompetitorDTO competitor : competitors) { + Polyline tail = racemap.fixesAndTails.getTail(competitor); + LatLngBounds bounds = null; + // TODO: Find a replacement for missing Polyline function getBounds() from v2 + // see also http://stackoverflow.com/questions/3284808/getting-the-bounds-of-a-polyine-in-google-maps-api-v3; + // optionally, consider providing a bounds cache with two sorted sets that organize the LatLng objects for O(1) bounds calculation and logarithmic add, ideally O(1) remove + if (tail != null && tail.getPath().getLength() >= 1) { + bounds = BoundsUtil.getAsBounds(tail.getPath().get(0)); + for (int i = 1; i < tail.getPath().getLength(); i++) { + bounds = bounds.extend(tail.getPath().get(i)); + } + } + if (bounds != null) { + if (newBounds == null) { + newBounds = bounds; + } else { + newBounds = newBounds.union(bounds); + } + } + } + return newBounds; + } + } + + public static class CourseMarksBoundsCalculator implements LatLngBoundsCalculator { + @Override + public LatLngBounds calculateNewBounds(RaceMap forMap) { + LatLngBounds newBounds = null; + Iterable marksToZoom = forMap.markDTOs.values(); + if (marksToZoom != null) { + for (MarkDTO markDTO : marksToZoom) { + if (newBounds == null) { + newBounds = BoundsUtil.getAsBounds(forMap.coordinateSystem.toLatLng(markDTO.position)); + } else { + newBounds = newBounds.extend(forMap.coordinateSystem.toLatLng(markDTO.position)); + } + } + } + return newBounds; + } + } + + public static class WindSensorsBoundsCalculator implements LatLngBoundsCalculator { + @Override + public LatLngBounds calculateNewBounds(RaceMap forMap) { + LatLngBounds newBounds = null; + Collection marksToZoom = forMap.windSensorOverlays.values(); + if (marksToZoom != null) { + for (WindSensorOverlay windSensorOverlay : marksToZoom) { + if (BoundsUtil.getAsPosition(windSensorOverlay.getLatLngPosition()) != null) { + LatLngBounds bounds = BoundsUtil.getAsBounds(windSensorOverlay.getLatLngPosition()); + if (newBounds == null) { + newBounds = bounds; + } else { + newBounds = newBounds.extend(windSensorOverlay.getLatLngPosition()); + } + } + } + } + return newBounds; + } + } + + @Override + public void initializeData(boolean showMapControls, boolean showHeaderPanel) { + loadMapsAPIV3(showMapControls, showHeaderPanel); + } + + @Override + public boolean isDataInitialized() { + return isMapInitialized; + } + + @Override + public void onResize() { + if (map != null) { + map.triggerResize(); + zoomMapToNewBounds(settings.getZoomSettings().getNewBounds(RaceMap.this)); + } + // Adjust RaceMap headers to avoid overlapping based on the RaceMap width + boolean isCompactHeader = this.getOffsetWidth() <= 600; + getLeftHeaderPanel().setStyleName(COMPACT_HEADER_STYLE, isCompactHeader); + getRightHeaderPanel().setStyleName(COMPACT_HEADER_STYLE, isCompactHeader); + + // Adjust combined wind and true north indicator panel indent, based on the RaceMap height + if (combinedWindPanel.getParent() != null && trueNorthIndicatorPanel.getParent() != null) { + this.adjustLeftControlsIndent(); + } + } + + private void adjustLeftControlsIndent() { + combinedWindPanel.getParent().setStyleName("CombinedWindPanelParentDiv"); + trueNorthIndicatorPanel.getParent().setStyleName("TrueNorthIndicatorPanelParentDiv"); + String leftControlsIndentStyle = getLeftControlsIndentStyle(); + if (leftControlsIndentStyle != null) { + combinedWindPanel.getParent().addStyleName(leftControlsIndentStyle); + trueNorthIndicatorPanel.getParent().addStyleName(leftControlsIndentStyle); + } + } + + @Override + public void competitorsListChanged(Iterable competitors) { + timeChanged(timer.getTime(), null); + } + + @Override + public void filteredCompetitorsListChanged(Iterable filteredCompetitors) { + timeChanged(timer.getTime(), null); + } + + @Override + public void filterChanged(FilterSet> oldFilterSet, + FilterSet> newFilterSet) { + // nothing to do; if the list of filtered competitors has changed, a separate call to filteredCompetitorsListChanged will occur + } + + @Override + public PolylineOptions createTailStyle(CompetitorDTO competitor, boolean isHighlighted) { + PolylineOptions options = PolylineOptions.newInstance(); + options.setClickable(true); + options.setGeodesic(true); + options.setStrokeOpacity(1.0); + boolean noCompetitorSelected = Util.isEmpty(competitorSelection.getSelectedCompetitors()); + if (isHighlighted || noCompetitorSelected) { + options.setStrokeColor(competitorSelection.getColor(competitor, raceIdentifier).getAsHtml()); + } else { + options.setStrokeColor(CssColor.make(200, 200, 200).toString()); + } + if (isHighlighted) { + options.setStrokeWeight(2); + } else { + options.setStrokeWeight(1); + } + options.setZindex(RaceMapOverlaysZIndexes.BOATTAILS_ZINDEX); + return options; + } + + @Override + public Polyline createTail(final CompetitorDTO competitor, List points) { + PolylineOptions options = createTailStyle(competitor, displayHighlighted(competitor)); + Polyline result = Polyline.newInstance(options); + MVCArray pointsAsArray = MVCArray.newInstance(points.toArray(new LatLng[0])); + result.setPath(pointsAsArray); + result.setMap(map); + Hoverline resultHoverline = new Hoverline(result, options, this); + final ClickMapHandler clickHandler = new ClickMapHandler() { + @Override + public void onEvent(ClickMapEvent event) { + showCompetitorInfoWindow(competitor, event.getMouseEvent().getLatLng()); + } + }; + result.addClickHandler(clickHandler); + resultHoverline.addClickHandler(clickHandler); + result.addMouseOverHandler(new MouseOverMapHandler() { + @Override + public void onEvent(MouseOverMapEvent event) { + map.setTitle(competitor.getSailID() + ", " + competitor.getName()); + } + }); + resultHoverline.addMouseOutMoveHandler(new MouseOutMapHandler() { + @Override + public void onEvent(MouseOutMapEvent event) { + map.setTitle(""); + } + }); + return result; + } + + @Override + public Integer getRank(CompetitorDTO competitor) { + final Integer result; + QuickRankDTO quickRank = quickRanks.get(competitor); + if (quickRank != null) { + result = quickRank.rank; + } else { + result = null; + } + return result; + } + + private Image createSAPLogo() { + ImageResource sapLogoResource = resources.sapLogoOverlay(); + Image sapLogo = new Image(sapLogoResource); + sapLogo.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + Window.open("http://www.sap.com", "_blank", null); + } + }); + sapLogo.setStyleName("raceBoard-Logo"); + return sapLogo; + } + + @Override + public String getDependentCssClassName() { + return "raceMap"; + } + + /** + * The default zoom bounds are defined by the boats + */ + private LatLngBounds getDefaultZoomBounds() { + return new BoatsBoundsCalculator().calculateNewBounds(RaceMap.this); + } + + private MapOptions getMapOptions(final boolean showMapControls, boolean windUp) { + MapOptions mapOptions = MapOptions.newInstance(); + mapOptions.setScrollWheel(true); + mapOptions.setMapTypeControl(showMapControls && !windUp); + mapOptions.setPanControl(showMapControls); + mapOptions.setZoomControl(showMapControls); + mapOptions.setScaleControl(true); + if (windUp) { + mapOptions.setMinZoom(8); + } else { + mapOptions.setMinZoom(0); + } + MapTypeStyle[] mapTypeStyles = new MapTypeStyle[4]; + + // hide all transit lines including ferry lines + mapTypeStyles[0] = GoogleMapStyleHelper.createHiddenStyle(MapTypeStyleFeatureType.TRANSIT); + // hide points of interest + mapTypeStyles[1] = GoogleMapStyleHelper.createHiddenStyle(MapTypeStyleFeatureType.POI); + // simplify road display + mapTypeStyles[2] = GoogleMapStyleHelper.createSimplifiedStyle(MapTypeStyleFeatureType.ROAD); + // set water color + // To play with the styles, check out http://gmaps-samples-v3.googlecode.com/svn/trunk/styledmaps/wizard/index.html. + // To convert an RGB color into the strange hue/saturation/lightness model used by the Google Map use + // http://software.stadtwerk.org/google_maps_colorizr/#water/all/123456/. + mapTypeStyles[3] = GoogleMapStyleHelper.createColorStyle(MapTypeStyleFeatureType.WATER, new RGBColor(0, 136, 255), 0, -70); + + MapTypeControlOptions mapTypeControlOptions = MapTypeControlOptions.newInstance(); + mapTypeControlOptions.setPosition(ControlPosition.BOTTOM_RIGHT); + mapOptions.setMapTypeControlOptions(mapTypeControlOptions); + + mapOptions.setMapTypeStyles(mapTypeStyles); + // no need to try to position the scale control; it always ends up at the right bottom corner + mapOptions.setStreetViewControl(false); + if (showMapControls) { + ZoomControlOptions zoomControlOptions = ZoomControlOptions.newInstance(); + zoomControlOptions.setPosition(ControlPosition.RIGHT_TOP); + mapOptions.setZoomControlOptions(zoomControlOptions); + PanControlOptions panControlOptions = PanControlOptions.newInstance(); + panControlOptions.setPosition(ControlPosition.RIGHT_TOP); + mapOptions.setPanControlOptions(panControlOptions); + } + return mapOptions; + } + + /** + * @return CSS style name to adjust the indent of left controls (combined wind and true north indicator). + */ + protected String getLeftControlsIndentStyle() { + return null; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapSettings.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapSettings.java index cece7272781..ef42bc32951 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapSettings.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapSettings.java @@ -19,6 +19,10 @@ public class RaceMapSettings extends AbstractSettings { private RaceMapZoomSettings zoomSettings; private RaceMapHelpLinesSettings helpLinesSettings; + + private boolean transparentHoverlines = true; + + private int hoverlineStrokeWeight = 10; private long tailLengthInMilliseconds = 100000l; @@ -55,6 +59,8 @@ public class RaceMapSettings extends AbstractSettings { public RaceMapSettings(RaceMapSettings settings) { this.buoyZoneRadiusInMeters = settings.buoyZoneRadiusInMeters; this.helpLinesSettings = new RaceMapHelpLinesSettings(settings.getHelpLinesSettings().getVisibleHelpLineTypes()); + this.transparentHoverlines = settings.transparentHoverlines; + this.hoverlineStrokeWeight = settings.hoverlineStrokeWeight; this.maneuverTypesToShow = settings.maneuverTypesToShow; this.showDouglasPeuckerPoints = settings.showDouglasPeuckerPoints; this.showOnlySelectedCompetitors = settings.showOnlySelectedCompetitors; @@ -153,6 +159,22 @@ public class RaceMapSettings extends AbstractSettings { public void setHelpLinesSettings(RaceMapHelpLinesSettings helpLinesSettings) { this.helpLinesSettings = helpLinesSettings; } + + public boolean getTransparentHoverlines() { + return this.transparentHoverlines; + } + + public void setTransparentHoverlines(boolean transparentHoverlines) { + this.transparentHoverlines = transparentHoverlines; + } + + public int getHoverlineStrokeWeight() { + return this.hoverlineStrokeWeight; + } + + public void setHoverlineStrokeWeight(int hoverlineStrokeWeight) { + this.hoverlineStrokeWeight = hoverlineStrokeWeight; + } public boolean isShowSelectedCompetitorsInfo() { return showSelectedCompetitorsInfo; 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 2297380652f..2abe153b1f8 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 @@ -24,11 +24,14 @@ import com.sap.sailing.gwt.ui.client.StringMessages; 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.sse.common.Util; +import com.sap.sse.gwt.client.controls.IntegerBox; import com.sap.sse.gwt.client.dialog.DataEntryDialog; import com.sap.sse.gwt.client.dialog.DataEntryDialog.Validator; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; public class RaceMapSettingsDialogComponent implements SettingsDialogComponent { + private static final int MAX_BUOY_ZONE_RADIUS_IN_METERS = 100; + private static final int MAX_STROKE_WEIGHT = 33; //Initializing the lists to prevent a null pointer exception in the first validation call private List> checkboxAndManeuverType = new ArrayList>(); private List> checkboxAndZoomType = new ArrayList>(); @@ -43,6 +46,8 @@ public class RaceMapSettingsDialogComponent implements SettingsDialogComponent 100.0)) { - errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.buoyZone(), "0", "100"); + && (valueToValidate.getBuoyZoneRadiusInMeters() < 0 || valueToValidate.getBuoyZoneRadiusInMeters() > MAX_BUOY_ZONE_RADIUS_IN_METERS)) { + errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.buoyZone(), 0, 100); + } else if (valueToValidate.getHoverlineStrokeWeight() < 0 || valueToValidate.getHoverlineStrokeWeight() > MAX_STROKE_WEIGHT) { + errorMessage = stringMessages.valueMustBeBetweenMinMax(stringMessages.bufferLineStrokeWeight(), 0, MAX_STROKE_WEIGHT); } return errorMessage; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/CompetitorFilter.css b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/CompetitorFilter.css index 3d626453747..5f27df5b24a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/CompetitorFilter.css +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/CompetitorFilter.css @@ -1,5 +1,6 @@ .competitorFilterContainer { - width: 400px; + max-width: 400px; + width: 100%; height: 2.14em; background-color: #FFFFFF; margin-left: 0px; @@ -22,7 +23,7 @@ .searchInput { font-size: .933333333333333em; height: 2.142857142857143em; - width: 280px; + width: 70%; border: none; background-color: #eaeaea; padding: 0; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ExpandableSortableColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ExpandableSortableColumn.java index 15fad2b3fc8..22a487be9bf 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ExpandableSortableColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ExpandableSortableColumn.java @@ -149,15 +149,15 @@ public abstract class ExpandableSortableColumn extends LeaderboardSortableCol */ public void toggleExpansion() { if (isExpansionEnabled()) { - final boolean oldBusyState = getLeaderboardPanel().getBusyIndicator().isBusy(); - getLeaderboardPanel().getBusyIndicator().setBusy(true); + final boolean oldBusyState = getLeaderboardPanel().isBusy(); + getLeaderboardPanel().setBusyState(true); setTogglingInProcess(true); final CellTable table = getLeaderboardPanel().getLeaderboardTable(); if (isExpanded()) { for (AbstractSortableColumnWithMinMax column : getAllVisibleChildren()) { getLeaderboardPanel().removeColumn(column); // removes only the children currently displayed } - getLeaderboardPanel().getBusyIndicator().setBusy(false); + getLeaderboardPanel().setBusyState(false); setTogglingInProcess(false); // important: toggle expanded state after asking for all visible children setExpanded(!isExpanded()); @@ -177,7 +177,7 @@ public abstract class ExpandableSortableColumn extends LeaderboardSortableCol } getLeaderboardPanel().getLeaderboardTable().redraw(); } - getLeaderboardPanel().getBusyIndicator().setBusy(oldBusyState); + getLeaderboardPanel().setBusyState(oldBusyState); setTogglingInProcess(false); } }); 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 1f54ca0315f..79f24c91d28 100755 --- 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 @@ -258,7 +258,7 @@ public class LeaderboardEntryPoint extends AbstractSailingEntryPoint { result = LeaderboardSettingsFactory.getInstance().createNewDefaultSettings(null, null, /* overallDetails */ overallDetails, null, /* autoExpandFirstRace */false, refreshIntervalMillis, numberOfLastRacesToShow, - raceColumnSelectionStrategy); + raceColumnSelectionStrategy, /* showCompetitorSailIdColumns */ true, /*showCompetitorFullNameColumn*/ true); } return result; } 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 11c7854898e..8d8cdaf1519 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 @@ -1,3280 +1,3292 @@ -package com.sap.sailing.gwt.ui.leaderboard; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import com.google.gwt.cell.client.AbstractSafeHtmlCell; -import com.google.gwt.cell.client.Cell; -import com.google.gwt.cell.client.Cell.Context; -import com.google.gwt.cell.client.CompositeCell; -import com.google.gwt.cell.client.TextCell; -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.dom.client.BrowserEvents; -import com.google.gwt.dom.client.Element; -import com.google.gwt.dom.client.Style.FontWeight; -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.shared.HandlerRegistration; -import com.google.gwt.i18n.client.NumberFormat; -import com.google.gwt.resources.client.ImageResource; -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.text.shared.AbstractSafeHtmlRenderer; -import com.google.gwt.user.cellview.client.Column; -import com.google.gwt.user.cellview.client.Header; -import com.google.gwt.user.cellview.client.SafeHtmlHeader; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.AbstractImagePrototype; -import com.google.gwt.user.client.ui.Anchor; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.DockPanel; -import com.google.gwt.user.client.ui.FlowPanel; -import com.google.gwt.user.client.ui.HasHorizontalAlignment; -import com.google.gwt.user.client.ui.HasVerticalAlignment; -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.user.client.ui.Widget; -import com.google.gwt.view.client.CellPreviewEvent; -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.DetailType; -import com.sap.sailing.domain.common.InvertibleComparator; -import com.sap.sailing.domain.common.MaxPointsReason; -import com.sap.sailing.domain.common.Mile; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.SortingOrder; -import com.sap.sailing.domain.common.Tack; -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.IncrementalOrFullLeaderboardDTO; -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.impl.InvertibleComparatorAdapter; -import com.sap.sailing.gwt.ui.actions.GetLeaderboardByNameAction; -import com.sap.sailing.gwt.ui.client.Collator; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; -import com.sap.sailing.gwt.ui.client.FlagImageResolver; -import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; -import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; -import com.sap.sailing.gwt.ui.client.RaceTimesInfoProviderListener; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.shared.controls.AbstractSortableColumnWithMinMax; -import com.sap.sailing.gwt.ui.client.shared.controls.SelectionCheckboxColumn; -import com.sap.sailing.gwt.ui.client.shared.filter.CompetitorRaceRankFilter; -import com.sap.sailing.gwt.ui.client.shared.filter.FilterWithUI; -import com.sap.sailing.gwt.ui.client.shared.filter.LeaderboardFetcher; -import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; -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.common.Util; -import com.sap.sse.common.filter.BinaryOperator; -import com.sap.sse.common.filter.Filter; -import com.sap.sse.common.filter.FilterSet; -import com.sap.sse.gwt.client.ErrorReporter; -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; -import com.sap.sse.gwt.client.player.TimeListener; -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.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.ComponentResources; -import com.sap.sse.gwt.client.shared.components.IsEmbeddableComponent; -import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; -import com.sap.sse.gwt.client.useragent.UserAgentDetails; - -/** - * A leaderboard essentially consists of a table widget that in its columns displays the entries. - * - * @author Axel Uhl (D043530) - * - */ -public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayStateListener, DisplayedLeaderboardRowsProvider, - Component, IsEmbeddableComponent, CompetitorSelectionChangeListener, LeaderboardFetcher { - public static final String LOAD_LEADERBOARD_DATA_CATEGORY = "loadLeaderboardData"; - - protected static final NumberFormat scoreFormat = NumberFormat.getFormat("0.##"); - - private final SailingServiceAsync sailingService; - - private static String IS_LIVE_TEXT_COLOR = "#FF0000"; - private static String DEFAULT_TEXT_COLOR = "#000000"; - - private static final String STYLE_LEADERBOARD_CONTENT = "leaderboardContent"; - private static final String STYLE_LEADERBOARD_INFO = "leaderboardInfo"; - private static final String STYLE_LEADERBOARD_TOOLBAR = "leaderboardContent-toolbar"; - private static final String STYLE_LEADERBOARD_LIVE_RACE = "leaderboardContent-liverace"; - - interface RaceColumnTemplates extends SafeHtmlTemplates { - @SafeHtmlTemplates.Template("

") - SafeHtml cellFrameWithTextColorAndFleetBorder(String textColor, String borderStyle); - - @SafeHtmlTemplates.Template("
") - SafeHtml cellFrameWithTextColor(String textColor); - - @SafeHtmlTemplates.Template("
") - SafeHtml cellWithImageResourceAndText(ImageResource image, String text); - } - - private static RaceColumnTemplates raceColumnTemplate = GWT.create(RaceColumnTemplates.class); - - /** - * The leaderboard name is used to - * {@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; - - private final ErrorReporter errorReporter; - - private final StringMessages stringMessages; - - private final SortedCellTable leaderboardTable; - - private final MultiSelectionModel leaderboardSelectionModel; - - private LeaderboardDTO leaderboard; - - private final TotalRankColumn totalRankColumn; - - private final SelectionCheckboxColumn selectionCheckboxColumn; - - /** - * Passed to the {@link ManeuverCountRaceColumn}. Modifications to this list will modify the column's children list - * when updated the next time. - */ - private final List selectedManeuverDetails; - - /** - * Passed to the {@link LegColumn}. Modifications to this list will modify the column's children list when updated - * the next time. - */ - private final List selectedLegDetails; - - /** - * Passed to the {@link TextRaceColumn}. Modifications to this list will modify the column's children list when - * updated the next time. - */ - private final List selectedRaceDetails; - - private final List selectedOverallDetailColumns; - - private final Map> overallDetailColumnMap; - - private RaceColumnSelection raceColumnSelection; - - protected final String RACE_COLUMN_HEADER_STYLE; - - protected final String LEG_DETAIL_COLUMN_HEADER_STYLE; - - protected final String LEG_DETAIL_COLUMN_STYLE; - - protected final String LEG_COLUMN_HEADER_STYLE; - - protected final String RACE_COLUMN_STYLE; - - protected final String LEG_COLUMN_STYLE; - - protected final String TOTAL_COLUMN_STYLE; - - private final Timer timer; - - /** - * A {@link LeaderboardDTO} tells something about the live delay through its {@link LeaderboardDTO#getDelayToLiveInMillisForLatestRace()} method. - * If this flag is true, the live delay in the {@link #timer} will be adjusted each time the {@link #updateLeaderboard(LeaderboardDTO)} - * method is invoked. Otherwise, the timer's delay will be left alone which is helpful, e.g., if the leaderboard panel is embedded - * in a race board panel that focuses on one particular race which may not be the same as the one controlling the leaderboard's overall - * live delay. - */ - private final boolean adjustTimerDelay; - - private boolean autoExpandPreSelectedRace; - - private boolean autoExpandLastRaceColumn; - - /** - * When true, the race columns don't display the competitors' scores in the race represented by the column - * but the cumulative score up to that race. - */ - private boolean showAddedScores; - - private boolean showCompetitorSailId; - private boolean showCompetitorFullName; - - /** - * When true then an additional column just before the overall points is displayed that sums up - * the number of races sailed per competitor. - */ - private boolean showOverallColumnWithNumberOfRacesCompletedPerCompetitor; - - /** - * Remembers whether the auto-expand of the pre-selected race (see {@link #autoExpandPreSelectedRace}) or last - * selected race {@link #autoExpandLastRaceColumn} has been performed once. It must not be performed another time. - */ - private boolean autoExpandPerformedOnce; - - /** - * This anchor's HTML holds the image tag for the play/pause button that needs to be updated when the {@link #timer} - * changes its playing state - */ - private Anchor playPause; - - private final CompetitorSelectionProvider competitorSelectionProvider; - private final HorizontalPanel filterControlPanel; - private Label filterStatusLabel; - private Button filterClearButton; - - /** - * The handler for changes in the leaderboard table's selection; its registration is kept in - * {@link #leaderboardAsTableSelectionModelRegistration} while it is registered as a selection handler - * on the {@link #leaderboardTable}. - */ - private final Handler selectionChangeHandler; - - /** - * While the {@link #selectionChangeHandler} is registered as a selection change handler on the - * {@link #leaderboardTable}'s selection model, this field holds the registration which can be used to - * remove the registration again. We'll use this to temporarily suspend selection events when actively - * modifying / adjusting the table selection to match the {@link #competitorSelectionProvider}. - */ - private HandlerRegistration leaderboardAsTableSelectionModelRegistration; - - /** - * If this is null, all leaderboard columns added by updating the leaderboard from the server are - * automatically added to the table. Otherwise, only the column whose - * {@link RaceColumnDTO#getRaceIdentifier(String) race identifier} matches the value of this attribute will be - * added. - */ - private final RegattaAndRaceIdentifier preSelectedRace; - - private final VerticalPanel contentPanel; - - private HorizontalPanel refreshAndSettingsPanel; - private Label scoreCorrectionLastUpdateTimeLabel; - private Label scoreCorrectionCommentLabel; - private Label liveRaceLabel; - - private boolean isEmbedded; - - private static final LeaderboardResources resources = GWT.create(LeaderboardResources.class); - private static final ComponentResources componentResources = GWT.create(ComponentResources.class); - private static final LeaderboardTableResources tableResources = GWT.create(LeaderboardTableResources.class); - - private ImageResource pauseIcon; - private ImageResource playIcon; - - private final BusyIndicator busyIndicator; - - /** - * Tells whether the leaderboard settings were explicitly changed by an external call to - * {@link #updateSettings(LeaderboardSettings)}. If so, a {@link #playStateChanged(PlayStates, PlayModes) play state - * change} will not automatically lead to a settings change. - */ - private boolean settingsUpdatedExplicitly = false; - - /** - * Tells if the leaderboard is currently handling a {@link #playStateChanged(PlayStates, PlayModes) play state - * change}. If this is the case, a call to {@link #updateSettings(LeaderboardSettings)} won't set the - * {@link #settingsUpdatedExplicitly} flag. - */ - private boolean currentlyHandlingPlayStateChange; - - private PlayModes oldPlayMode; - - private final AsyncActionsExecutor asyncActionsExecutor; - - /** - * See also {@link #getDefaultSortColumn()}. If no other column is explicitly selected for sorting and this - * attribute holds a non-null string identifying a valid race by name that is represented in this - * leaderboard panel then sort by it. Otherwise, default sorting will default to the overall rank column. - */ - private String raceNameForDefaultSorting; - - /** - * Can be used to disallow users to drill into the race details. - */ - private final boolean showRaceDetails; - - /** - * The {@link LastNRacesColumnSelection} column selection strategy requires a {@link RaceTimesInfoProvider}. This can either be injected - * by passing a non-null object of that type to the constructor, or such an object is created and remembered in this - * attribute when required the first time. - */ - private RaceTimesInfoProvider raceTimesInfoProvider; - private RaceTimesInfoProviderListener raceTimesInfoProviderListener; - - private int blurInOnSelectionChanged; - - /** - * When an element in the leaderboard receives focus, it needs to be blurred again to keep the surrounding scroll panel - * from scrolling anything into view - */ - private Element elementToBlur; - private boolean showSelectionCheckbox; - - private List leaderboardUpdateListener; - - private boolean initialCompetitorFilterHasBeenApplied = false; - private final boolean showCompetitorFilterStatus; - - private CompetitorFilterPanel competitorFilterPanel; - - protected StringMessages getStringMessages() { - return stringMessages; - } - - @Override - public Widget getEntryWidget() { - return this; - } - - @Override - public Widget getHeaderWidget() { - return null; - } - - @Override - public Widget getContentWidget() { - return contentPanel; - } - - @Override - public Widget getToolbarWidget() { - return null; - } - - @Override - public Widget getLegendWidget() { - return null; - } - - @Override - public boolean isEmbedded() { - return isEmbedded; - } - - public VerticalPanel getContentPanel() { - return contentPanel; - } - - protected ImageResource getSettingsIcon() { - return componentResources.settingsIcon(); - } - - public void updateSettings(final LeaderboardSettings newSettings) { - boolean oldShallAddOverallDetails = shallAddOverallDetails(); - if (newSettings.getOverallDetailsToShow() != null) { - selectedOverallDetailColumns.clear(); - selectedOverallDetailColumns.addAll(newSettings.getOverallDetailsToShow()); - } - if (!newSettings.isUpdateUponPlayStateChange() || !currentlyHandlingPlayStateChange) { - settingsUpdatedExplicitly = true; - } - setShowAddedScores(newSettings.isShowAddedScores()); - setShowCompetitorSailId(newSettings.isShowCompetitorSailIdColumn()); - setShowCompetitorFullName(newSettings.isShowCompetitorFullNameColumn()); - setShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(newSettings.isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor()); - final List> columnsToExpandAgain = new ArrayList>(); - for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { - Column c = getLeaderboardTable().getColumn(i); - if (c instanceof ExpandableSortableColumn) { - ExpandableSortableColumn expandableSortableColumn = (ExpandableSortableColumn) c; - if (expandableSortableColumn.isExpanded()) { - // now toggle expansion back and forth, - // enforcing a re-build of the visible - // child columns - expandableSortableColumn.toggleExpansion(); - columnsToExpandAgain.add(expandableSortableColumn); - } - } - } - if (newSettings.getManeuverDetailsToShow() != null) { - selectedManeuverDetails.clear(); - selectedManeuverDetails.addAll(newSettings.getManeuverDetailsToShow()); - } - if (newSettings.getLegDetailsToShow() != null) { - selectedLegDetails.clear(); - selectedLegDetails.addAll(newSettings.getLegDetailsToShow()); - } - if (newSettings.getRaceDetailsToShow() != null) { - selectedRaceDetails.clear(); - selectedRaceDetails.addAll(newSettings.getRaceDetailsToShow()); - } - // update strategy for determining the race columns to show; if settings' race columns to show is null, use the - // previously selected race columns / number of race columns for the new configuration - Iterable oldNamesOfRaceColumnsToShow = null; - if (newSettings.getNamesOfRaceColumnsToShow() == null) { - oldNamesOfRaceColumnsToShow = raceColumnSelection.getSelectedRaceColumnNames(); - } - switch (newSettings.getActiveRaceColumnSelectionStrategy()) { - case EXPLICIT: - if (preSelectedRace == null) { - raceColumnSelection = new ExplicitRaceColumnSelection(); - } else { - raceColumnSelection = new ExplicitRaceColumnSelectionWithPreselectedRace(preSelectedRace); - } - if (newSettings.getNamesOfRaceColumnsToShow() != null) { - raceColumnSelection.requestClear(); - for (String nameOfRaceColumnToShow : newSettings.getNamesOfRaceColumnsToShow()) { - RaceColumnDTO raceColumnToShow = getRaceByColumnName(nameOfRaceColumnToShow); - if (raceColumnToShow != null) { - raceColumnSelection.requestRaceColumnSelection(raceColumnToShow); - } - } - } else { - // apply the old column selections again - for (String oldNameOfRaceColumnToShow : oldNamesOfRaceColumnsToShow) { - final RaceColumnDTO raceColumnByName = getLeaderboard().getRaceColumnByName(oldNameOfRaceColumnToShow); - if (raceColumnByName != null) { - raceColumnSelection.requestRaceColumnSelection(raceColumnByName); - } - } - if (newSettings.getNamesOfRacesToShow() != null) { - raceColumnSelection.requestClear(); - for (String nameOfRaceToShow : newSettings.getNamesOfRacesToShow()) { - RaceColumnDTO raceColumnToShow = getRaceByName(nameOfRaceToShow); - if (raceColumnToShow != null) { - raceColumnSelection.requestRaceColumnSelection(raceColumnToShow); - } - } - } - } - break; - case LAST_N: - setRaceColumnSelectionToLastNStrategy(newSettings.getNumberOfLastRacesToShow()); - break; - } - - final boolean oldBusyState = getBusyIndicator().isBusy(); - getBusyIndicator().setBusy(true); - Runnable doWhenNecessaryDetailHasBeenLoaded = new Runnable() { - @Override - public void run() { - setAutoExpandPreSelectedRace(false); // avoid expansion during updateLeaderboard(...); will expand later - // if it was expanded before - // update leaderboard after settings panel column selection change - updateLeaderboard(leaderboard); - setAutoExpandPreSelectedRace(newSettings.isAutoExpandPreSelectedRace()); - - if (newSettings.getDelayBetweenAutoAdvancesInMilliseconds() != null) { - timer.setRefreshInterval(newSettings.getDelayBetweenAutoAdvancesInMilliseconds()); - } - for (ExpandableSortableColumn expandableSortableColumn : columnsToExpandAgain) { - expandableSortableColumn.toggleExpansion(); - } - if (newSettings.getNameOfRaceToSort() != null) { - final RaceColumn raceColumnByRaceName = getRaceColumnByRaceName(newSettings - .getNameOfRaceToSort()); - if (raceColumnByRaceName != null) { - getLeaderboardTable().sortColumn(raceColumnByRaceName, /* ascending */true); - } - } - getBusyIndicator().setBusy(oldBusyState); - } - }; - if (oldShallAddOverallDetails == shallAddOverallDetails() || oldShallAddOverallDetails || getLeaderboard().hasOverallDetails()) { - doWhenNecessaryDetailHasBeenLoaded.run(); - } else { // meaning that now the details need to be loaded from the server - updateLeaderboardAndRun(doWhenNecessaryDetailHasBeenLoaded); - } - } - - /** - * @param callWhenExpansionDataIsLoaded - */ - private void updateLeaderboardAndRun(final Runnable callWhenExpansionDataIsLoaded) { - final LeaderboardDTO previousLeaderboard = getLeaderboard(); - getSailingService().getLeaderboardByName(getLeaderboardName(), - timer.getPlayMode() == PlayModes.Live ? null : getLeaderboardDisplayDate(), - /* namesOfRacesForWhichToLoadLegDetails */getNamesOfExpandedRaces(), - shallAddOverallDetails(), previousLeaderboard.getId(), /* fillNetPointsUncorrected */ false, - 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 */); - } - })); - } - - public void setRaceColumnSelectionToLastNStrategy(final Integer numberOfLastRacesToShow) { - raceColumnSelection = new LastNRacesColumnSelection(numberOfLastRacesToShow, getRaceTimesInfoProvider()); - if (timer.getPlayState() != Timer.PlayStates.Playing) { - // wait for the first update and adjust leaderboard once the race times have been received - raceTimesInfoProviderListener = new RaceTimesInfoProviderListener() { - @Override - public void raceTimesInfosReceived(Map raceTimesInfo, long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) { - // remove - timer.adjustClientServerOffset(clientTimeWhenRequestWasSent, serverTimeDuringRequest, clientTimeWhenResponseWasReceived); - // remove the listener only in case a leaderboard has already been loaded - if(getLeaderboard() != null) { - updateLeaderboard(getLeaderboard()); - getRaceTimesInfoProvider().removeRaceTimesInfoProviderListener(raceTimesInfoProviderListener); - raceTimesInfoProviderListener = null; - } - } - }; - getRaceTimesInfoProvider().addRaceTimesInfoProviderListener(raceTimesInfoProviderListener); - } - } - - /** - * A leaderboard panel may have been provided with a valid {@link RaceTimesInfoProvider} upon creation; in this case, that object - * will be returned. If none was provided to the constructor, one is created and remembered if no previously created/remembered - * object exists.

- * - * Precondition: {@link #timer} is not null - */ - private RaceTimesInfoProvider getRaceTimesInfoProvider() { - if (raceTimesInfoProvider == null) { - final List trackedRacesIdentifiers; - if (leaderboard != null && getTrackedRacesIdentifiers() != null) { - trackedRacesIdentifiers = getTrackedRacesIdentifiers(); - } else { - trackedRacesIdentifiers = Collections.emptyList(); - } - raceTimesInfoProvider = new RaceTimesInfoProvider(getSailingService(), asyncActionsExecutor, errorReporter, - trackedRacesIdentifiers, timer.getRefreshInterval()); - } - return raceTimesInfoProvider; - } - - protected class CompetitorColumn extends LeaderboardSortableColumnWithMinMax { - private final CompetitorColumnBase base; - - protected CompetitorColumn(CompetitorColumnBase base) { - super(base.getCell(getLeaderboard()), SortingOrder.ASCENDING, LeaderboardPanel.this); - this.base = base; - } - - public CompetitorColumn(CompositeCell compositeCell, CompetitorColumnBase base) { - super(compositeCell, SortingOrder.ASCENDING, LeaderboardPanel.this); - this.base = base; - } - - @Override - public InvertibleComparator getComparator() { - return new InvertibleComparatorAdapter() { - @Override - public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { - return Collator.getInstance().compare(getLeaderboard().getDisplayName(o1.competitor), - getLeaderboard().getDisplayName(o2.competitor)); - } - }; - } - - @Override - public SafeHtmlHeader getHeader() { - return base.getHeader(); - } - - @Override - public LeaderboardRowDTO getValue(LeaderboardRowDTO object) { - return object; - } - - protected void defaultRender(Context context, LeaderboardRowDTO object, SafeHtmlBuilder sb) { - super.render(context, object, sb); - } - - @Override - public void render(Context context, LeaderboardRowDTO object, SafeHtmlBuilder sb) { - String competitorColor = LeaderboardPanel.this.competitorSelectionProvider.getColor(object.competitor, - LeaderboardPanel.this.preSelectedRace).getAsHtml(); - String competitorColorBarStyle; - if (LeaderboardPanel.this.isEmbedded && preSelectedRace != null) { - competitorColorBarStyle = "style=\"border-bottom: 2px solid " + competitorColor + ";\""; - } else { - competitorColorBarStyle = "style=\"border: none;\""; - } - base.render(object, competitorColorBarStyle, sb); - } - } - - /** - * Shows the country flag and sail ID, if present - * - * @author Axel Uhl (d043530) - * - */ - private class SailIDColumn extends LeaderboardSortableColumnWithMinMax { - private final CompetitorFetcher competitorFetcher; - - protected SailIDColumn(CompetitorFetcher competitorFetcher) { - super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); - this.competitorFetcher = competitorFetcher; - } - - @Override - public InvertibleComparator getComparator() { - return new InvertibleComparatorAdapter() { - @Override - public int compare(T o1, T o2) { - return competitorFetcher.getCompetitor(o1).getSailID() == null ? competitorFetcher.getCompetitor(o2).getSailID() == null ? 0 : -1 - : competitorFetcher.getCompetitor(o2).getSailID() == null ? 1 : Collator.getInstance().compare( - competitorFetcher.getCompetitor(o1).getSailID(), competitorFetcher.getCompetitor(o2).getSailID()); - } - }; - } - - @Override - public SafeHtmlHeader getHeader() { - return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.competitor()), stringMessages.sailIdColumnTooltip()); - } - - @Override - public void render(Context context, T object, SafeHtmlBuilder sb) { - ImageResourceRenderer renderer = new ImageResourceRenderer(); - CompetitorDTO competitor = competitorFetcher.getCompetitor(object); - final String twoLetterIsoCountryCode = competitor.getTwoLetterIsoCountryCode(); - final String flagImageURL = competitor.getFlagImageURL(); - - if (flagImageURL != null && !flagImageURL.isEmpty()) { - sb.appendHtmlConstant(""); - sb.appendHtmlConstant(" "); - } else { - 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()); - } - - @Override - public String getValue(T object) { - return competitorFetcher.getCompetitor(object).getSailID(); - } - } - - /** - * Displays net/total points and possible max-points reasons based on a {@link LeaderboardRowDTO} and a race name - * and makes the column sortable by the total points. - * - * @author Axel Uhl (D043530) - * - */ - protected abstract class RaceColumn extends ExpandableSortableColumn { - private RaceColumnDTO race; - - private final String headerStyle; - private final String columnStyle; - - public RaceColumn(RaceColumnDTO race, boolean enableExpansion, Cell cell, - SortingOrder preferredSortingOrder, String headerStyle, String columnStyle) { - super(LeaderboardPanel.this, enableExpansion, cell, preferredSortingOrder, stringMessages, - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, selectedRaceDetails, LeaderboardPanel.this); - setHorizontalAlignment(ALIGN_CENTER); - this.race = race; - this.headerStyle = headerStyle; - this.columnStyle = columnStyle; - } - - public RaceColumnDTO getRace() { - return race; - } - - public void setRace(RaceColumnDTO race) { - this.race = race; - } - - public String getRaceColumnName() { - return race.getRaceColumnName(); - } - - public boolean isMedalRace() { - return race.isMedalRace(); - } - - public boolean isLive(FleetDTO fleetDTO) { - return race.isLive(fleetDTO, timer.getLiveTimePointInMillis()); - } - - @Override - public String getColumnStyle() { - return columnStyle; - } - - /** - * Computes added scores for this RaceColumn - */ - private double computeAddedScores(LeaderboardRowDTO object) { - double addedScores = 0; - for (RaceColumnDTO raceColumn : getLeaderboard().getRaceList()) { - LeaderboardEntryDTO entryBefore = object.fieldsByRaceColumnName.get(raceColumn.getName()); - if (entryBefore.totalPoints != null) { - addedScores += entryBefore.totalPoints; - } - // else "add 0" - if (raceColumn.getName().equals(getRaceColumnName())) { - break; // we've reached the current column - stop here - } - } - return addedScores; - } - - /** - * Displays a combination of total points and maxPointsReason in bold, transparent, strike-through, depending on - * various criteria. Here's how: - * - *

-         *                                  total points                |    maxPointsReason
-         * -------------------------------+-----------------------------+-----------------------
-         *  not discarded, no maxPoints   | bold                        | none
-         *  not discarded, maxPoints      | bold                        | transparent
-         *  discarded, no maxPoints       | transparent, strike-through | none
-         *  discarded, maxPoints          | transparent, strike-through | transparent, strike-through
-         * 
- */ - @Override - public void render(Context context, LeaderboardRowDTO object, SafeHtmlBuilder html) { - LeaderboardEntryDTO entry = object.fieldsByRaceColumnName.get(getRaceColumnName()); - if (entry != null) { - boolean isLive = isLive(entry.fleet); - final String textColor = isLive ? IS_LIVE_TEXT_COLOR : DEFAULT_TEXT_COLOR; - final String addedScores = isShowAddedScores() ? scoreFormat.format(computeAddedScores(object)) : ""; - String totalOrAddedPointsAsText = isShowAddedScores() ? addedScores - : entry.totalPoints == null ? "" : scoreFormat.format(entry.totalPoints); - String netOrAddedPointsAsText = isShowAddedScores() ? addedScores - : entry.netPoints == null ? "" : scoreFormat.format(entry.netPoints); - if (entry.fleet != null && entry.fleet.getColor() != null) { - html.append(raceColumnTemplate.cellFrameWithTextColorAndFleetBorder(textColor, entry.fleet.getColor().getAsHtml())); - } else { - html.append(raceColumnTemplate.cellFrameWithTextColor(textColor)); - } - // don't show points if max points / penalty - if (entry.reasonForMaxPoints == null || entry.reasonForMaxPoints == MaxPointsReason.NONE) { - if (!entry.discarded) { - html.appendHtmlConstant(""); - html.appendHtmlConstant(totalOrAddedPointsAsText); - html.appendHtmlConstant(""); - } else { - html.appendHtmlConstant(" "); - html.appendHtmlConstant(netOrAddedPointsAsText); - html.appendHtmlConstant(""); - } - } else { - html.appendHtmlConstant(" "); - if (entry.discarded) { - html.appendHtmlConstant(""); - } - html.appendEscaped(entry.reasonForMaxPoints == MaxPointsReason.NONE ? "" : entry.reasonForMaxPoints.name()); - if (entry.discarded) { - html.appendHtmlConstant(""); - } - html.appendHtmlConstant(""); - } - html.appendHtmlConstant("
"); - } - } - - @Override - public InvertibleComparator getComparator() { - return new InvertibleComparatorAdapter() { - @Override - public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { - final int result; - if (isShowAddedScores()) { - double o1AddedScore = computeAddedScores(o1); - double o2AddedScore = computeAddedScores(o2); - double intermediate_result = o1AddedScore == 0. ? (o2AddedScore == 0. ? -1. - : isAscending() ? 1. : -1.) : o2AddedScore == 0. ? (isAscending() ? -1. : 1.) - : o1AddedScore - o2AddedScore; - if (!getLeaderboard().isHigherScoreBetter()) { - result = intermediate_result > 0 ? 1 : intermediate_result < 0 ? -1 : 0; - } else { - result = intermediate_result > 0 ? -1 : intermediate_result < 0 ? 1 : 0; - } - } else { - List competitorsFromBestToWorst = getLeaderboard().getCompetitorsFromBestToWorst( - race); - int o1Rank = competitorsFromBestToWorst.indexOf(o1.competitor) + 1; - int o2Rank = competitorsFromBestToWorst.indexOf(o2.competitor) + 1; - result = o1Rank == 0 ? o2Rank == 0 ? 0 : isAscending() ? 1 : -1 : o2Rank == 0 ? isAscending() ? -1 - : 1 : o1Rank - o2Rank; - } - return result; - } - }; - } - - @Override - public String getHeaderStyle() { - return headerStyle; - } - - @Override - public Header getHeader() { - SortableExpandableColumnHeader header = new SortableExpandableColumnHeader(/* title */race.getRaceColumnName(), - /* iconURL */race.isMedalRace() ? "/gwt/images/medal_small.png" : null, LeaderboardPanel.this, this, stringMessages); - return header; - } - } - - public static DetailType[] getAvailableRaceDetailColumnTypes() { - return new DetailType[] { DetailType.RACE_GAP_TO_LEADER_IN_SECONDS, - DetailType.RACE_AVERAGE_SPEED_OVER_GROUND_IN_KNOTS, - DetailType.RACE_DISTANCE_TRAVELED, DetailType.RACE_DISTANCE_TRAVELED_INCLUDING_GATE_START, - DetailType.RACE_TIME_TRAVELED, DetailType.RACE_CALCULATED_TIME_TRAVELED, - DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, - DetailType.RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS, DetailType.RACE_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD_IN_METERS, - DetailType.NUMBER_OF_MANEUVERS, DetailType.DISPLAY_LEGS, DetailType.CURRENT_LEG, - 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.TIME_BETWEEN_RACE_START_AND_COMPETITOR_START, - DetailType.SPEED_OVER_GROUND_AT_RACE_START, - DetailType.SPEED_OVER_GROUND_WHEN_PASSING_START, - DetailType.DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_PASSING_START_IN_METERS, - DetailType.START_TACK, - DetailType.RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL}; - } - - public static DetailType[] getAvailableOverallDetailColumnTypes() { - return new DetailType[] { DetailType.REGATTA_RANK, - DetailType.TOTAL_DISTANCE_TRAVELED, - DetailType.TOTAL_AVERAGE_SPEED_OVER_GROUND, - DetailType.TOTAL_TIME_SAILED_IN_SECONDS, - DetailType.MAXIMUM_SPEED_OVER_GROUND_IN_KNOTS, - DetailType.TIME_ON_TIME_FACTOR, - DetailType.TIME_ON_DISTANCE_ALLOWANCE_IN_SECONDS_PER_NAUTICAL_MILE}; - } - - private class TextRaceColumn extends RaceColumn implements RaceNameProvider { - /** - * Remembers the leg columns; null-padded, if {@link #getLegColumn(int)} asks for a column index - * not yet existing. It is important to remember the columns because column removal happens based on identity. - */ - private final List legColumns; - - public TextRaceColumn(RaceColumnDTO race, boolean expandable, SortingOrder preferredSortingOrder, - String headerStyle, String columnStyle) { - super(race, expandable, new TextCell(), preferredSortingOrder, headerStyle, columnStyle); - legColumns = new ArrayList(); - } - - public String getValue(LeaderboardRowDTO object) { - // The following code exists only for robustness. This method should never be called because - // RaceColumn implements its own render(...) method which doesn't make use of getValue(...) - final Double totalPoints = object.fieldsByRaceColumnName.get(getRaceColumnName()).totalPoints; - return "" + (totalPoints == null ? "" : scoreFormat.format(totalPoints)); - } - - @Override - protected void ensureExpansionDataIsLoaded(final Runnable callWhenExpansionDataIsLoaded) { - if (getLeaderboard().getLegCount(getRaceColumnName(), preSelectedRace) != -1) { - callWhenExpansionDataIsLoaded.run(); - } else { - updateLeaderboardAndRun(callWhenExpansionDataIsLoaded); - } - } - - @Override - protected Map> getDetailColumnMap( - LeaderboardPanel leaderboardPanel, StringMessages stringMessages, String detailHeaderStyle, - String detailColumnStyle) { - Map> result = new HashMap<>(); - result.put(DetailType.RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL, - new TimeSinceLastGpsFixColumn(DetailType.RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL, - new RaceRatioBetweenTimeSinceLastPositionFixAndAverageSamplingInterval(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.RACE_DISTANCE_TRAVELED, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_DISTANCE_TRAVELED, new RaceDistanceTraveledInMeters(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.RACE_DISTANCE_TRAVELED_INCLUDING_GATE_START, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_DISTANCE_TRAVELED_INCLUDING_GATE_START, new RaceDistanceTraveledIncludingGateStartInMeters(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.RACE_AVERAGE_SPEED_OVER_GROUND_IN_KNOTS, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_AVERAGE_SPEED_OVER_GROUND_IN_KNOTS, new RaceAverageSpeedInKnots(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.RACE_GAP_TO_LEADER_IN_SECONDS, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_GAP_TO_LEADER_IN_SECONDS, new RaceGapToLeaderInSeconds(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS, new CurrentSpeedOverGroundInKnots(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.RACE_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD_IN_METERS, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD_IN_METERS, new RaceDistanceToCompetitorFarthestAheadInMeters(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - 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, LeaderboardPanel.this)); - 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, LeaderboardPanel.this)); - 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(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.RACE_SPEED_OVER_GROUND_FIVE_SECONDS_BEFORE_START, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_SPEED_OVER_GROUND_FIVE_SECONDS_BEFORE_START, new SpeedFiveSecondsBeforeStartInKnots(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.DISTANCE_TO_START_AT_RACE_START, - new FormattedDoubleDetailTypeColumn(DetailType.DISTANCE_TO_START_AT_RACE_START, new DistanceToStartAtRaceStartInMeters(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.TIME_BETWEEN_RACE_START_AND_COMPETITOR_START, - new FormattedDoubleDetailTypeColumn(DetailType.TIME_BETWEEN_RACE_START_AND_COMPETITOR_START, new TimeBetweenRaceStartAndCompetitorStartInSeconds(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.SPEED_OVER_GROUND_AT_RACE_START, - new FormattedDoubleDetailTypeColumn(DetailType.SPEED_OVER_GROUND_AT_RACE_START, new SpeedOverGroundAtRaceStartInKnots(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.SPEED_OVER_GROUND_WHEN_PASSING_START, - new FormattedDoubleDetailTypeColumn(DetailType.SPEED_OVER_GROUND_WHEN_PASSING_START, new SpeedOverGroundWhenPassingStartInKnots(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_PASSING_START_IN_METERS, - new FormattedDoubleDetailTypeColumn(DetailType.DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_PASSING_START_IN_METERS, new DistanceToStarboardSideOfStartLineInMeters(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.START_TACK, new StartingTackColumn(new TackWhenStarting(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE)); - result.put(DetailType.NUMBER_OF_MANEUVERS, new ManeuverCountRaceColumn(getLeaderboardPanel(), this, stringMessages, - LeaderboardPanel.this.selectedManeuverDetails, LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, - LEG_DETAIL_COLUMN_HEADER_STYLE, LEG_DETAIL_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.CURRENT_LEG, - new FormattedDoubleDetailTypeColumn(DetailType.CURRENT_LEG, new CurrentLeg(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.RACE_TIME_TRAVELED, new TimeTraveledRaceColumn(getLeaderboardPanel(), this, - stringMessages, LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LEG_DETAIL_COLUMN_HEADER_STYLE, - LEG_DETAIL_COLUMN_STYLE)); - result.put(DetailType.RACE_CALCULATED_TIME_TRAVELED, - new FormattedDoubleDetailTypeColumn(DetailType.RACE_CALCULATED_TIME_TRAVELED, new RaceCalculatedTimeTraveledInSeconds(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - result.put(DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, new FormattedDoubleDetailTypeColumn( - DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, - new RaceCalculatedTimeAtEstimatedArrivalAtCompetitorFarthestAheadInSeconds(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); - return result; - } - - @Override - protected Iterable> getDirectChildren() { - List> result = new ArrayList<>(); - for (AbstractSortableColumnWithMinMax column : super.getDirectChildren()) { - result.add(column); - } - if (isExpanded() && selectedRaceDetails.contains(DetailType.DISPLAY_LEGS)) { - // it is important to re-use existing LegColumn objects because - // removing the columns from the table - // is based on column identity - int maxLegCount = getLeaderboard().getLegCount(getRaceColumnName(), preSelectedRace); - if (maxLegCount != -1) { - for (int i = 0; i < maxLegCount; i++) { - LegColumn legColumn = getLegColumn(i); - result.add(legColumn); - } - } else { - // the race is no longer part of the LeaderboardDTO; consider the non-null legs in legColumns: - for (LegColumn legColumn : legColumns) { - if (legColumn != null) { - result.add(legColumn); - } - } - } - } - return result; - } - - private LegColumn getLegColumn(int legNumber) { - LegColumn result; - if (legColumns.size() > legNumber && legColumns.get(legNumber) != null) { - result = legColumns.get(legNumber); - } else { - result = new LegColumn(LeaderboardPanel.this, getRaceColumnName(), legNumber, SortingOrder.ASCENDING, - stringMessages, Collections.unmodifiableList(selectedLegDetails), LEG_COLUMN_HEADER_STYLE, - LEG_COLUMN_STYLE, LEG_DETAIL_COLUMN_HEADER_STYLE, LEG_DETAIL_COLUMN_STYLE); - while (legColumns.size() <= legNumber) { - legColumns.add(null); - } - legColumns.set(legNumber, result); - } - return result; - } - - /** - * Reports the ratio of the time that passed since the last position fix and the average sampling interval of the - * competitor's track. On a perfect track, this value will never exceed 1.0. Immediately when a fix is received, this - * value goes to 0.0. For a competitor whose tracker is lagging, this value can grow considerably greater than 1. - * The value goes to null if there are no fixes in the track. - * - * @author Axel Uhl (D043530) - * - */ - private class RaceRatioBetweenTimeSinceLastPositionFixAndAverageSamplingInterval implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null && fieldsForRace.timeSinceLastPositionFixInSeconds != null && fieldsForRace.averageSamplingInterval != null) { - result = fieldsForRace.timeSinceLastPositionFixInSeconds / fieldsForRace.averageSamplingInterval.asSeconds(); - } - return result; - } - } - - /** - * Accumulates the average speed over all legs of a race - * - * @author Axel Uhl (D043530) - */ - private class RaceAverageSpeedInKnots implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null && fieldsForRace.legDetails != null) { - double distanceTraveledInMeters = 0; - long timeInMilliseconds = 0; - for (LegEntryDTO legDetail : fieldsForRace.legDetails) { - if (legDetail != null) { - if (legDetail.distanceTraveledInMeters != null && legDetail.timeInMilliseconds != null) { - distanceTraveledInMeters += legDetail.distanceTraveledInMeters; - timeInMilliseconds += legDetail.timeInMilliseconds; - } else { - distanceTraveledInMeters = 0; - timeInMilliseconds = 0; - break; - } - } - } - if (timeInMilliseconds != 0) { - result = distanceTraveledInMeters / (double) timeInMilliseconds * 1000 * 3600 / 1852; - } - } - return result; - } - } - - /** - * 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 RaceAverageAbsoluteCrossTrackErrorInMeters implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - 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; - } - } - - /** - * Fetches the competitor's distance to the start mark at the time the race started - * - * @author Axel Uhl (D043530) - */ - private class DistanceToStartAtRaceStartInMeters implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.distanceToStartLineAtStartOfRaceInMeters; - } - return result; - } - } - - /** - * Fetches the time between start of race and the competitors start mark passing, telling - * how long after the gun the competitor actually started. - * - * @author Axel Uhl (D043530) - */ - private class TimeBetweenRaceStartAndCompetitorStartInSeconds implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.timeBetweenRaceStartAndCompetitorStartInSeconds; - } - return result; - } - } - - /** - * Fetches the competitor's speed over ground at the time the race started - * - * @author Axel Uhl (D043530) - */ - private class SpeedOverGroundAtRaceStartInKnots implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.speedOverGroundAtStartOfRaceInKnots; - } - return result; - } - } - - /** - * Fetches the competitor's distance to the start mark five seconds before the race started - */ - private class DistanceToStartFiveSecondsBeforeStartInMeters implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.distanceToStartLineFiveSecondsBeforeStartInMeters; - } - return result; - } - } - - /** - * Fetches the competitor's speed over ground five seconds before the race started - */ - private class SpeedFiveSecondsBeforeStartInKnots implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.speedOverGroundFiveSecondsBeforeStartInKnots; - } - return result; - } - } - - /** - * Fetches the competitor's speed over ground at the time the competitor passed the start - * - * @author Axel Uhl (D043530) - */ - private class SpeedOverGroundWhenPassingStartInKnots implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.speedOverGroundAtPassingStartWaypointInKnots; - } - return result; - } - } - - /** - * Fetches the competitor's distance to the starboard side of the start line when competitor passed the start. - * If the start waypoint is not a gate/line, the distance to the single buoy is used. - * - * @author Axel Uhl (D043530) - */ - private class DistanceToStarboardSideOfStartLineInMeters implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.distanceToStarboardSideOfStartLineInMeters; - } - return result; - } - } - - /** - * Fetches the competitor's speed over ground at the time the competitor passed the start - * - * @author Axel Uhl (D043530) - */ - private class TackWhenStarting implements LegDetailField { - @Override - public Tack get(LeaderboardRowDTO row) { - Tack result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.startTack; - } - return result; - } - } - - /** - * Accumulates the distance traveled over all legs of a race - * - * @author Axel Uhl (D043530) - */ - private class RaceDistanceTraveledInMeters implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null && fieldsForRace.legDetails != null) { - for (LegEntryDTO legDetail : fieldsForRace.legDetails) { - if (legDetail != null) { - if (legDetail.distanceTraveledInMeters != null) { - if (result == null) { - result = 0.0; - } - result += legDetail.distanceTraveledInMeters; - } - } - } - } - return result; - } - } - - private class RaceCalculatedTimeTraveledInSeconds implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.calculatedTime == null ? null : fieldsForRace.calculatedTime.asSeconds(); - } - return result; - } - } - - private class RaceCalculatedTimeAtEstimatedArrivalAtCompetitorFarthestAheadInSeconds implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead == null ? null - : fieldsForRace.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead.asSeconds(); - } - return result; - } - } - - /** - * Accumulates the distance traveled over all legs of a race and considers the specifics of a gate start. The - * first leg of a gate start gets as additional distance traveled the distance from the port side of the line to - * the point where the competitor started which helps normalize the distance traveled and make them comparable - * across early and late starters. - * - * @author Axel Uhl (D043530) - */ - private class RaceDistanceTraveledIncludingGateStartInMeters implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null && fieldsForRace.legDetails != null) { - for (LegEntryDTO legDetail : fieldsForRace.legDetails) { - if (legDetail != null) { - if (legDetail.distanceTraveledIncludingGateStartInMeters != null) { - if (result == null) { - result = 0.0; - } - result += legDetail.distanceTraveledIncludingGateStartInMeters; - } - } - } - } - return result; - } - } - - private class CurrentLeg implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null && fieldsForRace.legDetails != null && !fieldsForRace.legDetails.isEmpty()) { - for (LegEntryDTO legDetail : fieldsForRace.legDetails) { - if (legDetail != null) { - if (legDetail.started) { - if (result == null) { - result = 0.0; - } - result++; - } else { - if (result != null) { - result++; - } - break; - } - } - } - } - return result; - } - } - - /** - * Computes the gap to leader exploiting the ordering of the leg detail columns - * - * @author Axel Uhl (D043530) - */ - private class RaceGapToLeaderInSeconds implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null) { - result = fieldsForRace.gapToLeaderInOwnTime == null ? null : fieldsForRace.gapToLeaderInOwnTime.asSeconds(); - } - return result; - } - } - - /** - * Computes the windward distance to the competitor farthest ahead in the race (not necessarily the leader with - * handicap scoring) in meters - * - * @author Axel Uhl (D043530) - */ - private class RaceDistanceToCompetitorFarthestAheadInMeters implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null && fieldsForRace.windwardDistanceToCompetitorFarthestAheadInMeters != null) { - result = fieldsForRace.windwardDistanceToCompetitorFarthestAheadInMeters; - } - return result; - } - } - - private class CurrentSpeedOverGroundInKnots implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null && fieldsForRace.legDetails != null) { - int lastLegIndex = fieldsForRace.legDetails.size() - 1; - if (lastLegIndex >= 0) { - LegEntryDTO lastLegDetail = fieldsForRace.legDetails.get(lastLegIndex); - // competitor may be in leg prior to the one the leader is in; find competitors current leg - while (lastLegDetail == null && lastLegIndex > 0) { - lastLegDetail = fieldsForRace.legDetails.get(--lastLegIndex); - } - if (lastLegDetail != null) { - result = lastLegDetail.currentSpeedOverGroundInKnots; - } - } - } - return result; - } - } - } - - /** - * Column that display the number of races a competitor has finished. Currently - * taking into account {@link MaxPointsReason#DNS}, {@link MaxPointsReason#DNF} - * and {@link MaxPointsReason#DNC}. Configurable over {@link LeaderboardSettings#isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor()}. - * - * @author Simon Marcel Pamies - */ - private class TotalRacesCompletedColumn extends LeaderboardSortableColumnWithMinMax { - private final String columnStyle; - private final MaxPointsReason[] MAX_POINTS_REASONS_THAT_IDENTIFY_NON_FINISHED_RACES = - new MaxPointsReason[] {MaxPointsReason.DNS, MaxPointsReason.DNF, MaxPointsReason.DNC}; - - protected TotalRacesCompletedColumn(String columnStyle) { - super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); - this.columnStyle = columnStyle; - setHorizontalAlignment(ALIGN_CENTER); - } - - private int computeNumberOfRacesCompleted(LeaderboardRowDTO object) { - int racesSailedInRow = 0; - for (RaceColumnDTO raceColumn : getLeaderboard().getRaceList()) { - LeaderboardEntryDTO entryBefore = object.fieldsByRaceColumnName.get(raceColumn.getName()); - if (entryBefore.totalPoints != null) { - if (entryBefore.reasonForMaxPoints.equals(MaxPointsReason.NONE) || - !Util.contains(Arrays.asList(MAX_POINTS_REASONS_THAT_IDENTIFY_NON_FINISHED_RACES), entryBefore.reasonForMaxPoints)) { - racesSailedInRow++; - } - } - } - return racesSailedInRow; - } - - @Override - public String getValue(LeaderboardRowDTO object) { - return "" + computeNumberOfRacesCompleted(object); - } - - @Override - public void render(Context context, LeaderboardRowDTO object, SafeHtmlBuilder sb) { - String textColor = getLeaderboard().hasLiveRace(timer.getLiveTimePointInMillis()) ? IS_LIVE_TEXT_COLOR : DEFAULT_TEXT_COLOR; - - sb.appendHtmlConstant(""); - sb.appendEscaped(getValue(object)); - sb.appendHtmlConstant(""); - } - - @Override - public InvertibleComparator getComparator() { - return new InvertibleComparatorAdapter() { - @Override - public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { - double o1RacesSailed = computeNumberOfRacesCompleted(o1); - double o2RacesSailed = computeNumberOfRacesCompleted(o2); - double result = o1RacesSailed == 0. ? o2RacesSailed == 0. ? 0. : isAscending() ? 1. : -1. : o2RacesSailed == 0. ? isAscending() ? 1. : -1. : o2RacesSailed - o1RacesSailed; - return result > 0 ? 1 : result < 0 ? -1 : 0; - } - }; - } - - @Override - public String getColumnStyle() { - return columnStyle; - } - - @Override - public SafeHtmlHeader getHeader() { - return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.racesScored()), stringMessages.racesScoredTooltip()); - } - } - - /** - * Displays the totals for a competitor for the entire leaderboard. - * - * @author Axel Uhl (D043530) - * - */ - private class TotalsColumn extends LeaderboardSortableColumnWithMinMax { - private final String columnStyle; - - protected TotalsColumn(String columnStyle) { - super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); - this.columnStyle = columnStyle; - setHorizontalAlignment(ALIGN_CENTER); - } - - @Override - public String getValue(LeaderboardRowDTO object) { - Double totalPoints = object.totalPoints; - return "" + (totalPoints == null ? "" : scoreFormat.format(totalPoints)); - } - - @Override - public void render(Context context, LeaderboardRowDTO object, SafeHtmlBuilder sb) { - String textColor = getLeaderboard().hasLiveRace(timer.getLiveTimePointInMillis()) ? IS_LIVE_TEXT_COLOR : DEFAULT_TEXT_COLOR; - - sb.appendHtmlConstant(""); - sb.appendEscaped(getValue(object)); - sb.appendHtmlConstant(""); - } - - @Override - public InvertibleComparator getComparator() { - return new InvertibleComparatorAdapter() { - @Override - public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { - return getLeaderboard().competitors.indexOf(o1.competitor) - - getLeaderboard().competitors.indexOf(o2.competitor); - } - }; - } - - @Override - public String getColumnStyle() { - return columnStyle; - } - - @Override - public SafeHtmlHeader getHeader() { - return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.total()), stringMessages.totalsColumnTooltip()); - } - } - - protected class CarryColumn extends LeaderboardSortableColumnWithMinMax { - public CarryColumn() { - super(new AbstractSafeHtmlCell(new AbstractSafeHtmlRenderer() { - @Override - public SafeHtml render(LeaderboardRowDTO object) { - return new SafeHtmlBuilder().appendEscaped(object.carriedPoints == null ? "" : scoreFormat.format(object.carriedPoints)).toSafeHtml(); - } - }) { - @Override - protected void render(com.google.gwt.cell.client.Cell.Context context, SafeHtml data, SafeHtmlBuilder sb) { - sb.append(data); - } - }, SortingOrder.ASCENDING, LeaderboardPanel.this); - setSortable(true); - } - - protected CarryColumn(Cell cell) { - super(cell, SortingOrder.ASCENDING, LeaderboardPanel.this); - setSortable(true); - } - - @Override - public LeaderboardRowDTO getValue(LeaderboardRowDTO object) { - return object; - } - - @Override - public InvertibleComparator getComparator() { - return new InvertibleComparatorAdapter() { - @Override - public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { - Double o1CarriedPoints = o1.carriedPoints; - if (o1CarriedPoints == null) { - o1CarriedPoints = 0.0; - } - Double o2CarriedPoints = o2.carriedPoints; - if (o2CarriedPoints == null) { - o2CarriedPoints = 0.0; - } - return o1CarriedPoints.compareTo(o2CarriedPoints); - } - }; - } - - @Override - public SafeHtmlHeader getHeader() { - return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.carry()), stringMessages.carryColumnTooltip()); - } - } - - private class LeaderboardSelectionCheckboxColumn extends com.sap.sailing.gwt.ui.client.shared.controls.SelectionCheckboxColumn - implements CompetitorSelectionChangeListener { - protected LeaderboardSelectionCheckboxColumn(final CompetitorSelectionProvider competitorSelectionProvider) { - super(tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell()); - competitorSelectionProvider.addCompetitorSelectionChangeListener(this); - } - - @Override - public Boolean getValue(LeaderboardRowDTO row) { - return competitorSelectionProvider.isSelected(row.competitor); - } - - @Override public void competitorsListChanged(Iterable competitors) {} - @Override public void filterChanged(FilterSet> oldFilterSet, FilterSet> newFilterSet) {} - @Override public void filteredCompetitorsListChanged(Iterable filteredCompetitors) {} - - /** - * Ensure that the checkbox is redrawn when the competitor selection changes - */ - @Override - public void addedToSelection(CompetitorDTO competitor) { - final LeaderboardRowDTO row = getRow(competitor); - if (row != null) { - redrawRow(row); - } - } - - private void redrawRow(final LeaderboardRowDTO row) { - final List leaderboardDataList = getData().getList(); - synchronized (leaderboardDataList) { - redrawRow(row, leaderboardDataList); - } - } - - /** - * Ensure that the checkbox is redrawn when the competitor selection changes - */ - @Override - public void removedFromSelection(CompetitorDTO competitor) { - final LeaderboardRowDTO row = getRow(competitor); - if (row != null) { - redrawRow(row); - } - } - - @Override - public void updateMinMax() {} - - @Override - protected ListDataProvider getListDataProvider() { - return getData(); - } - } - - private class TotalRankColumn extends LeaderboardSortableColumnWithMinMax { - public TotalRankColumn() { - super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); - setHorizontalAlignment(ALIGN_CENTER); - setSortable(true); - } - - @Override - public String getValue(LeaderboardRowDTO object) { - final int totalRank = getLeaderboard().getTotalRank(object.competitor); - return "" + (totalRank == 0 ? "" : totalRank); - } - - @Override - public InvertibleComparator getComparator() { - return new InvertibleComparatorAdapter() { - @Override - public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { - final int totalRank1 = getLeaderboard().getTotalRank(o1.competitor); - final int totalRank2 = getLeaderboard().getTotalRank(o2.competitor); - return totalRank1 == 0 ? totalRank2 == 0 ? 0 : 1 : totalRank2 == 0 ? -1 : totalRank1 - totalRank2; - } - }; - } - - @Override - public SafeHtmlHeader getHeader() { - return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.totalRegattaRank()), stringMessages.rankColumnTooltip()); - } - } - - private class StartingTackColumn extends DetailTypeColumn { - public StartingTackColumn(LegDetailField field, String headerStyle, String columnStyle) { - super(DetailType.START_TACK, field, new TextCell(), headerStyle, columnStyle, LeaderboardPanel.this); - } - - @Override - public String getValue(LeaderboardRowDTO row) { - return getField().get(row) == null ? null : getField().get(row) == Tack.PORT ? stringMessages.portTack() : stringMessages.starboardTack(); - } - } - - public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, - LeaderboardSettings settings, CompetitorSelectionProvider competitorSelectionProvider, - String leaderboardName, ErrorReporter errorReporter, final StringMessages stringMessages, - final UserAgentDetails userAgent, boolean showRaceDetails) { - this(sailingService, asyncActionsExecutor, settings, false, /* preSelectedRace */null, competitorSelectionProvider, - null, leaderboardName, errorReporter, stringMessages, userAgent, showRaceDetails); - } - - public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, - LeaderboardSettings settings, boolean isEmbedded, RegattaAndRaceIdentifier preSelectedRace, - CompetitorSelectionProvider competitorSelectionProvider, String leaderboardGroupName, - String leaderboardName, ErrorReporter errorReporter, final StringMessages stringMessages, - final UserAgentDetails userAgent, boolean showRaceDetails) { - this(sailingService, asyncActionsExecutor, settings, isEmbedded, preSelectedRace, competitorSelectionProvider, new Timer( - // 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, - /* competitorSearchTextBox */ null, /* showSelectionCheckbox */ true, /* optionalRaceTimesInfoProvider */ null, - /* autoExpandLastRaceColumn */ false, /* adjustTimerDelay */ true, /*autoApplyTop30Filter*/ false, false); - } - - public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, - LeaderboardSettings settings, boolean isEmbedded, RegattaAndRaceIdentifier preSelectedRace, - CompetitorSelectionProvider competitorSelectionProvider, Timer timer, String leaderboardGroupName, - String leaderboardName, final ErrorReporter errorReporter, final StringMessages stringMessages, - final UserAgentDetails userAgent, boolean showRaceDetails, CompetitorFilterPanel competitorSearchTextBox, - boolean showSelectionCheckbox, RaceTimesInfoProvider optionalRaceTimesInfoProvider, - boolean autoExpandLastRaceColumn, boolean adjustTimerDelay, boolean autoApplyTopNFilter, boolean showCompetitorFilterStatus) { - this.setTitle(stringMessages.leaderboard()); - this.showSelectionCheckbox = showSelectionCheckbox; - this.showRaceDetails = showRaceDetails; - this.sailingService = sailingService; - this.asyncActionsExecutor = asyncActionsExecutor; - this.isEmbedded = isEmbedded; - this.preSelectedRace = preSelectedRace; - this.competitorSelectionProvider = competitorSelectionProvider; - competitorSelectionProvider.addCompetitorSelectionChangeListener(this); - this.setLeaderboardName(leaderboardName); - this.errorReporter = errorReporter; - this.stringMessages = stringMessages; - this.selectedLegDetails = new ArrayList(); - this.selectedRaceDetails = new ArrayList(); - this.selectedOverallDetailColumns = new ArrayList(); - this.raceTimesInfoProvider = optionalRaceTimesInfoProvider; - this.selectedManeuverDetails = new ArrayList(); - this.adjustTimerDelay = adjustTimerDelay; - this.initialCompetitorFilterHasBeenApplied = !autoApplyTopNFilter; - this.showCompetitorFilterStatus = showCompetitorFilterStatus; - overallDetailColumnMap = createOverallDetailColumnMap(); - settingsUpdatedExplicitly = !settings.isUpdateUponPlayStateChange(); - this.leaderboardUpdateListener = new ArrayList(); - if (settings.getLegDetailsToShow() != null) { - selectedLegDetails.addAll(settings.getLegDetailsToShow()); - } - if (settings.getManeuverDetailsToShow() != null) { - selectedManeuverDetails.addAll(settings.getManeuverDetailsToShow()); - } - if (settings.getRaceDetailsToShow() != null) { - selectedRaceDetails.addAll(settings.getRaceDetailsToShow()); - } - if (settings.getOverallDetailsToShow() != null) { - selectedOverallDetailColumns.addAll(settings.getOverallDetailsToShow()); - } - setAutoExpandPreSelectedRace(settings.isAutoExpandPreSelectedRace()); - this.autoExpandLastRaceColumn = autoExpandLastRaceColumn; - if (settings.getDelayBetweenAutoAdvancesInMilliseconds() != null) { - timer.setRefreshInterval(settings.getDelayBetweenAutoAdvancesInMilliseconds()); - } - this.timer = timer; - timer.addPlayStateListener(this); - timer.addTimeListener(this); - switch (settings.getActiveRaceColumnSelectionStrategy()) { - case EXPLICIT: - if (preSelectedRace == null) { - raceColumnSelection = new ExplicitRaceColumnSelection(); - } else { - raceColumnSelection = new ExplicitRaceColumnSelectionWithPreselectedRace(preSelectedRace); - } - break; - case LAST_N: - setRaceColumnSelectionToLastNStrategy(settings.getNumberOfLastRacesToShow()); - break; - } - totalRankColumn = new TotalRankColumn(); - selectionCheckboxColumn = new LeaderboardSelectionCheckboxColumn(competitorSelectionProvider); - RACE_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableRaceColumnHeader(); - LEG_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableLegColumnHeader(); - LEG_DETAIL_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableLegDetailColumnHeader(); - RACE_COLUMN_STYLE = tableResources.cellTableStyle().cellTableRaceColumn(); - LEG_COLUMN_STYLE = tableResources.cellTableStyle().cellTableLegColumn(); - LEG_DETAIL_COLUMN_STYLE = tableResources.cellTableStyle().cellTableLegDetailColumn(); - TOTAL_COLUMN_STYLE = tableResources.cellTableStyle().cellTableTotalColumn(); - leaderboardTable = new SortedCellTableWithStylableHeaders( - /* pageSize */10000, tableResources); - leaderboardTable.addCellPreviewHandler(new CellPreviewEvent.Handler() { - @Override - public void onCellPreview(CellPreviewEvent event) { - if (BrowserEvents.FOCUS.equals(event.getNativeEvent().getType())) { - elementToBlur = event.getNativeEvent().getEventTarget().cast(); - elementToBlur.blur(); - blurInOnSelectionChanged = 2; // blur a couple of times; doing it one time only doesn't seem to work reliably - blurFocusedElementAfterSelectionChange(); - } - } - }); - leaderboardTable.ensureDebugId("LeaderboardCellTable"); - getLeaderboardTable().setWidth("100%"); - leaderboardSelectionModel = new MultiSelectionModel(); - // remember handler registration so we can temporarily remove it and re-add it to suspend selection events while we're actively changing it - selectionChangeHandler = new Handler() { - @Override - public void onSelectionChange(SelectionChangeEvent event) { - List selection = new ArrayList(); - for (LeaderboardRowDTO row : getSelectedRows()) { - selection.add(row.competitor); - } - LeaderboardPanel.this.competitorSelectionProvider.setSelection(selection, /* listenersNotToNotify */LeaderboardPanel.this); - if (blurInOnSelectionChanged > 0) { - blurInOnSelectionChanged--; - blurFocusedElementAfterSelectionChange(); - } - } - }; - leaderboardAsTableSelectionModelRegistration = leaderboardSelectionModel.addSelectionChangeHandler(selectionChangeHandler); - leaderboardTable.setSelectionModel(leaderboardSelectionModel, selectionCheckboxColumn.getSelectionManager()); - setShowAddedScores(settings.isShowAddedScores()); - setShowCompetitorSailId(settings.isShowCompetitorSailIdColumn()); - setShowCompetitorFullName(settings.isShowCompetitorFullNameColumn()); - setShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(settings.isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor()); - if (timer.isInitialized()) { - loadCompleteLeaderboard(getLeaderboardDisplayDate()); - } - - contentPanel = new VerticalPanel(); - leaderboardTable.getElement().getStyle().setMarginTop(10, Unit.PX); - contentPanel.setStyleName(STYLE_LEADERBOARD_CONTENT); - busyIndicator = new SimpleBusyIndicator(false, 0.8f); - busyIndicator.ensureDebugId("BusyIndicator"); - - - // the information panel - if (!isEmbedded) { - Widget toolbarPanel = createToolbarPanel(); - contentPanel.add(toolbarPanel); - } - if (competitorSearchTextBox != null) { - contentPanel.add(competitorSearchTextBox); - contentPanel.add(busyIndicator); - competitorSearchTextBox.getSettingsButton().addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - new SettingsDialog(LeaderboardPanel.this, stringMessages).show(); - } - }); - this.competitorFilterPanel = competitorSearchTextBox; - } - SortedCellTable leaderboardTable = getLeaderboardTable(); - // leaderboardTable.getElement().getStyle().setMarginTop(5, Unit.PX); - filterControlPanel = new HorizontalPanel(); - filterControlPanel.setStyleName("LeaderboardPanel-FilterControl-Panel"); - contentPanel.add(leaderboardTable); - if (showCompetitorFilterStatus) { - contentPanel.add(createFilterDeselectionControl()); - } - setWidget(contentPanel); - raceNameForDefaultSorting = settings.getNameOfRaceToSort(); - } - - private Widget createToolbarPanel() { - FlowPanel informationPanel = new FlowPanel(); - informationPanel.setStyleName(STYLE_LEADERBOARD_INFO); - scoreCorrectionLastUpdateTimeLabel = new Label(""); - scoreCorrectionCommentLabel = new Label(""); - informationPanel.add(scoreCorrectionCommentLabel); - informationPanel.add(scoreCorrectionLastUpdateTimeLabel); - - liveRaceLabel = new Label(stringMessages.live()); - liveRaceLabel.setStyleName(STYLE_LEADERBOARD_LIVE_RACE); - liveRaceLabel.getElement().getStyle().setFontWeight(FontWeight.BOLD); - liveRaceLabel.getElement().getStyle().setColor(IS_LIVE_TEXT_COLOR); - liveRaceLabel.setVisible(false); - informationPanel.add(liveRaceLabel); - - // the toolbar panel - DockPanel toolbarPanel = new DockPanel(); - toolbarPanel.ensureDebugId("ToolbarPanel"); - toolbarPanel.setStyleName(STYLE_LEADERBOARD_TOOLBAR); - if (!isEmbedded) { - toolbarPanel.add(informationPanel, DockPanel.WEST); - toolbarPanel.add(busyIndicator, DockPanel.WEST); - } - toolbarPanel.setWidth("100%"); - toolbarPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE); - ClickHandler playPauseHandler = new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - if (LeaderboardPanel.this.timer.getPlayState() == PlayStates.Playing) { - LeaderboardPanel.this.timer.pause(); - } else { - // playing the standalone leaderboard means putting it into live mode - LeaderboardPanel.this.timer.setPlayMode(PlayModes.Live); - } - } - }; - 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); - - refreshAndSettingsPanel.add(refreshPanel); - toolbarPanel.add(refreshAndSettingsPanel, DockPanel.EAST); - return toolbarPanel; - } - - private Widget createFilterDeselectionControl() { - filterStatusLabel = new Label(); - filterStatusLabel.setStyleName("LeaderboardPanel-FilterControl-StatusLabel"); - filterStatusLabel.setText(""); - filterControlPanel.add(filterStatusLabel); - filterClearButton = new Button(stringMessages.showAll()); - filterClearButton.setStyleName("LeaderboardPanel-FilterClear-Button"); - filterClearButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - getBusyIndicator().setBusy(true); - competitorFilterPanel.clearAllActiveFilters(); - timeChanged(new Date(), null); - } - }); - filterControlPanel.add(filterClearButton); - filterControlPanel.setCellHorizontalAlignment(filterClearButton, HasHorizontalAlignment.ALIGN_RIGHT); - setFilterControlStatus(); - return filterControlPanel; - } - - private void setFilterControlStatus() { - if (showCompetitorFilterStatus) { - boolean filtersActive = competitorSelectionProvider.hasActiveFilters(); - if (filtersActive) { - String labelText = ""; - for (Filter filter : competitorSelectionProvider.getCompetitorsFilterSet().getFilters()) { - if (filter instanceof FilterWithUI) { - labelText += ((FilterWithUI)filter).getLocalizedDescription(stringMessages) + ", "; - } else { - labelText += filter.getName() + ", "; - } - } - filterStatusLabel.setText("Active Filter(s): " + labelText.substring(0, labelText.length()-2)); - filterClearButton.setVisible(true); - filterControlPanel.setVisible(true); - } else { - filterStatusLabel.setText(""); - filterClearButton.setVisible(false); - filterControlPanel.setVisible(false); - if (competitorFilterPanel != null) { - competitorFilterPanel.clearSelection(); - } - } - } - } - - private static class TotalDistanceTraveledInMetersField implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - return row.totalDistanceTraveledInMeters; - } - } - - private static class TimeOnTimeFactorColumn implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - return row.competitor.getTimeOnTimeFactor(); - } - } - - private static class TimeOnDistanceAllowanceInSecondsPerNauticalMileColumn implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - return row.competitor.getTimeOnDistanceAllowancePerNauticalMile() == null ? null : - row.competitor.getTimeOnDistanceAllowancePerNauticalMile().asSeconds(); - } - } - - private static class TotalAverageSpeedOverGroundField implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - final Double result; - if (row.totalDistanceTraveledInMeters != null && row.totalTimeSailedInSeconds != null && row.totalTimeSailedInSeconds != 0.0) { - result = row.totalDistanceTraveledInMeters / row.totalTimeSailedInSeconds / Mile.METERS_PER_NAUTICAL_MILE * 3600; - } else { - result = null; - } - return result; - } - } - - private Map> createOverallDetailColumnMap() { - Map> result = new HashMap<>(); - result.put(DetailType.TOTAL_DISTANCE_TRAVELED, - new FormattedDoubleDetailTypeColumn(DetailType.TOTAL_DISTANCE_TRAVELED, - new TotalDistanceTraveledInMetersField(), RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this)); - result.put(DetailType.TOTAL_AVERAGE_SPEED_OVER_GROUND, - new FormattedDoubleDetailTypeColumn(DetailType.TOTAL_AVERAGE_SPEED_OVER_GROUND, - new TotalAverageSpeedOverGroundField(), RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this)); - result.put(DetailType.MAXIMUM_SPEED_OVER_GROUND_IN_KNOTS, new MaxSpeedOverallColumn(RACE_COLUMN_HEADER_STYLE, - RACE_COLUMN_STYLE, this)); - result.put(DetailType.TOTAL_TIME_SAILED_IN_SECONDS, createOverallTimeTraveledColumn()); - result.put(DetailType.TIME_ON_TIME_FACTOR, - new FormattedDoubleDetailTypeColumn(DetailType.TIME_ON_TIME_FACTOR, - new TimeOnTimeFactorColumn(), RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this)); - result.put(DetailType.TIME_ON_DISTANCE_ALLOWANCE_IN_SECONDS_PER_NAUTICAL_MILE, - new FormattedDoubleDetailTypeColumn(DetailType.TIME_ON_DISTANCE_ALLOWANCE_IN_SECONDS_PER_NAUTICAL_MILE, - new TimeOnDistanceAllowanceInSecondsPerNauticalMileColumn(), RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this)); - - return result; - } - - private OverallTimeTraveledColumn createOverallTimeTraveledColumn() { - return new OverallTimeTraveledColumn(this, stringMessages, RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE); - } - - /** - * Largely for subclasses to add more stuff to this panel, such as more toolbar buttons. - */ - protected HorizontalPanel getRefreshAndSettingsPanel() { - return refreshAndSettingsPanel; - } - - private RaceColumnDTO getRaceByName(String raceName) { - if (getLeaderboard() != null) { - for (RaceColumnDTO race : getLeaderboard().getRaceList()) { - for (FleetDTO fleet : race.getFleets()) { - if (race.getRaceIdentifier(fleet) != null - && raceName.equals(race.getRaceIdentifier(fleet).getRaceName())) { - return race; - } - } - } - } - return null; - } - - private RaceColumnDTO getRaceByColumnName(String columnName) { - if (getLeaderboard() != null) { - for (RaceColumnDTO race : getLeaderboard().getRaceList()) { - if (columnName.equals(race.getRaceColumnName())) { - return race; - } - } - } - return null; - } - - private RaceColumn getRaceColumnByRaceName(String raceName) { - for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { - Column column = getLeaderboardTable().getColumn(i); - if (column instanceof RaceColumn) { - RaceColumnDTO raceInLeaderboard = ((RaceColumn) column).getRace(); - for (FleetDTO fleet : raceInLeaderboard.getFleets()) { - final RegattaAndRaceIdentifier raceIdentifier = raceInLeaderboard.getRaceIdentifier(fleet); - if (raceIdentifier != null && raceIdentifier.getRaceName().equals(raceName)) { - return (RaceColumn) column; - } - } - } - } - return null; - } - - private RaceColumn getRaceColumnByRaceColumnName(String raceColumnName) { - for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { - Column column = getLeaderboardTable().getColumn(i); - if (column instanceof RaceColumn) { - if (((RaceColumn) column).getRaceColumnName().equals(raceColumnName)) { - return (RaceColumn) column; - } - } - } - return null; - } - - private SafeHtml getPlayPauseImgHtml(PlayStates playState) { - if (playState == PlayStates.Playing) { - return AbstractImagePrototype.create(pauseIcon).getSafeHtml(); - } else { - return AbstractImagePrototype.create(playIcon).getSafeHtml(); - } - } - - 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() { - return autoExpandPreSelectedRace; - } - - private boolean isAutoExpandLastRaceColumn() { - return autoExpandLastRaceColumn; - } - - private void setAutoExpandPreSelectedRace(boolean autoExpandPreSelectedRace) { - this.autoExpandPreSelectedRace = autoExpandPreSelectedRace; - if (autoExpandPreSelectedRace) { - autoExpandPerformedOnce = false; - } - } - - private boolean isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor() { - return showOverallColumnWithNumberOfRacesCompletedPerCompetitor; - } - - private void setShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(boolean showOverallColumnWithNumberOfRacesCompletedPerCompetitor) { - this.showOverallColumnWithNumberOfRacesCompletedPerCompetitor = showOverallColumnWithNumberOfRacesCompletedPerCompetitor; - } - - private boolean isShowAddedScores() { - return showAddedScores; - } - - private void setShowAddedScores(boolean showAddedScores) { - this.showAddedScores = showAddedScores; - } - - private boolean isShowCompetitorSailId() { - return showCompetitorSailId; - } - - private void setShowCompetitorSailId(boolean showCompetitorSailId) { - this.showCompetitorSailId = showCompetitorSailId; - } - - private boolean isShowCompetitorFullName() { - return showCompetitorFullName; - } - - private void setShowCompetitorFullName(boolean showCompetitorFullName) { - this.showCompetitorFullName = showCompetitorFullName; - } - - /** - * The time point for which the leaderboard currently shows results. In {@link PlayModes#Replay replay mode} this is - * the {@link #timer}'s time point. In {@link PlayModes#Live live mode} the {@link #timer}'s time is quantized to - * the closest full second to increase the likelihood of cache hits in the back end. - */ - protected Date getLeaderboardDisplayDate() { - return timer.getTime(); - } - - /** - * adds the column to the right end of the {@link #getLeaderboardTable() leaderboard table} and sets - * the column style according to the {@link LeaderboardSortableColumnWithMinMax#getColumnStyle() column's style definition}. - */ - protected void addColumn(AbstractSortableColumnWithMinMax column) { - leaderboardTable.addColumn(column, column.getHeader(), column.getComparator(), column - .getPreferredSortingOrder().isAscending()); - String columnStyle = column.getColumnStyle(); - if (columnStyle != null) { - getLeaderboardTable().addColumnStyleName(getLeaderboardTable().getColumnCount() - 1, columnStyle); - } - } - - protected void insertColumn(int beforeIndex, AbstractSortableColumnWithMinMax column) { - // remove column styles of those columns whose index will shift right by - // one: - removeColumnStyles(beforeIndex); - getLeaderboardTable().insertColumn(beforeIndex, column, column.getHeader(), column.getComparator(), - column.getPreferredSortingOrder().isAscending()); - addColumnStyles(beforeIndex); - } - - private void addColumnStyles(int startColumn) { - for (int i = startColumn; i < getLeaderboardTable().getColumnCount(); i++) { - AbstractSortableColumnWithMinMax columnToRemoveStyleFor = (AbstractSortableColumnWithMinMax) getLeaderboardTable() - .getColumn(i); - String columnStyle = columnToRemoveStyleFor.getColumnStyle(); - if (columnStyle != null) { - getLeaderboardTable().addColumnStyleName(i, columnStyle); - } - } - } - - private void removeColumnStyles(int startColumn) { - for (int i = startColumn; i < getLeaderboardTable().getColumnCount(); i++) { - AbstractSortableColumnWithMinMax columnToRemoveStyleFor = (AbstractSortableColumnWithMinMax) getLeaderboardTable() - .getColumn(i); - String columnStyle = columnToRemoveStyleFor.getColumnStyle(); - if (columnStyle != null) { - getLeaderboardTable().removeColumnStyleName(i, columnStyle); - } - } - } - - /** - * removes the column specified by columnIndex from the {@link #getLeaderboardTable() leaderboard - * table} and fixes the column styles again (see {@link #addColumnStyles(int)}). - */ - protected void removeColumn(int columnIndex) { - Column c = getLeaderboardTable().getColumn(columnIndex); - if (c instanceof ExpandableSortableColumn) { - ExpandableSortableColumn expandableColumn = (ExpandableSortableColumn) c; - if (expandableColumn.isExpanded()) { - // remove expanded child columns from the leaderboard... - expandableColumn.toggleExpansion(); - // them remember that column c was expanded: - expandableColumn.setExpanded(true); - } - } - removeColumnStyles(/* startColumn */columnIndex); - getLeaderboardTable().removeColumn(columnIndex); - addColumnStyles(/* startColumn */columnIndex); - } - - protected void removeColumn(Column c) { - int columnIndex = getLeaderboardTable().getColumnIndex(c); - if (columnIndex != -1) { - removeColumn(columnIndex); - } - } - - private void loadCompleteLeaderboard(Date date) { - if (needsDataLoading()) { - GetLeaderboardByNameAction getLeaderboardByNameAction = new GetLeaderboardByNameAction(sailingService, - getLeaderboardName(), useNullAsTimePoint() ? null : date, - /* namesOfRacesForWhichToLoadLegDetails */getNamesOfExpandedRaces(), shallAddOverallDetails(), /* previousLeaderboard */ - getLeaderboard(), isFillNetPointsUncorrected(), timer, errorReporter, stringMessages); - this.asyncActionsExecutor.execute(getLeaderboardByNameAction, LOAD_LEADERBOARD_DATA_CATEGORY, - new AsyncCallback() { - @Override - public void onSuccess(LeaderboardDTO result) { - updateLeaderboard(result); - getBusyIndicator().setBusy(false); - } - - @Override - public void onFailure(Throwable caught) { - getBusyIndicator().setBusy(false); - getErrorReporter() - .reportError("Error trying to obtain leaderboard contents: " + caught.getMessage(), - true /* silentMode */); - } - }); - } else { - getBusyIndicator().setBusy(false); - } - } - - protected boolean isFillNetPointsUncorrected() { - return false; - } - - /** - * 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(); - } - - /** - * Determine from column expansion state which races need their leg details - */ - private Collection getNamesOfExpandedRaces() { - Collection namesOfExpandedRaces = new ArrayList(); - for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { - Column column = getLeaderboardTable().getColumn(i); - if (column instanceof RaceColumn) { - RaceColumn raceColumn = (RaceColumn) column; - if (raceColumn.isExpanded()) { - namesOfExpandedRaces.add(raceColumn.getRaceColumnName()); - } - } - } - return namesOfExpandedRaces; - } - - private boolean shallAddOverallDetails() { - return !selectedOverallDetailColumns.isEmpty(); - } - - private void applyTop30FilterIfCompetitorSizeGreaterEqual40(LeaderboardDTO leaderboard) { - int maxRaceRank = 30; - if (leaderboard.competitors.size() >= 40) { - CompetitorRaceRankFilter raceRankFilter = new CompetitorRaceRankFilter(); - raceRankFilter.setLeaderboardFetcher(this); - raceRankFilter.setSelectedRace(preSelectedRace); - raceRankFilter.setQuickRankProvider(this.competitorFilterPanel.getQuickRankProvider()); - raceRankFilter.setOperator(new BinaryOperator(BinaryOperator.Operators.LessThanEquals)); - raceRankFilter.setValue(maxRaceRank); - FilterSet> activeFilterSet = - competitorSelectionProvider.getOrCreateCompetitorsFilterSet(stringMessages.topNCompetitorsByRaceRank(maxRaceRank)); - activeFilterSet.addFilter(raceRankFilter); - competitorSelectionProvider.setCompetitorsFilterSet(activeFilterSet); - } - } - - /** - * Assigns leaderboard to {@link #leaderboard} and updates the UI accordingly. Also updates the min/max - * values on the columns. - */ - protected void updateLeaderboard(LeaderboardDTO leaderboard) { - if (leaderboard != null) { - Collection> columnsToCollapseAndExpandAgain = getExpandedRaceColumnsWhoseDisplayedLegCountChanged(leaderboard); - for (RaceColumn columnToCollapseAndExpandAgain : columnsToCollapseAndExpandAgain) { - columnToCollapseAndExpandAgain.toggleExpansion(); - } - competitorSelectionProvider.setCompetitors(leaderboard.competitors, /* listenersNotToNotify */this); - if (!initialCompetitorFilterHasBeenApplied) { - applyTop30FilterIfCompetitorSizeGreaterEqual40(leaderboard); - initialCompetitorFilterHasBeenApplied = true; - } - raceColumnSelection.autoUpdateRaceColumnSelectionForUpdatedLeaderboard(getLeaderboard(), leaderboard); - setLeaderboard(leaderboard); - adjustColumnLayout(leaderboard); - updateRaceColumnDTOsToRaceColumns(leaderboard); - for (RaceColumn columnToCollapseAndExpandAgain : columnsToCollapseAndExpandAgain) { - columnToCollapseAndExpandAgain.toggleExpansion(); - } - adjustDelayToLive(); - final Map rowsToDisplay = getRowsToDisplay(); - Set rowsToAdd = new HashSet<>(rowsToDisplay.values()); - Map rowsToUpdate = new HashMap<>(); - synchronized (getData().getList()) { - int index = 0; - for (Iterator i = getData().getList().iterator(); i.hasNext(); ) { - LeaderboardRowDTO oldRow = i.next(); - LeaderboardRowDTO newRow = rowsToDisplay.get(oldRow.competitor); - if (newRow != null) { - rowsToUpdate.put(index++, newRow); // update row in place, preserving its selection state - rowsToAdd.remove(newRow); // no need to add this row when it was updated in-place - } else { - i.remove(); // old row's competitor not found in new rows' competitors; remove old row from table - } - } - for (Entry updateEntry : rowsToUpdate.entrySet()) { - LeaderboardRowDTO oldElement = getData().getList().set(updateEntry.getKey(), updateEntry.getValue()); - leaderboardSelectionModel.setSelected(oldElement, false); // make sure the old element is no longer part of the selection - updateSelection(updateEntry.getValue()); - } - for (LeaderboardRowDTO rowToAdd : rowsToAdd) { - getData().getList().add(rowToAdd); - updateSelection(rowToAdd); - } - } - RaceColumn lastRaceColumn = null; - for (int i=getLeaderboardTable().getColumnCount()-1; i>=0; i--) { - if (getLeaderboardTable().getColumn(i) instanceof RaceColumn) { - lastRaceColumn = (RaceColumn) getLeaderboardTable().getColumn(i); - break; - } - } - for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { - AbstractSortableColumnWithMinMax c = (AbstractSortableColumnWithMinMax) getLeaderboardTable().getColumn(i); - c.updateMinMax(); - // Toggle pre-selected race, if the setting is set and it isn't open yet, or the last race column if that was requested - if ((!autoExpandPerformedOnce && isAutoExpandPreSelectedRace() && c instanceof RaceColumn - && ((RaceColumn) c).getRace().hasTrackedRace(preSelectedRace)) || - (isAutoExpandLastRaceColumn() && c == lastRaceColumn)) { - ExpandableSortableColumn expandableSortableColumn = (ExpandableSortableColumn) c; - if (!expandableSortableColumn.isExpanded()) { - expandableSortableColumn.toggleExpansion(); - autoExpandPerformedOnce = true; - } - } - if (c instanceof RaceColumn && ((RaceColumn) c).getRace().hasTrackedRace(preSelectedRace)) { - RaceColumnDTO raceColumn = ((RaceColumn) c).getRace(); - informLeaderboardUpdateListenersAboutRaceSelected(preSelectedRace, raceColumn); - } - } - if (leaderboardTable.getCurrentlySortedColumn() != null) { - leaderboardTable.sort(); - } else { - AbstractSortableColumnWithMinMax columnToSortFor = getDefaultSortColumn(); - leaderboardTable.sortColumn(columnToSortFor, columnToSortFor.getPreferredSortingOrder().isAscending()); - } - - if (!isEmbedded) { - scoreCorrectionCommentLabel.setText(leaderboard.getComment() != null ? leaderboard.getComment() : ""); - if (leaderboard.getTimePointOfLastCorrectionsValidity() != null) { - Date lastCorrectionDate = leaderboard.getTimePointOfLastCorrectionsValidity(); - String lastUpdate = DateAndTimeFormatterUtil.longDateFormatter.render(lastCorrectionDate) + ", " - + DateAndTimeFormatterUtil.longTimeFormatter.render(lastCorrectionDate); - scoreCorrectionLastUpdateTimeLabel.setText(stringMessages.lastScoreUpdate() + ": " + lastUpdate); - } else { - scoreCorrectionLastUpdateTimeLabel.setText(""); - } - - List> liveRaces = leaderboard.getLiveRaces(timer.getLiveTimePointInMillis()); - boolean hasLiveRace = !liveRaces.isEmpty(); - liveRaceLabel.setText(hasLiveRace ? getLiveRacesText() : ""); - if (hasLiveRace) { - String liveRaceText = ""; - if(liveRaces.size() == 1) { - com.sap.sse.common.Util.Pair liveRace = liveRaces.get(0); - liveRaceText = stringMessages.raceIsLive("'" + liveRace.getA().getRaceColumnName() + "'"); - } else { - String raceNames = ""; - for (com.sap.sse.common.Util.Pair liveRace : liveRaces) { - raceNames += "'" + liveRace.getA().getRaceColumnName() + "', "; - } - // remove last ", " - raceNames = raceNames.substring(0, raceNames.length() - 2); - liveRaceText = stringMessages.racesAreLive(raceNames); - } - liveRaceLabel.setText(liveRaceText); - } else { - liveRaceLabel.setText(""); - } - scoreCorrectionLastUpdateTimeLabel.setVisible(!hasLiveRace); - liveRaceLabel.setVisible(hasLiveRace); - } - informLeaderboardUpdateListenersAboutLeaderboardUpdated(leaderboard); - getBusyIndicator().setBusy(false); - } - } - - /** - * Adjusts the row's selection in the {@link #leaderboardSelectionModel} so it matches its selection state in - * the {@link #competitorSelectionProvider}. - */ - private void updateSelection(LeaderboardRowDTO row) { - final boolean shallBeSelected = competitorSelectionProvider.isSelected(row.competitor); - if (leaderboardAsTableSelectionModelRegistration != null) { - // suspend selection events while actively adjusting the leaderboardSelectionModel to match the competitorSelectionProvider - leaderboardAsTableSelectionModelRegistration.removeHandler(); - leaderboardAsTableSelectionModelRegistration = null; - } - if (leaderboardSelectionModel.isSelected(row) != shallBeSelected) { - leaderboardSelectionModel.setSelected(row, shallBeSelected); - } - // register the selection change handler again - leaderboardAsTableSelectionModelRegistration = leaderboardTable.getSelectionModel().addSelectionChangeHandler(selectionChangeHandler); - } - - /** - * The race columns hold a now outdated copy of a {@link RaceColumnDTO} which needs to be updated from the {@link LeaderboardDTO} just received - */ - private void updateRaceColumnDTOsToRaceColumns(LeaderboardDTO leaderboard) { - for (RaceColumnDTO newRace : leaderboard.getRaceList()) { - RaceColumn raceColumn = getRaceColumnByRaceColumnName(newRace.getName()); - if (raceColumn != null) { - raceColumn.setRace(newRace); - } - } - } - - /** - * Due to a course change, a race may change its number of legs. All expanded race columns that show leg columns and - * whose leg count changed need to be collapsed before the leaderboard is replaced, and expanded afterwards again. - * Race columns whose toggling is {@link ExpandableSortableColumn#isTogglingInProcess() currently in progress} are - * not considered because their new state will be considered after replacing anyhow. - * - * @param newLeaderboard - * the new leaderboard before assigning to {@link #leaderboard} - * @return the columns that were collapsed in this step and that shall be expanded again after the leaderboard has - * been replaced - */ - private Collection> getExpandedRaceColumnsWhoseDisplayedLegCountChanged(LeaderboardDTO newLeaderboard) { - Set> result = new HashSet>(); - if (selectedRaceDetails.contains(DetailType.DISPLAY_LEGS)) { - for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { - Column c = getLeaderboardTable().getColumn(i); - if (c instanceof RaceColumn) { - RaceColumn rc = (RaceColumn) c; - // If the new leaderboard no longer contains the column, getLegCount will return -1, causing the column - // to be collapsed if it was expanded. This is correct because otherwise, removing it would no longer - // know the correct leg count. - if (!rc.isTogglingInProcess() && rc.isExpanded()) { - int oldLegCount = getLeaderboard().getLegCount(rc.getRaceColumnName(), preSelectedRace); - int newLegCount = newLeaderboard.getLegCount(rc.getRaceColumnName(), preSelectedRace); - if (oldLegCount != newLegCount) { - result.add(rc); - } - } - } - } - } - return result; - } - - /** - * Based on the {@link LeaderboardDTO#getDelayToLiveInMillisForLatestRace()} for the race that has the latest - * {@link RaceColumnDTO#getStartDate(FleetDTO) start time}, automatically adjusts the delay accordingly unless the - * {@link #adjustTimerDelay} flag is false - */ - private void adjustDelayToLive() { - if (adjustTimerDelay) { - if (leaderboard.getDelayToLiveInMillisForLatestRace() != null) { - setDelayInMilliseconds(leaderboard.getDelayToLiveInMillisForLatestRace()); - } - } - } - - /** - * Extracts the rows to display of the leaderboard. These are all {@link AbstractLeaderboardDTO#rows - * rows} in case {@link #preSelectedRace} is null, or only the rows of the competitors who scored in - * the race identified by {@link #preSelectedRace} otherwise. - */ - @Override - public Map getRowsToDisplay() { - Map result; - Iterable allFilteredCompetitors = competitorSelectionProvider.getFilteredCompetitors(); - result = new HashMap(); - if (preSelectedRace == null) { - for (CompetitorDTO competitor : leaderboard.rows.keySet()) { - if (Util.contains(allFilteredCompetitors, competitor)) { - result.put(competitor, leaderboard.rows.get(competitor)); - } - } - } else { - for (CompetitorDTO competitorInPreSelectedRace : getCompetitors(preSelectedRace)) { - if (Util.contains(allFilteredCompetitors, competitorInPreSelectedRace)) { - result.put(competitorInPreSelectedRace, leaderboard.rows.get(competitorInPreSelectedRace)); - } - } - } - return result; - } - - /** - * The {@link LeaderboardDTO} holds {@link LeaderboardDTO#getRaceList() races} as {@link RaceColumnDTO} objects. - * Those map their fleet names to the {@link RegattaAndRaceIdentifier} which identifies the tracked race - * representing the fleet race within the race column. - *

- * - * On the other hand, a {@link LeaderboardRowDTO} has {@link LeaderboardEntryDTO}, keyed by race column name. The - * entry DTOs, in turn, store the {@link RaceIdentifier} of the race in which the respective competitor achieved the - * score. - *

- * - * With this information it is possible to identify the competitors who participated in a particular tracked race, - * as identified through the race parameter. - * - * @return all competitors for which the {@link #getLeaderboard() leaderboard} has an entry whose - * {@link LeaderboardEntryDTO#race} equals race - */ - private Iterable getCompetitors(RaceIdentifier race) { - Set result = new HashSet(); - for (RaceColumnDTO raceColumn : getLeaderboard().getRaceList()) { - if (raceColumn.hasTrackedRace(race)) { - for (Map.Entry e : getLeaderboard().rows.entrySet()) { - LeaderboardEntryDTO entry = e.getValue().fieldsByRaceColumnName.get(raceColumn.getRaceColumnName()); - if (entry != null && entry.race != null && entry.race.equals(race)) { - result.add(e.getKey()); - } - } - } - } - return result; - } - - private AbstractSortableColumnWithMinMax getDefaultSortColumn() { - AbstractSortableColumnWithMinMax defaultSortColumn = null; - if (raceNameForDefaultSorting != null) { - defaultSortColumn = getRaceColumnByRaceName(raceNameForDefaultSorting); - } - if (defaultSortColumn == null) { - defaultSortColumn = getRankColumn(); - } - return defaultSortColumn; - } - - private TotalRankColumn getRankColumn() { - return totalRankColumn; - } - - protected void setLeaderboard(LeaderboardDTO leaderboard) { - this.leaderboard = leaderboard; - } - - @Override - public LeaderboardDTO getLeaderboard() { - return leaderboard; - } - - private void adjustColumnLayout(LeaderboardDTO leaderboard) { - int columnIndex = 0; - columnIndex = ensureSelectionCheckboxColumn(columnIndex); - columnIndex = ensureRankColumn(columnIndex); - columnIndex = ensureSailIDAndCompetitorColumn(columnIndex); - columnIndex = updateCarryColumn(leaderboard, columnIndex); - adjustOverallDetailColumns(leaderboard, columnIndex); - // first remove race columns no longer needed: - removeUnusedRaceColumns(leaderboard); - if (leaderboard != null) { - createMissingAndAdjustExistingRaceColumns(leaderboard); - ensureTotalsColumn(); - updateTotalRacesSailedColumn(); - } - } - - /** - * Ensures that the columns requested by {@link #selectedOverallDetailColumns} are in the table. Assumes that if there are - * any existing overall details columns, they start at indexOfFirstOverallDetailsColumn and are in the order - * defined by {@link #getAvailableOverallDetailColumnTypes()}. - * - * @param indexOfFirstOverallDetailsColumn - * tells the column index for the first overall details column - */ - private void adjustOverallDetailColumns(LeaderboardDTO leaderboard, int indexOfFirstOverallDetailsColumn) { - List> overallDetailColumnsToShow = new ArrayList>(); - // ensure the ordering in overallDetailColumnsToShow conforms to the ordering of getAvailableOverallDetailColumnTypes() - for (DetailType overallDetailType : getAvailableOverallDetailColumnTypes()) { - if (selectedOverallDetailColumns.contains(overallDetailType) && overallDetailColumnMap.containsKey(overallDetailType)) { - overallDetailColumnsToShow.add(overallDetailColumnMap.get(overallDetailType)); - } - } - int currentColumnIndex = indexOfFirstOverallDetailsColumn; - int i = 0; // index into overallDetailColumnToShow - Column currentColumn = currentColumnIndex < getLeaderboardTable().getColumnCount() ? - getLeaderboardTable().getColumn(currentColumnIndex) : null; - // repeat until no more column to check for removal and no more column left to check for need to insert - while (iraceColumn - */ - private void correctColumnData(RaceColumn raceColumn, RaceColumnDTO race) { - int columnIndex = getRaceColumnPosition(raceColumn); - if (raceColumn.isExpansionEnabled() != race.hasTrackedRaces() || race.isMedalRace() != raceColumn.isMedalRace()) { - if (raceColumn.isExpanded()) { - raceColumn.toggleExpansion(); // remove children from table - } - removeColumn(columnIndex); - insertColumn(columnIndex, createRaceColumn(race)); - } - } - - /** - * Removes all RaceColumns, starting at count {@link raceColumnStartIndex raceColumnStartIndex} - * - * @param raceColumnStartIndex - * The index of the race column should be deleted from. - * @param raceName - * The name of the racing column until the table should be cleared. - */ - private void removeRaceColumnFromRaceColumnStartIndexBeforeRace(int raceColumnStartIndex, RaceColumnDTO race) { - int counter = 0; - for (int leaderboardposition = 0; leaderboardposition < getLeaderboardTable().getColumnCount(); leaderboardposition++) { - Column c = getLeaderboardTable().getColumn(leaderboardposition); - if (c instanceof RaceColumn) { - RaceColumn raceColumn = (RaceColumn) c; - if (!raceColumn.getRaceColumnName().equals(race.getRaceColumnName())) { - if (raceColumnStartIndex == counter) { - removeColumn(raceColumn); - } - } else { - return; - } - counter++; - } - } - } - - /** - * Gets a ColumnPosition of a raceColumn - * - * @param raceColumn - * The column for which the position is to be found in the leaderboard table - * @return the position. Returns -1 if raceColumn not existing in leaderboardTable. - */ - private int getRaceColumnPosition(RaceColumn raceColumn) { - for (int leaderboardposition = 0; leaderboardposition < getLeaderboardTable().getColumnCount(); leaderboardposition++) { - Column c = getLeaderboardTable().getColumn(leaderboardposition); - if (c instanceof RaceColumn) { - RaceColumn rc = (RaceColumn) c; - if (rc.equals(raceColumn)) { - return leaderboardposition; - } - } - } - return -1; - } - - /** - * This method returns the position where a race column should get inserted. - * - * @param raceName - * the name of the race to insert - * @param listpos - * the position of the race in the {@link selectedRaceColumns selectedRaceColumns} - * @return the position of a race column right before which to insert the race column so that it is the - * listposth race in the table or -1 if no such race column was found, e.g., in case the column - * needs to be inserted as the last race in the table - */ - private int getColumnPositionToInsert(RaceColumnDTO race, int listpos) { - int raceColumnCounter = 0; - int noRaceColumnCounter = 0; - boolean raceColumnFound = false; - for (int leaderboardPosition = 0; !raceColumnFound - && leaderboardPosition < getLeaderboardTable().getColumnCount(); leaderboardPosition++) { - Column c = getLeaderboardTable().getColumn(leaderboardPosition); - if (c instanceof RaceColumn) { - // RaceColumn raceColumn = (RaceColumn) c; - if (raceColumnCounter == listpos) { - raceColumnFound = true; - } else { - raceColumnCounter++; - } - } else { - noRaceColumnCounter++; - } - } - if (raceColumnFound) { - return raceColumnCounter + noRaceColumnCounter; - } else { - return -1; - } - } - - /** - * Removes all columns of type {@link RaceColumn} from the leaderboard table if their name is not in the list of - * names of the selectedRaceColumns. - */ - private void removeRaceColumnsNotSelected(Iterable selectedRaceColumns) { - Set selectedRaceColumnNames = new HashSet(); - for (RaceColumnDTO selectedRaceColumn : selectedRaceColumns) { - selectedRaceColumnNames.add(selectedRaceColumn.getRaceColumnName()); - } - List> columnsToRemove = new ArrayList>(); - for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { - Column c = getLeaderboardTable().getColumn(i); - if (c instanceof RaceColumn - && (leaderboard == null || !selectedRaceColumnNames.contains(((RaceColumn) c) - .getRaceColumnName()))) { - columnsToRemove.add(c); - } - } - for (Column c : columnsToRemove) { - removeColumn(c); - } - } - - /** - * Existing and matching race columns may still need to be removed, re-created and inserted because the "tracked" - * property may have changed, changing the columns expandability. - */ - private void createMissingAndAdjustExistingRaceColumns(LeaderboardDTO leaderboard) { - // Correct order of races in selectedRaceColum - Iterable correctedOrderSelectedRaces = raceColumnSelection.getSelectedRaceColumnsOrderedAsInLeaderboard(leaderboard); - removeRaceColumnsNotSelected(correctedOrderSelectedRaces); - for (int selectedRaceCount = 0; selectedRaceCount < Util.size(correctedOrderSelectedRaces); selectedRaceCount++) { - RaceColumnDTO selectedRaceColumn = Util.get(correctedOrderSelectedRaces, selectedRaceCount); - final RaceColumn raceColumn = selectedRaceColumn == null ? null - : getRaceColumnByRaceColumnName(selectedRaceColumn.getName()); - if (raceColumn != null) { - // remove all raceColumns, starting at a specific selectedRaceCount, up to but excluding the selected - // raceName with the result that selectedRace is at position selectedRaceCount afterwards - removeRaceColumnFromRaceColumnStartIndexBeforeRace(selectedRaceCount, selectedRaceColumn); - correctColumnData(raceColumn, selectedRaceColumn); - } else { - // get correct position to insert the column - int positionToInsert = getColumnPositionToInsert(selectedRaceColumn, selectedRaceCount); - if (positionToInsert != -1) { - insertColumn(positionToInsert, createRaceColumn(selectedRaceColumn)); - } else { - // Add the raceColumn with addRaceColumn, if no RaceColumn is existing in leaderboard - addRaceColumn(createRaceColumn(selectedRaceColumn)); - } - } - } - } - - protected RaceColumn createRaceColumn(RaceColumnDTO raceInLeaderboard) { - TextRaceColumn textRaceColumn = new TextRaceColumn(raceInLeaderboard, shallExpandRaceColumn(raceInLeaderboard), - SortingOrder.ASCENDING, RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE); - return textRaceColumn; - } - - /** - * To be expandable, a race column needs to have one or more tracked races associated and needs to have at least - * some GPS data available. Wind data is not required for expandability because several metrics can reasonably be - * determined even without reliable wind information. - */ - private boolean shallExpandRaceColumn(RaceColumnDTO raceColumnDTO) { - return showRaceDetails && raceColumnDTO.hasTrackedRaces() && raceColumnDTO.hasGPSData(); - } - - private void removeUnusedRaceColumns(LeaderboardDTO leaderboard) { - List> columnsToRemove = new ArrayList>(); - for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { - Column c = getLeaderboardTable().getColumn(i); - if (c instanceof RaceColumn - && (leaderboard == null || !leaderboard.raceListContains(((RaceColumn) c).getRaceColumnName()))) { - columnsToRemove.add(c); - } - } - // Tricky issue: if the race column is currently expanded, we can't know anymore how many detail columns - // there are because the updated LeaderboardDTO object doesn't contain the race anymore. We have to - // collapse and remove all LegColumns following the RaceColumn - for (Column c : columnsToRemove) { - removeColumn(c); - } - } - - /** - * If the last column is the totals column, remove it. Add the race column as the last column. - */ - private void addRaceColumn(RaceColumn raceColumn) { - if (getLeaderboardTable().getColumn(getLeaderboardTable().getColumnCount() - 1) instanceof TotalsColumn) { - removeColumn(getLeaderboardTable().getColumnCount() - 1); - } - addColumn(raceColumn); - } - - /** - * The regatta rank column shall be displayed if an only if the {@link DetailType#REGATTA_RANK} detail is selected in the - * {@link #selectedOverallDetailColumns}. It will then be displayed after the selection checkbox column (if any) and - * before the sail number / competitor name columns. - */ - private boolean isShowRegattaRankColumn() { - return selectedOverallDetailColumns.contains(DetailType.REGATTA_RANK); - } - - /** - * @param rankColumnIndex - * the column index (0-based) where to put the rank column, if needed - * @return the column index (0-based) for the next column; will equal rankColumnIndex if the rank - * column is not {@link #isShowRegattaRankColumn() supposed to be shown}, or one greater otherwise. - */ - private int ensureRankColumn(int rankColumnIndex) { - final int indexOfNextColumn = rankColumnIndex + (isShowRegattaRankColumn() ? 1 : 0); - if (getLeaderboardTable().getColumnCount() > rankColumnIndex) { - if (isShowRegattaRankColumn()) { - if (getLeaderboardTable().getColumn(rankColumnIndex) != getRankColumn()) { - insertColumn(rankColumnIndex, getRankColumn()); - } // else, the column is needed and is already in place - } else { - if (getLeaderboardTable().getColumn(rankColumnIndex) == getRankColumn()) { - removeColumn(rankColumnIndex); - } - } - } else { - if (isShowRegattaRankColumn()) { - insertColumn(rankColumnIndex, getRankColumn()); - } - } - return indexOfNextColumn; - } - - /** - * @param selectionCheckboxColumnIndex - * the column index (0-based) where to put the selection checkbox column, if needed - * @return the column index (0-based) for the next column; will equal selectionCheckboxColumnIndex if - * the selection checkbox column is not {@link #showSelectionCheckbox supposed to be shown}, or one greater - * otherwise. - */ - private int ensureSelectionCheckboxColumn(int selectionCheckboxColumnIndex) { - final int indexOfNextColumn = selectionCheckboxColumnIndex + (showSelectionCheckbox ? 1 : 0); - if (getLeaderboardTable().getColumnCount() > selectionCheckboxColumnIndex) { - if (showSelectionCheckbox) { - if (getLeaderboardTable().getColumn(selectionCheckboxColumnIndex) != selectionCheckboxColumn) { - insertColumn(selectionCheckboxColumnIndex, selectionCheckboxColumn); - } // else, the column is needed and is already in place - } else { - if (getLeaderboardTable().getColumn(selectionCheckboxColumnIndex) == selectionCheckboxColumn) { - removeColumn(selectionCheckboxColumnIndex); - } - } - } else { - if (showSelectionCheckbox) { - insertColumn(selectionCheckboxColumnIndex, selectionCheckboxColumn); - } - } - return indexOfNextColumn; - } - - /** - * @return the 0-based index for the next column - */ - private int ensureSailIDAndCompetitorColumn(int columnIndexWhereToInsertTheNextColumn) { - if (isShowCompetitorSailId()) { - if (getLeaderboardTable().getColumnCount() <= columnIndexWhereToInsertTheNextColumn - || !(getLeaderboardTable().getColumn(columnIndexWhereToInsertTheNextColumn) instanceof SailIDColumn)) { - insertColumn(columnIndexWhereToInsertTheNextColumn, new SailIDColumn(new CompetitorFetcher() { - @Override - public CompetitorDTO getCompetitor(LeaderboardRowDTO t) { - return t.competitor; - } - })); - } - columnIndexWhereToInsertTheNextColumn++; - } else { - if (getLeaderboardTable().getColumnCount() > columnIndexWhereToInsertTheNextColumn - && getLeaderboardTable().getColumn(columnIndexWhereToInsertTheNextColumn) instanceof SailIDColumn) { - removeColumn(columnIndexWhereToInsertTheNextColumn); - } - } - if (isShowCompetitorFullName()) { - if (getLeaderboardTable().getColumnCount() <= columnIndexWhereToInsertTheNextColumn - || !(getLeaderboardTable().getColumn(columnIndexWhereToInsertTheNextColumn) instanceof CompetitorColumn)) { - insertColumn(columnIndexWhereToInsertTheNextColumn, createCompetitorColumn()); - } - columnIndexWhereToInsertTheNextColumn++; - } else { - if (getLeaderboardTable().getColumnCount() > columnIndexWhereToInsertTheNextColumn - && getLeaderboardTable().getColumn(columnIndexWhereToInsertTheNextColumn) instanceof CompetitorColumn) { - removeColumn(columnIndexWhereToInsertTheNextColumn); - } - } - return columnIndexWhereToInsertTheNextColumn; - } - - protected CompetitorColumn createCompetitorColumn() { - return new CompetitorColumn(new CompetitorColumnBase(this, getStringMessages(), - new CompetitorFetcher() { - @Override - public CompetitorDTO getCompetitor(LeaderboardRowDTO t) { - return t.competitor; - } - })); - } - - private void ensureTotalsColumn() { - // add a totals column on the right - if (getLeaderboardTable().getColumnCount() == 0 - || !(getLeaderboardTable().getColumn(getLeaderboardTable().getColumnCount() - 1) instanceof TotalsColumn)) { - addColumn(new TotalsColumn(TOTAL_COLUMN_STYLE)); - } - } - - private void ensureTotalRacesSailedColumn() { - // add a totals column on the right - if (getLeaderboardTable().getColumnCount() == 0 - || !(getLeaderboardTable().getColumn(getLeaderboardTable().getColumnCount() - 2) instanceof TotalRacesCompletedColumn)) { - insertColumn(getLeaderboardTable().getColumnCount() - 1, new TotalRacesCompletedColumn(TOTAL_COLUMN_STYLE)); - } - } - - private void ensureNoTotalRacesSailedColumn() { - if ((getLeaderboardTable().getColumn(getLeaderboardTable().getColumnCount() - 2) instanceof TotalRacesCompletedColumn)) { - removeColumn(getLeaderboardTable().getColumnCount() - 2); - } - } - - protected void updateTotalRacesSailedColumn() { - final boolean showTotalRacesCompletedColumn = isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(); - if (showTotalRacesCompletedColumn) { - ensureTotalRacesSailedColumn(); - } else { - ensureNoTotalRacesSailedColumn(); - } - } - - /** - * If the leaderboard {@link LeaderboardDTO#hasCarriedPoints has carried points} and if column #1 - * (second column, right of the competitor column) does not exist or is not of type {@link CarryColumn}, all columns - * starting from #1 will be removed and a {@link CarryColumn} will be added. If the leaderboard has no carried - * points but the display still shows a carry column, the column is removed. - * - * @param zeroBasedIndexOfCarryColumn - * the 0-based column index where to put the carry column, if any - * @return the 0-based index of the next column following this column if the carry column is to be shown, or - * zeroBasedIndexOfCarryColumn otherwise - */ - protected int updateCarryColumn(LeaderboardDTO leaderboard, int zeroBasedIndexOfCarryColumn) { - final boolean needsCarryColumn = leaderboard != null && leaderboard.hasCarriedPoints; - if (needsCarryColumn) { - ensureCarryColumn(zeroBasedIndexOfCarryColumn); - } else { - ensureNoCarryColumn(zeroBasedIndexOfCarryColumn); - } - return needsCarryColumn ? zeroBasedIndexOfCarryColumn+1 : zeroBasedIndexOfCarryColumn; - } - - private void ensureNoCarryColumn(int zeroBasedIndexOfCarryColumn) { - if (getLeaderboardTable().getColumnCount() > zeroBasedIndexOfCarryColumn - && getLeaderboardTable().getColumn(zeroBasedIndexOfCarryColumn) instanceof CarryColumn) { - removeColumn(zeroBasedIndexOfCarryColumn); - } - } - - protected void ensureCarryColumn(int zeroBasedIndexOfCarryColumn) { - if (getLeaderboardTable().getColumnCount() <= zeroBasedIndexOfCarryColumn - || !(getLeaderboardTable().getColumn(zeroBasedIndexOfCarryColumn) instanceof CarryColumn)) { - while (getLeaderboardTable().getColumnCount() > zeroBasedIndexOfCarryColumn) { - removeColumn(zeroBasedIndexOfCarryColumn); - } - addColumn(createCarryColumn()); - } - } - - protected CarryColumn createCarryColumn() { - return new CarryColumn(); - } - - public SortedCellTable getLeaderboardTable() { - return leaderboardTable; - } - - protected SailingServiceAsync getSailingService() { - return sailingService; - } - - protected String getLeaderboardName() { - return leaderboardName; - } - - protected void setLeaderboardName(String leaderboardName) { - this.leaderboardName = leaderboardName; - } - - protected ErrorReporter getErrorReporter() { - return errorReporter; - } - - protected ListDataProvider getData() { - return getLeaderboardTable().getDataProvider(); - } - - /** - * @param newTime - * ignored; may be null. The time for loading the leaderboard is determined using - * {@link #getLeaderboardDisplayDate()}. - */ - @Override - public void timeChanged(Date newTime, Date oldTime) { - loadCompleteLeaderboard(getLeaderboardDisplayDate()); - } - - @Override - public void playStateChanged(PlayStates playState, PlayModes playMode) { - currentlyHandlingPlayStateChange = true; - - if(!isEmbedded) { - playPause.setHTML(getPlayPauseImgHtml(playState)); - playPause.setTitle(playState == PlayStates.Playing ? stringMessages.pauseAutomaticRefresh() : stringMessages.autoRefresh()); - } - if (!settingsUpdatedExplicitly && playMode != oldPlayMode) { - // if settings weren't explicitly modified, auto-switch to live mode settings and sort for - // any pre-selected race; we need to copy the previously selected race columns to the new RaceColumnSelection - updateSettings(LeaderboardSettingsFactory.getInstance().createNewSettingsForPlayMode( - playMode, - /* don't touch columnToSort if no race was pre-selected */ preSelectedRace == null ? null - : preSelectedRace.getRaceName(), - /* don't change nameOfRaceColumnToShow */null, - /* set nameOfRaceToShow if race was pre-selected */preSelectedRace == null ? null : preSelectedRace - .getRaceName(), getRaceColumnSelection(), /* leave showRegattaRank and overall details unchanged */ null, - /* take into account state of competitor columns*/isShowCompetitorSailId(), isShowCompetitorFullName())); - } - currentlyHandlingPlayStateChange = false; - oldPlayMode = playMode; - } - - @Override - public void playSpeedFactorChanged(double newPlaySpeedFactor) { - // nothing to do - } - - private List getTrackedRacesIdentifiers() { - List result = new ArrayList(); - for (RaceColumnDTO raceColumn : getLeaderboard().getRaceList()) { - for (FleetDTO fleet : raceColumn.getFleets()) { - if (raceColumn.getRaceIdentifier(fleet) != null) { - result.add(raceColumn.getRaceIdentifier(fleet)); - } - } - } - return result; - } - - @Override - public boolean hasSettings() { - return true; - } - - @Override - public boolean hasToolbar() { - return false; - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return new LeaderboardSettingsDialogComponent(Collections.unmodifiableList(selectedManeuverDetails), - Collections.unmodifiableList(selectedLegDetails), Collections.unmodifiableList(selectedRaceDetails), - Collections.unmodifiableList(selectedOverallDetailColumns), /* All races to select */ leaderboard.getRaceList(), - raceColumnSelection.getSelectedRaceColumnsOrderedAsInLeaderboard(leaderboard), raceColumnSelection, autoExpandPreSelectedRace, - isShowAddedScores(), timer.getRefreshInterval(), isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(), - isShowCompetitorSailId(), isShowCompetitorFullName(), stringMessages); - } - - @Override - public String getLocalizedShortName() { - return stringMessages.leaderboard(); - } - - private LeaderboardRowDTO getRow(CompetitorDTO competitor) { - synchronized (getData().getList()) { - for (LeaderboardRowDTO row : getData().getList()) { - if (row.competitor.equals(competitor)) { - return row; - } - } - } - return null; - } - - @Override - public void addedToSelection(CompetitorDTO competitor) { - LeaderboardRowDTO row = getRow(competitor); - if (row != null) { - leaderboardSelectionModel.setSelected(row, true); - } - } - - @Override - public void removedFromSelection(CompetitorDTO competitor) { - LeaderboardRowDTO row = getRow(competitor); - if (row != null) { - leaderboardSelectionModel.setSelected(row, false); - } - } - - @Override - public BusyIndicator getBusyIndicator() { - return busyIndicator; - } - - @Override - public boolean hasBusyIndicator() { - return true; - } - - private Iterable getSelectedRows() { - return leaderboardSelectionModel.getSelectedSet(); - } - - @Override - public void competitorsListChanged(Iterable competitors) { - setFilterControlStatus(); - if (timer.isInitialized()) { - timeChanged(timer.getTime(), null); - } - } - - @Override - public void filteredCompetitorsListChanged(Iterable filteredCompetitors) { - setFilterControlStatus(); - updateLeaderboard(getLeaderboard()); - } - - @Override - public void filterChanged(FilterSet> oldFilterSet, - FilterSet> newFilterSet) { - // nothing to do; if the list of filtered competitors has changed, a separate call to filteredCompetitorsListChanged will occur - setFilterControlStatus(); - } - - public RaceColumnSelection getRaceColumnSelection() { - return raceColumnSelection; - } - - public void removeAllListeners() { - if (raceTimesInfoProviderListener != null) { - getRaceTimesInfoProvider().removeRaceTimesInfoProviderListener(raceTimesInfoProviderListener); - } - if (raceColumnSelection != null && raceColumnSelection.getType() == RaceColumnSelectionStrategies.LAST_N) { - getRaceTimesInfoProvider().removeRaceTimesInfoProviderListener( - (LastNRacesColumnSelection) raceColumnSelection); - } - if (timer != null) { - timer.removeTimeListener(this); - } - if(leaderboardUpdateListener != null) { - leaderboardUpdateListener.clear(); - } - } - - protected Timer getTimer() { - return timer; - } - - protected void blurFocusedElementAfterSelectionChange() { - // now "blur" the selected leaderboard element because it seems to cause the cell table to scroll to its top; - // see bug 2093. - blur(); - final ScheduledCommand blurCommand = new ScheduledCommand() { - @Override - public void execute() { - blur(); - } - }; - Scheduler.get().scheduleDeferred(blurCommand); - } - - private void blur() { - if (elementToBlur != null) { - elementToBlur.blur(); - } - } - - @Override - public void setVisible(boolean visible) { - super.setVisible(visible); - if (visible) { - timeChanged(getLeaderboardDisplayDate(), null); - } - } - - public void addLeaderboardUpdateListener(LeaderboardUpdateListener listener) { - this.leaderboardUpdateListener.add(listener); - } - - protected void informLeaderboardUpdateListenersAboutLeaderboardUpdated(LeaderboardDTO leaderboard) { - for (LeaderboardUpdateListener listener : this.leaderboardUpdateListener) { - listener.updatedLeaderboard(leaderboard); - } - } - - protected void informLeaderboardUpdateListenersAboutRaceSelected(RaceIdentifier raceIdentifier, RaceColumnDTO raceColumn) { - for (LeaderboardUpdateListener listener : this.leaderboardUpdateListener) { - listener.currentRaceSelected(raceIdentifier, raceColumn); - } - } - - public boolean hasLiveRace() { - return getLeaderboard().hasLiveRace(timer.getLiveTimePointInMillis()); - } - - public String getLiveRacesText() { - String result = ""; - List> liveRaces = leaderboard.getLiveRaces(timer.getLiveTimePointInMillis()); - if (!liveRaces.isEmpty()) { - if(liveRaces.size() == 1) { - com.sap.sse.common.Util.Pair liveRace = liveRaces.get(0); - result = stringMessages.raceIsLive("'" + liveRace.getA().getRaceColumnName() + "'"); - } else { - String raceNames = ""; - for (com.sap.sse.common.Util.Pair liveRace : liveRaces) { - raceNames += "'" + liveRace.getA().getRaceColumnName() + "', "; - } - // remove last ", " - raceNames = raceNames.substring(0, raceNames.length() - 2); - result = stringMessages.racesAreLive(raceNames); - } - } - return result; - } - - @Override - public String getDependentCssClassName() { - return "leaderboard"; - } -} +package com.sap.sailing.gwt.ui.leaderboard; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.google.gwt.cell.client.AbstractSafeHtmlCell; +import com.google.gwt.cell.client.Cell; +import com.google.gwt.cell.client.Cell.Context; +import com.google.gwt.cell.client.CompositeCell; +import com.google.gwt.cell.client.TextCell; +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.dom.client.BrowserEvents; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.FontWeight; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.i18n.client.NumberFormat; +import com.google.gwt.resources.client.ImageResource; +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.text.shared.AbstractSafeHtmlRenderer; +import com.google.gwt.user.cellview.client.Column; +import com.google.gwt.user.cellview.client.Header; +import com.google.gwt.user.cellview.client.SafeHtmlHeader; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.AbstractImagePrototype; +import com.google.gwt.user.client.ui.Anchor; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.DockPanel; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HasHorizontalAlignment; +import com.google.gwt.user.client.ui.HasVerticalAlignment; +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.user.client.ui.Widget; +import com.google.gwt.view.client.CellPreviewEvent; +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.DetailType; +import com.sap.sailing.domain.common.InvertibleComparator; +import com.sap.sailing.domain.common.MaxPointsReason; +import com.sap.sailing.domain.common.Mile; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.SortingOrder; +import com.sap.sailing.domain.common.Tack; +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.IncrementalOrFullLeaderboardDTO; +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.impl.InvertibleComparatorAdapter; +import com.sap.sailing.gwt.ui.actions.GetLeaderboardByNameAction; +import com.sap.sailing.gwt.ui.client.Collator; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionChangeListener; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; +import com.sap.sailing.gwt.ui.client.FlagImageResolver; +import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; +import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; +import com.sap.sailing.gwt.ui.client.RaceTimesInfoProviderListener; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.shared.controls.AbstractSortableColumnWithMinMax; +import com.sap.sailing.gwt.ui.client.shared.controls.SelectionCheckboxColumn; +import com.sap.sailing.gwt.ui.client.shared.filter.CompetitorRaceRankFilter; +import com.sap.sailing.gwt.ui.client.shared.filter.FilterWithUI; +import com.sap.sailing.gwt.ui.client.shared.filter.LeaderboardFetcher; +import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; +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.common.Util; +import com.sap.sse.common.filter.BinaryOperator; +import com.sap.sse.common.filter.Filter; +import com.sap.sse.common.filter.FilterSet; +import com.sap.sse.gwt.client.ErrorReporter; +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.BusyStateChangeListener; +import com.sap.sse.gwt.client.controls.busyindicator.BusyStateProvider; +import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; +import com.sap.sse.gwt.client.player.PlayStateListener; +import com.sap.sse.gwt.client.player.TimeListener; +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.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.ComponentResources; +import com.sap.sse.gwt.client.shared.components.IsEmbeddableComponent; +import com.sap.sse.gwt.client.shared.components.SettingsDialog; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; +import com.sap.sse.gwt.client.useragent.UserAgentDetails; + +/** + * A leaderboard essentially consists of a table widget that in its columns displays the entries. + * + * @author Axel Uhl (D043530) + * + */ +public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayStateListener, DisplayedLeaderboardRowsProvider, + Component, IsEmbeddableComponent, CompetitorSelectionChangeListener, LeaderboardFetcher, + BusyStateProvider { + public static final String LOAD_LEADERBOARD_DATA_CATEGORY = "loadLeaderboardData"; + + protected static final NumberFormat scoreFormat = NumberFormat.getFormat("0.##"); + + private final SailingServiceAsync sailingService; + + private static String IS_LIVE_TEXT_COLOR = "#FF0000"; + private static String DEFAULT_TEXT_COLOR = "#000000"; + + private static final String STYLE_LEADERBOARD_CONTENT = "leaderboardContent"; + private static final String STYLE_LEADERBOARD_INFO = "leaderboardInfo"; + private static final String STYLE_LEADERBOARD_TOOLBAR = "leaderboardContent-toolbar"; + private static final String STYLE_LEADERBOARD_LIVE_RACE = "leaderboardContent-liverace"; + + interface RaceColumnTemplates extends SafeHtmlTemplates { + @SafeHtmlTemplates.Template("

") + SafeHtml cellFrameWithTextColorAndFleetBorder(String textColor, String borderStyle); + + @SafeHtmlTemplates.Template("
") + SafeHtml cellFrameWithTextColor(String textColor); + + @SafeHtmlTemplates.Template("
") + SafeHtml cellWithImageResourceAndText(ImageResource image, String text); + } + + private static RaceColumnTemplates raceColumnTemplate = GWT.create(RaceColumnTemplates.class); + + /** + * The leaderboard name is used to + * {@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; + + private final ErrorReporter errorReporter; + + private final StringMessages stringMessages; + + private final SortedCellTable leaderboardTable; + + private final MultiSelectionModel leaderboardSelectionModel; + + private LeaderboardDTO leaderboard; + + private final TotalRankColumn totalRankColumn; + + private final SelectionCheckboxColumn selectionCheckboxColumn; + + /** + * Passed to the {@link ManeuverCountRaceColumn}. Modifications to this list will modify the column's children list + * when updated the next time. + */ + private final List selectedManeuverDetails; + + /** + * Passed to the {@link LegColumn}. Modifications to this list will modify the column's children list when updated + * the next time. + */ + private final List selectedLegDetails; + + /** + * Passed to the {@link TextRaceColumn}. Modifications to this list will modify the column's children list when + * updated the next time. + */ + private final List selectedRaceDetails; + + private final List selectedOverallDetailColumns; + + private final Map> overallDetailColumnMap; + + private RaceColumnSelection raceColumnSelection; + + protected final String RACE_COLUMN_HEADER_STYLE; + + protected final String LEG_DETAIL_COLUMN_HEADER_STYLE; + + protected final String LEG_DETAIL_COLUMN_STYLE; + + protected final String LEG_COLUMN_HEADER_STYLE; + + protected final String RACE_COLUMN_STYLE; + + protected final String LEG_COLUMN_STYLE; + + protected final String TOTAL_COLUMN_STYLE; + + private final Timer timer; + + /** + * A {@link LeaderboardDTO} tells something about the live delay through its {@link LeaderboardDTO#getDelayToLiveInMillisForLatestRace()} method. + * If this flag is true, the live delay in the {@link #timer} will be adjusted each time the {@link #updateLeaderboard(LeaderboardDTO)} + * method is invoked. Otherwise, the timer's delay will be left alone which is helpful, e.g., if the leaderboard panel is embedded + * in a race board panel that focuses on one particular race which may not be the same as the one controlling the leaderboard's overall + * live delay. + */ + private final boolean adjustTimerDelay; + + private boolean autoExpandPreSelectedRace; + + private boolean autoExpandLastRaceColumn; + + /** + * When true, the race columns don't display the competitors' scores in the race represented by the column + * but the cumulative score up to that race. + */ + private boolean showAddedScores; + + private boolean showCompetitorSailId; + private boolean showCompetitorFullName; + + /** + * When true then an additional column just before the overall points is displayed that sums up + * the number of races sailed per competitor. + */ + private boolean showOverallColumnWithNumberOfRacesCompletedPerCompetitor; + + /** + * Remembers whether the auto-expand of the pre-selected race (see {@link #autoExpandPreSelectedRace}) or last + * selected race {@link #autoExpandLastRaceColumn} has been performed once. It must not be performed another time. + */ + private boolean autoExpandPerformedOnce; + + /** + * This anchor's HTML holds the image tag for the play/pause button that needs to be updated when the {@link #timer} + * changes its playing state + */ + private Anchor playPause; + + private final CompetitorSelectionProvider competitorSelectionProvider; + private final HorizontalPanel filterControlPanel; + private Label filterStatusLabel; + private Button filterClearButton; + + /** + * The handler for changes in the leaderboard table's selection; its registration is kept in + * {@link #leaderboardAsTableSelectionModelRegistration} while it is registered as a selection handler + * on the {@link #leaderboardTable}. + */ + private final Handler selectionChangeHandler; + + /** + * While the {@link #selectionChangeHandler} is registered as a selection change handler on the + * {@link #leaderboardTable}'s selection model, this field holds the registration which can be used to + * remove the registration again. We'll use this to temporarily suspend selection events when actively + * modifying / adjusting the table selection to match the {@link #competitorSelectionProvider}. + */ + private HandlerRegistration leaderboardAsTableSelectionModelRegistration; + + /** + * If this is null, all leaderboard columns added by updating the leaderboard from the server are + * automatically added to the table. Otherwise, only the column whose + * {@link RaceColumnDTO#getRaceIdentifier(String) race identifier} matches the value of this attribute will be + * added. + */ + private final RegattaAndRaceIdentifier preSelectedRace; + + private final VerticalPanel contentPanel; + + private HorizontalPanel refreshAndSettingsPanel; + private Label scoreCorrectionLastUpdateTimeLabel; + private Label scoreCorrectionCommentLabel; + private Label liveRaceLabel; + + private boolean isEmbedded; + + private static final LeaderboardResources resources = GWT.create(LeaderboardResources.class); + private static final ComponentResources componentResources = GWT.create(ComponentResources.class); + private static final LeaderboardTableResources tableResources = GWT.create(LeaderboardTableResources.class); + + private ImageResource pauseIcon; + private ImageResource playIcon; + + private final BusyIndicator busyIndicator; + private final Set busyStateChangeListeners; + + /** + * Tells whether the leaderboard settings were explicitly changed by an external call to + * {@link #updateSettings(LeaderboardSettings)}. If so, a {@link #playStateChanged(PlayStates, PlayModes) play state + * change} will not automatically lead to a settings change. + */ + private boolean settingsUpdatedExplicitly = false; + + /** + * Tells if the leaderboard is currently handling a {@link #playStateChanged(PlayStates, PlayModes) play state + * change}. If this is the case, a call to {@link #updateSettings(LeaderboardSettings)} won't set the + * {@link #settingsUpdatedExplicitly} flag. + */ + private boolean currentlyHandlingPlayStateChange; + + private PlayModes oldPlayMode; + + private final AsyncActionsExecutor asyncActionsExecutor; + + /** + * See also {@link #getDefaultSortColumn()}. If no other column is explicitly selected for sorting and this + * attribute holds a non-null string identifying a valid race by name that is represented in this + * leaderboard panel then sort by it. Otherwise, default sorting will default to the overall rank column. + */ + private String raceNameForDefaultSorting; + + /** + * Can be used to disallow users to drill into the race details. + */ + private final boolean showRaceDetails; + + /** + * The {@link LastNRacesColumnSelection} column selection strategy requires a {@link RaceTimesInfoProvider}. This can either be injected + * by passing a non-null object of that type to the constructor, or such an object is created and remembered in this + * attribute when required the first time. + */ + private RaceTimesInfoProvider raceTimesInfoProvider; + private RaceTimesInfoProviderListener raceTimesInfoProviderListener; + + private int blurInOnSelectionChanged; + + /** + * When an element in the leaderboard receives focus, it needs to be blurred again to keep the surrounding scroll panel + * from scrolling anything into view + */ + private Element elementToBlur; + private boolean showSelectionCheckbox; + + private final List leaderboardUpdateListener; + + private boolean initialCompetitorFilterHasBeenApplied = false; + private final boolean showCompetitorFilterStatus; + + private CompetitorFilterPanel competitorFilterPanel; + + protected StringMessages getStringMessages() { + return stringMessages; + } + + @Override + public Widget getEntryWidget() { + return this; + } + + @Override + public Widget getHeaderWidget() { + return null; + } + + @Override + public Widget getContentWidget() { + return contentPanel; + } + + @Override + public Widget getToolbarWidget() { + return null; + } + + @Override + public Widget getLegendWidget() { + return null; + } + + @Override + public boolean isEmbedded() { + return isEmbedded; + } + + public VerticalPanel getContentPanel() { + return contentPanel; + } + + protected ImageResource getSettingsIcon() { + return componentResources.settingsIcon(); + } + + public void updateSettings(final LeaderboardSettings newSettings) { + boolean oldShallAddOverallDetails = shallAddOverallDetails(); + if (newSettings.getOverallDetailsToShow() != null) { + selectedOverallDetailColumns.clear(); + selectedOverallDetailColumns.addAll(newSettings.getOverallDetailsToShow()); + } + if (!newSettings.isUpdateUponPlayStateChange() || !currentlyHandlingPlayStateChange) { + settingsUpdatedExplicitly = true; + } + setShowAddedScores(newSettings.isShowAddedScores()); + setShowCompetitorSailId(newSettings.isShowCompetitorSailIdColumn()); + setShowCompetitorFullName(newSettings.isShowCompetitorFullNameColumn()); + setShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(newSettings.isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor()); + final List> columnsToExpandAgain = new ArrayList>(); + for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { + Column c = getLeaderboardTable().getColumn(i); + if (c instanceof ExpandableSortableColumn) { + ExpandableSortableColumn expandableSortableColumn = (ExpandableSortableColumn) c; + if (expandableSortableColumn.isExpanded()) { + // now toggle expansion back and forth, + // enforcing a re-build of the visible + // child columns + expandableSortableColumn.toggleExpansion(); + columnsToExpandAgain.add(expandableSortableColumn); + } + } + } + if (newSettings.getManeuverDetailsToShow() != null) { + selectedManeuverDetails.clear(); + selectedManeuverDetails.addAll(newSettings.getManeuverDetailsToShow()); + } + if (newSettings.getLegDetailsToShow() != null) { + selectedLegDetails.clear(); + selectedLegDetails.addAll(newSettings.getLegDetailsToShow()); + } + if (newSettings.getRaceDetailsToShow() != null) { + selectedRaceDetails.clear(); + selectedRaceDetails.addAll(newSettings.getRaceDetailsToShow()); + } + // update strategy for determining the race columns to show; if settings' race columns to show is null, use the + // previously selected race columns / number of race columns for the new configuration + Iterable oldNamesOfRaceColumnsToShow = null; + if (newSettings.getNamesOfRaceColumnsToShow() == null) { + oldNamesOfRaceColumnsToShow = raceColumnSelection.getSelectedRaceColumnNames(); + } + switch (newSettings.getActiveRaceColumnSelectionStrategy()) { + case EXPLICIT: + if (preSelectedRace == null) { + raceColumnSelection = new ExplicitRaceColumnSelection(); + } else { + raceColumnSelection = new ExplicitRaceColumnSelectionWithPreselectedRace(preSelectedRace); + } + if (newSettings.getNamesOfRaceColumnsToShow() != null) { + raceColumnSelection.requestClear(); + for (String nameOfRaceColumnToShow : newSettings.getNamesOfRaceColumnsToShow()) { + RaceColumnDTO raceColumnToShow = getRaceByColumnName(nameOfRaceColumnToShow); + if (raceColumnToShow != null) { + raceColumnSelection.requestRaceColumnSelection(raceColumnToShow); + } + } + } else { + // apply the old column selections again + for (String oldNameOfRaceColumnToShow : oldNamesOfRaceColumnsToShow) { + final RaceColumnDTO raceColumnByName = getLeaderboard().getRaceColumnByName(oldNameOfRaceColumnToShow); + if (raceColumnByName != null) { + raceColumnSelection.requestRaceColumnSelection(raceColumnByName); + } + } + if (newSettings.getNamesOfRacesToShow() != null) { + raceColumnSelection.requestClear(); + for (String nameOfRaceToShow : newSettings.getNamesOfRacesToShow()) { + RaceColumnDTO raceColumnToShow = getRaceByName(nameOfRaceToShow); + if (raceColumnToShow != null) { + raceColumnSelection.requestRaceColumnSelection(raceColumnToShow); + } + } + } + } + break; + case LAST_N: + setRaceColumnSelectionToLastNStrategy(newSettings.getNumberOfLastRacesToShow()); + break; + } + + final boolean oldBusyState = isBusy(); + setBusyState(true); + Runnable doWhenNecessaryDetailHasBeenLoaded = new Runnable() { + @Override + public void run() { + setAutoExpandPreSelectedRace(false); // avoid expansion during updateLeaderboard(...); will expand later + // if it was expanded before + // update leaderboard after settings panel column selection change + updateLeaderboard(leaderboard); + setAutoExpandPreSelectedRace(newSettings.isAutoExpandPreSelectedRace()); + + if (newSettings.getDelayBetweenAutoAdvancesInMilliseconds() != null) { + timer.setRefreshInterval(newSettings.getDelayBetweenAutoAdvancesInMilliseconds()); + } + for (ExpandableSortableColumn expandableSortableColumn : columnsToExpandAgain) { + expandableSortableColumn.toggleExpansion(); + } + if (newSettings.getNameOfRaceToSort() != null) { + final RaceColumn raceColumnByRaceName = getRaceColumnByRaceName(newSettings + .getNameOfRaceToSort()); + if (raceColumnByRaceName != null) { + getLeaderboardTable().sortColumn(raceColumnByRaceName, /* ascending */true); + } + } + setBusyState(oldBusyState); + } + }; + if (oldShallAddOverallDetails == shallAddOverallDetails() || oldShallAddOverallDetails || getLeaderboard().hasOverallDetails()) { + doWhenNecessaryDetailHasBeenLoaded.run(); + } else { // meaning that now the details need to be loaded from the server + updateLeaderboardAndRun(doWhenNecessaryDetailHasBeenLoaded); + } + } + + /** + * @param callWhenExpansionDataIsLoaded + */ + private void updateLeaderboardAndRun(final Runnable callWhenExpansionDataIsLoaded) { + final LeaderboardDTO previousLeaderboard = getLeaderboard(); + getSailingService().getLeaderboardByName(getLeaderboardName(), + timer.getPlayMode() == PlayModes.Live ? null : getLeaderboardDisplayDate(), + /* namesOfRacesForWhichToLoadLegDetails */getNamesOfExpandedRaces(), + shallAddOverallDetails(), previousLeaderboard.getId(), /* fillNetPointsUncorrected */ false, + 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 */); + } + })); + } + + public void setRaceColumnSelectionToLastNStrategy(final Integer numberOfLastRacesToShow) { + raceColumnSelection = new LastNRacesColumnSelection(numberOfLastRacesToShow, getRaceTimesInfoProvider()); + if (timer.getPlayState() != Timer.PlayStates.Playing) { + // wait for the first update and adjust leaderboard once the race times have been received + raceTimesInfoProviderListener = new RaceTimesInfoProviderListener() { + @Override + public void raceTimesInfosReceived(Map raceTimesInfo, long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) { + // remove + timer.adjustClientServerOffset(clientTimeWhenRequestWasSent, serverTimeDuringRequest, clientTimeWhenResponseWasReceived); + // remove the listener only in case a leaderboard has already been loaded + if(getLeaderboard() != null) { + updateLeaderboard(getLeaderboard()); + getRaceTimesInfoProvider().removeRaceTimesInfoProviderListener(raceTimesInfoProviderListener); + raceTimesInfoProviderListener = null; + } + } + }; + getRaceTimesInfoProvider().addRaceTimesInfoProviderListener(raceTimesInfoProviderListener); + } + } + + /** + * A leaderboard panel may have been provided with a valid {@link RaceTimesInfoProvider} upon creation; in this case, that object + * will be returned. If none was provided to the constructor, one is created and remembered if no previously created/remembered + * object exists.

+ * + * Precondition: {@link #timer} is not null + */ + private RaceTimesInfoProvider getRaceTimesInfoProvider() { + if (raceTimesInfoProvider == null) { + final List trackedRacesIdentifiers; + if (leaderboard != null && getTrackedRacesIdentifiers() != null) { + trackedRacesIdentifiers = getTrackedRacesIdentifiers(); + } else { + trackedRacesIdentifiers = Collections.emptyList(); + } + raceTimesInfoProvider = new RaceTimesInfoProvider(getSailingService(), asyncActionsExecutor, errorReporter, + trackedRacesIdentifiers, timer.getRefreshInterval()); + } + return raceTimesInfoProvider; + } + + protected class CompetitorColumn extends LeaderboardSortableColumnWithMinMax { + private final CompetitorColumnBase base; + + protected CompetitorColumn(CompetitorColumnBase base) { + super(base.getCell(getLeaderboard()), SortingOrder.ASCENDING, LeaderboardPanel.this); + this.base = base; + } + + public CompetitorColumn(CompositeCell compositeCell, CompetitorColumnBase base) { + super(compositeCell, SortingOrder.ASCENDING, LeaderboardPanel.this); + this.base = base; + } + + @Override + public InvertibleComparator getComparator() { + return new InvertibleComparatorAdapter() { + @Override + public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { + return Collator.getInstance().compare(getLeaderboard().getDisplayName(o1.competitor), + getLeaderboard().getDisplayName(o2.competitor)); + } + }; + } + + @Override + public SafeHtmlHeader getHeader() { + return base.getHeader(); + } + + @Override + public LeaderboardRowDTO getValue(LeaderboardRowDTO object) { + return object; + } + + protected void defaultRender(Context context, LeaderboardRowDTO object, SafeHtmlBuilder sb) { + super.render(context, object, sb); + } + + @Override + public void render(Context context, LeaderboardRowDTO object, SafeHtmlBuilder sb) { + String competitorColor = LeaderboardPanel.this.competitorSelectionProvider.getColor(object.competitor, + LeaderboardPanel.this.preSelectedRace).getAsHtml(); + String competitorColorBarStyle; + if (LeaderboardPanel.this.isEmbedded && preSelectedRace != null) { + competitorColorBarStyle = "style=\"border-bottom: 2px solid " + competitorColor + ";\""; + } else { + competitorColorBarStyle = "style=\"border: none;\""; + } + base.render(object, competitorColorBarStyle, sb); + } + } + + /** + * Shows the country flag and sail ID, if present + * + * @author Axel Uhl (d043530) + * + */ + private class SailIDColumn extends LeaderboardSortableColumnWithMinMax { + private final CompetitorFetcher competitorFetcher; + + protected SailIDColumn(CompetitorFetcher competitorFetcher) { + super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); + this.competitorFetcher = competitorFetcher; + } + + @Override + public InvertibleComparator getComparator() { + return new InvertibleComparatorAdapter() { + @Override + public int compare(T o1, T o2) { + return competitorFetcher.getCompetitor(o1).getSailID() == null ? competitorFetcher.getCompetitor(o2).getSailID() == null ? 0 : -1 + : competitorFetcher.getCompetitor(o2).getSailID() == null ? 1 : Collator.getInstance().compare( + competitorFetcher.getCompetitor(o1).getSailID(), competitorFetcher.getCompetitor(o2).getSailID()); + } + }; + } + + @Override + public SafeHtmlHeader getHeader() { + return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.competitor()), stringMessages.sailIdColumnTooltip()); + } + + @Override + public void render(Context context, T object, SafeHtmlBuilder sb) { + ImageResourceRenderer renderer = new ImageResourceRenderer(); + CompetitorDTO competitor = competitorFetcher.getCompetitor(object); + final String twoLetterIsoCountryCode = competitor.getTwoLetterIsoCountryCode(); + final String flagImageURL = competitor.getFlagImageURL(); + + boolean showBoatColor = !isShowCompetitorFullName() && LeaderboardPanel.this.isEmbedded && preSelectedRace != null; + if (showBoatColor) { + String competitorColor = LeaderboardPanel.this.competitorSelectionProvider.getColor( + competitorFetcher.getCompetitor(object), LeaderboardPanel.this.preSelectedRace).getAsHtml(); + sb.appendHtmlConstant("

"); + } + + if (flagImageURL != null && !flagImageURL.isEmpty()) { + sb.appendHtmlConstant(""); + sb.appendHtmlConstant(" "); + } else { + 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()); + if (showBoatColor) { + sb.appendHtmlConstant("
"); + } + } + + @Override + public String getValue(T object) { + return competitorFetcher.getCompetitor(object).getSailID(); + } + } + + /** + * Displays net/total points and possible max-points reasons based on a {@link LeaderboardRowDTO} and a race name + * and makes the column sortable by the total points. + * + * @author Axel Uhl (D043530) + * + */ + protected abstract class RaceColumn extends ExpandableSortableColumn { + private RaceColumnDTO race; + + private final String headerStyle; + private final String columnStyle; + + public RaceColumn(RaceColumnDTO race, boolean enableExpansion, Cell cell, + SortingOrder preferredSortingOrder, String headerStyle, String columnStyle) { + super(LeaderboardPanel.this, enableExpansion, cell, preferredSortingOrder, stringMessages, + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, selectedRaceDetails, LeaderboardPanel.this); + setHorizontalAlignment(ALIGN_CENTER); + this.race = race; + this.headerStyle = headerStyle; + this.columnStyle = columnStyle; + } + + public RaceColumnDTO getRace() { + return race; + } + + public void setRace(RaceColumnDTO race) { + this.race = race; + } + + public String getRaceColumnName() { + return race.getRaceColumnName(); + } + + public boolean isMedalRace() { + return race.isMedalRace(); + } + + public boolean isLive(FleetDTO fleetDTO) { + return race.isLive(fleetDTO, timer.getLiveTimePointInMillis()); + } + + @Override + public String getColumnStyle() { + return columnStyle; + } + + /** + * Computes added scores for this RaceColumn + */ + private double computeAddedScores(LeaderboardRowDTO object) { + double addedScores = 0; + for (RaceColumnDTO raceColumn : getLeaderboard().getRaceList()) { + LeaderboardEntryDTO entryBefore = object.fieldsByRaceColumnName.get(raceColumn.getName()); + if (entryBefore.totalPoints != null) { + addedScores += entryBefore.totalPoints; + } + // else "add 0" + if (raceColumn.getName().equals(getRaceColumnName())) { + break; // we've reached the current column - stop here + } + } + return addedScores; + } + + /** + * Displays a combination of total points and maxPointsReason in bold, transparent, strike-through, depending on + * various criteria. Here's how: + * + *
+         *                                  total points                |    maxPointsReason
+         * -------------------------------+-----------------------------+-----------------------
+         *  not discarded, no maxPoints   | bold                        | none
+         *  not discarded, maxPoints      | bold                        | transparent
+         *  discarded, no maxPoints       | transparent, strike-through | none
+         *  discarded, maxPoints          | transparent, strike-through | transparent, strike-through
+         * 
+ */ + @Override + public void render(Context context, LeaderboardRowDTO object, SafeHtmlBuilder html) { + LeaderboardEntryDTO entry = object.fieldsByRaceColumnName.get(getRaceColumnName()); + if (entry != null) { + boolean isLive = isLive(entry.fleet); + final String textColor = isLive ? IS_LIVE_TEXT_COLOR : DEFAULT_TEXT_COLOR; + final String addedScores = isShowAddedScores() ? scoreFormat.format(computeAddedScores(object)) : ""; + String totalOrAddedPointsAsText = isShowAddedScores() ? addedScores + : entry.totalPoints == null ? "" : scoreFormat.format(entry.totalPoints); + String netOrAddedPointsAsText = isShowAddedScores() ? addedScores + : entry.netPoints == null ? "" : scoreFormat.format(entry.netPoints); + if (entry.fleet != null && entry.fleet.getColor() != null) { + html.append(raceColumnTemplate.cellFrameWithTextColorAndFleetBorder(textColor, entry.fleet.getColor().getAsHtml())); + } else { + html.append(raceColumnTemplate.cellFrameWithTextColor(textColor)); + } + // don't show points if max points / penalty + if (entry.reasonForMaxPoints == null || entry.reasonForMaxPoints == MaxPointsReason.NONE) { + if (!entry.discarded) { + html.appendHtmlConstant(""); + html.appendHtmlConstant(totalOrAddedPointsAsText); + html.appendHtmlConstant(""); + } else { + html.appendHtmlConstant(" "); + html.appendHtmlConstant(netOrAddedPointsAsText); + html.appendHtmlConstant(""); + } + } else { + html.appendHtmlConstant(" "); + if (entry.discarded) { + html.appendHtmlConstant(""); + } + html.appendEscaped(entry.reasonForMaxPoints == MaxPointsReason.NONE ? "" : entry.reasonForMaxPoints.name()); + if (entry.discarded) { + html.appendHtmlConstant(""); + } + html.appendHtmlConstant(""); + } + html.appendHtmlConstant("
"); + } + } + + @Override + public InvertibleComparator getComparator() { + return new InvertibleComparatorAdapter() { + @Override + public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { + final int result; + if (isShowAddedScores()) { + double o1AddedScore = computeAddedScores(o1); + double o2AddedScore = computeAddedScores(o2); + double intermediate_result = o1AddedScore == 0. ? (o2AddedScore == 0. ? -1. + : isAscending() ? 1. : -1.) : o2AddedScore == 0. ? (isAscending() ? -1. : 1.) + : o1AddedScore - o2AddedScore; + if (!getLeaderboard().isHigherScoreBetter()) { + result = intermediate_result > 0 ? 1 : intermediate_result < 0 ? -1 : 0; + } else { + result = intermediate_result > 0 ? -1 : intermediate_result < 0 ? 1 : 0; + } + } else { + List competitorsFromBestToWorst = getLeaderboard().getCompetitorsFromBestToWorst( + race); + int o1Rank = competitorsFromBestToWorst.indexOf(o1.competitor) + 1; + int o2Rank = competitorsFromBestToWorst.indexOf(o2.competitor) + 1; + result = o1Rank == 0 ? o2Rank == 0 ? 0 : isAscending() ? 1 : -1 : o2Rank == 0 ? isAscending() ? -1 + : 1 : o1Rank - o2Rank; + } + return result; + } + }; + } + + @Override + public String getHeaderStyle() { + return headerStyle; + } + + @Override + public Header getHeader() { + SortableExpandableColumnHeader header = new SortableExpandableColumnHeader(/* title */race.getRaceColumnName(), + /* iconURL */race.isMedalRace() ? "/gwt/images/medal_small.png" : null, LeaderboardPanel.this, this, stringMessages); + return header; + } + } + + public static DetailType[] getAvailableRaceDetailColumnTypes() { + return new DetailType[] { DetailType.RACE_GAP_TO_LEADER_IN_SECONDS, + DetailType.RACE_AVERAGE_SPEED_OVER_GROUND_IN_KNOTS, + DetailType.RACE_DISTANCE_TRAVELED, DetailType.RACE_DISTANCE_TRAVELED_INCLUDING_GATE_START, + DetailType.RACE_TIME_TRAVELED, DetailType.RACE_CALCULATED_TIME_TRAVELED, + DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, + DetailType.RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS, DetailType.RACE_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD_IN_METERS, + DetailType.NUMBER_OF_MANEUVERS, DetailType.DISPLAY_LEGS, DetailType.CURRENT_LEG, + 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.TIME_BETWEEN_RACE_START_AND_COMPETITOR_START, + DetailType.SPEED_OVER_GROUND_AT_RACE_START, + DetailType.SPEED_OVER_GROUND_WHEN_PASSING_START, + DetailType.DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_PASSING_START_IN_METERS, + DetailType.START_TACK, + DetailType.RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL}; + } + + public static DetailType[] getAvailableOverallDetailColumnTypes() { + return new DetailType[] { DetailType.REGATTA_RANK, + DetailType.TOTAL_DISTANCE_TRAVELED, + DetailType.TOTAL_AVERAGE_SPEED_OVER_GROUND, + DetailType.TOTAL_TIME_SAILED_IN_SECONDS, + DetailType.MAXIMUM_SPEED_OVER_GROUND_IN_KNOTS, + DetailType.TIME_ON_TIME_FACTOR, + DetailType.TIME_ON_DISTANCE_ALLOWANCE_IN_SECONDS_PER_NAUTICAL_MILE}; + } + + private class TextRaceColumn extends RaceColumn implements RaceNameProvider { + /** + * Remembers the leg columns; null-padded, if {@link #getLegColumn(int)} asks for a column index + * not yet existing. It is important to remember the columns because column removal happens based on identity. + */ + private final List legColumns; + + public TextRaceColumn(RaceColumnDTO race, boolean expandable, SortingOrder preferredSortingOrder, + String headerStyle, String columnStyle) { + super(race, expandable, new TextCell(), preferredSortingOrder, headerStyle, columnStyle); + legColumns = new ArrayList(); + } + + public String getValue(LeaderboardRowDTO object) { + // The following code exists only for robustness. This method should never be called because + // RaceColumn implements its own render(...) method which doesn't make use of getValue(...) + final Double totalPoints = object.fieldsByRaceColumnName.get(getRaceColumnName()).totalPoints; + return "" + (totalPoints == null ? "" : scoreFormat.format(totalPoints)); + } + + @Override + protected void ensureExpansionDataIsLoaded(final Runnable callWhenExpansionDataIsLoaded) { + if (getLeaderboard().getLegCount(getRaceColumnName(), preSelectedRace) != -1) { + callWhenExpansionDataIsLoaded.run(); + } else { + updateLeaderboardAndRun(callWhenExpansionDataIsLoaded); + } + } + + @Override + protected Map> getDetailColumnMap( + LeaderboardPanel leaderboardPanel, StringMessages stringMessages, String detailHeaderStyle, + String detailColumnStyle) { + Map> result = new HashMap<>(); + result.put(DetailType.RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL, + new TimeSinceLastGpsFixColumn(DetailType.RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL, + new RaceRatioBetweenTimeSinceLastPositionFixAndAverageSamplingInterval(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.RACE_DISTANCE_TRAVELED, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_DISTANCE_TRAVELED, new RaceDistanceTraveledInMeters(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.RACE_DISTANCE_TRAVELED_INCLUDING_GATE_START, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_DISTANCE_TRAVELED_INCLUDING_GATE_START, new RaceDistanceTraveledIncludingGateStartInMeters(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.RACE_AVERAGE_SPEED_OVER_GROUND_IN_KNOTS, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_AVERAGE_SPEED_OVER_GROUND_IN_KNOTS, new RaceAverageSpeedInKnots(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.RACE_GAP_TO_LEADER_IN_SECONDS, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_GAP_TO_LEADER_IN_SECONDS, new RaceGapToLeaderInSeconds(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS, new CurrentSpeedOverGroundInKnots(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.RACE_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD_IN_METERS, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_DISTANCE_TO_COMPETITOR_FARTHEST_AHEAD_IN_METERS, new RaceDistanceToCompetitorFarthestAheadInMeters(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + 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, LeaderboardPanel.this)); + 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, LeaderboardPanel.this)); + 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(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.RACE_SPEED_OVER_GROUND_FIVE_SECONDS_BEFORE_START, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_SPEED_OVER_GROUND_FIVE_SECONDS_BEFORE_START, new SpeedFiveSecondsBeforeStartInKnots(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.DISTANCE_TO_START_AT_RACE_START, + new FormattedDoubleDetailTypeColumn(DetailType.DISTANCE_TO_START_AT_RACE_START, new DistanceToStartAtRaceStartInMeters(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.TIME_BETWEEN_RACE_START_AND_COMPETITOR_START, + new FormattedDoubleDetailTypeColumn(DetailType.TIME_BETWEEN_RACE_START_AND_COMPETITOR_START, new TimeBetweenRaceStartAndCompetitorStartInSeconds(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.SPEED_OVER_GROUND_AT_RACE_START, + new FormattedDoubleDetailTypeColumn(DetailType.SPEED_OVER_GROUND_AT_RACE_START, new SpeedOverGroundAtRaceStartInKnots(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.SPEED_OVER_GROUND_WHEN_PASSING_START, + new FormattedDoubleDetailTypeColumn(DetailType.SPEED_OVER_GROUND_WHEN_PASSING_START, new SpeedOverGroundWhenPassingStartInKnots(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_PASSING_START_IN_METERS, + new FormattedDoubleDetailTypeColumn(DetailType.DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_PASSING_START_IN_METERS, new DistanceToStarboardSideOfStartLineInMeters(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.START_TACK, new StartingTackColumn(new TackWhenStarting(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE)); + result.put(DetailType.NUMBER_OF_MANEUVERS, new ManeuverCountRaceColumn(getLeaderboardPanel(), this, stringMessages, + LeaderboardPanel.this.selectedManeuverDetails, LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, + LEG_DETAIL_COLUMN_HEADER_STYLE, LEG_DETAIL_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.CURRENT_LEG, + new FormattedDoubleDetailTypeColumn(DetailType.CURRENT_LEG, new CurrentLeg(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.RACE_TIME_TRAVELED, new TimeTraveledRaceColumn(getLeaderboardPanel(), this, + stringMessages, LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LEG_DETAIL_COLUMN_HEADER_STYLE, + LEG_DETAIL_COLUMN_STYLE)); + result.put(DetailType.RACE_CALCULATED_TIME_TRAVELED, + new FormattedDoubleDetailTypeColumn(DetailType.RACE_CALCULATED_TIME_TRAVELED, new RaceCalculatedTimeTraveledInSeconds(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + result.put(DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, new FormattedDoubleDetailTypeColumn( + DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, + new RaceCalculatedTimeAtEstimatedArrivalAtCompetitorFarthestAheadInSeconds(), + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + return result; + } + + @Override + protected Iterable> getDirectChildren() { + List> result = new ArrayList<>(); + for (AbstractSortableColumnWithMinMax column : super.getDirectChildren()) { + result.add(column); + } + if (isExpanded() && selectedRaceDetails.contains(DetailType.DISPLAY_LEGS)) { + // it is important to re-use existing LegColumn objects because + // removing the columns from the table + // is based on column identity + int maxLegCount = getLeaderboard().getLegCount(getRaceColumnName(), preSelectedRace); + if (maxLegCount != -1) { + for (int i = 0; i < maxLegCount; i++) { + LegColumn legColumn = getLegColumn(i); + result.add(legColumn); + } + } else { + // the race is no longer part of the LeaderboardDTO; consider the non-null legs in legColumns: + for (LegColumn legColumn : legColumns) { + if (legColumn != null) { + result.add(legColumn); + } + } + } + } + return result; + } + + private LegColumn getLegColumn(int legNumber) { + LegColumn result; + if (legColumns.size() > legNumber && legColumns.get(legNumber) != null) { + result = legColumns.get(legNumber); + } else { + result = new LegColumn(LeaderboardPanel.this, getRaceColumnName(), legNumber, SortingOrder.ASCENDING, + stringMessages, Collections.unmodifiableList(selectedLegDetails), LEG_COLUMN_HEADER_STYLE, + LEG_COLUMN_STYLE, LEG_DETAIL_COLUMN_HEADER_STYLE, LEG_DETAIL_COLUMN_STYLE); + while (legColumns.size() <= legNumber) { + legColumns.add(null); + } + legColumns.set(legNumber, result); + } + return result; + } + + /** + * Reports the ratio of the time that passed since the last position fix and the average sampling interval of the + * competitor's track. On a perfect track, this value will never exceed 1.0. Immediately when a fix is received, this + * value goes to 0.0. For a competitor whose tracker is lagging, this value can grow considerably greater than 1. + * The value goes to null if there are no fixes in the track. + * + * @author Axel Uhl (D043530) + * + */ + private class RaceRatioBetweenTimeSinceLastPositionFixAndAverageSamplingInterval implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null && fieldsForRace.timeSinceLastPositionFixInSeconds != null && fieldsForRace.averageSamplingInterval != null) { + result = fieldsForRace.timeSinceLastPositionFixInSeconds / fieldsForRace.averageSamplingInterval.asSeconds(); + } + return result; + } + } + + /** + * Accumulates the average speed over all legs of a race + * + * @author Axel Uhl (D043530) + */ + private class RaceAverageSpeedInKnots implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null && fieldsForRace.legDetails != null) { + double distanceTraveledInMeters = 0; + long timeInMilliseconds = 0; + for (LegEntryDTO legDetail : fieldsForRace.legDetails) { + if (legDetail != null) { + if (legDetail.distanceTraveledInMeters != null && legDetail.timeInMilliseconds != null) { + distanceTraveledInMeters += legDetail.distanceTraveledInMeters; + timeInMilliseconds += legDetail.timeInMilliseconds; + } else { + distanceTraveledInMeters = 0; + timeInMilliseconds = 0; + break; + } + } + } + if (timeInMilliseconds != 0) { + result = distanceTraveledInMeters / (double) timeInMilliseconds * 1000 * 3600 / 1852; + } + } + return result; + } + } + + /** + * 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 RaceAverageAbsoluteCrossTrackErrorInMeters implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + 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; + } + } + + /** + * Fetches the competitor's distance to the start mark at the time the race started + * + * @author Axel Uhl (D043530) + */ + private class DistanceToStartAtRaceStartInMeters implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.distanceToStartLineAtStartOfRaceInMeters; + } + return result; + } + } + + /** + * Fetches the time between start of race and the competitors start mark passing, telling + * how long after the gun the competitor actually started. + * + * @author Axel Uhl (D043530) + */ + private class TimeBetweenRaceStartAndCompetitorStartInSeconds implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.timeBetweenRaceStartAndCompetitorStartInSeconds; + } + return result; + } + } + + /** + * Fetches the competitor's speed over ground at the time the race started + * + * @author Axel Uhl (D043530) + */ + private class SpeedOverGroundAtRaceStartInKnots implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.speedOverGroundAtStartOfRaceInKnots; + } + return result; + } + } + + /** + * Fetches the competitor's distance to the start mark five seconds before the race started + */ + private class DistanceToStartFiveSecondsBeforeStartInMeters implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.distanceToStartLineFiveSecondsBeforeStartInMeters; + } + return result; + } + } + + /** + * Fetches the competitor's speed over ground five seconds before the race started + */ + private class SpeedFiveSecondsBeforeStartInKnots implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.speedOverGroundFiveSecondsBeforeStartInKnots; + } + return result; + } + } + + /** + * Fetches the competitor's speed over ground at the time the competitor passed the start + * + * @author Axel Uhl (D043530) + */ + private class SpeedOverGroundWhenPassingStartInKnots implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.speedOverGroundAtPassingStartWaypointInKnots; + } + return result; + } + } + + /** + * Fetches the competitor's distance to the starboard side of the start line when competitor passed the start. + * If the start waypoint is not a gate/line, the distance to the single buoy is used. + * + * @author Axel Uhl (D043530) + */ + private class DistanceToStarboardSideOfStartLineInMeters implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.distanceToStarboardSideOfStartLineInMeters; + } + return result; + } + } + + /** + * Fetches the competitor's speed over ground at the time the competitor passed the start + * + * @author Axel Uhl (D043530) + */ + private class TackWhenStarting implements LegDetailField { + @Override + public Tack get(LeaderboardRowDTO row) { + Tack result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.startTack; + } + return result; + } + } + + /** + * Accumulates the distance traveled over all legs of a race + * + * @author Axel Uhl (D043530) + */ + private class RaceDistanceTraveledInMeters implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null && fieldsForRace.legDetails != null) { + for (LegEntryDTO legDetail : fieldsForRace.legDetails) { + if (legDetail != null) { + if (legDetail.distanceTraveledInMeters != null) { + if (result == null) { + result = 0.0; + } + result += legDetail.distanceTraveledInMeters; + } + } + } + } + return result; + } + } + + private class RaceCalculatedTimeTraveledInSeconds implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.calculatedTime == null ? null : fieldsForRace.calculatedTime.asSeconds(); + } + return result; + } + } + + private class RaceCalculatedTimeAtEstimatedArrivalAtCompetitorFarthestAheadInSeconds implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead == null ? null + : fieldsForRace.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead.asSeconds(); + } + return result; + } + } + + /** + * Accumulates the distance traveled over all legs of a race and considers the specifics of a gate start. The + * first leg of a gate start gets as additional distance traveled the distance from the port side of the line to + * the point where the competitor started which helps normalize the distance traveled and make them comparable + * across early and late starters. + * + * @author Axel Uhl (D043530) + */ + private class RaceDistanceTraveledIncludingGateStartInMeters implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null && fieldsForRace.legDetails != null) { + for (LegEntryDTO legDetail : fieldsForRace.legDetails) { + if (legDetail != null) { + if (legDetail.distanceTraveledIncludingGateStartInMeters != null) { + if (result == null) { + result = 0.0; + } + result += legDetail.distanceTraveledIncludingGateStartInMeters; + } + } + } + } + return result; + } + } + + private class CurrentLeg implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null && fieldsForRace.legDetails != null && !fieldsForRace.legDetails.isEmpty()) { + for (LegEntryDTO legDetail : fieldsForRace.legDetails) { + if (legDetail != null) { + if (legDetail.started) { + if (result == null) { + result = 0.0; + } + result++; + } else { + if (result != null) { + result++; + } + break; + } + } + } + } + return result; + } + } + + /** + * Computes the gap to leader exploiting the ordering of the leg detail columns + * + * @author Axel Uhl (D043530) + */ + private class RaceGapToLeaderInSeconds implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null) { + result = fieldsForRace.gapToLeaderInOwnTime == null ? null : fieldsForRace.gapToLeaderInOwnTime.asSeconds(); + } + return result; + } + } + + /** + * Computes the windward distance to the competitor farthest ahead in the race (not necessarily the leader with + * handicap scoring) in meters + * + * @author Axel Uhl (D043530) + */ + private class RaceDistanceToCompetitorFarthestAheadInMeters implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null && fieldsForRace.windwardDistanceToCompetitorFarthestAheadInMeters != null) { + result = fieldsForRace.windwardDistanceToCompetitorFarthestAheadInMeters; + } + return result; + } + } + + private class CurrentSpeedOverGroundInKnots implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null && fieldsForRace.legDetails != null) { + int lastLegIndex = fieldsForRace.legDetails.size() - 1; + if (lastLegIndex >= 0) { + LegEntryDTO lastLegDetail = fieldsForRace.legDetails.get(lastLegIndex); + // competitor may be in leg prior to the one the leader is in; find competitors current leg + while (lastLegDetail == null && lastLegIndex > 0) { + lastLegDetail = fieldsForRace.legDetails.get(--lastLegIndex); + } + if (lastLegDetail != null) { + result = lastLegDetail.currentSpeedOverGroundInKnots; + } + } + } + return result; + } + } + } + + /** + * Column that display the number of races a competitor has finished. Currently + * taking into account {@link MaxPointsReason#DNS}, {@link MaxPointsReason#DNF} + * and {@link MaxPointsReason#DNC}. Configurable over {@link LeaderboardSettings#isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor()}. + * + * @author Simon Marcel Pamies + */ + private class TotalRacesCompletedColumn extends LeaderboardSortableColumnWithMinMax { + private final String columnStyle; + private final MaxPointsReason[] MAX_POINTS_REASONS_THAT_IDENTIFY_NON_FINISHED_RACES = + new MaxPointsReason[] {MaxPointsReason.DNS, MaxPointsReason.DNF, MaxPointsReason.DNC}; + + protected TotalRacesCompletedColumn(String columnStyle) { + super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); + this.columnStyle = columnStyle; + setHorizontalAlignment(ALIGN_CENTER); + } + + private int computeNumberOfRacesCompleted(LeaderboardRowDTO object) { + int racesSailedInRow = 0; + for (RaceColumnDTO raceColumn : getLeaderboard().getRaceList()) { + LeaderboardEntryDTO entryBefore = object.fieldsByRaceColumnName.get(raceColumn.getName()); + if (entryBefore.totalPoints != null) { + if (entryBefore.reasonForMaxPoints.equals(MaxPointsReason.NONE) || + !Util.contains(Arrays.asList(MAX_POINTS_REASONS_THAT_IDENTIFY_NON_FINISHED_RACES), entryBefore.reasonForMaxPoints)) { + racesSailedInRow++; + } + } + } + return racesSailedInRow; + } + + @Override + public String getValue(LeaderboardRowDTO object) { + return "" + computeNumberOfRacesCompleted(object); + } + + @Override + public void render(Context context, LeaderboardRowDTO object, SafeHtmlBuilder sb) { + String textColor = getLeaderboard().hasLiveRace(timer.getLiveTimePointInMillis()) ? IS_LIVE_TEXT_COLOR : DEFAULT_TEXT_COLOR; + + sb.appendHtmlConstant(""); + sb.appendEscaped(getValue(object)); + sb.appendHtmlConstant(""); + } + + @Override + public InvertibleComparator getComparator() { + return new InvertibleComparatorAdapter() { + @Override + public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { + double o1RacesSailed = computeNumberOfRacesCompleted(o1); + double o2RacesSailed = computeNumberOfRacesCompleted(o2); + double result = o1RacesSailed == 0. ? o2RacesSailed == 0. ? 0. : isAscending() ? 1. : -1. : o2RacesSailed == 0. ? isAscending() ? 1. : -1. : o2RacesSailed - o1RacesSailed; + return result > 0 ? 1 : result < 0 ? -1 : 0; + } + }; + } + + @Override + public String getColumnStyle() { + return columnStyle; + } + + @Override + public SafeHtmlHeader getHeader() { + return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.racesScored()), stringMessages.racesScoredTooltip()); + } + } + + /** + * Displays the totals for a competitor for the entire leaderboard. + * + * @author Axel Uhl (D043530) + * + */ + private class TotalsColumn extends LeaderboardSortableColumnWithMinMax { + private final String columnStyle; + + protected TotalsColumn(String columnStyle) { + super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); + this.columnStyle = columnStyle; + setHorizontalAlignment(ALIGN_CENTER); + } + + @Override + public String getValue(LeaderboardRowDTO object) { + Double totalPoints = object.totalPoints; + return "" + (totalPoints == null ? "" : scoreFormat.format(totalPoints)); + } + + @Override + public void render(Context context, LeaderboardRowDTO object, SafeHtmlBuilder sb) { + String textColor = getLeaderboard().hasLiveRace(timer.getLiveTimePointInMillis()) ? IS_LIVE_TEXT_COLOR : DEFAULT_TEXT_COLOR; + + sb.appendHtmlConstant(""); + sb.appendEscaped(getValue(object)); + sb.appendHtmlConstant(""); + } + + @Override + public InvertibleComparator getComparator() { + return new InvertibleComparatorAdapter() { + @Override + public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { + return getLeaderboard().competitors.indexOf(o1.competitor) + - getLeaderboard().competitors.indexOf(o2.competitor); + } + }; + } + + @Override + public String getColumnStyle() { + return columnStyle; + } + + @Override + public SafeHtmlHeader getHeader() { + return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.total()), stringMessages.totalsColumnTooltip()); + } + } + + protected class CarryColumn extends LeaderboardSortableColumnWithMinMax { + public CarryColumn() { + super(new AbstractSafeHtmlCell(new AbstractSafeHtmlRenderer() { + @Override + public SafeHtml render(LeaderboardRowDTO object) { + return new SafeHtmlBuilder().appendEscaped(object.carriedPoints == null ? "" : scoreFormat.format(object.carriedPoints)).toSafeHtml(); + } + }) { + @Override + protected void render(com.google.gwt.cell.client.Cell.Context context, SafeHtml data, SafeHtmlBuilder sb) { + sb.append(data); + } + }, SortingOrder.ASCENDING, LeaderboardPanel.this); + setSortable(true); + } + + protected CarryColumn(Cell cell) { + super(cell, SortingOrder.ASCENDING, LeaderboardPanel.this); + setSortable(true); + } + + @Override + public LeaderboardRowDTO getValue(LeaderboardRowDTO object) { + return object; + } + + @Override + public InvertibleComparator getComparator() { + return new InvertibleComparatorAdapter() { + @Override + public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { + Double o1CarriedPoints = o1.carriedPoints; + if (o1CarriedPoints == null) { + o1CarriedPoints = 0.0; + } + Double o2CarriedPoints = o2.carriedPoints; + if (o2CarriedPoints == null) { + o2CarriedPoints = 0.0; + } + return o1CarriedPoints.compareTo(o2CarriedPoints); + } + }; + } + + @Override + public SafeHtmlHeader getHeader() { + return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.carry()), stringMessages.carryColumnTooltip()); + } + } + + private class LeaderboardSelectionCheckboxColumn extends com.sap.sailing.gwt.ui.client.shared.controls.SelectionCheckboxColumn + implements CompetitorSelectionChangeListener { + protected LeaderboardSelectionCheckboxColumn(final CompetitorSelectionProvider competitorSelectionProvider) { + super(tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell()); + competitorSelectionProvider.addCompetitorSelectionChangeListener(this); + } + + @Override + public Boolean getValue(LeaderboardRowDTO row) { + return competitorSelectionProvider.isSelected(row.competitor); + } + + @Override public void competitorsListChanged(Iterable competitors) {} + @Override public void filterChanged(FilterSet> oldFilterSet, FilterSet> newFilterSet) {} + @Override public void filteredCompetitorsListChanged(Iterable filteredCompetitors) {} + + /** + * Ensure that the checkbox is redrawn when the competitor selection changes + */ + @Override + public void addedToSelection(CompetitorDTO competitor) { + final LeaderboardRowDTO row = getRow(competitor); + if (row != null) { + redrawRow(row); + } + } + + private void redrawRow(final LeaderboardRowDTO row) { + final List leaderboardDataList = getData().getList(); + synchronized (leaderboardDataList) { + redrawRow(row, leaderboardDataList); + } + } + + /** + * Ensure that the checkbox is redrawn when the competitor selection changes + */ + @Override + public void removedFromSelection(CompetitorDTO competitor) { + final LeaderboardRowDTO row = getRow(competitor); + if (row != null) { + redrawRow(row); + } + } + + @Override + public void updateMinMax() {} + + @Override + protected ListDataProvider getListDataProvider() { + return getData(); + } + } + + private class TotalRankColumn extends LeaderboardSortableColumnWithMinMax { + public TotalRankColumn() { + super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); + setHorizontalAlignment(ALIGN_CENTER); + setSortable(true); + } + + @Override + public String getValue(LeaderboardRowDTO object) { + final int totalRank = getLeaderboard().getTotalRank(object.competitor); + return "" + (totalRank == 0 ? "" : totalRank); + } + + @Override + public InvertibleComparator getComparator() { + return new InvertibleComparatorAdapter() { + @Override + public int compare(LeaderboardRowDTO o1, LeaderboardRowDTO o2) { + final int totalRank1 = getLeaderboard().getTotalRank(o1.competitor); + final int totalRank2 = getLeaderboard().getTotalRank(o2.competitor); + return totalRank1 == 0 ? totalRank2 == 0 ? 0 : 1 : totalRank2 == 0 ? -1 : totalRank1 - totalRank2; + } + }; + } + + @Override + public SafeHtmlHeader getHeader() { + return new SafeHtmlHeaderWithTooltip(SafeHtmlUtils.fromString(stringMessages.totalRegattaRank()), stringMessages.rankColumnTooltip()); + } + } + + private class StartingTackColumn extends DetailTypeColumn { + public StartingTackColumn(LegDetailField field, String headerStyle, String columnStyle) { + super(DetailType.START_TACK, field, new TextCell(), headerStyle, columnStyle, LeaderboardPanel.this); + } + + @Override + public String getValue(LeaderboardRowDTO row) { + return getField().get(row) == null ? null : getField().get(row) == Tack.PORT ? stringMessages.portTack() : stringMessages.starboardTack(); + } + } + + public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + LeaderboardSettings settings, CompetitorSelectionProvider competitorSelectionProvider, + String leaderboardName, ErrorReporter errorReporter, final StringMessages stringMessages, + final UserAgentDetails userAgent, boolean showRaceDetails) { + this(sailingService, asyncActionsExecutor, settings, false, /* preSelectedRace */null, competitorSelectionProvider, + null, leaderboardName, errorReporter, stringMessages, userAgent, showRaceDetails); + } + + public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + LeaderboardSettings settings, boolean isEmbedded, RegattaAndRaceIdentifier preSelectedRace, + CompetitorSelectionProvider competitorSelectionProvider, String leaderboardGroupName, + String leaderboardName, ErrorReporter errorReporter, final StringMessages stringMessages, + final UserAgentDetails userAgent, boolean showRaceDetails) { + this(sailingService, asyncActionsExecutor, settings, isEmbedded, preSelectedRace, competitorSelectionProvider, new Timer( + // 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, + /* competitorSearchTextBox */ null, /* showSelectionCheckbox */ true, /* optionalRaceTimesInfoProvider */ null, + /* autoExpandLastRaceColumn */ false, /* adjustTimerDelay */ true, /*autoApplyTop30Filter*/ false, false); + } + + public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + LeaderboardSettings settings, boolean isEmbedded, RegattaAndRaceIdentifier preSelectedRace, + CompetitorSelectionProvider competitorSelectionProvider, Timer timer, String leaderboardGroupName, + String leaderboardName, final ErrorReporter errorReporter, final StringMessages stringMessages, + final UserAgentDetails userAgent, boolean showRaceDetails, CompetitorFilterPanel competitorSearchTextBox, + boolean showSelectionCheckbox, RaceTimesInfoProvider optionalRaceTimesInfoProvider, + boolean autoExpandLastRaceColumn, boolean adjustTimerDelay, boolean autoApplyTopNFilter, boolean showCompetitorFilterStatus) { + this.setTitle(stringMessages.leaderboard()); + this.showSelectionCheckbox = showSelectionCheckbox; + this.showRaceDetails = showRaceDetails; + this.sailingService = sailingService; + this.asyncActionsExecutor = asyncActionsExecutor; + this.isEmbedded = isEmbedded; + this.preSelectedRace = preSelectedRace; + this.competitorSelectionProvider = competitorSelectionProvider; + competitorSelectionProvider.addCompetitorSelectionChangeListener(this); + this.setLeaderboardName(leaderboardName); + this.errorReporter = errorReporter; + this.stringMessages = stringMessages; + this.selectedLegDetails = new ArrayList(); + this.selectedRaceDetails = new ArrayList(); + this.selectedOverallDetailColumns = new ArrayList(); + this.raceTimesInfoProvider = optionalRaceTimesInfoProvider; + this.selectedManeuverDetails = new ArrayList(); + this.adjustTimerDelay = adjustTimerDelay; + this.initialCompetitorFilterHasBeenApplied = !autoApplyTopNFilter; + this.showCompetitorFilterStatus = showCompetitorFilterStatus; + overallDetailColumnMap = createOverallDetailColumnMap(); + settingsUpdatedExplicitly = !settings.isUpdateUponPlayStateChange(); + this.leaderboardUpdateListener = new ArrayList(); + if (settings.getLegDetailsToShow() != null) { + selectedLegDetails.addAll(settings.getLegDetailsToShow()); + } + if (settings.getManeuverDetailsToShow() != null) { + selectedManeuverDetails.addAll(settings.getManeuverDetailsToShow()); + } + if (settings.getRaceDetailsToShow() != null) { + selectedRaceDetails.addAll(settings.getRaceDetailsToShow()); + } + if (settings.getOverallDetailsToShow() != null) { + selectedOverallDetailColumns.addAll(settings.getOverallDetailsToShow()); + } + setAutoExpandPreSelectedRace(settings.isAutoExpandPreSelectedRace()); + this.autoExpandLastRaceColumn = autoExpandLastRaceColumn; + if (settings.getDelayBetweenAutoAdvancesInMilliseconds() != null) { + timer.setRefreshInterval(settings.getDelayBetweenAutoAdvancesInMilliseconds()); + } + this.timer = timer; + timer.addPlayStateListener(this); + timer.addTimeListener(this); + switch (settings.getActiveRaceColumnSelectionStrategy()) { + case EXPLICIT: + if (preSelectedRace == null) { + raceColumnSelection = new ExplicitRaceColumnSelection(); + } else { + raceColumnSelection = new ExplicitRaceColumnSelectionWithPreselectedRace(preSelectedRace); + } + break; + case LAST_N: + setRaceColumnSelectionToLastNStrategy(settings.getNumberOfLastRacesToShow()); + break; + } + totalRankColumn = new TotalRankColumn(); + selectionCheckboxColumn = new LeaderboardSelectionCheckboxColumn(competitorSelectionProvider); + RACE_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableRaceColumnHeader(); + LEG_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableLegColumnHeader(); + LEG_DETAIL_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableLegDetailColumnHeader(); + RACE_COLUMN_STYLE = tableResources.cellTableStyle().cellTableRaceColumn(); + LEG_COLUMN_STYLE = tableResources.cellTableStyle().cellTableLegColumn(); + LEG_DETAIL_COLUMN_STYLE = tableResources.cellTableStyle().cellTableLegDetailColumn(); + TOTAL_COLUMN_STYLE = tableResources.cellTableStyle().cellTableTotalColumn(); + leaderboardTable = new SortedCellTableWithStylableHeaders( + /* pageSize */10000, tableResources); + leaderboardTable.addCellPreviewHandler(new CellPreviewEvent.Handler() { + @Override + public void onCellPreview(CellPreviewEvent event) { + if (BrowserEvents.FOCUS.equals(event.getNativeEvent().getType())) { + elementToBlur = event.getNativeEvent().getEventTarget().cast(); + elementToBlur.blur(); + blurInOnSelectionChanged = 2; // blur a couple of times; doing it one time only doesn't seem to work reliably + blurFocusedElementAfterSelectionChange(); + } + } + }); + leaderboardTable.ensureDebugId("LeaderboardCellTable"); + getLeaderboardTable().setWidth("100%"); + leaderboardSelectionModel = new MultiSelectionModel(); + // remember handler registration so we can temporarily remove it and re-add it to suspend selection events while we're actively changing it + selectionChangeHandler = new Handler() { + @Override + public void onSelectionChange(SelectionChangeEvent event) { + List selection = new ArrayList(); + for (LeaderboardRowDTO row : getSelectedRows()) { + selection.add(row.competitor); + } + LeaderboardPanel.this.competitorSelectionProvider.setSelection(selection, /* listenersNotToNotify */LeaderboardPanel.this); + if (blurInOnSelectionChanged > 0) { + blurInOnSelectionChanged--; + blurFocusedElementAfterSelectionChange(); + } + } + }; + leaderboardAsTableSelectionModelRegistration = leaderboardSelectionModel.addSelectionChangeHandler(selectionChangeHandler); + leaderboardTable.setSelectionModel(leaderboardSelectionModel, selectionCheckboxColumn.getSelectionManager()); + setShowAddedScores(settings.isShowAddedScores()); + setShowCompetitorSailId(settings.isShowCompetitorSailIdColumn()); + setShowCompetitorFullName(settings.isShowCompetitorFullNameColumn()); + setShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(settings.isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor()); + if (timer.isInitialized()) { + loadCompleteLeaderboard(getLeaderboardDisplayDate()); + } + + contentPanel = new VerticalPanel(); + leaderboardTable.getElement().getStyle().setMarginTop(10, Unit.PX); + contentPanel.setStyleName(STYLE_LEADERBOARD_CONTENT); + busyIndicator = new SimpleBusyIndicator(false, 0.8f); + busyIndicator.ensureDebugId("BusyIndicator"); + busyStateChangeListeners = new HashSet<>(); + + // the information panel + if (!isEmbedded) { + Widget toolbarPanel = createToolbarPanel(); + contentPanel.add(toolbarPanel); + } + if (competitorSearchTextBox != null) { + contentPanel.add(competitorSearchTextBox); + contentPanel.add(busyIndicator); + competitorSearchTextBox.getSettingsButton().addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + new SettingsDialog(LeaderboardPanel.this, stringMessages).show(); + } + }); + this.competitorFilterPanel = competitorSearchTextBox; + } + SortedCellTable leaderboardTable = getLeaderboardTable(); + // leaderboardTable.getElement().getStyle().setMarginTop(5, Unit.PX); + filterControlPanel = new HorizontalPanel(); + filterControlPanel.setStyleName("LeaderboardPanel-FilterControl-Panel"); + contentPanel.add(leaderboardTable); + if (showCompetitorFilterStatus) { + contentPanel.add(createFilterDeselectionControl()); + } + setWidget(contentPanel); + raceNameForDefaultSorting = settings.getNameOfRaceToSort(); + } + + private Widget createToolbarPanel() { + FlowPanel informationPanel = new FlowPanel(); + informationPanel.setStyleName(STYLE_LEADERBOARD_INFO); + scoreCorrectionLastUpdateTimeLabel = new Label(""); + scoreCorrectionCommentLabel = new Label(""); + informationPanel.add(scoreCorrectionCommentLabel); + informationPanel.add(scoreCorrectionLastUpdateTimeLabel); + + liveRaceLabel = new Label(stringMessages.live()); + liveRaceLabel.setStyleName(STYLE_LEADERBOARD_LIVE_RACE); + liveRaceLabel.getElement().getStyle().setFontWeight(FontWeight.BOLD); + liveRaceLabel.getElement().getStyle().setColor(IS_LIVE_TEXT_COLOR); + liveRaceLabel.setVisible(false); + informationPanel.add(liveRaceLabel); + + // the toolbar panel + DockPanel toolbarPanel = new DockPanel(); + toolbarPanel.ensureDebugId("ToolbarPanel"); + toolbarPanel.setStyleName(STYLE_LEADERBOARD_TOOLBAR); + if (!isEmbedded) { + toolbarPanel.add(informationPanel, DockPanel.WEST); + toolbarPanel.add(busyIndicator, DockPanel.WEST); + } + toolbarPanel.setWidth("100%"); + toolbarPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE); + ClickHandler playPauseHandler = new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + if (LeaderboardPanel.this.timer.getPlayState() == PlayStates.Playing) { + LeaderboardPanel.this.timer.pause(); + } else { + // playing the standalone leaderboard means putting it into live mode + LeaderboardPanel.this.timer.setPlayMode(PlayModes.Live); + } + } + }; + 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); + + refreshAndSettingsPanel.add(refreshPanel); + toolbarPanel.add(refreshAndSettingsPanel, DockPanel.EAST); + return toolbarPanel; + } + + private Widget createFilterDeselectionControl() { + filterStatusLabel = new Label(); + filterStatusLabel.setStyleName("LeaderboardPanel-FilterControl-StatusLabel"); + filterStatusLabel.setText(""); + filterControlPanel.add(filterStatusLabel); + filterClearButton = new Button(stringMessages.showAll()); + filterClearButton.setStyleName("LeaderboardPanel-FilterClear-Button"); + filterClearButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + setBusyState(true); + competitorFilterPanel.clearAllActiveFilters(); + timeChanged(new Date(), null); + } + }); + filterControlPanel.add(filterClearButton); + filterControlPanel.setCellHorizontalAlignment(filterClearButton, HasHorizontalAlignment.ALIGN_RIGHT); + setFilterControlStatus(); + return filterControlPanel; + } + + private void setFilterControlStatus() { + if (showCompetitorFilterStatus) { + boolean filtersActive = competitorSelectionProvider.hasActiveFilters(); + if (filtersActive) { + String labelText = ""; + for (Filter filter : competitorSelectionProvider.getCompetitorsFilterSet().getFilters()) { + if (filter instanceof FilterWithUI) { + labelText += ((FilterWithUI)filter).getLocalizedDescription(stringMessages) + ", "; + } else { + labelText += filter.getName() + ", "; + } + } + filterStatusLabel.setText("Active Filter(s): " + labelText.substring(0, labelText.length()-2)); + filterClearButton.setVisible(true); + filterControlPanel.setVisible(true); + } else { + filterStatusLabel.setText(""); + filterClearButton.setVisible(false); + filterControlPanel.setVisible(false); + if (competitorFilterPanel != null) { + competitorFilterPanel.clearSelection(); + } + } + } + } + + private static class TotalDistanceTraveledInMetersField implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + return row.totalDistanceTraveledInMeters; + } + } + + private static class TimeOnTimeFactorColumn implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + return row.competitor.getTimeOnTimeFactor(); + } + } + + private static class TimeOnDistanceAllowanceInSecondsPerNauticalMileColumn implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + return row.competitor.getTimeOnDistanceAllowancePerNauticalMile() == null ? null : + row.competitor.getTimeOnDistanceAllowancePerNauticalMile().asSeconds(); + } + } + + private static class TotalAverageSpeedOverGroundField implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + final Double result; + if (row.totalDistanceTraveledInMeters != null && row.totalTimeSailedInSeconds != null && row.totalTimeSailedInSeconds != 0.0) { + result = row.totalDistanceTraveledInMeters / row.totalTimeSailedInSeconds / Mile.METERS_PER_NAUTICAL_MILE * 3600; + } else { + result = null; + } + return result; + } + } + + private Map> createOverallDetailColumnMap() { + Map> result = new HashMap<>(); + result.put(DetailType.TOTAL_DISTANCE_TRAVELED, + new FormattedDoubleDetailTypeColumn(DetailType.TOTAL_DISTANCE_TRAVELED, + new TotalDistanceTraveledInMetersField(), RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this)); + result.put(DetailType.TOTAL_AVERAGE_SPEED_OVER_GROUND, + new FormattedDoubleDetailTypeColumn(DetailType.TOTAL_AVERAGE_SPEED_OVER_GROUND, + new TotalAverageSpeedOverGroundField(), RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this)); + result.put(DetailType.MAXIMUM_SPEED_OVER_GROUND_IN_KNOTS, new MaxSpeedOverallColumn(RACE_COLUMN_HEADER_STYLE, + RACE_COLUMN_STYLE, this)); + result.put(DetailType.TOTAL_TIME_SAILED_IN_SECONDS, createOverallTimeTraveledColumn()); + result.put(DetailType.TIME_ON_TIME_FACTOR, + new FormattedDoubleDetailTypeColumn(DetailType.TIME_ON_TIME_FACTOR, + new TimeOnTimeFactorColumn(), RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this)); + result.put(DetailType.TIME_ON_DISTANCE_ALLOWANCE_IN_SECONDS_PER_NAUTICAL_MILE, + new FormattedDoubleDetailTypeColumn(DetailType.TIME_ON_DISTANCE_ALLOWANCE_IN_SECONDS_PER_NAUTICAL_MILE, + new TimeOnDistanceAllowanceInSecondsPerNauticalMileColumn(), RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, this)); + + return result; + } + + private OverallTimeTraveledColumn createOverallTimeTraveledColumn() { + return new OverallTimeTraveledColumn(this, stringMessages, RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE, + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE); + } + + /** + * Largely for subclasses to add more stuff to this panel, such as more toolbar buttons. + */ + protected HorizontalPanel getRefreshAndSettingsPanel() { + return refreshAndSettingsPanel; + } + + private RaceColumnDTO getRaceByName(String raceName) { + if (getLeaderboard() != null) { + for (RaceColumnDTO race : getLeaderboard().getRaceList()) { + for (FleetDTO fleet : race.getFleets()) { + if (race.getRaceIdentifier(fleet) != null + && raceName.equals(race.getRaceIdentifier(fleet).getRaceName())) { + return race; + } + } + } + } + return null; + } + + private RaceColumnDTO getRaceByColumnName(String columnName) { + if (getLeaderboard() != null) { + for (RaceColumnDTO race : getLeaderboard().getRaceList()) { + if (columnName.equals(race.getRaceColumnName())) { + return race; + } + } + } + return null; + } + + private RaceColumn getRaceColumnByRaceName(String raceName) { + for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { + Column column = getLeaderboardTable().getColumn(i); + if (column instanceof RaceColumn) { + RaceColumnDTO raceInLeaderboard = ((RaceColumn) column).getRace(); + for (FleetDTO fleet : raceInLeaderboard.getFleets()) { + final RegattaAndRaceIdentifier raceIdentifier = raceInLeaderboard.getRaceIdentifier(fleet); + if (raceIdentifier != null && raceIdentifier.getRaceName().equals(raceName)) { + return (RaceColumn) column; + } + } + } + } + return null; + } + + private RaceColumn getRaceColumnByRaceColumnName(String raceColumnName) { + for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { + Column column = getLeaderboardTable().getColumn(i); + if (column instanceof RaceColumn) { + if (((RaceColumn) column).getRaceColumnName().equals(raceColumnName)) { + return (RaceColumn) column; + } + } + } + return null; + } + + private SafeHtml getPlayPauseImgHtml(PlayStates playState) { + if (playState == PlayStates.Playing) { + return AbstractImagePrototype.create(pauseIcon).getSafeHtml(); + } else { + return AbstractImagePrototype.create(playIcon).getSafeHtml(); + } + } + + 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() { + return autoExpandPreSelectedRace; + } + + private boolean isAutoExpandLastRaceColumn() { + return autoExpandLastRaceColumn; + } + + private void setAutoExpandPreSelectedRace(boolean autoExpandPreSelectedRace) { + this.autoExpandPreSelectedRace = autoExpandPreSelectedRace; + if (autoExpandPreSelectedRace) { + autoExpandPerformedOnce = false; + } + } + + private boolean isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor() { + return showOverallColumnWithNumberOfRacesCompletedPerCompetitor; + } + + private void setShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(boolean showOverallColumnWithNumberOfRacesCompletedPerCompetitor) { + this.showOverallColumnWithNumberOfRacesCompletedPerCompetitor = showOverallColumnWithNumberOfRacesCompletedPerCompetitor; + } + + private boolean isShowAddedScores() { + return showAddedScores; + } + + private void setShowAddedScores(boolean showAddedScores) { + this.showAddedScores = showAddedScores; + } + + private boolean isShowCompetitorSailId() { + return showCompetitorSailId; + } + + private void setShowCompetitorSailId(boolean showCompetitorSailId) { + this.showCompetitorSailId = showCompetitorSailId; + } + + private boolean isShowCompetitorFullName() { + return showCompetitorFullName; + } + + private void setShowCompetitorFullName(boolean showCompetitorFullName) { + this.showCompetitorFullName = showCompetitorFullName; + } + + /** + * The time point for which the leaderboard currently shows results. In {@link PlayModes#Replay replay mode} this is + * the {@link #timer}'s time point. In {@link PlayModes#Live live mode} the {@link #timer}'s time is quantized to + * the closest full second to increase the likelihood of cache hits in the back end. + */ + protected Date getLeaderboardDisplayDate() { + return timer.getTime(); + } + + /** + * adds the column to the right end of the {@link #getLeaderboardTable() leaderboard table} and sets + * the column style according to the {@link LeaderboardSortableColumnWithMinMax#getColumnStyle() column's style definition}. + */ + protected void addColumn(AbstractSortableColumnWithMinMax column) { + leaderboardTable.addColumn(column, column.getHeader(), column.getComparator(), column + .getPreferredSortingOrder().isAscending()); + String columnStyle = column.getColumnStyle(); + if (columnStyle != null) { + getLeaderboardTable().addColumnStyleName(getLeaderboardTable().getColumnCount() - 1, columnStyle); + } + } + + protected void insertColumn(int beforeIndex, AbstractSortableColumnWithMinMax column) { + // remove column styles of those columns whose index will shift right by + // one: + removeColumnStyles(beforeIndex); + getLeaderboardTable().insertColumn(beforeIndex, column, column.getHeader(), column.getComparator(), + column.getPreferredSortingOrder().isAscending()); + addColumnStyles(beforeIndex); + } + + private void addColumnStyles(int startColumn) { + for (int i = startColumn; i < getLeaderboardTable().getColumnCount(); i++) { + AbstractSortableColumnWithMinMax columnToRemoveStyleFor = (AbstractSortableColumnWithMinMax) getLeaderboardTable() + .getColumn(i); + String columnStyle = columnToRemoveStyleFor.getColumnStyle(); + if (columnStyle != null) { + getLeaderboardTable().addColumnStyleName(i, columnStyle); + } + } + } + + private void removeColumnStyles(int startColumn) { + for (int i = startColumn; i < getLeaderboardTable().getColumnCount(); i++) { + AbstractSortableColumnWithMinMax columnToRemoveStyleFor = (AbstractSortableColumnWithMinMax) getLeaderboardTable() + .getColumn(i); + String columnStyle = columnToRemoveStyleFor.getColumnStyle(); + if (columnStyle != null) { + getLeaderboardTable().removeColumnStyleName(i, columnStyle); + } + } + } + + /** + * removes the column specified by columnIndex from the {@link #getLeaderboardTable() leaderboard + * table} and fixes the column styles again (see {@link #addColumnStyles(int)}). + */ + protected void removeColumn(int columnIndex) { + Column c = getLeaderboardTable().getColumn(columnIndex); + if (c instanceof ExpandableSortableColumn) { + ExpandableSortableColumn expandableColumn = (ExpandableSortableColumn) c; + if (expandableColumn.isExpanded()) { + // remove expanded child columns from the leaderboard... + expandableColumn.toggleExpansion(); + // them remember that column c was expanded: + expandableColumn.setExpanded(true); + } + } + removeColumnStyles(/* startColumn */columnIndex); + getLeaderboardTable().removeColumn(columnIndex); + addColumnStyles(/* startColumn */columnIndex); + } + + protected void removeColumn(Column c) { + int columnIndex = getLeaderboardTable().getColumnIndex(c); + if (columnIndex != -1) { + removeColumn(columnIndex); + } + } + + private void loadCompleteLeaderboard(Date date) { + if (needsDataLoading()) { + GetLeaderboardByNameAction getLeaderboardByNameAction = new GetLeaderboardByNameAction(sailingService, + getLeaderboardName(), useNullAsTimePoint() ? null : date, + /* namesOfRacesForWhichToLoadLegDetails */getNamesOfExpandedRaces(), shallAddOverallDetails(), /* previousLeaderboard */ + getLeaderboard(), isFillNetPointsUncorrected(), timer, errorReporter, stringMessages); + this.asyncActionsExecutor.execute(getLeaderboardByNameAction, LOAD_LEADERBOARD_DATA_CATEGORY, + new AsyncCallback() { + @Override + public void onSuccess(LeaderboardDTO result) { + updateLeaderboard(result); + setBusyState(false); + } + + @Override + public void onFailure(Throwable caught) { + setBusyState(false); + getErrorReporter() + .reportError("Error trying to obtain leaderboard contents: " + caught.getMessage(), + true /* silentMode */); + } + }); + } else { + setBusyState(false); + } + } + + protected boolean isFillNetPointsUncorrected() { + return false; + } + + /** + * 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(); + } + + /** + * Determine from column expansion state which races need their leg details + */ + private Collection getNamesOfExpandedRaces() { + Collection namesOfExpandedRaces = new ArrayList(); + for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { + Column column = getLeaderboardTable().getColumn(i); + if (column instanceof RaceColumn) { + RaceColumn raceColumn = (RaceColumn) column; + if (raceColumn.isExpanded()) { + namesOfExpandedRaces.add(raceColumn.getRaceColumnName()); + } + } + } + return namesOfExpandedRaces; + } + + private boolean shallAddOverallDetails() { + return !selectedOverallDetailColumns.isEmpty(); + } + + private void applyTop30FilterIfCompetitorSizeGreaterEqual40(LeaderboardDTO leaderboard) { + int maxRaceRank = 30; + if (leaderboard.competitors.size() >= 40) { + CompetitorRaceRankFilter raceRankFilter = new CompetitorRaceRankFilter(); + raceRankFilter.setLeaderboardFetcher(this); + raceRankFilter.setSelectedRace(preSelectedRace); + raceRankFilter.setQuickRankProvider(this.competitorFilterPanel.getQuickRankProvider()); + raceRankFilter.setOperator(new BinaryOperator(BinaryOperator.Operators.LessThanEquals)); + raceRankFilter.setValue(maxRaceRank); + FilterSet> activeFilterSet = + competitorSelectionProvider.getOrCreateCompetitorsFilterSet(stringMessages.topNCompetitorsByRaceRank(maxRaceRank)); + activeFilterSet.addFilter(raceRankFilter); + competitorSelectionProvider.setCompetitorsFilterSet(activeFilterSet); + } + } + + /** + * Assigns leaderboard to {@link #leaderboard} and updates the UI accordingly. Also updates the min/max + * values on the columns. + */ + protected void updateLeaderboard(LeaderboardDTO leaderboard) { + if (leaderboard != null) { + Collection> columnsToCollapseAndExpandAgain = getExpandedRaceColumnsWhoseDisplayedLegCountChanged(leaderboard); + for (RaceColumn columnToCollapseAndExpandAgain : columnsToCollapseAndExpandAgain) { + columnToCollapseAndExpandAgain.toggleExpansion(); + } + competitorSelectionProvider.setCompetitors(leaderboard.competitors, /* listenersNotToNotify */this); + if (!initialCompetitorFilterHasBeenApplied) { + applyTop30FilterIfCompetitorSizeGreaterEqual40(leaderboard); + initialCompetitorFilterHasBeenApplied = true; + } + raceColumnSelection.autoUpdateRaceColumnSelectionForUpdatedLeaderboard(getLeaderboard(), leaderboard); + setLeaderboard(leaderboard); + adjustColumnLayout(leaderboard); + updateRaceColumnDTOsToRaceColumns(leaderboard); + for (RaceColumn columnToCollapseAndExpandAgain : columnsToCollapseAndExpandAgain) { + columnToCollapseAndExpandAgain.toggleExpansion(); + } + adjustDelayToLive(); + final Map rowsToDisplay = getRowsToDisplay(); + Set rowsToAdd = new HashSet<>(rowsToDisplay.values()); + Map rowsToUpdate = new HashMap<>(); + synchronized (getData().getList()) { + int index = 0; + for (Iterator i = getData().getList().iterator(); i.hasNext(); ) { + LeaderboardRowDTO oldRow = i.next(); + LeaderboardRowDTO newRow = rowsToDisplay.get(oldRow.competitor); + if (newRow != null) { + rowsToUpdate.put(index++, newRow); // update row in place, preserving its selection state + rowsToAdd.remove(newRow); // no need to add this row when it was updated in-place + } else { + i.remove(); // old row's competitor not found in new rows' competitors; remove old row from table + } + } + for (Entry updateEntry : rowsToUpdate.entrySet()) { + LeaderboardRowDTO oldElement = getData().getList().set(updateEntry.getKey(), updateEntry.getValue()); + leaderboardSelectionModel.setSelected(oldElement, false); // make sure the old element is no longer part of the selection + updateSelection(updateEntry.getValue()); + } + for (LeaderboardRowDTO rowToAdd : rowsToAdd) { + getData().getList().add(rowToAdd); + updateSelection(rowToAdd); + } + } + RaceColumn lastRaceColumn = null; + for (int i=getLeaderboardTable().getColumnCount()-1; i>=0; i--) { + if (getLeaderboardTable().getColumn(i) instanceof RaceColumn) { + lastRaceColumn = (RaceColumn) getLeaderboardTable().getColumn(i); + break; + } + } + for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { + AbstractSortableColumnWithMinMax c = (AbstractSortableColumnWithMinMax) getLeaderboardTable().getColumn(i); + c.updateMinMax(); + // Toggle pre-selected race, if the setting is set and it isn't open yet, or the last race column if that was requested + if ((!autoExpandPerformedOnce && isAutoExpandPreSelectedRace() && c instanceof RaceColumn + && ((RaceColumn) c).getRace().hasTrackedRace(preSelectedRace)) || + (isAutoExpandLastRaceColumn() && c == lastRaceColumn)) { + ExpandableSortableColumn expandableSortableColumn = (ExpandableSortableColumn) c; + if (!expandableSortableColumn.isExpanded()) { + expandableSortableColumn.toggleExpansion(); + autoExpandPerformedOnce = true; + } + } + if (c instanceof RaceColumn && ((RaceColumn) c).getRace().hasTrackedRace(preSelectedRace)) { + RaceColumnDTO raceColumn = ((RaceColumn) c).getRace(); + informLeaderboardUpdateListenersAboutRaceSelected(preSelectedRace, raceColumn); + } + } + if (leaderboardTable.getCurrentlySortedColumn() != null) { + leaderboardTable.sort(); + } else { + AbstractSortableColumnWithMinMax columnToSortFor = getDefaultSortColumn(); + leaderboardTable.sortColumn(columnToSortFor, columnToSortFor.getPreferredSortingOrder().isAscending()); + } + + if (!isEmbedded) { + scoreCorrectionCommentLabel.setText(leaderboard.getComment() != null ? leaderboard.getComment() : ""); + if (leaderboard.getTimePointOfLastCorrectionsValidity() != null) { + Date lastCorrectionDate = leaderboard.getTimePointOfLastCorrectionsValidity(); + String lastUpdate = DateAndTimeFormatterUtil.longDateFormatter.render(lastCorrectionDate) + ", " + + DateAndTimeFormatterUtil.longTimeFormatter.render(lastCorrectionDate); + scoreCorrectionLastUpdateTimeLabel.setText(stringMessages.lastScoreUpdate() + ": " + lastUpdate); + } else { + scoreCorrectionLastUpdateTimeLabel.setText(""); + } + + boolean hasLiveRace = !leaderboard.getLiveRaces(timer.getLiveTimePointInMillis()).isEmpty(); + liveRaceLabel.setText(hasLiveRace ? getLiveRacesText() : ""); + scoreCorrectionLastUpdateTimeLabel.setVisible(!hasLiveRace); + liveRaceLabel.setVisible(hasLiveRace); + } + informLeaderboardUpdateListenersAboutLeaderboardUpdated(leaderboard); + setBusyState(false); + } + } + + /** + * Adjusts the row's selection in the {@link #leaderboardSelectionModel} so it matches its selection state in + * the {@link #competitorSelectionProvider}. + */ + private void updateSelection(LeaderboardRowDTO row) { + final boolean shallBeSelected = competitorSelectionProvider.isSelected(row.competitor); + if (leaderboardAsTableSelectionModelRegistration != null) { + // suspend selection events while actively adjusting the leaderboardSelectionModel to match the competitorSelectionProvider + leaderboardAsTableSelectionModelRegistration.removeHandler(); + leaderboardAsTableSelectionModelRegistration = null; + } + if (leaderboardSelectionModel.isSelected(row) != shallBeSelected) { + leaderboardSelectionModel.setSelected(row, shallBeSelected); + } + // register the selection change handler again + leaderboardAsTableSelectionModelRegistration = leaderboardTable.getSelectionModel().addSelectionChangeHandler(selectionChangeHandler); + } + + /** + * The race columns hold a now outdated copy of a {@link RaceColumnDTO} which needs to be updated from the {@link LeaderboardDTO} just received + */ + private void updateRaceColumnDTOsToRaceColumns(LeaderboardDTO leaderboard) { + for (RaceColumnDTO newRace : leaderboard.getRaceList()) { + RaceColumn raceColumn = getRaceColumnByRaceColumnName(newRace.getName()); + if (raceColumn != null) { + raceColumn.setRace(newRace); + } + } + } + + /** + * Due to a course change, a race may change its number of legs. All expanded race columns that show leg columns and + * whose leg count changed need to be collapsed before the leaderboard is replaced, and expanded afterwards again. + * Race columns whose toggling is {@link ExpandableSortableColumn#isTogglingInProcess() currently in progress} are + * not considered because their new state will be considered after replacing anyhow. + * + * @param newLeaderboard + * the new leaderboard before assigning to {@link #leaderboard} + * @return the columns that were collapsed in this step and that shall be expanded again after the leaderboard has + * been replaced + */ + private Collection> getExpandedRaceColumnsWhoseDisplayedLegCountChanged(LeaderboardDTO newLeaderboard) { + Set> result = new HashSet>(); + if (selectedRaceDetails.contains(DetailType.DISPLAY_LEGS)) { + for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { + Column c = getLeaderboardTable().getColumn(i); + if (c instanceof RaceColumn) { + RaceColumn rc = (RaceColumn) c; + // If the new leaderboard no longer contains the column, getLegCount will return -1, causing the column + // to be collapsed if it was expanded. This is correct because otherwise, removing it would no longer + // know the correct leg count. + if (!rc.isTogglingInProcess() && rc.isExpanded()) { + int oldLegCount = getLeaderboard().getLegCount(rc.getRaceColumnName(), preSelectedRace); + int newLegCount = newLeaderboard.getLegCount(rc.getRaceColumnName(), preSelectedRace); + if (oldLegCount != newLegCount) { + result.add(rc); + } + } + } + } + } + return result; + } + + /** + * Based on the {@link LeaderboardDTO#getDelayToLiveInMillisForLatestRace()} for the race that has the latest + * {@link RaceColumnDTO#getStartDate(FleetDTO) start time}, automatically adjusts the delay accordingly unless the + * {@link #adjustTimerDelay} flag is false + */ + private void adjustDelayToLive() { + if (adjustTimerDelay) { + if (leaderboard.getDelayToLiveInMillisForLatestRace() != null) { + setDelayInMilliseconds(leaderboard.getDelayToLiveInMillisForLatestRace()); + } + } + } + + /** + * Extracts the rows to display of the leaderboard. These are all {@link AbstractLeaderboardDTO#rows + * rows} in case {@link #preSelectedRace} is null, or only the rows of the competitors who scored in + * the race identified by {@link #preSelectedRace} otherwise. + */ + @Override + public Map getRowsToDisplay() { + Map result; + Iterable allFilteredCompetitors = competitorSelectionProvider.getFilteredCompetitors(); + result = new HashMap(); + if (preSelectedRace == null) { + for (CompetitorDTO competitor : leaderboard.rows.keySet()) { + if (Util.contains(allFilteredCompetitors, competitor)) { + result.put(competitor, leaderboard.rows.get(competitor)); + } + } + } else { + for (CompetitorDTO competitorInPreSelectedRace : getCompetitors(preSelectedRace)) { + if (Util.contains(allFilteredCompetitors, competitorInPreSelectedRace)) { + result.put(competitorInPreSelectedRace, leaderboard.rows.get(competitorInPreSelectedRace)); + } + } + } + return result; + } + + /** + * The {@link LeaderboardDTO} holds {@link LeaderboardDTO#getRaceList() races} as {@link RaceColumnDTO} objects. + * Those map their fleet names to the {@link RegattaAndRaceIdentifier} which identifies the tracked race + * representing the fleet race within the race column. + *

+ * + * On the other hand, a {@link LeaderboardRowDTO} has {@link LeaderboardEntryDTO}, keyed by race column name. The + * entry DTOs, in turn, store the {@link RaceIdentifier} of the race in which the respective competitor achieved the + * score. + *

+ * + * With this information it is possible to identify the competitors who participated in a particular tracked race, + * as identified through the race parameter. + * + * @return all competitors for which the {@link #getLeaderboard() leaderboard} has an entry whose + * {@link LeaderboardEntryDTO#race} equals race + */ + private Iterable getCompetitors(RaceIdentifier race) { + Set result = new HashSet(); + for (RaceColumnDTO raceColumn : getLeaderboard().getRaceList()) { + if (raceColumn.hasTrackedRace(race)) { + for (Map.Entry e : getLeaderboard().rows.entrySet()) { + LeaderboardEntryDTO entry = e.getValue().fieldsByRaceColumnName.get(raceColumn.getRaceColumnName()); + if (entry != null && entry.race != null && entry.race.equals(race)) { + result.add(e.getKey()); + } + } + } + } + return result; + } + + private AbstractSortableColumnWithMinMax getDefaultSortColumn() { + AbstractSortableColumnWithMinMax defaultSortColumn = null; + if (raceNameForDefaultSorting != null) { + defaultSortColumn = getRaceColumnByRaceName(raceNameForDefaultSorting); + } + if (defaultSortColumn == null) { + defaultSortColumn = getRankColumn(); + } + return defaultSortColumn; + } + + private TotalRankColumn getRankColumn() { + return totalRankColumn; + } + + protected void setLeaderboard(LeaderboardDTO leaderboard) { + this.leaderboard = leaderboard; + } + + @Override + public LeaderboardDTO getLeaderboard() { + return leaderboard; + } + + private void adjustColumnLayout(LeaderboardDTO leaderboard) { + int columnIndex = 0; + columnIndex = ensureSelectionCheckboxColumn(columnIndex); + columnIndex = ensureRankColumn(columnIndex); + columnIndex = ensureSailIDAndCompetitorColumn(columnIndex); + columnIndex = updateCarryColumn(leaderboard, columnIndex); + adjustOverallDetailColumns(leaderboard, columnIndex); + // first remove race columns no longer needed: + removeUnusedRaceColumns(leaderboard); + if (leaderboard != null) { + createMissingAndAdjustExistingRaceColumns(leaderboard); + ensureTotalsColumn(); + updateTotalRacesSailedColumn(); + } + } + + /** + * Ensures that the columns requested by {@link #selectedOverallDetailColumns} are in the table. Assumes that if there are + * any existing overall details columns, they start at indexOfFirstOverallDetailsColumn and are in the order + * defined by {@link #getAvailableOverallDetailColumnTypes()}. + * + * @param indexOfFirstOverallDetailsColumn + * tells the column index for the first overall details column + */ + private void adjustOverallDetailColumns(LeaderboardDTO leaderboard, int indexOfFirstOverallDetailsColumn) { + List> overallDetailColumnsToShow = new ArrayList>(); + // ensure the ordering in overallDetailColumnsToShow conforms to the ordering of getAvailableOverallDetailColumnTypes() + for (DetailType overallDetailType : getAvailableOverallDetailColumnTypes()) { + if (selectedOverallDetailColumns.contains(overallDetailType) && overallDetailColumnMap.containsKey(overallDetailType)) { + overallDetailColumnsToShow.add(overallDetailColumnMap.get(overallDetailType)); + } + } + int currentColumnIndex = indexOfFirstOverallDetailsColumn; + int i = 0; // index into overallDetailColumnToShow + Column currentColumn = currentColumnIndex < getLeaderboardTable().getColumnCount() ? + getLeaderboardTable().getColumn(currentColumnIndex) : null; + // repeat until no more column to check for removal and no more column left to check for need to insert + while (iraceColumn + */ + private void correctColumnData(RaceColumn raceColumn, RaceColumnDTO race) { + int columnIndex = getRaceColumnPosition(raceColumn); + if (raceColumn.isExpansionEnabled() != race.hasTrackedRaces() || race.isMedalRace() != raceColumn.isMedalRace()) { + if (raceColumn.isExpanded()) { + raceColumn.toggleExpansion(); // remove children from table + } + removeColumn(columnIndex); + insertColumn(columnIndex, createRaceColumn(race)); + } + } + + /** + * Removes all RaceColumns, starting at count {@link raceColumnStartIndex raceColumnStartIndex} + * + * @param raceColumnStartIndex + * The index of the race column should be deleted from. + * @param raceName + * The name of the racing column until the table should be cleared. + */ + private void removeRaceColumnFromRaceColumnStartIndexBeforeRace(int raceColumnStartIndex, RaceColumnDTO race) { + int counter = 0; + for (int leaderboardposition = 0; leaderboardposition < getLeaderboardTable().getColumnCount(); leaderboardposition++) { + Column c = getLeaderboardTable().getColumn(leaderboardposition); + if (c instanceof RaceColumn) { + RaceColumn raceColumn = (RaceColumn) c; + if (!raceColumn.getRaceColumnName().equals(race.getRaceColumnName())) { + if (raceColumnStartIndex == counter) { + removeColumn(raceColumn); + } + } else { + return; + } + counter++; + } + } + } + + /** + * Gets a ColumnPosition of a raceColumn + * + * @param raceColumn + * The column for which the position is to be found in the leaderboard table + * @return the position. Returns -1 if raceColumn not existing in leaderboardTable. + */ + private int getRaceColumnPosition(RaceColumn raceColumn) { + for (int leaderboardposition = 0; leaderboardposition < getLeaderboardTable().getColumnCount(); leaderboardposition++) { + Column c = getLeaderboardTable().getColumn(leaderboardposition); + if (c instanceof RaceColumn) { + RaceColumn rc = (RaceColumn) c; + if (rc.equals(raceColumn)) { + return leaderboardposition; + } + } + } + return -1; + } + + /** + * This method returns the position where a race column should get inserted. + * + * @param raceName + * the name of the race to insert + * @param listpos + * the position of the race in the {@link selectedRaceColumns selectedRaceColumns} + * @return the position of a race column right before which to insert the race column so that it is the + * listposth race in the table or -1 if no such race column was found, e.g., in case the column + * needs to be inserted as the last race in the table + */ + private int getColumnPositionToInsert(RaceColumnDTO race, int listpos) { + int raceColumnCounter = 0; + int noRaceColumnCounter = 0; + boolean raceColumnFound = false; + for (int leaderboardPosition = 0; !raceColumnFound + && leaderboardPosition < getLeaderboardTable().getColumnCount(); leaderboardPosition++) { + Column c = getLeaderboardTable().getColumn(leaderboardPosition); + if (c instanceof RaceColumn) { + // RaceColumn raceColumn = (RaceColumn) c; + if (raceColumnCounter == listpos) { + raceColumnFound = true; + } else { + raceColumnCounter++; + } + } else { + noRaceColumnCounter++; + } + } + if (raceColumnFound) { + return raceColumnCounter + noRaceColumnCounter; + } else { + return -1; + } + } + + /** + * Removes all columns of type {@link RaceColumn} from the leaderboard table if their name is not in the list of + * names of the selectedRaceColumns. + */ + private void removeRaceColumnsNotSelected(Iterable selectedRaceColumns) { + Set selectedRaceColumnNames = new HashSet(); + for (RaceColumnDTO selectedRaceColumn : selectedRaceColumns) { + selectedRaceColumnNames.add(selectedRaceColumn.getRaceColumnName()); + } + List> columnsToRemove = new ArrayList>(); + for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { + Column c = getLeaderboardTable().getColumn(i); + if (c instanceof RaceColumn + && (leaderboard == null || !selectedRaceColumnNames.contains(((RaceColumn) c) + .getRaceColumnName()))) { + columnsToRemove.add(c); + } + } + for (Column c : columnsToRemove) { + removeColumn(c); + } + } + + /** + * Existing and matching race columns may still need to be removed, re-created and inserted because the "tracked" + * property may have changed, changing the columns expandability. + */ + private void createMissingAndAdjustExistingRaceColumns(LeaderboardDTO leaderboard) { + // Correct order of races in selectedRaceColum + Iterable correctedOrderSelectedRaces = raceColumnSelection.getSelectedRaceColumnsOrderedAsInLeaderboard(leaderboard); + removeRaceColumnsNotSelected(correctedOrderSelectedRaces); + for (int selectedRaceCount = 0; selectedRaceCount < Util.size(correctedOrderSelectedRaces); selectedRaceCount++) { + RaceColumnDTO selectedRaceColumn = Util.get(correctedOrderSelectedRaces, selectedRaceCount); + final RaceColumn raceColumn = selectedRaceColumn == null ? null + : getRaceColumnByRaceColumnName(selectedRaceColumn.getName()); + if (raceColumn != null) { + // remove all raceColumns, starting at a specific selectedRaceCount, up to but excluding the selected + // raceName with the result that selectedRace is at position selectedRaceCount afterwards + removeRaceColumnFromRaceColumnStartIndexBeforeRace(selectedRaceCount, selectedRaceColumn); + correctColumnData(raceColumn, selectedRaceColumn); + } else { + // get correct position to insert the column + int positionToInsert = getColumnPositionToInsert(selectedRaceColumn, selectedRaceCount); + if (positionToInsert != -1) { + insertColumn(positionToInsert, createRaceColumn(selectedRaceColumn)); + } else { + // Add the raceColumn with addRaceColumn, if no RaceColumn is existing in leaderboard + addRaceColumn(createRaceColumn(selectedRaceColumn)); + } + } + } + } + + protected RaceColumn createRaceColumn(RaceColumnDTO raceInLeaderboard) { + TextRaceColumn textRaceColumn = new TextRaceColumn(raceInLeaderboard, shallExpandRaceColumn(raceInLeaderboard), + SortingOrder.ASCENDING, RACE_COLUMN_HEADER_STYLE, RACE_COLUMN_STYLE); + return textRaceColumn; + } + + /** + * To be expandable, a race column needs to have one or more tracked races associated and needs to have at least + * some GPS data available. Wind data is not required for expandability because several metrics can reasonably be + * determined even without reliable wind information. + */ + private boolean shallExpandRaceColumn(RaceColumnDTO raceColumnDTO) { + return showRaceDetails && raceColumnDTO.hasTrackedRaces() && raceColumnDTO.hasGPSData(); + } + + private void removeUnusedRaceColumns(LeaderboardDTO leaderboard) { + List> columnsToRemove = new ArrayList>(); + for (int i = 0; i < getLeaderboardTable().getColumnCount(); i++) { + Column c = getLeaderboardTable().getColumn(i); + if (c instanceof RaceColumn + && (leaderboard == null || !leaderboard.raceListContains(((RaceColumn) c).getRaceColumnName()))) { + columnsToRemove.add(c); + } + } + // Tricky issue: if the race column is currently expanded, we can't know anymore how many detail columns + // there are because the updated LeaderboardDTO object doesn't contain the race anymore. We have to + // collapse and remove all LegColumns following the RaceColumn + for (Column c : columnsToRemove) { + removeColumn(c); + } + } + + /** + * If the last column is the totals column, remove it. Add the race column as the last column. + */ + private void addRaceColumn(RaceColumn raceColumn) { + if (getLeaderboardTable().getColumn(getLeaderboardTable().getColumnCount() - 1) instanceof TotalsColumn) { + removeColumn(getLeaderboardTable().getColumnCount() - 1); + } + addColumn(raceColumn); + } + + /** + * The regatta rank column shall be displayed if an only if the {@link DetailType#REGATTA_RANK} detail is selected in the + * {@link #selectedOverallDetailColumns}. It will then be displayed after the selection checkbox column (if any) and + * before the sail number / competitor name columns. + */ + private boolean isShowRegattaRankColumn() { + return selectedOverallDetailColumns.contains(DetailType.REGATTA_RANK); + } + + /** + * @param rankColumnIndex + * the column index (0-based) where to put the rank column, if needed + * @return the column index (0-based) for the next column; will equal rankColumnIndex if the rank + * column is not {@link #isShowRegattaRankColumn() supposed to be shown}, or one greater otherwise. + */ + private int ensureRankColumn(int rankColumnIndex) { + final int indexOfNextColumn = rankColumnIndex + (isShowRegattaRankColumn() ? 1 : 0); + if (getLeaderboardTable().getColumnCount() > rankColumnIndex) { + if (isShowRegattaRankColumn()) { + if (getLeaderboardTable().getColumn(rankColumnIndex) != getRankColumn()) { + insertColumn(rankColumnIndex, getRankColumn()); + } // else, the column is needed and is already in place + } else { + if (getLeaderboardTable().getColumn(rankColumnIndex) == getRankColumn()) { + removeColumn(rankColumnIndex); + } + } + } else { + if (isShowRegattaRankColumn()) { + insertColumn(rankColumnIndex, getRankColumn()); + } + } + return indexOfNextColumn; + } + + /** + * @param selectionCheckboxColumnIndex + * the column index (0-based) where to put the selection checkbox column, if needed + * @return the column index (0-based) for the next column; will equal selectionCheckboxColumnIndex if + * the selection checkbox column is not {@link #showSelectionCheckbox supposed to be shown}, or one greater + * otherwise. + */ + private int ensureSelectionCheckboxColumn(int selectionCheckboxColumnIndex) { + final int indexOfNextColumn = selectionCheckboxColumnIndex + (showSelectionCheckbox ? 1 : 0); + if (getLeaderboardTable().getColumnCount() > selectionCheckboxColumnIndex) { + if (showSelectionCheckbox) { + if (getLeaderboardTable().getColumn(selectionCheckboxColumnIndex) != selectionCheckboxColumn) { + insertColumn(selectionCheckboxColumnIndex, selectionCheckboxColumn); + } // else, the column is needed and is already in place + } else { + if (getLeaderboardTable().getColumn(selectionCheckboxColumnIndex) == selectionCheckboxColumn) { + removeColumn(selectionCheckboxColumnIndex); + } + } + } else { + if (showSelectionCheckbox) { + insertColumn(selectionCheckboxColumnIndex, selectionCheckboxColumn); + } + } + return indexOfNextColumn; + } + + /** + * @return the 0-based index for the next column + */ + private int ensureSailIDAndCompetitorColumn(int columnIndexWhereToInsertTheNextColumn) { + if (isShowCompetitorSailId()) { + if (getLeaderboardTable().getColumnCount() <= columnIndexWhereToInsertTheNextColumn + || !(getLeaderboardTable().getColumn(columnIndexWhereToInsertTheNextColumn) instanceof SailIDColumn)) { + insertColumn(columnIndexWhereToInsertTheNextColumn, new SailIDColumn(new CompetitorFetcher() { + @Override + public CompetitorDTO getCompetitor(LeaderboardRowDTO t) { + return t.competitor; + } + })); + } + columnIndexWhereToInsertTheNextColumn++; + } else { + if (getLeaderboardTable().getColumnCount() > columnIndexWhereToInsertTheNextColumn + && getLeaderboardTable().getColumn(columnIndexWhereToInsertTheNextColumn) instanceof SailIDColumn) { + removeColumn(columnIndexWhereToInsertTheNextColumn); + } + } + if (isShowCompetitorFullName()) { + if (getLeaderboardTable().getColumnCount() <= columnIndexWhereToInsertTheNextColumn + || !(getLeaderboardTable().getColumn(columnIndexWhereToInsertTheNextColumn) instanceof CompetitorColumn)) { + insertColumn(columnIndexWhereToInsertTheNextColumn, createCompetitorColumn()); + } + columnIndexWhereToInsertTheNextColumn++; + } else { + if (getLeaderboardTable().getColumnCount() > columnIndexWhereToInsertTheNextColumn + && getLeaderboardTable().getColumn(columnIndexWhereToInsertTheNextColumn) instanceof CompetitorColumn) { + removeColumn(columnIndexWhereToInsertTheNextColumn); + } + } + return columnIndexWhereToInsertTheNextColumn; + } + + protected CompetitorColumn createCompetitorColumn() { + return new CompetitorColumn(new CompetitorColumnBase(this, getStringMessages(), + new CompetitorFetcher() { + @Override + public CompetitorDTO getCompetitor(LeaderboardRowDTO t) { + return t.competitor; + } + })); + } + + private void ensureTotalsColumn() { + // add a totals column on the right + if (getLeaderboardTable().getColumnCount() == 0 + || !(getLeaderboardTable().getColumn(getLeaderboardTable().getColumnCount() - 1) instanceof TotalsColumn)) { + addColumn(new TotalsColumn(TOTAL_COLUMN_STYLE)); + } + } + + private void ensureTotalRacesSailedColumn() { + // add a totals column on the right + if (getLeaderboardTable().getColumnCount() == 0 + || !(getLeaderboardTable().getColumn(getLeaderboardTable().getColumnCount() - 2) instanceof TotalRacesCompletedColumn)) { + insertColumn(getLeaderboardTable().getColumnCount() - 1, new TotalRacesCompletedColumn(TOTAL_COLUMN_STYLE)); + } + } + + private void ensureNoTotalRacesSailedColumn() { + if ((getLeaderboardTable().getColumn(getLeaderboardTable().getColumnCount() - 2) instanceof TotalRacesCompletedColumn)) { + removeColumn(getLeaderboardTable().getColumnCount() - 2); + } + } + + protected void updateTotalRacesSailedColumn() { + final boolean showTotalRacesCompletedColumn = isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(); + if (showTotalRacesCompletedColumn) { + ensureTotalRacesSailedColumn(); + } else { + ensureNoTotalRacesSailedColumn(); + } + } + + /** + * If the leaderboard {@link LeaderboardDTO#hasCarriedPoints has carried points} and if column #1 + * (second column, right of the competitor column) does not exist or is not of type {@link CarryColumn}, all columns + * starting from #1 will be removed and a {@link CarryColumn} will be added. If the leaderboard has no carried + * points but the display still shows a carry column, the column is removed. + * + * @param zeroBasedIndexOfCarryColumn + * the 0-based column index where to put the carry column, if any + * @return the 0-based index of the next column following this column if the carry column is to be shown, or + * zeroBasedIndexOfCarryColumn otherwise + */ + protected int updateCarryColumn(LeaderboardDTO leaderboard, int zeroBasedIndexOfCarryColumn) { + final boolean needsCarryColumn = leaderboard != null && leaderboard.hasCarriedPoints; + if (needsCarryColumn) { + ensureCarryColumn(zeroBasedIndexOfCarryColumn); + } else { + ensureNoCarryColumn(zeroBasedIndexOfCarryColumn); + } + return needsCarryColumn ? zeroBasedIndexOfCarryColumn+1 : zeroBasedIndexOfCarryColumn; + } + + private void ensureNoCarryColumn(int zeroBasedIndexOfCarryColumn) { + if (getLeaderboardTable().getColumnCount() > zeroBasedIndexOfCarryColumn + && getLeaderboardTable().getColumn(zeroBasedIndexOfCarryColumn) instanceof CarryColumn) { + removeColumn(zeroBasedIndexOfCarryColumn); + } + } + + protected void ensureCarryColumn(int zeroBasedIndexOfCarryColumn) { + if (getLeaderboardTable().getColumnCount() <= zeroBasedIndexOfCarryColumn + || !(getLeaderboardTable().getColumn(zeroBasedIndexOfCarryColumn) instanceof CarryColumn)) { + while (getLeaderboardTable().getColumnCount() > zeroBasedIndexOfCarryColumn) { + removeColumn(zeroBasedIndexOfCarryColumn); + } + addColumn(createCarryColumn()); + } + } + + protected CarryColumn createCarryColumn() { + return new CarryColumn(); + } + + public SortedCellTable getLeaderboardTable() { + return leaderboardTable; + } + + protected SailingServiceAsync getSailingService() { + return sailingService; + } + + protected String getLeaderboardName() { + return leaderboardName; + } + + protected void setLeaderboardName(String leaderboardName) { + this.leaderboardName = leaderboardName; + } + + protected ErrorReporter getErrorReporter() { + return errorReporter; + } + + protected ListDataProvider getData() { + return getLeaderboardTable().getDataProvider(); + } + + /** + * @param newTime + * ignored; may be null. The time for loading the leaderboard is determined using + * {@link #getLeaderboardDisplayDate()}. + */ + @Override + public void timeChanged(Date newTime, Date oldTime) { + loadCompleteLeaderboard(getLeaderboardDisplayDate()); + } + + @Override + public void playStateChanged(PlayStates playState, PlayModes playMode) { + currentlyHandlingPlayStateChange = true; + + if(!isEmbedded) { + playPause.setHTML(getPlayPauseImgHtml(playState)); + playPause.setTitle(playState == PlayStates.Playing ? stringMessages.pauseAutomaticRefresh() : stringMessages.autoRefresh()); + } + if (!settingsUpdatedExplicitly && playMode != oldPlayMode) { + // if settings weren't explicitly modified, auto-switch to live mode settings and sort for + // any pre-selected race; we need to copy the previously selected race columns to the new RaceColumnSelection + updateSettings(LeaderboardSettingsFactory.getInstance().createNewSettingsForPlayMode( + playMode, + /* don't touch columnToSort if no race was pre-selected */ preSelectedRace == null ? null + : preSelectedRace.getRaceName(), + /* don't change nameOfRaceColumnToShow */null, + /* set nameOfRaceToShow if race was pre-selected */preSelectedRace == null ? null : preSelectedRace + .getRaceName(), getRaceColumnSelection(), /* leave showRegattaRank and overall details unchanged */ null, + /* take into account state of competitor columns*/isShowCompetitorSailId(), isShowCompetitorFullName())); + } + currentlyHandlingPlayStateChange = false; + oldPlayMode = playMode; + } + + @Override + public void playSpeedFactorChanged(double newPlaySpeedFactor) { + // nothing to do + } + + private List getTrackedRacesIdentifiers() { + List result = new ArrayList(); + for (RaceColumnDTO raceColumn : getLeaderboard().getRaceList()) { + for (FleetDTO fleet : raceColumn.getFleets()) { + if (raceColumn.getRaceIdentifier(fleet) != null) { + result.add(raceColumn.getRaceIdentifier(fleet)); + } + } + } + return result; + } + + @Override + public boolean hasSettings() { + return true; + } + + @Override + public boolean hasToolbar() { + return false; + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return new LeaderboardSettingsDialogComponent(Collections.unmodifiableList(selectedManeuverDetails), + Collections.unmodifiableList(selectedLegDetails), Collections.unmodifiableList(selectedRaceDetails), + Collections.unmodifiableList(selectedOverallDetailColumns), /* All races to select */ leaderboard.getRaceList(), + raceColumnSelection.getSelectedRaceColumnsOrderedAsInLeaderboard(leaderboard), raceColumnSelection, autoExpandPreSelectedRace, + isShowAddedScores(), timer.getRefreshInterval(), isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(), + isShowCompetitorSailId(), isShowCompetitorFullName(), stringMessages); + } + + @Override + public String getLocalizedShortName() { + return stringMessages.leaderboard(); + } + + private LeaderboardRowDTO getRow(CompetitorDTO competitor) { + synchronized (getData().getList()) { + for (LeaderboardRowDTO row : getData().getList()) { + if (row.competitor.equals(competitor)) { + return row; + } + } + } + return null; + } + + @Override + public void addedToSelection(CompetitorDTO competitor) { + LeaderboardRowDTO row = getRow(competitor); + if (row != null) { + leaderboardSelectionModel.setSelected(row, true); + } + } + + @Override + public void removedFromSelection(CompetitorDTO competitor) { + LeaderboardRowDTO row = getRow(competitor); + if (row != null) { + leaderboardSelectionModel.setSelected(row, false); + } + } + + private Iterable getSelectedRows() { + return leaderboardSelectionModel.getSelectedSet(); + } + + @Override + public void competitorsListChanged(Iterable competitors) { + setFilterControlStatus(); + if (timer.isInitialized()) { + timeChanged(timer.getTime(), null); + } + } + + @Override + public void filteredCompetitorsListChanged(Iterable filteredCompetitors) { + setFilterControlStatus(); + updateLeaderboard(getLeaderboard()); + } + + @Override + public void filterChanged(FilterSet> oldFilterSet, + FilterSet> newFilterSet) { + // nothing to do; if the list of filtered competitors has changed, a separate call to filteredCompetitorsListChanged will occur + setFilterControlStatus(); + } + + public RaceColumnSelection getRaceColumnSelection() { + return raceColumnSelection; + } + + public void removeAllListeners() { + if (raceTimesInfoProviderListener != null) { + getRaceTimesInfoProvider().removeRaceTimesInfoProviderListener(raceTimesInfoProviderListener); + } + if (raceColumnSelection != null && raceColumnSelection.getType() == RaceColumnSelectionStrategies.LAST_N) { + getRaceTimesInfoProvider().removeRaceTimesInfoProviderListener( + (LastNRacesColumnSelection) raceColumnSelection); + } + if (timer != null) { + timer.removeTimeListener(this); + } + if (leaderboardUpdateListener != null) { + leaderboardUpdateListener.clear(); + } + } + + protected Timer getTimer() { + return timer; + } + + protected void blurFocusedElementAfterSelectionChange() { + // now "blur" the selected leaderboard element because it seems to cause the cell table to scroll to its top; + // see bug 2093. + blur(); + final ScheduledCommand blurCommand = new ScheduledCommand() { + @Override + public void execute() { + blur(); + } + }; + Scheduler.get().scheduleDeferred(blurCommand); + } + + private void blur() { + if (elementToBlur != null) { + elementToBlur.blur(); + } + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (visible) { + timeChanged(getLeaderboardDisplayDate(), null); + } + } + + public void addLeaderboardUpdateListener(LeaderboardUpdateListener listener) { + this.leaderboardUpdateListener.add(listener); + } + + protected void informLeaderboardUpdateListenersAboutLeaderboardUpdated(LeaderboardDTO leaderboard) { + for (LeaderboardUpdateListener listener : this.leaderboardUpdateListener) { + listener.updatedLeaderboard(leaderboard); + } + } + + protected void informLeaderboardUpdateListenersAboutRaceSelected(RaceIdentifier raceIdentifier, RaceColumnDTO raceColumn) { + for (LeaderboardUpdateListener listener : this.leaderboardUpdateListener) { + listener.currentRaceSelected(raceIdentifier, raceColumn); + } + } + + public boolean hasLiveRace() { + return getLeaderboard().hasLiveRace(timer.getLiveTimePointInMillis()); + } + + public String getLiveRacesText() { + String result = ""; + List> liveRaces = leaderboard.getLiveRaces(timer.getLiveTimePointInMillis()); + boolean isMeta = leaderboard.type.isMetaLeaderboard(); + if (!liveRaces.isEmpty()) { + if (liveRaces.size() == 1) { + com.sap.sse.common.Util.Pair liveRace = liveRaces.get(0); + String text = "'" + liveRace.getA().getRaceColumnName() + "'"; + result = isMeta ? stringMessages.regattaIsLive(text) : stringMessages.raceIsLive(text); + } else { + String names = ""; + for (com.sap.sse.common.Util.Pair liveRace : liveRaces) { + names += "'" + liveRace.getA().getRaceColumnName() + "', "; + } + // remove last ", " + names = names.substring(0, names.length() - 2); + result = isMeta ? stringMessages.regattasAreLive(names) : stringMessages.racesAreLive(names); + } + } + return result; + } + + @Override + public String getDependentCssClassName() { + return "leaderboard"; + } + + @Override + public void addBusyStateChangeListener(BusyStateChangeListener listener) { + busyStateChangeListeners.add(listener); + } + + @Override + public void removeBusyStateChangeListener(BusyStateChangeListener listener) { + busyStateChangeListeners.remove(listener); + } + + @Override + public boolean isBusy() { + return busyIndicator.isBusy(); + } + + @Override + public void setBusyState(boolean isBusy) { + if (busyIndicator.isBusy() != isBusy) { + busyIndicator.setBusy(isBusy); + for (BusyStateChangeListener listener : busyStateChangeListeners) { + listener.onBusyStateChange(isBusy); + } + } + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardSettingsFactory.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardSettingsFactory.java index e2d4275010b..8cdeefda0ea 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardSettingsFactory.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardSettingsFactory.java @@ -78,11 +78,11 @@ public class LeaderboardSettingsFactory { /* refresh interval */ null, /* name of race to sort */ nameOfRaceToSort, /* ascending */ true, /* updateUponPlayStateChange */ true, raceColumnSelection.getType(), /*showAddedScores*/ false, /*showOverallRacesCompleted*/ false, - /*showCompetitorSailIdColumns*/showCompetitorSailIdColumn, /*showCompetitorFullNameColumn*/showCompetitorNameColumn); + showCompetitorSailIdColumn, showCompetitorNameColumn); break; case Replay: settings = createNewDefaultSettings(namesOfRaceColumnsToShow, namesOfRacesToShow, nameOfRaceToSort, /* autoExpandFirstRace */ - nameOfRaceColumnToShow != null, showRegattaRank); + nameOfRaceColumnToShow != null, showRegattaRank, showCompetitorSailIdColumn, showCompetitorNameColumn); break; } return settings; @@ -127,28 +127,32 @@ public class LeaderboardSettingsFactory { * such a meta leaderboard exists; if multiple such meta leaderboards exist, they are all shown */ public LeaderboardSettings createNewDefaultSettings(List namesOfRaceColumnsToShow, - List namesOfRacesToShow, String nameOfRaceToSort, boolean autoExpandPreSelectedRace, Boolean showRegattaRank) { + List namesOfRacesToShow, String nameOfRaceToSort, boolean autoExpandPreSelectedRace, Boolean showRegattaRank, + boolean showCompetitorSailIdColumn, boolean showCompetitorFullNameColumn) { final List overallDetails = createOverallDetailsForShowRegattaRank(showRegattaRank); return createNewDefaultSettings(namesOfRaceColumnsToShow, namesOfRacesToShow, overallDetails, - nameOfRaceToSort, autoExpandPreSelectedRace, /* refreshIntervalMillis */ null); + nameOfRaceToSort, autoExpandPreSelectedRace, /* refreshIntervalMillis */ null, + showCompetitorSailIdColumn, showCompetitorFullNameColumn); } /** - * Like {@link #createNewDefaultSettings(List, List, String, boolean, boolean)}, only that an additional refresh interval for auto-refresh + * Like {@link #createNewDefaultSettings(List, List, String, boolean, boolean, boolean, boolean)}, only that an additional refresh interval for auto-refresh * may be specified; if null, no auto-refresh shall be performed */ - public LeaderboardSettings createNewDefaultSettings(List namesOfRaceColumnsToShow, + private LeaderboardSettings createNewDefaultSettings(List namesOfRaceColumnsToShow, List namesOfRacesToShow, List overallDetailsToShow, String nameOfRaceToSort, - boolean autoExpandPreSelectedRace, Long refreshIntervalMillis) { + boolean autoExpandPreSelectedRace, Long refreshIntervalMillis, boolean showCompetitorSailIdColumns, boolean showCompetitorFullNameColumn) { return createNewDefaultSettings(namesOfRaceColumnsToShow, namesOfRacesToShow, overallDetailsToShow, nameOfRaceToSort, autoExpandPreSelectedRace, refreshIntervalMillis, - /* numberOfLastRacesToShow */null, /* raceColumnSelectionStrategy */ RaceColumnSelectionStrategies.EXPLICIT); + /* numberOfLastRacesToShow */null, /* raceColumnSelectionStrategy */ RaceColumnSelectionStrategies.EXPLICIT, + showCompetitorSailIdColumns, showCompetitorFullNameColumn); } public LeaderboardSettings createNewDefaultSettings(List namesOfRaceColumnsToShow, List namesOfRacesToShow, List overallDetailsToShow, String nameOfRaceToSort, boolean autoExpandPreSelectedRace, Long refreshIntervalMillis, Integer numberOfLastRacesToShow, - RaceColumnSelectionStrategies raceColumnSelectionStrategy) { + RaceColumnSelectionStrategies raceColumnSelectionStrategy, boolean showCompetitorSailIdColumns, + boolean showCompetitorFullNameColumn) { if (namesOfRaceColumnsToShow != null && namesOfRacesToShow != null) { throw new IllegalArgumentException("Can specify race columns either by column or by race name, not both"); } @@ -168,7 +172,7 @@ public class LeaderboardSettingsFactory { autoExpandPreSelectedRace, refreshIntervalMillis, /* sort by column */ nameOfRaceToSort, /* ascending */ true, /* updateUponPlayStateChange */ true, raceColumnSelectionStrategy, /*showAddedScores*/ false, /*showOverallRacesCompleted*/ false, - /*showCompetitorSailIdColumns*/true, /*showCompetitorFullNameColumn*/true); + showCompetitorSailIdColumns, showCompetitorFullNameColumn); } public LeaderboardSettings mergeLeaderboardSettings(LeaderboardSettings settingsWithRaceSelection, LeaderboardSettings settingsWithDetails) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardUrlConfigurationDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardUrlConfigurationDialog.java index b3cc3e30c4f..dec01ccef93 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardUrlConfigurationDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardUrlConfigurationDialog.java @@ -106,7 +106,7 @@ public class LeaderboardUrlConfigurationDialog extends SettingsDialog implements TimeListener { +public class MultiLeaderboardPanel extends AbstractLazyComponent implements TimeListener, SelectedLeaderboardChangeProvider { private LeaderboardPanel selectedLeaderboardPanel; private FlowPanel selectedLeaderboardFlowPanel; @@ -60,7 +62,8 @@ public class MultiLeaderboardPanel extends AbstractLazyComponent leaderboardUpdateListener; + private final Set leaderboardUpdateListeners; + private final Set selectedLeaderboardChangeListeners; public MultiLeaderboardPanel(SailingServiceAsync sailingService, String metaLeaderboardName, AsyncActionsExecutor asyncActionsExecutor, Timer timer, boolean isEmbedded, String preselectedLeaderboardName, RaceIdentifier preselectedRace, @@ -81,13 +84,14 @@ public class MultiLeaderboardPanel extends AbstractLazyComponent>(); leaderboardNamesAndSettings = new HashMap(); - leaderboardUpdateListener = new ArrayList(); + leaderboardUpdateListeners = new HashSet(); + selectedLeaderboardChangeListeners = new HashSet(); } private LeaderboardSettings getOrCreateLeaderboardSettings(String leaderboardName, LeaderboardSettings currentLeaderboardSettings) { LeaderboardSettings newLeaderboardSettings = leaderboardNamesAndSettings.get(leaderboardName); if(newLeaderboardSettings == null) { - newLeaderboardSettings = LeaderboardSettingsFactory.getInstance().createNewDefaultSettings(null, null, null, false, /* showRegattaRank */ true); + newLeaderboardSettings = LeaderboardSettingsFactory.getInstance().createNewDefaultSettings(null, null, null, false, /* showRegattaRank */ true, /* showCompetitorSailIdColumn */ true, /* showCompetitorFullNameColumn */ true); } if(currentLeaderboardSettings != null) { newLeaderboardSettings = LeaderboardSettingsFactory.getInstance().mergeLeaderboardSettings(newLeaderboardSettings, currentLeaderboardSettings); @@ -152,7 +156,7 @@ public class MultiLeaderboardPanel extends AbstractLazyComponent> newLeaderboardNamesAndDisplayNames) { @@ -177,6 +181,10 @@ public class MultiLeaderboardPanel extends AbstractLazyComponentnull LeaderboardPanel + */ + void onSelectedLeaderboardChanged(LeaderboardPanel selectedLeaderboard); + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/SelectedLeaderboardChangeProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/SelectedLeaderboardChangeProvider.java new file mode 100644 index 00000000000..e19f52bd4bb --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/SelectedLeaderboardChangeProvider.java @@ -0,0 +1,11 @@ +package com.sap.sailing.gwt.ui.leaderboard; + + +public interface SelectedLeaderboardChangeProvider { + + void addSelectedLeaderboardChangeListener(SelectedLeaderboardChangeListener listener); + + void removeSelectedLeaderboardChangeListener(SelectedLeaderboardChangeListener listener); + + void setSelectedLeaderboard(LeaderboardPanel selectedLeaderboard); +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/EditableLeaderboardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/EditableLeaderboardPanel.java index 78e4426adf6..cb204479253 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/EditableLeaderboardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/EditableLeaderboardPanel.java @@ -319,13 +319,13 @@ public class EditableLeaderboardPanel extends LeaderboardPanel { final RowUpdateWhiteboard whiteboard = new RowUpdateWhiteboard( EditableLeaderboardPanel.this.getData()); getWhiteboardOwner().whiteboardProduced(whiteboard); - getBusyIndicator().setBusy(true); + setBusyState(true); getSailingService().updateLeaderboardMaxPointsReason(getLeaderboardName(), row.competitor.getIdAsString(), raceColumnName, value == null || value.trim().length() == 0 ? null : MaxPointsReason.valueOf(value.trim()), getLeaderboardDisplayDate(), new AsyncCallback>() { @Override public void onFailure(Throwable t) { - getBusyIndicator().setBusy(false); + setBusyState(false); getErrorReporter().reportError( "Error trying to update max points reason for competitor " + row.competitor.getName() + " in leaderboard " + getLeaderboardName() @@ -334,7 +334,7 @@ public class EditableLeaderboardPanel extends LeaderboardPanel { @Override public void onSuccess(Util.Triple newNetAndTotalPointsAndIsCorrected) { - getBusyIndicator().setBusy(false); + setBusyState(false); row.fieldsByRaceColumnName.get(raceColumnName).reasonForMaxPoints = value == null || value.length() == 0 ? null : MaxPointsReason.valueOf(value.trim()); row.fieldsByRaceColumnName.get(raceColumnName).netPoints = newNetAndTotalPointsAndIsCorrected.getA(); @@ -444,14 +444,14 @@ public class EditableLeaderboardPanel extends LeaderboardPanel { final RowUpdateWhiteboard whiteboard = new RowUpdateWhiteboard( EditableLeaderboardPanel.this.getData()); getWhiteboardOwner().whiteboardProduced(whiteboard); - getBusyIndicator().setBusy(true); + setBusyState(true); getSailingService().updateLeaderboardScoreCorrection(getLeaderboardName(), row.competitor.getIdAsString(), raceColumnName, value == null || value.trim().length() == 0 ? null : value.trim().equals("n/a") ? null : Double.valueOf(value.trim()), getLeaderboardDisplayDate(), new AsyncCallback>() { @Override public void onFailure(Throwable t) { - getBusyIndicator().setBusy(false); + setBusyState(false); getErrorReporter().reportError("Error trying to update score correction for competitor "+ row.competitor.getName()+" in leaderboard "+getLeaderboardName()+ " for race "+raceColumnName+": "+t.getMessage()+ @@ -460,7 +460,7 @@ public class EditableLeaderboardPanel extends LeaderboardPanel { @Override public void onSuccess(Util.Triple newNetAndTotalPointsAndIsCorrected) { - getBusyIndicator().setBusy(false); + setBusyState(false); final LeaderboardEntryDTO leaderboardEntryDTO = row.fieldsByRaceColumnName.get(raceColumnName); leaderboardEntryDTO.netPoints = value == null || value.length() == 0 ? newNetAndTotalPointsAndIsCorrected .getA() : Double.valueOf(value.trim()); @@ -523,13 +523,13 @@ public class EditableLeaderboardPanel extends LeaderboardPanel { row.fieldsByRaceColumnName.get(raceColumnName).netPoints, new DialogCallback>() { @Override public void ok(final Util.Pair editedObject) { - getBusyIndicator().setBusy(true); + setBusyState(true); getSailingService().updateLeaderboardScoreCorrection(getLeaderboardName(), row.competitor.getIdAsString(), raceColumnName, editedObject.getB(), getLeaderboardDisplayDate(), new AsyncCallback>() { @Override public void onFailure(Throwable t) { - getBusyIndicator().setBusy(false); + setBusyState(false); getErrorReporter().reportError("Error trying to update score correction for competitor "+ row.competitor.getName()+" in leaderboard "+getLeaderboardName()+ " for race "+raceColumnName+": "+t.getMessage()+ @@ -543,7 +543,7 @@ public class EditableLeaderboardPanel extends LeaderboardPanel { new AsyncCallback>() { @Override public void onFailure(Throwable t) { - getBusyIndicator().setBusy(false); + setBusyState(false); getErrorReporter().reportError("Error trying to update score correction for competitor "+ row.competitor.getName()+" in leaderboard "+getLeaderboardName()+ " for race "+raceColumnName+": "+t.getMessage()+ @@ -552,7 +552,7 @@ public class EditableLeaderboardPanel extends LeaderboardPanel { @Override public void onSuccess(Util.Triple newNetAndTotalPointsAndIsCorrected) { - getBusyIndicator().setBusy(false); + setBusyState(false); final LeaderboardEntryDTO leaderboardEntryDTO = row.fieldsByRaceColumnName.get(raceColumnName); leaderboardEntryDTO.reasonForMaxPoints = editedObject.getA(); leaderboardEntryDTO.netPoints = newNetAndTotalPointsAndIsCorrected.getA(); @@ -584,7 +584,7 @@ public class EditableLeaderboardPanel extends LeaderboardPanel { String leaderboardName, String leaderboardGroupName, final ErrorReporter errorReporter, final StringMessages stringMessages, UserAgentDetails userAgent) { super(sailingService, asyncActionsExecutor, LeaderboardSettingsFactory.getInstance().createNewDefaultSettings( - /* racesToShow */ null, /* namesOfRacesToShow */ null, null, /* autoExpandFirstRace */ false, /* showRegattaRank */ true), + /* racesToShow */ null, /* namesOfRacesToShow */ null, null, /* autoExpandFirstRace */ false, /* showRegattaRank */ true, /* showCompetitorSailIdColumn */ true, /* showCompetitorFullNameColumn */ true), new CompetitorSelectionModel(/* hasMultiSelection */true), leaderboardName, errorReporter, stringMessages, userAgent, /* showRaceDetails */ true); suppressedCompetitorsShown = new ListDataProvider(new ArrayList()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/MatchAndApplyScoreCorrectionsDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/MatchAndApplyScoreCorrectionsDialog.java index 61bf83687bb..066d0212239 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/MatchAndApplyScoreCorrectionsDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/MatchAndApplyScoreCorrectionsDialog.java @@ -409,11 +409,11 @@ public class MatchAndApplyScoreCorrectionsDialog extends DataEntryDialog() { @Override public void onFailure(Throwable caught) { - leaderboardPanel.getBusyIndicator().setBusy(false); + leaderboardPanel.setBusyState(false); errorReporter.reportError(stringMessages.errorUpdatingScoresForLeaderboard(result.getLeaderboardName(), caught.getMessage())); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/ResultSelectionAndApplyDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/ResultSelectionAndApplyDialog.java index ffe969fe32e..660be8c30ad 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/ResultSelectionAndApplyDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/ResultSelectionAndApplyDialog.java @@ -214,19 +214,19 @@ public class ResultSelectionAndApplyDialog extends DataEntryDialog() { @Override public void onFailure(Throwable caught) { - leaderboardPanel.getBusyIndicator().setBusy(false); + leaderboardPanel.setBusyState(false); errorReporter.reportError(stringMessages.errorObtainingScoreCorrections(scoreCorrectionProviderName, eventName, boatClassName, timePointWhenResultPublished.toString(), caught.getMessage())); } @Override public void onSuccess(RegattaScoreCorrectionDTO result) { - leaderboardPanel.getBusyIndicator().setBusy(false); + leaderboardPanel.setBusyState(false); new MatchAndApplyScoreCorrectionsDialog(leaderboardPanel, stringMessages, sailingService, errorReporter, result).show(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/EmbeddedMapAndWindChartEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/EmbeddedMapAndWindChartEntryPoint.java index 926e3ca3efe..868d60ac7c4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/EmbeddedMapAndWindChartEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/EmbeddedMapAndWindChartEntryPoint.java @@ -151,7 +151,8 @@ public class EmbeddedMapAndWindChartEntryPoint extends AbstractSailingEntryPoint // Use a TimePanel to manage wind chart zoom, although the TimePanel itself is not being displayed; // let the time panel always return to "live" mode. final TimePanel timePanel = new TimePanel( - timer, timeRangeWithZoomProvider, getStringMessages(), /* canReplayWhileLive */ false) { + timer, timeRangeWithZoomProvider, getStringMessages(), /* canReplayWhileLive */ false, + /* isScreenLargeEnoughToOfferChartSupport set to true iff wind chart will be displayed */ showWindChart) { protected boolean isLiveModeToBeMadePossible() { return true; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardEntryPoint.java index 219d66c3acb..488813e295a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardEntryPoint.java @@ -1,197 +1,205 @@ - -package com.sap.sailing.gwt.ui.raceboard; - -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -import com.google.gwt.core.client.GWT; -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.rpc.ServiceDefTarget; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.DockLayoutPanel; -import com.google.gwt.user.client.ui.FlowPanel; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.RootLayoutPanel; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.gwt.ui.client.AbstractSailingEntryPoint; -import com.sap.sailing.gwt.ui.client.LogoAndTitlePanel; -import com.sap.sailing.gwt.ui.client.MediaService; -import com.sap.sailing.gwt.ui.client.MediaServiceAsync; -import com.sap.sailing.gwt.ui.client.RaceSelectionModel; -import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; -import com.sap.sailing.gwt.ui.client.RemoteServiceMappingConstants; -import com.sap.sailing.gwt.ui.shared.RaceWithCompetitorsDTO; -import com.sap.sailing.gwt.ui.shared.RaceboardDataDTO; -import com.sap.sse.gwt.client.EntryPointHelper; -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.shared.GwtHttpRequestUtils; - -public class RaceBoardEntryPoint extends AbstractSailingEntryPoint { - private RaceWithCompetitorsDTO selectedRace; - - private static final String PARAM_REGATTA_NAME = "regattaName"; - private static final String PARAM_RACE_NAME = "raceName"; - private static final String PARAM_LEADERBOARD_NAME = "leaderboardName"; - private static final String PARAM_LEADERBOARD_GROUP_NAME = "leaderboardGroupName"; - private static final String PARAM_EVENT_ID = "eventId"; - - private String regattaName; - private String raceName; - private String leaderboardName; - private String leaderboardGroupName; - private UUID eventId; - private RaceBoardViewConfiguration raceboardViewConfig; - - private final MediaServiceAsync mediaService = GWT.create(MediaService.class); - - @Override - protected void doOnModuleLoad() { - super.doOnModuleLoad(); - EntryPointHelper.registerASyncService((ServiceDefTarget) mediaService, RemoteServiceMappingConstants.mediaServiceRemotePath); - // read mandatory parameters - regattaName = Window.Location.getParameter(PARAM_REGATTA_NAME); - raceName = Window.Location.getParameter(PARAM_RACE_NAME); - String leaderboardNameParamValue = Window.Location.getParameter(PARAM_LEADERBOARD_NAME); - String leaderboardGroupNameParamValue = Window.Location.getParameter(PARAM_LEADERBOARD_GROUP_NAME); - if (leaderboardNameParamValue != null && !leaderboardNameParamValue.isEmpty()) { - leaderboardName = leaderboardNameParamValue; - } - if (leaderboardGroupNameParamValue != null && !leaderboardGroupNameParamValue.isEmpty()) { - leaderboardGroupName = leaderboardGroupNameParamValue; - } - String eventIdParamValue = Window.Location.getParameter(PARAM_EVENT_ID); - if (eventIdParamValue != null && !eventIdParamValue.isEmpty()) { - eventId = UUID.fromString(eventIdParamValue); - } - if (leaderboardGroupNameParamValue != null && !leaderboardGroupNameParamValue.isEmpty()) { - leaderboardGroupName = leaderboardGroupNameParamValue; - } - if (regattaName == null || regattaName.isEmpty() || raceName == null || raceName.isEmpty() || - leaderboardName == null || leaderboardName.isEmpty()) { - createErrorPage("This page requires a valid regatta name, race name and leaderboard name."); - return; - } - - // read optional parameters - boolean showLeaderboard = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_LEADERBOARD, true /* default*/); - boolean showWindChart = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_WINDCHART, false /* default*/); - boolean showCompetitorsChart = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_COMPETITORSCHART, false /* default*/); - boolean showViewStreamlets = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_STREAMLETS, false /* default*/); - boolean showViewStreamletColors = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_STREAMLET_COLORS, false /* default*/); - boolean showViewSimulation = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_SIMULATION, true /* default*/); - String activeCompetitorsFilterSetName = GwtHttpRequestUtils.getStringParameter(RaceBoardViewConfiguration.PARAM_VIEW_COMPETITOR_FILTER, null /* default*/); - final boolean canReplayWhileLiveIsPossible = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_CAN_REPLAY_DURING_LIVE_RACES, false); - final boolean autoSelectMedia = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_AUTOSELECT_MEDIA, true); - final String defaultMedia = GwtHttpRequestUtils.getStringParameter(RaceBoardViewConfiguration.PARAM_DEFAULT_MEDIA, null /* default*/); - final boolean showMapControls = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_MAPCONTROLS, true /* default*/); - raceboardViewConfig = new RaceBoardViewConfiguration(activeCompetitorsFilterSetName, showLeaderboard, - showWindChart, showCompetitorsChart, showViewStreamlets, showViewStreamletColors, showViewSimulation, canReplayWhileLiveIsPossible, autoSelectMedia, defaultMedia); - - sailingService.getRaceboardData(regattaName, raceName, leaderboardName, leaderboardGroupName, eventId, new AsyncCallback() { - @Override - public void onSuccess(RaceboardDataDTO result) { - checkUrlParameters(result, canReplayWhileLiveIsPossible, showMapControls); - } - - @Override - public void onFailure(Throwable caught) { - reportError("Error trying to create the raceboard: " + caught.getMessage()); - } - }); - } - - private void createErrorPage(String message) { - final DockLayoutPanel vp = new DockLayoutPanel(Unit.PX); - LogoAndTitlePanel logoAndTitlePanel = new LogoAndTitlePanel(getStringMessages(), this, getUserService()); - logoAndTitlePanel.addStyleName("LogoAndTitlePanel"); - RootLayoutPanel.get().add(vp); - vp.addNorth(logoAndTitlePanel, 100); - vp.add(new Label(message)); - } - - private void checkUrlParameters(RaceboardDataDTO raceboardData, boolean canReplayWhileLiveIsPossible, boolean showMapControls) { - if (!raceboardData.isValidLeaderboard()) { - createErrorPage(getStringMessages().noSuchLeaderboard()); - return; - } - if (eventId != null && !raceboardData.isValidEvent()) { - createErrorPage(getStringMessages().noSuchEvent()); - } - if (leaderboardGroupName != null) { - if(!raceboardData.isValidLeaderboardGroup()) { - createErrorPage(getStringMessages().leaderboardNotContainedInLeaderboardGroup(leaderboardName, leaderboardGroupName)); - return; - } - if (eventId != null && raceboardData.isValidLeaderboardGroup() && !raceboardData.isValidEvent()) { - createErrorPage(getStringMessages().leaderboardGroupNotContainedInEvent(leaderboardGroupName, eventId.toString())); - return; - } - } - if (raceboardData.getRace() == null) { - createErrorPage("Could not obtain a race with name " + raceName + " for a regatta with name " + regattaName); - return; - } - selectedRace = raceboardData.getRace(); - Window.setTitle(selectedRace.getName()); - RaceSelectionModel raceSelectionModel = new RaceSelectionModel(); - List singletonList = Collections.singletonList(selectedRace.getRaceIdentifier()); - raceSelectionModel.setSelection(singletonList); - Timer timer = new Timer(PlayModes.Replay, 1000l); - AsyncActionsExecutor asyncActionsExecutor = new AsyncActionsExecutor(); - RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(sailingService, asyncActionsExecutor, this, singletonList, 5000l /* requestInterval*/); - RaceBoardPanel raceBoardPanel = new RaceBoardPanel(sailingService, mediaService, getUserService(), asyncActionsExecutor, - raceboardData.getCompetitorAndTheirBoats(), timer, raceSelectionModel, leaderboardName, leaderboardGroupName, eventId, - raceboardViewConfig, RaceBoardEntryPoint.this, getStringMessages(), userAgent, raceTimesInfoProvider, showMapControls); - - createRaceBoardInOneScreenMode(raceBoardPanel, raceboardViewConfig); - } - - private FlowPanel createTimePanel(RaceBoardPanel raceBoardPanel) { - FlowPanel timeLineInnerBgPanel = new FlowPanel(); - timeLineInnerBgPanel.addStyleName("timeLineInnerBgPanel"); - timeLineInnerBgPanel.add(raceBoardPanel.getTimePanel()); - - FlowPanel timeLineInnerPanel = new FlowPanel(); - timeLineInnerPanel.add(timeLineInnerBgPanel); - timeLineInnerPanel.addStyleName("timeLineInnerPanel"); - - FlowPanel timelinePanel = new FlowPanel(); - timelinePanel.add(timeLineInnerPanel); - timelinePanel.addStyleName("timeLinePanel"); - - return timelinePanel; - } - - private void createRaceBoardInOneScreenMode(final RaceBoardPanel raceBoardPanel, - RaceBoardViewConfiguration raceboardViewConfiguration) { - final DockLayoutPanel p = new DockLayoutPanel(Unit.PX); - RootLayoutPanel.get().add(p); - final FlowPanel timePanel = createTimePanel(raceBoardPanel); - final Button toggleButton = raceBoardPanel.getTimePanel().getAdvancedToggleButton(); - toggleButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - boolean advancedModeShown = raceBoardPanel.getTimePanel().toggleAdvancedMode(); - if (advancedModeShown) { - p.setWidgetSize(timePanel, 96); - toggleButton.removeStyleDependentName("Closed"); - toggleButton.addStyleDependentName("Open"); - } else { - p.setWidgetSize(timePanel, 67); - toggleButton.addStyleDependentName("Closed"); - toggleButton.removeStyleDependentName("Open"); - } - } - }); - p.addSouth(timePanel, 67); - p.add(raceBoardPanel); - p.addStyleName("dockLayoutPanel"); - } -} + +package com.sap.sailing.gwt.ui.raceboard; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.rpc.ServiceDefTarget; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.DockLayoutPanel; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.RootLayoutPanel; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.gwt.ui.client.AbstractSailingEntryPoint; +import com.sap.sailing.gwt.ui.client.LogoAndTitlePanel; +import com.sap.sailing.gwt.ui.client.MediaService; +import com.sap.sailing.gwt.ui.client.MediaServiceAsync; +import com.sap.sailing.gwt.ui.client.RaceSelectionModel; +import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; +import com.sap.sailing.gwt.ui.client.RemoteServiceMappingConstants; +import com.sap.sailing.gwt.ui.shared.RaceWithCompetitorsDTO; +import com.sap.sailing.gwt.ui.shared.RaceboardDataDTO; +import com.sap.sse.gwt.client.EntryPointHelper; +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.shared.GwtHttpRequestUtils; + +public class RaceBoardEntryPoint extends AbstractSailingEntryPoint { + private RaceWithCompetitorsDTO selectedRace; + + private static final String PARAM_REGATTA_NAME = "regattaName"; + private static final String PARAM_RACE_NAME = "raceName"; + private static final String PARAM_LEADERBOARD_NAME = "leaderboardName"; + private static final String PARAM_LEADERBOARD_GROUP_NAME = "leaderboardGroupName"; + private static final String PARAM_EVENT_ID = "eventId"; + + private String regattaName; + private String raceName; + private String leaderboardName; + private String leaderboardGroupName; + private UUID eventId; + private RaceBoardViewConfiguration raceboardViewConfig; + + private final MediaServiceAsync mediaService = GWT.create(MediaService.class); + + @Override + protected void doOnModuleLoad() { + super.doOnModuleLoad(); + EntryPointHelper.registerASyncService((ServiceDefTarget) mediaService, RemoteServiceMappingConstants.mediaServiceRemotePath); + // read mandatory parameters + regattaName = Window.Location.getParameter(PARAM_REGATTA_NAME); + raceName = Window.Location.getParameter(PARAM_RACE_NAME); + String leaderboardNameParamValue = Window.Location.getParameter(PARAM_LEADERBOARD_NAME); + String leaderboardGroupNameParamValue = Window.Location.getParameter(PARAM_LEADERBOARD_GROUP_NAME); + if (leaderboardNameParamValue != null && !leaderboardNameParamValue.isEmpty()) { + leaderboardName = leaderboardNameParamValue; + } + if (leaderboardGroupNameParamValue != null && !leaderboardGroupNameParamValue.isEmpty()) { + leaderboardGroupName = leaderboardGroupNameParamValue; + } + String eventIdParamValue = Window.Location.getParameter(PARAM_EVENT_ID); + if (eventIdParamValue != null && !eventIdParamValue.isEmpty()) { + eventId = UUID.fromString(eventIdParamValue); + } + if (leaderboardGroupNameParamValue != null && !leaderboardGroupNameParamValue.isEmpty()) { + leaderboardGroupName = leaderboardGroupNameParamValue; + } + if (regattaName == null || regattaName.isEmpty() || raceName == null || raceName.isEmpty() || + leaderboardName == null || leaderboardName.isEmpty()) { + createErrorPage("This page requires a valid regatta name, race name and leaderboard name."); + return; + } + + // read optional parameters + boolean showLeaderboard = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_LEADERBOARD, true /* default*/); + boolean showWindChart = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_WINDCHART, false /* default*/); + boolean showCompetitorsChart = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_COMPETITORSCHART, false /* default*/); + boolean showViewStreamlets = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_STREAMLETS, false /* default*/); + boolean showViewStreamletColors = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_STREAMLET_COLORS, false /* default*/); + boolean showViewSimulation = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_SIMULATION, true /* default*/); + String activeCompetitorsFilterSetName = GwtHttpRequestUtils.getStringParameter(RaceBoardViewConfiguration.PARAM_VIEW_COMPETITOR_FILTER, null /* default*/); + final boolean canReplayWhileLiveIsPossible = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_CAN_REPLAY_DURING_LIVE_RACES, false); + final boolean autoSelectMedia = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_AUTOSELECT_MEDIA, true); + final String defaultMedia = GwtHttpRequestUtils.getStringParameter(RaceBoardViewConfiguration.PARAM_DEFAULT_MEDIA, null /* default*/); + final boolean showMapControls = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_MAPCONTROLS, true /* default*/); + raceboardViewConfig = new RaceBoardViewConfiguration(activeCompetitorsFilterSetName, showLeaderboard, + showWindChart, showCompetitorsChart, showViewStreamlets, showViewStreamletColors, showViewSimulation, canReplayWhileLiveIsPossible, autoSelectMedia, defaultMedia); + + sailingService.getRaceboardData(regattaName, raceName, leaderboardName, leaderboardGroupName, eventId, new AsyncCallback() { + @Override + public void onSuccess(RaceboardDataDTO result) { + // Determine if the screen is large enough to display charts such as the competitor chart or the wind chart. + // This decision is made once based on the initial screen height. Resizing the window afterwards will have + // no impact on the chart support, i.e. they are available/unavailable based on the initial decision. + boolean isScreenLargeEnoughToOfferChartSupport = Document.get().getClientHeight() >= 600; + checkUrlParameters(result, canReplayWhileLiveIsPossible, showMapControls, + isScreenLargeEnoughToOfferChartSupport); + } + + @Override + public void onFailure(Throwable caught) { + reportError("Error trying to create the raceboard: " + caught.getMessage()); + } + }); + } + + private void createErrorPage(String message) { + final DockLayoutPanel vp = new DockLayoutPanel(Unit.PX); + LogoAndTitlePanel logoAndTitlePanel = new LogoAndTitlePanel(getStringMessages(), this, getUserService()); + logoAndTitlePanel.addStyleName("LogoAndTitlePanel"); + RootLayoutPanel.get().add(vp); + vp.addNorth(logoAndTitlePanel, 100); + vp.add(new Label(message)); + } + + private void checkUrlParameters(RaceboardDataDTO raceboardData, boolean canReplayWhileLiveIsPossible, boolean showMapControls, + boolean isScreenLargeEnoughToOfferChartSupport) { + if (!raceboardData.isValidLeaderboard()) { + createErrorPage(getStringMessages().noSuchLeaderboard()); + return; + } + if (eventId != null && !raceboardData.isValidEvent()) { + createErrorPage(getStringMessages().noSuchEvent()); + } + if (leaderboardGroupName != null) { + if(!raceboardData.isValidLeaderboardGroup()) { + createErrorPage(getStringMessages().leaderboardNotContainedInLeaderboardGroup(leaderboardName, leaderboardGroupName)); + return; + } + if (eventId != null && raceboardData.isValidLeaderboardGroup() && !raceboardData.isValidEvent()) { + createErrorPage(getStringMessages().leaderboardGroupNotContainedInEvent(leaderboardGroupName, eventId.toString())); + return; + } + } + if (raceboardData.getRace() == null) { + createErrorPage("Could not obtain a race with name " + raceName + " for a regatta with name " + regattaName); + return; + } + selectedRace = raceboardData.getRace(); + Window.setTitle(selectedRace.getName()); + RaceSelectionModel raceSelectionModel = new RaceSelectionModel(); + List singletonList = Collections.singletonList(selectedRace.getRaceIdentifier()); + raceSelectionModel.setSelection(singletonList); + Timer timer = new Timer(PlayModes.Replay, 1000l); + AsyncActionsExecutor asyncActionsExecutor = new AsyncActionsExecutor(); + RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(sailingService, asyncActionsExecutor, this, singletonList, 5000l /* requestInterval*/); + RaceBoardPanel raceBoardPanel = new RaceBoardPanel(sailingService, mediaService, getUserService(), asyncActionsExecutor, + raceboardData.getCompetitorAndTheirBoats(), timer, raceSelectionModel, leaderboardName, leaderboardGroupName, eventId, + raceboardViewConfig, RaceBoardEntryPoint.this, getStringMessages(), userAgent, raceTimesInfoProvider, showMapControls, + isScreenLargeEnoughToOfferChartSupport); + + createRaceBoardInOneScreenMode(raceBoardPanel, raceboardViewConfig); + } + + private FlowPanel createTimePanel(RaceBoardPanel raceBoardPanel) { + FlowPanel timeLineInnerBgPanel = new ResizableFlowPanel(); + timeLineInnerBgPanel.addStyleName("timeLineInnerBgPanel"); + timeLineInnerBgPanel.add(raceBoardPanel.getTimePanel()); + + FlowPanel timeLineInnerPanel = new ResizableFlowPanel(); + timeLineInnerPanel.add(timeLineInnerBgPanel); + timeLineInnerPanel.addStyleName("timeLineInnerPanel"); + + FlowPanel timelinePanel = new ResizableFlowPanel(); + timelinePanel.add(timeLineInnerPanel); + timelinePanel.addStyleName("timeLinePanel"); + + return timelinePanel; + } + + private void createRaceBoardInOneScreenMode(final RaceBoardPanel raceBoardPanel, + RaceBoardViewConfiguration raceboardViewConfiguration) { + final DockLayoutPanel p = new DockLayoutPanel(Unit.PX); + RootLayoutPanel.get().add(p); + final FlowPanel timePanel = createTimePanel(raceBoardPanel); + final Button toggleButton = raceBoardPanel.getTimePanel().getAdvancedToggleButton(); + toggleButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + boolean advancedModeShown = raceBoardPanel.getTimePanel().toggleAdvancedMode(); + if (advancedModeShown) { + p.setWidgetSize(timePanel, 96); + toggleButton.removeStyleDependentName("Closed"); + toggleButton.addStyleDependentName("Open"); + } else { + p.setWidgetSize(timePanel, 67); + toggleButton.addStyleDependentName("Closed"); + toggleButton.removeStyleDependentName("Open"); + } + } + }); + p.addSouth(timePanel, 67); + p.add(raceBoardPanel); + p.addStyleName("dockLayoutPanel"); + } +} 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 414c16d8f1e..a3b5ee333c2 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 @@ -1,437 +1,483 @@ -package com.sap.sailing.gwt.ui.raceboard; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import com.google.gwt.core.shared.GWT; -import com.google.gwt.dom.client.Document; -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.i18n.client.DateTimeFormat; -import com.google.gwt.user.client.Command; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.ui.Anchor; -import com.google.gwt.user.client.ui.FlowPanel; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.MenuBar; -import com.google.gwt.user.client.ui.SimplePanel; -import com.google.gwt.user.client.ui.UIObject; -import com.sap.sailing.domain.common.LeaderboardNameConstants; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.dto.BoatDTO; -import com.sap.sailing.domain.common.dto.CompetitorDTO; -import com.sap.sailing.domain.common.dto.FleetDTO; -import com.sap.sailing.domain.common.dto.LeaderboardDTO; -import com.sap.sailing.domain.common.dto.RaceColumnDTO; -import com.sap.sailing.domain.common.dto.RaceDTO; -import com.sap.sailing.gwt.ui.client.CompetitorColorProvider; -import com.sap.sailing.gwt.ui.client.CompetitorColorProviderImpl; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionModel; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; -import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; -import com.sap.sailing.gwt.ui.client.GlobalNavigationPanel; -import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; -import com.sap.sailing.gwt.ui.client.MediaServiceAsync; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; -import com.sap.sailing.gwt.ui.client.RaceTimePanel; -import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.media.MediaPlayerManagerComponent; -import com.sap.sailing.gwt.ui.client.media.PopupPositionProvider; -import com.sap.sailing.gwt.ui.client.shared.charts.EditMarkPassingsPanel; -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.filter.FilterWithUI; -import com.sap.sailing.gwt.ui.client.shared.filter.LeaderboardFetcher; -import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMap; -import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapResources; -import com.sap.sailing.gwt.ui.leaderboard.CompetitorFilterPanel; -import com.sap.sailing.gwt.ui.leaderboard.ExplicitRaceColumnSelectionWithPreselectedRace; -import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; -import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettings; -import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettingsFactory; -import com.sap.sse.common.filter.FilterSet; -import com.sap.sse.common.settings.AbstractSettings; -import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.URLEncoder; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.player.TimeRangeWithZoomModel; -import com.sap.sse.gwt.client.player.Timer; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.ComponentViewer; -import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.gwt.client.useragent.UserAgentDetails; -import com.sap.sse.security.ui.client.UserService; - -/** - * A view showing a list of components visualizing a race from the regattas announced by calls to {@link #fillRegattas(List)}. - * The race selection is provided by a {@link RaceSelectionProvider} for which this is a {@link RaceSelectionChangeListener listener}. - * {@link RaceIdentifier}-based race selection changes are converted to {@link RaceDTO} objects using the {@link #racesByIdentifier} - * map maintained during {@link #fillRegattas(List)}. The race selection provider is expected to be single selection only. - * - * @author Frank Mittag, Axel Uhl (d043530) - * - */ -public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeListener, LeaderboardUpdateListener, PopupPositionProvider { - private final SailingServiceAsync sailingService; - private final MediaServiceAsync mediaService; - private final UUID eventId; - private final StringMessages stringMessages; - private final ErrorReporter errorReporter; - private final RaceBoardViewConfiguration raceboardViewConfiguration; - private String raceBoardName; - - private final List componentViewers; - private RaceTimePanel timePanel; - private final Timer timer; - private final RaceSelectionProvider raceSelectionProvider; - private final UserAgentDetails userAgent; - private final CompetitorSelectionProvider competitorSelectionProvider; - private final TimeRangeWithZoomModel timeRangeWithZoomModel; - private final RegattaAndRaceIdentifier selectedRaceIdentifier; - - private final String leaderboardName; - private final LeaderboardPanel leaderboardPanel; - private WindChart windChart; - private MultiCompetitorRaceChart competitorChart; - private EditMarkPassingsPanel editMarkPassingPanel; - - /** - * The component viewer in ONESCREEN view mode. null if in CASCADE view mode - */ - private SideBySideComponentViewer leaderboardAndMapViewer; - - private final AsyncActionsExecutor asyncActionsExecutor; - - private final RaceTimesInfoProvider raceTimesInfoProvider; - private RaceMap raceMap; - - private final FlowPanel raceInformationHeader; - private final FlowPanel regattaAndRaceTimeInformationHeader; - private boolean currentRaceHasBeenSelectedOnce; - - private static final RaceMapResources raceMapResources = GWT.create(RaceMapResources.class); - - /** - * @param eventId - * an optional event that can be used for "back"-navigation in case the race board shows a race in the - * context of an event; may be null. - */ - public RaceBoardPanel(SailingServiceAsync sailingService, MediaServiceAsync mediaService, - UserService userService, AsyncActionsExecutor asyncActionsExecutor, Map competitorsAndTheirBoats, - Timer timer, RaceSelectionProvider theRaceSelectionProvider, String leaderboardName, - String leaderboardGroupName, UUID eventId, RaceBoardViewConfiguration raceboardViewConfiguration, - ErrorReporter errorReporter, final StringMessages stringMessages, - UserAgentDetails userAgent, RaceTimesInfoProvider raceTimesInfoProvider, boolean showMapControls) { - this.sailingService = sailingService; - this.mediaService = mediaService; - this.stringMessages = stringMessages; - this.raceboardViewConfiguration = raceboardViewConfiguration; - this.raceSelectionProvider = theRaceSelectionProvider; - this.raceTimesInfoProvider = raceTimesInfoProvider; - this.errorReporter = errorReporter; - this.userAgent = userAgent; - this.timer = timer; - this.eventId = eventId; - this.currentRaceHasBeenSelectedOnce = false; - this.leaderboardName = leaderboardName; - raceSelectionProvider.addRaceSelectionChangeListener(this); - selectedRaceIdentifier = raceSelectionProvider.getSelectedRaces().iterator().next(); - this.setRaceBoardName(selectedRaceIdentifier.getRaceName()); - this.asyncActionsExecutor = asyncActionsExecutor; - FlowPanel mainPanel = new FlowPanel(); - mainPanel.setSize("100%", "100%"); - setWidget(mainPanel); - raceInformationHeader = new FlowPanel(); - raceInformationHeader.setStyleName("RegattaRaceInformation-Header"); - regattaAndRaceTimeInformationHeader = new FlowPanel(); - regattaAndRaceTimeInformationHeader.setStyleName("RegattaAndRaceTime-Header"); - timeRangeWithZoomModel = new TimeRangeWithZoomModel(); - componentViewers = new ArrayList(); - final CompetitorColorProvider colorProvider = new CompetitorColorProviderImpl(selectedRaceIdentifier, competitorsAndTheirBoats); - competitorSelectionProvider = new CompetitorSelectionModel(/* hasMultiSelection */ true, colorProvider); - - raceMapResources.combinedWindPanelStyle().ensureInjected(); - raceMap = new RaceMap(sailingService, asyncActionsExecutor, errorReporter, timer, - competitorSelectionProvider, stringMessages, showMapControls, getConfiguration().isShowViewStreamlets(), getConfiguration().isShowViewStreamletColors(), getConfiguration().isShowViewSimulation(), - selectedRaceIdentifier, raceMapResources.combinedWindPanelStyle(), /* showHeaderPanel */ true); - CompetitorFilterPanel competitorSearchTextBox = new CompetitorFilterPanel(competitorSelectionProvider, stringMessages, raceMap, - new LeaderboardFetcher() { - @Override - public LeaderboardDTO getLeaderboard() { - return leaderboardPanel.getLeaderboard(); - } - }, selectedRaceIdentifier); - raceMap.getLeftHeaderPanel().add(raceInformationHeader); - raceMap.getRightHeaderPanel().add(regattaAndRaceTimeInformationHeader); - - leaderboardPanel = createLeaderboardPanel(leaderboardName, leaderboardGroupName, competitorSearchTextBox); - leaderboardPanel.setTitle(stringMessages.leaderboard()); - leaderboardPanel.getElement().getStyle().setMarginLeft(6, Unit.PX); - leaderboardPanel.getElement().getStyle().setMarginTop(10, Unit.PX); - createOneScreenView(leaderboardName, leaderboardGroupName, eventId, mainPanel, showMapControls, raceMap, userService); // initializes the raceMap field - leaderboardPanel.addLeaderboardUpdateListener(this); - // in case the URL configuration contains the name of a competitors filter set we try to activate it - // FIXME the competitorsFilterSets has now moved to CompetitorSearchTextBox (which should probably be renamed); pass on the parameters to the LeaderboardPanel and see what it does with it - if (raceboardViewConfiguration.getActiveCompetitorsFilterSetName() != null) { - for (FilterSet> filterSet : competitorSearchTextBox.getCompetitorsFilterSets() - .getFilterSets()) { - if (filterSet.getName().equals(raceboardViewConfiguration.getActiveCompetitorsFilterSetName())) { - competitorSearchTextBox.getCompetitorsFilterSets().setActiveFilterSet(filterSet); - break; - } - } - } - timePanel = new RaceTimePanel(timer, timeRangeWithZoomModel, stringMessages, raceTimesInfoProvider, - raceboardViewConfiguration.isCanReplayDuringLiveRaces()); - timeRangeWithZoomModel.addTimeZoomChangeListener(timePanel); - raceTimesInfoProvider.addRaceTimesInfoProviderListener(timePanel); - raceSelectionProvider.addRaceSelectionChangeListener(timePanel); - timePanel.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); - } - - /** - * @param event an optional event; may be null or else can be used to show some context information in - * the {@link GlobalNavigationPanel}. - */ - private void createOneScreenView(String leaderboardName, String leaderboardGroupName, UUID event, FlowPanel mainPanel, - boolean showMapControls, RaceMap raceMap, UserService userService) { - // create the default leaderboard and select the right race - raceTimesInfoProvider.addRaceTimesInfoProviderListener(raceMap); - raceMap.onRaceSelectionChange(Collections.singletonList(selectedRaceIdentifier)); - List> components = new ArrayList>(); - competitorChart = new MultiCompetitorRaceChart(sailingService, asyncActionsExecutor, competitorSelectionProvider, raceSelectionProvider, - timer, timeRangeWithZoomModel, stringMessages, errorReporter, true, true, leaderboardGroupName, leaderboardName); - competitorChart.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); - competitorChart.getEntryWidget().setTitle(stringMessages.competitorCharts()); - competitorChart.setVisible(false); - components.add(competitorChart); - windChart = new WindChart(sailingService, raceSelectionProvider, timer, timeRangeWithZoomModel, new WindChartSettings(), - stringMessages, asyncActionsExecutor, errorReporter, /* compactChart */ true); - windChart.setVisible(false); - windChart.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); - windChart.getEntryWidget().setTitle(stringMessages.windChart()); - components.add(windChart); - editMarkPassingPanel = new EditMarkPassingsPanel(sailingService, selectedRaceIdentifier, - stringMessages, competitorSelectionProvider, errorReporter, timer); - editMarkPassingPanel.setLeaderboard(leaderboardPanel.getLeaderboard()); - editMarkPassingPanel.getEntryWidget().setTitle(stringMessages.editMarkPassings()); - components.add(editMarkPassingPanel); - boolean autoSelectMedia = getConfiguration().isAutoSelectMedia(); - MediaPlayerManagerComponent mediaPlayerManagerComponent = new MediaPlayerManagerComponent( - selectedRaceIdentifier, raceTimesInfoProvider, timer, mediaService, userService, stringMessages, - errorReporter, userAgent, this, autoSelectMedia); - leaderboardAndMapViewer = new SideBySideComponentViewer(leaderboardPanel, raceMap, mediaPlayerManagerComponent, components, stringMessages, userService, editMarkPassingPanel); - componentViewers.add(leaderboardAndMapViewer); - for (ComponentViewer componentViewer : componentViewers) { - mainPanel.add(componentViewer.getViewerWidget()); - } - boolean showLeaderboard = getConfiguration().isShowLeaderboard(); - if (Document.get().getClientWidth() <= 1024) { - showLeaderboard = false; - } - setLeaderboardVisible(showLeaderboard); - setWindChartVisible(getConfiguration().isShowWindChart()); - setCompetitorChartVisible(getConfiguration().isShowCompetitorsChart()); - // make sure to load leaderboard data for filtering to work - if (!showLeaderboard) { - leaderboardPanel.setVisible(true); - leaderboardPanel.setVisible(false); - } - } - - @SuppressWarnings("unused") - private void addSettingsMenuItem(MenuBar settingsMenu, final Component component) { - if (component.hasSettings()) { - settingsMenu.addItem(component.getLocalizedShortName(), new Command() { - public void execute() { - new SettingsDialog(component, stringMessages).show(); - } - }); - } - } - - private LeaderboardPanel createLeaderboardPanel(String leaderboardName, String leaderboardGroupName, CompetitorFilterPanel competitorSearchTextBox) { - LeaderboardSettings leaderBoardSettings = LeaderboardSettingsFactory.getInstance() - .createNewSettingsForPlayMode(timer.getPlayMode(), - /* nameOfRaceToSort */ selectedRaceIdentifier.getRaceName(), - /* nameOfRaceColumnToShow */ null, /* nameOfRaceToShow */ selectedRaceIdentifier.getRaceName(), - new ExplicitRaceColumnSelectionWithPreselectedRace(selectedRaceIdentifier), /* showRegattaRank */ false, - /*showCompetitorSailIdColumn*/true, /*showCompetitorFullNameColumn*/true); - return new LeaderboardPanel(sailingService, asyncActionsExecutor, leaderBoardSettings, selectedRaceIdentifier != null, selectedRaceIdentifier, - competitorSelectionProvider, timer, leaderboardGroupName, leaderboardName, errorReporter, stringMessages, - userAgent, /* showRaceDetails */ true, competitorSearchTextBox, - /* showSelectionCheckbox */ true, raceTimesInfoProvider, /* autoExpandLastRaceColumn */ false, - /* don't adjust the timer's delay from the leaderboard; control it solely from the RaceTimesInfoProvider */ false, - /*autoApplyTopNFilter*/ false, /*showCompetitorFilterStatus*/ false); - } - - private void setComponentVisible(ComponentViewer componentViewer, Component component, boolean visible) { - component.setVisible(visible); - componentViewer.forceLayout(); - } - - /** - * Sets the collapsable panel for the leaderboard open or close, if in CASCADE view mode.
- * Displays or hides the leaderboard, if in ONESCREEN view mode.

- * - * The race board should be completely rendered before this method is called, or a few exceptions could be thrown. - * - * @param visible true if the leaderboard shall be open/visible - */ - public void setLeaderboardVisible(boolean visible) { - setComponentVisible(leaderboardAndMapViewer, leaderboardPanel, visible); - } - - /** - * Sets the collapsable panel for the wind chart open or close, if in CASCADE view mode.
- * Displays or hides the wind chart, if in ONESCREEN view mode.

- * - * The race board should be completely rendered before this method is called, or a few exceptions could be thrown. - * - * @param visible true if the wind chart shall be open/visible - */ - public void setWindChartVisible(boolean visible) { - setComponentVisible(leaderboardAndMapViewer, windChart, visible); - } - - /** - * Sets the collapsable panel for the competitor chart open or close, if in CASCADE view mode.
- * Displays or hides the competitor chart, if in ONESCREEN view mode.

- * - * The race board should be completely rendered before this method is called, or a few exceptions could be thrown. - * - * @param visible true if the competitor chart shall be open/visible - */ - public void setCompetitorChartVisible(boolean visible) { - setComponentVisible(leaderboardAndMapViewer, competitorChart, visible); - } - - public RaceTimePanel getTimePanel() { - return timePanel; - } - - protected SailingServiceAsync getSailingService() { - return sailingService; - } - - protected String getRaceBoardName() { - return raceBoardName; - } - - protected void setRaceBoardName(String raceBoardName) { - this.raceBoardName = raceBoardName; - } - - protected ErrorReporter getErrorReporter() { - return errorReporter; - } - - @Override - public void onRaceSelectionChange(List selectedRaces) { - } - - public RaceBoardViewConfiguration getConfiguration() { - return raceboardViewConfiguration; - } - - @Override - public void updatedLeaderboard(LeaderboardDTO leaderboard) { - leaderboardAndMapViewer.setLeftComponentWidth(leaderboardPanel.getContentPanel().getOffsetWidth()); - editMarkPassingPanel.setLeaderboard(leaderboard); - } - - @Override - public void currentRaceSelected(RaceIdentifier raceIdentifier, RaceColumnDTO raceColumn) { - if (!currentRaceHasBeenSelectedOnce) { - FleetDTO fleet = raceColumn.getFleet(raceIdentifier); - String seriesName = raceColumn.getSeriesName(); - if (LeaderboardNameConstants.DEFAULT_SERIES_NAME.equals(seriesName)) { - seriesName = ""; - } - String fleetForRaceName = fleet==null?"":fleet.getName(); - if (fleetForRaceName.equals(LeaderboardNameConstants.DEFAULT_FLEET_NAME)) { - fleetForRaceName = ""; - } else { - fleetForRaceName = " - "+fleetForRaceName; - } - final Label raceNameLabel = new Label(stringMessages.race() + " " + raceColumn.getRaceColumnName()); - raceNameLabel.setStyleName("RaceName-Label"); - final Label raceAdditionalInformationLabel = new Label(seriesName + fleetForRaceName); - raceAdditionalInformationLabel.setStyleName("RaceSeriesAndFleet-Label"); - raceInformationHeader.clear(); - raceInformationHeader.add(raceNameLabel); - raceInformationHeader.add(raceAdditionalInformationLabel); - final Anchor regattaNameAnchor = new Anchor(raceIdentifier.getRegattaName()); - if (eventId != null) { - // we don't use the EntryPointLinkFactory here, because of lacking support for Places - String debugParam = Window.Location.getParameter("gwt.codesvr"); - String link = "/gwt/Home.html"; - if (debugParam != null && !debugParam.isEmpty()) { - link += "?gwt.codesvr=" + debugParam; - } - link += "#EventPlace:eventId="+eventId.toString(); - link += "&navigationTab=Regatta&leaderboardName=" + URLEncoder.encode(leaderboardName); - regattaNameAnchor.setHref(link); - } else { - String leaderboardGroupNameParam = Window.Location.getParameter("leaderboardGroupName"); - if(leaderboardGroupNameParam != null) { - Map leaderboardGroupLinkParameters = new HashMap(); - leaderboardGroupLinkParameters.put("showRaceDetails", "true"); - leaderboardGroupLinkParameters.put("leaderboardGroupName", leaderboardGroupNameParam); - String leaderBoardGroupLink = EntryPointLinkFactory.createLeaderboardGroupLink(leaderboardGroupLinkParameters); - regattaNameAnchor.setHref(leaderBoardGroupLink); - } else { - // fallback - regattaNameAnchor.setHref("javascript:window.history.back();"); - } - } - regattaNameAnchor.setStyleName("RegattaName-Anchor"); - - // TODO: Strange behavior... check -// Window.addResizeHandler(new ResizeHandler() { -// @Override -// public void onResize(ResizeEvent event) { -// int headerPanelWidth = raceMap.getRightHeaderPanel().getOffsetWidth() - 150; // 150px is the width of the sapLogo -// int raceNameAndFleetLabelWidth = raceInformationHeader.getOffsetWidth(); -// int regattaAnchorWidth = regattaNameAnchor.getOffsetWidth(); -// boolean overlap = raceNameAndFleetLabelWidth + regattaAnchorWidth > headerPanelWidth; -// raceInformationHeader.setVisible(!overlap); -// } -// }); - - Label raceTimeLabel = computeRaceInformation(raceColumn, fleet); - raceTimeLabel.setStyleName("RaceTime-Label"); - regattaAndRaceTimeInformationHeader.clear(); - regattaAndRaceTimeInformationHeader.add(regattaNameAnchor); - regattaAndRaceTimeInformationHeader.add(raceTimeLabel); - currentRaceHasBeenSelectedOnce = true; - } - } - - @Override - public UIObject getXPositionUiObject() { - return timePanel; - } - - @Override - public UIObject getYPositionUiObject() { - return timePanel; - } - - private Label computeRaceInformation(RaceColumnDTO raceColumn, FleetDTO fleet) { - Label raceInformationLabel = new Label(); - raceInformationLabel.setStyleName("Race-Time-Label"); - DateTimeFormat formatter = DateTimeFormat.getFormat("E d/M/y"); - raceInformationLabel.setText(formatter.format(raceColumn.getStartDate(fleet))); - return raceInformationLabel; - } -} - +package com.sap.sailing.gwt.ui.raceboard; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.core.shared.GWT; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Anchor; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.MenuBar; +import com.google.gwt.user.client.ui.RequiresResize; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.UIObject; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.LeaderboardNameConstants; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.dto.BoatDTO; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.domain.common.dto.FleetDTO; +import com.sap.sailing.domain.common.dto.LeaderboardDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.domain.common.dto.RaceDTO; +import com.sap.sailing.gwt.ui.client.CompetitorColorProvider; +import com.sap.sailing.gwt.ui.client.CompetitorColorProviderImpl; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionModel; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; +import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; +import com.sap.sailing.gwt.ui.client.GlobalNavigationPanel; +import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; +import com.sap.sailing.gwt.ui.client.MediaServiceAsync; +import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; +import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; +import com.sap.sailing.gwt.ui.client.RaceTimePanel; +import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.media.MediaPlayerManagerComponent; +import com.sap.sailing.gwt.ui.client.media.PopupPositionProvider; +import com.sap.sailing.gwt.ui.client.shared.charts.EditMarkPassingsPanel; +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.filter.FilterWithUI; +import com.sap.sailing.gwt.ui.client.shared.filter.LeaderboardFetcher; +import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMap; +import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapResources; +import com.sap.sailing.gwt.ui.leaderboard.CompetitorFilterPanel; +import com.sap.sailing.gwt.ui.leaderboard.ExplicitRaceColumnSelectionWithPreselectedRace; +import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; +import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettings; +import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettingsFactory; +import com.sap.sse.common.filter.FilterSet; +import com.sap.sse.common.settings.AbstractSettings; +import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.URLEncoder; +import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.player.TimeRangeWithZoomModel; +import com.sap.sse.gwt.client.player.Timer; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.ComponentViewer; +import com.sap.sse.gwt.client.shared.components.SettingsDialog; +import com.sap.sse.gwt.client.useragent.UserAgentDetails; +import com.sap.sse.security.ui.client.UserService; + +/** + * A view showing a list of components visualizing a race from the regattas announced by calls to {@link #fillRegattas(List)}. + * The race selection is provided by a {@link RaceSelectionProvider} for which this is a {@link RaceSelectionChangeListener listener}. + * {@link RaceIdentifier}-based race selection changes are converted to {@link RaceDTO} objects using the {@link #racesByIdentifier} + * map maintained during {@link #fillRegattas(List)}. The race selection provider is expected to be single selection only. + * + * @author Frank Mittag, Axel Uhl (d043530) + * + */ +public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeListener, + LeaderboardUpdateListener, PopupPositionProvider, RequiresResize { + private final SailingServiceAsync sailingService; + private final MediaServiceAsync mediaService; + private final UUID eventId; + private final StringMessages stringMessages; + private final ErrorReporter errorReporter; + private final RaceBoardViewConfiguration raceboardViewConfiguration; + private String raceBoardName; + + private final List componentViewers; + private RaceTimePanel timePanel; + private final Timer timer; + private final RaceSelectionProvider raceSelectionProvider; + private final UserAgentDetails userAgent; + private final CompetitorSelectionProvider competitorSelectionProvider; + private final TimeRangeWithZoomModel timeRangeWithZoomModel; + private final RegattaAndRaceIdentifier selectedRaceIdentifier; + + private final String leaderboardName; + private final LeaderboardPanel leaderboardPanel; + private WindChart windChart; + private MultiCompetitorRaceChart competitorChart; + private EditMarkPassingsPanel editMarkPassingPanel; + + /** + * The component viewer in ONESCREEN view mode. null if in CASCADE view mode + */ + private SideBySideComponentViewer leaderboardAndMapViewer; + + private final AsyncActionsExecutor asyncActionsExecutor; + + private final RaceTimesInfoProvider raceTimesInfoProvider; + private RaceMap raceMap; + + private final FlowPanel raceInformationHeader; + private final FlowPanel regattaAndRaceTimeInformationHeader; + private boolean currentRaceHasBeenSelectedOnce; + + private static final RaceMapResources raceMapResources = GWT.create(RaceMapResources.class); + + /** + * @param eventId + * an optional event that can be used for "back"-navigation in case the race board shows a race in the + * context of an event; may be null. + * @param isScreenLargeEnoughToOfferChartSupport + * if the screen is large enough to display charts such as the competitor chart or the wind chart, a + * padding is provided for the RaceTimePanel that aligns its right border with that of the charts, and + * the charts are created. This decision is made once on startup in the {@link RaceBoardEntryPoint} class. + */ + public RaceBoardPanel(SailingServiceAsync sailingService, MediaServiceAsync mediaService, + UserService userService, AsyncActionsExecutor asyncActionsExecutor, Map competitorsAndTheirBoats, + Timer timer, RaceSelectionProvider theRaceSelectionProvider, String leaderboardName, + String leaderboardGroupName, UUID eventId, RaceBoardViewConfiguration raceboardViewConfiguration, + ErrorReporter errorReporter, final StringMessages stringMessages, + UserAgentDetails userAgent, RaceTimesInfoProvider raceTimesInfoProvider, boolean showMapControls, boolean isScreenLargeEnoughToOfferChartSupport) { + this.sailingService = sailingService; + this.mediaService = mediaService; + this.stringMessages = stringMessages; + this.raceboardViewConfiguration = raceboardViewConfiguration; + this.raceSelectionProvider = theRaceSelectionProvider; + this.raceTimesInfoProvider = raceTimesInfoProvider; + this.errorReporter = errorReporter; + this.userAgent = userAgent; + this.timer = timer; + this.eventId = eventId; + this.currentRaceHasBeenSelectedOnce = false; + this.leaderboardName = leaderboardName; + raceSelectionProvider.addRaceSelectionChangeListener(this); + selectedRaceIdentifier = raceSelectionProvider.getSelectedRaces().iterator().next(); + this.setRaceBoardName(selectedRaceIdentifier.getRaceName()); + this.asyncActionsExecutor = asyncActionsExecutor; + FlowPanel mainPanel = new ResizableFlowPanel(); + mainPanel.setSize("100%", "100%"); + setWidget(mainPanel); + raceInformationHeader = new FlowPanel(); + raceInformationHeader.setStyleName("RegattaRaceInformation-Header"); + regattaAndRaceTimeInformationHeader = new FlowPanel(); + regattaAndRaceTimeInformationHeader.setStyleName("RegattaAndRaceTime-Header"); + timeRangeWithZoomModel = new TimeRangeWithZoomModel(); + componentViewers = new ArrayList(); + final CompetitorColorProvider colorProvider = new CompetitorColorProviderImpl(selectedRaceIdentifier, competitorsAndTheirBoats); + competitorSelectionProvider = new CompetitorSelectionModel(/* hasMultiSelection */ true, colorProvider); + + raceMapResources.combinedWindPanelStyle().ensureInjected(); + raceMap = new RaceMap(sailingService, asyncActionsExecutor, errorReporter, timer, + competitorSelectionProvider, stringMessages, showMapControls, getConfiguration().isShowViewStreamlets(), getConfiguration().isShowViewStreamletColors(), getConfiguration().isShowViewSimulation(), + selectedRaceIdentifier, raceMapResources.combinedWindPanelStyle(), /* showHeaderPanel */ true) { + private static final String INDENT_SMALL_CONTROL_STYLE = "indentsmall"; + private static final String INDENT_BIG_CONTROL_STYLE = "indentbig"; + @Override + public void onResize() { + super.onResize(); + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + // Show/hide the leaderboard panels toggle button text based on the race map height + leaderboardAndMapViewer.setLeftComponentToggleButtonTextVisibilityAndDraggerPosition(raceMap.getOffsetHeight() > 400); + } + }); + } + + @Override + protected String getLeftControlsIndentStyle() { + // Calculate style name for left control indent based on race map height an leaderboard panel visibility + if (raceMap.getOffsetHeight() <= 300) { + return INDENT_BIG_CONTROL_STYLE; + } + if (leaderboardPanel.isVisible() && raceMap.getOffsetHeight() <= 500) { + return INDENT_SMALL_CONTROL_STYLE; + } + return super.getLeftControlsIndentStyle(); + } + }; + CompetitorFilterPanel competitorSearchTextBox = new CompetitorFilterPanel(competitorSelectionProvider, stringMessages, raceMap, + new LeaderboardFetcher() { + @Override + public LeaderboardDTO getLeaderboard() { + return leaderboardPanel.getLeaderboard(); + } + }, selectedRaceIdentifier); + raceMap.getLeftHeaderPanel().add(raceInformationHeader); + raceMap.getRightHeaderPanel().add(regattaAndRaceTimeInformationHeader); + + // Determine if the screen is large enough to initially display the leaderboard panel on the left side of the + // map based on the initial screen width. Afterwards, the leaderboard panel visibility can be toggled as usual. + boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard = Document.get().getClientWidth() >= 1024; + leaderboardPanel = createLeaderboardPanel(leaderboardName, leaderboardGroupName, competitorSearchTextBox, isScreenLargeEnoughToInitiallyDisplayLeaderboard); + leaderboardPanel.setTitle(stringMessages.leaderboard()); + leaderboardPanel.getElement().getStyle().setMarginLeft(6, Unit.PX); + leaderboardPanel.getElement().getStyle().setMarginTop(10, Unit.PX); + createOneScreenView(leaderboardName, leaderboardGroupName, eventId, mainPanel, showMapControls, isScreenLargeEnoughToOfferChartSupport, isScreenLargeEnoughToInitiallyDisplayLeaderboard, raceMap, userService); // initializes the raceMap field + leaderboardPanel.addLeaderboardUpdateListener(this); + // in case the URL configuration contains the name of a competitors filter set we try to activate it + // FIXME the competitorsFilterSets has now moved to CompetitorSearchTextBox (which should probably be renamed); pass on the parameters to the LeaderboardPanel and see what it does with it + if (raceboardViewConfiguration.getActiveCompetitorsFilterSetName() != null) { + for (FilterSet> filterSet : competitorSearchTextBox.getCompetitorsFilterSets() + .getFilterSets()) { + if (filterSet.getName().equals(raceboardViewConfiguration.getActiveCompetitorsFilterSetName())) { + competitorSearchTextBox.getCompetitorsFilterSets().setActiveFilterSet(filterSet); + break; + } + } + } + timePanel = new RaceTimePanel(timer, timeRangeWithZoomModel, stringMessages, raceTimesInfoProvider, + raceboardViewConfiguration.isCanReplayDuringLiveRaces(), isScreenLargeEnoughToOfferChartSupport); + timeRangeWithZoomModel.addTimeZoomChangeListener(timePanel); + raceTimesInfoProvider.addRaceTimesInfoProviderListener(timePanel); + raceSelectionProvider.addRaceSelectionChangeListener(timePanel); + timePanel.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); + } + + /** + * @param event + * an optional event; may be null or else can be used to show some context information in + * the {@link GlobalNavigationPanel}. + * @param isScreenLargeEnoughToOfferChartSupport + * if the screen is large enough to display charts such as the competitor chart or the wind chart, a + * padding is provided for the RaceTimePanel that aligns its right border with that of the charts, and + * the charts are created. + * @param isScreenLargeEnoughToInitiallyDisplayLeaderboard TODO + */ + private void createOneScreenView(String leaderboardName, String leaderboardGroupName, UUID event, FlowPanel mainPanel, + boolean showMapControls, boolean isScreenLargeEnoughToOfferChartSupport, boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard, RaceMap raceMap, UserService userService) { + // create the default leaderboard and select the right race + raceTimesInfoProvider.addRaceTimesInfoProviderListener(raceMap); + raceMap.onRaceSelectionChange(Collections.singletonList(selectedRaceIdentifier)); + List> components = new ArrayList>(); + if (isScreenLargeEnoughToOfferChartSupport) { + competitorChart = new MultiCompetitorRaceChart(sailingService, asyncActionsExecutor, competitorSelectionProvider, raceSelectionProvider, + timer, timeRangeWithZoomModel, stringMessages, errorReporter, true, true, leaderboardGroupName, leaderboardName); + competitorChart.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); + competitorChart.getEntryWidget().setTitle(stringMessages.competitorCharts()); + competitorChart.setVisible(false); + components.add(competitorChart); + windChart = new WindChart(sailingService, raceSelectionProvider, timer, timeRangeWithZoomModel, new WindChartSettings(), + stringMessages, asyncActionsExecutor, errorReporter, /* compactChart */ true); + windChart.setVisible(false); + windChart.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); + windChart.getEntryWidget().setTitle(stringMessages.windChart()); + components.add(windChart); + editMarkPassingPanel = new EditMarkPassingsPanel(sailingService, selectedRaceIdentifier, + stringMessages, competitorSelectionProvider, errorReporter, timer); + editMarkPassingPanel.setLeaderboard(leaderboardPanel.getLeaderboard()); + editMarkPassingPanel.getEntryWidget().setTitle(stringMessages.editMarkPassings()); + components.add(editMarkPassingPanel); + } + boolean autoSelectMedia = getConfiguration().isAutoSelectMedia(); + MediaPlayerManagerComponent mediaPlayerManagerComponent = new MediaPlayerManagerComponent( + selectedRaceIdentifier, raceTimesInfoProvider, timer, mediaService, userService, stringMessages, + errorReporter, userAgent, this, autoSelectMedia); + leaderboardAndMapViewer = new SideBySideComponentViewer(leaderboardPanel, raceMap, mediaPlayerManagerComponent, components, stringMessages, userService, editMarkPassingPanel); + componentViewers.add(leaderboardAndMapViewer); + for (ComponentViewer componentViewer : componentViewers) { + mainPanel.add(componentViewer.getViewerWidget()); + } + boolean showLeaderboard = getConfiguration().isShowLeaderboard() && isScreenLargeEnoughToInitiallyDisplayLeaderboard; + setLeaderboardVisible(showLeaderboard); + if (isScreenLargeEnoughToOfferChartSupport) { + setWindChartVisible(getConfiguration().isShowWindChart()); + setCompetitorChartVisible(getConfiguration().isShowCompetitorsChart()); + } + // make sure to load leaderboard data for filtering to work + if (!showLeaderboard) { + leaderboardPanel.setVisible(true); + leaderboardPanel.setVisible(false); + } + } + + @SuppressWarnings("unused") + private void addSettingsMenuItem(MenuBar settingsMenu, final Component component) { + if (component.hasSettings()) { + settingsMenu.addItem(component.getLocalizedShortName(), new Command() { + public void execute() { + new SettingsDialog(component, stringMessages).show(); + } + }); + } + } + + private LeaderboardPanel createLeaderboardPanel(String leaderboardName, String leaderboardGroupName, + CompetitorFilterPanel competitorSearchTextBox, boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard) { + LeaderboardSettings leaderBoardSettings = LeaderboardSettingsFactory.getInstance() + .createNewSettingsForPlayMode(timer.getPlayMode(), + /* nameOfRaceToSort */ selectedRaceIdentifier.getRaceName(), + /* nameOfRaceColumnToShow */ null, /* nameOfRaceToShow */ selectedRaceIdentifier.getRaceName(), + new ExplicitRaceColumnSelectionWithPreselectedRace(selectedRaceIdentifier), /* showRegattaRank */ false, + /*showCompetitorSailIdColumn*/true, + /* don't showCompetitorFullNameColumn in case screen is so small that we don't + * even display the leaderboard initially */ isScreenLargeEnoughToInitiallyDisplayLeaderboard); + return new LeaderboardPanel(sailingService, asyncActionsExecutor, leaderBoardSettings, selectedRaceIdentifier != null, selectedRaceIdentifier, + competitorSelectionProvider, timer, leaderboardGroupName, leaderboardName, errorReporter, stringMessages, + userAgent, /* showRaceDetails */ true, competitorSearchTextBox, + /* showSelectionCheckbox */ true, raceTimesInfoProvider, /* autoExpandLastRaceColumn */ false, + /* don't adjust the timer's delay from the leaderboard; control it solely from the RaceTimesInfoProvider */ false, + /*autoApplyTopNFilter*/ false, /*showCompetitorFilterStatus*/ false); + } + + private void setComponentVisible(ComponentViewer componentViewer, Component component, boolean visible) { + component.setVisible(visible); + componentViewer.forceLayout(); + } + + /** + * Sets the collapsable panel for the leaderboard open or close, if in CASCADE view mode.
+ * Displays or hides the leaderboard, if in ONESCREEN view mode.

+ * + * The race board should be completely rendered before this method is called, or a few exceptions could be thrown. + * + * @param visible true if the leaderboard shall be open/visible + */ + public void setLeaderboardVisible(boolean visible) { + setComponentVisible(leaderboardAndMapViewer, leaderboardPanel, visible); + } + + /** + * Sets the collapsable panel for the wind chart open or close, if in CASCADE view mode.
+ * Displays or hides the wind chart, if in ONESCREEN view mode.

+ * + * The race board should be completely rendered before this method is called, or a few exceptions could be thrown. + * + * @param visible true if the wind chart shall be open/visible + */ + public void setWindChartVisible(boolean visible) { + setComponentVisible(leaderboardAndMapViewer, windChart, visible); + } + + /** + * Sets the collapsable panel for the competitor chart open or close, if in CASCADE view mode.
+ * Displays or hides the competitor chart, if in ONESCREEN view mode.

+ * + * The race board should be completely rendered before this method is called, or a few exceptions could be thrown. + * + * @param visible true if the competitor chart shall be open/visible + */ + public void setCompetitorChartVisible(boolean visible) { + setComponentVisible(leaderboardAndMapViewer, competitorChart, visible); + } + + public RaceTimePanel getTimePanel() { + return timePanel; + } + + protected SailingServiceAsync getSailingService() { + return sailingService; + } + + protected String getRaceBoardName() { + return raceBoardName; + } + + protected void setRaceBoardName(String raceBoardName) { + this.raceBoardName = raceBoardName; + } + + protected ErrorReporter getErrorReporter() { + return errorReporter; + } + + @Override + public void onRaceSelectionChange(List selectedRaces) { + } + + public RaceBoardViewConfiguration getConfiguration() { + return raceboardViewConfiguration; + } + + @Override + public void updatedLeaderboard(LeaderboardDTO leaderboard) { + leaderboardAndMapViewer.setLeftComponentWidth(leaderboardPanel.getContentPanel().getOffsetWidth()); + if (editMarkPassingPanel != null) { + editMarkPassingPanel.setLeaderboard(leaderboard); + } + } + + @Override + public void currentRaceSelected(RaceIdentifier raceIdentifier, RaceColumnDTO raceColumn) { + if (!currentRaceHasBeenSelectedOnce) { + FleetDTO fleet = raceColumn.getFleet(raceIdentifier); + String seriesName = raceColumn.getSeriesName(); + if (LeaderboardNameConstants.DEFAULT_SERIES_NAME.equals(seriesName)) { + seriesName = ""; + } + String fleetForRaceName = fleet==null?"":fleet.getName(); + if (fleetForRaceName.equals(LeaderboardNameConstants.DEFAULT_FLEET_NAME)) { + fleetForRaceName = ""; + } else { + fleetForRaceName = (seriesName.isEmpty() ? "" : " - ") + fleetForRaceName; + } + final Label raceNameLabel = new Label(stringMessages.race() + " " + raceColumn.getRaceColumnName()); + raceNameLabel.setStyleName("RaceName-Label"); + final Label raceAdditionalInformationLabel = new Label(seriesName + fleetForRaceName); + raceAdditionalInformationLabel.setStyleName("RaceSeriesAndFleet-Label"); + raceInformationHeader.clear(); + raceInformationHeader.add(raceNameLabel); + raceInformationHeader.add(raceAdditionalInformationLabel); + final Anchor regattaNameAnchor = new Anchor(raceIdentifier.getRegattaName()); + regattaNameAnchor.setTitle(raceIdentifier.getRegattaName()); + if (eventId != null) { + // we don't use the EntryPointLinkFactory here, because of lacking support for Places + String debugParam = Window.Location.getParameter("gwt.codesvr"); + String link = "/gwt/Home.html"; + if (debugParam != null && !debugParam.isEmpty()) { + link += "?gwt.codesvr=" + debugParam; + } + link += "#EventPlace:eventId="+eventId.toString(); + link += "&navigationTab=Regatta&leaderboardName=" + URLEncoder.encode(leaderboardName); + regattaNameAnchor.setHref(link); + } else { + String leaderboardGroupNameParam = Window.Location.getParameter("leaderboardGroupName"); + if(leaderboardGroupNameParam != null) { + Map leaderboardGroupLinkParameters = new HashMap(); + leaderboardGroupLinkParameters.put("showRaceDetails", "true"); + leaderboardGroupLinkParameters.put("leaderboardGroupName", leaderboardGroupNameParam); + String leaderBoardGroupLink = EntryPointLinkFactory.createLeaderboardGroupLink(leaderboardGroupLinkParameters); + regattaNameAnchor.setHref(leaderBoardGroupLink); + } else { + // fallback + regattaNameAnchor.setHref("javascript:window.history.back();"); + } + } + regattaNameAnchor.setStyleName("RegattaName-Anchor"); + Label raceTimeLabel = computeRaceInformation(raceColumn, fleet); + raceTimeLabel.setStyleName("RaceTime-Label"); + regattaAndRaceTimeInformationHeader.clear(); + regattaAndRaceTimeInformationHeader.add(regattaNameAnchor); + regattaAndRaceTimeInformationHeader.add(raceTimeLabel); + currentRaceHasBeenSelectedOnce = true; + } + } + + @Override + public UIObject getXPositionUiObject() { + return timePanel; + } + + @Override + public UIObject getYPositionUiObject() { + return timePanel; + } + + private Label computeRaceInformation(RaceColumnDTO raceColumn, FleetDTO fleet) { + Label raceInformationLabel = new Label(); + raceInformationLabel.setStyleName("Race-Time-Label"); + DateTimeFormat formatter = DateTimeFormat.getFormat("E d/M/y"); + raceInformationLabel.setText(formatter.format(raceColumn.getStartDate(fleet))); + return raceInformationLabel; + } + + @Override + public void onResize() { + Widget child = getWidget(); + if (child != null && child instanceof RequiresResize) { + ((RequiresResize) child).onResize(); + } + } +} + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/ResizableFlowPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/ResizableFlowPanel.java new file mode 100644 index 00000000000..d83d0a64407 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/ResizableFlowPanel.java @@ -0,0 +1,23 @@ +package com.sap.sailing.gwt.ui.raceboard; + +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.RequiresResize; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.user.client.ui.WidgetCollection; + +/** + * Utility class extending {@link FlowPanel} and implementing {@link RequiresResize} interface. All + * {@link #getChildren() children} also implementing {@link RequiresResize} are informed by calling their + * {@link RequiresResize#onResize()} method. + */ +class ResizableFlowPanel extends FlowPanel implements RequiresResize { + @Override + public void onResize() { + WidgetCollection children = getChildren(); + for (Widget widget : children) { + if (widget instanceof RequiresResize) { + ((RequiresResize) widget).onResize(); + } + } + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java index d6ba3b34ed7..6ff63bfde4b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java @@ -1,305 +1,334 @@ -package com.sap.sailing.gwt.ui.raceboard; - -import java.util.ArrayList; -import java.util.List; - -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.user.client.ui.AbsolutePanel; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.DockLayoutPanel.Direction; -import com.google.gwt.user.client.ui.LayoutPanel; -import com.google.gwt.user.client.ui.Panel; -import com.google.gwt.user.client.ui.RequiresResize; -import com.google.gwt.user.client.ui.ScrollPanel; -import com.google.gwt.user.client.ui.Widget; -import com.google.gwt.user.client.ui.WidgetCollection; -import com.sap.sailing.domain.common.security.Roles; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.media.MediaManagementControl; -import com.sap.sailing.gwt.ui.client.media.MediaPlayerManager; -import com.sap.sailing.gwt.ui.client.media.MediaPlayerManager.PlayerChangeListener; -import com.sap.sailing.gwt.ui.client.media.MediaPlayerManagerComponent; -import com.sap.sailing.gwt.ui.client.media.MediaSingleSelectionControl; -import com.sap.sailing.gwt.ui.client.shared.charts.EditMarkPassingsPanel; -import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; -import com.sap.sse.common.Util.Pair; -import com.sap.sse.common.settings.AbstractSettings; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.ComponentViewer; -import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.security.shared.DefaultRoles; -import com.sap.sse.security.ui.client.UserService; -import com.sap.sse.security.ui.client.UserStatusEventHandler; -import com.sap.sse.security.ui.shared.UserDTO; - -/** - * Component Viewer that uses a {@link TouchSplitLayoutPanel} to display its components. - * - * TODO: Refactor to make sure it is really only performing operations that are related to view. Currently it is digging - * too deep into components and setting titles or even creating video buttons. - */ -public class SideBySideComponentViewer implements ComponentViewer, UserStatusEventHandler { - - private static final int DEFAULT_SOUTH_SPLIT_PANEL_HEIGHT = 200; - private final int MIN_LEADERBOARD_WIDTH = 432; // works well for 505 and ESS - - /** - * Absolute Panel that informs its children about a resize - */ - class ResizableAbsolutePanel extends AbsolutePanel implements RequiresResize { - public void onResize() { - WidgetCollection children = getChildren(); - for (Widget widget : children) { - if (widget instanceof RequiresResize) { - ((RequiresResize) widget).onResize(); - } - } - } - } - - private final LeaderboardPanel leftComponent; - private final Component rightComponent; - private final List> components; - private final ScrollPanel leftScrollPanel; - private final StringMessages stringMessages; - private final Button mediaSelectionButton; - private final Button mediaManagementButton; - private final EditMarkPassingsPanel markPassingsPanel; - - private LayoutPanel mainPanel; - - private TouchSplitLayoutPanel splitLayoutPanel; - private int savedSplitPosition = -1; - private boolean layoutForLeftComponentForcedOnce = false; - - public SideBySideComponentViewer(final LeaderboardPanel leftComponentP, final Component rightComponentP, - final MediaPlayerManagerComponent mediaPlayerManagerComponent, List> components, - final StringMessages stringMessages, UserService userService, EditMarkPassingsPanel markPassingsPanel) { - this.stringMessages = stringMessages; - this.leftComponent = leftComponentP; - this.rightComponent = rightComponentP; - this.components = components; - this.mediaSelectionButton = createMediaSelectionButton(mediaPlayerManagerComponent); - this.mediaManagementButton = createMediaManagementButton(mediaPlayerManagerComponent); - this.markPassingsPanel = markPassingsPanel; - - userService.addUserStatusEventHandler(this); - - mediaPlayerManagerComponent.setPlayerChangeListener(new PlayerChangeListener() { - - public void notifyStateChange(){ - String caption; - String tooltip; - switch (mediaPlayerManagerComponent.getAssignedMediaTracks().size()) { - case 0: - caption = stringMessages.mediaNoVideosCaption(); - tooltip = caption; - mediaSelectionButton.setVisible(false); - break; - case 1: - mediaSelectionButton.setVisible(true); - if(mediaPlayerManagerComponent.isPlaying()){ - caption = stringMessages.mediaHideVideoCaption(); - tooltip = stringMessages.mediaHideVideoTooltip(); - } - else{ - caption = stringMessages.mediaShowVideoCaption(); - tooltip = stringMessages.mediaShowVideoTooltip(mediaPlayerManagerComponent.getAssignedMediaTracks().iterator().next().title); - } - break; - default: - mediaSelectionButton.setVisible(true); - caption = stringMessages.mediaSelectVideoCaption(mediaPlayerManagerComponent.getAssignedMediaTracks().size()); - tooltip = stringMessages.mediaSelectVideoTooltip(); - break; - } - if (mediaPlayerManagerComponent.isPlaying()) { - mediaSelectionButton.addStyleDependentName("mediaplaying"); - } else { - mediaSelectionButton.removeStyleDependentName("mediaplaying"); - } - mediaSelectionButton.setText(caption); - mediaSelectionButton.setTitle(tooltip); - mediaManagementButton.setVisible(mediaPlayerManagerComponent.allowsEditing()); - } - - }); - this.leftScrollPanel = new ScrollPanel(); - this.leftScrollPanel.add(leftComponentP.getEntryWidget()); - this.leftScrollPanel.setTitle(leftComponentP.getEntryWidget().getTitle()); - this.mainPanel = new LayoutPanel(); - this.mainPanel.setSize("100%", "100%"); - this.mainPanel.getElement().getStyle().setMarginTop(-12, Unit.PX); - this.mainPanel.setStyleName("SideBySideComponentViewer-MainPanel"); - this.splitLayoutPanel = new TouchSplitLayoutPanel(/* horizontal splitter width */3, /* vertical splitter height */ - 25); - this.mainPanel.add(splitLayoutPanel); - - // initialize components - they need to be added before other widgets to get the right width - initializeComponents(); - - // initialize the leaderboard component - savedSplitPosition = MIN_LEADERBOARD_WIDTH; - splitLayoutPanel.insert(leftScrollPanel, leftComponent, Direction.WEST, savedSplitPosition); - - // create a panel that will contain the horizontal toggle buttons - ResizableAbsolutePanel panelForMapAndHorizontalToggleButtons = new ResizableAbsolutePanel(); - panelForMapAndHorizontalToggleButtons.add(rightComponent.getEntryWidget()); - splitLayoutPanel.insert(panelForMapAndHorizontalToggleButtons, rightComponent, Direction.CENTER, 0); - - // add additional toggle buttons panel that currently only contains the video button - List> additionalVerticalButtons = new ArrayList>(); - additionalVerticalButtons.add(new Pair(mediaSelectionButton, - mediaPlayerManagerComponent.getDependentCssClassName())); - if (/* TODO check for correct role; was: user != null */ true) { - additionalVerticalButtons.add(new Pair(mediaManagementButton, - "managemedia")); - } - onUserStatusChange(userService.getCurrentUser()); - - // ensure that toggle buttons are positioned right - splitLayoutPanel.lastComponentHasBeenAdded(this, panelForMapAndHorizontalToggleButtons, - additionalVerticalButtons); - } - - /** - * Create the video control button that shows or hides the video popup - * - * @param userAgent - */ - private Button createMediaSelectionButton(final MediaPlayerManager mediaPlayerManager) { - final Button result = new Button(); - final MediaSingleSelectionControl multiSelectionControl = new MediaSingleSelectionControl(mediaPlayerManager, - result, stringMessages); - result.addClickHandler(new ClickHandler() { - - @Override - public void onClick(ClickEvent event) { - if (mediaPlayerManager.getAssignedMediaTracks().size() == 1) { - if (mediaPlayerManager.isPlaying()) { - mediaPlayerManager.stopAll(); - } else { - mediaPlayerManager.playDefault(); - - } - } else { - multiSelectionControl.show(); - } - } - }); - - // hide button initially as we defer showing the button to the asynchroneous - // task that gets launched by the media service to get video tracks - result.setVisible(false); - return result; - } - - /** - * Create the video control button that shows or hides the video popup - * - * @param userAgent - */ - private Button createMediaManagementButton(final MediaPlayerManager mediaPlayerManager) { - final Button result = new Button(stringMessages.mediaManageMediaCaption()); - result.setTitle(stringMessages.mediaManageMediaTooltip()); - // onClick - final MediaManagementControl multiSelectionControl = new MediaManagementControl(mediaPlayerManager, - result, stringMessages); - result.addClickHandler(new ClickHandler() { - - @Override - public void onClick(ClickEvent event) { - multiSelectionControl.show(); - } - }); - - // hide button initially as we defer showing the button to the asynchroneous - // task that gets launched by the media service to get video tracks - result.setVisible(false); - return result; - } - - private void initializeComponents() { - for (final Component component : components) { - splitLayoutPanel.insert(component.getEntryWidget(), component, Direction.SOUTH, 200); - } - } - - public void showSettingsDialog(Component component) { - if (component.hasSettings()) { - new SettingsDialog(component, stringMessages).show(); - } - } - - /** - * Called whenever the layout of {@link TouchSplitLayoutPanel} or other components change. Controls the visibility - * based on the {@link Component}s visibility. Each {@link Component} is in charge to not display any data or update - * itself when it is not visible. - */ - public void forceLayout() { - if (!leftComponent.isVisible() && rightComponent.isVisible()) { - // the leaderboard is not visible, but the map is - if (isWidgetInSplitPanel(leftScrollPanel)) { - if (leftScrollPanel.getOffsetWidth() > 0) { - savedSplitPosition = leftScrollPanel.getOffsetWidth(); - } - splitLayoutPanel.setWidgetVisibility(leftScrollPanel, leftComponent, /* hidden */true, - savedSplitPosition); - } - } else if (leftComponent.isVisible() && rightComponent.isVisible()) { - // the leaderboard and the map are visible - splitLayoutPanel.setWidgetVisibility(leftScrollPanel, leftComponent, /* hidden */false, savedSplitPosition); - } else if (!leftComponent.isVisible() && !rightComponent.isVisible()) { - } - - for (Component component : components) { - final boolean isComponentVisible = component.isVisible(); - splitLayoutPanel.setWidgetVisibility(component.getEntryWidget(), component, !isComponentVisible, - DEFAULT_SOUTH_SPLIT_PANEL_HEIGHT); - } - splitLayoutPanel.forceLayout(); - } - - private boolean isWidgetInSplitPanel(Widget widget) { - int widgetIndex = splitLayoutPanel.getWidgetIndex(widget); - return widgetIndex >= 0; - } - - public Panel getViewerWidget() { - return mainPanel; - } - - public Component getRootComponent() { - return null; - } - - public String getViewerName() { - return ""; - } - - public void setLeftComponentWidth(int width) { - // TODO: The information provided by width is wrong - // need to find a way to get the correct information - if (!layoutForLeftComponentForcedOnce) { - savedSplitPosition = MIN_LEADERBOARD_WIDTH; - forceLayout(); - } - layoutForLeftComponentForcedOnce = true; - } - - @Override - public void onUserStatusChange(UserDTO user) { - Button toggleButton = splitLayoutPanel.getAssociatedSplitter(markPassingsPanel).getToggleButton(); - if (user != null - && (user.hasRole(DefaultRoles.ADMIN.getRolename()) || user.hasRole(Roles.eventmanager.getRolename()))) { - toggleButton.setVisible(true); - forceLayout(); - } else { - markPassingsPanel.setVisible(false); - toggleButton.setVisible(false); - forceLayout(); - } - } -} +package com.sap.sailing.gwt.ui.raceboard; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.AbsolutePanel; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.DockLayoutPanel.Direction; +import com.google.gwt.user.client.ui.LayoutPanel; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.RequiresResize; +import com.google.gwt.user.client.ui.ScrollPanel; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.user.client.ui.WidgetCollection; +import com.sap.sailing.domain.common.security.Roles; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.media.MediaManagementControl; +import com.sap.sailing.gwt.ui.client.media.MediaPlayerManager; +import com.sap.sailing.gwt.ui.client.media.MediaPlayerManager.PlayerChangeListener; +import com.sap.sailing.gwt.ui.client.media.MediaPlayerManagerComponent; +import com.sap.sailing.gwt.ui.client.media.MediaSingleSelectionControl; +import com.sap.sailing.gwt.ui.client.shared.charts.EditMarkPassingsPanel; +import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; +import com.sap.sailing.gwt.ui.raceboard.TouchSplitLayoutPanel.Splitter; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.settings.AbstractSettings; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.ComponentViewer; +import com.sap.sse.gwt.client.shared.components.SettingsDialog; +import com.sap.sse.security.shared.DefaultRoles; +import com.sap.sse.security.ui.client.UserService; +import com.sap.sse.security.ui.client.UserStatusEventHandler; +import com.sap.sse.security.ui.shared.UserDTO; + +/** + * Component Viewer that uses a {@link TouchSplitLayoutPanel} to display its components. + * + * TODO: Refactor to make sure it is really only performing operations that are related to view. Currently it is digging + * too deep into components and setting titles or even creating video buttons. + */ +public class SideBySideComponentViewer implements ComponentViewer, UserStatusEventHandler { + + private static final int DEFAULT_SOUTH_SPLIT_PANEL_HEIGHT = 200; + private final int MIN_LEADERBOARD_WIDTH = Math.min(432, Window.getClientWidth() - 40); // fallback value "432" works well for 505 and ESS + + /** + * Absolute Panel that informs its children about a resize + */ + class ResizableAbsolutePanel extends AbsolutePanel implements RequiresResize { + public void onResize() { + WidgetCollection children = getChildren(); + for (Widget widget : children) { + if (widget instanceof RequiresResize) { + ((RequiresResize) widget).onResize(); + } + } + } + } + + private final LeaderboardPanel leftComponent; + private final Component rightComponent; + private final List> components; + private final ScrollPanel leftScrollPanel; + private final StringMessages stringMessages; + private final Button mediaSelectionButton; + private final Button mediaManagementButton; + private final EditMarkPassingsPanel markPassingsPanel; + + private LayoutPanel mainPanel; + + private TouchSplitLayoutPanel splitLayoutPanel; + private int savedSplitPosition = -1; + private boolean layoutForLeftComponentForcedOnce = false; + + public SideBySideComponentViewer(final LeaderboardPanel leftComponentP, final Component rightComponentP, + final MediaPlayerManagerComponent mediaPlayerManagerComponent, List> components, + final StringMessages stringMessages, UserService userService, EditMarkPassingsPanel markPassingsPanel) { + this.stringMessages = stringMessages; + this.leftComponent = leftComponentP; + this.rightComponent = rightComponentP; + this.components = components; + this.mediaSelectionButton = createMediaSelectionButton(mediaPlayerManagerComponent); + this.mediaManagementButton = createMediaManagementButton(mediaPlayerManagerComponent); + this.markPassingsPanel = markPassingsPanel; + userService.addUserStatusEventHandler(this); + mediaPlayerManagerComponent.setPlayerChangeListener(new PlayerChangeListener() { + public void notifyStateChange() { + String caption; + String tooltip; + switch (mediaPlayerManagerComponent.getAssignedMediaTracks().size()) { + case 0: + caption = stringMessages.mediaNoVideosCaption(); + tooltip = caption; + mediaSelectionButton.setVisible(false); + break; + case 1: + mediaSelectionButton.setVisible(true); + if(mediaPlayerManagerComponent.isPlaying()){ + caption = stringMessages.mediaHideVideoCaption(); + tooltip = stringMessages.mediaHideVideoTooltip(); + } + else{ + caption = stringMessages.mediaShowVideoCaption(); + tooltip = stringMessages.mediaShowVideoTooltip(mediaPlayerManagerComponent.getAssignedMediaTracks().iterator().next().title); + } + break; + default: + mediaSelectionButton.setVisible(true); + caption = stringMessages.mediaSelectVideoCaption(mediaPlayerManagerComponent.getAssignedMediaTracks().size()); + tooltip = stringMessages.mediaSelectVideoTooltip(); + break; + } + if (mediaPlayerManagerComponent.isPlaying()) { + mediaSelectionButton.addStyleDependentName("mediaplaying"); + } else { + mediaSelectionButton.removeStyleDependentName("mediaplaying"); + } + mediaSelectionButton.setText(caption); + mediaSelectionButton.setTitle(tooltip); + mediaManagementButton.setVisible(mediaPlayerManagerComponent.allowsEditing()); + } + }); + this.leftScrollPanel = new ScrollPanel(); + this.leftScrollPanel.add(leftComponentP.getEntryWidget()); + this.leftScrollPanel.setTitle(leftComponentP.getEntryWidget().getTitle()); + this.mainPanel = new LayoutPanel() { + @Override + public void onResize() { + int leftWidth = leftScrollPanel.getOffsetWidth(); + // The left scroll panel is potentially resized to ensure it is not too wide when the screen gets narrower, + // e.g. when resizing browser window or changing mobile device orientation. An offset of 40px is used, so + // the panels size slider and its toggle button is always accessable if it is open. + savedSplitPosition = Math.min(leftWidth > 0 ? leftWidth : savedSplitPosition, Window.getClientWidth() - 40); + splitLayoutPanel.setWidgetSize(leftScrollPanel, savedSplitPosition); + super.onResize(); + } + }; + this.mainPanel.setSize("100%", "100%"); + this.mainPanel.getElement().getStyle().setMarginTop(-12, Unit.PX); + this.mainPanel.setStyleName("SideBySideComponentViewer-MainPanel"); + this.splitLayoutPanel = new TouchSplitLayoutPanel(/* horizontal splitter width */3, /* vertical splitter height */ 25); + this.mainPanel.add(splitLayoutPanel); + + // initialize components - they need to be added before other widgets to get the right width + initializeComponents(); + + // initialize the leaderboard component + savedSplitPosition = MIN_LEADERBOARD_WIDTH; + splitLayoutPanel.insert(leftScrollPanel, leftComponent, Direction.WEST, savedSplitPosition); + + // create a panel that will contain the horizontal toggle buttons + ResizableAbsolutePanel panelForMapAndHorizontalToggleButtons = new ResizableAbsolutePanel(); + panelForMapAndHorizontalToggleButtons.add(rightComponent.getEntryWidget()); + splitLayoutPanel.insert(panelForMapAndHorizontalToggleButtons, rightComponent, Direction.CENTER, 0); + + // add additional toggle buttons panel that currently only contains the video button + List> additionalVerticalButtons = new ArrayList>(); + additionalVerticalButtons.add(new Pair(mediaSelectionButton, + mediaPlayerManagerComponent.getDependentCssClassName())); + if (/* TODO check for correct role; was: user != null */ true) { + additionalVerticalButtons.add(new Pair(mediaManagementButton, + "managemedia")); + } + onUserStatusChange(userService.getCurrentUser()); + // ensure that toggle buttons are positioned right + splitLayoutPanel.lastComponentHasBeenAdded(this, panelForMapAndHorizontalToggleButtons, + additionalVerticalButtons); + } + + /** + * Create the video control button that shows or hides the video popup + * + * @param userAgent + */ + private Button createMediaSelectionButton(final MediaPlayerManager mediaPlayerManager) { + final Button result = new Button(); + final MediaSingleSelectionControl multiSelectionControl = new MediaSingleSelectionControl(mediaPlayerManager, + result, stringMessages); + result.addClickHandler(new ClickHandler() { + + @Override + public void onClick(ClickEvent event) { + if (mediaPlayerManager.getAssignedMediaTracks().size() == 1) { + if (mediaPlayerManager.isPlaying()) { + mediaPlayerManager.stopAll(); + } else { + mediaPlayerManager.playDefault(); + + } + } else { + multiSelectionControl.show(); + } + } + }); + + // hide button initially as we defer showing the button to the asynchroneous + // task that gets launched by the media service to get video tracks + result.setVisible(false); + return result; + } + + /** + * Create the video control button that shows or hides the video popup + * + * @param userAgent + */ + private Button createMediaManagementButton(final MediaPlayerManager mediaPlayerManager) { + final Button result = new Button(stringMessages.mediaManageMediaCaption()); + result.setTitle(stringMessages.mediaManageMediaTooltip()); + // onClick + final MediaManagementControl multiSelectionControl = new MediaManagementControl(mediaPlayerManager, + result, stringMessages); + result.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + multiSelectionControl.show(); + } + }); + // hide button initially as we defer showing the button to the asynchronous + // task that gets launched by the media service to get video tracks + result.setVisible(false); + return result; + } + + private void initializeComponents() { + for (final Component component : components) { + splitLayoutPanel.insert(component.getEntryWidget(), component, Direction.SOUTH, 200); + } + } + + public void showSettingsDialog(Component component) { + if (component.hasSettings()) { + new SettingsDialog(component, stringMessages).show(); + } + } + + /** + * Called whenever the layout of {@link TouchSplitLayoutPanel} or other components change. Controls the visibility + * based on the {@link Component}s visibility. Each {@link Component} is in charge to not display any data or update + * itself when it is not visible. + */ + public void forceLayout() { + if (!leftComponent.isVisible() && rightComponent.isVisible()) { + // the leaderboard is not visible, but the map is + if (isWidgetInSplitPanel(leftScrollPanel)) { + if (leftScrollPanel.getOffsetWidth() > 0) { + savedSplitPosition = Math.min(savedSplitPosition, leftScrollPanel.getOffsetWidth()); + } + splitLayoutPanel.setWidgetVisibility(leftScrollPanel, leftComponent, /* hidden */true, + savedSplitPosition); + } + } else if (leftComponent.isVisible() && rightComponent.isVisible()) { + // the leaderboard and the map are visible + splitLayoutPanel.setWidgetVisibility(leftScrollPanel, leftComponent, /* hidden */false, savedSplitPosition); + } else if (!leftComponent.isVisible() && !rightComponent.isVisible()) { + } + + for (Component component : components) { + final boolean isComponentVisible = component.isVisible(); + splitLayoutPanel.setWidgetVisibility(component.getEntryWidget(), component, !isComponentVisible, + DEFAULT_SOUTH_SPLIT_PANEL_HEIGHT); + } + splitLayoutPanel.forceLayout(); + } + + private boolean isWidgetInSplitPanel(Widget widget) { + int widgetIndex = splitLayoutPanel.getWidgetIndex(widget); + return widgetIndex >= 0; + } + + public Panel getViewerWidget() { + return mainPanel; + } + + public Component getRootComponent() { + return null; + } + + public String getViewerName() { + return ""; + } + + public void setLeftComponentWidth(int width) { + // TODO: The information provided by width is wrong + // need to find a way to get the correct information + if (!layoutForLeftComponentForcedOnce) { + savedSplitPosition = MIN_LEADERBOARD_WIDTH; + forceLayout(); + } + layoutForLeftComponentForcedOnce = true; + } + + @Override + public void onUserStatusChange(UserDTO user) { + final Splitter associatedSplitter = splitLayoutPanel.getAssociatedSplitter(markPassingsPanel); + if (associatedSplitter != null) { // if the panel is not present, the splitter may not be found + final Button toggleButton = associatedSplitter.getToggleButton(); + if (user != null + && (user.hasRole(DefaultRoles.ADMIN.getRolename()) || + user.hasRole(Roles.eventmanager.getRolename()))) { + toggleButton.setVisible(true); + forceLayout(); + } else { + markPassingsPanel.setVisible(false); + toggleButton.setVisible(false); + forceLayout(); + } + } + } + + /** + * Shows/hides the text on left components toggle button by modifying CSS font-size property and adjust + * the dragger position by modifying CSS margin-top property. + * + * @param visible + * true to show the button text, false to hide it + */ + void setLeftComponentToggleButtonTextVisibilityAndDraggerPosition(final boolean visible) { + Splitter leftScrollPanelSplitter = splitLayoutPanel.getAssociatedSplitter(leftScrollPanel); + if (leftScrollPanelSplitter != null) { + Style toggleButtonStyle = leftScrollPanelSplitter.getToggleButton().getElement().getStyle(); + if (visible) toggleButtonStyle.clearFontSize(); + else toggleButtonStyle.setFontSize(0, Unit.PX); + Style drapperStyle = leftScrollPanelSplitter.getDragger().getElement().getStyle(); + if (visible) drapperStyle.clearMarginTop(); + else drapperStyle.setMarginTop(-25, Unit.PX); + } + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/TouchSplitLayoutPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/TouchSplitLayoutPanel.java index 3f6b991c931..fed18802b88 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/TouchSplitLayoutPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/TouchSplitLayoutPanel.java @@ -613,7 +613,7 @@ public class TouchSplitLayoutPanel extends DockLayoutPanel { if (lastVerticalSplitter != null) { Panel panel = createToggleButtonPanel(allVerticalSplitters, "gwt-SplitLayoutPanel-NorthSouthToggleButton-Panel", "gwt-SplitLayoutPanel-NorthSouthToggleButton", - cm, additionalVerticalButtonsAndStyles); + cm, additionalVerticalButtonsAndStyles); // TODO additional buttons will only be displayed if at least one vertical splitter is... on purpose? lastVerticalSplitter.addToogleButtons(panel); lastVerticalSplitter.setVisible(true); lastVerticalSplitter.setDraggerVisible(false); @@ -621,7 +621,7 @@ public class TouchSplitLayoutPanel extends DockLayoutPanel { ensureVerticalToggleButtonPosition(); if (lastHorizontalSplitter != null) { Panel horizontalButtonsPanel = createToggleButtonPanel(allHorizontalSplitters, - "gwt-SplitLayoutPanel-EastToggleButton-Panel", "gwt-SplitLayoutPanel-EastToggleButton", cm, null); + "gwt-SplitLayoutPanel-EastToggleButton-Panel", "gwt-SplitLayoutPanel-EastToggleButton", cm, /* additional buttons and styles */ null); panelForHorizontalButtons.add(horizontalButtonsPanel); lastHorizontalSplitter.setVisible(false); lastHorizontalSplitter.setDraggerVisible(false); @@ -842,6 +842,24 @@ public class TouchSplitLayoutPanel extends DockLayoutPanel { } } } + + /** + * Sets the size of the given {@link Widget} if it is {@link Widget#isVisible() visible}. Otherwise, the given size + * will be persisted as the widgets {@link LayoutData#oldSize old size}. + * + * @param widget the {@link Widget} to resize + * @param size the new size + */ + public void setWidgetSize(Widget widget, int size) { + final Splitter splitter = getAssociatedSplitter(widget); + if (splitter != null) { + if (widget.isVisible()) { + splitter.setAssociatedWidgetSize(size, /* defer */false); + } else { + ((LayoutData) widget.getLayoutData()).oldSize = size; + } + } + } private void assertIsChild2(Widget widget) { assert (widget == null) || (widget.getParent() == this) : "The specified widget is not a child of this panel"; 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 253e42696e7..6fe3eb5aa90 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 @@ -233,6 +233,7 @@ import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCalculationReuseCache; import com.sap.sailing.domain.leaderboard.caching.LiveLeaderboardUpdater; +import com.sap.sailing.domain.leaderboard.meta.MetaLeaderboardColumn; import com.sap.sailing.domain.persistence.DomainObjectFactory; import com.sap.sailing.domain.persistence.MongoObjectFactory; import com.sap.sailing.domain.persistence.MongoRaceLogStoreFactory; @@ -853,7 +854,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S final RaceColumnDTO raceColumnDTO = columnFactory.createRaceColumnDTO(raceColumn.getName(), raceColumn.isMedalRace(), raceColumn.getExplicitFactor(), raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null); + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, + raceColumn instanceof MetaLeaderboardColumn); raceColumnDTOs.add(raceColumnDTO); } return raceColumnDTOs; @@ -1023,7 +1025,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public void trackWithTracTrac(RegattaIdentifier regattaToAddTo, Iterable rrs, String liveURI, String storedURI, String courseDesignUpdateURI, boolean trackWind, final boolean correctWindByDeclination, - final boolean simulateWithStartTimeNow, final boolean useInternalMarkPassingAlgorithm, String tracTracUsername, String tracTracPassword) + final Duration offsetToStartTimeOfSimulatedRace, final boolean useInternalMarkPassingAlgorithm, String tracTracUsername, String tracTracPassword) throws Exception { logger.info("tracWithTracTrac for regatta " + regattaToAddTo + " for race records " + rrs + " with liveURI " + liveURI + " and storedURI " + storedURI); @@ -1054,7 +1056,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S new URI(courseDesignUpdateURI), new MillisecondsTimePoint(record.getTrackingStartTime().asMillis()), new MillisecondsTimePoint(record.getTrackingEndTime().asMillis()), getRaceLogStore(), getRegattaLogStore(), - RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS, simulateWithStartTimeNow, + RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, tracTracUsername, tracTracPassword, record.getRaceStatus(), record.getRaceVisibility()); if (trackWind) { new Thread("Wind tracking starter for race " + record.getEventName() + "/" + record.getName()) { @@ -2408,7 +2410,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S RaceColumnDTO raceColumnDTO = leaderboardDTO.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), raceColumn.getFactor(), raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, - fleetDTO, raceColumn.isMedalRace(), raceIdentifier, raceDTO); + fleetDTO, raceColumn.isMedalRace(), raceIdentifier, raceDTO, raceColumn instanceof MetaLeaderboardColumn); RaceLog raceLog = raceColumn.getRaceLog(fleet); RaceLogTrackingState raceLogTrackingState = raceLog == null ? RaceLogTrackingState.NOT_A_RACELOG_TRACKED_RACE : new RaceLogTrackingStateAnalyzer(raceLog).analyze(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMainPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMainPanel.java index 815def789c1..a6723a5f6a5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMainPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMainPanel.java @@ -309,7 +309,9 @@ public class SimulatorMainPanel extends SimplePanel { timer.setTime(windParams.getStartTime().getTime()); int secondsTimeStep = (int) windParams.getTimeStep().asSeconds(); timer.setPlaySpeedFactor(secondsTimeStep); - timePanel = new TimePanel(timer, timeRangeProvider, stringMessages, false); + timePanel = new TimePanel(timer, timeRangeProvider, stringMessages, false, + /* isScreenLargeEnoughToOfferChartSupport: no wind or competitor chart is shown; use full horizontal + * extension of time panel */ false); busyIndicator = new SimpleBusyIndicator(false, 0.8f); simulatorMap = new SimulatorMap(simulatorSvc, stringMessages, errorReporter, xRes, yRes, border, streamletPars, timer, timePanel, windParams, busyIndicator, mode, this, showMapControls, diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html index 547230f6227..69c32d47aea 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html @@ -4,6 +4,31 @@

What's New - SAP Sailing Analytics

+
December 2015
+ +
    +
  • New line hover behavior in race map. When the mouse is moved onto a line, for example the start line, a transparent, + wider line is activated behind it that acts as a buffer for the thin line, where the text displayed by the hover is still shown. + This makes it easier to hover over the lines. These wider lines can be made semi transparent instead of transparent + by unticking the corresponding checkbox in the settings of the race map. Furthermore the stroke weight of the wider lines can be + configured in the same settings dialog. +
  • +
  • The race viewer (RaceBoard.html) now intelligently chooses a layout that suits the screen size. + When only little horizontal space is available, only the sail number and nationality but not the + competitor's long name is displayed by default. When vertical space is scarce, competitor and wind + charts are disabled and more horizontal space can be given to the time slider at the bottom. The + leaderboard section can then be scaled to smaller widths and will reduce the column widths better + than in the past. +
  • +
  • With the changes described above, the RaceBoard is now also accessible from the races page on mobile devices. +
  • +
  • For overall / series leaderboards such as a national sailing league or the Extreme Sailing Series, the + live status of the overall leaderboard is now indicated. As long as a live race is ongoing, the respective + column in the overall leaderboard is highlighted, and a comment is displayed that indicates which regatta + is currently still live. +
  • +
+
November 2015
    diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/pom.xml b/java/com.sap.sailing.server.gateway.serialization.shared.android/pom.xml index a54a4c39a47..6d3ec73f155 100644 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/pom.xml +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/pom.xml @@ -1,25 +1,25 @@ - - - 4.0.0 - - root - com.sap.sailing - 1.0.0-SNAPSHOT - - com.sap.sailing.server.gateway.serialization.shared.android - eclipse-plugin - - - - org.eclipse.tycho - tycho-compiler-plugin - ${tycho-version} - - 1.7 - 1.7 - - - - - + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sailing.server.gateway.serialization.shared.android + eclipse-plugin + + + + org.eclipse.tycho + tycho-compiler-plugin + ${tycho-version} + + 1.7 + 1.7 + + + + + diff --git a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/TrackRaceBoatCompetitorMetadataReplicationTest.java b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/TrackRaceBoatCompetitorMetadataReplicationTest.java index 6e4774ecea1..7d8f74e6bfd 100644 --- a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/TrackRaceBoatCompetitorMetadataReplicationTest.java +++ b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/TrackRaceBoatCompetitorMetadataReplicationTest.java @@ -70,7 +70,7 @@ public class TrackRaceBoatCompetitorMetadataReplicationTest extends AbstractServ trackingParams = com.sap.sailing.domain.tractracadapter.DomainFactory.INSTANCE .createTrackingConnectivityParameters(paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, /* delayToLiveInMillis */ - 0l, /* simulateWithStartTimeNow */false, /*ignoreTracTracMarkPassings*/ false, EmptyRaceLogStore.INSTANCE, + 0l, /* offsetToStartTimeOfSimulatedRace */null, /*ignoreTracTracMarkPassings*/ false, EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, tracTracUsername, tracTracPassword, "", ""); } diff --git a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/TrackRaceReplicationTest.java b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/TrackRaceReplicationTest.java index ae255e1f3fd..88765bd6284 100755 --- a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/TrackRaceReplicationTest.java +++ b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/TrackRaceReplicationTest.java @@ -88,7 +88,7 @@ public class TrackRaceReplicationTest extends AbstractServerReplicationTest { trackingParams = com.sap.sailing.domain.tractracadapter.DomainFactory.INSTANCE .createTrackingConnectivityParameters(paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, /* delayToLiveInMillis */ - 0l, /* simulateWithStartTimeNow */false, /*ignoreTracTracMarkPassings*/ false, EmptyRaceLogStore.INSTANCE, + 0l, /* offsetToStartTimeOfSimulatedRace */null, /*ignoreTracTracMarkPassings*/ false, EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, tracTracUsername, tracTracPassword, "", ""); } diff --git a/java/com.sap.sse.common/pom.xml b/java/com.sap.sse.common/pom.xml index de52eed27a2..e1006e659cc 100644 --- a/java/com.sap.sse.common/pom.xml +++ b/java/com.sap.sse.common/pom.xml @@ -11,30 +11,30 @@ eclipse-plugin - - - org.eclipse.tycho - tycho-compiler-plugin - ${tycho-version} - - 1.7 - 1.7 - - - - org.eclipse.tycho - tycho-source-plugin - ${tycho-version} - - - plugin-source - generate-sources - - plugin-source - - - - - + + + org.eclipse.tycho + tycho-compiler-plugin + ${tycho-version} + + 1.7 + 1.7 + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + diff --git a/java/com.sap.sse.datamining.annotations/pom.xml b/java/com.sap.sse.datamining.annotations/pom.xml index fa8f86b4af2..343b2434435 100644 --- a/java/com.sap.sse.datamining.annotations/pom.xml +++ b/java/com.sap.sse.datamining.annotations/pom.xml @@ -1,31 +1,31 @@ - 4.0.0 - - root - com.sap.sailing - 1.0.0-SNAPSHOT - - com.sap.sse.datamining.annotations - eclipse-plugin - - - - org.eclipse.tycho - tycho-source-plugin - ${tycho-version} - - - plugin-source - generate-sources - - plugin-source - - - - - - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" + xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sse.datamining.annotations + eclipse-plugin + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + + diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/IntegerBox.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/IntegerBox.java index 27273086f30..f5a5b18b7cb 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/IntegerBox.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/IntegerBox.java @@ -7,6 +7,9 @@ import com.google.gwt.text.client.IntegerParser; import com.google.gwt.text.shared.Renderer; import com.google.gwt.user.client.ui.ValueBox; +/** + * Renders integers in their box without any separators, non-localized, plain. + */ public class IntegerBox extends ValueBox { private static final Renderer RENDERER = new Renderer() { public String render(Integer object) { diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyIndicatorResources.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyIndicatorResources.java index 1add987b8c5..b3978b24713 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyIndicatorResources.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyIndicatorResources.java @@ -7,4 +7,7 @@ public interface BusyIndicatorResources extends ClientBundle { @Source("busy_indicator_circle.gif") ImageResource busyIndicatorCircle(); + + @Source("busy_indicator_circle_inverted.gif") + ImageResource busyIndicatorCircleInverted(); } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyStateChangeListener.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyStateChangeListener.java new file mode 100644 index 00000000000..f302065808d --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyStateChangeListener.java @@ -0,0 +1,14 @@ +package com.sap.sse.gwt.client.controls.busyindicator; + + +/** + * Allows UI components to observe the busy state of an operation. + */ +public interface BusyStateChangeListener { + + /** + * @param busyState the new busy state + */ + void onBusyStateChange(boolean busyState); + +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyStateProvider.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyStateProvider.java new file mode 100644 index 00000000000..d886b844853 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyStateProvider.java @@ -0,0 +1,13 @@ +package com.sap.sse.gwt.client.controls.busyindicator; + + +public interface BusyStateProvider { + + void addBusyStateChangeListener(BusyStateChangeListener listener); + + void removeBusyStateChangeListener(BusyStateChangeListener listener); + + void setBusyState(boolean isBusy); + + boolean isBusy(); +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/SimpleBusyIndicator.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/SimpleBusyIndicator.java index 5a607e233d6..1c4254c971d 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/SimpleBusyIndicator.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/SimpleBusyIndicator.java @@ -1,5 +1,6 @@ package com.sap.sse.gwt.client.controls.busyindicator; +import com.google.gwt.dom.client.Style.Visibility; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.user.client.ui.Image; @@ -12,9 +13,13 @@ public class SimpleBusyIndicator extends BusyIndicator { * The busy indicator component is a circling GIF. */ public SimpleBusyIndicator() { - this(false, 1.0f); + this(false, 1.0f, RESOURCES.busyIndicatorCircle()); } - + + public SimpleBusyIndicator(ImageResource imageResource) { + this(false, 1.0f, imageResource); + } + /** * Creates a new SimpleBusyIndicator with a custom busy state.
    * The busy indicator component is a circling GIF. @@ -23,22 +28,29 @@ public class SimpleBusyIndicator extends BusyIndicator { * @param scale Scales the displayed image. 1.0 is 100%, 0.50 is 50%, ... */ public SimpleBusyIndicator(boolean busy, float scale) { - this(busy, scale, STYLE_NAME_PREFIX + "Simple", STYLE_NAME_PREFIX + "Circle"); + this(busy, scale, RESOURCES.busyIndicatorCircle()); } - - public SimpleBusyIndicator(boolean busy, float scale, String panelCssClass, String imageCssClass) { - this.setStyleName(panelCssClass); - ImageResource resource = RESOURCES.busyIndicatorCircle(); - busyIndicator = new Image(resource.getSafeUri()); - busyIndicator.setStyleName(imageCssClass); - busyIndicator.setPixelSize((int) (resource.getWidth() * scale), (int) (resource.getHeight() * scale)); + + public SimpleBusyIndicator(boolean busy, float scale, ImageResource imageResource) { + this.setStyleName(STYLE_NAME_PREFIX + "Simple"); + busyIndicator = new Image(imageResource.getSafeUri()); + busyIndicator.setStyleName(STYLE_NAME_PREFIX + "Circle"); + busyIndicator.setPixelSize((int) (imageResource.getWidth() * scale), (int) (imageResource.getHeight() * scale)); add(busyIndicator); setBusy(busy); } + public void setPanelStyleClass(String panelCssClass) { + this.setStyleName(panelCssClass); + } + + public void setImageStyleClass(String imageCssClass) { + busyIndicator.setStyleName(imageCssClass); + } + @Override public void setBusy(boolean busy) { super.setBusy(busy); - busyIndicator.setVisible(busy); + busyIndicator.getElement().getStyle().setVisibility(busy ? Visibility.VISIBLE : Visibility.HIDDEN); } } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/busy_indicator_circle_inverted.gif b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/busy_indicator_circle_inverted.gif new file mode 100644 index 00000000000..d31fd9ec8c0 Binary files /dev/null and b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/busy_indicator_circle_inverted.gif differ diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/SliderBar.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/SliderBar.java index 93ffbc08988..3f31188524e 100755 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/SliderBar.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/SliderBar.java @@ -1,1256 +1,1256 @@ -package com.sap.sse.gwt.client.controls.slider; - -/* - * Copyright 2008 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -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.dom.client.Element; -import com.google.gwt.dom.client.Style.Display; -import com.google.gwt.dom.client.Style.Position; -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.dom.client.Style.Visibility; -import com.google.gwt.event.dom.client.KeyCodes; -import com.google.gwt.event.logical.shared.HasValueChangeHandlers; -import com.google.gwt.event.logical.shared.ValueChangeEvent; -import com.google.gwt.event.logical.shared.ValueChangeHandler; -import com.google.gwt.event.shared.HandlerRegistration; -import com.google.gwt.resources.client.ClientBundle; -import com.google.gwt.resources.client.CssResource; -import com.google.gwt.resources.client.CssResource.NotStrict; -import com.google.gwt.resources.client.ImageResource; -import com.google.gwt.user.client.DOM; -import com.google.gwt.user.client.Event; -import com.google.gwt.user.client.Timer; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.ui.FocusPanel; -import com.google.gwt.user.client.ui.HasValue; -import com.google.gwt.user.client.ui.Image; -import com.google.gwt.user.client.ui.RequiresResize; -import com.sap.sse.common.Util; - -/** - * A widget that allows the user to select a value within a range of possible values using a sliding bar that responds - * to mouse events. - * - *

    Keyboard Events

    - *

    - * SliderBar listens for the following key events. Holding down a key will repeat the action until the key is released. - *

      - *
    • left arrow - shift left one step
    • - *
    • right arrow - shift right one step
    • - *
    • ctrl+left arrow - jump left 10% of the distance
    • - *
    • ctrl+right arrow - jump right 10% of the distance
    • - *
    • home - jump to min value
    • - *
    • end - jump to max value
    • - *
    • space - jump to middle value
    • - *
    - *

    - * - *

    CSS Style Rules

    - *
      - *
    • .gwt-SliderBar-shell { primary style }
    • - *
    • .gwt-SliderBar-shell-focused { primary style when focused }
    • - *
    • .gwt-SliderBar-shell gwt-SliderBar-line { the line that the knob moves along }
    • - *
    • .gwt-SliderBar-shell gwt-SliderBar-line-sliding { the line that the knob moves along when sliding }
    • - *
    • .gwt-SliderBar-shell .gwt-SliderBar-knob { the sliding knob }
    • - *
    • .gwt-SliderBar-shell .gwt-SliderBar-knob-sliding { the sliding knob when sliding }
    • - *
    • - * .gwt-SliderBar-shell .gwt-SliderBar-tick { the ticks along the line }
    • - *
    • .gwt-SliderBar-shell .gwt-SliderBar-ticklabel { the text labels along the line }
    • - *
    - */ -public class SliderBar extends FocusPanel implements RequiresResize, HasValue, HasValueChangeHandlers { - /** - * A formatter used to format the labels displayed in the widget. - */ - public static interface LabelFormatter { - /** - * Generate the text to display in each label based on the label's value. - * - * Override this method to change the text displayed within the SliderBar. - * - * @param slider - * the Slider bar - * @param value - * the value the label displays - * @return the text to display for the label - */ - String formatLabel(SliderBar slider, Double value, Double previousValue); - } - - /** - * A {@link ClientBundle} that provides images for {@link SliderBar}. - */ - public static interface SliderBarImages extends ClientBundle { - public static final SliderBarImages INSTANCE = GWT.create(SliderBarImages.class); - - /** - * An image used for the sliding knob. - * - * @return a prototype of this image - */ - @Source("slider_blue.png") - ImageResource slider(); - - /** - * An image used for the disabled sliding knob. - * - * @return a prototype of this image - */ - @Source("slider_blue.png") - ImageResource sliderDisabled(); - - /** - * An image used for the sliding knob while sliding. - * - * @return a prototype of this image - */ - @Source("slider_blue.png") - ImageResource sliderSliding(); - - @NotStrict - @Source("SliderBar.css") - CssResource sliderBarCss(); - } - - /** - * The timer used to continue to shift the knob as the user holds down one of the left/right arrow keys. Only IE - * auto-repeats, so we just keep catching the events. - */ - private class KeyTimer extends Timer { - /** - * A bit indicating that this is the first run. - */ - private boolean firstRun = true; - - /** - * The delay between shifts, which shortens as the user holds down the button. - */ - private int repeatDelay = 30; - - /** - * A bit indicating whether we are shifting to a higher or lower value. - */ - private boolean shiftRight = false; - - /** - * The number of steps to shift with each press. - */ - private int multiplier = 1; - - /** - * This method will be called when a timer fires. Override it to implement the timer's logic. - */ - @Override - public void run() { - // Highlight the knob on first run - if (firstRun) { - firstRun = false; - startSliding(true, false); - } - - // Slide the slider bar - if (shiftRight) { - setCurrentValue(curValue + multiplier * stepSize); - } else { - setCurrentValue(curValue - multiplier * stepSize); - } - - // Repeat this timer until cancelled by keyup event - schedule(repeatDelay); - } - - /** - * Schedules a timer to elapse in the future. - * - * @param delayMillis - * how long to wait before the timer elapses, in milliseconds - * @param shiftRight - * whether to shift up or not - * @param multiplier - * the number of steps to shift - */ - public void schedule(int delayMillis, boolean shiftRight, int multiplier) { - firstRun = true; - this.shiftRight = shiftRight; - this.multiplier = multiplier; - super.schedule(delayMillis); - } - } - - /** - * The current value. - */ - protected Double curValue; - - /** - * The knob that slides across the line. - */ - protected Image knobImage = new Image(); - - /** - * The timer used to continue to shift the knob if the user holds down a key. - */ - private KeyTimer keyTimer = new KeyTimer(); - - /** - * The elements used to display labels above the ticks. - */ - protected List tickLabelElements = new ArrayList(); - - /** - * The elements used to display the marker labels. - */ - protected List markerLabelElements = new ArrayList(); - - /** - * The formatter used to generate label text. - */ - protected LabelFormatter tickLabelFormatter; - - /** - * The line that the knob moves over. - */ - protected Element lineElement; - - /** - * The offset between the edge of the shell and the line. - */ - protected int lineLeftOffset = 0; - - /** - * The maximum slider value. - */ - protected Double maxValue; - - /** - * The minimum slider value. - */ - protected Double minValue; - - /** - * The number of labels to show. - */ - private int numTickLabels = 0; - - /** - * The number of tick marks to show. - */ - protected int numTicks = 0; - - /** - * A bit indicating whether or not we are currently sliding the slider bar due to keyboard events. - */ - private boolean slidingKeyboard = false; - - /** - * A bit indicating whether or not we are currently sliding the slider bar due to mouse events. - */ - private boolean slidingMouse = false; - - /** - * A bit indicating whether or not the slider is enabled - */ - protected boolean enabled = true; - - /** - * The images used with the sliding bar. - */ - private SliderBarImages images; - - /** - * The size of the increments between knob positions. - */ - protected double stepSize; - - /** - * The elements used to display tick marks, which are the vertical lines along the slider bar. - */ - protected List tickElements = new ArrayList(); - - /** - * The elements used to display additional markers on the slider bar. - */ - protected List markerElements = new ArrayList(); - - private List markers = new ArrayList(); - - private class Marker { - String name; - Double position; - - public Marker(String name, Double position) { - super(); - this.name = name; - this.position = position; - } - } - - /** - * Create a slider bar. - */ - public SliderBar() { - this(null, null, null); - } - - /** - * Create a slider bar. - * - * @param minValue - * the minimum value in the range - * @param maxValue - * the maximum value in the range - */ - public SliderBar(double minValue, double maxValue) { - this(minValue, maxValue, null); - } - - /** - * Create a slider bar. - * - * @param minValue - * the minimum value in the range - * @param maxValue - * the maximum value in the range - * @param tickLabelFormatter - * the label formatter - */ - public SliderBar(Double minValue, Double maxValue, LabelFormatter tickLabelFormatter) { - this(minValue, maxValue, tickLabelFormatter, SliderBarImages.INSTANCE); - } - - /** - * Create a slider bar. - * - * @param minValue - * the minimum value in the range - * @param maxValue - * the maximum value in the range - * @param tickLabelFormatter - * the label formatter - * @param images - * the images to use for the slider - */ - public SliderBar(Double minValue, Double maxValue, LabelFormatter tickLabelFormatter, SliderBarImages images) { - super(); - images.sliderBarCss().ensureInjected(); - this.minValue = minValue; - this.maxValue = maxValue; - this.images = images; - setLabelFormatter(tickLabelFormatter); - - // Create the outer shell - getElement().getStyle().setPosition(Position.RELATIVE); - setStyleName("gwt-SliderBar-shell"); - - // Create the line - lineElement = DOM.createDiv(); - DOM.appendChild(getElement(), lineElement); - lineElement.getStyle().setPosition(Position.ABSOLUTE); - lineElement.setPropertyString("className", "gwt-SliderBar-line"); - - // Create the knob - knobImage.setResource(images.slider()); - Element knobElement = knobImage.getElement(); - DOM.appendChild(getElement(), knobElement); - knobElement.getStyle().setPosition(Position.ABSOLUTE); - knobElement.setPropertyString("className", "gwt-SliderBar-knob"); - - sinkEvents(Event.MOUSEEVENTS | Event.KEYEVENTS | Event.FOCUSEVENTS); - - // workaround to render properly when parent Widget does not - // implement ProvidesResize since DOM doesn't provide element - // height and width until onModuleLoad() finishes. - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - @Override - public void execute() { - onResize(); - } - }); - } - - public boolean isMinMaxInitialized() { - return minValue != null && maxValue != null; - } - - public HandlerRegistration addValueChangeHandler(ValueChangeHandler handler) { - return addHandler(handler, ValueChangeEvent.getType()); - } - - /** - * Return the current value. - * - * @return the current value - */ - public Double getCurrentValue() { - return curValue; - } - - /** - * Return the label formatter. - * - * @return the label formatter - */ - public LabelFormatter getLabelFormatter() { - return tickLabelFormatter; - } - - /** - * Return the max value. - * - * @return the max value - */ - public Double getMaxValue() { - return maxValue; - } - - /** - * Return the minimum value. - * - * @return the minimum value - */ - public Double getMinValue() { - return minValue; - } - - /** - * Return the number of labels. - * - * @return the number of labels - */ - public int getNumLabels() { - return numTickLabels; - } - - /** - * Return the number of ticks. - * - * @return the number of ticks - */ - public int getNumTicks() { - return numTicks; - } - - /** - * Return the step size. - * - * @return the step size - */ - public double getStepSize() { - return stepSize; - } - - /** - * Return the total range between the minimum and maximum values. - * - * @return the total range - */ - public double getTotalRange() { - if (minValue == null || maxValue == null || minValue > maxValue) { - return 0; - } else { - return maxValue - minValue; - } - } - - public Double getValue() { - return curValue; - } - - /** - * @return Gets whether this widget is enabled - */ - public boolean isEnabled() { - return enabled; - } - - /** - * Listen for events that will move the knob. - * - * @param event - * the event that occurred - */ - @Override - public void onBrowserEvent(Event event) { - super.onBrowserEvent(event); - if (enabled) { - switch (DOM.eventGetType(event)) { - // Unhighlight and cancel keyboard events - case Event.ONBLUR: - keyTimer.cancel(); - if (slidingMouse) { - DOM.releaseCapture(getElement()); - slidingMouse = false; - slideKnob(event); - stopSliding(true, true); - } else if (slidingKeyboard) { - slidingKeyboard = false; - stopSliding(true, true); - } - unhighlight(); - break; - - // Highlight on focus - case Event.ONFOCUS: - highlight(); - break; - - // Mousewheel events - case Event.ONMOUSEWHEEL: - int velocityY = event.getMouseWheelVelocityY(); - event.preventDefault(); - if (velocityY > 0) { - shiftRight(1); - } else { - shiftLeft(1); - } - break; - - // Shift left or right on key press - case Event.ONKEYDOWN: - if (!slidingKeyboard) { - int multiplier = 1; - if (event.getCtrlKey()) { - multiplier = (int) (getTotalRange() / stepSize / 10); - } - - switch (event.getKeyCode()) { - case KeyCodes.KEY_HOME: - event.preventDefault(); - setCurrentValue(minValue); - break; - case KeyCodes.KEY_END: - event.preventDefault(); - setCurrentValue(maxValue); - break; - case KeyCodes.KEY_LEFT: - event.preventDefault(); - slidingKeyboard = true; - startSliding(false, true); - shiftLeft(multiplier); - keyTimer.schedule(400, false, multiplier); - break; - case KeyCodes.KEY_RIGHT: - event.preventDefault(); - slidingKeyboard = true; - startSliding(false, true); - shiftRight(multiplier); - keyTimer.schedule(400, true, multiplier); - break; - case 32: - event.preventDefault(); - setCurrentValue(minValue + getTotalRange() / 2); - break; - } - } - break; - // Stop shifting on key up - case Event.ONKEYUP: - keyTimer.cancel(); - if (slidingKeyboard) { - slidingKeyboard = false; - stopSliding(true, true); - } - break; - - // Mouse Events - case Event.ONMOUSEDOWN: - setFocus(true); - slidingMouse = true; - DOM.setCapture(getElement()); - startSliding(true, true); - event.preventDefault(); - slideKnob(event); - break; - case Event.ONMOUSEUP: - if (slidingMouse) { - DOM.releaseCapture(getElement()); - slidingMouse = false; - slideKnob(event); - stopSliding(true, true); - } - break; - case Event.ONMOUSEMOVE: - if (slidingMouse) { - slideKnob(event); - } - break; - } - } - } - - /** - * This method is called when the dimensions of the parent element change. Subclasses should override this method as - * needed. - * - * @param width - * the new client width of the element - * @param height - * the new client height of the element - */ - public void onResize(int width, int height) { - // Center the line in the shell - int lineWidth = lineElement.getOffsetWidth(); - lineLeftOffset = (width / 2) - (lineWidth / 2); - lineElement.getStyle().setLeft(lineLeftOffset, Unit.PX); - - // Draw the other components - drawTickLabels(); - drawTicks(); - drawMarkers(); - drawMarkerLabels(); - drawKnob(); - } - - /** - * Redraw the progress bar when something changes the layout. - */ - public void redraw() { - if (isAttached()) { - int width = getElement().getClientWidth(); - int height = getElement().getClientHeight(); - onResize(width, height); - } - } - - /** - * Set the current value and fire the onValueChange event. - * - * @param curValue - * the current value - */ - public synchronized void setCurrentValue(Double curValue) { - setCurrentValue(curValue, true); - } - - /** - * Set the current value and optionally fire the onValueChange event. - * - * @param curValue - * the current value - * @param fireEvent - * fire the onValue change event if true - */ - public synchronized void setCurrentValue(Double curValue, boolean fireEvent) { - // Confine the value to the range - if (!isMinMaxInitialized() || curValue == null) { - return; - } - this.curValue = Math.max(minValue, Math.min(maxValue, curValue)); - double remainder = (this.curValue - minValue) % stepSize; - this.curValue -= remainder; - // Go to next step if more than halfway there - if ((remainder > (stepSize / 2)) && ((this.curValue + stepSize) <= maxValue)) { - this.curValue += stepSize; - } - // Redraw the knob - drawKnob(); - // Fire the ValueChangeEvent if the value actually changed - if (fireEvent && !curValue.equals(this.curValue)) { - ValueChangeEvent.fire(this, this.curValue); - } - } - - /** - * Sets whether this widget is enabled. - * - * @param enabled - * true to enable the widget, false to disable it - */ - public void setEnabled(boolean enabled) { - this.enabled = enabled; - if (enabled) { - knobImage.setResource(images.slider()); - lineElement.setPropertyString("className", "gwt-SliderBar-line"); - } else { - knobImage.setResource(images.sliderDisabled()); - lineElement.setPropertyString("className", "gwt-SliderBar-line gwt-SliderBar-line-disabled"); - } - redraw(); - } - - /** - * Set the label formatter. - * - * @param labelFormatter - * the label formatter - */ - public void setLabelFormatter(LabelFormatter labelFormatter) { - this.tickLabelFormatter = labelFormatter; - } - - /** - * Set the max value. - * - * @param maxValue - * the current value - */ - public void setMaxValue(Double maxValue, boolean fireEvent) { - if (!Util.equalsWithNull(maxValue, this.maxValue)) { - this.maxValue = maxValue; - onMinMaxValueChanged(fireEvent); - } - } - - /** - * Set the minimum value. - * - * @param minValue - * the current value - */ - public void setMinValue(Double minValue, boolean fireEvent) { - if (!Util.equalsWithNull(minValue, this.minValue)) { - this.minValue = minValue; - onMinMaxValueChanged(fireEvent); - } - } - - /** - * Set the minimum and maximum value - * - * @param minValue - * the current value for min - * @param maxValue - * the current valuefor max - */ - public void setMinAndMaxValue(Double minValue, Double maxValue, boolean fireEvent) { - boolean changed = false; - if (!Util.equalsWithNull(minValue, this.minValue)) { - this.minValue = minValue; - changed = true; - } - if (!Util.equalsWithNull(maxValue, this.maxValue)) { - this.maxValue = maxValue; - changed = true; - } - if (changed) { - onMinMaxValueChanged(fireEvent); - } - } - - /** - * Handle value changes of min and/or max value - */ - protected void onMinMaxValueChanged(boolean fireEvent) { - redraw(); - resetCurrentValue(fireEvent); - } - - /** - * Set the number of tick labels to show on the line. Tick labels indicate the value of the slider at that point. - * Use this method to enable tick labels. - * - * If you set the number of tick labels equal to the total range divided by the step size, you will get a properly - * aligned "jumping" effect where the knob jumps between tick labels. - * - * Note that the number of tick labels displayed will be one more than the number you specify, so specify 1 labels - * to show labels on either end of the line. In other words, numTickLabels is really the number of slots between the - * labels. - * - * setNumTickLabels(0) will disable the labels. - * - * @param numTickLabels - * the number of tick labels to show - */ - public void setNumTickLabels(int numTickLabels) { - this.numTickLabels = numTickLabels; - drawTickLabels(); - } - - /** - * Set the number of ticks to show on the line. A tick is a vertical line that represents a division of the overall - * line. Use this method to enable ticks. - * - * If you set the number of ticks equal to the total range divided by the step size, you will get a properly aligned - * "jumping" effect where the knob jumps between ticks. - * - * Note that the number of ticks displayed will be one more than the number you specify, so specify 1 tick to show - * ticks on either end of the line. In other words, numTicks is really the number of slots between the ticks. - * - * setNumTicks(0) will disable ticks. - * - * @param numTicks - * the number of ticks to show - */ - public void setNumTicks(int numTicks) { - this.numTicks = numTicks; - drawTicks(); - } - - /** - * Set the step size. - * - * @param stepSize - * the current value - */ - public void setStepSize(double stepSize, boolean fireEvent) { - this.stepSize = stepSize; - resetCurrentValue(fireEvent); - } - - public void setValue(Double value) { - setCurrentValue(value, false); - } - - public void setValue(Double value, boolean fireEvent) { - setCurrentValue(value, fireEvent); - } - - /** - * Shift to the left (smaller value). - * - * @param numSteps - * the number of steps to shift - */ - public void shiftLeft(int numSteps) { - setCurrentValue(getCurrentValue() - numSteps * stepSize); - } - - /** - * Shift to the right (greater value). - * - * @param numSteps - * the number of steps to shift - */ - public void shiftRight(int numSteps) { - setCurrentValue(getCurrentValue() + numSteps * stepSize); - } - - /** - * Format the label to display above the ticks - * - * Override this method in a subclass to customize the format. By default, this method returns the integer portion - * of the value. - * - * @param value - * the value at the label - * @return the text to put in the label - */ - protected String formatTickLabel(Double value, Double previousValue) { - if (tickLabelFormatter != null) { - return tickLabelFormatter.formatLabel(this, value, previousValue); - } else { - return (int) (10 * value) / 10.0 + ""; - } - } - - /** - * Get the percentage of the knob's position relative to the size of the line. The return value will be between 0.0 - * and 1.0. - * - * @return the current percent complete - */ - protected double getKnobPercent() { - // If we have no range - if (maxValue <= minValue) { - return 0; - } - - // Calculate the relative progress - double percent = (curValue - minValue) / (maxValue - minValue); - return Math.max(0.0, Math.min(1.0, percent)); - } - - /** - * This method is called immediately after a widget becomes attached to the browser's document. - */ - @Override - protected void onLoad() { - // Reset the position attribute of the parent element - getElement().getStyle().setPosition(Position.RELATIVE); - } - - /** - * Draw the knob where it is supposed to be relative to the line. - */ - protected void drawKnob() { - if (isAttached() && isMinMaxInitialized()) { - // Move the knob to the correct position - Element knobElement = knobImage.getElement(); - int lineWidth = lineElement.getOffsetWidth(); - int knobWidth = knobElement.getOffsetWidth(); - int knobLeftOffset = (int) (lineLeftOffset + (getKnobPercent() * lineWidth) - (knobWidth / 2)); - knobLeftOffset = Math.min(knobLeftOffset, lineLeftOffset + lineWidth - (knobWidth / 2) - 1); - knobElement.getStyle().setLeft(knobLeftOffset, Unit.PX); - } - } - - /** - * Draw the labels along the line. - */ - protected void drawTickLabels() { - if (isAttached() && isMinMaxInitialized()) { - // Draw the tick labels - int lineWidth = lineElement.getOffsetWidth(); - if (numTickLabels > 0) { - // Create the labels or make them visible - Double previousValue = null; - for (int i = 0; i <= numTickLabels; i++) { - Element label = null; - if (i < tickLabelElements.size()) { - label = tickLabelElements.get(i); - } else { // Create the new label - label = DOM.createDiv(); - label.getStyle().setPosition(Position.ABSOLUTE); - label.getStyle().setDisplay(Display.NONE); - if (enabled) { - label.setPropertyString("className", "gwt-SliderBar-ticklabel"); - } else { - label.setPropertyString("className", "gwt-SliderBar-ticklabel-disabled"); - } - DOM.appendChild(getElement(), label); - tickLabelElements.add(label); - } - - // Set the label text - double value = minValue + (getTotalRange() * i / numTickLabels); - label.getStyle().setVisibility(Visibility.HIDDEN); - label.getStyle().setProperty("display", ""); - label.setPropertyString("innerHTML", formatTickLabel(value, previousValue)); - - // Move to the left so the label width is not clipped by the shell - label.getStyle().setLeft(0, Unit.PX); - - // Position the label and make it visible - int labelWidth = label.getOffsetWidth(); - int labelLeftOffset = lineLeftOffset + (lineWidth * i / numTickLabels) - (labelWidth / 2); - labelLeftOffset = Math.min(labelLeftOffset, lineLeftOffset + lineWidth - labelWidth); - labelLeftOffset = Math.max(labelLeftOffset, lineLeftOffset); - label.getStyle().setLeft(labelLeftOffset, Unit.PX); - label.getStyle().setVisibility(Visibility.VISIBLE); - - previousValue = value; - } - - // Hide unused labels - for (int i = (numTickLabels + 1); i < tickLabelElements.size(); i++) { - tickLabelElements.get(i).getStyle().setDisplay(Display.NONE); - } - } else { // Hide all labels - for (Element elem : tickLabelElements) { - elem.getStyle().setDisplay(Display.NONE); - } - } - } - } - - /** - * Draw the tick along the line. - */ - protected void drawTicks() { - if (isAttached() && isMinMaxInitialized()) { - // Draw the ticks - int lineWidth = lineElement.getOffsetWidth(); - if (numTicks > 0) { - // Create the ticks or make them visible - for (int i = 0; i <= numTicks; i++) { - Element tick = null; - if (i < tickElements.size()) { - tick = tickElements.get(i); - } else { // Create the new tick - tick = DOM.createDiv(); - tick.getStyle().setPosition(Position.ABSOLUTE); - tick.getStyle().setDisplay(Display.NONE); - DOM.appendChild(getElement(), tick); - tickElements.add(tick); - } - if (enabled) { - tick.setPropertyString("className", "gwt-SliderBar-tick"); - } else { - tick.setPropertyString("className", "gwt-SliderBar-tick gwt-SliderBar-tick-disabled"); - } - // Position the tick and make it visible - tick.getStyle().setVisibility(Visibility.HIDDEN); - tick.getStyle().setProperty("display", ""); - int tickWidth = tick.getOffsetWidth(); - int tickLeftOffset = lineLeftOffset + (lineWidth * i / numTicks) - (tickWidth / 2); - tickLeftOffset = Math.min(tickLeftOffset, lineLeftOffset + lineWidth - tickWidth); - tick.getStyle().setLeft(tickLeftOffset, Unit.PX); - tick.getStyle().setVisibility(Visibility.VISIBLE); - } - - // Hide unused ticks - for (int i = (numTicks + 1); i < tickElements.size(); i++) { - tickElements.get(i).getStyle().setDisplay(Display.NONE); - } - } else { // Hide all ticks - for (Element elem : tickElements) { - elem.getStyle().setDisplay(Display.NONE); - } - } - } - } - - /** - * Draw the markers. - */ - private void drawMarkers() { - if (isAttached() && isMinMaxInitialized()) { - int numMarkers = markers.size(); - // Draw the markers - int lineWidth = lineElement.getOffsetWidth(); - if (numMarkers > 0) { - // Create the markers or make them visible - Element lastMarkerElement = null; - int lastMarkerOffsetLeft = 0; - for (int i = 0; i < numMarkers; i++) { - Marker marker = markers.get(i); - Element markerElem = null; - if (i < markerElements.size()) { - markerElem = markerElements.get(i); - } else { // Create the new markes - markerElem = DOM.createDiv(); - markerElem.getStyle().setPosition(Position.ABSOLUTE); - markerElem.getStyle().setDisplay(Display.NONE); - DOM.appendChild(getElement(), markerElem); - markerElements.add(markerElem); - } - if (enabled) { - markerElem.setPropertyString("className", "gwt-SliderBar-mark"); - } else { - markerElem.setPropertyString("className", "gwt-SliderBar-mark gwt-SliderBar-mark-disabled"); - } - // Position the marker and make it visible - markerElem.getStyle().setVisibility(Visibility.HIDDEN); - markerElem.getStyle().setProperty("display", ""); - double markerLinePosition = (marker.position - minValue) * lineWidth / getTotalRange(); - int markerWidth = markerElem.getOffsetWidth(); - int markerLeftOffset = lineLeftOffset + (int) markerLinePosition - (markerWidth / 2); - markerLeftOffset = Math.min(markerLeftOffset, lineLeftOffset + lineWidth - markerWidth); - markerElem.getStyle().setLeft(markerLeftOffset, Unit.PX); - markerElem.getStyle().setVisibility(Visibility.VISIBLE); - // change class of last marker if it is too close to the one just created - if (lastMarkerOffsetLeft > 0 && (markerLeftOffset-lastMarkerOffsetLeft) <= markerWidth) { - if (lastMarkerElement != null) { - lastMarkerElement.setPropertyString("className", "gwt-SliderBar-mark-tooClose"); - lastMarkerElement.getStyle().setLeft(markerLeftOffset-10, Unit.PX); - } - } - lastMarkerElement = markerElem; - lastMarkerOffsetLeft = markerLeftOffset; - } - - // Hide unused markers - for (int i = numMarkers; i < markerElements.size(); i++) { - markerElements.get(i).getStyle().setDisplay(Display.NONE); - } - } else { // Hide all markers - for (Element elem : markerElements) { - elem.getStyle().setDisplay(Display.NONE); - } - } - } - } - - /** - * Draw the marker labels. - */ - private void drawMarkerLabels() { - if (isAttached() && isMinMaxInitialized()) { - int numMarkers = markers.size(); - // Draw the marker labels - int lineWidth = lineElement.getOffsetWidth(); - if (numMarkers > 0) { - Element lastMarkerLabel = null; - Marker lastMarker = null; - int lastMarkerLabelOffsetLeft = 0; - // Create the labels or make them visible - for (int i = 0; i < numMarkers; i++) { - Marker marker = markers.get(i); - Element label = null; - if (i < markerLabelElements.size()) { - label = markerLabelElements.get(i); - } else { // Create the new label - label = DOM.createDiv(); - label.getStyle().setPosition(Position.ABSOLUTE); - label.getStyle().setDisplay(Display.NONE); - if (enabled) { - label.setPropertyString("className", "gwt-SliderBar-markerlabel"); - } else { - label.setPropertyString("className", "gwt-SliderBar-markerlabel-disabled"); - } - DOM.appendChild(getElement(), label); - markerLabelElements.add(label); - } - - // Set the marker label text - label.getStyle().setVisibility(Visibility.HIDDEN); - label.getStyle().setProperty("display", ""); - label.setPropertyString("innerHTML", marker.name); - - // Move to the left so the label width is not clipped by the shell - label.getStyle().setLeft(0, Unit.PX); - - // Position the label and make it visible - double markerLinePosition = (marker.position - minValue) * lineWidth / getTotalRange(); - int labelWidth = label.getOffsetWidth(); - int labelLeftOffset = lineLeftOffset + (int) markerLinePosition - (labelWidth / 2); - labelLeftOffset = Math.min(labelLeftOffset, lineLeftOffset + lineWidth - labelWidth); - - label.getStyle().setLeft(labelLeftOffset, Unit.PX); - label.getStyle().setVisibility(Visibility.VISIBLE); - - // hide last marker label if it is too close to the one just created - if (lastMarkerLabelOffsetLeft > 0 && (labelLeftOffset-lastMarkerLabelOffsetLeft) <= 20) { - if (lastMarkerLabel != null && lastMarker != null) { - lastMarkerLabel.setPropertyString("className", "gwt-SliderBar-markerlabel-tooClose"); - lastMarkerLabel.setPropertyString("innerHTML", lastMarker.name); // no space for more - int currentMarkerOffsetLeft = labelLeftOffset; - try { - currentMarkerOffsetLeft = Integer.parseInt(markerElements.get(i).getStyle().getLeft().replace("px", "")); - } catch (NumberFormatException ex) { - // ignore, can't do anything - } - lastMarkerLabel.getStyle().setLeft(currentMarkerOffsetLeft-8, Unit.PX); - } - } - lastMarker = marker; - lastMarkerLabel = label; - lastMarkerLabelOffsetLeft = labelLeftOffset; - } - - // Hide unused labels - for (int i = (numMarkers + 1); i < markerLabelElements.size(); i++) { - markerLabelElements.get(i).getStyle().setDisplay(Display.NONE); - } - } else { // Hide all labels - for (Element elem : markerLabelElements) { - elem.getStyle().setDisplay(Display.NONE); - } - } - } - } - - /** - * Highlight this widget. - */ - private void highlight() { - String styleName = getStylePrimaryName(); - getElement().setPropertyString("className", styleName + " " + styleName + "-focused"); - } - - /** - * Reset the progress to constrain the progress to the current range and redraw the knob as needed. - */ - private synchronized void resetCurrentValue(boolean fireEvent) { - setCurrentValue(getCurrentValue(), fireEvent); - } - - /** - * Slide the knob to a new location. - * - * @param event - * the mouse event - */ - private void slideKnob(Event event) { - // Adding scrollLeft to adjust the position, if the user had scrolled with the lower scroll bar - int x = event.getClientX() + Window.getScrollLeft(); - if (x > 0) { - int lineWidth = lineElement.getOffsetWidth(); - int lineLeft = lineElement.getAbsoluteLeft(); - double percent = (double) (x - lineLeft) / lineWidth * 1.0; - setCurrentValue(getTotalRange() * percent + minValue, true); - } - } - - /** - * Start sliding the knob. - * - * @param highlight - * true to change the style - * @param fireEvent - * true to fire the event - */ - private void startSliding(boolean highlight, boolean fireEvent) { - if (highlight) { - lineElement.setPropertyString("className", "gwt-SliderBar-line gwt-SliderBar-line-sliding"); - knobImage.getElement().setPropertyString("className", "gwt-SliderBar-knob gwt-SliderBar-knob-sliding"); - knobImage.setResource(images.sliderSliding()); - } - } - - /** - * Stop sliding the knob. - * - * @param unhighlight - * true to change the style - * @param fireEvent - * true to fire the event - */ - private void stopSliding(boolean unhighlight, boolean fireEvent) { - if (unhighlight) { - lineElement.setPropertyString("className", "gwt-SliderBar-line"); - knobImage.getElement().setPropertyString("className", "gwt-SliderBar-knob"); - knobImage.setResource(images.slider()); - } - } - - /** - * Unhighlight this widget. - */ - private void unhighlight() { - getElement().setPropertyString("className", getStylePrimaryName()); - } - - @Override - public void onResize() { - redraw(); - } - - public void clearMarkers() { - markers.clear(); - } - - public boolean addMarker(String markerName, Double markerPosition) { - return markers.add(new Marker(markerName, markerPosition)); - } - - public boolean setMarker(String markerName, Double markerPosition) { - Marker marker = findMarkerByName(markerName); - if (marker != null) { - marker.position = markerPosition; - return true; - } - return false; - } - - public boolean removeMarker(String markerName) { - Marker marker = findMarkerByName(markerName); - if (marker != null) { - return markers.remove(marker); - } - return false; - } - - public Iterator getMarkers() { - return markers.iterator(); - } - - private Marker findMarkerByName(String markerName) { - for (Marker marker : markers) { - if (marker.name.equals(markerName)) { - return marker; - } - } - return null; - } -} +package com.sap.sse.gwt.client.controls.slider; + +/* + * Copyright 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +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.dom.client.Element; +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.dom.client.Style.Visibility; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.logical.shared.HasValueChangeHandlers; +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.event.logical.shared.ValueChangeHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.resources.client.ClientBundle; +import com.google.gwt.resources.client.CssResource; +import com.google.gwt.resources.client.CssResource.NotStrict; +import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.FocusPanel; +import com.google.gwt.user.client.ui.HasValue; +import com.google.gwt.user.client.ui.Image; +import com.google.gwt.user.client.ui.RequiresResize; +import com.sap.sse.common.Util; + +/** + * A widget that allows the user to select a value within a range of possible values using a sliding bar that responds + * to mouse events. + * + *

    Keyboard Events

    + *

    + * SliderBar listens for the following key events. Holding down a key will repeat the action until the key is released. + *

      + *
    • left arrow - shift left one step
    • + *
    • right arrow - shift right one step
    • + *
    • ctrl+left arrow - jump left 10% of the distance
    • + *
    • ctrl+right arrow - jump right 10% of the distance
    • + *
    • home - jump to min value
    • + *
    • end - jump to max value
    • + *
    • space - jump to middle value
    • + *
    + *

    + * + *

    CSS Style Rules

    + *
      + *
    • .gwt-SliderBar-shell { primary style }
    • + *
    • .gwt-SliderBar-shell-focused { primary style when focused }
    • + *
    • .gwt-SliderBar-shell gwt-SliderBar-line { the line that the knob moves along }
    • + *
    • .gwt-SliderBar-shell gwt-SliderBar-line-sliding { the line that the knob moves along when sliding }
    • + *
    • .gwt-SliderBar-shell .gwt-SliderBar-knob { the sliding knob }
    • + *
    • .gwt-SliderBar-shell .gwt-SliderBar-knob-sliding { the sliding knob when sliding }
    • + *
    • + * .gwt-SliderBar-shell .gwt-SliderBar-tick { the ticks along the line }
    • + *
    • .gwt-SliderBar-shell .gwt-SliderBar-ticklabel { the text labels along the line }
    • + *
    + */ +public class SliderBar extends FocusPanel implements RequiresResize, HasValue, HasValueChangeHandlers { + /** + * A formatter used to format the labels displayed in the widget. + */ + public static interface LabelFormatter { + /** + * Generate the text to display in each label based on the label's value. + * + * Override this method to change the text displayed within the SliderBar. + * + * @param slider + * the Slider bar + * @param value + * the value the label displays + * @return the text to display for the label + */ + String formatLabel(SliderBar slider, Double value, Double previousValue); + } + + /** + * A {@link ClientBundle} that provides images for {@link SliderBar}. + */ + public static interface SliderBarImages extends ClientBundle { + public static final SliderBarImages INSTANCE = GWT.create(SliderBarImages.class); + + /** + * An image used for the sliding knob. + * + * @return a prototype of this image + */ + @Source("slider_blue.png") + ImageResource slider(); + + /** + * An image used for the disabled sliding knob. + * + * @return a prototype of this image + */ + @Source("slider_blue.png") + ImageResource sliderDisabled(); + + /** + * An image used for the sliding knob while sliding. + * + * @return a prototype of this image + */ + @Source("slider_blue.png") + ImageResource sliderSliding(); + + @NotStrict + @Source("SliderBar.css") + CssResource sliderBarCss(); + } + + /** + * The timer used to continue to shift the knob as the user holds down one of the left/right arrow keys. Only IE + * auto-repeats, so we just keep catching the events. + */ + private class KeyTimer extends Timer { + /** + * A bit indicating that this is the first run. + */ + private boolean firstRun = true; + + /** + * The delay between shifts, which shortens as the user holds down the button. + */ + private int repeatDelay = 30; + + /** + * A bit indicating whether we are shifting to a higher or lower value. + */ + private boolean shiftRight = false; + + /** + * The number of steps to shift with each press. + */ + private int multiplier = 1; + + /** + * This method will be called when a timer fires. Override it to implement the timer's logic. + */ + @Override + public void run() { + // Highlight the knob on first run + if (firstRun) { + firstRun = false; + startSliding(true, false); + } + + // Slide the slider bar + if (shiftRight) { + setCurrentValue(curValue + multiplier * stepSize); + } else { + setCurrentValue(curValue - multiplier * stepSize); + } + + // Repeat this timer until cancelled by keyup event + schedule(repeatDelay); + } + + /** + * Schedules a timer to elapse in the future. + * + * @param delayMillis + * how long to wait before the timer elapses, in milliseconds + * @param shiftRight + * whether to shift up or not + * @param multiplier + * the number of steps to shift + */ + public void schedule(int delayMillis, boolean shiftRight, int multiplier) { + firstRun = true; + this.shiftRight = shiftRight; + this.multiplier = multiplier; + super.schedule(delayMillis); + } + } + + /** + * The current value. + */ + protected Double curValue; + + /** + * The knob that slides across the line. + */ + protected Image knobImage = new Image(); + + /** + * The timer used to continue to shift the knob if the user holds down a key. + */ + private KeyTimer keyTimer = new KeyTimer(); + + /** + * The elements used to display labels above the ticks. + */ + protected List tickLabelElements = new ArrayList(); + + /** + * The elements used to display the marker labels. + */ + protected List markerLabelElements = new ArrayList(); + + /** + * The formatter used to generate label text. + */ + protected LabelFormatter tickLabelFormatter; + + /** + * The line that the knob moves over. + */ + protected Element lineElement; + + /** + * The offset between the edge of the shell and the line. + */ + protected int lineLeftOffset = 0; + + /** + * The maximum slider value. + */ + protected Double maxValue; + + /** + * The minimum slider value. + */ + protected Double minValue; + + /** + * The number of labels to show. + */ + private int numTickLabels = 0; + + /** + * The number of tick marks to show. + */ + protected int numTicks = 0; + + /** + * A bit indicating whether or not we are currently sliding the slider bar due to keyboard events. + */ + private boolean slidingKeyboard = false; + + /** + * A bit indicating whether or not we are currently sliding the slider bar due to mouse events. + */ + private boolean slidingMouse = false; + + /** + * A bit indicating whether or not the slider is enabled + */ + protected boolean enabled = true; + + /** + * The images used with the sliding bar. + */ + private SliderBarImages images; + + /** + * The size of the increments between knob positions. + */ + protected double stepSize; + + /** + * The elements used to display tick marks, which are the vertical lines along the slider bar. + */ + protected List tickElements = new ArrayList(); + + /** + * The elements used to display additional markers on the slider bar. + */ + protected List markerElements = new ArrayList(); + + private List markers = new ArrayList(); + + private class Marker { + String name; + Double position; + + public Marker(String name, Double position) { + super(); + this.name = name; + this.position = position; + } + } + + /** + * Create a slider bar. + */ + public SliderBar() { + this(null, null, null); + } + + /** + * Create a slider bar. + * + * @param minValue + * the minimum value in the range + * @param maxValue + * the maximum value in the range + */ + public SliderBar(double minValue, double maxValue) { + this(minValue, maxValue, null); + } + + /** + * Create a slider bar. + * + * @param minValue + * the minimum value in the range + * @param maxValue + * the maximum value in the range + * @param tickLabelFormatter + * the label formatter + */ + public SliderBar(Double minValue, Double maxValue, LabelFormatter tickLabelFormatter) { + this(minValue, maxValue, tickLabelFormatter, SliderBarImages.INSTANCE); + } + + /** + * Create a slider bar. + * + * @param minValue + * the minimum value in the range + * @param maxValue + * the maximum value in the range + * @param tickLabelFormatter + * the label formatter + * @param images + * the images to use for the slider + */ + public SliderBar(Double minValue, Double maxValue, LabelFormatter tickLabelFormatter, SliderBarImages images) { + super(); + images.sliderBarCss().ensureInjected(); + this.minValue = minValue; + this.maxValue = maxValue; + this.images = images; + setLabelFormatter(tickLabelFormatter); + + // Create the outer shell + getElement().getStyle().setPosition(Position.RELATIVE); + setStyleName("gwt-SliderBar-shell"); + + // Create the line + lineElement = DOM.createDiv(); + DOM.appendChild(getElement(), lineElement); + lineElement.getStyle().setPosition(Position.ABSOLUTE); + lineElement.setPropertyString("className", "gwt-SliderBar-line"); + + // Create the knob + knobImage.setResource(images.slider()); + Element knobElement = knobImage.getElement(); + DOM.appendChild(getElement(), knobElement); + knobElement.getStyle().setPosition(Position.ABSOLUTE); + knobElement.setPropertyString("className", "gwt-SliderBar-knob"); + + sinkEvents(Event.MOUSEEVENTS | Event.KEYEVENTS | Event.FOCUSEVENTS); + + // workaround to render properly when parent Widget does not + // implement ProvidesResize since DOM doesn't provide element + // height and width until onModuleLoad() finishes. + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + onResize(); + } + }); + } + + public boolean isMinMaxInitialized() { + return minValue != null && maxValue != null; + } + + public HandlerRegistration addValueChangeHandler(ValueChangeHandler handler) { + return addHandler(handler, ValueChangeEvent.getType()); + } + + /** + * Return the current value. + * + * @return the current value + */ + public Double getCurrentValue() { + return curValue; + } + + /** + * Return the label formatter. + * + * @return the label formatter + */ + public LabelFormatter getLabelFormatter() { + return tickLabelFormatter; + } + + /** + * Return the max value. + * + * @return the max value + */ + public Double getMaxValue() { + return maxValue; + } + + /** + * Return the minimum value. + * + * @return the minimum value + */ + public Double getMinValue() { + return minValue; + } + + /** + * Return the number of labels. + * + * @return the number of labels + */ + public int getNumLabels() { + return numTickLabels; + } + + /** + * Return the number of ticks. + * + * @return the number of ticks + */ + public int getNumTicks() { + return numTicks; + } + + /** + * Return the step size. + * + * @return the step size + */ + public double getStepSize() { + return stepSize; + } + + /** + * Return the total range between the minimum and maximum values. + * + * @return the total range + */ + public double getTotalRange() { + if (minValue == null || maxValue == null || minValue > maxValue) { + return 0; + } else { + return maxValue - minValue; + } + } + + public Double getValue() { + return curValue; + } + + /** + * @return Gets whether this widget is enabled + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Listen for events that will move the knob. + * + * @param event + * the event that occurred + */ + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (enabled) { + switch (DOM.eventGetType(event)) { + // Unhighlight and cancel keyboard events + case Event.ONBLUR: + keyTimer.cancel(); + if (slidingMouse) { + DOM.releaseCapture(getElement()); + slidingMouse = false; + slideKnob(event); + stopSliding(true, true); + } else if (slidingKeyboard) { + slidingKeyboard = false; + stopSliding(true, true); + } + unhighlight(); + break; + + // Highlight on focus + case Event.ONFOCUS: + highlight(); + break; + + // Mousewheel events + case Event.ONMOUSEWHEEL: + int velocityY = event.getMouseWheelVelocityY(); + event.preventDefault(); + if (velocityY > 0) { + shiftRight(1); + } else { + shiftLeft(1); + } + break; + + // Shift left or right on key press + case Event.ONKEYDOWN: + if (!slidingKeyboard) { + int multiplier = 1; + if (event.getCtrlKey()) { + multiplier = (int) (getTotalRange() / stepSize / 10); + } + + switch (event.getKeyCode()) { + case KeyCodes.KEY_HOME: + event.preventDefault(); + setCurrentValue(minValue); + break; + case KeyCodes.KEY_END: + event.preventDefault(); + setCurrentValue(maxValue); + break; + case KeyCodes.KEY_LEFT: + event.preventDefault(); + slidingKeyboard = true; + startSliding(false, true); + shiftLeft(multiplier); + keyTimer.schedule(400, false, multiplier); + break; + case KeyCodes.KEY_RIGHT: + event.preventDefault(); + slidingKeyboard = true; + startSliding(false, true); + shiftRight(multiplier); + keyTimer.schedule(400, true, multiplier); + break; + case 32: + event.preventDefault(); + setCurrentValue(minValue + getTotalRange() / 2); + break; + } + } + break; + // Stop shifting on key up + case Event.ONKEYUP: + keyTimer.cancel(); + if (slidingKeyboard) { + slidingKeyboard = false; + stopSliding(true, true); + } + break; + + // Mouse Events + case Event.ONMOUSEDOWN: + setFocus(true); + slidingMouse = true; + DOM.setCapture(getElement()); + startSliding(true, true); + event.preventDefault(); + slideKnob(event); + break; + case Event.ONMOUSEUP: + if (slidingMouse) { + DOM.releaseCapture(getElement()); + slidingMouse = false; + slideKnob(event); + stopSliding(true, true); + } + break; + case Event.ONMOUSEMOVE: + if (slidingMouse) { + slideKnob(event); + } + break; + } + } + } + + /** + * This method is called when the dimensions of the parent element change. Subclasses should override this method as + * needed. + * + * @param width + * the new client width of the element + * @param height + * the new client height of the element + */ + public void onResize(int width, int height) { + // Center the line in the shell + int lineWidth = lineElement.getOffsetWidth(); + lineLeftOffset = (width / 2) - (lineWidth / 2); + lineElement.getStyle().setLeft(lineLeftOffset, Unit.PX); + + // Draw the other components + drawTickLabels(); + drawTicks(); + drawMarkers(); + drawMarkerLabels(); + drawKnob(); + } + + /** + * Redraw the progress bar when something changes the layout. + */ + public void redraw() { + if (isAttached()) { + int width = getElement().getClientWidth(); + int height = getElement().getClientHeight(); + onResize(width, height); + } + } + + /** + * Set the current value and fire the onValueChange event. + * + * @param curValue + * the current value + */ + public synchronized void setCurrentValue(Double curValue) { + setCurrentValue(curValue, true); + } + + /** + * Set the current value and optionally fire the onValueChange event. + * + * @param curValue + * the current value + * @param fireEvent + * fire the onValue change event if true + */ + public synchronized void setCurrentValue(Double curValue, boolean fireEvent) { + // Confine the value to the range + if (!isMinMaxInitialized() || curValue == null) { + return; + } + this.curValue = Math.max(minValue, Math.min(maxValue, curValue)); + double remainder = (this.curValue - minValue) % stepSize; + this.curValue -= remainder; + // Go to next step if more than halfway there + if ((remainder > (stepSize / 2)) && ((this.curValue + stepSize) <= maxValue)) { + this.curValue += stepSize; + } + // Redraw the knob + drawKnob(); + // Fire the ValueChangeEvent if the value actually changed + if (fireEvent && !curValue.equals(this.curValue)) { + ValueChangeEvent.fire(this, this.curValue); + } + } + + /** + * Sets whether this widget is enabled. + * + * @param enabled + * true to enable the widget, false to disable it + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + if (enabled) { + knobImage.setResource(images.slider()); + lineElement.setPropertyString("className", "gwt-SliderBar-line"); + } else { + knobImage.setResource(images.sliderDisabled()); + lineElement.setPropertyString("className", "gwt-SliderBar-line gwt-SliderBar-line-disabled"); + } + redraw(); + } + + /** + * Set the label formatter. + * + * @param labelFormatter + * the label formatter + */ + public void setLabelFormatter(LabelFormatter labelFormatter) { + this.tickLabelFormatter = labelFormatter; + } + + /** + * Set the max value. + * + * @param maxValue + * the current value + */ + public void setMaxValue(Double maxValue, boolean fireEvent) { + if (!Util.equalsWithNull(maxValue, this.maxValue)) { + this.maxValue = maxValue; + onMinMaxValueChanged(fireEvent); + } + } + + /** + * Set the minimum value. + * + * @param minValue + * the current value + */ + public void setMinValue(Double minValue, boolean fireEvent) { + if (!Util.equalsWithNull(minValue, this.minValue)) { + this.minValue = minValue; + onMinMaxValueChanged(fireEvent); + } + } + + /** + * Set the minimum and maximum value + * + * @param minValue + * the current value for min + * @param maxValue + * the current valuefor max + */ + public void setMinAndMaxValue(Double minValue, Double maxValue, boolean fireEvent) { + boolean changed = false; + if (!Util.equalsWithNull(minValue, this.minValue)) { + this.minValue = minValue; + changed = true; + } + if (!Util.equalsWithNull(maxValue, this.maxValue)) { + this.maxValue = maxValue; + changed = true; + } + if (changed) { + onMinMaxValueChanged(fireEvent); + } + } + + /** + * Handle value changes of min and/or max value + */ + protected void onMinMaxValueChanged(boolean fireEvent) { + redraw(); + resetCurrentValue(fireEvent); + } + + /** + * Set the number of tick labels to show on the line. Tick labels indicate the value of the slider at that point. + * Use this method to enable tick labels. + * + * If you set the number of tick labels equal to the total range divided by the step size, you will get a properly + * aligned "jumping" effect where the knob jumps between tick labels. + * + * Note that the number of tick labels displayed will be one more than the number you specify, so specify 1 labels + * to show labels on either end of the line. In other words, numTickLabels is really the number of slots between the + * labels. + * + * setNumTickLabels(0) will disable the labels. + * + * @param numTickLabels + * the number of tick labels to show + */ + public void setNumTickLabels(int numTickLabels) { + this.numTickLabels = numTickLabels; + drawTickLabels(); + } + + /** + * Set the number of ticks to show on the line. A tick is a vertical line that represents a division of the overall + * line. Use this method to enable ticks. + * + * If you set the number of ticks equal to the total range divided by the step size, you will get a properly aligned + * "jumping" effect where the knob jumps between ticks. + * + * Note that the number of ticks displayed will be one more than the number you specify, so specify 1 tick to show + * ticks on either end of the line. In other words, numTicks is really the number of slots between the ticks. + * + * setNumTicks(0) will disable ticks. + * + * @param numTicks + * the number of ticks to show + */ + public void setNumTicks(int numTicks) { + this.numTicks = numTicks; + drawTicks(); + } + + /** + * Set the step size. + * + * @param stepSize + * the current value + */ + public void setStepSize(double stepSize, boolean fireEvent) { + this.stepSize = stepSize; + resetCurrentValue(fireEvent); + } + + public void setValue(Double value) { + setCurrentValue(value, false); + } + + public void setValue(Double value, boolean fireEvent) { + setCurrentValue(value, fireEvent); + } + + /** + * Shift to the left (smaller value). + * + * @param numSteps + * the number of steps to shift + */ + public void shiftLeft(int numSteps) { + setCurrentValue(getCurrentValue() - numSteps * stepSize); + } + + /** + * Shift to the right (greater value). + * + * @param numSteps + * the number of steps to shift + */ + public void shiftRight(int numSteps) { + setCurrentValue(getCurrentValue() + numSteps * stepSize); + } + + /** + * Format the label to display above the ticks + * + * Override this method in a subclass to customize the format. By default, this method returns the integer portion + * of the value. + * + * @param value + * the value at the label + * @return the text to put in the label + */ + protected String formatTickLabel(Double value, Double previousValue) { + if (tickLabelFormatter != null) { + return tickLabelFormatter.formatLabel(this, value, previousValue); + } else { + return (int) (10 * value) / 10.0 + ""; + } + } + + /** + * Get the percentage of the knob's position relative to the size of the line. The return value will be between 0.0 + * and 1.0. + * + * @return the current percent complete + */ + protected double getKnobPercent() { + // If we have no range + if (maxValue <= minValue) { + return 0; + } + + // Calculate the relative progress + double percent = (curValue - minValue) / (maxValue - minValue); + return Math.max(0.0, Math.min(1.0, percent)); + } + + /** + * This method is called immediately after a widget becomes attached to the browser's document. + */ + @Override + protected void onLoad() { + // Reset the position attribute of the parent element + getElement().getStyle().setPosition(Position.RELATIVE); + } + + /** + * Draw the knob where it is supposed to be relative to the line. + */ + protected void drawKnob() { + if (isAttached() && isMinMaxInitialized()) { + // Move the knob to the correct position + Element knobElement = knobImage.getElement(); + int lineWidth = lineElement.getOffsetWidth(); + int knobWidth = knobElement.getOffsetWidth(); + int knobLeftOffset = (int) (lineLeftOffset + (getKnobPercent() * lineWidth) - (knobWidth / 2)); + knobLeftOffset = Math.min(knobLeftOffset, lineLeftOffset + lineWidth - (knobWidth / 2) - 1); + knobElement.getStyle().setLeft(knobLeftOffset, Unit.PX); + } + } + + /** + * Draw the labels along the line. + */ + protected void drawTickLabels() { + if (isAttached() && isMinMaxInitialized()) { + // Draw the tick labels + int lineWidth = lineElement.getOffsetWidth(); + if (numTickLabels > 0) { + // Create the labels or make them visible + Double previousValue = null; + for (int i = 0; i <= numTickLabels; i++) { + Element label = null; + if (i < tickLabelElements.size()) { + label = tickLabelElements.get(i); + } else { // Create the new label + label = DOM.createDiv(); + label.getStyle().setPosition(Position.ABSOLUTE); + label.getStyle().setDisplay(Display.NONE); + if (enabled) { + label.setPropertyString("className", "gwt-SliderBar-ticklabel"); + } else { + label.setPropertyString("className", "gwt-SliderBar-ticklabel-disabled"); + } + DOM.appendChild(getElement(), label); + tickLabelElements.add(label); + } + + // Set the label text + double value = minValue + (getTotalRange() * i / numTickLabels); + label.getStyle().setVisibility(Visibility.HIDDEN); + label.getStyle().setProperty("display", ""); + label.setPropertyString("innerHTML", formatTickLabel(value, previousValue)); + + // Move to the left so the label width is not clipped by the shell + label.getStyle().setLeft(0, Unit.PX); + + // Position the label and make it visible + int labelWidth = label.getOffsetWidth(); + int labelLeftOffset = lineLeftOffset + (lineWidth * i / numTickLabels) - (labelWidth / 2); + labelLeftOffset = Math.min(labelLeftOffset, lineLeftOffset + lineWidth - labelWidth); + labelLeftOffset = Math.max(labelLeftOffset, lineLeftOffset); + label.getStyle().setLeft(labelLeftOffset, Unit.PX); + label.getStyle().setVisibility(Visibility.VISIBLE); + + previousValue = value; + } + + // Hide unused labels + for (int i = (numTickLabels + 1); i < tickLabelElements.size(); i++) { + tickLabelElements.get(i).getStyle().setDisplay(Display.NONE); + } + } else { // Hide all labels + for (Element elem : tickLabelElements) { + elem.getStyle().setDisplay(Display.NONE); + } + } + } + } + + /** + * Draw the tick along the line. + */ + protected void drawTicks() { + if (isAttached() && isMinMaxInitialized()) { + // Draw the ticks + int lineWidth = lineElement.getOffsetWidth(); + if (numTicks > 0) { + // Create the ticks or make them visible + for (int i = 0; i <= numTicks; i++) { + Element tick = null; + if (i < tickElements.size()) { + tick = tickElements.get(i); + } else { // Create the new tick + tick = DOM.createDiv(); + tick.getStyle().setPosition(Position.ABSOLUTE); + tick.getStyle().setDisplay(Display.NONE); + DOM.appendChild(getElement(), tick); + tickElements.add(tick); + } + if (enabled) { + tick.setPropertyString("className", "gwt-SliderBar-tick"); + } else { + tick.setPropertyString("className", "gwt-SliderBar-tick gwt-SliderBar-tick-disabled"); + } + // Position the tick and make it visible + tick.getStyle().setVisibility(Visibility.HIDDEN); + tick.getStyle().setProperty("display", ""); + int tickWidth = tick.getOffsetWidth(); + int tickLeftOffset = lineLeftOffset + (lineWidth * i / numTicks) - (tickWidth / 2); + tickLeftOffset = Math.min(tickLeftOffset, lineLeftOffset + lineWidth - tickWidth); + tick.getStyle().setLeft(tickLeftOffset, Unit.PX); + tick.getStyle().setVisibility(Visibility.VISIBLE); + } + + // Hide unused ticks + for (int i = (numTicks + 1); i < tickElements.size(); i++) { + tickElements.get(i).getStyle().setDisplay(Display.NONE); + } + } else { // Hide all ticks + for (Element elem : tickElements) { + elem.getStyle().setDisplay(Display.NONE); + } + } + } + } + + /** + * Draw the markers. + */ + private void drawMarkers() { + if (isAttached() && isMinMaxInitialized()) { + int numMarkers = markers.size(); + // Draw the markers + int lineWidth = lineElement.getOffsetWidth(); + if (numMarkers > 0) { + // Create the markers or make them visible + Element lastMarkerElement = null; + int lastMarkerOffsetLeft = 0; + for (int i = 0; i < numMarkers; i++) { + Marker marker = markers.get(i); + Element markerElem = null; + if (i < markerElements.size()) { + markerElem = markerElements.get(i); + } else { // Create the new markes + markerElem = DOM.createDiv(); + markerElem.getStyle().setPosition(Position.ABSOLUTE); + markerElem.getStyle().setDisplay(Display.NONE); + DOM.appendChild(getElement(), markerElem); + markerElements.add(markerElem); + } + if (enabled) { + markerElem.setPropertyString("className", "gwt-SliderBar-mark"); + } else { + markerElem.setPropertyString("className", "gwt-SliderBar-mark gwt-SliderBar-mark-disabled"); + } + // Position the marker and make it visible + markerElem.getStyle().setVisibility(Visibility.HIDDEN); + markerElem.getStyle().setProperty("display", ""); + double markerLinePosition = (marker.position - minValue) * lineWidth / getTotalRange(); + int markerWidth = markerElem.getOffsetWidth(); + int markerLeftOffset = lineLeftOffset + (int) markerLinePosition - (markerWidth / 2); + markerLeftOffset = Math.min(markerLeftOffset, lineLeftOffset + lineWidth - markerWidth); + markerElem.getStyle().setLeft(markerLeftOffset, Unit.PX); + markerElem.getStyle().setVisibility(Visibility.VISIBLE); + // change class of last marker if it is too close to the one just created + if (lastMarkerOffsetLeft > 0 && (markerLeftOffset-lastMarkerOffsetLeft) <= markerWidth) { + if (lastMarkerElement != null) { + lastMarkerElement.setPropertyString("className", "gwt-SliderBar-mark-tooClose"); + lastMarkerElement.getStyle().setLeft(markerLeftOffset-10, Unit.PX); + } + } + lastMarkerElement = markerElem; + lastMarkerOffsetLeft = markerLeftOffset; + } + + // Hide unused markers + for (int i = numMarkers; i < markerElements.size(); i++) { + markerElements.get(i).getStyle().setDisplay(Display.NONE); + } + } else { // Hide all markers + for (Element elem : markerElements) { + elem.getStyle().setDisplay(Display.NONE); + } + } + } + } + + /** + * Draw the marker labels. + */ + private void drawMarkerLabels() { + if (isAttached() && isMinMaxInitialized()) { + int numMarkers = markers.size(); + // Draw the marker labels + int lineWidth = lineElement.getOffsetWidth(); + if (numMarkers > 0) { + Element lastMarkerLabel = null; + Marker lastMarker = null; + int lastMarkerLabelOffsetLeft = 0; + // Create the labels or make them visible + for (int i = 0; i < numMarkers; i++) { + Marker marker = markers.get(i); + Element label = null; + if (i < markerLabelElements.size()) { + label = markerLabelElements.get(i); + } else { // Create the new label + label = DOM.createDiv(); + label.getStyle().setPosition(Position.ABSOLUTE); + label.getStyle().setDisplay(Display.NONE); + DOM.appendChild(getElement(), label); + markerLabelElements.add(label); + } + if (enabled) { + label.setPropertyString("className", "gwt-SliderBar-markerlabel"); + } else { + label.setPropertyString("className", "gwt-SliderBar-markerlabel-disabled"); + } + + // Set the marker label text + label.getStyle().setVisibility(Visibility.HIDDEN); + label.getStyle().setProperty("display", ""); + label.setPropertyString("innerHTML", marker.name); + + // Move to the left so the label width is not clipped by the shell + label.getStyle().setLeft(0, Unit.PX); + + // Position the label and make it visible + double markerLinePosition = (marker.position - minValue) * lineWidth / getTotalRange(); + int labelWidth = label.getOffsetWidth(); + int labelLeftOffset = lineLeftOffset + (int) markerLinePosition - (labelWidth / 2); + labelLeftOffset = Math.min(labelLeftOffset, lineLeftOffset + lineWidth - labelWidth); + + label.getStyle().setLeft(labelLeftOffset, Unit.PX); + label.getStyle().setVisibility(Visibility.VISIBLE); + + // hide last marker label if it is too close to the one just created + if (lastMarkerLabelOffsetLeft > 0 && (labelLeftOffset-lastMarkerLabelOffsetLeft) <= 20) { + if (lastMarkerLabel != null && lastMarker != null) { + lastMarkerLabel.setPropertyString("className", "gwt-SliderBar-markerlabel-tooClose"); + lastMarkerLabel.setPropertyString("innerHTML", lastMarker.name); // no space for more + int currentMarkerOffsetLeft = labelLeftOffset; + try { + currentMarkerOffsetLeft = Integer.parseInt(markerElements.get(i).getStyle().getLeft().replace("px", "")); + } catch (NumberFormatException ex) { + // ignore, can't do anything + } + lastMarkerLabel.getStyle().setLeft(currentMarkerOffsetLeft-8, Unit.PX); + } + } + lastMarker = marker; + lastMarkerLabel = label; + lastMarkerLabelOffsetLeft = labelLeftOffset; + } + + // Hide unused labels + for (int i = (numMarkers + 1); i < markerLabelElements.size(); i++) { + markerLabelElements.get(i).getStyle().setDisplay(Display.NONE); + } + } else { // Hide all labels + for (Element elem : markerLabelElements) { + elem.getStyle().setDisplay(Display.NONE); + } + } + } + } + + /** + * Highlight this widget. + */ + private void highlight() { + String styleName = getStylePrimaryName(); + getElement().setPropertyString("className", styleName + " " + styleName + "-focused"); + } + + /** + * Reset the progress to constrain the progress to the current range and redraw the knob as needed. + */ + private synchronized void resetCurrentValue(boolean fireEvent) { + setCurrentValue(getCurrentValue(), fireEvent); + } + + /** + * Slide the knob to a new location. + * + * @param event + * the mouse event + */ + private void slideKnob(Event event) { + // Adding scrollLeft to adjust the position, if the user had scrolled with the lower scroll bar + int x = event.getClientX() + Window.getScrollLeft(); + if (x > 0) { + int lineWidth = lineElement.getOffsetWidth(); + int lineLeft = lineElement.getAbsoluteLeft(); + double percent = (double) (x - lineLeft) / lineWidth * 1.0; + setCurrentValue(getTotalRange() * percent + minValue, true); + } + } + + /** + * Start sliding the knob. + * + * @param highlight + * true to change the style + * @param fireEvent + * true to fire the event + */ + private void startSliding(boolean highlight, boolean fireEvent) { + if (highlight) { + lineElement.setPropertyString("className", "gwt-SliderBar-line gwt-SliderBar-line-sliding"); + knobImage.getElement().setPropertyString("className", "gwt-SliderBar-knob gwt-SliderBar-knob-sliding"); + knobImage.setResource(images.sliderSliding()); + } + } + + /** + * Stop sliding the knob. + * + * @param unhighlight + * true to change the style + * @param fireEvent + * true to fire the event + */ + private void stopSliding(boolean unhighlight, boolean fireEvent) { + if (unhighlight) { + lineElement.setPropertyString("className", "gwt-SliderBar-line"); + knobImage.getElement().setPropertyString("className", "gwt-SliderBar-knob"); + knobImage.setResource(images.slider()); + } + } + + /** + * Unhighlight this widget. + */ + private void unhighlight() { + getElement().setPropertyString("className", getStylePrimaryName()); + } + + @Override + public void onResize() { + redraw(); + } + + public void clearMarkers() { + markers.clear(); + } + + public boolean addMarker(String markerName, Double markerPosition) { + return markers.add(new Marker(markerName, markerPosition)); + } + + public boolean setMarker(String markerName, Double markerPosition) { + Marker marker = findMarkerByName(markerName); + if (marker != null) { + marker.position = markerPosition; + return true; + } + return false; + } + + public boolean removeMarker(String markerName) { + Marker marker = findMarkerByName(markerName); + if (marker != null) { + return markers.remove(marker); + } + return false; + } + + public Iterator getMarkers() { + return markers.iterator(); + } + + private Marker findMarkerByName(String markerName) { + for (Marker marker : markers) { + if (marker.name.equals(markerName)) { + return marker; + } + } + return null; + } +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeSlider.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeSlider.java index 99dafe676bc..8383b5a6c60 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeSlider.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeSlider.java @@ -26,7 +26,9 @@ public class TimeSlider extends SliderBar { private List calculatedTimeTicks; private boolean isZoomed; - + + private int visibleLabelsInterval = 1; + private final int TICKCOUNT = 10; public TimeSlider() { @@ -84,7 +86,7 @@ public class TimeSlider extends SliderBar { int tickLeftOffset = lineLeftOffset + (int) pos - (tickWidth / 2); tickLeftOffset = Math.min(tickLeftOffset, lineLeftOffset + lineWidth - tickWidth); tick.getStyle().setLeft(tickLeftOffset, Unit.PX); - tick.getStyle().setVisibility(Visibility.VISIBLE); + tick.getStyle().setVisibility(isTickInVisibleRange(tickPosition) ? Visibility.VISIBLE : Visibility.HIDDEN); } } @@ -106,7 +108,7 @@ public class TimeSlider extends SliderBar { int lineWidth = lineElement.getOffsetWidth(); // Create the labels or make them visible Double previousValue = null; - for (int i = 0; i < calculatedTimeTicks.size(); i++) { + for (int i = 0, ticksInVisibleRange = 0; i < calculatedTimeTicks.size(); i++) { TickPosition tickPosition = calculatedTimeTicks.get(i); Element label = null; if (i < tickLabelElements.size()) { @@ -142,7 +144,8 @@ public class TimeSlider extends SliderBar { labelLeftOffset = Math.min(labelLeftOffset, lineLeftOffset + lineWidth - labelWidth); labelLeftOffset = Math.max(labelLeftOffset, lineLeftOffset); label.getStyle().setLeft(labelLeftOffset, Unit.PX); - label.getStyle().setVisibility(Visibility.VISIBLE); + boolean visible = isTickInVisibleRange(tickPosition) && ticksInVisibleRange++ % visibleLabelsInterval == 0; + label.getStyle().setVisibility(visible ? Visibility.VISIBLE : Visibility.HIDDEN); } previousValue = value; } @@ -152,6 +155,11 @@ public class TimeSlider extends SliderBar { tickLabelElements.get(i).getStyle().setDisplay(Display.NONE); } } + + private boolean isTickInVisibleRange(TickPosition tickPosition) { + long tickValue = tickPosition.getPosition().getTime(); + return tickValue >= minValue && tickValue <= maxValue; + } public void clearMarkersAndLabelsAndTicks() { clearMarkers(); @@ -245,4 +253,10 @@ public class TimeSlider extends SliderBar { public void setZoomed(boolean isZoomed) { this.isZoomed = isZoomed; } -} + + @Override + public void onResize() { + visibleLabelsInterval = (TICKCOUNT / Math.max(getOffsetWidth() / 35, 1)) + 1; + super.onResize(); + } +} \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeTicksCalculator.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeTicksCalculator.java index 8a28bd12fb2..735989a2776 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeTicksCalculator.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeTicksCalculator.java @@ -1,246 +1,247 @@ -package com.sap.sse.gwt.client.controls.slider; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -/** - * This calculator emulates the way the HighCharts charting library is computing the chart ticks - * in case of a x-axis based on time values. - */ -public class TimeTicksCalculator { - - public TimeTicksCalculator() { - } - - /** - * Get a normalized tick interval for dates. Returns a configuration object with unit range (interval), count and - * name. Used to prepare data for getTimeTicks. Previously this logic was part of getTimeTicks, but as getTimeTicks - * now runs of segments in stock charts, the normalizing logic was extracted in order to prevent it for running over - * again for each segment having the same interval. - */ - public NormalizedInterval normalizeTimeTickInterval(long tickInterval) { - TimeUnits[] units = TimeUnits.values(); - - TimeUnits unit = units[units.length - 1]; // default unit is years - long interval = unit.unitInMs; - int[] multiples = unit.allowedMultiples; - int count; - - // loop through the units to find the one that best fits the tickInterval - for (int i = 0; i < units.length; i++) { - unit = units[i]; - interval = unit.unitInMs; - multiples = unit.allowedMultiples; - - if (i + 1 < units.length) { - // lessThan is in the middle between the highest multiple and the next unit. - long lessThan = (interval * multiples[multiples.length - 1] + units[i + 1].unitInMs) / 2; - - // break and keep the current unit - if (tickInterval <= lessThan) { - break; - } - } - } - - // prevent 2.5 years intervals, though 25, 250 etc. are allowed - if (interval == TimeUnits.YEAR.unitInMs && tickInterval < 5 * interval) { - multiples = new int[] { 1, 2, 5 }; - } - - // get the count - count = (int) normalizeTickInterval(tickInterval / interval, multiples, 1); - - return new NormalizedInterval(unit.name(), interval, count); - } - - /** - * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5 - * - * @param {Number} interval - * @param {Array} multiples - * @param {Number} magnitude - * @param {Object} options - */ - public long normalizeTickInterval(long interval, int[] multiples, int magnitude) { - long normalized; - - // round to a tenfold of 1, 2, 2.5 or 5 - normalized = interval / magnitude; - - // normalize the interval to the nearest multiple - for (int i = 0; i < multiples.length; i++) { - interval = multiples[i]; - int addedValue = i + 1 < multiples.length ? multiples[i + 1] : multiples[i]; - if (normalized <= (multiples[i] + addedValue) / 2) { - break; - } - } - - // multiply back to the correct magnitude - interval *= magnitude; - - return interval; - } - - private long makeTime(int year, int month) { - return makeTime(year, month, 1, 0, 0, 0); - } - - private long makeTime(int year, int month, int date) { - return makeTime(year, month, date, 0, 0, 0); - } - - @SuppressWarnings("deprecation") - private long makeTime(int year, int month, int date, int hours, int minutes, int seconds) { - return new Date(year, month, date, hours, minutes, seconds).getTime(); - } - - /** - * Set the tick positions to a time unit that makes sense, for example on the first of each month or on every - * Monday. Return an array with the time positions. Used in datetime axes as well as for grouping data on a datetime - * axis. - * - * @param {Object} normalizedInterval The interval in axis values (ms) and the count - * @param {Number} min The minimum in axis values - * @param {Number} max The maximum in axis values - * @param {Number} startOfWeek - */ - @SuppressWarnings("deprecation") - public List calculateTimeTicks(NormalizedInterval normalizedInterval, long min, long max, - long startOfWeek) { - List tickPositions = new ArrayList(); - int i; - boolean useUTC = true; - int minYear = 0; // used in months and years as a basis for Date.UTC() - Date minDate = new Date(min); - long interval = normalizedInterval.unitRange; - int count = (int) normalizedInterval.count; - - - if (interval >= TimeUnits.SECOND.unitInMs) { // second - minDate.setTime(min - (min % 1000)); - minDate.setSeconds(interval >= TimeUnits.MINUTE.unitInMs ? 0 : count - * Math.round(minDate.getSeconds() / count)); - } - - if (interval >= TimeUnits.MINUTE.unitInMs) { // minute - minDate.setMinutes(interval >= TimeUnits.HOUR.unitInMs ? 0 : count - * Math.round(minDate.getMinutes() / count)); - } - - if (interval >= TimeUnits.HOUR.unitInMs) { // hour - minDate.setHours(interval >= TimeUnits.DAY.unitInMs ? 0 : count * Math.round(minDate.getHours() / count)); - } - - if (interval >= TimeUnits.DAY.unitInMs) { // day - minDate.setDate(interval >= TimeUnits.MONTH.unitInMs ? 1 : count * Math.round(minDate.getDate() / count)); - } - - if (interval >= TimeUnits.MONTH.unitInMs) { // month - minDate.setMonth(interval >= TimeUnits.YEAR.unitInMs ? 0 : count * Math.round(minDate.getMonth() + 1 / count) - 1); - minYear = minDate.getYear() + 1900; - } - - if (interval >= TimeUnits.YEAR.unitInMs) { // year - minYear -= minYear % count; - minDate.setYear(minYear - 1900); - } - - // week is a special case that runs outside the hierarchy - if (interval == TimeUnits.WEEK.unitInMs) { - // get start of current week, independent of count - long value = minDate.getDate() - minDate.getDay() + startOfWeek; - minDate.setDate((int) value); - } - - // get tick positions - i = 1; - minYear = minDate.getYear() + 1900; - int minMonth = minDate.getMonth() + 1; - int minDateDate = minDate.getDate(); - long time = minDate.getTime(); - - // iterate and add tick positions at appropriate values - while (time < max) { - if(time >= min) - tickPositions.add(new TickPosition(time)); - - // if the interval is years, use Date.UTC to increase years - if (interval == TimeUnits.YEAR.unitInMs) { - time = makeTime(minYear + i * count, 0); - - // if the interval is months, use Date.UTC to increase months - } else if (interval == TimeUnits.MONTH.unitInMs) { - time = makeTime(minYear, minMonth + i * count); - - // if we're using global time, the interval is not fixed as it jumps - // one hour at the DST crossover - } else if (!useUTC && (interval == TimeUnits.DAY.unitInMs || interval == TimeUnits.WEEK.unitInMs)) { - time = makeTime(minYear, minMonth, minDateDate + i * count * (interval == TimeUnits.DAY.unitInMs ? 1 : 7)); - - // else, the interval is fixed and we use simple addition - } else { - time += interval * count; - } - - i++; - } - - // push the last time - if(time <= max) - tickPositions.add(new TickPosition(time)); - - return tickPositions; - } - - public class NormalizedInterval { - public String unitName; - public long unitRange; - public int count; - - public NormalizedInterval(String unitName, long unitRange, int count) { - this.unitName = unitName; - this.unitRange = unitRange; - this.count = count; - } - - @Override - public String toString() { - return "NormalizedInterval [unitName=" + unitName + ", unitRange=" + unitRange + ", count=" + count + "]"; - } - } - - public class TickPosition { - private Date position; - - public TickPosition(long time) { - this.position = new Date(time); - } - - public Date getPosition() { - return position; - } - } - - enum TimeUnits { - MILLISECOND(1, new int[] { 1, 2, 5, 10, 20, 25, 50, 100, 200, 500 }), - SECOND(1000, new int[] { 1, 2, 5, 10, 15, 30 }), - MINUTE(60 * 1000, new int[] { 1, 2, 5, 10, 15, 30 }), - HOUR(60 * 60 * 1000, new int[] { 1, 2, 3, 4, 6, 8, 12 }), - DAY(24 * 3600000l, new int[] { 1, 2 }), - WEEK(7 * 24 * 3600000l, new int[] { 1, 2 }), - MONTH(30 * 24 * 3600000l, new int[] { 1, 2, 3, 4, 6 }), - YEAR(31556952000l, new int[] {}); - - int[] allowedMultiples; - - long unitInMs; - - TimeUnits(long unitInMs, int[] allowedMultiples) { - this.unitInMs = unitInMs; - this.allowedMultiples = allowedMultiples; - } - } -} +package com.sap.sse.gwt.client.controls.slider; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * This calculator emulates the way the HighCharts charting library is computing the chart ticks + * in case of a x-axis based on time values. + */ +public class TimeTicksCalculator { + + public TimeTicksCalculator() { + } + + /** + * Get a normalized tick interval for dates. Returns a configuration object with unit range (interval), count and + * name. Used to prepare data for getTimeTicks. Previously this logic was part of getTimeTicks, but as getTimeTicks + * now runs of segments in stock charts, the normalizing logic was extracted in order to prevent it for running over + * again for each segment having the same interval. + */ + public NormalizedInterval normalizeTimeTickInterval(long tickInterval) { + TimeUnits[] units = TimeUnits.values(); + + TimeUnits unit = units[units.length - 1]; // default unit is years + long interval = unit.unitInMs; + int[] multiples = unit.allowedMultiples; + int count; + + // loop through the units to find the one that best fits the tickInterval + for (int i = 0; i < units.length; i++) { + unit = units[i]; + interval = unit.unitInMs; + multiples = unit.allowedMultiples; + + if (i + 1 < units.length) { + // lessThan is in the middle between the highest multiple and the next unit. + double lessThan = (interval * multiples[multiples.length - 1] + units[i + 1].unitInMs) / 2; + + // break and keep the current unit + if (tickInterval <= lessThan) { + break; + } + } + } + + // prevent 2.5 years intervals, though 25, 250 etc. are allowed + if (interval == TimeUnits.YEAR.unitInMs && tickInterval < 5 * interval) { + multiples = new int[] { 1, 2, 5 }; + } + + // get the count + double temp = ((double) tickInterval) / ((double) interval); + count = (int) normalizeTickInterval(temp, multiples, 1); + + return new NormalizedInterval(unit.name(), interval, count); + } + + /** + * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5 + * + * @param {Number} interval + * @param {Array} multiples + * @param {Number} magnitude + * @param {Object} options + */ + public long normalizeTickInterval(double interval, int[] multiples, int magnitude) { + double normalized; + + // round to a tenfold of 1, 2, 2.5 or 5 + normalized = interval / magnitude; + + // normalize the interval to the nearest multiple + for (int i = 0; i < multiples.length; i++) { + interval = multiples[i]; + int addedValue = i + 1 < multiples.length ? multiples[i + 1] : multiples[i]; + if (normalized <= (multiples[i] + addedValue) / 2) { + break; + } + } + + // multiply back to the correct magnitude + interval *= magnitude; + + return Math.round(interval); + } + + private long makeTime(int year, int month) { + return makeTime(year, month, 1, 0, 0, 0); + } + + private long makeTime(int year, int month, int date) { + return makeTime(year, month, date, 0, 0, 0); + } + + @SuppressWarnings("deprecation") + private long makeTime(int year, int month, int date, int hours, int minutes, int seconds) { + return new Date(year, month, date, hours, minutes, seconds).getTime(); + } + + /** + * Set the tick positions to a time unit that makes sense, for example on the first of each month or on every + * Monday. Return an array with the time positions. Used in datetime axes as well as for grouping data on a datetime + * axis. + * + * @param {Object} normalizedInterval The interval in axis values (ms) and the count + * @param {Number} min The minimum in axis values + * @param {Number} max The maximum in axis values + * @param {Number} startOfWeek + */ + @SuppressWarnings("deprecation") + public List calculateTimeTicks(NormalizedInterval normalizedInterval, long min, long max, + long startOfWeek) { + List tickPositions = new ArrayList(); + int i; + boolean useUTC = true; + int minYear = 0; // used in months and years as a basis for Date.UTC() + Date minDate = new Date(min); + long interval = normalizedInterval.unitRange; + int count = (int) normalizedInterval.count; + + + if (interval >= TimeUnits.SECOND.unitInMs) { // second + minDate.setTime(min - (min % 1000)); + minDate.setSeconds(interval >= TimeUnits.MINUTE.unitInMs ? 0 : count + * Math.round(minDate.getSeconds() / count)); + } + + if (interval >= TimeUnits.MINUTE.unitInMs) { // minute + minDate.setMinutes(interval >= TimeUnits.HOUR.unitInMs ? 0 : count + * Math.round(minDate.getMinutes() / count)); + } + + if (interval >= TimeUnits.HOUR.unitInMs) { // hour + minDate.setHours(interval >= TimeUnits.DAY.unitInMs ? 0 : count * Math.round(minDate.getHours() / count)); + } + + if (interval >= TimeUnits.DAY.unitInMs) { // day + minDate.setDate(interval >= TimeUnits.MONTH.unitInMs ? 1 : count * Math.round(minDate.getDate() / count)); + } + + if (interval >= TimeUnits.MONTH.unitInMs) { // month + minDate.setMonth(interval >= TimeUnits.YEAR.unitInMs ? 0 : count * Math.round(minDate.getMonth() + 1 / count) - 1); + minYear = minDate.getYear() + 1900; + } + + if (interval >= TimeUnits.YEAR.unitInMs) { // year + minYear -= minYear % count; + minDate.setYear(minYear - 1900); + } + + // week is a special case that runs outside the hierarchy + if (interval == TimeUnits.WEEK.unitInMs) { + // get start of current week, independent of count + long value = minDate.getDate() - minDate.getDay() + startOfWeek; + minDate.setDate((int) value); + } + + // get tick positions + i = 1; + minYear = minDate.getYear() + 1900; + int minMonth = minDate.getMonth() + 1; + int minDateDate = minDate.getDate(); + long time = minDate.getTime(); + + // iterate and add tick positions at appropriate values + while (time < max) { + if(time >= min) + tickPositions.add(new TickPosition(time)); + + // if the interval is years, use Date.UTC to increase years + if (interval == TimeUnits.YEAR.unitInMs) { + time = makeTime(minYear + i * count, 0); + + // if the interval is months, use Date.UTC to increase months + } else if (interval == TimeUnits.MONTH.unitInMs) { + time = makeTime(minYear, minMonth + i * count); + + // if we're using global time, the interval is not fixed as it jumps + // one hour at the DST crossover + } else if (!useUTC && (interval == TimeUnits.DAY.unitInMs || interval == TimeUnits.WEEK.unitInMs)) { + time = makeTime(minYear, minMonth, minDateDate + i * count * (interval == TimeUnits.DAY.unitInMs ? 1 : 7)); + + // else, the interval is fixed and we use simple addition + } else { + time += interval * count; + } + + i++; + } + + // push the last time + if(time <= max) + tickPositions.add(new TickPosition(time)); + + return tickPositions; + } + + public class NormalizedInterval { + public String unitName; + public long unitRange; + public int count; + + public NormalizedInterval(String unitName, long unitRange, int count) { + this.unitName = unitName; + this.unitRange = unitRange; + this.count = count; + } + + @Override + public String toString() { + return "NormalizedInterval [unitName=" + unitName + ", unitRange=" + unitRange + ", count=" + count + "]"; + } + } + + public class TickPosition { + private Date position; + + public TickPosition(long time) { + this.position = new Date(time); + } + + public Date getPosition() { + return position; + } + } + + enum TimeUnits { + MILLISECOND(1l, 1, 2, 5, 10, 20, 25, 50, 100, 200, 500), + SECOND(1000l, 1, 2, 5, 10, 15, 30), + MINUTE(60 * 1000l, 1, 2, 5, 10, 15, 30), + HOUR(60 * 60 * 1000l, 1, 2, 3, 4, 6, 8, 12), + DAY(24 * 3600000l, 1, 2), + WEEK(7 * 24 * 3600000l, 1, 2), + MONTH(30 * 24 * 3600000l, 1, 2, 3, 4, 6), + YEAR(31556952000l); + + int[] allowedMultiples; + + long unitInMs; + + TimeUnits(long unitInMs, int... allowedMultiples) { + this.unitInMs = unitInMs; + this.allowedMultiples = allowedMultiples; + } + } +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/IsEmbeddableComponent.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/IsEmbeddableComponent.java index 0f83295b75c..f07d2bf1fbe 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/IsEmbeddableComponent.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/IsEmbeddableComponent.java @@ -1,7 +1,6 @@ package com.sap.sse.gwt.client.shared.components; import com.google.gwt.user.client.ui.Widget; -import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicator; public interface IsEmbeddableComponent { @@ -13,11 +12,7 @@ public interface IsEmbeddableComponent { Widget getLegendWidget(); - BusyIndicator getBusyIndicator(); - boolean isEmbedded(); boolean hasToolbar(); - - boolean hasBusyIndicator(); } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/resources/css/CommonControls.css b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/resources/css/CommonControls.css index a15efd6a2c7..a30d6ccc9a7 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/resources/css/CommonControls.css +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/resources/css/CommonControls.css @@ -169,8 +169,7 @@ body ::-webkit-scrollbar-track { .gwt-SplitLayoutPanel-EastToggleButton-Panel { position: absolute; - bottom: 77px; - margin-left: -35px; + bottom: -10px; } .gwt-SplitLayoutPanel-EastToggleButton { @@ -181,9 +180,14 @@ body ::-webkit-scrollbar-track { -moz-transform: rotate(-90deg); -o-transform: rotate(-90deg); -ms-transform: rotate(-90deg); + transform: rotate(-90deg); + -webkit-transform-origin: 0 0; + -moz-transform-origin: 0 0; + -o-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; padding-right: 25px; padding-left: 30px; - margin-left: -15px; border-bottom: 1px solid #cecece; border-right: 1px solid #cecece; border-left: 1px solid #cecece; diff --git a/java/com.sap.sse.security.common/pom.xml b/java/com.sap.sse.security.common/pom.xml index 063d3938b30..45259c58bad 100755 --- a/java/com.sap.sse.security.common/pom.xml +++ b/java/com.sap.sse.security.common/pom.xml @@ -1,32 +1,32 @@ - - - 4.0.0 - - root - com.sap.sailing - 1.0.0-SNAPSHOT - - com.sap.sse.security.common - eclipse-plugin - - - - - org.eclipse.tycho - tycho-source-plugin - ${tycho-version} - - - plugin-source - generate-sources - - plugin-source - - - - - - - - + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sse.security.common + eclipse-plugin + + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + + + + diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/User.java b/java/com.sap.sse.security/src/com/sap/sse/security/User.java index 147b7b729a1..a0cdbabc1bd 100644 --- a/java/com.sap.sse.security/src/com/sap/sse/security/User.java +++ b/java/com.sap.sse.security/src/com/sap/sse/security/User.java @@ -1,6 +1,7 @@ package com.sap.sse.security; import java.io.Serializable; +import java.security.SecureRandom; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; @@ -171,7 +172,7 @@ public class User implements Named, WithID { private String createRandomSecret() { final byte[] bytes1 = new byte[64]; - new Random().nextBytes(bytes1); + new SecureRandom().nextBytes(bytes1); final byte[] bytes2 = new byte[64]; new Random().nextBytes(bytes2); return new Sha256Hash(bytes1, bytes2, 1024).toBase64(); diff --git a/java/com.sap.sse/src/com/sap/sse/ServerInfo.java b/java/com.sap.sse/src/com/sap/sse/ServerInfo.java index 7e2360aa33c..6300fab7489 100644 --- a/java/com.sap.sse/src/com/sap/sse/ServerInfo.java +++ b/java/com.sap.sse/src/com/sap/sse/ServerInfo.java @@ -3,6 +3,8 @@ package com.sap.sse; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; +import java.util.logging.Level; +import java.util.logging.Logger; /** * A class providing static information about the running server. @@ -10,14 +12,16 @@ import java.io.FileReader; * */ public class ServerInfo { + private static final Logger logger = Logger.getLogger(ServerInfo.class.getName()); + public static String getBuildVersion() { String version = "Unknown or Development (" + ServerStartupConstants.SERVER_NAME + ")"; File versionfile = new File(ServerStartupConstants.JETTY_HOME + File.separator + "version.txt"); if (versionfile.exists()) { - try { - version = new BufferedReader(new FileReader(versionfile)).readLine(); + try (BufferedReader bufferedReader = new BufferedReader(new FileReader(versionfile))) { + version = bufferedReader.readLine(); } catch (Exception ex) { - /* ignore */ + logger.log(Level.WARNING, "Error trying to obtain version info", ex); } } return version; diff --git a/java/com.sap.sse/src/com/sap/sse/util/IdentityWrapper.java b/java/com.sap.sse/src/com/sap/sse/util/IdentityWrapper.java new file mode 100644 index 00000000000..2391349d401 --- /dev/null +++ b/java/com.sap.sse/src/com/sap/sse/util/IdentityWrapper.java @@ -0,0 +1,34 @@ +package com.sap.sse.util; + +import java.io.Serializable; + +/** + * Implements {@link Object#equals(Object)} and {@link Object#hashCode()} based on the wrapped object's + * identity, no matter what the object itself defines as {@code equals} and {@code hashCode}. + * + * @author Axel Uhl (D043530) + * + */ +public class IdentityWrapper implements Serializable { + private static final long serialVersionUID = 3514488568026067341L; + private final T t; + + public IdentityWrapper(T t) { + this.t = t; + } + + @Override + public int hashCode() { + return System.identityHashCode(t); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof IdentityWrapper) ? ((IdentityWrapper) obj).t == this.t : obj == t; + } + + @Override + public String toString() { + return "IdentityWrapper for "+t; + } +} diff --git a/java/org.json.simple/pom.xml b/java/org.json.simple/pom.xml index 375032ca86a..5b842829851 100644 --- a/java/org.json.simple/pom.xml +++ b/java/org.json.simple/pom.xml @@ -1,40 +1,40 @@ - - - 4.0.0 - - root - com.sap.sailing - 1.0.0-SNAPSHOT - - org.json.simple - 1.1.0-SNAPSHOT - eclipse-plugin - - - - org.eclipse.tycho - tycho-compiler-plugin - ${tycho-version} - - 1.7 - 1.7 - - - - org.eclipse.tycho - tycho-source-plugin - ${tycho-version} - - - plugin-source - generate-sources - - plugin-source - - - - - - - + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + org.json.simple + 1.1.0-SNAPSHOT + eclipse-plugin + + + + org.eclipse.tycho + tycho-compiler-plugin + ${tycho-version} + + 1.7 + 1.7 + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + + + diff --git a/java/pom.xml b/java/pom.xml index 9ae21eaa32d..fc283d0da09 100755 --- a/java/pom.xml +++ b/java/pom.xml @@ -134,8 +134,8 @@ com.sap.sailing.autoload com.sap.sailing.feature.p2build com.sap.sailing.polars - com.sap.sailing.polars.datamining - com.sap.sailing.polars.datamining.shared + com.sap.sailing.polars.datamining + com.sap.sailing.polars.datamining.shared com.sap.sailing.polars.test + org.apache.maven.plugins maven-dependency-plugin diff --git a/mobile/pom.xml b/mobile/pom.xml index 069aa8e6d97..e3fc75b072d 100644 --- a/mobile/pom.xml +++ b/mobile/pom.xml @@ -16,13 +16,13 @@ - + ../../java/com.sap.sailing.www/apps @@ -32,21 +32,21 @@ - - - android - android - 4.4.2_r4 - provided - + + + android + android + 4.4.2_r4 + provided + - - - android.support - compatibility-v4 - 19 - - + + + android.support + compatibility-v4 + 19 + + @@ -55,90 +55,90 @@ com.jayway.maven.plugins.android.generation2 android-maven-plugin - 3.9.0-rc.1 - - - 19 - - + 3.9.0-rc.1 + + + 19 + + true - - org.apache.maven.plugins - maven-dependency-plugin - 2.8 - - - copy-installed - install - - copy - - - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - ${additional-deployment-dir} - true - - - - - - org.apache.maven.plugins - maven-antrun-plugin - 1.3 - - - generate-version - install - - run - - - - - - - - - - - - - - - maven-clean-plugin - 2.5 - - - - ${additional-deployment-dir} - - ${project.build.finalName}.apk - ${additional-deployment-version-file} - - - *.gitignore - - false - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.7 - 1.7 - - + + org.apache.maven.plugins + maven-dependency-plugin + 2.8 + + + copy-installed + install + + copy + + + + + ${project.groupId} + ${project.artifactId} + ${project.version} + ${project.packaging} + + + ${additional-deployment-dir} + true + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.3 + + + generate-version + install + + run + + + + + + + + + + + + + + + maven-clean-plugin + 2.5 + + + + ${additional-deployment-dir} + + ${project.build.finalName}.apk + ${additional-deployment-version-file} + + + *.gitignore + + false + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.7 + 1.7 + + diff --git a/pom.xml b/pom.xml index 946b2122559..77c807e02f7 100755 --- a/pom.xml +++ b/pom.xml @@ -1,68 +1,68 @@ - 4.0.0 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" + xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + 4.0.0 - com.sap.sailing - workspace - 1.0.0-SNAPSHOT - pom + com.sap.sailing + workspace + 1.0.0-SNAPSHOT + pom - - java - + + java + - - - - with-mobile - - true - - - mobile - - - - - with-ios - - ios - - - + + + + with-mobile + + true + + + mobile + + + + + with-ios + + ios + + + - - UTF-8 - UTF-8 - - - - - equinox-sdk-3.9 - p2 - http://download.eclipse.org/releases/luna/ - - - - sailing-server-maven - http://maven.sapsailing.com/maven - + + sailing-server-maven + http://maven.sapsailing.com/maven + - - central - http://repo1.maven.org/maven2 - + + central + http://repo1.maven.org/maven2 + - - uni-luebeck - http://www.itm.uni-luebeck.de/projects/maven/releases - - + + uni-luebeck + http://www.itm.uni-luebeck.de/projects/maven/releases + + diff --git a/wiki/Log-File-Analysis.md b/wiki/Log-File-Analysis.md index 1cfbc853a9b..eddce4b9329 100644 --- a/wiki/Log-File-Analysis.md +++ b/wiki/Log-File-Analysis.md @@ -64,33 +64,103 @@ This uses the SetEnvIf module, tries to match an IP address at the start of the ### Amazon EC2 Elastic Load Balancer (ELB) Logs -An Amazon ELB can be configured to write log files to the S3 storage. The general format is [explained here](http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/access-log-collection.html#access-log-entry-format). It contains in particular the client IP where the request originated and can tell timing parameters for request forwarding and processing that otherwise would not be available. It seems a good idea to always capture these logs for archiving purposes. They end up on an S3 bucket, and by default we use `sapsailing-access-logs`. +An Amazon ELB can be configured to write log files to the S3 storage. The general format is [explained here](http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/access-log-collection.html#access-log-entry-format). It contains in particular the client IP where the request originated and can tell timing parameters for request forwarding and processing that otherwise would not be available. + +The format for these logs is + +
    +timestamp elb client:port backend:port request_processing_time backend_processing_time response_processing_time elb_status_code backend_status_code received_bytes sent_bytes "request" "user_agent" ssl_cipher ssl_protocol
    +
    + +It seems a good idea to always capture these logs for archiving purposes. They end up on an S3 bucket, and by default we use `sapsailing-access-logs`. Content can be synced from there to a local directory using the following command: ``s3cmd sync s3://sapsailing-access-logs/elb-access-logs ./elb-access-logs/`` +Our central web server at `www.sapsailing.com` does this periodically every day, sync'ing the logs to `/var/log/old/elb-access-logs/`. The corresponding cron job is defined in `/etc/cron.daily/syncEC2ElbLogs` which is a script also found in git in the `configuration/` folder. + +### Broken or Partly Missing Logs and How We Recover + +During a few events we unfortunately failed to create proper Apache log files according to the above rules for scenarios using a load-balanced setup ("ELB scenario"). In some cases, the Apache log format was missing the referrer URL. This is easy to patch into the files because we collected the logs on a per-server basis and we knew which server ran which event. For example, for Kieler Woche 2015 and Travemünder Woche 2015 we needed to insert `kielerwoche2015.sapsailing.com` and `tw2015.sapsailing.com`, respectively, to match the general log format used for analysis. + +The other problem, as described already above, is that of the original client IP address. Before we encountered how to log the original client IP from the `X-Forwarded-For` header field, only the ELB's IP addresses were logged in the Apache logs. While counting the correct number of hits is possible with such logs, they don't reveal the true number of unique visitors. They do, however, contain a valid referrer URL, telling us which leaderboards and races were the most popular. + +In case we had an ELB log active, storing the Amazon ELB logs to an S3 bucket, we were able to convert those with a script stored in our git at `configuration/convertELBLogToApacheFormat` into our general Apache log format. This allows us to count the unique visitors for those events, but the ELB logs don't contain the referrer URL, so we patch that to always be the plain event URL, such as `http://kielerwoche2015.sapsailing.com`. Therefore, while these converted logs tell the correct number of unique visitors and the correct number of hits, they don't allow for an analysis of the most popular leaderboards and races. + +We hope that these partially-logged events remain the exception and that future events are logged homogeneously according to the rules above. The following naming conventions have been established under `/var/log/old` that shall allow the automatic log analyzers to identify the log file contents by file name patterns: + + - `access_log*`: A proper Apache log file with virtual host name as the first field, followed by the original client's IP address and a valid referrer URL as the second-to-last field + - `elb-origin-access_log*`: An Apache log file whose origin IP addresses are usually only the ELB IP addresses, therefore not allowing for unique visitor analysis + - `bundesliga2015_elb_access_log.gz` and `tw2015_elb_access_log.gz`: ELB log files converted to Apache format, appended in order of ascending time stamps, prefixed with the virtual host name as the first field, using the original client IP address which allows for unique visitor analysis, and the virtual host name used again as generic referrer URL + - `original-access_log*`: Apache log file without leading virtual host name field, also usually only with ELB IP addresses instead of an original client IP; not good for any log file analysis without further conversion, but kept for reference purposes + ## Automatic Log File Rotation to /var/log/old - - describe how logrotate is used and automatically configured during instance start-up and where the logs go +When an EC2 server is launched from its Amazon Machine Image (AMI), the /etc/init.d/sailing script is executed with the "start" parameter. This script is really just a link to `/home/sailing/code/configuration/sailing` which comes from the git version checked out to the workspace at `/home/sailing/code`. This script patches the file `/etc/logrotate.d/httpd` such that when log rotation happens, the existence of the directory `/var/log/old/$SERVER_NAME/$SERVER_IP` is ensured and the log files are copied there. + +This way, logs end up in a per server-name and per server-ip directory. + +The general log rotation rules (size, time, compression, etc.) is governed by `/etc/logrotate.conf`. ## Analysis Tools ### goaccess - - mention specific parameters and .goaccess config file required to parse our format +The file `/root/.goaccessrc` on `www.sapsailing.com` looks as follows: + +``` +color_scheme 1 +date_format %d/%b/%Y +time_format %H:%M:%S +log_format %^ %h %^[%d:%^] "%r" %s %b "%R" "%u" +log_file STDIN +``` + +Still, specific parameters are required for `goaccess` in order to parse our regular Apache logs that have the virtual host name as their first field: + +`goaccess --no-global-config -a -f /var/log/httpd/access_log` + +The unique visitors that goaccess lists are based on the common definition that counts requests as originating from the same visitor if they happened on the same day with equal user agent and IP address. ### apachetop +The simple command `apachetop` shows the traffic on the Apache httpd server running on the host where the command is invoked, based on the access log file in the usual location `/var/log/httpd/access_log`. + ### AWStats - - specific log collection approach configured in /etc/awstats/*.conf script +We use AWStats to analyze our Apache access log files and produce per-month reports at [http://awstats.sapsailing.com](http://awstats.sapsailing.com/awstats/awstats.pl?output=main&config=www.sapsailing.com&framename=index). The username is `awstats`. Ask axel.uhl@sap.com or stefan.lacher@sap.com for the password. - - extension for per-virtual-host hit count +AWStats report production is controlled by three things: a cron job hooked up by `/etc/cron.weekly/awstats` which is basically a one-liner launching the `awstats` command like this: - - configuration of central apache to present results (awstats.sapsailing.com) +`exec /usr/share/awstats/tools/awstats_updateall.pl now -configdir="/etc/awstats" -awstatsprog="/usr/share/awstats/wwwroot/cgi-bin/awstats.pl"` + +and a configuration file located at `/etc/awstats/awstats.www.sapsailing.com.conf` which describes in its `LogFile` directive the filename pattern to use for collecting the log files that shall be analyzed. Currently, the file name pattern is this: + +`/var/log/httpd/access_log /var/log/old/access_log-???????? /var/log/old/access_log-????????.gz /var/log/old/*/elb-origin-access_log* /var/log/old/*/*/elb-origin-access_log* /var/log/old/*/access_log* /var/log/old/*/*/access_log*` + +and all log entries that contain `HealthChecker` are eliminated as they are only a "ping" request sent by the load balancer to check the instance's health status which is not to be counted as a "hit." Note that according to the above explanations of our log file formats and variants this focuses on hit analysis for those events with broken or partial / incomplete log files such as Kieler Woche 2015 and Travemünder Woche 2015. It uses the `elb-origin-*` flavors, tolerating the fact that these don't have the original client's IP address. + +The third element of AWStats configuration is how it is published through Apache. This is described in `/etc/httpd/conf.d/awstats.conf` and uses the password definition file `/etc/httpd/conf/passwd.awstats` which can be updated using the `htpasswd` command. ### Custom Scripts - - unique_ips_per_referrer - - convertELBLogToApacheFormat \ No newline at end of file +#### `unique_ips_per_referrer` + +The script is located in git at `configuration/unique_ips_per_referrer`. It has two variants next to it: `unique_ips_per_referrer_generate_results_only` and `unique_ips_per_referrer_generate_month_results_only`. The basic idea of this family of scripts is to count the unique visitors for each virtual host name (usually corresponding to an event), in total and by month. Neither the `goaccess` nor the AWStats tool provide us with these numbers. AWStats can only compute the _hits_ per virtual host name, and `goaccess` only lists unique visitors per day, not per virtual host. + +The script can be fed with a list of log files in our common Apache format, with virtual host name in first, original client IP in second, time stamp in third and user agent in last column. For all new files (files already analyzed are recorded in `/var/log/old/cache/unique-ips-per-referrer/visited) the script then reduces each line to its timestamp, original client IP and user agent fields, constituting the key for identifying a unique visitor. Each log entry that has been analyzed and reduced this way is then appended to a file under `/var/log/old/cache/unique-ips-per-referrer/stats/.ips`. When done with all inputs, the `*.ips` files produced are filtered for unique entries (using `sort -u`) and number of lines are counted, resulting in the total number of unique visitors per virtual host name since the beginning of our log file records. + +Additionally, a per-month analysis is carried out by splitting the `*.ips` files by the months found in the time stamps and again applying a unique count. The resulting files can be found at `/var/log/old/cache/unique-ips-per-referrer/stats/unique-ips-days-useragents-per-event` and `/var/log/old/cache/unique-ips-per-referrer/stats/unique-ips-days-useragents-per-event-MMM-YYYY` where `MMM` represents the three-letter month name such as `Apr` and `YYYY` stands for the year. + +The `unique_ips_per_referrer` script is registered as a weekly cron job in `/etc/cron.weekly` and is fed the special converted log files of Travemünder Woche 2015 and the Bundesliga logs from 2015, as well as all files matching the file name pattern `access_log-*` anywhere under `/var/log/old`. This only matches proper Apache log files with original client IP addresses, not those with ELB IP addresses only (which are named `elb-origin-access_log*` by convention, see above). + +The variant `unique_ips_per_referrer_generate_results_only` assumes that the `*.ips` files have already been updated and produces the total and per-month output files. + +The variant `unique_ips_per_referrer_generate_month_results_only` produces only the per-month output files, also assuming that the `*.ips` files have already been updated. + +The results are published under the link [http://awstats.sapsailing.com/unique-visitors/](http://awstats.sapsailing.com/unique-visitors/) which uses the same credentials as the main AWStats publishing page. + +#### `convertELBLogToApacheFormat` + +This script was already briefly mentioned above. It is found at `configuration/convertELBLogToApacheFormat` and converts and Amazon Elastic Load Balancer (ELB) log file to our common Apache log file format with leading virtual host name. The virtual host name is expected as the first parameter; all subsequent parameters are treated as file names of ELB log files, either in GZIP format with `.gz` extension or uncompressed. The conversion result is streamed to the standard output and may therefore be redirected into a file. \ No newline at end of file diff --git a/wiki/onboarding.md b/wiki/onboarding.md index 12d24ae467a..d4a53baa226 100644 --- a/wiki/onboarding.md +++ b/wiki/onboarding.md @@ -67,11 +67,13 @@ First of all, make sure you've looked at http://www.amazon.de/Patterns-Elements- * Rebuild all projects 4. Run the Race Analysis Suite * Start the MongoDB - * Start the appropriate Eclipse launch configuration (e.g. 'Sailing Server (Proxy)') You´ll find this in the run dropdown - * Run "Security UI sdm" in the run dropdown - * Run "SailingGWT" in the run dropdown -5. Within the Race Analysis Suite + * Start the appropriate Eclipse launch configuration (e.g. 'Sailing Server (Proxy)') You´ll find this in the debug dropdown + * Run "Security UI sdm" in the debug dropdown + * Run "SailingGWT" in the debug dropdown +5. Import races within the Race Analysis Suite + * Choose "Security UI sdm" in the upper left corner of the "Development Mode" Tab in Eclipse and open "...Login.html" in your browser * Default Login: user "admin", password "admin" + * Choose "Sailing GWT" in the "Development Mode" Tab and open "...AdminConsole.html..." (It is normal that the first try fails. Reload the page after the first try) * For TracTrac Events: (Date 27.11.2012) Use Live URI tcp://10.18.22.156:4412, Stored URI tcp://10.18.22.156:4413, JSON URL http://germanmaster.traclive.dk/events/event_20120905_erEuropean/jsonservice.php * Press List Races