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