/*
* OpenSeadragon - Viewer
*
* Copyright (C) 2009 CodePlex Foundation
* Copyright (C) 2010-2013 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of CodePlex Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function( $ ){
// dictionary from hash to private properties
var THIS = {};
/**
*
* 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.
*
* @class Viewer
* @classdesc The main OpenSeadragon viewer class.
*
* @memberof OpenSeadragon
* @extends OpenSeadragon.EventSource
* @extends OpenSeadragon.ControlDock
* @param {OpenSeadragon.Options} options - Viewer options.
*
**/
$.Viewer = function( options ) {
var args = arguments,
_this = this,
i;
//backward compatibility for positional args while prefering more
//idiomatic javascript options object as the only argument
if( !$.isPlainObject( options ) ){
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
};
}
//options.config and the general config argument are deprecated
//in favor of the more direct specification of optional settings
//being pass directly on the options object
if ( options.config ){
$.extend( true, options, options.config );
delete options.config;
}
//Public properties
//Allow the options object to override global defaults
$.extend( true, this, {
//internal state and dom identifiers
id: options.id,
hash: options.hash || options.id,
//dom nodes
/**
* The parent element of this Viewer instance, passed in when the Viewer was created.
* @member {Element} element
* @memberof OpenSeadragon.Viewer#
*/
element: null,
/**
* A <div> element (provided by {@link OpenSeadragon.ControlDock}), the base element of this Viewer instance.
* Child element of {@link OpenSeadragon.Viewer#element}.
* @member {Element} container
* @memberof OpenSeadragon.Viewer#
*/
container: null,
/**
* A <textarea> element, the element where keyboard events are handled.
* Child element of {@link OpenSeadragon.Viewer#container},
* positioned below {@link OpenSeadragon.Viewer#canvas}.
* @member {Element} keyboardCommandArea
* @memberof OpenSeadragon.Viewer#
*/
keyboardCommandArea: null,
/**
* A <div> element, the element where user-input events are handled for panning and zooming.
* Child element of {@link OpenSeadragon.Viewer#container},
* positioned on top of {@link OpenSeadragon.Viewer#keyboardCommandArea}.
* The parent of {@link OpenSeadragon.Drawer#canvas} instances.
* @member {Element} canvas
* @memberof OpenSeadragon.Viewer#
*/
canvas: null,
// Overlays list. An overlay allows to add html on top of the viewer.
overlays: [],
// Container inside the canvas where overlays are drawn.
overlaysContainer: null,
//private state properties
previousBody: [],
//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: [],
//These are originally not part options but declared as members
//in initialize. It's still considered idiomatic to put them here
source: null,
/**
* Handles rendering of tiles in the viewer. Created for each TileSource opened.
* @member {OpenSeadragon.Drawer} drawer
* @memberof OpenSeadragon.Viewer#
*/
drawer: null,
world: null,
/**
* Handles coordinate-related functionality - zoom, pan, rotation, etc. Created for each TileSource opened.
* @member {OpenSeadragon.Viewport} viewport
* @memberof OpenSeadragon.Viewer#
*/
viewport: null,
/**
* @member {OpenSeadragon.Navigator} navigator
* @memberof OpenSeadragon.Viewer#
*/
navigator: null,
//A collection viewport is a separate viewport used to provide
//simultaneous rendering of sets of tiles
collectionViewport: null,
collectionDrawer: null,
//UI image resources
//TODO: rename navImages to uiImages
navImages: null,
//interface button controls
buttons: null,
//TODO: this is defunct so safely remove it
profiler: null
}, $.DEFAULT_SETTINGS, options );
if ( typeof( this.hash) === "undefined" ) {
throw new Error("A hash must be defined, either by specifying options.id or options.hash.");
}
if ( typeof( THIS[ this.hash ] ) !== "undefined" ) {
// We don't want to throw an error here, as the user might have discarded
// the previous viewer with the same hash and now want to recreate it.
$.console.warn("Hash " + this.hash + " has already been used.");
}
//Private state properties
THIS[ this.hash ] = {
"fsBoundsDelta": new $.Point( 1, 1 ),
"prevContainerSize": null,
"animating": false,
"forceRedraw": false,
"mouseInside": false,
"group": null,
// whether we should be continuously zooming
"zooming": false,
// how much we should be continuously zooming by
"zoomFactor": null,
"lastZoomTime": null,
// did we decide this viewer has a sequence of tile sources
"sequenced": false,
"sequence": 0,
"fullPage": false,
"onfullscreenchange": null
};
this._firstOpen = true;
this._updateRequestId = null;
this.currentOverlays = [];
//Inherit some behaviors and properties
$.EventSource.call( this );
this.addHandler( 'open-failed', function ( event ) {
var msg = $.getString( "Errors.OpenFailed", event.eventSource, event.message);
_this._showMessage( msg );
});
$.ControlDock.call( this, options );
//Deal with tile sources
if ( this.xmlPath ){
//Deprecated option. Now it is preferred to use the tileSources option
this.tileSources = [ this.xmlPath ];
}
this.element = this.element || document.getElementById( this.id );
this.canvas = $.makeNeutralElement( "div" );
this.keyboardCommandArea = $.makeNeutralElement( "textarea" );
this.overlaysContainer = $.makeNeutralElement( "div" );
this.canvas.className = "openseadragon-canvas";
(function( style ){
style.width = "100%";
style.height = "100%";
style.overflow = "hidden";
style.position = "absolute";
style.top = "0px";
style.left = "0px";
}(this.canvas.style));
$.setElementTouchActionNone( this.canvas );
//the container is created through applying the ControlDock constructor above
this.container.className = "openseadragon-container";
(function( style ){
style.width = "100%";
style.height = "100%";
style.position = "relative";
style.overflow = "hidden";
style.left = "0px";
style.top = "0px";
style.textAlign = "left"; // needed to protect against
}( this.container.style ));
this.keyboardCommandArea.className = "keyboard-command-area";
(function( style ){
style.width = "100%";
style.height = "100%";
style.overflow = "hidden";
style.position = "absolute";
style.top = "0px";
style.left = "0px";
style.resize = "none";
}( this.keyboardCommandArea.style ));
this.container.insertBefore( this.canvas, this.container.firstChild );
this.container.insertBefore( this.keyboardCommandArea, this.container.firstChild );
this.element.appendChild( this.container );
this.canvas.appendChild( this.overlaysContainer );
//Used for toggling between fullscreen and default container size
//TODO: these can be closure private and shared across Viewer
// instances.
this.bodyWidth = document.body.style.width;
this.bodyHeight = document.body.style.height;
this.bodyOverflow = document.body.style.overflow;
this.docOverflow = document.documentElement.style.overflow;
this.keyboardCommandArea.innerTracker = new $.MouseTracker({
_this : this,
element: this.keyboardCommandArea,
focusHandler: function( event ){
if ( !event.preventDefaultAction ) {
var point = $.getElementPosition( this.element );
window.scrollTo( 0, point.y );
}
},
keyHandler: function( event ){
if ( !event.preventDefaultAction ) {
switch( event.keyCode ){
case 61://=|+
_this.viewport.zoomBy(1.1);
_this.viewport.applyConstraints();
return false;
case 45://-|_
_this.viewport.zoomBy(0.9);
_this.viewport.applyConstraints();
return false;
case 48://0|)
_this.viewport.goHome();
_this.viewport.applyConstraints();
return false;
case 119://w
case 87://W
case 38://up arrow
if ( event.shift ) {
_this.viewport.zoomBy(1.1);
} else {
_this.viewport.panBy(new $.Point(0, -0.05));
}
_this.viewport.applyConstraints();
return false;
case 115://s
case 83://S
case 40://down arrow
if ( event.shift ) {
_this.viewport.zoomBy(0.9);
} else {
_this.viewport.panBy(new $.Point(0, 0.05));
}
_this.viewport.applyConstraints();
return false;
case 97://a
case 37://left arrow
_this.viewport.panBy(new $.Point(-0.05, 0));
_this.viewport.applyConstraints();
return false;
case 100://d
case 39://right arrow
_this.viewport.panBy(new $.Point(0.05, 0));
_this.viewport.applyConstraints();
return false;
default:
//console.log( 'navigator keycode %s', event.keyCode );
return true;
}
}
}
}).setTracking( true ); // default state
this.innerTracker = new $.MouseTracker({
element: this.canvas,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
dblClickTimeThreshold: this.dblClickTimeThreshold,
dblClickDistThreshold: this.dblClickDistThreshold,
clickHandler: $.delegate( this, onCanvasClick ),
dblClickHandler: $.delegate( this, onCanvasDblClick ),
dragHandler: $.delegate( this, onCanvasDrag ),
dragEndHandler: $.delegate( this, onCanvasDragEnd ),
releaseHandler: $.delegate( this, onCanvasRelease ),
scrollHandler: $.delegate( this, onCanvasScroll ),
pinchHandler: $.delegate( this, onCanvasPinch )
}).setTracking( this.mouseNavEnabled ? true : false ); // default state
this.outerTracker = new $.MouseTracker({
element: this.container,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
dblClickTimeThreshold: this.dblClickTimeThreshold,
dblClickDistThreshold: this.dblClickDistThreshold,
enterHandler: $.delegate( this, onContainerEnter ),
exitHandler: $.delegate( this, onContainerExit ),
pressHandler: $.delegate( this, onContainerPress ),
releaseHandler: $.delegate( this, onContainerRelease )
}).setTracking( this.mouseNavEnabled ? true : false ); // always tracking
if( this.toolbar ){
this.toolbar = new $.ControlDock({ element: this.toolbar });
}
this.bindStandardControls();
THIS[ this.hash ].prevContainerSize = _getSafeElemSize( this.container );
// Create the world
this.world = new $.World({
viewer: this
});
this.world.addHandler('add-item', function(event) {
// For backwards compatibility, we maintain the source property
_this.source = _this.world.getItemAt(0).source;
THIS[ _this.hash ].forceRedraw = true;
if (!_this._updateRequestId) {
_this._updateRequestId = scheduleUpdate( _this, updateMulti );
}
});
this.world.addHandler('remove-item', function(event) {
// For backwards compatibility, we maintain the source property
if (_this.world.getItemCount()) {
_this.source = _this.world.getItemAt(0).source;
} else {
_this.source = null;
}
THIS[ _this.hash ].forceRedraw = true;
});
this.world.addHandler('metrics-change', function(event) {
if (_this.viewport) {
_this.viewport.setHomeBounds(_this.world.getHomeBounds(), _this.world.getContentFactor());
}
});
this.world.addHandler('item-index-change', function(event) {
// For backwards compatibility, we maintain the source property
_this.source = _this.world.getItemAt(0).source;
});
// Create the viewport
this.viewport = new $.Viewport({
containerSize: THIS[ this.hash ].prevContainerSize,
springStiffness: this.springStiffness,
animationTime: this.animationTime,
minZoomImageRatio: this.minZoomImageRatio,
maxZoomPixelRatio: this.maxZoomPixelRatio,
visibilityRatio: this.visibilityRatio,
wrapHorizontal: this.wrapHorizontal,
wrapVertical: this.wrapVertical,
defaultZoomLevel: this.defaultZoomLevel,
minZoomLevel: this.minZoomLevel,
maxZoomLevel: this.maxZoomLevel,
viewer: this,
degrees: this.degrees,
navigatorRotate: this.navigatorRotate,
homeFillsViewer: this.homeFillsViewer,
margins: this.viewportMargins
});
this.viewport.setHomeBounds(this.world.getHomeBounds(), this.world.getContentFactor());
// Create the image loader
this.imageLoader = new $.ImageLoader();
// Create the tile cache
this.tileCache = new $.TileCache({
maxImageCacheCount: this.maxImageCacheCount
});
// Create the drawer
this.drawer = new $.Drawer({
viewer: this,
viewport: this.viewport,
element: this.canvas,
opacity: this.opacity,
debugGridColor: this.debugGridColor
});
// Now that we have a drawer, see if it supports rotate. If not we need to remove the rotate buttons
if (!this.drawer.canRotate()) {
// Disable/remove the rotate left/right buttons since they aren't supported
if (this.rotateLeft) {
i = this.buttons.buttons.indexOf(this.rotateLeft);
this.buttons.buttons.splice(i, 1);
this.buttons.element.removeChild(this.rotateLeft.element);
}
if (this.rotateRight) {
i = this.buttons.buttons.indexOf(this.rotateRight);
this.buttons.buttons.splice(i, 1);
this.buttons.element.removeChild(this.rotateRight.element);
}
}
//Instantiate a navigator if configured
if ( this.showNavigator){
this.navigator = new $.Navigator({
id: this.navigatorId,
position: this.navigatorPosition,
sizeRatio: this.navigatorSizeRatio,
maintainSizeRatio: this.navigatorMaintainSizeRatio,
top: this.navigatorTop,
left: this.navigatorLeft,
width: this.navigatorWidth,
height: this.navigatorHeight,
autoResize: this.navigatorAutoResize,
tileHost: this.tileHost,
prefixUrl: this.prefixUrl,
viewer: this,
navigatorRotate: this.navigatorRotate
});
}
//Instantiate a referencestrip if configured
if ( this.showReferenceStrip ){
this.referenceStrip = new $.ReferenceStrip({
id: this.referenceStripElement,
position: this.referenceStripPosition,
sizeRatio: this.referenceStripSizeRatio,
scroll: this.referenceStripScroll,
height: this.referenceStripHeight,
width: this.referenceStripWidth,
tileSources: this.tileSources,
tileHost: this.tileHost,
prefixUrl: this.prefixUrl,
viewer: this
});
}
// Sequence mode
if (this.sequenceMode) {
THIS[ this.hash ].sequenced = true;
this.initialPage = Math.max(0, Math.min(this.tileSources.length - 1, this.initialPage));
THIS[ this.hash ].sequence = this.initialPage;
this.bindSequenceControls();
}
// Open initial tilesources
if ( this.tileSources ) {
if (this.sequenceMode) {
this.open(this.tileSources[this.initialPage]);
this._updateSequenceButtons( this.initialPage );
} else {
this.open( this.tileSources );
}
}
// Add custom controls
for ( i = 0; i < this.customControls.length; i++ ) {
this.addControl(
this.customControls[ i ].id,
{anchor: this.customControls[ i ].anchor}
);
}
// Initial fade out
$.requestAnimationFrame( function(){
beginControlsAutoHide( _this );
} );
};
$.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, /** @lends OpenSeadragon.Viewer.prototype */{
/**
* @function
* @return {Boolean}
*/
isOpen: function () {
return !!this.world.getItemCount();
},
// deprecated
openDzi: function ( dzi ) {
$.console.error( "[Viewer.openDzi] this function is deprecated; use Viewer.open() instead." );
return this.open( dzi );
},
// deprecated
openTileSource: function ( tileSource ) {
$.console.error( "[Viewer.openTileSource] this function is deprecated; use Viewer.open() instead." );
return this.open( tileSource );
},
/**
* Open tiled images into the viewer, closing any others.
* @function
* @param {Array|String|Object|Function} tileSources - This can be a TiledImage
* specifier, a TileSource specifier, or an array of either. A TiledImage specifier
* is the same as the options parameter for {@link OpenSeadragon.Viewer#addTiledImage},
* except for the index property; images are added in sequence.
* A TileSource specifier is anything you could pass as the tileSource property
* of the options parameter for {@link OpenSeadragon.Viewer#addTiledImage}.
* @return {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:open
* @fires OpenSeadragon.Viewer.event:open-failed
*/
open: function (tileSources) {
var _this = this;
this.close();
if (!tileSources) {
return;
}
if (!$.isArray(tileSources)) {
tileSources = [tileSources];
}
if (!tileSources.length) {
return;
}
var expected = tileSources.length;
var successes = 0;
var failures = 0;
var failEvent;
var checkCompletion = function() {
if (successes + failures === expected) {
if (successes) {
if (_this._firstOpen || !_this.preserveViewport) {
_this.viewport.goHome( true );
}
_this._firstOpen = false;
var source = tileSources[0];
if (source.tileSource) {
source = source.tileSource;
}
// Global overlays
for ( var i = 0; i < _this.overlays.length; i++ ) {
_this.currentOverlays[ i ] = getOverlayObject( _this, _this.overlays[ i ] );
}
/**
* Raised when the viewer has opened and loaded one or more TileSources.
*
* @event open
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {OpenSeadragon.TileSource} source - The tile source that was opened.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
// TODO: what if there are multiple sources?
_this.raiseEvent( 'open', { source: source } );
} else {
/**
* Raised when an error occurs loading a TileSource.
*
* @event open-failed
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {String} message - Information about what failed.
* @property {String} source - The tile source that failed.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
_this.raiseEvent( 'open-failed', failEvent );
}
}
};
var doOne = function(options) {
if (!$.isPlainObject(options) || !options.tileSource) {
options = {
tileSource: options
};
}
if (options.index !== undefined) {
$.console.error('[Viewer.open] setting indexes here is not supported; use addTiledImage instead');
delete options.index;
}
var originalSuccess = options.success;
options.success = function(event) {
successes++;
if (originalSuccess) {
originalSuccess(event);
}
checkCompletion();
};
var originalError = options.error;
options.error = function(event) {
failures++;
if (!failEvent) {
failEvent = event;
}
if (originalError) {
originalError(event);
}
checkCompletion();
};
_this.addTiledImage(options);
// For backwards compatibility. TODO: deprecate.
if (options.tileSource.overlays) {
for (var i = 0; i < options.tileSource.overlays.length; i++) {
_this.addOverlay(options.tileSource.overlays[i]);
}
}
};
// TileSources
for (var i = 0; i < tileSources.length; i++) {
doOne(tileSources[i]);
}
return this;
},
/**
* @function
* @return {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:close
*/
close: function ( ) {
if ( !THIS[ this.hash ] ) {
//this viewer has already been destroyed: returning immediately
return this;
}
if ( this.navigator ) {
this.navigator.close();
}
this.clearOverlays();
this.overlaysContainer.innerHTML = "";
THIS[ this.hash ].animating = false;
this.world.removeAll();
/**
* Raised when the viewer is closed (see {@link OpenSeadragon.Viewer#close}).
*
* @event close
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'close' );
return this;
},
/**
* Function to destroy the viewer and clean up everything created by OpenSeadragon.
*
* Example:
* var viewer = OpenSeadragon({
* [...]
* });
*
* //when you are done with the viewer:
* viewer.destroy();
* viewer = null; //important
*
* @function
*/
destroy: function( ) {
if ( !THIS[ this.hash ] ) {
//this viewer has already been destroyed: returning immediately
return;
}
this.close();
//TODO: implement this...
//this.unbindSequenceControls()
//this.unbindStandardControls()
if ( this._updateRequestId !== null ) {
$.cancelAnimationFrame( this._updateRequestId );
this._updateRequestId = null;
}
if ( this.drawer ) {
this.drawer.destroy();
}
this.removeAllHandlers();
// Go through top element (passed to us) and remove all children
// Use removeChild to make sure it handles SVG or any non-html
// also it performs better - http://jsperf.com/innerhtml-vs-removechild/15
if (this.element){
while (this.element.firstChild) {
this.element.removeChild(this.element.firstChild);
}
}
// destroy the mouse trackers
if (this.keyboardCommandArea){
this.keyboardCommandArea.innerTracker.destroy();
}
if (this.innerTracker){
this.innerTracker.destroy();
}
if (this.outerTracker){
this.outerTracker.destroy();
}
THIS[ this.hash ] = null;
delete THIS[ this.hash ];
// clear all our references to dom objects
this.canvas = null;
this.keyboardCommandArea = null;
this.container = null;
// clear our reference to the main element - they will need to pass it in again, creating a new viewer
this.element = null;
},
/**
* @function
* @return {Boolean}
*/
isMouseNavEnabled: function () {
return this.innerTracker.isTracking();
},
/**
* @function
* @param {Boolean} enabled - true to enable, false to disable
* @return {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:mouse-enabled
*/
setMouseNavEnabled: function( enabled ){
this.innerTracker.setTracking( enabled );
/**
* Raised when mouse/touch navigation is enabled or disabled (see {@link OpenSeadragon.Viewer#setMouseNavEnabled}).
*
* @event mouse-enabled
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Boolean} enabled
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'mouse-enabled', { enabled: enabled } );
return this;
},
/**
* @function
* @return {Boolean}
*/
areControlsEnabled: function () {
var enabled = this.controls.length,
i;
for( i = 0; i < this.controls.length; i++ ){
enabled = enabled && this.controls[ i ].isVisibile();
}
return enabled;
},
/**
* Shows or hides the controls (e.g. the default navigation buttons).
*
* @function
* @param {Boolean} true to show, false to hide.
* @return {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:controls-enabled
*/
setControlsEnabled: function( enabled ) {
if( enabled ){
abortControlsAutoHide( this );
} else {
beginControlsAutoHide( this );
}
/**
* Raised when the navigation controls are shown or hidden (see {@link OpenSeadragon.Viewer#setControlsEnabled}).
*
* @event controls-enabled
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Boolean} enabled
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'controls-enabled', { enabled: enabled } );
return this;
},
/**
* @function
* @return {Boolean}
*/
isFullPage: function () {
return THIS[ this.hash ].fullPage;
},
/**
* Toggle full page mode.
* @function
* @param {Boolean} fullPage
* If true, enter full page mode. If false, exit full page mode.
* @return {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:pre-full-page
* @fires OpenSeadragon.Viewer.event:full-page
*/
setFullPage: function( fullPage ) {
var body = document.body,
bodyStyle = body.style,
docStyle = document.documentElement.style,
_this = this,
hash,
nodes,
i;
//dont bother modifying the DOM if we are already in full page mode.
if ( fullPage == this.isFullPage() ) {
return this;
}
var fullPageEventArgs = {
fullPage: fullPage,
preventDefaultAction: false
};
/**
* Raised when the viewer is about to change to/from full-page mode (see {@link OpenSeadragon.Viewer#setFullPage}).
*
* @event pre-full-page
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Boolean} fullPage - True if entering full-page mode, false if exiting full-page mode.
* @property {Boolean} preventDefaultAction - Set to true to prevent full-page mode change. Default: false.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'pre-full-page', fullPageEventArgs );
if ( fullPageEventArgs.preventDefaultAction ) {
return this;
}
if ( fullPage ) {
this.elementSize = $.getElementSize( this.element );
this.pageScroll = $.getPageScroll();
this.elementMargin = this.element.style.margin;
this.element.style.margin = "0";
this.elementPadding = this.element.style.padding;
this.element.style.padding = "0";
this.bodyMargin = bodyStyle.margin;
this.docMargin = docStyle.margin;
bodyStyle.margin = "0";
docStyle.margin = "0";
this.bodyPadding = bodyStyle.padding;
this.docPadding = docStyle.padding;
bodyStyle.padding = "0";
docStyle.padding = "0";
this.bodyWidth = bodyStyle.width;
this.bodyHeight = bodyStyle.height;
bodyStyle.width = "100%";
bodyStyle.height = "100%";
//when entering full screen on the ipad it wasnt sufficient to leave
//the body intact as only only the top half of the screen would
//respond to touch events on the canvas, while the bottom half treated
//them as touch events on the document body. Thus we remove and store
//the bodies elements and replace them when we leave full screen.
this.previousBody = [];
THIS[ this.hash ].prevElementParent = this.element.parentNode;
THIS[ this.hash ].prevNextSibling = this.element.nextSibling;
THIS[ this.hash ].prevElementWidth = this.element.style.width;
THIS[ this.hash ].prevElementHeight = this.element.style.height;
nodes = body.childNodes.length;
for ( i = 0; i < nodes; i++ ) {
this.previousBody.push( body.childNodes[ 0 ] );
body.removeChild( body.childNodes[ 0 ] );
}
//If we've got a toolbar, we need to enable the user to use css to
//preserve it in fullpage mode
if ( this.toolbar && this.toolbar.element ) {
//save a reference to the parent so we can put it back
//in the long run we need a better strategy
this.toolbar.parentNode = this.toolbar.element.parentNode;
this.toolbar.nextSibling = this.toolbar.element.nextSibling;
body.appendChild( this.toolbar.element );
//Make sure the user has some ability to style the toolbar based
//on the mode
$.addClass( this.toolbar.element, 'fullpage' );
}
$.addClass( this.element, 'fullpage' );
body.appendChild( this.element );
this.element.style.height = $.getWindowSize().y + 'px';
this.element.style.width = $.getWindowSize().x + 'px';
if ( this.toolbar && this.toolbar.element ) {
this.element.style.height = (
$.getElementSize( this.element ).y - $.getElementSize( this.toolbar.element ).y
) + 'px';
}
THIS[ this.hash ].fullPage = true;
// mouse will be inside container now
$.delegate( this, onContainerEnter )( {} );
} else {
this.element.style.margin = this.elementMargin;
this.element.style.padding = this.elementPadding;
bodyStyle.margin = this.bodyMargin;
docStyle.margin = this.docMargin;
bodyStyle.padding = this.bodyPadding;
docStyle.padding = this.docPadding;
bodyStyle.width = this.bodyWidth;
bodyStyle.height = this.bodyHeight;
body.removeChild( this.element );
nodes = this.previousBody.length;
for ( i = 0; i < nodes; i++ ) {
body.appendChild( this.previousBody.shift() );
}
$.removeClass( this.element, 'fullpage' );
THIS[ this.hash ].prevElementParent.insertBefore(
this.element,
THIS[ this.hash ].prevNextSibling
);
//If we've got a toolbar, we need to enable the user to use css to
//reset it to its original state
if ( this.toolbar && this.toolbar.element ) {
body.removeChild( this.toolbar.element );
//Make sure the user has some ability to style the toolbar based
//on the mode
$.removeClass( this.toolbar.element, 'fullpage' );
this.toolbar.parentNode.insertBefore(
this.toolbar.element,
this.toolbar.nextSibling
);
delete this.toolbar.parentNode;
delete this.toolbar.nextSibling;
}
this.element.style.width = THIS[ this.hash ].prevElementWidth;
this.element.style.height = THIS[ this.hash ].prevElementHeight;
// After exiting fullPage or fullScreen, it can take some time
// before the browser can actually set the scroll.
var restoreScrollCounter = 0;
var restoreScroll = function() {
$.setPageScroll( _this.pageScroll );
var pageScroll = $.getPageScroll();
restoreScrollCounter++;
if ( restoreScrollCounter < 10 &&
pageScroll.x !== _this.pageScroll.x ||
pageScroll.y !== _this.pageScroll.y ) {
$.requestAnimationFrame( restoreScroll );
}
};
$.requestAnimationFrame( restoreScroll );
THIS[ this.hash ].fullPage = false;
// mouse will likely be outside now
$.delegate( this, onContainerExit )( { } );
}
if ( this.navigator && this.viewport ) {
this.navigator.update( this.viewport );
}
/**
* Raised when the viewer has changed to/from full-page mode (see {@link OpenSeadragon.Viewer#setFullPage}).
*
* @event full-page
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Boolean} fullPage - True if changed to full-page mode, false if exited full-page mode.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'full-page', { fullPage: fullPage } );
return this;
},
/**
* Toggle full screen mode if supported. Toggle full page mode otherwise.
* @function
* @param {Boolean} fullScreen
* If true, enter full screen mode. If false, exit full screen mode.
* @return {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:pre-full-screen
* @fires OpenSeadragon.Viewer.event:full-screen
*/
setFullScreen: function( fullScreen ) {
var _this = this;
if ( !$.supportsFullScreen ) {
return this.setFullPage( fullScreen );
}
if ( $.isFullScreen() === fullScreen ) {
return this;
}
var fullScreeEventArgs = {
fullScreen: fullScreen,
preventDefaultAction: false
};
/**
* Raised when the viewer is about to change to/from full-screen mode (see {@link OpenSeadragon.Viewer#setFullScreen}).
*
* @event pre-full-screen
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Boolean} fullScreen - True if entering full-screen mode, false if exiting full-screen mode.
* @property {Boolean} preventDefaultAction - Set to true to prevent full-screen mode change. Default: false.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'pre-full-screen', fullScreeEventArgs );
if ( fullScreeEventArgs.preventDefaultAction ) {
return this;
}
if ( fullScreen ) {
this.setFullPage( true );
// If the full page mode is not actually entered, we need to prevent
// the full screen mode.
if ( !this.isFullPage() ) {
return this;
}
this.fullPageStyleWidth = this.element.style.width;
this.fullPageStyleHeight = this.element.style.height;
this.element.style.width = '100%';
this.element.style.height = '100%';
var onFullScreenChange = function() {
var isFullScreen = $.isFullScreen();
if ( !isFullScreen ) {
$.removeEvent( document, $.fullScreenEventName, onFullScreenChange );
$.removeEvent( document, $.fullScreenErrorEventName, onFullScreenChange );
_this.setFullPage( false );
if ( _this.isFullPage() ) {
_this.element.style.width = _this.fullPageStyleWidth;
_this.element.style.height = _this.fullPageStyleHeight;
}
}
if ( _this.navigator && _this.viewport ) {
_this.navigator.update( _this.viewport );
}
/**
* Raised when the viewer has changed to/from full-screen mode (see {@link OpenSeadragon.Viewer#setFullScreen}).
*
* @event full-screen
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Boolean} fullScreen - True if changed to full-screen mode, false if exited full-screen mode.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
_this.raiseEvent( 'full-screen', { fullScreen: isFullScreen } );
};
$.addEvent( document, $.fullScreenEventName, onFullScreenChange );
$.addEvent( document, $.fullScreenErrorEventName, onFullScreenChange );
$.requestFullScreen( document.body );
} else {
$.exitFullScreen();
}
return this;
},
/**
* @function
* @return {Boolean}
*/
isVisible: function () {
return this.container.style.visibility != "hidden";
},
/**
* @function
* @param {Boolean} visible
* @return {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:visible
*/
setVisible: function( visible ){
this.container.style.visibility = visible ? "" : "hidden";
/**
* Raised when the viewer is shown or hidden (see {@link OpenSeadragon.Viewer#setVisible}).
*
* @event visible
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Boolean} visible
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'visible', { visible: visible } );
return this;
},
/**
* Add a tiled image to the viewer.
* options.tileSource can be anything that {@link OpenSeadragon.Viewer#open}
* supports except arrays of images.
* Note that you can specify options.width or options.height, but not both.
* The other dimension will be calculated according to the item's aspect ratio.
* @function
* @param {Object} options
* @param {String|Object|Function} options.tileSource - The TileSource specifier.
* A String implies a url used to determine the tileSource implementation
* based on the file extension of url. JSONP is implied by *.js,
* otherwise the url is retrieved as text and the resulting text is
* introspected to determine if its json, xml, or text and parsed.
* An Object implies an inline configuration which has a single
* property sufficient for being able to determine tileSource
* implementation. If the object has a property which is a function
* named 'getTileUrl', it is treated as a custom TileSource.
* @param {Number} [options.index] The index of the item. Added on top of
* all other items if not specified.
* @param {Number} [options.x=0] The X position for the image in world coordinates.
* @param {Number} [options.y=0] The Y position for the image in world coordinates.
* @param {Number} [options.width=1] The width for the image in world coordinates.
* @param {Number} [options.height] The height for the image in world coordinates.
* @param {Function} [options.success] A function that gets called when the image is
* successfully added. It's passed the event object which contains a single property:
* "item", the resulting TiledImage.
* @param {Function} [options.error] A function that gets called if the image is
* unable to be added. It's passed the error event object, which contains "message"
* and "source" properties.
* @fires OpenSeadragon.World.event:add-item
* @fires OpenSeadragon.Viewer.event:add-item-failed
*/
addTiledImage: function( options ) {
$.console.assert(options, "[Viewer.addTiledImage] options is required");
$.console.assert(options.tileSource, "[Viewer.addTiledImage] options.tileSource is required");
var _this = this,
tileSource = options.tileSource;
this._hideMessage();
function raiseAddItemFailed( event ) {
/**
* Raised when an error occurs while adding a item.
* @event add-item-failed
* @memberOf OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {String} message
* @property {String} source
* @property {Object} options The options passed to the addTiledImage method.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
_this.raiseEvent( 'add-item-failed', event );
if (options.error) {
options.error(event);
}
}
getTileSourceImplementation( this, tileSource, function( tileSource ) {
if ( tileSource instanceof Array ) {
raiseAddItemFailed({
message: "[Viewer.addTiledImage] Sequences can not be added; add them one at a time instead.",
source: tileSource,
options: options
});
return;
}
var tiledImage = new $.TiledImage({
viewer: _this,
source: tileSource,
viewport: _this.viewport,
drawer: _this.drawer,
tileCache: _this.tileCache,
imageLoader: _this.imageLoader,
x: options.x,
y: options.y,
width: options.width,
height: options.height,
imageLoaderLimit: _this.imageLoaderLimit,
minZoomImageRatio: _this.minZoomImageRatio,
wrapHorizontal: _this.wrapHorizontal,
wrapVertical: _this.wrapVertical,
immediateRender: _this.immediateRender,
blendTime: _this.blendTime,
alwaysBlend: _this.alwaysBlend,
minPixelRatio: _this.minPixelRatio,
debugMode: _this.debugMode,
debugGridColor: _this.debugGridColor
});
_this.world.addItem( tiledImage, {
index: options.index
});
if (_this.collectionMode) {
_this.world.arrange({
rows: _this.collectionRows,
layout: _this.collectionLayout,
tileSize: _this.collectionTileSize,
tileMargin: _this.collectionTileMargin
});
}
if (_this.world.getItemCount() === 1 && !_this.preserveViewport) {
_this.viewport.goHome(true);
}
if (_this.navigator) {
var optionsClone = $.extend({}, options, {
originalTiledImage: tiledImage,
tileSource: tileSource
});
_this.navigator.addTiledImage(optionsClone);
}
if (options.success) {
options.success({
item: tiledImage
});
}
}, function( event ) {
event.options = options;
raiseAddItemFailed(event);
} );
},
// deprecated
addLayer: function( options ) {
var _this = this;
$.console.error( "[Viewer.addLayer] this function is deprecated; use Viewer.addTiledImage() instead." );
var optionsClone = $.extend({}, options, {
success: function(event) {
_this.raiseEvent("add-layer", {
options: options,
drawer: event.item
});
},
error: function(event) {
_this.raiseEvent("add-layer-failed", event);
}
});
this.addTiledImage(optionsClone);
return this;
},
// deprecated
getLayerAtLevel: function( level ) {
$.console.error( "[Viewer.getLayerAtLevel] this function is deprecated; use World.getItemAt() instead." );
return this.world.getItemAt(level);
},
// deprecated
getLevelOfLayer: function( drawer ) {
$.console.error( "[Viewer.getLevelOfLayer] this function is deprecated; use World.getIndexOfItem() instead." );
return this.world.getIndexOfItem(drawer);
},
// deprecated
getLayersCount: function() {
$.console.error( "[Viewer.getLayersCount] this function is deprecated; use World.getItemCount() instead." );
return this.world.getItemCount();
},
// deprecated
setLayerLevel: function( drawer, level ) {
$.console.error( "[Viewer.setLayerLevel] this function is deprecated; use World.setItemIndex() instead." );
return this.world.setItemIndex(drawer, level);
},
// deprecated
removeLayer: function( drawer ) {
$.console.error( "[Viewer.removeLayer] this function is deprecated; use World.removeItem() instead." );
return this.world.removeItem(drawer);
},
/**
* Force the viewer to redraw its contents.
* @returns {OpenSeadragon.Viewer} Chainable.
*/
forceRedraw: function() {
THIS[ this.hash ].forceRedraw = true;
return this;
},
/**
* @function
* @return {OpenSeadragon.Viewer} Chainable.
*/
bindSequenceControls: function(){
//////////////////////////////////////////////////////////////////////////
// Image Sequence Controls
//////////////////////////////////////////////////////////////////////////
var onFocusHandler = $.delegate( this, onFocus ),
onBlurHandler = $.delegate( this, onBlur ),
onNextHandler = $.delegate( this, onNext ),
onPreviousHandler = $.delegate( this, onPrevious ),
navImages = this.navImages,
useGroup = true ;
if( this.showSequenceControl && THIS[ this.hash ].sequenced ){
if( this.previousButton || this.nextButton ){
//if we are binding to custom buttons then layout and
//grouping is the responsibility of the page author
useGroup = false;
}
this.previousButton = new $.Button({
element: this.previousButton ? $.getElement( this.previousButton ) : null,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
tooltip: $.getString( "Tooltips.PreviousPage" ),
srcRest: resolveUrl( this.prefixUrl, navImages.previous.REST ),
srcGroup: resolveUrl( this.prefixUrl, navImages.previous.GROUP ),
srcHover: resolveUrl( this.prefixUrl, navImages.previous.HOVER ),
srcDown: resolveUrl( this.prefixUrl, navImages.previous.DOWN ),
onRelease: onPreviousHandler,
onFocus: onFocusHandler,
onBlur: onBlurHandler
});
this.nextButton = new $.Button({
element: this.nextButton ? $.getElement( this.nextButton ) : null,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
tooltip: $.getString( "Tooltips.NextPage" ),
srcRest: resolveUrl( this.prefixUrl, navImages.next.REST ),
srcGroup: resolveUrl( this.prefixUrl, navImages.next.GROUP ),
srcHover: resolveUrl( this.prefixUrl, navImages.next.HOVER ),
srcDown: resolveUrl( this.prefixUrl, navImages.next.DOWN ),
onRelease: onNextHandler,
onFocus: onFocusHandler,
onBlur: onBlurHandler
});
if( !this.navPrevNextWrap ){
this.previousButton.disable();
}
if( useGroup ){
this.paging = new $.ButtonGroup({
buttons: [
this.previousButton,
this.nextButton
],
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold
});
this.pagingControl = this.paging.element;
if( this.toolbar ){
this.toolbar.addControl(
this.pagingControl,
{anchor: $.ControlAnchor.BOTTOM_RIGHT}
);
}else{
this.addControl(
this.pagingControl,
{anchor: this.sequenceControlAnchor || $.ControlAnchor.TOP_LEFT}
);
}
}
}
return this;
},
/**
* @function
* @return {OpenSeadragon.Viewer} Chainable.
*/
bindStandardControls: function(){
//////////////////////////////////////////////////////////////////////////
// Navigation Controls
//////////////////////////////////////////////////////////////////////////
var beginZoomingInHandler = $.delegate( this, beginZoomingIn ),
endZoomingHandler = $.delegate( this, endZooming ),
doSingleZoomInHandler = $.delegate( this, doSingleZoomIn ),
beginZoomingOutHandler = $.delegate( this, beginZoomingOut ),
doSingleZoomOutHandler = $.delegate( this, doSingleZoomOut ),
onHomeHandler = $.delegate( this, onHome ),
onFullScreenHandler = $.delegate( this, onFullScreen ),
onRotateLeftHandler = $.delegate( this, onRotateLeft ),
onRotateRightHandler = $.delegate( this, onRotateRight ),
onFocusHandler = $.delegate( this, onFocus ),
onBlurHandler = $.delegate( this, onBlur ),
navImages = this.navImages,
buttons = [],
useGroup = true ;
if ( this.showNavigationControl ) {
if( this.zoomInButton || this.zoomOutButton ||
this.homeButton || this.fullPageButton ||
this.rotateLeftButton || this.rotateRightButton ) {
//if we are binding to custom buttons then layout and
//grouping is the responsibility of the page author
useGroup = false;
}
if ( this.showZoomControl ) {
buttons.push( this.zoomInButton = new $.Button({
element: this.zoomInButton ? $.getElement( this.zoomInButton ) : null,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
tooltip: $.getString( "Tooltips.ZoomIn" ),
srcRest: resolveUrl( this.prefixUrl, navImages.zoomIn.REST ),
srcGroup: resolveUrl( this.prefixUrl, navImages.zoomIn.GROUP ),
srcHover: resolveUrl( this.prefixUrl, navImages.zoomIn.HOVER ),
srcDown: resolveUrl( this.prefixUrl, navImages.zoomIn.DOWN ),
onPress: beginZoomingInHandler,
onRelease: endZoomingHandler,
onClick: doSingleZoomInHandler,
onEnter: beginZoomingInHandler,
onExit: endZoomingHandler,
onFocus: onFocusHandler,
onBlur: onBlurHandler
}));
buttons.push( this.zoomOutButton = new $.Button({
element: this.zoomOutButton ? $.getElement( this.zoomOutButton ) : null,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
tooltip: $.getString( "Tooltips.ZoomOut" ),
srcRest: resolveUrl( this.prefixUrl, navImages.zoomOut.REST ),
srcGroup: resolveUrl( this.prefixUrl, navImages.zoomOut.GROUP ),
srcHover: resolveUrl( this.prefixUrl, navImages.zoomOut.HOVER ),
srcDown: resolveUrl( this.prefixUrl, navImages.zoomOut.DOWN ),
onPress: beginZoomingOutHandler,
onRelease: endZoomingHandler,
onClick: doSingleZoomOutHandler,
onEnter: beginZoomingOutHandler,
onExit: endZoomingHandler,
onFocus: onFocusHandler,
onBlur: onBlurHandler
}));
}
if ( this.showHomeControl ) {
buttons.push( this.homeButton = new $.Button({
element: this.homeButton ? $.getElement( this.homeButton ) : null,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
tooltip: $.getString( "Tooltips.Home" ),
srcRest: resolveUrl( this.prefixUrl, navImages.home.REST ),
srcGroup: resolveUrl( this.prefixUrl, navImages.home.GROUP ),
srcHover: resolveUrl( this.prefixUrl, navImages.home.HOVER ),
srcDown: resolveUrl( this.prefixUrl, navImages.home.DOWN ),
onRelease: onHomeHandler,
onFocus: onFocusHandler,
onBlur: onBlurHandler
}));
}
if ( this.showFullPageControl ) {
buttons.push( this.fullPageButton = new $.Button({
element: this.fullPageButton ? $.getElement( this.fullPageButton ) : null,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
tooltip: $.getString( "Tooltips.FullPage" ),
srcRest: resolveUrl( this.prefixUrl, navImages.fullpage.REST ),
srcGroup: resolveUrl( this.prefixUrl, navImages.fullpage.GROUP ),
srcHover: resolveUrl( this.prefixUrl, navImages.fullpage.HOVER ),
srcDown: resolveUrl( this.prefixUrl, navImages.fullpage.DOWN ),
onRelease: onFullScreenHandler,
onFocus: onFocusHandler,
onBlur: onBlurHandler
}));
}
if ( this.showRotationControl ) {
buttons.push( this.rotateLeftButton = new $.Button({
element: this.rotateLeftButton ? $.getElement( this.rotateLeftButton ) : null,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
tooltip: $.getString( "Tooltips.RotateLeft" ),
srcRest: resolveUrl( this.prefixUrl, navImages.rotateleft.REST ),
srcGroup: resolveUrl( this.prefixUrl, navImages.rotateleft.GROUP ),
srcHover: resolveUrl( this.prefixUrl, navImages.rotateleft.HOVER ),
srcDown: resolveUrl( this.prefixUrl, navImages.rotateleft.DOWN ),
onRelease: onRotateLeftHandler,
onFocus: onFocusHandler,
onBlur: onBlurHandler
}));
buttons.push( this.rotateRightButton = new $.Button({
element: this.rotateRightButton ? $.getElement( this.rotateRightButton ) : null,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
tooltip: $.getString( "Tooltips.RotateRight" ),
srcRest: resolveUrl( this.prefixUrl, navImages.rotateright.REST ),
srcGroup: resolveUrl( this.prefixUrl, navImages.rotateright.GROUP ),
srcHover: resolveUrl( this.prefixUrl, navImages.rotateright.HOVER ),
srcDown: resolveUrl( this.prefixUrl, navImages.rotateright.DOWN ),
onRelease: onRotateRightHandler,
onFocus: onFocusHandler,
onBlur: onBlurHandler
}));
}
if ( useGroup ) {
this.buttons = new $.ButtonGroup({
buttons: buttons,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold
});
this.navControl = this.buttons.element;
this.addHandler( 'open', $.delegate( this, lightUp ) );
if( this.toolbar ){
this.toolbar.addControl(
this.navControl,
{anchor: $.ControlAnchor.TOP_LEFT}
);
} else {
this.addControl(
this.navControl,
{anchor: this.navigationControlAnchor || $.ControlAnchor.TOP_LEFT}
);
}
}
}
return this;
},
/**
* Gets the active page of a sequence
* @function
* @return {Number}
*/
currentPage: function() {
return THIS[ this.hash ].sequence;
},
/**
* @function
* @return {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:page
*/
goToPage: function( page ){
if( page >= 0 && page < this.tileSources.length ){
/**
* Raised when the page is changed on a viewer configured with multiple image sources (see {@link OpenSeadragon.Viewer#goToPage}).
*
* @event page
* @memberof OpenSeadragon.Viewer
* @type {Object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Number} page - The page index.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'page', { page: page } );
THIS[ this.hash ].sequence = page;
this._updateSequenceButtons( page );
this.open( this.tileSources[ page ] );
if( this.referenceStrip ){
this.referenceStrip.setFocus( page );
}
}
return this;
},
/**
* Adds an html element as an overlay to the current viewport. Useful for
* highlighting words or areas of interest on an image or other zoomable
* interface. The overlays added via this method are removed when the viewport
* is closed which include when changing page.
* @method
* @param {Element|String|Object} element - A reference to an element or an id for
* the element which will overlayed. Or an Object specifying the configuration for the overlay
* @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or
* rectangle which will be overlayed.
* @param {OpenSeadragon.OverlayPlacement} placement - The position of the
* viewport which the location coordinates will be treated as relative
* to.
* @param {function} onDraw - If supplied the callback is called when the overlay
* needs to be drawn. It it the responsibility of the callback to do any drawing/positioning.
* It is passed position, size and element.
* @return {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:add-overlay
*/
addOverlay: function( element, location, placement, onDraw ) {
var options;
if( $.isPlainObject( element ) ){
options = element;
} else {
options = {
element: element,
location: location,
placement: placement,
onDraw: onDraw
};
}
element = $.getElement( options.element );
if ( getOverlayIndex( this.currentOverlays, element ) >= 0 ) {
// they're trying to add a duplicate overlay
return this;
}
this.currentOverlays.push( getOverlayObject( this, options ) );
THIS[ this.hash ].forceRedraw = true;
/**
* Raised when an overlay is added to the viewer (see {@link OpenSeadragon.Viewer#addOverlay}).
*
* @event add-overlay
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Element} element - The overlay element.
* @property {OpenSeadragon.Point|OpenSeadragon.Rect} location
* @property {OpenSeadragon.OverlayPlacement} placement
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'add-overlay', {
element: element,
location: options.location,
placement: options.placement
});
return this;
},
/**
* Updates the overlay represented by the reference to the element or
* element id moving it to the new location, relative to the new placement.
* @method
* @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or
* rectangle which will be overlayed.
* @param {OpenSeadragon.OverlayPlacement} placement - The position of the
* viewport which the location coordinates will be treated as relative
* to.
* @return {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:update-overlay
*/
updateOverlay: function( element, location, placement ) {
var i;
element = $.getElement( element );
i = getOverlayIndex( this.currentOverlays, element );
if ( i >= 0 ) {
this.currentOverlays[ i ].update( location, placement );
THIS[ this.hash ].forceRedraw = true;
/**
* Raised when an overlay's location or placement changes
* (see {@link OpenSeadragon.Viewer#updateOverlay}).
*
* @event update-overlay
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the
* Viewer which raised the event.
* @property {Element} element
* @property {OpenSeadragon.Point|OpenSeadragon.Rect} location
* @property {OpenSeadragon.OverlayPlacement} placement
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'update-overlay', {
element: element,
location: location,
placement: placement
});
}
return this;
},
/**
* Removes an overlay identified by the reference element or element id
* and schedules an update.
* @method
* @param {Element|String} element - A reference to the element or an
* element id which represent the ovelay content to be removed.
* @return {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:remove-overlay
*/
removeOverlay: function( element ) {
var i;
element = $.getElement( element );
i = getOverlayIndex( this.currentOverlays, element );
if ( i >= 0 ) {
this.currentOverlays[ i ].destroy();
this.currentOverlays.splice( i, 1 );
THIS[ this.hash ].forceRedraw = true;
/**
* Raised when an overlay is removed from the viewer
* (see {@link OpenSeadragon.Viewer#removeOverlay}).
*
* @event remove-overlay
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the
* Viewer which raised the event.
* @property {Element} element - The overlay element.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'remove-overlay', {
element: element
});
}
return this;
},
/**
* Removes all currently configured Overlays from this Viewer and schedules
* an update.
* @method
* @return {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:clear-overlay
*/
clearOverlays: function() {
while ( this.currentOverlays.length > 0 ) {
this.currentOverlays.pop().destroy();
}
THIS[ this.hash ].forceRedraw = true;
/**
* Raised when all overlays are removed from the viewer (see {@link OpenSeadragon.Drawer#clearOverlays}).
*
* @event clear-overlay
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'clear-overlay', {} );
return this;
},
/**
* Updates the sequence buttons.
* @function OpenSeadragon.Viewer.prototype._updateSequenceButtons
* @private
* @param {Number} Sequence Value
*/
_updateSequenceButtons: function( page ) {
if ( this.nextButton ) {
if( ( this.tileSources.length - 1 ) === page ) {
//Disable next button
if ( !this.navPrevNextWrap ) {
this.nextButton.disable();
}
} else {
this.nextButton.enable();
}
}
if ( this.previousButton ) {
if ( page > 0 ) {
//Enable previous button
this.previousButton.enable();
} else {
if ( !this.navPrevNextWrap ) {
this.previousButton.disable();
}
}
}
},
/**
* Display a message in the viewport
* @function OpenSeadragon.Viewer.prototype._showMessage
* @private
* @param {String} text message
*/
_showMessage: function ( message ) {
this._hideMessage();
var div = $.makeNeutralElement( "div" );
div.appendChild( document.createTextNode( message ) );
this.messageDiv = $.makeCenteredNode( div );
$.addClass(this.messageDiv, "openseadragon-message");
this.container.appendChild( this.messageDiv );
},
/**
* Hide any currently displayed viewport message
* @function OpenSeadragon.Viewer.prototype._hideMessage
* @private
*/
_hideMessage: function () {
var div = this.messageDiv;
if (div) {
div.parentNode.removeChild(div);
delete this.messageDiv;
}
},
/**
* Gets this viewer's gesture settings for the given pointer device type.
* @method
* @param {String} type - The pointer device type to get the gesture settings for ("mouse", "touch", "pen", etc.).
* @return {OpenSeadragon.GestureSettings}
*/
gestureSettingsByDeviceType: function ( type ) {
switch ( type ) {
case 'mouse':
return this.gestureSettingsMouse;
case 'touch':
return this.gestureSettingsTouch;
case 'pen':
return this.gestureSettingsPen;
default:
return this.gestureSettingsUnknown;
}
}
});
/**
* _getSafeElemSize is like getElementSize(), but refuses to return 0 for x or y,
* which was causing some calling operations to return NaN.
* @returns {Point}
* @private
*/
function _getSafeElemSize (oElement) {
oElement = $.getElement( oElement );
return new $.Point(
(oElement.clientWidth === 0 ? 1 : oElement.clientWidth),
(oElement.clientHeight === 0 ? 1 : oElement.clientHeight)
);
}
/**
* @function
* @private
*/
function getTileSourceImplementation( viewer, tileSource, successCallback,
failCallback ) {
var _this = viewer;
//allow plain xml strings or json strings to be parsed here
if ( $.type( tileSource ) == 'string' ) {
if ( tileSource.match( /\s*<.*/ ) ) {
tileSource = $.parseXml( tileSource );
} else if ( tileSource.match( /\s*[\{\[].*/ ) ) {
/*jshint evil:true*/
tileSource = eval( '(' + tileSource + ')' );
}
}
setTimeout( function() {
if ( $.type( tileSource ) == 'string' ) {
//If its still a string it means it must be a url at this point
tileSource = new $.TileSource( tileSource, function( event ) {
successCallback( event.tileSource );
});
tileSource.addHandler( 'open-failed', function( event ) {
failCallback( event );
} );
} else if ( $.isPlainObject( tileSource ) || tileSource.nodeType ) {
if ( $.isFunction( tileSource.getTileUrl ) ) {
//Custom tile source
var customTileSource = new $.TileSource( tileSource );
customTileSource.getTileUrl = tileSource.getTileUrl;
successCallback( customTileSource );
} else {
//inline configuration
var $TileSource = $.TileSource.determineType( _this, tileSource );
if ( !$TileSource ) {
failCallback( {
message: "Unable to load TileSource",
source: tileSource
});
return;
}
var options = $TileSource.prototype.configure.apply( _this, [ tileSource ] );
var readySource = new $TileSource( options );
successCallback( readySource );
}
} else {
//can assume it's already a tile source implementation
successCallback( tileSource );
}
}, 1 );
}
function getOverlayObject( viewer, overlay ) {
if ( overlay instanceof $.Overlay ) {
return overlay;
}
var element = null;
if ( overlay.element ) {
element = $.getElement( overlay.element );
} else {
var id = overlay.id ?
overlay.id :
"openseadragon-overlay-" + Math.floor( Math.random() * 10000000 );
element = $.getElement( overlay.id );
if ( !element ) {
element = document.createElement( "a" );
element.href = "#/overlay/" + id;
}
element.id = id;
$.addClass( element, overlay.className ?
overlay.className :
"openseadragon-overlay"
);
}
var location = overlay.location;
if ( !location ) {
if ( overlay.width && overlay.height ) {
location = overlay.px !== undefined ?
viewer.viewport.imageToViewportRectangle( new $.Rect(
overlay.px,
overlay.py,
overlay.width,
overlay.height
) ) :
new $.Rect(
overlay.x,
overlay.y,
overlay.width,
overlay.height
);
} else {
location = overlay.px !== undefined ?
viewer.viewport.imageToViewportCoordinates( new $.Point(
overlay.px,
overlay.py
) ) :
new $.Point(
overlay.x,
overlay.y
);
}
}
var placement = overlay.placement;
if ( placement && ( $.type( placement ) === "string" ) ) {
placement = $.OverlayPlacement[ overlay.placement.toUpperCase() ];
}
return new $.Overlay({
element: element,
location: location,
placement: placement,
onDraw: overlay.onDraw,
checkResize: overlay.checkResize
});
}
/**
* @private
* @inner
* Determines the index of the given overlay in the given overlays array.
*/
function getOverlayIndex( overlays, element ) {
var i;
for ( i = overlays.length - 1; i >= 0; i-- ) {
if ( overlays[ i ].element === element ) {
return i;
}
}
return -1;
}
function drawOverlays( viewport, overlays, container ) {
var i,
length = overlays.length;
for ( i = 0; i < length; i++ ) {
overlays[ i ].drawHTML( container, viewport );
}
}
///////////////////////////////////////////////////////////////////////////////
// Schedulers provide the general engine for animation
///////////////////////////////////////////////////////////////////////////////
function scheduleUpdate( viewer, updateFunc ){
return $.requestAnimationFrame( function(){
updateFunc( viewer );
} );
}
//provides a sequence in the fade animation
function scheduleControlsFade( viewer ) {
$.requestAnimationFrame( function(){
updateControlsFade( viewer );
});
}
//initiates an animation to hide the controls
function beginControlsAutoHide( viewer ) {
if ( !viewer.autoHideControls ) {
return;
}
viewer.controlsShouldFade = true;
viewer.controlsFadeBeginTime =
$.now() +
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 = $.now();
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--) {
if (viewer.controls[ i ].autoFade) {
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 );
}
}
///////////////////////////////////////////////////////////////////////////////
// Default view event handlers.
///////////////////////////////////////////////////////////////////////////////
function onFocus(){
abortControlsAutoHide( this );
}
function onBlur(){
beginControlsAutoHide( this );
}
function onCanvasClick( event ) {
var gestureSettings;
if ( !event.preventDefaultAction && this.viewport && event.quick ) {
gestureSettings = this.gestureSettingsByDeviceType( event.pointerType );
if ( gestureSettings.clickToZoom ) {
this.viewport.zoomBy(
event.shift ? 1.0 / this.zoomPerClick : this.zoomPerClick,
this.viewport.pointFromPixel( event.position, true )
);
this.viewport.applyConstraints();
}
}
/**
* Raised when a mouse press/release or touch/remove occurs on the {@link OpenSeadragon.Viewer#canvas} element.
*
* @event canvas-click
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
* @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
* @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
* @property {Boolean} quick - True only if the clickDistThreshold and clickTimeThreshold are both passed. Useful for differentiating between clicks and drags.
* @property {Boolean} shift - True if the shift key was pressed during this event.
* @property {Object} originalEvent - The original DOM event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'canvas-click', {
tracker: event.eventSource,
position: event.position,
quick: event.quick,
shift: event.shift,
originalEvent: event.originalEvent
});
}
function onCanvasDblClick( event ) {
var gestureSettings;
if ( !event.preventDefaultAction && this.viewport ) {
gestureSettings = this.gestureSettingsByDeviceType( event.pointerType );
if ( gestureSettings.dblClickToZoom ) {
this.viewport.zoomBy(
event.shift ? 1.0 / this.zoomPerClick : this.zoomPerClick,
this.viewport.pointFromPixel( event.position, true )
);
this.viewport.applyConstraints();
}
}
/**
* Raised when a double mouse press/release or touch/remove occurs on the {@link OpenSeadragon.Viewer#canvas} element.
*
* @event canvas-double-click
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
* @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
* @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
* @property {Boolean} shift - True if the shift key was pressed during this event.
* @property {Object} originalEvent - The original DOM event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'canvas-double-click', {
tracker: event.eventSource,
position: event.position,
shift: event.shift,
originalEvent: event.originalEvent
});
}
function onCanvasDrag( event ) {
var gestureSettings;
if ( !event.preventDefaultAction && this.viewport ) {
gestureSettings = this.gestureSettingsByDeviceType( event.pointerType );
if( !this.panHorizontal ){
event.delta.x = 0;
}
if( !this.panVertical ){
event.delta.y = 0;
}
this.viewport.panBy( this.viewport.deltaPointsFromPixels( event.delta.negate() ), gestureSettings.flickEnabled );
if( this.constrainDuringPan ){
this.viewport.applyConstraints();
}
}
/**
* Raised when a mouse or touch drag operation occurs on the {@link OpenSeadragon.Viewer#canvas} element.
*
* @event canvas-drag
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
* @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
* @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
* @property {OpenSeadragon.Point} delta - The x,y components of the difference between start drag and end drag.
* @property {Number} speed - Current computed speed, in pixels per second.
* @property {Number} direction - Current computed direction, expressed as an angle counterclockwise relative to the positive X axis (-pi to pi, in radians). Only valid if speed > 0.
* @property {Boolean} shift - True if the shift key was pressed during this event.
* @property {Object} originalEvent - The original DOM event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'canvas-drag', {
tracker: event.eventSource,
position: event.position,
delta: event.delta,
speed: event.speed,
direction: event.direction,
shift: event.shift,
originalEvent: event.originalEvent
});
}
function onCanvasDragEnd( event ) {
var gestureSettings;
if ( !event.preventDefaultAction && this.viewport ) {
gestureSettings = this.gestureSettingsByDeviceType( event.pointerType );
if ( gestureSettings.flickEnabled && event.speed >= gestureSettings.flickMinSpeed ) {
var amplitudeX = gestureSettings.flickMomentum * ( event.speed * Math.cos( event.direction - (Math.PI / 180 * this.viewport.degrees) ) ),
amplitudeY = gestureSettings.flickMomentum * ( event.speed * Math.sin( event.direction - (Math.PI / 180 * this.viewport.degrees) ) ),
center = this.viewport.pixelFromPoint( this.viewport.getCenter( true ) ),
target = this.viewport.pointFromPixel( new $.Point( center.x - amplitudeX, center.y - amplitudeY ) );
if( !this.panHorizontal ) {
target.x = center.x;
}
if( !this.panVertical ) {
target.y = center.y;
}
this.viewport.panTo( target, false );
}
this.viewport.applyConstraints();
}
/**
* Raised when a mouse or touch drag operation ends on the {@link OpenSeadragon.Viewer#canvas} element.
*
* @event canvas-drag-end
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
* @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
* @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
* @property {Number} speed - Speed at the end of a drag gesture, in pixels per second.
* @property {Number} direction - Direction at the end of a drag gesture, expressed as an angle counterclockwise relative to the positive X axis (-pi to pi, in radians). Only valid if speed > 0.
* @property {Boolean} shift - True if the shift key was pressed during this event.
* @property {Object} originalEvent - The original DOM event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'canvas-drag-end', {
tracker: event.eventSource,
position: event.position,
speed: event.speed,
direction: event.direction,
shift: event.shift,
originalEvent: event.originalEvent
});
}
function onCanvasRelease( event ) {
/**
* Raised when the mouse button is released or touch ends on the {@link OpenSeadragon.Viewer#canvas} element.
*
* @event canvas-release
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
* @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
* @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
* @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false.
* @property {Boolean} insideElementReleased - True if the cursor still inside the tracked element when the button was released.
* @property {Object} originalEvent - The original DOM event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'canvas-release', {
tracker: event.eventSource,
position: event.position,
insideElementPressed: event.insideElementPressed,
insideElementReleased: event.insideElementReleased,
originalEvent: event.originalEvent
});
}
function onCanvasPinch( event ) {
var gestureSettings,
centerPt,
lastCenterPt,
panByPt;
if ( !event.preventDefaultAction && this.viewport ) {
gestureSettings = this.gestureSettingsByDeviceType( event.pointerType );
if ( gestureSettings.pinchToZoom ) {
centerPt = this.viewport.pointFromPixel( event.center, true );
lastCenterPt = this.viewport.pointFromPixel( event.lastCenter, true );
panByPt = lastCenterPt.minus( centerPt );
if( !this.panHorizontal ) {
panByPt.x = 0;
}
if( !this.panVertical ) {
panByPt.y = 0;
}
this.viewport.zoomBy( event.distance / event.lastDistance, centerPt, true );
this.viewport.panBy( panByPt, true );
this.viewport.applyConstraints();
}
if ( gestureSettings.pinchRotate ) {
// Pinch rotate
var angle1 = Math.atan2(event.gesturePoints[0].currentPos.y - event.gesturePoints[1].currentPos.y,
event.gesturePoints[0].currentPos.x - event.gesturePoints[1].currentPos.x);
var angle2 = Math.atan2(event.gesturePoints[0].lastPos.y - event.gesturePoints[1].lastPos.y,
event.gesturePoints[0].lastPos.x - event.gesturePoints[1].lastPos.x);
this.viewport.setRotation(this.viewport.getRotation() + ((angle1 - angle2) * (180 / Math.PI)));
}
}
/**
* Raised when a pinch event occurs on the {@link OpenSeadragon.Viewer#canvas} element.
*
* @event canvas-pinch
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
* @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
* @property {Array.} gesturePoints - Gesture points associated with the gesture. Velocity data can be found here.
* @property {OpenSeadragon.Point} lastCenter - The previous center point of the two pinch contact points relative to the tracked element.
* @property {OpenSeadragon.Point} center - The center point of the two pinch contact points relative to the tracked element.
* @property {Number} lastDistance - The previous distance between the two pinch contact points in CSS pixels.
* @property {Number} distance - The distance between the two pinch contact points in CSS pixels.
* @property {Boolean} shift - True if the shift key was pressed during this event.
* @property {Object} originalEvent - The original DOM event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent('canvas-pinch', {
tracker: event.eventSource,
gesturePoints: event.gesturePoints,
lastCenter: event.lastCenter,
center: event.center,
lastDistance: event.lastDistance,
distance: event.distance,
shift: event.shift,
originalEvent: event.originalEvent
});
//cancels event
return false;
}
function onCanvasScroll( event ) {
var gestureSettings,
factor;
if ( !event.preventDefaultAction && this.viewport ) {
gestureSettings = this.gestureSettingsByDeviceType( event.pointerType );
if ( gestureSettings.scrollToZoom ) {
factor = Math.pow( this.zoomPerScroll, event.scroll );
this.viewport.zoomBy(
factor,
this.viewport.pointFromPixel( event.position, true )
);
this.viewport.applyConstraints();
}
}
/**
* Raised when a scroll event occurs on the {@link OpenSeadragon.Viewer#canvas} element (mouse wheel).
*
* @event canvas-scroll
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
* @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
* @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
* @property {Number} scroll - The scroll delta for the event.
* @property {Boolean} shift - True if the shift key was pressed during this event.
* @property {Object} originalEvent - The original DOM event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'canvas-scroll', {
tracker: event.eventSource,
position: event.position,
scroll: event.scroll,
shift: event.shift,
originalEvent: event.originalEvent
});
//cancels event
return false;
}
function onContainerExit( event ) {
if ( !event.insideElementPressed ) {
THIS[ this.hash ].mouseInside = false;
if ( !THIS[ this.hash ].animating ) {
beginControlsAutoHide( this );
}
}
/**
* Raised when the cursor leaves the {@link OpenSeadragon.Viewer#container} element.
*
* @event container-exit
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
* @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
* @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
* @property {Number} buttons - Current buttons pressed. A combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
* @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false.
* @property {Boolean} buttonDownAny - Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead.
* @property {Object} originalEvent - The original DOM event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'container-exit', {
tracker: event.eventSource,
position: event.position,
buttons: event.buttons,
insideElementPressed: event.insideElementPressed,
buttonDownAny: event.buttonDownAny,
originalEvent: event.originalEvent
});
}
function onContainerPress( event ) {
if ( event.pointerType === 'touch' && !$.MouseTracker.haveTouchEnter ) {
THIS[ this.hash ].mouseInside = true;
abortControlsAutoHide( this );
}
}
function onContainerRelease( event ) {
if ( !event.insideElementReleased || ( event.pointerType === 'touch' && !$.MouseTracker.haveTouchEnter ) ) {
THIS[ this.hash ].mouseInside = false;
if ( !THIS[ this.hash ].animating ) {
beginControlsAutoHide( this );
}
}
/**
* Raised when the mouse button is released or touch ends on the {@link OpenSeadragon.Viewer#container} element.
*
* @event container-release
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
* @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
* @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
* @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false.
* @property {Boolean} insideElementReleased - True if the cursor still inside the tracked element when the button was released.
* @property {Object} originalEvent - The original DOM event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'container-release', {
tracker: event.eventSource,
position: event.position,
insideElementPressed: event.insideElementPressed,
insideElementReleased: event.insideElementReleased,
originalEvent: event.originalEvent
});
}
function onContainerEnter( event ) {
THIS[ this.hash ].mouseInside = true;
abortControlsAutoHide( this );
/**
* Raised when the cursor enters the {@link OpenSeadragon.Viewer#container} element.
*
* @event container-enter
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
* @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
* @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
* @property {Number} buttons - Current buttons pressed. A combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
* @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false.
* @property {Boolean} buttonDownAny - Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead.
* @property {Object} originalEvent - The original DOM event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'container-enter', {
tracker: event.eventSource,
position: event.position,
buttons: event.buttons,
insideElementPressed: event.insideElementPressed,
buttonDownAny: event.buttonDownAny,
originalEvent: event.originalEvent
});
}
///////////////////////////////////////////////////////////////////////////////
// Page update routines ( aka Views - for future reference )
///////////////////////////////////////////////////////////////////////////////
function updateMulti( viewer ) {
updateOnce( viewer );
// Request the next frame, unless we've been closed
if ( viewer.isOpen() ) {
viewer._updateRequestId = scheduleUpdate( viewer, updateMulti );
} else {
viewer._updateRequestId = false;
}
}
function updateOnce( viewer ) {
var containerSize,
animated;
//viewer.profiler.beginUpdate();
if ( viewer.autoResize ) {
containerSize = _getSafeElemSize( viewer.container );
if ( !containerSize.equals( THIS[ viewer.hash ].prevContainerSize ) ) {
// maintain image position
var oldBounds = viewer.viewport.getBounds();
var oldCenter = viewer.viewport.getCenter();
resizeViewportAndRecenter(viewer, containerSize, oldBounds, oldCenter);
THIS[ viewer.hash ].prevContainerSize = containerSize;
THIS[ viewer.hash ].forceRedraw = true;
}
}
animated = viewer.viewport.update();
if( viewer.referenceStrip ){
animated = viewer.referenceStrip.update( viewer.viewport ) || animated;
}
if ( !THIS[ viewer.hash ].animating && animated ) {
/**
* Raised when any spring animation starts (zoom, pan, etc.).
*
* @event animation-start
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
viewer.raiseEvent( "animation-start" );
abortControlsAutoHide( viewer );
}
if ( animated || THIS[ viewer.hash ].forceRedraw || viewer.world.needsUpdate() ) {
updateWorld( viewer );
drawOverlays( viewer.viewport, viewer.currentOverlays, viewer.overlaysContainer );
if( viewer.navigator ){
viewer.navigator.update( viewer.viewport );
}
THIS[ viewer.hash ].forceRedraw = false;
if (animated) {
/**
* Raised when any spring animation update occurs (zoom, pan, etc.).
*
* @event animation
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
viewer.raiseEvent( "animation" );
}
}
if ( THIS[ viewer.hash ].animating && !animated ) {
/**
* Raised when any spring animation ends (zoom, pan, etc.).
*
* @event animation-finish
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
viewer.raiseEvent( "animation-finish" );
if ( !THIS[ viewer.hash ].mouseInside ) {
beginControlsAutoHide( viewer );
}
}
THIS[ viewer.hash ].animating = animated;
//viewer.profiler.endUpdate();
}
// This function resizes the viewport and recenters the image
// as it was before resizing.
// TODO: better adjust width and height. The new width and height
// should depend on the image dimensions and on the dimensions
// of the viewport before and after switching mode.
function resizeViewportAndRecenter( viewer, containerSize, oldBounds, oldCenter ) {
var viewport = viewer.viewport;
viewport.resize( containerSize, true );
// We try to remove blanks as much as possible
var worldBounds = viewer.world.getHomeBounds();
var aspectRatio = worldBounds.width / worldBounds.height;
var imageHeight = 1 / aspectRatio;
var newWidth = oldBounds.width <= 1 ? oldBounds.width : 1;
var newHeight = oldBounds.height <= imageHeight ?
oldBounds.height : imageHeight;
var newBounds = new $.Rect(
oldCenter.x - ( newWidth / 2.0 ),
oldCenter.y - ( newHeight / 2.0 ),
newWidth,
newHeight
);
viewport.fitBounds( newBounds, true );
}
function updateWorld( viewer ) {
viewer.drawer.clear();
viewer.world.update();
/**
* - Needs documentation -
*
* @event update-viewport
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
viewer.raiseEvent( 'update-viewport', {} );
}
///////////////////////////////////////////////////////////////////////////////
// Navigation Controls
///////////////////////////////////////////////////////////////////////////////
function resolveUrl( prefix, url ) {
return prefix ? prefix + url : url;
}
function beginZoomingIn() {
THIS[ this.hash ].lastZoomTime = $.now();
THIS[ this.hash ].zoomFactor = this.zoomPerSecond;
THIS[ this.hash ].zooming = true;
scheduleZoom( this );
}
function beginZoomingOut() {
THIS[ this.hash ].lastZoomTime = $.now();
THIS[ this.hash ].zoomFactor = 1.0 / this.zoomPerSecond;
THIS[ this.hash ].zooming = true;
scheduleZoom( this );
}
function endZooming() {
THIS[ this.hash ].zooming = false;
}
function scheduleZoom( viewer ) {
$.requestAnimationFrame( $.delegate( viewer, doZoom ) );
}
function doZoom() {
var currentTime,
deltaTime,
adjustedFactor;
if ( THIS[ this.hash ].zooming && this.viewport) {
currentTime = $.now();
deltaTime = currentTime - THIS[ this.hash ].lastZoomTime;
adjustedFactor = Math.pow( THIS[ this.hash ].zoomFactor, deltaTime / 1000 );
this.viewport.zoomBy( adjustedFactor );
this.viewport.applyConstraints();
THIS[ this.hash ].lastZoomTime = currentTime;
scheduleZoom( this );
}
}
function doSingleZoomIn() {
if ( this.viewport ) {
THIS[ this.hash ].zooming = false;
this.viewport.zoomBy(
this.zoomPerClick / 1.0
);
this.viewport.applyConstraints();
}
}
function doSingleZoomOut() {
if ( this.viewport ) {
THIS[ this.hash ].zooming = false;
this.viewport.zoomBy(
1.0 / this.zoomPerClick
);
this.viewport.applyConstraints();
}
}
function lightUp() {
this.buttons.emulateEnter();
this.buttons.emulateExit();
}
function onHome() {
if ( this.viewport ) {
this.viewport.goHome();
}
}
function onFullScreen() {
if ( this.isFullPage() && !$.isFullScreen() ) {
// Is fullPage but not fullScreen
this.setFullPage( false );
} else {
this.setFullScreen( !this.isFullPage() );
}
// correct for no mouseout event on change
if ( this.buttons ) {
this.buttons.emulateExit();
}
this.fullPageButton.element.focus();
if ( this.viewport ) {
this.viewport.applyConstraints();
}
}
/**
* Note: The current rotation feature is limited to 90 degree turns.
*/
function onRotateLeft() {
if ( this.viewport ) {
var currRotation = this.viewport.getRotation();
if (currRotation === 0) {
currRotation = 270;
}
else {
currRotation -= 90;
}
this.viewport.setRotation(currRotation);
}
}
/**
* Note: The current rotation feature is limited to 90 degree turns.
*/
function onRotateRight() {
if ( this.viewport ) {
var currRotation = this.viewport.getRotation();
if (currRotation === 270) {
currRotation = 0;
}
else {
currRotation += 90;
}
this.viewport.setRotation(currRotation);
}
}
function onPrevious(){
var previous = THIS[ this.hash ].sequence - 1;
if(this.navPrevNextWrap && previous < 0){
previous += this.tileSources.length;
}
this.goToPage( previous );
}
function onNext(){
var next = THIS[ this.hash ].sequence + 1;
if(this.navPrevNextWrap && next >= this.tileSources.length){
next = 0;
}
this.goToPage( next );
}
}( OpenSeadragon ));