openseadragon/src/viewer.js

903 lines
28 KiB
JavaScript
Raw Normal View History

(function( $ ){
/**
* OpenSeadragon Viewer
*
* The main point of entry into creating a zoomable image on the page.
*
* We have provided an idiomatic javascript constructor which takes
* a single object, but still support the legacy positional arguments.
*
* The options below are given in order that they appeared in the constructor
* as arguments and we translate a positional call into an idiomatic call.
*
* options:{
* element: String id of Element to attach to,
* xmlPath: String xpath ( TODO: not sure! ),
* prefixUrl: String url used to prepend to paths, eg button images,
* controls: Array of Seadragon.Controls,
* overlays: Array of Seadragon.Overlays,
* overlayControls: An Array of ( TODO: not sure! )
* }
*
*
**/
$.Viewer = function( options ) {
var args = arguments,
_this = this,
innerTracker,
outerTracker,
i;
if( typeof( options ) != 'object' ){
options = {
id: args[ 0 ],
xmlPath: args.length > 1 ? args[ 1 ] : undefined,
prefixUrl: args.length > 2 ? args[ 2 ] : undefined,
controls: args.length > 3 ? args[ 3 ] : undefined,
overlays: args.length > 4 ? args[ 4 ] : undefined,
overlayControls: args.length > 5 ? args[ 5 ] : undefined,
config: {}
};
}
//Allow the options object to override global defaults
$.extend( true, this, {
id: options.id,
xmlPath: null,
prefixUrl: '',
controls: [],
overlays: [],
overlayControls: [],
config: {
debugMode: true,
animationTime: 1.5,
blendTime: 0.5,
alwaysBlend: false,
autoHideControls: true,
immediateRender: false,
wrapHorizontal: false,
wrapVertical: false,
minZoomImageRatio: 0.8,
maxZoomPixelRatio: 2,
visibilityRatio: 0.5,
springStiffness: 5.0,
imageLoaderLimit: 2,
clickTimeThreshold: 200,
clickDistThreshold: 5,
zoomPerClick: 2.0,
zoomPerScroll: 1.2,
zoomPerSecond: 2.0,
showNavigationControl: true,
maxImageCacheCount: 100,
minPixelRatio: 0.5,
mouseNavEnabled: true,
navImages: {
zoomIn: {
REST: '/images/zoomin_rest.png',
GROUP: '/images/zoomin_grouphover.png',
HOVER: '/images/zoomin_hover.png',
DOWN: '/images/zoomin_pressed.png'
},
zoomOut: {
REST: '/images/zoomout_rest.png',
GROUP: '/images/zoomout_grouphover.png',
HOVER: '/images/zoomout_hover.png',
DOWN: '/images/zoomout_pressed.png'
},
home: {
REST: '/images/home_rest.png',
GROUP: '/images/home_grouphover.png',
HOVER: '/images/home_hover.png',
DOWN: '/images/home_pressed.png'
},
fullpage: {
REST: '/images/fullpage_rest.png',
GROUP: '/images/fullpage_grouphover.png',
HOVER: '/images/fullpage_hover.png',
DOWN: '/images/fullpage_pressed.png'
}
}
},
//These were referenced but never defined
controlsFadeDelay: 2000,
controlsFadeLength: 1500,
//These are originally not part options but declared as members
//in initialize. Its still considered idiomatic to put them here
source: null,
drawer: null,
viewport: null,
profiler: null,
//This was originally initialized in the constructor and so could never
//have anything in it. now it can because we allow it to be specified
//in the options and is only empty by default if not specified. Also
//this array was returned from get_controls which I find confusing
//since this object has a controls property which is treated in other
//functions like clearControls. I'm removing the accessors.
customControls: []
}, options );
this.element = document.getElementById( options.id );
this.container = $.Utils.makeNeutralElement("div");
this.canvas = $.Utils.makeNeutralElement("div");
this.events = new $.EventHandlerList();
this._fsBoundsDelta = new $.Point(1, 1);
this._prevContainerSize = null;
this._lastOpenStartTime = 0;
this._lastOpenEndTime = 0;
this._animating = false;
this._forceRedraw = false;
this._mouseInside = false;
innerTracker = new $.MouseTracker(
this.canvas,
this.config.clickTimeThreshold,
this.config.clickDistThreshold
);
innerTracker.clickHandler = $.delegate(this, onCanvasClick);
innerTracker.dragHandler = $.delegate(this, onCanvasDrag);
innerTracker.releaseHandler = $.delegate(this, onCanvasRelease);
innerTracker.scrollHandler = $.delegate(this, onCanvasScroll);
innerTracker.setTracking( true ); // default state
outerTracker = new $.MouseTracker(
this.container,
this.config.clickTimeThreshold,
this.config.clickDistThreshold
);
outerTracker.enterHandler = $.delegate(this, onContainerEnter);
outerTracker.exitHandler = $.delegate(this, onContainerExit);
outerTracker.releaseHandler = $.delegate(this, onContainerRelease);
outerTracker.setTracking( true ); // always tracking
(function( canvas ){
canvas.width = "100%";
canvas.height = "100%";
canvas.overflow = "hidden";
canvas.position = "absolute";
canvas.top = "0px";
canvas.left = "0px";
}( this.canvas.style ));
(function( container ){
container.width = "100%";
container.height = "100%";
container.position = "relative";
container.left = "0px";
container.top = "0px";
container.textAlign = "left"; // needed to protect against
}( this.container.style ));
var layouts = [ 'topleft', 'topright', 'bottomright', 'bottomleft'],
layout;
for( i = 0; i < layouts.length; i++ ){
layout = layouts[ i ]
this.controls[ layout ] = $.Utils.makeNeutralElement("div");
this.controls[ layout ].style.position = 'absolute';
if ( layout.match( 'left' ) ){
this.controls[ layout ].style.left = '0px';
}
if ( layout.match( 'right' ) ){
this.controls[ layout ].style.right = '0px';
}
if ( layout.match( 'top' ) ){
this.controls[ layout ].style.top = '0px';
}
if ( layout.match( 'bottom' ) ){
this.controls[ layout ].style.bottom = '0px';
}
}
if ( this.get_showNavigationControl() ) {
navControl = (new $.NavControl(this)).elmt;
navControl.style.marginRight = "4px";
navControl.style.marginBottom = "4px";
this.addControl(navControl, $.ControlAnchor.BOTTOM_RIGHT);
}
for ( i = 0; i < this.customControls.length; i++ ) {
this.addControl(
this.customControls[ i ].id,
this.customControls[ i ].anchor
);
}
this.container.appendChild( this.canvas );
this.container.appendChild( this.controls.topleft );
this.container.appendChild( this.controls.topright );
this.container.appendChild( this.controls.bottomright );
this.container.appendChild( this.controls.bottomleft );
this.element.appendChild( this.container );
window.setTimeout( function(){
beginControlsAutoHide( _this );
}, 1 ); // initial fade out
if (this.xmlPath){
this.openDzi( this.xmlPath );
}
};
$.Viewer.prototype = {
_onClose: function () {
this.source = null;
this.viewport = null;
this.drawer = null;
this.profiler = null;
this.canvas.innerHTML = "";
},
_beforeOpen: function () {
if (this.source) {
this._onClose();
}
this._lastOpenStartTime = new Date().getTime(); // to ignore earlier opens
window.setTimeout($.delegate(this, function () {
if (this._lastOpenStartTime > this._lastOpenEndTime) {
this._setMessage($.Strings.getString("Messages.Loading"));
}
}), 2000);
return this._lastOpenStartTime;
},
_onOpen: function (time, _source, error) {
this._lastOpenEndTime = new Date().getTime();
if (time < this._lastOpenStartTime) {
$.Debug.log("Ignoring out-of-date open.");
raiseEvent( this, "ignore" );
return;
} else if (!_source) {
this._setMessage(error);
raiseEvent( this, "error" );
return;
}
this.canvas.innerHTML = "";
this._prevContainerSize = $.Utils.getElementSize( this.container );
this.source = _source;
this.viewport = new $.Viewport(this._prevContainerSize, this.source.dimensions, this.config);
this.drawer = new $.Drawer(this.source, this.viewport, this.canvas);
this.profiler = new $.Profiler();
this._animating = false;
this._forceRedraw = true;
scheduleUpdate( this, this._updateMulti );
for (var i = 0; i < this.overlayControls.length; i++) {
var overlay = this.overlayControls[ i ];
if (overlay.point != null) {
this.drawer.addOverlay(overlay.id, new $.Point(overlay.point.X, overlay.point.Y), $.OverlayPlacement.TOP_LEFT);
}
else {
this.drawer.addOverlay(overlay.id, new $.Rect(overlay.rect.Point.X, overlay.rect.Point.Y, overlay.rect.Width, overlay.rect.Height), overlay.placement);
}
}
raiseEvent( this, "open" );
},
_updateMulti: function () {
if (!this.source) {
return;
}
var beginTime = new Date().getTime();
this._updateOnce();
scheduleUpdate( this, arguments.callee, beginTime );
},
_updateOnce: function () {
if (!this.source) {
return;
}
this.profiler.beginUpdate();
var containerSize = $.Utils.getElementSize( this.container );
if (!containerSize.equals(this._prevContainerSize)) {
this.viewport.resize(containerSize, true); // maintain image position
this._prevContainerSize = containerSize;
raiseEvent( this, "resize" );
}
var animated = this.viewport.update();
if (!this._animating && animated) {
raiseEvent( this, "animationstart" );
abortControlsAutoHide( this );
}
if (animated) {
this.drawer.update();
raiseEvent( this, "animation" );
} else if (this._forceRedraw || this.drawer.needsUpdate()) {
this.drawer.update();
this._forceRedraw = false;
} else {
this.drawer.idle();
}
if (this._animating && !animated) {
raiseEvent( this, "animationfinish" );
if (!this._mouseInside) {
beginControlsAutoHide( this );
}
}
this._animating = animated;
this.profiler.endUpdate();
},
getNavControl: function () {
return this._navControl;
},
get_element: function () {
return this._element;
},
get_debugMode: function () {
return this.config.debugMode;
},
set_debugMode: function (value) {
this.config.debugMode = value;
},
get_animationTime: function () {
return this.config.animationTime;
},
set_animationTime: function (value) {
this.config.animationTime = value;
},
get_blendTime: function () {
return this.config.blendTime;
},
set_blendTime: function (value) {
this.config.blendTime = value;
},
get_alwaysBlend: function () {
return this.config.alwaysBlend;
},
set_alwaysBlend: function (value) {
this.config.alwaysBlend = value;
},
get_autoHideControls: function () {
return this.config.autoHideControls;
},
set_autoHideControls: function (value) {
this.config.autoHideControls = value;
},
get_immediateRender: function () {
return this.config.immediateRender;
},
set_immediateRender: function (value) {
this.config.immediateRender = value;
},
get_wrapHorizontal: function () {
return this.config.wrapHorizontal;
},
set_wrapHorizontal: function (value) {
this.config.wrapHorizontal = value;
},
get_wrapVertical: function () {
return this.config.wrapVertical;
},
set_wrapVertical: function (value) {
this.config.wrapVertical = value;
},
get_minZoomImageRatio: function () {
return this.config.minZoomImageRatio;
},
set_minZoomImageRatio: function (value) {
this.config.minZoomImageRatio = value;
},
get_maxZoomPixelRatio: function () {
return this.config.maxZoomPixelRatio;
},
set_maxZoomPixelRatio: function (value) {
this.config.maxZoomPixelRatio = value;
},
get_visibilityRatio: function () {
return this.config.visibilityRatio;
},
set_visibilityRatio: function (value) {
this.config.visibilityRatio = value;
},
get_springStiffness: function () {
return this.config.springStiffness;
},
set_springStiffness: function (value) {
this.config.springStiffness = value;
},
get_imageLoaderLimit: function () {
return this.config.imageLoaderLimit;
},
set_imageLoaderLimit: function (value) {
this.config.imageLoaderLimit = value;
},
get_clickTimeThreshold: function () {
return this.config.clickTimeThreshold;
},
set_clickTimeThreshold: function (value) {
this.config.clickTimeThreshold = value;
},
get_clickDistThreshold: function () {
return this.config.clickDistThreshold;
},
set_clickDistThreshold: function (value) {
this.config.clickDistThreshold = value;
},
get_zoomPerClick: function () {
return this.config.zoomPerClick;
},
set_zoomPerClick: function (value) {
this.config.zoomPerClick = value;
},
get_zoomPerSecond: function () {
return this.config.zoomPerSecond;
},
set_zoomPerSecond: function (value) {
this.config.zoomPerSecond = value;
},
get_zoomPerScroll: function () {
return this.config.zoomPerScroll;
},
set_zoomPerScroll: function (value) {
this.config.zoomPerScroll = value;
},
get_maxImageCacheCount: function () {
return this.config.maxImageCacheCount;
},
set_maxImageCacheCount: function (value) {
this.config.maxImageCacheCount = value;
},
get_showNavigationControl: function () {
return this.config.showNavigationControl;
},
set_showNavigationControl: function (value) {
this.config.showNavigationControl = value;
},
get_minPixelRatio: function () {
return this.config.minPixelRatio;
},
set_minPixelRatio: function (value) {
this.config.minPixelRatio = value;
},
get_mouseNavEnabled: function () {
return this.config.mouseNavEnabled;
},
set_mouseNavEnabled: function (value) {
this.config.mouseNavEnabled = value;
},
add_open: function (handler) {
this.events.addHandler("open", handler);
},
remove_open: function (handler) {
this.events.removeHandler("open", handler);
},
add_error: function (handler) {
this.events.addHandler("error", handler);
},
remove_error: function (handler) {
this.events.removeHandler("error", handler);
},
add_ignore: function (handler) {
this.events.addHandler("ignore", handler);
},
remove_ignore: function (handler) {
this.events.removeHandler("ignore", handler);
},
add_resize: function (handler) {
this.events.addHandler("resize", handler);
},
remove_resize: function (handler) {
this.events.removeHandler("resize", handler);
},
add_animationstart: function (handler) {
this.events.addHandler("animationstart", handler);
},
remove_animationstart: function (handler) {
this.events.removeHandler("animationstart", handler);
},
add_animation: function (handler) {
this.events.addHandler("animation", handler);
},
remove_animation: function (handler) {
this.events.removeHandler("animation", handler);
},
add_animationfinish: function (handler) {
this.events.addHandler("animationfinish", handler);
},
remove_animationfinish: function (handler) {
this.events.removeHandler("animationfinish", handler);
},
addControl: function ( elmt, anchor ) {
var elmt = $.Utils.getElement( elmt ),
div = null;
if ( getControlIndex( this, elmt ) >= 0 ) {
return; // they're trying to add a duplicate control
}
switch ( anchor ) {
case $.ControlAnchor.TOP_RIGHT:
div = this.controls.topright;
elmt.style.position = "relative";
break;
case $.ControlAnchor.BOTTOM_RIGHT:
div = this.controls.bottomright;
elmt.style.position = "relative";
break;
case $.ControlAnchor.BOTTOM_LEFT:
div = this.controls.bottomleft;
elmt.style.position = "relative";
break;
case $.ControlAnchor.TOP_LEFT:
div = this.controls.topleft;
elmt.style.position = "relative";
break;
case $.ControlAnchor.NONE:
default:
div = this.container;
elmt.style.position = "absolute";
break;
}
this.controls.push(
new $.Control( elmt, anchor, div )
);
elmt.style.display = "inline-block";
},
isOpen: function () {
return !!this.source;
},
openDzi: function (xmlUrl, xmlString) {
var currentTime = this._beforeOpen();
$.DziTileSourceHelper.createFromXml(
xmlUrl,
xmlString,
$.Utils.createCallback(
null,
$.delegate(this, this._onOpen),
currentTime
)
);
},
openTileSource: function (tileSource) {
var currentTime = beforeOpen();
window.setTimeout($.delegate(this, function () {
onOpen(currentTime, tileSource);
}), 1);
},
close: function () {
if ( !this.source ) {
return;
}
this._onClose();
},
removeControl: function ( elmt ) {
var elmt = $.Utils.getElement( elmt ),
i = getControlIndex( this, elmt );
if ( i >= 0 ) {
this.controls[ i ].destroy();
this.controls.splice( i, 1 );
}
},
clearControls: function () {
while ( this.controls.length > 0 ) {
this.controls.pop().destroy();
}
},
isDashboardEnabled: function () {
var i;
for ( i = this.controls.length - 1; i >= 0; i-- ) {
if (this.controls[ i ].isVisible()) {
return true;
}
}
return false;
},
isFullPage: function () {
return this.container.parentNode == document.body;
},
isMouseNavEnabled: function () {
return this._innerTracker.isTracking();
},
isVisible: function () {
return this.container.style.visibility != "hidden";
},
setDashboardEnabled: function (enabled) {
var i;
for ( i = this.controls.length - 1; i >= 0; i-- ) {
this.controls[ i ].setVisible( enabled );
}
},
setFullPage: function( fullPage ) {
if ( fullPage == this.isFullPage() ) {
return;
}
var body = document.body,
bodyStyle = body.style,
docStyle = document.documentElement.style,
containerStyle = this.container.style,
canvasStyle = this.canvas.style,
oldBounds,
newBounds;
if ( fullPage ) {
bodyOverflow = bodyStyle.overflow;
docOverflow = docStyle.overflow;
bodyStyle.overflow = "hidden";
docStyle.overflow = "hidden";
bodyWidth = bodyStyle.width;
bodyHeight = bodyStyle.height;
bodyStyle.width = "100%";
bodyStyle.height = "100%";
canvasStyle.backgroundColor = "black";
canvasStyle.color = "white";
containerStyle.position = "fixed";
containerStyle.zIndex = "99999999";
body.appendChild( this.container );
this._prevContainerSize = $.Utils.getWindowSize();
// mouse will be inside container now
$.delegate( this, onContainerEnter )();
} else {
bodyStyle.overflow = bodyOverflow;
docStyle.overflow = docOverflow;
bodyStyle.width = bodyWidth;
bodyStyle.height = bodyHeight;
canvasStyle.backgroundColor = "";
canvasStyle.color = "";
containerStyle.position = "relative";
containerStyle.zIndex = "";
this.element.appendChild( this.container );
this._prevContainerSize = $.Utils.getElementSize( this.element );
// mouse will likely be outside now
$.delegate( this, onContainerExit )();
}
if ( this.viewport ) {
oldBounds = this.viewport.getBounds();
this.viewport.resize(this._prevContainerSize);
newBounds = this.viewport.getBounds();
if ( fullPage ) {
this._fsBoundsDelta = new $.Point(
newBounds.width / oldBounds.width,
newBounds.height / oldBounds.height
);
} else {
this.viewport.update();
this.viewport.zoomBy(
Math.max(
this._fsBoundsDelta.x,
this._fsBoundsDelta.y
),
null,
true
);
}
this._forceRedraw = true;
raiseEvent( this, "resize", this );
this._updateOnce();
}
},
setMouseNavEnabled: function( enabled ){
this._innerTracker.setTracking(enabled);
},
setVisible: function( visible ){
this.container.style.visibility = visible ? "" : "hidden";
}
};
///////////////////////////////////////////////////////////////////////////////
// Schedulers provide the general engine for animation
///////////////////////////////////////////////////////////////////////////////
function scheduleUpdate( viewer, updateFunc, prevUpdateTime ){
var currentTime,
prevUpdateTime,
targetTime,
deltaTime;
if (this._animating) {
return window.setTimeout(
$.delegate(viewer, updateFunc),
1
);
}
currentTime = +new Date();
prevUpdateTime = prevUpdateTime ? prevUpdateTime : currentTime;
targetTime = prevUpdateTime + 1000 / 60; // 60 fps ideal
deltaTime = Math.max(1, targetTime - currentTime);
return window.setTimeout($.delegate(viewer, updateFunc), deltaTime);
};
//provides a sequence in the fade animation
function scheduleControlsFade( viewer ) {
window.setTimeout( function(){
updateControlsFade( viewer );
}, 20);
};
//initiates an animation to hide the controls
function beginControlsAutoHide( viewer ) {
if ( !viewer.config.autoHideControls ) {
return;
}
viewer.controlsShouldFade = true;
viewer.controlsFadeBeginTime =
new Date().getTime() +
viewer.controlsFadeDelay;
window.setTimeout( function(){
scheduleControlsFade( viewer );
}, viewer.controlsFadeDelay );
};
//determines if fade animation is done or continues the animation
function updateControlsFade( viewer ) {
var currentTime,
deltaTime,
opacity,
i;
if ( viewer.controlsShouldFade ) {
currentTime = new Date().getTime();
deltaTime = currentTime - viewer.controlsFadeBeginTime;
opacity = 1.0 - deltaTime / viewer.controlsFadeLength;
opacity = Math.min( 1.0, opacity );
opacity = Math.max( 0.0, opacity );
for ( i = viewer.controls.length - 1; i >= 0; i--) {
viewer.controls[ i ].setOpacity( opacity );
}
if ( opacity > 0 ) {
// fade again
scheduleControlsFade( viewer );
}
}
};
//stop the fade animation on the controls and show them
function abortControlsAutoHide( viewer ) {
var i;
viewer.controlsShouldFade = false;
for ( i = viewer.controls.length - 1; i >= 0; i-- ) {
viewer.controls[ i ].setOpacity( 1.0 );
}
};
///////////////////////////////////////////////////////////////////////////////
// Event engine is simple, look up event handler and call.
///////////////////////////////////////////////////////////////////////////////
function raiseEvent( viewer, eventName, eventArgs) {
var handler = viewer.events.getHandler( eventName );
if ( handler ) {
if (!eventArgs) {
eventArgs = new Object();
}
handler( viewer, eventArgs );
}
};
///////////////////////////////////////////////////////////////////////////////
// Default view event handlers.
///////////////////////////////////////////////////////////////////////////////
function onCanvasClick(tracker, position, quick, shift) {
var zoomPreClick,
factor;
if (this.viewport && quick) { // ignore clicks where mouse moved
zoomPerClick = this.config.zoomPerClick;
factor = shift ? 1.0 / zoomPerClick : zoomPerClick;
this.viewport.zoomBy(factor, this.viewport.pointFromPixel(position, true));
this.viewport.applyConstraints();
}
};
function onCanvasDrag(tracker, position, delta, shift) {
if (this.viewport) {
this.viewport.panBy(this.viewport.deltaPointsFromPixels(delta.negate()));
}
};
function onCanvasRelease(tracker, position, insideElmtPress, insideElmtRelease) {
if (insideElmtPress && this.viewport) {
this.viewport.applyConstraints();
}
};
function onCanvasScroll(tracker, position, scroll, shift) {
var factor;
if (this.viewport) {
factor = Math.pow(this.config.zoomPerScroll, scroll);
this.viewport.zoomBy(factor, this.viewport.pointFromPixel(position, true));
this.viewport.applyConstraints();
}
};
function onContainerExit(tracker, position, buttonDownElmt, buttonDownAny) {
if (!buttonDownElmt) {
this._mouseInside = false;
if (!this._animating) {
beginControlsAutoHide( this );
}
}
};
function onContainerRelease(tracker, position, insideElmtPress, insideElmtRelease) {
if (!insideElmtRelease) {
this._mouseInside = false;
if (!this._animating) {
beginControlsAutoHide( this );
}
}
};
function onContainerEnter(tracker, position, buttonDownElmt, buttonDownAny) {
this._mouseInside = true;
abortControlsAutoHide( this );
};
///////////////////////////////////////////////////////////////////////////////
// Default view event handlers.
///////////////////////////////////////////////////////////////////////////////
function getControlIndex( viewer, elmt ) {
for ( i = viewer.controls.length - 1; i >= 0; i-- ) {
if ( viewer.controls[ i ].elmt == elmt ) {
return i;
}
}
return -1;
};
///////////////////////////////////////////////////////////////////////////////
// Page update routines ( aka Views - for future reference )
///////////////////////////////////////////////////////////////////////////////
}( OpenSeadragon ));