mirror of
https://github.com/openseadragon/openseadragon.git
synced 2024-11-29 08:36:10 +03:00
718082998b
Use correct option name (displayRects, not dispRects) in DziTileSource.
9532 lines
303 KiB
JavaScript
9532 lines
303 KiB
JavaScript
/**
|
|
* @version OpenSeadragon 0.9.90
|
|
*
|
|
* @fileOverview
|
|
* <h2>
|
|
* <strong>
|
|
* OpenSeadragon - Javascript Deep Zooming
|
|
* </strong>
|
|
* </h2>
|
|
* <p>
|
|
* OpenSeadragon is provides an html interface for creating
|
|
* deep zoom user interfaces. The simplest examples include deep
|
|
* zoom for large resolution images, and complex examples include
|
|
* zoomable map interfaces driven by SVG files.
|
|
* </p>
|
|
*
|
|
* @author <br/>(c) 2011, 2012 Christopher Thatcher
|
|
* @author <br/>(c) 2010 OpenSeadragon Team
|
|
* @author <br/>(c) 2010 CodePlex Foundation
|
|
*
|
|
* <p>
|
|
* <strong>Original license preserved below: </strong><br/>
|
|
* <pre>
|
|
* ----------------------------------------------------------------------------
|
|
*
|
|
* License: New BSD License (BSD)
|
|
* Copyright (c) 2010, OpenSeadragon
|
|
* All rights reserved.
|
|
*
|
|
* 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 OpenSeadragon 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.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* </pre>
|
|
* </p>
|
|
* <p>
|
|
* <strong> Work done by Chris Thatcher adds an MIT license </strong><br/>
|
|
* <pre>
|
|
* ----------------------------------------------------------------------------
|
|
* (c) Christopher Thatcher 2011, 2012. All rights reserved.
|
|
*
|
|
* Licensed with the MIT License
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
* ---------------------------------------------------------------------------
|
|
* </pre>
|
|
* </p>
|
|
**/
|
|
|
|
/**
|
|
* The root namespace for OpenSeadragon, this function also serves as a single
|
|
* point of instantiation for an {@link OpenSeadragon.Viewer}, including all
|
|
* combinations of out-of-the-box configurable features. All utility methods
|
|
* and classes are defined on or below this namespace.
|
|
*
|
|
* @namespace
|
|
* @function
|
|
* @name OpenSeadragon
|
|
* @exports $ as OpenSeadragon
|
|
*
|
|
* @param {Object} options All required and optional settings for instantiating
|
|
* a new instance of an OpenSeadragon image viewer.
|
|
*
|
|
* @param {String} options.xmlPath
|
|
* DEPRECATED. A relative path to load a DZI file from the server.
|
|
* Prefer the newer options.tileSources.
|
|
*
|
|
* @param {Array|String|Function|Object[]|Array[]|String[]|Function[]} options.tileSources
|
|
* As an Array, the tileSource can hold either be all Objects or mixed
|
|
* types of Arrays of Objects, String, Function. When a value is a String,
|
|
* the tileSource is used to create a {@link OpenSeadragon.DziTileSource}.
|
|
* When a value is a Function, the function is used to create a new
|
|
* {@link OpenSeadragon.TileSource} whose abstract method
|
|
* getUrl( level, x, y ) is implemented by the function. Finally, when it
|
|
* is an Array of objects, it is used to create a
|
|
* {@link OpenSeadragon.LegacyTileSource}.
|
|
*
|
|
* @param {Boolean} [options.debugMode=true]
|
|
* Currently does nothing. TODO: provide an in-screen panel providing event
|
|
* detail feedback.
|
|
*
|
|
* @param {Number} [options.animationTime=1.5]
|
|
* Specifies the animation duration per each {@link OpenSeadragon.Spring}
|
|
* which occur when the image is dragged or zoomed.
|
|
*
|
|
* @param {Number} [options.blendTime=0.5]
|
|
* Specifies the duration of animation as higher or lower level tiles are
|
|
* replacing the existing tile.
|
|
*
|
|
* @param {Boolean} [options.alwaysBlend=false]
|
|
* Forces the tile to always blend. By default the tiles skip blending
|
|
* when the blendTime is surpassed and the current animation frame would
|
|
* not complete the blend.
|
|
*
|
|
* @param {Boolean} [options.autoHideControls=true]
|
|
* If the user stops interacting with the viewport, fade the navigation
|
|
* controls. Useful for presentation since the controls are by default
|
|
* floated on top of the image the user is viewing.
|
|
*
|
|
* @param {Boolean} [options.immediateRender=false]
|
|
* Render the best closest level first, ignoring the lowering levels which
|
|
* provide the effect of very blurry to sharp. It is recommended to change
|
|
* setting to true for mobile devices.
|
|
*
|
|
* @param {Boolean} [options.wrapHorizontal=false]
|
|
* Set to true to force the image to wrap horizontally within the viewport.
|
|
* Useful for maps or images representing the surface of a sphere or cylinder.
|
|
*
|
|
* @param {Boolean} [options.wrapVertical=false]
|
|
* Set to true to force the image to wrap vertically within the viewport.
|
|
* Useful for maps or images representing the surface of a sphere or cylinder.
|
|
*
|
|
* @param {Number} [options.minZoomImageRatio=0.8]
|
|
* The minimum percentage ( expressed as a number between 0 and 1 ) of
|
|
* the viewport height or width at which the zoom out will be constrained.
|
|
* Setting it to 0, for example will allow you to zoom out infinitly.
|
|
*
|
|
* @param {Number} [options.maxZoomPixelRatio=2]
|
|
* The maximum ratio to allow a zoom-in to affect the highest level pixel
|
|
* ratio. This can be set to Infinity to allow 'infinite' zooming into the
|
|
* image though it is less effective visually if the HTML5 Canvas is not
|
|
* availble on the viewing device.
|
|
*
|
|
* @param {Number} [options.visibilityRatio=0.5]
|
|
* The percentage ( as a number from 0 to 1 ) of the source image which
|
|
* must be kept within the viewport. If the image is dragged beyond that
|
|
* limit, it will 'bounce' back until the minimum visibility ration is
|
|
* achieved. Setting this to 0 and wrapHorizontal ( or wrapVertical ) to
|
|
* true will provide the effect of an infinitely scrolling viewport.
|
|
*
|
|
* @param {Number} [options.springStiffness=5.0]
|
|
*
|
|
* @param {Number} [options.imageLoaderLimit=0]
|
|
* The maximum number of image requests to make concurrently. By default
|
|
* it is set to 0 allowing the browser to make the maximum number of
|
|
* image requests in parallel as allowed by the browsers policy.
|
|
*
|
|
* @param {Number} [options.clickTimeThreshold=200]
|
|
* If multiple mouse clicks occurs within less than this number of
|
|
* milliseconds, treat them as a single click.
|
|
*
|
|
* @param {Number} [options.clickDistThreshold=5]
|
|
* If a mouse or touch drag occurs and the distance to the starting drag
|
|
* point is less than this many pixels, ignore the drag event.
|
|
*
|
|
* @param {Number} [options.zoomPerClick=2.0]
|
|
* The "zoom distance" per mouse click or touch tap.
|
|
*
|
|
* @param {Number} [options.zoomPerScroll=1.2]
|
|
* The "zoom distance" per mouse scroll or touch pinch.
|
|
*
|
|
* @param {Number} [options.zoomPerSecond=2.0]
|
|
* The number of seconds to animate a single zoom event over.
|
|
*
|
|
* @param {Boolean} [options.showNavigationControl=true]
|
|
* Set to false to prevent the appearance of the default navigation controls.
|
|
*
|
|
* @param {Number} [options.controlsFadeDelay=2000]
|
|
* The number of milliseconds to wait once the user has stopped interacting
|
|
* with the interface before begining to fade the controls. Assumes
|
|
* showNavigationControl and autoHideControls are both true.
|
|
*
|
|
* @param {Number} [options.controlsFadeLength=1500]
|
|
* The number of milliseconds to animate the controls fading out.
|
|
*
|
|
* @param {Number} [options.maxImageCacheCount=100]
|
|
* The max number of images we should keep in memory (per drawer).
|
|
*
|
|
* @param {Number} [options.minPixelRatio=0.5]
|
|
* The higher the minPixelRatio, the lower the quality of the image that
|
|
* is considered sufficient to stop rendering a given zoom level. For
|
|
* example, if you are targeting mobile devices with less bandwith you may
|
|
* try setting this to 1.5 or higher.
|
|
*
|
|
* @param {Boolean} [options.mouseNavEnabled=true]
|
|
* Is the user able to interact with the image via mouse or touch. Default
|
|
* interactions include draging the image in a plane, and zooming in toward
|
|
* and away from the image.
|
|
*
|
|
* @param {Boolean} [options.preserveViewport=false]
|
|
* If the viewer has been configured with a sequence of tile sources, then
|
|
* normally navigating to through each image resets the viewport to 'home'
|
|
* position. If preserveViewport is set to true, then the viewport position
|
|
* is preserved when navigating between images in the sequence.
|
|
*
|
|
* @param {String} [options.prefixUrl='']
|
|
* Appends the prefixUrl to navImages paths, which is very useful
|
|
* since the default paths are rarely useful for production
|
|
* environments.
|
|
*
|
|
* @param {Object} [options.navImages=]
|
|
* An object with a property for each button or other built-in navigation
|
|
* control, eg the current 'zoomIn', 'zoomOut', 'home', and 'fullpage'.
|
|
* Each of those in turn provides an image path for each state of the botton
|
|
* or navigation control, eg 'REST', 'GROUP', 'HOVER', 'PRESS'. Finally the
|
|
* image paths, by default assume there is a folder on the servers root path
|
|
* called '/images', eg '/images/zoomin_rest.png'. If you need to adjust
|
|
* these paths, prefer setting the option.prefixUrl rather than overriding
|
|
* every image path directly through this setting.
|
|
*
|
|
* @returns {OpenSeadragon.Viewer}
|
|
*/
|
|
OpenSeadragon = window.OpenSeadragon || function( options ){
|
|
|
|
return new OpenSeadragon.Viewer( options );
|
|
|
|
};
|
|
|
|
(function( $ ){
|
|
|
|
|
|
/**
|
|
* Taken from jquery 1.6.1
|
|
* [[Class]] -> type pairs
|
|
* @private
|
|
*/
|
|
var class2type = {
|
|
'[object Boolean]': 'boolean',
|
|
'[object Number]': 'number',
|
|
'[object String]': 'string',
|
|
'[object Function]': 'function',
|
|
'[object Array]': 'array',
|
|
'[object Date]': 'date',
|
|
'[object RegExp]': 'regexp',
|
|
'[object Object]': 'object'
|
|
},
|
|
// Save a reference to some core methods
|
|
toString = Object.prototype.toString,
|
|
hasOwn = Object.prototype.hasOwnProperty,
|
|
push = Array.prototype.push,
|
|
slice = Array.prototype.slice,
|
|
trim = String.prototype.trim,
|
|
indexOf = Array.prototype.indexOf;
|
|
|
|
|
|
/**
|
|
* Taken from jQuery 1.6.1
|
|
* @name $.isFunction
|
|
* @function
|
|
* @see <a href='http://www.jquery.com/'>jQuery</a>
|
|
*/
|
|
$.isFunction = function( obj ) {
|
|
return $.type(obj) === "function";
|
|
};
|
|
|
|
|
|
/**
|
|
* Taken from jQuery 1.6.1
|
|
* @name $.isArray
|
|
* @function
|
|
* @see <a href='http://www.jquery.com/'>jQuery</a>
|
|
*/
|
|
$.isArray = Array.isArray || function( obj ) {
|
|
return $.type(obj) === "array";
|
|
};
|
|
|
|
|
|
/**
|
|
* A crude way of determining if an object is a window.
|
|
* Taken from jQuery 1.6.1
|
|
* @name $.isWindow
|
|
* @function
|
|
* @see <a href='http://www.jquery.com/'>jQuery</a>
|
|
*/
|
|
$.isWindow = function( obj ) {
|
|
return obj && typeof obj === "object" && "setInterval" in obj;
|
|
};
|
|
|
|
|
|
/**
|
|
* Taken from jQuery 1.6.1
|
|
* @name $.type
|
|
* @function
|
|
* @see <a href='http://www.jquery.com/'>jQuery</a>
|
|
*/
|
|
$.type = function( obj ) {
|
|
return obj == null ?
|
|
String( obj ) :
|
|
class2type[ toString.call(obj) ] || "object";
|
|
};
|
|
|
|
|
|
/**
|
|
* Taken from jQuery 1.6.1
|
|
* @name $.isPlainObject
|
|
* @function
|
|
* @see <a href='http://www.jquery.com/'>jQuery</a>
|
|
*/
|
|
$.isPlainObject = function( obj ) {
|
|
// Must be an Object.
|
|
// Because of IE, we also have to check the presence of the constructor property.
|
|
// Make sure that DOM nodes and window objects don't pass through, as well
|
|
if ( !obj || OpenSeadragon.type(obj) !== "object" || obj.nodeType || $.isWindow( obj ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Not own constructor property must be Object
|
|
if ( obj.constructor &&
|
|
!hasOwn.call(obj, "constructor") &&
|
|
!hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
|
|
return false;
|
|
}
|
|
|
|
// Own properties are enumerated firstly, so to speed up,
|
|
// if last one is own, then all properties are own.
|
|
|
|
var key;
|
|
for ( key in obj ) {}
|
|
|
|
return key === undefined || hasOwn.call( obj, key );
|
|
};
|
|
|
|
|
|
/**
|
|
* Taken from jQuery 1.6.1
|
|
* @name $.isEmptyObject
|
|
* @function
|
|
* @see <a href='http://www.jquery.com/'>jQuery</a>
|
|
*/
|
|
$.isEmptyObject = function( obj ) {
|
|
for ( var name in obj ) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
|
|
}( OpenSeadragon ));
|
|
|
|
/**
|
|
* This closure defines all static methods available to the OpenSeadragon
|
|
* namespace. Many, if not most, are taked directly from jQuery for use
|
|
* to simplify and reduce common programming patterns. More static methods
|
|
* from jQuery may eventually make their way into this though we are
|
|
* attempting to avoid substaintial plagarism or the more explicit dependency
|
|
* on jQuery only because OpenSeadragon is a broadly useful code base and
|
|
* would be made less broad by requiring jQuery fully.
|
|
*
|
|
* Some static methods have also been refactored from the original OpenSeadragon
|
|
* project.
|
|
*/
|
|
(function( $ ){
|
|
|
|
/**
|
|
* Taken from jQuery 1.6.1
|
|
* @see <a href='http://www.jquery.com/'>jQuery</a>
|
|
*/
|
|
$.extend = function() {
|
|
var options,
|
|
name,
|
|
src,
|
|
copy,
|
|
copyIsArray,
|
|
clone,
|
|
target = arguments[ 0 ] || {},
|
|
length = arguments.length,
|
|
deep = false,
|
|
i = 1;
|
|
|
|
// Handle a deep copy situation
|
|
if ( typeof target === "boolean" ) {
|
|
deep = target;
|
|
target = arguments[ 1 ] || {};
|
|
// skip the boolean and the target
|
|
i = 2;
|
|
}
|
|
|
|
// Handle case when target is a string or something (possible in deep copy)
|
|
if ( typeof target !== "object" && !OpenSeadragon.isFunction( target ) ) {
|
|
target = {};
|
|
}
|
|
|
|
// extend jQuery itself if only one argument is passed
|
|
if ( length === i ) {
|
|
target = this;
|
|
--i;
|
|
}
|
|
|
|
for ( ; i < length; i++ ) {
|
|
// Only deal with non-null/undefined values
|
|
if ( ( options = arguments[ i ] ) != null ) {
|
|
// Extend the base object
|
|
for ( name in options ) {
|
|
src = target[ name ];
|
|
copy = options[ name ];
|
|
|
|
// Prevent never-ending loop
|
|
if ( target === copy ) {
|
|
continue;
|
|
}
|
|
|
|
// Recurse if we're merging plain objects or arrays
|
|
if ( deep && copy && ( OpenSeadragon.isPlainObject( copy ) || ( copyIsArray = OpenSeadragon.isArray( copy ) ) ) ) {
|
|
if ( copyIsArray ) {
|
|
copyIsArray = false;
|
|
clone = src && OpenSeadragon.isArray( src ) ? src : [];
|
|
|
|
} else {
|
|
clone = src && OpenSeadragon.isPlainObject( src ) ? src : {};
|
|
}
|
|
|
|
// Never move original objects, clone them
|
|
target[ name ] = OpenSeadragon.extend( deep, clone, copy );
|
|
|
|
// Don't bring in undefined values
|
|
} else if ( copy !== undefined ) {
|
|
target[ name ] = copy;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return the modified object
|
|
return target;
|
|
};
|
|
|
|
|
|
$.extend( $, {
|
|
/**
|
|
* These are the default values for the optional settings documented
|
|
* in the {@link OpenSeadragon} constructor detail.
|
|
* @name $.DEFAULT_SETTINGS
|
|
* @static
|
|
*/
|
|
DEFAULT_SETTINGS: {
|
|
//DATA SOURCE DETAILS
|
|
xmlPath: null,
|
|
tileSources: null,
|
|
tileHost: null,
|
|
|
|
//INTERFACE FEATURES
|
|
animationTime: 1.5,
|
|
blendTime: 0.1,
|
|
alwaysBlend: false,
|
|
autoHideControls: true,
|
|
immediateRender: false,
|
|
wrapHorizontal: false,
|
|
wrapVertical: false,
|
|
panHorizontal: true,
|
|
panVertical: true,
|
|
|
|
visibilityRatio: 0.5,
|
|
springStiffness: 5.0,
|
|
clickTimeThreshold: 300,
|
|
clickDistThreshold: 5,
|
|
zoomPerClick: 2.0,
|
|
zoomPerScroll: 1.2,
|
|
zoomPerSecond: 2.0,
|
|
|
|
showNavigationControl: true,
|
|
showSequenceControl: true,
|
|
controlsFadeDelay: 2000,
|
|
controlsFadeLength: 1500,
|
|
mouseNavEnabled: true,
|
|
preserveViewport: false,
|
|
defaultZoomLevel: 0,
|
|
|
|
showNavigator: true, //promoted to default in 0.9.64
|
|
navigatorElement: null,
|
|
navigatorHeight: null,
|
|
navigatorWidth: null,
|
|
navigatorPosition: null,
|
|
navigatorSizeRatio: 0.2,
|
|
|
|
showReferenceStrip: false,
|
|
referenceStripScroll: 'horizontal',
|
|
referenceStripElement: null,
|
|
referenceStripHeight: null,
|
|
referenceStripWidth: null,
|
|
referenceStripPosition: 'BOTTOM_LEFT',
|
|
referenceStripSizeRatio: 0.2,
|
|
|
|
//COLLECTION VISUALIZATION SETTINGS
|
|
collectionRows: 3, //or columns depending on layout
|
|
collectionLayout: 'horizontal', //vertical
|
|
collectionMode: false,
|
|
collectionTileSize: 800,
|
|
|
|
//EVENT RELATED CALLBACKS
|
|
onPageChange: null,
|
|
|
|
//PERFORMANCE SETTINGS
|
|
minPixelRatio: 0.5,
|
|
imageLoaderLimit: 0,
|
|
maxImageCacheCount: 200,
|
|
minZoomImageRatio: 0.8,
|
|
maxZoomPixelRatio: 2,
|
|
|
|
//INTERFACE RESOURCE SETTINGS
|
|
prefixUrl: null,
|
|
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'
|
|
},
|
|
previous: {
|
|
REST: '/images/previous_rest.png',
|
|
GROUP: '/images/previous_grouphover.png',
|
|
HOVER: '/images/previous_hover.png',
|
|
DOWN: '/images/previous_pressed.png'
|
|
},
|
|
next: {
|
|
REST: '/images/next_rest.png',
|
|
GROUP: '/images/next_grouphover.png',
|
|
HOVER: '/images/next_hover.png',
|
|
DOWN: '/images/next_pressed.png'
|
|
}
|
|
},
|
|
|
|
//DEVELOPER SETTINGS
|
|
debugMode: false,
|
|
debugGridColor: '#437AB2'
|
|
},
|
|
|
|
|
|
/**
|
|
* TODO: get rid of this. I can't see how it's required at all. Looks
|
|
* like an early legacy code artifact.
|
|
* @static
|
|
* @ignore
|
|
*/
|
|
SIGNAL: "----seadragon----",
|
|
|
|
|
|
/**
|
|
* Invokes the the method as if it where a method belonging to the object.
|
|
* @name $.delegate
|
|
* @function
|
|
* @param {Object} object
|
|
* @param {Function} method
|
|
*/
|
|
delegate: function( object, method ) {
|
|
return function() {
|
|
if ( arguments === undefined )
|
|
arguments = [];
|
|
return method.apply( object, arguments );
|
|
};
|
|
},
|
|
|
|
|
|
/**
|
|
* An enumeration of Browser vendors including UNKNOWN, IE, FIREFOX,
|
|
* SAFARI, CHROME, and OPERA.
|
|
* @name $.BROWSERS
|
|
* @static
|
|
*/
|
|
BROWSERS: {
|
|
UNKNOWN: 0,
|
|
IE: 1,
|
|
FIREFOX: 2,
|
|
SAFARI: 3,
|
|
CHROME: 4,
|
|
OPERA: 5
|
|
},
|
|
|
|
|
|
/**
|
|
* Returns a DOM Element for the given id or element.
|
|
* @function
|
|
* @name OpenSeadragon.getElement
|
|
* @param {String|Element} element Accepts an id or element.
|
|
* @returns {Element} The element with the given id, null, or the element itself.
|
|
*/
|
|
getElement: function( element ) {
|
|
if ( typeof ( element ) == "string" ) {
|
|
element = document.getElementById( element );
|
|
}
|
|
return element;
|
|
},
|
|
|
|
|
|
/**
|
|
* Determines the position of the upper-left corner of the element.
|
|
* @function
|
|
* @name OpenSeadragon.getElementPosition
|
|
* @param {Element|String} element - the elemenet we want the position for.
|
|
* @returns {Point} - the position of the upper left corner of the element.
|
|
*/
|
|
getElementPosition: function( element ) {
|
|
var result = new $.Point(),
|
|
isFixed,
|
|
offsetParent;
|
|
|
|
element = $.getElement( element );
|
|
isFixed = $.getElementStyle( element ).position == "fixed";
|
|
offsetParent = getOffsetParent( element, isFixed );
|
|
|
|
while ( offsetParent ) {
|
|
|
|
result.x += element.offsetLeft;
|
|
result.y += element.offsetTop;
|
|
|
|
if ( isFixed ) {
|
|
result = result.plus( $.getPageScroll() );
|
|
}
|
|
|
|
element = offsetParent;
|
|
isFixed = $.getElementStyle( element ).position == "fixed";
|
|
offsetParent = getOffsetParent( element, isFixed );
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
|
|
/**
|
|
* Determines the height and width of the given element.
|
|
* @function
|
|
* @name OpenSeadragon.getElementSize
|
|
* @param {Element|String} element
|
|
* @returns {Point}
|
|
*/
|
|
getElementSize: function( element ) {
|
|
element = $.getElement( element );
|
|
|
|
return new $.Point(
|
|
element.clientWidth,
|
|
element.clientHeight
|
|
);
|
|
},
|
|
|
|
|
|
/**
|
|
* Returns the CSSStyle object for the given element.
|
|
* @function
|
|
* @name OpenSeadragon.getElementStyle
|
|
* @param {Element|String} element
|
|
* @returns {CSSStyle}
|
|
*/
|
|
getElementStyle:
|
|
document.documentElement.currentStyle ?
|
|
function( element ) {
|
|
element = $.getElement( element );
|
|
return element.currentStyle;
|
|
} :
|
|
function( element ) {
|
|
element = $.getElement( element );
|
|
return window.getComputedStyle( element, "" );
|
|
},
|
|
|
|
|
|
/**
|
|
* Gets the latest event, really only useful internally since its
|
|
* specific to IE behavior. TODO: Deprecate this from the api and
|
|
* use it internally.
|
|
* @function
|
|
* @name OpenSeadragon.getEvent
|
|
* @param {Event} [event]
|
|
* @returns {Event}
|
|
*/
|
|
getEvent: function( event ) {
|
|
if( event ){
|
|
$.getEvent = function( event ){
|
|
return event;
|
|
};
|
|
} else {
|
|
$.getEvent = function( event ){
|
|
return window.event;
|
|
};
|
|
}
|
|
return $.getEvent( event );
|
|
},
|
|
|
|
|
|
/**
|
|
* Gets the position of the mouse on the screen for a given event.
|
|
* @function
|
|
* @name OpenSeadragon.getMousePosition
|
|
* @param {Event} [event]
|
|
* @returns {Point}
|
|
*/
|
|
getMousePosition: function( event ) {
|
|
|
|
if ( typeof( event.pageX ) == "number" ) {
|
|
$.getMousePosition = function( event ){
|
|
var result = new $.Point();
|
|
|
|
event = $.getEvent( event );
|
|
result.x = event.pageX;
|
|
result.y = event.pageY;
|
|
|
|
return result;
|
|
};
|
|
} else if ( typeof( event.clientX ) == "number" ) {
|
|
$.getMousePosition = function( event ){
|
|
var result = new $.Point();
|
|
|
|
event = $.getEvent( event );
|
|
result.x =
|
|
event.clientX +
|
|
document.body.scrollLeft +
|
|
document.documentElement.scrollLeft;
|
|
result.y =
|
|
event.clientY +
|
|
document.body.scrollTop +
|
|
document.documentElement.scrollTop;
|
|
|
|
return result;
|
|
};
|
|
} else {
|
|
throw new Error(
|
|
"Unknown event mouse position, no known technique."
|
|
);
|
|
}
|
|
|
|
return $.getMousePosition( event );
|
|
},
|
|
|
|
|
|
/**
|
|
* Determines the pages current scroll position.
|
|
* @function
|
|
* @name OpenSeadragon.getPageScroll
|
|
* @returns {Point}
|
|
*/
|
|
getPageScroll: function() {
|
|
var docElement = document.documentElement || {},
|
|
body = document.body || {};
|
|
|
|
if ( typeof( window.pageXOffset ) == "number" ) {
|
|
$.getPageScroll = function(){
|
|
return new $.Point(
|
|
window.pageXOffset,
|
|
window.pageYOffset
|
|
);
|
|
};
|
|
} else if ( body.scrollLeft || body.scrollTop ) {
|
|
$.getPageScroll = function(){
|
|
return new $.Point(
|
|
document.body.scrollLeft,
|
|
document.body.scrollTop
|
|
);
|
|
};
|
|
} else if ( docElement.scrollLeft || docElement.scrollTop ) {
|
|
$.getPageScroll = function(){
|
|
return new $.Point(
|
|
document.documentElement.scrollLeft,
|
|
document.documentElement.scrollTop
|
|
);
|
|
};
|
|
} else {
|
|
$.getPageScroll = function(){
|
|
return new $.Point(0,0);
|
|
};
|
|
}
|
|
|
|
return $.getPageScroll();
|
|
},
|
|
|
|
|
|
/**
|
|
* Determines the size of the browsers window.
|
|
* @function
|
|
* @name OpenSeadragon.getWindowSize
|
|
* @returns {Point}
|
|
*/
|
|
getWindowSize: function() {
|
|
var docElement = document.documentElement || {},
|
|
body = document.body || {};
|
|
|
|
if ( typeof( window.innerWidth ) == 'number' ) {
|
|
$.getWindowSize = function(){
|
|
return new $.Point(
|
|
window.innerWidth,
|
|
window.innerHeight
|
|
);
|
|
}
|
|
} else if ( docElement.clientWidth || docElement.clientHeight ) {
|
|
$.getWindowSize = function(){
|
|
return new $.Point(
|
|
document.documentElement.clientWidth,
|
|
document.documentElement.clientHeight
|
|
);
|
|
}
|
|
} else if ( body.clientWidth || body.clientHeight ) {
|
|
$.getWindowSize = function(){
|
|
return new $.Point(
|
|
document.body.clientWidth,
|
|
document.body.clientHeight
|
|
);
|
|
}
|
|
} else {
|
|
throw new Error("Unknown window size, no known technique.");
|
|
}
|
|
|
|
return $.getWindowSize();
|
|
},
|
|
|
|
|
|
/**
|
|
* Wraps the given element in a nest of divs so that the element can
|
|
* be easily centered.
|
|
* @function
|
|
* @name OpenSeadragon.makeCenteredNode
|
|
* @param {Element|String} element
|
|
* @returns {Element}
|
|
*/
|
|
makeCenteredNode: function( element ) {
|
|
|
|
var div = $.makeNeutralElement( "div" ),
|
|
html = [],
|
|
innerDiv,
|
|
innerDivs;
|
|
|
|
element = $.getElement( element );
|
|
|
|
//TODO: I dont understand the use of # inside the style attributes
|
|
// below. Invetigate the results of the constructed html in
|
|
// the browser and clean up the mark-up to make this clearer.
|
|
html.push('<div style="display:table; height:100%; width:100%;');
|
|
html.push('border:none; margin:0px; padding:0px;'); // neutralizing
|
|
html.push('#position:relative; overflow:hidden; text-align:left;">');
|
|
html.push('<div style="#position:absolute; #top:50%; width:100%; ');
|
|
html.push('border:none; margin:0px; padding:0px;'); // neutralizing
|
|
html.push('display:table-cell; vertical-align:middle;">');
|
|
html.push('<div style="#position:relative; #top:-50%; width:100%; ');
|
|
html.push('border:none; margin:0px; padding:0px;'); // neutralizing
|
|
html.push('text-align:center;"></div></div></div>');
|
|
|
|
div.innerHTML = html.join( '' );
|
|
div = div.firstChild;
|
|
|
|
innerDiv = div;
|
|
innerDivs = div.getElementsByTagName( "div" );
|
|
while ( innerDivs.length > 0 ) {
|
|
innerDiv = innerDivs[ 0 ];
|
|
innerDivs = innerDiv.getElementsByTagName( "div" );
|
|
}
|
|
|
|
innerDiv.appendChild( element );
|
|
|
|
return div;
|
|
},
|
|
|
|
|
|
/**
|
|
* Creates an easily positionable element of the given type that therefor
|
|
* serves as an excellent container element.
|
|
* @function
|
|
* @name OpenSeadragon.makeNeutralElement
|
|
* @param {String} tagName
|
|
* @returns {Element}
|
|
*/
|
|
makeNeutralElement: function( tagName ) {
|
|
var element = document.createElement( tagName ),
|
|
style = element.style;
|
|
|
|
style.background = "transparent none";
|
|
style.border = "none";
|
|
style.margin = "0px";
|
|
style.padding = "0px";
|
|
style.position = "static";
|
|
|
|
return element;
|
|
},
|
|
|
|
|
|
/**
|
|
* Ensures an image is loaded correctly to support alpha transparency.
|
|
* Generally only IE has issues doing this correctly for formats like
|
|
* png.
|
|
* @function
|
|
* @name OpenSeadragon.makeTransparentImage
|
|
* @param {String} src
|
|
* @returns {Element}
|
|
*/
|
|
makeTransparentImage: function( src ) {
|
|
|
|
$.makeTransparentImage = function( src ){
|
|
var img = $.makeNeutralElement( "img" );
|
|
|
|
img.src = src;
|
|
|
|
return img;
|
|
};
|
|
|
|
if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 7 ) {
|
|
|
|
$.makeTransparentImage = function( src ){
|
|
var img = $.makeNeutralElement( "img" ),
|
|
element = null;
|
|
|
|
element = $.makeNeutralElement("span");
|
|
element.style.display = "inline-block";
|
|
|
|
img.onload = function() {
|
|
element.style.width = element.style.width || img.width + "px";
|
|
element.style.height = element.style.height || img.height + "px";
|
|
|
|
img.onload = null;
|
|
img = null; // to prevent memory leaks in IE
|
|
};
|
|
|
|
img.src = src;
|
|
element.style.filter =
|
|
"progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" +
|
|
src +
|
|
"', sizingMethod='scale')";
|
|
|
|
return element;
|
|
};
|
|
|
|
}
|
|
|
|
return $.makeTransparentImage( src );
|
|
},
|
|
|
|
|
|
/**
|
|
* Sets the opacity of the specified element.
|
|
* @function
|
|
* @name OpenSeadragon.setElementOpacity
|
|
* @param {Element|String} element
|
|
* @param {Number} opacity
|
|
* @param {Boolean} [usesAlpha]
|
|
*/
|
|
setElementOpacity: function( element, opacity, usesAlpha ) {
|
|
|
|
var previousFilter,
|
|
ieOpacity,
|
|
ieFilter;
|
|
|
|
element = $.getElement( element );
|
|
|
|
if ( usesAlpha && !$.Browser.alpha ) {
|
|
opacity = Math.round( opacity );
|
|
}
|
|
|
|
if ( opacity < 1 ) {
|
|
element.style.opacity = opacity;
|
|
} else {
|
|
element.style.opacity = "";
|
|
}
|
|
|
|
if ( opacity == 1 ) {
|
|
prevFilter = element.style.filter || "";
|
|
element.style.filter = prevFilter.replace(/alpha\(.*?\)/g, "");
|
|
return;
|
|
}
|
|
|
|
ieOpacity = Math.round( 100 * opacity );
|
|
ieFilter = " alpha(opacity=" + ieOpacity + ") ";
|
|
|
|
//TODO: find out why this uses a try/catch instead of a predetermined
|
|
// routine or at least an if/elseif/else
|
|
try {
|
|
if ( element.filters && element.filters.alpha ) {
|
|
element.filters.alpha.opacity = ieOpacity;
|
|
} else {
|
|
element.style.filter += ieFilter;
|
|
}
|
|
} catch ( e ) {
|
|
element.style.filter += ieFilter;
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* Adds an event listener for the given element, eventName and handler.
|
|
* @function
|
|
* @name OpenSeadragon.addEvent
|
|
* @param {Element|String} element
|
|
* @param {String} eventName
|
|
* @param {Function} handler
|
|
* @param {Boolean} [useCapture]
|
|
* @throws {Error}
|
|
*/
|
|
addEvent: function( element, eventName, handler, useCapture ) {
|
|
element = $.getElement( element );
|
|
|
|
//TODO: Why do this if/else on every method call instead of just
|
|
// defining this function once based on the same logic
|
|
if ( element.addEventListener ) {
|
|
$.addEvent = function( element, eventName, handler, useCapture ){
|
|
element = $.getElement( element );
|
|
element.addEventListener( eventName, handler, useCapture );
|
|
};
|
|
} else if ( element.attachEvent ) {
|
|
$.addEvent = function( element, eventName, handler, useCapture ){
|
|
element = $.getElement( element );
|
|
element.attachEvent( "on" + eventName, handler );
|
|
if ( useCapture && element.setCapture ) {
|
|
element.setCapture();
|
|
}
|
|
};
|
|
} else {
|
|
throw new Error(
|
|
"Unable to attach event handler, no known technique."
|
|
);
|
|
}
|
|
|
|
return $.addEvent( element, eventName, handler, useCapture );
|
|
},
|
|
|
|
|
|
/**
|
|
* Remove a given event listener for the given element, event type and
|
|
* handler.
|
|
* @function
|
|
* @name OpenSeadragon.removeEvent
|
|
* @param {Element|String} element
|
|
* @param {String} eventName
|
|
* @param {Function} handler
|
|
* @param {Boolean} [useCapture]
|
|
* @throws {Error}
|
|
*/
|
|
removeEvent: function( element, eventName, handler, useCapture ) {
|
|
element = $.getElement( element );
|
|
|
|
//TODO: Why do this if/else on every method call instead of just
|
|
// defining this function once based on the same logic
|
|
if ( element.removeEventListener ) {
|
|
$.removeEvent = function( element, eventName, handler, useCapture ) {
|
|
element = $.getElement( element );
|
|
element.removeEventListener( eventName, handler, useCapture );
|
|
};
|
|
} else if ( element.detachEvent ) {
|
|
$.removeEvent = function( element, eventName, handler, useCapture ) {
|
|
element = $.getElement( element );
|
|
element.detachEvent("on" + eventName, handler);
|
|
if ( useCapture && element.releaseCapture ) {
|
|
element.releaseCapture();
|
|
}
|
|
};
|
|
} else {
|
|
throw new Error(
|
|
"Unable to detach event handler, no known technique."
|
|
);
|
|
}
|
|
return $.removeEvent( element, eventName, handler, useCapture );
|
|
},
|
|
|
|
|
|
/**
|
|
* Cancels the default browser behavior had the event propagated all
|
|
* the way up the DOM to the window object.
|
|
* @function
|
|
* @name OpenSeadragon.cancelEvent
|
|
* @param {Event} [event]
|
|
*/
|
|
cancelEvent: function( event ) {
|
|
event = $.getEvent( event );
|
|
|
|
if ( event.preventDefault ) {
|
|
$.cancelEvent = function( event ){
|
|
// W3C for preventing default
|
|
event.preventDefault();
|
|
}
|
|
} else {
|
|
$.cancelEvent = function( event ){
|
|
event = $.getEvent( event );
|
|
// legacy for preventing default
|
|
event.cancel = true;
|
|
// IE for preventing default
|
|
event.returnValue = false;
|
|
};
|
|
}
|
|
$.cancelEvent( event );
|
|
},
|
|
|
|
|
|
/**
|
|
* Stops the propagation of the event up the DOM.
|
|
* @function
|
|
* @name OpenSeadragon.stopEvent
|
|
* @param {Event} [event]
|
|
*/
|
|
stopEvent: function( event ) {
|
|
event = $.getEvent( event );
|
|
|
|
if ( event.stopPropagation ) {
|
|
// W3C for stopping propagation
|
|
$.stopEvent = function( event ){
|
|
event.stopPropagation();
|
|
};
|
|
} else {
|
|
// IE for stopping propagation
|
|
$.stopEvent = function( event ){
|
|
event = $.getEvent( event );
|
|
event.cancelBubble = true;
|
|
};
|
|
|
|
}
|
|
|
|
$.stopEvent( event );
|
|
},
|
|
|
|
|
|
/**
|
|
* Similar to OpenSeadragon.delegate, but it does not immediately call
|
|
* the method on the object, returning a function which can be called
|
|
* repeatedly to delegate the method. It also allows additonal arguments
|
|
* to be passed during construction which will be added during each
|
|
* invocation, and each invocation can add additional arguments as well.
|
|
*
|
|
* @function
|
|
* @name OpenSeadragon.createCallback
|
|
* @param {Object} object
|
|
* @param {Function} method
|
|
* @param [args] any additional arguments are passed as arguments to the
|
|
* created callback
|
|
* @returns {Function}
|
|
*/
|
|
createCallback: function( object, method ) {
|
|
//TODO: This pattern is painful to use and debug. It's much cleaner
|
|
// to use pinning plus anonymous functions. Get rid of this
|
|
// pattern!
|
|
var initialArgs = [],
|
|
i;
|
|
for ( i = 2; i < arguments.length; i++ ) {
|
|
initialArgs.push( arguments[ i ] );
|
|
}
|
|
|
|
return function() {
|
|
var args = initialArgs.concat( [] ),
|
|
i;
|
|
for ( i = 0; i < arguments.length; i++ ) {
|
|
args.push( arguments[ i ] );
|
|
}
|
|
|
|
return method.apply( object, args );
|
|
};
|
|
},
|
|
|
|
|
|
/**
|
|
* Retreives the value of a url parameter from the window.location string.
|
|
* @function
|
|
* @name OpenSeadragon.getUrlParameter
|
|
* @param {String} key
|
|
* @returns {String} The value of the url parameter or null if no param matches.
|
|
*/
|
|
getUrlParameter: function( key ) {
|
|
var value = URLPARAMS[ key ];
|
|
return value ? value : null;
|
|
},
|
|
|
|
|
|
createAjaxRequest: function(){
|
|
var request;
|
|
|
|
if ( window.ActiveXObject ) {
|
|
//TODO: very bad...Why check every time using try/catch when
|
|
// we could determine once at startup which activeX object
|
|
// was supported. This will have significant impact on
|
|
// performance for IE Browsers DONE
|
|
for ( i = 0; i < ACTIVEX.length; i++ ) {
|
|
try {
|
|
request = new ActiveXObject( ACTIVEX[ i ] );
|
|
$.createAjaxRequest = function( ){
|
|
return new ActiveXObject( ACTIVEX[ i ] );
|
|
};
|
|
break;
|
|
} catch (e) {
|
|
continue;
|
|
}
|
|
}
|
|
} else if ( window.XMLHttpRequest ) {
|
|
$.createAjaxRequest = function( ){
|
|
return new XMLHttpRequest();
|
|
};
|
|
request = new XMLHttpRequest();
|
|
}
|
|
|
|
if ( !request ) {
|
|
throw new Error( "Browser doesn't support XMLHttpRequest." );
|
|
}
|
|
|
|
return request;
|
|
},
|
|
/**
|
|
* Makes an AJAX request.
|
|
* @function
|
|
* @name OpenSeadragon.makeAjaxRequest
|
|
* @param {String} url - the url to request
|
|
* @param {Function} [callback] - a function to call when complete
|
|
* @throws {Error}
|
|
*/
|
|
makeAjaxRequest: function( url, callback ) {
|
|
|
|
var async = true,
|
|
request = $.createAjaxRequest(),
|
|
actual,
|
|
options,
|
|
i;
|
|
|
|
|
|
if( $.isPlainObject( url ) ){
|
|
options.async = options.async || async;
|
|
}else{
|
|
options = {
|
|
url: url,
|
|
async: $.isFunction( callback ),
|
|
success: callback,
|
|
error: null
|
|
};
|
|
}
|
|
|
|
if ( options.async ) {
|
|
/** @ignore */
|
|
request.onreadystatechange = function() {
|
|
if ( request.readyState == 4) {
|
|
request.onreadystatechange = new function() { };
|
|
options.success( request );
|
|
}
|
|
};
|
|
}
|
|
|
|
try {
|
|
request.open( "GET", options.url, options.async );
|
|
request.send( null );
|
|
} catch (e) {
|
|
$.console.log(
|
|
"%s while making AJAX request: %s",
|
|
e.name,
|
|
e.message
|
|
);
|
|
|
|
request.onreadystatechange = null;
|
|
request = null;
|
|
|
|
if ( options.error && $.isFunction( options.error ) ) {
|
|
options.error( request );
|
|
}
|
|
}
|
|
|
|
if( !options.async && $.isFunction( options.success ) ){
|
|
options.success( request );
|
|
}
|
|
|
|
return options.async ? null : request;
|
|
},
|
|
|
|
|
|
/**
|
|
* Taken from jQuery 1.6.1
|
|
* @function
|
|
* @name OpenSeadragon.jsonp
|
|
* @param {Object} options
|
|
* @param {String} options.url
|
|
* @param {Function} options.callback
|
|
* @param {String} [options.param='callback'] The name of the url parameter
|
|
* to request the jsonp provider with.
|
|
* @param {String} [options.callbackName=] The name of the callback to
|
|
* request the jsonp provider with.
|
|
*/
|
|
jsonp: function( options ){
|
|
var script,
|
|
url = options.url,
|
|
head = document.head ||
|
|
document.getElementsByTagName( "head" )[ 0 ] ||
|
|
document.documentElement,
|
|
jsonpCallback = options.callbackName || 'openseadragon' + (+new Date()),
|
|
previous = window[ jsonpCallback ],
|
|
replace = "$1" + jsonpCallback + "$2",
|
|
callbackParam = options.param || 'callback',
|
|
callback = options.callback;
|
|
|
|
url = url.replace( /(\=)\?(&|$)|\?\?/i, replace );
|
|
// Add callback manually
|
|
url += (/\?/.test( url ) ? "&" : "?") + callbackParam + "=" + jsonpCallback;
|
|
|
|
// Install callback
|
|
window[ jsonpCallback ] = function( response ) {
|
|
if ( !previous ){
|
|
try{
|
|
delete window[ jsonpCallback ];
|
|
}catch(e){
|
|
//swallow
|
|
}
|
|
} else {
|
|
window[ jsonpCallback ] = previous;
|
|
}
|
|
if( callback && $.isFunction( callback ) ){
|
|
callback( response );
|
|
}
|
|
};
|
|
|
|
script = document.createElement( "script" );
|
|
|
|
//TODO: having an issue with async info requests
|
|
if( undefined !== options.async || false !== options.async ){
|
|
script.async = "async";
|
|
}
|
|
|
|
if ( options.scriptCharset ) {
|
|
script.charset = options.scriptCharset;
|
|
}
|
|
|
|
script.src = url;
|
|
|
|
// Attach handlers for all browsers
|
|
script.onload = script.onreadystatechange = function( _, isAbort ) {
|
|
|
|
if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
|
|
|
|
// Handle memory leak in IE
|
|
script.onload = script.onreadystatechange = null;
|
|
|
|
// Remove the script
|
|
if ( head && script.parentNode ) {
|
|
head.removeChild( script );
|
|
}
|
|
|
|
// Dereference the script
|
|
script = undefined;
|
|
}
|
|
};
|
|
// Use insertBefore instead of appendChild to circumvent an IE6 bug.
|
|
// This arises when a base node is used (#2709 and #4378).
|
|
head.insertBefore( script, head.firstChild );
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* Loads a Deep Zoom Image description from a url, XML string or JSON string
|
|
* and provides a callback hook for the resulting Document
|
|
* @function
|
|
* @name OpenSeadragon.createFromDZI
|
|
* @param {String} xmlUrl
|
|
* @param {String} xmlString
|
|
* @param {Function} callback
|
|
* @deprecated
|
|
*/
|
|
createFromDZI: function( dzi, callback, tileHost ) {
|
|
var async = typeof ( callback ) == "function",
|
|
dziUrl = (
|
|
dzi.substring(0,1) != '<' &&
|
|
dzi.substring(0,1) != '{'
|
|
) ? dzi : null,
|
|
dziString = dziUrl ? null : dzi,
|
|
error = null,
|
|
urlParts,
|
|
filename,
|
|
lastDot,
|
|
tilesUrl,
|
|
callbackName;
|
|
|
|
|
|
if( tileHost ){
|
|
|
|
tilesUrl = tileHost + "/_files/";
|
|
|
|
} else if( dziUrl ) {
|
|
|
|
urlParts = dziUrl.split( '/' );
|
|
filename = urlParts[ urlParts.length - 1 ];
|
|
if( filename.match(/_dzi\.js$/) ){
|
|
//for jsonp dzi specification, the '_dzi' needs to be removed
|
|
//from the filename to be consistent with the spec
|
|
filename = filename.replace('_dzi.js', '.js');
|
|
}
|
|
|
|
lastDot = filename.lastIndexOf( '.' );
|
|
|
|
if ( lastDot > -1 ) {
|
|
urlParts[ urlParts.length - 1 ] = filename.slice( 0, lastDot );
|
|
}
|
|
|
|
|
|
tilesUrl = urlParts.join( '/' ) + "_files/";
|
|
|
|
}
|
|
|
|
function finish( func, obj ) {
|
|
try {
|
|
return func( obj, tilesUrl );
|
|
} catch ( e ) {
|
|
if ( async ) {
|
|
return null;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( async ) {
|
|
if ( dziString ) {
|
|
window.setTimeout( function() {
|
|
var source = finish( processDZIXml, $.parseXml( xmlString ) );
|
|
// call after finish sets error
|
|
callback( source, error );
|
|
}, 1);
|
|
} else {
|
|
if( dziUrl.match(/_dzi\.js$/) ){
|
|
callbackName = dziUrl.split( '/' ).pop().replace('.js','');
|
|
$.jsonp({
|
|
url: dziUrl,
|
|
callbackName: callbackName,
|
|
callback: function( imageData ){
|
|
var source = finish( processDZIJSON, imageData.Image );
|
|
callback( source );
|
|
}
|
|
});
|
|
} else {
|
|
$.makeAjaxRequest( dziUrl, function( xhr ) {
|
|
var source = finish( processDZIResponse, xhr );
|
|
// call after finish sets error
|
|
callback( source, error );
|
|
});
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
if ( dziString ) {
|
|
return finish(
|
|
processDZIXml,
|
|
$.parseXml( dziString )
|
|
);
|
|
} else {
|
|
return finish(
|
|
processDZIResponse,
|
|
$.makeAjaxRequest( dziUrl )
|
|
);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Parses an XML string into a DOM Document.
|
|
* @function
|
|
* @name OpenSeadragon.parseXml
|
|
* @param {String} string
|
|
* @returns {Document}
|
|
*/
|
|
parseXml: function( string ) {
|
|
//TODO: yet another example where we can determine the correct
|
|
// implementation once at start-up instead of everytime we use
|
|
// the function. DONE.
|
|
if ( window.ActiveXObject ) {
|
|
|
|
$.parseXml = function( string ){
|
|
var xmlDoc = null,
|
|
parser;
|
|
|
|
xmlDoc = new ActiveXObject( "Microsoft.XMLDOM" );
|
|
xmlDoc.async = false;
|
|
xmlDoc.loadXML( string );
|
|
return xmlDoc;
|
|
};
|
|
|
|
} else if ( window.DOMParser ) {
|
|
|
|
$.parseXml = function( string ){
|
|
var xmlDoc = null,
|
|
parser;
|
|
|
|
parser = new DOMParser();
|
|
xmlDoc = parser.parseFromString( string, "text/xml" );
|
|
return xmlDoc;
|
|
};
|
|
|
|
} else {
|
|
throw new Error( "Browser doesn't support XML DOM." );
|
|
}
|
|
|
|
return $.parseXml( string );
|
|
},
|
|
|
|
|
|
/**
|
|
* Reports whether the image format is supported for tiling in this
|
|
* version.
|
|
* @function
|
|
* @name OpenSeadragon.imageFormatSupported
|
|
* @param {String} [extension]
|
|
* @returns {Boolean}
|
|
*/
|
|
imageFormatSupported: function( extension ) {
|
|
extension = extension ? extension : "";
|
|
return !!FILEFORMATS[ extension.toLowerCase() ];
|
|
}
|
|
|
|
});
|
|
|
|
|
|
/**
|
|
* The current browser vendor, version, and related information regarding
|
|
* detected features. Features include <br/>
|
|
* <strong>'alpha'</strong> - Does the browser support image alpha
|
|
* transparency.<br/>
|
|
* @name $.Browser
|
|
* @static
|
|
*/
|
|
$.Browser = {
|
|
vendor: $.BROWSERS.UNKNOWN,
|
|
version: 0,
|
|
alpha: true
|
|
};
|
|
|
|
|
|
var ACTIVEX = [
|
|
"Msxml2.XMLHTTP",
|
|
"Msxml3.XMLHTTP",
|
|
"Microsoft.XMLHTTP"
|
|
],
|
|
FILEFORMATS = {
|
|
"bmp": false,
|
|
"jpeg": true,
|
|
"jpg": true,
|
|
"png": true,
|
|
"tif": false,
|
|
"wdp": false
|
|
},
|
|
URLPARAMS = {};
|
|
|
|
(function() {
|
|
//A small auto-executing routine to determine the browser vendor,
|
|
//version and supporting feature sets.
|
|
var app = navigator.appName,
|
|
ver = navigator.appVersion,
|
|
ua = navigator.userAgent;
|
|
|
|
//console.error( 'appName: ' + navigator.appName );
|
|
//console.error( 'appVersion: ' + navigator.appVersion );
|
|
//console.error( 'userAgent: ' + navigator.userAgent );
|
|
|
|
switch( navigator.appName ){
|
|
case "Microsoft Internet Explorer":
|
|
if( !!window.attachEvent &&
|
|
!!window.ActiveXObject ) {
|
|
|
|
$.Browser.vendor = $.BROWSERS.IE;
|
|
$.Browser.version = parseFloat(
|
|
ua.substring(
|
|
ua.indexOf( "MSIE" ) + 5,
|
|
ua.indexOf( ";", ua.indexOf( "MSIE" ) ) )
|
|
);
|
|
}
|
|
break;
|
|
case "Netscape":
|
|
if( !!window.addEventListener ){
|
|
if ( ua.indexOf( "Firefox" ) >= 0 ) {
|
|
$.Browser.vendor = $.BROWSERS.FIREFOX;
|
|
$.Browser.version = parseFloat(
|
|
ua.substring( ua.indexOf( "Firefox" ) + 8 )
|
|
);
|
|
} else if ( ua.indexOf( "Safari" ) >= 0 ) {
|
|
$.Browser.vendor = ua.indexOf( "Chrome" ) >= 0 ?
|
|
$.BROWSERS.CHROME :
|
|
$.BROWSERS.SAFARI;
|
|
$.Browser.version = parseFloat(
|
|
ua.substring(
|
|
ua.substring( 0, ua.indexOf( "Safari" ) ).lastIndexOf( "/" ) + 1,
|
|
ua.indexOf( "Safari" )
|
|
)
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
case "Opera":
|
|
$.Browser.vendor = $.BROWSERS.OPERA;
|
|
$.Browser.version = parseFloat( ver );
|
|
break;
|
|
}
|
|
|
|
// ignore '?' portion of query string
|
|
var query = window.location.search.substring( 1 ),
|
|
parts = query.split('&'),
|
|
part,
|
|
sep,
|
|
i;
|
|
|
|
for ( i = 0; i < parts.length; i++ ) {
|
|
part = parts[ i ];
|
|
sep = part.indexOf( '=' );
|
|
|
|
if ( sep > 0 ) {
|
|
URLPARAMS[ part.substring( 0, sep ) ] =
|
|
decodeURIComponent( part.substring( sep + 1 ) );
|
|
}
|
|
}
|
|
|
|
//determine if this browser supports image alpha transparency
|
|
$.Browser.alpha = !(
|
|
(
|
|
$.Browser.vendor == $.BROWSERS.IE &&
|
|
$.Browser.version < 9
|
|
) || (
|
|
$.Browser.vendor == $.BROWSERS.CHROME &&
|
|
$.Browser.version < 2
|
|
)
|
|
);
|
|
|
|
})();
|
|
|
|
//TODO: $.console is often used inside a try/catch block which generally
|
|
// prevents allowings errors to occur with detection until a debugger
|
|
// is attached. Although I've been guilty of the same anti-pattern
|
|
// I eventually was convinced that errors should naturally propogate in
|
|
// all but the most special cases.
|
|
/**
|
|
* A convenient alias for console when available, and a simple null
|
|
* function when console is unavailable.
|
|
* @static
|
|
* @private
|
|
*/
|
|
var nullfunction = function( msg ){
|
|
//document.location.hash = msg;
|
|
};
|
|
|
|
$.console = window.console || {
|
|
log: nullfunction,
|
|
debug: nullfunction,
|
|
info: nullfunction,
|
|
warn: nullfunction,
|
|
error: nullfunction
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
* @param {Element} element
|
|
* @param {Boolean} [isFixed]
|
|
* @returns {Element}
|
|
*/
|
|
function getOffsetParent( element, isFixed ) {
|
|
if ( isFixed && element != document.body ) {
|
|
return document.body;
|
|
} else {
|
|
return element.offsetParent;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
* @param {XMLHttpRequest} xhr
|
|
* @param {String} tilesUrl
|
|
* @deprecated
|
|
*/
|
|
function processDZIResponse( xhr, tilesUrl ) {
|
|
var status,
|
|
statusText,
|
|
doc = null;
|
|
|
|
if ( !xhr ) {
|
|
throw new Error( $.getString( "Errors.Security" ) );
|
|
} else if ( xhr.status !== 200 && xhr.status !== 0 ) {
|
|
status = xhr.status;
|
|
statusText = ( status == 404 ) ?
|
|
"Not Found" :
|
|
xhr.statusText;
|
|
throw new Error( $.getString( "Errors.Status", status, statusText ) );
|
|
}
|
|
|
|
if ( xhr.responseXML && xhr.responseXML.documentElement ) {
|
|
doc = xhr.responseXML;
|
|
} else if ( xhr.responseText ) {
|
|
doc = $.parseXml( xhr.responseText );
|
|
}
|
|
|
|
return processDZIXml( doc, tilesUrl );
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
* @param {Document} xmlDoc
|
|
* @param {String} tilesUrl
|
|
* @deprecated
|
|
*/
|
|
function processDZIXml( xmlDoc, tilesUrl ) {
|
|
|
|
if ( !xmlDoc || !xmlDoc.documentElement ) {
|
|
throw new Error( $.getString( "Errors.Xml" ) );
|
|
}
|
|
|
|
var root = xmlDoc.documentElement,
|
|
rootName = root.tagName;
|
|
|
|
if ( rootName == "Image" ) {
|
|
try {
|
|
return processDZI( root, tilesUrl );
|
|
} catch ( e ) {
|
|
throw (e instanceof Error) ?
|
|
e :
|
|
new Error( $.getString("Errors.Dzi") );
|
|
}
|
|
} else if ( rootName == "Collection" ) {
|
|
throw new Error( $.getString( "Errors.Dzc" ) );
|
|
} else if ( rootName == "Error" ) {
|
|
return processDZIError( root );
|
|
}
|
|
|
|
throw new Error( $.getString( "Errors.Dzi" ) );
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
* @param {Element} imageNode
|
|
* @param {String} tilesUrl
|
|
* @deprecated
|
|
*/
|
|
function processDZI( imageNode, tilesUrl ) {
|
|
var fileFormat = imageNode.getAttribute( "Format" ),
|
|
sizeNode = imageNode.getElementsByTagName( "Size" )[ 0 ],
|
|
dispRectNodes = imageNode.getElementsByTagName( "DisplayRect" ),
|
|
width = parseInt( sizeNode.getAttribute( "Width" ) ),
|
|
height = parseInt( sizeNode.getAttribute( "Height" ) ),
|
|
tileSize = parseInt( imageNode.getAttribute( "TileSize" ) ),
|
|
tileOverlap = parseInt( imageNode.getAttribute( "Overlap" ) ),
|
|
dispRects = [],
|
|
dispRectNode,
|
|
rectNode,
|
|
i;
|
|
|
|
if ( !imageFormatSupported( fileFormat ) ) {
|
|
throw new Error(
|
|
$.getString( "Errors.ImageFormat", fileFormat.toUpperCase() )
|
|
);
|
|
}
|
|
|
|
for ( i = 0; i < dispRectNodes.length; i++ ) {
|
|
dispRectNode = dispRectNodes[ i ];
|
|
rectNode = dispRectNode.getElementsByTagName( "Rect" )[ 0 ];
|
|
|
|
dispRects.push( new $.DisplayRect(
|
|
parseInt( rectNode.getAttribute( "X" ) ),
|
|
parseInt( rectNode.getAttribute( "Y" ) ),
|
|
parseInt( rectNode.getAttribute( "Width" ) ),
|
|
parseInt( rectNode.getAttribute( "Height" ) ),
|
|
0, // ignore MinLevel attribute, bug in Deep Zoom Composer
|
|
parseInt( dispRectNode.getAttribute( "MaxLevel" ) )
|
|
));
|
|
}
|
|
return new $.DziTileSource(
|
|
width,
|
|
height,
|
|
tileSize,
|
|
tileOverlap,
|
|
tilesUrl,
|
|
fileFormat,
|
|
dispRects
|
|
);
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
* @param {Element} imageNode
|
|
* @param {String} tilesUrl
|
|
* @deprecated
|
|
*/
|
|
function processDZIJSON( imageData, tilesUrl ) {
|
|
var fileFormat = imageData.Format,
|
|
sizeData = imageData.Size,
|
|
dispRectData = imageData.DisplayRect || [],
|
|
width = parseInt( sizeData.Width ),
|
|
height = parseInt( sizeData.Height ),
|
|
tileSize = parseInt( imageData.TileSize ),
|
|
tileOverlap = parseInt( imageData.Overlap ),
|
|
dispRects = [],
|
|
rectData,
|
|
i;
|
|
|
|
if ( !imageFormatSupported( fileFormat ) ) {
|
|
throw new Error(
|
|
$.getString( "Errors.ImageFormat", fileFormat.toUpperCase() )
|
|
);
|
|
}
|
|
|
|
for ( i = 0; i < dispRectData.length; i++ ) {
|
|
rectData = dispRectData[ i ].Rect;
|
|
|
|
dispRects.push( new $.DisplayRect(
|
|
parseInt( rectData.X ),
|
|
parseInt( rectData.Y ),
|
|
parseInt( rectData.Width ),
|
|
parseInt( rectData.Height ),
|
|
0, // ignore MinLevel attribute, bug in Deep Zoom Composer
|
|
parseInt( rectData.MaxLevel )
|
|
));
|
|
}
|
|
return new $.DziTileSource(
|
|
width,
|
|
height,
|
|
tileSize,
|
|
tileOverlap,
|
|
tilesUrl,
|
|
fileFormat,
|
|
dispRects
|
|
);
|
|
};
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
* @param {Document} errorNode
|
|
* @throws {Error}
|
|
* @deprecated
|
|
*/
|
|
function processDZIError( errorNode ) {
|
|
var messageNode = errorNode.getElementsByTagName( "Message" )[ 0 ],
|
|
message = messageNode.firstChild.nodeValue;
|
|
|
|
throw new Error(message);
|
|
};
|
|
|
|
|
|
|
|
}( OpenSeadragon ));
|
|
|
|
(function($){
|
|
|
|
/**
|
|
* For use by classes which want to support custom, non-browser events.
|
|
* TODO: This is an aweful name! This thing represents an "event source",
|
|
* not an "event handler". PLEASE change the to EventSource. Also please
|
|
* change 'addHandler', 'removeHandler' and 'raiseEvent' to 'bind',
|
|
* 'unbind', and 'trigger' respectively. Finally add a method 'one' which
|
|
* automatically unbinds a listener after the first triggered event that
|
|
* matches.
|
|
* @class
|
|
*/
|
|
$.EventHandler = function() {
|
|
this.events = {};
|
|
};
|
|
|
|
$.EventHandler.prototype = {
|
|
|
|
/**
|
|
* Add an event handler for a given event.
|
|
* @function
|
|
* @param {String} eventName - Name of event to register.
|
|
* @param {Function} handler - Function to call when event is triggered.
|
|
*/
|
|
addHandler: function( eventName, handler ) {
|
|
var events = this.events[ eventName ];
|
|
if( !events ){
|
|
this.events[ eventName ] = events = [];
|
|
}
|
|
if( handler && $.isFunction( handler ) ){
|
|
events[ events.length ] = handler;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Remove a specific event handler for a given event.
|
|
* @function
|
|
* @param {String} eventName - Name of event for which the handler is to be removed.
|
|
* @param {Function} handler - Function to be removed.
|
|
*/
|
|
removeHandler: function( eventName, handler ) {
|
|
//Start Thatcher - unneccessary indirection. Also, because events were
|
|
// - not actually being removed, we need to add the code
|
|
// - to do the removal ourselves. TODO
|
|
var events = this.events[ eventName ];
|
|
if ( !events ){
|
|
return;
|
|
}
|
|
//End Thatcher
|
|
},
|
|
|
|
/**
|
|
* Retrive the list of all handlers registered for a given event.
|
|
* @function
|
|
* @param {String} eventName - Name of event to get handlers for.
|
|
*/
|
|
getHandler: function( eventName ) {
|
|
var events = this.events[ eventName ];
|
|
if ( !events || !events.length ){
|
|
return null;
|
|
}
|
|
events = events.length === 1 ?
|
|
[ events[ 0 ] ] :
|
|
Array.apply( null, events );
|
|
return function( source, args ) {
|
|
var i,
|
|
length = events.length;
|
|
for ( i = 0; i < length; i++ ) {
|
|
if( events[ i ] ){
|
|
events[ i ]( source, args );
|
|
}
|
|
}
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Trigger an event, optionally passing additional information.
|
|
* @function
|
|
* @param {String} eventName - Name of event to register.
|
|
* @param {Function} handler - Function to call when event is triggered.
|
|
*/
|
|
raiseEvent: function( eventName, eventArgs ) {
|
|
var handler = this.getHandler( eventName );
|
|
|
|
if ( handler ) {
|
|
if ( !eventArgs ) {
|
|
eventArgs = new Object();
|
|
}
|
|
|
|
handler( this, eventArgs );
|
|
}
|
|
}
|
|
};
|
|
|
|
}( OpenSeadragon ));
|
|
|
|
(function( $ ){
|
|
|
|
// is any button currently being pressed while mouse events occur
|
|
var IS_BUTTON_DOWN = false,
|
|
// is any tracker currently capturing?
|
|
IS_CAPTURING = false,
|
|
// dictionary from hash to MouseTracker
|
|
ACTIVE = {},
|
|
// list of trackers interested in capture
|
|
CAPTURING = [],
|
|
// dictionary from hash to private properties
|
|
THIS = {};
|
|
|
|
/**
|
|
* The MouseTracker allows other classes to set handlers for common mouse
|
|
* events on a specific element like, 'enter', 'exit', 'press', 'release',
|
|
* 'scroll', 'click', and 'drag'.
|
|
* @class
|
|
* @param {Object} options
|
|
* Allows configurable properties to be entirely specified by passing
|
|
* an options object to the constructor. The constructor also supports
|
|
* the original positional arguments 'elements', 'clickTimeThreshold',
|
|
* and 'clickDistThreshold' in that order.
|
|
* @param {Element|String} options.element
|
|
* A reference to an element or an element id for which the mouse
|
|
* events will be monitored.
|
|
* @param {Number} options.clickTimeThreshold
|
|
* The number of milliseconds within which mutliple mouse clicks
|
|
* will be treated as a single event.
|
|
* @param {Number} options.clickDistThreshold
|
|
* The distance between mouse click within multiple mouse clicks
|
|
* will be treated as a single event.
|
|
* @param {Function} options.enterHandler
|
|
* An optional handler for mouse enter.
|
|
* @param {Function} options.exitHandler
|
|
* An optional handler for mouse exit.
|
|
* @param {Function} options.pressHandler
|
|
* An optional handler for mouse press.
|
|
* @param {Function} options.releaseHandler
|
|
* An optional handler for mouse release.
|
|
* @param {Function} options.scrollHandler
|
|
* An optional handler for mouse scroll.
|
|
* @param {Function} options.clickHandler
|
|
* An optional handler for mouse click.
|
|
* @param {Function} options.dragHandler
|
|
* An optional handler for mouse drag.
|
|
* @property {Number} hash
|
|
* An unique hash for this tracker.
|
|
* @property {Element} element
|
|
* The element for which mouse event are being monitored.
|
|
* @property {Number} clickTimeThreshold
|
|
* The number of milliseconds within which mutliple mouse clicks
|
|
* will be treated as a single event.
|
|
* @property {Number} clickDistThreshold
|
|
* The distance between mouse click within multiple mouse clicks
|
|
* will be treated as a single event.
|
|
*/
|
|
$.MouseTracker = function ( options ) {
|
|
|
|
var args = arguments;
|
|
|
|
if( !$.isPlainObject( options ) ){
|
|
options = {
|
|
element: args[ 0 ],
|
|
clickTimeThreshold: args[ 1 ],
|
|
clickDistThreshold: args[ 2 ]
|
|
};
|
|
}
|
|
|
|
this.hash = Math.random();
|
|
this.element = $.getElement( options.element );
|
|
this.clickTimeThreshold = options.clickTimeThreshold;
|
|
this.clickDistThreshold = options.clickDistThreshold;
|
|
|
|
|
|
this.enterHandler = options.enterHandler || null;
|
|
this.exitHandler = options.exitHandler || null;
|
|
this.pressHandler = options.pressHandler || null;
|
|
this.releaseHandler = options.releaseHandler || null;
|
|
this.scrollHandler = options.scrollHandler || null;
|
|
this.clickHandler = options.clickHandler || null;
|
|
this.dragHandler = options.dragHandler || null;
|
|
this.keyHandler = options.keyHandler || null;
|
|
this.focusHandler = options.focusHandler || null;
|
|
this.blurHandler = options.blurHandler || null;
|
|
|
|
//Store private properties in a scope sealed hash map
|
|
var _this = this;
|
|
|
|
/**
|
|
* @private
|
|
* @property {Boolean} tracking
|
|
* Are we currently tracking mouse events.
|
|
* @property {Boolean} capturing
|
|
* Are we curruently capturing mouse events.
|
|
* @property {Boolean} buttonDown
|
|
* True if the left mouse button is currently being pressed and was
|
|
* initiated inside the tracked element, otherwise false.
|
|
* @property {Boolean} insideElement
|
|
* Are we currently inside the screen area of the tracked element.
|
|
* @property {OpenSeadragon.Point} lastPoint
|
|
* Position of last mouse down/move
|
|
* @property {Number} lastMouseDownTime
|
|
* Time of last mouse down.
|
|
* @property {OpenSeadragon.Point} lastMouseDownPoint
|
|
* Position of last mouse down
|
|
*/
|
|
THIS[ this.hash ] = {
|
|
"mouseover": function( event ){ onMouseOver( _this, event ); },
|
|
"mouseout": function( event ){ onMouseOut( _this, event ); },
|
|
"mousedown": function( event ){ onMouseDown( _this, event ); },
|
|
"mouseup": function( event ){ onMouseUp( _this, event ); },
|
|
"click": function( event ){ onMouseClick( _this, event ); },
|
|
"DOMMouseScroll": function( event ){ onMouseWheelSpin( _this, event ); },
|
|
"mousewheel": function( event ){ onMouseWheelSpin( _this, event ); },
|
|
"mouseupie": function( event ){ onMouseUpIE( _this, event ); },
|
|
"mousemoveie": function( event ){ onMouseMoveIE( _this, event ); },
|
|
"mouseupwindow": function( event ){ onMouseUpWindow( _this, event ); },
|
|
"mousemove": function( event ){ onMouseMove( _this, event ); },
|
|
"touchstart": function( event ){ onTouchStart( _this, event ); },
|
|
"touchmove": function( event ){ onTouchMove( _this, event ); },
|
|
"touchend": function( event ){ onTouchEnd( _this, event ); },
|
|
"keypress": function( event ){ onKeyPress( _this, event ); },
|
|
"focus": function( event ){ onFocus( _this, event ); },
|
|
"blur": function( event ){ onBlur( _this, event ); },
|
|
tracking: false,
|
|
capturing: false,
|
|
buttonDown: false,
|
|
insideElement: false,
|
|
lastPoint: null,
|
|
lastMouseDownTime: null,
|
|
lastMouseDownPoint: null,
|
|
lastPinchDelta: 0
|
|
};
|
|
|
|
};
|
|
|
|
$.MouseTracker.prototype = {
|
|
|
|
/**
|
|
* Are we currently tracking events on this element.
|
|
* @deprecated Just use this.tracking
|
|
* @function
|
|
* @returns {Boolean} Are we currently tracking events on this element.
|
|
*/
|
|
isTracking: function () {
|
|
return THIS[ this.hash ].tracking;
|
|
},
|
|
|
|
/**
|
|
* Enable or disable whether or not we are tracking events on this element.
|
|
* @function
|
|
* @param {Boolean} track True to start tracking, false to stop tracking.
|
|
* @returns {OpenSeadragon.MouseTracker} Chainable.
|
|
*/
|
|
setTracking: function ( track ) {
|
|
if ( track ) {
|
|
startTracking( this );
|
|
} else {
|
|
stopTracking( this );
|
|
}
|
|
//chain
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Implement or assign implmentation to these handlers during or after
|
|
* calling the constructor.
|
|
* @function
|
|
* @param {OpenSeadragon.MouseTracker} tracker
|
|
* A reference to the tracker instance.
|
|
* @param {OpenSeadragon.Point} position
|
|
* The poistion of the event on the screen.
|
|
* @param {Boolean} buttonDown
|
|
* True if the left mouse button is currently being pressed and was
|
|
* initiated inside the tracked element, otherwise false.
|
|
* @param {Boolean} buttonDownAny
|
|
* Was the button down anywhere in the screen during the event.
|
|
*/
|
|
enterHandler: function(){},
|
|
|
|
/**
|
|
* Implement or assign implmentation to these handlers during or after
|
|
* calling the constructor.
|
|
* @function
|
|
* @param {OpenSeadragon.MouseTracker} tracker
|
|
* A reference to the tracker instance.
|
|
* @param {OpenSeadragon.Point} position
|
|
* The poistion of the event on the screen.
|
|
* @param {Boolean} buttonDown
|
|
* True if the left mouse button is currently being pressed and was
|
|
* initiated inside the tracked element, otherwise false.
|
|
* @param {Boolean} buttonDownAny
|
|
* Was the button down anywhere in the screen during the event.
|
|
*/
|
|
exitHandler: function(){},
|
|
|
|
/**
|
|
* Implement or assign implmentation to these handlers during or after
|
|
* calling the constructor.
|
|
* @function
|
|
* @param {OpenSeadragon.MouseTracker} tracker
|
|
* A reference to the tracker instance.
|
|
* @param {OpenSeadragon.Point} position
|
|
* The poistion of the event on the screen.
|
|
*/
|
|
pressHandler: function(){},
|
|
|
|
/**
|
|
* Implement or assign implmentation to these handlers during or after
|
|
* calling the constructor.
|
|
* @function
|
|
* @param {OpenSeadragon.MouseTracker} tracker
|
|
* A reference to the tracker instance.
|
|
* @param {OpenSeadragon.Point} position
|
|
* The poistion of the event on the screen.
|
|
* @param {Boolean} buttonDown
|
|
* True if the left mouse button is currently being pressed and was
|
|
* initiated inside the tracked element, otherwise false.
|
|
* @param {Boolean} insideElementRelease
|
|
* Was the mouse still inside the tracked element when the button
|
|
* was released.
|
|
*/
|
|
releaseHandler: function(){},
|
|
|
|
/**
|
|
* Implement or assign implmentation to these handlers during or after
|
|
* calling the constructor.
|
|
* @function
|
|
* @param {OpenSeadragon.MouseTracker} tracker
|
|
* A reference to the tracker instance.
|
|
* @param {OpenSeadragon.Point} position
|
|
* The poistion of the event on the screen.
|
|
* @param {Number} scroll
|
|
* The scroll delta for the event.
|
|
* @param {Boolean} shift
|
|
* Was the shift key being pressed during this event?
|
|
*/
|
|
scrollHandler: function(){},
|
|
|
|
/**
|
|
* Implement or assign implmentation to these handlers during or after
|
|
* calling the constructor.
|
|
* @function
|
|
* @param {OpenSeadragon.MouseTracker} tracker
|
|
* A reference to the tracker instance.
|
|
* @param {OpenSeadragon.Point} position
|
|
* The poistion of the event on the screen.
|
|
* @param {Boolean} quick
|
|
* True only if the clickDistThreshold and clickDeltaThreshold are
|
|
* both pased. Useful for ignoring events.
|
|
* @param {Boolean} shift
|
|
* Was the shift key being pressed during this event?
|
|
*/
|
|
clickHandler: function(){},
|
|
|
|
/**
|
|
* Implement or assign implmentation to these handlers during or after
|
|
* calling the constructor.
|
|
* @function
|
|
* @param {OpenSeadragon.MouseTracker} tracker
|
|
* A reference to the tracker instance.
|
|
* @param {OpenSeadragon.Point} position
|
|
* The poistion of the event on the screen.
|
|
* @param {OpenSeadragon.Point} delta
|
|
* The x,y components of the difference between start drag and
|
|
* end drag. Usefule for ignoring or weighting the events.
|
|
* @param {Boolean} shift
|
|
* Was the shift key being pressed during this event?
|
|
*/
|
|
dragHandler: function(){},
|
|
|
|
/**
|
|
* Implement or assign implmentation to these handlers during or after
|
|
* calling the constructor.
|
|
* @function
|
|
* @param {OpenSeadragon.MouseTracker} tracker
|
|
* A reference to the tracker instance.
|
|
* @param {Number} keyCode
|
|
* The key code that was pressed.
|
|
* @param {Boolean} shift
|
|
* Was the shift key being pressed during this event?
|
|
*/
|
|
keyHandler: function(){},
|
|
|
|
focusHandler: function(){},
|
|
|
|
blurHandler: function(){}
|
|
};
|
|
|
|
/**
|
|
* Starts tracking mouse events on this element.
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function startTracking( tracker ) {
|
|
var events = [
|
|
"mouseover", "mouseout", "mousedown", "mouseup",
|
|
"click",
|
|
"DOMMouseScroll", "mousewheel",
|
|
"touchstart", "touchmove", "touchend",
|
|
"keypress",
|
|
"focus", "blur"
|
|
],
|
|
delegate = THIS[ tracker.hash ],
|
|
event,
|
|
i;
|
|
|
|
if ( !delegate.tracking ) {
|
|
for( i = 0; i < events.length; i++ ){
|
|
event = events[ i ];
|
|
$.addEvent(
|
|
tracker.element,
|
|
event,
|
|
delegate[ event ],
|
|
false
|
|
);
|
|
}
|
|
delegate.tracking = true;
|
|
ACTIVE[ tracker.hash ] = tracker;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Stops tracking mouse events on this element.
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function stopTracking( tracker ) {
|
|
var events = [
|
|
"mouseover", "mouseout", "mousedown", "mouseup",
|
|
"click",
|
|
"DOMMouseScroll", "mousewheel",
|
|
"touchstart", "touchmove", "touchend",
|
|
"keypress",
|
|
"focus", "blur"
|
|
],
|
|
delegate = THIS[ tracker.hash ],
|
|
event,
|
|
i;
|
|
|
|
if ( delegate.tracking ) {
|
|
for( i = 0; i < events.length; i++ ){
|
|
event = events[ i ];
|
|
$.removeEvent(
|
|
tracker.element,
|
|
event,
|
|
delegate[ event ],
|
|
false
|
|
);
|
|
}
|
|
|
|
releaseMouse( tracker );
|
|
delegate.tracking = false;
|
|
delete ACTIVE[ tracker.hash ];
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function hasMouse( tracker ) {
|
|
return THIS[ tracker.hash ].insideElement;
|
|
};
|
|
|
|
/**
|
|
* Begin capturing mouse events on this element.
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function captureMouse( tracker ) {
|
|
var delegate = THIS[ tracker.hash ];
|
|
if ( !delegate.capturing ) {
|
|
|
|
if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) {
|
|
$.removeEvent(
|
|
tracker.element,
|
|
"mouseup",
|
|
delegate[ "mouseup" ],
|
|
false
|
|
);
|
|
$.addEvent(
|
|
tracker.element,
|
|
"mouseup",
|
|
delegate[ "mouseupie" ],
|
|
true
|
|
);
|
|
$.addEvent(
|
|
tracker.element,
|
|
"mousemove",
|
|
delegate[ "mousemoveie" ],
|
|
true
|
|
);
|
|
} else {
|
|
$.addEvent(
|
|
window,
|
|
"mouseup",
|
|
delegate[ "mouseupwindow" ],
|
|
true
|
|
);
|
|
$.addEvent(
|
|
window,
|
|
"mousemove",
|
|
delegate[ "mousemove" ],
|
|
true
|
|
);
|
|
}
|
|
delegate.capturing = true;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Stop capturing mouse events on this element.
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function releaseMouse( tracker ) {
|
|
var delegate = THIS[ tracker.hash ];
|
|
if ( delegate.capturing ) {
|
|
|
|
if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) {
|
|
$.removeEvent(
|
|
tracker.element,
|
|
"mousemove",
|
|
delegate[ "mousemoveie" ],
|
|
true
|
|
);
|
|
$.removeEvent(
|
|
tracker.element,
|
|
"mouseup",
|
|
delegate[ "mouseupie" ],
|
|
true
|
|
);
|
|
$.addEvent(
|
|
tracker.element,
|
|
"mouseup",
|
|
delegate[ "mouseup" ],
|
|
false
|
|
);
|
|
} else {
|
|
$.removeEvent(
|
|
window,
|
|
"mousemove",
|
|
delegate[ "mousemove" ],
|
|
true
|
|
);
|
|
$.removeEvent(
|
|
window,
|
|
"mouseup",
|
|
delegate[ "mouseupwindow" ],
|
|
true
|
|
);
|
|
}
|
|
delegate.capturing = false;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function triggerOthers( tracker, handler, event ) {
|
|
var otherHash;
|
|
for ( otherHash in ACTIVE ) {
|
|
if ( ACTIVE.hasOwnProperty( otherHash ) && tracker.hash != otherHash ) {
|
|
handler( ACTIVE[ otherHash ], event );
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onFocus( tracker, event ){
|
|
//console.log( "focus %s", event );
|
|
var propagate;
|
|
if ( tracker.focusHandler ) {
|
|
propagate = tracker.focusHandler(
|
|
tracker,
|
|
event
|
|
);
|
|
if( propagate === false ){
|
|
$.cancelEvent( event );
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onBlur( tracker, event ){
|
|
//console.log( "blur %s", event );
|
|
var propagate;
|
|
if ( tracker.blurHandler ) {
|
|
propagate = tracker.blurHandler(
|
|
tracker,
|
|
event
|
|
);
|
|
if( propagate === false ){
|
|
$.cancelEvent( event );
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onKeyPress( tracker, event ){
|
|
//console.log( "keypress %s %s %s %s %s", event.keyCode, event.charCode, event.ctrlKey, event.shiftKey, event.altKey );
|
|
var propagate;
|
|
if ( tracker.keyHandler ) {
|
|
propagate = tracker.keyHandler(
|
|
tracker,
|
|
event.keyCode ? event.keyCode : event.charCode,
|
|
event.shiftKey
|
|
);
|
|
if( propagate === false ){
|
|
$.cancelEvent( event );
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onMouseOver( tracker, event ) {
|
|
|
|
var event = $.getEvent( event ),
|
|
delegate = THIS[ tracker.hash ],
|
|
propagate;
|
|
|
|
if ( $.Browser.vendor == $.BROWSERS.IE &&
|
|
$.Browser.version < 9 &&
|
|
delegate.capturing &&
|
|
!isChild( event.srcElement, tracker.element ) ) {
|
|
|
|
triggerOthers( tracker, onMouseOver, event );
|
|
|
|
}
|
|
|
|
var to = event.target ?
|
|
event.target :
|
|
event.srcElement,
|
|
from = event.relatedTarget ?
|
|
event.relatedTarget :
|
|
event.fromElement;
|
|
|
|
if ( !isChild( tracker.element, to ) ||
|
|
isChild( tracker.element, from ) ) {
|
|
return;
|
|
}
|
|
|
|
delegate.insideElement = true;
|
|
|
|
if ( tracker.enterHandler ) {
|
|
propagate = tracker.enterHandler(
|
|
tracker,
|
|
getMouseRelative( event, tracker.element ),
|
|
delegate.buttonDown,
|
|
IS_BUTTON_DOWN
|
|
);
|
|
if( propagate === false ){
|
|
$.cancelEvent( event );
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onMouseOut( tracker, event ) {
|
|
var event = $.getEvent( event ),
|
|
delegate = THIS[ tracker.hash ],
|
|
propagate;
|
|
|
|
if ( $.Browser.vendor == $.BROWSERS.IE &&
|
|
$.Browser.version < 9 &&
|
|
delegate.capturing &&
|
|
!isChild( event.srcElement, tracker.element ) ) {
|
|
|
|
triggerOthers( tracker, onMouseOut, event );
|
|
|
|
}
|
|
|
|
var from = event.target ?
|
|
event.target :
|
|
event.srcElement,
|
|
to = event.relatedTarget ?
|
|
event.relatedTarget :
|
|
event.toElement;
|
|
|
|
if ( !isChild( tracker.element, from ) ||
|
|
isChild( tracker.element, to ) ) {
|
|
return;
|
|
}
|
|
|
|
delegate.insideElement = false;
|
|
|
|
if ( tracker.exitHandler ) {
|
|
propagate = tracker.exitHandler(
|
|
tracker,
|
|
getMouseRelative( event, tracker.element ),
|
|
delegate.buttonDown,
|
|
IS_BUTTON_DOWN
|
|
);
|
|
|
|
if( propagate === false ){
|
|
$.cancelEvent( event );
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onMouseDown( tracker, event ) {
|
|
var event = $.getEvent( event ),
|
|
delegate = THIS[ tracker.hash ],
|
|
propagate;
|
|
|
|
if ( event.button == 2 ) {
|
|
return;
|
|
}
|
|
|
|
delegate.buttonDown = true;
|
|
|
|
delegate.lastPoint = getMouseAbsolute( event );
|
|
delegate.lastMouseDownPoint = delegate.lastPoint;
|
|
delegate.lastMouseDownTime = +new Date();
|
|
|
|
if ( tracker.pressHandler ) {
|
|
propagate = tracker.pressHandler(
|
|
tracker,
|
|
getMouseRelative( event, tracker.element )
|
|
);
|
|
if( propagate === false ){
|
|
$.cancelEvent( event );
|
|
}
|
|
}
|
|
|
|
if ( tracker.pressHandler || tracker.dragHandler ) {
|
|
$.cancelEvent( event );
|
|
}
|
|
|
|
if ( !( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) ||
|
|
!IS_CAPTURING ) {
|
|
captureMouse( tracker );
|
|
IS_CAPTURING = true;
|
|
// reset to empty & add us
|
|
CAPTURING = [ tracker ];
|
|
} else if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) {
|
|
// add us to the list
|
|
CAPTURING.push( tracker );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onTouchStart( tracker, event ) {
|
|
var touchA,
|
|
touchB;
|
|
|
|
if( event.touches.length == 1 &&
|
|
event.targetTouches.length == 1 &&
|
|
event.changedTouches.length == 1 ){
|
|
|
|
THIS[ tracker.hash ].lastTouch = event.touches[ 0 ];
|
|
onMouseOver( tracker, event.changedTouches[ 0 ] );
|
|
onMouseDown( tracker, event.touches[ 0 ] );
|
|
}
|
|
|
|
if( event.touches.length == 2 ){
|
|
|
|
touchA = getMouseAbsolute( event.touches[ 0 ] );
|
|
touchB = getMouseAbsolute( event.touches[ 1 ] );
|
|
THIS[ tracker.hash ].lastPinchDelta =
|
|
Math.abs( touchA.x - touchB.x ) +
|
|
Math.abs( touchA.y - touchB.y );
|
|
//$.console.debug("pinch start : "+THIS[ tracker.hash ].lastPinchDelta);
|
|
}
|
|
|
|
event.preventDefault();
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onMouseUp( tracker, event ) {
|
|
var event = $.getEvent( event ),
|
|
delegate = THIS[ tracker.hash ],
|
|
//were we inside the tracked element when we were pressed
|
|
insideElementPress = delegate.buttonDown,
|
|
//are we still inside the tracked element when we released
|
|
insideElementRelease = delegate.insideElement,
|
|
propagate;
|
|
|
|
if ( event.button == 2 ) {
|
|
return;
|
|
}
|
|
|
|
delegate.buttonDown = false;
|
|
|
|
if ( tracker.releaseHandler ) {
|
|
propagate = tracker.releaseHandler(
|
|
tracker,
|
|
getMouseRelative( event, tracker.element ),
|
|
insideElementPress,
|
|
insideElementRelease
|
|
);
|
|
if( propagate === false ){
|
|
$.cancelEvent( event );
|
|
}
|
|
}
|
|
|
|
if ( insideElementPress && insideElementRelease ) {
|
|
handleMouseClick( tracker, event );
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onTouchEnd( tracker, event ) {
|
|
|
|
if( event.touches.length == 0 &&
|
|
event.targetTouches.length == 0 &&
|
|
event.changedTouches.length == 1 ){
|
|
|
|
THIS[ tracker.hash ].lastTouch = null;
|
|
onMouseUp( tracker, event.changedTouches[ 0 ] );
|
|
onMouseOut( tracker, event.changedTouches[ 0 ] );
|
|
}
|
|
if( event.touches.length + event.changedTouches.length == 2 ){
|
|
THIS[ tracker.hash ].lastPinchDelta = null;
|
|
//$.console.debug("pinch end");
|
|
}
|
|
event.preventDefault();
|
|
};
|
|
|
|
|
|
/**
|
|
* Only triggered once by the deepest element that initially received
|
|
* the mouse down event. We want to make sure THIS event doesn't bubble.
|
|
* Instead, we want to trigger the elements that initially received the
|
|
* mouse down event (including this one) only if the mouse is no longer
|
|
* inside them. Then, we want to release capture, and emulate a regular
|
|
* mouseup on the event that this event was meant for.
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onMouseUpIE( tracker, event ) {
|
|
var event = $.getEvent( event ),
|
|
othertracker,
|
|
i;
|
|
|
|
if ( event.button == 2 ) {
|
|
return;
|
|
}
|
|
|
|
for ( i = 0; i < CAPTURING.length; i++ ) {
|
|
othertracker = CAPTURING[ i ];
|
|
if ( !hasMouse( othertracker ) ) {
|
|
onMouseUp( othertracker, event );
|
|
}
|
|
}
|
|
|
|
releaseMouse( tracker );
|
|
IS_CAPTURING = false;
|
|
event.srcElement.fireEvent(
|
|
"on" + event.type,
|
|
document.createEventObject( event )
|
|
);
|
|
|
|
$.stopEvent( event );
|
|
};
|
|
|
|
|
|
/**
|
|
* Only triggered in W3C browsers by elements within which the mouse was
|
|
* initially pressed, since they are now listening to the window for
|
|
* mouseup during the capture phase. We shouldn't handle the mouseup
|
|
* here if the mouse is still inside this element, since the regular
|
|
* mouseup handler will still fire.
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onMouseUpWindow( tracker, event ) {
|
|
if ( ! THIS[ tracker.hash ].insideElement ) {
|
|
onMouseUp( tracker, event );
|
|
}
|
|
releaseMouse( tracker );
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onMouseClick( tracker, event ) {
|
|
if ( tracker.clickHandler ) {
|
|
$.cancelEvent( event );
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onMouseWheelSpin( tracker, event ) {
|
|
var nDelta = 0,
|
|
propagate;
|
|
|
|
if ( !event ) { // For IE, access the global (window) event object
|
|
event = window.event;
|
|
}
|
|
|
|
if ( event.wheelDelta ) { // IE and Opera
|
|
nDelta = event.wheelDelta;
|
|
if ( window.opera ) { // Opera has the values reversed
|
|
nDelta = -nDelta;
|
|
}
|
|
} else if (event.detail) { // Mozilla FireFox
|
|
nDelta = -event.detail;
|
|
}
|
|
//The nDelta variable is gated to provide smooth z-index scrolling
|
|
//since the mouse wheel allows for substantial deltas meant for rapid
|
|
//y-index scrolling.
|
|
nDelta = nDelta > 0 ? 1 : -1;
|
|
|
|
if ( tracker.scrollHandler ) {
|
|
propagate = tracker.scrollHandler(
|
|
tracker,
|
|
getMouseRelative( event, tracker.element ),
|
|
nDelta,
|
|
event.shiftKey
|
|
);
|
|
if( propagate === false ){
|
|
$.cancelEvent( event );
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function handleMouseClick( tracker, event ) {
|
|
var event = $.getEvent( event ),
|
|
delegate = THIS[ tracker.hash ],
|
|
propagate;
|
|
|
|
if ( event.button == 2 ) {
|
|
return;
|
|
}
|
|
|
|
var time = +new Date() - delegate.lastMouseDownTime,
|
|
point = getMouseAbsolute( event ),
|
|
distance = delegate.lastMouseDownPoint.distanceTo( point ),
|
|
quick = time <= tracker.clickTimeThreshold &&
|
|
distance <= tracker.clickDistThreshold;
|
|
|
|
if ( tracker.clickHandler ) {
|
|
propagate = tracker.clickHandler(
|
|
tracker,
|
|
getMouseRelative( event, tracker.element ),
|
|
quick,
|
|
event.shiftKey
|
|
);
|
|
if( propagate === false ){
|
|
$.cancelEvent( event );
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onMouseMove( tracker, event ) {
|
|
var event = $.getEvent( event ),
|
|
delegate = THIS[ tracker.hash ],
|
|
point = getMouseAbsolute( event ),
|
|
delta = point.minus( delegate.lastPoint ),
|
|
propagate;
|
|
|
|
delegate.lastPoint = point;
|
|
|
|
if ( tracker.dragHandler ) {
|
|
propagate = tracker.dragHandler(
|
|
tracker,
|
|
getMouseRelative( event, tracker.element ),
|
|
delta,
|
|
event.shiftKey
|
|
);
|
|
if( propagate === false ){
|
|
$.cancelEvent( event );
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onTouchMove( tracker, event ) {
|
|
var touchA,
|
|
touchB,
|
|
pinchDelta;
|
|
|
|
if( event.touches.length === 1 &&
|
|
event.targetTouches.length === 1 &&
|
|
event.changedTouches.length === 1 &&
|
|
THIS[ tracker.hash ].lastTouch === event.touches[ 0 ]){
|
|
|
|
onMouseMove( tracker, event.touches[ 0 ] );
|
|
|
|
} else if ( event.touches.length === 2 ){
|
|
|
|
touchA = getMouseAbsolute( event.touches[ 0 ] );
|
|
touchB = getMouseAbsolute( event.touches[ 1 ] );
|
|
pinchDelta =
|
|
Math.abs( touchA.x - touchB.x ) +
|
|
Math.abs( touchA.y - touchB.y );
|
|
|
|
//TODO: make the 75px pinch threshold configurable
|
|
if( Math.abs( THIS[ tracker.hash ].lastPinchDelta - pinchDelta ) > 75 ){
|
|
//$.console.debug( "pinch delta : " + pinchDelta + " | previous : " + THIS[ tracker.hash ].lastPinchDelta);
|
|
|
|
onMouseWheelSpin( tracker, {
|
|
shift: false,
|
|
pageX: ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2,
|
|
pageY: ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2,
|
|
detail:(
|
|
THIS[ tracker.hash ].lastPinchDelta > pinchDelta
|
|
) ? 1 : -1
|
|
});
|
|
|
|
THIS[ tracker.hash ].lastPinchDelta = pinchDelta;
|
|
}
|
|
}
|
|
event.preventDefault();
|
|
};
|
|
|
|
/**
|
|
* Only triggered once by the deepest element that initially received
|
|
* the mouse down event. Since no other element has captured the mouse,
|
|
* we want to trigger the elements that initially received the mouse
|
|
* down event (including this one). The the param tracker isn't used
|
|
* but for consistency with the other event handlers we include it.
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onMouseMoveIE( tracker, event ) {
|
|
var i;
|
|
for ( i = 0; i < CAPTURING.length; i++ ) {
|
|
onMouseMove( CAPTURING[ i ], event );
|
|
}
|
|
|
|
$.stopEvent( event );
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function getMouseAbsolute( event ) {
|
|
return $.getMousePosition( event );
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function getMouseRelative( event, element ) {
|
|
var mouse = $.getMousePosition( event ),
|
|
offset = $.getElementPosition( element );
|
|
|
|
return mouse.minus( offset );
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* Returns true if elementB is a child node of elementA, or if they're equal.
|
|
*/
|
|
function isChild( elementA, elementB ) {
|
|
var body = document.body;
|
|
while ( elementB && elementA != elementB && body != elementB ) {
|
|
try {
|
|
elementB = elementB.parentNode;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
return elementA == elementB;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onGlobalMouseDown() {
|
|
IS_BUTTON_DOWN = true;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
*/
|
|
function onGlobalMouseUp() {
|
|
IS_BUTTON_DOWN = false;
|
|
};
|
|
|
|
|
|
(function () {
|
|
if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) {
|
|
$.addEvent( document, "mousedown", onGlobalMouseDown, false );
|
|
$.addEvent( document, "mouseup", onGlobalMouseUp, false );
|
|
} else {
|
|
$.addEvent( window, "mousedown", onGlobalMouseDown, true );
|
|
$.addEvent( window, "mouseup", onGlobalMouseUp, true );
|
|
}
|
|
})();
|
|
|
|
}( OpenSeadragon ));
|
|
|
|
(function( $ ){
|
|
|
|
/**
|
|
* An enumeration of supported locations where controls can be anchored,
|
|
* including NONE, TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, and BOTTOM_LEFT.
|
|
* The anchoring is always relative to the container
|
|
* @static
|
|
*/
|
|
$.ControlAnchor = {
|
|
NONE: 0,
|
|
TOP_LEFT: 1,
|
|
TOP_RIGHT: 2,
|
|
BOTTOM_RIGHT: 3,
|
|
BOTTOM_LEFT: 4
|
|
};
|
|
|
|
/**
|
|
* A Control represents any interface element which is meant to allow the user
|
|
* to interact with the zoomable interface. Any control can be anchored to any
|
|
* element.
|
|
* @class
|
|
* @param {Element} element - the contol element to be anchored in the container.
|
|
* @param {OpenSeadragon.ControlAnchor} anchor - the location to anchor at.
|
|
* @param {Element} container - the element to control will be anchored too.
|
|
*
|
|
* @property {Element} element - the element providing the user interface with
|
|
* some type of control. Eg a zoom-in button
|
|
* @property {OpenSeadragon.ControlAnchor} anchor - the position of the control
|
|
* relative to the container.
|
|
* @property {Element} container - the element within with the control is
|
|
* positioned.
|
|
* @property {Element} wrapper - a nuetral element surrounding the control
|
|
* element.
|
|
*/
|
|
$.Control = function ( element, anchor, container ) {
|
|
this.element = element;
|
|
this.anchor = anchor;
|
|
this.container = container;
|
|
this.wrapper = $.makeNeutralElement( "span" );
|
|
this.wrapper.style.display = "inline-block";
|
|
this.wrapper.appendChild( this.element );
|
|
|
|
if ( this.anchor == $.ControlAnchor.NONE ) {
|
|
// IE6 fix
|
|
this.wrapper.style.width = this.wrapper.style.height = "100%";
|
|
}
|
|
|
|
if ( this.anchor == $.ControlAnchor.TOP_RIGHT ||
|
|
this.anchor == $.ControlAnchor.BOTTOM_RIGHT ) {
|
|
this.container.insertBefore(
|
|
this.wrapper,
|
|
this.container.firstChild
|
|
);
|
|
} else {
|
|
this.container.appendChild( this.wrapper );
|
|
}
|
|
};
|
|
|
|
$.Control.prototype = {
|
|
|
|
/**
|
|
* Removes the control from the container.
|
|
* @function
|
|
*/
|
|
destroy: function() {
|
|
this.wrapper.removeChild( this.element );
|
|
this.container.removeChild( this.wrapper );
|
|
},
|
|
|
|
/**
|
|
* Determines if the control is currently visible.
|
|
* @function
|
|
* @return {Boolean} true if currenly visible, false otherwise.
|
|
*/
|
|
isVisible: function() {
|
|
return this.wrapper.style.display != "none";
|
|
},
|
|
|
|
/**
|
|
* Toggles the visibility of the control.
|
|
* @function
|
|
* @param {Boolean} visible - true to make visible, false to hide.
|
|
*/
|
|
setVisible: function( visible ) {
|
|
this.wrapper.style.display = visible ?
|
|
"inline-block" :
|
|
"none";
|
|
},
|
|
|
|
/**
|
|
* Sets the opacity level for the control.
|
|
* @function
|
|
* @param {Number} opactiy - a value between 1 and 0 inclusively.
|
|
*/
|
|
setOpacity: function( opacity ) {
|
|
if ( this.element[ $.SIGNAL ] && $.Browser.vendor == $.BROWSERS.IE ) {
|
|
$.setElementOpacity( this.element, opacity, true );
|
|
} else {
|
|
$.setElementOpacity( this.wrapper, opacity, true );
|
|
}
|
|
}
|
|
};
|
|
|
|
}( OpenSeadragon ));
|
|
(function( $ ){
|
|
|
|
//id hash for private properties;
|
|
var THIS = {};
|
|
|
|
/**
|
|
* @class
|
|
*/
|
|
$.ControlDock = function( options ){
|
|
var layouts = [ 'topleft', 'topright', 'bottomright', 'bottomleft'],
|
|
layout,
|
|
i;
|
|
|
|
$.extend( true, this, {
|
|
id: 'controldock-'+(+new Date())+'-'+Math.floor(Math.random()*1000000),
|
|
container: $.makeNeutralElement('form'),
|
|
controls: []
|
|
}, options );
|
|
|
|
if( this.element ){
|
|
this.element = $.getElement( this.element );
|
|
this.element.appendChild( this.container );
|
|
this.element.style.position = 'relative';
|
|
this.container.style.width = '100%';
|
|
this.container.style.height = '100%';
|
|
}
|
|
|
|
for( i = 0; i < layouts.length; i++ ){
|
|
layout = layouts[ i ];
|
|
this.controls[ layout ] = $.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';
|
|
}
|
|
}
|
|
|
|
this.container.appendChild( this.controls.topleft );
|
|
this.container.appendChild( this.controls.topright );
|
|
this.container.appendChild( this.controls.bottomright );
|
|
this.container.appendChild( this.controls.bottomleft );
|
|
};
|
|
|
|
$.ControlDock.prototype = {
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
addControl: function ( element, anchor ) {
|
|
var element = $.getElement( element ),
|
|
div = null;
|
|
|
|
if ( getControlIndex( this, element ) >= 0 ) {
|
|
return; // they're trying to add a duplicate control
|
|
}
|
|
|
|
switch ( anchor ) {
|
|
case $.ControlAnchor.TOP_RIGHT:
|
|
div = this.controls.topright;
|
|
element.style.position = "relative";
|
|
element.style.paddingRight = "0px";
|
|
element.style.paddingTop = "0px";
|
|
break;
|
|
case $.ControlAnchor.BOTTOM_RIGHT:
|
|
div = this.controls.bottomright;
|
|
element.style.position = "relative";
|
|
element.style.paddingRight = "0px";
|
|
element.style.paddingBottom = "0px";
|
|
break;
|
|
case $.ControlAnchor.BOTTOM_LEFT:
|
|
div = this.controls.bottomleft;
|
|
element.style.position = "relative";
|
|
element.style.paddingLeft = "0px";
|
|
element.style.paddingBottom = "0px";
|
|
break;
|
|
case $.ControlAnchor.TOP_LEFT:
|
|
div = this.controls.topleft;
|
|
element.style.position = "relative";
|
|
element.style.paddingLeft = "0px";
|
|
element.style.paddingTop = "0px";
|
|
break;
|
|
case $.ControlAnchor.NONE:
|
|
default:
|
|
div = this.container;
|
|
element.style.margin = "0px";
|
|
element.style.padding = "0px";
|
|
break;
|
|
}
|
|
|
|
this.controls.push(
|
|
new $.Control( element, anchor, div )
|
|
);
|
|
element.style.display = "inline-block";
|
|
},
|
|
|
|
|
|
/**
|
|
* @function
|
|
* @return {OpenSeadragon.ControlDock} Chainable.
|
|
*/
|
|
removeControl: function ( element ) {
|
|
var element = $.getElement( element ),
|
|
i = getControlIndex( this, element );
|
|
|
|
if ( i >= 0 ) {
|
|
this.controls[ i ].destroy();
|
|
this.controls.splice( i, 1 );
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @return {OpenSeadragon.ControlDock} Chainable.
|
|
*/
|
|
clearControls: function () {
|
|
while ( this.controls.length > 0 ) {
|
|
this.controls.pop().destroy();
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
|
|
/**
|
|
* @function
|
|
* @return {Boolean}
|
|
*/
|
|
areControlsEnabled: function () {
|
|
var i;
|
|
|
|
for ( i = this.controls.length - 1; i >= 0; i-- ) {
|
|
if ( this.controls[ i ].isVisible() ) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
|
|
/**
|
|
* @function
|
|
* @return {OpenSeadragon.ControlDock} Chainable.
|
|
*/
|
|
setControlsEnabled: function( enabled ) {
|
|
var i;
|
|
|
|
for ( i = this.controls.length - 1; i >= 0; i-- ) {
|
|
this.controls[ i ].setVisible( enabled );
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
};
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Utility methods
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
function getControlIndex( dock, element ) {
|
|
var controls = dock.controls,
|
|
i;
|
|
|
|
for ( i = controls.length - 1; i >= 0; i-- ) {
|
|
if ( controls[ i ].element == element ) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
};
|
|
|
|
}( OpenSeadragon ));(function( $ ){
|
|
|
|
// dictionary from hash to private properties
|
|
var THIS = {},
|
|
// We keep a list of viewers so we can 'wake-up' each viewer on
|
|
// a page after toggling between fullpage modes
|
|
VIEWERS = {};
|
|
|
|
/**
|
|
*
|
|
* 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
|
|
* @extends OpenSeadragon.EventHandler
|
|
* @extends OpenSeadragon.ControlDock
|
|
* @param {Object} options
|
|
* @param {String} options.element Id of Element to attach to,
|
|
* @param {String} options.xmlPath Xpath ( TODO: not sure! ),
|
|
* @param {String} options.prefixUrl Url used to prepend to paths, eg button
|
|
* images, etc.
|
|
* @param {Seadragon.Controls[]} options.controls Array of Seadragon.Controls,
|
|
* @param {Seadragon.Overlays[]} options.overlays Array of Seadragon.Overlays,
|
|
* @param {Seadragon.Controls[]} options.overlayControls An Array of ( TODO:
|
|
* not sure! )
|
|
*
|
|
**/
|
|
$.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,
|
|
overlayControls: args.length > 5 ? args[ 5 ] : 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.id,
|
|
|
|
//dom nodes
|
|
element: null,
|
|
canvas: null,
|
|
container: null,
|
|
|
|
//TODO: not sure how to best describe these
|
|
overlays: [],
|
|
overlayControls:[],
|
|
|
|
//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. Its still considered idiomatic to put them here
|
|
source: null,
|
|
drawer: null,
|
|
viewport: null,
|
|
navigator: null,
|
|
|
|
//A collection viewport is a seperate viewport used to provide
|
|
//simultanious rendering of sets of tiless
|
|
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 );
|
|
|
|
//Private state properties
|
|
THIS[ this.hash ] = {
|
|
"fsBoundsDelta": new $.Point( 1, 1 ),
|
|
"prevContainerSize": null,
|
|
"lastOpenStartTime": 0,
|
|
"lastOpenEndTime": 0,
|
|
"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
|
|
};
|
|
|
|
//Inherit some behaviors and properties
|
|
$.EventHandler.call( this );
|
|
$.ControlDock.call( this, options );
|
|
|
|
//Deal with tile sources
|
|
var initialTileSource,
|
|
customTileSource;
|
|
|
|
if ( this.xmlPath ){
|
|
//Deprecated option. Now it is preferred to use the tileSources option
|
|
this.tileSources = [ this.xmlPath ];
|
|
}
|
|
|
|
if ( this.tileSources ){
|
|
// tileSources is a complex option...
|
|
//
|
|
// It can be a string, object, or an array of any of strings and objects.
|
|
// At this point we only care about if it is an Array or not.
|
|
//
|
|
if( $.isArray( this.tileSources ) ){
|
|
|
|
//must be a sequence of tileSource since the first item
|
|
//is a legacy tile source
|
|
if( this.tileSources.length > 1 ){
|
|
THIS[ this.hash ].sequenced = true;
|
|
}
|
|
initialTileSource = this.tileSources[ 0 ];
|
|
} else {
|
|
initialTileSource = this.tileSources;
|
|
}
|
|
|
|
this.openTileSource( initialTileSource );
|
|
}
|
|
|
|
this.element = this.element || document.getElementById( this.id );
|
|
this.canvas = $.makeNeutralElement( "div" );
|
|
|
|
(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.overflow = "hidden";
|
|
container.left = "0px";
|
|
container.top = "0px";
|
|
container.textAlign = "left"; // needed to protect against
|
|
}( this.container.style ));
|
|
|
|
this.container.insertBefore( this.canvas, this.container.firstChild );
|
|
this.element.appendChild( this.container );
|
|
|
|
//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.innerTracker = new $.MouseTracker({
|
|
element: this.canvas,
|
|
clickTimeThreshold: this.clickTimeThreshold,
|
|
clickDistThreshold: this.clickDistThreshold,
|
|
clickHandler: $.delegate( this, onCanvasClick ),
|
|
dragHandler: $.delegate( this, onCanvasDrag ),
|
|
releaseHandler: $.delegate( this, onCanvasRelease ),
|
|
scrollHandler: $.delegate( this, onCanvasScroll )
|
|
}).setTracking( this.mouseNavEnabled ? true : false ); // default state
|
|
|
|
this.outerTracker = new $.MouseTracker({
|
|
element: this.container,
|
|
clickTimeThreshold: this.clickTimeThreshold,
|
|
clickDistThreshold: this.clickDistThreshold,
|
|
enterHandler: $.delegate( this, onContainerEnter ),
|
|
exitHandler: $.delegate( this, onContainerExit ),
|
|
releaseHandler: $.delegate( this, onContainerRelease )
|
|
}).setTracking( this.mouseNavEnabled ? true : false ); // always tracking
|
|
|
|
if( this.toolbar ){
|
|
this.toolbar = new $.ControlDock({ element: this.toolbar });
|
|
}
|
|
|
|
this.bindStandardControls();
|
|
this.bindSequenceControls();
|
|
|
|
for ( i = 0; i < this.customControls.length; i++ ) {
|
|
this.addControl(
|
|
this.customControls[ i ].id,
|
|
this.customControls[ i ].anchor
|
|
);
|
|
}
|
|
|
|
window.setTimeout( function(){
|
|
beginControlsAutoHide( _this );
|
|
}, 1 ); // initial fade out
|
|
|
|
};
|
|
|
|
$.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, {
|
|
|
|
|
|
/**
|
|
* @function
|
|
* @name OpenSeadragon.Viewer.prototype.isOpen
|
|
* @return {Boolean}
|
|
*/
|
|
isOpen: function () {
|
|
return !!this.source;
|
|
},
|
|
|
|
/**
|
|
* If the string is xml is simply parsed and opened, otherwise the string
|
|
* is treated as an URL and an xml document is requested via ajax, parsed
|
|
* and then opened in the viewer.
|
|
* @function
|
|
* @name OpenSeadragon.Viewer.prototype.openDzi
|
|
* @param {String} dzi and xml string or the url to a DZI xml document.
|
|
* @return {OpenSeadragon.Viewer} Chainable.
|
|
*
|
|
* @deprecated - use 'open' instead.
|
|
*/
|
|
openDzi: function ( dzi ) {
|
|
var _this = this;
|
|
$.createFromDZI(
|
|
dzi,
|
|
function( source ){
|
|
_this.open( source );
|
|
},
|
|
this.tileHost
|
|
);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* tileSources is a complex option...
|
|
*
|
|
* It can be a string, object, function, or an array of any of these:
|
|
*
|
|
* - 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.
|
|
* @function
|
|
* @name OpenSeadragon.Viewer.prototype.openTileSource
|
|
* @return {OpenSeadragon.Viewer} Chainable.
|
|
*/
|
|
openTileSource: function ( tileSource ) {
|
|
var _this = this,
|
|
customTileSource,
|
|
readySource,
|
|
$TileSource,
|
|
options;
|
|
|
|
setTimeout(function(){
|
|
if ( $.type( tileSource ) == 'string') {
|
|
//TODO: We cant assume a string implies a dzi since all
|
|
//complete TileSource implementations should have a getInfo
|
|
//which allows them to be configured via AJAX. Im not sure
|
|
//if its better to use file extension or url pattern, or to
|
|
//inspect the resulting info object.
|
|
tileSource = new $.TileSource( tileSource, function( readySource ){
|
|
_this.open( readySource );
|
|
});
|
|
|
|
} else if ( $.isPlainObject( tileSource ) ){
|
|
if( $.isFunction( tileSource.getTileUrl ) ){
|
|
//Custom tile source
|
|
customTileSource = new $.TileSource(tileSource);
|
|
customTileSource.getTileUrl = tileSource.getTileUrl;
|
|
_this.open( customTileSource );
|
|
} else {
|
|
//inline configuration
|
|
$TileSource = $.TileSource.determineType( _this, tileSource );
|
|
options = $TileSource.prototype.configure.apply( _this, [ tileSource ]);
|
|
readySource = new $TileSource( options );
|
|
_this.open( readySource );
|
|
}
|
|
} else {
|
|
//can assume it's already a tile source implementation
|
|
_this.open( tileSource );
|
|
}
|
|
}, 1);
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @name OpenSeadragon.Viewer.prototype.open
|
|
* @return {OpenSeadragon.Viewer} Chainable.
|
|
*/
|
|
open: function( source ) {
|
|
var _this = this,
|
|
overlay,
|
|
i;
|
|
|
|
if ( this.source ) {
|
|
this.close( );
|
|
}
|
|
|
|
// to ignore earlier opens
|
|
THIS[ this.hash ].lastOpenStartTime = +new Date();
|
|
|
|
window.setTimeout( function () {
|
|
if ( THIS[ _this.hash ].lastOpenStartTime > THIS[ _this.hash ].lastOpenEndTime ) {
|
|
THIS[ _this.hash ].setMessage( $.getString( "Messages.Loading" ) );
|
|
}
|
|
}, 2000);
|
|
|
|
THIS[ this.hash ].lastOpenEndTime = +new Date();
|
|
this.canvas.innerHTML = "";
|
|
THIS[ this.hash ].prevContainerSize = $.getElementSize( this.container );
|
|
|
|
|
|
if( this.collectionMode ){
|
|
this.source = new $.TileSourceCollection({
|
|
rows: this.collectionRows,
|
|
layout: this.collectionLayout,
|
|
tileSize: this.collectionTileSize,
|
|
tileSources: this.tileSources,
|
|
tileMargin: this.collectionTileMargin
|
|
});
|
|
this.viewport = this.viewport ? this.viewport : new $.Viewport({
|
|
collectionMode: true,
|
|
collectionTileSource: this.source,
|
|
containerSize: THIS[ this.hash ].prevContainerSize,
|
|
contentSize: this.source.dimensions,
|
|
springStiffness: this.springStiffness,
|
|
animationTime: this.animationTime,
|
|
minZoomImageRatio: 1,
|
|
maxZoomPixelRatio: 1
|
|
});
|
|
}else{
|
|
if( source ){
|
|
this.source = source;
|
|
}
|
|
this.viewport = this.viewport ? this.viewport : new $.Viewport({
|
|
containerSize: THIS[ this.hash ].prevContainerSize,
|
|
contentSize: this.source.dimensions,
|
|
springStiffness: this.springStiffness,
|
|
animationTime: this.animationTime,
|
|
minZoomImageRatio: this.minZoomImageRatio,
|
|
maxZoomPixelRatio: this.maxZoomPixelRatio,
|
|
visibilityRatio: this.visibilityRatio,
|
|
wrapHorizontal: this.wrapHorizontal,
|
|
wrapVertical: this.wrapVertical
|
|
});
|
|
}
|
|
|
|
if( this.preserveVewport ){
|
|
|
|
this.viewport.resetContentSize( this.source.dimensions );
|
|
|
|
} else if( this.defaultZoomLevel || this.collectionMode ){
|
|
|
|
if( this.collectionMode ){
|
|
/*this.viewport.zoomTo(
|
|
( this.viewport.getMaxZoom() + this.viewport.getMaxZoom())/ 2,
|
|
this.viewport.getCenter(),
|
|
true
|
|
);*/
|
|
}else{
|
|
this.viewport.zoomTo(
|
|
this.defaultZoomLevel,
|
|
this.viewport.getCenter(),
|
|
true
|
|
);
|
|
}
|
|
}
|
|
|
|
this.drawer = new $.Drawer({
|
|
source: this.source,
|
|
viewport: this.viewport,
|
|
element: this.canvas,
|
|
overlays: this.overlays,
|
|
maxImageCacheCount: this.maxImageCacheCount,
|
|
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
|
|
});
|
|
|
|
//Instantiate a navigator if configured
|
|
if ( this.showNavigator && ! this.navigator ){
|
|
this.navigator = new $.Navigator({
|
|
id: this.navigatorElement,
|
|
position: this.navigatorPosition,
|
|
sizeRatio: this.navigatorSizeRatio,
|
|
height: this.navigatorHeight,
|
|
width: this.navigatorWidth,
|
|
tileSources: this.tileSources,
|
|
tileHost: this.tileHost,
|
|
prefixUrl: this.prefixUrl,
|
|
overlays: this.overlays,
|
|
viewer: this
|
|
});
|
|
}
|
|
|
|
//Instantiate a referencestrip if configured
|
|
if ( this.showReferenceStrip && ! this.referenceStrip ){
|
|
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,
|
|
overlays: this.overlays,
|
|
viewer: this
|
|
});
|
|
}
|
|
|
|
//this.profiler = new $.Profiler();
|
|
|
|
THIS[ this.hash ].animating = false;
|
|
THIS[ this.hash ].forceRedraw = true;
|
|
scheduleUpdate( this, updateMulti );
|
|
|
|
//Assuming you had programatically created a bunch of overlays
|
|
//and added them via configuration
|
|
for ( i = 0; i < this.overlayControls.length; i++ ) {
|
|
|
|
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
|
|
);
|
|
|
|
}
|
|
}
|
|
VIEWERS[ this.hash ] = this;
|
|
this.raiseEvent( "open" );
|
|
|
|
if( this.navigator ){
|
|
this.navigator.open( source );
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @name OpenSeadragon.Viewer.prototype.close
|
|
* @return {OpenSeadragon.Viewer} Chainable.
|
|
*/
|
|
close: function ( ) {
|
|
this.source = null;
|
|
this.drawer = null;
|
|
this.viewport = this.preserveViewport ? this.viewport : null;
|
|
//this.profiler = null;
|
|
this.canvas.innerHTML = "";
|
|
|
|
VIEWERS[ this.hash ] = null;
|
|
delete VIEWERS[ this.hash ];
|
|
|
|
return this;
|
|
},
|
|
|
|
|
|
/**
|
|
* @function
|
|
* @name OpenSeadragon.Viewer.prototype.isMouseNavEnabled
|
|
* @return {Boolean}
|
|
*/
|
|
isMouseNavEnabled: function () {
|
|
return this.innerTracker.isTracking();
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @name OpenSeadragon.Viewer.prototype.setMouseNavEnabled
|
|
* @return {OpenSeadragon.Viewer} Chainable.
|
|
*/
|
|
setMouseNavEnabled: function( enabled ){
|
|
this.innerTracker.setTracking( enabled );
|
|
return this;
|
|
},
|
|
|
|
|
|
/**
|
|
* @function
|
|
* @name OpenSeadragon.Viewer.prototype.areControlsEnabled
|
|
* @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;
|
|
},
|
|
|
|
|
|
/**
|
|
* @function
|
|
* @name OpenSeadragon.Viewer.prototype.setDashboardEnabled
|
|
* @return {OpenSeadragon.Viewer} Chainable.
|
|
*/
|
|
setControlsEnabled: function( enabled ) {
|
|
if( enabled ){
|
|
abortControlsAutoHide( this );
|
|
} else {
|
|
beginControlsAutoHide( this );
|
|
};
|
|
},
|
|
|
|
|
|
/**
|
|
* @function
|
|
* @name OpenSeadragon.Viewer.prototype.isFullPage
|
|
* @return {Boolean}
|
|
*/
|
|
isFullPage: function () {
|
|
return this.element.parentNode == document.body;
|
|
},
|
|
|
|
|
|
/**
|
|
* Toggle full page mode.
|
|
* @function
|
|
* @name OpenSeadragon.Viewer.prototype.setFullPage
|
|
* @param {Boolean} fullPage
|
|
* If true, enter full page mode. If false, exit full page mode.
|
|
* @return {OpenSeadragon.Viewer} Chainable.
|
|
*/
|
|
setFullPage: function( fullPage ) {
|
|
|
|
var body = document.body,
|
|
bodyStyle = body.style,
|
|
docStyle = document.documentElement.style,
|
|
containerStyle = this.element.style,
|
|
canvasStyle = this.canvas.style,
|
|
oldBounds,
|
|
newBounds,
|
|
viewer,
|
|
hash,
|
|
nodes,
|
|
i;
|
|
|
|
//dont bother modifying the DOM if we are already in full page mode.
|
|
if ( fullPage == this.isFullPage() ) {
|
|
return;
|
|
}
|
|
|
|
if ( fullPage ) {
|
|
|
|
this.bodyOverflow = bodyStyle.overflow;
|
|
this.docOverflow = docStyle.overflow;
|
|
bodyStyle.overflow = "hidden";
|
|
docStyle.overflow = "hidden";
|
|
|
|
this.bodyWidth = bodyStyle.width;
|
|
this.bodyHeight = bodyStyle.height;
|
|
bodyStyle.width = "100%";
|
|
bodyStyle.height = "100%";
|
|
|
|
canvasStyle.backgroundColor = "black";
|
|
canvasStyle.color = "white";
|
|
|
|
//containerStyle.position = "fixed";
|
|
|
|
//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.prevNextSibling;
|
|
THIS[ this.hash ].prevElementSize = $.getElementSize( this.element );
|
|
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
|
|
this.toolbar.element.setAttribute(
|
|
'class',
|
|
this.toolbar.element.className +" fullpage"
|
|
);
|
|
//this.toolbar.element.style.position = 'fixed';
|
|
|
|
//this.container.style.top = $.getElementSize(
|
|
// this.toolbar.element
|
|
//).y + 'px';
|
|
}
|
|
body.appendChild( this.element );
|
|
if( this.toolbar && this.toolbar.element ){
|
|
this.element.style.height = (
|
|
$.getWindowSize().y - $.getElementSize( this.toolbar.element ).y
|
|
) + 'px';
|
|
}else{
|
|
this.element.style.height = $.getWindowSize().y + 'px';
|
|
}
|
|
this.element.style.width = $.getWindowSize().x + 'px';
|
|
|
|
// mouse will be inside container now
|
|
$.delegate( this, onContainerEnter )();
|
|
|
|
|
|
} else {
|
|
|
|
bodyStyle.overflow = this.bodyOverflow;
|
|
docStyle.overflow = this.docOverflow;
|
|
|
|
bodyStyle.width = this.bodyWidth;
|
|
bodyStyle.height = this.bodyHeight;
|
|
|
|
canvasStyle.backgroundColor = "";
|
|
canvasStyle.color = "";
|
|
|
|
//containerStyle.position = "relative";
|
|
//containerStyle.zIndex = "";
|
|
|
|
body.removeChild( this.element );
|
|
nodes = this.previousBody.length;
|
|
for ( i = 0; i < nodes; i++ ){
|
|
body.appendChild( this.previousBody.shift() );
|
|
}
|
|
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
|
|
this.toolbar.element.setAttribute(
|
|
'class',
|
|
this.toolbar.element.className.replace('fullpage','')
|
|
);
|
|
//this.toolbar.element.style.position = 'relative';
|
|
this.toolbar.parentNode.insertBefore(
|
|
this.toolbar.element,
|
|
this.toolbar.nextSibling
|
|
);
|
|
delete this.toolbar.parentNode;
|
|
delete this.toolbar.nextSibling;
|
|
|
|
//this.container.style.top = 'auto';
|
|
}
|
|
|
|
this.element.style.height = THIS[ this.hash ].prevElementSize.y + 'px';
|
|
this.element.style.width = THIS[ this.hash ].prevElementSize.x + 'px';
|
|
|
|
// mouse will likely be outside now
|
|
$.delegate( this, onContainerExit )();
|
|
|
|
}
|
|
|
|
if ( this.viewport ) {
|
|
oldBounds = this.viewport.getBounds();
|
|
this.viewport.resize( THIS[ this.hash ].prevContainerSize );
|
|
newBounds = this.viewport.getBounds();
|
|
|
|
if ( fullPage ) {
|
|
THIS[ this.hash ].fsBoundsDelta = new $.Point(
|
|
newBounds.width / oldBounds.width,
|
|
newBounds.height / oldBounds.height
|
|
);
|
|
} else {
|
|
this.viewport.update();
|
|
this.viewport.zoomBy(
|
|
Math.max(
|
|
THIS[ this.hash ].fsBoundsDelta.x,
|
|
THIS[ this.hash ].fsBoundsDelta.y
|
|
),
|
|
null,
|
|
true
|
|
);
|
|
//Ensures that if multiple viewers are on a page, the viewers that
|
|
//were hidden during fullpage are 'reopened'
|
|
for( hash in VIEWERS ){
|
|
viewer = VIEWERS[ hash ];
|
|
if( viewer !== this && viewer != this.navigator ){
|
|
viewer.open( viewer.source );
|
|
if( viewer.navigator ){
|
|
viewer.navigator.open( viewer.source );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
THIS[ this.hash ].forceRedraw = true;
|
|
this.raiseEvent( "resize", this );
|
|
updateOnce( this );
|
|
|
|
}
|
|
return this;
|
|
},
|
|
|
|
|
|
/**
|
|
* @function
|
|
* @name OpenSeadragon.Viewer.prototype.isVisible
|
|
* @return {Boolean}
|
|
*/
|
|
isVisible: function () {
|
|
return this.container.style.visibility != "hidden";
|
|
},
|
|
|
|
|
|
/**
|
|
* @function
|
|
* @name OpenSeadragon.Viewer.prototype.setVisible
|
|
* @return {OpenSeadragon.Viewer} Chainable.
|
|
*/
|
|
setVisible: function( visible ){
|
|
this.container.style.visibility = visible ? "" : "hidden";
|
|
return this;
|
|
},
|
|
|
|
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,
|
|
buttons = [],
|
|
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
|
|
});
|
|
|
|
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,
|
|
$.ControlAnchor.BOTTOM_RIGHT
|
|
);
|
|
}else{
|
|
this.addControl(
|
|
this.pagingControl,
|
|
$.ControlAnchor.TOP_LEFT
|
|
);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
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 ),
|
|
onFullPageHandler = $.delegate( this, onFullPage ),
|
|
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 ){
|
|
//if we are binding to custom buttons then layout and
|
|
//grouping is the responsibility of the page author
|
|
useGroup = false;
|
|
}
|
|
|
|
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
|
|
}));
|
|
|
|
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
|
|
}));
|
|
|
|
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: onFullPageHandler,
|
|
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,
|
|
$.ControlAnchor.TOP_LEFT
|
|
);
|
|
}else{
|
|
this.addControl(
|
|
this.navControl,
|
|
$.ControlAnchor.TOP_LEFT
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
},
|
|
|
|
goToPage: function( page ){
|
|
//page is a 1 based index so normalize now
|
|
//page = page;
|
|
if( this.tileSources.length > page ){
|
|
|
|
THIS[ this.hash ].sequence = page;
|
|
|
|
if( this.nextButton ){
|
|
if( ( this.tileSources.length - 1 ) === page ){
|
|
//Disable next button
|
|
this.nextButton.disable();
|
|
} else {
|
|
this.nextButton.enable();
|
|
}
|
|
}
|
|
if( this.previousButton ){
|
|
if( page > 0 ){
|
|
//Enable previous button
|
|
this.previousButton.enable();
|
|
} else {
|
|
this.previousButton.disable();
|
|
}
|
|
}
|
|
|
|
this.openTileSource( this.tileSources[ page ] );
|
|
}
|
|
if( $.isFunction( this.onPageChange ) ){
|
|
this.onPageChange({
|
|
page: page,
|
|
viewer: this
|
|
});
|
|
}
|
|
if( this.referenceStrip ){
|
|
this.referenceStrip.setFocus( page );
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Schedulers provide the general engine for animation
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
function scheduleUpdate( viewer, updateFunc, prevUpdateTime ){
|
|
var currentTime,
|
|
targetTime,
|
|
deltaTime;
|
|
|
|
if ( THIS[ viewer.hash ].animating ) {
|
|
return window.setTimeout( function(){
|
|
updateFunc( viewer );
|
|
}, 1 );
|
|
}
|
|
|
|
currentTime = +new Date();
|
|
prevUpdateTime = prevUpdateTime ? prevUpdateTime : currentTime;
|
|
// 60 frames per second is ideal
|
|
targetTime = prevUpdateTime + 1000 / 60;
|
|
deltaTime = Math.max( 1, targetTime - currentTime );
|
|
|
|
return window.setTimeout( function(){
|
|
updateFunc( viewer );
|
|
}, 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.autoHideControls ) {
|
|
return;
|
|
}
|
|
viewer.controlsShouldFade = true;
|
|
viewer.controlsFadeBeginTime =
|
|
+new Date() +
|
|
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 );
|
|
}
|
|
};
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Default view event handlers.
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
function onFocus(){
|
|
abortControlsAutoHide( this );
|
|
};
|
|
|
|
function onBlur(){
|
|
beginControlsAutoHide( this );
|
|
|
|
};
|
|
|
|
function onCanvasClick( tracker, position, quick, shift ) {
|
|
var zoomPreClick,
|
|
factor;
|
|
if ( this.viewport && quick ) { // ignore clicks where mouse moved
|
|
zoomPerClick = this.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 ) {
|
|
if( !this.panHorizontal ){
|
|
delta.x = 0;
|
|
}
|
|
if( !this.panVertical ){
|
|
delta.y = 0;
|
|
}
|
|
this.viewport.panBy(
|
|
this.viewport.deltaPointsFromPixels(
|
|
delta.negate()
|
|
)
|
|
);
|
|
}
|
|
};
|
|
|
|
function onCanvasRelease( tracker, position, insideElementPress, insideElementRelease ) {
|
|
if ( insideElementPress && this.viewport ) {
|
|
this.viewport.applyConstraints();
|
|
}
|
|
};
|
|
|
|
function onCanvasScroll( tracker, position, scroll, shift ) {
|
|
var factor;
|
|
if ( this.viewport ) {
|
|
factor = Math.pow( this.zoomPerScroll, scroll );
|
|
this.viewport.zoomBy(
|
|
factor,
|
|
this.viewport.pointFromPixel( position, true )
|
|
);
|
|
this.viewport.applyConstraints();
|
|
}
|
|
//cancels event
|
|
return false;
|
|
};
|
|
|
|
function onContainerExit( tracker, position, buttonDownElement, buttonDownAny ) {
|
|
if ( !buttonDownElement ) {
|
|
THIS[ this.hash ].mouseInside = false;
|
|
if ( !THIS[ this.hash ].animating ) {
|
|
beginControlsAutoHide( this );
|
|
}
|
|
}
|
|
};
|
|
|
|
function onContainerRelease( tracker, position, insideElementPress, insideElementRelease ) {
|
|
if ( !insideElementRelease ) {
|
|
THIS[ this.hash ].mouseInside = false;
|
|
if ( !THIS[ this.hash ].animating ) {
|
|
beginControlsAutoHide( this );
|
|
}
|
|
}
|
|
};
|
|
|
|
function onContainerEnter( tracker, position, buttonDownElement, buttonDownAny ) {
|
|
THIS[ this.hash ].mouseInside = true;
|
|
abortControlsAutoHide( this );
|
|
};
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Page update routines ( aka Views - for future reference )
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
function updateMulti( viewer ) {
|
|
|
|
var beginTime;
|
|
|
|
if ( !viewer.source ) {
|
|
return;
|
|
}
|
|
|
|
beginTime = +new Date();
|
|
updateOnce( viewer );
|
|
scheduleUpdate( viewer, arguments.callee, beginTime );
|
|
};
|
|
|
|
function updateOnce( viewer ) {
|
|
|
|
var containerSize,
|
|
animated;
|
|
|
|
if ( !viewer.source ) {
|
|
return;
|
|
}
|
|
|
|
//viewer.profiler.beginUpdate();
|
|
|
|
containerSize = $.getElementSize( viewer.container );
|
|
if ( !containerSize.equals( THIS[ viewer.hash ].prevContainerSize ) ) {
|
|
// maintain image position
|
|
viewer.viewport.resize( containerSize, true );
|
|
THIS[ viewer.hash ].prevContainerSize = containerSize;
|
|
viewer.raiseEvent( "resize" );
|
|
}
|
|
|
|
animated = viewer.viewport.update();
|
|
|
|
if( viewer.referenceStrip ){
|
|
animated = viewer.referenceStrip.update( viewer.viewport ) || animated;
|
|
}
|
|
|
|
if ( !THIS[ viewer.hash ].animating && animated ) {
|
|
viewer.raiseEvent( "animationstart" );
|
|
abortControlsAutoHide( viewer );
|
|
}
|
|
|
|
if ( animated ) {
|
|
viewer.drawer.update();
|
|
if( viewer.navigator ){
|
|
viewer.navigator.update( viewer.viewport );
|
|
}
|
|
viewer.raiseEvent( "animation" );
|
|
} else if ( THIS[ viewer.hash ].forceRedraw || viewer.drawer.needsUpdate() ) {
|
|
viewer.drawer.update();
|
|
if( viewer.navigator ){
|
|
viewer.navigator.update( viewer.viewport );
|
|
}
|
|
THIS[ viewer.hash ].forceRedraw = false;
|
|
}
|
|
|
|
if ( THIS[ viewer.hash ].animating && !animated ) {
|
|
viewer.raiseEvent( "animationfinish" );
|
|
|
|
if ( !THIS[ viewer.hash ].mouseInside ) {
|
|
beginControlsAutoHide( viewer );
|
|
}
|
|
}
|
|
|
|
THIS[ viewer.hash ].animating = animated;
|
|
|
|
//viewer.profiler.endUpdate();
|
|
};
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Navigation Controls
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
function resolveUrl( prefix, url ) {
|
|
return prefix ? prefix + url : url;
|
|
};
|
|
|
|
|
|
|
|
function beginZoomingIn() {
|
|
THIS[ this.hash ].lastZoomTime = +new Date();
|
|
THIS[ this.hash ].zoomFactor = this.zoomPerSecond;
|
|
THIS[ this.hash ].zooming = true;
|
|
scheduleZoom( this );
|
|
};
|
|
|
|
|
|
function beginZoomingOut() {
|
|
THIS[ this.hash ].lastZoomTime = +new Date();
|
|
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 ) {
|
|
window.setTimeout( $.delegate( viewer, doZoom ), 10 );
|
|
};
|
|
|
|
|
|
function doZoom() {
|
|
var currentTime,
|
|
deltaTime,
|
|
adjustFactor;
|
|
|
|
if ( THIS[ this.hash ].zooming && this.viewport) {
|
|
currentTime = +new Date();
|
|
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 onFullPage() {
|
|
this.setFullPage( !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();
|
|
}
|
|
};
|
|
|
|
|
|
function onPrevious(){
|
|
var previous = THIS[ this.hash ].sequence - 1;
|
|
this.goToPage( previous );
|
|
};
|
|
|
|
|
|
function onNext(){
|
|
var next = THIS[ this.hash ].sequence + 1;
|
|
this.goToPage( next );
|
|
};
|
|
|
|
|
|
}( OpenSeadragon ));
|
|
(function( $ ){
|
|
|
|
/**
|
|
* The Navigator provides a small view of the current image as fixed
|
|
* while representing the viewport as a moving box serving as a frame
|
|
* of reference in the larger viewport as to which portion of the image
|
|
* is currently being examined. The navigators viewport can be interacted
|
|
* with using the keyboard or the mouse.
|
|
* @class
|
|
* @name OpenSeadragon.Navigator
|
|
* @extends OpenSeadragon.Viewer
|
|
* @extends OpenSeadragon.EventHandler
|
|
* @param {Object} options
|
|
* @param {String} options.viewerId
|
|
*/
|
|
$.Navigator = function( options ){
|
|
|
|
var _this = this,
|
|
viewer = options.viewer,
|
|
viewerSize = $.getElementSize( viewer.element );
|
|
|
|
//We may need to create a new element and id if they did not
|
|
//provide the id for the existing element
|
|
if( !options.id ){
|
|
options.id = 'navigator-' + (+new Date());
|
|
this.element = $.makeNeutralElement( "div" );
|
|
this.element.id = options.id;
|
|
this.element.className = 'navigator';
|
|
}
|
|
|
|
options = $.extend( true, {
|
|
navigatorSizeRatio: $.DEFAULT_SETTINGS.navigatorSizeRatio
|
|
}, options, {
|
|
element: this.element,
|
|
//These need to be overridden to prevent recursion since
|
|
//the navigator is a viewer and a viewer has a navigator
|
|
showNavigator: false,
|
|
mouseNavEnabled: false,
|
|
showNavigationControl: false,
|
|
showSequenceControl: false
|
|
});
|
|
|
|
options.minPixelRatio = this.minPixelRatio = viewer.minPixelRatio;
|
|
|
|
(function( style ){
|
|
style.marginTop = '0px';
|
|
style.marginRight = '0px';
|
|
style.marginBottom = '0px';
|
|
style.marginLeft = '0px';
|
|
style.border = '2px solid #555';
|
|
style.background = '#000';
|
|
style.opacity = 0.8;
|
|
style.overflow = 'hidden';
|
|
}( this.element.style ));
|
|
|
|
this.displayRegion = $.makeNeutralElement( "textarea" );
|
|
this.displayRegion.id = this.element.id + '-displayregion';
|
|
this.displayRegion.className = 'displayregion';
|
|
|
|
(function( style ){
|
|
style.position = 'relative';
|
|
style.top = '0px';
|
|
style.left = '0px';
|
|
style.fontSize = '0px';
|
|
style.overflow = 'hidden';
|
|
style.border = '2px solid #900';
|
|
//TODO: IE doesnt like this property being set
|
|
//try{ style.outline = '2px auto #909'; }catch(e){/*ignore*/}
|
|
style.background = 'transparent';
|
|
style.float = 'left'; //Webkit
|
|
style.cssFloat = 'left'; //Firefox
|
|
style.styleFloat = 'left'; //IE
|
|
style.zIndex = 999999999;
|
|
style.cursor = 'default';
|
|
}( this.displayRegion.style ));
|
|
|
|
this.element.innerTracker = new $.MouseTracker({
|
|
element: this.element,
|
|
scrollHandler: function(){
|
|
//dont scroll the page up and down if the user is scrolling
|
|
//in the navigator
|
|
return false;
|
|
}
|
|
}).setTracking( true );
|
|
|
|
this.displayRegion.innerTracker = new $.MouseTracker({
|
|
element: this.displayRegion,
|
|
clickTimeThreshold: this.clickTimeThreshold,
|
|
clickDistThreshold: this.clickDistThreshold,
|
|
clickHandler: $.delegate( this, onCanvasClick ),
|
|
dragHandler: $.delegate( this, onCanvasDrag ),
|
|
releaseHandler: $.delegate( this, onCanvasRelease ),
|
|
scrollHandler: $.delegate( this, onCanvasScroll ),
|
|
focusHandler: function(){
|
|
var point = $.getElementPosition( _this.viewer.element );
|
|
|
|
window.scrollTo( 0, point.y );
|
|
|
|
_this.viewer.setControlsEnabled( true );
|
|
(function( style ){
|
|
style.border = '2px solid #437AB2';
|
|
//style.outline = '2px auto #437AB2';
|
|
}( this.element.style ));
|
|
|
|
},
|
|
blurHandler: function(){
|
|
_this.viewer.setControlsEnabled( false );
|
|
(function( style ){
|
|
style.border = '2px solid #900';
|
|
//style.outline = '2px auto #900';
|
|
}( this.element.style ));
|
|
},
|
|
keyHandler: function(tracker, keyCode, shiftKey){
|
|
//console.log( keyCode );
|
|
switch( keyCode ){
|
|
case 61://=|+
|
|
_this.viewer.viewport.zoomBy(1.1);
|
|
_this.viewer.viewport.applyConstraints();
|
|
return false;
|
|
case 45://-|_
|
|
_this.viewer.viewport.zoomBy(0.9);
|
|
_this.viewer.viewport.applyConstraints();
|
|
return false;
|
|
case 48://0|)
|
|
_this.viewer.viewport.goHome();
|
|
_this.viewer.viewport.applyConstraints();
|
|
return false;
|
|
case 119://w
|
|
case 87://W
|
|
case 38://up arrow
|
|
shiftKey ?
|
|
_this.viewer.viewport.zoomBy(1.1):
|
|
_this.viewer.viewport.panBy(new $.Point(0, -0.05));
|
|
_this.viewer.viewport.applyConstraints();
|
|
return false;
|
|
case 115://s
|
|
case 83://S
|
|
case 40://down arrow
|
|
shiftKey ?
|
|
_this.viewer.viewport.zoomBy(0.9):
|
|
_this.viewer.viewport.panBy(new $.Point(0, 0.05));
|
|
_this.viewer.viewport.applyConstraints();
|
|
return false;
|
|
case 97://a
|
|
case 37://left arrow
|
|
_this.viewer.viewport.panBy(new $.Point(-0.05, 0));
|
|
_this.viewer.viewport.applyConstraints();
|
|
return false;
|
|
case 100://d
|
|
case 39://right arrow
|
|
_this.viewer.viewport.panBy(new $.Point(0.05, 0));
|
|
_this.viewer.viewport.applyConstraints();
|
|
return false;
|
|
default:
|
|
//console.log( 'navigator keycode %s', keyCode );
|
|
return true;
|
|
}
|
|
}
|
|
}).setTracking( true ); // default state
|
|
|
|
/*this.displayRegion.outerTracker = new $.MouseTracker({
|
|
element: this.container,
|
|
clickTimeThreshold: this.clickTimeThreshold,
|
|
clickDistThreshold: this.clickDistThreshold,
|
|
enterHandler: $.delegate( this, onContainerEnter ),
|
|
exitHandler: $.delegate( this, onContainerExit ),
|
|
releaseHandler: $.delegate( this, onContainerRelease )
|
|
}).setTracking( this.mouseNavEnabled ? true : false ); // always tracking*/
|
|
|
|
|
|
viewer.addControl(
|
|
this.element,
|
|
$.ControlAnchor.TOP_RIGHT
|
|
);
|
|
|
|
if( options.width && options.height ){
|
|
this.element.style.width = options.width + 'px';
|
|
this.element.style.height = options.height + 'px';
|
|
} else {
|
|
this.element.style.width = ( viewerSize.x * options.navigatorSizeRatio ) + 'px';
|
|
this.element.style.height = ( viewerSize.y * options.navigatorSizeRatio ) + 'px';
|
|
}
|
|
|
|
$.Viewer.apply( this, [ options ] );
|
|
|
|
this.element.getElementsByTagName('form')[0].appendChild( this.displayRegion );
|
|
|
|
};
|
|
|
|
$.extend( $.Navigator.prototype, $.EventHandler.prototype, $.Viewer.prototype, {
|
|
|
|
/**
|
|
* @function
|
|
* @name OpenSeadragon.Navigator.prototype.update
|
|
*/
|
|
update: function( viewport ){
|
|
|
|
var bounds,
|
|
topleft,
|
|
bottomright;
|
|
|
|
if( viewport && this.viewport ){
|
|
bounds = viewport.getBounds( true );
|
|
topleft = this.viewport.pixelFromPoint( bounds.getTopLeft() );
|
|
bottomright = this.viewport.pixelFromPoint( bounds.getBottomRight() );
|
|
|
|
//update style for navigator-box
|
|
(function(style){
|
|
|
|
style.top = topleft.y + 'px';
|
|
style.left = topleft.x + 'px';
|
|
style.width = ( Math.abs( topleft.x - bottomright.x ) - 3 ) + 'px';
|
|
style.height = ( Math.abs( topleft.y - bottomright.y ) - 3 ) + 'px';
|
|
|
|
}( this.displayRegion.style ));
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
*/
|
|
function onCanvasClick( tracker, position, quick, shift ) {
|
|
this.displayRegion.focus();
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
*/
|
|
function onCanvasDrag( tracker, position, delta, shift ) {
|
|
if ( this.viewer.viewport ) {
|
|
if( !this.panHorizontal ){
|
|
delta.x = 0;
|
|
}
|
|
if( !this.panVertical ){
|
|
delta.y = 0;
|
|
}
|
|
this.viewer.viewport.panBy(
|
|
this.viewport.deltaPointsFromPixels(
|
|
delta
|
|
)
|
|
);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
*/
|
|
function onCanvasRelease( tracker, position, insideElementPress, insideElementRelease ) {
|
|
if ( insideElementPress && this.viewer.viewport ) {
|
|
this.viewer.viewport.applyConstraints();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
*/
|
|
function onCanvasScroll( tracker, position, scroll, shift ) {
|
|
var factor;
|
|
if ( this.viewer.viewport ) {
|
|
factor = Math.pow( this.zoomPerScroll, scroll );
|
|
this.viewer.viewport.zoomBy(
|
|
factor,
|
|
//this.viewport.pointFromPixel( position, true )
|
|
this.viewport.getCenter()
|
|
);
|
|
this.viewer.viewport.applyConstraints();
|
|
}
|
|
//cancels event
|
|
return false;
|
|
};
|
|
|
|
|
|
}( OpenSeadragon ));
|
|
(function( $ ){
|
|
|
|
//TODO: I guess this is where the i18n needs to be reimplemented. I'll look
|
|
// into existing patterns for i18n in javascript but i think that mimicking
|
|
// pythons gettext might be a reasonable approach.
|
|
var I18N = {
|
|
Errors: {
|
|
Failure: "Sorry, but Seadragon Ajax can't run on your browser!\n" +
|
|
"Please try using IE 7 or Firefox 3.\n",
|
|
Dzc: "Sorry, we don't support Deep Zoom Collections!",
|
|
Dzi: "Hmm, this doesn't appear to be a valid Deep Zoom Image.",
|
|
Xml: "Hmm, this doesn't appear to be a valid Deep Zoom Image.",
|
|
Empty: "You asked us to open nothing, so we did just that.",
|
|
ImageFormat: "Sorry, we don't support {0}-based Deep Zoom Images.",
|
|
Security: "It looks like a security restriction stopped us from " +
|
|
"loading this Deep Zoom Image.",
|
|
Status: "This space unintentionally left blank ({0} {1}).",
|
|
Unknown: "Whoops, something inexplicably went wrong. Sorry!"
|
|
},
|
|
|
|
Messages: {
|
|
Loading: "Loading..."
|
|
},
|
|
|
|
Tooltips: {
|
|
FullPage: "Toggle full page",
|
|
Home: "Go home",
|
|
ZoomIn: "Zoom in",
|
|
ZoomOut: "Zoom out",
|
|
NextPage: "Next page",
|
|
PreviousPage: "Previous page"
|
|
}
|
|
};
|
|
|
|
$.extend( $, {
|
|
|
|
/**
|
|
* @function
|
|
* @name OpenSeadragon.getString
|
|
* @param {String} property
|
|
*/
|
|
getString: function( prop ) {
|
|
|
|
var props = prop.split('.'),
|
|
string = null,
|
|
args = arguments,
|
|
i;
|
|
|
|
for ( i = 0; i < props.length; i++ ) {
|
|
// in case not a subproperty
|
|
string = I18N[ props[ i ] ] || {};
|
|
}
|
|
|
|
if ( typeof( string ) != "string" ) {
|
|
string = "";
|
|
}
|
|
|
|
return string.replace(/\{\d+\}/g, function(capture) {
|
|
var i = parseInt( capture.match( /\d+/ ) ) + 1;
|
|
return i < args.length ?
|
|
args[ i ] :
|
|
"";
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @name OpenSeadragon.setString
|
|
* @param {String} property
|
|
* @param {*} value
|
|
*/
|
|
setString: function( prop, value ) {
|
|
|
|
var props = prop.split('.'),
|
|
container = $.Strings,
|
|
i;
|
|
|
|
for ( i = 0; i < props.length - 1; i++ ) {
|
|
if ( !container[ props[ i ] ] ) {
|
|
container[ props[ i ] ] = {};
|
|
}
|
|
container = container[ props[ i ] ];
|
|
}
|
|
|
|
container[ props[ i ] ] = value;
|
|
}
|
|
|
|
});
|
|
|
|
}( OpenSeadragon ));
|
|
|
|
(function( $ ){
|
|
|
|
/**
|
|
* A Point is really used as a 2-dimensional vector, equally useful for
|
|
* representing a point on a plane, or the height and width of a plane
|
|
* not requiring any other frame of reference.
|
|
* @class
|
|
* @param {Number} [x] The vector component 'x'. Defaults to the origin at 0.
|
|
* @param {Number} [y] The vector component 'y'. Defaults to the origin at 0.
|
|
* @property {Number} [x] The vector component 'x'.
|
|
* @property {Number} [y] The vector component 'y'.
|
|
*/
|
|
$.Point = function( x, y ) {
|
|
this.x = typeof ( x ) == "number" ? x : 0;
|
|
this.y = typeof ( y ) == "number" ? y : 0;
|
|
};
|
|
|
|
$.Point.prototype = {
|
|
|
|
/**
|
|
* Add another Point to this point and return a new Point.
|
|
* @function
|
|
* @param {OpenSeadragon.Point} point The point to add vector components.
|
|
* @returns {OpenSeadragon.Point} A new point representing the sum of the
|
|
* vector components
|
|
*/
|
|
plus: function( point ) {
|
|
return new $.Point(
|
|
this.x + point.x,
|
|
this.y + point.y
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Add another Point to this point and return a new Point.
|
|
* @function
|
|
* @param {OpenSeadragon.Point} point The point to add vector components.
|
|
* @returns {OpenSeadragon.Point} A new point representing the sum of the
|
|
* vector components
|
|
*/
|
|
minus: function( point ) {
|
|
return new $.Point(
|
|
this.x - point.x,
|
|
this.y - point.y
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Add another Point to this point and return a new Point.
|
|
* @function
|
|
* @param {OpenSeadragon.Point} point The point to add vector components.
|
|
* @returns {OpenSeadragon.Point} A new point representing the sum of the
|
|
* vector components
|
|
*/
|
|
times: function( factor ) {
|
|
return new $.Point(
|
|
this.x * factor,
|
|
this.y * factor
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Add another Point to this point and return a new Point.
|
|
* @function
|
|
* @param {OpenSeadragon.Point} point The point to add vector components.
|
|
* @returns {OpenSeadragon.Point} A new point representing the sum of the
|
|
* vector components
|
|
*/
|
|
divide: function( factor ) {
|
|
return new $.Point(
|
|
this.x / factor,
|
|
this.y / factor
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Add another Point to this point and return a new Point.
|
|
* @function
|
|
* @param {OpenSeadragon.Point} point The point to add vector components.
|
|
* @returns {OpenSeadragon.Point} A new point representing the sum of the
|
|
* vector components
|
|
*/
|
|
negate: function() {
|
|
return new $.Point( -this.x, -this.y );
|
|
},
|
|
|
|
/**
|
|
* Add another Point to this point and return a new Point.
|
|
* @function
|
|
* @param {OpenSeadragon.Point} point The point to add vector components.
|
|
* @returns {OpenSeadragon.Point} A new point representing the sum of the
|
|
* vector components
|
|
*/
|
|
distanceTo: function( point ) {
|
|
return Math.sqrt(
|
|
Math.pow( this.x - point.x, 2 ) +
|
|
Math.pow( this.y - point.y, 2 )
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Add another Point to this point and return a new Point.
|
|
* @function
|
|
* @param {OpenSeadragon.Point} point The point to add vector components.
|
|
* @returns {OpenSeadragon.Point} A new point representing the sum of the
|
|
* vector components
|
|
*/
|
|
apply: function( func ) {
|
|
return new $.Point( func( this.x ), func( this.y ) );
|
|
},
|
|
|
|
/**
|
|
* Add another Point to this point and return a new Point.
|
|
* @function
|
|
* @param {OpenSeadragon.Point} point The point to add vector components.
|
|
* @returns {OpenSeadragon.Point} A new point representing the sum of the
|
|
* vector components
|
|
*/
|
|
equals: function( point ) {
|
|
return (
|
|
point instanceof $.Point
|
|
) && (
|
|
this.x === point.x
|
|
) && (
|
|
this.y === point.y
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Add another Point to this point and return a new Point.
|
|
* @function
|
|
* @param {OpenSeadragon.Point} point The point to add vector components.
|
|
* @returns {OpenSeadragon.Point} A new point representing the sum of the
|
|
* vector components
|
|
*/
|
|
toString: function() {
|
|
return "(" + Math.round(this.x) + "," + Math.round(this.y) + ")";
|
|
}
|
|
};
|
|
|
|
}( OpenSeadragon ));
|
|
|
|
(function( $ ){
|
|
|
|
|
|
/**
|
|
* The TileSource contains the most basic implementation required to create a
|
|
* smooth transition between layer in an image pyramid. It has only a single key
|
|
* interface that must be implemented to complete it key functionality:
|
|
* 'getTileUrl'. It also has several optional interfaces that can be
|
|
* implemented if a new TileSource wishes to support configuration via a simple
|
|
* object or array ('configure') and if the tile source supports or requires
|
|
* configuration via retreival of a document on the network ala AJAX or JSONP,
|
|
* ('getImageInfo').
|
|
* <br/>
|
|
* By default the image pyramid is split into N layers where the images longest
|
|
* side in M (in pixels), where N is the smallest integer which satisfies
|
|
* <strong>2^(N+1) >= M</strong>.
|
|
* @class
|
|
* @extends OpenSeadragon.EventHandler
|
|
* @param {Number|Object|Array|String} width
|
|
* If more than a single argument is supplied, the traditional use of
|
|
* positional parameters is supplied and width is expected to be the width
|
|
* source image at it's max resolution in pixels. If a single argument is supplied and
|
|
* it is an Object or Array, the construction is assumed to occur through
|
|
* the extending classes implementation of 'configure'. Finally if only a
|
|
* single argument is supplied and it is a String, the extending class is
|
|
* expected to implement 'getImageInfo' and 'configure'.
|
|
* @param {Number} height
|
|
* Width of the source image at max resolution in pixels.
|
|
* @param {Number} tileSize
|
|
* The size of the tiles to assumed to make up each pyramid layer in pixels.
|
|
* Tile size determines the point at which the image pyramid must be
|
|
* divided into a matrix of smaller images.
|
|
* @param {Number} tileOverlap
|
|
* The number of pixels each tile is expected to overlap touching tiles.
|
|
* @param {Number} minLevel
|
|
* The minimum level to attempt to load.
|
|
* @param {Number} maxLevel
|
|
* The maximum level to attempt to load.
|
|
* @property {Number} aspectRatio
|
|
* Ratio of width to height
|
|
* @property {OpenSeadragon.Point} dimensions
|
|
* Vector storing x and y dimensions ( width and height respectively ).
|
|
* @property {Number} tileSize
|
|
* The size of the image tiles used to compose the image.
|
|
* @property {Number} tileOverlap
|
|
* The overlap in pixels each tile shares with it's adjacent neighbors.
|
|
* @property {Number} minLevel
|
|
* The minimum pyramid level this tile source supports or should attempt to load.
|
|
* @property {Number} maxLevel
|
|
* The maximum pyramid level this tile source supports or should attempt to load.
|
|
*/
|
|
$.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLevel ) {
|
|
var _this = this,
|
|
callback = null,
|
|
readyHandler = null,
|
|
args = arguments,
|
|
options,
|
|
i;
|
|
|
|
if( $.isPlainObject( width ) ){
|
|
options = width;
|
|
}else{
|
|
options = {
|
|
width: args[0],
|
|
height: args[1],
|
|
tileSize: args[2],
|
|
tileOverlap: args[3],
|
|
minlevel: args[4],
|
|
maxLevel: args[5]
|
|
};
|
|
}
|
|
|
|
//Tile sources supply some events, namely 'ready' when they must be configured
|
|
//by asyncronously fetching their configuration data.
|
|
$.EventHandler.call( this );
|
|
|
|
//we allow options to override anything we dont treat as
|
|
//required via idiomatic options or which is functionally
|
|
//set depending on the state of the readiness of this tile
|
|
//source
|
|
$.extend( true, this, options );
|
|
|
|
//Any functions that are passed as arguments are bound to the ready callback
|
|
for( i = 0; i < arguments.length; i++ ){
|
|
if( $.isFunction( arguments[i] ) ){
|
|
callback = arguments[ i ];
|
|
this.addHandler( 'ready', function( placeHolderSource, readySource ){
|
|
callback( readySource );
|
|
});
|
|
//only one callback per constructor
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( 'string' == $.type( arguments[ 0 ] ) ){
|
|
//in case the getImageInfo method is overriden and/or implies an
|
|
//async mechanism set some safe defaults first
|
|
this.aspectRatio = 1;
|
|
this.dimensions = new $.Point( 10, 10 );
|
|
this.tileSize = 0;
|
|
this.tileOverlap = 0;
|
|
this.minLevel = 0;
|
|
this.maxLevel = 0;
|
|
this.ready = false;
|
|
//configuration via url implies the extending class
|
|
//implements and 'configure'
|
|
this.getImageInfo( arguments[ 0 ] );
|
|
|
|
} else {
|
|
|
|
//explicit configuration via positional args in constructor
|
|
//or the more idiomatic 'options' object
|
|
this.ready = true;
|
|
this.aspectRatio = ( options.width && options.height ) ?
|
|
( options.width / options.height ) : 1;
|
|
this.dimensions = new $.Point( options.width, options.height );
|
|
this.tileSize = options.tileSize ? options.tileSize : 0;
|
|
this.tileOverlap = options.tileOverlap ? options.tileOverlap : 0;
|
|
this.minLevel = options.minLevel ? options.minLevel : 0;
|
|
this.maxLevel = ( undefined !== options.maxLevel && null !== options.maxLevel ) ?
|
|
options.maxLevel : (
|
|
( options.width && options.height ) ? Math.ceil(
|
|
Math.log( Math.max( options.width, options.height ) ) /
|
|
Math.log( 2 )
|
|
) : 0
|
|
);
|
|
if( callback && $.isFunction( callback ) ){
|
|
callback( this );
|
|
}
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
$.TileSource.prototype = {
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} level
|
|
*/
|
|
getLevelScale: function( level ) {
|
|
return 1 / ( 1 << ( this.maxLevel - level ) );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} level
|
|
*/
|
|
getNumTiles: function( level ) {
|
|
var scale = this.getLevelScale( level ),
|
|
x = Math.ceil( scale * this.dimensions.x / this.tileSize ),
|
|
y = Math.ceil( scale * this.dimensions.y / this.tileSize );
|
|
|
|
return new $.Point( x, y );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} level
|
|
*/
|
|
getPixelRatio: function( level ) {
|
|
var imageSizeScaled = this.dimensions.times( this.getLevelScale( level ) ),
|
|
rx = 1.0 / imageSizeScaled.x,
|
|
ry = 1.0 / imageSizeScaled.y;
|
|
|
|
return new $.Point(rx, ry);
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} level
|
|
* @param {OpenSeadragon.Point} point
|
|
*/
|
|
getTileAtPoint: function( level, point ) {
|
|
var pixel = point.times( this.dimensions.x ).times( this.getLevelScale(level ) ),
|
|
tx = Math.floor( pixel.x / this.tileSize ),
|
|
ty = Math.floor( pixel.y / this.tileSize );
|
|
|
|
return new $.Point( tx, ty );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} level
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
getTileBounds: function( level, x, y ) {
|
|
var dimensionsScaled = this.dimensions.times( this.getLevelScale( level ) ),
|
|
px = ( x === 0 ) ? 0 : this.tileSize * x - this.tileOverlap,
|
|
py = ( y === 0 ) ? 0 : this.tileSize * y - this.tileOverlap,
|
|
sx = this.tileSize + ( x === 0 ? 1 : 2 ) * this.tileOverlap,
|
|
sy = this.tileSize + ( y === 0 ? 1 : 2 ) * this.tileOverlap,
|
|
scale = 1.0 / dimensionsScaled.x;
|
|
|
|
sx = Math.min( sx, dimensionsScaled.x - px );
|
|
sy = Math.min( sy, dimensionsScaled.y - py );
|
|
|
|
return new $.Rect( px * scale, py * scale, sx * scale, sy * scale );
|
|
},
|
|
|
|
|
|
/**
|
|
* Responsible for retrieving, and caching the
|
|
* image metadata pertinent to this TileSources implementation.
|
|
* @function
|
|
* @param {String} url
|
|
* @throws {Error}
|
|
*/
|
|
getImageInfo: function( url ) {
|
|
var _this = this,
|
|
url = url,
|
|
error,
|
|
callbackName,
|
|
callback,
|
|
readySource,
|
|
options,
|
|
urlParts,
|
|
filename,
|
|
lastDot,
|
|
tilesUrl;
|
|
|
|
|
|
if( url ) {
|
|
urlParts = url.split( '/' );
|
|
filename = urlParts[ urlParts.length - 1 ];
|
|
lastDot = filename.lastIndexOf( '.' );
|
|
if ( lastDot > -1 ) {
|
|
urlParts[ urlParts.length - 1 ] = filename.slice( 0, lastDot );
|
|
}
|
|
}
|
|
|
|
callback = function( data ){
|
|
var $TileSource = $.TileSource.determineType( _this, data, url );
|
|
options = $TileSource.prototype.configure.apply( _this, [ data, url ]);
|
|
readySource = new $TileSource( options );
|
|
_this.ready = true;
|
|
_this.raiseEvent( 'ready', readySource );
|
|
};
|
|
|
|
if( url.match(/\.js$/) ){
|
|
//TODO: Its not very flexible to require tile sources to end jsonp
|
|
// request for info with a url that ends with '.js' but for
|
|
// now it's the only way I see to distinguish uniformly.
|
|
callbackName = url.split( '/' ).pop().replace('.js','');
|
|
$.jsonp({
|
|
url: url,
|
|
async: false,
|
|
callbackName: callbackName,
|
|
callback: callback
|
|
});
|
|
} else {
|
|
//TODO: struggling a little with TileSource rewrite to make info
|
|
// requests work asyncronously. For now I'm opting to make
|
|
// all xhr info request syncronous.
|
|
$.makeAjaxRequest( url, function( xhr ) {
|
|
var data = processResponse( xhr );
|
|
callback( data );
|
|
});
|
|
}
|
|
|
|
},
|
|
|
|
/**
|
|
* Responsible determining if a the particular TileSource supports the
|
|
* data format ( and allowed to apply logic against the url the data was
|
|
* loaded from, if any ). Overriding implementations are expected to do
|
|
* something smart with data and / or url to determine support. Also
|
|
* understand that iteration order of TileSources is not guarunteed so
|
|
* please make sure your data or url is expressive enough to ensure a simple
|
|
* and sufficient mechanisim for clear determination.
|
|
* @function
|
|
* @param {String|Object|Array|Document} data
|
|
* @param {String} url - the url the data was loaded
|
|
* from if any.
|
|
* @return {Boolean}
|
|
*/
|
|
supports: function( data, url ) {
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Responsible for parsing and configuring the
|
|
* image metadata pertinent to this TileSources implementation.
|
|
* This method is not implemented by this class other than to throw an Error
|
|
* announcing you have to implement it. Because of the variety of tile
|
|
* server technologies, and various specifications for building image
|
|
* pyramids, this method is here to allow easy integration.
|
|
* @function
|
|
* @param {String|Object|Array|Document} data
|
|
* @param {String} url - the url the data was loaded
|
|
* from if any.
|
|
* @return {Object} options - A dictionary of keyword arguments sufficient
|
|
* to configure this tile sources constructor.
|
|
* @throws {Error}
|
|
*/
|
|
configure: function( data, url ) {
|
|
throw new Error( "Method not implemented." );
|
|
},
|
|
|
|
/**
|
|
* Responsible for retriving the url which will return an image for the
|
|
* region speified by the given x, y, and level components.
|
|
* This method is not implemented by this class other than to throw an Error
|
|
* announcing you have to implement it. Because of the variety of tile
|
|
* server technologies, and various specifications for building image
|
|
* pyramids, this method is here to allow easy integration.
|
|
* @function
|
|
* @param {Number} level
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @throws {Error}
|
|
*/
|
|
getTileUrl: function( level, x, y ) {
|
|
throw new Error( "Method not implemented." );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} level
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
tileExists: function( level, x, y ) {
|
|
var numTiles = this.getNumTiles( level );
|
|
return level >= this.minLevel &&
|
|
level <= this.maxLevel &&
|
|
x >= 0 &&
|
|
y >= 0 &&
|
|
x < numTiles.x &&
|
|
y < numTiles.y;
|
|
}
|
|
};
|
|
|
|
|
|
$.extend( true, $.TileSource.prototype, $.EventHandler.prototype );
|
|
|
|
|
|
/**
|
|
* Decides whether to try to process the response as xml, json, or hand back
|
|
* the text
|
|
* @eprivate
|
|
* @inner
|
|
* @function
|
|
* @param {XMLHttpRequest} xhr - the completed network request
|
|
*/
|
|
function processResponse( xhr ){
|
|
var responseText = xhr.responseText,
|
|
status = xhr.status,
|
|
statusText,
|
|
data;
|
|
|
|
if ( !xhr ) {
|
|
throw new Error( $.getString( "Errors.Security" ) );
|
|
} else if ( xhr.status !== 200 && xhr.status !== 0 ) {
|
|
status = xhr.status;
|
|
statusText = ( status == 404 ) ?
|
|
"Not Found" :
|
|
xhr.statusText;
|
|
throw new Error( $.getString( "Errors.Status", status, statusText ) );
|
|
}
|
|
|
|
if( responseText.match(/\s*<.*/) ){
|
|
try{
|
|
data = ( xhr.responseXML && xhr.responseXML.documentElement ) ?
|
|
xhr.responseXML :
|
|
$.parseXml( responseText );
|
|
} catch (e){
|
|
data = xhr.responseText;
|
|
}
|
|
}else if( responseText.match(/\s*[\{\[].*/) ){
|
|
data = eval( responseText );
|
|
}else{
|
|
data = responseText;
|
|
}
|
|
return data;
|
|
};
|
|
|
|
|
|
/**
|
|
* Determines the TileSource Implementation by introspection of OpenSeadragon
|
|
* namespace, calling each TileSource implementation of 'isType'
|
|
* @eprivate
|
|
* @inner
|
|
* @function
|
|
* @param {Object|Array} data - the tile source configuration object
|
|
* @param {String} url - the url where the tile source configuration object was
|
|
* loaded from, if any.
|
|
*/
|
|
$.TileSource.determineType = function( tileSource, data, url ){
|
|
var property;
|
|
for( property in OpenSeadragon ){
|
|
if( property.match(/.+TileSource$/) &&
|
|
$.isFunction( OpenSeadragon[ property ] ) &&
|
|
$.isFunction( OpenSeadragon[ property ].prototype.supports ) &&
|
|
OpenSeadragon[ property ].prototype.supports.call( tileSource, data, url )
|
|
){
|
|
return OpenSeadragon[ property ];
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
}( OpenSeadragon ));
|
|
|
|
(function( $ ){
|
|
|
|
/**
|
|
* @class
|
|
* @extends OpenSeadragon.TileSource
|
|
* @param {Number|Object} width - the pixel width of the image or the idiomatic
|
|
* options object which is used instead of positional arguments.
|
|
* @param {Number} height
|
|
* @param {Number} tileSize
|
|
* @param {Number} tileOverlap
|
|
* @param {String} tilesUrl
|
|
* @param {String} fileFormat
|
|
* @param {OpenSeadragon.DisplayRect[]} displayRects
|
|
* @property {String} tilesUrl
|
|
* @property {String} fileFormat
|
|
* @property {OpenSeadragon.DisplayRect[]} displayRects
|
|
*/
|
|
$.DziTileSource = function( width, height, tileSize, tileOverlap, tilesUrl, fileFormat, displayRects ) {
|
|
var i,
|
|
rect,
|
|
level,
|
|
options;
|
|
|
|
if( $.isPlainObject( width ) ){
|
|
options = width;
|
|
}else{
|
|
options = {
|
|
width: arguments[ 0 ],
|
|
height: arguments[ 1 ],
|
|
tileSize: arguments[ 2 ],
|
|
tileOverlap: arguments[ 3 ],
|
|
tilesUrl: arguments[ 4 ],
|
|
fileFormat: arguments[ 5 ],
|
|
displayRects: arguments[ 6 ]
|
|
};
|
|
}
|
|
|
|
this._levelRects = {};
|
|
this.tilesUrl = options.tilesUrl;
|
|
this.fileFormat = options.fileFormat;
|
|
this.displayRects = options.displayRects;
|
|
|
|
if ( this.displayRects ) {
|
|
for ( i = this.displayRects.length - 1; i >= 0; i-- ) {
|
|
rect = this.displayRects[ i ];
|
|
for ( level = rect.minLevel; level <= rect.maxLevel; level++ ) {
|
|
if ( !this._levelRects[ level ] ) {
|
|
this._levelRects[ level ] = [];
|
|
}
|
|
this._levelRects[ level ].push( rect );
|
|
}
|
|
}
|
|
}
|
|
|
|
$.TileSource.apply( this, [ options ] );
|
|
|
|
};
|
|
|
|
$.extend( $.DziTileSource.prototype, $.TileSource.prototype, {
|
|
|
|
|
|
/**
|
|
* Determine if the data and/or url imply the image service is supported by
|
|
* this tile source.
|
|
* @function
|
|
* @name OpenSeadragon.DziTileSource.prototype.supports
|
|
* @param {Object|Array} data
|
|
* @param {String} optional - url
|
|
*/
|
|
supports: function( data, url ){
|
|
var ns;
|
|
if ( data.Image ) {
|
|
ns = data.Image.xmlns;
|
|
} else if ( data.documentElement && "Image" == data.documentElement.tagName ) {
|
|
ns = data.documentElement.namespaceURI;
|
|
}
|
|
|
|
return ( "http://schemas.microsoft.com/deepzoom/2008" == ns ||
|
|
"http://schemas.microsoft.com/deepzoom/2009" == ns );
|
|
},
|
|
|
|
/**
|
|
*
|
|
* @function
|
|
* @name OpenSeadragon.DziTileSource.prototype.configure
|
|
* @param {Object|XMLDocument} data - the raw configuration
|
|
* @param {String} url - the url the data was retreived from if any.
|
|
* @return {Object} options - A dictionary of keyword arguments sufficient
|
|
* to configure this tile sources constructor.
|
|
*/
|
|
configure: function( data, url ){
|
|
|
|
var dziPath,
|
|
dziName,
|
|
tilesUrl,
|
|
options,
|
|
host;
|
|
|
|
if( !$.isPlainObject(data) ){
|
|
|
|
options = configureFromXML( this, data );
|
|
|
|
}else{
|
|
|
|
options = configureFromObject( this, data );
|
|
}
|
|
|
|
if( url && !options.tilesUrl ){
|
|
if( !( 'http' == url.substring( 0, 4 ) ) ){
|
|
host = location.protocol + '//' + location.host;
|
|
}
|
|
dziPath = url.split('/');
|
|
dziName = dziPath.pop();
|
|
dziName = dziName.substring(0, dziName.indexOf('.'));
|
|
dziPath = '/' + dziPath.join('/') + '/' + dziName + '_files/';
|
|
tilesUrl = dziPath;
|
|
if( host ){
|
|
tilesUrl = host + tilesUrl;
|
|
}
|
|
options.tilesUrl = tilesUrl;
|
|
}
|
|
|
|
return options;
|
|
},
|
|
|
|
|
|
/**
|
|
* @function
|
|
* @name OpenSeadragon.DziTileSource.prototype.getTileUrl
|
|
* @param {Number} level
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
getTileUrl: function( level, x, y ) {
|
|
return [ this.tilesUrl, level, '/', x, '_', y, '.', this.fileFormat ].join( '' );
|
|
},
|
|
|
|
|
|
/**
|
|
* @function
|
|
* @name OpenSeadragon.DziTileSource.prototype.tileExists
|
|
* @param {Number} level
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
tileExists: function( level, x, y ) {
|
|
var rects = this._levelRects[ level ],
|
|
rect,
|
|
scale,
|
|
xMin,
|
|
yMin,
|
|
xMax,
|
|
yMax,
|
|
i;
|
|
|
|
if ( !rects || !rects.length ) {
|
|
return true;
|
|
}
|
|
|
|
for ( i = rects.length - 1; i >= 0; i-- ) {
|
|
rect = rects[ i ];
|
|
|
|
if ( level < rect.minLevel || level > rect.maxLevel ) {
|
|
continue;
|
|
}
|
|
|
|
scale = this.getLevelScale( level );
|
|
xMin = rect.x * scale;
|
|
yMin = rect.y * scale;
|
|
xMax = xMin + rect.width * scale;
|
|
yMax = yMin + rect.height * scale;
|
|
|
|
xMin = Math.floor( xMin / this.tileSize );
|
|
yMin = Math.floor( yMin / this.tileSize );
|
|
xMax = Math.ceil( xMax / this.tileSize );
|
|
yMax = Math.ceil( yMax / this.tileSize );
|
|
|
|
if ( xMin <= x && x < xMax && yMin <= y && y < yMax ) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
*/
|
|
function configureFromXML( tileSource, xmlDoc ){
|
|
|
|
if ( !xmlDoc || !xmlDoc.documentElement ) {
|
|
throw new Error( $.getString( "Errors.Xml" ) );
|
|
}
|
|
|
|
var root = xmlDoc.documentElement,
|
|
rootName = root.tagName,
|
|
configuration = null,
|
|
displayRects = [],
|
|
dispRectNodes,
|
|
dispRectNode,
|
|
rectNode,
|
|
sizeNode,
|
|
i;
|
|
|
|
if ( rootName == "Image" ) {
|
|
|
|
try {
|
|
sizeNode = root.getElementsByTagName( "Size" )[ 0 ];
|
|
configuration = {
|
|
Image: {
|
|
xmlns: "http://schemas.microsoft.com/deepzoom/2008",
|
|
Format: root.getAttribute( "Format" ),
|
|
DisplayRect: null,
|
|
Overlap: parseInt( root.getAttribute( "Overlap" ) ),
|
|
TileSize: parseInt( root.getAttribute( "TileSize" ) ),
|
|
Size: {
|
|
Height: parseInt( sizeNode.getAttribute( "Height" ) ),
|
|
Width: parseInt( sizeNode.getAttribute( "Width" ) )
|
|
}
|
|
}
|
|
};
|
|
|
|
if ( !$.imageFormatSupported( configuration.Image.Format ) ) {
|
|
throw new Error(
|
|
$.getString( "Errors.ImageFormat", configuration.Image.Format.toUpperCase() )
|
|
);
|
|
}
|
|
|
|
dispRectNodes = root.getElementsByTagName( "DisplayRect" );
|
|
for ( i = 0; i < dispRectNodes.length; i++ ) {
|
|
dispRectNode = dispRectNodes[ i ];
|
|
rectNode = dispRectNode.getElementsByTagName( "Rect" )[ 0 ];
|
|
|
|
displayRects.push({
|
|
Rect: {
|
|
X: parseInt( rectNode.getAttribute( "X" ) ),
|
|
Y: parseInt( rectNode.getAttribute( "Y" ) ),
|
|
Width: parseInt( rectNode.getAttribute( "Width" ) ),
|
|
Height: parseInt( rectNode.getAttribute( "Height" ) ),
|
|
MinLevel: 0, // ignore MinLevel attribute, bug in Deep Zoom Composer
|
|
MaxLevel: parseInt( dispRectNode.getAttribute( "MaxLevel" ) )
|
|
}
|
|
});
|
|
}
|
|
|
|
if( displayRects.length ){
|
|
configuration.Image.DisplayRect = displayRects;
|
|
}
|
|
|
|
return configureFromObject( tileSource, configuration );
|
|
|
|
} catch ( e ) {
|
|
throw (e instanceof Error) ?
|
|
e :
|
|
new Error( $.getString("Errors.Dzi") );
|
|
}
|
|
} else if ( rootName == "Collection" ) {
|
|
throw new Error( $.getString( "Errors.Dzc" ) );
|
|
} else if ( rootName == "Error" ) {
|
|
return processDZIError( root );
|
|
}
|
|
|
|
throw new Error( $.getString( "Errors.Dzi" ) );
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
*/
|
|
function configureFromObject( tileSource, configuration ){
|
|
var imageData = configuration.Image,
|
|
tilesUrl = imageData.Url,
|
|
fileFormat = imageData.Format,
|
|
sizeData = imageData.Size,
|
|
dispRectData = imageData.DisplayRect || [],
|
|
width = parseInt( sizeData.Width ),
|
|
height = parseInt( sizeData.Height ),
|
|
tileSize = parseInt( imageData.TileSize ),
|
|
tileOverlap = parseInt( imageData.Overlap ),
|
|
displayRects = [],
|
|
rectData,
|
|
i;
|
|
|
|
//TODO: need to figure out out to better handle image format compatibility
|
|
// which actually includes additional file formats like xml and pdf
|
|
// and plain text for various tilesource implementations to avoid low
|
|
// level errors.
|
|
//
|
|
// For now, just don't perform the check.
|
|
//
|
|
/*if ( !imageFormatSupported( fileFormat ) ) {
|
|
throw new Error(
|
|
$.getString( "Errors.ImageFormat", fileFormat.toUpperCase() )
|
|
);
|
|
}*/
|
|
|
|
for ( i = 0; i < dispRectData.length; i++ ) {
|
|
rectData = dispRectData[ i ].Rect;
|
|
|
|
displayRects.push( new $.DisplayRect(
|
|
parseInt( rectData.X ),
|
|
parseInt( rectData.Y ),
|
|
parseInt( rectData.Width ),
|
|
parseInt( rectData.Height ),
|
|
0, // ignore MinLevel attribute, bug in Deep Zoom Composer
|
|
parseInt( rectData.MaxLevel )
|
|
));
|
|
}
|
|
|
|
|
|
return {
|
|
width: width, /* width *required */
|
|
height: height, /* height *required */
|
|
tileSize: tileSize, /* tileSize *required */
|
|
tileOverlap: tileOverlap, /* tileOverlap *required */
|
|
minLevel: null, /* minLevel */
|
|
maxLevel: null, /* maxLevel */
|
|
tilesUrl: tilesUrl, /* tilesUrl */
|
|
fileFormat: fileFormat, /* fileFormat */
|
|
displayRects: displayRects /* displayRects */
|
|
};
|
|
|
|
};
|
|
|
|
}( OpenSeadragon ));
|
|
|
|
(function( $ ){
|
|
|
|
|
|
/**
|
|
* The LegacyTileSource allows simple, traditional image pyramids to be loaded
|
|
* into an OpenSeadragon Viewer. Basically, this translates to the historically
|
|
* common practice of starting with a 'master' image, maybe a tiff for example,
|
|
* and generating a set of 'service' images like one or more thumbnails, a medium
|
|
* resolution image and a high resolution image in standard web formats like
|
|
* png or jpg.
|
|
* @class
|
|
* @param {Array} levels An array of file descriptions, each is an object with
|
|
* a 'url', a 'width', and a 'height'. Overriding classes can expect more
|
|
* properties but these properties are sufficient for this implementation.
|
|
* Additionally, the levels are required to be listed in order from
|
|
* smallest to largest.
|
|
* @property {Number} aspectRatio
|
|
* @property {Number} dimensions
|
|
* @property {Number} tileSize
|
|
* @property {Number} tileOverlap
|
|
* @property {Number} minLevel
|
|
* @property {Number} maxLevel
|
|
* @property {Array} levels
|
|
*/
|
|
$.LegacyTileSource = function( levels ) {
|
|
|
|
var options,
|
|
width,
|
|
height;
|
|
|
|
if( $.isArray( levels ) ){
|
|
options = {
|
|
type: 'legacy-image-pyramid',
|
|
levels: levels
|
|
};
|
|
}
|
|
|
|
//clean up the levels to make sure we support all formats
|
|
options.levels = filterFiles( options.levels );
|
|
width = options.levels[ options.levels.length - 1 ].width;
|
|
height = options.levels[ options.levels.length - 1 ].height;
|
|
|
|
$.extend( true, options, {
|
|
width: width,
|
|
height: height,
|
|
tileSize: Math.max( height, width ),
|
|
tileOverlap: 0,
|
|
minLevel: 0,
|
|
maxLevel: options.levels.length - 1
|
|
});
|
|
|
|
$.TileSource.apply( this, [ options ] );
|
|
|
|
this.levels = options.levels;
|
|
};
|
|
|
|
$.extend( $.LegacyTileSource.prototype, $.TileSource.prototype, {
|
|
/**
|
|
* Determine if the data and/or url imply the image service is supported by
|
|
* this tile source.
|
|
* @function
|
|
* @name OpenSeadragon.DziTileSource.prototype.supports
|
|
* @param {Object|Array} data
|
|
* @param {String} optional - url
|
|
*/
|
|
supports: function( data, url ){
|
|
return (
|
|
data.type &&
|
|
"legacy-image-pyramid" == data.type
|
|
) || (
|
|
data.documentElement &&
|
|
"legacy-image-pyramid" == data.documentElement.getAttribute('type')
|
|
);
|
|
},
|
|
|
|
|
|
/**
|
|
*
|
|
* @function
|
|
* @name OpenSeadragon.DziTileSource.prototype.configure
|
|
* @param {Object|XMLDocument} configuration - the raw configuration
|
|
* @param {String} dataUrl - the url the data was retreived from if any.
|
|
* @return {Object} options - A dictionary of keyword arguments sufficient
|
|
* to configure this tile sources constructor.
|
|
*/
|
|
configure: function( configuration, dataUrl ){
|
|
|
|
var options;
|
|
|
|
if( !$.isPlainObject(configuration) ){
|
|
|
|
options = configureFromXML( this, configuration );
|
|
|
|
}else{
|
|
|
|
options = configureFromObject( this, configuration );
|
|
}
|
|
|
|
return options;
|
|
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} level
|
|
*/
|
|
getLevelScale: function( level ) {
|
|
var levelScale = NaN;
|
|
if ( level >= this.minLevel && level <= this.maxLevel ){
|
|
levelScale =
|
|
this.levels[ level ].width /
|
|
this.levels[ this.maxLevel ].width;
|
|
}
|
|
return levelScale;
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} level
|
|
*/
|
|
getNumTiles: function( level ) {
|
|
var scale = this.getLevelScale( level );
|
|
if ( scale ){
|
|
return new $.Point( 1, 1 );
|
|
} else {
|
|
return new $.Point( 0, 0 );
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} level
|
|
* @param {OpenSeadragon.Point} point
|
|
*/
|
|
getTileAtPoint: function( level, point ) {
|
|
return new $.Point( 0, 0 );
|
|
},
|
|
|
|
|
|
/**
|
|
* This method is not implemented by this class other than to throw an Error
|
|
* announcing you have to implement it. Because of the variety of tile
|
|
* server technologies, and various specifications for building image
|
|
* pyramids, this method is here to allow easy integration.
|
|
* @function
|
|
* @param {Number} level
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @throws {Error}
|
|
*/
|
|
getTileUrl: function( level, x, y ) {
|
|
var url = null;
|
|
if( level >= this.minLevel && level <= this.maxLevel ){
|
|
url = this.levels[ level ].url;
|
|
}
|
|
return url;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* This method removes any files from the Array which dont conform to our
|
|
* basic requirements for a 'level' in the LegacyTileSource.
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
*/
|
|
function filterFiles( files ){
|
|
var filtered = [],
|
|
file,
|
|
i;
|
|
for( i = 0; i < files.length; i++ ){
|
|
file = files[ i ];
|
|
if( file.height &&
|
|
file.width &&
|
|
file.url && (
|
|
file.url.toLowerCase().match(/^.*\.(png|jpg|jpeg|gif)$/) || (
|
|
file.mimetype &&
|
|
file.mimetype.toLowerCase().match(/^.*\/(png|jpg|jpeg|gif)$/)
|
|
)
|
|
) ){
|
|
//This is sufficient to serve as a level
|
|
filtered.push({
|
|
url: file.url,
|
|
width: Number( file.width ),
|
|
height: Number( file.height )
|
|
});
|
|
}
|
|
}
|
|
|
|
return filtered.sort(function(a,b){
|
|
return a.height - b.height;
|
|
});
|
|
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
*/
|
|
function configureFromXML( tileSource, xmlDoc ){
|
|
|
|
if ( !xmlDoc || !xmlDoc.documentElement ) {
|
|
throw new Error( $.getString( "Errors.Xml" ) );
|
|
}
|
|
|
|
var root = xmlDoc.documentElement,
|
|
rootName = root.tagName,
|
|
conf = null,
|
|
levels = [],
|
|
level,
|
|
i;
|
|
|
|
if ( rootName == "image" ) {
|
|
|
|
try {
|
|
conf = {
|
|
type: root.getAttribute( "type" ),
|
|
levels: []
|
|
};
|
|
|
|
levels = root.getElementsByTagName( "level" );
|
|
for ( i = 0; i < levels.length; i++ ) {
|
|
level = levels[ i ];
|
|
|
|
conf.levels .push({
|
|
url: level.getAttribute( "url" ),
|
|
width: parseInt( level.getAttribute( "width" ) ),
|
|
height: parseInt( level.getAttribute( "height" ) )
|
|
});
|
|
}
|
|
|
|
return configureFromObject( tileSource, conf );
|
|
|
|
} catch ( e ) {
|
|
throw (e instanceof Error) ?
|
|
e :
|
|
new Error( 'Unknown error parsing Legacy Image Pyramid XML.' );
|
|
}
|
|
} else if ( rootName == "collection" ) {
|
|
throw new Error( 'Legacy Image Pyramid Collections not yet supported.' );
|
|
} else if ( rootName == "error" ) {
|
|
throw new Error( 'Error: ' + xmlDoc );
|
|
}
|
|
|
|
throw new Error( 'Unknown element ' + rootName );
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
*/
|
|
function configureFromObject( tileSource, configuration ){
|
|
|
|
return configuration.levels;
|
|
|
|
};
|
|
|
|
}( OpenSeadragon ));
|
|
|
|
(function( $ ){
|
|
|
|
/**
|
|
* @class
|
|
* @extends OpenSeadragon.TileSourceCollection
|
|
*/
|
|
$.TileSourceCollection = function( tileSize, tileSources, rows, layout ) {
|
|
|
|
|
|
if( $.isPlainObject( tileSize ) ){
|
|
options = tileSize;
|
|
}else{
|
|
options = {
|
|
tileSize: arguments[ 0 ],
|
|
tileSources: arguments[ 1 ],
|
|
rows: arguments[ 2 ],
|
|
layout: arguments[ 3 ]
|
|
};
|
|
}
|
|
|
|
if( !options.layout ){
|
|
options.layout = 'horizontal';
|
|
}
|
|
|
|
var minLevel = 0,
|
|
levelSize = 1.0,
|
|
tilesPerRow = Math.ceil( options.tileSources.length / options.rows ),
|
|
longSide = tilesPerRow >= options.rows ?
|
|
tilesPerRow :
|
|
options.rows
|
|
|
|
if( 'horizontal' == options.layout ){
|
|
options.width = ( options.tileSize ) * tilesPerRow;
|
|
options.height = ( options.tileSize ) * options.rows;
|
|
} else {
|
|
options.height = ( options.tileSize + ( options.tileMargin * 2 ) ) * tilesPerRow;
|
|
options.width = ( options.tileSize + ( options.tileMargin * 2 ) ) * options.rows;
|
|
}
|
|
|
|
options.tileOverlap = -options.tileMargin;
|
|
options.tilesPerRow = tilesPerRow;
|
|
|
|
//Set min level to avoid loading sublevels since collection is a
|
|
//different kind of abstraction
|
|
|
|
while( levelSize < ( options.tileSize ) * longSide ){
|
|
//$.console.log( '%s levelSize %s minLevel %s', options.tileSize * longSide, levelSize, minLevel );
|
|
levelSize = levelSize * 2.0;
|
|
minLevel++;
|
|
}
|
|
options.minLevel = minLevel;
|
|
|
|
//for( var name in options ){
|
|
// $.console.log( 'Collection %s %s', name, options[ name ] );
|
|
//}
|
|
|
|
$.TileSource.apply( this, [ options ] );
|
|
|
|
};
|
|
|
|
$.extend( $.TileSourceCollection.prototype, $.TileSource.prototype, {
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} level
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
getTileBounds: function( level, x, y ) {
|
|
var dimensionsScaled = this.dimensions.times( this.getLevelScale( level ) ),
|
|
px = this.tileSize * x - this.tileOverlap,
|
|
py = this.tileSize * y - this.tileOverlap,
|
|
sx = this.tileSize + 1 * this.tileOverlap,
|
|
sy = this.tileSize + 1 * this.tileOverlap,
|
|
scale = 1.0 / dimensionsScaled.x;
|
|
|
|
sx = Math.min( sx, dimensionsScaled.x - px );
|
|
sy = Math.min( sy, dimensionsScaled.y - py );
|
|
|
|
return new $.Rect( px * scale, py * scale, sx * scale, sy * scale );
|
|
},
|
|
|
|
/**
|
|
*
|
|
* @function
|
|
* @name OpenSeadragon.TileSourceCollection.prototype.configure
|
|
*/
|
|
configure: function( data, url ){
|
|
return
|
|
},
|
|
|
|
|
|
/**
|
|
* @function
|
|
* @name OpenSeadragon.TileSourceCollection.prototype.getTileUrl
|
|
* @param {Number} level
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
getTileUrl: function( level, x, y ) {
|
|
//$.console.log([ level, '/', x, '_', y ].join( '' ));
|
|
return null;
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
}( OpenSeadragon ));
|
|
|
|
|
|
(function( $ ){
|
|
|
|
/**
|
|
* An enumeration of button states including, REST, GROUP, HOVER, and DOWN
|
|
* @static
|
|
*/
|
|
$.ButtonState = {
|
|
REST: 0,
|
|
GROUP: 1,
|
|
HOVER: 2,
|
|
DOWN: 3
|
|
};
|
|
|
|
/**
|
|
* Manages events, hover states for individual buttons, tool-tips, as well
|
|
* as fading the bottons out when the user has not interacted with them
|
|
* for a specified period.
|
|
* @class
|
|
* @extends OpenSeadragon.EventHandler
|
|
* @param {Object} options
|
|
* @param {String} options.tooltip Provides context help for the button we the
|
|
* user hovers over it.
|
|
* @param {String} options.srcRest URL of image to use in 'rest' state
|
|
* @param {String} options.srcGroup URL of image to use in 'up' state
|
|
* @param {String} options.srcHover URL of image to use in 'hover' state
|
|
* @param {String} options.srcDown URL of image to use in 'domn' state
|
|
* @param {Element} [options.element] Element to use as a container for the
|
|
* button.
|
|
* @property {String} tooltip Provides context help for the button we the
|
|
* user hovers over it.
|
|
* @property {String} srcRest URL of image to use in 'rest' state
|
|
* @property {String} srcGroup URL of image to use in 'up' state
|
|
* @property {String} srcHover URL of image to use in 'hover' state
|
|
* @property {String} srcDown URL of image to use in 'domn' state
|
|
* @property {Object} config Configurable settings for this button. DEPRECATED.
|
|
* @property {Element} [element] Element to use as a container for the
|
|
* button.
|
|
* @property {Number} fadeDelay How long to wait before fading
|
|
* @property {Number} fadeLength How long should it take to fade the button.
|
|
* @property {Number} fadeBeginTime When the button last began to fade.
|
|
* @property {Boolean} shouldFade Whether this button should fade after user
|
|
* stops interacting with the viewport.
|
|
this.fadeDelay = 0; // begin fading immediately
|
|
this.fadeLength = 2000; // fade over a period of 2 seconds
|
|
this.fadeBeginTime = null;
|
|
this.shouldFade = false;
|
|
*/
|
|
$.Button = function( options ) {
|
|
|
|
var _this = this;
|
|
|
|
$.EventHandler.call( this );
|
|
|
|
$.extend( true, this, {
|
|
|
|
tooltip: null,
|
|
srcRest: null,
|
|
srcGroup: null,
|
|
srcHover: null,
|
|
srcDown: null,
|
|
clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold,
|
|
clickDistThreshold: $.DEFAULT_SETTINGS.clickDistThreshold,
|
|
// begin fading immediately
|
|
fadeDelay: 0,
|
|
// fade over a period of 2 seconds
|
|
fadeLength: 2000,
|
|
onPress: null,
|
|
onRelease: null,
|
|
onClick: null,
|
|
onEnter: null,
|
|
onExit: null,
|
|
onFocus: null,
|
|
onBlur: null
|
|
|
|
}, options );
|
|
|
|
this.element = options.element || $.makeNeutralElement( "button" );
|
|
this.element.href = this.element.href || '#';
|
|
|
|
//if the user has specified the element to bind the control to explicitly
|
|
//then do not add the default control images
|
|
if( !options.element ){
|
|
this.imgRest = $.makeTransparentImage( this.srcRest );
|
|
this.imgGroup = $.makeTransparentImage( this.srcGroup );
|
|
this.imgHover = $.makeTransparentImage( this.srcHover );
|
|
this.imgDown = $.makeTransparentImage( this.srcDown );
|
|
|
|
this.element.appendChild( this.imgRest );
|
|
this.element.appendChild( this.imgGroup );
|
|
this.element.appendChild( this.imgHover );
|
|
this.element.appendChild( this.imgDown );
|
|
|
|
this.imgGroup.style.position =
|
|
this.imgHover.style.position =
|
|
this.imgDown.style.position =
|
|
"absolute";
|
|
|
|
this.imgGroup.style.top =
|
|
this.imgHover.style.top =
|
|
this.imgDown.style.top =
|
|
"0px";
|
|
|
|
this.imgGroup.style.left =
|
|
this.imgHover.style.left =
|
|
this.imgDown.style.left =
|
|
"0px";
|
|
|
|
this.imgHover.style.visibility =
|
|
this.imgDown.style.visibility =
|
|
"hidden";
|
|
|
|
if ( $.Browser.vendor == $.BROWSERS.FIREFOX && $.Browser.version < 3 ){
|
|
this.imgGroup.style.top =
|
|
this.imgHover.style.top =
|
|
this.imgDown.style.top =
|
|
"";
|
|
}
|
|
}
|
|
|
|
|
|
this.addHandler( "onPress", this.onPress );
|
|
this.addHandler( "onRelease", this.onRelease );
|
|
this.addHandler( "onClick", this.onClick );
|
|
this.addHandler( "onEnter", this.onEnter );
|
|
this.addHandler( "onExit", this.onExit );
|
|
this.addHandler( "onFocus", this.onFocus );
|
|
this.addHandler( "onBlur", this.onBlur );
|
|
|
|
this.currentState = $.ButtonState.GROUP;
|
|
|
|
this.fadeBeginTime = null;
|
|
this.shouldFade = false;
|
|
|
|
this.element.style.display = "inline-block";
|
|
this.element.style.position = "relative";
|
|
this.element.title = this.tooltip;
|
|
|
|
this.tracker = new $.MouseTracker({
|
|
|
|
element: this.element,
|
|
clickTimeThreshold: this.clickTimeThreshold,
|
|
clickDistThreshold: this.clickDistThreshold,
|
|
|
|
enterHandler: function( tracker, position, buttonDownElement, buttonDownAny ) {
|
|
if ( buttonDownElement ) {
|
|
inTo( _this, $.ButtonState.DOWN );
|
|
_this.raiseEvent( "onEnter", _this );
|
|
} else if ( !buttonDownAny ) {
|
|
inTo( _this, $.ButtonState.HOVER );
|
|
}
|
|
},
|
|
|
|
focusHandler: function( tracker, position, buttonDownElement, buttonDownAny ) {
|
|
this.enterHandler( tracker, position, buttonDownElement, buttonDownAny );
|
|
_this.raiseEvent( "onFocus", _this );
|
|
},
|
|
|
|
exitHandler: function( tracker, position, buttonDownElement, buttonDownAny ) {
|
|
outTo( _this, $.ButtonState.GROUP );
|
|
if ( buttonDownElement ) {
|
|
_this.raiseEvent( "onExit", _this );
|
|
}
|
|
},
|
|
|
|
blurHandler: function( tracker, position, buttonDownElement, buttonDownAny ) {
|
|
this.exitHandler( tracker, position, buttonDownElement, buttonDownAny );
|
|
_this.raiseEvent( "onBlur", _this );
|
|
},
|
|
|
|
pressHandler: function( tracker, position ) {
|
|
inTo( _this, $.ButtonState.DOWN );
|
|
_this.raiseEvent( "onPress", _this );
|
|
},
|
|
|
|
releaseHandler: function( tracker, position, insideElementPress, insideElementRelease ) {
|
|
if ( insideElementPress && insideElementRelease ) {
|
|
outTo( _this, $.ButtonState.HOVER );
|
|
_this.raiseEvent( "onRelease", _this );
|
|
} else if ( insideElementPress ) {
|
|
outTo( _this, $.ButtonState.GROUP );
|
|
} else {
|
|
inTo( _this, $.ButtonState.HOVER );
|
|
}
|
|
},
|
|
|
|
clickHandler: function( tracker, position, quick, shift ) {
|
|
if ( quick ) {
|
|
_this.raiseEvent("onClick", _this);
|
|
}
|
|
},
|
|
|
|
keyHandler: function( tracker, key ){
|
|
//console.log( "%s : handling key %s!", _this.tooltip, key);
|
|
if( 13 === key ){
|
|
_this.raiseEvent( "onClick", _this );
|
|
_this.raiseEvent( "onRelease", _this );
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
}).setTracking( true );
|
|
|
|
outTo( this, $.ButtonState.REST );
|
|
};
|
|
|
|
$.extend( $.Button.prototype, $.EventHandler.prototype, {
|
|
|
|
/**
|
|
* TODO: Determine what this function is intended to do and if it's actually
|
|
* useful as an API point.
|
|
* @function
|
|
* @name OpenSeadragon.Button.prototype.notifyGroupEnter
|
|
*/
|
|
notifyGroupEnter: function() {
|
|
inTo( this, $.ButtonState.GROUP );
|
|
},
|
|
|
|
/**
|
|
* TODO: Determine what this function is intended to do and if it's actually
|
|
* useful as an API point.
|
|
* @function
|
|
* @name OpenSeadragon.Button.prototype.notifyGroupExit
|
|
*/
|
|
notifyGroupExit: function() {
|
|
outTo( this, $.ButtonState.REST );
|
|
},
|
|
|
|
disable: function(){
|
|
this.notifyGroupExit();
|
|
this.element.disabled = true;
|
|
$.setElementOpacity( this.element, 0.2, true );
|
|
},
|
|
|
|
enable: function(){
|
|
this.element.disabled = false;
|
|
$.setElementOpacity( this.element, 1.0, true );
|
|
this.notifyGroupEnter();
|
|
}
|
|
|
|
});
|
|
|
|
|
|
function scheduleFade( button ) {
|
|
window.setTimeout(function(){
|
|
updateFade( button );
|
|
}, 20 );
|
|
};
|
|
|
|
function updateFade( button ) {
|
|
var currentTime,
|
|
deltaTime,
|
|
opacity;
|
|
|
|
if ( button.shouldFade ) {
|
|
currentTime = +new Date();
|
|
deltaTime = currentTime - button.fadeBeginTime;
|
|
opacity = 1.0 - deltaTime / button.fadeLength;
|
|
opacity = Math.min( 1.0, opacity );
|
|
opacity = Math.max( 0.0, opacity );
|
|
|
|
if( button.imgGroup ){
|
|
$.setElementOpacity( button.imgGroup, opacity, true );
|
|
}
|
|
if ( opacity > 0 ) {
|
|
// fade again
|
|
scheduleFade( button );
|
|
}
|
|
}
|
|
};
|
|
|
|
function beginFading( button ) {
|
|
button.shouldFade = true;
|
|
button.fadeBeginTime = +new Date() + button.fadeDelay;
|
|
window.setTimeout( function(){
|
|
scheduleFade( button );
|
|
}, button.fadeDelay );
|
|
};
|
|
|
|
function stopFading( button ) {
|
|
button.shouldFade = false;
|
|
if( button.imgGroup ){
|
|
$.setElementOpacity( button.imgGroup, 1.0, true );
|
|
}
|
|
};
|
|
|
|
function inTo( button, newState ) {
|
|
|
|
if( button.element.disabled ){
|
|
return;
|
|
}
|
|
|
|
if ( newState >= $.ButtonState.GROUP &&
|
|
button.currentState == $.ButtonState.REST ) {
|
|
stopFading( button );
|
|
button.currentState = $.ButtonState.GROUP;
|
|
}
|
|
|
|
if ( newState >= $.ButtonState.HOVER &&
|
|
button.currentState == $.ButtonState.GROUP ) {
|
|
if( button.imgHover ){
|
|
button.imgHover.style.visibility = "";
|
|
}
|
|
button.currentState = $.ButtonState.HOVER;
|
|
}
|
|
|
|
if ( newState >= $.ButtonState.DOWN &&
|
|
button.currentState == $.ButtonState.HOVER ) {
|
|
if( button.imgDown ){
|
|
button.imgDown.style.visibility = "";
|
|
}
|
|
button.currentState = $.ButtonState.DOWN;
|
|
}
|
|
};
|
|
|
|
|
|
function outTo( button, newState ) {
|
|
|
|
if( button.element.disabled ){
|
|
return;
|
|
}
|
|
|
|
if ( newState <= $.ButtonState.HOVER &&
|
|
button.currentState == $.ButtonState.DOWN ) {
|
|
if( button.imgDown ){
|
|
button.imgDown.style.visibility = "hidden";
|
|
}
|
|
button.currentState = $.ButtonState.HOVER;
|
|
}
|
|
|
|
if ( newState <= $.ButtonState.GROUP &&
|
|
button.currentState == $.ButtonState.HOVER ) {
|
|
if( button.imgHover ){
|
|
button.imgHover.style.visibility = "hidden";
|
|
}
|
|
button.currentState = $.ButtonState.GROUP;
|
|
}
|
|
|
|
if ( newState <= $.ButtonState.REST &&
|
|
button.currentState == $.ButtonState.GROUP ) {
|
|
beginFading( button );
|
|
button.currentState = $.ButtonState.REST;
|
|
}
|
|
};
|
|
|
|
|
|
|
|
}( OpenSeadragon ));
|
|
|
|
(function( $ ){
|
|
/**
|
|
* Manages events on groups of buttons.
|
|
* @class
|
|
* @param {Object} options - a dictionary of settings applied against the entire
|
|
* group of buttons
|
|
* @param {Array} options.buttons Array of buttons
|
|
* @param {Element} [options.group] Element to use as the container,
|
|
* @param {Object} options.config Object with Viewer settings ( TODO: is
|
|
* this actually used anywhere? )
|
|
* @param {Function} [options.enter] Function callback for when the mouse
|
|
* enters group
|
|
* @param {Function} [options.exit] Function callback for when mouse leaves
|
|
* the group
|
|
* @param {Function} [options.release] Function callback for when mouse is
|
|
* released
|
|
* @property {Array} buttons - An array containing the buttons themselves.
|
|
* @property {Element} element - The shared container for the buttons.
|
|
* @property {Object} config - Configurable settings for the group of buttons.
|
|
* @property {OpenSeadragon.MouseTracker} tracker - Tracks mouse events accross
|
|
* the group of buttons.
|
|
**/
|
|
$.ButtonGroup = function( options ) {
|
|
|
|
$.extend( true, this, {
|
|
buttons: [],
|
|
clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold,
|
|
clickDistThreshold: $.DEFAULT_SETTINGS.clickDistThreshold,
|
|
labelText: ""
|
|
}, options );
|
|
|
|
// copy the botton elements
|
|
var buttons = this.buttons.concat([]),
|
|
_this = this,
|
|
i;
|
|
|
|
this.element = options.element || $.makeNeutralElement( "fieldgroup" );
|
|
|
|
if( !options.group ){
|
|
this.label = $.makeNeutralElement( "label" );
|
|
//TODO: support labels for ButtonGroups
|
|
//this.label.innerHTML = this.labelText;
|
|
this.element.style.display = "inline-block";
|
|
this.element.appendChild( this.label );
|
|
for ( i = 0; i < buttons.length; i++ ) {
|
|
this.element.appendChild( buttons[ i ].element );
|
|
}
|
|
}
|
|
|
|
this.tracker = new $.MouseTracker({
|
|
element: this.element,
|
|
clickTimeThreshold: this.clickTimeThreshold,
|
|
clickDistThreshold: this.clickDistThreshold,
|
|
enterHandler: function() {
|
|
var i;
|
|
for ( i = 0; i < _this.buttons.length; i++ ) {
|
|
_this.buttons[ i ].notifyGroupEnter();
|
|
}
|
|
},
|
|
exitHandler: function() {
|
|
var i,
|
|
buttonDownElement = arguments.length > 2 ?
|
|
arguments[ 2 ] :
|
|
null;
|
|
if ( !buttonDownElement ) {
|
|
for ( i = 0; i < _this.buttons.length; i++ ) {
|
|
_this.buttons[ i ].notifyGroupExit();
|
|
}
|
|
}
|
|
},
|
|
releaseHandler: function() {
|
|
var i,
|
|
insideElementRelease = arguments.length > 3 ?
|
|
arguments[ 3 ] :
|
|
null;
|
|
if ( !insideElementRelease ) {
|
|
for ( i = 0; i < _this.buttons.length; i++ ) {
|
|
_this.buttons[ i ].notifyGroupExit();
|
|
}
|
|
}
|
|
}
|
|
}).setTracking( true );
|
|
};
|
|
|
|
$.ButtonGroup.prototype = {
|
|
|
|
/**
|
|
* TODO: Figure out why this is used on the public API and if a more useful
|
|
* api can be created.
|
|
* @function
|
|
* @name OpenSeadragon.ButtonGroup.prototype.emulateEnter
|
|
*/
|
|
emulateEnter: function() {
|
|
this.tracker.enterHandler();
|
|
},
|
|
|
|
/**
|
|
* TODO: Figure out why this is used on the public API and if a more useful
|
|
* api can be created.
|
|
* @function
|
|
* @name OpenSeadragon.ButtonGroup.prototype.emulateExit
|
|
*/
|
|
emulateExit: function() {
|
|
this.tracker.exitHandler();
|
|
}
|
|
};
|
|
|
|
|
|
}( OpenSeadragon ));
|
|
|
|
(function( $ ){
|
|
|
|
/**
|
|
* A Rectangle really represents a 2x2 matrix where each row represents a
|
|
* 2 dimensional vector component, the first is (x,y) and the second is
|
|
* (width, height). The latter component implies the equation of a simple
|
|
* plane.
|
|
*
|
|
* @class
|
|
* @param {Number} x The vector component 'x'.
|
|
* @param {Number} y The vector component 'y'.
|
|
* @param {Number} width The vector component 'height'.
|
|
* @param {Number} height The vector component 'width'.
|
|
* @property {Number} x The vector component 'x'.
|
|
* @property {Number} y The vector component 'y'.
|
|
* @property {Number} width The vector component 'width'.
|
|
* @property {Number} height The vector component 'height'.
|
|
*/
|
|
$.Rect = function( x, y, width, height ) {
|
|
this.x = typeof ( x ) == "number" ? x : 0;
|
|
this.y = typeof ( y ) == "number" ? y : 0;
|
|
this.width = typeof ( width ) == "number" ? width : 0;
|
|
this.height = typeof ( height ) == "number" ? height : 0;
|
|
};
|
|
|
|
$.Rect.prototype = {
|
|
|
|
/**
|
|
* The aspect ratio is simply the ratio of width to height.
|
|
* @function
|
|
* @returns {Number} The ratio of width to height.
|
|
*/
|
|
getAspectRatio: function() {
|
|
return this.width / this.height;
|
|
},
|
|
|
|
/**
|
|
* Provides the coordinates of the upper-left corner of the rectanglea s a
|
|
* point.
|
|
* @function
|
|
* @returns {OpenSeadragon.Point} The coordinate of the upper-left corner of
|
|
* the rectangle.
|
|
*/
|
|
getTopLeft: function() {
|
|
return new $.Point( this.x, this.y );
|
|
},
|
|
|
|
/**
|
|
* Provides the coordinates of the bottom-right corner of the rectangle as a
|
|
* point.
|
|
* @function
|
|
* @returns {OpenSeadragon.Point} The coordinate of the bottom-right corner of
|
|
* the rectangle.
|
|
*/
|
|
getBottomRight: function() {
|
|
return new $.Point(
|
|
this.x + this.width,
|
|
this.y + this.height
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Computes the center of the rectangle.
|
|
* @function
|
|
* @returns {OpenSeadragon.Point} The center of the rectangle as represnted
|
|
* as represented by a 2-dimensional vector (x,y)
|
|
*/
|
|
getCenter: function() {
|
|
return new $.Point(
|
|
this.x + this.width / 2.0,
|
|
this.y + this.height / 2.0
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Returns the width and height component as a vector OpenSeadragon.Point
|
|
* @function
|
|
* @returns {OpenSeadragon.Point} The 2 dimensional vector represnting the
|
|
* the width and height of the rectangle.
|
|
*/
|
|
getSize: function() {
|
|
return new $.Point( this.width, this.height );
|
|
},
|
|
|
|
/**
|
|
* Determines if two Rectanlges have equivalent components.
|
|
* @function
|
|
* @param {OpenSeadragon.Rect} rectangle The Rectangle to compare to.
|
|
* @return {Boolean} 'true' if all components are equal, otherwise 'false'.
|
|
*/
|
|
equals: function( other ) {
|
|
return ( other instanceof $.Rect ) &&
|
|
( this.x === other.x ) &&
|
|
( this.y === other.y ) &&
|
|
( this.width === other.width ) &&
|
|
( this.height === other.height );
|
|
},
|
|
|
|
/**
|
|
* Provides a string representation of the retangle which is useful for
|
|
* debugging.
|
|
* @function
|
|
* @returns {String} A string representation of the rectangle.
|
|
*/
|
|
toString: function() {
|
|
return "[" +
|
|
Math.round(this.x*100) + "," +
|
|
Math.round(this.y*100) + "," +
|
|
Math.round(this.width*100) + "x" +
|
|
Math.round(this.height*100) +
|
|
"]";
|
|
}
|
|
};
|
|
|
|
|
|
}( OpenSeadragon ));
|
|
(function( $ ){
|
|
|
|
// dictionary from id to private properties
|
|
var THIS = {};
|
|
|
|
/**
|
|
* The CollectionDrawer is a reimplementation if the Drawer API that
|
|
* focuses on allowing a viewport to be redefined as a collection
|
|
* of smaller viewports, defined by a clear number of rows and / or
|
|
* columns of which each item in the matrix of viewports has its own
|
|
* source.
|
|
*
|
|
* This idea is a reexpression of the idea of dzi collections
|
|
* which allows a clearer algorithm to reuse the tile sources already
|
|
* supported by OpenSeadragon, in heterogenious or homogenious
|
|
* sequences just like mixed groups already supported by the viewer
|
|
* for the purpose of image sequnces.
|
|
*
|
|
* TODO: The difficult part of this feature is figuring out how to express
|
|
* this functionality as a combination of the functionality already
|
|
* provided by Drawer, Viewport, TileSource, and Navigator. It may
|
|
* require better abstraction at those points in order to effeciently
|
|
* reuse those paradigms.
|
|
*/
|
|
$.ReferenceStrip = function( options ){
|
|
|
|
var _this = this,
|
|
viewer = options.viewer,
|
|
viewerSize = $.getElementSize( viewer.element ),
|
|
miniViewer,
|
|
minPixelRatio,
|
|
element,
|
|
i;
|
|
|
|
//We may need to create a new element and id if they did not
|
|
//provide the id for the existing element
|
|
if( !options.id ){
|
|
options.id = 'referencestrip-' + (+new Date());
|
|
this.element = $.makeNeutralElement( "div" );
|
|
this.element.id = options.id;
|
|
this.element.className = 'referencestrip';
|
|
}
|
|
|
|
options = $.extend( true, {
|
|
sizeRatio: $.DEFAULT_SETTINGS.referenceStripSizeRatio,
|
|
position: $.DEFAULT_SETTINGS.referenceStripPosition,
|
|
scroll: $.DEFAULT_SETTINGS.referenceStripScroll,
|
|
clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold
|
|
}, options, {
|
|
//required overrides
|
|
element: this.element,
|
|
//These need to be overridden to prevent recursion since
|
|
//the navigator is a viewer and a viewer has a navigator
|
|
showNavigator: false,
|
|
mouseNavEnabled: false,
|
|
showNavigationControl: false,
|
|
showSequenceControl: false
|
|
});
|
|
|
|
$.extend( this, options );
|
|
|
|
//Private state properties
|
|
THIS[ this.id ] = {
|
|
"animating": false
|
|
};
|
|
|
|
this.minPixelRatio = this.viewer.minPixelRatio;
|
|
|
|
(function( style ){
|
|
style.marginTop = '0px';
|
|
style.marginRight = '0px';
|
|
style.marginBottom = '0px';
|
|
style.marginLeft = '0px';
|
|
style.left = '0px';
|
|
style.bottom = '0px';
|
|
style.border = '0px';
|
|
style.background = '#000';
|
|
style.position = 'relative';
|
|
}( this.element.style ));
|
|
|
|
$.setElementOpacity( this.element, 0.8 );
|
|
|
|
this.viewer = viewer;
|
|
this.innerTracker = new $.MouseTracker({
|
|
element: this.element,
|
|
dragHandler: $.delegate( this, onStripDrag ),
|
|
scrollHandler: $.delegate( this, onStripScroll ),
|
|
enterHandler: $.delegate( this, onStripEnter ),
|
|
exitHandler: $.delegate( this, onStripExit ),
|
|
keyHandler: $.delegate( this, onKeyPress )
|
|
}).setTracking( true );
|
|
|
|
|
|
|
|
//Controls the position and orientation of the reference strip and sets the
|
|
//appropriate width and height
|
|
if( options.width && options.height ){
|
|
this.element.style.width = options.width + 'px';
|
|
this.element.style.height = options.height + 'px';
|
|
viewer.addControl(
|
|
this.element,
|
|
$.ControlAnchor.BOTTOM_LEFT
|
|
);
|
|
} else {
|
|
if( "horizontal" == options.scroll ){
|
|
this.element.style.width = (
|
|
viewerSize.x *
|
|
options.sizeRatio *
|
|
viewer.tileSources.length
|
|
) + ( 12 * viewer.tileSources.length ) + 'px';
|
|
|
|
this.element.style.height = (
|
|
viewerSize.y *
|
|
options.sizeRatio
|
|
) + 'px';
|
|
|
|
viewer.addControl(
|
|
this.element,
|
|
$.ControlAnchor.BOTTOM_LEFT
|
|
);
|
|
}else {
|
|
this.element.style.height = (
|
|
viewerSize.y *
|
|
options.sizeRatio *
|
|
viewer.tileSources.length
|
|
) + ( 12 * viewer.tileSources.length ) + 'px';
|
|
|
|
this.element.style.width = (
|
|
viewerSize.x *
|
|
options.sizeRatio
|
|
) + 'px';
|
|
|
|
viewer.addControl(
|
|
this.element,
|
|
$.ControlAnchor.TOP_LEFT
|
|
);
|
|
|
|
}
|
|
}
|
|
|
|
this.panelWidth = ( viewerSize.x * this.sizeRatio ) + 8;
|
|
this.panelHeight = ( viewerSize.y * this.sizeRatio ) + 8;
|
|
this.panels = [];
|
|
|
|
for( i = 0; i < viewer.tileSources.length; i++ ){
|
|
|
|
element = $.makeNeutralElement('div');
|
|
element.id = this.element.id + "-" + i;
|
|
|
|
(function(style){
|
|
style.width = _this.panelWidth + 'px';
|
|
style.height = _this.panelHeight + 'px';
|
|
style.display = 'inline';
|
|
style.float = 'left'; //Webkit
|
|
style.cssFloat = 'left'; //Firefox
|
|
style.styleFloat = 'left'; //IE
|
|
style.padding = '2px';
|
|
}(element.style));
|
|
|
|
element.innerTracker = new $.MouseTracker({
|
|
element: element,
|
|
clickTimeThreshold: this.clickTimeThreshold,
|
|
clickDistThreshold: this.clickDistThreshold,
|
|
pressHandler: function( tracker ){
|
|
tracker.dragging = +new Date;
|
|
},
|
|
releaseHandler: function( tracker, position, insideElementPress, insideElementRelease ){
|
|
var id = tracker.element.id,
|
|
page = Number( id.split( '-' )[ 2 ] ),
|
|
now = +new Date;
|
|
|
|
if ( insideElementPress &&
|
|
insideElementRelease &&
|
|
tracker.dragging &&
|
|
( now - tracker.dragging ) < tracker.clickTimeThreshold ){
|
|
tracker.dragging = null;
|
|
viewer.goToPage( page );
|
|
}
|
|
}
|
|
}).setTracking( true );
|
|
|
|
this.element.appendChild( element );
|
|
|
|
element.activePanel = false;
|
|
|
|
this.panels.push( element );
|
|
|
|
}
|
|
loadPanels( this, this.scroll == 'vertical' ? viewerSize.y : viewerSize.y, 0);
|
|
this.setFocus( 0 );
|
|
|
|
};
|
|
|
|
$.extend( $.ReferenceStrip.prototype, $.EventHandler.prototype, $.Viewer.prototype, {
|
|
|
|
setFocus: function( page ){
|
|
var element = $.getElement( this.element.id + '-' + page ),
|
|
viewerSize = $.getElementSize( this.viewer.canvas ),
|
|
scrollWidth = Number(this.element.style.width.replace('px','')),
|
|
scrollHeight = Number(this.element.style.height.replace('px','')),
|
|
offsetLeft = -Number(this.element.style.marginLeft.replace('px','')),
|
|
offsetTop = -Number(this.element.style.marginTop.replace('px','')),
|
|
offset;
|
|
|
|
if ( this.currentSelected !== element ){
|
|
if( this.currentSelected ){
|
|
this.currentSelected.style.background = '#000';
|
|
}
|
|
this.currentSelected = element;
|
|
this.currentSelected.style.background = '#999';
|
|
|
|
if( 'horizontal' == this.scroll ){
|
|
//right left
|
|
offset = (Number(page)) * ( this.panelWidth + 3 );
|
|
if( offset > offsetLeft + viewerSize.x - this.panelWidth){
|
|
offset = Math.min(offset, (scrollWidth - viewerSize.x));
|
|
this.element.style.marginLeft = -offset + 'px';
|
|
loadPanels( this, viewerSize.x, -offset );
|
|
}else if( offset < offsetLeft ){
|
|
offset = Math.max(0, offset - viewerSize.x / 2);
|
|
this.element.style.marginLeft = -offset + 'px';
|
|
loadPanels( this, viewerSize.x, -offset );
|
|
}
|
|
}else{
|
|
offset = (Number(page) ) * ( this.panelHeight + 3 );
|
|
if( offset > offsetTop + viewerSize.y - this.panelHeight){
|
|
offset = Math.min(offset, (scrollHeight - viewerSize.y));
|
|
this.element.style.marginTop = -offset + 'px';
|
|
loadPanels( this, viewerSize.y, -offset );
|
|
}else if( offset < offsetTop ){
|
|
offset = Math.max(0, offset - viewerSize.y / 2);
|
|
this.element.style.marginTop = -offset + 'px';
|
|
loadPanels( this, viewerSize.y, -offset );
|
|
}
|
|
}
|
|
|
|
this.currentPage = page;
|
|
$.getElement( element.id + '-displayregion' ).focus();
|
|
onStripEnter.call( this, this.innerTracker );
|
|
}
|
|
},
|
|
/**
|
|
* @function
|
|
* @name OpenSeadragon.Navigator.prototype.update
|
|
*/
|
|
update: function( viewport ){
|
|
|
|
if( THIS[ this.id ].animating ){
|
|
$.console.log('image reference strip update');
|
|
return true;
|
|
}
|
|
return false;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
*/
|
|
function onStripDrag( tracker, position, delta, shift ) {
|
|
|
|
var offsetLeft = Number(this.element.style.marginLeft.replace('px','')),
|
|
offsetTop = Number(this.element.style.marginTop.replace('px','')),
|
|
scrollWidth = Number(this.element.style.width.replace('px','')),
|
|
scrollHeight = Number(this.element.style.height.replace('px','')),
|
|
viewerSize = $.getElementSize( this.viewer.canvas );
|
|
this.dragging = true;
|
|
if ( this.element ) {
|
|
if( 'horizontal' == this.scroll ){
|
|
if ( -delta.x > 0 ) {
|
|
//forward
|
|
if( offsetLeft > -(scrollWidth - viewerSize.x)){
|
|
this.element.style.marginLeft = ( offsetLeft + (delta.x * 2) ) + 'px';
|
|
loadPanels( this, viewerSize.x, offsetLeft + (delta.x * 2) );
|
|
}
|
|
} else if ( -delta.x < 0 ) {
|
|
//reverse
|
|
if( offsetLeft < 0 ){
|
|
this.element.style.marginLeft = ( offsetLeft + (delta.x * 2) ) + 'px';
|
|
loadPanels( this, viewerSize.x, offsetLeft + (delta.x * 2) );
|
|
}
|
|
}
|
|
}else{
|
|
if ( -delta.y > 0 ) {
|
|
//forward
|
|
if( offsetTop > -(scrollHeight - viewerSize.y)){
|
|
this.element.style.marginTop = ( offsetTop + (delta.y * 2) ) + 'px';
|
|
loadPanels( this, viewerSize.y, offsetTop + (delta.y * 2) );
|
|
}
|
|
} else if ( -delta.y < 0 ) {
|
|
//reverse
|
|
if( offsetTop < 0 ){
|
|
this.element.style.marginTop = ( offsetTop + (delta.y * 2) ) + 'px';
|
|
loadPanels( this, viewerSize.y, offsetTop + (delta.y * 2) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
*/
|
|
function onStripScroll( tracker, position, scroll, shift ) {
|
|
var offsetLeft = Number(this.element.style.marginLeft.replace('px','')),
|
|
offsetTop = Number(this.element.style.marginTop.replace('px','')),
|
|
scrollWidth = Number(this.element.style.width.replace('px','')),
|
|
scrollHeight = Number(this.element.style.height.replace('px','')),
|
|
viewerSize = $.getElementSize( this.viewer.canvas );
|
|
if ( this.element ) {
|
|
if( 'horizontal' == this.scroll ){
|
|
if ( scroll > 0 ) {
|
|
//forward
|
|
if( offsetLeft > -(scrollWidth - viewerSize.x)){
|
|
this.element.style.marginLeft = ( offsetLeft - (scroll * 60) ) + 'px';
|
|
loadPanels( this, viewerSize.x, offsetLeft - (scroll * 60) );
|
|
}
|
|
} else if ( scroll < 0 ) {
|
|
//reverse
|
|
if( offsetLeft < 0 ){
|
|
this.element.style.marginLeft = ( offsetLeft - (scroll * 60) ) + 'px';
|
|
loadPanels( this, viewerSize.x, offsetLeft - (scroll * 60) );
|
|
}
|
|
}
|
|
}else{
|
|
if ( scroll < 0 ) {
|
|
//scroll up
|
|
if( offsetTop > viewerSize.y - scrollHeight ){
|
|
this.element.style.marginTop = ( offsetTop + (scroll * 60) ) + 'px';
|
|
loadPanels( this, viewerSize.y, offsetTop + (scroll * 60) );
|
|
}
|
|
} else if ( scroll > 0 ) {
|
|
//scroll dowm
|
|
if( offsetTop < 0 ){
|
|
this.element.style.marginTop = ( offsetTop + (scroll * 60) ) + 'px';
|
|
loadPanels( this, viewerSize.y, offsetTop + (scroll * 60) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//cancels event
|
|
return false;
|
|
};
|
|
|
|
|
|
function loadPanels(strip, viewerSize, scroll){
|
|
var panelSize,
|
|
activePanelsStart,
|
|
activePanelsEnd,
|
|
miniViewer,
|
|
i;
|
|
if( 'horizontal' == strip.scroll ){
|
|
panelSize = strip.panelWidth;
|
|
}else{
|
|
panelSize = strip.panelHeight;
|
|
}
|
|
activePanelsStart = Math.ceil( viewerSize / panelSize ) + 5;
|
|
activePanelsEnd = Math.ceil( (Math.abs(scroll) + viewerSize ) / panelSize ) + 1;
|
|
activePanelsStart = activePanelsEnd - activePanelsStart;
|
|
activePanelsStart = activePanelsStart < 0 ? 0 : activePanelsStart;
|
|
|
|
for( i = activePanelsStart; i < activePanelsEnd && i < strip.panels.length; i++ ){
|
|
element = strip.panels[ i ];
|
|
if ( !element.activePanel ){
|
|
miniViewer = new $.Viewer( {
|
|
id: element.id,
|
|
tileSources: [ strip.viewer.tileSources[ i ] ],
|
|
element: element,
|
|
navigatorSizeRatio: strip.sizeRatio,
|
|
minPixelRatio: strip.minPixelRatio,
|
|
showNavigator: false,
|
|
mouseNavEnabled: false,
|
|
showNavigationControl: false,
|
|
showSequenceControl: false
|
|
} );
|
|
|
|
miniViewer.displayRegion = $.makeNeutralElement( "textarea" );
|
|
miniViewer.displayRegion.id = element.id + '-displayregion';
|
|
miniViewer.displayRegion.className = 'displayregion';
|
|
|
|
(function( style ){
|
|
style.position = 'relative';
|
|
style.top = '0px';
|
|
style.left = '0px';
|
|
style.fontSize = '0px';
|
|
style.overflow = 'hidden';
|
|
style.float = 'left'; //Webkit
|
|
style.cssFloat = 'left'; //Firefox
|
|
style.styleFloat = 'left'; //IE
|
|
style.zIndex = 999999999;
|
|
style.cursor = 'default';
|
|
style.width = ( strip.panelWidth - 4 ) + 'px';
|
|
style.height = ( strip.panelHeight - 4 ) + 'px';
|
|
}( miniViewer.displayRegion.style ));
|
|
|
|
miniViewer.displayRegion.innerTracker = new $.MouseTracker({
|
|
element: miniViewer.displayRegion
|
|
});
|
|
|
|
element.getElementsByTagName('form')[ 0 ].appendChild(
|
|
miniViewer.displayRegion
|
|
);
|
|
|
|
element.activePanel = true;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
*/
|
|
function onStripEnter( tracker ) {
|
|
|
|
//$.setElementOpacity(tracker.element, 0.8);
|
|
|
|
//tracker.element.style.border = '1px solid #555';
|
|
//tracker.element.style.background = '#000';
|
|
|
|
if( 'horizontal' == this.scroll ){
|
|
|
|
//tracker.element.style.paddingTop = "0px";
|
|
tracker.element.style.marginBottom = "0px";
|
|
|
|
} else {
|
|
|
|
//tracker.element.style.paddingRight = "0px";
|
|
tracker.element.style.marginLeft = "0px";
|
|
|
|
}
|
|
return false
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
*/
|
|
function onStripExit( tracker ) {
|
|
|
|
var viewerSize = $.getElementSize( this.viewer.element );
|
|
|
|
//$.setElementOpacity(tracker.element, 0.4);
|
|
//tracker.element.style.border = 'none';
|
|
//tracker.element.style.background = '#fff';
|
|
|
|
|
|
if( 'horizontal' == this.scroll ){
|
|
|
|
//tracker.element.style.paddingTop = "10px";
|
|
tracker.element.style.marginBottom = "-" + ( $.getElementSize( tracker.element ).y / 2 ) + "px";
|
|
|
|
} else {
|
|
|
|
//tracker.element.style.paddingRight = "10px";
|
|
tracker.element.style.marginLeft = "-" + ( $.getElementSize( tracker.element ).x / 2 )+ "px";
|
|
|
|
}
|
|
return false;
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* @function
|
|
*/
|
|
function onKeyPress( tracker, keyCode, shiftKey ){
|
|
//console.log( keyCode );
|
|
|
|
switch( keyCode ){
|
|
case 61://=|+
|
|
onStripScroll.call(this, this.tracker, null, 1, null);
|
|
return false;
|
|
case 45://-|_
|
|
onStripScroll.call(this, this.tracker, null, -1, null);
|
|
return false;
|
|
case 48://0|)
|
|
case 119://w
|
|
case 87://W
|
|
case 38://up arrow
|
|
onStripScroll.call(this, this.tracker, null, 1, null);
|
|
return false;
|
|
case 115://s
|
|
case 83://S
|
|
case 40://down arrow
|
|
onStripScroll.call(this, this.tracker, null, -1, null);
|
|
return false;
|
|
case 97://a
|
|
case 37://left arrow
|
|
onStripScroll.call(this, this.tracker, null, -1, null);
|
|
return false;
|
|
case 100://d
|
|
case 39://right arrow
|
|
onStripScroll.call(this, this.tracker, null, 1, null);
|
|
return false;
|
|
default:
|
|
//console.log( 'navigator keycode %s', keyCode );
|
|
return true;
|
|
}
|
|
};
|
|
|
|
|
|
|
|
}( OpenSeadragon ));
|
|
(function( $ ){
|
|
|
|
/**
|
|
* A display rectanlge is very similar to the OpenSeadragon.Rect but adds two
|
|
* fields, 'minLevel' and 'maxLevel' which denote the supported zoom levels
|
|
* for this rectangle.
|
|
* @class
|
|
* @extends OpenSeadragon.Rect
|
|
* @param {Number} x The vector component 'x'.
|
|
* @param {Number} y The vector component 'y'.
|
|
* @param {Number} width The vector component 'height'.
|
|
* @param {Number} height The vector component 'width'.
|
|
* @param {Number} minLevel The lowest zoom level supported.
|
|
* @param {Number} maxLevel The highest zoom level supported.
|
|
* @property {Number} minLevel The lowest zoom level supported.
|
|
* @property {Number} maxLevel The highest zoom level supported.
|
|
*/
|
|
$.DisplayRect = function( x, y, width, height, minLevel, maxLevel ) {
|
|
$.Rect.apply( this, [ x, y, width, height ] );
|
|
|
|
this.minLevel = minLevel;
|
|
this.maxLevel = maxLevel;
|
|
}
|
|
|
|
$.extend( $.DisplayRect.prototype, $.Rect.prototype );
|
|
|
|
}( OpenSeadragon ));
|
|
|
|
(function( $ ){
|
|
|
|
/**
|
|
* @class
|
|
* @param {Object} options - Spring configuration settings.
|
|
* @param {Number} options.initial - Initial value of spring, default to 0 so
|
|
* spring is not in motion initally by default.
|
|
* @param {Number} options.springStiffness - Spring stiffness.
|
|
* @param {Number} options.animationTime - Animation duration per spring.
|
|
*
|
|
* @property {Number} initial - Initial value of spring, default to 0 so
|
|
* spring is not in motion initally by default.
|
|
* @property {Number} springStiffness - Spring stiffness.
|
|
* @property {Number} animationTime - Animation duration per spring.
|
|
* @property {Object} current
|
|
* @property {Number} start
|
|
* @property {Number} target
|
|
*/
|
|
$.Spring = function( options ) {
|
|
var args = arguments;
|
|
|
|
if( typeof( options ) != 'object' ){
|
|
//allows backward compatible use of ( initialValue, config ) as
|
|
//constructor parameters
|
|
options = {
|
|
initial: args.length && typeof ( args[ 0 ] ) == "number" ?
|
|
args[ 0 ] :
|
|
0,
|
|
springStiffness: args.length > 1 ?
|
|
args[ 1 ].springStiffness :
|
|
5.0,
|
|
animationTime: args.length > 1 ?
|
|
args[ 1 ].animationTime :
|
|
1.5
|
|
};
|
|
}
|
|
|
|
$.extend( true, this, options);
|
|
|
|
|
|
this.current = {
|
|
value: typeof ( this.initial ) == "number" ?
|
|
this.initial :
|
|
0,
|
|
time: new Date().getTime() // always work in milliseconds
|
|
};
|
|
|
|
this.start = {
|
|
value: this.current.value,
|
|
time: this.current.time
|
|
};
|
|
|
|
this.target = {
|
|
value: this.current.value,
|
|
time: this.current.time
|
|
};
|
|
};
|
|
|
|
$.Spring.prototype = {
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} target
|
|
*/
|
|
resetTo: function( target ) {
|
|
this.target.value = target;
|
|
this.target.time = this.current.time;
|
|
this.start.value = this.target.value;
|
|
this.start.time = this.target.time;
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} target
|
|
*/
|
|
springTo: function( target ) {
|
|
this.start.value = this.current.value;
|
|
this.start.time = this.current.time;
|
|
this.target.value = target;
|
|
this.target.time = this.start.time + 1000 * this.animationTime;
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Number} delta
|
|
*/
|
|
shiftBy: function( delta ) {
|
|
this.start.value += delta;
|
|
this.target.value += delta;
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
update: function() {
|
|
this.current.time = new Date().getTime();
|
|
this.current.value = (this.current.time >= this.target.time) ?
|
|
this.target.value :
|
|
this.start.value +
|
|
( this.target.value - this.start.value ) *
|
|
transform(
|
|
this.springStiffness,
|
|
( this.current.time - this.start.time ) /
|
|
( this.target.time - this.start.time )
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
function transform( stiffness, x ) {
|
|
return ( 1.0 - Math.exp( stiffness * -x ) ) /
|
|
( 1.0 - Math.exp( -stiffness ) );
|
|
};
|
|
|
|
}( OpenSeadragon ));
|
|
|
|
(function( $ ){
|
|
|
|
/**
|
|
* @class
|
|
* @param {Number} level The zoom level this tile belongs to.
|
|
* @param {Number} x The vector component 'x'.
|
|
* @param {Number} y The vector component 'y'.
|
|
* @param {OpenSeadragon.Point} bounds Where this tile fits, in normalized
|
|
* coordinates.
|
|
* @param {Boolean} exists Is this tile a part of a sparse image? ( Also has
|
|
* this tile failed to load? )
|
|
* @param {String} url The URL of this tile's image.
|
|
*
|
|
* @property {Number} level The zoom level this tile belongs to.
|
|
* @property {Number} x The vector component 'x'.
|
|
* @property {Number} y The vector component 'y'.
|
|
* @property {OpenSeadragon.Point} bounds Where this tile fits, in normalized
|
|
* coordinates
|
|
* @property {Boolean} exists Is this tile a part of a sparse image? ( Also has
|
|
* this tile failed to load?
|
|
* @property {String} url The URL of this tile's image.
|
|
* @property {Boolean} loaded Is this tile loaded?
|
|
* @property {Boolean} loading Is this tile loading
|
|
* @property {Element} element The HTML element for this tile
|
|
* @property {Image} image The Image object for this tile
|
|
* @property {String} style The alias of this.element.style.
|
|
* @property {String} position This tile's position on screen, in pixels.
|
|
* @property {String} size This tile's size on screen, in pixels
|
|
* @property {String} blendStart The start time of this tile's blending
|
|
* @property {String} opacity The current opacity this tile should be.
|
|
* @property {String} distance The distance of this tile to the viewport center
|
|
* @property {String} visibility The visibility score of this tile.
|
|
* @property {Boolean} beingDrawn Whether this tile is currently being drawn
|
|
* @property {Number} lastTouchTime Timestamp the tile was last touched.
|
|
*/
|
|
$.Tile = function(level, x, y, bounds, exists, url) {
|
|
this.level = level;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.bounds = bounds;
|
|
this.exists = exists;
|
|
this.url = url;
|
|
this.loaded = false;
|
|
this.loading = false;
|
|
|
|
this.element = null;
|
|
this.image = null;
|
|
|
|
this.style = null;
|
|
this.position = null;
|
|
this.size = null;
|
|
this.blendStart = null;
|
|
this.opacity = null;
|
|
this.distance = null;
|
|
this.visibility = null;
|
|
|
|
this.beingDrawn = false;
|
|
this.lastTouchTime = 0;
|
|
};
|
|
|
|
$.Tile.prototype = {
|
|
|
|
/**
|
|
* Provides a string representation of this tiles level and (x,y)
|
|
* components.
|
|
* @function
|
|
* @returns {String}
|
|
*/
|
|
toString: function() {
|
|
return this.level + "/" + this.x + "_" + this.y;
|
|
},
|
|
|
|
/**
|
|
* Renders the tile in an html container.
|
|
* @function
|
|
* @param {Element} container
|
|
*/
|
|
drawHTML: function( container ) {
|
|
|
|
var position = this.position.apply( Math.floor ),
|
|
size = this.size.apply( Math.ceil );
|
|
|
|
if ( !this.loaded || !this.image ) {
|
|
$.console.warn(
|
|
"Attempting to draw tile %s when it's not yet loaded.",
|
|
this.toString()
|
|
);
|
|
return;
|
|
}
|
|
|
|
if ( !this.element ) {
|
|
this.element = $.makeNeutralElement("img");
|
|
this.element.src = this.url;
|
|
this.style = this.element.style;
|
|
|
|
this.style.position = "absolute";
|
|
this.style.msInterpolationMode = "nearest-neighbor";
|
|
}
|
|
|
|
|
|
if ( this.element.parentNode != container ) {
|
|
container.appendChild( this.element );
|
|
}
|
|
|
|
this.element.style.left = position.x + "px";
|
|
this.element.style.top = position.y + "px";
|
|
this.element.style.width = size.x + "px";
|
|
this.element.style.height = size.y + "px";
|
|
|
|
$.setElementOpacity( this.element, this.opacity );
|
|
|
|
},
|
|
|
|
/**
|
|
* Renders the tile in a canvas-based context.
|
|
* @function
|
|
* @param {Canvas} context
|
|
*/
|
|
drawCanvas: function( context ) {
|
|
|
|
var position = this.position,
|
|
size = this.size;
|
|
|
|
if ( !this.loaded || !this.image ) {
|
|
$.console.warn(
|
|
"Attempting to draw tile %s when it's not yet loaded.",
|
|
this.toString()
|
|
);
|
|
return;
|
|
}
|
|
context.globalAlpha = this.opacity;
|
|
context.drawImage( this.image, position.x, position.y, size.x, size.y );
|
|
},
|
|
|
|
/**
|
|
* Removes tile from it's contianer.
|
|
* @function
|
|
*/
|
|
unload: function() {
|
|
if ( this.element && this.element.parentNode ) {
|
|
this.element.parentNode.removeChild( this.element );
|
|
}
|
|
|
|
this.element = null;
|
|
this.image = null;
|
|
this.loaded = false;
|
|
this.loading = false;
|
|
}
|
|
};
|
|
|
|
}( OpenSeadragon ));
|
|
|
|
(function( $ ){
|
|
|
|
/**
|
|
* An enumeration of positions that an overlay may be assigned relative
|
|
* to the viewport including CENTER, TOP_LEFT (default), TOP, TOP_RIGHT,
|
|
* RIGHT, BOTTOM_RIGHT, BOTTOM, BOTTOM_LEFT, and LEFT.
|
|
* @static
|
|
*/
|
|
$.OverlayPlacement = {
|
|
CENTER: 0,
|
|
TOP_LEFT: 1,
|
|
TOP: 2,
|
|
TOP_RIGHT: 3,
|
|
RIGHT: 4,
|
|
BOTTOM_RIGHT: 5,
|
|
BOTTOM: 6,
|
|
BOTTOM_LEFT: 7,
|
|
LEFT: 8
|
|
};
|
|
|
|
/**
|
|
* An Overlay provides a
|
|
* @class
|
|
*/
|
|
$.Overlay = function( element, location, placement ) {
|
|
this.element = element;
|
|
this.scales = location instanceof $.Rect;
|
|
this.bounds = new $.Rect(
|
|
location.x,
|
|
location.y,
|
|
location.width,
|
|
location.height
|
|
);
|
|
this.position = new $.Point(
|
|
location.x,
|
|
location.y
|
|
);
|
|
this.size = new $.Point(
|
|
location.width,
|
|
location.height
|
|
);
|
|
this.style = element.style;
|
|
// rects are always top-left
|
|
this.placement = location instanceof $.Point ?
|
|
placement :
|
|
$.OverlayPlacement.TOP_LEFT;
|
|
};
|
|
|
|
$.Overlay.prototype = {
|
|
|
|
/**
|
|
* @function
|
|
* @param {OpenSeadragon.OverlayPlacement} position
|
|
* @param {OpenSeadragon.Point} size
|
|
*/
|
|
adjust: function( position, size ) {
|
|
switch ( this.placement ) {
|
|
case $.OverlayPlacement.TOP_LEFT:
|
|
break;
|
|
case $.OverlayPlacement.TOP:
|
|
position.x -= size.x / 2;
|
|
break;
|
|
case $.OverlayPlacement.TOP_RIGHT:
|
|
position.x -= size.x;
|
|
break;
|
|
case $.OverlayPlacement.RIGHT:
|
|
position.x -= size.x;
|
|
position.y -= size.y / 2;
|
|
break;
|
|
case $.OverlayPlacement.BOTTOM_RIGHT:
|
|
position.x -= size.x;
|
|
position.y -= size.y;
|
|
break;
|
|
case $.OverlayPlacement.BOTTOM:
|
|
position.x -= size.x / 2;
|
|
position.y -= size.y;
|
|
break;
|
|
case $.OverlayPlacement.BOTTOM_LEFT:
|
|
position.y -= size.y;
|
|
break;
|
|
case $.OverlayPlacement.LEFT:
|
|
position.y -= size.y / 2;
|
|
break;
|
|
case $.OverlayPlacement.CENTER:
|
|
default:
|
|
position.x -= size.x / 2;
|
|
position.y -= size.y / 2;
|
|
break;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
destroy: function() {
|
|
var element = this.element,
|
|
style = this.style;
|
|
|
|
if ( element.parentNode ) {
|
|
element.parentNode.removeChild( element );
|
|
}
|
|
|
|
style.top = "";
|
|
style.left = "";
|
|
style.position = "";
|
|
|
|
if ( this.scales ) {
|
|
style.width = "";
|
|
style.height = "";
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Element} container
|
|
*/
|
|
drawHTML: function( container ) {
|
|
var element = this.element,
|
|
style = this.style,
|
|
scales = this.scales,
|
|
position,
|
|
size;
|
|
|
|
if ( element.parentNode != container ) {
|
|
container.appendChild( element );
|
|
}
|
|
|
|
if ( !scales ) {
|
|
this.size = $.getElementSize( element );
|
|
}
|
|
|
|
position = this.position;
|
|
size = this.size;
|
|
|
|
this.adjust( position, size );
|
|
|
|
position = position.apply( Math.floor );
|
|
size = size.apply( Math.ceil );
|
|
|
|
style.left = position.x + "px";
|
|
style.top = position.y + "px";
|
|
style.position = "absolute";
|
|
|
|
if ( scales ) {
|
|
style.width = size.x + "px";
|
|
style.height = size.y + "px";
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {OpenSeadragon.Point|OpenSeadragon.Rect} location
|
|
* @param {OpenSeadragon.OverlayPlacement} position
|
|
*/
|
|
update: function( location, placement ) {
|
|
this.scales = location instanceof $.Rect;
|
|
this.bounds = new $.Rect(
|
|
location.x,
|
|
location.y,
|
|
location.width,
|
|
location.height
|
|
);
|
|
// rects are always top-left
|
|
this.placement = location instanceof $.Point ?
|
|
placement :
|
|
$.OverlayPlacement.TOP_LEFT;
|
|
}
|
|
|
|
};
|
|
|
|
}( OpenSeadragon ));
|
|
|
|
(function( $ ){
|
|
|
|
var TIMEOUT = 5000,
|
|
DEVICE_SCREEN = $.getWindowSize(),
|
|
BROWSER = $.Browser.vendor,
|
|
BROWSER_VERSION = $.Browser.version,
|
|
|
|
SUBPIXEL_RENDERING = (
|
|
( BROWSER == $.BROWSERS.FIREFOX ) ||
|
|
( BROWSER == $.BROWSERS.OPERA ) ||
|
|
( BROWSER == $.BROWSERS.SAFARI && BROWSER_VERSION >= 4 ) ||
|
|
( BROWSER == $.BROWSERS.CHROME && BROWSER_VERSION >= 2 ) ||
|
|
( BROWSER == $.BROWSERS.IE && BROWSER_VERSION >= 9 )
|
|
),
|
|
|
|
USE_CANVAS = SUBPIXEL_RENDERING
|
|
&& !( DEVICE_SCREEN.x <= 400 || DEVICE_SCREEN.y <= 400 )
|
|
&& !( navigator.appVersion.match( 'Mobile' ) )
|
|
&& $.isFunction( document.createElement( "canvas" ).getContext );
|
|
|
|
//console.error( 'USE_CANVAS ' + USE_CANVAS );
|
|
|
|
/**
|
|
* @class
|
|
* @param {OpenSeadragon.TileSource} source - Reference to Viewer tile source.
|
|
* @param {OpenSeadragon.Viewport} viewport - Reference to Viewer viewport.
|
|
* @param {Element} element - Reference to Viewer 'canvas'.
|
|
* @property {OpenSeadragon.TileSource} source - Reference to Viewer tile source.
|
|
* @property {OpenSeadragon.Viewport} viewport - Reference to Viewer viewport.
|
|
* @property {Element} container - Reference to Viewer 'canvas'.
|
|
* @property {Element|Canvas} canvas - TODO
|
|
* @property {CanvasContext} context - TODO
|
|
* @property {Object} config - Reference to Viewer config.
|
|
* @property {Number} downloading - How many images are currently being loaded in parallel.
|
|
* @property {Number} normHeight - Ratio of zoomable image height to width.
|
|
* @property {Object} tilesMatrix - A '3d' dictionary [level][x][y] --> Tile.
|
|
* @property {Array} tilesLoaded - An unordered list of Tiles with loaded images.
|
|
* @property {Object} coverage - A '3d' dictionary [level][x][y] --> Boolean.
|
|
* @property {Array} overlays - An unordered list of Overlays added.
|
|
* @property {Array} lastDrawn - An unordered list of Tiles drawn last frame.
|
|
* @property {Number} lastResetTime - Last time for which the drawer was reset.
|
|
* @property {Boolean} midUpdate - Is the drawer currently updating the viewport?
|
|
* @property {Boolean} updateAgain - Does the drawer need to update the viewort again?
|
|
* @property {Element} element - DEPRECATED Alias for container.
|
|
*/
|
|
$.Drawer = function( options ) {
|
|
|
|
//backward compatibility for positional args while prefering more
|
|
//idiomatic javascript options object as the only argument
|
|
var args = arguments,
|
|
i;
|
|
|
|
if( !$.isPlainObject( options ) ){
|
|
options = {
|
|
source: args[ 0 ],
|
|
viewport: args[ 1 ],
|
|
element: args[ 2 ]
|
|
};
|
|
}
|
|
|
|
$.extend( true, this, {
|
|
|
|
//internal state properties
|
|
downloading: 0,
|
|
tilesMatrix: {},
|
|
tilesLoaded: [],
|
|
coverage: {},
|
|
lastDrawn: [],
|
|
lastResetTime: 0,
|
|
midUpdate: false,
|
|
updateAgain: true,
|
|
|
|
//internal state / configurable settings
|
|
overlays: [],
|
|
collectionOverlays: {},
|
|
|
|
//configurable settings
|
|
maxImageCacheCount: $.DEFAULT_SETTINGS.maxImageCacheCount,
|
|
imageLoaderLimit: $.DEFAULT_SETTINGS.imageLoaderLimit,
|
|
minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio,
|
|
wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal,
|
|
wrapVertical: $.DEFAULT_SETTINGS.wrapVertical,
|
|
immediateRender: $.DEFAULT_SETTINGS.immediateRender,
|
|
blendTime: $.DEFAULT_SETTINGS.blendTime,
|
|
alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend,
|
|
minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio,
|
|
debugMode: $.DEFAULT_SETTINGS.debugMode
|
|
|
|
}, options );
|
|
|
|
this.container = $.getElement( this.element );
|
|
this.canvas = $.makeNeutralElement( USE_CANVAS ? "canvas" : "div" );
|
|
this.context = USE_CANVAS ? this.canvas.getContext( "2d" ) : null;
|
|
this.normHeight = this.source.dimensions.y / this.source.dimensions.x;
|
|
this.element = this.container;
|
|
|
|
|
|
this.canvas.style.width = "100%";
|
|
this.canvas.style.height = "100%";
|
|
this.canvas.style.position = "absolute";
|
|
|
|
// explicit left-align
|
|
this.container.style.textAlign = "left";
|
|
this.container.appendChild( this.canvas );
|
|
|
|
//create the correct type of overlay by convention if the overlays
|
|
//are not already OpenSeadragon.Overlays
|
|
for( i = 0; i < this.overlays.length; i++ ){
|
|
if( $.isPlainObject( this.overlays[ i ] ) ){
|
|
|
|
(function( _this, overlay ){
|
|
|
|
var link = document.createElement("a"),
|
|
rect = new $.Rect(
|
|
overlay.x,
|
|
overlay.y,
|
|
overlay.width,
|
|
overlay.height
|
|
),
|
|
id = Math.floor(Math.random()*10000000);
|
|
|
|
link.href = "#/overlay/"+id;
|
|
link.id = id;
|
|
link.className = overlay.className ?
|
|
overlay.className :
|
|
"openseadragon-overlay";
|
|
|
|
_this.overlays[ i ] = new $.Overlay( link, rect );
|
|
|
|
}( this, this.overlays[ i ] ));
|
|
|
|
} else if ( $.isFunction( this.overlays[ i ] ) ){
|
|
//TODO
|
|
}
|
|
}
|
|
|
|
//this.profiler = new $.Profiler();
|
|
};
|
|
|
|
$.Drawer.prototype = {
|
|
|
|
/**
|
|
* 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.
|
|
* @method
|
|
* @param {Element|String} element - A reference to an element or an id for
|
|
* the element which will overlayed.
|
|
* @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.
|
|
*/
|
|
addOverlay: function( element, location, placement ) {
|
|
element = $.getElement( element );
|
|
|
|
if ( getOverlayIndex( this.overlays, element ) >= 0 ) {
|
|
// they're trying to add a duplicate overlay
|
|
return;
|
|
}
|
|
|
|
this.overlays.push( new $.Overlay( element, location, placement ) );
|
|
this.updateAgain = true;
|
|
},
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
updateOverlay: function( element, location, placement ) {
|
|
var i;
|
|
|
|
element = $.getElement( element );
|
|
i = getOverlayIndex( this.overlays, element );
|
|
|
|
if ( i >= 0 ) {
|
|
this.overlays[ i ].update( location, placement );
|
|
this.updateAgain = true;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Removes and overlay identified by the reference element or element id
|
|
* and schedules and update.
|
|
* @method
|
|
* @param {Element|String} element - A reference to the element or an
|
|
* element id which represent the ovelay content to be removed.
|
|
*/
|
|
removeOverlay: function( element ) {
|
|
var i;
|
|
|
|
element = $.getElement( element );
|
|
i = getOverlayIndex( this.overlays, element );
|
|
|
|
if ( i >= 0 ) {
|
|
this.overlays[ i ].destroy();
|
|
this.overlays.splice( i, 1 );
|
|
this.updateAgain = true;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Removes all currently configured Overlays from this Drawer and schedules
|
|
* and update.
|
|
* @method
|
|
*/
|
|
clearOverlays: function() {
|
|
while ( this.overlays.length > 0 ) {
|
|
this.overlays.pop().destroy();
|
|
this.updateAgain = true;
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* Returns whether the Drawer is scheduled for an update at the
|
|
* soonest possible opportunity.
|
|
* @method
|
|
* @returns {Boolean} - Whether the Drawer is scheduled for an update at the
|
|
* soonest possible opportunity.
|
|
*/
|
|
needsUpdate: function() {
|
|
return this.updateAgain;
|
|
},
|
|
|
|
/**
|
|
* Returns the total number of tiles that have been loaded by this Drawer.
|
|
* @method
|
|
* @returns {Number} - The total number of tiles that have been loaded by
|
|
* this Drawer.
|
|
*/
|
|
numTilesLoaded: function() {
|
|
return this.tilesLoaded.length;
|
|
},
|
|
|
|
/**
|
|
* Clears all tiles and triggers an update on the next call to
|
|
* Drawer.prototype.update().
|
|
* @method
|
|
*/
|
|
reset: function() {
|
|
clearTiles( this );
|
|
this.lastResetTime = +new Date();
|
|
this.updateAgain = true;
|
|
},
|
|
|
|
/**
|
|
* Forces the Drawer to update.
|
|
* @method
|
|
*/
|
|
update: function() {
|
|
//this.profiler.beginUpdate();
|
|
this.midUpdate = true;
|
|
updateViewport( this );
|
|
this.midUpdate = false;
|
|
//this.profiler.endUpdate();
|
|
},
|
|
|
|
/**
|
|
* Used internally to load images when required. May also be used to
|
|
* preload a set of images so the browser will have them available in
|
|
* the local cache to optimize user experience in certain cases. Because
|
|
* the number of parallel image loads is configurable, if too many images
|
|
* are currently being loaded, the request will be ignored. Since by
|
|
* default drawer.imageLoaderLimit is 0, the native browser parallel
|
|
* image loading policy will be used.
|
|
* @method
|
|
* @param {String} src - The url of the image to load.
|
|
* @param {Function} callback - The function that will be called with the
|
|
* Image object as the only parameter, whether on 'load' or on 'abort'.
|
|
* For now this means the callback is expected to distinguish between
|
|
* error and success conditions by inspecting the Image object.
|
|
* @return {Boolean} loading - Wheter the request was submitted or ignored
|
|
* based on OpenSeadragon.DEFAULT_SETTINGS.imageLoaderLimit.
|
|
*/
|
|
loadImage: function( src, callback ) {
|
|
var _this = this,
|
|
loading = false,
|
|
image,
|
|
jobid,
|
|
complete;
|
|
|
|
if ( !this.imageLoaderLimit ||
|
|
this.downloading < this.imageLoaderLimit ) {
|
|
|
|
this.downloading++;
|
|
|
|
image = new Image();
|
|
|
|
complete = function( imagesrc ){
|
|
_this.downloading--;
|
|
if (typeof ( callback ) == "function") {
|
|
try {
|
|
callback( image );
|
|
} catch ( e ) {
|
|
$.console.error(
|
|
"%s while executing %s callback: %s",
|
|
e.name,
|
|
src,
|
|
e.message,
|
|
e
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
image.onload = function(){
|
|
finishLoadingImage( image, complete, true );
|
|
};
|
|
|
|
image.onabort = image.onerror = function(){
|
|
finishLoadingImage( image, complete, false );
|
|
};
|
|
|
|
jobid = window.setTimeout( function(){
|
|
finishLoadingImage( image, complete, false, jobid );
|
|
}, TIMEOUT );
|
|
|
|
loading = true;
|
|
image.src = src;
|
|
}
|
|
|
|
return loading;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* Pretty much every other line in this needs to be documented so its clear
|
|
* how each piece of this routine contributes to the drawing process. That's
|
|
* why there are so many TODO's inside this function.
|
|
*/
|
|
function updateViewport( drawer ) {
|
|
|
|
drawer.updateAgain = false;
|
|
|
|
var tile,
|
|
level,
|
|
best = null,
|
|
haveDrawn = false,
|
|
currentTime = +new Date(),
|
|
viewportSize = drawer.viewport.getContainerSize(),
|
|
viewportBounds = drawer.viewport.getBounds( true ),
|
|
viewportTL = viewportBounds.getTopLeft(),
|
|
viewportBR = viewportBounds.getBottomRight(),
|
|
zeroRatioC = drawer.viewport.deltaPixelsFromPoints(
|
|
drawer.source.getPixelRatio( 0 ),
|
|
true
|
|
).x,
|
|
lowestLevel = Math.max(
|
|
drawer.source.minLevel,
|
|
Math.floor(
|
|
Math.log( drawer.minZoomImageRatio ) /
|
|
Math.log( 2 )
|
|
)
|
|
),
|
|
highestLevel = Math.min(
|
|
Math.abs(drawer.source.maxLevel),
|
|
Math.abs(Math.floor(
|
|
Math.log( zeroRatioC / drawer.minPixelRatio ) /
|
|
Math.log( 2 )
|
|
))
|
|
),
|
|
renderPixelRatioC,
|
|
renderPixelRatioT,
|
|
zeroRatioT,
|
|
optimalRatio,
|
|
levelOpacity,
|
|
levelVisibility;
|
|
|
|
//TODO
|
|
while ( drawer.lastDrawn.length > 0 ) {
|
|
tile = drawer.lastDrawn.pop();
|
|
tile.beingDrawn = false;
|
|
}
|
|
|
|
//TODO
|
|
drawer.canvas.innerHTML = "";
|
|
if ( USE_CANVAS ) {
|
|
drawer.canvas.width = viewportSize.x;
|
|
drawer.canvas.height = viewportSize.y;
|
|
drawer.context.clearRect( 0, 0, viewportSize.x, viewportSize.y );
|
|
}
|
|
|
|
//TODO
|
|
if ( !drawer.wrapHorizontal &&
|
|
( viewportBR.x < 0 || viewportTL.x > 1 ) ) {
|
|
return;
|
|
} else if
|
|
( !drawer.wrapVertical &&
|
|
( viewportBR.y < 0 || viewportTL.y > drawer.normHeight ) ) {
|
|
return;
|
|
}
|
|
|
|
//TODO
|
|
if ( !drawer.wrapHorizontal ) {
|
|
viewportTL.x = Math.max( viewportTL.x, 0 );
|
|
viewportBR.x = Math.min( viewportBR.x, 1 );
|
|
}
|
|
if ( !drawer.wrapVertical ) {
|
|
viewportTL.y = Math.max( viewportTL.y, 0 );
|
|
viewportBR.y = Math.min( viewportBR.y, drawer.normHeight );
|
|
}
|
|
|
|
//TODO
|
|
lowestLevel = Math.min( lowestLevel, highestLevel );
|
|
|
|
//TODO
|
|
for ( level = highestLevel; level >= lowestLevel; level-- ) {
|
|
|
|
//Avoid calculations for draw if we have already drawn this
|
|
renderPixelRatioC = drawer.viewport.deltaPixelsFromPoints(
|
|
drawer.source.getPixelRatio( level ),
|
|
true
|
|
).x;
|
|
|
|
if ( ( !haveDrawn && renderPixelRatioC >= drawer.minPixelRatio ) ||
|
|
( level == lowestLevel ) ) {
|
|
drawLevel = true;
|
|
haveDrawn = true;
|
|
} else if ( !haveDrawn ) {
|
|
continue;
|
|
}
|
|
|
|
renderPixelRatioT = drawer.viewport.deltaPixelsFromPoints(
|
|
drawer.source.getPixelRatio( level ),
|
|
false
|
|
).x;
|
|
|
|
zeroRatioT = drawer.viewport.deltaPixelsFromPoints(
|
|
drawer.source.getPixelRatio( 0 ),
|
|
false
|
|
).x;
|
|
|
|
optimalRatio = drawer.immediateRender ?
|
|
1 :
|
|
zeroRatioT;
|
|
|
|
levelOpacity = Math.min( 1, ( renderPixelRatioC - 0.5 ) / 0.5 );
|
|
|
|
levelVisibility = optimalRatio / Math.abs(
|
|
optimalRatio - renderPixelRatioT
|
|
);
|
|
|
|
//TODO
|
|
best = updateLevel(
|
|
drawer,
|
|
haveDrawn,
|
|
level,
|
|
levelOpacity,
|
|
levelVisibility,
|
|
viewportTL,
|
|
viewportBR,
|
|
currentTime,
|
|
best
|
|
);
|
|
|
|
//TODO
|
|
if ( providesCoverage( drawer.coverage, level ) ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
//TODO
|
|
drawTiles( drawer, drawer.lastDrawn );
|
|
drawOverlays( drawer.viewport, drawer.overlays, drawer.container );
|
|
|
|
//TODO
|
|
if ( best ) {
|
|
loadTile( drawer, best, currentTime );
|
|
// because we haven't finished drawing, so
|
|
drawer.updateAgain = true;
|
|
}
|
|
};
|
|
|
|
|
|
function updateLevel( drawer, haveDrawn, level, levelOpacity, levelVisibility, viewportTL, viewportBR, currentTime, best ){
|
|
|
|
var x, y,
|
|
tileTL,
|
|
tileBR,
|
|
numberOfTiles,
|
|
viewportCenter = drawer.viewport.pixelFromPoint( drawer.viewport.getCenter() );
|
|
|
|
|
|
//OK, a new drawing so do your calculations
|
|
tileTL = drawer.source.getTileAtPoint( level, viewportTL );
|
|
tileBR = drawer.source.getTileAtPoint( level, viewportBR );
|
|
numberOfTiles = drawer.source.getNumTiles( level );
|
|
|
|
resetCoverage( drawer.coverage, level );
|
|
|
|
if ( !drawer.wrapHorizontal ) {
|
|
tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 );
|
|
}
|
|
if ( !drawer.wrapVertical ) {
|
|
tileBR.y = Math.min( tileBR.y, numberOfTiles.y - 1 );
|
|
}
|
|
|
|
for ( x = tileTL.x; x <= tileBR.x; x++ ) {
|
|
for ( y = tileTL.y; y <= tileBR.y; y++ ) {
|
|
|
|
best = updateTile(
|
|
drawer,
|
|
drawLevel,
|
|
haveDrawn,
|
|
x, y,
|
|
level,
|
|
levelOpacity,
|
|
levelVisibility,
|
|
viewportCenter,
|
|
numberOfTiles,
|
|
currentTime,
|
|
best
|
|
);
|
|
|
|
}
|
|
}
|
|
return best;
|
|
};
|
|
|
|
function updateTile( drawer, drawLevel, haveDrawn, x, y, level, levelOpacity, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){
|
|
|
|
var tile = getTile(
|
|
x, y,
|
|
level,
|
|
drawer.source,
|
|
drawer.tilesMatrix,
|
|
currentTime,
|
|
numberOfTiles,
|
|
drawer.normHeight
|
|
),
|
|
drawTile = drawLevel,
|
|
newbest;
|
|
|
|
setCoverage( drawer.coverage, level, x, y, false );
|
|
|
|
if ( !tile.exists ) {
|
|
return best;
|
|
}
|
|
|
|
if ( haveDrawn && !drawTile ) {
|
|
if ( isCovered( drawer.coverage, level, x, y ) ) {
|
|
setCoverage( drawer.coverage, level, x, y, true );
|
|
} else {
|
|
drawTile = true;
|
|
}
|
|
}
|
|
|
|
if ( !drawTile ) {
|
|
return best;
|
|
}
|
|
|
|
positionTile(
|
|
tile,
|
|
drawer.source.tileOverlap,
|
|
drawer.viewport,
|
|
viewportCenter,
|
|
levelVisibility
|
|
);
|
|
|
|
if ( tile.loaded ) {
|
|
|
|
drawer.updateAgain = blendTile(
|
|
drawer,
|
|
tile,
|
|
x, y,
|
|
level,
|
|
levelOpacity,
|
|
currentTime
|
|
);
|
|
} else if ( tile.loading ) {
|
|
// the tile is already in the download queue
|
|
// thanks josh1093 for finally translating this typo
|
|
} else {
|
|
best = compareTiles( best, tile );
|
|
}
|
|
|
|
return best;
|
|
};
|
|
|
|
function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, normHeight ) {
|
|
var xMod,
|
|
yMod,
|
|
bounds,
|
|
exists,
|
|
url,
|
|
tile;
|
|
|
|
if ( !tilesMatrix[ level ] ) {
|
|
tilesMatrix[ level ] = {};
|
|
}
|
|
if ( !tilesMatrix[ level ][ x ] ) {
|
|
tilesMatrix[ level ][ x ] = {};
|
|
}
|
|
|
|
if ( !tilesMatrix[ level ][ x ][ y ] ) {
|
|
xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x;
|
|
yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y;
|
|
bounds = tileSource.getTileBounds( level, xMod, yMod );
|
|
exists = tileSource.tileExists( level, xMod, yMod );
|
|
url = tileSource.getTileUrl( level, xMod, yMod );
|
|
|
|
bounds.x += 1.0 * ( x - xMod ) / numTiles.x;
|
|
bounds.y += normHeight * ( y - yMod ) / numTiles.y;
|
|
|
|
tilesMatrix[ level ][ x ][ y ] = new $.Tile(
|
|
level,
|
|
x,
|
|
y,
|
|
bounds,
|
|
exists,
|
|
url
|
|
);
|
|
}
|
|
|
|
tile = tilesMatrix[ level ][ x ][ y ];
|
|
tile.lastTouchTime = time;
|
|
|
|
return tile;
|
|
};
|
|
|
|
|
|
function loadTile( drawer, tile, time ) {
|
|
tile.loading = drawer.loadImage(
|
|
tile.url,
|
|
function( image ){
|
|
onTileLoad( drawer, tile, time, image );
|
|
}
|
|
);
|
|
};
|
|
|
|
function onTileLoad( drawer, tile, time, image ) {
|
|
var insertionIndex,
|
|
cutoff,
|
|
worstTile,
|
|
worstTime,
|
|
worstLevel,
|
|
worstTileIndex,
|
|
prevTile,
|
|
prevTime,
|
|
prevLevel,
|
|
i;
|
|
|
|
tile.loading = false;
|
|
|
|
if ( drawer.midUpdate ) {
|
|
$.console.warn( "Tile load callback in middle of drawing routine." );
|
|
return;
|
|
} else if ( !image ) {
|
|
$.console.log( "Tile %s failed to load: %s", tile, tile.url );
|
|
tile.exists = false;
|
|
return;
|
|
} else if ( time < drawer.lastResetTime ) {
|
|
$.console.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url );
|
|
return;
|
|
}
|
|
|
|
tile.loaded = true;
|
|
tile.image = image;
|
|
|
|
insertionIndex = drawer.tilesLoaded.length;
|
|
|
|
if ( drawer.tilesLoaded.length >= drawer.maxImageCacheCount ) {
|
|
cutoff = Math.ceil( Math.log( drawer.source.tileSize ) / Math.log( 2 ) );
|
|
|
|
worstTile = null;
|
|
worstTileIndex = -1;
|
|
|
|
for ( i = drawer.tilesLoaded.length - 1; i >= 0; i-- ) {
|
|
prevTile = drawer.tilesLoaded[ i ];
|
|
|
|
if ( prevTile.level <= drawer.cutoff || prevTile.beingDrawn ) {
|
|
continue;
|
|
} else if ( !worstTile ) {
|
|
worstTile = prevTile;
|
|
worstTileIndex = i;
|
|
continue;
|
|
}
|
|
|
|
prevTime = prevTile.lastTouchTime;
|
|
worstTime = worstTile.lastTouchTime;
|
|
prevLevel = prevTile.level;
|
|
worstLevel = worstTile.level;
|
|
|
|
if ( prevTime < worstTime ||
|
|
( prevTime == worstTime && prevLevel > worstLevel ) ) {
|
|
worstTile = prevTile;
|
|
worstTileIndex = i;
|
|
}
|
|
}
|
|
|
|
if ( worstTile && worstTileIndex >= 0 ) {
|
|
worstTile.unload();
|
|
insertionIndex = worstTileIndex;
|
|
}
|
|
}
|
|
|
|
drawer.tilesLoaded[ insertionIndex ] = tile;
|
|
drawer.updateAgain = true;
|
|
};
|
|
|
|
|
|
function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility ){
|
|
var boundsTL = tile.bounds.getTopLeft(),
|
|
boundsSize = tile.bounds.getSize(),
|
|
positionC = viewport.pixelFromPoint( boundsTL, true ),
|
|
positionT = viewport.pixelFromPoint( boundsTL, false ),
|
|
sizeC = viewport.deltaPixelsFromPoints( boundsSize, true ),
|
|
sizeT = viewport.deltaPixelsFromPoints( boundsSize, false ),
|
|
tileCenter = positionT.plus( sizeT.divide( 2 ) ),
|
|
tileDistance = viewportCenter.distanceTo( tileCenter );
|
|
|
|
if ( !overlap ) {
|
|
sizeC = sizeC.plus( new $.Point( 1, 1 ) );
|
|
}
|
|
|
|
tile.position = positionC;
|
|
tile.size = sizeC;
|
|
tile.distance = tileDistance;
|
|
tile.visibility = levelVisibility;
|
|
};
|
|
|
|
|
|
function blendTile( drawer, tile, x, y, level, levelOpacity, currentTime ){
|
|
var blendTimeMillis = 1000 * drawer.blendTime,
|
|
deltaTime,
|
|
opacity;
|
|
|
|
if ( !tile.blendStart ) {
|
|
tile.blendStart = currentTime;
|
|
}
|
|
|
|
deltaTime = currentTime - tile.blendStart;
|
|
opacity = Math.min( 1, deltaTime / blendTimeMillis );
|
|
|
|
if ( drawer.alwaysBlend ) {
|
|
opacity *= levelOpacity;
|
|
}
|
|
|
|
tile.opacity = opacity;
|
|
|
|
drawer.lastDrawn.push( tile );
|
|
|
|
if ( opacity == 1 ) {
|
|
setCoverage( drawer.coverage, level, x, y, true );
|
|
} else if ( deltaTime < blendTimeMillis ) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
|
|
function clearTiles( drawer ) {
|
|
drawer.tilesMatrix = {};
|
|
drawer.tilesLoaded = [];
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* Returns true if the given tile provides coverage to lower-level tiles of
|
|
* lower resolution representing the same content. If neither x nor y is
|
|
* given, returns true if the entire visible level provides coverage.
|
|
*
|
|
* Note that out-of-bounds tiles provide coverage in this sense, since
|
|
* there's no content that they would need to cover. Tiles at non-existent
|
|
* levels that are within the image bounds, however, do not.
|
|
*/
|
|
function providesCoverage( coverage, level, x, y ) {
|
|
var rows,
|
|
cols,
|
|
i, j;
|
|
|
|
if ( !coverage[ level ] ) {
|
|
return false;
|
|
}
|
|
|
|
if ( x === undefined || y === undefined ) {
|
|
rows = coverage[ level ];
|
|
for ( i in rows ) {
|
|
if ( rows.hasOwnProperty( i ) ) {
|
|
cols = rows[ i ];
|
|
for ( j in cols ) {
|
|
if ( cols.hasOwnProperty( j ) && !cols[ j ] ) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return (
|
|
coverage[ level ][ x] === undefined ||
|
|
coverage[ level ][ x ][ y ] === undefined ||
|
|
coverage[ level ][ x ][ y ] === true
|
|
);
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* Returns true if the given tile is completely covered by higher-level
|
|
* tiles of higher resolution representing the same content. If neither x
|
|
* nor y is given, returns true if the entire visible level is covered.
|
|
*/
|
|
function isCovered( coverage, level, x, y ) {
|
|
if ( x === undefined || y === undefined ) {
|
|
return providesCoverage( coverage, level + 1 );
|
|
} else {
|
|
return (
|
|
providesCoverage( coverage, level + 1, 2 * x, 2 * y ) &&
|
|
providesCoverage( coverage, level + 1, 2 * x, 2 * y + 1 ) &&
|
|
providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y ) &&
|
|
providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y + 1 )
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* Sets whether the given tile provides coverage or not.
|
|
*/
|
|
function setCoverage( coverage, level, x, y, covers ) {
|
|
if ( !coverage[ level ] ) {
|
|
$.console.warn(
|
|
"Setting coverage for a tile before its level's coverage has been reset: %s",
|
|
level
|
|
);
|
|
return;
|
|
}
|
|
|
|
if ( !coverage[ level ][ x ] ) {
|
|
coverage[ level ][ x ] = {};
|
|
}
|
|
|
|
coverage[ level ][ x ][ y ] = covers;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* Resets coverage information for the given level. This should be called
|
|
* after every draw routine. Note that at the beginning of the next draw
|
|
* routine, coverage for every visible tile should be explicitly set.
|
|
*/
|
|
function resetCoverage( coverage, level ) {
|
|
coverage[ level ] = {};
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* Determines the 'z-index' of the given overlay. Overlays are ordered in
|
|
* a z-index based on the order they are added to the Drawer.
|
|
*/
|
|
function getOverlayIndex( overlays, element ) {
|
|
var i;
|
|
for ( i = overlays.length - 1; i >= 0; i-- ) {
|
|
if ( overlays[ i ].element == element ) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @inner
|
|
* Determines whether the 'last best' tile for the area is better than the
|
|
* tile in question.
|
|
*/
|
|
function compareTiles( previousBest, tile ) {
|
|
if ( !previousBest ) {
|
|
return tile;
|
|
}
|
|
|
|
if ( tile.visibility > previousBest.visibility ) {
|
|
return tile;
|
|
} else if ( tile.visibility == previousBest.visibility ) {
|
|
if ( tile.distance < previousBest.distance ) {
|
|
return tile;
|
|
}
|
|
}
|
|
|
|
return previousBest;
|
|
};
|
|
|
|
function finishLoadingImage( image, callback, successful, jobid ){
|
|
|
|
image.onload = null;
|
|
image.onabort = null;
|
|
image.onerror = null;
|
|
|
|
if ( jobid ) {
|
|
window.clearTimeout( jobid );
|
|
}
|
|
window.setTimeout( function() {
|
|
callback( image.src, successful ? image : null);
|
|
}, 1 );
|
|
|
|
};
|
|
|
|
|
|
function drawOverlays( viewport, overlays, container ){
|
|
var i,
|
|
length = overlays.length;
|
|
for ( i = 0; i < length; i++ ) {
|
|
drawOverlay( viewport, overlays[ i ], container );
|
|
}
|
|
};
|
|
|
|
function drawOverlay( viewport, overlay, container ){
|
|
|
|
overlay.position = viewport.pixelFromPoint(
|
|
overlay.bounds.getTopLeft(),
|
|
true
|
|
);
|
|
overlay.size = viewport.deltaPixelsFromPoints(
|
|
overlay.bounds.getSize(),
|
|
true
|
|
);
|
|
overlay.drawHTML( container );
|
|
};
|
|
|
|
function drawTiles( drawer, lastDrawn ){
|
|
var i,
|
|
tile,
|
|
tileKey,
|
|
viewer,
|
|
viewport,
|
|
position,
|
|
tileSource,
|
|
collectionTileSource;
|
|
|
|
for ( i = lastDrawn.length - 1; i >= 0; i-- ) {
|
|
tile = lastDrawn[ i ];
|
|
|
|
if( drawer.debugMode ){
|
|
try{
|
|
drawDebugInfo( drawer, tile, lastDrawn.length, i );
|
|
}catch(e){
|
|
$.console.error(e);
|
|
}
|
|
}
|
|
|
|
//We dont actually 'draw' a collection tile, rather its used to house
|
|
//an overlay which does the drawing in its own viewport
|
|
if( drawer.viewport.collectionMode ){
|
|
tileKey = tile.x + '/' + tile.y;
|
|
viewport = drawer.viewport;
|
|
collectionTileSource = viewport.collectionTileSource;
|
|
if( !drawer.collectionOverlays[ tileKey ] ){
|
|
position = collectionTileSource.layout == 'horizontal' ?
|
|
tile.x + ( tile.y * collectionTileSource.tilesPerRow ) :
|
|
tile.y + ( tile.x * collectionTileSource.tilesPerRow ),
|
|
tileSource = position < collectionTileSource.tileSources.length ?
|
|
collectionTileSource.tileSources[ position ] :
|
|
null;
|
|
//$.console.log("Rendering collection tile [%s] %s", position, tileKey);
|
|
if( tileSource ){
|
|
drawer.collectionOverlays[ tileKey ] = viewer = new $.Viewer({
|
|
element: $.makeNeutralElement( "div" ),
|
|
mouseNavEnabled: false,
|
|
showNavigator: false,
|
|
showSequenceControl: false,
|
|
showNavigationControl: false,
|
|
//visibilityRatio: 1,
|
|
//debugMode: true,
|
|
//debugGridColor: 'red',
|
|
tileSources: [
|
|
tileSource
|
|
]
|
|
});
|
|
|
|
(function(style){
|
|
style['-webkit-box-reflect'] =
|
|
'below 0px -webkit-gradient('+
|
|
'linear,left '+
|
|
'top,left '+
|
|
'bottom,from(transparent),color-stop(60%,transparent),to(rgba(255,255,255,0.4))'+
|
|
')';
|
|
style['border'] = '1px solid rgba(255,255,255,0.2)';
|
|
//style['borderRight'] = '1px solid #fff';
|
|
}(viewer.element.style));
|
|
|
|
drawer.addOverlay(
|
|
viewer.element,
|
|
tile.bounds
|
|
);
|
|
}
|
|
|
|
}else{
|
|
viewer = drawer.collectionOverlays[ tileKey ];
|
|
if( viewer.viewport ){
|
|
viewer.viewport.resize( tile.size, true );
|
|
viewer.viewport.goHome( true );
|
|
}
|
|
}
|
|
|
|
} else {
|
|
|
|
if ( USE_CANVAS ) {
|
|
tile.drawCanvas( drawer.context );
|
|
} else {
|
|
tile.drawHTML( drawer.canvas );
|
|
}
|
|
|
|
tile.beingDrawn = true;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
function drawDebugInfo( drawer, tile, count, i ){
|
|
|
|
if ( USE_CANVAS ) {
|
|
drawer.context.lineWidth = 2;
|
|
drawer.context.font = 'small-caps bold 12px ariel';
|
|
drawer.context.strokeStyle = drawer.debugGridColor;
|
|
drawer.context.fillStyle = drawer.debugGridColor;
|
|
drawer.context.strokeRect(
|
|
tile.position.x,
|
|
tile.position.y,
|
|
tile.size.x,
|
|
tile.size.y
|
|
);
|
|
if( tile.x == 0 && tile.y == 0 ){
|
|
drawer.context.fillText(
|
|
"Zoom: " + drawer.viewport.getZoom(),
|
|
tile.position.x,
|
|
tile.position.y - 30
|
|
);
|
|
drawer.context.fillText(
|
|
"Pan: " + drawer.viewport.getBounds().toString(),
|
|
tile.position.x,
|
|
tile.position.y - 20
|
|
);
|
|
}
|
|
drawer.context.fillText(
|
|
"Level: " + tile.level,
|
|
tile.position.x + 10,
|
|
tile.position.y + 20
|
|
);
|
|
drawer.context.fillText(
|
|
"Column: " + tile.x,
|
|
tile.position.x + 10,
|
|
tile.position.y + 30
|
|
);
|
|
drawer.context.fillText(
|
|
"Row: " + tile.y,
|
|
tile.position.x + 10,
|
|
tile.position.y + 40
|
|
);
|
|
drawer.context.fillText(
|
|
"Order: " + i + " of " + count,
|
|
tile.position.x + 10,
|
|
tile.position.y + 50
|
|
);
|
|
drawer.context.fillText(
|
|
"Size: " + tile.size.toString(),
|
|
tile.position.x + 10,
|
|
tile.position.y + 60
|
|
);
|
|
drawer.context.fillText(
|
|
"Position: " + tile.position.toString(),
|
|
tile.position.x + 10,
|
|
tile.position.y + 70
|
|
);
|
|
}
|
|
};
|
|
|
|
|
|
}( OpenSeadragon ));
|
|
|
|
(function( $ ){
|
|
|
|
|
|
/**
|
|
* @class
|
|
*/
|
|
$.Viewport = function( options ) {
|
|
|
|
//backward compatibility for positional args while prefering more
|
|
//idiomatic javascript options object as the only argument
|
|
var args = arguments;
|
|
if( args.length && args[ 0 ] instanceof $.Point ){
|
|
options = {
|
|
containerSize: args[ 0 ],
|
|
contentSize: args[ 1 ],
|
|
config: args[ 2 ]
|
|
};
|
|
}
|
|
|
|
//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;
|
|
}
|
|
|
|
$.extend( true, this, {
|
|
|
|
//required settings
|
|
containerSize: null,
|
|
contentSize: null,
|
|
|
|
//internal state properties
|
|
zoomPoint: null,
|
|
|
|
//configurable options
|
|
springStiffness: $.DEFAULT_SETTINGS.springStiffness,
|
|
animationTime: $.DEFAULT_SETTINGS.animationTime,
|
|
minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio,
|
|
maxZoomPixelRatio: $.DEFAULT_SETTINGS.maxZoomPixelRatio,
|
|
visibilityRatio: $.DEFAULT_SETTINGS.visibilityRatio,
|
|
wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal,
|
|
wrapVertical: $.DEFAULT_SETTINGS.wrapVertical
|
|
|
|
}, options );
|
|
|
|
this.centerSpringX = new $.Spring({
|
|
initial: 0,
|
|
springStiffness: this.springStiffness,
|
|
animationTime: this.animationTime
|
|
});
|
|
this.centerSpringY = new $.Spring({
|
|
initial: 0,
|
|
springStiffness: this.springStiffness,
|
|
animationTime: this.animationTime
|
|
});
|
|
this.zoomSpring = new $.Spring({
|
|
initial: 1,
|
|
springStiffness: this.springStiffness,
|
|
animationTime: this.animationTime
|
|
});
|
|
|
|
this.resetContentSize( this.contentSize );
|
|
this.goHome( true );
|
|
//this.fitHorizontally( true );
|
|
this.update();
|
|
};
|
|
|
|
$.Viewport.prototype = {
|
|
|
|
resetContentSize: function( contentSize ){
|
|
this.contentSize = contentSize;
|
|
this.contentAspectX = this.contentSize.x / this.contentSize.y;
|
|
this.contentAspectY = this.contentSize.y / this.contentSize.x;
|
|
this.fitWidthBounds = new $.Rect( 0, 0, 1, this.contentAspectX );
|
|
this.fitHeightBounds = new $.Rect( 0, 0, 1, this.contentAspectY );
|
|
|
|
this.homeBounds = this.fitHeightBounds;
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
getHomeZoom: function() {
|
|
|
|
var aspectFactor =
|
|
this.contentAspectX / this.getAspectRatio();
|
|
|
|
return ( aspectFactor >= 1 ) ?
|
|
1 :
|
|
aspectFactor;
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
getMinZoom: function() {
|
|
var homeZoom = this.getHomeZoom(),
|
|
zoom = this.minZoomImageRatio * homeZoom;
|
|
|
|
return Math.min( zoom, homeZoom );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
getMaxZoom: function() {
|
|
var zoom =
|
|
this.contentSize.x *
|
|
this.maxZoomPixelRatio /
|
|
this.containerSize.x;
|
|
return Math.max( zoom, this.getHomeZoom() );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
getAspectRatio: function() {
|
|
return this.containerSize.x / this.containerSize.y;
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
getContainerSize: function() {
|
|
return new $.Point(
|
|
this.containerSize.x,
|
|
this.containerSize.y
|
|
);
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
getBounds: function( current ) {
|
|
var center = this.getCenter( current ),
|
|
width = 1.0 / this.getZoom( current ),
|
|
height = width / this.getAspectRatio();
|
|
|
|
return new $.Rect(
|
|
center.x - ( width / 2.0 ),
|
|
center.y - ( height / 2.0 ),
|
|
width,
|
|
height
|
|
);
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
getCenter: function( current ) {
|
|
var centerCurrent = new $.Point(
|
|
this.centerSpringX.current.value,
|
|
this.centerSpringY.current.value
|
|
),
|
|
centerTarget = new $.Point(
|
|
this.centerSpringX.target.value,
|
|
this.centerSpringY.target.value
|
|
),
|
|
oldZoomPixel,
|
|
zoom,
|
|
width,
|
|
height,
|
|
bounds,
|
|
newZoomPixel,
|
|
deltaZoomPixels,
|
|
deltaZoomPoints;
|
|
|
|
if ( current ) {
|
|
return centerCurrent;
|
|
} else if ( !this.zoomPoint ) {
|
|
return centerTarget;
|
|
}
|
|
|
|
oldZoomPixel = this.pixelFromPoint(this.zoomPoint, true);
|
|
|
|
zoom = this.getZoom();
|
|
width = 1.0 / zoom;
|
|
height = width / this.getAspectRatio();
|
|
bounds = new $.Rect(
|
|
centerCurrent.x - width / 2.0,
|
|
centerCurrent.y - height / 2.0,
|
|
width,
|
|
height
|
|
);
|
|
|
|
newZoomPixel = this.zoomPoint.minus(
|
|
bounds.getTopLeft()
|
|
).times(
|
|
this.containerSize.x / bounds.width
|
|
);
|
|
deltaZoomPixels = newZoomPixel.minus( oldZoomPixel );
|
|
deltaZoomPoints = deltaZoomPixels.divide( this.containerSize.x * zoom );
|
|
|
|
return centerTarget.plus( deltaZoomPoints );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
getZoom: function( current ) {
|
|
if ( current ) {
|
|
return this.zoomSpring.current.value;
|
|
} else {
|
|
return this.zoomSpring.target.value;
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
applyConstraints: function( immediately ) {
|
|
var actualZoom = this.getZoom(),
|
|
constrainedZoom = Math.max(
|
|
Math.min( actualZoom, this.getMaxZoom() ),
|
|
this.getMinZoom()
|
|
),
|
|
bounds,
|
|
horizontalThreshold,
|
|
verticalThreshold,
|
|
left,
|
|
right,
|
|
top,
|
|
bottom,
|
|
dx = 0,
|
|
dy = 0;
|
|
|
|
if ( actualZoom != constrainedZoom ) {
|
|
this.zoomTo( constrainedZoom, this.zoomPoint, immediately );
|
|
}
|
|
|
|
bounds = this.getBounds();
|
|
|
|
horizontalThreshold = this.visibilityRatio * bounds.width;
|
|
verticalThreshold = this.visibilityRatio * bounds.height;
|
|
|
|
left = bounds.x + bounds.width;
|
|
right = 1 - bounds.x;
|
|
top = bounds.y + bounds.height;
|
|
bottom = this.contentAspectY - bounds.y;
|
|
|
|
if ( this.wrapHorizontal ) {
|
|
//do nothing
|
|
} else if ( left < horizontalThreshold ) {
|
|
dx = horizontalThreshold - left;
|
|
} else if ( right < horizontalThreshold ) {
|
|
dx = right - horizontalThreshold;
|
|
}
|
|
|
|
if ( this.wrapVertical ) {
|
|
//do nothing
|
|
} else if ( top < verticalThreshold ) {
|
|
dy = verticalThreshold - top;
|
|
} else if ( bottom < verticalThreshold ) {
|
|
dy = bottom - verticalThreshold;
|
|
}
|
|
|
|
if ( dx || dy ) {
|
|
bounds.x += dx;
|
|
bounds.y += dy;
|
|
this.fitBounds( bounds, immediately );
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Boolean} immediately
|
|
*/
|
|
ensureVisible: function( immediately ) {
|
|
this.applyConstraints( immediately );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {OpenSeadragon.Rect} bounds
|
|
* @param {Boolean} immediately
|
|
*/
|
|
fitBounds: function( bounds, immediately ) {
|
|
var aspect = this.getAspectRatio(),
|
|
center = bounds.getCenter(),
|
|
newBounds = new $.Rect(
|
|
bounds.x,
|
|
bounds.y,
|
|
bounds.width,
|
|
bounds.height
|
|
),
|
|
oldBounds,
|
|
oldZoom,
|
|
newZoom,
|
|
referencePoint;
|
|
|
|
if ( newBounds.getAspectRatio() >= aspect ) {
|
|
newBounds.height = bounds.width / aspect;
|
|
newBounds.y = center.y - newBounds.height / 2;
|
|
} else {
|
|
newBounds.width = bounds.height * aspect;
|
|
newBounds.x = center.x - newBounds.width / 2;
|
|
}
|
|
|
|
this.panTo( this.getCenter( true ), true );
|
|
this.zoomTo( this.getZoom( true ), null, true );
|
|
|
|
oldBounds = this.getBounds();
|
|
oldZoom = this.getZoom();
|
|
newZoom = 1.0 / newBounds.width;
|
|
if ( newZoom == oldZoom || newBounds.width == oldBounds.width ) {
|
|
this.panTo( center, immediately );
|
|
return;
|
|
}
|
|
|
|
referencePoint = oldBounds.getTopLeft().times(
|
|
this.containerSize.x / oldBounds.width
|
|
).minus(
|
|
newBounds.getTopLeft().times(
|
|
this.containerSize.x / newBounds.width
|
|
)
|
|
).divide(
|
|
this.containerSize.x / oldBounds.width -
|
|
this.containerSize.x / newBounds.width
|
|
);
|
|
|
|
this.zoomTo( newZoom, referencePoint, immediately );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Boolean} immediately
|
|
*/
|
|
goHome: function( immediately ) {
|
|
if( this.contentSize.x <= this.contentSize.y ){
|
|
return this.fitVertically( immediately );
|
|
} else {
|
|
return this.fitHorizontally( immediately );
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Boolean} immediately
|
|
*/
|
|
fitVertically: function( immediately ) {
|
|
var center = this.getCenter();
|
|
|
|
if ( this.wrapHorizontal ) {
|
|
center.x = ( 1 + ( center.x % 1 ) ) % 1;
|
|
this.centerSpringX.resetTo( center.x );
|
|
this.centerSpringX.update();
|
|
}
|
|
|
|
if ( this.wrapVertical ) {
|
|
center.y = (
|
|
this.contentAspectY + ( center.y % this.contentAspectY )
|
|
) % this.contentAspectY;
|
|
this.centerSpringY.resetTo( center.y );
|
|
this.centerSpringY.update();
|
|
}
|
|
|
|
this.fitBounds( this.homeBounds, immediately );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {Boolean} immediately
|
|
*/
|
|
fitHorizontally: function( immediately ) {
|
|
var center = this.getCenter();
|
|
|
|
if ( this.wrapHorizontal ) {
|
|
center.x = (
|
|
this.contentAspectX + ( center.x % this.contentAspectX )
|
|
) % this.contentAspectX;
|
|
this.centerSpringX.resetTo( center.x );
|
|
this.centerSpringX.update();
|
|
}
|
|
|
|
if ( this.wrapVertical ) {
|
|
center.y = ( 1 + ( center.y % 1 ) ) % 1;
|
|
this.centerSpringY.resetTo( center.y );
|
|
this.centerSpringY.update();
|
|
}
|
|
|
|
this.fitBounds( this.homeBounds, immediately );
|
|
},
|
|
|
|
|
|
/**
|
|
* @function
|
|
* @param {OpenSeadragon.Point} delta
|
|
* @param {Boolean} immediately
|
|
*/
|
|
panBy: function( delta, immediately ) {
|
|
var center = new $.Point(
|
|
this.centerSpringX.target.value,
|
|
this.centerSpringY.target.value
|
|
);
|
|
this.panTo( center.plus( delta ), immediately );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
* @param {OpenSeadragon.Point} center
|
|
* @param {Boolean} immediately
|
|
*/
|
|
panTo: function( center, immediately ) {
|
|
if ( immediately ) {
|
|
this.centerSpringX.resetTo( center.x );
|
|
this.centerSpringY.resetTo( center.y );
|
|
} else {
|
|
this.centerSpringX.springTo( center.x );
|
|
this.centerSpringY.springTo( center.y );
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
zoomBy: function( factor, refPoint, immediately ) {
|
|
this.zoomTo( this.zoomSpring.target.value * factor, refPoint, immediately );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
zoomTo: function( zoom, refPoint, immediately ) {
|
|
|
|
if ( immediately ) {
|
|
this.zoomSpring.resetTo( zoom );
|
|
} else {
|
|
this.zoomSpring.springTo( zoom );
|
|
}
|
|
|
|
this.zoomPoint = refPoint instanceof $.Point ?
|
|
refPoint :
|
|
null;
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
resize: function( newContainerSize, maintain ) {
|
|
var oldBounds = this.getBounds(),
|
|
newBounds = oldBounds,
|
|
widthDeltaFactor = newContainerSize.x / this.containerSize.x;
|
|
|
|
this.containerSize = new $.Point(
|
|
newContainerSize.x,
|
|
newContainerSize.y
|
|
);
|
|
|
|
if (maintain) {
|
|
newBounds.width = oldBounds.width * widthDeltaFactor;
|
|
newBounds.height = newBounds.width / this.getAspectRatio();
|
|
}
|
|
|
|
this.fitBounds( newBounds, true );
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
update: function() {
|
|
var oldCenterX = this.centerSpringX.current.value,
|
|
oldCenterY = this.centerSpringY.current.value,
|
|
oldZoom = this.zoomSpring.current.value,
|
|
oldZoomPixel,
|
|
newZoomPixel,
|
|
deltaZoomPixels,
|
|
deltaZoomPoints;
|
|
|
|
if (this.zoomPoint) {
|
|
oldZoomPixel = this.pixelFromPoint( this.zoomPoint, true );
|
|
}
|
|
|
|
this.zoomSpring.update();
|
|
|
|
if (this.zoomPoint && this.zoomSpring.current.value != oldZoom) {
|
|
newZoomPixel = this.pixelFromPoint( this.zoomPoint, true );
|
|
deltaZoomPixels = newZoomPixel.minus( oldZoomPixel );
|
|
deltaZoomPoints = this.deltaPointsFromPixels( deltaZoomPixels, true );
|
|
|
|
this.centerSpringX.shiftBy( deltaZoomPoints.x );
|
|
this.centerSpringY.shiftBy( deltaZoomPoints.y );
|
|
} else {
|
|
this.zoomPoint = null;
|
|
}
|
|
|
|
this.centerSpringX.update();
|
|
this.centerSpringY.update();
|
|
|
|
return this.centerSpringX.current.value != oldCenterX ||
|
|
this.centerSpringY.current.value != oldCenterY ||
|
|
this.zoomSpring.current.value != oldZoom;
|
|
},
|
|
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
deltaPixelsFromPoints: function( deltaPoints, current ) {
|
|
return deltaPoints.times(
|
|
this.containerSize.x * this.getZoom( current )
|
|
);
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
deltaPointsFromPixels: function( deltaPixels, current ) {
|
|
return deltaPixels.divide(
|
|
this.containerSize.x * this.getZoom( current )
|
|
);
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
pixelFromPoint: function( point, current ) {
|
|
var bounds = this.getBounds( current );
|
|
return point.minus(
|
|
bounds.getTopLeft()
|
|
).times(
|
|
this.containerSize.x / bounds.width
|
|
);
|
|
},
|
|
|
|
/**
|
|
* @function
|
|
*/
|
|
pointFromPixel: function( pixel, current ) {
|
|
var bounds = this.getBounds( current );
|
|
return pixel.divide(
|
|
this.containerSize.x / bounds.width
|
|
).plus(
|
|
bounds.getTopLeft()
|
|
);
|
|
}
|
|
};
|
|
|
|
}( OpenSeadragon ));
|