Merge branch 'master' into bug5214

This commit is contained in:
Thomas Wiese committed 2020-09-17 15:04:16 +02:00
commit 90d24beaec
408 files changed
+8566 -5117

No files matched your search

Regular → Executable
View File
File mode changed.
@@ -0,0 +1,62 @@
#!/bin/bash
# You need an installation of the 2020-06 release of "Eclipse IDE for Eclipse Committers" matching you OS and JDK (32 vs 64 Bit):
# https://www.eclipse.org/downloads/packages/release/2020-06/r/eclipse-ide-eclipse-committers
if [[ $1 == "" ]]; then
echo "You need to specify the Eclipse installation directory"
exit 1
fi
installPath="$1"
installPlugins() {
"$installPath/eclipse" -application org.eclipse.equinox.p2.director -noSplash -roaming -repository $1 -installIU $2
echo ""
}
updatePlugins() {
"$installPath/eclipse" -application org.eclipse.equinox.p2.director -noSplash -roaming -repository $1 -uninstallIU $2 -installIU $2
echo ""
}
uninstallPlugins() {
"$installPath/eclipse" -application org.eclipse.equinox.p2.director -noSplash -roaming -uninstallIU $1
echo ""
}
# Not necessary if using Eclipse for JEE developers
echo "Installing webtools (WTP/WST/JPT) and m2e that are required by the GWT plugin..."
installPlugins http://download.eclipse.org/releases/2020-06/ org.eclipse.wst.web_ui.feature.feature.group,org.eclipse.jst.web_ui.feature.feature.group,org.eclipse.wst.xml_ui.feature.feature.group,org.eclipse.jpt.common.feature.feature.group,org.eclipse.jpt.jpa.feature.feature.group,org.eclipse.m2e.feature.feature.group,org.eclipse.m2e.wtp.feature.feature.group
echo "Installing GWT plugin..."
installPlugins http://storage.googleapis.com/gwt-eclipse-plugin/v3/release com.gwtplugins.eclipse.suite.v3.feature.feature.group
echo "Installing Mission Control plugin..."
installPlugins http://download.oracle.com/technology/products/missioncontrol/updatesites/base/6.0.0/eclipse com.oracle.jmc.feature.ide.feature.group,com.oracle.jmc.feature.flightrecorder.feature.group,com.oracle.jmc.feature.core.feature.group,com.oracle.jmc.feature.console.feature.group,com.oracle.jmc.feature.core.feature.group,com.oracle.jmc.feature.core.feature.group
echo "Installing GWT SDM debug bridge..."
installPlugins http://p2.sapsailing.com/p2/sdbg com.github.sdbg.feature.feature.group
echo "Installing EasyShell..."
installPlugins http://anb0s.github.io/EasyShell de.anbos.eclipse.easyshell.feature.feature.group
echo "Installing Memory Analyzer..."
installPlugins http://download.eclipse.org/releases/2020-06 org.eclipse.mat.feature.feature.group,org.eclipse.mat.chart.feature.feature.group
echo "Installing SAP JVM Tools (profiler) ..."
installPlugins https://tools.hana.ondemand.com/oxygen com.sap.jvm.profiling.feature.group
echo "Installing UMLet ..."
installPlugins https://www.umlet.com/umlet_latest/repository/ umlet-eclipse-feature.feature.group
echo "Installing javax.xml.bind for news feed polling (see https://stackoverflow.com/questions/52528693/eclipse-internal-error-polling-news-feeds)..."
installPlugins http://download.eclipse.org/tools/orbit/downloads/drops/R20200224183213/repository javax.xml.bind
echo "Installing Enhanced Class Decompiler"
installPlugins https://ecd-plugin.github.io/update org.sf.feeling.decompiler.feature.group,org.sf.feeling.decompiler.cfr.feature.group,org.sf.feeling.decompiler.jad.feature.group,org.sf.feeling.decompiler.jd.feature.group,org.sf.feeling.decompiler.procyon.feature.group
echo "Uninstalling Wild Web Developer"
uninstallPlugins org.eclipse.wildwebdeveloper.feature.feature.group
echo "Installation completed!"
+1 -1
View File
@@ -1,4 +1,4 @@
FROM donaldduck70/sapjvm8:8.1.062
FROM donaldduck70/sapjvm8:8.1.064
LABEL maintainer=axel.uhl@sap.com
# Download and extract the release
WORKDIR /home/sailing/servers/server
+1 -1
View File
@@ -6,7 +6,7 @@ ENV JAVA_HOME=/opt/sapjvm_8
WORKDIR /opt
RUN apt-get update \
&& apt-get install -y wget unzip \
&& curl --cookie eula_3_1_agreed=tools.hana.ondemand.com/developer-license-3_1.txt "https://tools.hana.ondemand.com/additional/sapjvm-8.1.062-linux-x64.zip" --output sapjvm8-linux-x64.zip \
&& curl --cookie eula_3_1_agreed=tools.hana.ondemand.com/developer-license-3_1.txt "https://tools.hana.ondemand.com/additional/sapjvm-8.1.064-linux-x64.zip" --output sapjvm8-linux-x64.zip \
&& unzip sapjvm8-linux-x64.zip \
&& rm sapjvm8-linux-x64.zip \
&& echo "export JAVA_HOME=/opt/sapjvm_8; export PATH=\${PATH}:/opt/sapjvm_8/bin" > /etc/profile.d/javahome.sh
@@ -1,8 +1,4 @@
/* InfoPlist.strings
SAPTracker
Created by Raimund Wege on 04.11.16.
Copyright © 2016 com.sap.sailing. All rights reserved. */
/* InfoPlist.strings\n SAPTracker\n \n Created by Raimund Wege on 04.11.16.\n Copyright © 2016 com.sap.sailing. All rights reserved. */
"NSLocationAlwaysUsageDescription" = "Para poder realizar el rastreo de toda una carrera, es necesario que su teléfono continúe rastreando cuando apague o bloquee la pantalla, lo que ayuda a ahorrar batería y evita tener que parar el proceso de rastreo de forma involuntaria.";
"NSLocationWhenInUseUsageDescription" = "Para poder realizar el rastreo de toda una carrera, es necesario que su teléfono continúe rastreando cuando apague o bloquee la pantalla, lo que ayuda a ahorrar batería y evita tener que parar el proceso de rastreo de forma involuntaria.";
@@ -1,8 +1,4 @@
/* InfoPlist.strings
SAPTracker
Created by Raimund Wege on 04.11.16.
Copyright © 2016 com.sap.sailing. All rights reserved. */
/* InfoPlist.strings\n SAPTracker\n \n Created by Raimund Wege on 04.11.16.\n Copyright © 2016 com.sap.sailing. All rights reserved. */
"NSLocationAlwaysUsageDescription" = "Pour pouvoir suivre l'intégralité de votre course, votre téléphone doit continuer le suivi même si vous l'éteignez ou verrouillez l'écran (pour économiser de l'énergie). Ainsi, vous évitez un arrêt non voulu du suivi.";
"NSLocationWhenInUseUsageDescription" = "Pour pouvoir suivre l'intégralité de votre course, votre téléphone doit continuer le suivi même si vous l'éteignez ou verrouillez l'écran (pour économiser de l'énergie). Ainsi, vous évitez un arrêt non voulu du suivi.";
@@ -1,8 +1,4 @@
/* InfoPlist.strings
SAPTracker
Created by Raimund Wege on 04.11.16.
Copyright © 2016 com.sap.sailing. All rights reserved. */
/* InfoPlist.strings\n SAPTracker\n \n Created by Raimund Wege on 04.11.16.\n Copyright © 2016 com.sap.sailing. All rights reserved. */
"NSLocationAlwaysUsageDescription" = "Per tracciare l'intera gara il tuo telefono deve continuare il tracciamento anche quando disattivi o blocchi lo schermo per risparmiare la batteria ed evitare di arrestare involontariamente il processo di tracciamento.";
"NSLocationWhenInUseUsageDescription" = "Per tracciare l'intera gara il tuo telefono deve continuare il tracciamento anche quando disattivi o blocchi lo schermo per risparmiare la batteria ed evitare di arrestare involontariamente il processo di tracciamento.";
@@ -1,8 +1,4 @@
/* InfoPlist.strings
SAPTracker
Created by Raimund Wege on 04.11.16.
Copyright © 2016 com.sap.sailing. All rights reserved. */
/* InfoPlist.strings\n SAPTracker\n \n Created by Raimund Wege on 04.11.16.\n Copyright © 2016 com.sap.sailing. All rights reserved. */
"NSLocationAlwaysUsageDescription" = "レース全体を追跡するには、画面のオフ時またはロック時にも電話で追跡が続行される必要があり、これは電池の節約や追跡プロセスの意図しない停止の回避に役立ちます。";
"NSLocationWhenInUseUsageDescription" = "レース全体を追跡するには、画面のオフ時またはロック時にも電話で追跡が続行される必要があり、これは電池の節約や追跡プロセスの意図しない停止の回避に役立ちます。";
@@ -1,8 +1,4 @@
/* InfoPlist.strings
SAPTracker
Created by Raimund Wege on 04.11.16.
Copyright © 2016 com.sap.sailing. All rights reserved. */
/* InfoPlist.strings\n SAPTracker\n \n Created by Raimund Wege on 04.11.16.\n Copyright © 2016 com.sap.sailing. All rights reserved. */
"NSLocationAlwaysUsageDescription" = "Para rastrear a corrida completa, seu telefone precisa continuar o rastreamento mesmo quando você desativa ou bloqueia a tela, o que ajuda a poupar bateria e evita a paragem acidental do processo de rastreamento.";
"NSLocationWhenInUseUsageDescription" = "Para rastrear a corrida completa, seu telefone precisa continuar o rastreamento mesmo quando você desativa ou bloqueia a tela, o que ajuda a poupar bateria e evita a paragem acidental do processo de rastreamento.";
@@ -1,8 +1,4 @@
/* InfoPlist.strings
SAPTracker
Created by Raimund Wege on 04.11.16.
Copyright © 2016 com.sap.sailing. All rights reserved. */
/* InfoPlist.strings\n SAPTracker\n \n Created by Raimund Wege on 04.11.16.\n Copyright © 2016 com.sap.sailing. All rights reserved. */
"NSLocationAlwaysUsageDescription" = "Для отслеживания всей гонки ваш телефон должен продолжать отслеживание даже при выключении или блокировке экрана, что помогает экономить заряд аккумулятора и избегать непреднамеренной остановки процесса отслеживания.";
"NSLocationWhenInUseUsageDescription" = "Для отслеживания всей гонки ваш телефон должен продолжать отслеживание даже при выключении или блокировке экрана, что помогает экономить заряд аккумулятора и избегать непреднамеренной остановки процесса отслеживания.";
@@ -1,6 +1,7 @@
package com.sap.sailing.competitorimport;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -44,4 +45,11 @@ public interface CompetitorProvider extends Named {
* <code>eventName</code> as returned by {@link #getHasCompetitorsForRegattasInEvent()}.
*/
Iterable<CompetitorDescriptor> getCompetitorDescriptors(String eventName, String regattaName) throws JAXBException, IOException;
/**
* A hint for users about this competitor provider; ideally mapped to the user's locale with i18n support.
*
* @return may return {@code null}
*/
String getHint(Locale locale);
}
@@ -29,6 +29,7 @@ FoilingSegmentsSailingDomainRetrieverChain=Apéndices de foil
FoilingSegmentName=Nombre de apéndice de foil
WindFixSailingDomainRetrieverChain=Puntos de viento
MarkPassingSailingDomainRetrieverChain=Superaciones de marca
TrackedRaceResultOfCompetitorRetrieverChain=Carreras
RaceResultSailingDomainRetrieverChain=Resultados de la carrera
RaceOfCompetitorSailingDomainRetrieverChain=Carreras de los competidores
ManeuverSailingDomainRetrieverChain=Maniobra
@@ -29,6 +29,7 @@ FoilingSegmentsSailingDomainRetrieverChain=Tronçons en planing
FoilingSegmentName=Nom du tronçon en planing
WindFixSailingDomainRetrieverChain=Points vent
MarkPassingSailingDomainRetrieverChain=Passages des marques
TrackedRaceResultOfCompetitorRetrieverChain=Courses
RaceResultSailingDomainRetrieverChain=Résultats de course
RaceOfCompetitorSailingDomainRetrieverChain=Courses des concurrents
ManeuverSailingDomainRetrieverChain=Manœuvre
@@ -29,6 +29,7 @@ FoilingSegmentsSailingDomainRetrieverChain=Segmenti percorsi su foil
FoilingSegmentName=Nome del segmento percorso su foil
WindFixSailingDomainRetrieverChain=Punti nave vento
MarkPassingSailingDomainRetrieverChain=Passaggi di segnali
TrackedRaceResultOfCompetitorRetrieverChain=Gare
RaceResultSailingDomainRetrieverChain=Risultati gara
RaceOfCompetitorSailingDomainRetrieverChain=Gare di concorrenti
ManeuverSailingDomainRetrieverChain=Manovra
@@ -29,6 +29,7 @@ FoilingSegmentsSailingDomainRetrieverChain=フォイリングしたセグメン
FoilingSegmentName=フォイリングしたセグメント名
WindFixSailingDomainRetrieverChain=風フィックス
MarkPassingSailingDomainRetrieverChain=マーク通過
TrackedRaceResultOfCompetitorRetrieverChain=レース
RaceResultSailingDomainRetrieverChain=レース結果
RaceOfCompetitorSailingDomainRetrieverChain=競技者のレース
ManeuverSailingDomainRetrieverChain=マニューバー
@@ -29,6 +29,7 @@ FoilingSegmentsSailingDomainRetrieverChain=Segmentos com hidrofólio
FoilingSegmentName=Nome do segmento com hidrofólio
WindFixSailingDomainRetrieverChain=Pontos fixos de vento
MarkPassingSailingDomainRetrieverChain=Passagens na marca
TrackedRaceResultOfCompetitorRetrieverChain=Corridas
RaceResultSailingDomainRetrieverChain=Resultados da corrida
RaceOfCompetitorSailingDomainRetrieverChain=Corridas de competidores
ManeuverSailingDomainRetrieverChain=Manobra
@@ -29,6 +29,7 @@ FoilingSegmentsSailingDomainRetrieverChain=Сегменты в крыльево
FoilingSegmentName=Имя сегмента в крыльевом режиме
WindFixSailingDomainRetrieverChain=Замеры ветра
MarkPassingSailingDomainRetrieverChain=Прохождения отметок
TrackedRaceResultOfCompetitorRetrieverChain=Гонки
RaceResultSailingDomainRetrieverChain=Результаты гонки
RaceOfCompetitorSailingDomainRetrieverChain=Гонки участников
ManeuverSailingDomainRetrieverChain=Маневр
@@ -29,6 +29,7 @@ FoilingSegmentsSailingDomainRetrieverChain=水翼腾空段
FoilingSegmentName=水翼腾空段名称
WindFixSailingDomainRetrieverChain=风力修复
MarkPassingSailingDomainRetrieverChain=绕标
TrackedRaceResultOfCompetitorRetrieverChain=比赛轮次
RaceResultSailingDomainRetrieverChain=比赛轮次结果
RaceOfCompetitorSailingDomainRetrieverChain=参赛队的比赛轮次
ManeuverSailingDomainRetrieverChain=操作
@@ -8,5 +8,6 @@ Bundle-Vendor: SAP
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-ActivationPolicy: lazy
Require-Bundle: com.sap.sailing.domain.common,
com.sap.sse.common
com.sap.sse.common,
com.sap.sse
Export-Package: com.sap.sailing.declination
@@ -2,6 +2,10 @@ package com.sap.sailing.declination.impl;
import java.io.IOException;
import java.text.ParseException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -12,6 +16,7 @@ import org.xml.sax.SAXException;
import com.sap.sailing.declination.Declination;
import com.sap.sailing.domain.common.Position;
import com.sap.sse.common.TimePoint;
import com.sap.sse.util.ThreadPoolUtil;
public abstract class DeclinationImporter {
private static final Logger logger = Logger.getLogger(DeclinationImporter.class.getName());
@@ -33,34 +38,21 @@ public abstract class DeclinationImporter {
*/
public Declination getDeclination(final Position position, final TimePoint timePoint,
long timeoutForOnlineFetchInMilliseconds) throws IOException, ParseException {
final Declination[] result = new Declination[1];
Thread fetcher = new Thread("Declination fetcher for "+position+"@"+timePoint) {
@Override
public void run() {
try {
Declination fetched = importRecord(position, timePoint);
synchronized (result) {
result[0] = fetched;
result.notifyAll();
}
} catch (IOException | ParserConfigurationException | SAXException e) {
logger.log(Level.INFO, "Exception while trying to load magnetic declination online", e);
synchronized (result) {
result.notifyAll(); // wake up waiter; no result will show up anymore
}
}
}
};
fetcher.start();
synchronized (result) {
if (result[0] == null) {
try {
result.wait(timeoutForOnlineFetchInMilliseconds);
} catch (InterruptedException e) {
// ignore; simply use value from file in this case
}
}
ExecutorService executorService = ThreadPoolUtil.INSTANCE.
createBackgroundTaskThreadPoolExecutor(1, "DeclinationImporterThreadPoolExecutor");
try {
return executorService.submit(
() -> importRecord(position, timePoint)
).get(timeoutForOnlineFetchInMilliseconds, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
logger.log(Level.INFO, "Timeout while getting declination.", e);
// ignore; simply use value from file in this case
} catch (InterruptedException e) {
logger.log(Level.INFO, "InterruptedException while getting declination", e);
// ignore; simply use value from file in this case
} catch (ExecutionException e) {
logger.log(Level.INFO, "Exception while trying to load magnetic declination online", e);
}
return result[0];
return null;
}
}
@@ -68,6 +68,8 @@ public enum BoatClassMasterdata {
J70 ("J/70", true, 6.93, 2.25, BoatHullType.MONOHULL, true, "J70", "J-70"),
J80 ("J/80", true, 8.0, 2.51, BoatHullType.MONOHULL, true, "J80", "J-80"),
J88 ("J/88", true, 8.9, 1.89, BoatHullType.MONOHULL, true, "J88", "J-88"),
J92 ("J/92", true, 9.14, 3.05, BoatHullType.MONOHULL, true, "J92", "J-92"),
J92S ("J/92S", true, 9.14, 3.05, BoatHullType.MONOHULL, true, "J92S", "J-92S"),
J105 ("J/105", true, 10.52, 3.35, BoatHullType.MONOHULL, true, "J105", "J-105"),
J111 ("J/111", true, 11.1, 3.29, BoatHullType.MONOHULL, true, "J111", "J-111"),
JK_20 ("20qm Jollenkreuzer", true, 7.75, 2.50, BoatHullType.MONOHULL, true, "JK 20", "JK20", "20er"),
@@ -136,6 +138,7 @@ public enum BoatClassMasterdata {
VX_ONE ("VX ONE", true, 5.79, 2.19, BoatHullType.MONOHULL, true, "VX-ONE"),
WAYFARER ("Wayfarer", true, 4.82, 1.85, BoatHullType.MONOHULL, false),
WETA ("Weta", true, 4.4, 3.5, BoatHullType.TRIMARAN, true, "Weta Trimaran"),
X_332 ("X-332", true, 10.06, 3.30, BoatHullType.MONOHULL, true, "X332"),
X_99 ("X-99", true, 9.96, 2.95, BoatHullType.MONOHULL, true, "X99"),
// multi-class "boat classes"; to be replaced at some later point in time by something like a RegattaClass
@@ -1,5 +1,7 @@
package com.sap.sailing.domain.common;
import java.util.Arrays;
/**
* The reasons why a competitor may get the maximum number of points, usually equaling the
* number of competitors enlisted for the regatta plus one.
@@ -80,4 +82,10 @@ public enum MaxPointsReason {
public boolean isAppliesAtStartOfRace() {
return appliesAtStartOfRace;
}
public static MaxPointsReason[] getLexicographicalValues() {
MaxPointsReason[] result = MaxPointsReason.values();
Arrays.sort(result, (o1, o2) -> o1.name().compareToIgnoreCase(o2.name()));
return result;
}
}
@@ -13,5 +13,14 @@ package com.sap.sailing.domain.common;
*
*/
public interface WindFinderReviewedSpotsCollectionIdProvider {
Iterable<String> getWindFinderReviewedSpotsCollectionIds();
Iterable<String> getAllWindFinderReviewedSpotsCollectionIds();
/**
* If WindFinder spot collections are provided scoped to events, regattas, or races, this method
* allows for a qualified search for WindFinder spot collections, using a regatta identifier as a
* qualifier. For example, if events specify WindFinder spot collection IDs, the regatta identifier
* can be traced to the events to which this regatta belongs, and then only those events' WindFinder
* spot collection IDs will be considered.
*/
Iterable<String> getWindFinderReviewedSpotsCollectionIdsByRegatta(RegattaIdentifier regattaIdentifier);
}
@@ -1,18 +0,0 @@
package com.sap.sailing.domain.common.tracking;
/**
* Identifies the tracking technology used for a TrackedRace.
*/
public enum TrackingConnectorType {
TracTrac("https://www.tractrac.com/"), SwissTiming(null), RaceLog(null);
private final String defaultUrl;
private TrackingConnectorType(String defaultUrl) {
this.defaultUrl = defaultUrl;
}
public String getDefaultUrl() {
return defaultUrl;
}
}
@@ -13,4 +13,5 @@ Require-Bundle: org.junit;bundle-version="4.8.2",
com.sap.sse.common,
com.sap.sse.security.testsupport,
org.mockito.mockito-core;bundle-version="1.10.14",
org.hamcrest;bundle-version="1.1.0"
org.hamcrest;bundle-version="1.1.0",
org.objenesis;bundle-version="2.1.0"
@@ -43,22 +43,24 @@ public class FixFactory {
JSONArray timePointsMillis = (JSONArray) fixesJson.get("t");
int fixIndex = 0;
for (Object timePointMillis : timePointsMillis) {
TimePoint timePoint = new MillisecondsTimePoint(((Number) timePointMillis).longValue());
Map<Integer, Object> valuesPerSubindex = new HashMap<>();
int i=1;
JSONArray values;
while ((values=(JSONArray) fixesJson.get(""+i)) != null) {
valuesPerSubindex.put(i, values.get(fixIndex));
i++;
}
Sensor sensor = new SensorImpl(deviceSerialNumber,
fixTypeAndOptionalColonSeparatedSensorsSubId.length < 2 ? 0
: Long.valueOf(fixTypeAndOptionalColonSeparatedSensorsSubId[1]));
final Type ft = Type.valueOf(fixType);
if (ft != null) {
Fix fix = createFix(sensor, ft, timePoint, valuesPerSubindex);
result.add(fix);
fixIndex++;
if (timePointMillis != null) {
TimePoint timePoint = new MillisecondsTimePoint(((Number) timePointMillis).longValue());
Map<Integer, Object> valuesPerSubindex = new HashMap<>();
int i=1;
JSONArray values;
while ((values=(JSONArray) fixesJson.get(""+i)) != null) {
valuesPerSubindex.put(i, values.get(fixIndex));
i++;
}
Sensor sensor = new SensorImpl(deviceSerialNumber,
fixTypeAndOptionalColonSeparatedSensorsSubId.length < 2 ? 0
: Long.valueOf(fixTypeAndOptionalColonSeparatedSensorsSubId[1]));
final Type ft = Type.valueOf(fixType);
if (ft != null) {
Fix fix = createFix(sensor, ft, timePoint, valuesPerSubindex);
result.add(fix);
fixIndex++;
}
}
}
}
@@ -32,6 +32,8 @@ import com.sap.sse.common.Util.Pair;
import com.sap.sse.common.mail.MailException;
public interface RaceLogTrackingAdapter {
String NAME = "RaceLog";
String DEFAULT_URL = null;
/**
* Performs the necessary steps to ensure that the race is tracked (aka that a {@link TrackedRace} is created from
* the data in this {@code RaceLog}).
@@ -64,8 +64,8 @@ import com.sap.sailing.domain.common.abstractlog.TimePointSpecificationFoundInLo
import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus;
import com.sap.sailing.domain.common.racelog.tracking.RaceLogTrackingState;
import com.sap.sailing.domain.common.racelog.tracking.RaceNotCreatedException;
import com.sap.sailing.domain.common.tracking.TrackingConnectorType;
import com.sap.sailing.domain.racelog.RaceLogAndTrackedRaceResolver;
import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapter;
import com.sap.sailing.domain.regattalike.IsRegattaLike;
import com.sap.sailing.domain.tracking.AbstractRaceTrackerBaseImpl;
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
@@ -369,7 +369,7 @@ public class RaceLogRaceTracker extends AbstractRaceTrackerBaseImpl {
params.getDelayToLiveInMillis(), WindTrack.DEFAULT_MILLISECONDS_OVER_WHICH_TO_AVERAGE_WIND,
boatClass.getApproximateManeuverDurationInMilliseconds(), null, /*useMarkPassingCalculator*/ true, raceLogResolver,
/* Not needed because the RaceTracker is not active on a replica */ Optional.empty(),
new TrackingConnectorInfoImpl(TrackingConnectorType.RaceLog, /* webUrl */ null));
new TrackingConnectorInfoImpl(RaceLogTrackingAdapter.NAME, RaceLogTrackingAdapter.DEFAULT_URL, /* no webUrl */ null));
notifyRaceCreationListeners();
logger.info(String.format("Started tracking race-log race (%s)", raceLog));
// this wakes up all waiting race handles
@@ -13,7 +13,7 @@ public interface RaceStateChangedListener {
* TODO: We need to recheck the AbortFinder to enable these interface methods
* void onAborted(RaceState2 state);
* void onGeneralRecall(RaceState2 state);*/
void onFinishingPositioningsChanged(ReadonlyRaceState state);
void onFinishingPositionsChanged(ReadonlyRaceState state);
void onFinishingPositionsConfirmed(ReadonlyRaceState state);
void onCourseDesignChanged(ReadonlyRaceState state);
void onWindFixChanged(ReadonlyRaceState state);
@@ -44,7 +44,7 @@ public abstract class BaseRaceStateChangedListener implements RaceStateChangedLi
}
@Override
public void onFinishingPositioningsChanged(ReadonlyRaceState state) {
public void onFinishingPositionsChanged(ReadonlyRaceState state) {
}
@@ -85,9 +85,9 @@ public class RaceStateChangedListeners extends HashSet<RaceStateChangedListener>
}
@Override
public void onFinishingPositioningsChanged(ReadonlyRaceState state) {
public void onFinishingPositionsChanged(ReadonlyRaceState state) {
for (RaceStateChangedListener listener : getWorkingCopyOfListeners()) {
listener.onFinishingPositioningsChanged(state);
listener.onFinishingPositionsChanged(state);
}
}
@@ -557,7 +557,7 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL
CompetitorResults positionedCompetitors = finishPositioningListAnalyzer.analyze().getCompetitorResults();
if (!Util.equalsWithNull(cachedPositionedCompetitors, positionedCompetitors)) {
cachedPositionedCompetitors = positionedCompetitors;
changedListeners.onFinishingPositioningsChanged(this);
changedListeners.onFinishingPositionsChanged(this);
}
CompetitorResultsAndTheirCreationTimePoints confirmedPositionedCompetitors = confirmedFinishPositioningListAnalyzer.analyze();
if (!Util.equalsWithNull(cachedConfirmedPositionedCompetitors, confirmedPositionedCompetitors)) {
@@ -6,9 +6,8 @@ import com.sap.sse.common.NamedWithID;
import com.sap.sse.common.Renamable;
import com.sap.sse.security.shared.WithQualifiedObjectIdentifier;
public interface LeaderboardGroupBase extends Renamable, NamedWithID, WithQualifiedObjectIdentifier {
public interface LeaderboardGroupBase extends Renamable, NamedWithID, WithQualifiedObjectIdentifier, WithDescription {
UUID getId();
String getDescription();
void setDescriptiom(String description);
boolean hasOverallLeaderboard();
String getDisplayName();
@@ -1,9 +1,7 @@
package com.sap.sailing.domain.tracking;
import java.io.Serializable;
import java.net.URL;
import com.sap.sailing.domain.common.tracking.TrackingConnectorType;
/**
* Identifies the tracking connector that was used to create a TrackedRace.
@@ -12,13 +10,19 @@ import com.sap.sailing.domain.common.tracking.TrackingConnectorType;
public interface TrackingConnectorInfo extends Serializable {
/**
* gets the {@link TrackingConnectorType} associated with the tracking technology used for the Race
* gets the name associated with the tracking technology used for the Race
*/
TrackingConnectorType getTrackingConnectorType();
String getTrackingConnectorName();
/**
* gets a {@link URL} representation of the web-URL associated with the Event;
* may be {@code null} if the API of the respective Tracking-Service does not provide a URL;
* gets a {@link String} representation of the default web-URL associated with the tracking technology used for the Race.
* may be {@code null} if there is none provided in the adapter.
*/
URL getWebUrl();
String getTrackingConnectorDefaultUrl();
/**
* gets a {@link String} representation of the web-URL associated with the Event.
* may be {@code null} if the API of the respective Tracking-Service does not provide a URL.
*/
String getWebUrl();
}
@@ -1,26 +1,29 @@
package com.sap.sailing.domain.tracking.impl;
import java.net.URL;
import com.sap.sailing.domain.common.tracking.TrackingConnectorType;
import com.sap.sailing.domain.tracking.TrackingConnectorInfo;
public class TrackingConnectorInfoImpl implements TrackingConnectorInfo {
private static final long serialVersionUID = 7970268841592389145L;
private final TrackingConnectorType trackingConnectorType;
private final URL webUrl;
private final String trackingConnectorName;
private final String TtackingConnectorDefaultUrl;
private final String webUrl;
public TrackingConnectorInfoImpl(TrackingConnectorType trackingConnectorType, URL webUrl) {
public TrackingConnectorInfoImpl(String trackingConnectorName, String trackingConnectorDefaultUrl, String webUrl) {
super();
this.trackingConnectorType = trackingConnectorType;
this.trackingConnectorName = trackingConnectorName;
TtackingConnectorDefaultUrl = trackingConnectorDefaultUrl;
this.webUrl = webUrl;
}
public TrackingConnectorType getTrackingConnectorType() {
return trackingConnectorType;
public String getTrackingConnectorDefaultUrl() {
return TtackingConnectorDefaultUrl;
}
public URL getWebUrl() {
public String getTrackingConnectorName() {
return trackingConnectorName;
}
public String getWebUrl() {
return webUrl;
}
@@ -28,7 +31,8 @@ public class TrackingConnectorInfoImpl implements TrackingConnectorInfo {
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((trackingConnectorType == null) ? 0 : trackingConnectorType.hashCode());
result = prime * result + ((TtackingConnectorDefaultUrl == null) ? 0 : TtackingConnectorDefaultUrl.hashCode());
result = prime * result + ((trackingConnectorName == null) ? 0 : trackingConnectorName.hashCode());
result = prime * result + ((webUrl == null) ? 0 : webUrl.hashCode());
return result;
}
@@ -42,10 +46,15 @@ public class TrackingConnectorInfoImpl implements TrackingConnectorInfo {
if (getClass() != obj.getClass())
return false;
TrackingConnectorInfoImpl other = (TrackingConnectorInfoImpl) obj;
if (trackingConnectorType == null) {
if (other.trackingConnectorType != null)
if (TtackingConnectorDefaultUrl == null) {
if (other.TtackingConnectorDefaultUrl != null)
return false;
} else if (!trackingConnectorType.equals(other.trackingConnectorType))
} else if (!TtackingConnectorDefaultUrl.equals(other.TtackingConnectorDefaultUrl))
return false;
if (trackingConnectorName == null) {
if (other.trackingConnectorName != null)
return false;
} else if (!trackingConnectorName.equals(other.trackingConnectorName))
return false;
if (webUrl == null) {
if (other.webUrl != null)
@@ -34,7 +34,7 @@ public class RPDMessageParsingTest {
private void assertRPDMessageCanBeParsed(final String rpdMessage)
throws InterruptedException, ParseException, IOException {
final SailMasterMessage message = new SailMasterMessageImpl(rpdMessage);
final SailMasterConnectorImpl connector = new SailMasterConnectorImpl(/* host */ "8.8.8.8", /* port */ 443, /* raceId */ null, /* raceName */ null, /* raceDescription */ null, /* boatClass */ null, /* SwissTimingRaceTracker */ null);
final SailMasterConnectorForSocket connector = new SailMasterConnectorForSocket(/* host */ "8.8.8.8", /* port */ 443, /* raceId */ null, /* raceName */ null, /* raceDescription */ null, /* boatClass */ null, /* SwissTimingRaceTracker */ null);
final boolean[] receivedPositionData = new boolean[1];
final SailMasterListener listener = new SailMasterAdapter() {
@Override
@@ -24,6 +24,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
import com.sap.sailing.domain.common.TrackedRaceStatusEnum;
import com.sap.sailing.domain.swisstimingadapter.Competitor;
import com.sap.sailing.domain.swisstimingadapter.Course;
import com.sap.sailing.domain.swisstimingadapter.Fix;
@@ -64,9 +65,9 @@ public class SailMasterConnectivityTest {
Race race4702 = new RaceImpl("W4702", "R2", "470 Women Race 2");
Race race4711 = new RaceImpl("4711", "R2", "A wonderful test race");
Race race4712 = new RaceImpl("4712", "R2", "Not such a wonderful race");
connector4702 = SwissTimingFactory.INSTANCE.getOrCreateSailMasterConnector("localhost", port, race4702.getRaceID(), race4702.getRaceName(), race4702.getDescription(), null /* boat class */, /* SwissTimingRaceTracker */ null);
connector4711 = SwissTimingFactory.INSTANCE.getOrCreateSailMasterConnector("localhost", port, race4711.getRaceID(), race4702.getRaceName(), race4711.getDescription(), null /* boat class */, /* SwissTimingRaceTracker */ null);
connector4712 = SwissTimingFactory.INSTANCE.getOrCreateSailMasterConnector("localhost", port, race4712.getRaceID(), race4702.getRaceName(), race4712.getDescription(), null /* boat class */, /* SwissTimingRaceTracker */ null);
connector4702 = SwissTimingFactory.INSTANCE.getOrCreateSailMasterConnector("localhost", port, race4702.getRaceID(), /* raceDataUrl */ null, race4702.getRaceName(), race4702.getDescription(), null /* boat class */, /* SwissTimingRaceTracker */ null);
connector4711 = SwissTimingFactory.INSTANCE.getOrCreateSailMasterConnector("localhost", port, race4711.getRaceID(), /* raceDataUrl */ null, race4702.getRaceName(), race4711.getDescription(), null /* boat class */, /* SwissTimingRaceTracker */ null);
connector4712 = SwissTimingFactory.INSTANCE.getOrCreateSailMasterConnector("localhost", port, race4712.getRaceID(), /* raceDataUrl */ null, race4702.getRaceName(), race4712.getDescription(), null /* boat class */, /* SwissTimingRaceTracker */ null);
Thread.sleep(1000); // wait until all connectors are really connected to the SailMasterDummy
}
@@ -111,7 +112,7 @@ public class SailMasterConnectivityTest {
@Override
public void receivedAvailableRaces(Iterable<Race> races) {}
@Override
public void storedDataProgress(String raceID, double progress) {}
public void storedDataProgress(String raceID, double progress, TrackedRaceStatusEnum statusAfterLoadingComplete) {}
@Override
public void receivedWindData(String raceID, int zeroBasedMarkIndex, double windDirectionTrueDegrees, double windSpeedInKnots) {}
@Override
@@ -153,7 +154,7 @@ public class SailMasterConnectivityTest {
@Override
public void receivedAvailableRaces(Iterable<Race> races) {}
@Override
public void storedDataProgress(String raceID, double progress) {}
public void storedDataProgress(String raceID, double progress, TrackedRaceStatusEnum statusAfterLoadingComplete) {}
@Override
public void receivedWindData(String raceID, int zeroBasedMarkIndex, double windDirectionTrueDegrees, double windSpeedInKnots) {}
@Override
@@ -18,6 +18,7 @@ import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import com.sap.sailing.domain.common.TrackedRaceStatusEnum;
import com.sap.sailing.domain.swisstimingadapter.Competitor;
import com.sap.sailing.domain.swisstimingadapter.Course;
import com.sap.sailing.domain.swisstimingadapter.Fix;
@@ -41,7 +42,7 @@ public class SwissTimingSailMasterLiveTest implements SailMasterListener {
@Before
public void connect() throws InterruptedException, ParseException {
connector = SwissTimingFactory.INSTANCE.getOrCreateSailMasterConnector("gps.sportresult.com", 40300, "W4702", "R2", "Women 470 Race 2", null /* boat class*/, /* SwissTimingRaceTracker */ null);
connector = SwissTimingFactory.INSTANCE.getOrCreateSailMasterConnector("gps.sportresult.com", 40300, "W4702", /* raceDataUrl */ null, "R2", "Women 470 Race 2", null /* boat class*/, /* SwissTimingRaceTracker */ null);
}
@After
@@ -195,7 +196,7 @@ public class SwissTimingSailMasterLiveTest implements SailMasterListener {
}
@Override
public void storedDataProgress(String raceID, double progress) {
public void storedDataProgress(String raceID, double progress, TrackedRaceStatusEnum statusAfterLoadingComplete) {
}
@Override
@@ -3,6 +3,7 @@ package com.sap.sailing.domain.swisstimingadapter;
import java.util.Collection;
import java.util.List;
import com.sap.sailing.domain.common.TrackedRaceStatusEnum;
import com.sap.sse.common.Distance;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
@@ -38,7 +39,7 @@ public abstract class SailMasterAdapter implements SailMasterListener {
}
@Override
public void storedDataProgress(String raceID, double progress) {
public void storedDataProgress(String raceID, double progress, TrackedRaceStatusEnum statusAfterLoadingComplete) {
}
@Override
@@ -4,6 +4,7 @@ import java.net.URISyntaxException;
import java.util.Collection;
import java.util.List;
import com.sap.sailing.domain.common.TrackedRaceStatusEnum;
import com.sap.sse.common.Distance;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
@@ -26,8 +27,12 @@ public interface SailMasterListener {
/**
* Announces the progress of data loaded from a local persistent store (as opposed to receiving it live from a
* SailMaster instance). 0.0 means no progress so far, 1.0 means loading from persistent store completed.
*
* @param statusAfterLoadingComplete
* the status into which to put the race if 100% loading status have been reached; usually one of
* TRACKING or FINISHED
*/
void storedDataProgress(String raceID, double progress);
void storedDataProgress(String raceID, double progress, TrackedRaceStatusEnum statusAfterLoadingComplete);
void receivedWindData(String raceID, int zeroBasedMarkIndex, double windDirectionTrueDegrees, double windSpeedInKnots);
}
@@ -9,6 +9,8 @@ public interface SailMasterTransceiver {
/**
* Sends a message, prefixing it with a start byte and suffixing it with an end byte, as required by the
* SwissTiming SailMaster protocol.
*
* @param os if {@code null}, this method is a no-op
*/
void sendMessage(String message, OutputStream os) throws IOException;
@@ -16,6 +18,8 @@ public interface SailMasterTransceiver {
* Sends the message, prefixing it with a start byte and suffixing it with an end byte, as required by the
* SwissTiming SailMaster protocol. Before the start byte, the ASCII-encoded
* {@link SailMasterMessage#getSequenceNumber() sequence number} is transmitted.
*
* @param os if {@code null}, this method is a no-op
*/
void sendMessage(SailMasterMessage message, OutputStream os) throws IOException;
@@ -16,6 +16,9 @@ import com.sap.sailing.domain.tracking.TrackerManager;
import com.sap.sailing.xrr.schema.RegattaResults;
public interface SwissTimingAdapter {
String NAME = "SwissTiming";
String DEFAULT_URL = null;
List<com.sap.sailing.domain.swisstimingadapter.RaceRecord> getSwissTimingRaceRecords(String hostname, int port) throws InterruptedException, UnknownHostException, IOException, ParseException;
/**
@@ -42,5 +45,5 @@ public interface SwissTimingAdapter {
SwissTimingFactory getSwissTimingFactory();
com.sap.sailing.domain.swisstimingadapter.DomainFactory getSwissTimingDomainFactory();
}
@@ -2,6 +2,7 @@ package com.sap.sailing.domain.swisstimingadapter;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.text.ParseException;
@@ -42,10 +43,13 @@ public interface SwissTimingFactory {
* Generally, the connector needs to be instructed for which races it shall handle events using calls to the
* {@link SailMasterConnector#trackRace} and {@link SailMasterConnector#stopTrackingRace} operations.
* {@link MessageType#isRaceSpecific() Race-specific messages} for other races are ignored and not forwarded to any
* listener.
* listener.<p>
*
* When the {@code hostname} is {@code null}, this is assumed to request a URL-based connection, and {@code raceDataUrl} has
* to provide a valid URL leading to a downloadable log file.
*/
SailMasterConnector getOrCreateSailMasterConnector(String hostname, int port, String raceId, String raceName,
String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker)
SailMasterConnector getOrCreateSailMasterConnector(String hostname, int port, String raceId, URL raceDataUrl,
String raceName, String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker)
throws InterruptedException, ParseException;
SailMasterConnector getOrCreateSailMasterLiveSimulatorConnector(String host, int port, String raceId, String raceName,
@@ -3,7 +3,6 @@ package com.sap.sailing.domain.swisstimingadapter.impl;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.text.DateFormat;
@@ -30,6 +29,7 @@ import java.util.stream.Collectors;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.SpeedWithBearing;
import com.sap.sailing.domain.common.TrackedRaceStatusEnum;
import com.sap.sailing.domain.common.impl.DegreePosition;
import com.sap.sailing.domain.common.impl.KilometersPerHourSpeedImpl;
import com.sap.sailing.domain.common.impl.KnotSpeedImpl;
@@ -83,12 +83,9 @@ import com.sap.sse.util.impl.UUIDHelper;
* @author Axel Uhl (d043530)
*
*/
public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implements SailMasterConnector, Runnable {
private static final Logger logger = Logger.getLogger(SailMasterConnectorImpl.class.getName());
public abstract class AbstractSailMasterConnector extends SailMasterTransceiverImpl implements SailMasterConnector, Runnable {
private static final Logger logger = Logger.getLogger(AbstractSailMasterConnector.class.getName());
private final String host;
private final int port;
private Socket socket;
private final DateFormat dateFormat;
private final Set<SailMasterListener> listeners;
private final Thread receiverThread;
@@ -131,7 +128,11 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
private Long numberOfStoredMessages;
public SailMasterConnectorImpl(String host, int port, String raceId, String raceName, String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker) throws InterruptedException, ParseException {
/**
* Subclasses must invoke {@link #startReceiverThread()} in their constructor before the constructor completes, but
* usually after finishing the initialization of all their fields.
*/
protected AbstractSailMasterConnector(String raceId, String raceName, String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker) throws InterruptedException, ParseException {
super();
maxSequenceNumber = -1l;
this.raceId = raceId; // from this time on, the connector interprets messages for raceID
@@ -139,12 +140,13 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
this.raceDescription = raceDescription;
this.boatClass = boatClass;
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
this.host = host;
this.port = port;
this.listeners = new HashSet<SailMasterListener>();
this.unprocessedMessagesByType = new HashMap<MessageType, BlockingQueue<SailMasterMessage>>();
this.listeners = new HashSet<>();
this.unprocessedMessagesByType = new HashMap<>();
this.addSailMasterListener(swissTimingRaceTracker);
receiverThread = new Thread(this, "SwissTiming SailMaster Receiver");
}
protected void startReceiverThread() {
receiverThread.start();
}
@@ -152,15 +154,15 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
try {
while (!stopped) {
try {
ensureSocketIsOpen(); // the result of this may be that the connector w\s stopped in between, so another check is required
if (!stopped && socket != null) {
String receivedMessage = receiveMessage(socket.getInputStream());
ensureConnected(); // the result of this may be that the connector w\s stopped in between, so another check is required
if (!stopped && isConnected()) {
String receivedMessage = receiveMessage(getInputStream());
if (receivedMessage == null) {
logger.info("Reached EOF for "+this+"; disconnecting");
// reached EOF; this means the socket is or can be closed
if (socket != null && !socket.isClosed()) {
socket.close();
if (isConnected()) {
disconnect();
}
socket = null;
} else {
SailMasterMessage message = new SailMasterMessageImpl(receivedMessage);
// drop race-specific messages for non-tracked races
@@ -188,12 +190,12 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
} catch (SocketException se) {
// This occurs if the socket was closed which may mean the connector was stopped. Check in while
logger.info("Caught exception "+se+" during socket operation; setting socket to null");
socket = null;
disconnect();
Thread.sleep(1000); // try again in 1s
}
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Exception in sail master connector "+SailMasterConnectorImpl.class.getName()+".run for "+this, e);
logger.log(Level.SEVERE, "Exception in sail master connector "+AbstractSailMasterConnector.class.getName()+".run for "+this, e);
}
logger.info("Stopping Sail Master connector thread for "+this);
stopped = true;
@@ -261,13 +263,15 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
private void notifyListenersStoredDataProgress(String raceID, double progress) {
for (SailMasterListener listener : getListeners()) {
try {
listener.storedDataProgress(raceID, progress);
listener.storedDataProgress(raceID, progress, getStatusAfterLoadingIsComplete());
} catch (Exception e) {
logger.info("Exception occurred trying to notify listener "+listener+" about progress "+progress);
logger.throwing(SailMasterConnectorImpl.class.getName(), "notifyStoredDataProgress", e);
logger.throwing(AbstractSailMasterConnector.class.getName(), "notifyStoredDataProgress", e);
}
}
}
protected abstract TrackedRaceStatusEnum getStatusAfterLoadingIsComplete();
private void notifyListenersWND(SailMasterMessage message) {
// example message: WND|W4702|1|320|5.4
@@ -280,7 +284,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
listener.receivedWindData(raceID, zeroBasedMarkIndex, windDirectionTrueDegrees, windSpeedInKnots);
} catch (Exception e) {
logger.info("Exception occurred trying to notify listener "+listener+" about "+message+": "+e.getMessage());
logger.throwing(SailMasterConnectorImpl.class.getName(), "notifyListenersWND", e);
logger.throwing(AbstractSailMasterConnector.class.getName(), "notifyListenersWND", e);
}
}
}
@@ -304,7 +308,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
listener.receivedTimingData(raceID, competitorIdAsString, markIndicesRanksAndTimesSinceStartInMilliseconds);
} catch (Exception e) {
logger.info("Exception occurred trying to notify listener "+listener+" about "+message+": "+e.getMessage());
logger.throwing(SailMasterConnectorImpl.class.getName(), "notifyListenersTMD", e);
logger.throwing(AbstractSailMasterConnector.class.getName(), "notifyListenersTMD", e);
}
}
}
@@ -316,7 +320,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
listener.receivedClockAtMark(message.getSections()[1], clockAtMarkResults);
} catch (Exception e) {
logger.info("Exception occurred trying to notify listener "+listener+" about "+message+": "+e.getMessage());
logger.throwing(SailMasterConnectorImpl.class.getName(), "notifyListenersCAM", e);
logger.throwing(AbstractSailMasterConnector.class.getName(), "notifyListenersCAM", e);
}
}
}
@@ -328,7 +332,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
listener.receivedStartList(message.getSections()[1], startListMessage);
} catch (Exception e) {
logger.info("Exception occurred trying to notify listener "+listener+" about "+message+": "+e.getMessage());
logger.throwing(SailMasterConnectorImpl.class.getName(), "notifyListenersSTL", e);
logger.throwing(AbstractSailMasterConnector.class.getName(), "notifyListenersSTL", e);
}
}
}
@@ -340,7 +344,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
listener.receivedCourseConfiguration(message.getSections()[1], course);
} catch (Exception e) {
logger.info("Exception occurred trying to notify listener "+listener+" about "+message+": "+e.getMessage());
logger.throwing(SailMasterConnectorImpl.class.getName(), "notifyListenersCCG", e);
logger.throwing(AbstractSailMasterConnector.class.getName(), "notifyListenersCCG", e);
}
}
}
@@ -352,7 +356,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
listener.receivedAvailableRaces(races);
} catch (Exception e) {
logger.info("Exception occurred trying to notify listener "+listener+" about "+message+": "+e.getMessage());
logger.throwing(SailMasterConnectorImpl.class.getName(), "notifyListenersRAC", e);
logger.throwing(AbstractSailMasterConnector.class.getName(), "notifyListenersRAC", e);
}
}
// not race specific; no need to notify any listener from raceSpecificListeners
@@ -447,7 +451,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
millisecondsSinceRaceStart, nextMarkIndexForLeader, distanceToNextMarkForLeader, fixes);
} catch (Exception e) {
logger.info("Exception occurred trying to notify listener "+listener+" about "+message+": "+e.getMessage());
logger.throwing(SailMasterConnectorImpl.class.getName(), "notifyListenersRPD", e);
logger.throwing(AbstractSailMasterConnector.class.getName(), "notifyListenersRPD", e);
}
}
}
@@ -458,19 +462,11 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
}
}
@Override
public String toString() {
return "SailMasterConnector listening on port "+port+" with socket "+socket+" for race "+raceName+" with ID "+raceId+" ("+raceDescription+")";
}
@Override
public void stop() throws IOException {
logger.info("Stopping "+this);
stopped = true;
if (socket != null) {
socket.close();
socket = null;
}
logger.info("Stopping "+this);
disconnect();
synchronized (this) {
notifyAll();
}
@@ -499,13 +495,13 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
}
public SailMasterMessage sendRequestAndGetResponse(MessageType messageType, String... args) throws UnknownHostException, IOException, InterruptedException {
ensureSocketIsOpen();
ensureConnected();
return sendRequestAndGetResponseAssumingSocketIsOpen(messageType, args);
}
private SailMasterMessage sendRequestAndGetResponseAssumingSocketIsOpen(MessageType messageType, String... args)
throws IOException, InterruptedException {
OutputStream os = socket.getOutputStream();
OutputStream os = getOutputStream();
final SailMasterMessage sailMasterMessage = createSailMasterMessage(messageType, args);
sendMessage(sailMasterMessage, os);
return receiveMessage(messageType);
@@ -525,17 +521,18 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
final SailMasterMessage sailMasterMessage = new SailMasterMessageImpl(requestMessage.toString());
return sailMasterMessage;
}
protected abstract OutputStream getOutputStream() throws IOException;
protected abstract InputStream getInputStream() throws IOException;
private final Object ensureSocketIsOpenSemaphor = new Object();
private void ensureSocketIsOpen() throws InterruptedException {
private void ensureConnected() throws InterruptedException, NumberFormatException, IOException {
synchronized (ensureSocketIsOpenSemaphor) {
while (!stopped && socket == null) {
while (!stopped && !isConnected()) {
try {
logger.info("Opening socket to " + host + ":" + port + " and sending " + MessageType.OPN.name()
+ " message...");
socket = new Socket(host, port);
final OutputStream os = socket.getOutputStream();
final InputStream is = socket.getInputStream();
connect();
final OutputStream os = getOutputStream();
final InputStream is = getInputStream();
final SailMasterMessage opnRequest = createSailMasterMessage(MessageType.OPN, raceId);
sendMessage(opnRequest, os);
SailMasterMessage opnResponse = new SailMasterMessageImpl(receiveMessage(is));
@@ -543,7 +540,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
logger.info("Recevied non-OK response " + opnResponse + " in "+this+" for our request " + opnRequest
+ ". Closing socket and stopping because we have no hope for recovery");
stopped = true;
closeSocket();
disconnect();
} else {
logger.info("Received " + opnResponse + " in "+this+" which seems OK. Continuing with "
+ MessageType.LSN.name() + " request...");
@@ -565,7 +562,7 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
if (lsnResponse.getType() != MessageType.LSN || !"OK".equals(lsnResponse.getSections()[1])) {
logger.info("Received non-OK response " + lsnResponse + " for our request " + lsnRequest
+ " in "+this+". Closing socket and trying again in 1s...");
closeAndNullSocketAndWaitABit();
disconnectAndWaitABit();
} else {
logger.info("Received " + lsnResponse
+ " which seems to be OK. I think we're connected in "+this+"!");
@@ -573,28 +570,23 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
}
} catch (IOException e) {
logger.log(Level.INFO, "Exception trying to establish connection in "+this+". Trying again in 1s.", e);
closeAndNullSocketAndWaitABit();
disconnectAndWaitABit();
}
}
}
}
private void closeAndNullSocketAndWaitABit() throws InterruptedException {
closeSocket();
protected abstract void connect() throws IOException;
protected abstract boolean isConnected() throws IOException;
protected abstract void disconnect() throws IOException;
private void disconnectAndWaitABit() throws InterruptedException, IOException {
disconnect();
Thread.sleep(1000);
}
private void closeSocket() {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
logger.log(Level.INFO, "Exception trying to close socket. Maybe already closed. Continuing", e);
}
socket = null;
}
}
@Override
public Race getRace() {
return new RaceImpl(raceId, raceName, raceDescription, boatClass);
@@ -857,5 +849,5 @@ public class SailMasterConnectorImpl extends SailMasterTransceiverImpl implement
public void disableRacePositionData() throws UnknownHostException, IOException, InterruptedException {
sendRequestAndGetResponse(MessageType.RPD, "0");
}
}
@@ -0,0 +1,96 @@
package com.sap.sailing.domain.swisstimingadapter.impl;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.text.ParseException;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.common.TrackedRaceStatusEnum;
import com.sap.sailing.domain.swisstimingadapter.MessageType;
/**
* Implements the connector to the SwissTiming Sail Master system. It uses a host name and port number to establish the
* connection via TCP. The connector offers a number of explicit service request methods. Additionally, the connector
* can receive "spontaneous" events sent by the sail master system. Clients can register for those spontaneous events
* (see {@link #addSailMasterListener}).
* <p>
*
* When the connector is used with SailMaster instances hidden behind a "bridge" / firewall, no explicit requests are
* possible, and the connector has to rely solely on the events it receives. It may, though, load recorded race-specific
* messages through a {@link RaceSpecificMessageLoader} object. If a non-<code>null</code> {@link RaceSpecificMessageLoader}
* is provided to the constructor, the connector will fetch the {@link #getRace() race} from that loader.
* Additionally, the connector will use the loader upon each {@link #trackRace(String)} to load all messages recorded
* by the loader for the race requested so far.
* <p>
*
* Generally, the connector needs to be instructed for which races it shall handle events using calls to the
* {@link #trackRace} and {@link #stopTrackingRace} operations. {@link MessageType#isRaceSpecific() Race-specific
* messages} for other races are ignored and not forwarded to any listener.<p>
*
* Clients that want to wait until the connector changes to {@link #isStopped()} can {@link Object#wait()} on this
* object because it notifies all waiters when changing from !{@link #isStopped()} to {@link #isStopped()}.
*
* @author Axel Uhl (d043530)
*
*/
public class SailMasterConnectorForSocket extends AbstractSailMasterConnector {
private static final Logger logger = Logger.getLogger(SailMasterConnectorForSocket.class.getName());
private final String host;
private final int port;
/**
* Will have a socket connecting to {@link #host}:{@link #port}
*/
private Socket socket;
public SailMasterConnectorForSocket(String host, int port, String raceId, String raceName, String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker) throws InterruptedException, ParseException {
super(raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker);
this.host = host;
this.port = port;
startReceiverThread();
}
@Override
protected void disconnect() {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
logger.log(Level.INFO, "Exception trying to close socket. Maybe already closed. Continuing", e);
}
socket = null;
}
}
@Override
protected OutputStream getOutputStream() throws IOException {
return socket.getOutputStream();
}
@Override
protected InputStream getInputStream() throws IOException {
return socket.getInputStream();
}
@Override
protected void connect() throws IOException {
logger.info("Opening socket to " + host + ":" + port + " and sending " + MessageType.OPN.name()
+ " message...");
socket = new Socket(host, port);
}
@Override
protected boolean isConnected() {
return socket != null && socket.isConnected();
}
@Override
protected TrackedRaceStatusEnum getStatusAfterLoadingIsComplete() {
return TrackedRaceStatusEnum.TRACKING; // more stuff can come through the socket
}
}
@@ -0,0 +1,95 @@
package com.sap.sailing.domain.swisstimingadapter.impl;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.text.ParseException;
import java.util.logging.Logger;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.common.TrackedRaceStatusEnum;
import com.sap.sailing.domain.swisstimingadapter.MessageType;
import com.sap.sse.util.HttpUrlConnectionHelper;
/**
* Implements the connector to the SwissTiming Sail Master system. It uses a host name and port number to establish the
* connection via TCP. The connector offers a number of explicit service request methods. Additionally, the connector
* can receive "spontaneous" events sent by the sail master system. Clients can register for those spontaneous events
* (see {@link #addSailMasterListener}).
* <p>
*
* When the connector is used with SailMaster instances hidden behind a "bridge" / firewall, no explicit requests are
* possible, and the connector has to rely solely on the events it receives. It may, though, load recorded race-specific
* messages through a {@link RaceSpecificMessageLoader} object. If a non-<code>null</code> {@link RaceSpecificMessageLoader}
* is provided to the constructor, the connector will fetch the {@link #getRace() race} from that loader.
* Additionally, the connector will use the loader upon each {@link #trackRace(String)} to load all messages recorded
* by the loader for the race requested so far.
* <p>
*
* Generally, the connector needs to be instructed for which races it shall handle events using calls to the
* {@link #trackRace} and {@link #stopTrackingRace} operations. {@link MessageType#isRaceSpecific() Race-specific
* messages} for other races are ignored and not forwarded to any listener.<p>
*
* Clients that want to wait until the connector changes to {@link #isStopped()} can {@link Object#wait()} on this
* object because it notifies all waiters when changing from !{@link #isStopped()} to {@link #isStopped()}.
*
* @author Axel Uhl (d043530)
*
*/
public class SailMasterConnectorForUrlDownload extends AbstractSailMasterConnector {
private static final Logger logger = Logger.getLogger(SailMasterConnectorForUrlDownload.class.getName());
private final URL raceDataUrl;
/**
* If the {@link #socket} is not being used, a {@link HttpURLConnection} is expected to provide the
* {@link InputStream} from which this connector reads.
*/
private URLConnection urlConnection;
public SailMasterConnectorForUrlDownload(String raceId, URL raceDataUrl, String raceName, String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker) throws InterruptedException, ParseException {
super(raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker);
this.raceDataUrl = raceDataUrl;
startReceiverThread();
}
@Override
protected OutputStream getOutputStream() throws IOException {
return null; // we don't write to the URL connection's output stream
}
@Override
protected InputStream getInputStream() throws IOException {
return urlConnection.getInputStream();
}
@Override
protected void connect() throws IOException {
logger.info("Connecting to "+raceDataUrl);
urlConnection = HttpUrlConnectionHelper.redirectConnection(raceDataUrl);
}
@Override
protected boolean isConnected() throws IOException {
return urlConnection != null && getInputStream() != null;
}
@Override
protected void disconnect() throws IOException {
if (urlConnection != null) {
if (urlConnection.getInputStream() != null) {
urlConnection.getInputStream().close();
}
urlConnection = null;
stop(); // disconnecting from the URL also means to stop the connector
}
}
@Override
protected TrackedRaceStatusEnum getStatusAfterLoadingIsComplete() {
return TrackedRaceStatusEnum.FINISHED; // nothing more to come
}
}
@@ -1,33 +1,41 @@
package com.sap.sailing.domain.swisstimingadapter.impl;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.swisstimingadapter.SailMasterMessage;
public class SailMasterLiveSimulatorConnectorImpl extends SailMasterConnectorImpl {
/**
* Delivers SwissTiming messages with a constant delay (default 250ms) between them. This produces a simulated "live feed"
* that may be used for testing purposes.
*/
public class SailMasterLiveSimulatorConnectorImpl extends SailMasterConnectorForSocket {
private final static Logger logger = Logger.getLogger(SailMasterLiveSimulatorConnectorImpl.class.getName());
private final List<SailMasterMessage> bufferedMessageList;
private long messageDeliveryIntervalInMs = Long.valueOf(System.getProperty("simulateLiveMode.delayInMillis", "250"));
public SailMasterLiveSimulatorConnectorImpl(String host, int port, String raceId, String raceName, String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker)
throws InterruptedException, ParseException {
super(host, port, raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker);
bufferedMessageList = Collections.synchronizedList(new ArrayList<SailMasterMessage>());
super(host, port, raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker); // causes original delivery to this.notifyListeners(...)
bufferedMessageList = Collections.synchronizedList(new LinkedList<>());
Thread messageDeliveryThread = new Thread("SailMasterLiveSimulatorConnector") {
public void run() {
while(true) {
while (true) {
try {
if(!bufferedMessageList.isEmpty()) {
SailMasterMessage message = bufferedMessageList.get(0);
if (!bufferedMessageList.isEmpty()) {
SailMasterMessage message = bufferedMessageList.remove(0);
notifyParentListeners(message);
bufferedMessageList.remove(0);
}
}
Thread.sleep(messageDeliveryIntervalInMs);
} catch (Exception e) {
logger.log(Level.SEVERE, "Error trying to simulate SwissTiming messages", e);
}
}
}
@@ -27,23 +27,27 @@ public class SailMasterTransceiverImpl implements SailMasterTransceiver {
*/
@Override
public synchronized void sendMessage(String message, OutputStream os) throws IOException {
os.write(STX);
os.write(message.getBytes(Charset.forName("cp1252")));
os.write(ETX);
if (os != null) {
os.write(STX);
os.write(message.getBytes(Charset.forName("cp1252")));
os.write(ETX);
}
}
@Override
public synchronized void sendMessage(SailMasterMessage message, OutputStream os) throws IOException {
if (message.getSequenceNumber() == null) {
sendMessage(message.getMessage(), os);
} else {
StringBuilder sb = new StringBuilder();
sb.append(message.getSequenceNumber());
sb.append('|');
sb.append(message.getMessage());
sendMessage(sb.toString(), os);
if (os != null) {
if (message.getSequenceNumber() == null) {
sendMessage(message.getMessage(), os);
} else {
StringBuilder sb = new StringBuilder();
sb.append(message.getSequenceNumber());
sb.append('|');
sb.append(message.getMessage());
sendMessage(sb.toString(), os);
}
os.flush();
}
os.flush();
}
/**
@@ -34,6 +34,7 @@ import com.sap.sailing.xrr.schema.Person;
import com.sap.sailing.xrr.schema.RaceResult;
import com.sap.sailing.xrr.schema.RegattaResults;
import com.sap.sailing.xrr.schema.Team;
import com.sap.sse.util.HttpUrlConnectionHelper;
public class SwissTimingAdapterImpl implements SwissTimingAdapter {
private final SwissTimingFactory swissTimingFactory;
@@ -145,7 +146,7 @@ public class SwissTimingAdapterImpl implements SwissTimingAdapter {
public RegattaResults readRegattaEntryListFromXrrUrl(String xrrEntryListUrl) throws IOException, JAXBException {
// try to read the entry list from manage2sail
URL regattaEntryListUrl = new URL(xrrEntryListUrl);
URLConnection regattaEntryListConn = regattaEntryListUrl.openConnection();
URLConnection regattaEntryListConn = HttpUrlConnectionHelper.redirectConnection(regattaEntryListUrl);
RegattaResults regattaResults = ParserFactory.INSTANCE
.createParser((InputStream) regattaEntryListConn.getContent(), xrrEntryListUrl).parse();
return regattaResults;
@@ -2,6 +2,7 @@ package com.sap.sailing.domain.swisstimingadapter.impl;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.util.HashMap;
@@ -44,12 +45,12 @@ public class SwissTimingFactoryImpl implements SwissTimingFactory {
}
@Override
public SailMasterConnector getOrCreateSailMasterConnector(String host, int port, String raceId, String raceName,
String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker) throws InterruptedException, ParseException {
public SailMasterConnector getOrCreateSailMasterConnector(String host, int port, String raceId, URL raceDataUrl,
String raceName, String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker) throws InterruptedException, ParseException {
if (Boolean.valueOf(System.getProperty("simulateLiveMode", "false"))) {
return getOrCreateSailMasterLiveSimulatorConnector(host, port, raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker);
} else {
Util.Triple<String, Integer, String> key = new Util.Triple<String, Integer, String>(host, port, raceId);
Util.Triple<String, Integer, String> key = new Util.Triple<>(host, port, raceId);
SailMasterConnector result = connectors.get(key);
if (result == null || result.isStopped()) {
if (result == null) {
@@ -57,7 +58,14 @@ public class SwissTimingFactoryImpl implements SwissTimingFactory {
} else {
logger.info("Creating a new connector for "+key+" because the old one was stopped");
}
result = new SailMasterConnectorImpl(host, port, raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker);
if (host == null) {
if (raceDataUrl == null) {
throw new IllegalArgumentException("raceDataUrl must be provided if no host/port is provided");
}
result = new SailMasterConnectorForUrlDownload(raceId, raceDataUrl, raceName, raceDescription, boatClass, swissTimingRaceTracker);
} else {
result = new SailMasterConnectorForSocket(host, port, raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker);
}
connectors.put(key, result);
// TODO how do connectors get stopped, terminated and removed from the connectors map again?
} else {
@@ -72,13 +80,10 @@ public class SwissTimingFactoryImpl implements SwissTimingFactory {
String raceDescription, BoatClass boatClass, SwissTimingRaceTrackerImpl swissTimingRaceTracker) throws InterruptedException, ParseException {
Util.Triple<String, Integer, String> key = new Util.Triple<>(host, port, raceId);
SailMasterConnector result = connectors.get(key);
if (result == null) {
if (result == null || result.isStopped()) {
result = new SailMasterLiveSimulatorConnectorImpl(host, port, raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker);
connectors.put(key, result);
// TODO how do connectors get stopped, terminated and removed from the connectors map again?
} else if (result.isStopped()) {
result = new SailMasterLiveSimulatorConnectorImpl(host, port, raceId, raceName, raceDescription, boatClass, swissTimingRaceTracker);
connectors.put(key, result);
}
return result;
}
@@ -4,6 +4,7 @@ import java.io.IOException;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.util.Collection;
@@ -36,7 +37,6 @@ import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sailing.domain.common.impl.WindImpl;
import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID;
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
import com.sap.sailing.domain.common.tracking.TrackingConnectorType;
import com.sap.sailing.domain.racelog.RaceLogAndTrackedRaceResolver;
import com.sap.sailing.domain.racelog.RaceLogStore;
import com.sap.sailing.domain.regattalog.RegattaLogStore;
@@ -49,6 +49,7 @@ import com.sap.sailing.domain.swisstimingadapter.RacingStatus;
import com.sap.sailing.domain.swisstimingadapter.SailMasterConnector;
import com.sap.sailing.domain.swisstimingadapter.SailMasterListener;
import com.sap.sailing.domain.swisstimingadapter.StartList;
import com.sap.sailing.domain.swisstimingadapter.SwissTimingAdapter;
import com.sap.sailing.domain.swisstimingadapter.SwissTimingFactory;
import com.sap.sailing.domain.swisstimingadapter.SwissTimingRaceTracker;
import com.sap.sailing.domain.tracking.AbstractRaceTrackerImpl;
@@ -60,13 +61,13 @@ import com.sap.sailing.domain.tracking.MarkPassing;
import com.sap.sailing.domain.tracking.RaceHandle;
import com.sap.sailing.domain.tracking.RaceTracker;
import com.sap.sailing.domain.tracking.RaceTrackingHandler;
import com.sap.sailing.domain.tracking.RaceTrackingHandler.DefaultRaceTrackingHandler;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.TrackedRaceStatus;
import com.sap.sailing.domain.tracking.TrackedRegattaRegistry;
import com.sap.sailing.domain.tracking.TrackingDataLoader;
import com.sap.sailing.domain.tracking.WindStore;
import com.sap.sailing.domain.tracking.WindTrack;
import com.sap.sailing.domain.tracking.RaceTrackingHandler.DefaultRaceTrackingHandler;
import com.sap.sailing.domain.tracking.impl.TrackedRaceStatusImpl;
import com.sap.sailing.domain.tracking.impl.TrackingConnectorInfoImpl;
import com.sap.sailing.domain.tracking.impl.UpdateHandler;
@@ -187,8 +188,32 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl
}
// Ensure this is called last, otherwise the connector starts running without having a complete racetracker
this.connector = factory.getOrCreateSailMasterConnector(connectivityParams.getHostname(),
connectivityParams.getPort(), connectivityParams.getRaceID(), connectivityParams.getRaceName(),
connectivityParams.getRaceDescription(), connectivityParams.getBoatClass(), this);
connectivityParams.getPort(), connectivityParams.getRaceID(), getRaceDataUrl(connectivityParams),
connectivityParams.getRaceName(), connectivityParams.getRaceDescription(), connectivityParams.getBoatClass(), this);
}
/**
* If a valid hostname/port combination is set in the connectivity parameters, {@code null} is returned. Otherwise,
* a {@link URL} is constructed from the {@link SwissTimingTrackingConnectivityParameters#getManage2SailEventUrl()}
* by stripping all URL parameters and replacing the last path element which is expected to be a file name for the
* {@code .json} file by the {race-id}.log combination, expecting the race data to reside in the same folder as
* the JSON file describing the Manage2Sail event.
*/
private URL getRaceDataUrl(SwissTimingTrackingConnectivityParameters connectivityParams) throws MalformedURLException {
final URL result;
if (connectivityParams.getHostname() == null) {
final java.net.URL manage2SailEventURL = new URL(connectivityParams.getManage2SailEventUrl());
final String path = manage2SailEventURL.getPath();
final String logFile = path.substring(0, path.lastIndexOf('/')+1) + connectivityParams.getRaceID()+".log";
if (manage2SailEventURL.getPort() != -1) {
result = new java.net.URL(manage2SailEventURL.getProtocol(), manage2SailEventURL.getHost(), manage2SailEventURL.getPort(), logFile);
} else {
result = new java.net.URL(manage2SailEventURL.getProtocol(), manage2SailEventURL.getHost(), logFile);
}
} else {
result = null;
}
return result;
}
@Override
@@ -415,18 +440,25 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl
}
@Override
public void storedDataProgress(String raceID, double progress) {
public void storedDataProgress(String raceID, double progress, TrackedRaceStatusEnum statusAfterLoadingComplete) {
assert this.raceID.equals(raceID);
if (isTrackedRaceStillReachable()) {
final TrackedRaceStatusImpl newStatus;
if (progress == 0.0) {
newStatus = new TrackedRaceStatusImpl(TrackedRaceStatusEnum.PREPARED, 0.0);
} else if (progress == 1.0) {
newStatus = new TrackedRaceStatusImpl(TrackedRaceStatusEnum.TRACKING, progress);
newStatus = new TrackedRaceStatusImpl(statusAfterLoadingComplete, progress);
} else {
newStatus = new TrackedRaceStatusImpl(TrackedRaceStatusEnum.LOADING, progress);
}
trackedRace.onStatusChanged(this, newStatus);
if (newStatus.getStatus() == TrackedRaceStatusEnum.FINISHED) {
try {
trackedRegattaRegistry.stopTracker(getRegatta(), this);
} catch (IOException | InterruptedException e) {
logger.log(Level.SEVERE, "Error trying to stop the tracker "+this, e);
}
}
}
}
@@ -508,7 +540,7 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl
}
}, useInternalMarkPassingAlgorithm, raceLogResolver,
/* Not needed because the RaceTracker is not active on a replica */ Optional.empty(),
new TrackingConnectorInfoImpl(TrackingConnectorType.SwissTiming, /*no api connection to query the webUrl*/ null));
new TrackingConnectorInfoImpl(SwissTimingAdapter.NAME, SwissTimingAdapter.DEFAULT_URL,/*no api connection to query the webUrl*/ null));
addUpdateHandlers();
notifyRaceCreationListeners();
logger.info("Created SwissTiming RaceDefinition and TrackedRace for "+race.getName());
@@ -17,7 +17,6 @@ import com.sap.sailing.domain.tracking.TrackedRegattaRegistry;
import com.sap.sailing.domain.tracking.TrackerManager;
public interface SwissTimingReplayService {
List<SwissTimingReplayRace> parseJSONObject(InputStream inputStream, String swissTimingUrlText) throws IOException,
ParseException, org.json.simple.parser.ParseException;
@@ -36,12 +36,12 @@ import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sailing.domain.common.impl.WindImpl;
import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID;
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
import com.sap.sailing.domain.common.tracking.TrackingConnectorType;
import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl;
import com.sap.sailing.domain.racelog.RaceLogAndTrackedRaceResolver;
import com.sap.sailing.domain.racelog.RaceLogStore;
import com.sap.sailing.domain.regattalog.RegattaLogStore;
import com.sap.sailing.domain.swisstimingadapter.DomainFactory;
import com.sap.sailing.domain.swisstimingadapter.SwissTimingAdapter;
import com.sap.sailing.domain.swisstimingreplayadapter.CompetitorStatus;
import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayListener;
import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayParser;
@@ -417,7 +417,7 @@ public class SwissTimingReplayToDomainAdapter extends SwissTimingReplayAdapter i
/* time over which to average speed: */ race.getBoatClass().getApproximateManeuverDurationInMilliseconds(),
/* raceDefinitionSetToUpdate */ null, useInternalMarkPassingAlgorithm, raceLogResolver,
/* Not needed because the RaceTracker is not active on a replica */ Optional.empty(),
new TrackingConnectorInfoImpl(TrackingConnectorType.SwissTiming, /*no api connection to query the webUrl*/ null));
new TrackingConnectorInfoImpl(SwissTimingAdapter.NAME, SwissTimingAdapter.DEFAULT_URL,/*no api connection to query the webUrl*/ null));
trackedRace.onStatusChanged(this, new TrackedRaceStatusImpl(TrackedRaceStatusEnum.LOADING, 0));
TimePoint bestStartTimeKnownSoFar = bestStartTimePerRaceID.get(currentRaceID);
if (bestStartTimeKnownSoFar != null) {
@@ -24,6 +24,9 @@ import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
public interface TracTracAdapter {
static final String NAME = "TracTrac";
static final String DEFAULT_URL = "https://www.tractrac.com/";
DomainFactory getTracTracDomainFactory();
/**
@@ -105,5 +108,5 @@ public interface TracTracAdapter {
TracTracConfiguration createTracTracConfiguration(String creatorName, String name, String jsonURL,
String liveDataURI,
String storedDataURI, String courseDesignUpdateURI, String tracTracUsername, String tracTracPassword);
}
@@ -56,7 +56,6 @@ import com.sap.sailing.domain.common.RankingMetrics;
import com.sap.sailing.domain.common.ScoringSchemeType;
import com.sap.sailing.domain.common.impl.DegreePosition;
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
import com.sap.sailing.domain.common.tracking.TrackingConnectorType;
import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl;
import com.sap.sailing.domain.leaderboard.Leaderboard;
import com.sap.sailing.domain.leaderboard.LeaderboardGroup;
@@ -90,6 +89,7 @@ import com.sap.sailing.domain.tractracadapter.MetadataParser.BoatMetaData;
import com.sap.sailing.domain.tractracadapter.MetadataParser.ControlPointMetaData;
import com.sap.sailing.domain.tractracadapter.Receiver;
import com.sap.sailing.domain.tractracadapter.ReceiverType;
import com.sap.sailing.domain.tractracadapter.TracTracAdapter;
import com.sap.sailing.domain.tractracadapter.TracTracConfiguration;
import com.sap.sailing.domain.tractracadapter.TracTracControlPoint;
import com.sap.sailing.domain.tractracadapter.TracTracRaceTracker;
@@ -633,11 +633,13 @@ public class DomainFactoryImpl implements DomainFactory {
@Override
public DynamicTrackedRace getOrCreateRaceDefinitionAndTrackedRace(DynamicTrackedRegatta trackedRegatta, UUID raceId,
String raceName, BoatClass boatClass, Map<Competitor, Boat> competitorsAndBoats, Course course, Iterable<Sideline> sidelines, WindStore windStore,
long delayToLiveInMillis, long millisecondsOverWhichToAverageWind,
DynamicRaceDefinitionSet raceDefinitionSetToUpdate, URI tracTracUpdateURI, UUID tracTracEventUuid,
String tracTracUsername, String tracTracPassword, boolean ignoreTracTracMarkPassings, RaceLogAndTrackedRaceResolver raceLogResolver,
Consumer<DynamicTrackedRace> runBeforeExposingRace, IRace tractracRace, RaceTrackingHandler raceTrackingHandler) {
String raceName, BoatClass boatClass, Map<Competitor, Boat> competitorsAndBoats, Course course,
Iterable<Sideline> sidelines, WindStore windStore, long delayToLiveInMillis,
long millisecondsOverWhichToAverageWind, DynamicRaceDefinitionSet raceDefinitionSetToUpdate,
URI tracTracUpdateURI, UUID tracTracEventUuid, String tracTracUsername, String tracTracPassword,
boolean ignoreTracTracMarkPassings, RaceLogAndTrackedRaceResolver raceLogResolver,
Consumer<DynamicTrackedRace> runBeforeExposingRace, IRace tractracRace,
RaceTrackingHandler raceTrackingHandler) {
synchronized (raceCache) {
RaceDefinition raceDefinition = raceCache.get(raceId);
if (raceDefinition == null) {
@@ -661,7 +663,10 @@ public class DomainFactoryImpl implements DomainFactory {
trackedRegatta.getRegatta().addRace(raceDefinition);
TrackingConnectorInfo trackingConnectorInfo = null;
if (tractracRace != null) {
trackingConnectorInfo = new TrackingConnectorInfoImpl(TrackingConnectorType.TracTrac, tractracRace.getEvent().getWebURL());
final URL webUrl = tractracRace.getEvent().getWebURL();
final String webUrlString = webUrl == null ? null : webUrl.toString();
trackingConnectorInfo = new TrackingConnectorInfoImpl(TracTracAdapter.NAME,
TracTracAdapter.DEFAULT_URL, webUrlString);
}
trackedRace = createTrackedRace(trackedRegatta, raceDefinition, sidelines, windStore,
delayToLiveInMillis, millisecondsOverWhichToAverageWind, raceDefinitionSetToUpdate, ignoreTracTracMarkPassings,
@@ -1,6 +1,7 @@
package com.sap.sailing.domain.tractracadapter.impl;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@@ -20,7 +21,6 @@ import com.sap.sailing.domain.base.RaceDefinition;
import com.sap.sailing.domain.base.Regatta;
import com.sap.sailing.domain.base.Sideline;
import com.sap.sailing.domain.common.PassingInstruction;
import com.sap.sailing.domain.common.tracking.TrackingConnectorType;
import com.sap.sailing.domain.leaderboard.LeaderboardGroupResolver;
import com.sap.sailing.domain.racelog.RaceLogAndTrackedRaceResolver;
import com.sap.sailing.domain.tracking.DynamicRaceDefinitionSet;
@@ -31,6 +31,7 @@ import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.WindStore;
import com.sap.sailing.domain.tracking.impl.TrackingConnectorInfoImpl;
import com.sap.sailing.domain.tractracadapter.DomainFactory;
import com.sap.sailing.domain.tractracadapter.TracTracAdapter;
import com.sap.sailing.domain.tractracadapter.TracTracControlPoint;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util.Pair;
@@ -269,14 +270,17 @@ public class RaceCourseReceiver extends AbstractReceiverWithQueue<IControlRoute,
private DynamicTrackedRace createTrackedRace(RaceDefinition race, Iterable<Sideline> sidelines,
Consumer<DynamicTrackedRace> runAfterCreatingTrackedRace) {
DynamicTrackedRace trackedRace = raceTrackingHandler.createTrackedRace(getTrackedRegatta(), race, sidelines, windStore,
delayToLiveInMillis, millisecondsOverWhichToAverageWind,
final URL webUrl = tractracRace.getEvent().getWebURL();
final String webUrlString = webUrl == null ? null : webUrl.toString();
DynamicTrackedRace trackedRace = raceTrackingHandler.createTrackedRace(getTrackedRegatta(), race, sidelines,
windStore, delayToLiveInMillis, millisecondsOverWhichToAverageWind,
/* time over which to average speed: */ race.getBoatClass()
.getApproximateManeuverDurationInMilliseconds(),
raceDefinitionSetToUpdate, useInternalMarkPassingAlgorithm, raceLogResolver,
/* ThreadLocalTransporter not needed because the RaceTracker is not active on a replica */ Optional
.empty(),
new TrackingConnectorInfoImpl(TrackingConnectorType.TracTrac, tractracRace.getEvent().getWebURL()));
new TrackingConnectorInfoImpl(TracTracAdapter.NAME, TracTracAdapter.DEFAULT_URL,
webUrlString));
if (runAfterCreatingTrackedRace != null) {
runAfterCreatingTrackedRace.accept(trackedRace);
}
@@ -18,6 +18,7 @@ import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import com.sap.sailing.domain.base.RaceDefinition;
import com.sap.sailing.domain.base.Regatta;
import com.sap.sailing.domain.common.Wind;
import com.sap.sailing.domain.common.WindSource;
import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID;
@@ -34,8 +35,11 @@ public class WindFinderPollerTest {
factory.addReviewedSpotCollection(new ReviewedSpotsCollectionImpl("schilksee"));
final List<Pair<Wind, WindSource>> wind = Collections.synchronizedList(new ArrayList<>());
final RaceDefinition mockedRaceDefinition = mock(RaceDefinition.class);
final Regatta mockedRegatta = mock(Regatta.class);
final DynamicTrackedRegatta mockedTrackedRegatta = mock(DynamicTrackedRegatta.class);
final DynamicTrackedRace mockedTrackedRace = mock(DynamicTrackedRace.class);
when(mockedTrackedRace.getTrackedRegatta()).thenReturn(mockedTrackedRegatta);
when(mockedTrackedRegatta.getRegatta()).thenReturn(mockedRegatta);
when(mockedTrackedRegatta.getExistingTrackedRace(mockedRaceDefinition)).thenReturn(mockedTrackedRace);
when(mockedTrackedRegatta.getTrackedRace(mockedRaceDefinition)).thenReturn(mockedTrackedRace);
when(mockedTrackedRace.recordWind(any(Wind.class), any(WindSourceWithAdditionalID.class))).thenAnswer(new Answer<Boolean>() {
@@ -104,10 +104,15 @@ public class ReviewedSpotsCollectionImpl implements ReviewedSpotsCollection {
private Iterable<Spot> loadSpots() throws IOException, ParseException, MalformedURLException {
final Iterable<Spot> result;
JSONArray spotsAsJson = (JSONArray) new JSONParser().parse(new InputStreamReader(
(InputStream) new URL(Activator.BASE_URL_FOR_JSON_DOCUMENTS+"/"+getId()+SPOT_LIST_DOCUMENT_SUFFIX).getContent()));
result = parser.parseSpots(spotsAsJson, this);
return result;
final InputStreamReader in = new InputStreamReader(
(InputStream) new URL(Activator.BASE_URL_FOR_JSON_DOCUMENTS+"/"+getId()+SPOT_LIST_DOCUMENT_SUFFIX).getContent());
try {
JSONArray spotsAsJson = (JSONArray) new JSONParser().parse(in);
result = parser.parseSpots(spotsAsJson, this);
return result;
} finally {
in.close();
}
}
@Override
@@ -7,8 +7,13 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.simple.JSONArray;
import org.json.simple.parser.JSONParser;
@@ -18,34 +23,83 @@ import com.sap.sailing.domain.common.Wind;
import com.sap.sailing.domain.common.windfinder.SpotDTO;
import com.sap.sailing.domain.windfinder.ReviewedSpotsCollection;
import com.sap.sailing.domain.windfinder.Spot;
import com.sap.sailing.domain.windfinder.WindFinderSpotListener;
import com.sap.sse.common.Duration;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
import com.sap.sse.util.ThreadPoolUtil;
public class SpotImpl extends SpotDTO implements Spot {
private static final long serialVersionUID = -7793334775486020313L;
private static final Logger logger = Logger.getLogger(SpotImpl.class.getName());
private final WindFinderReportParser parser;
private final ReviewedSpotsCollection collection;
private final ConcurrentMap<WindFinderSpotListener, Boolean> listeners;
private TimePoint latestMeasurement;
private static final Duration POLL_EVERY = Duration.ONE_MINUTE;
private ScheduledFuture<?> poller;
public SpotImpl(String name, String id, String keyword, String englishCountryName, Position position, WindFinderReportParser parser, ReviewedSpotsCollection collection) {
super(name, id, keyword, englishCountryName, position);
this.parser = parser;
this.collection = collection;
listeners = new ConcurrentHashMap<>();
}
@Override
public Wind getLatestMeasurement() throws NumberFormatException, ParseException, org.json.simple.parser.ParseException, MalformedURLException, IOException {
final Iterable<Wind> measurements = getAllMeasurements();
final Wind result;
if (measurements != null && !Util.isEmpty(measurements)) {
final List<Wind> measurementsSortedByTimepoint = new ArrayList<>();
Util.addAll(measurements, measurementsSortedByTimepoint);
Collections.sort(measurementsSortedByTimepoint, (w1, w2)->w1.getTimePoint().compareTo(w2.getTimePoint()));
result = Util.last(measurementsSortedByTimepoint);
} else {
result = null;
public void addListener(WindFinderSpotListener listener) {
if (listeners.isEmpty()) {
ensurePollerRunning();
}
listeners.put(listener, true);
}
private synchronized void ensurePollerRunning() {
if (poller == null) {
logger.info("Starting to poll from WindFinder spot "+getId()+" as the first listener started listening.");
poller = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor().scheduleAtFixedRate(this::poll,
/* initialDelay */ 0, /* period */ POLL_EVERY.asMillis(), TimeUnit.MILLISECONDS);
}
}
/**
* Fetches wind measurements from this spot and notifies new ones to the {@link #listeners}.
*/
private void poll() {
try {
final Iterable<Wind> windFixes = getAllMeasurementsAfter(latestMeasurement);
logger.fine("Received "+Util.size(windFixes)+" wind fixes from WindFinder spot "+getId()+". Notifying "+listeners.size()+" listeners.");
for (final Wind wind : windFixes) {
if (latestMeasurement == null || wind.getTimePoint().after(latestMeasurement)) {
latestMeasurement = wind.getTimePoint();
}
}
notifyListeners(windFixes);
} catch (IOException | ParseException | org.json.simple.parser.ParseException e) {
logger.log(Level.SEVERE, "Error trying to obtain WindFinder measurements from spot "+getId(), e);
}
}
private void notifyListeners(Iterable<Wind> windFixes) {
for (final WindFinderSpotListener listener : listeners.keySet()) {
listener.windDataReceived(windFixes, this);
}
}
private synchronized void stopPoller() {
logger.info("Stopping polling from WindFinder spot "+getId()+" as last listener stopped listening.");
poller.cancel(/* mayInterruptIfRunning */ false);
poller = null;
}
@Override
public void removeListener(WindFinderSpotListener listener) {
listeners.remove(listener);
if (listeners.isEmpty()) {
stopPoller();
}
return result;
}
@Override
@@ -20,6 +20,7 @@ import org.json.simple.parser.ParseException;
import org.osgi.util.tracker.ServiceTracker;
import com.sap.sailing.domain.base.RaceDefinition;
import com.sap.sailing.domain.common.RegattaIdentifier;
import com.sap.sailing.domain.common.WindFinderReviewedSpotsCollectionIdProvider;
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
import com.sap.sailing.domain.tracking.DynamicTrackedRegatta;
@@ -47,7 +48,7 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory {
/**
* Can hold reviewed spots collections. Starts out valid but empty. Useful if we're running outside of an OSGi
* environment such as a test set-up. The collections provided here will be added to the set returned by
* {@link #getReviewedSpotsCollections(boolean)}.
* {@link #getAllReviewedSpotsCollections(boolean)}.
*/
private Set<ReviewedSpotsCollection> reviewedSpotsCollections;
private final ServiceTracker<WindFinderReviewedSpotsCollectionIdProvider, WindFinderReviewedSpotsCollectionIdProvider> reviewedSpotsCollectionIdProvider;
@@ -63,9 +64,8 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory {
reviewedSpotsCollectionsByIdCache = executor.schedule(
()->{
final ConcurrentMap<String, ReviewedSpotsCollection> result = new ConcurrentHashMap<>();
reviewedSpotsCollectionIdProvider.getService();
try {
for (final ReviewedSpotsCollection c : loadSpotCollectionsFromProvider(/* lookupInCache */ false)) {
for (final ReviewedSpotsCollection c : loadAllSpotCollectionsFromProvider(/* lookupInCache */ false)) {
result.put(c.getId(), c);
}
} catch (Exception e) {
@@ -119,13 +119,26 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory {
}
@Override
public Iterable<ReviewedSpotsCollection> getReviewedSpotsCollections(boolean cached) throws InterruptedException, ExecutionException {
public Iterable<ReviewedSpotsCollection> getReviewedSpotsCollections(RegattaIdentifier regattaIdentifier) throws InterruptedException, ExecutionException {
final Set<ReviewedSpotsCollection> result = new HashSet<>();
result.addAll(reviewedSpotsCollections);
final WindFinderReviewedSpotsCollectionIdProvider provider;
if (reviewedSpotsCollectionIdProvider != null && (provider = reviewedSpotsCollectionIdProvider.getService()) != null) {
for (String id : provider.getWindFinderReviewedSpotsCollectionIdsByRegatta(regattaIdentifier)) {
result.add(getReviewedSpotsCollectionById(id, /* lookupInCache, re-use existing spot collection objects */ true));
}
}
return result;
}
@Override
public Iterable<ReviewedSpotsCollection> getAllReviewedSpotsCollections(boolean cached) throws InterruptedException, ExecutionException {
final Set<ReviewedSpotsCollection> result = new HashSet<>();
if (cached) {
result.addAll(reviewedSpotsCollectionsByIdCache.get().values());
} else {
result.addAll(reviewedSpotsCollections);
Util.addAll(loadSpotCollectionsFromProvider(/* lookupInCache */ true), result);
Util.addAll(loadAllSpotCollectionsFromProvider(/* lookupInCache */ true), result);
for (final ReviewedSpotsCollection c : result) {
reviewedSpotsCollectionsByIdCache.get().putIfAbsent(c.getId(), c);
}
@@ -133,11 +146,11 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory {
return result;
}
private Iterable<ReviewedSpotsCollection> loadSpotCollectionsFromProvider(boolean lookupInCache) throws InterruptedException, ExecutionException {
private Iterable<ReviewedSpotsCollection> loadAllSpotCollectionsFromProvider(boolean lookupInCache) throws InterruptedException, ExecutionException {
final Set<ReviewedSpotsCollection> result = new HashSet<>();
final WindFinderReviewedSpotsCollectionIdProvider provider;
if (reviewedSpotsCollectionIdProvider != null && (provider = reviewedSpotsCollectionIdProvider.getService()) != null) {
for (String id : provider.getWindFinderReviewedSpotsCollectionIds()) {
for (String id : provider.getAllWindFinderReviewedSpotsCollectionIds()) {
result.add(getReviewedSpotsCollectionById(id, lookupInCache));
}
}
@@ -158,7 +171,7 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory {
@Override
public Spot getSpotById(String spotId, boolean cached) throws MalformedURLException, IOException, ParseException, InterruptedException, ExecutionException {
Spot result = null;
for (final ReviewedSpotsCollection coll : getReviewedSpotsCollections(/* cached */ cached)) {
for (final ReviewedSpotsCollection coll : getAllReviewedSpotsCollections(/* cached */ cached)) {
for (final Spot spot : coll.getSpots(/* cached */ cached)) {
if (Util.equalsWithNull(spot.getId(), spotId)) {
result = spot;
@@ -178,4 +191,5 @@ public class WindFinderTrackerFactoryImpl implements WindFinderTrackerFactory {
reviewedSpotsCollections.remove(collection);
reviewedSpotsCollectionsByIdCache.get().remove(collection.getId());
}
}
@@ -6,10 +6,6 @@ import java.util.HashSet;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.simple.parser.ParseException;
@@ -21,10 +17,8 @@ import com.sap.sailing.domain.tracking.WindTrack;
import com.sap.sailing.domain.tracking.WindTracker;
import com.sap.sailing.domain.windfinder.ReviewedSpotsCollection;
import com.sap.sailing.domain.windfinder.Spot;
import com.sap.sse.common.Duration;
import com.sap.sailing.domain.windfinder.WindFinderSpotListener;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
import com.sap.sse.util.ThreadPoolUtil;
/**
* When an object of this tracker type is created and there are WindFinder stations available, it schedules regular
@@ -42,74 +36,53 @@ import com.sap.sse.util.ThreadPoolUtil;
* @author Axel Uhl (D043530)
*
*/
public class WindFinderWindTracker implements WindTracker, Runnable {
private static final Logger logger = Logger.getLogger(WindFinderWindTracker.class.getName());
private static final Duration POLL_EVERY = Duration.ONE_MINUTE;
public class WindFinderWindTracker implements WindTracker, WindFinderSpotListener {
private final DynamicTrackedRace trackedRace;
private final WindFinderTrackerFactoryImpl factory;
private final ScheduledFuture<?> poller;
private WeakHashMap<Spot, TimePoint> timePointOfLastMeasurement;
private final Iterable<Spot> spots;
/**
* The set of all {@link ReviewedSpotsCollection}s delivered by the {@link #factory} when this tracker
* was created. This is the basis for {@link #getUsefulSpots()} when evaluating, e.g., based on the
* {@link #trackedRace race's} location, which of the spots is actually useful for this tracker.
*/
private final Iterable<ReviewedSpotsCollection> allSpotCollections;
public WindFinderWindTracker(DynamicTrackedRace trackedRace, WindFinderTrackerFactoryImpl factory) throws InterruptedException, ExecutionException {
public WindFinderWindTracker(DynamicTrackedRace trackedRace, WindFinderTrackerFactoryImpl factory)
throws InterruptedException, ExecutionException, MalformedURLException, IOException, ParseException {
this.trackedRace = trackedRace;
this.factory = factory;
this.allSpotCollections = factory.getReviewedSpotsCollections(/* cached */ false); // obtain fresh copy of all spots, updating cache
this.timePointOfLastMeasurement = new WeakHashMap<>();
this.poller = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor().scheduleAtFixedRate(this,
/* initialDelay */ 0, /* period */ POLL_EVERY.asMillis(), TimeUnit.MILLISECONDS);
spots = subscribeToUsefulSpots();
}
/**
* This method is executed in order to poll the WindFinder spots for new measurements
*/
@Override
public void run() {
try {
final Iterable<Spot> usefulSpots = getUsefulSpots();
for (final Spot usefulSpot : usefulSpots) {
final Iterable<Wind> windFixes = usefulSpot.getAllMeasurementsAfter(timePointOfLastMeasurement.get(usefulSpot));
if (!Util.isEmpty(windFixes)) {
final WindSourceWithAdditionalID windSource = new WindSourceWithAdditionalID(WindSourceType.WINDFINDER, usefulSpot.getId());
final WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource);
for (final Wind wind : windFixes) {
final Wind existingFix = windTrack==null?null/*test scenario*/:windTrack.getFirstRawFixAtOrAfter(wind.getTimePoint());
if (existingFix == null || !existingFix.getTimePoint().equals(wind.getTimePoint())) {
// avoid duplicates by adding the same fix again
trackedRace.recordWind(wind, windSource);
if (!timePointOfLastMeasurement.containsKey(usefulSpot) || wind.getTimePoint().after(timePointOfLastMeasurement.get(usefulSpot))) {
timePointOfLastMeasurement.put(usefulSpot, wind.getTimePoint());
}
}
}
public void windDataReceived(Iterable<Wind> windFixes, Spot spot) {
final WindSourceWithAdditionalID windSource = new WindSourceWithAdditionalID(WindSourceType.WINDFINDER, spot.getId());
final WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource);
for (final Wind wind : windFixes) {
final Wind existingFix = windTrack==null?null/*test scenario*/:windTrack.getFirstRawFixAtOrAfter(wind.getTimePoint());
if (existingFix == null || !existingFix.getTimePoint().equals(wind.getTimePoint())) {
// avoid duplicates by adding the same fix again
trackedRace.recordWind(wind, windSource);
if (!timePointOfLastMeasurement.containsKey(spot) || wind.getTimePoint().after(timePointOfLastMeasurement.get(spot))) {
timePointOfLastMeasurement.put(spot, wind.getTimePoint());
}
}
} catch (Exception e) {
logger.log(Level.WARNING, "Exception trying to obtain WindFinder data for race "+trackedRace.getRace().getName(), e);
}
}
private Iterable<Spot> getUsefulSpots() throws MalformedURLException, IOException, ParseException, InterruptedException, ExecutionException {
final Set<Spot> spots = new HashSet<>();
for (final ReviewedSpotsCollection collection : allSpotCollections) {
// TODO bug1301 judge each spot's usefulness given the location of trackedRace
Util.addAll(collection.getSpots(/* cached */ false), spots);
public Iterable<Spot> subscribeToUsefulSpots()
throws MalformedURLException, IOException, ParseException, InterruptedException, ExecutionException {
final Set<Spot> result = new HashSet<>();
for (final ReviewedSpotsCollection spotCollection : factory.getReviewedSpotsCollections(trackedRace.getTrackedRegatta().getRegatta().getRegattaIdentifier())) {
for (final Spot spot : spotCollection.getSpots(/* cached */ true)) {
result.add(spot);
spot.addListener(this);
}
}
return spots;
return result;
}
@Override
public void stop() {
poller.cancel(/* mayInterruptIfRunning */ false);
for (final Spot spot : spots) {
spot.removeListener(this);
}
factory.trackerStopped(trackedRace.getRace());
}
@@ -481,43 +481,49 @@ public class CandidateChooserImpl implements CandidateChooser {
@Override
public void setFixedPassing(Competitor c, Integer zeroBasedIndexOfWaypoint, TimePoint t) {
LockUtil.lockForWrite(perCompetitorLocks.get(c));
try {
Candidate fixedCan = new CandidateForFixedMarkPassingImpl(zeroBasedIndexOfWaypoint + 1, t, 1, Util.get(race.getRace().getCourse().getWaypoints(), zeroBasedIndexOfWaypoint));
NavigableSet<Candidate> fixed = fixedPassings.get(c);
if (fixed != null) { // can only set the mark passing if the competitor is still part of this race
if (!fixed.add(fixedCan)) {
Candidate old = fixed.ceiling(fixedCan);
fixed.remove(old);
removeCandidates(c, Collections.singleton(old));
fixed.add(fixedCan);
final NamedReentrantReadWriteLock lock = perCompetitorLocks.get(c);
if (lock != null) { // otherwise this is a phony competitor and we aren't interested in fixed mark passings for it
LockUtil.lockForWrite(lock);
try {
Candidate fixedCan = new CandidateForFixedMarkPassingImpl(zeroBasedIndexOfWaypoint + 1, t, 1, Util.get(race.getRace().getCourse().getWaypoints(), zeroBasedIndexOfWaypoint));
NavigableSet<Candidate> fixed = fixedPassings.get(c);
if (fixed != null) { // can only set the mark passing if the competitor is still part of this race
if (!fixed.add(fixedCan)) {
Candidate old = fixed.ceiling(fixedCan);
fixed.remove(old);
removeCandidates(c, Collections.singleton(old));
fixed.add(fixedCan);
}
addCandidates(c, Collections.singleton(fixedCan));
findShortestPath(c);
}
addCandidates(c, Collections.singleton(fixedCan));
findShortestPath(c);
} finally {
LockUtil.unlockAfterWrite(lock);
}
} finally {
LockUtil.unlockAfterWrite(perCompetitorLocks.get(c));
}
}
@Override
public void removeFixedPassing(Competitor c, Integer zeroBasedIndexOfWaypoint) {
LockUtil.lockForWrite(perCompetitorLocks.get(c));
try {
Candidate toRemove = null;
for (Candidate can : fixedPassings.get(c)) {
if (can.getOneBasedIndexOfWaypoint() - 1 == zeroBasedIndexOfWaypoint) {
toRemove = can;
break;
final NamedReentrantReadWriteLock lock = perCompetitorLocks.get(c);
if (lock != null) { // otherwise this is a phony competitor and we aren't interested in fixed mark passings for it
LockUtil.lockForWrite(lock);
try {
Candidate toRemove = null;
for (Candidate can : fixedPassings.get(c)) {
if (can.getOneBasedIndexOfWaypoint() - 1 == zeroBasedIndexOfWaypoint) {
toRemove = can;
break;
}
}
if (toRemove != null) {
fixedPassings.get(c).remove(toRemove);
removeCandidates(c, Arrays.asList(toRemove));
findShortestPath(c);
}
} finally {
LockUtil.unlockAfterWrite(lock);
}
if (toRemove != null) {
fixedPassings.get(c).remove(toRemove);
removeCandidates(c, Arrays.asList(toRemove));
findShortestPath(c);
}
} finally {
LockUtil.unlockAfterWrite(perCompetitorLocks.get(c));
}
}
@@ -20,7 +20,10 @@ public class TimeRangeWithNullStartMeaningEmpty {
private final boolean toIsInclusive;
public TimeRangeWithNullStartMeaningEmpty(TimePoint from, TimePoint to, boolean toIsInclusive) {
timeRangeOrNull = from == null ? null : new TimeRangeImpl(from, to, toIsInclusive);
// the time range will be adjusted to an empty time range starting at "to" in case "from" is after
// "to"; this to avoid an IllegalArgumentException should "from" be after "to"; see bug 5362
timeRangeOrNull = from == null ? null : new TimeRangeImpl(
to==null || from.before(to) ? from : to, to, toIsInclusive);
this.from = from;
this.to = to;
this.toIsInclusive = toIsInclusive;
@@ -54,6 +57,11 @@ public class TimeRangeWithNullStartMeaningEmpty {
return getWithNewTo(toExclusive, /* toIsInclusive */ false);
}
/**
* @return {@code null} if {@link #from} is {@code null}, representing a time range that is open at its start;
* should {@link #from} have been set to a time point later than {@link #to}, the time range returned is
* {@link TimeRange#isEmpty() empty} and positioned at {@link #to}.
*/
public TimeRange getTimeRangeOrNull() {
return timeRangeOrNull;
}
@@ -16,8 +16,10 @@ import com.sap.sse.common.TimePoint;
*
*/
public interface Spot extends SpotBase {
Wind getLatestMeasurement() throws NumberFormatException, ParseException, org.json.simple.parser.ParseException, MalformedURLException, IOException;
void addListener(WindFinderSpotListener listener);
void removeListener(WindFinderSpotListener listener);
Iterable<Wind> getAllMeasurements() throws IOException, MalformedURLException, ParseException, org.json.simple.parser.ParseException;
/**
@@ -0,0 +1,7 @@
package com.sap.sailing.domain.windfinder;
import com.sap.sailing.domain.common.Wind;
public interface WindFinderSpotListener {
void windDataReceived(Iterable<Wind> windFixes, Spot spot);
}
@@ -6,14 +6,15 @@ import java.util.concurrent.ExecutionException;
import org.json.simple.parser.ParseException;
import com.sap.sailing.domain.common.RegattaIdentifier;
import com.sap.sailing.domain.tracking.WindTrackerFactory;
public interface WindFinderTrackerFactory extends WindTrackerFactory {
/**
* Obtains the reviewed spot collections that this factory knows about. This set is constructed from the collections
* provided explicitly using {@link #addReviewedSpotCollection(ReviewedSpotCollection)} and
* {@link #removeReviewedSpotCollection(ReviewedSpotsCollection)}, and is extended by the collections obtained
* provided explicitly using {@code addReviewedSpotCollection(ReviewedSpotCollection)} and
* {@code removeReviewedSpotCollection(ReviewedSpotsCollection)}, and is extended by the collections obtained
* through {@link #reviewedSpotsCollectionIdProvider} if a corresponding service can be resolved.
*
* @param cached
@@ -23,7 +24,13 @@ public interface WindFinderTrackerFactory extends WindTrackerFactory {
*
* @return a non-live set of spots collections known by this factory at this point in time
*/
Iterable<ReviewedSpotsCollection> getReviewedSpotsCollections(boolean cached) throws InterruptedException, ExecutionException;
Iterable<ReviewedSpotsCollection> getAllReviewedSpotsCollections(boolean cached) throws InterruptedException, ExecutionException;
/**
* Like {@link #getAllReviewedSpotsCollections(boolean)}, but never using caching, always asking the underlying provider for
* a fresh copy of spot collections, qualified by the regatta to which they shall apply.
*/
Iterable<ReviewedSpotsCollection> getReviewedSpotsCollections(RegattaIdentifier regattaIdentifier) throws InterruptedException, ExecutionException;
ReviewedSpotsCollection getReviewedSpotsCollectionById(String spotsCollectionId, boolean lookupInCache) throws InterruptedException, ExecutionException;
@@ -8,7 +8,11 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta property="og:type" content="website" />
<meta property="og:title" content="SAP Sailing" />
<meta property="og:description" content="Help sailors analyze performance and optimize strategy &#8226; Bring fans closer to the action
&#8226; Provide the media with information and insights to deliver a greater informed commentary" />
<meta property="og:image" content="http://media.sapsailing.com/2014/505Worlds/Images_Homepage/505Worlds2014_eventteaser.jpg" />
<!-- -->
<!-- Consider inlining CSS to reduce the number of requested files -->
<!-- -->
@@ -9,6 +9,11 @@
<head>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta property="og:type" content="website" />
<meta property="og:title" content="SAP Sailing" />
<meta property="og:description" content="Help sailors analyze performance and optimize strategy &#8226; Bring fans closer to the action
&#8226; Provide the media with information and insights to deliver a greater informed commentary" />
<meta property="og:image" content="http://media.sapsailing.com/2014/505Worlds/Images_Homepage/505Worlds2014_eventteaser.jpg" />
<!-- -->
<!-- Consider inlining CSS to reduce the number of requested files -->
+1 -1
View File
@@ -317,11 +317,11 @@ input.magnifier {
.dataMiningListBox {
min-width: 120px;
margin-right: 5px;
}
.queryDefinitionViewerContent {
background-color: white;
padding: 5px;
}
.filterDimensionsTitleLabel {
@@ -7,6 +7,11 @@
<meta content='width = device-width, initial-scale = 1.0, user-scalable = yes' name='viewport'>
<meta content='yes' name='apple-mobile-web-app-capable'>
<meta content='black' name='apple-mobile-web-app-status-bar-style'>
<meta property="og:type" content="website" />
<meta property="og:title" content="SAP Sailing" />
<meta property="og:description" content="Help sailors analyze performance and optimize strategy &#8226; Bring fans closer to the action
&#8226; Provide the media with information and insights to deliver a greater informed commentary" />
<meta property="og:image" content="http://media.sapsailing.com/2014/505Worlds/Images_Homepage/505Worlds2014_eventteaser.jpg" />
<!-- <link href='iphone-splash-screen.png' rel='apple-touch-startup-image'> -->
<link href='sap-sailing-app-icon${whitelabeled}.png' rel='apple-touch-icon'>
<link rel="shortcut icon" type="image/x-icon" href="images/sap${whitelabeled}.ico" />
+7 -4
View File
@@ -3,7 +3,11 @@
<head>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<link rel="shortcut icon" type="image/x-icon" href="images/sap${whitelabeled}.ico" selenium-id="shortcutIcon"/>
<meta property="og:type" content="website" />
<meta property="og:title" content="SAP Sailing" />
<meta property="og:description" content="Help sailors analyze performance and optimize strategy &#8226; Bring fans closer to the action
&#8226; Provide the media with information and insights to deliver a greater informed commentary" />
<meta property="og:image" content="http://media.sapsailing.com/2014/505Worlds/Images_Homepage/505Worlds2014_eventteaser.jpg" /> <link rel="shortcut icon" type="image/x-icon" href="images/sap${whitelabeled}.ico" selenium-id="shortcutIcon"/>
<link href='sap-sailing-app-icon${whitelabeled}.png' rel='apple-touch-icon' selenium-id="appIcon">
<title>${SAP}Sailing - Homepage</title>
@@ -14,8 +18,7 @@
href="/sailing-fontface-1.0.cache.css">
<script src="js/jquery-3.3.1.min.js"></script>
<link rel="stylesheet" type="text/css" href="js/jquery.slick/slick-1.8.1.css"/>
<script type="text/javascript" src="js/jquery.slick/slick-1.8.1.min.js"></script>
<script type="text/javascript" src="js/jquery.slick/slick-1.8.1.min.js"></script>
<link href="js/video-js/video-js.min.css" rel="stylesheet">
<link href="js/video-js/video-js-sapsailing.css" rel="stylesheet">
<script src="js/video-js/video.min.js"></script>
@@ -24,7 +27,7 @@
<script src="js/Youtube.min.js"></script>
<script src="js/Vimeo.js"></script>
<script>
videojs.options.techOrder = ['vimeo', 'youtube', 'html5'];
videojs.options.techOrder = ['vimeo', 'youtube', 'html5'];
</script>
<script>
document.clientConfigurationContext = {
@@ -2,6 +2,11 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta property="og:type" content="website" />
<meta property="og:title" content="SAP Sailing" />
<meta property="og:description" content="Help sailors analyze performance and optimize strategy &#8226; Bring fans closer to the action
&#8226; Provide the media with information and insights to deliver a greater informed commentary" />
<meta property="og:image" content="http://media.sapsailing.com/2014/505Worlds/Images_Homepage/505Worlds2014_eventteaser.jpg" />
<link rel="shortcut icon" type="image/x-icon" href="images/sap${whitelabeled}.ico" />
<link href='sap-sailing-app-icon${whitelabeled}.png' rel='apple-touch-icon'>
<title>${SAP}Sailing - Homepage</title>
@@ -2,6 +2,11 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta property="og:type" content="website" />
<meta property="og:title" content="SAP Sailing" />
<meta property="og:description" content="Help sailors analyze performance and optimize strategy &#8226; Bring fans closer to the action
&#8226; Provide the media with information and insights to deliver a greater informed commentary" />
<meta property="og:image" content="http://media.sapsailing.com/2014/505Worlds/Images_Homepage/505Worlds2014_eventteaser.jpg" />
<link rel="shortcut icon" type="image/x-icon" href="images/sap${whitelabeled}.ico" />
<link href='sap-sailing-app-icon${whitelabeled}.png' rel='apple-touch-icon'>
<title>${SAP}Sailing - Homepage</title>
+26 -5
View File
@@ -169,7 +169,7 @@ table button {
}
.busyIndicator-Simple {
margin-top: 7px;
margin-top: 7px;
}
.busyIndicator-Circle {
@@ -188,11 +188,24 @@ table button {
table.editableLeaderboardTable tr th:first-child { display: none; }
table.editableLeaderboardTable tr td:first-child { display: none; }
/* edit button of editable leaderboard table */
.editableLeaderboardContentPanel.mobile {
display: inline-block;
width: auto;
}
.editableLeaderboardContentPanel.mobile .editableLeaderboardTable {
width: auto;
}
.editableLeaderboardContentPanel.mobile .editableLeaderboardTable .totalRankColumn {
width: 60px;
}
/* edit button of editable leaderboard table
.editableLeaderboardTable .editButton {
padding: 0;
margin-left: 3px;
cursor: pointer; /* Mouse pointer on hover */
cursor: pointer;
width: 20px;
height: 20px;
display: flex;
@@ -201,11 +214,19 @@ table.editableLeaderboardTable tr td:first-child { display: none; }
background: url(images/edit.png) center center no-repeat;
width: 16px;
height: 16px;
} */
.editableLeaderboardTable .editButton {
margin-left: 3px;
padding: 0 2px;
cursor: pointer;
height: 20px;
font-size: 10px;
}
.editableLeaderboardTable .suppressButton, .suppressedCompetitorsTable .suppressButton {
.editableLeaderboardTable .suppressButton,
.suppressedCompetitorsTable .suppressButton {
padding: 0 2px;
cursor: pointer; /* Mouse pointer on hover */
cursor: pointer;
height: 20px;
font-size: 10px;
}
@@ -10,6 +10,11 @@
<meta content='width = device-width, initial-scale = 1.0, user-scalable = yes' name='viewport'>
<meta content='yes' name='apple-mobile-web-app-capable'>
<meta content='black' name='apple-mobile-web-app-status-bar-style'>
<meta property="og:type" content="website" />
<meta property="og:title" content="SAP Sailing" />
<meta property="og:description" content="Help sailors analyze performance and optimize strategy &#8226; Bring fans closer to the action
&#8226; Provide the media with information and insights to deliver a greater informed commentary" />
<meta property="og:image" content="http://media.sapsailing.com/2014/505Worlds/Images_Homepage/505Worlds2014_eventteaser.jpg" />
<!-- <link href='iphone-splash-screen.png' rel='apple-touch-startup-image'> -->
<link href='sap-sailing-app-icon${whitelabeled}.png' rel='apple-touch-icon'>
<link rel="shortcut icon" type="image/x-icon" href="images/sap${whitelabeled}.ico" />
@@ -8,6 +8,11 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta property="og:type" content="website" />
<meta property="og:title" content="SAP Sailing" />
<meta property="og:description" content="Help sailors analyze performance and optimize strategy &#8226; Bring fans closer to the action
&#8226; Provide the media with information and insights to deliver a greater informed commentary" />
<meta property="og:image" content="http://media.sapsailing.com/2014/505Worlds/Images_Homepage/505Worlds2014_eventteaser.jpg" />
<link rel="shortcut icon" type="image/x-icon" href="images/sap${whitelabeled}.ico" />
<!-- -->
@@ -10,6 +10,11 @@
<meta content='width = device-width, initial-scale = 1.0, user-scalable = yes' name='viewport'>
<meta content='yes' name='apple-mobile-web-app-capable'>
<meta content='black' name='apple-mobile-web-app-status-bar-style'>
<meta property="og:type" content="website" />
<meta property="og:title" content="SAP Sailing" />
<meta property="og:description" content="Help sailors analyze performance and optimize strategy &#8226; Bring fans closer to the action
&#8226; Provide the media with information and insights to deliver a greater informed commentary" />
<meta property="og:image" content="http://media.sapsailing.com/2014/505Worlds/Images_Homepage/505Worlds2014_eventteaser.jpg" />
<!-- <link href='iphone-splash-screen.png' rel='apple-touch-startup-image'> -->
<link href='sap-sailing-app-icon${whitelabeled}.png' rel='apple-touch-icon'>
<link rel="shortcut icon" type="image/x-icon" href="images/sap${whitelabeled}.ico" />
@@ -10,6 +10,11 @@
<meta content='width = device-width, initial-scale = 1.0, user-scalable = yes' name='viewport'>
<meta content='yes' name='apple-mobile-web-app-capable'>
<meta content='black' name='apple-mobile-web-app-status-bar-style'>
<meta property="og:type" content="website" />
<meta property="og:title" content="SAP Sailing" />
<meta property="og:description" content="Help sailors analyze performance and optimize strategy &#8226; Bring fans closer to the action
&#8226; Provide the media with information and insights to deliver a greater informed commentary" />
<meta property="og:image" content="http://media.sapsailing.com/2014/505Worlds/Images_Homepage/505Worlds2014_eventteaser.jpg" />
<!-- <link href='iphone-splash-screen.png' rel='apple-touch-startup-image'> -->
<link href='sap-sailing-app-icon${whitelabeled}.png' rel='apple-touch-icon'>
<link rel="shortcut icon" type="image/x-icon" href="images/sap${whitelabeled}.ico" />
@@ -8,7 +8,11 @@
<meta content='width = device-width, initial-scale = 1.0, user-scalable = yes' name='viewport'>
<meta content='yes' name='apple-mobile-web-app-capable'>
<meta content='black' name='apple-mobile-web-app-status-bar-style'>
<meta property="og:type" content="website" />
<meta property="og:title" content="SAP Sailing" />
<meta property="og:description" content="Help sailors analyze performance and optimize strategy &#8226; Bring fans closer to the action
&#8226; Provide the media with information and insights to deliver a greater informed commentary" />
<meta property="og:image" content="http://media.sapsailing.com/2014/505Worlds/Images_Homepage/505Worlds2014_eventteaser.jpg" />
<link href='sap-sailing-app-icon${whitelabeled}.png' rel='apple-touch-icon'>
<link rel="shortcut icon" type="image/x-icon" href="images/sap${whitelabeled}.ico" />
@@ -7,6 +7,11 @@
<meta content='width = device-width, initial-scale = 1.0, user-scalable = yes' name='viewport'>
<meta content='yes' name='apple-mobile-web-app-capable'>
<meta content='black' name='apple-mobile-web-app-status-bar-style'>
<meta property="og:type" content="website" />
<meta property="og:title" content="SAP Sailing" />
<meta property="og:description" content="Help sailors analyze performance and optimize strategy &#8226; Bring fans closer to the action
&#8226; Provide the media with information and insights to deliver a greater informed commentary" />
<meta property="og:image" content="http://media.sapsailing.com/2014/505Worlds/Images_Homepage/505Worlds2014_eventteaser.jpg" />
<!-- <link href='iphone-splash-screen.png' rel='apple-touch-startup-image'> -->
<link href='sap-sailing-app-icon${whitelabeled}.png' rel='apple-touch-icon'>
<link rel="shortcut icon" type="image/x-icon" href="images/sap${whitelabeled}.ico" />
@@ -12,6 +12,11 @@
<meta content='width = device-width, initial-scale = 1.0, user-scalable = yes' name='viewport'>
<meta content='yes' name='apple-mobile-web-app-capable'>
<meta content='black' name='apple-mobile-web-app-status-bar-style'>
<meta property="og:type" content="website" />
<meta property="og:title" content="SAP Sailing" />
<meta property="og:description" content="Help sailors analyze performance and optimize strategy &#8226; Bring fans closer to the action
&#8226; Provide the media with information and insights to deliver a greater informed commentary" />
<meta property="og:image" content="http://media.sapsailing.com/2014/505Worlds/Images_Homepage/505Worlds2014_eventteaser.jpg" />
<!-- <link href='iphone-splash-screen.png' rel='apple-touch-startup-image'> -->
<link href='sap-sailing-app-icon${whitelabeled}.png' rel='apple-touch-icon'>
<link rel="shortcut icon" type="image/x-icon" href="images/sap${whitelabeled}.ico" />
+44 -44
View File
@@ -13,9 +13,9 @@
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>useFileMappedBuffer</param-name>
<param-value>false</param-value>
</init-param>
<param-name>useFileMappedBuffer</param-name>
<param-value>false</param-value>
</init-param>
</servlet>
<!-- Apache Shiro -->
@@ -60,29 +60,29 @@
<url-pattern>*.html</url-pattern>
</filter-mapping>
<servlet>
<display-name>ClientConfigurationServlet</display-name>
<servlet-name>ClientConfigurationServlet</servlet-name>
<servlet-class>com.sap.sse.debranding.ClientConfigurationServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ClientConfigurationServlet</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
<servlet>
<display-name>ClientConfigurationServlet</display-name>
<servlet-name>ClientConfigurationServlet</servlet-name>
<servlet-class>com.sap.sse.debranding.ClientConfigurationServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ClientConfigurationServlet</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
<servlet>
<display-name>Status</display-name>
<servlet-name>Status</servlet-name>
<servlet-class>com.sap.sailing.gwt.ui.server.StatusServlet</servlet-class>
<display-name>Status</display-name>
<servlet-name>Status</servlet-name>
<servlet-class>com.sap.sailing.gwt.ui.server.StatusServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Status</servlet-name>
<url-pattern>/status</url-pattern>
</servlet-mapping>
<servlet>
<display-name>Sailing Service</display-name>
<servlet-name>SailingService</servlet-name>
<servlet-class>com.sap.sailing.gwt.ui.server.SailingServiceImpl</servlet-class>
<display-name>Sailing Service</display-name>
<servlet-name>SailingService</servlet-name>
<servlet-class>com.sap.sailing.gwt.ui.server.SailingServiceImpl</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>SailingService</servlet-name>
@@ -90,21 +90,21 @@
<url-pattern>/service/sailing/*</url-pattern>
</servlet-mapping>
<servlet>
<display-name>Sailing ServiceWrite</display-name>
<servlet-name>SailingServiceWrite</servlet-name>
<servlet-class>com.sap.sailing.gwt.ui.server.SailingServiceWriteImpl</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>SailingServiceWrite</servlet-name>
<url-pattern>/service/sailingmaster</url-pattern>
<url-pattern>/service/sailingmaster/*</url-pattern>
</servlet-mapping>
<servlet>
<display-name>Sailing ServiceWrite</display-name>
<servlet-name>SailingServiceWrite</servlet-name>
<servlet-class>com.sap.sailing.gwt.ui.server.SailingServiceWriteImpl</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>SailingServiceWrite</servlet-name>
<url-pattern>/service/sailingmaster</url-pattern>
<url-pattern>/service/sailingmaster/*</url-pattern>
</servlet-mapping>
<servlet>
<display-name>Server Configuration Service</display-name>
<servlet-name>ServerConfigurationService</servlet-name>
<servlet-class>com.sap.sailing.gwt.server.ServerConfigurationServiceImpl</servlet-class>
<display-name>Server Configuration Service</display-name>
<servlet-name>ServerConfigurationService</servlet-name>
<servlet-class>com.sap.sailing.gwt.server.ServerConfigurationServiceImpl</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ServerConfigurationService</servlet-name>
@@ -112,9 +112,9 @@
</servlet-mapping>
<servlet>
<display-name>Sailing Dispatch Service</display-name>
<servlet-name>sailingDispatch</servlet-name>
<servlet-class>com.sap.sailing.gwt.home.server.servlets.SailingDispatchServlet</servlet-class>
<display-name>Sailing Dispatch Service</display-name>
<servlet-name>sailingDispatch</servlet-name>
<servlet-class>com.sap.sailing.gwt.home.server.servlets.SailingDispatchServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>sailingDispatch</servlet-name>
@@ -123,9 +123,9 @@
</servlet-mapping>
<servlet>
<display-name>Media Service</display-name>
<servlet-name>MediaService</servlet-name>
<servlet-class>com.sap.sailing.gwt.ui.server.MediaServiceImpl</servlet-class>
<display-name>Media Service</display-name>
<servlet-name>MediaService</servlet-name>
<servlet-class>com.sap.sailing.gwt.ui.server.MediaServiceImpl</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>MediaService</servlet-name>
@@ -143,9 +143,9 @@
</servlet-mapping>
<servlet>
<display-name>Simulator Service</display-name>
<servlet-name>SimulatorService</servlet-name>
<servlet-class>com.sap.sailing.gwt.ui.server.SimulatorServiceImpl</servlet-class>
<display-name>Simulator Service</display-name>
<servlet-name>SimulatorService</servlet-name>
<servlet-class>com.sap.sailing.gwt.ui.server.SimulatorServiceImpl</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>SimulatorService</servlet-name>
@@ -153,9 +153,9 @@
</servlet-mapping>
<servlet>
<display-name>Data Mining Service</display-name>
<servlet-name>DataMiningService</servlet-name>
<servlet-class>com.sap.sailing.gwt.ui.server.DataMiningServiceImpl</servlet-class>
<display-name>Data Mining Service</display-name>
<servlet-name>DataMiningService</servlet-name>
<servlet-class>com.sap.sailing.gwt.ui.server.DataMiningServiceImpl</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DataMiningService</servlet-name>
+2 -1
View File
@@ -55,5 +55,6 @@ bin.includes = META-INF/,\
PairingList.css,\
PairingList.html,\
com.sap.sailing.gwt.ui.PairingList/,\
src/main/resources/com/sap/sailing/gwt/ui/client/images/boatclass/
src/main/resources/com/sap/sailing/gwt/ui/client/images/boatclass/,\
src/main/resources/com/sap/sailing/gwt/home/shared/
output.. = WEB-INF/classes/
@@ -14,12 +14,9 @@ public class AutoPlayEntryPoint
extends AbstractMvpEntryPoint<StringMessages, AutoPlayClientFactory> {
@Override
public void doOnModuleLoad() {
AutoPlayClientFactory clientFactory = new AutoPlayClientFactoryImpl();
AutoPlayHistoryMapper applicationHistoryMapper = GWT.create(AutoPlayHistoryMapper.class);
initMvp(clientFactory, applicationHistoryMapper, new AutoPlayActivityMapperImpl(clientFactory));
SharedResources.INSTANCE.mediaCss().ensureInjected();
SharedResources.INSTANCE.mainCss().ensureInjected();
Document.get().getBody().addClassName(SharedResources.INSTANCE.mainCss().desktop());
@@ -59,7 +59,7 @@ public class AutoPlayContextImpl implements AutoPlayContext {
RegattaAndRaceIdentifier currentLiveRace) {
if (this.liveRace != null) {
if (!this.liveRace.equals(currentLiveRace)) {
LOG.info("Autoplay: Lastrace was " + lastRace + " liferace is now " + liveRace);
LOG.info("Autoplay: Last race was " + lastRace + " live race is now " + liveRace);
this.lastRace = this.liveRace;
}
}
@@ -39,12 +39,10 @@ public class LiveRaceBoardNode extends FiresPlaceNode implements RaceTimesInfoPr
private final Timer raceboardTimer;
private final RaceTimesInfoProvider raceTimesInfoProvider;
public LiveRaceBoardNode(AutoPlayClientFactory cf) {
super(LiveRaceBoardNode.class.getName());
this.cf = cf;
SailingServiceAsync sailingService = cf.getSailingService();
raceboardTimer = new Timer(PlayModes.Live, TIMER_ADVANCE_STEPSIZE);
raceboardTimer.setRefreshInterval(REFRESH_INTERVAL_IN_MILLIS_RACEBOARD);
raceboardTimer.play();
@@ -52,14 +50,12 @@ public class LiveRaceBoardNode extends FiresPlaceNode implements RaceTimesInfoPr
cf.getErrorReporter(),
new ArrayList<RegattaAndRaceIdentifier>(), RACETIME_UPDATE_INTERVAL);
raceTimesInfoProvider.addRaceTimesInfoProviderListener(this);
}
public void onStart() {
raceboardTimer.setTime(MillisecondsTimePoint.now().asMillis());
raceboardTimer.play();
raceTimesInfoProvider.addRaceIdentifier(cf.getAutoPlayCtxSignalError().getLifeOrPreLiveRace(), true);
PerspectiveCompositeSettings<AutoplayPerspectiveOwnSettings> settings = cf.getAutoPlayCtxSignalError().getAutoplaySettings();
AutoplayPerspectiveLifecycle autoplayLifecycle = cf.getAutoPlayCtxSignalError().getAutoplayLifecycle();
UserService userService = cf.getUserService();
@@ -26,7 +26,7 @@ public class RaceEndWithCompetitorsBoatsNode extends FiresPlaceNode {
RaceEndWithCompetitorsBoatsPlace place = new RaceEndWithCompetitorsBoatsPlace();
RegattaAndRaceIdentifier lastRace = cf.getAutoPlayCtxSignalError().getLastRace();
place.setLifeRace(lastRace);
place.setLiveRace(lastRace);
setPlaceToGo(place);
cf.getDispatch().execute(new GetSixtyInchStatisticAction(lastRace.getRaceName(), lastRace.getRegattaName()),
@@ -24,14 +24,11 @@ public class RaceEndWithCompetitorsFlagsNode extends FiresPlaceNode {
public void onStart() {
RaceEndWithCompetitorFlagsPlace place = new RaceEndWithCompetitorFlagsPlace();
RegattaAndRaceIdentifier lastRace = cf.getAutoPlayCtxSignalError().getLastRace();
place.setLifeRace(lastRace);
place.setLiveRace(lastRace);
setPlaceToGo(place);
cf.getDispatch().execute(new GetSixtyInchStatisticAction(lastRace.getRaceName(), lastRace.getRegattaName()),
new AsyncCallback<GetSixtyInchStatisticDTO>() {
@Override
public void onFailure(Throwable caught) {
LOGGER.log(Level.WARNING, "Could not get statistics" , caught);
@@ -59,7 +59,6 @@ public abstract class RootNodeBase extends BaseCompositeNode {
checkTimer.schedule(UPDATE_STATE_TIMER);
final UUID eventUUID = cf.getAutoPlayCtxSignalError().getContextDefinition().getEventId();
cf.getSailingService().getEventById(eventUUID, true, new AsyncCallback<EventDTO>() {
@Override
public void onSuccess(final EventDTO event) {
if (firstTimeEventLoaded) {
@@ -90,7 +89,6 @@ public abstract class RootNodeBase extends BaseCompositeNode {
private void _doCheck() {
final RegattaAndRaceIdentifier currentPreLiveRace = cf.getAutoPlayCtxSignalError().getPreLiveRace();
final RegattaAndRaceIdentifier currentLiveRace = cf.getAutoPlayCtxSignalError().getLiveRace();
this.leaderBoardName = cf.getAutoPlayCtxSignalError().getContextDefinition().getLeaderboardName();
AutoplayHelper.getLiveRace(cf.getSailingService(), cf.getErrorReporter(), cf.getAutoPlayCtxSignalError().getEvent(),
leaderBoardName, cf.getDispatch(), getWaitTimeAfterRaceEndInMillis(),
@@ -98,12 +96,9 @@ public abstract class RootNodeBase extends BaseCompositeNode {
@Override
public void onSuccess(Pair<Long, RegattaAndRaceIdentifier> result) {
errorCount = 0;
// we have no race, or we have one, and had a different one in the past
if (result == null || result.getB() == null) {
boolean comingFromLiveRace = currentLiveRace != null || currentPreLiveRace != null;
if (comingFromLiveRace) {
log("No live race found, coming from live race");
setCurrentState(false, null, RootNodeState.AFTER_LIVE, currentState);
@@ -111,22 +106,18 @@ public abstract class RootNodeBase extends BaseCompositeNode {
setCurrentState(false, null, RootNodeState.IDLE, currentState);
}
} else {
final Long timeToRaceStartInMs = result.getA();
final RegattaAndRaceIdentifier loadedLiveRace = result.getB();
boolean isPreLiveRace = timeToRaceStartInMs > LIVE_SWITCH_DELAY;
// exit
if (currentLiveRace != null && !loadedLiveRace.equals(currentLiveRace)) {
log("Received different live race, hard switching to AFTER_LIVE race");
setCurrentState(isPreLiveRace, loadedLiveRace, RootNodeState.AFTER_LIVE, currentState);
} else {
log("New " + (isPreLiveRace ? "live " : "pre live") + " race found: " + loadedLiveRace
log("New " + (isPreLiveRace ? "live" : "pre live") + " race found: " + loadedLiveRace
+ " starting in " + (timeToRaceStartInMs / 1000) + "s");
setCurrentState(isPreLiveRace, loadedLiveRace,
isPreLiveRace ? RootNodeState.PRE_RACE : RootNodeState.LIVE, currentState);
}
}
}
@@ -13,7 +13,7 @@ public abstract class AbstractRaceEndWithImagesTop3Place extends Place {
return lifeRace;
}
public void setLifeRace(RegattaAndRaceIdentifier lifeRace) {
public void setLiveRace(RegattaAndRaceIdentifier lifeRace) {
this.lifeRace = lifeRace;
}
@@ -14,7 +14,7 @@ import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
import com.sap.sailing.gwt.autoplay.client.app.AutoPlayClientFactory;
import com.sap.sailing.gwt.autoplay.client.app.AutoPlayPresenterConfigured;
import com.sap.sailing.gwt.common.client.DateUtil;
import com.sap.sailing.gwt.home.shared.resources.SharedHomeResources;
import com.sap.sailing.gwt.home.shared.SharedHomeResources;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.common.media.MediaTagConstants;
import com.sap.sse.gwt.client.media.ImageDTO;
@@ -49,7 +49,7 @@ public class AutoplayHelper {
private static final RaceMapResources raceMapResources = GWT.create(RaceMapResources.class);
private static Timer fastCurrentTimeProvider = new Timer(PlayModes.Live,
/* delayBetweenAutoAdvancesInMilliseconds */1000l);
private static Date startOfLifeRace;
private static Date startOfLiveRace;
public static final AsyncActionsExecutor asyncActionsExecutor = new AsyncActionsExecutor();
/**
* If a racestart is longer ago, the race is never considered live, even if all other checks pass
@@ -57,8 +57,8 @@ public class AutoplayHelper {
private static final long NEGATIVE_SANITY_CHECK = -24 * 60 * 60 * 1000;
public static long durationOfCurrentLiveRaceRunning() {
if (startOfLifeRace != null) {
return fastCurrentTimeProvider.getLiveTimePointInMillis() - startOfLifeRace.getTime();
if (startOfLiveRace != null) {
return fastCurrentTimeProvider.getLiveTimePointInMillis() - startOfLiveRace.getTime();
} else {
return 0;
}
@@ -70,11 +70,9 @@ public class AutoplayHelper {
if (fastCurrentTimeProvider.getRefreshInterval() != 1000) {
fastCurrentTimeProvider.setRefreshInterval(1000);
}
// only update once (very high timer to retry, meanwhile the forceUpdate will terminate the provider)
RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(sailingService, asyncActionsExecutor,
errorReporter, new ArrayList<RegattaAndRaceIdentifier>(), 10000);
StrippedLeaderboardDTO selectedLeaderboard = getSelectedLeaderboard(event, leaderBoardName);
if (selectedLeaderboard != null) {
for (RaceColumnDTO race : selectedLeaderboard.getRaceList()) {
@@ -124,7 +122,10 @@ public class AutoplayHelper {
}
/**
* Side effect free method to get a LifeRace from a timesProvider and a leaderboard
* Side effect free method to get a live race from a timesProvider and a leaderboard.
*
* @return the time to the start of the live race in milliseconds, and the identifier of the live race; or
* {@code null} if no live race is found
*/
public static Pair<Long, RegattaAndRaceIdentifier> checkForLiveRace(AbstractLeaderboardDTO currentLeaderboard,
Date serverTimeDuringRequest, RaceTimesInfoProvider raceTimesInfoProvider,
@@ -146,14 +147,14 @@ public class AutoplayHelper {
long startTimeInMs = raceTimes.getStartOfRace().getTime();
long startIn = startTimeInMs - serverTimeDuringRequest.getTime() - raceTimes.delayToLiveInMs;
if (startIn <= switchBeforeRaceStartInMillis && startIn > NEGATIVE_SANITY_CHECK) {
startOfLifeRace = raceTimes.getStartOfRace();
startOfLiveRace = raceTimes.getStartOfRace();
return new Pair<Long, RegattaAndRaceIdentifier>(startIn, raceIdentifier);
}
}
}
}
}
startOfLifeRace = null;
startOfLiveRace = null;
return null;
}
@@ -82,6 +82,8 @@ public class BoatClassImageResolver {
boatClassIconsMap.put(BoatClassMasterdata.J70.getDisplayName(), imageResources.J70Icon());
boatClassIconsMap.put(BoatClassMasterdata.J80.getDisplayName(), imageResources.J80Icon());
boatClassIconsMap.put(BoatClassMasterdata.J88.getDisplayName(), imageResources.J88Icon());
boatClassIconsMap.put(BoatClassMasterdata.J92.getDisplayName(), imageResources.J92Icon());
boatClassIconsMap.put(BoatClassMasterdata.J92S.getDisplayName(), imageResources.J92SIcon());
boatClassIconsMap.put(BoatClassMasterdata.J105.getDisplayName(), imageResources.J105Icon());
boatClassIconsMap.put(BoatClassMasterdata.J111.getDisplayName(), imageResources.J111Icon());
boatClassIconsMap.put(BoatClassMasterdata.JK_20.getDisplayName(), imageResources.JK20Icon());
@@ -148,6 +150,7 @@ public class BoatClassImageResolver {
boatClassIconsMap.put(BoatClassMasterdata.VX_ONE.getDisplayName(), imageResources.VXOneIcon());
boatClassIconsMap.put(BoatClassMasterdata.WAYFARER.getDisplayName(), imageResources.WayfarerIcon());
boatClassIconsMap.put(BoatClassMasterdata.WETA.getDisplayName(), imageResources.WetaIcon());
boatClassIconsMap.put(BoatClassMasterdata.X_332.getDisplayName(), imageResources.X332Icon());
boatClassIconsMap.put(BoatClassMasterdata.X_99.getDisplayName(), imageResources.X99Icon());
boatClassIconsMap.put(BoatClassMasterdata.RUNNING.getDisplayName(), imageResources.RunningIcon());
}
@@ -216,6 +216,14 @@ public interface BoatClassImageResources extends ClientBundle {
@ImageOptions(preventInlining = true)
ImageResource J88Icon();
@Source("com/sap/sailing/gwt/ui/client/images/boatclass/J92.png")
@ImageOptions(preventInlining = true)
ImageResource J92Icon();
@Source("com/sap/sailing/gwt/ui/client/images/boatclass/J92S.png")
@ImageOptions(preventInlining = true)
ImageResource J92SIcon();
@Source("com/sap/sailing/gwt/ui/client/images/boatclass/J105.png")
@ImageOptions(preventInlining = true)
ImageResource J105Icon();
@@ -380,10 +388,6 @@ public interface BoatClassImageResources extends ClientBundle {
@ImageOptions(preventInlining = true)
ImageResource TornadoIcon();
@Source("com/sap/sailing/gwt/ui/client/images/boatclass/X_99.png")
@ImageOptions(preventInlining = true)
ImageResource X99Icon();
@Source("com/sap/sailing/gwt/ui/client/images/boatclass/DYAS.png")
@ImageOptions(preventInlining = true)
ImageResource DyasIcon();
@@ -532,4 +536,12 @@ public interface BoatClassImageResources extends ClientBundle {
@ImageOptions(preventInlining = true)
ImageResource VariantaIcon();
@Source("com/sap/sailing/gwt/ui/client/images/boatclass/X_99.png")
@ImageOptions(preventInlining = true)
ImageResource X99Icon();
@Source("com/sap/sailing/gwt/ui/client/images/boatclass/X_332.png")
@ImageOptions(preventInlining = true)
ImageResource X332Icon();
}
Loaded 100 of 408 files, more files were not shown because too many files have changed in this diff. Show more