Merge branch 'master' into bug1301

Change-Id: Ide785c2c981f7968672c66d71bac3ca9838d3b2c
This commit is contained in:
Axel Uhl committed 2018-01-26 00:18:21 +01:00
commit b56d46f1c0
21 files changed
+866 -272

No files matched your search

@@ -3,19 +3,24 @@ package com.sap.sailing.domain.common.dto;
import java.io.Serializable;
import java.util.Date;
import com.sap.sse.common.Duration;
public class VideoMetadataDTO implements Serializable {
private static final long serialVersionUID = 1L;
private boolean canDownload;
private Date recordStartedTime;
private boolean spherical;
private String message;
private Duration duration;
public VideoMetadataDTO() {
}
public VideoMetadataDTO(boolean canDownload, boolean spherical, Date recordStartedTime, String message) {
public VideoMetadataDTO(boolean canDownload, Duration duration, boolean spherical, Date recordStartedTime,
String message) {
this.canDownload = canDownload;
this.duration = duration;
this.spherical = spherical;
this.recordStartedTime = recordStartedTime;
this.message = message;
@@ -25,6 +30,10 @@ public class VideoMetadataDTO implements Serializable {
return canDownload;
}
public Duration getDuration() {
return duration;
}
public boolean isSpherical() {
return spherical;
}
@@ -51,7 +51,7 @@ public class RaceBoardPerspectiveOwnSettings extends AbstractGenericSerializable
this.showCompetitorsChart = new BooleanSetting("showCompetitorsChart", this, false);
this.activeCompetitorsFilterSetName = new StringSetting("activeCompetitorsFilterSetName", this, null);
this.canReplayDuringLiveRaces = new BooleanSetting("canReplayDuringLiveRaces", this, false);
this.initialDurationAfterRaceStartInReplay = new DurationSetting("initialDurationAfterRaceStartInReplay", this, null);
this.initialDurationAfterRaceStartInReplay = new DurationSetting(PARAM_TIME_AFTER_RACE_START_AS_HOURS_COLON_MILLIS_COLON_SECONDS, this, null);
}
public RaceBoardPerspectiveOwnSettings(String activeCompetitorsFilterSetName, Boolean showLeaderboard,
@@ -25,12 +25,15 @@ public class DurationConverter implements ValueConverter<Duration> {
@Override
public String toStringValue(Duration value) {
return value == null ? null : Long.toString(value.asMillis());
// String value is used in URL where we accept the formats matching [[hh:]mm:]ss
// and the number of digits per element is not restricted to 2.
// This makes an integer number to simply be parsed as seconds value.
return value == null ? null : Long.toString((long)value.asSeconds());
}
@Override
public Duration fromStringValue(String stringValue) {
return stringValue == null ? null : new MillisecondsDurationImpl(Long.parseLong(stringValue));
return stringValue == null ? null : parseDuration(stringValue);
}
@Override
@@ -43,4 +46,22 @@ public class DurationConverter implements ValueConverter<Duration> {
public Value toValue(Duration value) {
return value == null ? null : new LongValue(value.asMillis());
}
/**
* Understands [[hh:]mm:]ss and parses into a {@link Duration}. If {@code durationAsString} is {@code null} then
* so is the result.
*/
private static Duration parseDuration(String durationAsString) {
final Duration result;
if (durationAsString == null) {
result = null;
} else {
long seconds = 0;
for (final String hhmmss : durationAsString.split(":")) {
seconds = 60*seconds + Long.valueOf(hhmmss);
}
result = new MillisecondsDurationImpl(1000l * seconds);
}
return result;
}
}
@@ -27,6 +27,7 @@ public interface MediaService extends RemoteService {
void updateRace(MediaTrack mediaTrack);
VideoMetadataDTO checkMetadata(String url);
VideoMetadataDTO checkMetadata(byte[] start, byte[] end, Long skipped);
VideoMetadataDTO checkMetadata(String url);
}
@@ -29,6 +29,8 @@ public interface MediaServiceAsync {
void updateRace(MediaTrack mediaTrack, AsyncCallback<Void> asyncCallback);
void checkMetadata(byte[] jStart, byte[] jEnd, Long skipped, AsyncCallback<VideoMetadataDTO> asyncCallback);
void checkMetadata(String url, AsyncCallback<VideoMetadataDTO> asyncCallback);
}
@@ -2049,6 +2049,7 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages,
String importFinishedGotoEvent();
String chooseAName();
String unexpectedErrorDuringFileImport();
String couldNotDownload(String url);
String windFinderWindSourceTypeName();
String windFinderWindSourceTypeTooltip();
String windFinder();
@@ -2037,6 +2037,7 @@ importFinishedGotoRaceboard=Open raceboard
importFinishedGotoEvent=Open event
chooseAName=Choose a name
unexpectedErrorDuringFileImport=An unexpected error occurred during file upload.
couldNotDownload=Could not download file: {0}
windFinderWindSourceTypeName=WindFinder
windFinderWindSourceTypeTooltip=Measured wind from one or more spots of www.windfinder.com
windFinder=WindFinder
@@ -2034,6 +2034,7 @@ importFinishedGotoRaceboard=Raceboard öffnen
importFinishedGotoEvent=Event öffnen
chooseAName=Namen auswählen
unexpectedErrorDuringFileImport=Ein unerwarteter Fehler ist beim Datei-Import aufgetreten.
couldNotDownload=Konnte Datei nicht laden: {0}
windFinderWindSourceTypeName=WindFinder
windFinderWindSourceTypeTooltip=Gemessener Wind von einer oder mehreren Stationen von www.windfinder.com
windFinder=WindFinder
@@ -0,0 +1,166 @@
package com.sap.sailing.gwt.ui.client.media;
import com.google.gwt.typedarrays.shared.Int8Array;
/**
* Helper class to obtain the last and the first REQUIRED_SIZE bytes of a resource defined by a given url.
*/
public class JSDownloadUtils {
private static final Double REQUIRED_SIZE = 1000000.0;
interface JSDownloadCallback {
void progress(Double current, Double total);
void error(Object msg);
void complete(Int8Array start, Int8Array end, Double skipped);
}
interface JSSizeCallback {
void size(Double total);
}
/**
* Downloads the required parts of the file, or if necessary the full file and slices the required parts from it.
*/
public static void getData(String url, JSDownloadCallback callback){
getFileSizeIfFastPath(url, new JSSizeCallback() {
@Override
public void size(Double total) {
if (total != 0 && total > 0) {
//used if range and accept headers are set correctly
getDataFast(url, callback, total, REQUIRED_SIZE);
} else {
//fallback will always work, if cors is supported, does not require any other server configuration
getDataSlow(url, callback, REQUIRED_SIZE);
}
}
});
}
/**
* Fast file download method based on range requests, only the first and the last REQUIRED_SIZE is loaded.
*/
private native static void getDataFast(String url, JSDownloadCallback callback, Double length, Double REQUIRED_SIZE) /*-{
try {
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.setRequestHeader("Range", "bytes=0-" + REQUIRED_SIZE);
xhr.responseType = "arraybuffer";
xhr.onprogress = function(evt) {
callback.@com.sap.sailing.gwt.ui.client.media.JSDownloadUtils.JSDownloadCallback::progress(Ljava/lang/Double;Ljava/lang/Double;)(evt.loaded, evt.total);
}
xhr.error = function(error) {
callback.@com.sap.sailing.gwt.ui.client.media.JSDownloadUtils.JSDownloadCallback::error(Ljava/lang/Object;)(error);
}
xhr.onreadystatechange = function() {
var state = xhr.readyState;
if (state == 4) {
if (xhr.response) {
var startLength = xhr.response.byteLength;
var start = new Int8Array(xhr.response);
try {
var xhr2 = new XMLHttpRequest();
xhr2.open("GET", url, true);
xhr2.setRequestHeader("Range", "bytes=" + (length - REQUIRED_SIZE));
xhr2.responseType = "arraybuffer";
xhr2.onprogress = function(evt) {
callback.@com.sap.sailing.gwt.ui.client.media.JSDownloadUtils.JSDownloadCallback::progress(Ljava/lang/Double;Ljava/lang/Double;)(evt.loaded, evt.total);
}
xhr2.error = function(error) {
callback.@com.sap.sailing.gwt.ui.client.media.JSDownloadUtils.JSDownloadCallback::error(Ljava/lang/Object;)(error);
}
xhr2.onreadystatechange = function() {
var state = xhr2.readyState;
if (state == 4) {
if (xhr2.response) {
var endLength = xhr2.response.byteLength;
var end = new Int8Array(xhr2.response);
var sparse = length - startLength - endLength;
callback.@com.sap.sailing.gwt.ui.client.media.JSDownloadUtils.JSDownloadCallback::complete(Lcom/google/gwt/typedarrays/shared/Int8Array;Lcom/google/gwt/typedarrays/shared/Int8Array;Ljava/lang/Double;)(start, end, sparse);
}
}
};
xhr2.send();
} catch (error) {
callback.@com.sap.sailing.gwt.ui.client.media.JSDownloadUtils.JSDownloadCallback::error(Ljava/lang/Object;)(error);
}
}
}
};
xhr.send();
} catch (error) {
callback.@com.sap.sailing.gwt.ui.client.media.JSDownloadUtils.JSDownloadCallback::error(Ljava/lang/Object;)(error);
}
}-*/;
/**
* Determines if a server can deliver a file with range requests, and if so returns the size of the file
*/
private native static void getFileSizeIfFastPath(String url, JSSizeCallback callback) /*-{
try {
var xhr = new XMLHttpRequest();
xhr.open("HEAD", url, true);
xhr.responseType = "arraybuffer";
xhr.error = function(error) {
callback.@com.sap.sailing.gwt.ui.client.media.JSDownloadUtils.JSSizeCallback::size(Ljava/lang/Double;)(-1.0);
}
xhr.onreadystatechange = function() {
var state = xhr.readyState;
if (state == 4) {
if (xhr.response) {
var length = xhr.getResponseHeader("Content-Length");
var range = xhr.getResponseHeader("Content-Range");
callback.@com.sap.sailing.gwt.ui.client.media.JSDownloadUtils.JSSizeCallback::size(Ljava/lang/Double;)(length);
} else {
callback.@com.sap.sailing.gwt.ui.client.media.JSDownloadUtils.JSSizeCallback::size(Ljava/lang/Double;)(-1.0);
}
}
};
xhr.send();
} catch (error) {
callback.@com.sap.sailing.gwt.ui.client.media.JSDownloadUtils.JSSizeCallback::size(Ljava/lang/Double;)(-1.0);
}
}-*/;
/**
* If range requests are not possible, the whole file is downloaded and then slices to the required parts
*/
private native static void getDataSlow(String url, JSDownloadCallback callback, Double REQUIRED_SIZE) /*-{
try {
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "arraybuffer";
xhr.onprogress = function(evt) {
callback.@com.sap.sailing.gwt.ui.client.media.JSDownloadUtils.JSDownloadCallback::progress(Ljava/lang/Double;Ljava/lang/Double;)(evt.loaded, evt.total);
}
xhr.error = function(error) {
callback.@com.sap.sailing.gwt.ui.client.media.JSDownloadUtils.JSDownloadCallback::error(Ljava/lang/Object;)(error);
}
xhr.onreadystatechange = function() {
var state = xhr.readyState;
if (state == 4) {
if (xhr.response) {
var length = xhr.response.byteLength;
var start = new Int8Array(xhr.response.slice(0, REQUIRED_SIZE));
var end = new Int8Array(xhr.response.slice(length - REQUIRED_SIZE));
var sparse = length - 2 * REQUIRED_SIZE;
callback.@com.sap.sailing.gwt.ui.client.media.JSDownloadUtils.JSDownloadCallback::complete(Lcom/google/gwt/typedarrays/shared/Int8Array;Lcom/google/gwt/typedarrays/shared/Int8Array;Ljava/lang/Double;)(start, end, sparse);
} else {
callback.@com.sap.sailing.gwt.ui.client.media.JSDownloadUtils.JSDownloadCallback::error(Ljava/lang/Object;)("No result");
}
}
};
xhr.send();
} catch (error) {
callback.@com.sap.sailing.gwt.ui.client.media.JSDownloadUtils.JSDownloadCallback::error(Ljava/lang/Object;)(error);
}
}-*/;
}
@@ -526,6 +526,15 @@ public class MediaPlayerManagerComponent extends AbstractComponent<MediaPlayerSe
return null;
}
}
private TimePoint getTrackingStartTime() {
Date startOfTracking = raceTimesInfoProvider.getRaceTimesInfo(getCurrentRace()).startOfTracking;
if (startOfTracking != null) {
return new MillisecondsTimePoint(startOfTracking);
} else {
return null;
}
}
@Override
public void closeFloatingVideo(MediaTrack videoTrack) {
@@ -606,8 +615,10 @@ public class MediaPlayerManagerComponent extends AbstractComponent<MediaPlayerSe
@Override
public void addMediaTrack() {
TimePoint raceStartTime = getRaceStartTime();
TimePoint defaultStartTime = raceStartTime;
TimePoint defaultStartTime = getRaceStartTime();
if (defaultStartTime == null) {
defaultStartTime = getTrackingStartTime();
}
NewMediaDialog dialog = new NewMediaDialog(mediaService, defaultStartTime,
MediaPlayerManagerComponent.this.stringMessages, this.getCurrentRace(),
new DialogCallback<MediaTrack>() {
@@ -1,5 +1,6 @@
package com.sap.sailing.gwt.ui.client.media;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
@@ -19,10 +20,10 @@ public class MediaSynchControl implements EditFlag {
private static final int FAST = 1000;
private static final int SLOW = 100;
private final MediaServiceAsync mediaService;
private final MediaSynchAdapter mediaSynchAdapter;
private final ErrorReporter errorReporter;
private final ErrorReporter errorReporter;
private final MediaTrack backupVideoTrack;
private final FlowPanel mainPanel;
@@ -31,19 +32,32 @@ public class MediaSynchControl implements EditFlag {
private final FlowPanel fineTuningPanel;
private final TextBox titleEdit;
private final Button editButton;
private final EditButtonProxy editButton;
private final Button previewButton;
private final Button saveButton;
private final Button discardButton;
private boolean isEditing = false;
public MediaSynchControl(MediaSynchAdapter mediaSynchAdapter, MediaServiceAsync mediaService, ErrorReporter errorReporter) {
/**
* We dont want to force the caller to use a specific button or anchor class for future flexibility
*/
interface EditButtonProxy {
void addAction(Runnable runnable);
void setTitle(String string);
void setEnabled(boolean b);
}
public MediaSynchControl(MediaSynchAdapter mediaSynchAdapter, MediaServiceAsync mediaService,
ErrorReporter errorReporter, EditButtonProxy editButtonProxy) {
this.mediaService = mediaService;
this.mediaSynchAdapter = mediaSynchAdapter;
this.errorReporter = errorReporter;
MediaTrack videoTrack = this.mediaSynchAdapter.getMediaTrack();
backupVideoTrack = new MediaTrack(videoTrack.title, videoTrack.url, videoTrack.startTime, videoTrack.duration, videoTrack.mimeType, videoTrack.assignedRaces);
MediaTrack videoTrack = this.mediaSynchAdapter.getMediaTrack();
backupVideoTrack = new MediaTrack(videoTrack.title, videoTrack.url, videoTrack.startTime, videoTrack.duration,
videoTrack.mimeType, videoTrack.assignedRaces);
mainPanel = new FlowPanel();
mainPanel.addStyleName("main-panel");
editPanel = new FlowPanel();
@@ -51,12 +65,14 @@ public class MediaSynchControl implements EditFlag {
fineTuningPanel.addStyleName("finetuning-panel");
commitPanel = new FlowPanel();
commitPanel.addStyleName("button-panel");
this.editButton = editButtonProxy;
titleEdit = new TextBox();
titleEdit.setText(videoTrack.title);
titleEdit.addStyleName("title-edit");
titleEdit.addKeyUpHandler(new KeyUpHandler() {
@Override
public void onKeyUp(KeyUpEvent event) {
String text = titleEdit.getText();
@@ -64,7 +80,7 @@ public class MediaSynchControl implements EditFlag {
updateUiState();
}
});
Button fastRewindButton = new Button("-1s &#171;", new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
@@ -89,22 +105,25 @@ public class MediaSynchControl implements EditFlag {
fastForward();
}
});
editButton = new Button("Edit", new ClickHandler() {
editButton.addAction(new Runnable() {
@Override
public void onClick(ClickEvent event) {
public void run() {
edit();
}
});
editButton.setTitle("Pauses race and decouples race from video playback. Use video controls, race time slider or fine tuning buttons for time alignment, then press Preview to re-couple race and video playback.");
editButton.setTitle(
"Pauses race and decouples race from video playback. Use video controls, race time slider or fine tuning buttons for time alignment, then press Preview to re-couple race and video playback.");
previewButton = new Button("Preview", new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
preview();
}
});
previewButton.setTitle("Re-couples race and video playback. If ok, press Save to write changes back to database. Press Cancel to reset the changes.");
previewButton.setTitle(
"Re-couples race and video playback. If ok, press Save to write changes back to database. Press Cancel to reset the changes.");
saveButton = new Button("Save", new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
@@ -123,17 +142,15 @@ public class MediaSynchControl implements EditFlag {
fineTuningPanel.add(slowRewindButton);
fineTuningPanel.add(slowForwardButton);
fineTuningPanel.add(fastForwardButton);
mainPanel.add(titleEdit);
editPanel.add(editButton);
editPanel.add(previewButton);
mainPanel.add(editPanel);
mainPanel.add(fineTuningPanel);
commitPanel.add(saveButton);
commitPanel.add(discardButton);
mainPanel.add(commitPanel);
updateUiState();
@@ -166,14 +183,14 @@ public class MediaSynchControl implements EditFlag {
pausePlayback();
mediaSynchAdapter.forceAlign();
updateUiState();
// For now, only start time can be changed.
// getMediaTrack().title = backupVideoTrack.title;
// getMediaTrack().url = backupVideoTrack.url;
// getMediaTrack().duration = backupVideoTrack.duration;
// For now, only start time can be changed.
// getMediaTrack().title = backupVideoTrack.title;
// getMediaTrack().url = backupVideoTrack.url;
// getMediaTrack().duration = backupVideoTrack.duration;
}
private void save() {
if (!backupVideoTrack.startTime.equals(mediaSynchAdapter.getMediaTrack().startTime)) {
mediaService.updateStartTime(mediaSynchAdapter.getMediaTrack(), new AsyncCallback<Void>() {
@@ -216,17 +233,19 @@ public class MediaSynchControl implements EditFlag {
}
}
mediaSynchAdapter.setControlsVisible(isEditing);
editButton.setEnabled(!isEditing);
previewButton.setEnabled(isEditing);
mainPanel.getElement().getStyle().setDisplay(this.isEditing() ? Display.BLOCK : Display.NONE);
boolean isDirty = isDirty();
saveButton.setEnabled(!isEditing && isDirty);
saveButton.setEnabled(isEditing || isDirty);
discardButton.setEnabled(isEditing || isDirty);
}
private boolean isDirty() {
return !backupVideoTrack.startTime.equals(mediaSynchAdapter.getMediaTrack().startTime) || !backupVideoTrack.title.equals(titleEdit.getText());
return !backupVideoTrack.startTime.equals(mediaSynchAdapter.getMediaTrack().startTime)
|| !backupVideoTrack.title.equals(titleEdit.getText());
}
private void fastForward() {
@@ -16,6 +16,7 @@ import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.media.client.Audio;
import com.google.gwt.media.client.MediaBase;
import com.google.gwt.typedarrays.shared.Int8Array;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
@@ -33,6 +34,7 @@ import com.sap.sailing.domain.common.dto.VideoMetadataDTO;
import com.sap.sailing.domain.common.media.MediaTrack;
import com.sap.sailing.gwt.ui.client.MediaServiceAsync;
import com.sap.sailing.gwt.ui.client.StringMessages;
import com.sap.sailing.gwt.ui.client.media.JSDownloadUtils.JSDownloadCallback;
import com.sap.sailing.gwt.ui.common.client.YoutubeApi;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.impl.MillisecondsDurationImpl;
@@ -140,13 +142,11 @@ public class NewMediaDialog extends DataEntryDialog<MediaTrack> {
}
native void addLoadMetadataHandler(MediaElement mediaElement) /*-{
var that = this;
mediaElement
.addEventListener(
'loadedmetadata',
function() {
that.@com.sap.sailing.gwt.ui.client.media.NewMediaDialog::loadedmetadata(Lcom/google/gwt/dom/client/MediaElement;)(mediaElement);
});
var that = this;
mediaElement.addEventListener('loadedmetadata',
function() {
that.@com.sap.sailing.gwt.ui.client.media.NewMediaDialog::loadedmetadata(Lcom/google/gwt/dom/client/MediaElement;)(mediaElement);
});
}-*/;
public void loadedmetadata(MediaElement mediaElement) {
@@ -242,6 +242,11 @@ public class NewMediaDialog extends DataEntryDialog<MediaTrack> {
remoteMp4WasStarted = false;
remoteMp4WasFinished = false;
refreshUI();
if (MimeType.mp4.equals(mediaTrack.mimeType)) {
processMp4(mediaTrack);
} else {
loadMediaDuration();
}
}
private String sliceBefore(String lastPathSegment, String slicer) {
@@ -265,13 +270,13 @@ public class NewMediaDialog extends DataEntryDialog<MediaTrack> {
}
private native void registerNativeMethods() /*-{
var that = this;
window.youtubeMetadataCallback = function(metadata) {
var title = metadata.entry.media$group.media$title.$t;
var duration = metadata.entry.media$group.yt$duration.seconds;
var description = metadata.entry.media$group.media$description.$t;
that.@com.sap.sailing.gwt.ui.client.media.NewMediaDialog::youtubeMetadataCallback(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)(title, duration, description);
}
var that = this;
window.youtubeMetadataCallback = function(metadata) {
var title = metadata.entry.media$group.media$title.$t;
var duration = metadata.entry.media$group.yt$duration.seconds;
var description = metadata.entry.media$group.media$description.$t;
that.@com.sap.sailing.gwt.ui.client.media.NewMediaDialog::youtubeMetadataCallback(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)(title, duration, description);
}
}-*/;
/**
@@ -280,28 +285,24 @@ public class NewMediaDialog extends DataEntryDialog<MediaTrack> {
* @param youtubeId
*/
public native void loadYoutubeMetadata(String youtubeId) /*-{
var that = this;
//Create temporary script element.
window.youtubeMetadataCallbackScript = document.createElement("script");
window.youtubeMetadataCallbackScript.src = "http://gdata.youtube.com/feeds/api/videos/"
+ youtubeId
+ "?alt=json&orderby=published&format=6&callback=youtubeMetadataCallback";
document.body.appendChild(window.youtubeMetadataCallbackScript);
// Cancel meta data capturing after has 2-seconds timeout.
setTimeout(
function() {
//Remove temporary script element.
if (window != null && window.youtubeMetadataCallbackScript != null) {
document.body
.removeChild(window.youtubeMetadataCallbackScript);
delete window.youtubeMetadataCallbackScript;
}
that.@com.sap.sailing.gwt.ui.client.media.NewMediaDialog::setUiEnabled(Z)(true);
}, 2000);
var that = this;
//Create temporary script element.
window.youtubeMetadataCallbackScript = document.createElement("script");
window.youtubeMetadataCallbackScript.src = "http://gdata.youtube.com/feeds/api/videos/"
+ youtubeId
+ "?alt=json&orderby=published&format=6&callback=youtubeMetadataCallback";
document.body.appendChild(window.youtubeMetadataCallbackScript);
// Cancel meta data capturing after has 2-seconds timeout.
setTimeout(
function() {
//Remove temporary script element.
if (window != null && window.youtubeMetadataCallbackScript != null) {
document.body.removeChild(window.youtubeMetadataCallbackScript);
delete window.youtubeMetadataCallbackScript;
}
that.@com.sap.sailing.gwt.ui.client.media.NewMediaDialog::setUiEnabled(Z)(true);
}, 2000);
}-*/;
public void youtubeMetadataCallback(String title, String durationInSeconds, String description) {
@@ -372,17 +373,84 @@ public class NewMediaDialog extends DataEntryDialog<MediaTrack> {
setUiEnabled(true);
remoteMp4WasFinished = true;
manualMimeTypeSelection(caught.getMessage(), mediaTrack);
loadMediaDuration();
}
});
}
/**
* For a given url that points to an mp4 video, attempts are made to parse the header, to determine the actual
* starttime of the video and to check for a 360° flag. The video will be analyzed by the backendserver, either via
* direct download, or proxied by the client, if a video is only available locally. If the video header cannot be
* read, default values are used instead.
*/
private void checkMetadata(String url, Label lbl, AsyncCallback<VideoMetadataDTO> asyncCallback) {
// check on server first
mediaService.checkMetadata(mediaTrack.url, new AsyncCallback<VideoMetadataDTO>() {
@Override
public void onSuccess(VideoMetadataDTO result) {
remoteMp4WasFinished = true;
if (result.isDownloadable()) {
setUiEnabled(true);
mp4MetadataResult(result);
} else {
checkMetadataOnClient(url, lbl, asyncCallback);
}
}
@Override
public void onFailure(Throwable caught) {
// try on client instead
checkMetadataOnClient(url, lbl, asyncCallback);
}
private void checkMetadataOnClient(String url, Label lbl, AsyncCallback<VideoMetadataDTO> asyncCallback) {
JSDownloadUtils.getData(url, new JSDownloadCallback() {
@Override
public void progress(Double current, Double total) {
lbl.setText(stringMessages.transferStarted() + " " + Math.round(current / 1024 / 1024) + "/"
+ Math.round(total / 1024 / 1024) + " MB");
infoLabel.setWidget(lbl);
}
@Override
public void error(Object msg) {
asyncCallback
.onSuccess(new VideoMetadataDTO(false, null, false, null, msg == null ? "" : msg.toString()));
}
@Override
public void complete(Int8Array start, Int8Array end, Double skipped) {
lbl.setText(stringMessages.analyze());
infoLabel.setWidget(lbl);
byte[] jStart = new byte[start.byteLength()];
for (int i = 0; i < start.byteLength(); i++) {
jStart[i] = start.get(i);
}
byte[] jEnd = new byte[end.byteLength()];
for (int i = 0; i < end.byteLength(); i++) {
jEnd[i] = end.get(i);
}
// Due to js represeting everything as 64double, the max safe file is around 4 petabytes
mediaService.checkMetadata(jStart, jEnd, skipped.longValue(), asyncCallback);
}
});
}
});
}
protected void mp4MetadataResult(VideoMetadataDTO result) {
if (!result.isDownloadable()) {
Window.alert("Could not download file " + mediaTrack.url);
Window.alert(stringMessages.couldNotDownload(mediaTrack.url));
manualMimeTypeSelection(result.getMessage(), mediaTrack);
} else {
mediaTrack.duration = result.getDuration();
mediaTrack.mimeType = result.isSpherical() ? MimeType.mp4panorama : MimeType.mp4;
mediaTrack.startTime = new MillisecondsTimePoint(result.getRecordStartedTime());
if(result.getRecordStartedTime() != null){
mediaTrack.startTime = new MillisecondsTimePoint(result.getRecordStartedTime());
}
refreshUI();
}
}
@@ -403,7 +471,7 @@ public class NewMediaDialog extends DataEntryDialog<MediaTrack> {
mediaTrack.mimeType = MimeType.valueOf(mimeTypeListBox.getSelectedValue());
}
});
mimeTypeListBox.setSelectedIndex(MimeType.mp4.equals(mediaTrack.mimeType)?0:1);
mimeTypeListBox.setSelectedIndex(MimeType.mp4.equals(mediaTrack.mimeType) ? 0 : 1);
fp.add(mimeTypeListBox);
infoLabel.setWidget(fp);
}
@@ -0,0 +1,9 @@
package com.sap.sailing.gwt.ui.client.media;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.ImageResource;
interface VideoContainerRessource extends ClientBundle {
@Source("com/sap/sailing/gwt/ui/client/images/edit.png")
ImageResource editIcon();
}
@@ -1,35 +1,70 @@
package com.sap.sailing.gwt.ui.client.media;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.PopupPanel;
import com.sap.sailing.gwt.ui.client.MediaServiceAsync;
import com.sap.sailing.gwt.ui.client.media.MediaSynchControl.EditButtonProxy;
import com.sap.sailing.gwt.ui.client.media.popup.PopoutWindowPlayer.PlayerCloseListener;
import com.sap.sailing.gwt.ui.client.media.shared.VideoSynchPlayer;
import com.sap.sse.gwt.client.ErrorReporter;
import com.sap.sse.gwt.client.dialog.WindowBox;
public class VideoFloatingContainer extends AbstractVideoContainer implements VideoContainer {
VideoContainerRessource res = GWT.create(VideoContainerRessource.class);
private static final int MIN_WIDTH = 420;
private final WindowBox dialogBox;
private final MediaSynchControl mediaSynchControl;
private final PopupPositionProvider popupPositionProvider;
public VideoFloatingContainer(VideoSynchPlayer videoPlayer, PopupPositionProvider popupPositionProvider, boolean showSynchControls, MediaServiceAsync mediaService, ErrorReporter errorReporter, PlayerCloseListener playerCloseListener, PopoutListener popoutListener) {
private Anchor edit;
public VideoFloatingContainer(VideoSynchPlayer videoPlayer, PopupPositionProvider popupPositionProvider,
boolean showSynchControls, MediaServiceAsync mediaService, ErrorReporter errorReporter,
PlayerCloseListener playerCloseListener, PopoutListener popoutListener) {
super(new FlowPanel(), videoPlayer, popoutListener, playerCloseListener);
this.popupPositionProvider = popupPositionProvider;
rootPanel.addStyleName("video-root-panel");
rootPanel.add(videoPlayer.getWidget());
if (showSynchControls) {
mediaSynchControl = new MediaSynchControl(this.videoPlayer, mediaService, errorReporter);
mediaSynchControl.widget().addStyleName("media-synch-control");
if (showSynchControls) {
this.edit = new Anchor();
this.edit.getElement().getStyle().setBackgroundImage("url('" + res.editIcon().getSafeUri().asString() + "')");
EditButtonProxy proxy = new EditButtonProxy() {
@Override
public void setTitle(String string) {
edit.setTitle(string);
}
@Override
public void addAction(Runnable runnable) {
edit.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
runnable.run();
}
});
}
@Override
public void setEnabled(boolean b) {
edit.getElement().getStyle().setDisplay(b ? Display.BLOCK : Display.NONE);
}
};
mediaSynchControl = new MediaSynchControl(this.videoPlayer, mediaService, errorReporter, proxy);
mediaSynchControl.widget().addStyleName("media-synch-control");
rootPanel.add(mediaSynchControl.widget());
} else {
mediaSynchControl = null;
@@ -37,14 +72,20 @@ public class VideoFloatingContainer extends AbstractVideoContainer implements Vi
videoPlayer.setEditFlag(mediaSynchControl);
this.dialogBox = new WindowBox(videoPlayer.getMediaTrack().title, videoPlayer.getMediaTrack().toString(), rootPanel, new WindowBox.PopoutHandler() {
this.dialogBox = new WindowBox(videoPlayer.getMediaTrack().title, videoPlayer.getMediaTrack().toString(),
rootPanel, new WindowBox.PopoutHandler() {
@Override
public void popout() {
VideoFloatingContainer.this.popoutListener.popoutVideo(VideoFloatingContainer.this.videoPlayer.getMediaTrack());
}
});
@Override
public void popout() {
VideoFloatingContainer.this.popoutListener
.popoutVideo(VideoFloatingContainer.this.videoPlayer.getMediaTrack());
}
});
// values required for js and youtube player to stay useable
dialogBox.setMinWidth(MIN_WIDTH);
if(edit != null){
dialogBox.addBeforeBarButtons(edit);
}
dialogBox.addCloseHandler(new CloseHandler<PopupPanel>() {
@Override
@@ -62,7 +103,7 @@ public class VideoFloatingContainer extends AbstractVideoContainer implements Vi
void show() {
dialogBox.show();
dialogBox.setVisible(false);
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
@@ -10,6 +10,7 @@ import com.sap.sailing.domain.common.RaceIdentifier;
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
import com.sap.sailing.domain.common.dto.LeaderboardDTO;
import com.sap.sailing.domain.common.dto.RaceColumnDTO;
import com.sap.sailing.gwt.settings.client.raceboard.RaceBoardPerspectiveOwnSettings;
import com.sap.sailing.gwt.ui.actions.GetLeaderboardByNameAction;
import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener;
import com.sap.sailing.gwt.ui.client.RaceTimePanel;
@@ -19,7 +20,11 @@ import com.sap.sailing.gwt.ui.client.shared.racemap.RaceCompetitorSet.Competitor
import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel;
import com.sap.sailing.gwt.ui.leaderboard.SingleRaceLeaderboardPanel;
import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO;
import com.sap.sse.common.Duration;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.impl.MillisecondsTimePoint;
import com.sap.sse.gwt.client.player.Timer;
import com.sap.sse.gwt.client.player.Timer.PlayModes;
/**
* Abstract base class for implementing a {@link RaceBoardMode}. The {@link #applyTo(RaceBoardPanel)} method registers
@@ -191,4 +196,25 @@ public abstract class AbstractRaceBoardMode implements RaceBoardMode, RaceTimesI
protected RaceColumnDTO getRaceColumn() {
return raceColumn;
}
protected void setTimerOrUseCustomStart(final TimePoint startPlayingAt) {
final PlayModes playMode = getTimer().getPlayMode();
TimePoint startPlayingAtOverride = null;
if (playMode != PlayModes.Live && getRaceTimesInfoForRace() != null
&& getRaceTimesInfoForRace().getStartOfRace() != null) {
final Date startOfRace = getRaceTimesInfoForRace().getStartOfRace();
final RaceBoardPerspectiveOwnSettings perspectiveOwnSettings = raceBoardPanel.getSettings()
.getPerspectiveOwnSettings();
if (perspectiveOwnSettings != null) {
final Duration initialDurationAfterRaceStartInReplay = perspectiveOwnSettings
.getInitialDurationAfterRaceStartInReplay();
if (initialDurationAfterRaceStartInReplay != null) {
startPlayingAtOverride = new MillisecondsTimePoint(
startOfRace.getTime() + initialDurationAfterRaceStartInReplay.asMillis());
}
}
}
getTimer().setTime(
startPlayingAtOverride != null ? startPlayingAtOverride.asMillis() : startPlayingAt.asMillis());
}
}
@@ -7,6 +7,7 @@ import com.sap.sailing.domain.common.DetailType;
import com.sap.sailing.gwt.settings.client.leaderboard.LeaderboardSettingsFactory;
import com.sap.sailing.gwt.settings.client.leaderboard.SingleRaceLeaderboardSettings;
import com.sap.sailing.gwt.ui.leaderboard.SingleRaceLeaderboardPanel;
import com.sap.sse.common.impl.MillisecondsTimePoint;
import com.sap.sse.gwt.client.player.Timer.PlayModes;
import com.sap.sse.gwt.client.player.Timer.PlayStates;
import com.sap.sse.security.ui.settings.ComponentContextWithSettingsStorageAndAdditionalSettingsLayers.OnSettingsReloadedCallback;
@@ -34,7 +35,7 @@ public class FullAnalysisMode extends AbstractRaceBoardMode {
if (getTimer().getPlayMode() == PlayModes.Live) {
getTimer().setPlayMode(PlayModes.Replay);
}
getTimer().setTime(getRaceTimesInfoForRace().endOfRace.getTime());
setTimerOrUseCustomStart(new MillisecondsTimePoint(getRaceTimesInfoForRace().endOfRace));
}
if (!leaderboardSettingsAdjusted && getLeaderboard() != null) {
leaderboardSettingsAdjusted = true;
@@ -69,7 +69,7 @@ public class PlayerMode extends AbstractRaceBoardMode {
}
if (startPlayingAt != null) {
timerAdjusted = true;
getTimer().setTime(startPlayingAt.asMillis());
setTimerOrUseCustomStart(startPlayingAt);
getTimer().play();
// we've done our adjustments; remove listener and let go
stopReceivingRaceTimesInfos();
@@ -1,6 +1,14 @@
package com.sap.sailing.gwt.ui.server;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URL;
import java.nio.channels.Channels;
import java.text.DateFormat;
@@ -9,6 +17,7 @@ import java.util.Date;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.mp4parser.IsoFile;
import org.mp4parser.boxes.UserBox;
@@ -20,6 +29,7 @@ import org.osgi.util.tracker.ServiceTracker;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
@@ -27,15 +37,19 @@ import com.sap.sailing.domain.common.dto.VideoMetadataDTO;
import com.sap.sailing.domain.common.media.MediaTrack;
import com.sap.sailing.gwt.ui.client.MediaService;
import com.sap.sailing.server.RacingEventService;
import com.sap.sse.common.Duration;
import com.sap.sse.common.impl.MillisecondsDurationImpl;
public class MediaServiceImpl extends RemoteServiceServlet implements MediaService {
// private static final Logger logger = Logger.getLogger(MediaServiceImpl.class.getName());
// private static final Logger logger = Logger.getLogger(MediaServiceImpl.class.getName());
private static final int METADATA_CONNECTION_TIMEOUT = 10000;
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
private ServiceTracker<RacingEventService, RacingEventService> racingEventServiceTracker;
private static final int REQUIRED_SIZE = 1000000;
private static final long serialVersionUID = -8917349579281305977L;
public MediaServiceImpl() {
@@ -49,12 +63,12 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
private RacingEventService racingEventService() {
return racingEventServiceTracker.getService();
}
@Override
public Iterable<MediaTrack> getMediaTracksForRace(RegattaAndRaceIdentifier regattaAndRaceIdentifier) {
return racingEventService().getMediaTracksForRace(regattaAndRaceIdentifier);
}
@Override
public Iterable<MediaTrack> getMediaTracksInTimeRange(RegattaAndRaceIdentifier regattaAndRaceIdentifier) {
return racingEventService().getMediaTracksInTimeRange(regattaAndRaceIdentifier);
@@ -106,42 +120,173 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi
@Override
public VideoMetadataDTO checkMetadata(String url) {
boolean canDownload = false;
VideoMetadataDTO response = null;
try {
URL input = new URL(url);
// check size and do rangerequests if possible
long fileSize = determineFileSize(input);
if (fileSize > 0) {
response = checkMetadataByPartialDownloads(input, fileSize);
} else {
response = checkMetadataByFullFileDownload(input);
}
} catch (Exception e) {
response = new VideoMetadataDTO(false, null, false, null, e.getMessage());
}
return response;
}
private long determineFileSize(URL input) throws IOException, ProtocolException {
//we need the size for efficient downloading
HttpURLConnection connection = (HttpURLConnection) input.openConnection();
connection.setConnectTimeout(METADATA_CONNECTION_TIMEOUT);
connection.setRequestMethod("HEAD");
long fileSize = -1;
try (InputStream inStream = connection.getInputStream()) {
fileSize = connection.getContentLengthLong();
} finally {
connection.disconnect();
}
connection = (HttpURLConnection) input.openConnection();
connection.setConnectTimeout(METADATA_CONNECTION_TIMEOUT);
connection.setRequestMethod("GET");
connection.setRequestProperty("Range", "bytes=0-100");
try (InputStream inStream = connection.getInputStream()) {
if(connection.getResponseCode() != 206){
fileSize = -1;
}
} finally {
connection.disconnect();
}
return fileSize;
}
private VideoMetadataDTO checkMetadataByPartialDownloads(URL input, long fileSize)
throws IOException, ProtocolException {
byte[] start = new byte[REQUIRED_SIZE];
byte[] end = new byte[REQUIRED_SIZE];
downloadPartOfFile(input, start, "bytes=0-" + REQUIRED_SIZE);
downloadPartOfFile(input, end, "bytes=" + (fileSize - REQUIRED_SIZE) + "-");
long skipped = fileSize - start.length - end.length;
return checkMetadata(start, end, skipped);
}
private void downloadPartOfFile(URL input, byte[] store, String range) throws IOException, ProtocolException {
HttpURLConnection connection;
connection = (HttpURLConnection) input.openConnection();
connection.setConnectTimeout(METADATA_CONNECTION_TIMEOUT);
connection.setRequestMethod("GET");
connection.setRequestProperty("Range", range);
try (InputStream inStream = connection.getInputStream()) {
DataInputStream dataInStream = new DataInputStream(inStream);
dataInStream.readFully(store);
} finally {
connection.disconnect();
}
}
private VideoMetadataDTO checkMetadataByFullFileDownload(URL input)
throws ParserConfigurationException, SAXException, IOException {
try (IsoFile isof = new IsoFile(Channels.newChannel(input.openStream()))) {
boolean canDownload = true;
Date recordStartedTimer = determineRecordingStart(isof);
Duration duration = determineDuration(isof);
boolean spherical = determine360(isof);
return new VideoMetadataDTO(canDownload, duration, spherical, recordStartedTimer, "");
}
}
@Override
public VideoMetadataDTO checkMetadata(byte[] start, byte[] end, Long skipped) {
File tmp = null;
boolean spherical = false;
Duration duration = null;
Date recordStartedTimer = null;
String message = "";
try {
URL input = new URL(url);
try (IsoFile isof = new IsoFile(Channels.newChannel(input.openStream()))) {
canDownload = true;
// MovieHeaderBox movieHeaderBox = Path.getPath(isof, "moov[0]/mvhd");
// System.out.println(movieHeaderBox.getCreationTime());
UserBox uuidBox = Path.getPath(isof, "moov[0]/trak[0]/uuid");
MovieBox mbox = isof.getMovieBox();
if (mbox != null) {
MovieHeaderBox mhb = mbox.getMovieHeaderBox();
if (mhb != null) {
recordStartedTimer = mhb.getCreationTime();
}
}
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(new ByteArrayInputStream(uuidBox.getData()));
NodeList childs = doc.getDocumentElement().getChildNodes();
for (int i = 0; i < childs.getLength(); i++) {
Node child = childs.item(i);
if (child.getNodeName().toLowerCase().contains(":spherical")) {
spherical = true;
}
}
tmp = createFileFromData(start, end, skipped);
try (IsoFile isof = new IsoFile(tmp)) {
recordStartedTimer = determineRecordingStart(isof);
spherical = determine360(isof);
duration = determineDuration(isof);
}
throw new RuntimeException("remote error test");
} catch (Exception e) {
message = e.getMessage();
} finally {
if (tmp != null) {
tmp.delete();
}
}
return new VideoMetadataDTO(canDownload, spherical, recordStartedTimer, message);
return new VideoMetadataDTO(true, duration, spherical, recordStartedTimer, message);
}
/**
* Creates a dummy mp4 file, only containing the start and the end, the middle is filled with 0 so that all file
* internal pointers from start to end are working
*/
private File createFileFromData(byte[] start, byte[] end, Long skipped) throws IOException, FileNotFoundException {
File tmp;
tmp = File.createTempFile("upload", "metadataCheck");
try (FileOutputStream fw = new FileOutputStream(tmp)) {
fw.write(start);
while (skipped > 0) {
// ensure that no absurd amount of memory is required, and that more then 4gb files can be analysed
if (skipped > 10000000) {
byte[] dummy = new byte[10000000];
fw.write(dummy);
skipped = skipped - 10000000;
} else {
byte[] dummy = new byte[skipped.intValue()];
fw.write(dummy);
skipped = skipped - skipped.intValue();
}
}
fw.write(end);
}
return tmp;
}
private boolean determine360(IsoFile isof) throws ParserConfigurationException, SAXException, IOException {
boolean spherical = false;
UserBox uuidBox = Path.getPath(isof, "moov[0]/trak[0]/uuid");
if (uuidBox != null) {
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(new ByteArrayInputStream(uuidBox.getData()));
NodeList childs = doc.getDocumentElement().getChildNodes();
for (int i = 0; i < childs.getLength(); i++) {
Node child = childs.item(i);
if (child.getNodeName().toLowerCase().contains(":spherical")) {
spherical = true;
}
}
}
return spherical;
}
private Duration determineDuration(IsoFile isof) {
Duration duration = null;
MovieBox mbox = isof.getMovieBox();
if (mbox != null) {
MovieHeaderBox mhb = mbox.getMovieHeaderBox();
if (mhb != null) {
duration = new MillisecondsDurationImpl(mhb.getDuration()*1000 / mhb.getTimescale());
}
}
return duration;
}
private Date determineRecordingStart(IsoFile isof) {
Date creationTime = null;
MovieBox mbox = isof.getMovieBox();
if (mbox != null) {
MovieHeaderBox mhb = mbox.getMovieHeaderBox();
if (mhb != null) {
creationTime = mhb.getCreationTime();
}
}
return creationTime;
}
}
@@ -24,6 +24,10 @@
<h2 class="articleSubheadline">January 2018</h2>
<ul class="bulletList">
<li>The auto detection for the media add dialog was improved. It will now attempt to only load the parts of a video, that are required to parse the tags. Additionally if the server cannot reach a video, the client (browser) will make an attempt to load the video. This allows to analyze locally hosted videos without the need to upload them.</li>
<li>In the raceboard the floating video box was improved, leading to a more solid resizing and moving behavior.</li>
<li>In the raceboard, the adding of videos has now an additional fallback startDate, if no startOfRace could be determined, and the file did not contain a proper creationtime tag, startOfTracking is used.</li>
<li>In the raceboard, the floating video box will now only display an edit button in the header, next to close and minimize. Upon clicking it, the box will expand and show the prior always shown editing buttons. This saves a lot of screenspace for logged in admin enabled users.
<li>Events have now an additional "WindFinder" tab in their configuration dialog.
WindFinder can provide reviewed subsets of their measurement spots and
gives an ID for such a reviewed spot collection. Enter one or more of such
@@ -39,12 +43,12 @@
<h2 class="articleSubheadline">December 2017</h2>
<ul class="bulletList">
<li>In the raceboard it is now possible to upload 360� videos in addition to the already supported video types.
360° Videos must use MimeType mp4panorama, 2d mp4 videos must still use mimetype mp4, as in older releases.
<li>In the raceboard it is now possible to upload 360&deg; videos in addition to the already supported video types.
360&deg; Videos must use MimeType mp4panorama, 2d mp4 videos must still use mimetype mp4, as in older releases.
The dialog will try an auto-detection for the mimetype upon entering the link.
Additionally, for mp4 videos it is now possible to use the start time, written in the file, as
an start time preset for the mediatrack, simplifying the video linking.</li>
<li> In Home it is now also possible to use 360� videos, for example as an highlight video</li>
Additionally, for mp4 videos it is now possible to use the start time, written in the file, as
a start time preset for the mediatrack, simplifying the video linking.</li>
<li>In Home it is now also possible to use 360&deg; videos, for example as a highlight video</li>
<li>Various Fix importers can now properly report errors and other states</li>
<li>Pairing list generation is now supported. Add competitors to a regatta, then use the new
action icons in the Leaderboards/Leaderboards panel in the administration console to generate
@@ -212,7 +216,7 @@
64-bit "double" value for all components such at latitude, longitude, speed over ground or course over
ground, "smaller" data types such as "int" and "short" are now employed. The reduction of accuracy
that this brings about is negligible given the accuracy of the tracking systems used. For example,
the latitude values are encoded such that while covering the full range from -90° to +90°, the
the latitude values are encoded such that while covering the full range from -90° to +90°, the
resolution is at 4.6mm. Similarly, speeds are represented in their compact form in such a way
that speeds up to 500kts can be represented at a resolution of 0.015kts.
</li>
@@ -3,7 +3,7 @@
}
.gwt-extras-WindowBox .dialogTopCenterInner {
background-color: #bbb;
background-color: transparent;
}
.gwt-extras-WindowBox .Caption {
@@ -60,6 +60,11 @@
background: url(images/resize_arrow.png) center center no-repeat;
}
.gwt-extras-WindowBox .dialogBottomLeftInner {
width: 10px;
height: 10px;
}
.gwt-extras-WindowBox .media-synch-control {
background: #efefef;
padding: 10px;
@@ -23,6 +23,8 @@ import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style.Cursor;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.DomEvent;
@@ -44,9 +46,9 @@ import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
/**
* Extension of the standard GWT DialogBox to provide a more "window"-like functionality. The WindowBox has
* two control-buttons in the top right corner of the header, which allow the box to be turned into a dedicated browser window
* ("popout") or the whole box to be closed. <br>
* Extension of the standard GWT DialogBox to provide a more "window"-like functionality. The WindowBox has two
* control-buttons in the top right corner of the header, which allow the box to be turned into a dedicated browser
* window ("popout") or the whole box to be closed. <br>
* <br>
* The WindowBox relies on the css settings of {@link DialogBox} for styling of the border and header. It also uses the
* following classes to style the additional elements:
@@ -83,7 +85,6 @@ public class WindowBox extends DialogBox {
CssResource windowBoxCss();
}
public interface PopoutHandler {
void popout();
}
@@ -102,59 +103,111 @@ public class WindowBox extends DialogBox {
private int minWidth = MIN_WIDTH;
private int minHeight = MIN_HEIGHT;
private int dragMode;
private enum DragMode {
NORTH_WEST_RESIZE(true, false, true, false), SOUTH_EAST_RESIZE(false, true, false, true), NORTH_RESIZE(true,
false, false, false), NORTH_EAST_RESIZE(true, false, false, true), WEST_RESIZE(false, false, true,
false), EAST_RESIZE(false, false, false, true), SOUTH_WEST_RESIZE(false, true, true,
false), SOUTH_RESIZE(false, true, false, false), MOVE(false, false, false, false);
private boolean north;
private boolean south;
private boolean west;
private boolean east;
DragMode(boolean north, boolean south, boolean west, boolean east) {
this.north = north;
this.south = south;
this.west = west;
this.east = east;
}
public boolean isNorth() {
return north;
}
public boolean isSouth() {
return south;
}
public boolean isWest() {
return west;
}
public boolean isEast() {
return east;
}
}
private DragMode dragMode;
private PopoutHandler popoutHandler;
private FlowPanel blocker;
private Grid ctrlGrid;
public void setMinHeight(int minHeight) {
this.minHeight = minHeight;
}
public void setMinWidth(int minWidth) {
this.minWidth = minWidth;
}
/**
* Static helper method to change the cursor for a given element when resizing is enabled.
*
* @param dm
* @param dragMode
* The code describing the position of the element in question
* @param element
* The {@link com.google.gwt.dom.client.Element} to set the cursor on
*/
protected static void updateCursor(int dm, com.google.gwt.dom.client.Element element) {
protected void updateCursor(DragMode dragMode, com.google.gwt.dom.client.Element element) {
Cursor cursor;
switch (dm) {
case 0:
cursor = Cursor.NW_RESIZE;
break;
if (dragMode != null)
switch (dragMode) {
case MOVE:
cursor = Cursor.MOVE;
break;
case 1:
cursor = Cursor.N_RESIZE;
break;
case NORTH_WEST_RESIZE:
cursor = Cursor.NW_RESIZE;
break;
case 2:
cursor = Cursor.NE_RESIZE;
break;
case NORTH_RESIZE:
cursor = Cursor.N_RESIZE;
break;
case 3:
cursor = Cursor.W_RESIZE;
break;
case NORTH_EAST_RESIZE:
cursor = Cursor.NE_RESIZE;
break;
case 5:
cursor = Cursor.E_RESIZE;
break;
case WEST_RESIZE:
cursor = Cursor.W_RESIZE;
break;
case 6:
cursor = Cursor.SW_RESIZE;
break;
case EAST_RESIZE:
cursor = Cursor.E_RESIZE;
break;
case 7:
cursor = Cursor.S_RESIZE;
break;
case SOUTH_WEST_RESIZE:
cursor = Cursor.SW_RESIZE;
break;
case 8:
cursor = Cursor.SE_RESIZE;
break;
case SOUTH_RESIZE:
cursor = Cursor.S_RESIZE;
break;
default:
case SOUTH_EAST_RESIZE:
cursor = Cursor.SE_RESIZE;
break;
default:
cursor = Cursor.AUTO;
break;
}
else {
cursor = Cursor.AUTO;
break;
}
element.getStyle().setCursor(cursor);
}
public WindowBox(String title, String titleTooltip, Widget content, PopoutHandler popoutHandler) {
@@ -163,6 +216,7 @@ public class WindowBox extends DialogBox {
this.popoutHandler = popoutHandler;
this.setStyleName("gwt-extras-WindowBox", true);
this.container = new FlowPanel();
super.setWidget(container);
this.close = new Anchor();
this.close.setStyleName("gwt-extras-dialog-close");
this.close.addClickHandler(new ClickHandler() {
@@ -177,45 +231,44 @@ public class WindowBox extends DialogBox {
onPopoutClick(event);
}
});
Grid ctrlGrid = null;
ctrlGrid = null;
if (popoutHandler != null) {
ctrlGrid = new Grid(1, 2);
ctrlGrid.setWidget(0, 0, this.minimize);
ctrlGrid.setWidget(0, 1, this.close);
ctrlGrid = new Grid(1, 3);
ctrlGrid.setWidget(0, 1, this.minimize);
ctrlGrid.setWidget(0, 2, this.close);
} else {
ctrlGrid = new Grid(1, 1);
ctrlGrid.setWidget(0, 0, this.close);
ctrlGrid = new Grid(1, 2);
ctrlGrid.setWidget(0, 1, this.close);
}
this.controlsPanel = new FlowPanel();
this.controlsPanel.setStyleName("gwt-extras-dialog-controls");
this.controlsPanel.add(ctrlGrid);
this.dragMode = -1;
this.container.add(this.controlsPanel);
setTitle(titleTooltip);
setText(title);
setWidget(content);
blocker = new FlowPanel();
blocker.getElement().getStyle().setZIndex(1000);
blocker.getElement().getStyle().setTop(0, Unit.PX);
blocker.getElement().getStyle().setLeft(0, Unit.PX);
blocker.getElement().getStyle().setOpacity(0.1f);
blocker.getElement().getStyle().setBackgroundColor("grey");
blocker.getElement().getStyle().setPosition(Position.ABSOLUTE);
dragResizeWidget(this, 0, 0);
}
public void addBeforeBarButtons(Widget widget) {
ctrlGrid.setWidget(0, 0, widget);
}
/**
* Sets the cursor to indicate resizability for a specified "drag-mode" (i.e. how the box is being resized) on the
* dialog box. The position is described by an integer, as follows:
*
* <pre>
* 0-- --1-- --2
* | |
*
* | |
* 3 -1 5
* | |
*
* | |
* 6-- --7-- --8
* </pre>
*
* passing <code>-1</code> resets the cursor to the default.
* dialog box. The position is described by an enum
*
* @param dragMode
*/
protected void updateCursor(int dragMode) {
protected void updateCursor(DragMode dragMode) {
updateCursor(dragMode, this.getElement());
com.google.gwt.dom.client.Element top = this.getCellElement(0, 1);
updateCursor(dragMode, top);
@@ -240,7 +293,7 @@ public class WindowBox extends DialogBox {
case Event.ONMOUSEMOVE:
case Event.ONMOUSEOVER:
case Event.ONMOUSEOUT:
if (this.dragMode >= 0 || calcDragMode(event.getClientX(), event.getClientY()) >= 0) {
if (this.dragMode != null || calcDragMode(event.getClientX(), event.getClientY()) != null) {
// paste'n'copy from Widget.onBrowserEvent
switch (DOM.eventGetType(event)) {
case Event.ONMOUSEOVER:
@@ -258,7 +311,7 @@ public class WindowBox extends DialogBox {
DomEvent.fireNativeEvent(event, this, this.getElement());
return;
}
if (this.dragMode < 0) {
if (this.dragMode == null) {
this.updateCursor(this.dragMode);
}
}
@@ -296,15 +349,16 @@ public class WindowBox extends DialogBox {
* @return A value in range [-1..8] describing the position of the mouse (see {@link #updateCursor(int)} for more
* information)
*/
protected int calcDragMode(int clientX, int clientY) {
protected DragMode calcDragMode(int clientX, int clientY) {
int tolerance = 5;
com.google.gwt.dom.client.Element resize = this.getCellElement(2, 2).getParentElement();
int xr = this.getRelX(resize, clientX);
int yr = this.getRelY(resize, clientY);
int w = resize.getClientWidth();
int h = resize.getClientHeight();
if ((xr >= 0 && xr <= w && yr >= -tolerance && yr <= h) || (yr >= 0 && yr <= h && xr >= -tolerance && xr <= w)) {
return 8;
if ((xr >= 0 && xr <= w && yr >= -tolerance && yr <= h)
|| (yr >= 0 && yr <= h && xr >= -tolerance && xr <= w)) {
return DragMode.SOUTH_EAST_RESIZE;
}
resize = this.getCellElement(2, 0).getParentElement();
xr = this.getRelX(resize, clientX);
@@ -313,33 +367,8 @@ public class WindowBox extends DialogBox {
h = resize.getClientHeight();
if ((xr >= 0 && xr <= w && yr >= -tolerance && yr <= h)
|| (yr >= 0 && yr <= h && xr >= 0 && xr <= w + tolerance)) {
return 6;
return DragMode.SOUTH_WEST_RESIZE;
}
/*
* resize = this.getCellElement(0, 2).getParentElement(); xr = this.getRelX(resize, clientX); yr =
* this.getRelY(resize, clientY);
*
* w = resize.getClientWidth(); h = resize.getClientHeight();
*
* if ((xr >= 0 && xr <= w && yr >= 0 && yr <= h + tolerance) || (yr >= 0 && yr <= h && xr >= -tolerance && xr
* <= w)) return 2;
*
* resize = this.getCellElement(0, 0).getParentElement(); xr = this.getRelX(resize, clientX); yr =
* this.getRelY(resize, clientY);
*
* w = resize.getClientWidth(); h = resize.getClientHeight();
*
* if ((xr >= 0 && xr <= w && yr >= 0 && yr <= h + tolerance) || (yr >= 0 && yr <= h && xr >= 0 && xr <= w +
* tolerance)) return 0;
*
* resize = this.getCellElement(0, 1).getParentElement(); xr = this.getRelX(resize, clientX); yr =
* this.getRelY(resize, clientY);
*
* w = resize.getClientWidth(); h = resize.getClientHeight();
*
* if (yr >= 0 && yr <= h) return 1;
*/
resize = this.getCellElement(1, 0).getParentElement();
xr = this.getRelX(resize, clientX);
yr = this.getRelY(resize, clientY);
@@ -348,7 +377,7 @@ public class WindowBox extends DialogBox {
h = resize.getClientHeight();
if (xr >= 0 && xr <= w)
return 3;
return DragMode.WEST_RESIZE;
resize = this.getCellElement(2, 1).getParentElement();
xr = this.getRelX(resize, clientX);
@@ -358,7 +387,7 @@ public class WindowBox extends DialogBox {
h = resize.getClientHeight();
if (yr >= 0 && yr <= h)
return 7;
return DragMode.SOUTH_RESIZE;
resize = this.getCellElement(1, 2).getParentElement();
xr = this.getRelX(resize, clientX);
@@ -368,9 +397,18 @@ public class WindowBox extends DialogBox {
h = resize.getClientHeight();
if (xr >= 0 && xr <= w) {
return 5;
return DragMode.EAST_RESIZE;
}
return -1;
resize = this.getCellElement(0, 1).getParentElement();
xr = this.getRelX(resize, clientX);
yr = this.getRelY(resize, clientY);
w = resize.getClientWidth();
h = resize.getClientHeight();
if (yr >= 0 && yr <= h && xr < w - 100)
return DragMode.MOVE;
return null;
}
/**
@@ -381,38 +419,59 @@ public class WindowBox extends DialogBox {
* @param dy
*/
protected void dragResizeWidget(PopupPanel panel, int dx, int dy) {
Widget widget = panel.getWidget();
int x = this.getPopupLeft();
int y = this.getPopupTop();
Widget widget = panel.getWidget();
// left + right
if ((this.dragMode % 3) != 1) {
int w = widget.getOffsetWidth();
// left edge -> move left
if ((this.dragMode % 3) == 0) {
x += dx;
w -= dx;
} else {
w += dx;
int w = widget.getOffsetWidth();
int h = widget.getOffsetHeight();
if (dragMode != null) {
// left + right
if (this.dragMode.isEast() || dragMode.isWest()) {
// left edge -> move left
if (dragMode.isWest()) {
x += dx;
w -= dx;
if (w < minWidth) {
int toAdd = minWidth - w;
x -= toAdd;
w += toAdd;
}
} else {
w += dx;
if (w < minWidth) {
w = minWidth;
}
}
}
w = w < this.minWidth ? this.minWidth : w;
widget.setWidth(w + "px");
}
// up + down
if ((this.dragMode / 3) != 1) {
int h = widget.getOffsetHeight();
// up = dy is negative
if ((this.dragMode / 3) == 0) {
y += dy;
h -= dy;
} else {
h += dy;
// up + down
if (dragMode.isNorth() || dragMode.isSouth()) {
// up = dy is negative
if (dragMode.isNorth()) {
y += dy;
h -= dy;
if (h < minHeight) {
int toAdd = minHeight - h;
h += toAdd;
y -= toAdd;
}
} else {
h += dy;
if (h < minHeight) {
h = minHeight;
}
}
h = h < this.minHeight ? this.minHeight : h;
}
h = h < this.minHeight ? this.minHeight : h;
widget.setHeight(h + "px");
}
if (this.dragMode / 3 == 0 || this.dragMode % 3 == 0) {
panel.setPopupPosition(x, y);
}
widget.setHeight(h + "px");
widget.setWidth(w + "px");
panel.setPopupPosition(x, y);
blocker.setWidth(getOffsetWidth() + "px");
blocker.setHeight(getOffsetHeight() + "px");
}
/*
@@ -422,8 +481,11 @@ public class WindowBox extends DialogBox {
*/
@Override
protected void beginDragging(MouseDownEvent event) {
int dm = this.calcDragMode(event.getClientX(), event.getClientY());
if (dm >= 0) {
container.add(blocker);
blocker.setWidth(getOffsetWidth() + "px");
blocker.setHeight(getOffsetHeight() + "px");
DragMode dm = this.calcDragMode(event.getClientX(), event.getClientY());
if (dm != null && dm != DragMode.MOVE) {
this.dragMode = dm;
DOM.setCapture(getElement());
this.dragX = event.getClientX();
@@ -441,7 +503,7 @@ public class WindowBox extends DialogBox {
*/
@Override
protected void continueDragging(MouseMoveEvent event) {
if (this.dragMode >= 0) {
if (this.dragMode != null && dragMode != DragMode.MOVE) {
this.updateCursor(this.dragMode);
int dx = event.getClientX() - this.dragX;
int dy = event.getClientY() - this.dragY;
@@ -450,7 +512,7 @@ public class WindowBox extends DialogBox {
dragResizeWidget(this, dx, dy);
} else {
// this updates the cursor when dragging is NOT activated
int dm = calcDragMode(event.getClientX(), event.getClientY());
DragMode dm = calcDragMode(event.getClientX(), event.getClientY());
this.updateCursor(dm);
super.continueDragging(event);
}
@@ -469,7 +531,7 @@ public class WindowBox extends DialogBox {
// is dragged.
NativeEvent nativeEvent = event.getNativeEvent();
if (!event.isCanceled() && (event.getTypeInt() == Event.ONMOUSEDOWN)
&& calcDragMode(nativeEvent.getClientX(), nativeEvent.getClientY()) >= 0) {
&& calcDragMode(nativeEvent.getClientX(), nativeEvent.getClientY()) != null) {
nativeEvent.preventDefault();
}
super.onPreviewNativeEvent(event);
@@ -482,11 +544,13 @@ public class WindowBox extends DialogBox {
*/
@Override
protected void endDragging(MouseUpEvent event) {
if (this.dragMode >= 0) {
container.remove(blocker);
if (this.dragMode != null && this.dragMode != DragMode.MOVE) {
DOM.releaseCapture(getElement());
this.dragX = event.getClientX() - this.dragX;
this.dragY = event.getClientY() - this.dragY;
this.dragMode = -1;
this.dragMode = null;
this.updateCursor(this.dragMode);
RootPanel.get().getElement().getStyle().setCursor(Cursor.AUTO);
} else {
@@ -501,18 +565,11 @@ public class WindowBox extends DialogBox {
*/
@Override
public void setWidget(Widget widget) {
if (this.container.getWidgetCount() == 0) {
// setup
this.container.add(this.controlsPanel);
// this.container.add(this.content);
super.setWidget(this.container);
} else {
// remove the old one
this.container.remove(1);
Widget old = getWidget();
if (old != null) {
this.container.remove(old);
}
this.container.add(widget);
// add the new widget
// this.content.add(widget);
}
/*
@@ -522,11 +579,17 @@ public class WindowBox extends DialogBox {
*/
@Override
public Widget getWidget() {
if (this.container.getWidgetCount() > 1) {
return this.container.getWidget(1);
} else {
return null;
for (int i = 0; i < container.getWidgetCount(); i++) {
Widget w = container.getWidget(i);
if (w == blocker) {
continue;
}
if (w == controlsPanel) {
continue;
}
return w;
}
return null;
}
/*