diff --git a/configuration/backup_full.sh b/configuration/backup_full.sh new file mode 100755 index 00000000000..afca4a1c32c --- /dev/null +++ b/configuration/backup_full.sh @@ -0,0 +1,122 @@ +#!/bin/sh + +# This is a template for creating backup of data and persist them +# to a central backup server. Make sure to adapt this script to your needs. +# +# Please do not change the name of the bakup (bup save -n) because that can +# have strange effects. If you change it you absolutely need to make sure that +# only the same root directories are associated to that name. It is NOT possible +# to backup different root directories to the same backup name. +# +# Maintainer: simon.marcel.pamies@sap.com, axel.uhl@sap.com + +# Directories to backup +BACKUP_DIRECTORIES="/etc /var/log/mongodb" + +# Prefix for backup - set that to the hostname of your server +# Make sure to change this! +PREFIX="database" + +# Directory for temporary files +TARGET_DIR=/var/lib/mysql/backup + +# Configuration for MongoDB backup +MONGO_PORTS="10202 10201" +MONGO_INSTALLATION=/opt/mongodb-linux-x86_64-2.6.7 +MONGO_CMD=$MONGO_INSTALLATION/bin/mongo +MONGOEXPORT_CMD=$MONGO_INSTALLATION/bin/mongoexport +MONGODB_COLLECTION_WIND="WIND_TRACKS" +MONGODB_COLLECTION_LOCAL="local" + +# Configuration for MySQL +MYSQL_DATABASES="bugs mysql" +MYSQLEXPORT_CMD="mysqldump -u root --password=sailaway" + +# Configuration for external backup server - needs a ssh key +BACKUP_SERVER="backup@172.31.25.136" + +# Set date for this backup - this makes it possible to restore +# files from different branches (named backups) +BACKUP_DATE=`date +%s` + +# Aliases +BUP_CMD="/opt/bup/bup" +BUP_ADDITIONAL="-r $BACKUP_SERVER:/home/backup/$PREFIX" +BUP_IGNORES='--exclude-rx=/war/$' +BUP_CMD_INDEX="$BUP_CMD index $BUP_IGNORES" +BUP_CMD_SAVE="$BUP_CMD save --date=$BACKUP_DATE $BUP_ADDITIONAL" + +PARAM=$@ + +# Make sure to install the right libraries +yum install python-devel fuse-python pyxattr pylibacl perl-Time-HiRes + +# We need to set HOME in case it isn't set right - assuming root is calling +export HOME=/root + +# Make sure to init local and remote repository +# It is perfectly safe to reinitialize a repository w/o overwriting data +$BUP_CMD init +ssh $BACKUP_SERVER "/opt/bup/bup init -r /home/backup/$PREFIX" + +# Backup general directories +for OTHER_DIR in $BACKUP_DIRECTORIES; do + NORMALIZED_DIR_NAME=${OTHER_DIR//\//-} + echo "Backing up $OTHER_DIR" + $BUP_CMD_INDEX $OTHER_DIR + $BUP_CMD_SAVE -n dir$NORMALIZED_DIR_NAME $OTHER_DIR + # no need to remove any backup files here +done + +# ************************************** +# Start of database specific backup +# ************************************** + +# Export MongoDB instances with wind upon request (weekly parameter provided) +for MONGO_PORT in $MONGO_PORTS; do + echo Backing up MongoDB running on port $MONGO_PORT + MONGODB_DATABASES=`echo "show dbs" | $MONGO_CMD --port $MONGO_PORT | tail -n +3 | grep -v "^bye$" | awk '{print $1;}'` + for i in $MONGODB_DATABASES; do + echo " Backing up database $i in MongoDB running on port $MONGO_PORT" + MONGODB_COLLECTIONS=`echo "use $i +show collections" | $MONGO_CMD --port $MONGO_PORT | tail -n +4 | grep -v "^system.indexes$" | grep -v "^bye$"` + echo "Collections in MongoDB $i running on port $MONGO_PORT: $MONGODB_COLLECTIONS" + for j in $MONGODB_COLLECTIONS; do + if [ "$j" = "$MONGODB_COLLECTION_WIND" ]; then + WIND_SUFFIX="-wind" + else + WIND_SUFFIX= + fi + if [ "$i" != "winddb" ]; then + DB_DIR=$TARGET_DIR/mongodb-$MONGO_PORT-$i$WIND_SUFFIX + else + DB_DIR=$TARGET_DIR/mongodb-$MONGO_PORT$WIND_SUFFIX + fi + if [ "$i" != "$MONGODB_COLLECTION_LOCAL" -a \( "$i" != "$MONGODB_COLLECTION_WIND" -o "$PARAM" = "weekly" \) ]; then + echo " Backing up collection $j in MongoDB $i running on port $MONGO_PORT" + echo "$MONGOEXPORT_CMD --port $MONGO_PORT -d $i -c $j > $DB_DIR/$j.json" + mkdir -p $DB_DIR + $MONGOEXPORT_CMD --port $MONGO_PORT -d $i -c $j > $DB_DIR/$j.json + fi + done + done +done + +# Export MySQL dump for selected databases +for MYSQL_DB in $MYSQL_DATABASES; do + mkdir $TARGET_DIR/mysql + echo "$MYSQLEXPORT_CMD $MYSQL_DB > $TARGET_DIR/mysql/$MYSQL_DB.sql" + $MYSQLEXPORT_CMD $MYSQL_DB > $TARGET_DIR/mysql/$MYSQL_DB.sql +done + +# Backup the whole thing now and push to external server +$BUP_CMD_INDEX $TARGET_DIR +$BUP_CMD_SAVE -n databases $TARGET_DIR/ + +# clean up +find $TARGET_DIR -name *.json | xargs rm -f +rm -f $TARGET_DIR/mysql/*.sql + +# ************************************** +# End of database specific backup +# ************************************** diff --git a/configuration/backup_new.sh b/configuration/backup_new.sh deleted file mode 100755 index ef8d2650da3..00000000000 --- a/configuration/backup_new.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/bin/sh - -# This is a template for creating backup of data and persist them -# to a central backup server. Make sure to adapt this script to your needs. -# -# Please do not change the name of the bakup (bup save -n) because that can -# have strange effects. If you change it you absolutely need to make sure that -# only the same root directories are associated to that name. It is NOT possible -# to backup different root directories to the same backup name. -# -# Maintainer: simon.marcel.pamies@sap.com -# -# ###################################################################### -# CHANGELOG -# -# 2016-09-13 (Steffen Wagner): -# - extracting all databases from a mongo connection instead of defining -# the exportable databases manually -# - made databases excludeable (by regex and name) -# - -# Directories to backup -BACKUP_DIRECTORIES="/etc /var/log/mongodb" - -# Prefix for backup - set that to the hostname of your server -# Make sure to change this! -PREFIX="database" - -# Directory for temporary files -TARGET_DIR=/var/lib/mysql/backup - -# Configuration for MongoDB backup -MONGO_CONNECTIONS="localhost:10201 localhost:10202" -MONGOEXPORT_CMD="/opt/mongodb-linux-x86_64-1.8.1/bin/mongoexport" -MONGODB_COLLECTIONS_NO_WIND="ActiveFileStorageService COMMAND_MESSAGES COMPETITORS CONFIGURATIONS EVENTS FileStorageServiceProperties GPS_FIXES GPS_FIXES_METADATA IGTIMI_ACCESS_TOKENS LAST_MESSAGE_COUNT LEADERBOARDS LEADERBOARD_GROUPS LEADERBOARD_GROUP_LINKS_FOR_EVENTS PREFERENCES RACES_MESSAGES RACE_LOGS REGATTAS REGATTA_FOR_RACE_ID REGATTA_LOGS RESULT_URLS SAILING_SERVERS SETTINGS SWISSTIMING_ARCHIVE_CONFIGURATIONS SWISSTIMING_CONFIGURATIONS TRACTRAC_CONFIGURATIONS USERS VIDEOS" -MONGODB_COLLECTIONS_WIND="WIND_TRACKS" -MONGODB_EXCLUDEDB="10201:LOAD-TEST 10201:winddb 10201:live 10201:local 10201:admin 10202:DNSTEST2 10202:imagetest 10202:live 10202:replication-test-master 10202:load-test 10202:admin 10202:dev 10202:dev-2010 10202:BYC-2010 10202:sapsailinganalytics-dev 10202:test" - -# Configuration for MySQL -MYSQL_DATABASES="bugs mysql" -MYSQLEXPORT_CMD="mysqldump -u root --password=sailaway" - -# Configuration for external backup server - needs a ssh key -BACKUP_SERVER="backup@172.31.25.136" - -# Set date for this backup - this makes it possible to restore -# files from different branches (named backups) -BACKUP_DATE=`date +%s` - -# Aliases -BUP_CMD="/opt/bup/bup" -BUP_ADDITIONAL="-r $BACKUP_SERVER:/home/backup/$PREFIX" -BUP_IGNORES='--exclude-rx=/war/$' -BUP_CMD_INDEX="$BUP_CMD index $BUP_IGNORES" -BUP_CMD_SAVE="$BUP_CMD save --date=$BACKUP_DATE $BUP_ADDITIONAL" - -PARAM=$@ - -# Make sure to install the right libraries -yum install python-devel fuse-python pyxattr pylibacl perl-Time-HiRes - -# We need to set HOME in case it isn't set right - assuming root is calling -export HOME=/root - -# Make sure to init local and remote repository -# It is perfectly safe to reinitialize a repository w/o overwriting data -$BUP_CMD init -ssh $BACKUP_SERVER "/opt/bup/bup init -r /home/backup/$PREFIX" - -# Backup general directories -for OTHER_DIR in $BACKUP_DIRECTORIES; do - NORMALIZED_DIR_NAME=${OTHER_DIR//\//-} - echo "Backing up $OTHER_DIR" - $BUP_CMD_INDEX $OTHER_DIR - $BUP_CMD_SAVE -n dir$NORMALIZED_DIR_NAME $OTHER_DIR - # no need to remove any backup files here -done - -# ************************************** -# Start of database backups -# ************************************** - -# Backup all databases from all mongo connections -for MONGO_DB in $MONGO_CONNECTIONS; do - CONNECTION=(${MONGO_DB//:/ }) - DATABASES_UNFORMATTED=($(mongo --host ${CONNECTION[0]} --port ${CONNECTION[1]} --quiet --eval "JSON.stringify(db.adminCommand('listDatabases'))")) - DATABASES_FORMATTED=`echo ${DATABASES_UNFORMATTED} | jq -r --arg port ${CONNECTION[1]} '.databases| map($port + ":" + .name)|@csv' | tr -d '"' | tr ',' ' '` - - # Remove all explicit excluded databases - LIST_DATABASES_FORMATTED=( $DATABASES_FORMATTED ) - LIST_MONGODB_EXCLUDEDB=( $MONGODB_EXCLUDEDB ) - DATABASES_EXCLUDED=`echo ${LIST_DATABASES_FORMATTED[@]} ${LIST_MONGODB_EXCLUDEDB[@]} | tr ' ' '\n' | sort | uniq -u` - - # Remove all regex excluded databases - DATABASES_FINAL=`echo ${DATABASES_EXCLUDED} | sed -r 's/[0-9]{5}:replica[0-9]+//gi'` - - # Export MongoDB instances with wind upon request (weekly parameter provided) - if [ "$PARAM" = "weekly" ]; then - for MONGO_CONFIG in $DATABASES_FINAL; do - PARTS=(${MONGO_CONFIG//:/ }) - if [ "${PARTS[1]}" != "winddb" ]; then - mkdir $TARGET_DIR/mongodb-${PARTS[0]}-${PARTS[1]}-wind - DB_SUFFIX=${PARTS[0]}-${PARTS[1]}-wind - else - mkdir $TARGET_DIR/mongodb-${PARTS[0]}-wind - DB_SUFFIX=${PARTS[0]}-wind - fi - for COLLECTION in $MONGODB_COLLECTIONS_WIND; do - echo "$MONGOEXPORT_CMD --port ${PARTS[0]} -d ${PARTS[1]} -c $COLLECTION > $TARGET_DIR/mongodb-$DB_SUFFIX/$COLLECTION.json" - $MONGOEXPORT_CMD --port ${PARTS[0]} -d ${PARTS[1]} -c $COLLECTION > $TARGET_DIR/mongodb-$DB_SUFFIX/$COLLECTION.json - done - done - fi - - # Export MongoDB instances without wind - for MONGO_CONFIG in $DATABASES_FINAL; do - PARTS=(${MONGO_CONFIG//:/ }) - if [ "${PARTS[1]}" != "winddb" ]; then - mkdir $TARGET_DIR/mongodb-${PARTS[0]}-${PARTS[1]} - DB_SUFFIX=${PARTS[0]}-${PARTS[1]} - else - mkdir $TARGET_DIR/mongodb-${PARTS[0]} - DB_SUFFIX=${PARTS[0]} - fi - for COLLECTION in $MONGODB_COLLECTIONS_NO_WIND; do - echo "$MONGOEXPORT_CMD --port ${PARTS[0]} -d ${PARTS[1]} -c $COLLECTION > $TARGET_DIR/mongodb-$DB_SUFFIX/$COLLECTION.json" - $MONGOEXPORT_CMD --port ${PARTS[0]} -d ${PARTS[1]} -c $COLLECTION > $TARGET_DIR/mongodb-$DB_SUFFIX/$COLLECTION.json - done - done -done - -# Export MySQL dump for selected databases -for MYSQL_DB in $MYSQL_DATABASES; do - mkdir $TARGET_DIR/mysql - echo "$MYSQLEXPORT_CMD $MYSQL_DB > $TARGET_DIR/mysql/$MYSQL_DB.sql" - $MYSQLEXPORT_CMD $MYSQL_DB > $TARGET_DIR/mysql/$MYSQL_DB.sql -done - -# Backup the whole thing now and push to external server -$BUP_CMD_INDEX $TARGET_DIR -$BUP_CMD_SAVE -n databases $TARGET_DIR/ - -# clean up -find $TARGET_DIR -name *.json | xargs rm -f -rm -f $TARGET_DIR/mysql/*.sql - -# ************************************** -# End of database specific backup -# ************************************** \ No newline at end of file diff --git a/configuration/buildAndUpdateProduct.sh b/configuration/buildAndUpdateProduct.sh index a8133831ce8..a4f1c035436 100755 --- a/configuration/buildAndUpdateProduct.sh +++ b/configuration/buildAndUpdateProduct.sh @@ -634,6 +634,9 @@ if [[ "$@" == "build" ]] || [[ "$@" == "all" ]]; then SDK_MANAGER="$ANDROID_HOME/tools/bin/sdkmanager.bat" fi + # Uncomment the following line for testing an artifact stages in the SAP-central Nexus system: + # mobile_extra="-P -with-not-android-relevant -P with-mobile -P use-staged-third-party-artifacts -Dmaven.repo.local=${TMP}/temp_maven_repo" + # Use the following line for regular builds with no staged Nexus artifacts: mobile_extra="-P -with-not-android-relevant -P with-mobile" if [ $testing -eq 0 ]; then diff --git a/configuration/finduserbyemail.sh b/configuration/finduserbyemail.sh index 87468208813..62bea8c2288 100755 --- a/configuration/finduserbyemail.sh +++ b/configuration/finduserbyemail.sh @@ -3,7 +3,7 @@ # Executes the following script on the DB server: ##!/bin/sh #useremail=$1 -#for i in `echo "show dbs" | /opt/mongodb-linux-x86_64-2.6.7/bin/mongo --port 10202 | grep -v "^bye$" | awk '{print $1;'}`; do +#for i in `echo "show dbs" | /opt/mongodb-linux-x86_64-2.6.7/bin/mongo --port 10202 | tail -n +3 | grep -v "^bye$" | awk '{print $1;}'`; do # match=`echo "db.USERS.find({EMAIL: '$useremail'}); db.COMPETITORS.find({email: '$useremail'})" | /opt/mongodb-linux-x86_64-2.6.7/bin/mongo --quiet --port 10202 $i` # if [ "$match" != "" ]; then # echo ${i}: $match diff --git a/configuration/installPluginsForEclipsePhoton.sh b/configuration/installPluginsForEclipsePhoton.sh old mode 100644 new mode 100755 diff --git a/configuration/maven-settings.xml b/configuration/maven-settings.xml index 8206a71f912..537d51554cb 100644 --- a/configuration/maven-settings.xml +++ b/configuration/maven-settings.xml @@ -33,6 +33,22 @@ + + + use-staged-third-party-artifacts + + + staged-third-party-artifacts + http://nexus.wdf.sap.corp:8081/nexus/content/repositories/3rd-party.releases.manual-uploads.all-closed-stages/ + + + + + staged-third-party-artifacts + http://nexus.wdf.sap.corp:8081/nexus/content/repositories/3rd-party.releases.manual-uploads.all-closed-stages/ + + + diff --git a/java/com.google.gwt.servlet/.classpath b/java/com.google.gwt.servlet/.classpath index a070fc0c879..fa521b36030 100644 --- a/java/com.google.gwt.servlet/.classpath +++ b/java/com.google.gwt.servlet/.classpath @@ -1,6 +1,6 @@ - + diff --git a/java/com.sap.sailing.dashboards.gwt/GWT Dashboards DevMode.launch b/java/com.sap.sailing.dashboards.gwt/GWT Dashboards DevMode.launch index 7310aa92bc6..24ad905fc2b 100644 --- a/java/com.sap.sailing.dashboards.gwt/GWT Dashboards DevMode.launch +++ b/java/com.sap.sailing.dashboards.gwt/GWT Dashboards DevMode.launch @@ -1,47 +1,49 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.dashboards.gwt/GWT Dashboards SDM.launch b/java/com.sap.sailing.dashboards.gwt/GWT Dashboards SDM.launch index c5fc2f809c2..db0d10b4903 100644 --- a/java/com.sap.sailing.dashboards.gwt/GWT Dashboards SDM.launch +++ b/java/com.sap.sailing.dashboards.gwt/GWT Dashboards SDM.launch @@ -1,48 +1,50 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF b/java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF index 904e059fb74..d508c6cedfb 100644 --- a/java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF @@ -17,19 +17,14 @@ Require-Bundle: com.sap.sailing.domain, com.sap.sailing.gwt.ui, org.moxieapps.gwt.highcharts;bundle-version="1.6.0";resolution:=optional, com.sap.sse.gwt, - com.sap.sailing.domain.igtimiadapter, - com.sap.sailing.expeditionconnector, - com.sap.sailing.server.gateway, - com.sap.sailing.declination, - org.json.simple;bundle-version="1.1.0", - com.sap.sailing.udpconnector, + com.sap.sailing.expeditionconnector.common, com.googlecode.mgwt;bundle-version="2.0.0";resolution:=optional, com.sap.sse.gwt.adminconsole, org.apache.commons.math;bundle-version="2.1.0", - com.sap.sse.security.ui + com.sap.sse.security.ui, + com.sap.sse Bundle-ActivationPolicy: lazy -Import-Package: com.sap.sse.util, - javax.servlet;version="3.1.0", +Import-Package: javax.servlet;version="3.1.0", javax.servlet.descriptor;version="3.1.0", javax.servlet.http;version="3.1.0", org.apache.commons.io, diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactoryImpl.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactoryImpl.java index 57e263299b0..7076e8407c0 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactoryImpl.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactoryImpl.java @@ -21,7 +21,7 @@ public class DashboardClientFactoryImpl implements DashboardClientFactory{ public DashboardClientFactoryImpl() { dispatch = new DashboardDispatchSystemImpl(); - sailingService = SailingServiceHelper.createSailingServiceInstance(false); + sailingService = SailingServiceHelper.createSailingServiceInstance(true); dashboardFiveSecondsTimer = new Timer(PlayModes.Live); dashboardFiveSecondsTimer.setRefreshInterval(DASHBOARD_RERFRESH_INTERVAL); diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardEntryPoint.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardEntryPoint.java index 82aabb4338f..e1d65f16d70 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardEntryPoint.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardEntryPoint.java @@ -21,7 +21,7 @@ public class RibDashboardEntryPoint extends AbstractEntryPoint { public void doOnModuleLoad() { applyMGWTSettings(); DashboardClientFactory dashboardClientFactory = new DashboardClientFactoryImpl(); - DashboardPanel rootPanel = new DashboardPanel(dashboardClientFactory); + DashboardPanel rootPanel = new DashboardPanel(dashboardClientFactory, this); RootLayoutPanel.get().add(rootPanel); WindBotDataRetriever windBotDataRetriever = new WindBotDataRetriever(dashboardClientFactory); windBotDataRetriever.addNumberOfWindBotsChangeListeners(rootPanel); diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dashboardpanel/DashboardPanel.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dashboardpanel/DashboardPanel.java index 56c672e0951..bb4caa36a73 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dashboardpanel/DashboardPanel.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dashboardpanel/DashboardPanel.java @@ -36,6 +36,7 @@ import com.sap.sailing.dashboards.gwt.shared.dto.RaceIdDTO; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.EventDTO; +import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.media.ImageDTO; /** @@ -88,12 +89,12 @@ public class DashboardPanel extends Composite implements NumberOfWindBotsChangeL DashboardClientFactory dashboardClientFactory; private static final Logger logger = Logger.getLogger(DashboardPanel.class.getName()); - public DashboardPanel(DashboardClientFactory dashboardClientFactory) { + public DashboardPanel(DashboardClientFactory dashboardClientFactory, ErrorReporter errorReporter) { DashboardPanelResources.INSTANCE.style().ensureInjected(); this.dashboardClientFactory = dashboardClientFactory; leftWindBotWidget = new WindBotWidget(dashboardClientFactory); rightWindBotWidget = new WindBotWidget(dashboardClientFactory); - startanalysisComponent = new StartAnalysisWidget(this.dashboardClientFactory); + startanalysisComponent = new StartAnalysisWidget(this.dashboardClientFactory, errorReporter); startlineAdvantagesByWindComponent = new StartlineAdvantagesByWindWidget(this.dashboardClientFactory); startlineAdvantageByGeometryWidget = new StartlineAdvantageByGeometryWidget(this.dashboardClientFactory); stringConstants = StringMessages.INSTANCE; diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/StartAnalysisWidget.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/StartAnalysisWidget.java index 787fe700495..f5d3c783b83 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/StartAnalysisWidget.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/StartAnalysisWidget.java @@ -43,6 +43,7 @@ import com.sap.sailing.dashboards.gwt.shared.dto.StartAnalysisDTO; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapResources; +import com.sap.sse.gwt.client.ErrorReporter; /** * The class contains an collection of {@link StartAnalysisCard}s that are displayed in horizontal aligned pages. It @@ -112,11 +113,12 @@ public class StartAnalysisWidget extends Composite implements HasWidgets, PollsL * */ private int numberOfStartAnalysisCards = 0; private boolean displaysCards; - private List starts; - private List pageChangeListener; + private final List starts; + private final List pageChangeListener; private BottomNotification bottomNotification; private CompetitorSelectionPopup competitorSelectionPopup; - private DashboardClientFactory dashboardClientFactory; + private final DashboardClientFactory dashboardClientFactory; + private final ErrorReporter errorReporter; private final String cookieKeyForSelectedCompetitorInLeaderboard; @@ -131,8 +133,9 @@ public class StartAnalysisWidget extends Composite implements HasWidgets, PollsL /** * Component that contains handles, displays and loads startanalysis cards. * */ - public StartAnalysisWidget(DashboardClientFactory dashboardClientFactory) { + public StartAnalysisWidget(DashboardClientFactory dashboardClientFactory, ErrorReporter errorReporter) { StartAnalysisWidgetResources.INSTANCE.gss().ensureInjected(); + this.errorReporter = errorReporter; dashboardWidgetHeaderAndNoDataMessage = new DashboardWidgetHeaderAndNoDataMessage(); selectedCompetitorWithSettingsButton = new SettingsButtonWithSelectionIndicationLabel(); selectedCompetitorWithSettingsButton.disable(); @@ -378,7 +381,7 @@ public class StartAnalysisWidget extends Composite implements HasWidgets, PollsL } final StartAnalysisCard startlineAnalysisCard = new StartAnalysisCard(numberOfStartAnalysisCards * SCROLL_OFFSET_STARTANALYSIS_CARDS + MARGIN_LEFT_STARTANALYSIS_CARD, numberOfStartAnalysisCards, - startAnalysisDTO, dashboardClientFactory.getSailingService(), raceMapResources); + startAnalysisDTO, dashboardClientFactory.getSailingService(), errorReporter, raceMapResources); startanalysis_card_container.add(startlineAnalysisCard); startlineAnalysisCard.startAnalysisComponentPageChangedToIndexAndStartAnalysis(page, startAnalysisDTO); registerPageChangeListener(startlineAnalysisCard); diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/card/StartAnalysisCard.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/card/StartAnalysisCard.java index 497dc402680..961e25cec89 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/card/StartAnalysisCard.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/card/StartAnalysisCard.java @@ -4,6 +4,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; +import java.util.Map; +import java.util.stream.Collectors; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.Scheduler; @@ -23,6 +25,8 @@ import com.sap.sailing.dashboards.gwt.client.widgets.startanalysis.StartAnalysis import com.sap.sailing.dashboards.gwt.client.widgets.startanalysis.rankingtable.StartAnalysisStartRankTable; import com.sap.sailing.dashboards.gwt.shared.StartlineAdvantageType; import com.sap.sailing.dashboards.gwt.shared.dto.StartAnalysisDTO; +import com.sap.sailing.domain.common.dto.BoatDTO; +import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.domain.common.racelog.RacingProcedureType; import com.sap.sailing.gwt.ui.client.RaceCompetitorSelectionModel; import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; @@ -38,6 +42,7 @@ import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapResources; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapSettings; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapZoomSettings; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapZoomSettings.ZoomTypes; +import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; import com.sap.sse.gwt.client.player.Timer.PlayModes; @@ -70,8 +75,9 @@ public class StartAnalysisCard extends Composite implements HasWidgets, StartAna private StartAnalysisDTO startAnalysisDTO; private RaceMap raceMap; - private SailingServiceAsync sailingServiceAsync; - private StringMessages stringMessages; + private final SailingServiceAsync sailingServiceAsync; + private final ErrorReporter errorReporter; + private final StringMessages stringMessages; private final RaceCompetitorSelectionModel competitorSelectionModel; private final double WIND_LINE_ADVANTAGE_DIV_WIDTH_IN_PT = 185; @@ -80,11 +86,15 @@ public class StartAnalysisCard extends Composite implements HasWidgets, StartAna private RaceMapResources raceMapResources; - public StartAnalysisCard(double leftCSSProperty, int cardId, StartAnalysisDTO startAnalysisDTO, SailingServiceAsync sailingServiceAsync, RaceMapResources raceMapResources) { + public StartAnalysisCard(double leftCSSProperty, int cardId, StartAnalysisDTO startAnalysisDTO, + SailingServiceAsync sailingServiceAsync, ErrorReporter errorReporter, RaceMapResources raceMapResources) { stringMessages = StringMessages.INSTANCE; this.sailingServiceAsync = sailingServiceAsync; + this.errorReporter = errorReporter; this.raceMapResources = raceMapResources; - competitorSelectionModel = new RaceCompetitorSelectionModel(/* hasMultiSelection */true); + final Map competitorsToBoats = startAnalysisDTO.startAnalysisCompetitorDTOs.stream() + .collect(Collectors.toMap(c -> c.competitorDTO, c -> c.boatDTO)); + competitorSelectionModel = new RaceCompetitorSelectionModel(/* hasMultiSelection */true, competitorsToBoats); competitorSelectionModel.setCompetitors(startAnalysisDTO.getCompetitorDTOsFromStartAnaylsisCompetitorDTOs(), raceMap); initWidget(uiBinder.createAndBindUi(this)); startanalysis_card.getElement().getStyle().setLeft(leftCSSProperty, Unit.PCT); @@ -167,10 +177,11 @@ public class StartAnalysisCard extends Composite implements HasWidgets, StartAna RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(sailingServiceAsync, - asyncActionsExecutor, null, Collections.singletonList(startAnalysisDTO.regattaAndRaceIdentifier), 5000l /* requestInterval */); + asyncActionsExecutor, errorReporter, + Collections.singletonList(startAnalysisDTO.regattaAndRaceIdentifier), 5000l /* requestInterval */); raceMap = new RaceMap(null, null, new RaceMapLifecycle(StringMessages.INSTANCE), raceMapSettings, sailingServiceAsync, - asyncActionsExecutor, null, timer, competitorSelectionModel, + asyncActionsExecutor, errorReporter, timer, competitorSelectionModel, new RaceCompetitorSet(competitorSelectionModel), StringMessages.INSTANCE, startAnalysisDTO.regattaAndRaceIdentifier, raceMapResources, /* showHeaderPanel */ true, new DefaultQuickRanksDTOProvider()); raceTimesInfoProvider.addRaceTimesInfoProviderListener(raceMap); diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startanalysis/StartAnalysisDTOFactory.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startanalysis/StartAnalysisDTOFactory.java index 3f330bec180..a5ce58cd6c2 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startanalysis/StartAnalysisDTOFactory.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startanalysis/StartAnalysisDTOFactory.java @@ -193,6 +193,8 @@ public final class StartAnalysisDTOFactory extends AbstractStartAnalysisCreation StartAnalysisCompetitorDTO startAnalysisCompetitorDTOsForRace = new StartAnalysisCompetitorDTO(); startAnalysisCompetitorDTOsForRace.competitorDTO = dashboardDispatchContext.getRacingEventService().getBaseDomainFactory().getCompetitorAndBoatStore() .convertToCompetitorDTO(competitor); + startAnalysisCompetitorDTOsForRace.boatDTO = dashboardDispatchContext.getRacingEventService().getBaseDomainFactory().getCompetitorAndBoatStore() + .convertToBoatDTO(trackedRace.getBoatOfCompetitor(competitor)); startAnalysisCompetitorDTOsForRace.rankingTableEntryDTO = createRankTableEntry(trackedRace, rank, competitor); return startAnalysisCompetitorDTOsForRace; } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartAnalysisCompetitorDTO.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartAnalysisCompetitorDTO.java index 42c2acd9715..db6c345e84d 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartAnalysisCompetitorDTO.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartAnalysisCompetitorDTO.java @@ -2,6 +2,7 @@ package com.sap.sailing.dashboards.gwt.shared.dto; import java.io.Serializable; +import com.sap.sailing.domain.common.dto.BoatDTO; import com.sap.sailing.domain.common.dto.CompetitorDTO; public class StartAnalysisCompetitorDTO implements Serializable{ @@ -9,5 +10,6 @@ public class StartAnalysisCompetitorDTO implements Serializable{ private static final long serialVersionUID = -4236444628127858568L; public CompetitorDTO competitorDTO; + public BoatDTO boatDTO; public StartAnalysisRankingTableEntryDTO rankingTableEntryDTO; } diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_es.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_es.properties index 038c4898bc2..22d67d92adb 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_es.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_es.properties @@ -188,7 +188,7 @@ AbsRelativeBearingToNextMarkAfterManeuver=Rumbo relativo absoluto a la siguiente RatioBetweenDistanceSailedWithAndWithoutManeuver=Ratio entre distancia navegada con y sin maniobra RatioBetweenDistanceSailedTowardMiddleAngleProjectionWithAndWithoutManeuver=Ratio entre distancia navegada hacia la proyección del ángulo medio con y sin maniobra DurationLostByManeuver=Tiempo perdido por maniobra -DurationLostByManeuverTowardMiddleAngleProjection=tiempo perdido por maniobra hacia la proyección del ángulo medio +DurationLostByManeuverTowardMiddleAngleProjection=Tiempo perdido por maniobra hacia la proyección del ángulo medio JibingCount=Recuento de trasluchadas TackingCount=Recuento de viradas AbsTwaAtMaxTurningRate=TWA absoluto en velocidad máxima de giro @@ -199,3 +199,4 @@ PercentageOfMainCurveProgressUntilHighestSpeed=Porcentaje de progreso de curva p PercentageOfMainCurveProgressUntilMaxTurningRate=Porcentaje de progreso de curva principal hasta velocidad máxima de giro AbsTwaAtManeuverMiddle=TWA absoluto en mitad de maniobra GpsSamplingRate=Velocidad de muestreo de GPS +RaceDuration=Duración de carrera diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_fr.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_fr.properties index 6d9e3781cb4..3c9baf274ae 100755 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_fr.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_fr.properties @@ -199,3 +199,4 @@ PercentageOfMainCurveProgressUntilHighestSpeed=Pourcentage de progression de la PercentageOfMainCurveProgressUntilMaxTurningRate=Pourcentage de progression de la courbe principale jusqu''à la vitesse angulaire max. AbsTwaAtManeuverMiddle=TWA absolu au milieu de la manœuvre GpsSamplingRate=Fréquence d''échantillonage GPS +RaceDuration=Durée de la course diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ja.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ja.properties index 153c78f1b5f..af7b95d25a7 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ja.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ja.properties @@ -199,3 +199,4 @@ PercentageOfMainCurveProgressUntilHighestSpeed=最高速度までの主曲線進 PercentageOfMainCurveProgressUntilMaxTurningRate=回転率最大速度までの主曲線進行の比率 (%) AbsTwaAtManeuverMiddle=マニューバー中間での絶対 TWA GpsSamplingRate=GPS サンプリング率 +RaceDuration=レース持続時間 diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_pt.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_pt.properties index 48c61759887..46904285579 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_pt.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_pt.properties @@ -188,7 +188,7 @@ AbsRelativeBearingToNextMarkAfterManeuver=Rumo relativo absoluto até a marca se RatioBetweenDistanceSailedWithAndWithoutManeuver=Proporção entre distância navegada com e sem manobra RatioBetweenDistanceSailedTowardMiddleAngleProjectionWithAndWithoutManeuver=Proporção entre distância navegada em direção à projeção do ângulo médio com e sem manobra DurationLostByManeuver=Duração perdida por manobra -DurationLostByManeuverTowardMiddleAngleProjection=duração perdida por manobra em direção à projeção do ângulo médio +DurationLostByManeuverTowardMiddleAngleProjection=Duração perdida por manobra em direção à projeção do ângulo médio JibingCount=Contagem de jaibe TackingCount=Contagem de cambada AbsTwaAtMaxTurningRate=TWA absoluto na taxa de viragem máxima @@ -199,3 +199,4 @@ PercentageOfMainCurveProgressUntilHighestSpeed=Porcentagem de progresso da curva PercentageOfMainCurveProgressUntilMaxTurningRate=Porcentagem de progresso da curva principal até velocidade de taxa de viragem máxima AbsTwaAtManeuverMiddle=TWA absoluto no meio da manobra GpsSamplingRate=Taxa de amostra de GPS +RaceDuration=Duração da corrida diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ru.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ru.properties index bc36275620f..01ef682a6e3 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ru.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ru.properties @@ -188,7 +188,7 @@ AbsRelativeBearingToNextMarkAfterManeuver=Абс. относительное н RatioBetweenDistanceSailedWithAndWithoutManeuver=Соотношение пройденного расстояния с маневром и без RatioBetweenDistanceSailedTowardMiddleAngleProjectionWithAndWithoutManeuver=Соотношение пройденного расстояния на срединную угловую проекцию с маневром и без DurationLostByManeuver=Потеря длительности за счет маневра -DurationLostByManeuverTowardMiddleAngleProjection=потеря длительности за счет маневра на срединную угловую проекцию +DurationLostByManeuverTowardMiddleAngleProjection=Потеря длительности за счет маневра на срединную угловую проекцию JibingCount=Поворотов фордевинд TackingCount=Поворотов оверштаг AbsTwaAtMaxTurningRate=Абс. TWA при макс. скорости поворота @@ -199,3 +199,4 @@ PercentageOfMainCurveProgressUntilHighestSpeed=Процент прохожден PercentageOfMainCurveProgressUntilMaxTurningRate=Процент прохождения основной кривой до макс. скорости поворота AbsTwaAtManeuverMiddle=Абс. TWA в середине маневра GpsSamplingRate=Частота дискретизации GPS +RaceDuration=Длительность гонки diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_zh.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_zh.properties index 3c6a3559da7..4156966e834 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_zh.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_zh.properties @@ -199,3 +199,4 @@ PercentageOfMainCurveProgressUntilHighestSpeed=最高速度前主要曲线进度 PercentageOfMainCurveProgressUntilMaxTurningRate=最大回转率速度前主要曲线进度的百分比 AbsTwaAtManeuverMiddle=操作中间的绝对实际风向角 GpsSamplingRate=GPS 采样率 +RaceDuration=比赛轮次持续时间 diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java index a5d8736439f..b20c3ac4b5a 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java @@ -17,7 +17,7 @@ public enum BoatClassMasterdata { _49ER ("49er", true, 4.88, 1.93, BoatHullType.MONOHULL, true), _49ERFX ("49erFX", true, 4.88, 1.93, BoatHullType.MONOHULL, true, "49FX"), _420 ("420", true, 4.20, 1.65, BoatHullType.MONOHULL, true, "420er", "420M", "420W"), - _470 ("470", true, 4.70, 1.68, BoatHullType.MONOHULL, true, "470er", "470M", "470W", "470 - M", "470 - W"), + _470 ("470", true, 4.70, 1.68, BoatHullType.MONOHULL, true, "470er", "470M", "470W", "470 - M", "470 - W", "470 Men", "470 Women"), _5O5 ("5O5", true, 5.03, 1.88, BoatHullType.MONOHULL, true, "505", "5o5", "505er", "5o5er"), _5_5MR ("5.5mR", true, 9.50, 1.92, BoatHullType.MONOHULL, true, "5.5 Meter", "5.5 Metre", "5.5", "5.5M", "5.5-metre", "5.5 metre"), _6MR ("6mR", true, 11, 2.2, BoatHullType.MONOHULL, true, "6 Meter", "6 Metre", "6", "6M", "6-metre", "6 metre"), diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/CollectionNames.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/CollectionNames.java index 19329ceb66f..626c339eb85 100755 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/CollectionNames.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/CollectionNames.java @@ -68,8 +68,6 @@ public enum CollectionNames { */ REGATTA_FOR_RACE_ID, - - /** * Legacy store for competitors before implementation of bug2822 * Contains the old competitors with contained boats. diff --git a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/PassAwareRaceLogAnalyzerTest.java b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/PassAwareRaceLogAnalyzerTest.java index b200b547f9d..74818b821de 100644 --- a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/PassAwareRaceLogAnalyzerTest.java +++ b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/PassAwareRaceLogAnalyzerTest.java @@ -67,10 +67,8 @@ public abstract class PassAwareRaceLogAnalyzerTest T createEvent(Class type, long milliseconds) { - return createEvent(/* priority */ 0, type, milliseconds); + final T result = createEvent(/* priority */ 0, type, milliseconds); + return result; } protected static T createEvent(int priority, Class type, long milliseconds) { diff --git a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/RaceStatusAnalyzerTest.java b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/RaceStatusAnalyzerTest.java index 288fa7f2f0f..b564f94ac9c 100644 --- a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/RaceStatusAnalyzerTest.java +++ b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/RaceStatusAnalyzerTest.java @@ -7,6 +7,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Arrays; +import java.util.Collections; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; @@ -18,21 +19,36 @@ import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEventVisitor; import com.sap.sailing.domain.abstractlog.race.RaceLogRaceStatusEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogStartProcedureChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceStatusAnalyzer; +import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceStatusAnalyzer.Clock; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogRaceStatusEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartProcedureChangedEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartTimeEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.SimpleRaceLogIdentifierImpl; +import com.sap.sailing.domain.abstractlog.race.state.ReadonlyRaceState; +import com.sap.sailing.domain.abstractlog.race.state.impl.ReadonlyRaceStateImpl; import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.RacingProcedure; +import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.impl.ReadonlyRacingProcedureFactory; +import com.sap.sailing.domain.base.configuration.impl.EmptyRegattaConfiguration; import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; +import com.sap.sailing.domain.common.racelog.RacingProcedureType; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util.Pair; import com.sap.sse.common.impl.MillisecondsTimePoint; -public class RaceStatusAnalyzerTest extends PassAwareRaceLogAnalyzerTest { +public class RaceStatusAnalyzerTest extends PassAwareRaceLogAnalyzerTest> { private RacingProcedure racingProcedure = mock(RacingProcedure.class); + private TimePoint now = MillisecondsTimePoint.now(); + private Clock constantTimeClock = ()->now; @Override protected RaceStatusAnalyzer createAnalyzer(RaceLog raceLog) { - return new RaceStatusAnalyzer(mock(RaceLogResolver.class), raceLog, racingProcedure); + return new RaceStatusAnalyzer(mock(RaceLogResolver.class), raceLog, constantTimeClock, racingProcedure); } @Override @@ -40,7 +56,7 @@ public class RaceStatusAnalyzerTest extends PassAwareRaceLogAnalyzerTest(event.getNextStatus(), now)); } @Override @@ -49,38 +65,46 @@ public class RaceStatusAnalyzerTest extends PassAwareRaceLogAnalyzerTest(event.getNextStatus(), now)); } @Test public void testUnscheduldedForNone() { RaceLogEvent event1 = createEvent(RaceLogEvent.class, 1); raceLog.add(event1); - assertEquals(RaceLogRaceStatus.UNSCHEDULED, analyzer.analyze()); + assertEquals(RaceLogRaceStatus.UNSCHEDULED, analyzer.analyze().getA()); } @Test public void testMostRecent() { - RaceLogRaceStatusEvent event1 = createEvent(RaceLogRaceStatusEvent.class, 1); - when(event1.getNextStatus()).thenReturn(RaceLogRaceStatus.FINISHING); - RaceLogRaceStatusEvent event2 = createEvent(RaceLogRaceStatusEvent.class, 2); - when(event2.getNextStatus()).thenReturn(RaceLogRaceStatus.FINISHED); - doAnswer(new StatusVisitorAnswer()).when(event2).accept(any(RaceLogEventVisitor.class)); - + LogEventAuthorImpl author = new LogEventAuthorImpl("author", 0); + RaceLogRaceStatusEvent event1 = new RaceLogRaceStatusEventImpl( + new MillisecondsTimePoint(1), author, /* passId */ 1, RaceLogRaceStatus.FINISHING); + RaceLogRaceStatusEvent event2 = new RaceLogRaceStatusEventImpl( + new MillisecondsTimePoint(2), author, /* passId */ 1, RaceLogRaceStatus.FINISHED); raceLog.add(event1); raceLog.add(event2); - - assertEquals(event2.getNextStatus(), analyzer.analyze()); + assertEquals(event2.getNextStatus(), analyzer.analyze().getA()); + } + + @Test + public void testFinishingWithFinishedInFuture() { + now = new MillisecondsTimePoint(2); + LogEventAuthorImpl author = new LogEventAuthorImpl("author", 0); + RaceLogRaceStatusEvent event1 = new RaceLogRaceStatusEventImpl( + new MillisecondsTimePoint(1), author, /* passId */ 1, RaceLogRaceStatus.FINISHING); + RaceLogRaceStatusEvent event2 = new RaceLogRaceStatusEventImpl( + new MillisecondsTimePoint(3), author, /* passId */ 1, RaceLogRaceStatus.FINISHED); + raceLog.add(event1); + raceLog.add(event2); + assertEquals(event1.getNextStatus(), analyzer.analyze().getA()); } @Test public void testStartphaseNotYetActive() { - when(racingProcedure.isStartphaseActive(any(TimePoint.class), any(TimePoint.class))).thenReturn(false); - RaceLogStartTimeEvent event = createStartTimeEvent(MillisecondsTimePoint.now().plus(20000).asMillis(), true); raceLog.add(event); - - assertEquals(RaceLogRaceStatus.SCHEDULED, analyzer.analyze()); + assertEquals(RaceLogRaceStatus.SCHEDULED, analyzer.analyze().getA()); } @Test @@ -90,7 +114,7 @@ public class RaceStatusAnalyzerTest extends PassAwareRaceLogAnalyzerTest { diff --git a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/state/test/RaceStateTest.java b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/state/test/RaceStateTest.java index a1d5062a69a..357ae765a9e 100644 --- a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/state/test/RaceStateTest.java +++ b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/state/test/RaceStateTest.java @@ -140,33 +140,27 @@ public class RaceStateTest { } @Test - public void testAdvancePass() { + public void testAdvancePass() throws InterruptedException { state.addChangedListener(listener); int oldPassId = raceLog.getCurrentPassId(); - state.setAdvancePass(mock(TimePoint.class)); - assertEquals(oldPassId + 1, raceLog.getCurrentPassId()); verify(listener).onAdvancePass(state); verifyNoMoreInteractions(listener); } @Test - public void testAbort() { + public void testAbort() throws InterruptedException { state.addChangedListener(listener); - state.setAborted(mock(TimePoint.class), false, Flags.NONE); - // TODO: change test when interface is complete verifyNoMoreInteractions(listener); } @Test - public void testGeneralRecall() { + public void testGeneralRecall() throws InterruptedException { state.addChangedListener(listener); - state.setGeneralRecall(mock(TimePoint.class)); - // TODO: change test when interface is complete verifyNoMoreInteractions(listener); } @@ -174,14 +168,11 @@ public class RaceStateTest { @Test public void testInvalidateAfterAdvancePass() throws InterruptedException { state.addChangedListener(listener); - state.forceNewStartTime(nowMock, new MillisecondsTimePoint(1)); Thread.sleep(100); state.setFinishedTime(new MillisecondsTimePoint(10)); state.setCourseDesign(nowMock, mock(CourseBase.class), CourseDesignerMode.ADMIN_CONSOLE); - state.setAdvancePass(mock(TimePoint.class)); - assertNull(state.getStartTime()); assertEquals(RaceLogRaceStatus.UNSCHEDULED, state.getStatus()); verify(listener, times(1)).onAdvancePass(state); diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/RaceStatusAnalyzer.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/RaceStatusAnalyzer.java index 5c7410afa56..bb5d74b37e9 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/RaceStatusAnalyzer.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/RaceStatusAnalyzer.java @@ -1,5 +1,9 @@ package com.sap.sailing.domain.abstractlog.race.analyzing.impl; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sailing.domain.abstractlog.race.RaceLogDependentStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; @@ -10,11 +14,19 @@ import com.sap.sailing.domain.abstractlog.race.impl.RaceLogRaceStatusEventCompar import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.ReadonlyRacingProcedure; import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util.Pair; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.util.impl.ArrayListNavigableSet; -public class RaceStatusAnalyzer extends RaceLogAnalyzer { - +/** + * Returns the status and the clock {@link TimePoint} at which it was calculated. That time point is obtained + * from the {@link Clock} passed to this analyzer at construction time. It is queried at the time the analysis + * takes place ({@link #performAnalysis()}. + * + * @author Axel Uhl (d043530) + * + */ +public class RaceStatusAnalyzer extends RaceLogAnalyzer> { public interface Clock { TimePoint now(); } @@ -24,26 +36,27 @@ public class RaceStatusAnalyzer extends RaceLogAnalyzer { public TimePoint now() { return MillisecondsTimePoint.now(); } - } - private final EventDispatcher eventDispatcher; - private RaceLogResolver resolver; + private final RaceLogResolver resolver; + private final Clock clock; + private final ReadonlyRacingProcedure racingProcedure; public RaceStatusAnalyzer(RaceLogResolver resolver, RaceLog raceLog, ReadonlyRacingProcedure racingProcedure) { - super(raceLog); - this.eventDispatcher = new EventDispatcher(new StandardClock(), racingProcedure); - this.resolver = resolver; + this(resolver, raceLog, new StandardClock(), racingProcedure); } public RaceStatusAnalyzer(RaceLogResolver resolver, RaceLog raceLog, Clock clock, ReadonlyRacingProcedure racingProcedure) { super(raceLog); - this.eventDispatcher = new EventDispatcher(clock, racingProcedure); this.resolver = resolver; + this.clock = clock; + this.racingProcedure = racingProcedure; } + private static final Set statusTypesToIgnoreIfEventNotValidYet = new HashSet<>(Arrays.asList( + new RaceLogRaceStatus[] { RaceLogRaceStatus.FINISHED, RaceLogRaceStatus.FINISHING })); @Override - protected RaceLogRaceStatus performAnalysis() { + protected Pair performAnalysis() { ArrayListNavigableSet statusEvents = new ArrayListNavigableSet<>( RaceLogRaceStatusEventComparator.INSTANCE); for (RaceLogEvent event : getPassEvents()) { @@ -51,21 +64,42 @@ public class RaceStatusAnalyzer extends RaceLogAnalyzer { statusEvents.add((RaceLogRaceStatusEvent) event); } } + final TimePoint now = clock.now(); + final EventDispatcher eventDispatcher = new EventDispatcher(now, racingProcedure); + Set statusesToIgnore = new HashSet<>(); + RaceLogRaceStatus result = RaceLogRaceStatus.UNSCHEDULED; for (RaceLogRaceStatusEvent event : statusEvents.descendingSet()) { - event.accept(eventDispatcher); - return eventDispatcher.nextStatus; + // We need to read across a not yet valid FINISHED event and record that other FINISHED + // events need to be ignored; if we find a not yet valid FINISHING event, ignore further FINISHING + // events; but dispatch all other events (particularly start-related events) to the dispatcher: + final boolean dispatch; + if (statusTypesToIgnoreIfEventNotValidYet.contains(event.getNextStatus())) { + if (event.getLogicalTimePoint().after(now)) { // event not valid yet + statusesToIgnore.add(event.getNextStatus()); // but ignore events of same status with lesser relevance + dispatch = false; + } else { + dispatch = true; + } + } else { + dispatch = true; + } + if (dispatch) { + event.accept(eventDispatcher); + result = eventDispatcher.nextStatus; + break; + } } - return RaceLogRaceStatus.UNSCHEDULED; + return new Pair<>(result, now); } private class EventDispatcher extends BaseRaceLogEventVisitor { - private final Clock clock; + private final TimePoint now; private final ReadonlyRacingProcedure racingProcedure; public RaceLogRaceStatus nextStatus; - public EventDispatcher(Clock clock, ReadonlyRacingProcedure racingProcedure) { - this.clock = clock; + public EventDispatcher(TimePoint now, ReadonlyRacingProcedure racingProcedure) { + this.now = now; this.racingProcedure = racingProcedure; this.nextStatus = RaceLogRaceStatus.UNKNOWN; } @@ -80,7 +114,6 @@ public class RaceStatusAnalyzer extends RaceLogAnalyzer { if (startTime == null) { nextStatus = RaceLogRaceStatus.PRESCHEDULED; } else { - TimePoint now = clock.now(); if (racingProcedure.isStartphaseActive(startTime, now)) { nextStatus = RaceLogRaceStatus.STARTPHASE; } else if (now.before(startTime)) { diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/RaceStateEventScheduler.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/RaceStateEventScheduler.java index 0129123d77b..3637f3cdb3d 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/RaceStateEventScheduler.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/RaceStateEventScheduler.java @@ -1,12 +1,10 @@ package com.sap.sailing.domain.abstractlog.race.state; -import java.util.Collection; - import com.sap.sailing.domain.abstractlog.race.state.impl.RaceStateEvents; public interface RaceStateEventScheduler { - void scheduleStateEvents(Collection stateEvents); + void scheduleStateEvents(Iterable stateEvents); void unscheduleStateEvent(RaceStateEvents stateEventName); diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/RaceStateImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/RaceStateImpl.java index 6f310aeda6c..7d09c0c9ad5 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/RaceStateImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/RaceStateImpl.java @@ -78,7 +78,7 @@ public class RaceStateImpl extends ReadonlyRaceStateImpl implements RaceState { private RaceStateImpl(RaceLogResolver raceLogResolver, RaceLog raceLog, AbstractLogEventAuthor author, RaceStatusAnalyzer.Clock analyzersClock, RacingProcedureFactory procedureFactory) { super(raceLogResolver, raceLog, /* forRaceLogIdentifier */ null, analyzersClock, procedureFactory, - Collections.emptyMap(), /* update */ true); + Collections.emptyMap()); this.author = author; } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java index 4904be72af4..accf15e0227 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java @@ -2,6 +2,8 @@ package com.sap.sailing.domain.abstractlog.race.state.impl; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import com.sap.sailing.domain.abstractlog.race.CompetitorResults; @@ -44,6 +46,8 @@ import com.sap.sailing.domain.common.racelog.RacingProcedureType; import com.sap.sse.common.TimePoint; import com.sap.sse.common.TimeRange; import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.impl.TimeRangeImpl; import com.sap.sse.util.WeakIdentityHashMap; /** @@ -67,6 +71,13 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL * See also bug4704. */ public static ReadonlyRaceState getOrCreate(RaceLogResolver raceLogResolver, RaceLog raceLog) { + return getOrCreate(raceLogResolver, raceLog, /* forRaceLogIdentifier */null, + Collections. emptyMap()); + } + + public static ReadonlyRaceState getOrCreate(RaceLogResolver raceLogResolver, RaceLog raceLog, + SimpleRaceLogIdentifier forRaceLogIdentifier, + Map dependentRaceStates) { WeakIdentityHashMap raceStatesForRaceLog; ReadonlyRaceState result = null; synchronized (raceStateCache) { @@ -78,7 +89,7 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL result = raceStatesForRaceLog.get(raceLogResolver); } if (result == null) { - result = createInternal(raceLogResolver, raceLog); + result = createInternal(raceLogResolver, raceLog, forRaceLogIdentifier, dependentRaceStates); raceStatesForRaceLog.put(raceLogResolver, result); } } @@ -86,9 +97,10 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL } - private static final ReadonlyRaceState createInternal(RaceLogResolver raceLogResolver, RaceLog raceLog) { - return create(raceLogResolver, raceLog, /* forRaceLogIdentifier */null, - Collections. emptyMap()); + private static final ReadonlyRaceState createInternal(RaceLogResolver raceLogResolver, RaceLog raceLog, + SimpleRaceLogIdentifier forRaceLogIdentifier, + Map dependentRaceStates) { + return create(raceLogResolver, raceLog, forRaceLogIdentifier, dependentRaceStates); } /** @@ -139,7 +151,8 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL private RaceStateEventScheduler scheduler; /** - * We need one for each change in {@link RacingProcedureType}. + * We need one for each change in {@link RacingProcedureType}. The analyzer uses the {@link #statusAnalyzerClock} + * as its time source. */ private RaceStatusAnalyzer statusAnalyzer; private final RacingProcedureTypeAnalyzer racingProcedureAnalyzer; @@ -168,6 +181,22 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL private RacingProcedureType cachedRacingProcedureTypeNoFallback; private RaceLogRaceStatus cachedRaceStatus; + + /** + * The time taken from {@link #statusAnalyzerClock} when the race status was last updated. Set to a valid + * value whenever {@link #cachedRaceStatus} is set. + */ + private TimePoint clockTimePointWhenCachedRaceStatusWasSet; + + /** + * Time points determined during {@link #updateCachedRaceStatusAndValidity(RaceLogRaceStatus)} at which + * the race status may change. If the time range between {@link #clockTimePointWhenCachedRaceStatusWasSet} and + * the current {@link #statusAnalyzerClock clock} time point spans any of these, the {@link #cachedRaceStatus} + * is to be considered invalid and needs to be re-calculated. Set to a valid but possibly empty iterable + * whenever {@link #cachedRaceStatus} is set. + */ + private Iterable timePointsWhenRaceStatusMayChange; + private int cachedPassId; private StartTimeFinderResult cachedStartTimeFinderResult; private TimePoint cachedFinishingTime; @@ -187,7 +216,7 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL SimpleRaceLogIdentifier forRaceLogIdentifier, RacingProcedureFactory procedureFactory, Map dependentRaceStates) { this(raceLogResolver, raceLog, forRaceLogIdentifier, new RaceStatusAnalyzer.StandardClock(), - procedureFactory, dependentRaceStates, /* update */true); + procedureFactory, dependentRaceStates); } /** @@ -196,14 +225,11 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL * new race state. This can help avoid endless recursions because this race state can be passed along * together with the identifier so that when again along a dependent call chain a race state is required * for that race log identifier, this one can be used. - * @param update - * if true, the race state will be updated whenever the underlying raceLog - * changes. */ protected ReadonlyRaceStateImpl(RaceLogResolver raceLogResolver, RaceLog raceLog, SimpleRaceLogIdentifier forRaceLogIdentifier, Clock analyzersClock, RacingProcedureFactory procedureFactory, - final Map dependentRaceStates, boolean update) { + final Map dependentRaceStates) { this.raceLog = raceLog; this.raceLogResolver = raceLogResolver; this.procedureFactory = procedureFactory; @@ -325,9 +351,30 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL @Override public RaceLogRaceStatus getStatus() { + final TimePoint timePointForAnalysis = statusAnalyzerClock.now(); + final TimeRange timeRangeSinceLastStatusUpdate = new TimeRangeImpl(clockTimePointWhenCachedRaceStatusWasSet, timePointForAnalysis); + if (overlapsTimePointForPotentialStatusChange(timeRangeSinceLastStatusUpdate)) { + analyzeAndUpdateCachedRaceStatus(); + } return cachedRaceStatus; } + /** + * Finds out whether any of the time points in {@link #timePointsWhenRaceStatusMayChange} falls into the time range + * provided. In this case we assume that a race status update may have happened between the time point when the + * {@link #cachedRaceStatus} was updated last and the current request time point, forming the time range provided + * as parameter here. + */ + private boolean overlapsTimePointForPotentialStatusChange(TimeRange timeRangeSinceLastStatusUpdate) { + for (final TimePoint e : timePointsWhenRaceStatusMayChange) { + if (timeRangeSinceLastStatusUpdate.includes(e)) { + return true; + } + } + return false; + } + + @Override public TimePoint getStartTime() { return cachedStartTimeFinderResult.getStartTime(); @@ -434,7 +481,7 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL } else { RaceLog resolvedRaceLog = raceLogResolver.resolve(dependentOnRaceIdentifier); if (resolvedRaceLog != null) { - raceStateToObserve = ReadonlyRaceStateImpl.create(raceLogResolver, resolvedRaceLog, + raceStateToObserve = ReadonlyRaceStateImpl.getOrCreate(raceLogResolver, resolvedRaceLog, dependentOnRaceIdentifier, dependentRaceStates); raceStateToObserve.addChangedListener(raceStateToObserveListener); } @@ -463,13 +510,6 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL recreateRacingProcedure(dependentRaceStates); changedListeners.onRacingProcedureChanged(this); } - - RaceLogRaceStatus status = statusAnalyzer.analyze(); - if (!Util.equalsWithNull(cachedRaceStatus, status)) { - cachedRaceStatus = status; - changedListeners.onStatusChanged(this); - } - int passId = raceLog.getCurrentPassId(); if (cachedPassId != passId) { cachedPassId = passId; @@ -478,38 +518,33 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL cachedRacingProcedureType = null; cachedRacingProcedureTypeNoFallback = null; } - StartTimeFinderResult startTimeFinderResult = startTimeAnalyzer.analyze(); if (!Util.equalsWithNull(cachedStartTimeFinderResult, startTimeFinderResult)) { cachedStartTimeFinderResult = startTimeFinderResult; changedListeners.onStartTimeChanged(this); } adjustObserverForRelativeStartTime(startTimeFinderResult, dependentRaceStates); - TimePoint finishingTime = finishingTimeAnalyzer.analyze(); if (!Util.equalsWithNull(cachedFinishingTime, finishingTime)) { cachedFinishingTime = finishingTime; changedListeners.onFinishingTimeChanged(this); } - TimePoint finishedTime = finishedTimeAnalyzer.analyze(); if (!Util.equalsWithNull(cachedFinishedTime, finishedTime)) { cachedFinishedTime = finishedTime; changedListeners.onFinishedTimeChanged(this); } - + analyzeAndUpdateCachedRaceStatus(); // do this after start and finishing times have been updated TimeRange protest = protestTimeAnalyzer.analyze(); if (!Util.equalsWithNull(cachedProtest, protest)) { cachedProtest = protest; changedListeners.onProtestTimeChanged(this); } - CompetitorResults positionedCompetitors = finishPositioningListAnalyzer.analyze(); if (!Util.equalsWithNull(cachedPositionedCompetitors, positionedCompetitors)) { cachedPositionedCompetitors = positionedCompetitors; changedListeners.onFinishingPositioningsChanged(this); } - CompetitorResults confirmedPositionedCompetitors = confirmedFinishPositioningListAnalyzer.analyze(); if (!Util.equalsWithNull(cachedConfirmedPositionedCompetitors, confirmedPositionedCompetitors)) { cachedConfirmedPositionedCompetitors = confirmedPositionedCompetitors; @@ -517,13 +552,11 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL changedListeners.onFinishingPositionsConfirmed(this); } } - CourseBase courseDesign = courseDesignerAnalyzer.analyze(); if (!Util.equalsWithNull(cachedCourseDesign, courseDesign)) { cachedCourseDesign = courseDesign; changedListeners.onCourseDesignChanged(this); } - Wind windFix = lastWindFixAnalyzer.analyze(); if (!Util.equalsWithNull(cachedWindFix, windFix)) { cachedWindFix = windFix; @@ -531,6 +564,46 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL } } + /** + * This method has protected visibility (and not private) to allow instrumenting by test cases + */ + protected void analyzeAndUpdateCachedRaceStatus() { + Pair statusAndTimePoint = statusAnalyzer.analyze(); + if (!Util.equalsWithNull(cachedRaceStatus, statusAndTimePoint)) { + updateCachedRaceStatusAndValidity(statusAndTimePoint); + } + } + + /** + * Updates the {@link #cachedRaceStatus} and calculates and remembers its validity, based on + * {@link ReadonlyRacingProcedure#createStartStateEvents(TimePoint)} and which of those have already + * passed at the current clock time and which ones are still lying ahead. + */ + private void updateCachedRaceStatusAndValidity(Pair statusAndTimePoint) { + final RaceLogRaceStatus oldRaceStatus = cachedRaceStatus; + cachedRaceStatus = statusAndTimePoint.getA(); + clockTimePointWhenCachedRaceStatusWasSet = statusAndTimePoint.getB(); + if (getStartTimeFinderResult() == null || getStartTime() == null) { + timePointsWhenRaceStatusMayChange = Collections.emptyList(); + } else { + final TimePoint startTime = getStartTime(); + List newTimePointsWhenRaceStatusMayChange = new LinkedList<>(); + for (final RaceStateEvent e : racingProcedure.createStartStateEvents(startTime)) { + newTimePointsWhenRaceStatusMayChange.add(e.getTimePoint()); + } + if (getFinishingTime() != null) { + newTimePointsWhenRaceStatusMayChange.add(getFinishingTime()); + } + if (getFinishedTime() != null) { + newTimePointsWhenRaceStatusMayChange.add(getFinishedTime()); + } + timePointsWhenRaceStatusMayChange = newTimePointsWhenRaceStatusMayChange; + } + if (!Util.equalsWithNull(oldRaceStatus, cachedRaceStatus)) { + changedListeners.onStatusChanged(this); + } + } + private void recreateRacingProcedure(Map dependentRaceStates) { if (racingProcedure != null) { removeChangedListener(racingProcedure); @@ -539,10 +612,8 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL racingProcedure = procedureFactory.createRacingProcedure(cachedRacingProcedureType, raceLog, raceLogResolver); racingProcedure.setStateEventScheduler(scheduler); addChangedListener(racingProcedure); - statusAnalyzer = new RaceStatusAnalyzer(raceLogResolver, raceLog, statusAnalyzerClock, racingProcedure); // let's do an update because status might have changed with new procedure update(dependentRaceStates); } - } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ReadonlyRacingProcedure.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ReadonlyRacingProcedure.java index 92613aa02d7..323f21ea714 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ReadonlyRacingProcedure.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ReadonlyRacingProcedure.java @@ -4,6 +4,7 @@ import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogAnalyzer; import com.sap.sailing.domain.abstractlog.race.state.RaceState; import com.sap.sailing.domain.abstractlog.race.state.RaceStateChangedListener; +import com.sap.sailing.domain.abstractlog.race.state.RaceStateEvent; import com.sap.sailing.domain.abstractlog.race.state.RaceStateEventProcessor; import com.sap.sailing.domain.abstractlog.race.state.RaceStateEventScheduler; import com.sap.sailing.domain.abstractlog.race.state.ReadonlyRaceState; @@ -122,6 +123,13 @@ public interface ReadonlyRacingProcedure extends RaceStateEventProcessor, RaceSt RacingProcedurePrerequisite checkPrerequisitesForStart(TimePoint now, TimePoint startTime, RacingProcedurePrerequisite.FulfillmentFunction function); + + /** + * Delivers the time points and types of the events around the start, each optionally + * leading to a change in race status. + */ + Iterable createStartStateEvents(TimePoint startTime); + /** * This method is called by the framework. You don't need to call it. */ diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/basic/impl/BasicRacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/basic/impl/BasicRacingProcedureImpl.java index ffea26c734a..da6f0efee9d 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/basic/impl/BasicRacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/basic/impl/BasicRacingProcedureImpl.java @@ -1,7 +1,6 @@ package com.sap.sailing.domain.abstractlog.race.state.racingprocedure.basic.impl; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; @@ -66,7 +65,7 @@ public class BasicRacingProcedureImpl extends BaseRacingProcedure { } @Override - protected Collection createStartStateEvents(TimePoint startTime) { + public Iterable createStartStateEvents(TimePoint startTime) { return Arrays.asList(new RaceStateEventImpl(startTime, RaceStateEvents.START)); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ess/impl/ESSRacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ess/impl/ESSRacingProcedureImpl.java index ea009a6554f..7be1ec4160b 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ess/impl/ESSRacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ess/impl/ESSRacingProcedureImpl.java @@ -1,7 +1,6 @@ package com.sap.sailing.domain.abstractlog.race.state.racingprocedure.ess.impl; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; @@ -64,7 +63,7 @@ public class ESSRacingProcedureImpl extends BaseRacingProcedure implements ESSRa } @Override - protected Collection createStartStateEvents(TimePoint startTime) { + public Iterable createStartStateEvents(TimePoint startTime) { return Arrays. asList(new RaceStateEventImpl(startTime.minus(startPhaseAPDownInterval), RaceStateEvents.ESS_AP_DOWN), new RaceStateEventImpl(startTime.minus(startPhaseESSThreeUpInterval), RaceStateEvents.ESS_THREE_UP), new RaceStateEventImpl(startTime.minus(startPhaseESSTwoUpInterval), diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/gate/impl/GateStartRacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/gate/impl/GateStartRacingProcedureImpl.java index fddb5aad31a..5daae5a52ec 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/gate/impl/GateStartRacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/gate/impl/GateStartRacingProcedureImpl.java @@ -1,7 +1,6 @@ package com.sap.sailing.domain.abstractlog.race.state.racingprocedure.gate.impl; import java.util.Arrays; -import java.util.Collection; import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; import com.sap.sailing.domain.abstractlog.race.RaceLog; @@ -119,7 +118,7 @@ public class GateStartRacingProcedureImpl extends BaseRacingProcedure implements } @Override - protected Collection createStartStateEvents(TimePoint startTime) { + public Iterable createStartStateEvents(TimePoint startTime) { return Arrays. asList( new RaceStateEventImpl(startTime.minus(startPhaseClassOverGolfUpInterval), RaceStateEvents.GATE_CLASS_OVER_GOLF_UP), diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/impl/BaseRacingProcedure.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/impl/BaseRacingProcedure.java index 1bccfd7b31b..017d0065bf8 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/impl/BaseRacingProcedure.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/impl/BaseRacingProcedure.java @@ -276,8 +276,6 @@ public abstract class BaseRacingProcedure extends BaseRaceStateChangedListener i return configuration; } - protected abstract Collection createStartStateEvents(TimePoint startTime); - @Override public void onStartTimeChanged(ReadonlyRaceState state) { getChangedListeners().onActiveFlagsChanged(this); diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/league/impl/LeagueRacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/league/impl/LeagueRacingProcedureImpl.java index a7098a8d0e5..7dab122f8fb 100755 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/league/impl/LeagueRacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/league/impl/LeagueRacingProcedureImpl.java @@ -1,7 +1,6 @@ package com.sap.sailing.domain.abstractlog.race.state.racingprocedure.league.impl; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; @@ -81,7 +80,7 @@ public class LeagueRacingProcedureImpl extends BaseRacingProcedure implements Le } @Override - protected Collection createStartStateEvents(TimePoint startTime) { + public Iterable createStartStateEvents(TimePoint startTime) { // Remark: We are reusing here CLASS_UP, STARTMODE_UP and STARTMODE_DOWN from the RRS26 procedure // The question is why there are no generic CLASS_UP, STARTMODE_UP and STARTMODE_DOWN events return Arrays. asList( diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/ConfigurableStartModeFlagRacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/ConfigurableStartModeFlagRacingProcedureImpl.java index 1e85749df58..684fe233401 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/ConfigurableStartModeFlagRacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/ConfigurableStartModeFlagRacingProcedureImpl.java @@ -28,17 +28,14 @@ public abstract class ConfigurableStartModeFlagRacingProcedureImpl extends BaseR public ConfigurableStartModeFlagRacingProcedureImpl(RaceLog raceLog, AbstractLogEventAuthor author, ConfigurableStartModeFlagRacingProcedureConfiguration configuration, RaceLogResolver raceLogResolver) { super(raceLog, author, configuration, raceLogResolver); - RacingProcedureTypeAnalyzer procedureAnalyzer = new RacingProcedureTypeAnalyzer(raceLog); if (configuration.getStartModeFlags() != null) { this.startModeFlagAnalyzer = new StartModeFlagFinder(procedureAnalyzer, raceLog, configuration.getStartModeFlags()); } else { this.startModeFlagAnalyzer = new StartModeFlagFinder(procedureAnalyzer, raceLog, getDefaultStartModeFlags()); } - this.cachedStartmodeFlag = getDefaultStartMode(); this.startmodeFlagHasBeenSet = false; - update(); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/RRS26RacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/RRS26RacingProcedureImpl.java index 198b00ecc63..bb728fda311 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/RRS26RacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/RRS26RacingProcedureImpl.java @@ -1,7 +1,6 @@ package com.sap.sailing.domain.abstractlog.race.state.racingprocedure.line.impl; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.List; @@ -64,7 +63,7 @@ public class RRS26RacingProcedureImpl extends ConfigurableStartModeFlagRacingPro } @Override - protected Collection createStartStateEvents(TimePoint startTime) { + public Iterable createStartStateEvents(TimePoint startTime) { return Arrays. asList( new RaceStateEventImpl(startTime.minus(startPhaseClassUpInterval), RaceStateEvents.RRS26_CLASS_UP), new RaceStateEventImpl(startTime.minus(startPhaseStartModeUpInterval), RaceStateEvents.RRS26_STARTMODE_UP), diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/SWCRacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/SWCRacingProcedureImpl.java index 2a72bcddb68..57f5428d08b 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/SWCRacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/SWCRacingProcedureImpl.java @@ -1,7 +1,6 @@ package com.sap.sailing.domain.abstractlog.race.state.racingprocedure.line.impl; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.List; @@ -69,7 +68,7 @@ public class SWCRacingProcedureImpl extends ConfigurableStartModeFlagRacingProce } @Override - protected Collection createStartStateEvents(TimePoint startTime) { + public Iterable createStartStateEvents(TimePoint startTime) { return Arrays. asList( new RaceStateEventImpl(startTime.minus(CLASS_AND_STARTMODE_UP_INTERVAL), RaceStateEvents.SWC_CLASS_AND_STARTMODE_UP), new RaceStateEventImpl(startTime.minus(FIVE_MINUTES_FLAG_UP_INTERVAL), RaceStateEvents.SWC_FIVE_UP), diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingFactory.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingFactory.java index 33ea647eaa3..2dbbd85bff9 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingFactory.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingFactory.java @@ -1,6 +1,7 @@ package com.sap.sailing.domain.swisstimingadapter; import java.io.IOException; +import java.net.URISyntaxException; import java.net.UnknownHostException; import java.text.ParseException; @@ -53,11 +54,11 @@ public interface SwissTimingFactory { SwissTimingRaceTracker createRaceTracker(RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, DomainFactory domainFactory, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, SwissTimingTrackingConnectivityParameters connectivityParams) - throws InterruptedException, UnknownHostException, IOException, ParseException; + throws InterruptedException, UnknownHostException, IOException, ParseException, URISyntaxException; RaceTracker createRaceTracker(Regatta regatta, WindStore windStore, DomainFactory domainFactory, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, SwissTimingTrackingConnectivityParameters connectivityParams) - throws UnknownHostException, InterruptedException, IOException, ParseException; + throws UnknownHostException, InterruptedException, IOException, ParseException, URISyntaxException; Race createRace(String raceId, String raceName, String description, BoatClass boatClass); diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java index 70236e46404..278bab7895d 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java @@ -1,6 +1,7 @@ package com.sap.sailing.domain.swisstimingadapter.impl; import java.io.IOException; +import java.net.URISyntaxException; import java.net.UnknownHostException; import java.text.ParseException; import java.util.HashMap; @@ -91,7 +92,7 @@ public class SwissTimingFactoryImpl implements SwissTimingFactory { public SwissTimingRaceTracker createRaceTracker(RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, DomainFactory domainFactory, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, SwissTimingTrackingConnectivityParameters connectivityParams) - throws InterruptedException, UnknownHostException, IOException, ParseException { + throws InterruptedException, UnknownHostException, IOException, ParseException, URISyntaxException { return new SwissTimingRaceTrackerImpl(raceLogStore, regattaLogStore, windStore, domainFactory, this, trackedRegattaRegistry, raceLogResolver, connectivityParams); } @@ -100,7 +101,7 @@ public class SwissTimingFactoryImpl implements SwissTimingFactory { public RaceTracker createRaceTracker(Regatta regatta, WindStore windStore, DomainFactory domainFactory, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, SwissTimingTrackingConnectivityParameters connectivityParams) - throws UnknownHostException, InterruptedException, IOException, ParseException { + throws UnknownHostException, InterruptedException, IOException, ParseException, URISyntaxException { return new SwissTimingRaceTrackerImpl(regatta, windStore, domainFactory, this, trackedRegattaRegistry, raceLogStore, regattaLogStore, raceLogResolver, connectivityParams); } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java index 8b960a35e78..dad734cb2a5 100644 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java @@ -128,7 +128,7 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl WindStore windStore, DomainFactory domainFactory, SwissTimingFactory factory, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, SwissTimingTrackingConnectivityParameters connectivityParams) - throws InterruptedException, UnknownHostException, IOException, ParseException { + throws InterruptedException, UnknownHostException, IOException, ParseException, URISyntaxException { this(/* regatta */ null, windStore, domainFactory, factory, trackedRegattaRegistry, raceLogStore, regattaLogStore, raceLogResolver, connectivityParams); } @@ -136,7 +136,7 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl SwissTimingFactory factory, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, RaceLogResolver raceLogResolver, SwissTimingTrackingConnectivityParameters connectivityParams) - throws InterruptedException, UnknownHostException, IOException, ParseException { + throws InterruptedException, UnknownHostException, IOException, ParseException, URISyntaxException { super(connectivityParams); this.raceLogResolver = raceLogResolver; this.tmdMessageQueue = new TMDMessageQueue(this); @@ -155,6 +155,8 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl connectivityParams.getRaceID(), connectivityParams.getRaceName(), connectivityParams.getRaceDescription(), connectivityParams.getBoatClass()); this.domainFactory = domainFactory; this.raceID = connectivityParams.getRaceID(); + // start out with an empty course, so we don't depend on receiving the CCG message before the timeout + this.course = new CourseImpl(this.raceID, /* start with empty marks list */ Collections.emptyList()); this.raceName = connectivityParams.getRaceName(); this.startList = connectivityParams.getStartList(); this.startListFromManage2Sail = connectivityParams.getStartList() != null; @@ -170,6 +172,9 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl this.updateURL = connectivityParams.getUpdateURL(); this.updateUsername = connectivityParams.getUpdateUsername(); this.updatePassword = connectivityParams.getUpdatePassword(); + if (connectivityParams.getStartList() != null) { + createRaceDefinition(course); + } } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/places/screens/liveraceloop/racemapwithleaderboard/LiveRaceWithRacemapAndLeaderBoardPresenterImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/places/screens/liveraceloop/racemapwithleaderboard/LiveRaceWithRacemapAndLeaderBoardPresenterImpl.java index 3debae7a209..2177bfcbf95 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/places/screens/liveraceloop/racemapwithleaderboard/LiveRaceWithRacemapAndLeaderBoardPresenterImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/places/screens/liveraceloop/racemapwithleaderboard/LiveRaceWithRacemapAndLeaderBoardPresenterImpl.java @@ -12,16 +12,20 @@ import com.google.gwt.i18n.client.NumberFormat; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.AcceptsOneWidget; import com.sap.sailing.domain.common.DetailType; +import com.sap.sailing.domain.common.RaceIdentifier; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.domain.common.dto.LeaderboardDTO; import com.sap.sailing.domain.common.dto.LeaderboardRowDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; import com.sap.sailing.gwt.autoplay.client.app.AnimationPanel; import com.sap.sailing.gwt.autoplay.client.app.AutoPlayClientFactory; import com.sap.sailing.gwt.autoplay.client.app.AutoPlayPresenterConfigured; import com.sap.sailing.gwt.autoplay.client.utils.AutoplayHelper; import com.sap.sailing.gwt.settings.client.leaderboard.SingleRaceLeaderboardSettings; import com.sap.sailing.gwt.ui.client.FlagImageResolverImpl; +import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceCompetitorSet; @@ -152,12 +156,7 @@ public class LiveRaceWithRacemapAndLeaderBoardPresenterImpl view.showErrorNoLive(this, panel, new IllegalStateException("No race is live")); return; } - final SingleRaceLeaderboardSettings leaderboardSettings = new SingleRaceLeaderboardSettings( - /* maneuverDetailsToShow */ null, /* legDetailsToShow */ null, /* raceDetailsToShow */ null, - /* overallDetailsToShow */ null, /* delayBetweenAutoAdvancesInMilliseconds */ null, - /* showAddedScores */ false, /* showCompetitorShortNameColumn */ true, - /* showCompetitorFullNameColumn */ false, /* isCompetitorNationalityColumnVisible */ false, - /* showCompetitorBoatInfoColumn */ false, /* showRaceRankColumn */ true); + final SingleRaceLeaderboardSettings leaderboardSettings = getLeaderboardSettings(false); timer = new com.sap.sse.gwt.client.player.Timer( // perform the first request as "live" but don't by default auto-play PlayModes.Live, PlayStates.Playing, @@ -167,12 +166,42 @@ public class LiveRaceWithRacemapAndLeaderBoardPresenterImpl getSlideCtx().getContextDefinition().getLeaderboardName(), errorReporter, StringMessages.INSTANCE, false, null, false, null, false, true, false, false, false, new SixtyInchLeaderboardStyle(true), FlagImageResolverImpl.get(), Arrays.asList(DetailType.values())); + + leaderboardPanel.addLeaderboardUpdateListener(new LeaderboardUpdateListener() { + + @Override + public void updatedLeaderboard(LeaderboardDTO leaderboard) { + // if this is a changing boat race, enable the boat column + if (leaderboardPanel.getLeaderboard() != null) { + LeaderboardDTO leaderboardDTO = leaderboardPanel.getLeaderboard(); + boolean boatsRequired = leaderboardDTO.canBoatsOfCompetitorsChangePerRace; + if (leaderboardPanel.getSettings().isShowCompetitorBoatInfoColumn() != boatsRequired) { + leaderboardPanel.updateSettings(getLeaderboardSettings(boatsRequired)); + } + } + } + + @Override + public void currentRaceSelected(RaceIdentifier raceIdentifier, RaceColumnDTO raceColumn) { + } + }); + getPlace().getRaceMap().setQuickRanksDTOProvider(new QuickRanksDTOFromLeaderboardDTOProvider( new RaceCompetitorSet(getPlace().getRaceMapSelectionProvider()), liveRace)); view.startingWith(this, panel, getPlace().getRaceMap(), leaderboardPanel); selectionTimer.schedule(SWITCH_COMPETITOR_DELAY + AnimationPanel.ANIMATION_DURATION + AnimationPanel.DELAY); } + private SingleRaceLeaderboardSettings getLeaderboardSettings(boolean showBoatColumn) { + final SingleRaceLeaderboardSettings leaderboardSettings = new SingleRaceLeaderboardSettings( + /* maneuverDetailsToShow */ null, /* legDetailsToShow */ null, /* raceDetailsToShow */ null, + /* overallDetailsToShow */ null, /* delayBetweenAutoAdvancesInMilliseconds */ null, + /* showAddedScores */ false, /* showCompetitorShortNameColumn */ true, + /* showCompetitorFullNameColumn */ false, showBoatColumn, + /* showCompetitorBoatInfoColumn */ false, /* showRaceRankColumn */ true); + return leaderboardSettings; + } + @Override public void onStop() { if (timer != null) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListSteps.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListSteps.java index aba27539ffb..30d3ba92244 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListSteps.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListSteps.java @@ -48,10 +48,11 @@ public class MultiRegattaListSteps extends Composite { double parts = Math.pow(seriesProgress.getMaxRacesPerFleet(), 0.4); sumParts += parts; } + final boolean showSeriesName = regattaProgress.getSeries().size() > 1; for(RegattaProgressSeriesDTO seriesProgress : regattaProgress.getSeries()) { double parts = Math.pow(seriesProgress.getMaxRacesPerFleet(), 0.4); double percentage = 100.0 * parts / sumParts; - MultiRegattaListStepsBody stepsBody = new MultiRegattaListStepsBody(seriesProgress); + MultiRegattaListStepsBody stepsBody = new MultiRegattaListStepsBody(seriesProgress, showSeriesName); allMultiregattaSteps.add(stepsBody); stepsBody.getElement().getStyle().setWidth(percentage, Unit.PCT); stepsContainerUi.appendChild(stepsBody.getElement()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListStepsBody.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListStepsBody.java index f1ce515f0bb..a94a09658ef 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListStepsBody.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListStepsBody.java @@ -1,7 +1,5 @@ package com.sap.sailing.gwt.home.desktop.partials.multiregattalist; -import static com.sap.sailing.domain.common.LeaderboardNameConstants.DEFAULT_SERIES_NAME; - import java.util.Map; import java.util.Map.Entry; @@ -42,17 +40,17 @@ public class MultiRegattaListStepsBody extends UIObject implements RequiresResiz private final String seriesName, seriesNameMedium, seriesNameShort; private int seriesNameLength, seriesNameMediumLength; - public MultiRegattaListStepsBody(RegattaProgressSeriesDTO seriesProgress) { + MultiRegattaListStepsBody(RegattaProgressSeriesDTO seriesProgress, boolean showName) { setElement(uiBinder.createAndBindUi(this)); textContainerUi.getStyle().setVisibility(Visibility.HIDDEN); - longNameDummyUi.setInnerText(seriesName = caculateSeriesName(seriesProgress)); + longNameDummyUi.setInnerText(seriesName = caculateSeriesName(seriesProgress, showName)); mediumNameDummyUi.setInnerText(seriesNameMedium = caculateSeriesNameMedium()); seriesNameShort = caculateSeriesNameShort(); if (seriesProgress.isCompleted()) { - progressUi.setInnerText(String.valueOf(seriesProgress.getTotalRaceCount())); + progressUi.setInnerText(I18N.racesCount(seriesProgress.getTotalRaceCount())); } else { checkUi.getStyle().setDisplay(Display.NONE); - progressUi.setInnerText(I18N.currentOfTotal( + progressUi.setInnerText(I18N.currentOfTotalRaces( seriesProgress.getProgressRaceCount(), seriesProgress.getTotalRaceCount())); } addFleetProgresses(seriesProgress.getFleetState(), seriesProgress.getMaxRacesPerFleet()); @@ -93,9 +91,9 @@ public class MultiRegattaListStepsBody extends UIObject implements RequiresResiz } } - private String caculateSeriesName(RegattaProgressSeriesDTO seriesProgress) { - if (seriesProgress.getName() == null || seriesProgress.getName().isEmpty() - || DEFAULT_SERIES_NAME.equals(seriesProgress.getName())) { + private String caculateSeriesName(RegattaProgressSeriesDTO seriesProgress, boolean showName) { + if (!showName || seriesProgress.getName() == null || seriesProgress.getName().isEmpty()) { + nameUi.getStyle().setDisplay(Display.NONE); return I18N.races(); } else { return seriesProgress.getName(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/racestab/RegattaRacesPlace.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/racestab/RegattaRacesPlace.java index 574760b72cb..4ae3f62a362 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/racestab/RegattaRacesPlace.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/racestab/RegattaRacesPlace.java @@ -1,5 +1,7 @@ package com.sap.sailing.gwt.home.desktop.places.event.regatta.racestab; +import java.util.Optional; + import com.google.gwt.place.shared.Prefix; import com.sap.sailing.gwt.home.desktop.places.event.regatta.AbstractEventRegattaPlace; import com.sap.sailing.gwt.home.shared.app.HasMobileVersion; @@ -8,24 +10,37 @@ import com.sap.sailing.gwt.home.shared.places.event.AbstractEventPlace; import com.sap.sailing.gwt.home.shared.places.event.EventContext; public class RegattaRacesPlace extends AbstractEventRegattaPlace implements HasMobileVersion { + + private final String preferredSeriesName; + public RegattaRacesPlace(String id, String leaderboardName) { super(id, leaderboardName); + this.preferredSeriesName = null; } public RegattaRacesPlace(EventContext context) { + this(context, null); + } + + public RegattaRacesPlace(EventContext context, String preferredSeriesName) { super(context); + this.preferredSeriesName = preferredSeriesName; + } + + public final Optional getPreferredSeriesName() { + return Optional.ofNullable(preferredSeriesName); } @Override public AbstractEventRegattaPlace newInstanceWithContext(EventContext ctx) { - return new RegattaRacesPlace(ctx); + return new RegattaRacesPlace(ctx, null); } @Prefix(PlaceTokenPrefixes.RegattaRaces) public static class Tokenizer extends AbstractEventPlace.Tokenizer { @Override protected RegattaRacesPlace getRealPlace(EventContext context) { - return new RegattaRacesPlace(context); + return new RegattaRacesPlace(context, null); } } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/app/MobileActivityMapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/app/MobileActivityMapper.java index 7a31ba34000..5bac0e34c98 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/app/MobileActivityMapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/app/MobileActivityMapper.java @@ -34,6 +34,7 @@ import com.sap.sailing.gwt.home.shared.places.user.confirmation.ConfirmationPlac import com.sap.sailing.gwt.home.shared.places.user.passwordreset.PasswordResetActivityProxy; import com.sap.sailing.gwt.home.shared.places.user.passwordreset.PasswordResetPlace; import com.sap.sailing.gwt.home.shared.places.user.profile.AbstractUserProfilePlace; +import com.sap.sse.gwt.client.DOMUtils; public class MobileActivityMapper implements ActivityMapper { private final MobileApplicationClientFactory clientFactory; @@ -52,6 +53,7 @@ public class MobileActivityMapper implements ActivityMapper { SwitchingEntryPoint.reloadApp(); return null; } + DOMUtils.scrollToTop(clientFactory.getRoot()); if (place instanceof ErrorPlace) { return new ErrorActivityProxy((ErrorPlace) place, clientFactory); } else if (place instanceof StartPlace) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.gss b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.gss index 282125f546d..e3911716fb0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.gss +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.gss @@ -21,6 +21,9 @@ .eventsteps_phases_phaseinactive { color: #999; } +.eventsteps_phases_phase_link { + color: #000; +} /* .eventsteps_phases_phase:nth-child(2n-1) { */ /* padding-right: 0.333333333333333em; */ /* } */ @@ -35,6 +38,7 @@ /* padding: .666666666666667em 0; */ display: inline; vertical-align: middle; + margin-right: 0.333333333333333rem; } .eventsteps_phases_phase_progressbar { height: 1.266666666666667em; @@ -58,7 +62,7 @@ img.eventsteps_phases_phase_progress_icon { vertical-align: middle; height: 1.133333333333333em; width: 1.133333333333333em; - margin: 0 0.333333333333333em; + margin-right: 0.333333333333333em; } .eventsteps_phases_phase_progress_text { font-size: 0.866666666666667em; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.java index 53cc73e4a0e..a13a5fd4120 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.java @@ -1,5 +1,7 @@ package com.sap.sailing.gwt.home.mobile.partials.eventsteps; +import java.util.function.Function; + import com.google.gwt.core.client.GWT; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.uibinder.client.UiBinder; @@ -11,6 +13,7 @@ import com.sap.sailing.gwt.home.communication.regatta.RegattaProgressSeriesDTO; import com.sap.sailing.gwt.home.communication.regatta.RegattaWithProgressDTO; import com.sap.sailing.gwt.home.mobile.partials.section.MobileSection; import com.sap.sailing.gwt.home.mobile.partials.sectionHeader.SectionHeaderContent; +import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; import com.sap.sailing.gwt.home.shared.refresh.RefreshableWidget; public class EventSteps extends Composite implements RefreshableWidget { @@ -22,8 +25,10 @@ public class EventSteps extends Composite implements RefreshableWidget> racesNavigationFactory; - public EventSteps(RegattaMetadataDTO regatta) { + public EventSteps(RegattaMetadataDTO regatta, Function> racesNavigationFactory) { + this.racesNavigationFactory = racesNavigationFactory; EventStepsResources.INSTANCE.css().ensureInjected(); initWidget(regattaProgessUi = uiBinder.createAndBindUi(this)); ImageResource boatClassIcon = BoatClassImageResolver.getBoatClassIconResource(regatta.getBoatClass()); @@ -36,8 +41,10 @@ public class EventSteps extends Composite implements RefreshableWidget 1; for (RegattaProgressSeriesDTO seriesProgress : data.getProgress().getSeries()) { - regattaProgessUi.addContent(new EventStepsPhase(seriesProgress)); + final PlaceNavigation navigation = racesNavigationFactory.apply(seriesProgress.getName()); + regattaProgessUi.addContent(new EventStepsPhase(seriesProgress, navigation, showSeriesName)); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.java index 058d4bd75b9..9728319df95 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.java @@ -1,11 +1,10 @@ package com.sap.sailing.gwt.home.mobile.partials.eventsteps; -import static com.sap.sailing.domain.common.LeaderboardNameConstants.DEFAULT_SERIES_NAME; - import java.util.Map; import java.util.Map.Entry; import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.AnchorElement; import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.ImageElement; import com.google.gwt.dom.client.Style.Display; @@ -16,7 +15,9 @@ import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.gwt.home.communication.race.FleetMetadataDTO; import com.sap.sailing.gwt.home.communication.regatta.RegattaProgressFleetDTO; import com.sap.sailing.gwt.home.communication.regatta.RegattaProgressSeriesDTO; +import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.gwt.client.LinkUtil; public class EventStepsPhase extends Composite { @@ -26,20 +27,26 @@ public class EventStepsPhase extends Composite { interface EventStepsPhaseUiBinder extends UiBinder { } + @UiField AnchorElement anchorUi; @UiField DivElement nameUi; @UiField ImageElement checkUi; @UiField DivElement progressUi; @UiField DivElement fleetsContainerUi; - public EventStepsPhase(RegattaProgressSeriesDTO seriesProgress) { + EventStepsPhase(RegattaProgressSeriesDTO seriesProgress, PlaceNavigation placeNavigation, boolean showName) { initWidget(uiBinder.createAndBindUi(this)); - nameUi.setInnerText(DEFAULT_SERIES_NAME.equals(seriesProgress.getName()) ? I18N.races() : seriesProgress.getName()); + LinkUtil.configureForAction(anchorUi, () -> placeNavigation.goToPlace()); + if (showName) { + nameUi.setInnerText(seriesProgress.getName()); + } else { + nameUi.removeFromParent(); + } if (seriesProgress.isCompleted()) { - progressUi.setInnerText(String.valueOf(seriesProgress.getTotalRaceCount())); + progressUi.setInnerText(I18N.racesCount(seriesProgress.getTotalRaceCount())); } else { checkUi.getStyle().setDisplay(Display.NONE); int current = seriesProgress.getProgressRaceCount(), total = seriesProgress.getTotalRaceCount(); - progressUi.setInnerText(I18N.currentOfTotal(current, total)); + progressUi.setInnerText(I18N.currentOfTotalRaces(current, total)); } if (seriesProgress.getProgressRaceCount() == 0) { addStyleName(EventStepsResources.INSTANCE.css().eventsteps_phases_phaseinactive()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.ui.xml index 7cece0b816b..2ac9004e380 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.ui.xml @@ -6,11 +6,13 @@ -
-
- -
-
-
+ +
+
+ +
+
+
+
\ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsResources.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsResources.java index 1bc6a0dfbd2..46e49f87206 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsResources.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsResources.java @@ -16,6 +16,7 @@ public interface EventStepsResources extends ClientBundle { String eventsteps_phases(); String eventsteps_phases_phase(); String eventsteps_phases_phaseinactive(); + String eventsteps_phases_phase_link(); String eventsteps_phases_phase_name(); String eventsteps_phases_phase_progressbar(); String eventsteps_phases_phase_progressbar_fleet(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/regattacompetition/RegattaCompetition.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/regattacompetition/RegattaCompetition.java index 45a344513b3..9c0c0e3be8e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/regattacompetition/RegattaCompetition.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/regattacompetition/RegattaCompetition.java @@ -1,5 +1,8 @@ package com.sap.sailing.gwt.home.mobile.partials.regattacompetition; +import java.util.HashMap; +import java.util.Map; + import com.google.gwt.core.client.GWT; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; @@ -8,6 +11,7 @@ import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.gwt.home.communication.event.RaceCompetitionFormatSeriesDTO; import com.sap.sailing.gwt.home.shared.partials.regattacompetition.RegattaCompetitionView; +import com.sap.sse.gwt.client.DOMUtils; public class RegattaCompetition extends Composite implements RegattaCompetitionView { @@ -17,6 +21,7 @@ public class RegattaCompetition extends Composite implements RegattaCompetitionV } @UiField FlowPanel regattaSeriesContainerUi; + private final Map seriesNameToUiComponent = new HashMap<>(); public RegattaCompetition() { RegattaCompetitionResources.INSTANCE.css().ensureInjected(); @@ -31,8 +36,12 @@ public class RegattaCompetition extends Composite implements RegattaCompetitionV @Override public RegattaCompetitionSeriesView addSeriesView(RaceCompetitionFormatSeriesDTO series) { RegattaCompetitionSeries seriesView = new RegattaCompetitionSeries(series); + seriesNameToUiComponent.put(series.getSeriesName(), seriesView); regattaSeriesContainerUi.add(seriesView); return seriesView; } + public void scrollToSeries(final String seriesName) { + DOMUtils.scrollToTop(seriesNameToUiComponent.get(seriesName)); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/RegattaRacesNavigationProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/RegattaRacesNavigationProvider.java index d4f921b1a54..8c3655e1abf 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/RegattaRacesNavigationProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/RegattaRacesNavigationProvider.java @@ -5,4 +5,6 @@ import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; public interface RegattaRacesNavigationProvider { PlaceNavigation getRegattaRacesNavigation(String regattaId); + + PlaceNavigation getRegattaRacesNavigation(String regattaId, String seriesName); } 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 0ac77756ff0..96e653ce512 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 @@ -247,6 +247,12 @@ public abstract class AbstractEventActivity ex EventContext ctx = new EventContext(getCtx()).withRegattaId(regattaId).withRegattaAnalyticsManager(null); return clientFactory.getNavigator().getEventNavigation(new RegattaRacesPlace(ctx), null, false); } + + @Override + public PlaceNavigation getRegattaRacesNavigation(String regattaId, String prefSeriesName) { + EventContext ctx = new EventContext(getCtx()).withRegattaId(regattaId).withRegattaAnalyticsManager(null); + return clientFactory.getNavigator().getEventNavigation(new RegattaRacesPlace(ctx, prefSeriesName), null, false); + } public PlaceNavigation getLatesNewsNavigation() { EventContext ctx = new EventContext(getCtx()).withRegattaId(null).withRegattaAnalyticsManager(null); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/overview/regatta/RegattaOverviewImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/overview/regatta/RegattaOverviewImpl.java index 83403e8d3a8..afe0f4b5e58 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/overview/regatta/RegattaOverviewImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/overview/regatta/RegattaOverviewImpl.java @@ -3,6 +3,7 @@ package com.sap.sailing.gwt.home.mobile.places.event.overview.regatta; import java.util.Collection; import java.util.Map; import java.util.Set; +import java.util.function.Function; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Panel; @@ -19,6 +20,7 @@ import com.sap.sailing.gwt.home.mobile.partials.quickfinder.Quickfinder; import com.sap.sailing.gwt.home.mobile.places.QuickfinderPresenter; import com.sap.sailing.gwt.home.mobile.places.event.EventViewBase; import com.sap.sailing.gwt.home.mobile.places.event.overview.AbstractEventOverview; +import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; import com.sap.sailing.gwt.ui.client.FlagImageResolver; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -54,7 +56,9 @@ public class RegattaOverviewImpl extends AbstractEventOverview { } private void setupProgress(Panel container) { - eventStepsUi = new EventSteps(currentPresenter.getRegatta()); + final Function> racesNavigationFactory = prefSeriesName -> currentPresenter + .getRegattaRacesNavigation(getRegattaId(), prefSeriesName); + eventStepsUi = new EventSteps(currentPresenter.getRegatta(), racesNavigationFactory); refreshManager.add(eventStepsUi, new GetRegattaWithProgressAction(getEventId(), getRegattaId())); container.add(eventStepsUi); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesActivity.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesActivity.java index bac62f55092..52165919c9a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesActivity.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesActivity.java @@ -1,6 +1,7 @@ package com.sap.sailing.gwt.home.mobile.places.event.races; import java.util.List; +import java.util.Optional; import com.sap.sailing.gwt.home.communication.eventview.EventViewDTO; import com.sap.sailing.gwt.home.desktop.places.event.regatta.racestab.RegattaRacesPlace; @@ -32,4 +33,9 @@ public class RacesActivity extends AbstractEventActivity impl initQuickfinder(view, true); return view; } + + @Override + public Optional getPreferredSeriesName() { + return getPlace().getPreferredSeriesName(); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesView.java index e457c2af812..eca0d41c55d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesView.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesView.java @@ -1,10 +1,14 @@ package com.sap.sailing.gwt.home.mobile.places.event.races; +import java.util.Optional; + import com.sap.sailing.gwt.home.mobile.places.event.EventViewBase; public interface RacesView extends EventViewBase { public interface Presenter extends EventViewBase.Presenter { + + Optional getPreferredSeriesName(); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesViewImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesViewImpl.java index 1aca7f4fea0..89bc76e0473 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesViewImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesViewImpl.java @@ -5,13 +5,16 @@ import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.Scheduler; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.gwt.home.communication.event.EventReferenceWithStateDTO; import com.sap.sailing.gwt.home.communication.event.GetCompetitionFormatRacesAction; +import com.sap.sailing.gwt.home.communication.event.RaceCompetitionFormatSeriesDTO; import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorDTO; import com.sap.sailing.gwt.home.communication.eventview.RegattaMetadataDTO; import com.sap.sailing.gwt.home.communication.race.SimpleRaceMetadataDTO; @@ -27,6 +30,7 @@ import com.sap.sailing.gwt.home.shared.partials.filter.RacesByCompetitorTextBoxF import com.sap.sailing.gwt.home.shared.partials.placeholder.InfoPlaceholder; import com.sap.sailing.gwt.home.shared.partials.regattacompetition.RegattaCompetitionPresenter; import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.gwt.dispatch.shared.commands.ListResult; public class RacesViewImpl extends AbstractEventView implements RacesView { @@ -71,6 +75,13 @@ public class RacesViewImpl extends AbstractEventView implem protected String getRaceViewerURL(SimpleRaceMetadataDTO raceMetadata, String mode) { return currentPresenter.getRaceViewerURL(raceMetadata, mode); } + + @Override + public void setData(ListResult data) { + super.setData(data); + final Consumer scrollCommand = regattaCompetitionUi::scrollToSeries; + Scheduler.get().scheduleDeferred(() -> currentPresenter.getPreferredSeriesName().ifPresent(scrollCommand)); + } } private class RacesViewImplFilterPresenter extends FilterPresenter { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardPanelLifecycle.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardPanelLifecycle.java index f533c6d71e8..381b304b9c5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardPanelLifecycle.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardPanelLifecycle.java @@ -12,7 +12,7 @@ public abstract class LeaderboardPanelLifecycle protected final Iterable availableDetailTypes; - public LeaderboardPanelLifecycle(StringMessages stringMessages, Iterable availableDetailTypes) { + protected LeaderboardPanelLifecycle(StringMessages stringMessages, Iterable availableDetailTypes) { this.stringMessages = stringMessages; this.availableDetailTypes = availableDetailTypes; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettings.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettings.java index 99ad8b70c2e..1409a1046d9 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettings.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettings.java @@ -66,7 +66,8 @@ public abstract class LeaderboardSettings extends AbstractGenericSerializableSet showCompetitorBoatInfoColumn = new BooleanSetting("showCompetitorBoatInfoColumn", this, false); } - public LeaderboardSettings() { + public LeaderboardSettings(boolean showCompetitorBoatInfoColumnDefault) { + showCompetitorBoatInfoColumn.setDefaultValue(showCompetitorBoatInfoColumnDefault); } /** diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsDialogComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsDialogComponent.java index 285bfba3f93..1b02fe8b583 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsDialogComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsDialogComponent.java @@ -46,7 +46,8 @@ public abstract class LeaderboardSettingsDialogComponent availableDetailTypes; - public LeaderboardSettingsDialogComponent(T initialSettings, StringMessages stringMessages, Iterable availableDetailTypes, boolean canBoatInfoBeShown) { + protected LeaderboardSettingsDialogComponent(T initialSettings, StringMessages stringMessages, + Iterable availableDetailTypes, boolean canBoatInfoBeShown) { this.initialSettings = initialSettings; this.stringMessages = stringMessages; this.canBoatInfoBeShown = canBoatInfoBeShown; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsFactory.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsFactory.java index 1dd0fb242a0..1fa2202485b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsFactory.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsFactory.java @@ -44,7 +44,7 @@ public class LeaderboardSettingsFactory { } public SingleRaceLeaderboardSettings createNewSettingsWithCustomRaceDetails(List raceDetailsToShow) { - SingleRaceLeaderboardSettings defaultSettings = new SingleRaceLeaderboardSettings(); + SingleRaceLeaderboardSettings defaultSettings = new SingleRaceLeaderboardSettings(false); return new SingleRaceLeaderboardSettings( defaultSettings.getManeuverDetailsToShow(), defaultSettings.getLegDetailsToShow(), diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/MultiRaceLeaderboardPanelLifecycle.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/MultiRaceLeaderboardPanelLifecycle.java index 4e45d743d51..29a5b5a89e6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/MultiRaceLeaderboardPanelLifecycle.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/MultiRaceLeaderboardPanelLifecycle.java @@ -11,11 +11,13 @@ import com.sap.sse.common.settings.generic.support.SettingsUtil; public class MultiRaceLeaderboardPanelLifecycle extends LeaderboardPanelLifecycle { protected final List namesOfRaceColumns; + private final boolean canBoatInfoBeShown; public MultiRaceLeaderboardPanelLifecycle(AbstractLeaderboardDTO leaderboard, StringMessages stringMessages, Iterable availableDetailTypes) { super(stringMessages, availableDetailTypes); this.namesOfRaceColumns = leaderboard != null ? leaderboard.getNamesOfRaceColumns() : new ArrayList(); + this.canBoatInfoBeShown = leaderboard != null ? !leaderboard.canBoatsOfCompetitorsChangePerRace : false; } @Override public MultiRaceLeaderboardSettings extractUserSettings(MultiRaceLeaderboardSettings currentLeaderboardSettings) { @@ -55,7 +57,8 @@ public class MultiRaceLeaderboardPanelLifecycle extends LeaderboardPanelLifecycl @Override public MultiRaceLeaderboardSettingsDialogComponent getSettingsDialogComponent(MultiRaceLeaderboardSettings settings) { - return new MultiRaceLeaderboardSettingsDialogComponent(settings, namesOfRaceColumns, stringMessages, availableDetailTypes, false); + return new MultiRaceLeaderboardSettingsDialogComponent(settings, namesOfRaceColumns, stringMessages, + availableDetailTypes, canBoatInfoBeShown); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/MultiRaceLeaderboardSettings.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/MultiRaceLeaderboardSettings.java index 74232f6c701..fc9714d71fd 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/MultiRaceLeaderboardSettings.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/MultiRaceLeaderboardSettings.java @@ -28,7 +28,7 @@ public class MultiRaceLeaderboardSettings extends LeaderboardSettings { public MultiRaceLeaderboardSettings() { - super(); + super(false); } public MultiRaceLeaderboardSettings(Collection maneuverDetailsToShow, @@ -48,6 +48,7 @@ public class MultiRaceLeaderboardSettings extends LeaderboardSettings { } public MultiRaceLeaderboardSettings(Iterable namesOfRaceColumnsToShow) { + super(false); this.namesOfRaceColumnsToShow.setValues(namesOfRaceColumnsToShow); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/OverallLeaderboardSettingsDialogComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/OverallLeaderboardSettingsDialogComponent.java index a66666974f9..be79914e79e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/OverallLeaderboardSettingsDialogComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/OverallLeaderboardSettingsDialogComponent.java @@ -9,8 +9,10 @@ import com.sap.sse.gwt.client.dialog.DataEntryDialog; public class OverallLeaderboardSettingsDialogComponent extends MultiRaceLeaderboardSettingsDialogComponent { - public OverallLeaderboardSettingsDialogComponent(MultiRaceLeaderboardSettings initialSettings, List allRaceColumnNames, StringMessages stringMessages, Iterable availableDetailTypes) { - super(initialSettings, allRaceColumnNames, stringMessages, availableDetailTypes, false); + public OverallLeaderboardSettingsDialogComponent(MultiRaceLeaderboardSettings initialSettings, + List allRaceColumnNames, StringMessages stringMessages, Iterable availableDetailTypes) { + super(initialSettings, allRaceColumnNames, stringMessages, availableDetailTypes, + /* canBoatInfoBeShown */ false); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettings.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettings.java index edf6c10639a..5c85a5d8a37 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettings.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettings.java @@ -10,15 +10,16 @@ public class SingleRaceLeaderboardSettings extends LeaderboardSettings { private BooleanSetting showRaceRankColumn; - public SingleRaceLeaderboardSettings() { - super(); + public SingleRaceLeaderboardSettings(boolean showCompetitorBoatInfoColumnDefault) { + super(showCompetitorBoatInfoColumnDefault); } public SingleRaceLeaderboardSettings(Collection maneuverDetailsToShow, Collection legDetailsToShow, Collection raceDetailsToShow, Collection overallDetailsToShow, Long delayBetweenAutoAdvancesInMilliseconds, - boolean showAddedScores, boolean showCompetitorShortNameColumn, - boolean showCompetitorFullNameColumn, boolean showCompetitorBoatInfoColumn, boolean isCompetitorNationalityColumnVisible,boolean showRaceRankColumn) { + boolean showAddedScores, boolean showCompetitorShortNameColumn, boolean showCompetitorFullNameColumn, + boolean showCompetitorBoatInfoColumn, boolean isCompetitorNationalityColumnVisible, + boolean showRaceRankColumn) { super(maneuverDetailsToShow, legDetailsToShow, raceDetailsToShow, overallDetailsToShow, delayBetweenAutoAdvancesInMilliseconds, showAddedScores, showCompetitorShortNameColumn, showCompetitorFullNameColumn, showCompetitorBoatInfoColumn, isCompetitorNationalityColumnVisible); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettingsDialogComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettingsDialogComponent.java index 3f40d626b88..e911ec0cc11 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettingsDialogComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettingsDialogComponent.java @@ -13,11 +13,11 @@ import com.sap.sse.gwt.client.dialog.DataEntryDialog.Validator; public class SingleRaceLeaderboardSettingsDialogComponent extends LeaderboardSettingsDialogComponent { - protected CheckBox showRaceRankColumn; + private CheckBox showRaceRankColumn; public SingleRaceLeaderboardSettingsDialogComponent(SingleRaceLeaderboardSettings initialSettings, StringMessages stringMessages, Iterable availableDetailTypes) { - super(initialSettings, stringMessages, availableDetailTypes, true /*canBoatInfoBeShown*/); + super(initialSettings, stringMessages, availableDetailTypes, /* canBoatInfoBeShown */ true); } @Override @@ -33,7 +33,8 @@ public class SingleRaceLeaderboardSettingsDialogComponent 1000l * (delayBetweenAutoAdvancesValue == null ? 0l : delayBetweenAutoAdvancesValue.longValue()), /* showAddedScores */ showAddedScoresCheckBox.getValue().booleanValue(), showCompetitorShortNameColumnCheckBox.getValue(), showCompetitorFullNameColumnCheckBox.getValue(), - showCompetitorBoatInfoColumnCheckBox.getValue(), isCompetitorNationalityColumnVisible.getValue(), showRaceRankColumn.getValue()); + showCompetitorBoatInfoColumnCheckBox.getValue(), isCompetitorNationalityColumnVisible.getValue(), + showRaceRankColumn.getValue()); return newSettings; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceCompetitorSelectionModel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceCompetitorSelectionModel.java index dd58672ea10..61ce0985489 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceCompetitorSelectionModel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceCompetitorSelectionModel.java @@ -16,6 +16,12 @@ public class RaceCompetitorSelectionModel extends CompetitorSelectionModel imple boatsOfCompetitors = new HashMap<>(); } + public RaceCompetitorSelectionModel(boolean hasMultiSelection, Map boatsOfCompetitors) { + super(hasMultiSelection); + this.boatsOfCompetitors = boatsOfCompetitors; + addAll(boatsOfCompetitors.keySet()); + } + public RaceCompetitorSelectionModel(boolean hasMultiSelection, CompetitorColorProvider competitorColorProvider, Map boatsOfCompetitors) { super(hasMultiSelection, competitorColorProvider); this.boatsOfCompetitors = boatsOfCompetitors; @@ -29,11 +35,11 @@ public class RaceCompetitorSelectionModel extends CompetitorSelectionModel imple @Override public void setBoat(CompetitorDTO competitor, BoatDTO boat) { - boatsOfCompetitors.put(competitor, boat); + boatsOfCompetitors.put(competitor, boat); } @Override public BoatDTO getBoat(CompetitorDTO competitor) { - return boatsOfCompetitors.get(competitor); + return boatsOfCompetitors.get(competitor); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceHelper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceHelper.java index 261db38393a..c4400b452d4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceHelper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceHelper.java @@ -2,14 +2,13 @@ package com.sap.sailing.gwt.ui.client; import com.google.gwt.core.client.GWT; import com.google.gwt.user.client.rpc.ServiceDefTarget; +import com.sap.sse.gwt.client.EntryPointHelper; import com.sap.sse.gwt.client.ServiceRoutingProvider; /** * Set of convenience factory methods that create a {@link SailingServiceAsync} instance. */ public abstract class SailingServiceHelper { - private final static String moduleBaseURL = GWT.getModuleBaseURL(); - private SailingServiceHelper() { } @@ -41,20 +40,20 @@ public abstract class SailingServiceHelper { * @param sameBundle {@code false} if using {@link SailingService} from a different OSGi bundle (e.g. dashboards), {@code true} otherwise */ public static SailingServiceAsync createSailingServiceInstance(boolean sameBundle, ServiceRoutingProvider routingProvider) { - SailingServiceAsync service = GWT.create(SailingService.class); - ServiceDefTarget serviceToRegister = (ServiceDefTarget) service; - StringBuilder baseURL = new StringBuilder(); - baseURL.append(moduleBaseURL.substring(0, moduleBaseURL.lastIndexOf('/', moduleBaseURL.length() - 2) + 1)); - if (!sameBundle) { - baseURL.append(RemoteServiceMappingConstants.WEB_CONTEXT_PATH).append("/"); - } - - baseURL.append(RemoteServiceMappingConstants.sailingServiceRemotePath); + final SailingServiceAsync service = GWT.create(SailingService.class); + final ServiceDefTarget serviceToRegister = (ServiceDefTarget) service; + final StringBuilder servicePath = new StringBuilder(RemoteServiceMappingConstants.sailingServiceRemotePath); if (routingProvider != null) { - baseURL.append(routingProvider.routingSuffixPath()); + servicePath.append(routingProvider.routingSuffixPath()); + } + + final String servicePathWithRoutingSuffix = servicePath.toString(); + if (sameBundle) { + EntryPointHelper.registerASyncService(serviceToRegister, servicePathWithRoutingSuffix); + } else { + EntryPointHelper.registerASyncService(serviceToRegister, RemoteServiceMappingConstants.WEB_CONTEXT_PATH, servicePathWithRoutingSuffix); } - serviceToRegister.setServiceEntryPoint(baseURL.toString()); return service; } } 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 685abe1504a..07c4ab47af7 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -1441,7 +1441,7 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String courseGeometry(); String degreesToWind(String diff); String miniLeeaderboardLegendText(); - String currentOfTotal(int current, int total); + String currentOfTotalRaces(int current, @PluralCount(DefaultRule_1_0n.class) int total); String noFinishedRaces(); String racesOverview(); String listFormatLabel(); 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 19651a58c72..b6985381df1 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -1428,7 +1428,8 @@ numberOfRacesShort=# Races courseGeometry=Course Geometry degreesToWind={0}° to wind miniLeeaderboardLegendText=Shows the points along with the number of races per competitor -currentOfTotal={0,number}/{1,number} +currentOfTotalRaces={0,number}/{1,number} races +currentOfTotalRaces[one]={0,number}/{1,number} race noFinishedRaces=There are no finished races yet racesOverview=Races overview listFormatLabel=List format 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 50f122fc377..554093305d3 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -1411,7 +1411,8 @@ numberOfRacesShort=# Rennen courseGeometry=Kursgeometrie degreesToWind={0}° zum Wind miniLeeaderboardLegendText=Es werden die Punkte und die Anzahl Rennen je Team angezeigt -currentOfTotal={0,number}/{1,number} +currentOfTotalRaces={0,number}/{1,number} Rennen +currentOfTotalRaces[one]={0,number}/{1,number} Rennen noFinishedRaces=Es wurden noch keine Wettfahrten beendet. racesOverview=Wettfahrtübersicht listFormatLabel=Listenansicht diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties index 6cddd2a64fb..822a996829a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties @@ -1424,7 +1424,6 @@ numberOfRacesShort=Nº regatas courseGeometry=Geometría de la regata degreesToWind={0}° respecto al viento miniLeeaderboardLegendText=Muestra los puntos y el número de carreras por competidor -currentOfTotal={0,number}/{1,number} noFinishedRaces=Todavía no ha finalizado ninguna carrera racesOverview=Resumen de carreras listFormatLabel=Formato de lista diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties index 4eceacdde57..1a952f87a36 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties @@ -1424,7 +1424,6 @@ numberOfRacesShort=Nombre de courses courseGeometry=Géométrie du parcours degreesToWind={0}° au vent miniLeeaderboardLegendText=Affiche les points associés au nombre de courses par concurrent -currentOfTotal={0,number}/{1,number} noFinishedRaces=Aucune course terminée pour l''instant. racesOverview=Synthèse des courses listFormatLabel=Format de liste diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties index 6bcbca3f0b9..6709ff37144 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties @@ -1424,7 +1424,6 @@ numberOfRacesShort=# レース courseGeometry=コース配置 degreesToWind=風と {0}° miniLeeaderboardLegendText=競技者別にレース数とともに得点を表示 -currentOfTotal={0,number}/{1,number} noFinishedRaces=フィニッシュしたレースがまだありません。 racesOverview=レース概要 listFormatLabel=レース一覧 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties index d13909efea6..df38c1bfe0b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties @@ -1424,7 +1424,6 @@ numberOfRacesShort=Nº de corridas courseGeometry=Geometria do percurso degreesToWind={0}° para vento miniLeeaderboardLegendText=Exibe os pontos juntamente com o número de corridas por competidor -currentOfTotal={0,number}/{1,number} noFinishedRaces=Ainda não existem corridas concluídas. racesOverview=Síntese de corridas listFormatLabel=Formato de lista 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 4bf82159e3e..674df8274c5 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 @@ -1424,7 +1424,6 @@ numberOfRacesShort=Гонки courseGeometry=Геометрия курса degreesToWind={0}° к ветру miniLeeaderboardLegendText=Показывает баллы и число гонок каждого участника -currentOfTotal={0,number}/{1,number} noFinishedRaces=Завершенных гонок еще нет racesOverview=Обзор гонок listFormatLabel=Формат списка 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 e8e848faa28..5b69e84fc19 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 @@ -1424,7 +1424,6 @@ numberOfRacesShort=比赛轮次数 courseGeometry=场地几何结构 degreesToWind={0}°(相对于风) miniLeeaderboardLegendText=显示分数以及各参赛队的比赛轮次数 -currentOfTotal={0,number}/{1,number} noFinishedRaces=没有结束的比赛轮次 racesOverview=比赛轮次总览 listFormatLabel=清单格式 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 5e7d29b4d3e..36deb163bac 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -885,6 +885,7 @@ public class RaceMap extends AbstractCompositeComponent impleme @Override public void timeChanged(final Date newTime, final Date oldTime) { + boolean isRedraw = oldTime == null; if (newTime != null && isMapInitialized) { if (raceIdentifier != null) { RegattaAndRaceIdentifier race = raceIdentifier; @@ -894,7 +895,8 @@ public class RaceMap extends AbstractCompositeComponent impleme final com.sap.sse.common.Util.Triple, Map, Map> fromAndToAndOverlap = fixesAndTails.computeFromAndTo(newTime, competitorsToShow, settings.getEffectiveTailLengthInMilliseconds()); // Request map data update, possibly in two calls; see method details - callGetRaceMapDataForAllOverlappingAndTipsOfNonOverlappingAndGetBoatPositionsForAllOthers(fromAndToAndOverlap, race, newTime, transitionTimeInMillis, competitorsToShow); + callGetRaceMapDataForAllOverlappingAndTipsOfNonOverlappingAndGetBoatPositionsForAllOthers( + fromAndToAndOverlap, race, newTime, transitionTimeInMillis, competitorsToShow, isRedraw); // draw the wind into the map, get the combined wind List windSourceTypeNames = new ArrayList(); windSourceTypeNames.add(WindSourceType.EXPEDITION.name()); @@ -950,6 +952,7 @@ public class RaceMap extends AbstractCompositeComponent impleme public WindInfoForRaceDTO getLastCombinedWindTrackInfoDTO() { return lastCombinedWindTrackInfoDTO; } + /** * Requests updates for map data and, when received, updates the map structures accordingly. *

@@ -970,12 +973,14 @@ public class RaceMap extends AbstractCompositeComponent impleme * 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 these non-overlapping requests, this method creates a * {@link GetBoatPositionsAction} request loading the entire tail required, but not quick ranks, sidelines and mark - * positions. Updating the results of this call is done in {@link #updateBoatPositions(Date, long, Map, Iterable, Map, boolean)}. + * positions. Updating the results of this call is done in + * {@link #updateBoatPositions(Date, long, Map, Iterable, Map, boolean)}. *

*/ private void callGetRaceMapDataForAllOverlappingAndTipsOfNonOverlappingAndGetBoatPositionsForAllOthers( final Triple, Map, Map> fromAndToAndOverlap, - RegattaAndRaceIdentifier race, final Date newTime, final long transitionTimeInMillis, final Iterable competitorsToShow) { + RegattaAndRaceIdentifier race, final Date newTime, final long transitionTimeInMillis, + final Iterable competitorsToShow, boolean isRedraw) { final Map fromTimesForQuickCall = new HashMap<>(); final Map toTimesForQuickCall = new HashMap<>(); final Map fromTimesForNonOverlappingTailsCall = new HashMap<>(); @@ -1009,7 +1014,8 @@ public class RaceMap extends AbstractCompositeComponent impleme (settings.isShowSimulationOverlay() ? simulationOverlay.getLegIdentifier() : null), raceCompetitorSet.getMd5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID(), newTime, settings.isShowEstimatedDuration()), GET_RACE_MAP_DATA_CATEGORY, - getRaceMapDataCallback(newTime, transitionTimeInMillis, fromAndToAndOverlap.getC(), competitorsToShow, ++boatPositionRequestIDCounter)); + getRaceMapDataCallback(newTime, transitionTimeInMillis, fromAndToAndOverlap.getC(), competitorsToShow, + ++boatPositionRequestIDCounter, isRedraw)); // next, if necessary, do the full thing; the two calls have different action classes, so throttling should not drop one for the other if (!fromTimesForNonOverlappingTailsCall.keySet().isEmpty()) { asyncActionsExecutor.execute(new GetBoatPositionsAction(sailingService, race, fromTimesForNonOverlappingTailsCall, toTimesForNonOverlappingTailsCall, @@ -1037,7 +1043,7 @@ public class RaceMap extends AbstractCompositeComponent impleme final Date newTime, final long transitionTimeInMillis, final Map hasTailOverlapForCompetitor, - final Iterable competitorsToShow, final int requestID) { + final Iterable competitorsToShow, final int requestID, boolean isRedraw) { return new MarkedAsyncCallback<>(new AsyncCallback() { @Override public void onFailure(Throwable caught) { @@ -1066,10 +1072,14 @@ public class RaceMap extends AbstractCompositeComponent impleme // Do boat specific actions Map> boatData = raceMapDataDTO.boatPositions; updateBoatPositions(newTime, transitionTimeInMillis, hasTailOverlapForCompetitor, competitorsToShow, boatData, /* updateTailsOnly */ false); - if (douglasMarkers != null) { - removeAllMarkDouglasPeuckerpoints(); + + if (!isRedraw) { + // only remove markers if the time is actually changed + if (douglasMarkers != null) { + removeAllMarkDouglasPeuckerpoints(); + } + maneuverMarkersAndLossIndicators.clearAllManeuverMarkers(); } - maneuverMarkersAndLossIndicators.clearAllManeuverMarkers(); if (requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown) { updateCoordinateSystemFromSettings(); } @@ -2433,7 +2443,7 @@ public class RaceMap extends AbstractCompositeComponent impleme showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); } else { // adding a single competitor; may need to re-load data, so refresh: - timeChanged(timer.getTime(), null); + redraw(); } } else { // only change highlighting @@ -2446,7 +2456,7 @@ public class RaceMap extends AbstractCompositeComponent impleme // 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); + redraw(); } } // Now update tails for all competitors because selection change may also affect all unselected competitors @@ -2469,13 +2479,13 @@ public class RaceMap extends AbstractCompositeComponent impleme 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); + redraw(); } 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); + redraw(); } else { // otherwise remove only deselected competitor's boat images and tail BoatOverlay removedBoatOverlay = boatOverlays.remove(competitor); @@ -2755,12 +2765,12 @@ public class RaceMap extends AbstractCompositeComponent impleme @Override public void competitorsListChanged(Iterable competitors) { - timeChanged(timer.getTime(), null); + redraw(); } @Override public void filteredCompetitorsListChanged(Iterable filteredCompetitors) { - timeChanged(timer.getTime(), null); + redraw(); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MultiRaceLeaderboardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MultiRaceLeaderboardPanel.java index 8c5dadc347a..42c94961eee 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MultiRaceLeaderboardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MultiRaceLeaderboardPanel.java @@ -6,7 +6,6 @@ import java.util.List; import java.util.Map; import com.sap.sailing.domain.common.DetailType; -import com.sap.sailing.domain.common.LeaderboardType; import com.sap.sailing.domain.common.dto.AbstractLeaderboardDTO; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.domain.common.dto.LeaderboardDTO; @@ -96,8 +95,7 @@ public class MultiRaceLeaderboardPanel extends LeaderboardPanel getSettingsDialogComponent( MultiRaceLeaderboardSettings useTheseSettings) { - return new MultiRaceLeaderboardSettingsDialogComponent((MultiRaceLeaderboardSettings) useTheseSettings, - leaderboard.getNamesOfRaceColumns(), stringMessages, availableDetailTypes, !leaderboard.canBoatsOfCompetitorsChangePerRace); + return new MultiRaceLeaderboardSettingsDialogComponent(useTheseSettings, leaderboard.getNamesOfRaceColumns(), + stringMessages, availableDetailTypes, canShowCompetitorBoatInfo()); } @Override 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 4b6cfbe7256..487d63cf647 100644 --- 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 @@ -2,6 +2,9 @@ package com.sap.sailing.gwt.ui.raceboard; import java.util.Collections; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Style.Unit; @@ -12,6 +15,7 @@ import com.google.gwt.user.client.ui.DockLayoutPanel; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.RootLayoutPanel; import com.sap.sailing.domain.common.DetailType; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.gwt.common.authentication.FixedSailingAuthentication; import com.sap.sailing.gwt.common.authentication.SAPSailingHeaderWithAuthentication; import com.sap.sailing.gwt.common.communication.routing.ProvidesLeaderboardRouting; @@ -26,13 +30,15 @@ import com.sap.sailing.gwt.ui.client.RemoteServiceMappingConstants; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.RaceWithCompetitorsAndBoatsDTO; import com.sap.sailing.gwt.ui.shared.RaceboardDataDTO; +import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; +import com.sap.sse.common.settings.Settings; import com.sap.sse.gwt.client.EntryPointHelper; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; import com.sap.sse.gwt.client.formfactor.DeviceDetector; import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.player.Timer.PlayModes; -import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeSettings; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; import com.sap.sse.gwt.client.shared.settings.DefaultOnSettingsLoadedCallback; import com.sap.sse.gwt.settings.SettingsToUrlSerializer; import com.sap.sse.security.ui.authentication.generic.sapheader.SAPHeaderWithAuthentication; @@ -40,7 +46,6 @@ import com.sap.sse.security.ui.settings.ComponentContextWithSettingsStorage; import com.sap.sse.security.ui.settings.StoredSettingsLocation; public class RaceBoardEntryPoint extends AbstractSailingEntryPoint implements ProvidesLeaderboardRouting { - private RaceWithCompetitorsAndBoatsDTO selectedRace; /** * Controls the predefined mode into which to switch or configure the race viewer. @@ -48,131 +53,83 @@ public class RaceBoardEntryPoint extends AbstractSailingEntryPoint implements Pr private final MediaServiceAsync mediaService = GWT.create(MediaService.class); private RaceboardContextDefinition raceboardContextDefinition; - - private String leaderBoardName; @Override protected void doOnModuleLoad() { super.doOnModuleLoad(); EntryPointHelper.registerASyncService((ServiceDefTarget) mediaService, RemoteServiceMappingConstants.mediaServiceRemotePath); - raceboardContextDefinition = new SettingsToUrlSerializer() - .deserializeFromCurrentLocation(new RaceboardContextDefinition()); - final RaceBoardModes finalMode; - if (raceboardContextDefinition.getMode() != null) { - finalMode = RaceBoardModes.valueOf(raceboardContextDefinition.getMode()); - } else { - finalMode = null; - } - if (raceboardContextDefinition.getRegattaName() == null || raceboardContextDefinition.getRegattaName().isEmpty() - || raceboardContextDefinition.getRaceName() == null - || raceboardContextDefinition.getRaceName().isEmpty() - || raceboardContextDefinition.getLeaderboardName() == null - || raceboardContextDefinition.getLeaderboardName().isEmpty()) { - createErrorPage(getStringMessages().requiresRegattaRaceAndLeaderboard()); - return; - } - - leaderBoardName = raceboardContextDefinition.getLeaderboardName(); - - AsyncCallback asyncCallback = new AsyncCallback() { - @Override - public void onSuccess(RaceboardDataDTO raceboardData) { - if (!raceboardData.isValidLeaderboard()) { - createErrorPage(getStringMessages().noSuchLeaderboard()); - return; - } - if (raceboardContextDefinition.getEventId() != null && !raceboardData.isValidEvent()) { - createErrorPage(getStringMessages().noSuchEvent()); - } - if (raceboardContextDefinition.getLeaderboardGroupName() != null) { - if (!raceboardData.isValidLeaderboardGroup()) { - createErrorPage(getStringMessages().leaderboardNotContainedInLeaderboardGroup( - raceboardContextDefinition.getLeaderboardName(), - raceboardContextDefinition.getLeaderboardGroupName())); - return; - } - if (raceboardContextDefinition.getEventId() != null && raceboardData.isValidLeaderboardGroup() - && !raceboardData.isValidEvent()) { - createErrorPage(getStringMessages().leaderboardGroupNotContainedInEvent( - raceboardContextDefinition.getLeaderboardGroupName(), - raceboardContextDefinition.getEventId().toString())); - return; - } - } - if (raceboardData.getRace() == null) { - createErrorPage(getStringMessages().couldNotFindRaceInRegatta( - raceboardContextDefinition.getRaceName(), raceboardContextDefinition.getRegattaName())); - return; - } - final boolean showChartMarkEditMediaButtonsAndVideo = !DeviceDetector.isMobile(); - final StoredSettingsLocation storageDefinition = StoredSettingsLocationFactory - .createStoredSettingsLocatorForRaceBoard(raceboardContextDefinition, - finalMode != null ? finalMode.name() : null); - // TODO bug2822: also determine regatta's boat change parameter and parameterize Leaderboard settings - // --> show boat info column yes/no? - getSailingService().determineDetailTypesForCompetitorChart( - raceboardContextDefinition.getLeaderboardGroupName(), - raceboardData.getRace().getRaceIdentifier(), new AsyncCallback>() { - - @Override - public void onFailure(Throwable caught) { - reportError("Error trying to create the raceboard: " + caught.getMessage()); - } - - @Override - public void onSuccess(Iterable chartAllowedTypes) { - getSailingService().getAvailableDetailTypesForLeaderboard( - raceboardContextDefinition.getLeaderboardName(), - raceboardData.getRace().getRaceIdentifier(), new AsyncCallback>() { - @Override - public void onFailure(Throwable caught) { - reportError( - "Error trying to create the raceboard: " + caught.getMessage()); - } - - @Override - public void onSuccess(Iterable leaderboardAllowedTypes) { - final RaceBoardPerspectiveLifecycle lifeCycle = new RaceBoardPerspectiveLifecycle( - StringMessages.INSTANCE, chartAllowedTypes, getUserService(), - leaderboardAllowedTypes); - RaceBoardComponentContext componentContext = new RaceBoardComponentContext( - lifeCycle, getUserService(), storageDefinition); - - componentContext.getInitialSettings( - new DefaultOnSettingsLoadedCallback>() { - @Override - public void onSuccess( - PerspectiveCompositeSettings initialSettings) { - Timer timer = new Timer(PlayModes.Replay, 1000l); - final RaceBoardPanel raceBoardPanel = createPerspectivePage( - null, componentContext, initialSettings, - raceboardData, - showChartMarkEditMediaButtonsAndVideo, - lifeCycle, timer, leaderboardAllowedTypes); - if (finalMode != null) { - finalMode.getMode().applyTo(raceBoardPanel); - } - } - }); - } - }); - } + checkRaceboardContextPreConditions(new RaceboardContextDefinition()).ifPresent(raceboardContextDefinition -> { + RaceBoardEntryPoint.this.raceboardContextDefinition = raceboardContextDefinition; + final String modeString = raceboardContextDefinition.getMode(); + final RaceBoardModes mode = modeString == null ? null : RaceBoardModes.valueOf(modeString); + checkRaceboardDataConditions(raceboardData -> { + loadAvailableDetailTypes(raceboardData, (chartDetailTypes, lbDetailTypes) -> { + loadLeaderboardInformation(leaderboard -> { + initLifecycle(mode, chartDetailTypes, lbDetailTypes, leaderboard, (lifecycle, compContext) -> { + loadInitialSettings(compContext, settings -> { + createPerspective(mode, compContext, settings, raceboardData, lifecycle, lbDetailTypes); + }); }); - } + }); + }); + }); + }); + } + + private Optional checkRaceboardContextPreConditions(C settings) { + final C raceboardContext = new SettingsToUrlSerializer().deserializeFromCurrentLocation(settings); + if (raceboardContext.getRegattaName() == null || raceboardContext.getRegattaName().isEmpty() + || raceboardContext.getRaceName() == null || raceboardContext.getRaceName().isEmpty() + || raceboardContext.getLeaderboardName() == null || raceboardContext.getLeaderboardName().isEmpty()) { + createErrorPage(getStringMessages().requiresRegattaRaceAndLeaderboard()); + return Optional.empty(); + } + return Optional.of(raceboardContext); + } - @Override - public void onFailure(Throwable caught) { - reportError("Error trying to create the raceboard: " + caught.getMessage()); - } - }; + private void checkRaceboardDataConditions(final Consumer callback) { getSailingService().getRaceboardData(raceboardContextDefinition.getRegattaName(), raceboardContextDefinition.getRaceName(), raceboardContextDefinition.getLeaderboardName(), raceboardContextDefinition.getLeaderboardGroupName(), raceboardContextDefinition.getEventId(), - asyncCallback); + new AbstractRaceBoardAsyncCallback() { + + @Override + public void onSuccess(RaceboardDataDTO raceboardData) { + if (!raceboardData.isValidLeaderboard()) { + createErrorPage(getStringMessages().noSuchLeaderboard()); + return; + } + if (raceboardContextDefinition.getEventId() != null && !raceboardData.isValidEvent()) { + createErrorPage(getStringMessages().noSuchEvent()); + } + if (raceboardContextDefinition.getLeaderboardGroupName() != null) { + if (!raceboardData.isValidLeaderboardGroup()) { + createErrorPage(getStringMessages().leaderboardNotContainedInLeaderboardGroup( + raceboardContextDefinition.getLeaderboardName(), + raceboardContextDefinition.getLeaderboardGroupName())); + return; + } + if (raceboardContextDefinition.getEventId() != null + && raceboardData.isValidLeaderboardGroup() && !raceboardData.isValidEvent()) { + createErrorPage(getStringMessages().leaderboardGroupNotContainedInEvent( + raceboardContextDefinition.getLeaderboardGroupName(), + raceboardContextDefinition.getEventId().toString())); + return; + } + } + if (raceboardData.getRace() == null) { + createErrorPage(getStringMessages().couldNotFindRaceInRegatta( + raceboardContextDefinition.getRaceName(), + raceboardContextDefinition.getRegattaName())); + return; + } + callback.accept(raceboardData); + } + }); } - + private void createErrorPage(String message) { final DockLayoutPanel vp = new DockLayoutPanel(Unit.PX); final SAPHeaderWithAuthentication header = new SAPSailingHeaderWithAuthentication(); @@ -183,31 +140,99 @@ public class RaceBoardEntryPoint extends AbstractSailingEntryPoint implements Pr infoText.getElement().getStyle().setMargin(1, Unit.EM); vp.add(infoText); // TODO: Styling of error page slightly differs from the other usages of SAPSailingHeaderWithAuthentication - // because of the root font-size. Adjustments are postponed because they might affect the hole page content. + // because of the root font-size. Adjustments are postponed because they might affect the whole page content. } - private RaceBoardPanel createPerspectivePage(Component parent, + private void loadAvailableDetailTypes(final RaceboardDataDTO raceboardData, + final BiConsumer, Iterable> callback) { + final RegattaAndRaceIdentifier raceIdentifier = raceboardData.getRace().getRaceIdentifier(); + getSailingService().determineDetailTypesForCompetitorChart(raceboardContextDefinition.getLeaderboardGroupName(), + raceIdentifier, new AbstractRaceBoardAsyncCallback>() { + + @Override + public void onSuccess(Iterable allowedChartDetailTypes) { + final String leaderboardName = raceboardContextDefinition.getLeaderboardName(); + getSailingService().getAvailableDetailTypesForLeaderboard(leaderboardName, raceIdentifier, + new AbstractRaceBoardAsyncCallback>() { + + @Override + public void onSuccess(Iterable allowedLeaderboardDetailTypes) { + callback.accept(allowedChartDetailTypes, allowedLeaderboardDetailTypes); + } + }); + } + }); + } + + private void loadLeaderboardInformation(final Consumer callback) { + getSailingService().getLeaderboard(raceboardContextDefinition.getLeaderboardName(), + new AbstractRaceBoardAsyncCallback() { + + @Override + public void onSuccess(StrippedLeaderboardDTO result) { + callback.accept(result); + } + }); + } + + private void initLifecycle(final RaceBoardModes raceBoardMode, final Iterable chartDetailTypes, + final Iterable leaderboardDetailTypes, final StrippedLeaderboardDTO leaderboard, + final BiConsumer callback) { + final StoredSettingsLocation storageDefinition = StoredSettingsLocationFactory + .createStoredSettingsLocatorForRaceBoard(raceboardContextDefinition, + raceBoardMode != null ? raceBoardMode.name() : null); + final RaceBoardPerspectiveLifecycle lifeCycle = new RaceBoardPerspectiveLifecycle(leaderboard, + StringMessages.INSTANCE, chartDetailTypes, getUserService(), leaderboardDetailTypes); + RaceBoardComponentContext componentContext = new RaceBoardComponentContext(lifeCycle, getUserService(), + storageDefinition); + callback.accept(lifeCycle, componentContext); + } + + private void loadInitialSettings(final ComponentContext ctx, final Consumer callback) { + ctx.getInitialSettings(new DefaultOnSettingsLoadedCallback() { + + @Override + public void onSuccess(S settings) { + callback.accept(settings); + } + }); + } + + private void createPerspective(final RaceBoardModes raceBoardMode, ComponentContextWithSettingsStorage> context, PerspectiveCompositeSettings settings, RaceboardDataDTO raceboardData, - boolean showChartMarkEditMediaButtonsAndVideo, RaceBoardPerspectiveLifecycle raceLifeCycle, Timer timer, - Iterable availableDetailTypes) { - selectedRace = raceboardData.getRace(); + RaceBoardPerspectiveLifecycle raceLifeCycle, Iterable availableDetailTypes) { + final Timer timer = new Timer(PlayModes.Replay, 1000l); + final boolean showChartMarkEditMediaButtonsAndVideo = !DeviceDetector.isMobile(); + + final RaceWithCompetitorsAndBoatsDTO selectedRace = raceboardData.getRace(); Window.setTitle(selectedRace.getName()); AsyncActionsExecutor asyncActionsExecutor = new AsyncActionsExecutor(); - RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(getSailingService(), asyncActionsExecutor, this, - Collections.singletonList(selectedRace.getRaceIdentifier()), 5000l /* requestInterval*/); - RaceBoardPanel raceBoardPerspective = new RaceBoardPanel(parent, context, raceLifeCycle, settings, + RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(getSailingService(), + asyncActionsExecutor, this, Collections.singletonList(selectedRace.getRaceIdentifier()), + 5000l /* requestInterval */); + RaceBoardPanel raceBoardPerspective = new RaceBoardPanel(/* parent */ null, context, raceLifeCycle, settings, getSailingService(), mediaService, getUserService(), asyncActionsExecutor, raceboardData.getCompetitorAndTheirBoats(), timer, selectedRace.getRaceIdentifier(), raceboardContextDefinition.getLeaderboardName(), raceboardContextDefinition.getLeaderboardGroupName(), raceboardContextDefinition.getEventId(), RaceBoardEntryPoint.this, getStringMessages(), userAgent, raceTimesInfoProvider, showChartMarkEditMediaButtonsAndVideo, true, availableDetailTypes); RootLayoutPanel.get().add(raceBoardPerspective.getEntryWidget()); - return raceBoardPerspective; + + if (raceBoardMode != null) { + raceBoardMode.getMode().applyTo(raceBoardPerspective); + } } @Override public String getLeaderboardName() { - return leaderBoardName; + return raceboardContextDefinition.getLeaderboardName(); + } + + private abstract class AbstractRaceBoardAsyncCallback implements AsyncCallback { + @Override + public final void onFailure(Throwable caught) { + reportError("Error trying to create the raceboard: " + caught.getMessage()); + } } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPerspectiveLifecycle.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPerspectiveLifecycle.java index 7125da14d0b..f0f860d7cd0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPerspectiveLifecycle.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPerspectiveLifecycle.java @@ -28,13 +28,6 @@ public class RaceBoardPerspectiveLifecycle extends AbstractPerspectiveLifecycle< public static final String ID = "rb"; - //constructor used by Standalone RaceBoard - public RaceBoardPerspectiveLifecycle(StringMessages stringMessages, - Iterable competitorChartAllowedDetailTypes, UserService userService, - Iterable availableDetailTypes) { - this(null, stringMessages, competitorChartAllowedDetailTypes, userService, availableDetailTypes); - } - public RaceBoardPerspectiveLifecycle(AbstractLeaderboardDTO leaderboard, StringMessages stringMessages, Iterable competitorChartAllowedDetailTypes, UserService userService, Iterable availableDetailTypes) { @@ -42,7 +35,8 @@ public class RaceBoardPerspectiveLifecycle extends AbstractPerspectiveLifecycle< raceMapLifecycle = new RaceMapLifecycle(stringMessages); windChartLifecycle = new WindChartLifecycle(stringMessages); maneuverTableLifecycle = new ManeuverTableLifecycle(stringMessages); - leaderboardPanelLifecycle = new SingleRaceLeaderboardPanelLifecycle(stringMessages, availableDetailTypes); + leaderboardPanelLifecycle = new SingleRaceLeaderboardPanelLifecycle(stringMessages, availableDetailTypes, + leaderboard.canBoatsOfCompetitorsChangePerRace); multiCompetitorRaceChartLifecycle = new MultiCompetitorRaceChartLifecycle(stringMessages, competitorChartAllowedDetailTypes); mediaPlayerLifecycle = new MediaPlayerLifecycle(stringMessages); raceTimePanelLifecycle = new RaceTimePanelLifecycle(stringMessages, userService); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SingleRaceLeaderboardPanelLifecycle.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SingleRaceLeaderboardPanelLifecycle.java index d75f0c8245f..6e06ca8f3eb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SingleRaceLeaderboardPanelLifecycle.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SingleRaceLeaderboardPanelLifecycle.java @@ -16,10 +16,13 @@ public class SingleRaceLeaderboardPanelLifecycle extends LeaderboardPanelLifecyc private static final long DEFAULT_REFRESH_INTERVAL = 1000L; private final boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard; + private final boolean canBoatsOfCompetitorsChangePerRace; - public SingleRaceLeaderboardPanelLifecycle(StringMessages stringMessages, Iterable availableDetailTypes) { + public SingleRaceLeaderboardPanelLifecycle(final StringMessages stringMessages, + final Iterable availableDetailTypes, final boolean canBoatsOfCompetitorsChangePerRace) { super(stringMessages, availableDetailTypes); this.isScreenLargeEnoughToInitiallyDisplayLeaderboard = Document.get().getClientWidth() >= 1024; + this.canBoatsOfCompetitorsChangePerRace = canBoatsOfCompetitorsChangePerRace; } @Override @@ -31,27 +34,24 @@ public class SingleRaceLeaderboardPanelLifecycle extends LeaderboardPanelLifecyc raceDetails.add(DetailType.NUMBER_OF_MANEUVERS); raceDetails.add(DetailType.RACE_DISPLAY_LEGS); List overallDetails = new ArrayList<>(); - SingleRaceLeaderboardSettings defaultSettings = new SingleRaceLeaderboardSettings(); + SingleRaceLeaderboardSettings defaultSettings = new SingleRaceLeaderboardSettings(canBoatsOfCompetitorsChangePerRace); + // don't show competitor fullName column if even leaderboard isn't shown initially + final boolean showCompetitorFullNameColumn = isScreenLargeEnoughToInitiallyDisplayLeaderboard; SingleRaceLeaderboardSettings settings = new SingleRaceLeaderboardSettings( defaultSettings.getManeuverDetailsToShow(), defaultSettings.getLegDetailsToShow(), defaultSettings.getRaceDetailsToShow(), overallDetails, DEFAULT_REFRESH_INTERVAL, defaultSettings.isShowAddedScores(), /* showCompetitorShortNameColumn */ true, - /* - * don't showCompetitorFullNameColumn in case screen is so small that we don't even display the - * leaderboard initially - */ isScreenLargeEnoughToInitiallyDisplayLeaderboard, - /* showCompetitorBoatInfoColumn */ true, false, false); + showCompetitorFullNameColumn, + /* showCompetitorBoatInfoColumn */ canBoatsOfCompetitorsChangePerRace, + /* isCompetitorNationalityColumnVisible */ false, + /* showRaceRankColumn */ false); SettingsUtil.copyDefaultsFromValues(settings, settings); return settings; } - public boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard() { - return isScreenLargeEnoughToInitiallyDisplayLeaderboard; - } - @Override public SingleRaceLeaderboardSettings extractDocumentSettings(SingleRaceLeaderboardSettings currentLeaderboardSettings) { SingleRaceLeaderboardSettings defaultLeaderboardSettings = createDefaultSettings(); diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/spi/MasterDataResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/spi/MasterDataResource.java index 793c1a068cd..0bd41e30623 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/spi/MasterDataResource.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/spi/MasterDataResource.java @@ -91,7 +91,7 @@ public class MasterDataResource extends AbstractSailingServerResource { getService().getSensorFixStore(), exportWind, deviceConfigurations); final StreamingOutput streamingOutput; if (compress) { - streamingOutput= new CompressingStreamingOutput(masterData, competitorIds, startTime); + streamingOutput = new CompressingStreamingOutput(masterData, competitorIds, startTime); } else { streamingOutput = new NonCompressingStreamingOutput(masterData, competitorIds, startTime); } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/DOMUtils.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/DOMUtils.java new file mode 100644 index 00000000000..0e0f8ef8183 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/DOMUtils.java @@ -0,0 +1,40 @@ +package com.sap.sse.gwt.client; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.ui.Widget; + +/** + * Utility class contained convenience methods regarding DOM operations. + */ +public final class DOMUtils { + + private DOMUtils() { + } + + /** + * Scrolls the top of the provided {@link Widget widget} to the topmost possible position in the viewport. + * + * @param widget + * {@link Widget} to scroll to the topmost possible position + */ + public static void scrollToTop(final Widget widget) { + getViewportElement().setScrollTop(widget.getAbsoluteTop()); + } + + /** + * Scrolls the top of the provided {@link Element element} to the topmost possible position in the viewport. + * + * @param element + * {@link Element} to scroll to the topmost possible position + */ + public static void scrollToTop(final Element element) { + getViewportElement().setScrollTop(element.getAbsoluteTop()); + } + + private static Element getViewportElement() { + final Document doc = Document.get(); + return doc.isCSS1Compat() ? doc.getDocumentElement() : doc.getBody(); + } + +} \ No newline at end of file diff --git a/java/com.sap.sse/src/com/sap/sse/util/apachelog/LogEntry.java b/java/com.sap.sse/src/com/sap/sse/util/apachelog/LogEntry.java index 01f885db337..42aa9396098 100755 --- a/java/com.sap.sse/src/com/sap/sse/util/apachelog/LogEntry.java +++ b/java/com.sap.sse/src/com/sap/sse/util/apachelog/LogEntry.java @@ -36,7 +36,7 @@ public class LogEntry { public LogEntry(String logEntry) { final Iterator i = Util.splitAlongWhitespaceRespectingDoubleQuotedPhrases(logEntry).iterator(); - hostname = i.next(); + hostname = i.next().trim(); requestorIpString = i.next(); remoteLogname = i.next(); remoteUser = i.next(); diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/services/RaceStateEventSchedulerOnService.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/services/RaceStateEventSchedulerOnService.java index 29ac1d61ec8..0a22ca93ba4 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/services/RaceStateEventSchedulerOnService.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/services/RaceStateEventSchedulerOnService.java @@ -1,7 +1,5 @@ package com.sap.sailing.racecommittee.app.services; -import java.util.Collection; - import com.sap.sailing.domain.abstractlog.race.state.RaceStateEvent; import com.sap.sailing.domain.abstractlog.race.state.RaceStateEventScheduler; import com.sap.sailing.domain.abstractlog.race.state.impl.RaceStateEvents; @@ -18,7 +16,7 @@ public class RaceStateEventSchedulerOnService implements RaceStateEventScheduler } @Override - public void scheduleStateEvents(Collection stateEvents) { + public void scheduleStateEvents(Iterable stateEvents) { for (RaceStateEvent stateEvent : stateEvents) { service.setAlarm(race, stateEvent); } diff --git a/wiki/howto/onboarding.md b/wiki/howto/onboarding.md index afbd2b303da..eaa122384b4 100644 --- a/wiki/howto/onboarding.md +++ b/wiki/howto/onboarding.md @@ -13,8 +13,8 @@ First of all, make sure you've looked at [http://www.amazon.de/Patterns-Elements 3. Git (e.g. Git for Windows v2.18), [http://git-scm.com](http://git-scm.com) / [https://git-for-windows.github.io](https://git-for-windows.github.io) 4. MongoDB (e.g. Production Release 2.6.7), download: [https://www.mongodb.com/](https://www.mongodb.com/) 5. RabbitMQ, download from [http://www.rabbitmq.com](http://www.rabbitmq.com). Requires Erlang to be installed. RabbitMQ installer will assist in installing Erlang. Some sources report that there may be trouble with latest versions of RabbitMQ. In some cases, McAffee seems to block the installation of the latest version on SAP hardware; in other cases connection problems to newest versions have been reported. We know that version 3.6.8 works well. [https://github.com/rabbitmq/rabbitmq-server/releases/tag/rabbitmq_v3_6_8](https://github.com/rabbitmq/rabbitmq-server/releases/tag/rabbitmq_v3_6_8) is the link. -6. JDK 1.7 (Java SE 7), [http://www.oracle.com/technetwork/java/javase/downloads/java-archive-downloads-javase7-521261.html](http://www.oracle.com/technetwork/java/javase/downloads/java-archive-downloads-javase7-521261.html) -7. JDK 1.8 (Java SE 8), [http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) +6. JDK 1.7 (Java SE 7), [http://www.oracle.com/technetwork/java/javase/downloads/java-archive-downloads-javase7-521261.html](http://www.oracle.com/technetwork/java/javase/downloads/java-archive-downloads-javase7-521261.html) --- Alternatively you can use the SAPJVM 1.7: Go to [http://sapjvm.wdf.sap.corp:1080/downloads](http://sapjvm.wdf.sap.corp:1080/downloads), select JVM 1.7, extract the downloaded .zip into desired location (e.g. C:\Program Files\Java), then Go to Window -> Preferences -> Java -> Installed JREs and add the VM. +7. JDK 1.8 (Java SE 8), [http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) --- Alternatively you can use the SAPJVM 1.8: Go to [http://sapjvm.wdf.sap.corp:1080/downloads](http://sapjvm.wdf.sap.corp:1080/downloads), select JVM 1.8, extract the downloaded .zip into desired location (e.g. C:\Program Files\Java), then Go to Window -> Preferences -> Java -> Installed JREs and add the VM. 8. Maven 3.1.1 (or higher), [http://maven.apache.org](http://maven.apache.org) 9. GWT SDK 2.8.2 ([http://www.gwtproject.org/download.html](http://www.gwtproject.org/download.html)) 10. Standalone Android SDK (see section "Additional steps required for Android projects"). OPTIONALLY: You may additionally install Android Studio ([https://developer.android.com/tools/studio/index.html](https://developer.android.com/tools/studio/index.html)) or IntelliJ IDEA ([https://www.jetbrains.com/idea/download/](https://www.jetbrains.com/idea/download/)). @@ -119,6 +119,7 @@ Copy the settings.xml **and** the toolchains.xml from the top-level git folder t #### Further hints - Configure Eclipse to use Chrome or Firefox as the default browser - Install the GWT Browser Plugin for the GWT Development mode (Chrome or Firefox; as of this writing (2013-11-05), Firefox is the only platform where the plug-in runs stably) (As of 2016-08-31 Firefox is the only browser supporting the GWT plugin, you have to download Firefox version 24 for it to work) +- Use SAP JVM Profiler. If you used the script above and installed the SAPJVM instead of the jdk, you can now open the profiling perspective by clicking on Window -> Perspective -> Open Perspective -> Profiling) #### Git usage troubleshooting diff --git a/wiki/images/amazon/CopyingAlbDnsName.png b/wiki/images/amazon/CopyingAlbDnsName.png new file mode 100755 index 00000000000..b89bc5626f6 Binary files /dev/null and b/wiki/images/amazon/CopyingAlbDnsName.png differ diff --git a/wiki/images/amazon/DNS1.png b/wiki/images/amazon/DNS1.png new file mode 100755 index 00000000000..b65e132cd64 Binary files /dev/null and b/wiki/images/amazon/DNS1.png differ diff --git a/wiki/images/amazon/DNS2.png b/wiki/images/amazon/DNS2.png new file mode 100755 index 00000000000..841c53e4290 Binary files /dev/null and b/wiki/images/amazon/DNS2.png differ diff --git a/wiki/images/amazon/DNS3.png b/wiki/images/amazon/DNS3.png new file mode 100755 index 00000000000..aec99b63a8f Binary files /dev/null and b/wiki/images/amazon/DNS3.png differ diff --git a/wiki/images/amazon/DNS4.png b/wiki/images/amazon/DNS4.png new file mode 100755 index 00000000000..04792da7097 Binary files /dev/null and b/wiki/images/amazon/DNS4.png differ diff --git a/wiki/info/landscape/amazon-ec2.md b/wiki/info/landscape/amazon-ec2.md index 1f2668dec92..2b5a4d78ee8 100644 --- a/wiki/info/landscape/amazon-ec2.md +++ b/wiki/info/landscape/amazon-ec2.md @@ -430,6 +430,12 @@ In a live event scenario, the SAP Sailing Analytics are largely bandwidth bound. To still get the usual logging and URL re-writing features, replicas need to run their local Apache server with a bit of configuration. Luckily, most of the grunt work is done for you automatically. You simply need to tell the replicas in their instance details to start replicating automatically, provide an `EVENT_ID` and set the `SERVER_NAME` variable properly. The Apache configuration on the replica will then automatically be adjusted such that the lower-case version of $SERVER_NAME.sapsailing.com will re-direct users to the event page for the event with ID $EVENT_ID. +Amazon puts up limits regarding to the maximum number of rules that an Application Load Balancer (ALB) may have. We use one such ALB as the DNS CNAME target for ``*.sapsailing.com`` (Sailing-eu-west-1-135628335.eu-west-1.elb.amazonaws.com). Adding rules to this ALB is especially convenient because no DNS / Route53 manipulation is necessary at all. New sub-domains can be mapped to target groups this way quite flexibly and quickly. + +However, as the number of sub-domains we use grows, we also approach the limit of 100 rules for this load balancer. In order to keep this flexibility in particular for event set-ups, we started introducing more ALBs in August 2018 that use dedicated Route 53 DNS CNAME records for sepcific sub-domains. This way, with the current AWS limits for load balancers (see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-limits.html) we will have up to 20 ALBs per region with 100 rules each, giving us 2000 rules per region which should suffice for the foreseeable future. + +The set-up process needs to distinguish now between only adding a rule to an ALB listener targeted by the ``*.sapsailing.com`` DNS entry, and adding a rule to an ALB listener targeted only by DNS rules for specific sub-domains. In the latter case, a DNS record set needs to be created, providing the CNAME of the ALB that maps the sub-domain to the target group. + Here are the steps to create a load balanced setup, assuming there is already an "Application" load balancer defined in the region(s) where you need them: - Add a master+replica target group for the master and its replicas that external users will be directed to, using HTTP port 80 as the protocol settings. Note: as this target group will also be used for the HTTPS listener, "SSL offloading" will take place here. The re-directing from HTTP to HTTPS that shall occur when the user hits the server with an HTTP request will happen in the central instance's Apache server if and only if the `X-Forwarded-Proto` is `http` (https://stackoverflow.com/questions/26620670/apache-httpx-forwarded-proto-in-htaccess-is-causing-redirect-loop-in-dev-envir explains how a. See also http://docs.aws.amazon.com/elasticloadbalancing/latest/classic/x-forwarded-headers.html#x-forwarded-proto.) @@ -441,7 +447,22 @@ Here are the steps to create a load balanced setup, assuming there is already an - Add a rule to the HTTPS listener for the hostname ${SERVER_NAME}-master.sapsailing.com that forwards traffic to the master-only target group just created. - Add the master to the master-only target group. - For both target groups configure the health checks, choosing HTTP as the protocol, using the default "traffic port" and setting the path to /index.html. Lower the interval to 10s and the "Healthy threshold" to 2 to ensure that servers are quickly recognized after adding them to the ELB. With the default settings (30 seconds interval, healthy threshold 10) this would last up to 5 minutes. -- When using the Race Committee App (RCApp), make sure the app is configured to send its data to the ${SERVER_NAME}-master.sapsailing.com URL (otherwise, write requests may end up at replicas which then have to reverse-replicate these to the master which adds significant overhead). +- When using the Race Committee App (RCApp), make sure the app is configured to send its data to the ${SERVER_NAME}-master.sapsailing.com URL (otherwise, write requests may end up at replicas which then have to reverse-replicate these to the master which adds significant overhead). + +The steps to register such a sub-domain mapping also in Route53 in case you've chosen an ALB that is not the target of ``*.sapsailing.com`` work as follows: + +Start by creating a new record set: + + +Then enter the sub-domain name you'd like to map. Choose ``CNAME`` for the type, reduce the default TTL to 60s and paste the DNS name of the ALB you'd like to target: + + +The DNS name of your load balancer can be copied from the "Basic Configuration" section in the "Description" tab: + + +The insertion of the rule into the ALB that maps your sub-domain's name to the corresponding target group works as usual and as described above: + + It is important to understand that it wouldn't help to let all traffic run through our central Apache httpd server which usually acts as a reverse proxy with comprehensive URL rewriting rules and macros. This would make the Apache server the bandwidth bottleneck. Instead, the event traffic needs to go straight to the ELB. This is established by the *.sapsailing.com DNS entry pointing to the Application ELB which then applies its filter rules to dispatch to the URL-specific target groups. Other than adding the hostname filter rules in the ELB as described above, no interaction with the Route 53 DNS is generally needed. Neither is it necessary to manually modify any 001-events.conf Apache configuration file. diff --git a/wiki/projects/cloud-orchestrator.md b/wiki/projects/cloud-orchestrator.md new file mode 100644 index 00000000000..0fdf6024670 --- /dev/null +++ b/wiki/projects/cloud-orchestrator.md @@ -0,0 +1,226 @@ +# Cloud Orchestration - Project Plan + +[[_TOC_]] + +The cloud landscape used to run the SAP Sailing Analytics as well as its development infrastructure has grown increasingly comprehensive and elaborate over the last few years. It has largely been configured through manual efforts on behalf of event and infrastructure operators and administrators. + +With a growing number of events---be it through sailing leagues or championship events in one or more classes---and with a plan to scale the SAP Sailing Analytics to a broader audience through the use of the SAP Race Manager and the SAP Sail InSight apps, more automation is required for the cloud infrastructure. + +We believe that a central *orchestrator* approach should be used to solve this challenge. Such an orchestrator, a bit like outlined in the figure below, will accept requests immediately from authenticated and authorized users through a Web interface, as well as from processes running on other servers in the cloud, through REST APIs on behalf of authenticated and authorized users. + +![](https://wiki.sapsailing.com/wiki/images/orchestration/architecture.png) + +## Orchestrator Architecture + +In order to authenticate and authorize user requests the orchestrator will benefit from a powerful security infrastructure. The *Shiro* framework that is being used by the SAP Sailing Analytics has proven to be sufficiently configurable, powerful and extensible to support all our needs. It seems desirable to share the security service between the orchestrator and the application servers in the landscape. This will allow us to extend landscape-related permissions to users that can already be authenticated by the application and which can then make requests to the orchestrator, such as providing a new event with a new dedicated database, install a sub-domain prefix with the corresponding load balancer settings, or move an event to a dedicated server/replica cluster with its own scalability limits based on the user's credentials. + +There will also need to be a fair amount of negotiations between the application instances and the orchestrator, relating domain aspects such as event dates, numbers of competitors, number of races to be tracked and the number of regattas, with infrastructure provisioning aspects such as server and network capacities required to support the application set-up. + +### Java OSGi + +This, together with the skill set profile and the set of components found in our team, suggests the use of the architecture that proved successful during the construction of the SAP Sailing Analytics themselves. The orchestrator shall be developed as a Java OSGi application that uses the existing *com.sap.sse...* bundles for basic aspects such as security (Shiro), replication, mail services and replication (e.g., for replicating the central SecurityService with its user store). + +The orchestrator could become the host for the central user store / SecurityService that other application instances replicate by means of partial replication. + +Managing an Amazon Web Services (AWS) Elastic Cloud Computing (EC2) landscape will be carried out through Amazon's AWS Java SDK. + +### Google Web Toolkit (GWT) + +The Web UI of the orchestrator shall be built using the Google Web Toolkit (GWT), using the AdminConsole components from *com.sap.sse.gwt.adminconsole* as well as further GWT UI support from *com.sap.sse.gwt*. This will also allow us to use the *UserManagementPanel* in the administration UI. + +### JAX-RS for REST + +The REST APIs will be built according to the same patterns of OSGi Web Bundles that use JAX-RS as the implementation pattern for the servlets implementing the APIs. Other than for the grown application domain model, here we may choose to use the *Jackson* framework in a controlled way to pass structured information back and forth between application instances and the orchestrator. + +The REST APIs will use the existing authentication/authorization realm implementation that we use already in the application in just the same way that the security-enabled REST endpoints that already exist for the SAP Sailing Analytics do. + +### In-Memory with Write-Through Persistence with MongoDB + +A running orchestrator instance shall not need to access a persistence layer for reading requests. Just like the applications, the orchestrator has to be able to launch / re-launch with an already established landscape. In this case, the orchestrator will need to be able to find out about the current status of the landscape it is expected to orchestrate. Instead of having to explore the world-wide cloud landscape to find instances the orchestrator may be expected to orchestrate, a persistence layer will help it to recover the last managed status. + +Instead of introducing another persistence architecture, the orchestrator should use the existing MongoDB persistence pattern. It has proven useful for the scenarios we have in mind. The orchestrator will not have to deal with excessive write loads, and data volumes we expect to remain in the few megabytes range at most as all these data need to describe are the instances and their roles. + +### Logging + +The orchestrator needs to log in detail on behalf of which user which landscape interaction has taken place. In particular, this must include all cost-driving interactions such as launching instances, and creating load balancers. The log can be a *java.util.logging* (*jul*) log where the set-up needs to ensure that no log is ever lost upon rotation. www.sapsailing.com:/var/log/old could be the host for those logs as it is the ever-growing place for all sorts of logs we never want to lose. + +## Orchestration Elements + +### Application Load Balancers (ALBs) + +Up to 20 application load balancers (ALBs) can be used in a single AWS region. Each of them can have up to 100 rules that map requests to a target group based on hostname and URL path patterns. We use a default ALB that is the CNAME target for **.sapsailing.com*, and as such allows the orchestrator to add rules to it without the need to add new DNS records to Route53. Dedicated ALBs can be used in addition to circumvent the limit of 100 rules per ALB. To ensure those dedicated ALBs are targeted by user requests, DNS records for the sub-domains that those ALBs have rules for need to be added to Route53. + +All ALBs need default rules that forward requests to the archive server (production or fail-over). + +Currently, several sub-domain names exist (e.g., wiki, bugzilla and hudson) that are also handled by the default ALB, forwarding to our central webserver which then acts as a reverse proxy, using rewrite rules to redirect the clients to the specific hosts running those services. We may consider changing this and introduce DNS records for those service-based sub-domains, mapping them explicitly to the elastic IP of the central webserver. This would allow us to separate the Apache httpd instances for those central services from the httpd instances used for the archive servers. + +### Target Groups + +Those hold the nodes responding to requests. In a replication cluster we usually want to have one for the "-master" URL that event administrators use + +### Volumes + +There are a number of disk volumes that are critical to monitor and, where necessary, scale. This includes: + +- Backup server (*/home/backup*) +- Database server (*/var/lib/mongodb* and sub-mounts, */var/lib/mysql*) +- Central Webserver (*/var/log/old*, */var/log/old/cache*, */var/www/static*, */var/www/home* hosting the git repository) + +Through the AWS API the read/write throughputs can be observed, and peaks as well as bottlenecks may be identified. At least as importantly, the file system fill state has to be observed which is not possible through the AWS API and needs to happen through the respective instances' operating systems. + +It shall be possible to define alerts based on file systems whose free space drops below a given threshold. Re-sizing volumes automatically may be an interesting option in the future. + +### Alerts + +Alerts can be defined for different metrics and with different notification targets. SMS text messages and e-mail notifications are available. + +The orchestrator shall be able of managing such alerts. It shall be possible to attach such alerting rules to an entire replication cluster, meaning that all target groups used for managing the cluster shall receive the same monitoring and alerting rule (see also [Sharding](#sharding)). + +### DNS Record Sets + +As the number of rules per load balancer is limited to 100 as of now, and only one ALB can be set up to handle **.sapsailing.com*, this ALB can only be used for the volatile, fast-changing sub-domain names and event-driven set-ups, given there are not even too many of those happening concurrently to exceed the limit. + +All other, specifically the longer-running, sub-domains shall be mapped through a dedicated Route53 DNS entry in the *sapsailing.com* hosted zone. Up to 20 ALBs can be created per region, and each of those can have up to 100 rules. This should suffice for some time to come. We should also consider asking users to pay for the special service of a dedicated sub-domain in the future. + +Those dedicated DNS entries then point to a non-default ALB that has the rules for that sub-domain. Those ALBs also default to the archive server and central web server if no other rule matches. Should a sub-domain's content be archived, the DNS entry can be removed. The default rules of both, the dedicated ALB and the default ALB for **.sapsailing.com* will forward requests to the archive server. + +### Apache Web Servers + +The landscape relies on Apache *httpd* web servers mostly for two reasons: + +- consolidated, harmonized logging of web requests + +- powerful reverse proxying with redirect macros and SSL support + +All httpd instances have common elements for SSL certificate configuration as well as a set of redirect and rewrite macros that are aware of the application and its URL configurations, for example Plain-SSL-Redirect, Home-SSL-Redirect, Series-SSL-Redirect, and Event-SSL-Redirect. These macros can then be used to rewrite requests made for a base URL such as *worlds2018.sapsailing.com* to the corresponding event landing page. + +Other macros set up end points for server monitoring (*/internal-server-status*) and for the health checks performed by the load balancer's target group which uses the host's internal IP address as the request server name. + +All our hosts can run their own Apache httpd process. Currently, the two archive servers don't. Instead, they rely on the single central www.sapsailing.com webserver which hosts all redirect macro usages for all sub-domains that shall be handled by the archive server. This should probably be changed in the future, such that each archive server runs its own httpd process. It would remove the single point of failure that the current central webserver represents and may make room for a more clever set-up where the fail-over archive server is handled by a target group that is the default in the ALB rule set even after the default rule that points at the target group for the production archive server. This way, together with an alarm defined, failover to the secondary archive server could be automatic and instant. + +The interface for such a web server will need to allow the orchestrator to add and remove such rewrite macro usages, configure the macro parameters such as the event ID to use for the Event-SSL-Redirect macro, and to tell the *httpd* process to re-load its configuration to make any changes effective. + +### Java Application Instances and their Health + +Application nodes have to provide a REST API with reliable health information. + +A replica is not healthy while its initial load is about to start, or is still on-going or its replication queue is yet to drain after the initial load has finished. A replica will take harm from requests received while the initial load is received or being processed. Requests may be permitted after the initial load has finished processing and while the replication queue is being drained, although the replica will not yet have reached the state that the master is in. + +A master is not healthy until it has finished loading all data from the MongoDB during the activation of the *RacingEventService*. It will take harm from requests received during this loading phase. After loading the "master data," a master server will try to restore all race trackers, starting to track the races for which a so-called "restore record" exists. During this phase the master is not fully ready yet but will not take harm from requests after loading all master data has completed. For example, in an emergency situation where otherwise the replication cluster would be unavailable it may be useful to already direct requests at such a master to at least display a landing page or a meaningful error page. + +The Java instances have a few and shall have more interesting observable parameters. Some values can be observed through the AWS API, such as general network and CPU loads. So far, the number of leaderboards and the restore process can be observed using JMX managed beans, as can be seen in the JConsole. Other interesting parameters to observe by the orchestrator would be the memory usage and garbage collection (GC) stats, as well as information about the thread pools, their usage and their contention. It would be great if the orchestrator could find out about bottlenecks and how they may be avoided, e.g., hitting bandwidth limitations or not having enough CPUs available to serialize data fast enough for the bandwidth available and the demand observed. + +### Multi-Instances + +A "multi-instance" is a single EC2 host that runs several application processes that share the physical memory and a large locally-attached SSD (typically 2TB or more) used for swap space. Each of these processes has its own configuration in an *env.sh* file which provides the process with a server name, the amount of heap space memory to reserve, a specific MongoDB configuration (DB name, host, port), a RabbitMQ configuration in case the process shall scale with replicas, a port through which the embedded Jetty web server will be reachable, a telnet port at which the OSGi console registers itself, as well as a UDP port for the Expedition connector. + +Each server process has its own directory under */home/sailing/servers* that is named after the server name. Should a multi-instance host be re-booted, it will launch all application processes it finds under */home/sailing/servers*. This is important to keep in mind for the scenario of temporarily or permanently migrating a node from a multi-instance set-up to a dedicated replication cluster set-up. + +Scaling a multi-instance with replication has not yet been exercised. It may pose a few new challenges due to the replicas likely serving the application on a standard port (usually *8888*) as opposed to the node on the multi-instance host which uses a different port. In this case, separate target groups for the master and the replicas will be required, and the master will not be able to register with the public-facing target group which then only can contain replicas. + +### Dedicated Instances + +A dedicated server will typically be used for an event whose size in terms of competitors and races tracked and the number of users concurrently watching those races live exceeds what we think a multi-instance set-up can reasonably carry. Dedicated CPU and memory resources will ensure that such a live event runs smoothly and can scale elastically, independent of other nodes that would be running on the same host in a multi-instance set-up. + +As in the multi-instance case, replication can be used for dedicated instances. Here, since the dedicated instance will also run on the default port, optionally the master and the replica instances can all be put into the public-facing load balancer target group. + +A dedicated instance will grab the amount of RAM it reasonably can with the physical RAM provided by the EC2 host it is deployed to. Its HTTP port will default to 8888, the telnet port to 14888 and the Expedition UDP port to 2010. Like a multi-instance it has a server name as well as a MongoDB and RabbitMQ configuration. + +### Master Data Import (MDI) + +An instance can run a Master Data Import (MDI) for one or more leaderboard groups from another server instance. The typical scenario is that of importing an event into the archive server, but other scenarios may be conceived. + +We will need a secured API to trigger an MDI on a server instance. Beyond this, connectivity data needs to be obtained from the exporting server so that all tracked races can be restored on the importing server. Afterwards, a comparison has to be made, ensuring that all data has arrived correctly on the importing side. + +Part of such a scenario can also be to manage the remote server references on the importing server: should the importing instance be the archive server and should the archive server have had a remote server reference to the exporting server, this reference can be cleared after the import has succeeded. + +### Archive Servers + +We run a production copy of the "archive server" that hosts selected events for which we decide that they have sufficient quality to be promoted through sapsailing.com's landing page. Most events sponsored by SAP fall into this category. The archive server is the target of a "Master Data Import" (MDI) after an event is finished, where data is moved from a dedicated event server into the archive. + +As the archive server requires several hours for a re-start, a failover instance exists that usually runs a release that is not as new as the one on the production archive server. The reason for this choice of release is that in case we run into a regression that tests have not revealed, switching to the failover archive allows us to "revert" to a release that hopefully does not have this regression while we then have some time to fix the issue on the production archive server. + +The failover archive server may be used for special purposes such as tricky data-mining queries that we don't want to affect the performance of the production archive server, or acting as an additional Hudson build slave because its CPUs are usually idling. + +The orchestrator shall be aware of the two archive server instances and shall know which one is the production and which one the fail-over copy. It should have an awareness of the releases installed and should offer the release upgrade as an action that will install the latest release to the fail-over copy, restart it, wait for it to become "healthy" and then compare the contents with the production archive server. If all runs well, routing/ALB/Apache rules can be switched to swap production and fail-over copy. + +There should in the future be dedicated target groups for production and fail-over archive servers that can be used to define an automatic fail-over order in the ALB. A severe alert should be sent out if the production archive target group has no healthy target. + +### Sharding + +Requests to the */gwt/service/sailing* RPC service that cause calculations for a specific leaderboard are suffixed with the leaderboard name to which they are specific. Example: */gwt/service/sailing/leaderboard/Sailing_World_Championships_Aarhus_2018___Laser_Radial*. The leaderboard name undergoes escaping of special characters which are then replaced by an underscore character each. + +With this approach, a load balancer can use the URL path with the escaped leaderboard name as criterion in a rule that dispatches requests to target groups based on the leaderboard. This way, although all replicas in a replication cluster maintain an equal memory content, calculations in a replica can be constrained to only a subset of the leaderboards the replica maintains. + +Without this mechanism, all replicas in a replication cluster would be targeted with requests for all leaderboards in a random, round-robin fashion, leading to all replicas running the live calculations for all leaderboards redundantly. As more replicas are added to a replication cluster for a domain with several live leaderboards, using this sharding feature makes more sense because the recalculation load can be split evenly across the replicas. + +A target group for each subset of leaderboards has to be created. The ALB will then have to have a rule for each leaderboard name, deciding the target group to which to route requests for that leaderboard. A default target group for the replication cluster shall exist which catches all other requests and which becomes the default in case a sharding target group runs out of healthy hosts. + +### MongoDB Databases + +We currently have a single server node *dbserver.internal.sapsailing.com* on which there are four MongoDB processes running. Three of those are for testing (*dev.sapsailing.com* uses the process listening on port 10201) or legacy purposes, and only one, listening on port 10202, has all the content relevant for the entire production landscape. + +This MongoDB process hosts various MongoDB databases, each with their separate names and separate collections. With the exception of the production and fail-over archive servers which use the *winddb* database, all other master servers use a DB name that should equal that of the server name which usually is a technical short name for the event. Example: *KW2018* represents the server and DB name for the *Kieler Woche 2018* event. + +Replication clusters use a second database that is used by all replicas in the cluster. Right now, replicas don't interact in any well-defined way with the database, and we say that a database accessed by a replica is not in a well-defined state. Replicas don't read from their database which is the reason why this does not matter. For example, in addition to the *KW2018* database there is a *KW2018-replica* database used by all replicas in the *KW2018* replication cluster. + +The MongoDB host currently has a fast EBS volume attached to which it stores all the databases. There is only a single instance in the landscape, making this currently a single point of failure. We should consider changing this. + +The orchestrator shall know about the MongoDB instances we run in the landscape, shall know their ports and hosts and thereby shall be able to monitor in particular the disk volume holding the DB contents. Alerts should be put in place in case those volumes reach critical fill states. In the future, the orchestrator should learn to run MongoDB in various regions, optionally in replicated mode also across regions, making it easier for us to deploy master/replica set-ups across regions. There may even be MongoDB instances in each region that are not part of the cluster that are used only for the phony replica databases. + +### RabbitMQ Servers and Exchanges + +Replication is based on RabbitMQ which is used to establish a channel through which the initial load can be transferred safely even under unstable network conditions, and through which the shipping of operations from master to replicas happens, using a "fanout" exchange. + +We currently have only a single RabbitMQ instance in the landscape that hosts all exchanges and all initial load queues. As such, it represents a single point of failure. + +The orchestrator should know about all RabbitMQ instances in the landscape and should know how its exchanges relate to replication clusters, and how its queues relate to the master and replica instances in the landscape. It can thus observe whether all those objects are cleanly removed again when replicas or replication clusters are dismantled and can, if necessary, clean up remnants. + +### Backup Server + +A single EC2 instance called "Backup" with large EBS disk volumes attached hosts various forms of backups. We use *bup*, a variant of *git* capable of dealing well with large binary files. This gives us historizing backups where each version may be fully restored. + +Subject to backup are all production MongoDB databases, the MySQL database content for our Bugzilla server, as well as the file system of the central Webserver, there in particular the configuration and log files. + +The backup script on *dbserver.internal.sapsailing.com:/opt/backup.sh* does *not* backup all MongoDB databases available. I think that it should. See also *configuration/backup_full.sh* in our git repository for a new version. + +## Orchestration Use Cases + +### Create a New Event on a Dedicated Replication Cluster + +A user wants to create a new event. But instead of creating it on an existing server instance, such as the archive server or a club server, he/she would like to create a new dedicated server instance such that the server is a master which later may receive its own replicas and thus form the core of a new replication cluster. The user defines the technical event name, such as *TW2018* for the "Travemünder Woche 2018," which is then used as the server name, MongoDB database name, and replication channel name. + +The steps are: + +- Launch a new instance from a prepared AMI (either the AMI is regularly updated with the latest packages and kernel patches, or a "yum update" needs to be run and then the instance rebooted) +- The latest (or a specified) release is installed to */home/sailing/servers/server* +- The */home/sailing/servers/server/env.sh* file is adjusted to reflect the server name, MongoDB settings, as well as the ports to be used (for a dedicated server probably the defaults at 8888 for the HTTP server, 14888 for the OSGi console telnet port, and 2010 for the Expedition UDP connector) +- The Java process is launched +- An event is created; it doesn't necessarily have to have the correct name and attributes yet; it's only important to obtain its UUID. +- With the new security implementation, the event will be owned by the user requesting the dedicated replication cluster, and a new group named after the unique server name is created of which the user is made a part. +- The user obtains a qualified *admin:<servername>* role that grants him/her administrative permissions for all objects owned by the group pertinent to the new replication cluster. +- An Apache *httpd* macro call for the event with its UUID is inserted into a *.conf* file in the instance's */etc/httpd/conf.d* directory +- The Apache *httpd* server is launched +- Two target groups *S-ded-<servername>* and *S-ded-<servername>-master* are created, and the new instance is added to both of them +- Two new ALB rules for *<servername>.sapsailing.com* and *<servername>-master.sapsailing.com* are created, forwarding their requests to the respective target group from the previous step + +asdf + +### Create a New "Club Set-Up" in a Multi-Instance + +### Migrate from a Multi-Instance to a Dedicated Replication Cluster + +### Migrate from a Dedicated Replication Cluster into a Multi-Instance + +### Observe and Automatically Scale a Replication Cluster + +### Archiving + +### Archive Server Upgrade and Switch + +### Automatic Archive Server Fail-Over + +### Configure Sharding in a Replication Cluster + +### Amazon Machine Image (AMI) Upgrades +