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:
Henri Kohlberg committed 2018-09-26 15:10:17 +02:00
1 parent 0dd4cd75b5
commit e4ea366a1e
47 files changed
+722 -380

No files matched your search

@@ -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)
@@ -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
@@ -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);
@@ -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();
@@ -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
@@ -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
@@ -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
@@ -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=更新送信のパスワード
@@ -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
@@ -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=Пароль для отправки обновлений
@@ -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=用于发送更新的密码
@@ -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);
}
/**
@@ -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);
}
}
}
@@ -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() {
@@ -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() {
@@ -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);
}
}
@@ -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());
@@ -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 {
@@ -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
@@ -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);
@@ -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}&amp;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 -1
View File
@@ -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/
+1 -1
View File
@@ -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>
@@ -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);
}
}
}