mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-11-10 21:02:40 +00:00
Merge branch 'master' of ssh://sapsailing.com/home/trac/git into bug4104
# Conflicts: # java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java
This commit is contained in:
1 parent
0dd4cd75b5
commit
e4ea366a1e
47 files changed
+722
-380
No files matched your search
+1
@@ -80,6 +80,7 @@ RankGainsOrLossesBetweenFirstMarkAndFinish=Rank Gains or Losses (1st Mark -> Fin
|
||||
IsTracked=Is Tracked
|
||||
NumberOfCompetitorFixes=Number of Competitor Fixes
|
||||
NumberOfMarkFixes=Number of Mark Fixes
|
||||
NumberOfWindFixes=Number of Wind Fixes
|
||||
ManeuverType=Maneuver Type
|
||||
Tack=Tack
|
||||
WindSpeedVsManeuverLoss=Wind Speed in Knots (X) vs. Maneuver Loss in Meters (Y)
|
||||
|
||||
+1
@@ -79,6 +79,7 @@ RankGainsOrLossesBetweenFirstMarkAndFinish=Positions-Verluste oder -Gewinne (1te
|
||||
IsTracked=Ist getracked
|
||||
NumberOfCompetitorFixes=Anzahl der Teilnehmer-Fixes
|
||||
NumberOfMarkFixes=Anzahl der Tonnen-Fixes
|
||||
NumberOfWindFixes=Anzahl der Windmessungen
|
||||
ManeuverType=Art des Manövers
|
||||
Tack=Bug
|
||||
SpeedBefore=Geschwindigkeit bevor dem Manöver
|
||||
|
||||
+3
@@ -56,6 +56,9 @@ public interface HasTrackedRaceContext {
|
||||
@Statistic(messageKey="NumberOfMarkFixes", resultDecimals=0, ordinal=2)
|
||||
public int getNumberOfMarkFixes();
|
||||
|
||||
@Statistic(messageKey="NumberOfWindFixes", resultDecimals=0, ordinal=1)
|
||||
public int getNumberOfWindFixes();
|
||||
|
||||
// Convenience methods for race dependent calculation to avoid code duplication
|
||||
public Double getRelativeScoreForCompetitor(Competitor competitor);
|
||||
|
||||
|
||||
+28
-23
@@ -1,21 +1,23 @@
|
||||
package com.sap.sailing.datamining.impl.data;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import com.sap.sailing.datamining.data.HasLeaderboardContext;
|
||||
import com.sap.sailing.datamining.data.HasTrackedRaceContext;
|
||||
import com.sap.sailing.domain.base.Competitor;
|
||||
import com.sap.sailing.domain.base.CourseArea;
|
||||
import com.sap.sailing.domain.base.Fleet;
|
||||
import com.sap.sailing.domain.base.Mark;
|
||||
import com.sap.sailing.domain.base.RaceColumn;
|
||||
import com.sap.sailing.domain.base.RaceDefinition;
|
||||
import com.sap.sailing.domain.base.Regatta;
|
||||
import com.sap.sailing.domain.common.NauticalSide;
|
||||
import com.sap.sailing.domain.common.tracking.GPSFix;
|
||||
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
|
||||
import com.sap.sailing.domain.tracking.GPSFixTrack;
|
||||
import com.sap.sailing.domain.common.WindSource;
|
||||
import com.sap.sailing.domain.common.WindSourceType;
|
||||
import com.sap.sailing.domain.tracking.LineDetails;
|
||||
import com.sap.sailing.domain.tracking.Track;
|
||||
import com.sap.sailing.domain.tracking.TrackedRace;
|
||||
import com.sap.sse.common.Duration;
|
||||
import com.sap.sse.common.TimePoint;
|
||||
@@ -136,26 +138,10 @@ public class TrackedRaceWithContext implements HasTrackedRaceContext {
|
||||
return duration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumberOfCompetitorFixes() {
|
||||
private <T> int getNumberOfRawFixes(Iterable<T> tracksFor, BiFunction<T, TrackedRace, Track<?>> trackProvider) {
|
||||
int number = 0;
|
||||
for (Competitor competitor : getRace().getCompetitors()) {
|
||||
GPSFixTrack<Competitor, GPSFixMoving> track = getTrackedRace().getTrack(competitor);
|
||||
track.lockForRead();
|
||||
try {
|
||||
number += Util.size(track.getRawFixes());
|
||||
} finally {
|
||||
track.unlockAfterRead();
|
||||
}
|
||||
}
|
||||
return number;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumberOfMarkFixes() {
|
||||
int number = 0;
|
||||
for (Mark mark : getTrackedRace().getMarks()) {
|
||||
GPSFixTrack<Mark, GPSFix> track = getTrackedRace().getTrack(mark);
|
||||
for (T trackedObject : tracksFor) {
|
||||
Track<?> track = trackProvider.apply(trackedObject, getTrackedRace());
|
||||
track.lockForRead();
|
||||
try {
|
||||
number += Util.size(track.getRawFixes());
|
||||
@@ -166,6 +152,25 @@ public class TrackedRaceWithContext implements HasTrackedRaceContext {
|
||||
return number;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumberOfCompetitorFixes() {
|
||||
return getNumberOfRawFixes(getRace().getCompetitors(), (competitor, trackedRace)->trackedRace.getTrack(competitor));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumberOfMarkFixes() {
|
||||
return getNumberOfRawFixes(getTrackedRace().getMarks(), (mark, trackedRace)->trackedRace.getTrack(mark));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumberOfWindFixes() {
|
||||
final Iterable<WindSource> windSources = getTrackedRace().getWindSources();
|
||||
final Iterable<WindSource> windSourcesToExclude = getTrackedRace().getWindSourcesToExclude();
|
||||
final Iterable<WindSource> windSourcesToUse = StreamSupport.stream(windSources.spliterator(), /* parallel */ false).filter(ws->!Util.contains(windSourcesToExclude, ws)).
|
||||
filter(ws->ws.getType()!=WindSourceType.TRACK_BASED_ESTIMATION).collect(Collectors.toList());
|
||||
return getNumberOfRawFixes(windSourcesToUse, (windSource, trackedRace)->trackedRace.getOrCreateWindTrack(windSource));
|
||||
}
|
||||
|
||||
// Convenience methods for race dependent calculation to avoid code duplication
|
||||
public Double getRelativeScoreForCompetitor(Competitor competitor) {
|
||||
final TimePoint now = MillisecondsTimePoint.now();
|
||||
|
||||
+1
-1
@@ -420,7 +420,7 @@ public class DomainFactoryImpl implements DomainFactory {
|
||||
@Override
|
||||
public void addUpdateHandlers(String updateURL, String username, String password, Serializable eventId,
|
||||
RaceDefinition raceDefinition, DynamicTrackedRace trackedRace) throws URISyntaxException {
|
||||
final URI updateURI = new URI(updateURL);
|
||||
final URI updateURI = updateURL == null ? null : new URI(updateURL);
|
||||
CourseDesignUpdateHandler courseDesignHandler = new CourseDesignUpdateHandler(
|
||||
updateURI, username, password, eventId, raceDefinition.getId());
|
||||
StartTimeUpdateHandler startTimeHandler = new StartTimeUpdateHandler(
|
||||
|
||||
@@ -158,6 +158,6 @@
|
||||
id="org.mp4parser.isoparser"
|
||||
download-size="0"
|
||||
install-size="0"
|
||||
version="1.9.31"/>
|
||||
version="1.9.37"/>
|
||||
|
||||
</feature>
|
||||
@@ -57,7 +57,7 @@ Require-Bundle: com.sap.sailing.domain,
|
||||
com.sap.sailing.competitorimport,
|
||||
com.sap.sailing.expeditionconnector,
|
||||
com.sap.sailing.expeditionconnector.common,
|
||||
org.mp4parser.isoparser;bundle-version="1.9.31",
|
||||
org.mp4parser.isoparser;bundle-version="1.9.37",
|
||||
com.sap.sse.datamining.ui
|
||||
Bundle-Activator: com.sap.sailing.gwt.ui.server.Activator
|
||||
Bundle-ActivationPolicy: lazy
|
||||
|
||||
+12
-1
@@ -1194,6 +1194,14 @@ raceIsInLiveTimePanelMode=La carrera es en directo
|
||||
backToLiveTimePanelMode=Volver a directo
|
||||
windChart=Gráfico de vientos
|
||||
windChartLoading=Cargando datos de vientos...
|
||||
mediaNoVideosCaption=No hay rastreos de medios
|
||||
mediaShowVideoCaption=Mostrar rasteo de medios
|
||||
mediaShowVideoTooltip=Haga clic para mostrar rastreo de medios: {0}
|
||||
mediaHideVideoCaption=Ocultar rastreo de medios
|
||||
mediaHideVideoTooltip=Haga clic para ocultar rastreo de medios
|
||||
mediaSelectVideoCaption={0} rastreos de medios
|
||||
mediaSelectVideoTooltip=Haga clic para seleccionar rastreo de medios
|
||||
mediaManageMediaCaption=Gestionar medios
|
||||
mediaManageMediaTooltip=Configurar clips de audio y vídeo, sincronizar tiempos
|
||||
showAll=Mostrar todo
|
||||
raceVisibilityColumn=Visibilidad
|
||||
@@ -1416,7 +1424,8 @@ numberOfRacesShort=Nº regatas
|
||||
courseGeometry=Geometría de la regata
|
||||
degreesToWind={0}° respecto al viento
|
||||
miniLeeaderboardLegendText=Muestra los puntos y el número de carreras por competidor
|
||||
currentOfTotal={0,number}/{1,number}
|
||||
currentOfTotalRaces={0,number}/{1,number} carreras
|
||||
currentOfTotalRaces[one]={0,number}/{1,number} carrera
|
||||
noFinishedRaces=Todavía no ha finalizado ninguna carrera
|
||||
racesOverview=Resumen de carreras
|
||||
listFormatLabel=Formato de lista
|
||||
@@ -2075,6 +2084,8 @@ manage2SailEventIdBox=ID de evento Manage2Sail
|
||||
manage2SailEventURLBox=URL de evento Manage2Sail (json)
|
||||
manage2SailEventIdBoxTooltip=ID de evento o URL que contiene el ID de evento de Manage2Sail
|
||||
manage2SailPort=Puerto
|
||||
audioFiles=Pistas de audio
|
||||
selectMedia=Seleccionar medio
|
||||
swissTimingUpdateURL=Actualizar URL (por ejemplo, para la hora de salida y el feedback sobre la modificación del rumbo)
|
||||
swissTimingUpdateUsername=Nombre de usuario para enviar actualizaciones
|
||||
swissTimingUpdatePassword=Contraseña para enviar actualizaciones
|
||||
+12
-1
@@ -1194,6 +1194,14 @@ raceIsInLiveTimePanelMode=La course est en direct.
|
||||
backToLiveTimePanelMode=Retour au direct
|
||||
windChart=Graphique du vent
|
||||
windChartLoading=Chargement des données de vent...
|
||||
mediaNoVideosCaption=Aucune piste média
|
||||
mediaShowVideoCaption=Afficher piste média
|
||||
mediaShowVideoTooltip=Cliquer pour afficher la piste média : {0}
|
||||
mediaHideVideoCaption=Masquer piste média
|
||||
mediaHideVideoTooltip=Cliquer pour masquer la piste média
|
||||
mediaSelectVideoCaption={0} pistes médias
|
||||
mediaSelectVideoTooltip=Cliquer pour sélectionner la piste média
|
||||
mediaManageMediaCaption=Gestion des médias
|
||||
mediaManageMediaTooltip=Configurer des clips audio et vidéo, synchroniser durées
|
||||
showAll=Afficher tout
|
||||
raceVisibilityColumn=Visibilité
|
||||
@@ -1416,7 +1424,8 @@ numberOfRacesShort=Nombre de courses
|
||||
courseGeometry=Géométrie du parcours
|
||||
degreesToWind={0}° au vent
|
||||
miniLeeaderboardLegendText=Affiche les points associés au nombre de courses par concurrent
|
||||
currentOfTotal={0,number}/{1,number}
|
||||
currentOfTotalRaces={0,number}/{1,number} courses
|
||||
currentOfTotalRaces[one]={0,number}/{1,number} course
|
||||
noFinishedRaces=Aucune course terminée pour l''instant.
|
||||
racesOverview=Synthèse des courses
|
||||
listFormatLabel=Format de liste
|
||||
@@ -2075,6 +2084,8 @@ manage2SailEventIdBox=ID de manifestation Manage2Sail
|
||||
manage2SailEventURLBox=URL de la manifestation Manage2Sail (json)
|
||||
manage2SailEventIdBoxTooltip=ID d''événement ou URL contenant l''ID d''événement issu de Manage2Sail
|
||||
manage2SailPort=Bâbord
|
||||
audioFiles=Pistes audio
|
||||
selectMedia=Sélectionner média
|
||||
swissTimingUpdateURL=Mettre à jour URL (par exemple pour l''heure de début ou pour les commentaires sur le changement de trajectoire)
|
||||
swissTimingUpdateUsername=Nom d''utilisateur pour l''envoi des mises à jour
|
||||
swissTimingUpdatePassword=Mot de passe pour l''envoi des mises à jour
|
||||
+12
-1
@@ -1194,6 +1194,14 @@ raceIsInLiveTimePanelMode=レースがライブ中
|
||||
backToLiveTimePanelMode=ライブに戻る
|
||||
windChart=風チャート
|
||||
windChartLoading=風データをロード中...
|
||||
mediaNoVideosCaption=メディアトラックなし
|
||||
mediaShowVideoCaption=メディアトラック表示
|
||||
mediaShowVideoTooltip=クリックしてメディアトラックを表示: {0}
|
||||
mediaHideVideoCaption=メディアトラック非表示
|
||||
mediaHideVideoTooltip=クリックしてメディアトラックを非表示
|
||||
mediaSelectVideoCaption={0} メディアトラック
|
||||
mediaSelectVideoTooltip=クリックしてメディアトラックを選択
|
||||
mediaManageMediaCaption=メディア管理
|
||||
mediaManageMediaTooltip=音声および動画クリックを設定して、時間を同期
|
||||
showAll=全表示
|
||||
raceVisibilityColumn=可視性
|
||||
@@ -1416,7 +1424,8 @@ numberOfRacesShort=# レース
|
||||
courseGeometry=コース配置
|
||||
degreesToWind=風と {0}°
|
||||
miniLeeaderboardLegendText=競技者別にレース数とともに得点を表示
|
||||
currentOfTotal={0,number}/{1,number}
|
||||
currentOfTotalRaces={0,number}/{1,number} レース
|
||||
currentOfTotalRaces[one]={0,number}/{1,number} レース
|
||||
noFinishedRaces=フィニッシュしたレースがまだありません。
|
||||
racesOverview=レース概要
|
||||
listFormatLabel=レース一覧
|
||||
@@ -2075,6 +2084,8 @@ manage2SailEventIdBox=Manage2Sail イベント ID
|
||||
manage2SailEventURLBox=Manage2Sail イベント URL (JSON)
|
||||
manage2SailEventIdBoxTooltip=イベント ID または Manage2Sail からのイベント ID が入っている URL
|
||||
manage2SailPort=ポート
|
||||
audioFiles=音声トラック
|
||||
selectMedia=メディア選択
|
||||
swissTimingUpdateURL=URL の更新 (スタート時刻やコース変更のフィードバックなど)
|
||||
swissTimingUpdateUsername=更新送信のユーザ名
|
||||
swissTimingUpdatePassword=更新送信のパスワード
|
||||
+12
-1
@@ -1194,6 +1194,14 @@ raceIsInLiveTimePanelMode=Corrida ao vivo
|
||||
backToLiveTimePanelMode=Voltar ao tempo real
|
||||
windChart=Carta de ventos
|
||||
windChartLoading=Carregando dados do vento...
|
||||
mediaNoVideosCaption=Nenhuma faixa de mídia
|
||||
mediaShowVideoCaption=Exibir faixa de mídia
|
||||
mediaShowVideoTooltip=Clicar para exibir faixa de mídia: {0}
|
||||
mediaHideVideoCaption=Ocultar faixa de mídia
|
||||
mediaHideVideoTooltip=Clicar para ocultar faixa de mídia
|
||||
mediaSelectVideoCaption={0} faixas de mídia
|
||||
mediaSelectVideoTooltip=Clicar para selecionar faixa de mídia
|
||||
mediaManageMediaCaption=Administrar mídia
|
||||
mediaManageMediaTooltip=Configurar clipes de áudio e vídeo, sincronizar horas
|
||||
showAll=Visualizar tudo
|
||||
raceVisibilityColumn=Visibilidade
|
||||
@@ -1416,7 +1424,8 @@ numberOfRacesShort=Nº de corridas
|
||||
courseGeometry=Geometria do percurso
|
||||
degreesToWind={0}° para vento
|
||||
miniLeeaderboardLegendText=Exibe os pontos juntamente com o número de corridas por competidor
|
||||
currentOfTotal={0,number}/{1,number}
|
||||
currentOfTotalRaces={0,number}/{1,number} corridas
|
||||
currentOfTotalRaces[one]={0,number}/{1,number} corrida
|
||||
noFinishedRaces=Ainda não existem corridas concluídas.
|
||||
racesOverview=Síntese de corridas
|
||||
listFormatLabel=Formato de lista
|
||||
@@ -2075,6 +2084,8 @@ manage2SailEventIdBox=ID de evento Manage2Sail
|
||||
manage2SailEventURLBox=URL de evento Manage2Sail (json)
|
||||
manage2SailEventIdBoxTooltip=ID de evento ou um URL que contenha o ID de evento do Manage2Sail
|
||||
manage2SailPort=Porta
|
||||
audioFiles=Faixas de áudio
|
||||
selectMedia=Selecionar mídia
|
||||
swissTimingUpdateURL=Atualizar URL (por exemplo, para hora de partida e feedback de mudança de percurso)
|
||||
swissTimingUpdateUsername=Nome de usuário para enviar atualizações
|
||||
swissTimingUpdatePassword=Senha para enviar atualizações
|
||||
+12
-1
@@ -1194,6 +1194,14 @@ raceIsInLiveTimePanelMode=Гонка активна
|
||||
backToLiveTimePanelMode=Вернуться в оперативный режим
|
||||
windChart=Диаграмма ветра
|
||||
windChartLoading=Загрузка данных о ветре...
|
||||
mediaNoVideosCaption=Нет объектов мультимедиа
|
||||
mediaShowVideoCaption=Показать объект мультимедиа
|
||||
mediaShowVideoTooltip=Щелкните, чтобы показать объект мультимедиа: {0}
|
||||
mediaHideVideoCaption=Скрыть объект мультимедиа
|
||||
mediaHideVideoTooltip=Щелкните, чтобы скрыть объект мультимедиа
|
||||
mediaSelectVideoCaption=Объекты мультимедиа: {0}
|
||||
mediaSelectVideoTooltip=Щелкните, чтобы выбрать объект мультимедиа
|
||||
mediaManageMediaCaption=Управление мультимедиа
|
||||
mediaManageMediaTooltip=Настройка аудио- и видеоклипов, синхронизация времени
|
||||
showAll=Показать все
|
||||
raceVisibilityColumn=Видимость
|
||||
@@ -1416,7 +1424,8 @@ numberOfRacesShort=Гонки
|
||||
courseGeometry=Геометрия курса
|
||||
degreesToWind={0}° к ветру
|
||||
miniLeeaderboardLegendText=Показывает баллы и число гонок каждого участника
|
||||
currentOfTotal={0,number}/{1,number}
|
||||
currentOfTotalRaces={0,number}/{1,number} гонок
|
||||
currentOfTotalRaces[one]={0,number}/{1,number} гонка
|
||||
noFinishedRaces=Завершенных гонок еще нет
|
||||
racesOverview=Обзор гонок
|
||||
listFormatLabel=Формат списка
|
||||
@@ -2075,6 +2084,8 @@ manage2SailEventIdBox=Ид. события Manage2Sail
|
||||
manage2SailEventURLBox=URL события Manage2Sail (json)
|
||||
manage2SailEventIdBoxTooltip=Ид. события или URL, содержащий ид. события из Manage2Sail
|
||||
manage2SailPort=Порт
|
||||
audioFiles=Аудиообъект
|
||||
selectMedia=Выбрать мультимедиа
|
||||
swissTimingUpdateURL=Обновить URL (например, для времени начала и отзыва об изменении курса)
|
||||
swissTimingUpdateUsername=Имя пользователя для отправки обновлений
|
||||
swissTimingUpdatePassword=Пароль для отправки обновлений
|
||||
+12
-1
@@ -1194,6 +1194,14 @@ raceIsInLiveTimePanelMode=正在直播比赛轮次
|
||||
backToLiveTimePanelMode=返回实况
|
||||
windChart=风力记录
|
||||
windChartLoading=正在加载风力数据...
|
||||
mediaNoVideosCaption=无媒体轨道
|
||||
mediaShowVideoCaption=显示媒体轨道
|
||||
mediaShowVideoTooltip=点击显示媒体轨道:{0}
|
||||
mediaHideVideoCaption=隐藏媒体轨道
|
||||
mediaHideVideoTooltip=点击隐藏媒体轨道
|
||||
mediaSelectVideoCaption={0}个媒体轨道
|
||||
mediaSelectVideoTooltip=点击选择媒体轨道
|
||||
mediaManageMediaCaption=管理媒体
|
||||
mediaManageMediaTooltip=配置音频和视频短片,同步时间
|
||||
showAll=显示全部
|
||||
raceVisibilityColumn=可见性
|
||||
@@ -1416,7 +1424,8 @@ numberOfRacesShort=比赛轮次数
|
||||
courseGeometry=场地几何结构
|
||||
degreesToWind={0}°(相对于风)
|
||||
miniLeeaderboardLegendText=显示分数以及各参赛队的比赛轮次数
|
||||
currentOfTotal={0,number}/{1,number}
|
||||
currentOfTotalRaces={0,number}/{1,number} 个比赛轮次
|
||||
currentOfTotalRaces[one]={0,number}/{1,number} 个比赛轮次
|
||||
noFinishedRaces=没有结束的比赛轮次
|
||||
racesOverview=比赛轮次总览
|
||||
listFormatLabel=清单格式
|
||||
@@ -2075,6 +2084,8 @@ manage2SailEventIdBox=Manage2Sail 活动编号
|
||||
manage2SailEventURLBox=Manage2Sail 活动 URL (json)
|
||||
manage2SailEventIdBoxTooltip=包含 Manage2Sail 中活动编号的活动编号或 URL
|
||||
manage2SailPort=左舷
|
||||
audioFiles=音频轨道
|
||||
selectMedia=选择媒体
|
||||
swissTimingUpdateURL=更新 URL(例如开始时间和航向改变反馈)
|
||||
swissTimingUpdateUsername=用于发送更新的用户名
|
||||
swissTimingUpdatePassword=用于发送更新的密码
|
||||
+7
-3
@@ -56,6 +56,7 @@ public class BoatOverlay extends CanvasOverlayV3 {
|
||||
private Integer lastHeight;
|
||||
private Size lastScale;
|
||||
private Color lastColor;
|
||||
private DisplayMode lastDisplayMode;
|
||||
|
||||
public static enum DisplayMode { DEFAULT, SELECTED, NOT_SELECTED };
|
||||
private DisplayMode displayMode;
|
||||
@@ -93,7 +94,8 @@ public class BoatOverlay extends CanvasOverlayV3 {
|
||||
if (lastWidth == null || canvasWidth != lastWidth || lastHeight == null || canvasHeight != lastHeight) {
|
||||
setCanvasSize(canvasWidth, canvasHeight);
|
||||
}
|
||||
if (needToDraw(boatFix.legType, boatFix.tack, isSelected(), canvasWidth, canvasHeight, boatSizeScaleFactor, color)) {
|
||||
if (needToDraw(boatFix.legType, boatFix.tack, isSelected(), canvasWidth, canvasHeight, boatSizeScaleFactor,
|
||||
color, displayMode)) {
|
||||
boatVectorGraphics.drawBoatToCanvas(getCanvas().getContext2d(), boatFix.legType, boatFix.tack, getDisplayMode(),
|
||||
canvasWidth, canvasHeight, boatSizeScaleFactor, color);
|
||||
lastLegType = boatFix.legType;
|
||||
@@ -103,6 +105,7 @@ public class BoatOverlay extends CanvasOverlayV3 {
|
||||
lastHeight = canvasHeight;
|
||||
lastScale = boatSizeScaleFactor;
|
||||
lastColor = color;
|
||||
lastDisplayMode = displayMode;
|
||||
}
|
||||
LatLng latLngPosition = coordinateSystem.toLatLng(boatFix.position);
|
||||
Point boatPositionInPx = mapProjection.fromLatLngToDivPixel(latLngPosition);
|
||||
@@ -123,11 +126,12 @@ public class BoatOverlay extends CanvasOverlayV3 {
|
||||
* changed, the result is <code>true</code>.
|
||||
*/
|
||||
private boolean needToDraw(LegType legType, Tack tack, boolean isSelected, double width, double height,
|
||||
Size scaleFactor, Color color) {
|
||||
Size scaleFactor, Color color, DisplayMode displayMode) {
|
||||
return lastLegType == null || lastLegType != legType || lastTack == null || lastTack != tack
|
||||
|| lastSelected == null || lastSelected != isSelected || lastWidth == null || lastWidth != width
|
||||
|| lastHeight == null || lastHeight != height || lastScale == null || !lastScale.equals(scaleFactor)
|
||||
|| lastColor == null || !lastColor.equals(color);
|
||||
|| lastColor == null || !lastColor.equals(color) || lastDisplayMode == null
|
||||
|| !lastDisplayMode.equals(displayMode);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+84
-52
@@ -70,7 +70,6 @@ import com.google.gwt.user.client.ui.Label;
|
||||
import com.google.gwt.user.client.ui.RequiresResize;
|
||||
import com.google.gwt.user.client.ui.VerticalPanel;
|
||||
import com.google.gwt.user.client.ui.Widget;
|
||||
import com.sap.sailing.domain.common.Bounds;
|
||||
import com.sap.sailing.domain.common.ManeuverType;
|
||||
import com.sap.sailing.domain.common.Position;
|
||||
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
|
||||
@@ -175,6 +174,7 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
|
||||
private AbsolutePanel rootPanel = new AbsolutePanel();
|
||||
|
||||
private MapWidget map;
|
||||
private Collection<Runnable> mapInitializedListener;
|
||||
|
||||
/**
|
||||
* 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
|
||||
public boolean contains(T t) {
|
||||
return (map.containsKey(t));
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
map.clear();
|
||||
}
|
||||
|
||||
public void removeAll(T t) {
|
||||
map.remove(t);
|
||||
}
|
||||
}
|
||||
|
||||
private class AdvantageLineUpdater implements QuickRanksListener {
|
||||
@@ -546,6 +554,7 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
|
||||
updateCoordinateSystemFromSettings();
|
||||
lastTimeChangeBeforeInitialization = null;
|
||||
isMapInitialized = false;
|
||||
mapInitializedListener = new ArrayList<>();
|
||||
this.hasPolar = false;
|
||||
headerPanel = new FlowPanel();
|
||||
headerPanel.setStyleName("RaceMap-HeaderPanel");
|
||||
@@ -764,6 +773,21 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
|
||||
if (streamletOverlay != null && settings.isShowWindStreamletOverlay()) {
|
||||
streamletOverlay.setCanvasSettings();
|
||||
}
|
||||
if (!currentlyDragging) {
|
||||
refreshMapWithoutAnimation();
|
||||
}
|
||||
if (!mapFirstZoomDone) {
|
||||
zoomMapToNewBounds(settings.getZoomSettings().getNewBounds(RaceMap.this));
|
||||
redraw();
|
||||
}
|
||||
if (!isMapInitialized) {
|
||||
RaceMap.this.isMapInitialized = true;
|
||||
mapInitializedListener.forEach(c -> c.run());
|
||||
mapInitializedListener.clear();
|
||||
// ensure at least one redraw after starting the map, but not before other things like modes
|
||||
// initialize, as they might change the timer or other settings
|
||||
redraw();
|
||||
}
|
||||
}
|
||||
});
|
||||
map.addBoundsChangeHandler(new BoundsChangeMapHandler() {
|
||||
@@ -777,7 +801,7 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
|
||||
currentMapBounds = map.getBounds();
|
||||
currentZoomLevel = newZoomLevel;
|
||||
headerPanel.getElement().getStyle().setWidth(map.getOffsetWidth(), Unit.PX);
|
||||
refreshMapWithoutAnimation();
|
||||
refreshMapWithoutAnimationButLeaveTransitionsAlive();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -812,7 +836,6 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
|
||||
createSettingsButton(map);
|
||||
}
|
||||
// Data has been initialized
|
||||
RaceMap.this.isMapInitialized = true;
|
||||
RaceMap.this.redraw();
|
||||
trueNorthIndicatorPanel.redraw();
|
||||
showAdditionalControls(map);
|
||||
@@ -910,6 +933,7 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
|
||||
}
|
||||
|
||||
public void redraw() {
|
||||
remoteCallsToSkipInExecution.removeAll(timer.getTime());
|
||||
timeChanged(timer.getTime(), null);
|
||||
}
|
||||
|
||||
@@ -964,6 +988,10 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
|
||||
return timer.getPlayMode() == PlayModes.Live;
|
||||
}
|
||||
|
||||
private void refreshMapWithoutAnimationButLeaveTransitionsAlive() {
|
||||
remoteCallsToSkipInExecution.addAll(remoteCallsInExecution);
|
||||
}
|
||||
|
||||
private void refreshMapWithoutAnimation() {
|
||||
removeTransitions();
|
||||
remoteCallsToSkipInExecution.addAll(remoteCallsInExecution);
|
||||
@@ -1041,7 +1069,6 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
|
||||
});
|
||||
}
|
||||
else {
|
||||
GWT.log("added identical call to skip");
|
||||
remoteCallsToSkipInExecution.add(newTime);
|
||||
}
|
||||
}
|
||||
@@ -1049,8 +1076,8 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
|
||||
@Override
|
||||
public void timeChanged(final Date newTime, final Date oldTime) {
|
||||
boolean isRedraw = oldTime == null;
|
||||
if (newTime != null && isMapInitialized) {
|
||||
if (raceIdentifier != null) {
|
||||
if (isMapInitialized) {
|
||||
if (newTime != null) {
|
||||
if (raceIdentifier != null) {
|
||||
final long transitionTimeInMillis = calculateTimeForPositionTransitionInMillis(newTime, oldTime);
|
||||
refreshMap(newTime, transitionTimeInMillis, isRedraw);
|
||||
@@ -1203,28 +1230,32 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
|
||||
showCourseSidelinesOnMap(raceMapDataDTO.courseSidelines);
|
||||
showStartAndFinishAndCourseMiddleLines(raceMapDataDTO.coursePositions);
|
||||
showStartLineToFirstMarkTriangle(raceMapDataDTO.coursePositions);
|
||||
|
||||
|
||||
// Rezoom the map
|
||||
LatLngBounds zoomToBounds = null;
|
||||
if (!settings.getZoomSettings().containsZoomType(ZoomTypes.NONE)) { // Auto zoom if setting is not manual
|
||||
if (!settings.getZoomSettings().containsZoomType(ZoomTypes.NONE)) {
|
||||
// Auto zoom if setting is not manual
|
||||
|
||||
zoomToBounds = settings.getZoomSettings().getNewBounds(RaceMap.this);
|
||||
if (zoomToBounds == null && !mapFirstZoomDone) {
|
||||
zoomToBounds = getDefaultZoomBounds(); // the user-specified zoom couldn't find what it was looking for; try defaults once
|
||||
// the user-specified zoom couldn't find what it was looking for; try defaults once
|
||||
zoomToBounds = getDefaultZoomBounds();
|
||||
}
|
||||
} else if (!mapFirstZoomDone) { // Zoom once to the marks if marks exist
|
||||
}
|
||||
if (!mapFirstZoomDone) {
|
||||
// Zoom once to the marks if marks exist
|
||||
zoomToBounds = new CourseMarksBoundsCalculator().calculateNewBounds(RaceMap.this);
|
||||
if (zoomToBounds == null) {
|
||||
zoomToBounds = getDefaultZoomBounds(); // use default zoom, e.g.,
|
||||
// use default zoom, e.g.,
|
||||
zoomToBounds = getDefaultZoomBounds();
|
||||
}
|
||||
/*
|
||||
* Reset the mapZoomedOrPannedSinceLastRaceSelection: In spite of the fact that
|
||||
* the map was just zoomed to the bounds of the marks, it was not a zoom or pan
|
||||
* triggered by the user. As a consequence the
|
||||
* mapZoomedOrPannedSinceLastRaceSelection option has to reset again.
|
||||
* Reset the mapZoomedOrPannedSinceLastRaceSelection: In spite of the fact that the map was
|
||||
* just zoomed to the bounds of the marks, it was not a zoom or pan triggered by the user.
|
||||
* As a consequence the mapZoomedOrPannedSinceLastRaceSelection option has to reset again.
|
||||
*/
|
||||
}
|
||||
zoomMapToNewBounds(zoomToBounds);
|
||||
mapFirstZoomDone = true;
|
||||
updateEstimatedDuration(raceMapDataDTO.estimatedDuration);
|
||||
}
|
||||
} else {
|
||||
@@ -1277,7 +1308,7 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
|
||||
private void updateBoatPositions(final Date newTime, final long transitionTimeInMillis,
|
||||
final Map<CompetitorDTO, Boolean> hasTailOverlapForCompetitor,
|
||||
final Iterable<CompetitorDTO> competitorsToShow, Map<CompetitorDTO, List<GPSFixDTOWithSpeedWindTackAndLegType>> boatData, boolean updateTailsOnly) {
|
||||
if (zoomingAnimationsInProgress == 0 && !currentlyDragging) {
|
||||
if (zoomingAnimationsInProgress == 0) {
|
||||
fixesAndTails.updateFixes(boatData, hasTailOverlapForCompetitor, RaceMap.this, transitionTimeInMillis);
|
||||
showBoatsOnMap(newTime, transitionTimeInMillis,
|
||||
/* re-calculate; it could have changed since the asynchronous request was made: */
|
||||
@@ -2076,18 +2107,13 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
|
||||
int zoomLat = (int) Math.floor(Math.log(map.getDiv().getClientHeight() * 2 * Math.PI / deltaLat / GLOBE_PXSIZE) / LOG2);
|
||||
return Math.min(Math.min(zoomLat, zoomLng), MAX_ZOOM);
|
||||
}
|
||||
|
||||
|
||||
private void zoomMapToNewBounds(LatLngBounds newBounds) {
|
||||
if (newBounds != null) {
|
||||
LatLngBounds currentMapBounds;
|
||||
if (map.getBounds() == null
|
||||
|| !BoundsUtil.contains((currentMapBounds = map.getBounds()), newBounds)
|
||||
|| graticuleAreaRatio(currentMapBounds, newBounds) > 10) {
|
||||
// only change bounds if the new bounds don't fit into the current map zoom
|
||||
int newZoomLevel = getZoomLevel(newBounds);
|
||||
if (mapNeedsToPanOrZoom(newBounds, newZoomLevel)) {
|
||||
Iterable<ZoomTypes> oldZoomTypesToConsiderSettings = settings.getZoomSettings().getTypesToConsiderOnZoom();
|
||||
setAutoZoomInProgress(true);
|
||||
autoZoomLatLngBounds = newBounds;
|
||||
int newZoomLevel = getZoomLevel(autoZoomLatLngBounds);
|
||||
if (newZoomLevel != map.getZoom()) {
|
||||
// distinguish between zoom-in and zoom-out, because the sequence of panTo() and setZoom()
|
||||
// appears different on the screen due to map-animations
|
||||
@@ -2097,37 +2123,39 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
|
||||
autoZoomIn = newZoomLevel > map.getZoom();
|
||||
autoZoomOut = !autoZoomIn;
|
||||
autoZoomLevel = newZoomLevel;
|
||||
removeTransitions();
|
||||
if (autoZoomIn) {
|
||||
map.panTo(autoZoomLatLngBounds.getCenter());
|
||||
map.panTo(newBounds.getCenter());
|
||||
} else {
|
||||
map.setZoom(autoZoomLevel);
|
||||
}
|
||||
} else {
|
||||
map.panTo(autoZoomLatLngBounds.getCenter());
|
||||
map.panTo(newBounds.getCenter());
|
||||
}
|
||||
autoZoomLatLngBounds = newBounds;
|
||||
RaceMapZoomSettings restoredZoomSettings = new RaceMapZoomSettings(oldZoomTypesToConsiderSettings, settings.getZoomSettings().isZoomToSelectedCompetitors());
|
||||
settings = new RaceMapSettings(settings, restoredZoomSettings);
|
||||
setAutoZoomInProgress(false);
|
||||
mapFirstZoomDone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean mapNeedsToPanOrZoom(LatLngBounds newBounds, int newZoomLevel) {
|
||||
// we never updated, update now
|
||||
if (currentMapBounds == null) {
|
||||
return true;
|
||||
}
|
||||
// we do not fit the required bounds, update now
|
||||
if (!BoundsUtil.contains(currentMapBounds, newBounds)) {
|
||||
return true;
|
||||
}
|
||||
// we do fit the required bounds, however we might be to far zoomed out, check if we can zoom to a better level
|
||||
if (newZoomLevel != map.getZoom()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private double graticuleAreaRatio(LatLngBounds containing, LatLngBounds contained) {
|
||||
assert BoundsUtil.contains(containing, contained);
|
||||
double containingAreaRatio = getGraticuleArea(containing) / getGraticuleArea(contained);
|
||||
return containingAreaRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
* A much simplified "area" calculation for a {@link Bounds} object, multiplying the differences in latitude and longitude degrees.
|
||||
* The result therefore is in the order of magnitude of 60*60 square nautical miles.
|
||||
*/
|
||||
private double getGraticuleArea(LatLngBounds bounds) {
|
||||
return ((BoundsUtil.isCrossesDateLine(bounds) ? bounds.getNorthEast().getLongitude()+360 : bounds.getNorthEast().getLongitude())-bounds.getSouthWest().getLongitude()) *
|
||||
(bounds.getNorthEast().getLatitude() - bounds.getSouthWest().getLatitude());
|
||||
}
|
||||
|
||||
private void setAutoZoomInProgress(boolean autoZoomInProgress) {
|
||||
this.autoZoomInProgress = autoZoomInProgress;
|
||||
}
|
||||
@@ -2563,9 +2591,6 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
|
||||
}
|
||||
}
|
||||
showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors());
|
||||
} else {
|
||||
// adding a single competitor; may need to re-load data, so refresh:
|
||||
redraw();
|
||||
}
|
||||
} else {
|
||||
// only change highlighting
|
||||
@@ -2574,11 +2599,6 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
|
||||
boatCanvas.setDisplayMode(displayHighlighted(competitor));
|
||||
boatCanvas.draw();
|
||||
showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors());
|
||||
} else {
|
||||
// seems like an internal error not to find the lowlighted marker; but maybe the
|
||||
// competitor was added late to the race;
|
||||
// data for newly selected competitor supposedly missing; refresh
|
||||
redraw();
|
||||
}
|
||||
}
|
||||
// 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
|
||||
if (!zoomSettings.containsZoomType(ZoomTypes.NONE) && zoomSettings.isZoomToSelectedCompetitors()) {
|
||||
zoomMapToNewBounds(zoomSettings.getNewBounds(this));
|
||||
}
|
||||
redraw();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -2865,7 +2886,9 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
|
||||
public void onResize() {
|
||||
if (map != null) {
|
||||
map.triggerResize();
|
||||
zoomMapToNewBounds(settings.getZoomSettings().getNewBounds(RaceMap.this));
|
||||
if (isMapInitialized) {
|
||||
zoomMapToNewBounds(settings.getZoomSettings().getNewBounds(RaceMap.this));
|
||||
}
|
||||
}
|
||||
// Adjust RaceMap headers to avoid overlapping based on the RaceMap width
|
||||
boolean isCompactHeader = this.getOffsetWidth() <= 600;
|
||||
@@ -3121,4 +3144,13 @@ public class RaceMap extends AbstractCompositeComponent<RaceMapSettings> impleme
|
||||
public RaceMapLifecycle getLifecycle() {
|
||||
return raceMapLifecycle;
|
||||
}
|
||||
|
||||
public void addMapInitializedListener(Runnable runnable) {
|
||||
if (isMapInitialized) {
|
||||
runnable.run();
|
||||
} else {
|
||||
this.mapInitializedListener.add(runnable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-7
@@ -78,8 +78,19 @@ public abstract class AbstractRaceBoardMode implements RaceBoardMode, RaceTimesI
|
||||
leaderboardPanel.setAutoExpandPreSelected(true);
|
||||
this.timer = raceBoardPanel.getTimer();
|
||||
this.raceIdentifier = raceBoardPanel.getSelectedRaceIdentifier();
|
||||
raceBoardPanel.getMap().addMapInitializedListener(new Runnable() {
|
||||
public void run() {
|
||||
checkIfTrigger();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
protected void checkIfTrigger() {
|
||||
if (raceBoardPanel.getMap().isDataInitialized()) {
|
||||
trigger();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever new information of any sort has become available. Subclasses can use this
|
||||
* 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
|
||||
long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) {
|
||||
this.raceTimesInfo = raceTimesInfo;
|
||||
this.raceTimesInfoForRace = raceTimesInfo.get(getRaceIdentifier());
|
||||
trigger();
|
||||
checkIfTrigger();
|
||||
}
|
||||
|
||||
protected void stopReceivingRaceTimesInfos() {
|
||||
@@ -131,7 +142,8 @@ public abstract class AbstractRaceBoardMode implements RaceBoardMode, RaceTimesI
|
||||
@Override
|
||||
public void updatedLeaderboard(LeaderboardDTO leaderboard) {
|
||||
this.leaderboard = leaderboard;
|
||||
trigger();
|
||||
checkIfTrigger();
|
||||
;
|
||||
}
|
||||
|
||||
protected void stopReceivingLeaderboard() {
|
||||
@@ -140,9 +152,9 @@ public abstract class AbstractRaceBoardMode implements RaceBoardMode, RaceTimesI
|
||||
|
||||
/**
|
||||
* Fetches the leaderboard named {@code leaderboardName} for {@code timePoint} with details for race column
|
||||
* {@code raceColumnName} and stores it in {@link #leaderboardForSpecificTimePoint}, then invokes {@link #trigger()}.
|
||||
* If a {@link #getLeaderboard()} has been received already, it is passed on in the call in order to reduce the
|
||||
* bandwidth required, based on a differential approach.
|
||||
* {@code raceColumnName} and stores it in {@link #leaderboardForSpecificTimePoint}, then invokes
|
||||
* {@link #checkIfTrigger()}. If a {@link #getLeaderboard()} has been received already, it is passed on in the call
|
||||
* in order to reduce the bandwidth required, based on a differential approach.
|
||||
*/
|
||||
protected void loadLeaderboardForSpecificTimePoint(String leaderboardName,
|
||||
String raceColumnName, Date timePoint) {
|
||||
@@ -156,7 +168,7 @@ public abstract class AbstractRaceBoardMode implements RaceBoardMode, RaceTimesI
|
||||
public void onSuccess(LeaderboardDTO result) {
|
||||
leaderboardForSpecificTimePoint = result;
|
||||
getLeaderboardPanel().updateLeaderboard(result);
|
||||
trigger();
|
||||
checkIfTrigger();
|
||||
}
|
||||
};
|
||||
final ArrayList<String> raceColumnNameAsList = new ArrayList<>();
|
||||
@@ -174,7 +186,7 @@ public abstract class AbstractRaceBoardMode implements RaceBoardMode, RaceTimesI
|
||||
@Override
|
||||
public void currentRaceSelected(RaceIdentifier raceIdentifier, RaceColumnDTO raceColumn) {
|
||||
this.raceColumn = raceColumn;
|
||||
trigger();
|
||||
checkIfTrigger();
|
||||
}
|
||||
|
||||
protected Map<RegattaAndRaceIdentifier, RaceTimesInfoDTO> getRaceTimesInfo() {
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ public abstract class RaceBoardModeWithPerRaceCompetitors extends AbstractRaceBo
|
||||
@Override
|
||||
public void competitorsForRaceDefined(Iterable<CompetitorDTO> competitorsInRace) {
|
||||
this.competitorsInRace = competitorsInRace;
|
||||
trigger();
|
||||
checkIfTrigger();
|
||||
}
|
||||
|
||||
protected Iterable<CompetitorDTO> getCompetitorsInRace() {
|
||||
|
||||
+52
-153
@@ -1,16 +1,13 @@
|
||||
package com.sap.sailing.gwt.ui.server;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.URL;
|
||||
@@ -23,29 +20,21 @@ import java.nio.file.Files;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.mp4parser.AbstractBoxParser;
|
||||
import org.mp4parser.IsoFile;
|
||||
import org.mp4parser.boxes.UserBox;
|
||||
import org.mp4parser.boxes.iso14496.part12.MediaDataBox;
|
||||
import org.mp4parser.boxes.iso14496.part12.MovieBox;
|
||||
import org.mp4parser.boxes.iso14496.part12.MovieHeaderBox;
|
||||
import org.mp4parser.tools.Path;
|
||||
import org.mp4parser.PropertyBoxParserImpl;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.osgi.util.tracker.ServiceTracker;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import com.google.gwt.thirdparty.json.JSONArray;
|
||||
import com.google.gwt.thirdparty.json.JSONException;
|
||||
import com.google.gwt.thirdparty.json.JSONObject;
|
||||
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
|
||||
@@ -55,6 +44,8 @@ import com.sap.sailing.domain.common.media.MediaTrack;
|
||||
import com.sap.sailing.domain.common.security.Permission;
|
||||
import com.sap.sailing.domain.common.security.Permission.Mode;
|
||||
import com.sap.sailing.gwt.ui.client.MediaService;
|
||||
import com.sap.sailing.media.mp4.MP4MediaParser;
|
||||
import com.sap.sailing.media.mp4.MP4ParserFakeFile;
|
||||
import com.sap.sailing.server.RacingEventService;
|
||||
import com.sap.sse.common.Duration;
|
||||
import com.sap.sse.common.impl.MillisecondsDurationImpl;
|
||||
@@ -70,7 +61,6 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
|
||||
private ServiceTracker<RacingEventService, RacingEventService> racingEventServiceTracker;
|
||||
|
||||
|
||||
private static final int REQUIRED_SIZE_IN_BYTES = 10000000;
|
||||
private static final long serialVersionUID = -8917349579281305977L;
|
||||
|
||||
public MediaServiceImpl() {
|
||||
@@ -158,14 +148,14 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
|
||||
URL input = new URL(url);
|
||||
// check size and do rangerequests if possible
|
||||
long fileSize = determineFileSize(input);
|
||||
if (fileSize > 2 * REQUIRED_SIZE_IN_BYTES) {
|
||||
if (fileSize > 2 * MP4MediaParser.REQUIRED_SIZE_IN_BYTES) {
|
||||
response = checkMetadataByPartialDownloads(input, fileSize);
|
||||
} else {
|
||||
response = checkMetadataByFullFileDownload(input);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.log(Level.WARNING, "Error in video analysis ", e);
|
||||
response = new VideoMetadataDTO(false, null, false, null, e.getMessage());
|
||||
response = new VideoMetadataDTO(false, null, false, null, "");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
@@ -198,10 +188,10 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
|
||||
|
||||
private VideoMetadataDTO checkMetadataByPartialDownloads(URL input, long fileSize)
|
||||
throws IOException, ProtocolException {
|
||||
byte[] start = new byte[REQUIRED_SIZE_IN_BYTES];
|
||||
byte[] end = new byte[REQUIRED_SIZE_IN_BYTES];
|
||||
downloadPartOfFile(input, start, "bytes=0-" + REQUIRED_SIZE_IN_BYTES);
|
||||
downloadPartOfFile(input, end, "bytes=" + (fileSize - REQUIRED_SIZE_IN_BYTES) + "-");
|
||||
byte[] start = new byte[MP4MediaParser.REQUIRED_SIZE_IN_BYTES];
|
||||
byte[] end = new byte[MP4MediaParser.REQUIRED_SIZE_IN_BYTES];
|
||||
downloadPartOfFile(input, start, "bytes=0-" + MP4MediaParser.REQUIRED_SIZE_IN_BYTES);
|
||||
downloadPartOfFile(input, end, "bytes=" + (fileSize - MP4MediaParser.REQUIRED_SIZE_IN_BYTES) + "-");
|
||||
long skipped = fileSize - start.length - end.length;
|
||||
return checkMetadata(start, end, skipped);
|
||||
}
|
||||
@@ -212,9 +202,10 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
|
||||
connection.setConnectTimeout(METADATA_CONNECTION_TIMEOUT);
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setRequestProperty("Range", range);
|
||||
try (InputStream inStream = connection.getInputStream()) {
|
||||
DataInputStream dataInStream = new DataInputStream(inStream);
|
||||
dataInStream.readFully(store);
|
||||
try (final InputStream inStream = connection.getInputStream()) {
|
||||
try (final DataInputStream dataInStream = new DataInputStream(inStream)) {
|
||||
dataInStream.readFully(store);
|
||||
}
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
@@ -223,146 +214,53 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
|
||||
private VideoMetadataDTO checkMetadataByFullFileDownload(URL input)
|
||||
throws ParserConfigurationException, SAXException, IOException {
|
||||
final File tmp = File.createTempFile("upload", "metadataCheck");
|
||||
VideoMetadataDTO result;
|
||||
try {
|
||||
final ReadableByteChannel rbc = Channels.newChannel(input.openStream());
|
||||
try (FileOutputStream fos = new FileOutputStream(tmp)) {
|
||||
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
|
||||
try (IsoFile isof = new IsoFile(tmp)) {
|
||||
final boolean canDownload = true;
|
||||
final Date recordStartedTimer = determineRecordingStart(isof);
|
||||
final Duration duration = determineDuration(isof);
|
||||
final boolean spherical = determine360(isof);
|
||||
removeTempFiles(isof);
|
||||
return new VideoMetadataDTO(canDownload, duration, spherical, recordStartedTimer, "");
|
||||
try (final ReadableByteChannel rbc = Channels.newChannel(input.openStream())) {
|
||||
try (final FileOutputStream fos = new FileOutputStream(tmp)) {
|
||||
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
|
||||
try (final MP4ParserFakeFile inputFile = new MP4ParserFakeFile(tmp)) {
|
||||
Files.delete(tmp.toPath());
|
||||
result = checkMetadata(inputFile);
|
||||
} catch (Exception e) {
|
||||
result = new VideoMetadataDTO(true, null, false, null, e.getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
result = new VideoMetadataDTO(false, null, false, null, e.getMessage());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Files.delete(tmp.toPath());
|
||||
// delete the file if not already deleted in main path
|
||||
tmp.delete();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoMetadataDTO checkMetadata(byte[] start, byte[] end, Long skipped) {
|
||||
ensureUserCanManageMedia();
|
||||
File tmp = null;
|
||||
boolean spherical = false;
|
||||
Duration duration = null;
|
||||
Date recordStartedTimer = null;
|
||||
String message = "";
|
||||
try {
|
||||
tmp = createFileFromData(start, end, skipped);
|
||||
try (IsoFile isof = new IsoFile(tmp)) {
|
||||
try {
|
||||
recordStartedTimer = determineRecordingStart(isof);
|
||||
spherical = determine360(isof);
|
||||
duration = determineDuration(isof);
|
||||
} finally {
|
||||
removeTempFiles(isof);
|
||||
}
|
||||
}
|
||||
VideoMetadataDTO result;
|
||||
try (MP4ParserFakeFile input = new MP4ParserFakeFile(start, end, skipped)) {
|
||||
result = checkMetadata(input);
|
||||
} catch (Exception e) {
|
||||
logger.log(Level.WARNING, "Error in video analysis ", e);
|
||||
message = e.getMessage();
|
||||
} finally {
|
||||
if (tmp != null) {
|
||||
try {
|
||||
Files.delete(tmp.toPath());
|
||||
} catch (IOException e) {
|
||||
logger.log(Level.SEVERE, "Could not delete tmp mp4 file", e);
|
||||
}
|
||||
}
|
||||
result = new VideoMetadataDTO(true, null, false, null, e.getMessage());
|
||||
}
|
||||
return new VideoMetadataDTO(true, duration, spherical, recordStartedTimer, message);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Some boxes (we don't actually need to read) create internal tempfiles, we delete them here, as the VM could run
|
||||
* quite long
|
||||
*/
|
||||
private void removeTempFiles(IsoFile isof) {
|
||||
List<MediaDataBox> boxesWithTempFiles = isof.getBoxes(MediaDataBox.class, true);
|
||||
for (MediaDataBox box : boxesWithTempFiles) {
|
||||
try {
|
||||
Field field = box.getClass().getDeclaredField("dataFile");
|
||||
field.setAccessible(true);
|
||||
File data = (File) field.get(box);
|
||||
Files.delete(data.toPath());
|
||||
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException
|
||||
| IOException e) {
|
||||
logger.log(Level.WARNING, "Could not delete mp4 temp files", e);
|
||||
}
|
||||
private VideoMetadataDTO checkMetadata(MP4ParserFakeFile input)
|
||||
throws ParserConfigurationException, SAXException, IOException {
|
||||
AbstractBoxParser boxParserImpl = new PropertyBoxParserImpl();
|
||||
boxParserImpl.skippingBoxes(new String[] { "mdat" });
|
||||
try (IsoFile isof = new IsoFile(input, boxParserImpl)) {
|
||||
Date recordStartedTimer = MP4MediaParser.determineRecordingStart(isof);
|
||||
boolean spherical = MP4MediaParser.determine360(isof);
|
||||
Duration duration = new MillisecondsDurationImpl(MP4MediaParser.determineDurationInMillis(isof));
|
||||
return new VideoMetadataDTO(true, duration, spherical, recordStartedTimer, "");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a dummy mp4 file, only containing the start and the end, the middle is filled with 0 so that all file
|
||||
* internal pointers from start to end are working
|
||||
*/
|
||||
private File createFileFromData(byte[] start, byte[] end, Long skipped) throws IOException, FileNotFoundException {
|
||||
File tmp;
|
||||
tmp = File.createTempFile("upload", "metadataCheck");
|
||||
try (FileOutputStream fw = new FileOutputStream(tmp)) {
|
||||
fw.write(start);
|
||||
while (skipped > 0) {
|
||||
// ensure that no absurd amount of memory is required, and that more then 4gb files can be analysed
|
||||
if (skipped > 10000000) {
|
||||
byte[] dummy = new byte[10000000];
|
||||
fw.write(dummy);
|
||||
skipped = skipped - 10000000;
|
||||
} else {
|
||||
byte[] dummy = new byte[skipped.intValue()];
|
||||
fw.write(dummy);
|
||||
skipped = skipped - skipped.intValue();
|
||||
}
|
||||
}
|
||||
fw.write(end);
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
private boolean determine360(IsoFile isof) throws ParserConfigurationException, SAXException, IOException {
|
||||
boolean spherical = false;
|
||||
UserBox uuidBox = Path.getPath(isof, "moov[0]/trak[0]/uuid");
|
||||
if (uuidBox != null) {
|
||||
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
|
||||
Document doc = dBuilder.parse(new ByteArrayInputStream(uuidBox.getData()));
|
||||
|
||||
NodeList childs = doc.getDocumentElement().getChildNodes();
|
||||
for (int i = 0; i < childs.getLength(); i++) {
|
||||
Node child = childs.item(i);
|
||||
if (child.getNodeName().toLowerCase().contains(":spherical")) {
|
||||
spherical = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return spherical;
|
||||
}
|
||||
|
||||
private Duration determineDuration(IsoFile isof) {
|
||||
Duration duration = null;
|
||||
MovieBox mbox = isof.getMovieBox();
|
||||
if (mbox != null) {
|
||||
MovieHeaderBox mhb = mbox.getMovieHeaderBox();
|
||||
if (mhb != null) {
|
||||
duration = new MillisecondsDurationImpl(mhb.getDuration()*1000 / mhb.getTimescale());
|
||||
}
|
||||
}
|
||||
return duration;
|
||||
}
|
||||
|
||||
private Date determineRecordingStart(IsoFile isof) {
|
||||
Date creationTime = null;
|
||||
MovieBox mbox = isof.getMovieBox();
|
||||
if (mbox != null) {
|
||||
MovieHeaderBox mhb = mbox.getMovieHeaderBox();
|
||||
if (mhb != null) {
|
||||
creationTime = mhb.getCreationTime();
|
||||
}
|
||||
}
|
||||
return creationTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaTrack getMediaTrackByUrl(String url) {
|
||||
MediaTrack result = null;
|
||||
@@ -384,6 +282,7 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
|
||||
if (videoId.isEmpty()) {
|
||||
message = "Empty id";
|
||||
} else {
|
||||
// sanitize, as we inject it into an url with our api key!
|
||||
videoId = URLEncoder.encode(videoId, StandardCharsets.UTF_8.name());
|
||||
try {
|
||||
URL apiURL = new URL(
|
||||
@@ -396,10 +295,13 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
|
||||
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String pageText = reader.lines().collect(Collectors.joining("\n"));
|
||||
JSONObject jsonAnswer = new JSONObject(pageText);
|
||||
final JSONObject item = jsonAnswer.getJSONArray("items").getJSONObject(0);
|
||||
message = item.getJSONObject("snippet").getString("title");
|
||||
String rawDuration = item.getJSONObject("contentDetails").getString("duration");
|
||||
duration = new MillisecondsDurationImpl(java.time.Duration.parse(rawDuration).toMillis());
|
||||
final JSONArray dataArray = jsonAnswer.getJSONArray("items");
|
||||
if (dataArray.length() > 0) {
|
||||
final JSONObject item = dataArray.getJSONObject(0);
|
||||
message = item.getJSONObject("snippet").getString("title");
|
||||
String rawDuration = item.getJSONObject("contentDetails").getString("duration");
|
||||
duration = new MillisecondsDurationImpl(java.time.Duration.parse(rawDuration).toMillis());
|
||||
}
|
||||
canDownload = true;
|
||||
} catch (JSONException e) {
|
||||
message = e.getMessage();
|
||||
@@ -410,9 +312,6 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
|
||||
logger.log(Level.WARNING, "Error in youtube metadata call", e);
|
||||
}
|
||||
}
|
||||
//sanitize, as we inject it into an url with our api key!
|
||||
|
||||
|
||||
return new VideoMetadataDTO(canDownload, duration, false, null, message);
|
||||
}
|
||||
}
|
||||
+16
-20
@@ -6784,13 +6784,12 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet
|
||||
RegattaLogEvent event = null;
|
||||
TimePoint from = mapping.getTimeRange().hasOpenBeginning() ? null : mapping.getTimeRange().from();
|
||||
TimePoint to = mapping.getTimeRange().hasOpenEnd() ? null : mapping.getTimeRange().to();
|
||||
if (dto.mappedTo instanceof MarkDTO) {
|
||||
Mark mark = convertToMark(((MarkDTO) dto.mappedTo), true);
|
||||
event = new RegattaLogDeviceMarkMappingEventImpl(now, now, getService().getServerAuthor(),
|
||||
UUID.randomUUID(), mark, mapping.getDevice(), from, to);
|
||||
} else if (dto.mappedTo instanceof CompetitorWithBoatDTO) {
|
||||
Competitor competitor = getService().getCompetitorAndBoatStore()
|
||||
.getExistingCompetitorByIdAsString(((CompetitorWithBoatDTO) dto.mappedTo).getIdAsString());
|
||||
if (mapping.getMappedTo() instanceof Mark) {
|
||||
Mark mark = (Mark) mapping.getMappedTo();
|
||||
event = new RegattaLogDeviceMarkMappingEventImpl(now, now, getService().getServerAuthor(), UUID.randomUUID(),
|
||||
mark, mapping.getDevice(), from, to);
|
||||
} else if (mapping.getMappedTo() instanceof Competitor) {
|
||||
Competitor competitor = (Competitor) mapping.getMappedTo();
|
||||
if (mapping.getDevice().getIdentifierType().equals(ExpeditionSensorDeviceIdentifier.TYPE)) {
|
||||
event = new RegattaLogDeviceCompetitorExpeditionExtendedMappingEventImpl(now, now,
|
||||
getService().getServerAuthor(), UUID.randomUUID(), competitor, mapping.getDevice(), from, to);
|
||||
@@ -6798,11 +6797,10 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet
|
||||
event = new RegattaLogDeviceCompetitorMappingEventImpl(now, now, getService().getServerAuthor(),
|
||||
UUID.randomUUID(), competitor, mapping.getDevice(), from, to);
|
||||
}
|
||||
} else if (dto.mappedTo instanceof BoatDTO) {
|
||||
final Boat boat = getService().getCompetitorAndBoatStore()
|
||||
.getExistingBoatByIdAsString(((BoatDTO) dto.mappedTo).getIdAsString());
|
||||
event = new RegattaLogDeviceBoatMappingEventImpl(now, now, getService().getServerAuthor(),
|
||||
UUID.randomUUID(), boat, mapping.getDevice(), from, to);
|
||||
} else if (mapping.getMappedTo() instanceof Boat) {
|
||||
final Boat boat = (Boat) mapping.getMappedTo();
|
||||
event = new RegattaLogDeviceBoatMappingEventImpl(now, now, getService().getServerAuthor(), UUID.randomUUID(),
|
||||
boat, mapping.getDevice(), from, to);
|
||||
} else {
|
||||
throw new RuntimeException("Can only map devices to competitors, boats or marks");
|
||||
}
|
||||
@@ -6851,14 +6849,12 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet
|
||||
TimeRange timeRange = new TimeRangeImpl(from, to);
|
||||
if (dto.mappedTo instanceof MarkDTO) {
|
||||
Mark mark = convertToMark(((MarkDTO) dto.mappedTo), true);
|
||||
// expect UUIDs
|
||||
return new DeviceMappingImpl<Mark>(mark, device, timeRange, dto.originalRaceLogEventIds,
|
||||
RegattaLogDeviceMarkMappingEventImpl.class);
|
||||
} else if (dto.mappedTo instanceof CompetitorWithBoatDTO) {
|
||||
Competitor competitor = getService().getCompetitorAndBoatStore()
|
||||
.getExistingCompetitorByIdAsString(((CompetitorWithBoatDTO) dto.mappedTo).getIdAsString());
|
||||
return new DeviceMappingImpl<Competitor>(competitor, device, timeRange, dto.originalRaceLogEventIds,
|
||||
RegattaLogDeviceCompetitorMappingEventImpl.class);
|
||||
//expect UUIDs
|
||||
return new DeviceMappingImpl<Mark>(mark, device, timeRange, dto.originalRaceLogEventIds, RegattaLogDeviceMarkMappingEventImpl.class);
|
||||
} else if (dto.mappedTo instanceof CompetitorDTO) {
|
||||
Competitor competitor = getService().getCompetitorAndBoatStore().getExistingCompetitorByIdAsString(
|
||||
((CompetitorDTO) dto.mappedTo).getIdAsString());
|
||||
return new DeviceMappingImpl<Competitor>(competitor, device, timeRange, dto.originalRaceLogEventIds, RegattaLogDeviceCompetitorMappingEventImpl.class);
|
||||
} else if (dto.mappedTo instanceof BoatDTO) {
|
||||
final Boat boat = getService().getCompetitorAndBoatStore()
|
||||
.getExistingBoatByIdAsString(dto.mappedTo.getIdAsString());
|
||||
|
||||
+1
-2
@@ -228,6 +228,7 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay implemen
|
||||
if (this.swarm != null) {
|
||||
this.swarm.stop();
|
||||
this.swarm.getColorMapper().removeListener(this);
|
||||
swarm = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,12 +336,10 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay implemen
|
||||
if (getCanvas() != null) {
|
||||
if (isVisible) {
|
||||
this.startStreamlets();
|
||||
this.visible = isVisible;
|
||||
if (colored) {
|
||||
this.streamletLegend.setVisible(true);
|
||||
}
|
||||
swarm.setColors(colored);
|
||||
this.startStreamlets();
|
||||
if (isAttached) {
|
||||
Scheduler.get().scheduleDeferred(() -> addObserverIfNecessary());
|
||||
} else {
|
||||
|
||||
+74
-66
@@ -6,6 +6,7 @@ import java.util.Map;
|
||||
|
||||
import com.google.gwt.canvas.client.Canvas;
|
||||
import com.google.gwt.canvas.dom.client.Context2d;
|
||||
import com.google.gwt.core.client.Duration;
|
||||
import com.google.gwt.event.shared.HandlerRegistration;
|
||||
import com.google.gwt.maps.client.MapWidget;
|
||||
import com.google.gwt.maps.client.base.LatLng;
|
||||
@@ -54,7 +55,6 @@ public class Swarm implements TimeListener {
|
||||
private boolean swarmOffScreen = false;
|
||||
|
||||
private int swarmPause = 0;
|
||||
private boolean swarmContinue = true;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
}
|
||||
|
||||
public void start(final int animationIntervalMillis) {
|
||||
removeBoundsChangeHandler();
|
||||
fullcanvas.setCanvasSettings();
|
||||
// if map is not yet loaded, wait for it
|
||||
if (map.getBounds() != null) {
|
||||
startWithMap(animationIntervalMillis);
|
||||
} else {
|
||||
removeBoundsChangeHandler();
|
||||
BoundsChangeMapHandler handler = new BoundsChangeMapHandler() {
|
||||
@Override
|
||||
public void onEvent(BoundsChangeMapEvent event) {
|
||||
if (swarmContinue) {
|
||||
startWithMap(animationIntervalMillis);
|
||||
}
|
||||
if (loopTimer == null) {
|
||||
loopTimer = new com.google.gwt.user.client.Timer() {
|
||||
public void run() {
|
||||
updateSwarmOneTick(animationIntervalMillis);
|
||||
}
|
||||
};
|
||||
boundsChangeHandlers.put(handler, map.addBoundsChangeHandler(handler));
|
||||
}
|
||||
|
||||
// the map already exists, ensure swam is started
|
||||
if (map.getBounds() != null) {
|
||||
startSwarmIfNecessaryAndUpdateProjection();
|
||||
loopTimer.schedule(0);
|
||||
}
|
||||
// add handler, so the swarm updates itself without calls from the map required
|
||||
BoundsChangeMapHandler handler = new BoundsChangeMapHandler() {
|
||||
@Override
|
||||
public void onEvent(BoundsChangeMapEvent event) {
|
||||
startSwarmIfNecessaryAndUpdateProjection();
|
||||
loopTimer.schedule(0);
|
||||
}
|
||||
};
|
||||
boundsChangeHandlers.put(handler, map.addBoundsChangeHandler(handler));
|
||||
}
|
||||
|
||||
private void startSwarmIfNecessaryAndUpdateProjection() {
|
||||
if (projection == null) {
|
||||
projection = new Mercator(fullcanvas, map);
|
||||
|
||||
}
|
||||
// ensure projection fits the map
|
||||
projection.calibrate();
|
||||
// ensure canvas fits the map
|
||||
updateBounds();
|
||||
if (particles == null) {
|
||||
// if this is the first start, also createParticles
|
||||
createParticles();
|
||||
} else {
|
||||
// clear all tails, as else there will be long smears over the map
|
||||
clearNextFrame = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void startWithMap(int animationIntervalMillis) {
|
||||
projection = new Mercator(fullcanvas, map);
|
||||
projection.calibrate();
|
||||
updateBounds();
|
||||
if (particles == null) {
|
||||
createParticles();
|
||||
} else {
|
||||
clearNextFrame = true;
|
||||
private void updateSwarmOneTick(int animationIntervalMillis) {
|
||||
double time0 = Duration.currentTimeMillis();
|
||||
// upon zooming the swarm is shortly paused, to ensure no strange rendering during the css zoom
|
||||
// animations
|
||||
if (swarmPause > 1) {
|
||||
swarmPause--;
|
||||
} else if (swarmPause == 1) {
|
||||
// ensure bounds and projections are up to date
|
||||
startSwarmIfNecessaryAndUpdateProjection();
|
||||
if (zoomChanged) {
|
||||
// ensure amount of particles is updated
|
||||
diffPx = new Vector(0, 0);
|
||||
recycleParticles();
|
||||
zoomChanged = false;
|
||||
} else {
|
||||
// process the offset
|
||||
diffPx = fullcanvas.getDiffPx();
|
||||
}
|
||||
swarmPause = 0;
|
||||
}
|
||||
swarmContinue = true;
|
||||
startLoopIfNecessary(animationIntervalMillis);
|
||||
// update the swarm if it is not paused
|
||||
if ((!swarmOffScreen) && (swarmPause == 0)) {
|
||||
execute(diffPx);
|
||||
diffPx = new Vector(0, 0);
|
||||
}
|
||||
double time1 = Duration.currentTimeMillis();
|
||||
// wait at least 10ms for the next iteration; try to get one iteration done every
|
||||
// animationIntervalMillis if possible
|
||||
double timeDelta = time1 - time0;
|
||||
// log("fps: "+(1000.0/timeDelta));
|
||||
loopTimer.schedule((int) Math.max(10, animationIntervalMillis - timeDelta));
|
||||
}
|
||||
|
||||
private void removeBoundsChangeHandler() {
|
||||
for (HandlerRegistration reg : boundsChangeHandlers.values()) {
|
||||
reg.removeHandler();
|
||||
}
|
||||
boundsChangeHandlers.clear();
|
||||
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
swarmContinue = false;
|
||||
removeBoundsChangeHandler();
|
||||
projection.clearCanvas();
|
||||
loopTimer.cancel();
|
||||
}
|
||||
|
||||
private Particle recycleOrCreateParticle(Particle particle) {
|
||||
@@ -226,52 +274,12 @@ public class Swarm implements TimeListener {
|
||||
+ visibleBoundsOfField.getNorthEast().getLatitude() / 180. * Math.PI) / 2);
|
||||
}
|
||||
|
||||
private void startLoopIfNecessary(final int animationIntervalMillis) {
|
||||
if (loopTimer == null) {
|
||||
// Create animation-loop based on timer timeout
|
||||
loopTimer = new com.google.gwt.user.client.Timer() {
|
||||
public void run() {
|
||||
Date time0 = new Date();
|
||||
if (swarmPause > 1) {
|
||||
swarmPause--;
|
||||
} else if (swarmPause == 1) {
|
||||
fullcanvas.setCanvasSettings();
|
||||
projection.calibrate();
|
||||
updateBounds();
|
||||
if (zoomChanged) {
|
||||
diffPx = new Vector(0, 0);
|
||||
recycleParticles();
|
||||
zoomChanged = false;
|
||||
} else {
|
||||
diffPx = fullcanvas.getDiffPx();
|
||||
}
|
||||
swarmPause = 0;
|
||||
}
|
||||
if ((!swarmOffScreen) && (swarmPause == 0)) {
|
||||
execute(diffPx);
|
||||
diffPx = new Vector(0, 0);
|
||||
}
|
||||
Date time1 = new Date();
|
||||
if (swarmContinue) {
|
||||
// wait at least 10ms for the next iteration; try to get one iteration done every
|
||||
// animationIntervalMillis if possible
|
||||
long timeDelta = time1.getTime() - time0.getTime();
|
||||
// log("fps: "+(1000.0/timeDelta));
|
||||
loopTimer.schedule((int) Math.max(10, animationIntervalMillis - timeDelta));
|
||||
} else {
|
||||
projection.clearCanvas();
|
||||
}
|
||||
}
|
||||
};
|
||||
loopTimer.schedule(animationIntervalMillis);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawSwarm() {
|
||||
Context2d ctxt = canvas.getContext2d();
|
||||
// clearing neds to be done with a little over zero, thanks IE
|
||||
ctxt.setGlobalAlpha(0.06);
|
||||
if (clearNextFrame) {
|
||||
// ctxt.setGlobalAlpha(1);
|
||||
// skip this frame, as it will contain an extreme delta due to the map movement
|
||||
clearNextFrame = false;
|
||||
} else {
|
||||
ctxt.setGlobalCompositeOperation("destination-out");
|
||||
|
||||
@@ -85,9 +85,9 @@ bearerToken = com.sap.sailing.server.security.BearerTokenOrBasicOrFormAuthentica
|
||||
# Secure access to additional apis used for SailRacer
|
||||
/api/v1/leaderboards/*/enableRaceLogForCompetitorRegistration/** = bearerToken
|
||||
/api/v1/leaderboards/*/denoteForTracking/** = bearerToken
|
||||
/api/v1/leaderboards/*/removeTrackedRace/** = bearerToken
|
||||
/api/v1/regattas/updateOrCreateSeries = bearerToken
|
||||
/api/v1/mark/addMarkToRegatta/** = bearerToken
|
||||
/api/v1/mark/addMarkFix/** = bearerToken
|
||||
/api/v1/mark/addCourseDefinitionToRaceLog/** = bearerToken
|
||||
/api/v1/wind/putWind/** = bearerToken
|
||||
|
||||
+24
-1
@@ -119,6 +119,8 @@ import com.sap.sailing.server.gateway.serialization.coursedata.impl.WaypointJson
|
||||
import com.sap.sailing.server.gateway.serialization.impl.CompetitorAndBoatJsonSerializer;
|
||||
import com.sap.sailing.server.gateway.serialization.impl.FlatGPSFixJsonSerializer;
|
||||
import com.sap.sailing.server.gateway.serialization.impl.MarkJsonSerializerWithPosition;
|
||||
import com.sap.sailing.server.operationaltransformation.RemoveAndUntrackRace;
|
||||
import com.sap.sailing.server.operationaltransformation.StopTrackingRace;
|
||||
import com.sap.sse.InvalidDateException;
|
||||
import com.sap.sse.common.Bearing;
|
||||
import com.sap.sse.common.Distance;
|
||||
@@ -845,6 +847,18 @@ public class LeaderboardsResource extends AbstractLeaderboardsResource {
|
||||
return bearingSum == null ? null : bearingSum.divide(size);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("{leaderboardName}/removeTrackedRace")
|
||||
public Response removeTrackedRace(@PathParam("leaderboardName") String leaderboardName,
|
||||
@QueryParam(RaceLogServletConstants.PARAMS_RACE_COLUMN_NAME) String raceColumnName,
|
||||
@QueryParam(RaceLogServletConstants.PARAMS_RACE_FLEET_NAME) String fleetName)
|
||||
throws MalformedURLException, IOException, InterruptedException {
|
||||
SecurityUtils.getSubject()
|
||||
.checkPermission(Permission.LEADERBOARD.getStringPermissionForObjects(Mode.UPDATE, leaderboardName));
|
||||
return stopOrRemoveTrackedRace(leaderboardName, raceColumnName, fleetName, true);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("{leaderboardName}/stoptracking")
|
||||
@@ -852,6 +866,11 @@ public class LeaderboardsResource extends AbstractLeaderboardsResource {
|
||||
@QueryParam(RaceLogServletConstants.PARAMS_RACE_COLUMN_NAME) String raceColumnName,
|
||||
@QueryParam(RaceLogServletConstants.PARAMS_RACE_FLEET_NAME) String fleetName) throws MalformedURLException, IOException, InterruptedException {
|
||||
SecurityUtils.getSubject().checkPermission(Permission.LEADERBOARD.getStringPermissionForObjects(Mode.UPDATE, leaderboardName));
|
||||
return stopOrRemoveTrackedRace(leaderboardName, raceColumnName, fleetName, false);
|
||||
}
|
||||
|
||||
private Response stopOrRemoveTrackedRace(String leaderboardName, String raceColumnName, String fleetName, boolean remove)
|
||||
throws MalformedURLException, IOException, InterruptedException {
|
||||
final Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName);
|
||||
final Response result;
|
||||
if (leaderboard == null) {
|
||||
@@ -869,7 +888,11 @@ public class LeaderboardsResource extends AbstractLeaderboardsResource {
|
||||
if (trackedRace != null) {
|
||||
final Regatta regatta = trackedRace.getTrackedRegatta().getRegatta();
|
||||
final RaceDefinition race = trackedRace.getRace();
|
||||
getService().stopTracking(regatta, race);
|
||||
if(remove) {
|
||||
getService().apply(new RemoveAndUntrackRace(trackedRace.getRaceIdentifier()));
|
||||
} else {
|
||||
getService().apply(new StopTrackingRace(trackedRace.getRaceIdentifier()));
|
||||
}
|
||||
jsonResult.put("regatta", regatta.getName());
|
||||
jsonResult.put("race", race.getName());
|
||||
jsonResultArray.add(jsonResult);
|
||||
|
||||
+1
-1
@@ -296,7 +296,7 @@ public class LeaderboardsResourceV2 extends AbstractLeaderboardsResource {
|
||||
break;
|
||||
case RACE_CURRENT_SPEED_OVER_GROUND_IN_KNOTS:
|
||||
name = "currentSpeedOverGround-kts";
|
||||
if (currentLegEntry != null) {
|
||||
if (currentLegEntry != null && currentLegEntry.currentSpeedOverGroundInKnots != null) {
|
||||
value = roundDouble(currentLegEntry.currentSpeedOverGroundInKnots, 2);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -205,6 +205,10 @@ name to make sure you get to the event you're interested in.
|
||||
<td><a href="leaderboardRaceStopTracking.html">/api/v1/leaderboards/{name}/stoptracking</a></td>
|
||||
<td>Stops tracking for a race</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="leaderboardRaceRemove.html">/api/v1/leaderboards/{name}/removeTrackedRace</a></td>
|
||||
<td>Removes a tracked race</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="leaderboardRaceDenoteForTracking.html">/api/v1/leaderboards/{name}/denoteForTracking</a></td>
|
||||
<td>Denotes a race for tracking</td>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
|
||||
<link href='../../../../fontface.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
|
||||
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
|
||||
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
|
||||
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
|
||||
<h2>URL: /api/v1/leaderboards/{name}/removeTrackedRace?race_column={name}&fleet={name}</h2>
|
||||
|
||||
<b>Description:</b>
|
||||
<p>Removes the tracked race(s) identified by the race_column and fleet parameters.
|
||||
</p>
|
||||
<br/>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Webservice Type:</td>
|
||||
<td>REST</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Output format:</td>
|
||||
<td>Json</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Mandatory parameters:</td>
|
||||
<td>None</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Optional parameters:</td>
|
||||
<td>race_column: if provided, restricts stopping to races in this race column; otherwise, races from all race columns will stop tracking
|
||||
if the fleet parameter matches; see below<p>
|
||||
fleet: if provided, stopping is restricted to races in this fleet only; otherwise, races from all fleets are stopped<p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Request method:</td>
|
||||
<td>POST</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Example 1:</td>
|
||||
<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>
|
||||
delivers an array with the regatta and race names of the races whose tracking got stopped:<pre>
|
||||
[{"regatta":"Smartphone Tracking Test 22.06","race":"Smartphone Tracking Test 22.06 K1 Blue"}]
|
||||
</pre></td>
|
||||
<td>Example 2:</td>
|
||||
<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>
|
||||
delivers an array with the regatta and race names of the races whose tracking got stopped:<pre>
|
||||
[{"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"}]
|
||||
</pre></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="height: 1em;"></div>
|
||||
<a href="index.html">Back to Web Service Overview</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
<b>Description:</b>
|
||||
<p>Gets the details of a competitor</p>
|
||||
Note that while nautically wind is usually specified in the direction it's coming <em>from</em>, for <br>
|
||||
computational purposes internally we measure the wind in the direction <em>to</em> which it blows. <br>
|
||||
<br/>
|
||||
<table>
|
||||
<tr>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Average=Promedio
|
||||
PairAverage=Promedio
|
||||
Median=Mediana
|
||||
PairCollecting=Identidad
|
||||
PairCollecting=Dispersión
|
||||
Sum=Totalizar
|
||||
Maximum=Máximo
|
||||
Minimum=Mínimo
|
||||
@@ -12,3 +12,4 @@ Knots=nudos
|
||||
Collecting=Colección
|
||||
ResultSignifier={0} ({1})
|
||||
SignifierInUnit={0} en {1}
|
||||
Identity=Identidad
|
||||
@@ -1,7 +1,7 @@
|
||||
Average=Moyenne
|
||||
PairAverage=Moyenne
|
||||
Median=Médiane
|
||||
PairCollecting=Identité
|
||||
PairCollecting=Nuage de points
|
||||
Sum=Somme
|
||||
Maximum=Maximum
|
||||
Minimum=Minimum
|
||||
@@ -12,3 +12,4 @@ Knots=nd
|
||||
Collecting=Collection
|
||||
ResultSignifier={0} ({1})
|
||||
SignifierInUnit={0} dans {1}
|
||||
Identity=Identité
|
||||
@@ -1,7 +1,7 @@
|
||||
Average=平均
|
||||
PairAverage=平均
|
||||
Median=中央値
|
||||
PairCollecting=ID
|
||||
PairCollecting=散布図
|
||||
Sum=合計
|
||||
Maximum=最大
|
||||
Minimum=最小
|
||||
@@ -12,3 +12,4 @@ Knots=kn
|
||||
Collecting=コレクション
|
||||
ResultSignifier={0} ({1})
|
||||
SignifierInUnit={0} / {1}
|
||||
Identity=ID
|
||||
@@ -1,7 +1,7 @@
|
||||
Average=Média
|
||||
PairAverage=Média
|
||||
Median=Mediana
|
||||
PairCollecting=Identidade
|
||||
PairCollecting=Dispersão
|
||||
Sum=Total
|
||||
Maximum=Máximo
|
||||
Minimum=Mínimo
|
||||
@@ -12,3 +12,4 @@ Knots=nó
|
||||
Collecting=Coleção
|
||||
ResultSignifier={0} ({1})
|
||||
SignifierInUnit={0} em {1}
|
||||
Identity=Identidade
|
||||
@@ -1,7 +1,7 @@
|
||||
Average=Среднее
|
||||
PairAverage=Среднее
|
||||
Median=Медиана
|
||||
PairCollecting=Идентичность
|
||||
PairCollecting=Точечная
|
||||
Sum=Сумма
|
||||
Maximum=Максимум
|
||||
Minimum=Минимум
|
||||
@@ -12,3 +12,4 @@ Knots=уз.
|
||||
Collecting=Коллекция
|
||||
ResultSignifier={0} ({1})
|
||||
SignifierInUnit={0} в {1}
|
||||
Identity=Идентичность
|
||||
@@ -1,7 +1,7 @@
|
||||
Average=平均值
|
||||
PairAverage=平均值
|
||||
Median=中值
|
||||
PairCollecting=身份
|
||||
PairCollecting=散点图
|
||||
Sum=求和
|
||||
Maximum=最大值
|
||||
Minimum=最小值
|
||||
@@ -12,3 +12,4 @@ Knots=kn
|
||||
Collecting=集合
|
||||
ResultSignifier={0} ({1})
|
||||
SignifierInUnit={0}(单位:{1})
|
||||
Identity=身份
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" path="src"/>
|
||||
<classpathentry exported="true" kind="lib" path="lib/isoparser-1.9.31.jar"/>
|
||||
<classpathentry exported="true" kind="lib" path="lib/isoparser-1.9.37.jar" sourcepath="lib/isoparser-1.9.37-sources.jar"/>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
|
||||
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
|
||||
<classpathentry kind="output" path="bin"/>
|
||||
|
||||
@@ -2,11 +2,12 @@ Manifest-Version: 1.0
|
||||
Bundle-ManifestVersion: 2
|
||||
Bundle-Name: Selenium
|
||||
Bundle-SymbolicName: org.mp4parser.isoparser
|
||||
Bundle-Version: 1.9.31
|
||||
Bundle-ClassPath: lib/isoparser-1.9.31.jar
|
||||
Bundle-Version: 1.9.37
|
||||
Bundle-ClassPath: .,
|
||||
lib/isoparser-1.9.37.jar
|
||||
Bundle-Vendor: Sebastian Annies
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
|
||||
Export-Package: com.sap.sailing.media,
|
||||
Export-Package: com.sap.sailing.media.mp4,
|
||||
org.mp4parser,
|
||||
org.mp4parser.boxes,
|
||||
org.mp4parser.boxes.iso14496.part12,
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
bin.includes = META-INF/,\
|
||||
lib/isoparser-1.9.31.jar
|
||||
lib/isoparser-1.9.37.jar
|
||||
jars.compile.order = .
|
||||
source.. = src/
|
||||
output.. = bin/
|
||||
Binary file not shown.
Binary file not shown.
@@ -8,6 +8,6 @@
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>org.mp4parser.isoparser</artifactId>
|
||||
<version>1.9.31</version>
|
||||
<version>1.9.37</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
</project>
|
||||
Executable → Regular
+39
-24
@@ -1,9 +1,7 @@
|
||||
package com.sap.sailing.media;
|
||||
package com.sap.sailing.media.mp4;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.util.Date;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
@@ -20,24 +18,24 @@ import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
public class MediaFileParser {
|
||||
public static void main(String[] args) throws MalformedURLException, ParserConfigurationException, SAXException, IOException {
|
||||
new MediaFileParser().checkMetadata(new File(args[0]));
|
||||
}
|
||||
|
||||
public void checkMetadata(File file) throws IOException, ParserConfigurationException, SAXException {
|
||||
boolean spherical = false;
|
||||
long durationInMillis = -1;
|
||||
Date recordStartedTimer = null;
|
||||
try (IsoFile isof = new IsoFile(file)) {
|
||||
recordStartedTimer = determineRecordingStart(isof);
|
||||
spherical = determine360(isof);
|
||||
durationInMillis = determineDurationInMillis(isof);
|
||||
System.out.println("Start: "+recordStartedTimer.toString()+", duration: "+durationInMillis+"ms; spherical: "+spherical);
|
||||
}
|
||||
}
|
||||
public class MP4MediaParser {
|
||||
/**
|
||||
* The relevant size at the start and end of a file, that most likely will contain all metadata
|
||||
*/
|
||||
public static final int REQUIRED_SIZE_IN_BYTES = 10000000;
|
||||
|
||||
private boolean determine360(IsoFile isof) throws ParserConfigurationException, SAXException, IOException {
|
||||
/**
|
||||
* Determine whether or not the provided {@link IsoFile ISO file} contains any "spherical" information within the
|
||||
* user defined box <code>moov[0]/trak[0]/uuid</code>.
|
||||
*
|
||||
* @param isof
|
||||
* {@link IsoFile ISO file} to analyze
|
||||
* @return <code>true</code> if spherical information are contained, <code>false</code> otherwise
|
||||
* @throws ParserConfigurationException
|
||||
* @throws SAXException
|
||||
* @throws IOException
|
||||
*/
|
||||
public static boolean determine360(IsoFile isof) throws ParserConfigurationException, SAXException, IOException {
|
||||
boolean spherical = false;
|
||||
UserBox uuidBox = Path.getPath(isof, "moov[0]/trak[0]/uuid");
|
||||
if (uuidBox != null) {
|
||||
@@ -56,19 +54,36 @@ public class MediaFileParser {
|
||||
return spherical;
|
||||
}
|
||||
|
||||
private long determineDurationInMillis(IsoFile isof) {
|
||||
/**
|
||||
* Tries to determine the {@link Long duration} of the provided {@link IsoFile ISO file} based on its
|
||||
* {@link IsoFile#getMovieBox() movie header}.
|
||||
*
|
||||
* @param isof
|
||||
* {@link IsoFile ISO file} to analyze
|
||||
* @return the determined duration in {@link Long millis}, <code>-1</code> if the duration could not be determined
|
||||
*/
|
||||
public static long determineDurationInMillis(IsoFile isof) {
|
||||
long duration = -1;
|
||||
MovieBox mbox = isof.getMovieBox();
|
||||
if (mbox != null) {
|
||||
MovieHeaderBox mhb = mbox.getMovieHeaderBox();
|
||||
if (mhb != null) {
|
||||
duration = mhb.getDuration()*1000 / mhb.getTimescale();
|
||||
duration = mhb.getDuration() * 1000 / mhb.getTimescale();
|
||||
}
|
||||
}
|
||||
return duration;
|
||||
}
|
||||
|
||||
private Date determineRecordingStart(IsoFile isof) {
|
||||
|
||||
/**
|
||||
* Tries to determine the {@link Date point in time} when the recording started from the provided {@link IsoFile ISO
|
||||
* file} based on its {@link IsoFile#getMovieBox() movie header}.
|
||||
*
|
||||
* @param isof
|
||||
* {@link IsoFile ISO file} to analyze
|
||||
* @return the determined {@link Date point in time} when the recording started, <code>null</code> if the recording
|
||||
* start could not be determined
|
||||
*/
|
||||
public static Date determineRecordingStart(IsoFile isof) {
|
||||
Date creationTime = null;
|
||||
MovieBox mbox = isof.getMovieBox();
|
||||
if (mbox != null) {
|
||||
@@ -0,0 +1,169 @@
|
||||
package com.sap.sailing.media.mp4;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.MappedByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.nio.file.Files;
|
||||
|
||||
/**
|
||||
* This class pretends to be a complete larger file, while only carrying the first 10 and last 10 megabytes. It is
|
||||
* specifically meant to be used for the mp4Parser and only supports the required subset for it. It extends FileChannel
|
||||
* as the library uses that to skip input for skipped boxes
|
||||
*/
|
||||
public class MP4ParserFakeFile extends FileChannel {
|
||||
private final long totalSizeOfFile;
|
||||
private long currentPos = 0;
|
||||
private byte[] startOfFileByteArray;
|
||||
private byte[] endOfFileByteArray;
|
||||
|
||||
public MP4ParserFakeFile(byte[] start, byte[] end, Long skipped) {
|
||||
this.startOfFileByteArray = start;
|
||||
this.endOfFileByteArray = end;
|
||||
this.totalSizeOfFile = start.length + end.length + skipped;
|
||||
}
|
||||
|
||||
public MP4ParserFakeFile(File tmp) {
|
||||
try (SeekableByteChannel inChannel = Files.newByteChannel(tmp.toPath())) {
|
||||
totalSizeOfFile = tmp.length();
|
||||
if (totalSizeOfFile < MP4MediaParser.REQUIRED_SIZE_IN_BYTES * 2) {
|
||||
throw new IllegalStateException(
|
||||
"The file is to small to be analysed < " + (MP4MediaParser.REQUIRED_SIZE_IN_BYTES * 2));
|
||||
} else {
|
||||
startOfFileByteArray = new byte[MP4MediaParser.REQUIRED_SIZE_IN_BYTES];
|
||||
endOfFileByteArray = new byte[MP4MediaParser.REQUIRED_SIZE_IN_BYTES];
|
||||
ByteBuffer startOfFileByteBuffer = ByteBuffer.wrap(startOfFileByteArray);
|
||||
ByteBuffer endOfFileByteBuffer = ByteBuffer.wrap(endOfFileByteArray);
|
||||
int startRead = inChannel.read(startOfFileByteBuffer);
|
||||
if (startRead != startOfFileByteBuffer.capacity()) {
|
||||
throw new IllegalArgumentException("Could not read fileStart");
|
||||
}
|
||||
inChannel.position(totalSizeOfFile - (MP4MediaParser.REQUIRED_SIZE_IN_BYTES + 1));
|
||||
int endRead = inChannel.read(endOfFileByteBuffer);
|
||||
if (endRead != endOfFileByteBuffer.capacity()) {
|
||||
throw new IllegalArgumentException("Could not read fileStart");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// File system error? Re-throwing, cause it cannot be handled here.
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer dst) throws IOException {
|
||||
// Ignore boundary crossings into nulled part, as it cannot be parsed in that case.
|
||||
// Only the actual movie data should be nulled.
|
||||
int read = 0;
|
||||
int toRead = dst.remaining();
|
||||
final long startOfEndSegment = totalSizeOfFile - (endOfFileByteArray.length + 1);
|
||||
if (currentPos < startOfFileByteArray.length) {
|
||||
// in start buffer, read one byte
|
||||
dst.put(startOfFileByteArray, (int) currentPos, toRead);
|
||||
read = toRead;
|
||||
} else if (currentPos < startOfEndSegment) {
|
||||
throw new IllegalArgumentException(
|
||||
"The nulled part should only be in the skipped boxes, else reading would be corrupt");
|
||||
} else {
|
||||
int remaining = (int) (totalSizeOfFile - currentPos);
|
||||
int relative = endOfFileByteArray.length - remaining;
|
||||
if (currentPos + toRead > totalSizeOfFile) {
|
||||
toRead = (int) (totalSizeOfFile - currentPos);
|
||||
}
|
||||
if (toRead > 0) {
|
||||
dst.put(endOfFileByteArray, relative, toRead);
|
||||
read = toRead;
|
||||
} else {
|
||||
read = -1;
|
||||
}
|
||||
}
|
||||
currentPos += read;
|
||||
return read;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int write(ByteBuffer src) throws IOException {
|
||||
throw new IllegalArgumentException("MP4 files should only be read");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long read(ByteBuffer[] dsts, int offset, int length) throws IOException {
|
||||
throw new IllegalArgumentException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long write(ByteBuffer[] srcs, int offset, int length) throws IOException {
|
||||
throw new IllegalArgumentException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long position() throws IOException {
|
||||
return currentPos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileChannel position(long newPosition) throws IOException {
|
||||
currentPos = newPosition;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws IOException {
|
||||
throw new IllegalArgumentException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileChannel truncate(long size) throws IOException {
|
||||
throw new IllegalArgumentException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void force(boolean metaData) throws IOException {
|
||||
throw new IllegalArgumentException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long transferTo(long position, long count, WritableByteChannel target) throws IOException {
|
||||
throw new IllegalArgumentException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long transferFrom(ReadableByteChannel src, long position, long count) throws IOException {
|
||||
throw new IllegalArgumentException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer dst, long position) throws IOException {
|
||||
throw new IllegalArgumentException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int write(ByteBuffer src, long position) throws IOException {
|
||||
throw new IllegalArgumentException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException {
|
||||
throw new IllegalArgumentException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileLock lock(long position, long size, boolean shared) throws IOException {
|
||||
throw new IllegalArgumentException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileLock tryLock(long position, long size, boolean shared) throws IOException {
|
||||
throw new IllegalArgumentException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void implCloseChannel() throws IOException {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.sap.sailing.media.mp4;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Date;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import org.mp4parser.AbstractBoxParser;
|
||||
import org.mp4parser.IsoFile;
|
||||
import org.mp4parser.PropertyBoxParserImpl;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
public class MediaFileParser {
|
||||
public static void main(String[] args)
|
||||
throws MalformedURLException, ParserConfigurationException, SAXException, IOException {
|
||||
new MediaFileParser().checkMetadata(new File(args[0]));
|
||||
}
|
||||
|
||||
public void checkMetadata(File file) throws IOException, ParserConfigurationException, SAXException {
|
||||
boolean spherical = false;
|
||||
long durationInMillis = -1;
|
||||
Date recordStartedTimer = null;
|
||||
AbstractBoxParser boxParserImpl = new PropertyBoxParserImpl();
|
||||
boxParserImpl.skippingBoxes(new String[] { "mdat" });
|
||||
try (IsoFile isof = new IsoFile(Files.newByteChannel(file.toPath()), boxParserImpl)) {
|
||||
recordStartedTimer = MP4MediaParser.determineRecordingStart(isof);
|
||||
spherical = MP4MediaParser.determine360(isof);
|
||||
durationInMillis = MP4MediaParser.determineDurationInMillis(isof);
|
||||
System.out.println("Start: " + recordStartedTimer.toString() + ", duration: " + durationInMillis
|
||||
+ "ms; spherical: " + spherical);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user