LADS.Util.makeNamespace("LADS.AnnotatedImage");
/**
* Representation of deepzoom image with associated media. Contains
* touch handlers and a method for creating associated media objects.
* @class LADS.AnnotatedImage
* @constructor
* @param {Object} options some options for the artwork and assoc media
* @return {Object} some public methods and variables
*/
LADS.AnnotatedImage = function (options) { // rootElt, doq, split, callback, shouldNotLoadHotspots) {
"use strict";
var // input options
root = options.root, // root of the artwork viewing page
doq = options.doq, // doq for the artwork
callback = options.callback, // called after associated media are retrieved from server
noMedia = options.noMedia, // should we not have assoc media? (set to true in artwork editor)
// constants
FIX_PATH = LADS.Worktop.Database.fixPath, // prepend server address to given path
// misc initialized variables
that = {}, // the object to be returned
artworkName = doq.Name, // artwork's title
associatedMedia = { guids: [] }, // object of associated media objects for this artwork, keyed by media GUID;
// also contains an array of GUIDs for cleaner iteration
// misc uninitialized variables
assetCanvas;
// get things rolling
init();
return {
getAssociatedMedia: getAssociatedMedia,
unload: unload,
dzManip: dzManip,
dzScroll: dzScroll,
openArtwork: openArtwork,
addAnimateHandler: addAnimateHandler
}
/**
* Return list of associatedMedia
* @method getAssociatedMedia
* @return {Object} associated media object
*/
function getAssociatedMedia() {
return associatedMedia;
}
/**
* Open the deepzoom image
* @method openArtwork
* @param {doq} doq artwork doq to open
* @return {Boolean} whether opening was successful
*/
function openArtwork(doq) {
if(!that.viewer || !doq || !doq.Metadata || !doq.Metadata.DeepZoom) {
debugger;
console.log("ERROR IN openDZI");
return false;
}
that.viewer.openDzi(FIX_PATH(doq.Metadata.DeepZoom));
return true;
}
/**
* Wrapper around Seadragon.Drawer.updateOverlay; moves an HTML element "overlay."
* Used mostly in conjunction with hotspot circles (this function is currently
* only called from ArtworkEditor.js)
* @method updateOverlay
* @param {HTML element} element the overlay element to move
* @param {Seadragon.OverlayPlacement} placement the new placement of the overlay
*/
function updateOverlay(element, placement) {
var $elt = $(element),
top = parseFloat($elt.css('top')),
left = parseFloat($elt.css('left'));
if (top && left) { // TODO is this check necessary?
that.viewer.drawer.updateOverlay(element, that.viewer.viewport.pointFromPixel(new Seadragon.Point(left, top)), placement);
}
}
/**
* Wrapper around Seadragon.Drawer.addOverlay; adds an HTML overlay to the seadragon
* canvas. Currently only used in ArtworkEditor.js.
* @method addOverlay
* @param {HTML element} element the overlay element to add
* @param {Seadragon.Point} point the point at which to add the overlay
* @param {Seadragon.OverlayPlacement} placement the placement at the given point
*/
function addOverlay(element, point, placement) {
if (!that.viewer.isOpen()) {
that.viewer.addEventListener('open', function () {
that.viewer.drawer.addOverlay(element, point, placement);
that.viewer.drawer.updateOverlay(element, point, placement);
});
} else {
that.viewer.drawer.addOverlay(element, point, placement);
that.viewer.drawer.updateOverlay(element, point, placement);
}
}
/**
* Wrapper around Seadragon.Drawer.removeOverlay. Removes an HTML overlay from the seadragon
* canvas.
* @method removeOverlay
* @param {HTML element} the ovlerlay element to remove
*/
function removeOverlay(element) {
if (!that.viewer.isOpen()) {
that.viewer.addEventListener('open', function () {
that.viewer.drawer.removeOverlay(element);
});
} else {
that.viewer.drawer.removeOverlay(element);
}
};
/**
* Unloads the seadragon viewer
* @method unload
*/
function unload() {
that.viewer && that.viewer.unload();
}
/**
* Manipulation/drag handler for makeManipulatable on the deepzoom image
* @method dzManip
* @param {Object} pivot location of the event (x,y)
* @param {Object} translation distance translated in x and y
* @oaram {Number} scale scale factor
*/
function dzManip(pivot, translation, scale) {
that.viewer.viewport.zoomBy(scale, that.viewer.viewport.pointFromPixel(new Seadragon.Point(pivot.x, pivot.y)), false);
that.viewer.viewport.panBy(that.viewer.viewport.deltaPointsFromPixels(new Seadragon.Point(-translation.x, -translation.y)), false);
that.viewer.viewport.applyConstraints();
}
/**
* Scroll/pinch-zoom handler for makeManipulatable on the deepzoom image
* @method dzScroll
* @param {Number} scale scale factor
* @param {Object} pivot location of event (x,y)
*/
function dzScroll(scale, pivot) {
that.viewer.viewport.zoomBy(scale, that.viewer.viewport.pointFromPixel(new Seadragon.Point(pivot.x, pivot.y)));
that.viewer.viewport.applyConstraints();
}
/**
* Initialize seadragon, set up handlers for the deepzoom image, load assoc media if necessary
* @method init
*/
function init() {
var viewerelt,
canvas;
if(Seadragon.Config) {
Seadragon.Config.visibilityRatio = 0.8; // TODO see why Seadragon.Config isn't defined; should it be?
}
viewerelt = $(document.createElement('div'));
viewerelt.attr('id', 'annotatedImageViewer');
viewerelt.on('mousedown scroll click mousemove resize', function(evt) {
evt.preventDefault();
});
root.append(viewerelt);
that.viewer = new Seadragon.Viewer(viewerelt[0]);
that.viewer.setMouseNavEnabled(false);
that.viewer.clearControls();
canvas = $(that.viewer.canvas);
canvas.addClass('artworkCanvasTesting');
LADS.Util.makeManipulatable(canvas[0], {
onScroll: function (delta, pivot) {
dzScroll(delta, pivot);
},
onManipulate: function (res) {
dzManip(res.pivot, res.translation, res.scale); // TODO change dzManip to just accept res
}
}, null, true); // NO ACCELERATION FOR NOW
assetCanvas = $(document.createElement('div'));
assetCanvas.attr('id', 'annotatedImageAssetCanvas');
root.append(assetCanvas);
// this is stupid, but it seems to work (for being able to reference zoomimage in artmode)
noMedia ? setTimeout(function() { callback && callback() }, 1) : loadAssociatedMedia(callback);
}
/**
* Adds an animation handler to the annotated image. This is used to allow the image to move
* when the minimap is manipulated.
* @method addAnimationHandler
* @param {Function} handler the handler to add
*/
function addAnimateHandler(handler) {
that.viewer.addEventListener("animation", handler);
}
/**
* Retrieves associated media from server and stores them in the
* associatedMedia array.
* @method {Function} callback function to call after loading associated media
*/
function loadAssociatedMedia(callback) {
var done = 0,
total;
LADS.Worktop.Database.getAssocMediaTo(doq.Identifier, mediaSuccess, null, mediaSuccess);
/**
* Success callback frunction for .getAssocMediaTo call above. If the list of media is
* non-null and non-empty, it gets the linq between each doq and the artwork
* @method mediaSuccess
* @param {Array} doqs the media doqs
*/
function mediaSuccess(doqs) {
var i;
total = doqs ? doqs.length : 0;
if (total > 0) {
for (i = 0; i < doqs.length; i++) {
LADS.Worktop.Database.getLinq(doq.Identifier, doqs[i].Identifier, createLinqSuccess(doqs[i]), null, createLinqSuccess(doqs[i]));
}
} else {
callback && callback(associatedMedia);
}
}
/**
* Helper function for the calls to .getLinq above. It accepts an assoc media doc and returns
* a success callback function that accepts a linq. Using this information, it creates a new
* hotspot from the doq and linq
* @method createLinqSuccess
* @param {doq} assocMedia the relevant associated media doq
*/
function createLinqSuccess(assocMedia) {
return function (linq) {
associatedMedia[assocMedia.Identifier] = createMediaObject(assocMedia, linq);
associatedMedia.guids.push(assocMedia.Identifier);
if (++done >= total && callback) {
callback(associatedMedia);
}
}
}
}
/**
* Creates an associated media object to be added to associatedMedia.
* This object contains methods that could be called in Artmode.js or
* ArtworkEditor.js. This could be in its own file.
* @method createMediaObject
* @param {mdoq} doq the media doq
* @param {linq} linq the linq between the media doq and the artwork doq
* @return {Object} some public methods to be used in other files
*/
function createMediaObject(mdoq, linq) {
var // DOM-related
outerContainer = $(document.createElement('div')).addClass('mediaOuterContainer'),
innerContainer = $(document.createElement('div')).addClass('mediaInnerContainer'),
mediaContainer = $(document.createElement('div')).addClass('mediaMediaContainer'),
rootHeight = $('#tagRoot').height(),
rootWidth = $('#tagRoot').width(),
// constants
IS_HOTSPOT = linq.Metadata.Type ? (linq.Metadata.Type === "Hotspot") : false,
X = parseFloat(linq.Offset._x),
Y = parseFloat(linq.Offset._y),
TITLE = LADS.Util.htmlEntityDecode(mdoq.Name),
CONTENT_TYPE = mdoq.Metadata.ContentType,
SOURCE = mdoq.Metadata.Source,
DESCRIPTION = LADS.Util.htmlEntityDecode(mdoq.Metadata.Description),
THUMBNAIL = mdoq.Metadata.Thumbnail,
RELATED_ARTWORK = false,
// misc initialized variables
mediaHidden = true,
currentlySeeking = false,
// misc uninitialized variables
circle,
position,
mediaLoaded,
mediaHidden,
mediaElt,
titleDiv,
descDiv,
thumbnailButton,
play;
// get things rolling
initMediaObject();
/**
* Initialize various parts of the media object: UI, manipulation handlers
* @method initMediaObject
*/
function initMediaObject() {
// set up divs for the associated media
outerContainer.css('width', Math.min(Math.max(250, (rootWidth / 5)), 450) + 'px');
innerContainer.css('backgroundColor', 'rgba(0,0,0,0.65)');
if (TITLE) {
titleDiv = $(document.createElement('div'));
titleDiv.addClass('annotatedImageMediaTitle');
titleDiv.text(TITLE);
innerContainer.append(titleDiv);
}
innerContainer.append(mediaContainer);
if (DESCRIPTION) {
descDiv = $(document.createElement('div'));
descDiv.addClass('annotatedImageMediaDescription');
descDiv.html(Autolinker.link(DESCRIPTION, {email: false, twitter: false}));
innerContainer.append(descDiv);
}
if (RELATED_ARTWORK) {
// TODO append related artwork button here
}
outerContainer.append(innerContainer);
assetCanvas.append(outerContainer);
outerContainer.hide();
// create hotspot circle if need be
if (IS_HOTSPOT) {
circle = $(document.createElement("img"));
position = new Seadragon.Point(X, Y);
circle.attr('src', tagPath + 'images/icons/hotspot_circle.svg');
circle.addClass('annotatedImageHotspotCircle');
root.append(circle);
}
// allows asset to be dragged, despite the name
LADS.Util.disableDrag(outerContainer);
// register handlers
LADS.Util.makeManipulatable(outerContainer[0], {
onManipulate: mediaManip,
onScroll: mediaScroll
}, null, true); // NO ACCELERATION FOR NOW
}
/**
* Initialize any media controls
* @method initMediaControls
* @param {HTML element} elt video or audio element
*/
function initMediaControls() {
var elt = mediaElt,
$elt = $(elt),
controlPanel = $(document.createElement('div')).addClass('annotatedImageMediaControlPanel'),
vol = $(document.createElement('img')).addClass('mediaControls'),
seekBar,
timeContainer = $(document.createElement('div')),
currentTimeDisplay = $(document.createElement('span')).addClass('mediaControls'),
playHolder = $(document.createElement('div')),
volHolder = $(document.createElement('div')),
sliderContainer = $(document.createElement('div')),
sliderPoint = $(document.createElement('div'));
controlPanel.attr('id', 'media-control-panel-' + mdoq.Identifier);
play = $(document.createElement('img')).addClass('mediaControls');
play.attr('src', tagPath + 'images/icons/PlayWhite.svg');
vol.attr('src', tagPath+'images/icons/VolumeUpWhite.svg');
currentTimeDisplay.text("00:00");
// TODO move this css to styl file
play.css({
'position': 'relative',
'height': '20px',
'width': '20px',
'display': 'inline-block',
});
playHolder.css({
'position': 'relative',
'height': '20px',
'width': '20px',
'display': 'inline-block',
'margin': '0px 1% 0px 1%',
});
sliderContainer.css({
'position': 'absolute',
'height': '7px',
'width': '100%',
'left': '0px',
'bottom': '0px'
});
sliderPoint.css({
'position': 'absolute',
'height': '100%',
'background-color': '#3cf',
'width': '0%',
'left': '0%'
});
vol.css({
'height': '20px',
'width': '20px',
'position': 'relative',
'display': 'inline-block',
});
volHolder.css({
'height': '20px',
'width': '20px',
'position': 'absolute',
'right': '5px',
'top': '0px'
});
timeContainer.css({
'height': '20px',
'width': '40px',
'right': volHolder.width() + 25 + 'px',
'position': 'absolute',
'vertical-align': 'top',
'padding': '0',
'display': 'inline-block',
'overflow': 'hidden',
});
playHolder.append(play);
sliderContainer.append(sliderPoint);
volHolder.append(vol);
// set up handlers
play.on('click', function () {
if (elt.paused) {
elt.play();
play.attr('src', tagPath + 'images/icons/PauseWhite.svg');
} else {
elt.pause();
play.attr('src', tagPath + 'images/icons/PlayWhite.svg');
}
});
vol.on('click', function () {
if (elt.muted) {
elt.muted = false;
vol.attr('src', tagPath + 'images/icons/VolumeUpWhite.svg');
} else {
elt.muted = true;
vol.attr('src', tagPath + 'images/icons/VolumeDownWhite.svg');
}
});
$elt.on('ended', function () {
elt.pause();
play.attr('src', tagPath + 'images/icons/PlayWhite.svg');
});
sliderContainer.on('mousedown', function(evt) {
var time = elt.duration * (evt.offsetX / sliderContainer.width()),
origPoint = evt.pageX,
origTime = elt.currentTime,
timePxRatio = elt.duration / sliderContainer.width(),
currTime = Math.max(0, Math.min(elt.duration, origTime)),
currPx = currTime / timePxRatio,
minutes = Math.floor(currTime / 60),
seconds = Math.floor(currTime % 60),
adjMin = (minutes < 10) ? '0'+minutes : minutes,
adjSec = (seconds < 10) ? '0'+seconds : seconds;
evt.stopPropagation();
if(!isNaN(time)) {
elt.currentTime = time;
}
$('body').on('mousemove.seek', function(e) {
var currPoint = e.pageX,
timeDiff = (currPoint - origPoint) * timePxRatio;
currTime = Math.max(0, Math.min(video.duration, origTime + timeDiff));
currPx = currTime / timePxRatio;
minutes = Math.floor(currTime / 60);
seconds = Math.floor(currTime % 60);
adjMin = (minutes < 10) ? '0'+minutes : minutes;
adjSec = (seconds < 10) ? '0'+seconds : seconds;
if(!isNaN(currTime)) {
currentTimeDisplay.text(adjMin + ":" + adjSec);
elt.currentTime = currTime;
sliderPoint.css('width', 100*(currPx / sliderContainer.width()) + '%');
}
});
$('body').on('mouseup.seek mouseleave.seek', function() {
$('body').off('mouseup.seek mouseleave.seek mousemove.seek');
if(!isNaN(currTime)) {
currentTimeDisplay.text(adjMin + ":" + adjSec);
elt.currentTime = currTime;
sliderPoint.css('width', 100*(currPx / sliderContainer.width()) + '%');
}
});
});
// Update the seek bar as the video plays
$elt.on("timeupdate", function () {
var value = 100 * elt.currentTime / elt.duration,
timePxRatio = elt.duration / sliderContainer.width(),
currPx = elt.currentTime / timePxRatio,
minutes = Math.floor(elt.currentTime / 60),
seconds = Math.floor(elt.currentTime % 60),
adjMin = (minutes < 10) ? '0' + minutes : minutes,
adjSec = (seconds < 10) ? '0' + seconds : seconds;
if(!isNaN(elt.currentTime)) {
currentTimeDisplay.text(adjMin + ":" + adjSec);
sliderPoint.css('width', 100*(currPx / sliderContainer.width()) + '%');
}
});
mediaContainer.append(elt);
mediaContainer.append(controlPanel);
controlPanel.append(playHolder);
controlPanel.append(sliderContainer);
timeContainer.append(currentTimeDisplay);
controlPanel.append(timeContainer);
controlPanel.append(volHolder);
}
/**
* Load the actual image/video/audio; this can take a while if there are
* a lot of media, so just do it when the thumbnail button is clicked
* @method createMediaElements
*/
function createMediaElements() {
var $mediaElt,
img;
if(!mediaLoaded) {
mediaLoaded = true;
} else {
return;
}
if (CONTENT_TYPE === 'Image') {
img = document.createElement('img');
img.src = FIX_PATH(SOURCE);
$(img).css({
position: 'relative',
width: '100%',
height: 'auto'
});
mediaContainer.append(img);
mediaLoaded = true;
} else if (CONTENT_TYPE === 'Video') {
mediaElt = document.createElement('video');
$mediaElt = $(mediaElt);
$mediaElt.attr({
preload: 'none',
poster: (THUMBNAIL && !THUMBNAIL.match(/.mp4/)) ? FIX_PATH(THUMBNAIL) : '',
src: FIX_PATH(SOURCE),
type: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
controls: false
});
// TODO need to use <source> tags rather than setting the source and type of the
// video in the <video> tag's attributes; see video player code
$mediaElt.css({
position: 'relative',
width: '100%'
});
initMediaControls(mediaElt);
} else if (CONTENT_TYPE === 'Audio') {
mediaElt = document.createElement('audio');
$mediaElt = $(mediaElt);
$mediaElt.attr({
preload: 'none',
type: 'audio/mp3',
src: FIX_PATH(SOURCE),
controls: false
});
initMediaControls(mediaElt);
}
}
/**
* Drag/manipulation handler for associated media
* @method mediaManip
* @param {Object} res object containing hammer event info
*/
function mediaManip(res) {
if (currentlySeeking || !res) {
return;
}
var scale = res.scale,
trans = res.translation,
pivot = res.pivot,
t = parseFloat(outerContainer.css('top')),
l = parseFloat(outerContainer.css('left')),
w = outerContainer.width(),
h = outerContainer.height(),
newW = w * scale,
maxW,
minW;
// these values are somewhat arbitrary; TODO determine good values
if (CONTENT_TYPE === 'Image') {
maxW = 3000;
minW = 200;
} else if (CONTENT_TYPE === 'Video') {
maxW = 1000;
minW = 250;
} else if (CONTENT_TYPE === 'Audio') {
maxW = 800;
minW = 250;
}
// constrain new width
if(newW < minW || maxW < newW) {
scale = 1;
newW = Math.min(maxW, Math.max(minW, newW));
}
// zoom from touch point: change left and top of outerContainer
if ((0 < t + h) && (t < rootHeight) && (0 < l + w) && (l< rootWidth)) {
outerContainer.css("top", (t + trans.y + (1 - scale) * pivot.y) + "px");
outerContainer.css("left", (l + trans.x + (1 - scale) * pivot.x) + "px");
} else {
hideMediaObject();
pauseResetMediaObject();
return;
}
// zoom from touch point: change width and height of outerContainer
outerContainer.css("width", newW + "px");
outerContainer.css("height", "auto");
// TODO this shouldn't be necessary; style of controls should take care of it
// if ((CONTENT_TYPE === 'Video' || CONTENT_TYPE === 'Audio') && scale !== 1) {
// resizeControlElements();
// }
}
/**
* Zoom handler for associated media (e.g., for mousewheel scrolling)
* @method onScroll
* @param {Number} scale scale factor
* @param {Object} pivot point of contact
*/
function mediaScroll(scale, pivot) {
mediaManip({
scale: scale,
translation: {
x: 0,
y: 0
},
pivot: pivot
});
}
/**
* Show the associated media on the seadragon canvas. If the media is not
* a hotspot, show it in a slightly random position.
* @method showMediaObject
*/
function showMediaObject() {
var t,
l,
h = outerContainer.height(),
w = outerContainer.width();
if(IS_HOTSPOT) {
circle.css('visibility', 'visible');
addOverlay(circle[0], position, Seadragon.OverlayPlacement.TOP_LEFT);
that.viewer.viewport.panTo(position, false);
t = Math.max(10, (rootHeight - h)/2); // tries to put middle of outer container at circle level
l = rootWidth/2 + circle.width()*3/4;
} else {
t = rootHeight * 1/10 + Math.random() * rootHeight * 2/10;
l = rootWidth * 3/10 + Math.random() * rootWidth * 2/10;
}
outerContainer.css({
'top': t + "px",
'left': l + "px",
'position': "absolute",
'z-index': 1000,
'pointer-events': 'all'
});
outerContainer.show();
assetCanvas.append(outerContainer);
if(!thumbnailButton) {
thumbnailButton = $('#thumbnailButton-' + mdoq.Identifier);
}
thumbnailButton.css({
'color': 'black',
'background-color': 'rgba(255,255,255, 0.3)'
});
// TODO is this necessary?
// if ((info.contentType === 'Video') || (info.contentType === 'Audio')) {
// resizeControlElements();
// }
mediaHidden = false;
}
/**
* Hide the associated media
* @method hideMediaObject
*/
function hideMediaObject() {
pauseResetMediaObject();
IS_HOTSPOT && removeOverlay(circle[0]);
outerContainer.hide();
mediaHidden = true;
if(!thumbnailButton) {
thumbnailButton = $('#thumbnailButton-' + mdoq.Identifier);
}
thumbnailButton.css({
'color': 'white',
'background-color': ''
});
}
/**
* Show if hidden, hide if shown
* @method toggleMediaObject
*/
function toggleMediaObject() {
mediaHidden ? showMediaObject() : hideMediaObject();
}
/**
* Returns whether the media object is visible
* @method isVisible
* @return {Boolean}
*/
function isVisible() {
return !mediaHidden;
}
/**
* Pauses and resets (to time 0) the media if the content type is video or audio
* @pauseResetMediaObject
*/
function pauseResetMediaObject() {
if(!mediaElt || mediaElt.readyState < 4) { // see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement
return;
}
mediaElt.currentTime = 0;
mediaElt.pause();
play.attr('src', tagPath + 'images/icons/PlayWhite.svg');
}
return {
doq: mdoq,
linq: linq,
show: showMediaObject,
hide: hideMediaObject,
create: createMediaElements,
pauseReset: pauseResetMediaObject,
toggle: toggleMediaObject,
createMediaElements: createMediaElements,
isVisible: isVisible
};
}
};