java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages.properties
... ...
@@ -80,6 +80,7 @@ RankGainsOrLossesBetweenFirstMarkAndFinish=Rank Gains or Losses (1st Mark -> Fin
80 80
IsTracked=Is Tracked
81 81
NumberOfCompetitorFixes=Number of Competitor Fixes
82 82
NumberOfMarkFixes=Number of Mark Fixes
83
+NumberOfWindFixes=Number of Wind Fixes
83 84
ManeuverType=Maneuver Type
84 85
Tack=Tack
85 86
WindSpeedVsManeuverLoss=Wind Speed in Knots (X) vs. Maneuver Loss in Meters (Y)
java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_de.properties
... ...
@@ -79,6 +79,7 @@ RankGainsOrLossesBetweenFirstMarkAndFinish=Positions-Verluste oder -Gewinne (1te
79 79
IsTracked=Ist getracked
80 80
NumberOfCompetitorFixes=Anzahl der Teilnehmer-Fixes
81 81
NumberOfMarkFixes=Anzahl der Tonnen-Fixes
82
+NumberOfWindFixes=Anzahl der Windmessungen
82 83
ManeuverType=Art des Manövers
83 84
Tack=Bug
84 85
SpeedBefore=Geschwindigkeit bevor dem Manöver
java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/data/HasTrackedRaceContext.java
... ...
@@ -56,6 +56,9 @@ public interface HasTrackedRaceContext {
56 56
@Statistic(messageKey="NumberOfMarkFixes", resultDecimals=0, ordinal=2)
57 57
public int getNumberOfMarkFixes();
58 58
59
+ @Statistic(messageKey="NumberOfWindFixes", resultDecimals=0, ordinal=1)
60
+ public int getNumberOfWindFixes();
61
+
59 62
// Convenience methods for race dependent calculation to avoid code duplication
60 63
public Double getRelativeScoreForCompetitor(Competitor competitor);
61 64
java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/TrackedRaceWithContext.java
... ...
@@ -1,21 +1,23 @@
1 1
package com.sap.sailing.datamining.impl.data;
2 2
3 3
import java.util.Calendar;
4
+import java.util.function.BiFunction;
5
+import java.util.stream.Collectors;
6
+import java.util.stream.StreamSupport;
4 7
5 8
import com.sap.sailing.datamining.data.HasLeaderboardContext;
6 9
import com.sap.sailing.datamining.data.HasTrackedRaceContext;
7 10
import com.sap.sailing.domain.base.Competitor;
8 11
import com.sap.sailing.domain.base.CourseArea;
9 12
import com.sap.sailing.domain.base.Fleet;
10
-import com.sap.sailing.domain.base.Mark;
11 13
import com.sap.sailing.domain.base.RaceColumn;
12 14
import com.sap.sailing.domain.base.RaceDefinition;
13 15
import com.sap.sailing.domain.base.Regatta;
14 16
import com.sap.sailing.domain.common.NauticalSide;
15
-import com.sap.sailing.domain.common.tracking.GPSFix;
16
-import com.sap.sailing.domain.common.tracking.GPSFixMoving;
17
-import com.sap.sailing.domain.tracking.GPSFixTrack;
17
+import com.sap.sailing.domain.common.WindSource;
18
+import com.sap.sailing.domain.common.WindSourceType;
18 19
import com.sap.sailing.domain.tracking.LineDetails;
20
+import com.sap.sailing.domain.tracking.Track;
19 21
import com.sap.sailing.domain.tracking.TrackedRace;
20 22
import com.sap.sse.common.Duration;
21 23
import com.sap.sse.common.TimePoint;
... ...
@@ -136,11 +138,10 @@ public class TrackedRaceWithContext implements HasTrackedRaceContext {
136 138
return duration;
137 139
}
138 140
139
- @Override
140
- public int getNumberOfCompetitorFixes() {
141
+ private <T> int getNumberOfRawFixes(Iterable<T> tracksFor, BiFunction<T, TrackedRace, Track<?>> trackProvider) {
141 142
int number = 0;
142
- for (Competitor competitor : getRace().getCompetitors()) {
143
- GPSFixTrack<Competitor, GPSFixMoving> track = getTrackedRace().getTrack(competitor);
143
+ for (T trackedObject : tracksFor) {
144
+ Track<?> track = trackProvider.apply(trackedObject, getTrackedRace());
144 145
track.lockForRead();
145 146
try {
146 147
number += Util.size(track.getRawFixes());
... ...
@@ -150,22 +151,26 @@ public class TrackedRaceWithContext implements HasTrackedRaceContext {
150 151
}
151 152
return number;
152 153
}
154
+
155
+ @Override
156
+ public int getNumberOfCompetitorFixes() {
157
+ return getNumberOfRawFixes(getRace().getCompetitors(), (competitor, trackedRace)->trackedRace.getTrack(competitor));
158
+ }
153 159
154 160
@Override
155 161
public int getNumberOfMarkFixes() {
156
- int number = 0;
157
- for (Mark mark : getTrackedRace().getMarks()) {
158
- GPSFixTrack<Mark, GPSFix> track = getTrackedRace().getTrack(mark);
159
- track.lockForRead();
160
- try {
161
- number += Util.size(track.getRawFixes());
162
- } finally {
163
- track.unlockAfterRead();
164
- }
165
- }
166
- return number;
162
+ return getNumberOfRawFixes(getTrackedRace().getMarks(), (mark, trackedRace)->trackedRace.getTrack(mark));
167 163
}
168 164
165
+ @Override
166
+ public int getNumberOfWindFixes() {
167
+ final Iterable<WindSource> windSources = getTrackedRace().getWindSources();
168
+ final Iterable<WindSource> windSourcesToExclude = getTrackedRace().getWindSourcesToExclude();
169
+ final Iterable<WindSource> windSourcesToUse = StreamSupport.stream(windSources.spliterator(), /* parallel */ false).filter(ws->!Util.contains(windSourcesToExclude, ws)).
170
+ filter(ws->ws.getType()!=WindSourceType.TRACK_BASED_ESTIMATION).collect(Collectors.toList());
171
+ return getNumberOfRawFixes(windSourcesToUse, (windSource, trackedRace)->trackedRace.getOrCreateWindTrack(windSource));
172
+ }
173
+
169 174
// Convenience methods for race dependent calculation to avoid code duplication
170 175
public Double getRelativeScoreForCompetitor(Competitor competitor) {
171 176
final TimePoint now = MillisecondsTimePoint.now();
java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/DomainFactoryImpl.java
... ...
@@ -420,7 +420,7 @@ public class DomainFactoryImpl implements DomainFactory {
420 420
@Override
421 421
public void addUpdateHandlers(String updateURL, String username, String password, Serializable eventId,
422 422
RaceDefinition raceDefinition, DynamicTrackedRace trackedRace) throws URISyntaxException {
423
- final URI updateURI = new URI(updateURL);
423
+ final URI updateURI = updateURL == null ? null : new URI(updateURL);
424 424
CourseDesignUpdateHandler courseDesignHandler = new CourseDesignUpdateHandler(
425 425
updateURI, username, password, eventId, raceDefinition.getId());
426 426
StartTimeUpdateHandler startTimeHandler = new StartTimeUpdateHandler(
java/com.sap.sailing.feature.runtime/feature.xml
... ...
@@ -158,6 +158,6 @@
158 158
id="org.mp4parser.isoparser"
159 159
download-size="0"
160 160
install-size="0"
161
- version="1.9.31"/>
161
+ version="1.9.37"/>
162 162
163 163
</feature>
java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF
... ...
@@ -57,7 +57,7 @@ Require-Bundle: com.sap.sailing.domain,
57 57
com.sap.sailing.competitorimport,
58 58
com.sap.sailing.expeditionconnector,
59 59
com.sap.sailing.expeditionconnector.common,
60
- org.mp4parser.isoparser;bundle-version="1.9.31",
60
+ org.mp4parser.isoparser;bundle-version="1.9.37",
61 61
com.sap.sse.datamining.ui
62 62
Bundle-Activator: com.sap.sailing.gwt.ui.server.Activator
63 63
Bundle-ActivationPolicy: lazy
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties
... ...
@@ -1194,6 +1194,14 @@ raceIsInLiveTimePanelMode=La carrera es en directo
1194 1194
backToLiveTimePanelMode=Volver a directo
1195 1195
windChart=Gráfico de vientos
1196 1196
windChartLoading=Cargando datos de vientos...
1197
+mediaNoVideosCaption=No hay rastreos de medios
1198
+mediaShowVideoCaption=Mostrar rasteo de medios
1199
+mediaShowVideoTooltip=Haga clic para mostrar rastreo de medios: {0}
1200
+mediaHideVideoCaption=Ocultar rastreo de medios
1201
+mediaHideVideoTooltip=Haga clic para ocultar rastreo de medios
1202
+mediaSelectVideoCaption={0} rastreos de medios
1203
+mediaSelectVideoTooltip=Haga clic para seleccionar rastreo de medios
1204
+mediaManageMediaCaption=Gestionar medios
1197 1205
mediaManageMediaTooltip=Configurar clips de audio y vídeo, sincronizar tiempos
1198 1206
showAll=Mostrar todo
1199 1207
raceVisibilityColumn=Visibilidad
... ...
@@ -1416,7 +1424,8 @@ numberOfRacesShort=Nº regatas
1416 1424
courseGeometry=Geometría de la regata
1417 1425
degreesToWind={0}° respecto al viento
1418 1426
miniLeeaderboardLegendText=Muestra los puntos y el número de carreras por competidor
1419
-currentOfTotal={0,number}/{1,number}
1427
+currentOfTotalRaces={0,number}/{1,number} carreras
1428
+currentOfTotalRaces[one]={0,number}/{1,number} carrera
1420 1429
noFinishedRaces=Todavía no ha finalizado ninguna carrera
1421 1430
racesOverview=Resumen de carreras
1422 1431
listFormatLabel=Formato de lista
... ...
@@ -2075,6 +2084,8 @@ manage2SailEventIdBox=ID de evento Manage2Sail
2075 2084
manage2SailEventURLBox=URL de evento Manage2Sail (json)
2076 2085
manage2SailEventIdBoxTooltip=ID de evento o URL que contiene el ID de evento de Manage2Sail
2077 2086
manage2SailPort=Puerto
2087
+audioFiles=Pistas de audio
2088
+selectMedia=Seleccionar medio
2078 2089
swissTimingUpdateURL=Actualizar URL (por ejemplo, para la hora de salida y el feedback sobre la modificación del rumbo)
2079 2090
swissTimingUpdateUsername=Nombre de usuario para enviar actualizaciones
2080 2091
swissTimingUpdatePassword=Contraseña para enviar actualizaciones
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties
... ...
@@ -1194,6 +1194,14 @@ raceIsInLiveTimePanelMode=La course est en direct.
1194 1194
backToLiveTimePanelMode=Retour au direct
1195 1195
windChart=Graphique du vent
1196 1196
windChartLoading=Chargement des données de vent...
1197
+mediaNoVideosCaption=Aucune piste média
1198
+mediaShowVideoCaption=Afficher piste média
1199
+mediaShowVideoTooltip=Cliquer pour afficher la piste média : {0}
1200
+mediaHideVideoCaption=Masquer piste média
1201
+mediaHideVideoTooltip=Cliquer pour masquer la piste média
1202
+mediaSelectVideoCaption={0} pistes médias
1203
+mediaSelectVideoTooltip=Cliquer pour sélectionner la piste média
1204
+mediaManageMediaCaption=Gestion des médias
1197 1205
mediaManageMediaTooltip=Configurer des clips audio et vidéo, synchroniser durées
1198 1206
showAll=Afficher tout
1199 1207
raceVisibilityColumn=Visibilité
... ...
@@ -1416,7 +1424,8 @@ numberOfRacesShort=Nombre de courses
1416 1424
courseGeometry=Géométrie du parcours
1417 1425
degreesToWind={0}° au vent
1418 1426
miniLeeaderboardLegendText=Affiche les points associés au nombre de courses par concurrent
1419
-currentOfTotal={0,number}/{1,number}
1427
+currentOfTotalRaces={0,number}/{1,number} courses
1428
+currentOfTotalRaces[one]={0,number}/{1,number} course
1420 1429
noFinishedRaces=Aucune course terminée pour l''instant.
1421 1430
racesOverview=Synthèse des courses
1422 1431
listFormatLabel=Format de liste
... ...
@@ -2075,6 +2084,8 @@ manage2SailEventIdBox=ID de manifestation Manage2Sail
2075 2084
manage2SailEventURLBox=URL de la manifestation Manage2Sail (json)
2076 2085
manage2SailEventIdBoxTooltip=ID d''événement ou URL contenant l''ID d''événement issu de Manage2Sail
2077 2086
manage2SailPort=Bâbord
2087
+audioFiles=Pistes audio
2088
+selectMedia=Sélectionner média
2078 2089
swissTimingUpdateURL=Mettre à jour URL (par exemple pour l''heure de début ou pour les commentaires sur le changement de trajectoire)
2079 2090
swissTimingUpdateUsername=Nom d''utilisateur pour l''envoi des mises à jour
2080 2091
swissTimingUpdatePassword=Mot de passe pour l''envoi des mises à jour
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties
... ...
@@ -1194,6 +1194,14 @@ raceIsInLiveTimePanelMode=レースがライブ中
1194 1194
backToLiveTimePanelMode=ライブに戻る
1195 1195
windChart=風チャート
1196 1196
windChartLoading=風データをロード中...
1197
+mediaNoVideosCaption=メディアトラックなし
1198
+mediaShowVideoCaption=メディアトラック表示
1199
+mediaShowVideoTooltip=クリックしてメディアトラックを表示: {0}
1200
+mediaHideVideoCaption=メディアトラック非表示
1201
+mediaHideVideoTooltip=クリックしてメディアトラックを非表示
1202
+mediaSelectVideoCaption={0} メディアトラック
1203
+mediaSelectVideoTooltip=クリックしてメディアトラックを選択
1204
+mediaManageMediaCaption=メディア管理
1197 1205
mediaManageMediaTooltip=音声および動画クリックを設定して、時間を同期
1198 1206
showAll=全表示
1199 1207
raceVisibilityColumn=可視性
... ...
@@ -1416,7 +1424,8 @@ numberOfRacesShort=# レース
1416 1424
courseGeometry=コース配置
1417 1425
degreesToWind=風と {0}°
1418 1426
miniLeeaderboardLegendText=競技者別にレース数とともに得点を表示
1419
-currentOfTotal={0,number}/{1,number}
1427
+currentOfTotalRaces={0,number}/{1,number} レース
1428
+currentOfTotalRaces[one]={0,number}/{1,number} レース
1420 1429
noFinishedRaces=フィニッシュしたレースがまだありません。
1421 1430
racesOverview=レース概要
1422 1431
listFormatLabel=レース一覧
... ...
@@ -2075,6 +2084,8 @@ manage2SailEventIdBox=Manage2Sail イベント ID
2075 2084
manage2SailEventURLBox=Manage2Sail イベント URL (JSON)
2076 2085
manage2SailEventIdBoxTooltip=イベント ID または Manage2Sail からのイベント ID が入っている URL
2077 2086
manage2SailPort=ポート
2087
+audioFiles=音声トラック
2088
+selectMedia=メディア選択
2078 2089
swissTimingUpdateURL=URL の更新 (スタート時刻やコース変更のフィードバックなど)
2079 2090
swissTimingUpdateUsername=更新送信のユーザ名
2080 2091
swissTimingUpdatePassword=更新送信のパスワード
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties
... ...
@@ -1194,6 +1194,14 @@ raceIsInLiveTimePanelMode=Corrida ao vivo
1194 1194
backToLiveTimePanelMode=Voltar ao tempo real
1195 1195
windChart=Carta de ventos
1196 1196
windChartLoading=Carregando dados do vento...
1197
+mediaNoVideosCaption=Nenhuma faixa de mídia
1198
+mediaShowVideoCaption=Exibir faixa de mídia
1199
+mediaShowVideoTooltip=Clicar para exibir faixa de mídia: {0}
1200
+mediaHideVideoCaption=Ocultar faixa de mídia
1201
+mediaHideVideoTooltip=Clicar para ocultar faixa de mídia
1202
+mediaSelectVideoCaption={0} faixas de mídia
1203
+mediaSelectVideoTooltip=Clicar para selecionar faixa de mídia
1204
+mediaManageMediaCaption=Administrar mídia
1197 1205
mediaManageMediaTooltip=Configurar clipes de áudio e vídeo, sincronizar horas
1198 1206
showAll=Visualizar tudo
1199 1207
raceVisibilityColumn=Visibilidade
... ...
@@ -1416,7 +1424,8 @@ numberOfRacesShort=Nº de corridas
1416 1424
courseGeometry=Geometria do percurso
1417 1425
degreesToWind={0}° para vento
1418 1426
miniLeeaderboardLegendText=Exibe os pontos juntamente com o número de corridas por competidor
1419
-currentOfTotal={0,number}/{1,number}
1427
+currentOfTotalRaces={0,number}/{1,number} corridas
1428
+currentOfTotalRaces[one]={0,number}/{1,number} corrida
1420 1429
noFinishedRaces=Ainda não existem corridas concluídas.
1421 1430
racesOverview=Síntese de corridas
1422 1431
listFormatLabel=Formato de lista
... ...
@@ -2075,6 +2084,8 @@ manage2SailEventIdBox=ID de evento Manage2Sail
2075 2084
manage2SailEventURLBox=URL de evento Manage2Sail (json)
2076 2085
manage2SailEventIdBoxTooltip=ID de evento ou um URL que contenha o ID de evento do Manage2Sail
2077 2086
manage2SailPort=Porta
2087
+audioFiles=Faixas de áudio
2088
+selectMedia=Selecionar mídia
2078 2089
swissTimingUpdateURL=Atualizar URL (por exemplo, para hora de partida e feedback de mudança de percurso)
2079 2090
swissTimingUpdateUsername=Nome de usuário para enviar atualizações
2080 2091
swissTimingUpdatePassword=Senha para enviar atualizações
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties
... ...
@@ -1194,6 +1194,14 @@ raceIsInLiveTimePanelMode=Гонка активна
1194 1194
backToLiveTimePanelMode=Вернуться в оперативный режим
1195 1195
windChart=Диаграмма ветра
1196 1196
windChartLoading=Загрузка данных о ветре...
1197
+mediaNoVideosCaption=Нет объектов мультимедиа
1198
+mediaShowVideoCaption=Показать объект мультимедиа
1199
+mediaShowVideoTooltip=Щелкните, чтобы показать объект мультимедиа: {0}
1200
+mediaHideVideoCaption=Скрыть объект мультимедиа
1201
+mediaHideVideoTooltip=Щелкните, чтобы скрыть объект мультимедиа
1202
+mediaSelectVideoCaption=Объекты мультимедиа: {0}
1203
+mediaSelectVideoTooltip=Щелкните, чтобы выбрать объект мультимедиа
1204
+mediaManageMediaCaption=Управление мультимедиа
1197 1205
mediaManageMediaTooltip=Настройка аудио- и видеоклипов, синхронизация времени
1198 1206
showAll=Показать все
1199 1207
raceVisibilityColumn=Видимость
... ...
@@ -1416,7 +1424,8 @@ numberOfRacesShort=Гонки
1416 1424
courseGeometry=Геометрия курса
1417 1425
degreesToWind={0}° к ветру
1418 1426
miniLeeaderboardLegendText=Показывает баллы и число гонок каждого участника
1419
-currentOfTotal={0,number}/{1,number}
1427
+currentOfTotalRaces={0,number}/{1,number} гонок
1428
+currentOfTotalRaces[one]={0,number}/{1,number} гонка
1420 1429
noFinishedRaces=Завершенных гонок еще нет
1421 1430
racesOverview=Обзор гонок
1422 1431
listFormatLabel=Формат списка
... ...
@@ -2075,6 +2084,8 @@ manage2SailEventIdBox=Ид. события Manage2Sail
2075 2084
manage2SailEventURLBox=URL события Manage2Sail (json)
2076 2085
manage2SailEventIdBoxTooltip=Ид. события или URL, содержащий ид. события из Manage2Sail
2077 2086
manage2SailPort=Порт
2087
+audioFiles=Аудиообъект
2088
+selectMedia=Выбрать мультимедиа
2078 2089
swissTimingUpdateURL=Обновить URL (например, для времени начала и отзыва об изменении курса)
2079 2090
swissTimingUpdateUsername=Имя пользователя для отправки обновлений
2080 2091
swissTimingUpdatePassword=Пароль для отправки обновлений
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties
... ...
@@ -1194,6 +1194,14 @@ raceIsInLiveTimePanelMode=正在直播比赛轮次
1194 1194
backToLiveTimePanelMode=返回实况
1195 1195
windChart=风力记录
1196 1196
windChartLoading=正在加载风力数据...
1197
+mediaNoVideosCaption=无媒体轨道
1198
+mediaShowVideoCaption=显示媒体轨道
1199
+mediaShowVideoTooltip=点击显示媒体轨道:{0}
1200
+mediaHideVideoCaption=隐藏媒体轨道
1201
+mediaHideVideoTooltip=点击隐藏媒体轨道
1202
+mediaSelectVideoCaption={0}个媒体轨道
1203
+mediaSelectVideoTooltip=点击选择媒体轨道
1204
+mediaManageMediaCaption=管理媒体
1197 1205
mediaManageMediaTooltip=配置音频和视频短片,同步时间
1198 1206
showAll=显示全部
1199 1207
raceVisibilityColumn=可见性
... ...
@@ -1416,7 +1424,8 @@ numberOfRacesShort=比赛轮次数
1416 1424
courseGeometry=场地几何结构
1417 1425
degreesToWind={0}°(相对于风)
1418 1426
miniLeeaderboardLegendText=显示分数以及各参赛队的比赛轮次数
1419
-currentOfTotal={0,number}/{1,number}
1427
+currentOfTotalRaces={0,number}/{1,number} 个比赛轮次
1428
+currentOfTotalRaces[one]={0,number}/{1,number} 个比赛轮次
1420 1429
noFinishedRaces=没有结束的比赛轮次
1421 1430
racesOverview=比赛轮次总览
1422 1431
listFormatLabel=清单格式
... ...
@@ -2075,6 +2084,8 @@ manage2SailEventIdBox=Manage2Sail 活动编号
2075 2084
manage2SailEventURLBox=Manage2Sail 活动 URL (json)
2076 2085
manage2SailEventIdBoxTooltip=包含 Manage2Sail 中活动编号的活动编号或 URL
2077 2086
manage2SailPort=左舷
2087
+audioFiles=音频轨道
2088
+selectMedia=选择媒体
2078 2089
swissTimingUpdateURL=更新 URL(例如开始时间和航向改变反馈)
2079 2090
swissTimingUpdateUsername=用于发送更新的用户名
2080 2091
swissTimingUpdatePassword=用于发送更新的密码
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/BoatOverlay.java
... ...
@@ -56,6 +56,7 @@ public class BoatOverlay extends CanvasOverlayV3 {
56 56
private Integer lastHeight;
57 57
private Size lastScale;
58 58
private Color lastColor;
59
+ private DisplayMode lastDisplayMode;
59 60
60 61
public static enum DisplayMode { DEFAULT, SELECTED, NOT_SELECTED };
61 62
private DisplayMode displayMode;
... ...
@@ -93,7 +94,8 @@ public class BoatOverlay extends CanvasOverlayV3 {
93 94
if (lastWidth == null || canvasWidth != lastWidth || lastHeight == null || canvasHeight != lastHeight) {
94 95
setCanvasSize(canvasWidth, canvasHeight);
95 96
}
96
- if (needToDraw(boatFix.legType, boatFix.tack, isSelected(), canvasWidth, canvasHeight, boatSizeScaleFactor, color)) {
97
+ if (needToDraw(boatFix.legType, boatFix.tack, isSelected(), canvasWidth, canvasHeight, boatSizeScaleFactor,
98
+ color, displayMode)) {
97 99
boatVectorGraphics.drawBoatToCanvas(getCanvas().getContext2d(), boatFix.legType, boatFix.tack, getDisplayMode(),
98 100
canvasWidth, canvasHeight, boatSizeScaleFactor, color);
99 101
lastLegType = boatFix.legType;
... ...
@@ -103,6 +105,7 @@ public class BoatOverlay extends CanvasOverlayV3 {
103 105
lastHeight = canvasHeight;
104 106
lastScale = boatSizeScaleFactor;
105 107
lastColor = color;
108
+ lastDisplayMode = displayMode;
106 109
}
107 110
LatLng latLngPosition = coordinateSystem.toLatLng(boatFix.position);
108 111
Point boatPositionInPx = mapProjection.fromLatLngToDivPixel(latLngPosition);
... ...
@@ -123,11 +126,12 @@ public class BoatOverlay extends CanvasOverlayV3 {
123 126
* changed, the result is <code>true</code>.
124 127
*/
125 128
private boolean needToDraw(LegType legType, Tack tack, boolean isSelected, double width, double height,
126
- Size scaleFactor, Color color) {
129
+ Size scaleFactor, Color color, DisplayMode displayMode) {
127 130
return lastLegType == null || lastLegType != legType || lastTack == null || lastTack != tack
128 131
|| lastSelected == null || lastSelected != isSelected || lastWidth == null || lastWidth != width
129 132
|| lastHeight == null || lastHeight != height || lastScale == null || !lastScale.equals(scaleFactor)
130
- || lastColor == null || !lastColor.equals(color);
133
+ || lastColor == null || !lastColor.equals(color) || lastDisplayMode == null
134
+ || !lastDisplayMode.equals(displayMode);
131 135
}
132 136
133 137
/**
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java
... ...
@@ -70,7 +70,6 @@ import com.google.gwt.user.client.ui.Label;
70 70
import com.google.gwt.user.client.ui.RequiresResize;
71 71
import com.google.gwt.user.client.ui.VerticalPanel;
72 72
import com.google.gwt.user.client.ui.Widget;
73
-import com.sap.sailing.domain.common.Bounds;
74 73
import com.sap.sailing.domain.common.ManeuverType;
75 74
import com.sap.sailing.domain.common.Position;
76 75
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
... ...
@@ -175,6 +174,7 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
175 174
private AbsolutePanel rootPanel = new AbsolutePanel();
176 175
177 176
private MapWidget map;
177
+ private Collection<Runnable> mapInitializedListener;
178 178
179 179
/**
180 180
* Always valid, non-<code>null</code>. Must be used to map all coordinates, headings, bearings, and directions
... ...
@@ -481,6 +481,14 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
481 481
public boolean contains(T t) {
482 482
return (map.containsKey(t));
483 483
}
484
+
485
+ public void clear() {
486
+ map.clear();
487
+ }
488
+
489
+ public void removeAll(T t) {
490
+ map.remove(t);
491
+ }
484 492
}
485 493
486 494
private class AdvantageLineUpdater implements QuickRanksListener {
... ...
@@ -546,6 +554,7 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
546 554
updateCoordinateSystemFromSettings();
547 555
lastTimeChangeBeforeInitialization = null;
548 556
isMapInitialized = false;
557
+ mapInitializedListener = new ArrayList<>();
549 558
this.hasPolar = false;
550 559
headerPanel = new FlowPanel();
551 560
headerPanel.setStyleName("RaceMap-HeaderPanel");
... ...
@@ -764,6 +773,21 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
764 773
if (streamletOverlay != null && settings.isShowWindStreamletOverlay()) {
765 774
streamletOverlay.setCanvasSettings();
766 775
}
776
+ if (!currentlyDragging) {
777
+ refreshMapWithoutAnimation();
778
+ }
779
+ if (!mapFirstZoomDone) {
780
+ zoomMapToNewBounds(settings.getZoomSettings().getNewBounds(RaceMap.this));
781
+ redraw();
782
+ }
783
+ if (!isMapInitialized) {
784
+ RaceMap.this.isMapInitialized = true;
785
+ mapInitializedListener.forEach(c -> c.run());
786
+ mapInitializedListener.clear();
787
+ // ensure at least one redraw after starting the map, but not before other things like modes
788
+ // initialize, as they might change the timer or other settings
789
+ redraw();
790
+ }
767 791
}
768 792
});
769 793
map.addBoundsChangeHandler(new BoundsChangeMapHandler() {
... ...
@@ -777,7 +801,7 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
777 801
currentMapBounds = map.getBounds();
778 802
currentZoomLevel = newZoomLevel;
779 803
headerPanel.getElement().getStyle().setWidth(map.getOffsetWidth(), Unit.PX);
780
- refreshMapWithoutAnimation();
804
+ refreshMapWithoutAnimationButLeaveTransitionsAlive();
781 805
}
782 806
});
783 807
... ...
@@ -812,7 +836,6 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
812 836
createSettingsButton(map);
813 837
}
814 838
// Data has been initialized
815
- RaceMap.this.isMapInitialized = true;
816 839
RaceMap.this.redraw();
817 840
trueNorthIndicatorPanel.redraw();
818 841
showAdditionalControls(map);
... ...
@@ -910,6 +933,7 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
910 933
}
911 934
912 935
public void redraw() {
936
+ remoteCallsToSkipInExecution.removeAll(timer.getTime());
913 937
timeChanged(timer.getTime(), null);
914 938
}
915 939
... ...
@@ -964,6 +988,10 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
964 988
return timer.getPlayMode() == PlayModes.Live;
965 989
}
966 990
991
+ private void refreshMapWithoutAnimationButLeaveTransitionsAlive() {
992
+ remoteCallsToSkipInExecution.addAll(remoteCallsInExecution);
993
+ }
994
+
967 995
private void refreshMapWithoutAnimation() {
968 996
removeTransitions();
969 997
remoteCallsToSkipInExecution.addAll(remoteCallsInExecution);
... ...
@@ -1041,7 +1069,6 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
1041 1069
});
1042 1070
}
1043 1071
else {
1044
- GWT.log("added identical call to skip");
1045 1072
remoteCallsToSkipInExecution.add(newTime);
1046 1073
}
1047 1074
}
... ...
@@ -1049,8 +1076,8 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
1049 1076
@Override
1050 1077
public void timeChanged(final Date newTime, final Date oldTime) {
1051 1078
boolean isRedraw = oldTime == null;
1052
- if (newTime != null && isMapInitialized) {
1053
- if (raceIdentifier != null) {
1079
+ if (isMapInitialized) {
1080
+ if (newTime != null) {
1054 1081
if (raceIdentifier != null) {
1055 1082
final long transitionTimeInMillis = calculateTimeForPositionTransitionInMillis(newTime, oldTime);
1056 1083
refreshMap(newTime, transitionTimeInMillis, isRedraw);
... ...
@@ -1203,28 +1230,32 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
1203 1230
showCourseSidelinesOnMap(raceMapDataDTO.courseSidelines);
1204 1231
showStartAndFinishAndCourseMiddleLines(raceMapDataDTO.coursePositions);
1205 1232
showStartLineToFirstMarkTriangle(raceMapDataDTO.coursePositions);
1206
-
1233
+
1207 1234
// Rezoom the map
1208 1235
LatLngBounds zoomToBounds = null;
1209
- if (!settings.getZoomSettings().containsZoomType(ZoomTypes.NONE)) { // Auto zoom if setting is not manual
1236
+ if (!settings.getZoomSettings().containsZoomType(ZoomTypes.NONE)) {
1237
+ // Auto zoom if setting is not manual
1238
+
1210 1239
zoomToBounds = settings.getZoomSettings().getNewBounds(RaceMap.this);
1211 1240
if (zoomToBounds == null && !mapFirstZoomDone) {
1212
- zoomToBounds = getDefaultZoomBounds(); // the user-specified zoom couldn't find what it was looking for; try defaults once
1241
+ // the user-specified zoom couldn't find what it was looking for; try defaults once
1242
+ zoomToBounds = getDefaultZoomBounds();
1213 1243
}
1214
- } else if (!mapFirstZoomDone) { // Zoom once to the marks if marks exist
1244
+ }
1245
+ if (!mapFirstZoomDone) {
1246
+ // Zoom once to the marks if marks exist
1215 1247
zoomToBounds = new CourseMarksBoundsCalculator().calculateNewBounds(RaceMap.this);
1216 1248
if (zoomToBounds == null) {
1217
- zoomToBounds = getDefaultZoomBounds(); // use default zoom, e.g.,
1249
+ // use default zoom, e.g.,
1250
+ zoomToBounds = getDefaultZoomBounds();
1218 1251
}
1219 1252
/*
1220
- * Reset the mapZoomedOrPannedSinceLastRaceSelection: In spite of the fact that
1221
- * the map was just zoomed to the bounds of the marks, it was not a zoom or pan
1222
- * triggered by the user. As a consequence the
1223
- * mapZoomedOrPannedSinceLastRaceSelection option has to reset again.
1253
+ * Reset the mapZoomedOrPannedSinceLastRaceSelection: In spite of the fact that the map was
1254
+ * just zoomed to the bounds of the marks, it was not a zoom or pan triggered by the user.
1255
+ * As a consequence the mapZoomedOrPannedSinceLastRaceSelection option has to reset again.
1224 1256
*/
1225 1257
}
1226 1258
zoomMapToNewBounds(zoomToBounds);
1227
- mapFirstZoomDone = true;
1228 1259
updateEstimatedDuration(raceMapDataDTO.estimatedDuration);
1229 1260
}
1230 1261
} else {
... ...
@@ -1277,7 +1308,7 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
1277 1308
private void updateBoatPositions(final Date newTime, final long transitionTimeInMillis,
1278 1309
final Map<CompetitorDTO, Boolean> hasTailOverlapForCompetitor,
1279 1310
final Iterable<CompetitorDTO> competitorsToShow, Map<CompetitorDTO, List<GPSFixDTOWithSpeedWindTackAndLegType>> boatData, boolean updateTailsOnly) {
1280
- if (zoomingAnimationsInProgress == 0 && !currentlyDragging) {
1311
+ if (zoomingAnimationsInProgress == 0) {
1281 1312
fixesAndTails.updateFixes(boatData, hasTailOverlapForCompetitor, RaceMap.this, transitionTimeInMillis);
1282 1313
showBoatsOnMap(newTime, transitionTimeInMillis,
1283 1314
/* re-calculate; it could have changed since the asynchronous request was made: */
... ...
@@ -2076,18 +2107,13 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
2076 2107
int zoomLat = (int) Math.floor(Math.log(map.getDiv().getClientHeight() * 2 * Math.PI / deltaLat / GLOBE_PXSIZE) / LOG2);
2077 2108
return Math.min(Math.min(zoomLat, zoomLng), MAX_ZOOM);
2078 2109
}
2079
-
2110
+
2080 2111
private void zoomMapToNewBounds(LatLngBounds newBounds) {
2081 2112
if (newBounds != null) {
2082
- LatLngBounds currentMapBounds;
2083
- if (map.getBounds() == null
2084
- || !BoundsUtil.contains((currentMapBounds = map.getBounds()), newBounds)
2085
- || graticuleAreaRatio(currentMapBounds, newBounds) > 10) {
2086
- // only change bounds if the new bounds don't fit into the current map zoom
2113
+ int newZoomLevel = getZoomLevel(newBounds);
2114
+ if (mapNeedsToPanOrZoom(newBounds, newZoomLevel)) {
2087 2115
Iterable<ZoomTypes> oldZoomTypesToConsiderSettings = settings.getZoomSettings().getTypesToConsiderOnZoom();
2088 2116
setAutoZoomInProgress(true);
2089
- autoZoomLatLngBounds = newBounds;
2090
- int newZoomLevel = getZoomLevel(autoZoomLatLngBounds);
2091 2117
if (newZoomLevel != map.getZoom()) {
2092 2118
// distinguish between zoom-in and zoom-out, because the sequence of panTo() and setZoom()
2093 2119
// appears different on the screen due to map-animations
... ...
@@ -2097,37 +2123,39 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
2097 2123
autoZoomIn = newZoomLevel > map.getZoom();
2098 2124
autoZoomOut = !autoZoomIn;
2099 2125
autoZoomLevel = newZoomLevel;
2100
- removeTransitions();
2101 2126
if (autoZoomIn) {
2102
- map.panTo(autoZoomLatLngBounds.getCenter());
2127
+ map.panTo(newBounds.getCenter());
2103 2128
} else {
2104 2129
map.setZoom(autoZoomLevel);
2105 2130
}
2106 2131
} else {
2107
- map.panTo(autoZoomLatLngBounds.getCenter());
2132
+ map.panTo(newBounds.getCenter());
2108 2133
}
2134
+ autoZoomLatLngBounds = newBounds;
2109 2135
RaceMapZoomSettings restoredZoomSettings = new RaceMapZoomSettings(oldZoomTypesToConsiderSettings, settings.getZoomSettings().isZoomToSelectedCompetitors());
2110 2136
settings = new RaceMapSettings(settings, restoredZoomSettings);
2111 2137
setAutoZoomInProgress(false);
2138
+ mapFirstZoomDone = true;
2112 2139
}
2113 2140
}
2114 2141
}
2115
-
2116
- private double graticuleAreaRatio(LatLngBounds containing, LatLngBounds contained) {
2117
- assert BoundsUtil.contains(containing, contained);
2118
- double containingAreaRatio = getGraticuleArea(containing) / getGraticuleArea(contained);
2119
- return containingAreaRatio;
2120
- }
2121 2142
2122
- /**
2123
- * A much simplified "area" calculation for a {@link Bounds} object, multiplying the differences in latitude and longitude degrees.
2124
- * The result therefore is in the order of magnitude of 60*60 square nautical miles.
2125
- */
2126
- private double getGraticuleArea(LatLngBounds bounds) {
2127
- return ((BoundsUtil.isCrossesDateLine(bounds) ? bounds.getNorthEast().getLongitude()+360 : bounds.getNorthEast().getLongitude())-bounds.getSouthWest().getLongitude()) *
2128
- (bounds.getNorthEast().getLatitude() - bounds.getSouthWest().getLatitude());
2143
+ private boolean mapNeedsToPanOrZoom(LatLngBounds newBounds, int newZoomLevel) {
2144
+ // we never updated, update now
2145
+ if (currentMapBounds == null) {
2146
+ return true;
2147
+ }
2148
+ // we do not fit the required bounds, update now
2149
+ if (!BoundsUtil.contains(currentMapBounds, newBounds)) {
2150
+ return true;
2151
+ }
2152
+ // we do fit the required bounds, however we might be to far zoomed out, check if we can zoom to a better level
2153
+ if (newZoomLevel != map.getZoom()) {
2154
+ return true;
2155
+ }
2156
+ return false;
2129 2157
}
2130
-
2158
+
2131 2159
private void setAutoZoomInProgress(boolean autoZoomInProgress) {
2132 2160
this.autoZoomInProgress = autoZoomInProgress;
2133 2161
}
... ...
@@ -2563,9 +2591,6 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
2563 2591
}
2564 2592
}
2565 2593
showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors());
2566
- } else {
2567
- // adding a single competitor; may need to re-load data, so refresh:
2568
- redraw();
2569 2594
}
2570 2595
} else {
2571 2596
// only change highlighting
... ...
@@ -2574,11 +2599,6 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
2574 2599
boatCanvas.setDisplayMode(displayHighlighted(competitor));
2575 2600
boatCanvas.draw();
2576 2601
showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors());
2577
- } else {
2578
- // seems like an internal error not to find the lowlighted marker; but maybe the
2579
- // competitor was added late to the race;
2580
- // data for newly selected competitor supposedly missing; refresh
2581
- redraw();
2582 2602
}
2583 2603
}
2584 2604
// Now update tails for all competitors because selection change may also affect all unselected competitors
... ...
@@ -2594,6 +2614,7 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
2594 2614
if (!zoomSettings.containsZoomType(ZoomTypes.NONE) && zoomSettings.isZoomToSelectedCompetitors()) {
2595 2615
zoomMapToNewBounds(zoomSettings.getNewBounds(this));
2596 2616
}
2617
+ redraw();
2597 2618
}
2598 2619
2599 2620
@Override
... ...
@@ -2865,7 +2886,9 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
2865 2886
public void onResize() {
2866 2887
if (map != null) {
2867 2888
map.triggerResize();
2868
- zoomMapToNewBounds(settings.getZoomSettings().getNewBounds(RaceMap.this));
2889
+ if (isMapInitialized) {
2890
+ zoomMapToNewBounds(settings.getZoomSettings().getNewBounds(RaceMap.this));
2891
+ }
2869 2892
}
2870 2893
// Adjust RaceMap headers to avoid overlapping based on the RaceMap width
2871 2894
boolean isCompactHeader = this.getOffsetWidth() <= 600;
... ...
@@ -3121,4 +3144,13 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
3121 3144
public RaceMapLifecycle getLifecycle() {
3122 3145
return raceMapLifecycle;
3123 3146
}
3147
+
3148
+ public void addMapInitializedListener(Runnable runnable) {
3149
+ if (isMapInitialized) {
3150
+ runnable.run();
3151
+ } else {
3152
+ this.mapInitializedListener.add(runnable);
3153
+ }
3154
+ }
3124 3155
}
3156
+
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/AbstractRaceBoardMode.java
... ...
@@ -78,8 +78,19 @@ public abstract class AbstractRaceBoardMode implements RaceBoardMode, RaceTimesI
78 78
leaderboardPanel.setAutoExpandPreSelected(true);
79 79
this.timer = raceBoardPanel.getTimer();
80 80
this.raceIdentifier = raceBoardPanel.getSelectedRaceIdentifier();
81
+ raceBoardPanel.getMap().addMapInitializedListener(new Runnable() {
82
+ public void run() {
83
+ checkIfTrigger();
84
+ };
85
+ });
81 86
}
82 87
88
+ protected void checkIfTrigger() {
89
+ if (raceBoardPanel.getMap().isDataInitialized()) {
90
+ trigger();
91
+ }
92
+ }
93
+
83 94
/**
84 95
* Called whenever new information of any sort has become available. Subclasses can use this
85 96
* to decide whether everything they need has been received in order to carry out their actions.
... ...
@@ -118,7 +129,7 @@ public abstract class AbstractRaceBoardMode implements RaceBoardMode, RaceTimesI
118 129
long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) {
119 130
this.raceTimesInfo = raceTimesInfo;
120 131
this.raceTimesInfoForRace = raceTimesInfo.get(getRaceIdentifier());
121
- trigger();
132
+ checkIfTrigger();
122 133
}
123 134
124 135
protected void stopReceivingRaceTimesInfos() {
... ...
@@ -131,7 +142,8 @@ public abstract class AbstractRaceBoardMode implements RaceBoardMode, RaceTimesI
131 142
@Override
132 143
public void updatedLeaderboard(LeaderboardDTO leaderboard) {
133 144
this.leaderboard = leaderboard;
134
- trigger();
145
+ checkIfTrigger();
146
+ ;
135 147
}
136 148
137 149
protected void stopReceivingLeaderboard() {
... ...
@@ -140,9 +152,9 @@ public abstract class AbstractRaceBoardMode implements RaceBoardMode, RaceTimesI
140 152
141 153
/**
142 154
* Fetches the leaderboard named {@code leaderboardName} for {@code timePoint} with details for race column
143
- * {@code raceColumnName} and stores it in {@link #leaderboardForSpecificTimePoint}, then invokes {@link #trigger()}.
144
- * If a {@link #getLeaderboard()} has been received already, it is passed on in the call in order to reduce the
145
- * bandwidth required, based on a differential approach.
155
+ * {@code raceColumnName} and stores it in {@link #leaderboardForSpecificTimePoint}, then invokes
156
+ * {@link #checkIfTrigger()}. If a {@link #getLeaderboard()} has been received already, it is passed on in the call
157
+ * in order to reduce the bandwidth required, based on a differential approach.
146 158
*/
147 159
protected void loadLeaderboardForSpecificTimePoint(String leaderboardName,
148 160
String raceColumnName, Date timePoint) {
... ...
@@ -156,7 +168,7 @@ public abstract class AbstractRaceBoardMode implements RaceBoardMode, RaceTimesI
156 168
public void onSuccess(LeaderboardDTO result) {
157 169
leaderboardForSpecificTimePoint = result;
158 170
getLeaderboardPanel().updateLeaderboard(result);
159
- trigger();
171
+ checkIfTrigger();
160 172
}
161 173
};
162 174
final ArrayList<String> raceColumnNameAsList = new ArrayList<>();
... ...
@@ -174,7 +186,7 @@ public abstract class AbstractRaceBoardMode implements RaceBoardMode, RaceTimesI
174 186
@Override
175 187
public void currentRaceSelected(RaceIdentifier raceIdentifier, RaceColumnDTO raceColumn) {
176 188
this.raceColumn = raceColumn;
177
- trigger();
189
+ checkIfTrigger();
178 190
}
179 191
180 192
protected Map<RegattaAndRaceIdentifier, RaceTimesInfoDTO> getRaceTimesInfo() {
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardModeWithPerRaceCompetitors.java
... ...
@@ -42,7 +42,7 @@ public abstract class RaceBoardModeWithPerRaceCompetitors extends AbstractRaceBo
42 42
@Override
43 43
public void competitorsForRaceDefined(Iterable<CompetitorDTO> competitorsInRace) {
44 44
this.competitorsInRace = competitorsInRace;
45
- trigger();
45
+ checkIfTrigger();
46 46
}
47 47
48 48
protected Iterable<CompetitorDTO> getCompetitorsInRace() {
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/MediaServiceImpl.java
... ...
@@ -1,16 +1,13 @@
1 1
package com.sap.sailing.gwt.ui.server;
2 2
3 3
import java.io.BufferedReader;
4
-import java.io.ByteArrayInputStream;
5 4
import java.io.DataInputStream;
6 5
import java.io.File;
7
-import java.io.FileNotFoundException;
8 6
import java.io.FileOutputStream;
9 7
import java.io.IOException;
10 8
import java.io.InputStream;
11 9
import java.io.InputStreamReader;
12 10
import java.io.UnsupportedEncodingException;
13
-import java.lang.reflect.Field;
14 11
import java.net.HttpURLConnection;
15 12
import java.net.ProtocolException;
16 13
import java.net.URL;
... ...
@@ -23,29 +20,21 @@ import java.nio.file.Files;
23 20
import java.text.DateFormat;
24 21
import java.text.SimpleDateFormat;
25 22
import java.util.Date;
26
-import java.util.List;
27 23
import java.util.logging.Level;
28 24
import java.util.logging.Logger;
29 25
import java.util.stream.Collectors;
30 26
31
-import javax.xml.parsers.DocumentBuilder;
32
-import javax.xml.parsers.DocumentBuilderFactory;
33 27
import javax.xml.parsers.ParserConfigurationException;
34 28
35 29
import org.apache.shiro.SecurityUtils;
30
+import org.mp4parser.AbstractBoxParser;
36 31
import org.mp4parser.IsoFile;
37
-import org.mp4parser.boxes.UserBox;
38
-import org.mp4parser.boxes.iso14496.part12.MediaDataBox;
39
-import org.mp4parser.boxes.iso14496.part12.MovieBox;
40
-import org.mp4parser.boxes.iso14496.part12.MovieHeaderBox;
41
-import org.mp4parser.tools.Path;
32
+import org.mp4parser.PropertyBoxParserImpl;
42 33
import org.osgi.framework.BundleContext;
43 34
import org.osgi.util.tracker.ServiceTracker;
44
-import org.w3c.dom.Document;
45
-import org.w3c.dom.Node;
46
-import org.w3c.dom.NodeList;
47 35
import org.xml.sax.SAXException;
48 36
37
+import com.google.gwt.thirdparty.json.JSONArray;
49 38
import com.google.gwt.thirdparty.json.JSONException;
50 39
import com.google.gwt.thirdparty.json.JSONObject;
51 40
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
... ...
@@ -55,6 +44,8 @@ import com.sap.sailing.domain.common.media.MediaTrack;
55 44
import com.sap.sailing.domain.common.security.Permission;
56 45
import com.sap.sailing.domain.common.security.Permission.Mode;
57 46
import com.sap.sailing.gwt.ui.client.MediaService;
47
+import com.sap.sailing.media.mp4.MP4MediaParser;
48
+import com.sap.sailing.media.mp4.MP4ParserFakeFile;
58 49
import com.sap.sailing.server.RacingEventService;
59 50
import com.sap.sse.common.Duration;
60 51
import com.sap.sse.common.impl.MillisecondsDurationImpl;
... ...
@@ -70,7 +61,6 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
70 61
private ServiceTracker<RacingEventService, RacingEventService> racingEventServiceTracker;
71 62
72 63
73
- private static final int REQUIRED_SIZE_IN_BYTES = 10000000;
74 64
private static final long serialVersionUID = -8917349579281305977L;
75 65
76 66
public MediaServiceImpl() {
... ...
@@ -158,14 +148,14 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
158 148
URL input = new URL(url);
159 149
// check size and do rangerequests if possible
160 150
long fileSize = determineFileSize(input);
161
- if (fileSize > 2 * REQUIRED_SIZE_IN_BYTES) {
151
+ if (fileSize > 2 * MP4MediaParser.REQUIRED_SIZE_IN_BYTES) {
162 152
response = checkMetadataByPartialDownloads(input, fileSize);
163 153
} else {
164 154
response = checkMetadataByFullFileDownload(input);
165 155
}
166 156
} catch (Exception e) {
167 157
logger.log(Level.WARNING, "Error in video analysis ", e);
168
- response = new VideoMetadataDTO(false, null, false, null, e.getMessage());
158
+ response = new VideoMetadataDTO(false, null, false, null, "");
169 159
}
170 160
return response;
171 161
}
... ...
@@ -198,10 +188,10 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
198 188
199 189
private VideoMetadataDTO checkMetadataByPartialDownloads(URL input, long fileSize)
200 190
throws IOException, ProtocolException {
201
- byte[] start = new byte[REQUIRED_SIZE_IN_BYTES];
202
- byte[] end = new byte[REQUIRED_SIZE_IN_BYTES];
203
- downloadPartOfFile(input, start, "bytes=0-" + REQUIRED_SIZE_IN_BYTES);
204
- downloadPartOfFile(input, end, "bytes=" + (fileSize - REQUIRED_SIZE_IN_BYTES) + "-");
191
+ byte[] start = new byte[MP4MediaParser.REQUIRED_SIZE_IN_BYTES];
192
+ byte[] end = new byte[MP4MediaParser.REQUIRED_SIZE_IN_BYTES];
193
+ downloadPartOfFile(input, start, "bytes=0-" + MP4MediaParser.REQUIRED_SIZE_IN_BYTES);
194
+ downloadPartOfFile(input, end, "bytes=" + (fileSize - MP4MediaParser.REQUIRED_SIZE_IN_BYTES) + "-");
205 195
long skipped = fileSize - start.length - end.length;
206 196
return checkMetadata(start, end, skipped);
207 197
}
... ...
@@ -212,9 +202,10 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
212 202
connection.setConnectTimeout(METADATA_CONNECTION_TIMEOUT);
213 203
connection.setRequestMethod("GET");
214 204
connection.setRequestProperty("Range", range);
215
- try (InputStream inStream = connection.getInputStream()) {
216
- DataInputStream dataInStream = new DataInputStream(inStream);
217
- dataInStream.readFully(store);
205
+ try (final InputStream inStream = connection.getInputStream()) {
206
+ try (final DataInputStream dataInStream = new DataInputStream(inStream)) {
207
+ dataInStream.readFully(store);
208
+ }
218 209
} finally {
219 210
connection.disconnect();
220 211
}
... ...
@@ -223,144 +214,51 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
223 214
private VideoMetadataDTO checkMetadataByFullFileDownload(URL input)
224 215
throws ParserConfigurationException, SAXException, IOException {
225 216
final File tmp = File.createTempFile("upload", "metadataCheck");
217
+ VideoMetadataDTO result;
226 218
try {
227
- final ReadableByteChannel rbc = Channels.newChannel(input.openStream());
228
- try (FileOutputStream fos = new FileOutputStream(tmp)) {
229
- fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
230
- try (IsoFile isof = new IsoFile(tmp)) {
231
- final boolean canDownload = true;
232
- final Date recordStartedTimer = determineRecordingStart(isof);
233
- final Duration duration = determineDuration(isof);
234
- final boolean spherical = determine360(isof);
235
- removeTempFiles(isof);
236
- return new VideoMetadataDTO(canDownload, duration, spherical, recordStartedTimer, "");
219
+ try (final ReadableByteChannel rbc = Channels.newChannel(input.openStream())) {
220
+ try (final FileOutputStream fos = new FileOutputStream(tmp)) {
221
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
222
+ try (final MP4ParserFakeFile inputFile = new MP4ParserFakeFile(tmp)) {
223
+ Files.delete(tmp.toPath());
224
+ result = checkMetadata(inputFile);
225
+ } catch (Exception e) {
226
+ result = new VideoMetadataDTO(true, null, false, null, e.getMessage());
227
+ }
228
+ } catch (Exception e) {
229
+ result = new VideoMetadataDTO(false, null, false, null, e.getMessage());
237 230
}
238 231
}
239 232
} finally {
240
- Files.delete(tmp.toPath());
233
+ // delete the file if not already deleted in main path
234
+ tmp.delete();
241 235
}
236
+ return result;
242 237
}
243 238
244 239
@Override
245 240
public VideoMetadataDTO checkMetadata(byte[] start, byte[] end, Long skipped) {
246 241
ensureUserCanManageMedia();
247
- File tmp = null;
248
- boolean spherical = false;
249
- Duration duration = null;
250
- Date recordStartedTimer = null;
251
- String message = "";
252
- try {
253
- tmp = createFileFromData(start, end, skipped);
254
- try (IsoFile isof = new IsoFile(tmp)) {
255
- try {
256
- recordStartedTimer = determineRecordingStart(isof);
257
- spherical = determine360(isof);
258
- duration = determineDuration(isof);
259
- } finally {
260
- removeTempFiles(isof);
261
- }
262
- }
242
+ VideoMetadataDTO result;
243
+ try (MP4ParserFakeFile input = new MP4ParserFakeFile(start, end, skipped)) {
244
+ result = checkMetadata(input);
263 245
} catch (Exception e) {
264 246
logger.log(Level.WARNING, "Error in video analysis ", e);
265
- message = e.getMessage();
266
- } finally {
267
- if (tmp != null) {
268
- try {
269
- Files.delete(tmp.toPath());
270
- } catch (IOException e) {
271
- logger.log(Level.SEVERE, "Could not delete tmp mp4 file", e);
272
- }
273
- }
247
+ result = new VideoMetadataDTO(true, null, false, null, e.getMessage());
274 248
}
275
- return new VideoMetadataDTO(true, duration, spherical, recordStartedTimer, message);
276
- }
277
-
278
- /**
279
- * Some boxes (we don't actually need to read) create internal tempfiles, we delete them here, as the VM could run
280
- * quite long
281
- */
282
- private void removeTempFiles(IsoFile isof) {
283
- List<MediaDataBox> boxesWithTempFiles = isof.getBoxes(MediaDataBox.class, true);
284
- for (MediaDataBox box : boxesWithTempFiles) {
285
- try {
286
- Field field = box.getClass().getDeclaredField("dataFile");
287
- field.setAccessible(true);
288
- File data = (File) field.get(box);
289
- Files.delete(data.toPath());
290
- } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException
291
- | IOException e) {
292
- logger.log(Level.WARNING, "Could not delete mp4 temp files", e);
293
- }
294
- }
295
- }
296
-
297
- /**
298
- * Creates a dummy mp4 file, only containing the start and the end, the middle is filled with 0 so that all file
299
- * internal pointers from start to end are working
300
- */
301
- private File createFileFromData(byte[] start, byte[] end, Long skipped) throws IOException, FileNotFoundException {
302
- File tmp;
303
- tmp = File.createTempFile("upload", "metadataCheck");
304
- try (FileOutputStream fw = new FileOutputStream(tmp)) {
305
- fw.write(start);
306
- while (skipped > 0) {
307
- // ensure that no absurd amount of memory is required, and that more then 4gb files can be analysed
308
- if (skipped > 10000000) {
309
- byte[] dummy = new byte[10000000];
310
- fw.write(dummy);
311
- skipped = skipped - 10000000;
312
- } else {
313
- byte[] dummy = new byte[skipped.intValue()];
314
- fw.write(dummy);
315
- skipped = skipped - skipped.intValue();
316
- }
317
- }
318
- fw.write(end);
319
- }
320
- return tmp;
321
- }
322
-
323
- private boolean determine360(IsoFile isof) throws ParserConfigurationException, SAXException, IOException {
324
- boolean spherical = false;
325
- UserBox uuidBox = Path.getPath(isof, "moov[0]/trak[0]/uuid");
326
- if (uuidBox != null) {
327
- DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
328
- DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
329
- Document doc = dBuilder.parse(new ByteArrayInputStream(uuidBox.getData()));
330
-
331
- NodeList childs = doc.getDocumentElement().getChildNodes();
332
- for (int i = 0; i < childs.getLength(); i++) {
333
- Node child = childs.item(i);
334
- if (child.getNodeName().toLowerCase().contains(":spherical")) {
335
- spherical = true;
336
- }
337
- }
338
- }
339
- return spherical;
249
+ return result;
340 250
}
341 251
342
- private Duration determineDuration(IsoFile isof) {
343
- Duration duration = null;
344
- MovieBox mbox = isof.getMovieBox();
345
- if (mbox != null) {
346
- MovieHeaderBox mhb = mbox.getMovieHeaderBox();
347
- if (mhb != null) {
348
- duration = new MillisecondsDurationImpl(mhb.getDuration()*1000 / mhb.getTimescale());
349
- }
350
- }
351
- return duration;
352
- }
353
-
354
- private Date determineRecordingStart(IsoFile isof) {
355
- Date creationTime = null;
356
- MovieBox mbox = isof.getMovieBox();
357
- if (mbox != null) {
358
- MovieHeaderBox mhb = mbox.getMovieHeaderBox();
359
- if (mhb != null) {
360
- creationTime = mhb.getCreationTime();
361
- }
252
+ private VideoMetadataDTO checkMetadata(MP4ParserFakeFile input)
253
+ throws ParserConfigurationException, SAXException, IOException {
254
+ AbstractBoxParser boxParserImpl = new PropertyBoxParserImpl();
255
+ boxParserImpl.skippingBoxes(new String[] { "mdat" });
256
+ try (IsoFile isof = new IsoFile(input, boxParserImpl)) {
257
+ Date recordStartedTimer = MP4MediaParser.determineRecordingStart(isof);
258
+ boolean spherical = MP4MediaParser.determine360(isof);
259
+ Duration duration = new MillisecondsDurationImpl(MP4MediaParser.determineDurationInMillis(isof));
260
+ return new VideoMetadataDTO(true, duration, spherical, recordStartedTimer, "");
362 261
}
363
- return creationTime;
364 262
}
365 263
366 264
@Override
... ...
@@ -384,6 +282,7 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
384 282
if (videoId.isEmpty()) {
385 283
message = "Empty id";
386 284
} else {
285
+ // sanitize, as we inject it into an url with our api key!
387 286
videoId = URLEncoder.encode(videoId, StandardCharsets.UTF_8.name());
388 287
try {
389 288
URL apiURL = new URL(
... ...
@@ -396,10 +295,13 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
396 295
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
397 296
String pageText = reader.lines().collect(Collectors.joining("\n"));
398 297
JSONObject jsonAnswer = new JSONObject(pageText);
399
- final JSONObject item = jsonAnswer.getJSONArray("items").getJSONObject(0);
400
- message = item.getJSONObject("snippet").getString("title");
401
- String rawDuration = item.getJSONObject("contentDetails").getString("duration");
402
- duration = new MillisecondsDurationImpl(java.time.Duration.parse(rawDuration).toMillis());
298
+ final JSONArray dataArray = jsonAnswer.getJSONArray("items");
299
+ if (dataArray.length() > 0) {
300
+ final JSONObject item = dataArray.getJSONObject(0);
301
+ message = item.getJSONObject("snippet").getString("title");
302
+ String rawDuration = item.getJSONObject("contentDetails").getString("duration");
303
+ duration = new MillisecondsDurationImpl(java.time.Duration.parse(rawDuration).toMillis());
304
+ }
403 305
canDownload = true;
404 306
} catch (JSONException e) {
405 307
message = e.getMessage();
... ...
@@ -410,9 +312,6 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
410 312
logger.log(Level.WARNING, "Error in youtube metadata call", e);
411 313
}
412 314
}
413
- //sanitize, as we inject it into an url with our api key!
414
-
415
-
416 315
return new VideoMetadataDTO(canDownload, duration, false, null, message);
417 316
}
418 317
}
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java
... ...
@@ -6784,13 +6784,12 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet
6784 6784
RegattaLogEvent event = null;
6785 6785
TimePoint from = mapping.getTimeRange().hasOpenBeginning() ? null : mapping.getTimeRange().from();
6786 6786
TimePoint to = mapping.getTimeRange().hasOpenEnd() ? null : mapping.getTimeRange().to();
6787
- if (dto.mappedTo instanceof MarkDTO) {
6788
- Mark mark = convertToMark(((MarkDTO) dto.mappedTo), true);
6789
- event = new RegattaLogDeviceMarkMappingEventImpl(now, now, getService().getServerAuthor(),
6790
- UUID.randomUUID(), mark, mapping.getDevice(), from, to);
6791
- } else if (dto.mappedTo instanceof CompetitorWithBoatDTO) {
6792
- Competitor competitor = getService().getCompetitorAndBoatStore()
6793
- .getExistingCompetitorByIdAsString(((CompetitorWithBoatDTO) dto.mappedTo).getIdAsString());
6787
+ if (mapping.getMappedTo() instanceof Mark) {
6788
+ Mark mark = (Mark) mapping.getMappedTo();
6789
+ event = new RegattaLogDeviceMarkMappingEventImpl(now, now, getService().getServerAuthor(), UUID.randomUUID(),
6790
+ mark, mapping.getDevice(), from, to);
6791
+ } else if (mapping.getMappedTo() instanceof Competitor) {
6792
+ Competitor competitor = (Competitor) mapping.getMappedTo();
6794 6793
if (mapping.getDevice().getIdentifierType().equals(ExpeditionSensorDeviceIdentifier.TYPE)) {
6795 6794
event = new RegattaLogDeviceCompetitorExpeditionExtendedMappingEventImpl(now, now,
6796 6795
getService().getServerAuthor(), UUID.randomUUID(), competitor, mapping.getDevice(), from, to);
... ...
@@ -6798,11 +6797,10 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet
6798 6797
event = new RegattaLogDeviceCompetitorMappingEventImpl(now, now, getService().getServerAuthor(),
6799 6798
UUID.randomUUID(), competitor, mapping.getDevice(), from, to);
6800 6799
}
6801
- } else if (dto.mappedTo instanceof BoatDTO) {
6802
- final Boat boat = getService().getCompetitorAndBoatStore()
6803
- .getExistingBoatByIdAsString(((BoatDTO) dto.mappedTo).getIdAsString());
6804
- event = new RegattaLogDeviceBoatMappingEventImpl(now, now, getService().getServerAuthor(),
6805
- UUID.randomUUID(), boat, mapping.getDevice(), from, to);
6800
+ } else if (mapping.getMappedTo() instanceof Boat) {
6801
+ final Boat boat = (Boat) mapping.getMappedTo();
6802
+ event = new RegattaLogDeviceBoatMappingEventImpl(now, now, getService().getServerAuthor(), UUID.randomUUID(),
6803
+ boat, mapping.getDevice(), from, to);
6806 6804
} else {
6807 6805
throw new RuntimeException("Can only map devices to competitors, boats or marks");
6808 6806
}
... ...
@@ -6851,14 +6849,12 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet
6851 6849
TimeRange timeRange = new TimeRangeImpl(from, to);
6852 6850
if (dto.mappedTo instanceof MarkDTO) {
6853 6851
Mark mark = convertToMark(((MarkDTO) dto.mappedTo), true);
6854
- // expect UUIDs
6855
- return new DeviceMappingImpl<Mark>(mark, device, timeRange, dto.originalRaceLogEventIds,
6856
- RegattaLogDeviceMarkMappingEventImpl.class);
6857
- } else if (dto.mappedTo instanceof CompetitorWithBoatDTO) {
6858
- Competitor competitor = getService().getCompetitorAndBoatStore()
6859
- .getExistingCompetitorByIdAsString(((CompetitorWithBoatDTO) dto.mappedTo).getIdAsString());
6860
- return new DeviceMappingImpl<Competitor>(competitor, device, timeRange, dto.originalRaceLogEventIds,
6861
- RegattaLogDeviceCompetitorMappingEventImpl.class);
6852
+ //expect UUIDs
6853
+ return new DeviceMappingImpl<Mark>(mark, device, timeRange, dto.originalRaceLogEventIds, RegattaLogDeviceMarkMappingEventImpl.class);
6854
+ } else if (dto.mappedTo instanceof CompetitorDTO) {
6855
+ Competitor competitor = getService().getCompetitorAndBoatStore().getExistingCompetitorByIdAsString(
6856
+ ((CompetitorDTO) dto.mappedTo).getIdAsString());
6857
+ return new DeviceMappingImpl<Competitor>(competitor, device, timeRange, dto.originalRaceLogEventIds, RegattaLogDeviceCompetitorMappingEventImpl.class);
6862 6858
} else if (dto.mappedTo instanceof BoatDTO) {
6863 6859
final Boat boat = getService().getCompetitorAndBoatStore()
6864 6860
.getExistingBoatByIdAsString(dto.mappedTo.getIdAsString());
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/WindStreamletsRaceboardOverlay.java
... ...
@@ -228,6 +228,7 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay implemen
228 228
if (this.swarm != null) {
229 229
this.swarm.stop();
230 230
this.swarm.getColorMapper().removeListener(this);
231
+ swarm = null;
231 232
}
232 233
}
233 234
... ...
@@ -335,12 +336,10 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay implemen
335 336
if (getCanvas() != null) {
336 337
if (isVisible) {
337 338
this.startStreamlets();
338
- this.visible = isVisible;
339 339
if (colored) {
340 340
this.streamletLegend.setVisible(true);
341 341
}
342 342
swarm.setColors(colored);
343
- this.startStreamlets();
344 343
if (isAttached) {
345 344
Scheduler.get().scheduleDeferred(() -> addObserverIfNecessary());
346 345
} else {
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/streamlets/Swarm.java
... ...
@@ -6,6 +6,7 @@ import java.util.Map;
6 6
7 7
import com.google.gwt.canvas.client.Canvas;
8 8
import com.google.gwt.canvas.dom.client.Context2d;
9
+import com.google.gwt.core.client.Duration;
9 10
import com.google.gwt.event.shared.HandlerRegistration;
10 11
import com.google.gwt.maps.client.MapWidget;
11 12
import com.google.gwt.maps.client.base.LatLng;
... ...
@@ -54,7 +55,6 @@ public class Swarm implements TimeListener {
54 55
private boolean swarmOffScreen = false;
55 56
56 57
private int swarmPause = 0;
57
- private boolean swarmContinue = true;
58 58
59 59
/**
60 60
* Bounds in the map's coordinate system which may have undergone rotation and translation. See the
... ...
@@ -96,47 +96,95 @@ public class Swarm implements TimeListener {
96 96
}
97 97
98 98
public void start(final int animationIntervalMillis) {
99
+ removeBoundsChangeHandler();
99 100
fullcanvas.setCanvasSettings();
100
- // if map is not yet loaded, wait for it
101
- if (map.getBounds() != null) {
102
- startWithMap(animationIntervalMillis);
103
- } else {
104
- removeBoundsChangeHandler();
105
- BoundsChangeMapHandler handler = new BoundsChangeMapHandler() {
106
- @Override
107
- public void onEvent(BoundsChangeMapEvent event) {
108
- if (swarmContinue) {
109
- startWithMap(animationIntervalMillis);
110
- }
101
+ if (loopTimer == null) {
102
+ loopTimer = new com.google.gwt.user.client.Timer() {
103
+ public void run() {
104
+ updateSwarmOneTick(animationIntervalMillis);
111 105
}
112 106
};
113
- boundsChangeHandlers.put(handler, map.addBoundsChangeHandler(handler));
114 107
}
108
+
109
+ // the map already exists, ensure swam is started
110
+ if (map.getBounds() != null) {
111
+ startSwarmIfNecessaryAndUpdateProjection();
112
+ loopTimer.schedule(0);
113
+ }
114
+ // add handler, so the swarm updates itself without calls from the map required
115
+ BoundsChangeMapHandler handler = new BoundsChangeMapHandler() {
116
+ @Override
117
+ public void onEvent(BoundsChangeMapEvent event) {
118
+ startSwarmIfNecessaryAndUpdateProjection();
119
+ loopTimer.schedule(0);
120
+ }
121
+ };
122
+ boundsChangeHandlers.put(handler, map.addBoundsChangeHandler(handler));
115 123
}
116 124
117
- private void startWithMap(int animationIntervalMillis) {
118
- projection = new Mercator(fullcanvas, map);
125
+ private void startSwarmIfNecessaryAndUpdateProjection() {
126
+ if (projection == null) {
127
+ projection = new Mercator(fullcanvas, map);
128
+
129
+ }
130
+ // ensure projection fits the map
119 131
projection.calibrate();
132
+ // ensure canvas fits the map
120 133
updateBounds();
121 134
if (particles == null) {
135
+ // if this is the first start, also createParticles
122 136
createParticles();
123 137
} else {
138
+ // clear all tails, as else there will be long smears over the map
124 139
clearNextFrame = true;
125 140
}
126
- swarmContinue = true;
127
- startLoopIfNecessary(animationIntervalMillis);
141
+ }
142
+
143
+ private void updateSwarmOneTick(int animationIntervalMillis) {
144
+ double time0 = Duration.currentTimeMillis();
145
+ // upon zooming the swarm is shortly paused, to ensure no strange rendering during the css zoom
146
+ // animations
147
+ if (swarmPause > 1) {
148
+ swarmPause--;
149
+ } else if (swarmPause == 1) {
150
+ // ensure bounds and projections are up to date
151
+ startSwarmIfNecessaryAndUpdateProjection();
152
+ if (zoomChanged) {
153
+ // ensure amount of particles is updated
154
+ diffPx = new Vector(0, 0);
155
+ recycleParticles();
156
+ zoomChanged = false;
157
+ } else {
158
+ // process the offset
159
+ diffPx = fullcanvas.getDiffPx();
160
+ }
161
+ swarmPause = 0;
162
+ }
163
+ // update the swarm if it is not paused
164
+ if ((!swarmOffScreen) && (swarmPause == 0)) {
165
+ execute(diffPx);
166
+ diffPx = new Vector(0, 0);
167
+ }
168
+ double time1 = Duration.currentTimeMillis();
169
+ // wait at least 10ms for the next iteration; try to get one iteration done every
170
+ // animationIntervalMillis if possible
171
+ double timeDelta = time1 - time0;
172
+ // log("fps: "+(1000.0/timeDelta));
173
+ loopTimer.schedule((int) Math.max(10, animationIntervalMillis - timeDelta));
128 174
}
129 175
130 176
private void removeBoundsChangeHandler() {
131 177
for (HandlerRegistration reg : boundsChangeHandlers.values()) {
132 178
reg.removeHandler();
133 179
}
180
+ boundsChangeHandlers.clear();
134 181
135 182
}
136 183
137 184
public void stop() {
138
- swarmContinue = false;
139 185
removeBoundsChangeHandler();
186
+ projection.clearCanvas();
187
+ loopTimer.cancel();
140 188
}
141 189
142 190
private Particle recycleOrCreateParticle(Particle particle) {
... ...
@@ -226,52 +274,12 @@ public class Swarm implements TimeListener {
226 274
+ visibleBoundsOfField.getNorthEast().getLatitude() / 180. * Math.PI) / 2);
227 275
}
228 276
229
- private void startLoopIfNecessary(final int animationIntervalMillis) {
230
- if (loopTimer == null) {
231
- // Create animation-loop based on timer timeout
232
- loopTimer = new com.google.gwt.user.client.Timer() {
233
- public void run() {
234
- Date time0 = new Date();
235
- if (swarmPause > 1) {
236
- swarmPause--;
237
- } else if (swarmPause == 1) {
238
- fullcanvas.setCanvasSettings();
239
- projection.calibrate();
240
- updateBounds();
241
- if (zoomChanged) {
242
- diffPx = new Vector(0, 0);
243
- recycleParticles();
244
- zoomChanged = false;
245
- } else {
246
- diffPx = fullcanvas.getDiffPx();
247
- }
248
- swarmPause = 0;
249
- }
250
- if ((!swarmOffScreen) && (swarmPause == 0)) {
251
- execute(diffPx);
252
- diffPx = new Vector(0, 0);
253
- }
254
- Date time1 = new Date();
255
- if (swarmContinue) {
256
- // wait at least 10ms for the next iteration; try to get one iteration done every
257
- // animationIntervalMillis if possible
258
- long timeDelta = time1.getTime() - time0.getTime();
259
- // log("fps: "+(1000.0/timeDelta));
260
- loopTimer.schedule((int) Math.max(10, animationIntervalMillis - timeDelta));
261
- } else {
262
- projection.clearCanvas();
263
- }
264
- }
265
- };
266
- loopTimer.schedule(animationIntervalMillis);
267
- }
268
- }
269
-
270 277
private void drawSwarm() {
271 278
Context2d ctxt = canvas.getContext2d();
279
+ // clearing neds to be done with a little over zero, thanks IE
272 280
ctxt.setGlobalAlpha(0.06);
273 281
if (clearNextFrame) {
274
- // ctxt.setGlobalAlpha(1);
282
+ // skip this frame, as it will contain an extreme delta due to the map movement
275 283
clearNextFrame = false;
276 284
} else {
277 285
ctxt.setGlobalCompositeOperation("destination-out");
java/com.sap.sailing.server.gateway/resources/shiro.ini
... ...
@@ -85,9 +85,9 @@ bearerToken = com.sap.sailing.server.security.BearerTokenOrBasicOrFormAuthentica
85 85
# Secure access to additional apis used for SailRacer
86 86
/api/v1/leaderboards/*/enableRaceLogForCompetitorRegistration/** = bearerToken
87 87
/api/v1/leaderboards/*/denoteForTracking/** = bearerToken
88
+/api/v1/leaderboards/*/removeTrackedRace/** = bearerToken
88 89
/api/v1/regattas/updateOrCreateSeries = bearerToken
89 90
/api/v1/mark/addMarkToRegatta/** = bearerToken
90 91
/api/v1/mark/addMarkFix/** = bearerToken
91 92
/api/v1/mark/addCourseDefinitionToRaceLog/** = bearerToken
92 93
/api/v1/wind/putWind/** = bearerToken
93
-
java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResource.java
... ...
@@ -119,6 +119,8 @@ import com.sap.sailing.server.gateway.serialization.coursedata.impl.WaypointJson
119 119
import com.sap.sailing.server.gateway.serialization.impl.CompetitorAndBoatJsonSerializer;
120 120
import com.sap.sailing.server.gateway.serialization.impl.FlatGPSFixJsonSerializer;
121 121
import com.sap.sailing.server.gateway.serialization.impl.MarkJsonSerializerWithPosition;
122
+import com.sap.sailing.server.operationaltransformation.RemoveAndUntrackRace;
123
+import com.sap.sailing.server.operationaltransformation.StopTrackingRace;
122 124
import com.sap.sse.InvalidDateException;
123 125
import com.sap.sse.common.Bearing;
124 126
import com.sap.sse.common.Distance;
... ...
@@ -847,11 +849,28 @@ public class LeaderboardsResource extends AbstractLeaderboardsResource {
847 849
848 850
@POST
849 851
@Produces(MediaType.APPLICATION_JSON)
852
+ @Path("{leaderboardName}/removeTrackedRace")
853
+ public Response removeTrackedRace(@PathParam("leaderboardName") String leaderboardName,
854
+ @QueryParam(RaceLogServletConstants.PARAMS_RACE_COLUMN_NAME) String raceColumnName,
855
+ @QueryParam(RaceLogServletConstants.PARAMS_RACE_FLEET_NAME) String fleetName)
856
+ throws MalformedURLException, IOException, InterruptedException {
857
+ SecurityUtils.getSubject()
858
+ .checkPermission(Permission.LEADERBOARD.getStringPermissionForObjects(Mode.UPDATE, leaderboardName));
859
+ return stopOrRemoveTrackedRace(leaderboardName, raceColumnName, fleetName, true);
860
+ }
861
+
862
+ @POST
863
+ @Produces(MediaType.APPLICATION_JSON)
850 864
@Path("{leaderboardName}/stoptracking")
851 865
public Response stopTracking(@PathParam("leaderboardName") String leaderboardName,
852 866
@QueryParam(RaceLogServletConstants.PARAMS_RACE_COLUMN_NAME) String raceColumnName,
853 867
@QueryParam(RaceLogServletConstants.PARAMS_RACE_FLEET_NAME) String fleetName) throws MalformedURLException, IOException, InterruptedException {
854 868
SecurityUtils.getSubject().checkPermission(Permission.LEADERBOARD.getStringPermissionForObjects(Mode.UPDATE, leaderboardName));
869
+ return stopOrRemoveTrackedRace(leaderboardName, raceColumnName, fleetName, false);
870
+ }
871
+
872
+ private Response stopOrRemoveTrackedRace(String leaderboardName, String raceColumnName, String fleetName, boolean remove)
873
+ throws MalformedURLException, IOException, InterruptedException {
855 874
final Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName);
856 875
final Response result;
857 876
if (leaderboard == null) {
... ...
@@ -869,7 +888,11 @@ public class LeaderboardsResource extends AbstractLeaderboardsResource {
869 888
if (trackedRace != null) {
870 889
final Regatta regatta = trackedRace.getTrackedRegatta().getRegatta();
871 890
final RaceDefinition race = trackedRace.getRace();
872
- getService().stopTracking(regatta, race);
891
+ if(remove) {
892
+ getService().apply(new RemoveAndUntrackRace(trackedRace.getRaceIdentifier()));
893
+ } else {
894
+ getService().apply(new StopTrackingRace(trackedRace.getRaceIdentifier()));
895
+ }
873 896
jsonResult.put("regatta", regatta.getName());
874 897
jsonResult.put("race", race.getName());
875 898
jsonResultArray.add(jsonResult);
java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResourceV2.java
... ...
@@ -296,7 +296,7 @@ public class LeaderboardsResourceV2 extends AbstractLeaderboardsResource {
296 296
break;
297 297
case RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS:
298 298
name = "currentSpeedOverGround-kts";
299
- if (currentLegEntry != null) {
299
+ if (currentLegEntry != null && currentLegEntry.currentSpeedOverGroundInKnots != null) {
300 300
value = roundDouble(currentLegEntry.currentSpeedOverGroundInKnots, 2);
301 301
}
302 302
break;
java/com.sap.sailing.server.gateway/webservices/api/v1/index.html
... ...
@@ -206,6 +206,10 @@ name to make sure you get to the event you're interested in.
206 206
<td>Stops tracking for a race</td>
207 207
</tr>
208 208
<tr>
209
+ <td><a href="leaderboardRaceRemove.html">/api/v1/leaderboards/{name}/removeTrackedRace</a></td>
210
+ <td>Removes a tracked race</td>
211
+ </tr>
212
+ <tr>
209 213
<td><a href="leaderboardRaceDenoteForTracking.html">/api/v1/leaderboards/{name}/denoteForTracking</a></td>
210 214
<td>Denotes a race for tracking</td>
211 215
</tr>
java/com.sap.sailing.server.gateway/webservices/api/v1/leaderboardRaceRemove.html
... ...
@@ -0,0 +1,57 @@
1
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
2
+<html>
3
+<head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
5
+ <link href='../../../../fontface.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
6
+ <link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
7
+ <link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
8
+ <title>SAP Sailing Analytics Webservices API Version 1.0</title>
9
+</head>
10
+<body>
11
+<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
12
+<h2>URL: /api/v1/leaderboards/{name}/removeTrackedRace?race_column={name}&amp;fleet={name}</h2>
13
+
14
+<b>Description:</b>
15
+<p>Removes the tracked race(s) identified by the race_column and fleet parameters.
16
+</p>
17
+<br/>
18
+<table>
19
+ <tr>
20
+ <td>Webservice Type:</td>
21
+ <td>REST</td>
22
+ </tr>
23
+ <tr>
24
+ <td>Output format:</td>
25
+ <td>Json</td>
26
+ </tr>
27
+ <tr>
28
+ <td>Mandatory parameters:</td>
29
+ <td>None</td>
30
+ </tr>
31
+ <tr>
32
+ <td>Optional parameters:</td>
33
+ <td>race_column: if provided, restricts stopping to races in this race column; otherwise, races from all race columns will stop tracking
34
+ if the fleet parameter matches; see below<p>
35
+ fleet: if provided, stopping is restricted to races in this fleet only; otherwise, races from all fleets are stopped<p></td>
36
+ </tr>
37
+ <tr>
38
+ <td>Request method:</td>
39
+ <td>POST</td>
40
+ </tr>
41
+ <tr>
42
+ <td>Example 1:</td>
43
+ <td>curl -X POST -H "Authorization: Bearer w9YSIfr/Zf6a127SZDddkhdAtQuuv0Jm1PSXlo1Yvd0=" "http://127.0.0.1:8888/sailingserver/api/v1/leaderboards/Smartphone%20Tracking%20Test%2022.06/removeTrackedRace?race_column=K1&fleet=Blue"<br>
44
+ delivers an array with the regatta and race names of the races whose tracking got stopped:<pre>
45
+ [{"regatta":"Smartphone Tracking Test 22.06","race":"Smartphone Tracking Test 22.06 K1 Blue"}]
46
+ </pre></td>
47
+ <td>Example 2:</td>
48
+ <td>curl -X POST -H "Authorization: Bearer w9YSIfr/Zf6a127SZDddkhdAtQuuv0Jm1PSXlo1Yvd0=" "http://127.0.0.1:8888/sailingserver/api/v1/leaderboards/Smartphone%20Tracking%20Test%2022.06/removeTrackedRace?race_column=K1"<br>
49
+ delivers an array with the regatta and race names of the races whose tracking got stopped:<pre>
50
+ [{"regatta":"Smartphone Tracking Test 22.06","race":"Smartphone Tracking Test 22.06 K1 Blue"}, {"regatta":"Smartphone Tracking Test 22.06","race":"Smartphone Tracking Test 22.06 K1 Yellow"}]
51
+ </pre></td>
52
+ </tr>
53
+</table>
54
+<div style="height: 1em;"></div>
55
+<a href="index.html">Back to Web Service Overview</a>
56
+</body>
57
+</html>
java/com.sap.sailing.server.gateway/webservices/api/v1/putWind.html
... ...
@@ -13,6 +13,8 @@
13 13
14 14
<b>Description:</b>
15 15
<p>Gets the details of a competitor</p>
16
+Note that while nautically wind is usually specified in the direction it's coming <em>from</em>, for <br>
17
+computational purposes internally we measure the wind in the direction <em>to</em> which it blows. <br>
16 18
<br/>
17 19
<table>
18 20
<tr>
java/com.sap.sse.datamining/resources/stringmessages/StringMessages_es.properties
... ...
@@ -1,7 +1,7 @@
1 1
Average=Promedio
2 2
PairAverage=Promedio
3 3
Median=Mediana
4
-PairCollecting=Identidad
4
+PairCollecting=Dispersión
5 5
Sum=Totalizar
6 6
Maximum=Máximo
7 7
Minimum=Mínimo
... ...
@@ -12,3 +12,4 @@ Knots=nudos
12 12
Collecting=Colección
13 13
ResultSignifier={0} ({1})
14 14
SignifierInUnit={0} en {1}
15
+Identity=Identidad
java/com.sap.sse.datamining/resources/stringmessages/StringMessages_fr.properties
... ...
@@ -1,7 +1,7 @@
1 1
Average=Moyenne
2 2
PairAverage=Moyenne
3 3
Median=Médiane
4
-PairCollecting=Identité
4
+PairCollecting=Nuage de points
5 5
Sum=Somme
6 6
Maximum=Maximum
7 7
Minimum=Minimum
... ...
@@ -12,3 +12,4 @@ Knots=nd
12 12
Collecting=Collection
13 13
ResultSignifier={0} ({1})
14 14
SignifierInUnit={0} dans {1}
15
+Identity=Identité
java/com.sap.sse.datamining/resources/stringmessages/StringMessages_ja.properties
... ...
@@ -1,7 +1,7 @@
1 1
Average=平均
2 2
PairAverage=平均
3 3
Median=中央値
4
-PairCollecting=ID
4
+PairCollecting=散布図
5 5
Sum=合計
6 6
Maximum=最大
7 7
Minimum=最小
... ...
@@ -12,3 +12,4 @@ Knots=kn
12 12
Collecting=コレクション
13 13
ResultSignifier={0} ({1})
14 14
SignifierInUnit={0} / {1}
15
+Identity=ID
java/com.sap.sse.datamining/resources/stringmessages/StringMessages_pt.properties
... ...
@@ -1,7 +1,7 @@
1 1
Average=Média
2 2
PairAverage=Média
3 3
Median=Mediana
4
-PairCollecting=Identidade
4
+PairCollecting=Dispersão
5 5
Sum=Total
6 6
Maximum=Máximo
7 7
Minimum=Mínimo
... ...
@@ -12,3 +12,4 @@ Knots=nó
12 12
Collecting=Coleção
13 13
ResultSignifier={0} ({1})
14 14
SignifierInUnit={0} em {1}
15
+Identity=Identidade
java/com.sap.sse.datamining/resources/stringmessages/StringMessages_ru.properties
... ...
@@ -1,7 +1,7 @@
1 1
Average=Среднее
2 2
PairAverage=Среднее
3 3
Median=Медиана
4
-PairCollecting=Идентичность
4
+PairCollecting=Точечная
5 5
Sum=Сумма
6 6
Maximum=Максимум
7 7
Minimum=Минимум
... ...
@@ -12,3 +12,4 @@ Knots=уз.
12 12
Collecting=Коллекция
13 13
ResultSignifier={0} ({1})
14 14
SignifierInUnit={0} в {1}
15
+Identity=Идентичность
java/com.sap.sse.datamining/resources/stringmessages/StringMessages_zh.properties
... ...
@@ -1,7 +1,7 @@
1 1
Average=平均值
2 2
PairAverage=平均值
3 3
Median=中值
4
-PairCollecting=身份
4
+PairCollecting=散点图
5 5
Sum=求和
6 6
Maximum=最大值
7 7
Minimum=最小值
... ...
@@ -12,3 +12,4 @@ Knots=kn
12 12
Collecting=集合
13 13
ResultSignifier={0} ({1})
14 14
SignifierInUnit={0}(单位:{1})
15
+Identity=身份
java/org.mp4parser.isoparser/.classpath
... ...
@@ -1,7 +1,7 @@
1 1
<?xml version="1.0" encoding="UTF-8"?>
2 2
<classpath>
3 3
<classpathentry kind="src" path="src"/>
4
- <classpathentry exported="true" kind="lib" path="lib/isoparser-1.9.31.jar"/>
4
+ <classpathentry exported="true" kind="lib" path="lib/isoparser-1.9.37.jar" sourcepath="lib/isoparser-1.9.37-sources.jar"/>
5 5
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
6 6
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
7 7
<classpathentry kind="output" path="bin"/>
java/org.mp4parser.isoparser/META-INF/MANIFEST.MF
... ...
@@ -2,11 +2,12 @@ Manifest-Version: 1.0
2 2
Bundle-ManifestVersion: 2
3 3
Bundle-Name: Selenium
4 4
Bundle-SymbolicName: org.mp4parser.isoparser
5
-Bundle-Version: 1.9.31
6
-Bundle-ClassPath: lib/isoparser-1.9.31.jar
5
+Bundle-Version: 1.9.37
6
+Bundle-ClassPath: .,
7
+ lib/isoparser-1.9.37.jar
7 8
Bundle-Vendor: Sebastian Annies
8 9
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
9
-Export-Package: com.sap.sailing.media,
10
+Export-Package: com.sap.sailing.media.mp4,
10 11
org.mp4parser,
11 12
org.mp4parser.boxes,
12 13
org.mp4parser.boxes.iso14496.part12,
java/org.mp4parser.isoparser/build.properties
... ...
@@ -1,2 +1,5 @@
1 1
bin.includes = META-INF/,\
2
- lib/isoparser-1.9.31.jar
2
+ lib/isoparser-1.9.37.jar
3
+jars.compile.order = .
4
+source.. = src/
5
+output.. = bin/
java/org.mp4parser.isoparser/lib/isoparser-1.9.37-sources.jar
... ...
Binary files /dev/null and b/java/org.mp4parser.isoparser/lib/isoparser-1.9.37-sources.jar differ
java/org.mp4parser.isoparser/lib/isoparser-1.9.37.jar
... ...
Binary files /dev/null and b/java/org.mp4parser.isoparser/lib/isoparser-1.9.37.jar differ
java/org.mp4parser.isoparser/pom.xml
... ...
@@ -8,6 +8,6 @@
8 8
<version>1.0.0-SNAPSHOT</version>
9 9
</parent>
10 10
<artifactId>org.mp4parser.isoparser</artifactId>
11
- <version>1.9.31</version>
11
+ <version>1.9.37</version>
12 12
<packaging>eclipse-plugin</packaging>
13 13
</project>
java/org.mp4parser.isoparser/src/com/sap/sailing/media/MediaFileParser.java
... ...
@@ -1,82 +0,0 @@
1
-package com.sap.sailing.media;
2
-
3
-import java.io.ByteArrayInputStream;
4
-import java.io.File;
5
-import java.io.IOException;
6
-import java.net.MalformedURLException;
7
-import java.util.Date;
8
-
9
-import javax.xml.parsers.DocumentBuilder;
10
-import javax.xml.parsers.DocumentBuilderFactory;
11
-import javax.xml.parsers.ParserConfigurationException;
12
-
13
-import org.mp4parser.IsoFile;
14
-import org.mp4parser.boxes.UserBox;
15
-import org.mp4parser.boxes.iso14496.part12.MovieBox;
16
-import org.mp4parser.boxes.iso14496.part12.MovieHeaderBox;
17
-import org.mp4parser.tools.Path;
18
-import org.w3c.dom.Document;
19
-import org.w3c.dom.Node;
20
-import org.w3c.dom.NodeList;
21
-import org.xml.sax.SAXException;
22
-
23
-public class MediaFileParser {
24
- public static void main(String[] args) throws MalformedURLException, ParserConfigurationException, SAXException, IOException {
25
- new MediaFileParser().checkMetadata(new File(args[0]));
26
- }
27
-
28
- public void checkMetadata(File file) throws IOException, ParserConfigurationException, SAXException {
29
- boolean spherical = false;
30
- long durationInMillis = -1;
31
- Date recordStartedTimer = null;
32
- try (IsoFile isof = new IsoFile(file)) {
33
- recordStartedTimer = determineRecordingStart(isof);
34
- spherical = determine360(isof);
35
- durationInMillis = determineDurationInMillis(isof);
36
- System.out.println("Start: "+recordStartedTimer.toString()+", duration: "+durationInMillis+"ms; spherical: "+spherical);
37
- }
38
- }
39
-
40
- private boolean determine360(IsoFile isof) throws ParserConfigurationException, SAXException, IOException {
41
- boolean spherical = false;
42
- UserBox uuidBox = Path.getPath(isof, "moov[0]/trak[0]/uuid");
43
- if (uuidBox != null) {
44
- DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
45
- DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
46
- Document doc = dBuilder.parse(new ByteArrayInputStream(uuidBox.getData()));
47
-
48
- NodeList childs = doc.getDocumentElement().getChildNodes();
49
- for (int i = 0; i < childs.getLength(); i++) {
50
- Node child = childs.item(i);
51
- if (child.getNodeName().toLowerCase().contains(":spherical")) {
52
- spherical = true;
53
- }
54
- }
55
- }
56
- return spherical;
57
- }
58
-
59
- private long determineDurationInMillis(IsoFile isof) {
60
- long duration = -1;
61
- MovieBox mbox = isof.getMovieBox();
62
- if (mbox != null) {
63
- MovieHeaderBox mhb = mbox.getMovieHeaderBox();
64
- if (mhb != null) {
65
- duration = mhb.getDuration()*1000 / mhb.getTimescale();
66
- }
67
- }
68
- return duration;
69
- }
70
-
71
- private Date determineRecordingStart(IsoFile isof) {
72
- Date creationTime = null;
73
- MovieBox mbox = isof.getMovieBox();
74
- if (mbox != null) {
75
- MovieHeaderBox mhb = mbox.getMovieHeaderBox();
76
- if (mhb != null) {
77
- creationTime = mhb.getCreationTime();
78
- }
79
- }
80
- return creationTime;
81
- }
82
-}
java/org.mp4parser.isoparser/src/com/sap/sailing/media/mp4/MP4MediaParser.java
... ...
@@ -0,0 +1,97 @@
1
+package com.sap.sailing.media.mp4;
2
+
3
+import java.io.ByteArrayInputStream;
4
+import java.io.IOException;
5
+import java.util.Date;
6
+
7
+import javax.xml.parsers.DocumentBuilder;
8
+import javax.xml.parsers.DocumentBuilderFactory;
9
+import javax.xml.parsers.ParserConfigurationException;
10
+
11
+import org.mp4parser.IsoFile;
12
+import org.mp4parser.boxes.UserBox;
13
+import org.mp4parser.boxes.iso14496.part12.MovieBox;
14
+import org.mp4parser.boxes.iso14496.part12.MovieHeaderBox;
15
+import org.mp4parser.tools.Path;
16
+import org.w3c.dom.Document;
17
+import org.w3c.dom.Node;
18
+import org.w3c.dom.NodeList;
19
+import org.xml.sax.SAXException;
20
+
21
+public class MP4MediaParser {
22
+ /**
23
+ * The relevant size at the start and end of a file, that most likely will contain all metadata
24
+ */
25
+ public static final int REQUIRED_SIZE_IN_BYTES = 10000000;
26
+
27
+ /**
28
+ * Determine whether or not the provided {@link IsoFile ISO file} contains any "spherical" information within the
29
+ * user defined box <code>moov[0]/trak[0]/uuid</code>.
30
+ *
31
+ * @param isof
32
+ * {@link IsoFile ISO file} to analyze
33
+ * @return <code>true</code> if spherical information are contained, <code>false</code> otherwise
34
+ * @throws ParserConfigurationException
35
+ * @throws SAXException
36
+ * @throws IOException
37
+ */
38
+ public static boolean determine360(IsoFile isof) throws ParserConfigurationException, SAXException, IOException {
39
+ boolean spherical = false;
40
+ UserBox uuidBox = Path.getPath(isof, "moov[0]/trak[0]/uuid");
41
+ if (uuidBox != null) {
42
+ DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
43
+ DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
44
+ Document doc = dBuilder.parse(new ByteArrayInputStream(uuidBox.getData()));
45
+
46
+ NodeList childs = doc.getDocumentElement().getChildNodes();
47
+ for (int i = 0; i < childs.getLength(); i++) {
48
+ Node child = childs.item(i);
49
+ if (child.getNodeName().toLowerCase().contains(":spherical")) {
50
+ spherical = true;
51
+ }
52
+ }
53
+ }
54
+ return spherical;
55
+ }
56
+
57
+ /**
58
+ * Tries to determine the {@link Long duration} of the provided {@link IsoFile ISO file} based on its
59
+ * {@link IsoFile#getMovieBox() movie header}.
60
+ *
61
+ * @param isof
62
+ * {@link IsoFile ISO file} to analyze
63
+ * @return the determined duration in {@link Long millis}, <code>-1</code> if the duration could not be determined
64
+ */
65
+ public static long determineDurationInMillis(IsoFile isof) {
66
+ long duration = -1;
67
+ MovieBox mbox = isof.getMovieBox();
68
+ if (mbox != null) {
69
+ MovieHeaderBox mhb = mbox.getMovieHeaderBox();
70
+ if (mhb != null) {
71
+ duration = mhb.getDuration() * 1000 / mhb.getTimescale();
72
+ }
73
+ }
74
+ return duration;
75
+ }
76
+
77
+ /**
78
+ * Tries to determine the {@link Date point in time} when the recording started from the provided {@link IsoFile ISO
79
+ * file} based on its {@link IsoFile#getMovieBox() movie header}.
80
+ *
81
+ * @param isof
82
+ * {@link IsoFile ISO file} to analyze
83
+ * @return the determined {@link Date point in time} when the recording started, <code>null</code> if the recording
84
+ * start could not be determined
85
+ */
86
+ public static Date determineRecordingStart(IsoFile isof) {
87
+ Date creationTime = null;
88
+ MovieBox mbox = isof.getMovieBox();
89
+ if (mbox != null) {
90
+ MovieHeaderBox mhb = mbox.getMovieHeaderBox();
91
+ if (mhb != null) {
92
+ creationTime = mhb.getCreationTime();
93
+ }
94
+ }
95
+ return creationTime;
96
+ }
97
+}
java/org.mp4parser.isoparser/src/com/sap/sailing/media/mp4/MP4ParserFakeFile.java
... ...
@@ -0,0 +1,169 @@
1
+package com.sap.sailing.media.mp4;
2
+
3
+import java.io.File;
4
+import java.io.IOException;
5
+import java.nio.ByteBuffer;
6
+import java.nio.MappedByteBuffer;
7
+import java.nio.channels.FileChannel;
8
+import java.nio.channels.FileLock;
9
+import java.nio.channels.ReadableByteChannel;
10
+import java.nio.channels.SeekableByteChannel;
11
+import java.nio.channels.WritableByteChannel;
12
+import java.nio.file.Files;
13
+
14
+/**
15
+ * This class pretends to be a complete larger file, while only carrying the first 10 and last 10 megabytes. It is
16
+ * specifically meant to be used for the mp4Parser and only supports the required subset for it. It extends FileChannel
17
+ * as the library uses that to skip input for skipped boxes
18
+ */
19
+public class MP4ParserFakeFile extends FileChannel {
20
+ private final long totalSizeOfFile;
21
+ private long currentPos = 0;
22
+ private byte[] startOfFileByteArray;
23
+ private byte[] endOfFileByteArray;
24
+
25
+ public MP4ParserFakeFile(byte[] start, byte[] end, Long skipped) {
26
+ this.startOfFileByteArray = start;
27
+ this.endOfFileByteArray = end;
28
+ this.totalSizeOfFile = start.length + end.length + skipped;
29
+ }
30
+
31
+ public MP4ParserFakeFile(File tmp) {
32
+ try (SeekableByteChannel inChannel = Files.newByteChannel(tmp.toPath())) {
33
+ totalSizeOfFile = tmp.length();
34
+ if (totalSizeOfFile < MP4MediaParser.REQUIRED_SIZE_IN_BYTES * 2) {
35
+ throw new IllegalStateException(
36
+ "The file is to small to be analysed < " + (MP4MediaParser.REQUIRED_SIZE_IN_BYTES * 2));
37
+ } else {
38
+ startOfFileByteArray = new byte[MP4MediaParser.REQUIRED_SIZE_IN_BYTES];
39
+ endOfFileByteArray = new byte[MP4MediaParser.REQUIRED_SIZE_IN_BYTES];
40
+ ByteBuffer startOfFileByteBuffer = ByteBuffer.wrap(startOfFileByteArray);
41
+ ByteBuffer endOfFileByteBuffer = ByteBuffer.wrap(endOfFileByteArray);
42
+ int startRead = inChannel.read(startOfFileByteBuffer);
43
+ if (startRead != startOfFileByteBuffer.capacity()) {
44
+ throw new IllegalArgumentException("Could not read fileStart");
45
+ }
46
+ inChannel.position(totalSizeOfFile - (MP4MediaParser.REQUIRED_SIZE_IN_BYTES + 1));
47
+ int endRead = inChannel.read(endOfFileByteBuffer);
48
+ if (endRead != endOfFileByteBuffer.capacity()) {
49
+ throw new IllegalArgumentException("Could not read fileStart");
50
+ }
51
+ }
52
+ } catch (Exception e) {
53
+ // File system error? Re-throwing, cause it cannot be handled here.
54
+ throw new RuntimeException(e);
55
+ }
56
+ }
57
+
58
+ @Override
59
+ public int read(ByteBuffer dst) throws IOException {
60
+ // Ignore boundary crossings into nulled part, as it cannot be parsed in that case.
61
+ // Only the actual movie data should be nulled.
62
+ int read = 0;
63
+ int toRead = dst.remaining();
64
+ final long startOfEndSegment = totalSizeOfFile - (endOfFileByteArray.length + 1);
65
+ if (currentPos < startOfFileByteArray.length) {
66
+ // in start buffer, read one byte
67
+ dst.put(startOfFileByteArray, (int) currentPos, toRead);
68
+ read = toRead;
69
+ } else if (currentPos < startOfEndSegment) {
70
+ throw new IllegalArgumentException(
71
+ "The nulled part should only be in the skipped boxes, else reading would be corrupt");
72
+ } else {
73
+ int remaining = (int) (totalSizeOfFile - currentPos);
74
+ int relative = endOfFileByteArray.length - remaining;
75
+ if (currentPos + toRead > totalSizeOfFile) {
76
+ toRead = (int) (totalSizeOfFile - currentPos);
77
+ }
78
+ if (toRead > 0) {
79
+ dst.put(endOfFileByteArray, relative, toRead);
80
+ read = toRead;
81
+ } else {
82
+ read = -1;
83
+ }
84
+ }
85
+ currentPos += read;
86
+ return read;
87
+ }
88
+
89
+ @Override
90
+ public int write(ByteBuffer src) throws IOException {
91
+ throw new IllegalArgumentException("MP4 files should only be read");
92
+ }
93
+
94
+ @Override
95
+ public long read(ByteBuffer[] dsts, int offset, int length) throws IOException {
96
+ throw new IllegalArgumentException("Not supported");
97
+ }
98
+
99
+ @Override
100
+ public long write(ByteBuffer[] srcs, int offset, int length) throws IOException {
101
+ throw new IllegalArgumentException("Not supported");
102
+ }
103
+
104
+ @Override
105
+ public long position() throws IOException {
106
+ return currentPos;
107
+ }
108
+
109
+ @Override
110
+ public FileChannel position(long newPosition) throws IOException {
111
+ currentPos = newPosition;
112
+ return this;
113
+ }
114
+
115
+ @Override
116
+ public long size() throws IOException {
117
+ throw new IllegalArgumentException("Not supported");
118
+ }
119
+
120
+ @Override
121
+ public FileChannel truncate(long size) throws IOException {
122
+ throw new IllegalArgumentException("Not supported");
123
+ }
124
+
125
+ @Override
126
+ public void force(boolean metaData) throws IOException {
127
+ throw new IllegalArgumentException("Not supported");
128
+ }
129
+
130
+ @Override
131
+ public long transferTo(long position, long count, WritableByteChannel target) throws IOException {
132
+ throw new IllegalArgumentException("Not supported");
133
+ }
134
+
135
+ @Override
136
+ public long transferFrom(ReadableByteChannel src, long position, long count) throws IOException {
137
+ throw new IllegalArgumentException("Not supported");
138
+ }
139
+
140
+ @Override
141
+ public int read(ByteBuffer dst, long position) throws IOException {
142
+ throw new IllegalArgumentException("Not supported");
143
+ }
144
+
145
+ @Override
146
+ public int write(ByteBuffer src, long position) throws IOException {
147
+ throw new IllegalArgumentException("Not supported");
148
+ }
149
+
150
+ @Override
151
+ public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException {
152
+ throw new IllegalArgumentException("Not supported");
153
+ }
154
+
155
+ @Override
156
+ public FileLock lock(long position, long size, boolean shared) throws IOException {
157
+ throw new IllegalArgumentException("Not supported");
158
+ }
159
+
160
+ @Override
161
+ public FileLock tryLock(long position, long size, boolean shared) throws IOException {
162
+ throw new IllegalArgumentException("Not supported");
163
+ }
164
+
165
+ @Override
166
+ protected void implCloseChannel() throws IOException {
167
+ }
168
+
169
+}
java/org.mp4parser.isoparser/src/com/sap/sailing/media/mp4/MediaFileParser.java
... ...
@@ -0,0 +1,36 @@
1
+package com.sap.sailing.media.mp4;
2
+
3
+import java.io.File;
4
+import java.io.IOException;
5
+import java.net.MalformedURLException;
6
+import java.nio.file.Files;
7
+import java.util.Date;
8
+
9
+import javax.xml.parsers.ParserConfigurationException;
10
+
11
+import org.mp4parser.AbstractBoxParser;
12
+import org.mp4parser.IsoFile;
13
+import org.mp4parser.PropertyBoxParserImpl;
14
+import org.xml.sax.SAXException;
15
+
16
+public class MediaFileParser {
17
+ public static void main(String[] args)
18
+ throws MalformedURLException, ParserConfigurationException, SAXException, IOException {
19
+ new MediaFileParser().checkMetadata(new File(args[0]));
20
+ }
21
+
22
+ public void checkMetadata(File file) throws IOException, ParserConfigurationException, SAXException {
23
+ boolean spherical = false;
24
+ long durationInMillis = -1;
25
+ Date recordStartedTimer = null;
26
+ AbstractBoxParser boxParserImpl = new PropertyBoxParserImpl();
27
+ boxParserImpl.skippingBoxes(new String[] { "mdat" });
28
+ try (IsoFile isof = new IsoFile(Files.newByteChannel(file.toPath()), boxParserImpl)) {
29
+ recordStartedTimer = MP4MediaParser.determineRecordingStart(isof);
30
+ spherical = MP4MediaParser.determine360(isof);
31
+ durationInMillis = MP4MediaParser.determineDurationInMillis(isof);
32
+ System.out.println("Start: " + recordStartedTimer.toString() + ", duration: " + durationInMillis
33
+ + "ms; spherical: " + spherical);
34
+ }
35
+ }
36
+}
wiki/images/aws_automation/activity_diagram.png
... ...
Binary files /dev/null and b/wiki/images/aws_automation/activity_diagram.png differ
wiki/images/aws_automation/alb_overview.png
... ...
Binary files /dev/null and b/wiki/images/aws_automation/alb_overview.png differ
wiki/images/aws_automation/sequence_diagram_1.png
... ...
Binary files /dev/null and b/wiki/images/aws_automation/sequence_diagram_1.png differ
wiki/images/aws_automation/sequence_diagram_2.png
... ...
Binary files /dev/null and b/wiki/images/aws_automation/sequence_diagram_2.png differ
wiki/info/landscape/aws-automation.md
... ...
@@ -45,7 +45,7 @@ SERVER_STARTUP_NOTIFY=leon.radeck@sap.com
45 45
46 46
### 2. SAP instance configuration
47 47
48
-<img style="float: right" src="https://photos-1.dropbox.com/t/2/AABX17gY1wPcAmgxTjIUQju9wMnxgRFUa4OgOfUcpQPvkA/12/379951689/png/32x32/1/_/1/2/image1.PNG/EJmPwIIDGL0ZIAcoBw/vkl8xdvmZzFhJzi9lV_d78jD6jKNURBkF7C-XX2xVUk?preserve_transparency=1&size=1600x1200&size_mode=3" />
48
+<img style="float: right" src="https://wiki.sapsailing.com/wiki/images/aws_automation/activity_diagram.png" />
49 49
50 50
Necessary configuration steps:
51 51
... ...
@@ -78,7 +78,7 @@ To reach the SAP instance by a specific URL (e.g. wcsantander2017.sapsailing.com
78 78
- Configure the health check of the target group
79 79
- Register instance within the target group
80 80
81
-<img style="float: right" src="https://photos-4.dropbox.com/t/2/AAAYdNOq2vlgkb-kUcQjkKOAxeJI2HoYvvl1El8_gUwHbA/12/379951689/png/32x32/1/_/1/2/image2.PNG/EJmPwIIDGL0ZIAcoBw/qm7Nd0NMACr7EJ67c_7xK6seOn-4raimfvpE_07Pkys?preserve_transparency=1&size=1600x1200&size_mode=3" />
81
+<img style="float: right" src="https://wiki.sapsailing.com/wiki/images/aws_automation/alb_overview.png" />
82 82
83 83
Translation: Way of a HTTP/HTTPS request from the load balancer to the sap instance (simplified)
84 84
... ...
@@ -249,7 +249,7 @@ The script is built for the processing of parameters but the functionality is cu
249 249
250 250
User input flow example:
251 251
252
-<img style="float: right" src="https://photos-4.dropbox.com/t/2/AAA8lXNig-MSsQhRmhyIYOD1sgi59kfQr8NDqs5bPRenQA/12/379951689/png/32x32/1/_/1/2/image3.PNG/EJmPwIIDGL0ZIAcoBw/fF8VkVypSPKmR_NttUndnjGFJKkEk_ILuOF19hHAVbg?preserve_transparency=1&size=1600x1200&size_mode=3" />
252
+<img style="float: right" src="https://wiki.sapsailing.com/wiki/images/aws_automation/sequence_diagram_1.png" />
253 253
254 254
Translation comment (1):
255 255
... ...
@@ -262,7 +262,7 @@ The user input is assigned to the global variable region.
262 262
263 263
If the input variable is not a text but a type of resource from AWS (e.g. load balancer) the following mechanism will take effect:
264 264
265
-<img style="float: right" src="https://photos-6.dropbox.com/t/2/AADRrPHFrAKtSKzuWZTPrcZbfuW_vB0tUp8nan8iJ0Zzcg/12/379951689/png/32x32/1/_/1/2/image4.PNG/EJmPwIIDGL0ZIAcoBw/LMRyG1oabKBs0d_vv7rPwDpTEY4lzGIHqBfV2rNU5hY?preserve_transparency=1&size=1600x1200&size_mode=3" />
265
+<img style="float: right" src="https://wiki.sapsailing.com/wiki/images/aws_automation/sequence_diagram_2.png" />
266 266
267 267
Translation comment (1):
268 268
... ...
@@ -356,7 +356,7 @@ The user can assign values to that variable that are then used as default propos
356 356
357 357
1. Start EC2 instance
358 358
2. Query for its dns name for later ssh connection
359
-3. WWait until ssh connection is established
359
+3. Wait until ssh connection is established
360 360
4. Create event and change admin password if necessary
361 361
5. Create launch template for replica with user data variable "REPLICATION_CHANNEL" matching to its master
362 362