Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Ralph Krimmel 2016-05-24 08:59:28 +02:00
commit 96a4de7b87
29 changed files with 3186 additions and 1064 deletions

View File

@ -28,6 +28,7 @@ module.exports = function(grunt) {
"src/mousetracker.js", "src/mousetracker.js",
"src/control.js", "src/control.js",
"src/controldock.js", "src/controldock.js",
"src/placement.js",
"src/viewer.js", "src/viewer.js",
"src/navigator.js", "src/navigator.js",
"src/strings.js", "src/strings.js",

View File

@ -1,12 +1,18 @@
OPENSEADRAGON CHANGELOG OPENSEADRAGON CHANGELOG
======================= =======================
2.1.1: (in progress) 2.2.0: (in progress)
* BREAKING CHANGE: Viewport.homeBounds, Viewport.contentSize, Viewport.contentAspectX and * BREAKING CHANGE: Viewport.homeBounds, Viewport.contentSize, Viewport.contentAspectX and
Viewport.contentAspectY have been removed. Viewport.contentAspectY have been removed. (#846)
* DEPRECATION: Viewport.setHomeBounds has been deprecated * BREAKING CHANGE: The Overlay.getBounds method now takes the viewport as parameter. (#896)
* DEPRECATION: the Viewport constructor is now ignoring the contentSize option * DEPRECATION: Overlay.scales, Overlay.bounds and Overlay.position have been deprecated. (#896)
* Overlay.width !== null should be used to test whether the overlay scales horizontally
* Overlay.height !== null should be used to test whether the overlay scales vertically
* The Overlay.getBounds method should be used to get the bounds of the overlay in viewport coordinates
* Overlay.location replaces Overlay.position
* DEPRECATION: Viewport.setHomeBounds has been deprecated (#846)
* DEPRECATION: the Viewport constructor is now ignoring the contentSize option (#846)
* Tile edge smoothing at high zoom (#764) * Tile edge smoothing at high zoom (#764)
* Fixed issue with reference strip popping up virtual keyboard on mobile devices (#779) * Fixed issue with reference strip popping up virtual keyboard on mobile devices (#779)
* Now supporting rotation in the Rect class (#782) * Now supporting rotation in the Rect class (#782)
@ -25,6 +31,22 @@ Viewport.contentAspectY have been removed.
* Fixed: with scrollToZoom disabled, the viewer caused page scrolling to slow down (#858) * Fixed: with scrollToZoom disabled, the viewer caused page scrolling to slow down (#858)
* Added Viewer.getOverlayById and Overlay.getBounds functions (#853) * Added Viewer.getOverlayById and Overlay.getBounds functions (#853)
* Tiled images with 0 opacity no longer load their tiles or do drawing calculations (#859) * Tiled images with 0 opacity no longer load their tiles or do drawing calculations (#859)
* Fixed issue with edge smoothing with PNG tiles at high zoom (#860)
* Fixed: Images with transparency were clearing images layered below them (#861)
* Fixed issue causing HTML pages to jump unwantedly to the reference strip upon loading (#872)
* Added addOnceHandler method to EventSource (#887)
* Added TiledImage.fitBounds method (#888)
* Overlays can now be scaled in a single dimension by providing a point location and either width or height (#896)
* Added full rotation support to overlays (#729, #193)
* Viewport.goHome() now takes clipping into account (#910)
* Improved zoom to point (#923)
* Optimized sketch canvas clearing and blending for images with opacity or transfer modes (#927)
* Now taking rotation into account in viewport getBounds and fitBounds methods (#934)
* Added option to disable navigator auto-fade (#935)
* Fixed issue with maintaining viewport position with full screen (#940)
* Fixed an issue with simultaneous touch events (#930)
* Avoid loading clipped out tiles (#939)
* Improved precision for subtle moves with fitBounds (#939)
2.1.0: 2.1.0:

View File

@ -259,13 +259,17 @@ $.Drawer.prototype = {
} }
}, },
_clear: function ( useSketch ) { _clear: function (useSketch, bounds) {
if ( !this.useCanvas ) { if (!this.useCanvas) {
return; return;
} }
var context = this._getContext( useSketch ); var context = this._getContext(useSketch);
var canvas = context.canvas; if (bounds) {
context.clearRect( 0, 0, canvas.width, canvas.height ); context.clearRect(bounds.x, bounds.y, bounds.width, bounds.height);
} else {
var canvas = context.canvas;
context.clearRect(0, 0, canvas.width, canvas.height);
}
}, },
/** /**
@ -382,47 +386,80 @@ $.Drawer.prototype = {
/** /**
* Blends the sketch canvas in the main canvas. * Blends the sketch canvas in the main canvas.
* @param {Float} opacity The opacity of the blending. * @param {Object} options The options
* @param {Float} [scale=1] The scale at which tiles were drawn on the sketch. Default is 1. * @param {Float} options.opacity The opacity of the blending.
* Use scale to draw at a lower scale and then enlarge onto the main canvas. * @param {Float} [options.scale=1] The scale at which tiles were drawn on
* @param {OpenSeadragon.Point} [translate] A translation vector that was used to draw the tiles * the sketch. Default is 1.
* @param {String} [options.compositeOperation] - How the image is composited onto other images; see compositeOperation in {@link OpenSeadragon.Options} for possible values. * Use scale to draw at a lower scale and then enlarge onto the main canvas.
* @returns {undefined} * @param {OpenSeadragon.Point} [options.translate] A translation vector
* that was used to draw the tiles
* @param {String} [options.compositeOperation] - How the image is
* composited onto other images; see compositeOperation in
* {@link OpenSeadragon.Options} for possible values.
* @param {OpenSeadragon.Rect} [options.bounds] The part of the sketch
* canvas to blend in the main canvas. If specified, options.scale and
* options.translate get ignored.
*/ */
blendSketch: function(opacity, scale, translate, compositeOperation) { blendSketch: function(opacity, scale, translate, compositeOperation) {
var options = opacity;
if (!$.isPlainObject(options)) {
options = {
opacity: opacity,
scale: scale,
translate: translate,
compositeOperation: compositeOperation
};
}
if (!this.useCanvas || !this.sketchCanvas) { if (!this.useCanvas || !this.sketchCanvas) {
return; return;
} }
scale = scale || 1; opacity = options.opacity;
var position = translate instanceof $.Point ? compositeOperation = options.compositeOperation;
translate : var bounds = options.bounds;
new $.Point(0, 0);
var widthExt = 0;
var heightExt = 0;
if (translate) {
var widthDiff = this.sketchCanvas.width - this.canvas.width;
var heightDiff = this.sketchCanvas.height - this.canvas.height;
widthExt = Math.round(widthDiff / 2);
heightExt = Math.round(heightDiff / 2);
}
this.context.save(); this.context.save();
this.context.globalAlpha = opacity; this.context.globalAlpha = opacity;
if (compositeOperation) { if (compositeOperation) {
this.context.globalCompositeOperation = compositeOperation; this.context.globalCompositeOperation = compositeOperation;
} }
this.context.drawImage( if (bounds) {
this.sketchCanvas, this.context.drawImage(
position.x - widthExt * scale, this.sketchCanvas,
position.y - heightExt * scale, bounds.x,
(this.canvas.width + 2 * widthExt) * scale, bounds.y,
(this.canvas.height + 2 * heightExt) * scale, bounds.width,
-widthExt, bounds.height,
-heightExt, bounds.x,
this.canvas.width + 2 * widthExt, bounds.y,
this.canvas.height + 2 * heightExt bounds.width,
); bounds.height
);
} else {
scale = options.scale || 1;
translate = options.translate;
var position = translate instanceof $.Point ?
translate : new $.Point(0, 0);
var widthExt = 0;
var heightExt = 0;
if (translate) {
var widthDiff = this.sketchCanvas.width - this.canvas.width;
var heightDiff = this.sketchCanvas.height - this.canvas.height;
widthExt = Math.round(widthDiff / 2);
heightExt = Math.round(heightDiff / 2);
}
this.context.drawImage(
this.sketchCanvas,
position.x - widthExt * scale,
position.y - heightExt * scale,
(this.canvas.width + 2 * widthExt) * scale,
(this.canvas.height + 2 * heightExt) * scale,
-widthExt,
-heightExt,
this.canvas.width + 2 * widthExt,
this.canvas.height + 2 * heightExt
);
}
this.context.restore(); this.context.restore();
}, },

View File

@ -301,7 +301,9 @@ function configureFromXML( tileSource, xmlDoc ){
} else if ( rootName == "Collection" ) { } else if ( rootName == "Collection" ) {
throw new Error( $.getString( "Errors.Dzc" ) ); throw new Error( $.getString( "Errors.Dzc" ) );
} else if ( rootName == "Error" ) { } else if ( rootName == "Error" ) {
return $._processDZIError( root ); var messageNode = root.getElementsByTagName("Message")[0];
var message = messageNode.firstChild.nodeValue;
throw new Error(message);
} }
throw new Error( $.getString( "Errors.Dzi" ) ); throw new Error( $.getString( "Errors.Dzi" ) );

View File

@ -56,7 +56,31 @@ $.EventSource = function() {
/** @lends OpenSeadragon.EventSource.prototype */ /** @lends OpenSeadragon.EventSource.prototype */
$.EventSource.prototype = { $.EventSource.prototype = {
// TODO: Add a method 'one' which automatically unbinds a listener after the first triggered event that matches. /**
* Add an event handler to be triggered only once (or a given number of times)
* for a given event.
* @function
* @param {String} eventName - Name of event to register.
* @param {OpenSeadragon.EventHandler} handler - Function to call when event
* is triggered.
* @param {Object} [userData=null] - Arbitrary object to be passed unchanged
* to the handler.
* @param {Number} [times=1] - The number of times to handle the event
* before removing it.
*/
addOnceHandler: function(eventName, handler, userData, times) {
var self = this;
times = times || 1;
var count = 0;
var onceHandler = function(event) {
count++;
if (count === times) {
self.removeHandler(eventName, onceHandler);
}
handler(event);
};
this.addHandler(eventName, onceHandler, userData);
},
/** /**
* Add an event handler for a given event. * Add an event handler for a given event.

View File

@ -1357,11 +1357,11 @@
* @private * @private
* @inner * @inner
*/ */
function capturePointer( tracker, pointerType ) { function capturePointer( tracker, pointerType, pointerCount ) {
var pointsList = tracker.getActivePointersListByType( pointerType ), var pointsList = tracker.getActivePointersListByType( pointerType ),
eventParams; eventParams;
pointsList.captureCount++; pointsList.captureCount += (pointerCount || 1);
if ( pointsList.captureCount === 1 ) { if ( pointsList.captureCount === 1 ) {
if ( $.Browser.vendor === $.BROWSERS.IE && $.Browser.version < 9 ) { if ( $.Browser.vendor === $.BROWSERS.IE && $.Browser.version < 9 ) {
@ -1400,11 +1400,11 @@
* @private * @private
* @inner * @inner
*/ */
function releasePointer( tracker, pointerType ) { function releasePointer( tracker, pointerType, pointerCount ) {
var pointsList = tracker.getActivePointersListByType( pointerType ), var pointsList = tracker.getActivePointersListByType( pointerType ),
eventParams; eventParams;
pointsList.captureCount--; pointsList.captureCount -= (pointerCount || 1);
if ( pointsList.captureCount === 0 ) { if ( pointsList.captureCount === 0 ) {
if ( $.Browser.vendor === $.BROWSERS.IE && $.Browser.version < 9 ) { if ( $.Browser.vendor === $.BROWSERS.IE && $.Browser.version < 9 ) {
@ -2074,7 +2074,7 @@
if ( updatePointersDown( tracker, event, gPoints, 0 ) ) { // 0 means primary button press/release or touch contact if ( updatePointersDown( tracker, event, gPoints, 0 ) ) { // 0 means primary button press/release or touch contact
$.stopEvent( event ); $.stopEvent( event );
capturePointer( tracker, 'touch' ); capturePointer( tracker, 'touch', touchCount );
} }
$.cancelEvent( event ); $.cancelEvent( event );
@ -2128,7 +2128,7 @@
} }
if ( updatePointersUp( tracker, event, gPoints, 0 ) ) { if ( updatePointersUp( tracker, event, gPoints, 0 ) ) {
releasePointer( tracker, 'touch' ); releasePointer( tracker, 'touch', touchCount );
} }
// simulate touchleave on our tracked element // simulate touchleave on our tracked element

View File

@ -62,7 +62,7 @@ $.Navigator = function( options ){
options.controlOptions = { options.controlOptions = {
anchor: $.ControlAnchor.TOP_RIGHT, anchor: $.ControlAnchor.TOP_RIGHT,
attachToViewer: true, attachToViewer: true,
autoFade: true autoFade: options.autoFade
}; };
if( options.position ){ if( options.position ){
@ -306,8 +306,8 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /*
this.updateSize(); this.updateSize();
} }
if( viewport && this.viewport ) { if (viewport && this.viewport) {
bounds = viewport.getBounds( true ); bounds = viewport.getBoundsNoRotate(true);
topleft = this.viewport.pixelFromPointNoRotate(bounds.getTopLeft(), false); topleft = this.viewport.pixelFromPointNoRotate(bounds.getTopLeft(), false);
bottomright = this.viewport.pixelFromPointNoRotate(bounds.getBottomRight(), false) bottomright = this.viewport.pixelFromPointNoRotate(bounds.getBottomRight(), false)
.minus( this.totalBorderWidths ); .minus( this.totalBorderWidths );

View File

@ -82,28 +82,9 @@
*/ */
/**
* @version <%= pkg.name %> <%= pkg.version %>
*
* @file
* <h2><strong>OpenSeadragon - Javascript Deep Zooming</strong></h2>
* <p>
* OpenSeadragon 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>
*
*/
/**
* @module OpenSeadragon
*
*/
/** /**
* @namespace OpenSeadragon * @namespace OpenSeadragon
* * @version <%= pkg.name %> <%= pkg.version %>
* @classdesc The root namespace for OpenSeadragon. All utility methods * @classdesc The root namespace for OpenSeadragon. All utility methods
* and classes are defined on or below this namespace. * and classes are defined on or below this namespace.
* *
@ -154,7 +135,7 @@
* created. * created.
* * placement a string to define the relative position to the viewport. * * placement a string to define the relative position to the viewport.
* Only used if no width and height are specified. Default: 'TOP_LEFT'. * Only used if no width and height are specified. Default: 'TOP_LEFT'.
* See {@link OpenSeadragon.OverlayPlacement} for possible values. * See {@link OpenSeadragon.Placement} for possible values.
* *
* @property {String} [xmlPath=null] * @property {String} [xmlPath=null]
* <strong>DEPRECATED</strong>. A relative path to load a DZI file from the server. * <strong>DEPRECATED</strong>. A relative path to load a DZI file from the server.
@ -373,16 +354,16 @@
* *
* @property {String} [navigatorId=navigator-GENERATED DATE] * @property {String} [navigatorId=navigator-GENERATED DATE]
* The ID of a div to hold the navigator minimap. * The ID of a div to hold the navigator minimap.
* If an ID is specified, the navigatorPosition, navigatorSizeRatio, navigatorMaintainSizeRatio, and navigatorTop|Left|Height|Width options will be ignored. * If an ID is specified, the navigatorPosition, navigatorSizeRatio, navigatorMaintainSizeRatio, navigator[Top|Left|Height|Width] and navigatorAutoFade options will be ignored.
* If an ID is not specified, a div element will be generated and placed on top of the main image. * If an ID is not specified, a div element will be generated and placed on top of the main image.
* *
* @property {String} [navigatorPosition='TOP_RIGHT'] * @property {String} [navigatorPosition='TOP_RIGHT']
* Valid values are 'TOP_LEFT', 'TOP_RIGHT', 'BOTTOM_LEFT', 'BOTTOM_RIGHT', or 'ABSOLUTE'.<br> * Valid values are 'TOP_LEFT', 'TOP_RIGHT', 'BOTTOM_LEFT', 'BOTTOM_RIGHT', or 'ABSOLUTE'.<br>
* If 'ABSOLUTE' is specified, then navigatorTop|Left|Height|Width determines the size and position of the navigator minimap in the viewer, and navigatorSizeRatio and navigatorMaintainSizeRatio are ignored.<br> * If 'ABSOLUTE' is specified, then navigator[Top|Left|Height|Width] determines the size and position of the navigator minimap in the viewer, and navigatorSizeRatio and navigatorMaintainSizeRatio are ignored.<br>
* For 'TOP_LEFT', 'TOP_RIGHT', 'BOTTOM_LEFT', and 'BOTTOM_RIGHT', the navigatorSizeRatio or navigatorHeight|Width values determine the size of the navigator minimap. * For 'TOP_LEFT', 'TOP_RIGHT', 'BOTTOM_LEFT', and 'BOTTOM_RIGHT', the navigatorSizeRatio or navigator[Height|Width] values determine the size of the navigator minimap.
* *
* @property {Number} [navigatorSizeRatio=0.2] * @property {Number} [navigatorSizeRatio=0.2]
* Ratio of navigator size to viewer size. Ignored if navigatorHeight|Width are specified. * Ratio of navigator size to viewer size. Ignored if navigator[Height|Width] are specified.
* *
* @property {Boolean} [navigatorMaintainSizeRatio=false] * @property {Boolean} [navigatorMaintainSizeRatio=false]
* If true, the navigator minimap is resized (using navigatorSizeRatio) when the viewer size changes. * If true, the navigator minimap is resized (using navigatorSizeRatio) when the viewer size changes.
@ -405,6 +386,10 @@
* Set to false to prevent polling for navigator size changes. Useful for providing custom resize behavior. * Set to false to prevent polling for navigator size changes. Useful for providing custom resize behavior.
* Setting to false can also improve performance when the navigator is configured to a fixed size. * Setting to false can also improve performance when the navigator is configured to a fixed size.
* *
* @property {Boolean} [navigatorAutoFade=true]
* If the user stops interacting with the viewport, fade the navigator minimap.
* Setting to false will make the navigator minimap always visible.
*
* @property {Boolean} [navigatorRotate=true] * @property {Boolean} [navigatorRotate=true]
* If true, the navigator will be rotated together with the viewer. * If true, the navigator will be rotated together with the viewer.
* *
@ -691,8 +676,6 @@
* This function serves as a single point of instantiation for an {@link OpenSeadragon.Viewer}, including all * This function serves as a single point of instantiation for an {@link OpenSeadragon.Viewer}, including all
* combinations of out-of-the-box configurable features. * combinations of out-of-the-box configurable features.
* *
* @function OpenSeadragon
* @memberof module:OpenSeadragon
* @param {OpenSeadragon.Options} options - Viewer options. * @param {OpenSeadragon.Options} options - Viewer options.
* @returns {OpenSeadragon.Viewer} * @returns {OpenSeadragon.Viewer}
*/ */
@ -842,6 +825,21 @@ if (typeof define === 'function' && define.amd) {
return true; return true;
}; };
/**
* Shim around Object.freeze. Does nothing if Object.freeze is not supported.
* @param {Object} obj The object to freeze.
* @return {Object} obj The frozen object.
*/
$.freezeObject = function(obj) {
if (Object.freeze) {
$.freezeObject = Object.freeze;
} else {
$.freezeObject = function(obj) {
return obj;
};
}
return $.freezeObject(obj);
};
/** /**
* True if the browser supports the HTML5 canvas element * True if the browser supports the HTML5 canvas element
@ -1065,6 +1063,7 @@ if (typeof define === 'function' && define.amd) {
navigatorHeight: null, navigatorHeight: null,
navigatorWidth: null, navigatorWidth: null,
navigatorAutoResize: true, navigatorAutoResize: true,
navigatorAutoFade: true,
navigatorRotate: true, navigatorRotate: true,
// INITIAL ROTATION // INITIAL ROTATION
@ -1322,6 +1321,49 @@ if (typeof define === 'function' && define.amd) {
return window.getComputedStyle( element, "" ); return window.getComputedStyle( element, "" );
}, },
/**
* Returns the property with the correct vendor prefix appended.
* @param {String} property the property name
* @returns {String} the property with the correct prefix or null if not
* supported.
*/
getCssPropertyWithVendorPrefix: function(property) {
var memo = {};
$.getCssPropertyWithVendorPrefix = function(property) {
if (memo[property] !== undefined) {
return memo[property];
}
var style = document.createElement('div').style;
var result = null;
if (style[property] !== undefined) {
result = property;
} else {
var prefixes = ['Webkit', 'Moz', 'MS', 'O',
'webkit', 'moz', 'ms', 'o'];
var suffix = $.capitalizeFirstLetter(property);
for (var i = 0; i < prefixes.length; i++) {
var prop = prefixes[i] + suffix;
if (style[prop] !== undefined) {
result = prop;
break;
}
}
}
memo[property] = result;
return result;
};
return $.getCssPropertyWithVendorPrefix(property);
},
/**
* Capitalizes the first letter of a string
* @param {String} string
* @returns {String} The string with the first letter capitalized
*/
capitalizeFirstLetter: function(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
},
/** /**
* Determines if a point is within the bounding rectangle of the given element (hit-test). * Determines if a point is within the bounding rectangle of the given element (hit-test).
@ -2533,185 +2575,4 @@ if (typeof define === 'function' && define.amd) {
} }
} }
/** }(OpenSeadragon));
* @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" ), 10 ),
height = parseInt( sizeNode.getAttribute( "Height" ), 10 ),
tileSize = parseInt( imageNode.getAttribute( "TileSize" ), 10 ),
tileOverlap = parseInt( imageNode.getAttribute( "Overlap" ), 10 ),
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" ), 10 ),
parseInt( rectNode.getAttribute( "Y" ), 10 ),
parseInt( rectNode.getAttribute( "Width" ), 10 ),
parseInt( rectNode.getAttribute( "Height" ), 10 ),
0, // ignore MinLevel attribute, bug in Deep Zoom Composer
parseInt( dispRectNode.getAttribute( "MaxLevel" ), 10 )
));
}
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, 10 ),
height = parseInt( sizeData.Height, 10 ),
tileSize = parseInt( imageData.TileSize, 10 ),
tileOverlap = parseInt( imageData.Overlap, 10 ),
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, 10 ),
parseInt( rectData.Y, 10 ),
parseInt( rectData.Width, 10 ),
parseInt( rectData.Height, 10 ),
0, // ignore MinLevel attribute, bug in Deep Zoom Composer
parseInt( rectData.MaxLevel, 10 )
));
}
return new $.DziTileSource(
width,
height,
tileSize,
tileOverlap,
tilesUrl,
fileFormat,
dispRects
);
}
/**
* @private
* @inner
* @function
* @param {Document} errorNode
* @throws {Error}
* @deprecated
*/
$._processDZIError = function ( errorNode ) {
var messageNode = errorNode.getElementsByTagName( "Message" )[ 0 ],
message = messageNode.firstChild.nodeValue;
throw new Error(message);
};
}( OpenSeadragon ));

View File

@ -32,14 +32,17 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
(function( $ ){ (function($) {
/** /**
* An enumeration of positions that an overlay may be assigned relative to * An enumeration of positions that an overlay may be assigned relative to
* the viewport. * the viewport.
* It is identical to OpenSeadragon.Placement but is kept for backward
* compatibility.
* @member OverlayPlacement * @member OverlayPlacement
* @memberof OpenSeadragon * @memberof OpenSeadragon
* @static * @static
* @readonly
* @type {Object} * @type {Object}
* @property {Number} CENTER * @property {Number} CENTER
* @property {Number} TOP_LEFT * @property {Number} TOP_LEFT
@ -51,17 +54,26 @@
* @property {Number} BOTTOM_LEFT * @property {Number} BOTTOM_LEFT
* @property {Number} LEFT * @property {Number} LEFT
*/ */
$.OverlayPlacement = { $.OverlayPlacement = $.Placement;
CENTER: 0,
TOP_LEFT: 1, /**
TOP: 2, * An enumeration of possible ways to handle overlays rotation
TOP_RIGHT: 3, * @member OverlayRotationMode
RIGHT: 4, * @memberOf OpenSeadragon
BOTTOM_RIGHT: 5, * @static
BOTTOM: 6, * @readonly
BOTTOM_LEFT: 7, * @property {Number} NO_ROTATION The overlay ignore the viewport rotation.
LEFT: 8 * @property {Number} EXACT The overlay use CSS 3 transforms to rotate with
}; * the viewport. If the overlay contains text, it will get rotated as well.
* @property {Number} BOUNDING_BOX The overlay adjusts for rotation by
* taking the size of the bounding box of the rotated bounds.
* Only valid for overlays with Rect location and scalable in both directions.
*/
$.OverlayRotationMode = $.freezeObject({
NO_ROTATION: 1,
EXACT: 2,
BOUNDING_BOX: 3
});
/** /**
* @class Overlay * @class Overlay
@ -72,19 +84,27 @@
* @param {Element} options.element * @param {Element} options.element
* @param {OpenSeadragon.Point|OpenSeadragon.Rect} options.location - The * @param {OpenSeadragon.Point|OpenSeadragon.Rect} options.location - The
* location of the overlay on the image. If a {@link OpenSeadragon.Point} * location of the overlay on the image. If a {@link OpenSeadragon.Point}
* is specified, the overlay will keep a constant size independently of the * is specified, the overlay will be located at this location with respect
* zoom. If a {@link OpenSeadragon.Rect} is specified, the overlay size will * to the placement option. If a {@link OpenSeadragon.Rect} is specified,
* be adjusted when the zoom changes. * the overlay will be placed at this location with the corresponding width
* @param {OpenSeadragon.OverlayPlacement} [options.placement=OpenSeadragon.OverlayPlacement.TOP_LEFT] * and height and placement TOP_LEFT.
* Relative position to the viewport. * @param {OpenSeadragon.Placement} [options.placement=OpenSeadragon.Placement.TOP_LEFT]
* Only used if location is a {@link OpenSeadragon.Point}. * Defines what part of the overlay should be at the specified options.location
* @param {OpenSeadragon.Overlay.OnDrawCallback} [options.onDraw] * @param {OpenSeadragon.Overlay.OnDrawCallback} [options.onDraw]
* @param {Boolean} [options.checkResize=true] Set to false to avoid to * @param {Boolean} [options.checkResize=true] Set to false to avoid to
* check the size of the overlay everytime it is drawn when using a * check the size of the overlay everytime it is drawn in the directions
* {@link OpenSeadragon.Point} as options.location. It will improve * which are not scaled. It will improve performances but will cause a
* performances but will cause a misalignment if the overlay size changes. * misalignment if the overlay size changes.
* @param {Number} [options.width] The width of the overlay in viewport
* coordinates. If specified, the width of the overlay will be adjusted when
* the zoom changes.
* @param {Number} [options.height] The height of the overlay in viewport
* coordinates. If specified, the height of the overlay will be adjusted when
* the zoom changes.
* @param {Boolean} [options.rotationMode=OpenSeadragon.OverlayRotationMode.EXACT]
* How to handle the rotation of the viewport.
*/ */
$.Overlay = function( element, location, placement ) { $.Overlay = function(element, location, placement) {
/** /**
* onDraw callback signature used by {@link OpenSeadragon.Overlay}. * onDraw callback signature used by {@link OpenSeadragon.Overlay}.
@ -97,7 +117,7 @@
*/ */
var options; var options;
if ( $.isPlainObject( element ) ) { if ($.isPlainObject(element)) {
options = element; options = element;
} else { } else {
options = { options = {
@ -107,73 +127,67 @@
}; };
} }
this.element = options.element; this.element = options.element;
this.scales = options.location instanceof $.Rect; this.style = options.element.style;
this.bounds = new $.Rect( this._init(options);
options.location.x,
options.location.y,
options.location.width,
options.location.height
);
this.position = new $.Point(
options.location.x,
options.location.y
);
this.size = new $.Point(
options.location.width,
options.location.height
);
this.style = options.element.style;
// rects are always top-left
this.placement = options.location instanceof $.Point ?
options.placement :
$.OverlayPlacement.TOP_LEFT;
this.onDraw = options.onDraw;
this.checkResize = options.checkResize === undefined ?
true : options.checkResize;
}; };
/** @lends OpenSeadragon.Overlay.prototype */ /** @lends OpenSeadragon.Overlay.prototype */
$.Overlay.prototype = { $.Overlay.prototype = {
// private
_init: function(options) {
this.location = options.location;
this.placement = options.placement === undefined ?
$.Placement.TOP_LEFT : options.placement;
this.onDraw = options.onDraw;
this.checkResize = options.checkResize === undefined ?
true : options.checkResize;
// When this.width is not null, the overlay get scaled horizontally
this.width = options.width === undefined ? null : options.width;
// When this.height is not null, the overlay get scaled vertically
this.height = options.height === undefined ? null : options.height;
this.rotationMode = options.rotationMode || $.OverlayRotationMode.EXACT;
// Having a rect as location is a syntactic sugar
if (this.location instanceof $.Rect) {
this.width = this.location.width;
this.height = this.location.height;
this.location = this.location.getTopLeft();
this.placement = $.Placement.TOP_LEFT;
}
// Deprecated properties kept for backward compatibility.
this.scales = this.width !== null && this.height !== null;
this.bounds = new $.Rect(
this.location.x, this.location.y, this.width, this.height);
this.position = this.location;
},
/** /**
* Internal function to adjust the position of an overlay
* depending on it size and placement.
* @function * @function
* @param {OpenSeadragon.OverlayPlacement} position * @param {OpenSeadragon.Point} position
* @param {OpenSeadragon.Point} size * @param {OpenSeadragon.Point} size
*/ */
adjust: function( position, size ) { adjust: function(position, size) {
switch ( this.placement ) { var properties = $.Placement.properties[this.placement];
case $.OverlayPlacement.TOP_LEFT: if (!properties) {
break; return;
case $.OverlayPlacement.TOP: }
position.x -= size.x / 2; if (properties.isHorizontallyCentered) {
break; position.x -= size.x / 2;
case $.OverlayPlacement.TOP_RIGHT: } else if (properties.isRight) {
position.x -= size.x; position.x -= size.x;
break; }
case $.OverlayPlacement.RIGHT: if (properties.isVerticallyCentered) {
position.x -= size.x; position.y -= size.y / 2;
position.y -= size.y / 2; } else if (properties.isBottom) {
break; position.y -= size.y;
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;
default:
case $.OverlayPlacement.CENTER:
position.x -= size.x / 2;
position.y -= size.y / 2;
break;
} }
}, },
@ -181,20 +195,20 @@
* @function * @function
*/ */
destroy: function() { destroy: function() {
var element = this.element, var element = this.element;
style = this.style; var style = this.style;
if ( element.parentNode ) { if (element.parentNode) {
element.parentNode.removeChild( element ); element.parentNode.removeChild(element);
//this should allow us to preserve overlays when required between //this should allow us to preserve overlays when required between
//pages //pages
if ( element.prevElementParent ) { if (element.prevElementParent) {
style.display = 'none'; style.display = 'none';
//element.prevElementParent.insertBefore( //element.prevElementParent.insertBefore(
// element, // element,
// element.prevNextSibling // element.prevNextSibling
//); //);
document.body.appendChild( element ); document.body.appendChild(element);
} }
} }
@ -205,122 +219,258 @@
style.left = ""; style.left = "";
style.position = ""; style.position = "";
if ( this.scales ) { if (this.width !== null) {
style.width = ""; style.width = "";
}
if (this.height !== null) {
style.height = ""; style.height = "";
} }
var transformOriginProp = $.getCssPropertyWithVendorPrefix(
'transformOrigin');
var transformProp = $.getCssPropertyWithVendorPrefix(
'transform');
if (transformOriginProp && transformProp) {
style[transformOriginProp] = "";
style[transformProp] = "";
}
}, },
/** /**
* @function * @function
* @param {Element} container * @param {Element} container
*/ */
drawHTML: function( container, viewport ) { drawHTML: function(container, viewport) {
var element = this.element, var element = this.element;
style = this.style, if (element.parentNode !== container) {
scales = this.scales,
degrees = viewport.degrees,
position = viewport.pixelFromPoint(
this.bounds.getTopLeft(),
true
),
size,
overlayCenter;
if ( element.parentNode != container ) {
//save the source parent for later if we need it //save the source parent for later if we need it
element.prevElementParent = element.parentNode; element.prevElementParent = element.parentNode;
element.prevNextSibling = element.nextSibling; element.prevNextSibling = element.nextSibling;
container.appendChild( element ); container.appendChild(element);
this.size = $.getElementSize( element );
// this.size is used by overlays which don't get scaled in at
// least one direction when this.checkResize is set to false.
this.size = $.getElementSize(element);
} }
if ( scales ) { var positionAndSize = this._getOverlayPositionAndSize(viewport);
size = viewport.deltaPixelsFromPoints(
this.bounds.getSize(),
true
);
} else if ( this.checkResize ) {
size = $.getElementSize( element );
} else {
size = this.size;
}
this.position = position; var position = positionAndSize.position;
this.size = size; var size = this.size = positionAndSize.size;
var rotate = positionAndSize.rotate;
this.adjust( position, size );
position = position.apply( Math.round );
size = size.apply( Math.round );
// rotate the position of the overlay
// TODO only rotate overlays if in canvas mode
// TODO replace the size rotation with CSS3 transforms
// TODO add an option to overlays to not rotate with the image
// Currently only rotates position and size
if( degrees !== 0 && this.scales ) {
overlayCenter = new $.Point( size.x / 2, size.y / 2 );
var drawerCenter = new $.Point(
viewport.viewer.drawer.canvas.width / 2,
viewport.viewer.drawer.canvas.height / 2
);
position = position.plus( overlayCenter ).rotate(
degrees,
drawerCenter
).minus( overlayCenter );
size = size.rotate( degrees, new $.Point( 0, 0 ) );
size = new $.Point( Math.abs( size.x ), Math.abs( size.y ) );
}
// call the onDraw callback if it exists to allow one to overwrite // call the onDraw callback if it exists to allow one to overwrite
// the drawing/positioning/sizing of the overlay // the drawing/positioning/sizing of the overlay
if ( this.onDraw ) { if (this.onDraw) {
this.onDraw( position, size, element ); this.onDraw(position, size, this.element);
} else { } else {
style.left = position.x + "px"; var style = this.style;
style.top = position.y + "px"; style.left = position.x + "px";
style.top = position.y + "px";
if (this.width !== null) {
style.width = size.x + "px";
}
if (this.height !== null) {
style.height = size.y + "px";
}
var transformOriginProp = $.getCssPropertyWithVendorPrefix(
'transformOrigin');
var transformProp = $.getCssPropertyWithVendorPrefix(
'transform');
if (transformOriginProp && transformProp) {
if (rotate) {
style[transformOriginProp] = this._getTransformOrigin();
style[transformProp] = "rotate(" + rotate + "deg)";
} else {
style[transformOriginProp] = "";
style[transformProp] = "";
}
}
style.position = "absolute"; style.position = "absolute";
if (style.display != 'none') { if (style.display !== 'none') {
style.display = 'block'; style.display = 'block';
}
if ( scales ) {
style.width = size.x + "px";
style.height = size.y + "px";
} }
} }
}, },
/** // private
* @function _getOverlayPositionAndSize: function(viewport) {
* @param {OpenSeadragon.Point|OpenSeadragon.Rect} location var position = viewport.pixelFromPoint(this.location, true);
* @param {OpenSeadragon.OverlayPlacement} position var size = this._getSizeInPixels(viewport);
*/ this.adjust(position, size);
update: function( location, placement ) {
this.scales = location instanceof $.Rect; var rotate = 0;
this.bounds = new $.Rect( if (viewport.degrees &&
location.x, this.rotationMode !== $.OverlayRotationMode.NO_ROTATION) {
location.y, // BOUNDING_BOX is only valid if both directions get scaled.
location.width, // Get replaced by EXACT otherwise.
location.height if (this.rotationMode === $.OverlayRotationMode.BOUNDING_BOX &&
); this.width !== null && this.height !== null) {
// rects are always top-left var rect = new $.Rect(position.x, position.y, size.x, size.y);
this.placement = location instanceof $.Point ? var boundingBox = this._getBoundingBox(rect, viewport.degrees);
placement : position = boundingBox.getTopLeft();
$.OverlayPlacement.TOP_LEFT; size = boundingBox.getSize();
} else {
rotate = viewport.degrees;
}
}
return {
position: position,
size: size,
rotate: rotate
};
},
// private
_getSizeInPixels: function(viewport) {
var width = this.size.x;
var height = this.size.y;
if (this.width !== null || this.height !== null) {
var scaledSize = viewport.deltaPixelsFromPointsNoRotate(
new $.Point(this.width || 0, this.height || 0), true);
if (this.width !== null) {
width = scaledSize.x;
}
if (this.height !== null) {
height = scaledSize.y;
}
}
if (this.checkResize &&
(this.width === null || this.height === null)) {
var eltSize = this.size = $.getElementSize(this.element);
if (this.width === null) {
width = eltSize.x;
}
if (this.height === null) {
height = eltSize.y;
}
}
return new $.Point(width, height);
},
// private
_getBoundingBox: function(rect, degrees) {
var refPoint = this._getPlacementPoint(rect);
return rect.rotate(degrees, refPoint).getBoundingBox();
},
// private
_getPlacementPoint: function(rect) {
var result = new $.Point(rect.x, rect.y);
var properties = $.Placement.properties[this.placement];
if (properties) {
if (properties.isHorizontallyCentered) {
result.x += rect.width / 2;
} else if (properties.isRight) {
result.x += rect.width;
}
if (properties.isVerticallyCentered) {
result.y += rect.height / 2;
} else if (properties.isBottom) {
result.y += rect.height;
}
}
return result;
},
// private
_getTransformOrigin: function() {
var result = "";
var properties = $.Placement.properties[this.placement];
if (!properties) {
return result;
}
if (properties.isLeft) {
result = "left";
} else if (properties.isRight) {
result = "right";
}
if (properties.isTop) {
result += " top";
} else if (properties.isBottom) {
result += " bottom";
}
return result;
}, },
/** /**
* Changes the overlay settings.
* @function * @function
* @param {OpenSeadragon.Point|OpenSeadragon.Rect|Object} location
* If an object is specified, the options are the same than the constructor
* except for the element which can not be changed.
* @param {OpenSeadragon.Placement} position
*/
update: function(location, placement) {
var options = $.isPlainObject(location) ? location : {
location: location,
placement: placement
};
this._init({
location: options.location || this.location,
placement: options.placement !== undefined ?
options.placement : this.placement,
onDraw: options.onDraw || this.onDraw,
checkResize: options.checkResize || this.checkResize,
width: options.width !== undefined ? options.width : this.width,
height: options.height !== undefined ? options.height : this.height,
rotationMode: options.rotationMode || this.rotationMode
});
},
/**
* Returns the current bounds of the overlay in viewport coordinates
* @function
* @param {OpenSeadragon.Viewport} viewport the viewport
* @returns {OpenSeadragon.Rect} overlay bounds * @returns {OpenSeadragon.Rect} overlay bounds
*/ */
getBounds: function() { getBounds: function(viewport) {
return this.bounds.clone(); $.console.assert(viewport,
'A viewport must now be passed to Overlay.getBounds.');
var width = this.width;
var height = this.height;
if (width === null || height === null) {
var size = viewport.deltaPointsFromPixelsNoRotate(this.size, true);
if (width === null) {
width = size.x;
}
if (height === null) {
height = size.y;
}
}
var location = this.location.clone();
this.adjust(location, new $.Point(width, height));
return this._adjustBoundsForRotation(
viewport, new $.Rect(location.x, location.y, width, height));
},
// private
_adjustBoundsForRotation: function(viewport, bounds) {
if (!viewport ||
viewport.degrees === 0 ||
this.rotationMode === $.OverlayRotationMode.EXACT) {
return bounds;
}
if (this.rotationMode === $.OverlayRotationMode.BOUNDING_BOX) {
// If overlay not fully scalable, BOUNDING_BOX falls back to EXACT
if (this.width === null || this.height === null) {
return bounds;
}
// It is easier to just compute the position and size and
// convert to viewport coordinates.
var positionAndSize = this._getOverlayPositionAndSize(viewport);
return viewport.viewerElementToViewportRectangle(new $.Rect(
positionAndSize.position.x,
positionAndSize.position.y,
positionAndSize.size.x,
positionAndSize.size.y));
}
// NO_ROTATION case
return bounds.rotate(-viewport.degrees,
this._getPlacementPoint(bounds));
} }
}; };
}( OpenSeadragon )); }(OpenSeadragon));

138
src/placement.js Normal file
View File

@ -0,0 +1,138 @@
/*
* OpenSeadragon - Placement
*
* Copyright (C) 2010-2016 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of CodePlex Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function($) {
/**
* An enumeration of positions to anchor an element.
* @member Placement
* @memberOf OpenSeadragon
* @static
* @readonly
* @property {OpenSeadragon.Placement} CENTER
* @property {OpenSeadragon.Placement} TOP_LEFT
* @property {OpenSeadragon.Placement} TOP
* @property {OpenSeadragon.Placement} TOP_RIGHT
* @property {OpenSeadragon.Placement} RIGHT
* @property {OpenSeadragon.Placement} BOTTOM_RIGHT
* @property {OpenSeadragon.Placement} BOTTOM
* @property {OpenSeadragon.Placement} BOTTOM_LEFT
* @property {OpenSeadragon.Placement} LEFT
*/
$.Placement = $.freezeObject({
CENTER: 0,
TOP_LEFT: 1,
TOP: 2,
TOP_RIGHT: 3,
RIGHT: 4,
BOTTOM_RIGHT: 5,
BOTTOM: 6,
BOTTOM_LEFT: 7,
LEFT: 8,
properties: {
0: {
isLeft: false,
isHorizontallyCentered: true,
isRight: false,
isTop: false,
isVerticallyCentered: true,
isBottom: false
},
1: {
isLeft: true,
isHorizontallyCentered: false,
isRight: false,
isTop: true,
isVerticallyCentered: false,
isBottom: false
},
2: {
isLeft: false,
isHorizontallyCentered: true,
isRight: false,
isTop: true,
isVerticallyCentered: false,
isBottom: false
},
3: {
isLeft: false,
isHorizontallyCentered: false,
isRight: true,
isTop: true,
isVerticallyCentered: false,
isBottom: false
},
4: {
isLeft: false,
isHorizontallyCentered: false,
isRight: true,
isTop: false,
isVerticallyCentered: true,
isBottom: false
},
5: {
isLeft: false,
isHorizontallyCentered: false,
isRight: true,
isTop: false,
isVerticallyCentered: false,
isBottom: true
},
6: {
isLeft: false,
isHorizontallyCentered: true,
isRight: false,
isTop: false,
isVerticallyCentered: false,
isBottom: true
},
7: {
isLeft: true,
isHorizontallyCentered: false,
isRight: false,
isTop: false,
isVerticallyCentered: false,
isBottom: true
},
8: {
isLeft: true,
isHorizontallyCentered: false,
isRight: false,
isTop: false,
isVerticallyCentered: true,
isBottom: false
}
}
});
}(OpenSeadragon));

View File

@ -110,6 +110,33 @@ $.Rect = function(x, y, width, height, degrees) {
} }
}; };
/**
* Builds a rectangle having the 3 specified points as summits.
* @static
* @memberof OpenSeadragon.Rect
* @param {OpenSeadragon.Point} topLeft
* @param {OpenSeadragon.Point} topRight
* @param {OpenSeadragon.Point} bottomLeft
* @returns {OpenSeadragon.Rect}
*/
$.Rect.fromSummits = function(topLeft, topRight, bottomLeft) {
var width = topLeft.distanceTo(topRight);
var height = topLeft.distanceTo(bottomLeft);
var diff = topRight.minus(topLeft);
var radians = Math.atan(diff.y / diff.x);
if (diff.x < 0) {
radians += Math.PI;
} else if (diff.y < 0) {
radians += 2 * Math.PI;
}
return new $.Rect(
topLeft.x,
topLeft.y,
width,
height,
radians / Math.PI * 180);
};
/** @lends OpenSeadragon.Rect.prototype */ /** @lends OpenSeadragon.Rect.prototype */
$.Rect.prototype = { $.Rect.prototype = {
/** /**
@ -280,11 +307,137 @@ $.Rect.prototype = {
bottom - top); bottom - top);
}, },
/**
* Returns the bounding box of the intersection of this rectangle with the
* given rectangle.
* @param {OpenSeadragon.Rect} rect
* @return {OpenSeadragon.Rect} the bounding box of the intersection
* or null if the rectangles don't intersect.
*/
intersection: function(rect) {
// Simplified version of Weiler Atherton clipping algorithm
// https://en.wikipedia.org/wiki/Weiler%E2%80%93Atherton_clipping_algorithm
// Because we just want the bounding box of the intersection,
// we can just compute the bounding box of:
// 1. all the summits of this which are inside rect
// 2. all the summits of rect which are inside this
// 3. all the intersections of rect and this
var EPSILON = 0.0000000001;
var intersectionPoints = [];
var thisTopLeft = this.getTopLeft();
if (rect.containsPoint(thisTopLeft, EPSILON)) {
intersectionPoints.push(thisTopLeft);
}
var thisTopRight = this.getTopRight();
if (rect.containsPoint(thisTopRight, EPSILON)) {
intersectionPoints.push(thisTopRight);
}
var thisBottomLeft = this.getBottomLeft();
if (rect.containsPoint(thisBottomLeft, EPSILON)) {
intersectionPoints.push(thisBottomLeft);
}
var thisBottomRight = this.getBottomRight();
if (rect.containsPoint(thisBottomRight, EPSILON)) {
intersectionPoints.push(thisBottomRight);
}
var rectTopLeft = rect.getTopLeft();
if (this.containsPoint(rectTopLeft, EPSILON)) {
intersectionPoints.push(rectTopLeft);
}
var rectTopRight = rect.getTopRight();
if (this.containsPoint(rectTopRight, EPSILON)) {
intersectionPoints.push(rectTopRight);
}
var rectBottomLeft = rect.getBottomLeft();
if (this.containsPoint(rectBottomLeft, EPSILON)) {
intersectionPoints.push(rectBottomLeft);
}
var rectBottomRight = rect.getBottomRight();
if (this.containsPoint(rectBottomRight, EPSILON)) {
intersectionPoints.push(rectBottomRight);
}
var thisSegments = this._getSegments();
var rectSegments = rect._getSegments();
for (var i = 0; i < thisSegments.length; i++) {
var thisSegment = thisSegments[i];
for (var j = 0; j < rectSegments.length; j++) {
var rectSegment = rectSegments[j];
var intersect = getIntersection(thisSegment[0], thisSegment[1],
rectSegment[0], rectSegment[1]);
if (intersect) {
intersectionPoints.push(intersect);
}
}
}
// Get intersection point of segments [a,b] and [c,d]
function getIntersection(a, b, c, d) {
// http://stackoverflow.com/a/1968345/1440403
var abVector = b.minus(a);
var cdVector = d.minus(c);
var denom = -cdVector.x * abVector.y + abVector.x * cdVector.y;
if (denom === 0) {
return null;
}
var s = (abVector.x * (a.y - c.y) - abVector.y * (a.x - c.x)) / denom;
var t = (cdVector.x * (a.y - c.y) - cdVector.y * (a.x - c.x)) / denom;
if (-EPSILON <= s && s <= 1 - EPSILON &&
-EPSILON <= t && t <= 1 - EPSILON) {
return new $.Point(a.x + t * abVector.x, a.y + t * abVector.y);
}
return null;
}
if (intersectionPoints.length === 0) {
return null;
}
var minX = intersectionPoints[0].x;
var maxX = intersectionPoints[0].x;
var minY = intersectionPoints[0].y;
var maxY = intersectionPoints[0].y;
for (var k = 1; k < intersectionPoints.length; k++) {
var point = intersectionPoints[k];
if (point.x < minX) {
minX = point.x;
}
if (point.x > maxX) {
maxX = point.x;
}
if (point.y < minY) {
minY = point.y;
}
if (point.y > maxY) {
maxY = point.y;
}
}
return new $.Rect(minX, minY, maxX - minX, maxY - minY);
},
// private
_getSegments: function() {
var topLeft = this.getTopLeft();
var topRight = this.getTopRight();
var bottomLeft = this.getBottomLeft();
var bottomRight = this.getBottomRight();
return [[topLeft, topRight],
[topRight, bottomRight],
[bottomRight, bottomLeft],
[bottomLeft, topLeft]];
},
/** /**
* Rotates a rectangle around a point. * Rotates a rectangle around a point.
* @function * @function
* @param {Number} degrees The angle in degrees to rotate. * @param {Number} degrees The angle in degrees to rotate.
* @param {OpenSeadragon.Point} pivot The point about which to rotate. * @param {OpenSeadragon.Point} [pivot] The point about which to rotate.
* Defaults to the center of the rectangle. * Defaults to the center of the rectangle.
* @return {OpenSeadragon.Rect} * @return {OpenSeadragon.Rect}
*/ */
@ -340,6 +493,51 @@ $.Rect.prototype = {
maxY - minY); maxY - minY);
}, },
/**
* Retrieves the smallest horizontal (degrees=0) rectangle which contains
* this rectangle and has integers x, y, width and height
* @returns {OpenSeadragon.Rect}
*/
getIntegerBoundingBox: function() {
var boundingBox = this.getBoundingBox();
var x = Math.floor(boundingBox.x);
var y = Math.floor(boundingBox.y);
var width = Math.ceil(boundingBox.width + boundingBox.x - x);
var height = Math.ceil(boundingBox.height + boundingBox.y - y);
return new $.Rect(x, y, width, height);
},
/**
* Determines whether a point is inside this rectangle (edge included).
* @function
* @param {OpenSeadragon.Point} point
* @param {Number} [epsilon=0] the margin of error allowed
* @returns {Boolean} true if the point is inside this rectangle, false
* otherwise.
*/
containsPoint: function(point, epsilon) {
epsilon = epsilon || 0;
// See http://stackoverflow.com/a/2752754/1440403 for explanation
var topLeft = this.getTopLeft();
var topRight = this.getTopRight();
var bottomLeft = this.getBottomLeft();
var topDiff = topRight.minus(topLeft);
var leftDiff = bottomLeft.minus(topLeft);
return ((point.x - topLeft.x) * topDiff.x +
(point.y - topLeft.y) * topDiff.y >= -epsilon) &&
((point.x - topRight.x) * topDiff.x +
(point.y - topRight.y) * topDiff.y <= epsilon) &&
((point.x - topLeft.x) * leftDiff.x +
(point.y - topLeft.y) * leftDiff.y >= -epsilon) &&
((point.x - bottomLeft.x) * leftDiff.x +
(point.y - bottomLeft.y) * leftDiff.y <= epsilon);
},
/** /**
* Provides a string representation of the rectangle which is useful for * Provides a string representation of the rectangle which is useful for
* debugging. * debugging.
@ -348,10 +546,10 @@ $.Rect.prototype = {
*/ */
toString: function() { toString: function() {
return "[" + return "[" +
(Math.round(this.x * 100) / 100) + "," + (Math.round(this.x * 100) / 100) + ", " +
(Math.round(this.y * 100) / 100) + "," + (Math.round(this.y * 100) / 100) + ", " +
(Math.round(this.width * 100) / 100) + "x" + (Math.round(this.width * 100) / 100) + "x" +
(Math.round(this.height * 100) / 100) + "," + (Math.round(this.height * 100) / 100) + ", " +
(Math.round(this.degrees * 100) / 100) + "deg" + (Math.round(this.degrees * 100) / 100) + "deg" +
"]"; "]";
} }

View File

@ -276,7 +276,6 @@ $.extend( $.ReferenceStrip.prototype, $.EventSource.prototype, $.Viewer.prototyp
} }
this.currentPage = page; this.currentPage = page;
$.getElement( element.id + '-displayregion' ).focus();
onStripEnter.call( this, { eventSource: this.innerTracker } ); onStripEnter.call( this, { eventSource: this.innerTracker } );
} }
}, },

View File

@ -234,6 +234,15 @@ $.Spring.prototype = {
} else { } else {
this.current.value = currentValue; this.current.value = currentValue;
} }
},
/**
* Returns whether the spring is at the target value
* @function
* @returns {Boolean} True if at target value, false otherwise
*/
isAtTargetValue: function() {
return this.current.value === this.target.value;
} }
}; };

View File

@ -193,6 +193,11 @@ $.Tile.prototype = {
return this.level + "/" + this.x + "_" + this.y; return this.level + "/" + this.x + "_" + this.y;
}, },
// private
_hasTransparencyChannel: function() {
return !!this.context2D || this.url.match('.png');
},
/** /**
* Renders the tile in an html container. * Renders the tile in an html container.
* @function * @function
@ -280,27 +285,6 @@ $.Tile.prototype = {
context.globalAlpha = this.opacity; context.globalAlpha = this.opacity;
//if we are supposed to be rendering fully opaque rectangle,
//ie its done fading or fading is turned off, and if we are drawing
//an image with an alpha channel, then the only way
//to avoid seeing the tile underneath is to clear the rectangle
if (context.globalAlpha === 1 &&
(this.context2D || this.url.match('.png'))) {
//clearing only the inside of the rectangle occupied
//by the png prevents edge flikering
context.clearRect(
position.x + 1,
position.y + 1,
size.x - 2,
size.y - 2
);
}
// This gives the application a chance to make image manipulation
// changes as we are rendering the image
drawingHandler({context: context, tile: this, rendered: rendered});
if (typeof scale === 'number' && scale !== 1) { if (typeof scale === 'number' && scale !== 1) {
// draw tile at a different scale // draw tile at a different scale
position = position.times(scale); position = position.times(scale);
@ -312,6 +296,25 @@ $.Tile.prototype = {
position = position.plus(translate); position = position.plus(translate);
} }
//if we are supposed to be rendering fully opaque rectangle,
//ie its done fading or fading is turned off, and if we are drawing
//an image with an alpha channel, then the only way
//to avoid seeing the tile underneath is to clear the rectangle
if (context.globalAlpha === 1 && this._hasTransparencyChannel()) {
//clearing only the inside of the rectangle occupied
//by the png prevents edge flikering
context.clearRect(
position.x + 1,
position.y + 1,
size.x - 2,
size.y - 2
);
}
// This gives the application a chance to make image manipulation
// changes as we are rendering the image
drawingHandler({context: context, tile: this, rendered: rendered});
context.drawImage( context.drawImage(
rendered.canvas, rendered.canvas,
0, 0,
@ -333,15 +336,18 @@ $.Tile.prototype = {
* @return {Float} * @return {Float}
*/ */
getScaleForEdgeSmoothing: function() { getScaleForEdgeSmoothing: function() {
if (!this.cacheImageRecord) { var context;
if (this.cacheImageRecord) {
context = this.cacheImageRecord.getRenderedContext();
} else if (this.context2D) {
context = this.context2D;
} else {
$.console.warn( $.console.warn(
'[Tile.drawCanvas] attempting to get tile scale %s when tile\'s not cached', '[Tile.drawCanvas] attempting to get tile scale %s when tile\'s not cached',
this.toString()); this.toString());
return 1; return 1;
} }
return context.canvas.width / (this.size.x * $.pixelDensityRatio);
var rendered = this.cacheImageRecord.getRenderedContext();
return rendered.canvas.width / this.size.times($.pixelDensityRatio).x;
}, },
/** /**

View File

@ -52,6 +52,10 @@
* @param {Number} [options.y=0] - Top position, in viewport coordinates. * @param {Number} [options.y=0] - Top position, in viewport coordinates.
* @param {Number} [options.width=1] - Width, in viewport coordinates. * @param {Number} [options.width=1] - Width, in viewport coordinates.
* @param {Number} [options.height] - Height, in viewport coordinates. * @param {Number} [options.height] - Height, in viewport coordinates.
* @param {OpenSeadragon.Rect} [options.fitBounds] The bounds in viewport coordinates
* to fit the image into. If specified, x, y, width and height get ignored.
* @param {OpenSeadragon.Placement} [options.fitBoundsPlacement=OpenSeadragon.Placement.CENTER]
* How to anchor the image in the bounds if options.fitBounds is set.
* @param {OpenSeadragon.Rect} [options.clip] - An area, in image pixels, to clip to * @param {OpenSeadragon.Rect} [options.clip] - An area, in image pixels, to clip to
* (portions of the image outside of this area will not be visible). Only works on * (portions of the image outside of this area will not be visible). Only works on
* browsers that support the HTML5 canvas. * browsers that support the HTML5 canvas.
@ -122,6 +126,11 @@ $.TiledImage = function( options ) {
delete options.height; delete options.height;
} }
var fitBounds = options.fitBounds;
delete options.fitBounds;
var fitBoundsPlacement = options.fitBoundsPlacement || OpenSeadragon.Placement.CENTER;
delete options.fitBoundsPlacement;
$.extend( true, this, { $.extend( true, this, {
//internal state properties //internal state properties
@ -172,6 +181,10 @@ $.TiledImage = function( options ) {
this._updateForScale(); this._updateForScale();
if (fitBounds) {
this.fitBounds(fitBounds, fitBoundsPlacement, true);
}
// We need a callback to give image manipulation a chance to happen // We need a callback to give image manipulation a chance to happen
this._drawingHandler = function(args) { this._drawingHandler = function(args) {
/** /**
@ -274,6 +287,26 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
return this.getBounds(); return this.getBounds();
}, },
/**
* Get the bounds of the displayed part of the tiled image.
* @param {Boolean} [current=false] Pass true for the current location,
* false for the target location.
* @returns {$.Rect} The clipped bounds in viewport coordinates.
*/
getClippedBounds: function(current) {
var bounds = this.getBounds(current);
if (this._clip) {
var ratio = this._worldWidthCurrent / this.source.dimensions.x;
var clip = this._clip.times(ratio);
bounds = new $.Rect(
bounds.x + clip.x,
bounds.y + clip.y,
clip.width,
clip.height);
}
return bounds;
},
/** /**
* @returns {OpenSeadragon.Point} This TiledImage's content size, in original pixels. * @returns {OpenSeadragon.Point} This TiledImage's content size, in original pixels.
*/ */
@ -543,6 +576,67 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
this._setScale(height / this.normHeight, immediately); this._setScale(height / this.normHeight, immediately);
}, },
/**
* Positions and scales the TiledImage to fit in the specified bounds.
* Note: this method fires OpenSeadragon.TiledImage.event:bounds-change
* twice
* @param {OpenSeadragon.Rect} bounds The bounds to fit the image into.
* @param {OpenSeadragon.Placement} [anchor=OpenSeadragon.Placement.CENTER]
* How to anchor the image in the bounds.
* @param {Boolean} [immediately=false] Whether to animate to the new size
* or snap immediately.
* @fires OpenSeadragon.TiledImage.event:bounds-change
*/
fitBounds: function(bounds, anchor, immediately) {
anchor = anchor || $.Placement.CENTER;
var anchorProperties = $.Placement.properties[anchor];
var aspectRatio = this.contentAspectX;
var xOffset = 0;
var yOffset = 0;
var displayedWidthRatio = 1;
var displayedHeightRatio = 1;
if (this._clip) {
aspectRatio = this._clip.getAspectRatio();
displayedWidthRatio = this._clip.width / this.source.dimensions.x;
displayedHeightRatio = this._clip.height / this.source.dimensions.y;
if (bounds.getAspectRatio() > aspectRatio) {
xOffset = this._clip.x / this._clip.height * bounds.height;
yOffset = this._clip.y / this._clip.height * bounds.height;
} else {
xOffset = this._clip.x / this._clip.width * bounds.width;
yOffset = this._clip.y / this._clip.width * bounds.width;
}
}
if (bounds.getAspectRatio() > aspectRatio) {
// We will have margins on the X axis
var height = bounds.height / displayedHeightRatio;
var marginLeft = 0;
if (anchorProperties.isHorizontallyCentered) {
marginLeft = (bounds.width - bounds.height * aspectRatio) / 2;
} else if (anchorProperties.isRight) {
marginLeft = bounds.width - bounds.height * aspectRatio;
}
this.setPosition(
new $.Point(bounds.x - xOffset + marginLeft, bounds.y - yOffset),
immediately);
this.setHeight(height, immediately);
} else {
// We will have margins on the Y axis
var width = bounds.width / displayedWidthRatio;
var marginTop = 0;
if (anchorProperties.isVerticallyCentered) {
marginTop = (bounds.height - bounds.width / aspectRatio) / 2;
} else if (anchorProperties.isBottom) {
marginTop = bounds.height - bounds.width / aspectRatio;
}
this.setPosition(
new $.Point(bounds.x - xOffset, bounds.y - yOffset + marginTop),
immediately);
this.setWidth(width, immediately);
}
},
/** /**
* @returns {OpenSeadragon.Rect|null} The TiledImage's current clip rectangle, * @returns {OpenSeadragon.Rect|null} The TiledImage's current clip rectangle,
* in image pixels, or null if none. * in image pixels, or null if none.
@ -650,6 +744,11 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @property {?Object} userData - Arbitrary subscriber-defined object. * @property {?Object} userData - Arbitrary subscriber-defined object.
*/ */
this.raiseEvent('bounds-change'); this.raiseEvent('bounds-change');
},
// private
_isBottomItem: function() {
return this.viewer.world.getItemAt(0) === this;
} }
}); });
@ -688,7 +787,6 @@ function updateViewport( tiledImage ) {
Math.log( 2 ) Math.log( 2 )
)) ))
), ),
degrees = tiledImage.viewport.degrees,
renderPixelRatioC, renderPixelRatioC,
renderPixelRatioT, renderPixelRatioT,
zeroRatioT, zeroRatioT,
@ -696,26 +794,23 @@ function updateViewport( tiledImage ) {
levelOpacity, levelOpacity,
levelVisibility; levelVisibility;
viewportBounds.x -= tiledImage._xSpring.current.value;
viewportBounds.y -= tiledImage._ySpring.current.value;
// Reset tile's internal drawn state // Reset tile's internal drawn state
while ( tiledImage.lastDrawn.length > 0 ) { while (tiledImage.lastDrawn.length > 0) {
tile = tiledImage.lastDrawn.pop(); tile = tiledImage.lastDrawn.pop();
tile.beingDrawn = false; tile.beingDrawn = false;
} }
//Change bounds for rotation if (!tiledImage.wrapHorizontal && !tiledImage.wrapVertical) {
if (degrees === 90 || degrees === 270) { var tiledImageBounds = tiledImage.getClippedBounds(true);
viewportBounds = viewportBounds.rotate( degrees ); var intersection = viewportBounds.intersection(tiledImageBounds);
} else if (degrees !== 0 && degrees !== 180) { if (intersection === null) {
// This is just an approximation. return;
var orthBounds = viewportBounds.rotate(90); }
viewportBounds.x -= orthBounds.width / 2; viewportBounds = intersection;
viewportBounds.y -= orthBounds.height / 2;
viewportBounds.width += orthBounds.width;
viewportBounds.height += orthBounds.height;
} }
viewportBounds = viewportBounds.getBoundingBox();
viewportBounds.x -= tiledImage._xSpring.current.value;
viewportBounds.y -= tiledImage._ySpring.current.value;
var viewportTL = viewportBounds.getTopLeft(); var viewportTL = viewportBounds.getTopLeft();
var viewportBR = viewportBounds.getBottomRight(); var viewportBR = viewportBounds.getBottomRight();
@ -1324,22 +1419,22 @@ function compareTiles( previousBest, tile ) {
} }
function drawTiles( tiledImage, lastDrawn ) { function drawTiles( tiledImage, lastDrawn ) {
var i, if (lastDrawn.length === 0) {
tile = lastDrawn[0];
if ( tiledImage.opacity <= 0 ) {
drawDebugInfo( tiledImage, lastDrawn );
return; return;
} }
var tile = lastDrawn[0];
var useSketch = tiledImage.opacity < 1 || var useSketch = tiledImage.opacity < 1 ||
(tiledImage.compositeOperation && tiledImage.compositeOperation !== 'source-over'); (tiledImage.compositeOperation &&
tiledImage.compositeOperation !== 'source-over') ||
(!tiledImage._isBottomItem() && tile._hasTransparencyChannel());
var sketchScale; var sketchScale;
var sketchTranslate; var sketchTranslate;
var zoom = tiledImage.viewport.getZoom(true); var zoom = tiledImage.viewport.getZoom(true);
var imageZoom = tiledImage.viewportToImageZoom(zoom); var imageZoom = tiledImage.viewportToImageZoom(zoom);
if (imageZoom > tiledImage.smoothTileEdgesMinZoom && tile) { if (imageZoom > tiledImage.smoothTileEdgesMinZoom) {
// When zoomed in a lot (>100%) the tile edges are visible. // When zoomed in a lot (>100%) the tile edges are visible.
// So we have to composite them at ~100% and scale them up together. // So we have to composite them at ~100% and scale them up together.
useSketch = true; useSketch = true;
@ -1349,8 +1444,17 @@ function drawTiles( tiledImage, lastDrawn ) {
tiledImage._drawer.getCanvasSize(true)); tiledImage._drawer.getCanvasSize(true));
} }
if ( useSketch ) { var bounds;
tiledImage._drawer._clear( true ); if (useSketch) {
if (!sketchScale) {
// Except when edge smoothing, we only clean the part of the
// sketch canvas we are going to use for performance reasons.
bounds = tiledImage.viewport.viewportToViewerElementRectangle(
tiledImage.getClippedBounds(true))
.getIntegerBoundingBox()
.times($.pixelDensityRatio);
}
tiledImage._drawer._clear(true, bounds);
} }
// When scaling, we must rotate only when blending the sketch canvas to avoid // When scaling, we must rotate only when blending the sketch canvas to avoid
@ -1396,7 +1500,7 @@ function drawTiles( tiledImage, lastDrawn ) {
tiledImage._drawer.drawRectangle(placeholderRect, fillStyle, useSketch); tiledImage._drawer.drawRectangle(placeholderRect, fillStyle, useSketch);
} }
for ( i = lastDrawn.length - 1; i >= 0; i-- ) { for (var i = lastDrawn.length - 1; i >= 0; i--) {
tile = lastDrawn[ i ]; tile = lastDrawn[ i ];
tiledImage._drawer.drawTile( tile, tiledImage._drawingHandler, useSketch, sketchScale, sketchTranslate ); tiledImage._drawer.drawTile( tile, tiledImage._drawingHandler, useSketch, sketchScale, sketchTranslate );
tile.beingDrawn = true; tile.beingDrawn = true;
@ -1433,7 +1537,13 @@ function drawTiles( tiledImage, lastDrawn ) {
if (offsetForRotation) { if (offsetForRotation) {
tiledImage._drawer._offsetForRotation(tiledImage.viewport.degrees, false); tiledImage._drawer._offsetForRotation(tiledImage.viewport.degrees, false);
} }
tiledImage._drawer.blendSketch(tiledImage.opacity, sketchScale, sketchTranslate, tiledImage.compositeOperation); tiledImage._drawer.blendSketch({
opacity: tiledImage.opacity,
scale: sketchScale,
translate: sketchTranslate,
compositeOperation: tiledImage.compositeOperation,
bounds: bounds
});
if (offsetForRotation) { if (offsetForRotation) {
tiledImage._drawer._restoreRotationChanges(false); tiledImage._drawer._restoreRotationChanges(false);
} }

View File

@ -190,7 +190,7 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve
this.aspectRatio = ( options.width && options.height ) ? this.aspectRatio = ( options.width && options.height ) ?
( options.width / options.height ) : 1; ( options.width / options.height ) : 1;
this.dimensions = new $.Point( options.width, options.height ); this.dimensions = new $.Point( options.width, options.height );
if ( this.tileSize ){ if ( this.tileSize ){
this._tileWidth = this._tileHeight = this.tileSize; this._tileWidth = this._tileHeight = this.tileSize;
delete this.tileSize; delete this.tileSize;
@ -212,7 +212,7 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve
this._tileHeight = 0; this._tileHeight = 0;
} }
} }
this.tileOverlap = options.tileOverlap ? options.tileOverlap : 0; this.tileOverlap = options.tileOverlap ? options.tileOverlap : 0;
this.minLevel = options.minLevel ? options.minLevel : 0; this.minLevel = options.minLevel ? options.minLevel : 0;
this.maxLevel = ( undefined !== options.maxLevel && null !== options.maxLevel ) ? this.maxLevel = ( undefined !== options.maxLevel && null !== options.maxLevel ) ?
@ -240,7 +240,7 @@ $.TileSource.prototype = {
); );
return this._tileWidth; return this._tileWidth;
}, },
/** /**
* Return the tileWidth for a given level. * Return the tileWidth for a given level.
* Subclasses should override this if tileWidth can be different at different levels * Subclasses should override this if tileWidth can be different at different levels
@ -331,7 +331,7 @@ $.TileSource.prototype = {
Math.floor( rect.x / this.getTileWidth(i) ), Math.floor( rect.x / this.getTileWidth(i) ),
Math.floor( rect.y / this.getTileHeight(i) ) Math.floor( rect.y / this.getTileHeight(i) )
); );
if( tiles.x + 1 >= tilesPerSide.x && tiles.y + 1 >= tilesPerSide.y ){ if( tiles.x + 1 >= tilesPerSide.x && tiles.y + 1 >= tilesPerSide.y ){
break; break;
} }
@ -544,7 +544,7 @@ $.TileSource.prototype = {
/** /**
* Responsible for retriving the url which will return an image for the * Responsible for retriving the url which will return an image for the
* region speified by the given x, y, and level components. * region specified by the given x, y, and level components.
* This method is not implemented by this class other than to throw an Error * 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 * announcing you have to implement it. Because of the variety of tile
* server technologies, and various specifications for building image * server technologies, and various specifications for building image

View File

@ -40,14 +40,19 @@ var nextHash = 1;
/** /**
* *
* The main point of entry into creating a zoomable image on the page. * The main point of entry into creating a zoomable image on the page.<br>
* * <br>
* We have provided an idiomatic javascript constructor which takes * We have provided an idiomatic javascript constructor which takes
* a single object, but still support the legacy positional arguments. * a single object, but still support the legacy positional arguments.<br>
* * <br>
* The options below are given in order that they appeared in the constructor * 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. * as arguments and we translate a positional call into an idiomatic call.<br>
* * <br>
* To create a viewer, you can use either of this methods:<br>
* <ul>
* <li><code>var viewer = new OpenSeadragon.Viewer(options);</code></li>
* <li><code>var viewer = OpenSeadragon(options);</code></li>
* </ul>
* @class Viewer * @class Viewer
* @classdesc The main OpenSeadragon viewer class. * @classdesc The main OpenSeadragon viewer class.
* *
@ -234,7 +239,9 @@ $.Viewer = function( options ) {
style.left = "0px"; style.left = "0px";
}(this.canvas.style)); }(this.canvas.style));
$.setElementTouchActionNone( this.canvas ); $.setElementTouchActionNone( this.canvas );
this.canvas.tabIndex = (options.tabIndex === undefined ? 0 : options.tabIndex); if (options.tabIndex !== "") {
this.canvas.tabIndex = (options.tabIndex === undefined ? 0 : options.tabIndex);
}
//the container is created through applying the ControlDock constructor above //the container is created through applying the ControlDock constructor above
this.container.className = "openseadragon-container"; this.container.className = "openseadragon-container";
@ -410,6 +417,7 @@ $.Viewer = function( options ) {
width: this.navigatorWidth, width: this.navigatorWidth,
height: this.navigatorHeight, height: this.navigatorHeight,
autoResize: this.navigatorAutoResize, autoResize: this.navigatorAutoResize,
autoFade: this.navigatorAutoFade,
prefixUrl: this.prefixUrl, prefixUrl: this.prefixUrl,
viewer: this, viewer: this,
navigatorRotate: this.navigatorRotate, navigatorRotate: this.navigatorRotate,
@ -911,9 +919,14 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
docStyle.padding = "0"; docStyle.padding = "0";
this.bodyWidth = bodyStyle.width; this.bodyWidth = bodyStyle.width;
this.bodyHeight = bodyStyle.height; this.docWidth = docStyle.width;
bodyStyle.width = "100%"; bodyStyle.width = "100%";
docStyle.width = "100%";
this.bodyHeight = bodyStyle.height;
this.docHeight = docStyle.height;
bodyStyle.height = "100%"; bodyStyle.height = "100%";
docStyle.height = "100%";
//when entering full screen on the ipad it wasnt sufficient to leave //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 //the body intact as only only the top half of the screen would
@ -974,7 +987,10 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
docStyle.padding = this.docPadding; docStyle.padding = this.docPadding;
bodyStyle.width = this.bodyWidth; bodyStyle.width = this.bodyWidth;
docStyle.width = this.docWidth;
bodyStyle.height = this.bodyHeight; bodyStyle.height = this.bodyHeight;
docStyle.height = this.docHeight;
body.removeChild( this.element ); body.removeChild( this.element );
nodes = this.previousBody.length; nodes = this.previousBody.length;
@ -1204,6 +1220,10 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
* @param {Number} [options.y=0] The Y position for the image in viewport coordinates. * @param {Number} [options.y=0] The Y position for the image in viewport coordinates.
* @param {Number} [options.width=1] The width for the image in viewport coordinates. * @param {Number} [options.width=1] The width for the image in viewport coordinates.
* @param {Number} [options.height] The height for the image in viewport coordinates. * @param {Number} [options.height] The height for the image in viewport coordinates.
* @param {OpenSeadragon.Rect} [options.fitBounds] The bounds in viewport coordinates
* to fit the image into. If specified, x, y, width and height get ignored.
* @param {OpenSeadragon.Placement} [options.fitBoundsPlacement=OpenSeadragon.Placement.CENTER]
* How to anchor the image in the bounds if options.fitBounds is set.
* @param {OpenSeadragon.Rect} [options.clip] - An area, in image pixels, to clip to * @param {OpenSeadragon.Rect} [options.clip] - An area, in image pixels, to clip to
* (portions of the image outside of this area will not be visible). Only works on * (portions of the image outside of this area will not be visible). Only works on
* browsers that support the HTML5 canvas. * browsers that support the HTML5 canvas.
@ -1339,6 +1359,8 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
y: queueItem.options.y, y: queueItem.options.y,
width: queueItem.options.width, width: queueItem.options.width,
height: queueItem.options.height, height: queueItem.options.height,
fitBounds: queueItem.options.fitBounds,
fitBoundsPlacement: queueItem.options.fitBoundsPlacement,
clip: queueItem.options.clip, clip: queueItem.options.clip,
placeholderFillStyle: queueItem.options.placeholderFillStyle, placeholderFillStyle: queueItem.options.placeholderFillStyle,
opacity: queueItem.options.opacity, opacity: queueItem.options.opacity,
@ -1780,10 +1802,12 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
* is closed which include when changing page. * is closed which include when changing page.
* @method * @method
* @param {Element|String|Object} element - A reference to an element or an id for * @param {Element|String|Object} element - A reference to an element or an id for
* the element which will be overlayed. Or an Object specifying the configuration for the overlay * the element which will be overlayed. Or an Object specifying the configuration for the overlay.
* If using an object, see {@link OpenSeadragon.Overlay} for a list of
* all available options.
* @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or
* rectangle which will be overlayed. * rectangle which will be overlayed. This is a viewport relative location.
* @param {OpenSeadragon.OverlayPlacement} placement - The position of the * @param {OpenSeadragon.Placement} placement - The position of the
* viewport which the location coordinates will be treated as relative * viewport which the location coordinates will be treated as relative
* to. * to.
* @param {function} onDraw - If supplied the callback is called when the overlay * @param {function} onDraw - If supplied the callback is called when the overlay
@ -1825,7 +1849,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Element} element - The overlay element. * @property {Element} element - The overlay element.
* @property {OpenSeadragon.Point|OpenSeadragon.Rect} location * @property {OpenSeadragon.Point|OpenSeadragon.Rect} location
* @property {OpenSeadragon.OverlayPlacement} placement * @property {OpenSeadragon.Placement} placement
* @property {?Object} userData - Arbitrary subscriber-defined object. * @property {?Object} userData - Arbitrary subscriber-defined object.
*/ */
this.raiseEvent( 'add-overlay', { this.raiseEvent( 'add-overlay', {
@ -1843,8 +1867,8 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
* @param {Element|String} element - A reference to an element or an id for * @param {Element|String} element - A reference to an element or an id for
* the element which is overlayed. * the element which is overlayed.
* @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or
* rectangle which will be overlayed. * rectangle which will be overlayed. This is a viewport relative location.
* @param {OpenSeadragon.OverlayPlacement} placement - The position of the * @param {OpenSeadragon.Placement} placement - The position of the
* viewport which the location coordinates will be treated as relative * viewport which the location coordinates will be treated as relative
* to. * to.
* @return {OpenSeadragon.Viewer} Chainable. * @return {OpenSeadragon.Viewer} Chainable.
@ -1870,7 +1894,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
* Viewer which raised the event. * Viewer which raised the event.
* @property {Element} element * @property {Element} element
* @property {OpenSeadragon.Point|OpenSeadragon.Rect} location * @property {OpenSeadragon.Point|OpenSeadragon.Rect} location
* @property {OpenSeadragon.OverlayPlacement} placement * @property {OpenSeadragon.Placement} placement
* @property {?Object} userData - Arbitrary subscriber-defined object. * @property {?Object} userData - Arbitrary subscriber-defined object.
*/ */
this.raiseEvent( 'update-overlay', { this.raiseEvent( 'update-overlay', {
@ -2191,37 +2215,28 @@ function getOverlayObject( viewer, overlay ) {
} }
var location = overlay.location; var location = overlay.location;
if ( !location ) { var width = overlay.width;
if ( overlay.width && overlay.height ) { var height = overlay.height;
location = overlay.px !== undefined ? if (!location) {
viewer.viewport.imageToViewportRectangle( new $.Rect( var x = overlay.x;
overlay.px, var y = overlay.y;
overlay.py, if (overlay.px !== undefined) {
overlay.width, var rect = viewer.viewport.imageToViewportRectangle(new $.Rect(
overlay.height overlay.px,
) ) : overlay.py,
new $.Rect( width || 0,
overlay.x, height || 0));
overlay.y, x = rect.x;
overlay.width, y = rect.y;
overlay.height width = width !== undefined ? rect.width : undefined;
); height = height !== undefined ? rect.height : undefined;
} else {
location = overlay.px !== undefined ?
viewer.viewport.imageToViewportCoordinates( new $.Point(
overlay.px,
overlay.py
) ) :
new $.Point(
overlay.x,
overlay.y
);
} }
location = new $.Point(x, y);
} }
var placement = overlay.placement; var placement = overlay.placement;
if ( placement && ( $.type( placement ) === "string" ) ) { if (placement && $.type(placement) === "string") {
placement = $.OverlayPlacement[ overlay.placement.toUpperCase() ]; placement = $.Placement[overlay.placement.toUpperCase()];
} }
return new $.Overlay({ return new $.Overlay({
@ -2229,7 +2244,10 @@ function getOverlayObject( viewer, overlay ) {
location: location, location: location,
placement: placement, placement: placement,
onDraw: overlay.onDraw, onDraw: overlay.onDraw,
checkResize: overlay.checkResize checkResize: overlay.checkResize,
width: width,
height: height,
rotationMode: overlay.rotationMode
}); });
} }
@ -2542,22 +2560,25 @@ function onCanvasDrag( event ) {
} }
function onCanvasDragEnd( event ) { function onCanvasDragEnd( event ) {
var gestureSettings; if (!event.preventDefaultAction && this.viewport) {
var gestureSettings = this.gestureSettingsByDeviceType(event.pointerType);
if ( !event.preventDefaultAction && this.viewport ) { if (gestureSettings.flickEnabled &&
gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); event.speed >= gestureSettings.flickMinSpeed) {
if ( gestureSettings.flickEnabled && event.speed >= gestureSettings.flickMinSpeed ) { var amplitudeX = 0;
var amplitudeX = gestureSettings.flickMomentum * ( event.speed * Math.cos( event.direction - (Math.PI / 180 * this.viewport.degrees) ) ), if (this.panHorizontal) {
amplitudeY = gestureSettings.flickMomentum * ( event.speed * Math.sin( event.direction - (Math.PI / 180 * this.viewport.degrees) ) ), amplitudeX = gestureSettings.flickMomentum * event.speed *
center = this.viewport.pixelFromPoint( this.viewport.getCenter( true ) ), Math.cos(event.direction);
target = this.viewport.pointFromPixel( new $.Point( center.x - amplitudeX, center.y - amplitudeY ) );
if( !this.panHorizontal ) {
target.x = center.x;
} }
if( !this.panVertical ) { var amplitudeY = 0;
target.y = center.y; if (this.panVertical) {
amplitudeY = gestureSettings.flickMomentum * event.speed *
Math.sin(event.direction);
} }
this.viewport.panTo( target, false ); var center = this.viewport.pixelFromPoint(
this.viewport.getCenter(true));
var target = this.viewport.pointFromPixel(
new $.Point(center.x - amplitudeX, center.y - amplitudeY));
this.viewport.panTo(target, false);
} }
this.viewport.applyConstraints(); this.viewport.applyConstraints();
} }
@ -2576,7 +2597,7 @@ function onCanvasDragEnd( event ) {
* @property {Object} originalEvent - The original DOM event. * @property {Object} originalEvent - The original DOM event.
* @property {?Object} userData - Arbitrary subscriber-defined object. * @property {?Object} userData - Arbitrary subscriber-defined object.
*/ */
this.raiseEvent( 'canvas-drag-end', { this.raiseEvent('canvas-drag-end', {
tracker: event.eventSource, tracker: event.eventSource,
position: event.position, position: event.position,
speed: event.speed, speed: event.speed,
@ -2963,33 +2984,26 @@ function updateOnce( viewer ) {
return; return;
} }
var containerSize; if (viewer.autoResize) {
if ( viewer.autoResize ) { var containerSize = _getSafeElemSize(viewer.container);
containerSize = _getSafeElemSize( viewer.container ); var prevContainerSize = THIS[viewer.hash].prevContainerSize;
if ( !containerSize.equals( THIS[ viewer.hash ].prevContainerSize ) ) { if (!containerSize.equals(prevContainerSize)) {
if ( viewer.preserveImageSizeOnResize ) { var viewport = viewer.viewport;
var prevContainerSize = THIS[ viewer.hash ].prevContainerSize; if (viewer.preserveImageSizeOnResize) {
var bounds = viewer.viewport.getBounds(true); var resizeRatio = prevContainerSize.x / containerSize.x;
var deltaX = (containerSize.x - prevContainerSize.x); var zoom = viewport.getZoom() * resizeRatio;
var deltaY = (containerSize.y - prevContainerSize.y); var center = viewport.getCenter();
var viewportDiff = viewer.viewport.deltaPointsFromPixels(new OpenSeadragon.Point(deltaX, deltaY), true); viewport.resize(containerSize, false);
viewer.viewport.resize(new OpenSeadragon.Point(containerSize.x, containerSize.y), false); viewport.zoomTo(zoom, null, true);
viewport.panTo(center, true);
// Keep the center of the image in the center and just adjust the amount of image shown } else {
bounds.width += viewportDiff.x;
bounds.height += viewportDiff.y;
bounds.x -= (viewportDiff.x / 2);
bounds.y -= (viewportDiff.y / 2);
viewer.viewport.fitBoundsWithConstraints(bounds, true);
}
else {
// maintain image position // maintain image position
var oldBounds = viewer.viewport.getBounds(); var oldBounds = viewport.getBounds();
var oldCenter = viewer.viewport.getCenter(); viewport.resize(containerSize, true);
resizeViewportAndRecenter(viewer, containerSize, oldBounds, oldCenter); viewport.fitBoundsWithConstraints(oldBounds, true);
} }
THIS[ viewer.hash ].prevContainerSize = containerSize; THIS[viewer.hash].prevContainerSize = containerSize;
THIS[ viewer.hash ].forceRedraw = true; THIS[viewer.hash].forceRedraw = true;
} }
} }
@ -3074,27 +3088,6 @@ function updateOnce( viewer ) {
//viewer.profiler.endUpdate(); //viewer.profiler.endUpdate();
} }
// This function resizes the viewport and recenters the image
// as it was before resizing.
// TODO: better adjust width and height. The new width and height
// should depend on the image dimensions and on the dimensions
// of the viewport before and after switching mode.
function resizeViewportAndRecenter( viewer, containerSize, oldBounds, oldCenter ) {
var viewport = viewer.viewport;
viewport.resize( containerSize, true );
var newBounds = new $.Rect(
oldCenter.x - ( oldBounds.width / 2.0 ),
oldCenter.y - ( oldBounds.height / 2.0 ),
oldBounds.width,
oldBounds.height
);
// let the viewport decide if the bounds are too big or too small
viewport.fitBoundsWithConstraints( newBounds, true );
}
function drawWorld( viewer ) { function drawWorld( viewer ) {
viewer.imageLoader.clear(); viewer.imageLoader.clear();
viewer.drawer.clear(); viewer.drawer.clear();

View File

@ -237,6 +237,17 @@ $.Viewport.prototype = {
* @returns {OpenSeadragon.Rect} The home bounds in vewport coordinates. * @returns {OpenSeadragon.Rect} The home bounds in vewport coordinates.
*/ */
getHomeBounds: function() { getHomeBounds: function() {
return this.getHomeBoundsNoRotate().rotate(-this.getRotation());
},
/**
* Returns the home bounds in viewport coordinates.
* This method ignores the viewport rotation. Use
* {@link OpenSeadragon.Viewport#getHomeBounds} to take it into account.
* @function
* @returns {OpenSeadragon.Rect} The home bounds in vewport coordinates.
*/
getHomeBoundsNoRotate: function() {
var center = this._contentBounds.getCenter(); var center = this._contentBounds.getCenter();
var width = 1.0 / this.getHomeZoom(); var width = 1.0 / this.getHomeZoom();
var height = width / this.getAspectRatio(); var height = width / this.getAspectRatio();
@ -254,8 +265,8 @@ $.Viewport.prototype = {
* @param {Boolean} immediately * @param {Boolean} immediately
* @fires OpenSeadragon.Viewer.event:home * @fires OpenSeadragon.Viewer.event:home
*/ */
goHome: function( immediately ) { goHome: function(immediately) {
if( this.viewer ){ if (this.viewer) {
/** /**
* Raised when the "home" operation occurs (see {@link OpenSeadragon.Viewport#goHome}). * Raised when the "home" operation occurs (see {@link OpenSeadragon.Viewport#goHome}).
* *
@ -266,11 +277,11 @@ $.Viewport.prototype = {
* @property {Boolean} immediately * @property {Boolean} immediately
* @property {?Object} userData - Arbitrary subscriber-defined object. * @property {?Object} userData - Arbitrary subscriber-defined object.
*/ */
this.viewer.raiseEvent( 'home', { this.viewer.raiseEvent('home', {
immediately: immediately immediately: immediately
}); });
} }
return this.fitBounds( this.getHomeBounds(), immediately ); return this.fitBounds(this.getHomeBounds(), immediately);
}, },
/** /**
@ -317,8 +328,8 @@ $.Viewport.prototype = {
}, },
/** /**
* @function
* The margins push the "home" region in from the sides by the specified amounts. * The margins push the "home" region in from the sides by the specified amounts.
* @function
* @returns {Object} Properties (Numbers, in screen coordinates): left, top, right, bottom. * @returns {Object} Properties (Numbers, in screen coordinates): left, top, right, bottom.
*/ */
getMargins: function() { getMargins: function() {
@ -326,8 +337,8 @@ $.Viewport.prototype = {
}, },
/** /**
* @function
* The margins push the "home" region in from the sides by the specified amounts. * The margins push the "home" region in from the sides by the specified amounts.
* @function
* @param {Object} margins - Properties (Numbers, in screen coordinates): left, top, right, bottom. * @param {Object} margins - Properties (Numbers, in screen coordinates): left, top, right, bottom.
*/ */
setMargins: function(margins) { setMargins: function(margins) {
@ -341,7 +352,9 @@ $.Viewport.prototype = {
}, margins); }, margins);
this._updateContainerInnerSize(); this._updateContainerInnerSize();
this.viewer.forceRedraw(); if (this.viewer) {
this.viewer.forceRedraw();
}
}, },
/** /**
@ -350,14 +363,26 @@ $.Viewport.prototype = {
* @param {Boolean} current - Pass true for the current location; defaults to false (target location). * @param {Boolean} current - Pass true for the current location; defaults to false (target location).
* @returns {OpenSeadragon.Rect} The location you are zoomed/panned to, in viewport coordinates. * @returns {OpenSeadragon.Rect} The location you are zoomed/panned to, in viewport coordinates.
*/ */
getBounds: function( current ) { getBounds: function(current) {
var center = this.getCenter( current ), return this.getBoundsNoRotate(current).rotate(-this.getRotation());
width = 1.0 / this.getZoom( current ), },
height = width / this.getAspectRatio();
/**
* Returns the bounds of the visible area in viewport coordinates.
* This method ignores the viewport rotation. Use
* {@link OpenSeadragon.Viewport#getBounds} to take it into account.
* @function
* @param {Boolean} current - Pass true for the current location; defaults to false (target location).
* @returns {OpenSeadragon.Rect} The location you are zoomed/panned to, in viewport coordinates.
*/
getBoundsNoRotate: function(current) {
var center = this.getCenter(current);
var width = 1.0 / this.getZoom(current);
var height = width / this.getAspectRatio();
return new $.Rect( return new $.Rect(
center.x - ( width / 2.0 ), center.x - (width / 2.0),
center.y - ( height / 2.0 ), center.y - (height / 2.0),
width, width,
height height
); );
@ -369,8 +394,19 @@ $.Viewport.prototype = {
* @returns {OpenSeadragon.Rect} The location you are zoomed/panned to, * @returns {OpenSeadragon.Rect} The location you are zoomed/panned to,
* including the space taken by margins, in viewport coordinates. * including the space taken by margins, in viewport coordinates.
*/ */
getBoundsWithMargins: function( current ) { getBoundsWithMargins: function(current) {
var bounds = this.getBounds(current); return this.getBoundsNoRotateWithMargins(current).rotate(
-this.getRotation(), this.getCenter(current));
},
/**
* @function
* @param {Boolean} current - Pass true for the current location; defaults to false (target location).
* @returns {OpenSeadragon.Rect} The location you are zoomed/panned to,
* including the space taken by margins, in viewport coordinates.
*/
getBoundsNoRotateWithMargins: function(current) {
var bounds = this.getBoundsNoRotate(current);
var factor = this._containerInnerSize.x * this.getZoom(current); var factor = this._containerInnerSize.x * this.getZoom(current);
bounds.x -= this._margins.left / factor; bounds.x -= this._margins.left / factor;
bounds.y -= this._margins.top / factor; bounds.y -= this._margins.top / factor;
@ -438,6 +474,13 @@ $.Viewport.prototype = {
} }
}, },
// private
_applyZoomConstraints: function(zoom) {
return Math.max(
Math.min(zoom, this.getMaxZoom()),
this.getMinZoom());
},
/** /**
* @function * @function
* @private * @private
@ -519,40 +562,44 @@ $.Viewport.prototype = {
}, },
/** /**
* Enforces the minZoom, maxZoom and visibilityRatio constraints by
* zooming and panning to the closest acceptable zoom and location.
* @function * @function
* @param {Boolean} [immediately=false]
* @return {OpenSeadragon.Viewport} Chainable. * @return {OpenSeadragon.Viewport} Chainable.
* @fires OpenSeadragon.Viewer.event:constrain * @fires OpenSeadragon.Viewer.event:constrain
*/ */
applyConstraints: function( immediately ) { applyConstraints: function(immediately) {
var actualZoom = this.getZoom(), var actualZoom = this.getZoom();
constrainedZoom = Math.max( var constrainedZoom = this._applyZoomConstraints(actualZoom);
Math.min( actualZoom, this.getMaxZoom() ),
this.getMinZoom()
),
bounds,
constrainedBounds;
if ( actualZoom != constrainedZoom ) { if (actualZoom !== constrainedZoom) {
this.zoomTo( constrainedZoom, this.zoomPoint, immediately ); this.zoomTo(constrainedZoom, this.zoomPoint, immediately);
} }
bounds = this.getBounds(); var bounds = this.getBoundsNoRotate();
var constrainedBounds = this._applyBoundaryConstraints(
bounds, immediately);
constrainedBounds = this._applyBoundaryConstraints( bounds, immediately ); if (bounds.x !== constrainedBounds.x ||
bounds.y !== constrainedBounds.y ||
if ( bounds.x !== constrainedBounds.x || bounds.y !== constrainedBounds.y || immediately ){ immediately) {
this.fitBounds( constrainedBounds, immediately ); this.fitBounds(
constrainedBounds.rotate(-this.getRotation()),
immediately);
} }
return this; return this;
}, },
/** /**
* Equivalent to {@link OpenSeadragon.Viewport#applyConstraints}
* @function * @function
* @param {Boolean} immediately * @param {Boolean} [immediately=false]
* @return {OpenSeadragon.Viewport} Chainable.
* @fires OpenSeadragon.Viewer.event:constrain
*/ */
ensureVisible: function( immediately ) { ensureVisible: function(immediately) {
return this.applyConstraints( immediately ); return this.applyConstraints(immediately);
}, },
/** /**
@ -562,41 +609,37 @@ $.Viewport.prototype = {
* @param {Object} options (immediately=false, constraints=false) * @param {Object} options (immediately=false, constraints=false)
* @return {OpenSeadragon.Viewport} Chainable. * @return {OpenSeadragon.Viewport} Chainable.
*/ */
_fitBounds: function( bounds, options ) { _fitBounds: function(bounds, options) {
options = options || {}; options = options || {};
var immediately = options.immediately || false; var immediately = options.immediately || false;
var constraints = options.constraints || false; var constraints = options.constraints || false;
var aspect = this.getAspectRatio(); var aspect = this.getAspectRatio();
var center = bounds.getCenter(); var center = bounds.getCenter();
// Compute width and height of bounding box.
var newBounds = new $.Rect( var newBounds = new $.Rect(
bounds.x, bounds.x,
bounds.y, bounds.y,
bounds.width, bounds.width,
bounds.height bounds.height,
); bounds.degrees + this.getRotation())
.getBoundingBox();
if ( newBounds.getAspectRatio() >= aspect ) { if (newBounds.getAspectRatio() >= aspect) {
newBounds.height = bounds.width / aspect; newBounds.height = newBounds.width / aspect;
newBounds.y = center.y - newBounds.height / 2;
} else { } else {
newBounds.width = bounds.height * aspect; newBounds.width = newBounds.height * aspect;
newBounds.x = center.x - newBounds.width / 2;
} }
this.panTo( this.getCenter( true ), true ); // Compute x and y from width, height and center position
this.zoomTo( this.getZoom( true ), null, true ); newBounds.x = center.x - newBounds.width / 2;
newBounds.y = center.y - newBounds.height / 2;
var oldBounds = this.getBounds(); var newZoom = 1.0 / newBounds.width;
var oldZoom = this.getZoom();
var newZoom = 1.0 / newBounds.width;
if (constraints) { if (constraints) {
var newBoundsAspectRatio = newBounds.getAspectRatio(); var newBoundsAspectRatio = newBounds.getAspectRatio();
var newConstrainedZoom = Math.max( var newConstrainedZoom = this._applyZoomConstraints(newZoom);
Math.min(newZoom, this.getMaxZoom() ),
this.getMinZoom()
);
if (newZoom !== newConstrainedZoom) { if (newZoom !== newConstrainedZoom) {
newZoom = newConstrainedZoom; newZoom = newConstrainedZoom;
@ -606,58 +649,70 @@ $.Viewport.prototype = {
newBounds.y = center.y - newBounds.height / 2; newBounds.y = center.y - newBounds.height / 2;
} }
newBounds = this._applyBoundaryConstraints( newBounds, immediately ); newBounds = this._applyBoundaryConstraints(newBounds, immediately);
center = newBounds.getCenter(); center = newBounds.getCenter();
} }
if (immediately) { if (immediately) {
this.panTo( center, true ); this.panTo(center, true);
return this.zoomTo(newZoom, null, true); return this.zoomTo(newZoom, null, true);
} }
if (Math.abs(newZoom - oldZoom) < 0.00000001 || this.panTo(this.getCenter(true), true);
Math.abs(newBounds.width - oldBounds.width) < 0.00000001) { this.zoomTo(this.getZoom(true), null, true);
return this.panTo( center, immediately );
var oldBounds = this.getBounds();
var oldZoom = this.getZoom();
if (oldZoom === 0 || Math.abs(newZoom / oldZoom - 1) < 0.00000001) {
this.zoomTo(newZoom, true);
return this.panTo(center, immediately);
} }
var referencePoint = oldBounds.getTopLeft().times( newBounds = newBounds.rotate(-this.getRotation());
this._containerInnerSize.x / oldBounds.width var referencePoint = newBounds.getTopLeft().times(newZoom)
).minus( .minus(oldBounds.getTopLeft().times(oldZoom))
newBounds.getTopLeft().times( .divide(newZoom - oldZoom);
this._containerInnerSize.x / newBounds.width
)
).divide(
this._containerInnerSize.x / oldBounds.width -
this._containerInnerSize.x / newBounds.width
);
return this.zoomTo( newZoom, referencePoint, immediately ); return this.zoomTo(newZoom, referencePoint, immediately);
}, },
/** /**
* Makes the viewport zoom and pan so that the specified bounds take
* as much space as possible in the viewport.
* Note: this method ignores the constraints (minZoom, maxZoom and
* visibilityRatio).
* Use {@link OpenSeadragon.Viewport#fitBoundsWithConstraints} to enforce
* them.
* @function * @function
* @param {OpenSeadragon.Rect} bounds * @param {OpenSeadragon.Rect} bounds
* @param {Boolean} immediately * @param {Boolean} [immediately=false]
* @return {OpenSeadragon.Viewport} Chainable. * @return {OpenSeadragon.Viewport} Chainable.
*/ */
fitBounds: function( bounds, immediately ) { fitBounds: function(bounds, immediately) {
return this._fitBounds( bounds, { return this._fitBounds(bounds, {
immediately: immediately, immediately: immediately,
constraints: false constraints: false
} ); });
}, },
/** /**
* Makes the viewport zoom and pan so that the specified bounds take
* as much space as possible in the viewport while enforcing the constraints
* (minZoom, maxZoom and visibilityRatio).
* Note: because this method enforces the constraints, part of the
* provided bounds may end up outside of the viewport.
* Use {@link OpenSeadragon.Viewport#fitBounds} to ignore them.
* @function * @function
* @param {OpenSeadragon.Rect} bounds * @param {OpenSeadragon.Rect} bounds
* @param {Boolean} immediately * @param {Boolean} [immediately=false]
* @return {OpenSeadragon.Viewport} Chainable. * @return {OpenSeadragon.Viewport} Chainable.
*/ */
fitBoundsWithConstraints: function( bounds, immediately ) { fitBoundsWithConstraints: function(bounds, immediately) {
return this._fitBounds( bounds, { return this._fitBounds(bounds, {
immediately: immediately, immediately: immediately,
constraints: true constraints: true
} ); });
}, },
/** /**
@ -752,7 +807,12 @@ $.Viewport.prototype = {
}, },
/** /**
* Zooms to the specified zoom level
* @function * @function
* @param {Number} zoom The zoom level to zoom to.
* @param {OpenSeadragon.Point} [refPoint] The point which will stay at
* the same screen location. Defaults to the viewport center.
* @param {Boolean} [immediately=false]
* @return {OpenSeadragon.Viewport} Chainable. * @return {OpenSeadragon.Viewport} Chainable.
* @fires OpenSeadragon.Viewer.event:zoom * @fires OpenSeadragon.Viewer.event:zoom
*/ */
@ -842,7 +902,7 @@ $.Viewport.prototype = {
* @fires OpenSeadragon.Viewer.event:resize * @fires OpenSeadragon.Viewer.event:resize
*/ */
resize: function( newContainerSize, maintain ) { resize: function( newContainerSize, maintain ) {
var oldBounds = this.getBounds(), var oldBounds = this.getBoundsNoRotate(),
newBounds = oldBounds, newBounds = oldBounds,
widthDeltaFactor; widthDeltaFactor;
@ -888,37 +948,37 @@ $.Viewport.prototype = {
}, },
/** /**
* Update the zoom and center (X and Y) springs.
* @function * @function
* @returns {Boolean} True if any change has been made, false otherwise.
*/ */
update: function() { update: function() {
var oldZoomPixel,
newZoomPixel,
deltaZoomPixels,
deltaZoomPoints;
if (this.zoomPoint) { if (this.zoomPoint) {
oldZoomPixel = this.pixelFromPoint( this.zoomPoint, true ); var oldZoomPixel = this.pixelFromPoint(this.zoomPoint, true);
} this.zoomSpring.update();
var newZoomPixel = this.pixelFromPoint(this.zoomPoint, true);
this.zoomSpring.update(); var deltaZoomPixels = newZoomPixel.minus(oldZoomPixel);
var deltaZoomPoints = this.deltaPointsFromPixels(
deltaZoomPixels, true);
if (this.zoomPoint && this.zoomSpring.current.value != this._oldZoom) { this.centerSpringX.shiftBy(deltaZoomPoints.x);
newZoomPixel = this.pixelFromPoint( this.zoomPoint, true ); this.centerSpringY.shiftBy(deltaZoomPoints.y);
deltaZoomPixels = newZoomPixel.minus( oldZoomPixel );
deltaZoomPoints = this.deltaPointsFromPixels( deltaZoomPixels, true );
this.centerSpringX.shiftBy( deltaZoomPoints.x ); if (this.zoomSpring.isAtTargetValue()) {
this.centerSpringY.shiftBy( deltaZoomPoints.y ); this.zoomPoint = null;
}
} else { } else {
this.zoomPoint = null; this.zoomSpring.update();
} }
this.centerSpringX.update(); this.centerSpringX.update();
this.centerSpringY.update(); this.centerSpringY.update();
var changed = this.centerSpringX.current.value != this._oldCenterX || var changed = this.centerSpringX.current.value !== this._oldCenterX ||
this.centerSpringY.current.value != this._oldCenterY || this.centerSpringY.current.value !== this._oldCenterY ||
this.zoomSpring.current.value != this._oldZoom; this.zoomSpring.current.value !== this._oldZoom;
this._oldCenterX = this.centerSpringX.current.value; this._oldCenterX = this.centerSpringX.current.value;
this._oldCenterY = this.centerSpringY.current.value; this._oldCenterY = this.centerSpringY.current.value;
@ -994,7 +1054,8 @@ $.Viewport.prototype = {
* @returns {OpenSeadragon.Point} * @returns {OpenSeadragon.Point}
*/ */
pixelFromPointNoRotate: function(point, current) { pixelFromPointNoRotate: function(point, current) {
return this._pixelFromPointNoRotate(point, this.getBounds(current)); return this._pixelFromPointNoRotate(
point, this.getBoundsNoRotate(current));
}, },
/** /**
@ -1005,7 +1066,7 @@ $.Viewport.prototype = {
* @returns {OpenSeadragon.Point} * @returns {OpenSeadragon.Point}
*/ */
pixelFromPoint: function(point, current) { pixelFromPoint: function(point, current) {
return this._pixelFromPoint(point, this.getBounds(current)); return this._pixelFromPoint(point, this.getBoundsNoRotate(current));
}, },
// private // private
@ -1036,7 +1097,7 @@ $.Viewport.prototype = {
* @returns {OpenSeadragon.Point} * @returns {OpenSeadragon.Point}
*/ */
pointFromPixelNoRotate: function(pixel, current) { pointFromPixelNoRotate: function(pixel, current) {
var bounds = this.getBounds( current ); var bounds = this.getBoundsNoRotate(current);
return pixel.minus( return pixel.minus(
new $.Point(this._margins.left, this._margins.top) new $.Point(this._margins.left, this._margins.top)
).divide( ).divide(
@ -1085,8 +1146,18 @@ $.Viewport.prototype = {
return this.viewportToImageCoordinates(viewerX.x, viewerX.y); return this.viewportToImageCoordinates(viewerX.x, viewerX.y);
} }
if (this.viewer && this.viewer.world.getItemCount() > 1) { if (this.viewer) {
$.console.error('[Viewport.viewportToImageCoordinates] is not accurate with multi-image; use TiledImage.viewportToImageCoordinates instead.'); var count = this.viewer.world.getItemCount();
if (count > 1) {
$.console.error('[Viewport.viewportToImageCoordinates] is not accurate ' +
'with multi-image; use TiledImage.viewportToImageCoordinates instead.');
} else if (count === 1) {
// It is better to use TiledImage.viewportToImageCoordinates
// because this._contentBoundsNoRotate can not be relied on
// with clipping.
var item = this.viewer.world.getItemAt(0);
return item.viewportToImageCoordinates(viewerX, viewerY, true);
}
} }
return this._viewportToImageDelta( return this._viewportToImageDelta(
@ -1119,8 +1190,18 @@ $.Viewport.prototype = {
return this.imageToViewportCoordinates(imageX.x, imageX.y); return this.imageToViewportCoordinates(imageX.x, imageX.y);
} }
if (this.viewer && this.viewer.world.getItemCount() > 1) { if (this.viewer) {
$.console.error('[Viewport.imageToViewportCoordinates] is not accurate with multi-image; use TiledImage.imageToViewportCoordinates instead.'); var count = this.viewer.world.getItemCount();
if (count > 1) {
$.console.error('[Viewport.imageToViewportCoordinates] is not accurate ' +
'with multi-image; use TiledImage.imageToViewportCoordinates instead.');
} else if (count === 1) {
// It is better to use TiledImage.viewportToImageCoordinates
// because this._contentBoundsNoRotate can not be relied on
// with clipping.
var item = this.viewer.world.getItemAt(0);
return item.imageToViewportCoordinates(imageX, imageY, true);
}
} }
var point = this._imageToViewportDelta(imageX, imageY); var point = this._imageToViewportDelta(imageX, imageY);
@ -1150,6 +1231,21 @@ $.Viewport.prototype = {
rect = new $.Rect(imageX, imageY, pixelWidth, pixelHeight); rect = new $.Rect(imageX, imageY, pixelWidth, pixelHeight);
} }
if (this.viewer) {
var count = this.viewer.world.getItemCount();
if (count > 1) {
$.console.error('[Viewport.imageToViewportRectangle] is not accurate ' +
'with multi-image; use TiledImage.imageToViewportRectangle instead.');
} else if (count === 1) {
// It is better to use TiledImage.imageToViewportRectangle
// because this._contentBoundsNoRotate can not be relied on
// with clipping.
var item = this.viewer.world.getItemAt(0);
return item.imageToViewportRectangle(
imageX, imageY, pixelWidth, pixelHeight, true);
}
}
var coordA = this.imageToViewportCoordinates(rect.x, rect.y); var coordA = this.imageToViewportCoordinates(rect.x, rect.y);
var coordB = this._imageToViewportDelta(rect.width, rect.height); var coordB = this._imageToViewportDelta(rect.width, rect.height);
return new $.Rect( return new $.Rect(
@ -1183,6 +1279,21 @@ $.Viewport.prototype = {
rect = new $.Rect(viewerX, viewerY, pointWidth, pointHeight); rect = new $.Rect(viewerX, viewerY, pointWidth, pointHeight);
} }
if (this.viewer) {
var count = this.viewer.world.getItemCount();
if (count > 1) {
$.console.error('[Viewport.viewportToImageRectangle] is not accurate ' +
'with multi-image; use TiledImage.viewportToImageRectangle instead.');
} else if (count === 1) {
// It is better to use TiledImage.viewportToImageCoordinates
// because this._contentBoundsNoRotate can not be relied on
// with clipping.
var item = this.viewer.world.getItemAt(0);
return item.viewportToImageRectangle(
viewerX, viewerY, pointWidth, pointHeight, true);
}
}
var coordA = this.viewportToImageCoordinates(rect.x, rect.y); var coordA = this.viewportToImageCoordinates(rect.x, rect.y);
var coordB = this._viewportToImageDelta(rect.width, rect.height); var coordB = this._viewportToImageDelta(rect.width, rect.height);
return new $.Rect( return new $.Rect(
@ -1224,10 +1335,12 @@ $.Viewport.prototype = {
* @param {OpenSeadragon.Point} pixel * @param {OpenSeadragon.Point} pixel
* @returns {OpenSeadragon.Point} * @returns {OpenSeadragon.Point}
*/ */
windowToImageCoordinates: function( pixel ) { windowToImageCoordinates: function(pixel) {
$.console.assert(this.viewer,
"[Viewport.windowToImageCoordinates] the viewport must have a viewer.");
var viewerCoordinates = pixel.minus( var viewerCoordinates = pixel.minus(
OpenSeadragon.getElementPosition( this.viewer.element )); $.getElementPosition(this.viewer.element));
return this.viewerElementToImageCoordinates( viewerCoordinates ); return this.viewerElementToImageCoordinates(viewerCoordinates);
}, },
/** /**
@ -1236,10 +1349,12 @@ $.Viewport.prototype = {
* @param {OpenSeadragon.Point} pixel * @param {OpenSeadragon.Point} pixel
* @returns {OpenSeadragon.Point} * @returns {OpenSeadragon.Point}
*/ */
imageToWindowCoordinates: function( pixel ) { imageToWindowCoordinates: function(pixel) {
var viewerCoordinates = this.imageToViewerElementCoordinates( pixel ); $.console.assert(this.viewer,
"[Viewport.imageToWindowCoordinates] the viewport must have a viewer.");
var viewerCoordinates = this.imageToViewerElementCoordinates(pixel);
return viewerCoordinates.plus( return viewerCoordinates.plus(
OpenSeadragon.getElementPosition( this.viewer.element )); $.getElementPosition(this.viewer.element));
}, },
/** /**
@ -1262,15 +1377,45 @@ $.Viewport.prototype = {
return this.pixelFromPoint( point, true ); return this.pixelFromPoint( point, true );
}, },
/**
* Convert a rectangle in pixel coordinates relative to the viewer element
* to viewport coordinates.
* @param {OpenSeadragon.Rect} rectangle the rectangle to convert
* @returns {OpenSeadragon.Rect} the converted rectangle
*/
viewerElementToViewportRectangle: function(rectangle) {
return $.Rect.fromSummits(
this.pointFromPixel(rectangle.getTopLeft(), true),
this.pointFromPixel(rectangle.getTopRight(), true),
this.pointFromPixel(rectangle.getBottomLeft(), true)
);
},
/**
* Convert a rectangle in viewport coordinates to pixel coordinates relative
* to the viewer element.
* @param {OpenSeadragon.Rect} rectangle the rectangle to convert
* @returns {OpenSeadragon.Rect} the converted rectangle
*/
viewportToViewerElementRectangle: function(rectangle) {
return $.Rect.fromSummits(
this.pixelFromPoint(rectangle.getTopLeft(), true),
this.pixelFromPoint(rectangle.getTopRight(), true),
this.pixelFromPoint(rectangle.getBottomLeft(), true)
);
},
/** /**
* Convert pixel coordinates relative to the window to viewport coordinates. * Convert pixel coordinates relative to the window to viewport coordinates.
* @param {OpenSeadragon.Point} pixel * @param {OpenSeadragon.Point} pixel
* @returns {OpenSeadragon.Point} * @returns {OpenSeadragon.Point}
*/ */
windowToViewportCoordinates: function( pixel ) { windowToViewportCoordinates: function(pixel) {
$.console.assert(this.viewer,
"[Viewport.windowToViewportCoordinates] the viewport must have a viewer.");
var viewerCoordinates = pixel.minus( var viewerCoordinates = pixel.minus(
OpenSeadragon.getElementPosition( this.viewer.element )); $.getElementPosition(this.viewer.element));
return this.viewerElementToViewportCoordinates( viewerCoordinates ); return this.viewerElementToViewportCoordinates(viewerCoordinates);
}, },
/** /**
@ -1278,10 +1423,12 @@ $.Viewport.prototype = {
* @param {OpenSeadragon.Point} point * @param {OpenSeadragon.Point} point
* @returns {OpenSeadragon.Point} * @returns {OpenSeadragon.Point}
*/ */
viewportToWindowCoordinates: function( point ) { viewportToWindowCoordinates: function(point) {
var viewerCoordinates = this.viewportToViewerElementCoordinates( point ); $.console.assert(this.viewer,
"[Viewport.viewportToWindowCoordinates] the viewport must have a viewer.");
var viewerCoordinates = this.viewportToViewerElementCoordinates(point);
return viewerCoordinates.plus( return viewerCoordinates.plus(
OpenSeadragon.getElementPosition( this.viewer.element )); $.getElementPosition(this.viewer.element));
}, },
/** /**
@ -1296,9 +1443,19 @@ $.Viewport.prototype = {
* target zoom. * target zoom.
* @returns {Number} imageZoom The image zoom * @returns {Number} imageZoom The image zoom
*/ */
viewportToImageZoom: function( viewportZoom ) { viewportToImageZoom: function(viewportZoom) {
if (this.viewer && this.viewer.world.getItemCount() > 1) { if (this.viewer) {
$.console.error('[Viewport.viewportToImageZoom] is not accurate with multi-image.'); var count = this.viewer.world.getItemCount();
if (count > 1) {
$.console.error('[Viewport.viewportToImageZoom] is not ' +
'accurate with multi-image.');
} else if (count === 1) {
// It is better to use TiledImage.viewportToImageZoom
// because this._contentBoundsNoRotate can not be relied on
// with clipping.
var item = this.viewer.world.getItemAt(0);
return item.viewportToImageZoom(viewportZoom);
}
} }
var imageWidth = this._contentSizeNoRotate.x; var imageWidth = this._contentSizeNoRotate.x;
@ -1320,9 +1477,19 @@ $.Viewport.prototype = {
* target zoom. * target zoom.
* @returns {Number} viewportZoom The viewport zoom * @returns {Number} viewportZoom The viewport zoom
*/ */
imageToViewportZoom: function( imageZoom ) { imageToViewportZoom: function(imageZoom) {
if (this.viewer && this.viewer.world.getItemCount() > 1) { if (this.viewer) {
$.console.error('[Viewport.imageToViewportZoom] is not accurate with multi-image.'); var count = this.viewer.world.getItemCount();
if (count > 1) {
$.console.error('[Viewport.imageToViewportZoom] is not accurate ' +
'with multi-image.');
} else if (count === 1) {
// It is better to use TiledImage.imageToViewportZoom
// because this._contentBoundsNoRotate can not be relied on
// with clipping.
var item = this.viewer.world.getItemAt(0);
return item.imageToViewportZoom(imageZoom);
}
} }
var imageWidth = this._contentSizeNoRotate.x; var imageWidth = this._contentSizeNoRotate.x;

View File

@ -375,34 +375,40 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
var oldContentSize = this._contentSize ? this._contentSize.clone() : null; var oldContentSize = this._contentSize ? this._contentSize.clone() : null;
var oldContentFactor = this._contentFactor || 0; var oldContentFactor = this._contentFactor || 0;
if ( !this._items.length ) { if (!this._items.length) {
this._homeBounds = new $.Rect(0, 0, 1, 1); this._homeBounds = new $.Rect(0, 0, 1, 1);
this._contentSize = new $.Point(1, 1); this._contentSize = new $.Point(1, 1);
this._contentFactor = 1; this._contentFactor = 1;
} else { } else {
var bounds = this._items[0].getBounds(); var item = this._items[0];
this._contentFactor = this._items[0].getContentSize().x / bounds.width; var bounds = item.getBounds();
var left = bounds.x; this._contentFactor = item.getContentSize().x / bounds.width;
var top = bounds.y; var clippedBounds = item.getClippedBounds();
var right = bounds.x + bounds.width; var left = clippedBounds.x;
var bottom = bounds.y + bounds.height; var top = clippedBounds.y;
var box; var right = clippedBounds.x + clippedBounds.width;
for ( var i = 1; i < this._items.length; i++ ) { var bottom = clippedBounds.y + clippedBounds.height;
box = this._items[i].getBounds(); for (var i = 1; i < this._items.length; i++) {
this._contentFactor = Math.max(this._contentFactor, this._items[i].getContentSize().x / box.width); item = this._items[i];
left = Math.min( left, box.x ); bounds = item.getBounds();
top = Math.min( top, box.y ); this._contentFactor = Math.max(this._contentFactor,
right = Math.max( right, box.x + box.width ); item.getContentSize().x / bounds.width);
bottom = Math.max( bottom, box.y + box.height ); clippedBounds = item.getClippedBounds();
left = Math.min(left, clippedBounds.x);
top = Math.min(top, clippedBounds.y);
right = Math.max(right, clippedBounds.x + clippedBounds.width);
bottom = Math.max(bottom, clippedBounds.y + clippedBounds.height);
} }
this._homeBounds = new $.Rect( left, top, right - left, bottom - top ); this._homeBounds = new $.Rect(left, top, right - left, bottom - top);
this._contentSize = new $.Point(this._homeBounds.width * this._contentFactor, this._contentSize = new $.Point(
this._homeBounds.width * this._contentFactor,
this._homeBounds.height * this._contentFactor); this._homeBounds.height * this._contentFactor);
} }
if (this._contentFactor !== oldContentFactor || !this._homeBounds.equals(oldHomeBounds) || if (this._contentFactor !== oldContentFactor ||
!this._contentSize.equals(oldContentSize)) { !this._homeBounds.equals(oldHomeBounds) ||
!this._contentSize.equals(oldContentSize)) {
/** /**
* Raised when the home bounds or content factor change. * Raised when the home bounds or content factor change.
* @event metrics-change * @event metrics-change

View File

@ -22,6 +22,7 @@
<script src="/src/mousetracker.js"></script> <script src="/src/mousetracker.js"></script>
<script src="/src/control.js"></script> <script src="/src/control.js"></script>
<script src="/src/controldock.js"></script> <script src="/src/controldock.js"></script>
<script src="/src/placement.js"></script>
<script src="/src/viewer.js"></script> <script src="/src/viewer.js"></script>
<script src="/src/navigator.js"></script> <script src="/src/navigator.js"></script>
<script src="/src/strings.js"></script> <script src="/src/strings.js"></script>
@ -52,6 +53,7 @@
<!-- Helpers --> <!-- Helpers -->
<script src="/test/helpers/legacy.mouse.shim.js"></script> <script src="/test/helpers/legacy.mouse.shim.js"></script>
<script src="/test/helpers/test.js"></script> <script src="/test/helpers/test.js"></script>
<script src="/test/helpers/touch.js"></script>
<!-- Modules --> <!-- Modules -->
<!-- Polyfill must be inserted first because it is testing functions <!-- Polyfill must be inserted first because it is testing functions

View File

@ -14,7 +14,9 @@
<body> <body>
<div id="contentDiv" class="openseadragon1"></div> <div id="contentDiv" class="openseadragon1"></div>
<div id="annotation-div"> <div id="annotation-div">
<input type="button" value="Hide Overlay" id="hideOverlay"> <input type="button" value="Hide Overlays" id="hideOverlays">
<input type="button" value="Rotate" id="rotate">
<span id="degrees">0deg</span>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
@ -22,19 +24,74 @@
var viewer = OpenSeadragon({ var viewer = OpenSeadragon({
id: "contentDiv", id: "contentDiv",
prefixUrl: "../../build/openseadragon/images/", prefixUrl: "../../build/openseadragon/images/",
tileSources: "../data/testpattern.dzi" tileSources: "../data/testpattern.dzi",
minZoomImageRatio: 0,
maxZoomPixelRatio: 10
}); });
viewer.addHandler("open", function(event) { viewer.addHandler("open", function(event) {
var elt = document.createElement("div"); var elt = document.createElement("div");
elt.className = "runtime-overlay";
elt.style.background = "green"; elt.style.background = "green";
elt.id = "runtime-overlay"; elt.style.outline = "3px solid red";
elt.style.border = "1px solid red"; elt.style.opacity = "0.7";
viewer.addOverlay( elt, new OpenSeadragon.Rect(0.2, 0.2, 0.75, 0.75) ); elt.textContent = "Scaled overlay";
}); viewer.addOverlay({
element: elt,
location: new OpenSeadragon.Rect(0.21, 0.21, 0.099, 0.099),
rotationMode: OpenSeadragon.OverlayRotationMode.BOUNDING_BOX
});
$("#hideOverlay").click(function(){ elt = document.createElement("div");
$("#runtime-overlay").toggle(); elt.className = "runtime-overlay";
elt.style.background = "white";
elt.style.outline = "3px solid red";
elt.style.width = "100px";
elt.textContent = "Scaled vertically";
viewer.addOverlay({
element: elt,
location: new OpenSeadragon.Point(0.6, 0.6),
height: 0.1,
placement: OpenSeadragon.Placement.TOP_LEFT,
rotationMode: OpenSeadragon.OverlayRotationMode.NO_ROTATION
});
elt = document.createElement("div");
elt.className = "runtime-overlay";
elt.style.background = "white";
elt.style.opacity = "0.5";
elt.style.outline = "1px solid blue";
elt.style.height = "100px";
elt.textContent = "Scaled horizontally";
viewer.addOverlay({
element: elt,
location: new OpenSeadragon.Point(0.1, 0.5),
width: 0.1
});
elt = document.createElement("div");
elt.className = "runtime-overlay";
elt.style.background = "white";
elt.style.opacity = "0.5";
elt.style.outline = "5px solid pink";
elt.style.width = "100px";
elt.style.height = "100px";
elt.textContent = "Not scaled, centered in the middle";
viewer.addOverlay({
element: elt,
location: new OpenSeadragon.Point(0.5, 0.5),
placement: OpenSeadragon.Placement.CENTER,
checkResize: false,
rotationMode: OpenSeadragon.OverlayRotationMode.EXACT
});
});
$("#hideOverlays").click(function(){
$(".runtime-overlay").toggle();
});
$("#rotate").click(function() {
viewer.viewport.setRotation(viewer.viewport.getRotation() + 22.5);
$("#degrees").text(viewer.viewport.getRotation() + "deg");
}); });
</script> </script>

134
test/helpers/touch.js Normal file
View File

@ -0,0 +1,134 @@
/* global TouchUtil, $ */
(function () {
var touches,
identifier,
target;
// ----------
window.TouchUtil = {
reset: function () {
touches = [];
identifier = 0;
},
initTracker: function ( tracker ) {
// for testing in other touch-enabled browsers
if ( !('ontouchstart' in window) ) {
tracker.setTracking( false );
OpenSeadragon.MouseTracker.subscribeEvents.push( 'touchstart', 'touchend' );
tracker.setTracking( true );
}
target = tracker.element;
},
resetTracker: function ( tracker ) {
// for testing in other touch-enabled browsers
if ( !('ontouchstart' in window) ) {
tracker.setTracking( false );
['touchstart', 'touchend'].forEach(function ( type ) {
var index = OpenSeadragon.MouseTracker.subscribeEvents.indexOf( type );
if ( index > -1 ) {
OpenSeadragon.MouseTracker.subscribeEvents.splice( index, 1 );
}
});
tracker.setTracking( true );
}
target = null;
},
start: function () {
var touch,
event,
newTouches = [];
for ( var i = 0; i < arguments.length; i++ ) {
touch = createTouch(
target.offsetLeft + arguments[ i ][ 0 ],
target.offsetTop + arguments[ i ][ 1 ]
);
touches.push( touch );
newTouches.push( touch );
}
event = createTouchEvent( 'touchstart', newTouches );
target.dispatchEvent( event );
return newTouches.length === 1 ? newTouches[ 0 ] : newTouches;
},
end: function ( changedTouches ) {
if ( !$.isArray( changedTouches ) ) {
changedTouches = [ changedTouches ];
}
var event;
touches = touches.filter(function ( touch ) {
return changedTouches.indexOf( touch ) === -1;
});
event = createTouchEvent( 'touchend', changedTouches );
target.dispatchEvent( event );
}
};
// ----------
function createTouch( x, y ) {
try {
// new spec
return new Touch({
identifier: identifier++,
target: target,
pageX: target.offsetLeft + x,
pageY: target.offsetTop + y
} );
} catch (e) {
// legacy
return document.createTouch( window, target, identifier++, x, y, x, y );
}
}
function createTouchList( touches ) {
// legacy
return document.createTouchList.apply( document, touches );
}
function createTouchEvent( type, changedTouches ) {
try {
// new spec
return new TouchEvent( type, {
view: window,
bubbles: true,
cancelable: true,
touches: touches,
targetTouches: touches,
changedTouches: changedTouches
} );
} catch (e) {
// legacy
var touchEvent = document.createEvent( 'TouchEvent' );
var touch1 = changedTouches[ 0 ];
touchEvent.initTouchEvent(
createTouchList( touches ), // touches
createTouchList( touches ), // targetTouches
createTouchList( changedTouches ), // changedTouches
type, // type
window, // view
touch1.screenX, // screenX
touch1.screenY, // screenY
touch1.clientX, // clientX
touch1.clientY, // clientY
false, // ctrlKey
false, // altKey
false, // shiftKey
false // metaKey
);
return touchEvent;
}
}
})();

View File

@ -1,4 +1,4 @@
/* global module, asyncTest, $, ok, equal, notEqual, start, test, Util, testLog */ /* global module, asyncTest, $, ok, equal, notEqual, start, test, TouchUtil, Util, testLog */
(function () { (function () {
var viewer; var viewer;
@ -678,45 +678,96 @@
} ); } );
// ---------- // ----------
asyncTest( 'Viewer: preventDefaultAction', function () { if ('TouchEvent' in window) {
var $canvas = $( viewer.element ).find( '.openseadragon-canvas' ).not( '.navigator .openseadragon-canvas' ), asyncTest( 'MouseTracker: touch events', function () {
tracker = viewer.innerTracker, var $canvas = $( viewer.element ).find( '.openseadragon-canvas' ).not( '.navigator .openseadragon-canvas' ),
origClickHandler, tracker = viewer.innerTracker,
origDragHandler, touches;
dragCount = 10,
originalZoom = 0,
originalBounds = null;
var onOpen = function ( event ) { var reset = function () {
viewer.removeHandler( 'open', onOpen ); touches = [];
TouchUtil.reset();
// Hook viewer events to set preventDefaultAction
origClickHandler = tracker.clickHandler;
tracker.clickHandler = function ( event ) {
event.preventDefaultAction = true;
return origClickHandler( event );
};
origDragHandler = tracker.dragHandler;
tracker.dragHandler = function ( event ) {
event.preventDefaultAction = true;
return origDragHandler( event );
}; };
originalZoom = viewer.viewport.getZoom(); var assessTouchExpectations = function ( expected ) {
originalBounds = viewer.viewport.getBounds(); var pointersList = tracker.getActivePointersListByType( 'touch' );
if ('captureCount' in expected) {
var event = { equal( pointersList.captureCount, expected.captureCount, expected.description + 'Pointer capture count matches expected (' + expected.captureCount + ')' );
clientX:1, }
clientY:1 if ('contacts' in expected) {
equal( pointersList.contacts, expected.contacts, expected.description + 'Pointer contact count matches expected (' + expected.contacts + ')' );
}
if ('trackedPointers' in expected) {
equal( pointersList.getLength(), expected.trackedPointers, expected.description + 'Tracked pointer count matches expected (' + expected.trackedPointers + ')' );
}
}; };
var onOpen = function ( event ) {
viewer.removeHandler( 'open', onOpen );
TouchUtil.initTracker( tracker );
// start-end-end (multi-touch start event)
reset();
touches = TouchUtil.start( [0,0], [20,20] );
assessTouchExpectations({
description: 'start-end-end (multi-touch start event) [capture]: ',
captureCount: 2,
contacts: 2,
trackedPointers: 2
});
TouchUtil.end( touches[1] );
TouchUtil.end( touches[0] );
assessTouchExpectations({
description: 'start-end-end (multi-touch start event) [release]: ',
captureCount: 0,
contacts: 0,
trackedPointers: 0
});
// start-start-end (multi-touch end event)
reset();
touches.push( TouchUtil.start([0, 0]) );
touches.push( TouchUtil.start([20, 20]) );
assessTouchExpectations({
description: 'start-start-end (multi-touch end event) [capture]: ',
captureCount: 2,
contacts: 2,
trackedPointers: 2
});
TouchUtil.end( touches );
assessTouchExpectations({
description: 'start-start-end (multi-touch end event) [release]: ',
captureCount: 0,
contacts: 0,
trackedPointers: 0
});
TouchUtil.resetTracker( tracker );
viewer.close();
start();
};
viewer.addHandler( 'open', onOpen );
viewer.open( '/test/data/testpattern.dzi' );
} );
}
// ----------
asyncTest('Viewer: preventDefaultAction', function() {
var $canvas = $(viewer.element).find('.openseadragon-canvas')
.not('.navigator .openseadragon-canvas');
var tracker = viewer.innerTracker;
var epsilon = 0.0000001;
function simulateClickAndDrag() {
$canvas.simulate( 'focus', event ); $canvas.simulate( 'focus', event );
// Drag to pan // Drag to pan
Util.simulateViewerClickWithDrag( { Util.simulateViewerClickWithDrag( {
viewer: viewer, viewer: viewer,
widthFactor: 0.25, widthFactor: 0.25,
heightFactor: 0.25, heightFactor: 0.25,
dragCount: dragCount, dragCount: 10,
dragDx: 1, dragDx: 1,
dragDy: 1 dragDy: 1
} ); } );
@ -730,20 +781,57 @@
dragDy: 0 dragDy: 0
} ); } );
$canvas.simulate( 'blur', event ); $canvas.simulate( 'blur', event );
}
var zoom = viewer.viewport.getZoom(), var onOpen = function() {
bounds = viewer.viewport.getBounds(); viewer.removeHandler('open', onOpen);
equal( zoom, originalZoom, "Zoom prevented" ); // Hook viewer events to set preventDefaultAction
ok( bounds.x == originalBounds.x && bounds.y == originalBounds.y, 'Pan prevented' ); var origClickHandler = tracker.clickHandler;
tracker.clickHandler = function(event) {
event.preventDefaultAction = true;
return origClickHandler(event);
};
var origDragHandler = tracker.dragHandler;
tracker.dragHandler = function(event) {
event.preventDefaultAction = true;
return origDragHandler(event);
};
var originalZoom = viewer.viewport.getZoom();
var originalBounds = viewer.viewport.getBounds();
simulateClickAndDrag();
var zoom = viewer.viewport.getZoom();
var bounds = viewer.viewport.getBounds();
Util.assessNumericValue(zoom, originalZoom, epsilon,
"Zoom should be prevented");
Util.assertRectangleEquals(bounds, originalBounds, epsilon,
'Pan should be prevented');
tracker.clickHandler = origClickHandler;
tracker.dragHandler = origDragHandler;
simulateClickAndDrag();
var zoom = viewer.viewport.getZoom();
var bounds = viewer.viewport.getBounds();
Util.assessNumericValue(zoom, 0.002, epsilon,
"Zoom should not be prevented");
Util.assertRectangleEquals(
bounds,
new OpenSeadragon.Rect(-250, -0.25, 500, 0.5),
epsilon,
'Pan should not be prevented');
viewer.close(); viewer.close();
start(); start();
}; };
viewer.addHandler( 'open', onOpen ); viewer.addHandler('open', onOpen);
viewer.open( '/test/data/testpattern.dzi' ); viewer.open('/test/data/testpattern.dzi');
} ); });
// ---------- // ----------
asyncTest( 'EventSource/MouseTracker/Viewer: event.originalEvent event.userData canvas-drag canvas-drag-end canvas-release canvas-click', function () { asyncTest( 'EventSource/MouseTracker/Viewer: event.originalEvent event.userData canvas-drag canvas-drag-end canvas-release canvas-click', function () {
@ -949,6 +1037,59 @@
viewer.open( '/test/data/testpattern.dzi' ); viewer.open( '/test/data/testpattern.dzi' );
} ); } );
// ----------
test('EventSource: addOnceHandler', function() {
var eventSource = new OpenSeadragon.EventSource();
var userData = 'data';
var eventData = {
foo: 1
};
var handlerCalledCount = 0;
eventSource.addOnceHandler('test-event', function(event) {
handlerCalledCount++;
strictEqual(event.foo, eventData.foo,
'Event data should be transmitted to the event.');
strictEqual(event.userData, userData,
'User data should be transmitted to the event.');
}, userData);
strictEqual(0, handlerCalledCount,
'Handler should not have been called yet.');
eventSource.raiseEvent('test-event', eventData);
strictEqual(1, handlerCalledCount,
'Handler should have been called once.');
eventSource.raiseEvent('test-event', eventData);
strictEqual(1, handlerCalledCount,
'Handler should still have been called once.');
});
// ----------
test('EventSource: addOnceHandler 2 times', function() {
var eventSource = new OpenSeadragon.EventSource();
var userData = 'data';
var eventData = {
foo: 1
};
var handlerCalledCount = 0;
eventSource.addOnceHandler('test-event', function(event) {
handlerCalledCount++;
strictEqual(event.foo, eventData.foo,
'Event data should be transmitted to the event.');
strictEqual(event.userData, userData,
'User data should be transmitted to the event.');
}, userData, 2);
strictEqual(0, handlerCalledCount,
'Handler should not have been called yet.');
eventSource.raiseEvent('test-event', eventData);
strictEqual(1, handlerCalledCount,
'Handler should have been called once.');
eventSource.raiseEvent('test-event', eventData);
strictEqual(2, handlerCalledCount,
'Handler should have been called twice.');
eventSource.raiseEvent('test-event', eventData);
strictEqual(2, handlerCalledCount,
'Handler should still have been called twice.');
});
// ---------- // ----------
asyncTest( 'Viewer: tile-drawing event', function () { asyncTest( 'Viewer: tile-drawing event', function () {
var tileDrawing = function ( event ) { var tileDrawing = function ( event ) {

View File

@ -5,12 +5,12 @@
module( 'Multi-Image', { module( 'Multi-Image', {
setup: function() { setup: function() {
$( '<div id="itemsexample"></div>' ).appendTo( "#qunit-fixture" ); $( '<div id="example"></div>' ).appendTo( "#qunit-fixture" );
testLog.reset(); testLog.reset();
viewer = OpenSeadragon( { viewer = OpenSeadragon( {
id: 'itemsexample', id: 'example',
prefixUrl: '/build/openseadragon/images/', prefixUrl: '/build/openseadragon/images/',
springStiffness: 100 // Faster animation = faster tests springStiffness: 100 // Faster animation = faster tests
}); });
@ -21,7 +21,7 @@
} }
viewer = null; viewer = null;
$( "#itemsexample" ).remove(); $("#example").remove();
} }
} ); } );
@ -208,4 +208,57 @@
viewer.open('/test/data/testpattern.dzi'); viewer.open('/test/data/testpattern.dzi');
}); });
asyncTest('Transparent image on top of others', function() {
viewer.open('/test/data/testpattern.dzi');
// TODO: replace with fully-loaded event listener when available.
setTimeout(function() {
var imageData = viewer.drawer.context.getImageData(0, 0, 500, 500);
// Pixel 250,250 will be in the hole of the A
var expectedVal = getPixelValue(imageData, 250, 250);
notEqual(expectedVal.r, 0, 'Red channel should not be 0');
notEqual(expectedVal.g, 0, 'Green channel should not be 0');
notEqual(expectedVal.b, 0, 'Blue channel should not be 0');
notEqual(expectedVal.a, 0, 'Alpha channel should not be 0');
viewer.addSimpleImage({
url: '/test/data/A.png'
});
// TODO: replace with fully-loaded event listener when available.
setTimeout(function() {
var imageData = viewer.drawer.context.getImageData(0, 0, 500, 500);
var actualVal = getPixelValue(imageData, 250, 250);
equal(actualVal.r, expectedVal.r,
'Red channel should not change in transparent part of the A');
equal(actualVal.g, expectedVal.g,
'Green channel should not change in transparent part of the A');
equal(actualVal.b, expectedVal.b,
'Blue channel should not change in transparent part of the A');
equal(actualVal.a, expectedVal.a,
'Alpha channel should not change in transparent part of the A');
var onAVal = getPixelValue(imageData, 333, 250);
equal(onAVal.r, 0, 'Red channel should be null on the A');
equal(onAVal.g, 0, 'Green channel should be null on the A');
equal(onAVal.b, 0, 'Blue channel should be null on the A');
equal(onAVal.a, 255, 'Alpha channel should be 255 on the A');
start();
}, 500);
}, 500);
function getPixelValue(imageData, x, y) {
var offset = 4 * (y * imageData.width + x);
return {
r: imageData.data[offset],
g: imageData.data[offset + 1],
b: imageData.data[offset + 2],
a: imageData.data[offset + 3]
};
}
});
})(); })();

File diff suppressed because it is too large Load Diff

View File

@ -161,6 +161,65 @@
"Incorrect union with non horizontal rectangles."); "Incorrect union with non horizontal rectangles.");
}); });
test('intersection', function() {
var rect1 = new OpenSeadragon.Rect(2, 2, 2, 3);
var rect2 = new OpenSeadragon.Rect(0, 1, 1, 1);
var expected = null;
var actual = rect1.intersection(rect2);
equal(expected, actual,
"Rectangle " + rect2 + " should not intersect " + rect1);
actual = rect2.intersection(rect1);
equal(expected, actual,
"Rectangle " + rect1 + " should not intersect " + rect2);
rect1 = new OpenSeadragon.Rect(0, 0, 2, 1);
rect2 = new OpenSeadragon.Rect(1, 0, 2, 2);
expected = new OpenSeadragon.Rect(1, 0, 1, 1);
actual = rect1.intersection(rect2);
Util.assertRectangleEquals(expected, actual, precision,
"Intersection of " + rect2 + " with " + rect1 + " should be " +
expected);
actual = rect2.intersection(rect1);
Util.assertRectangleEquals(expected, actual, precision,
"Intersection of " + rect1 + " with " + rect2 + " should be " +
expected);
rect1 = new OpenSeadragon.Rect(0, 0, 3, 3);
rect2 = new OpenSeadragon.Rect(1, 1, 1, 1);
expected = new OpenSeadragon.Rect(1, 1, 1, 1);
actual = rect1.intersection(rect2);
Util.assertRectangleEquals(expected, actual, precision,
"Intersection of " + rect2 + " with " + rect1 + " should be " +
expected);
actual = rect2.intersection(rect1);
Util.assertRectangleEquals(expected, actual, precision,
"Intersection of " + rect1 + " with " + rect2 + " should be " +
expected);
rect1 = new OpenSeadragon.Rect(2, 2, 2, 3, 45);
rect2 = new OpenSeadragon.Rect(0, 1, 1, 1);
expected = null;
actual = rect1.intersection(rect2);
equal(expected, actual,
"Rectangle " + rect2 + " should not intersect " + rect1);
actual = rect2.intersection(rect1);
equal(expected, actual,
"Rectangle " + rect1 + " should not intersect " + rect2);
rect1 = new OpenSeadragon.Rect(2, 0, 2, 3, 45);
rect2 = new OpenSeadragon.Rect(0, 1, 1, 1);
expected = new OpenSeadragon.Rect(0, 1, 1, 1);
actual = rect1.intersection(rect2);
Util.assertRectangleEquals(expected, actual, precision,
"Intersection of " + rect2 + " with " + rect1 + " should be " +
expected);
actual = rect2.intersection(rect1);
Util.assertRectangleEquals(expected, actual, precision,
"Intersection of " + rect1 + " with " + rect2 + " should be " +
expected);
});
test('rotate', function() { test('rotate', function() {
var rect = new OpenSeadragon.Rect(0, 0, 2, 1); var rect = new OpenSeadragon.Rect(0, 0, 2, 1);
@ -218,4 +277,27 @@
"Bounding box of rect rotated 270deg."); "Bounding box of rect rotated 270deg.");
}); });
test('containsPoint', function() {
var rect = new OpenSeadragon.Rect(0, 0, 1, 1, 45);
ok(rect.containsPoint(new OpenSeadragon.Point(0, 0)),
'Point 0,0 should be inside ' + rect);
ok(rect.containsPoint(rect.getTopRight()),
'Top right vertex should be inside ' + rect);
ok(rect.containsPoint(rect.getBottomRight()),
'Bottom right vertex should be inside ' + rect);
ok(rect.containsPoint(rect.getBottomLeft()),
'Bottom left vertex should be inside ' + rect);
ok(rect.containsPoint(rect.getCenter()),
'Center should be inside ' + rect);
notOk(rect.containsPoint(new OpenSeadragon.Point(1, 0)),
'Point 1,0 should not be inside ' + rect);
ok(rect.containsPoint(new OpenSeadragon.Point(0.5, 0.5)),
'Point 0.5,0.5 should be inside ' + rect);
ok(rect.containsPoint(new OpenSeadragon.Point(0.4, 0.5)),
'Point 0.4,0.5 should be inside ' + rect);
notOk(rect.containsPoint(new OpenSeadragon.Point(0.6, 0.5)),
'Point 0.6,0.5 should not be inside ' + rect);
});
})(); })();

View File

@ -4,18 +4,18 @@
var viewer; var viewer;
module('TiledImage', { module('TiledImage', {
setup: function () { setup: function() {
var example = $('<div id="example"></div>').appendTo("#qunit-fixture"); var example = $('<div id="example"></div>').appendTo("#qunit-fixture");
testLog.reset(); testLog.reset();
viewer = OpenSeadragon({ viewer = OpenSeadragon({
id: 'example', id: 'example',
prefixUrl: '/build/openseadragon/images/', prefixUrl: '/build/openseadragon/images/',
springStiffness: 100 // Faster animation = faster tests springStiffness: 100 // Faster animation = faster tests
}); });
}, },
teardown: function () { teardown: function() {
if (viewer && viewer.close) { if (viewer && viewer.close) {
viewer.close(); viewer.close();
} }
@ -87,7 +87,7 @@
// ---------- // ----------
asyncTest('animation', function() { asyncTest('animation', function() {
viewer.addHandler("open", function () { viewer.addHandler("open", function() {
var image = viewer.world.getItemAt(0); var image = viewer.world.getItemAt(0);
propEqual(image.getBounds(), new OpenSeadragon.Rect(0, 0, 1, 1), 'target bounds on open'); propEqual(image.getBounds(), new OpenSeadragon.Rect(0, 0, 1, 1), 'target bounds on open');
propEqual(image.getBounds(true), new OpenSeadragon.Rect(0, 0, 1, 1), 'current bounds on open'); propEqual(image.getBounds(true), new OpenSeadragon.Rect(0, 0, 1, 1), 'current bounds on open');
@ -206,10 +206,12 @@
propEqual(image.getClip(), clip, 'clip is set correctly'); propEqual(image.getClip(), clip, 'clip is set correctly');
Util.spyOnce(viewer.drawer, 'setClip', function(rect) { Util.spyOnce(viewer.drawer, 'setClip', function(rect) {
ok(true, 'drawer.setClip is called'); var homeBounds = viewer.viewport.getHomeBounds();
var pixelRatio = viewer.viewport.getContainerSize().x / image.getContentSize().x; var canvasClip = viewer.viewport
var canvasClip = clip.times(pixelRatio * OpenSeadragon.pixelDensityRatio); .viewportToViewerElementRectangle(homeBounds);
propEqual(rect, canvasClip, 'clipping to correct rect'); var precision = 0.00000001;
Util.assertRectangleEquals(rect, canvasClip, precision,
'clipping should be ' + canvasClip);
start(); start();
}); });
}); });
@ -220,6 +222,39 @@
}); });
}); });
asyncTest('getClipBounds', function() {
var clip = new OpenSeadragon.Rect(100, 200, 800, 500);
viewer.addHandler('open', function() {
var image = viewer.world.getItemAt(0);
var bounds = image.getClippedBounds();
var expectedBounds = new OpenSeadragon.Rect(1.2, 1.4, 1.6, 1);
propEqual(bounds, expectedBounds,
'getClipBounds should take clipping into account.');
image = viewer.world.getItemAt(1);
bounds = image.getClippedBounds();
expectedBounds = new OpenSeadragon.Rect(1, 2, 2, 2);
propEqual(bounds, expectedBounds,
'getClipBounds should work when no clipping set.');
start();
});
viewer.open([{
tileSource: '/test/data/testpattern.dzi',
clip: clip,
x: 1,
y: 1,
width: 2
}, {
tileSource: '/test/data/testpattern.dzi',
x: 1,
y: 2,
width: 2
}]);
});
// ---------- // ----------
asyncTest('opacity', function() { asyncTest('opacity', function() {
@ -257,4 +292,138 @@
}); });
}); });
asyncTest('fitBounds', function() {
function assertRectEquals(actual, expected, message) {
ok(actual.equals(expected), message + ' should be ' +
expected.toString() + ', found ' + actual.toString());
}
viewer.addHandler('open', function openHandler() {
viewer.removeHandler('open', openHandler);
var squareImage = viewer.world.getItemAt(0);
squareImage.fitBounds(
new OpenSeadragon.Rect(0, 0, 1, 2),
OpenSeadragon.Placement.CENTER,
true);
var actualBounds = squareImage.getBounds(true);
var expectedBounds = new OpenSeadragon.Rect(0, 0.5, 1, 1);
assertRectEquals(actualBounds, expectedBounds, 'Square image bounds');
var tallImage = viewer.world.getItemAt(1);
tallImage.fitBounds(
new OpenSeadragon.Rect(0, 0, 1, 2),
OpenSeadragon.Placement.TOP_LEFT,
true);
actualBounds = tallImage.getBounds(true);
expectedBounds = new OpenSeadragon.Rect(0, 0, 0.5, 2);
assertRectEquals(actualBounds, expectedBounds, 'Tall image bounds');
var wideImage = viewer.world.getItemAt(2);
wideImage.fitBounds(
new OpenSeadragon.Rect(0, 0, 1, 2),
OpenSeadragon.Placement.BOTTOM_RIGHT,
true);
actualBounds = wideImage.getBounds(true);
expectedBounds = new OpenSeadragon.Rect(0, 1.75, 1, 0.25);
assertRectEquals(actualBounds, expectedBounds, 'Wide image bounds');
start();
});
viewer.open([
'/test/data/testpattern.dzi',
'/test/data/tall.dzi',
'/test/data/wide.dzi'
]);
});
asyncTest('fitBounds in constructor', function() {
function assertRectEquals(actual, expected, message) {
ok(actual.equals(expected), message + ' should be ' +
expected.toString() + ', found ' + actual.toString());
}
viewer.addHandler('open', function openHandler() {
viewer.removeHandler('open', openHandler);
var squareImage = viewer.world.getItemAt(0);
var actualBounds = squareImage.getBounds(true);
var expectedBounds = new OpenSeadragon.Rect(0, 0.5, 1, 1);
assertRectEquals(actualBounds, expectedBounds, 'Square image bounds');
var tallImage = viewer.world.getItemAt(1);
actualBounds = tallImage.getBounds(true);
expectedBounds = new OpenSeadragon.Rect(0, 0, 0.5, 2);
assertRectEquals(actualBounds, expectedBounds, 'Tall image bounds');
var wideImage = viewer.world.getItemAt(2);
actualBounds = wideImage.getBounds(true);
expectedBounds = new OpenSeadragon.Rect(0, 1.75, 1, 0.25);
assertRectEquals(actualBounds, expectedBounds, 'Wide image bounds');
start();
});
viewer.open([{
tileSource: '/test/data/testpattern.dzi',
x: 1, // should be ignored
y: 1, // should be ignored
width: 2, // should be ignored
fitBounds: new OpenSeadragon.Rect(0, 0, 1, 2)
// No placement specified, should default to CENTER
}, {
tileSource: '/test/data/tall.dzi',
fitBounds: new OpenSeadragon.Rect(0, 0, 1, 2),
fitBoundsPlacement: OpenSeadragon.Placement.TOP_LEFT
}, {
tileSource: '/test/data/wide.dzi',
fitBounds: new OpenSeadragon.Rect(0, 0, 1, 2),
fitBoundsPlacement: OpenSeadragon.Placement.BOTTOM_RIGHT
}]);
});
asyncTest('fitBounds with clipping', function() {
function assertRectEquals(actual, expected, message) {
ok(actual.equals(expected), message + ' should be ' +
expected.toString() + ', found ' + actual.toString());
}
viewer.addHandler('open', function openHandler() {
viewer.removeHandler('open', openHandler);
var squareImage = viewer.world.getItemAt(0);
var actualBounds = squareImage.getBounds(true);
var expectedBounds = new OpenSeadragon.Rect(-1, -1, 2, 2);
assertRectEquals(actualBounds, expectedBounds, 'Square image bounds');
var tallImage = viewer.world.getItemAt(1);
actualBounds = tallImage.getBounds(true);
expectedBounds = new OpenSeadragon.Rect(1, 1, 2, 8);
assertRectEquals(actualBounds, expectedBounds, 'Tall image bounds');
var wideImage = viewer.world.getItemAt(2);
actualBounds = wideImage.getBounds(true);
expectedBounds = new OpenSeadragon.Rect(1, 1, 16, 4);
assertRectEquals(actualBounds, expectedBounds, 'Wide image bounds');
start();
});
viewer.open([{
tileSource: '/test/data/testpattern.dzi',
clip: new OpenSeadragon.Rect(500, 500, 500, 500),
fitBounds: new OpenSeadragon.Rect(0, 0, 1, 1)
}, {
tileSource: '/test/data/tall.dzi',
clip: new OpenSeadragon.Rect(0, 0, 250, 100),
fitBounds: new OpenSeadragon.Rect(1, 1, 1, 2),
fitBoundsPlacement: OpenSeadragon.Placement.TOP
}, {
tileSource: '/test/data/wide.dzi',
clip: new OpenSeadragon.Rect(0, 0, 100, 250),
fitBounds: new OpenSeadragon.Rect(1, 1, 1, 2),
fitBoundsPlacement: OpenSeadragon.Placement.TOP_LEFT
}]);
});
})(); })();

View File

@ -5,6 +5,7 @@
var VIEWER_ID = "example"; var VIEWER_ID = "example";
var PREFIX_URL = "/build/openseadragon/images/"; var PREFIX_URL = "/build/openseadragon/images/";
var SPRING_STIFFNESS = 100; // Faster animation = faster tests var SPRING_STIFFNESS = 100; // Faster animation = faster tests
var EPSILON = 0.0000000001;
module("viewport", { module("viewport", {
setup: function () { setup: function () {
@ -218,7 +219,27 @@
}); });
}); });
asyncTest('getHomeBoundsWithRotation', function() { asyncTest('getHomeBoundsNoRotate with rotation', function() {
function openHandler() {
viewer.removeHandler('open', openHandler);
var viewport = viewer.viewport;
viewport.setRotation(-675);
Util.assertRectangleEquals(
viewport.getHomeBoundsNoRotate(),
new OpenSeadragon.Rect(
(1 - Math.sqrt(2)) / 2,
(1 - Math.sqrt(2)) / 2,
Math.sqrt(2),
Math.sqrt(2)),
0.00000001,
"Test getHomeBoundsNoRotate with degrees = -675");
start();
}
viewer.addHandler('open', openHandler);
viewer.open(DZI_PATH);
});
asyncTest('getHomeBounds with rotation', function() {
function openHandler() { function openHandler() {
viewer.removeHandler('open', openHandler); viewer.removeHandler('open', openHandler);
var viewport = viewer.viewport; var viewport = viewer.viewport;
@ -226,10 +247,11 @@
Util.assertRectangleEquals( Util.assertRectangleEquals(
viewport.getHomeBounds(), viewport.getHomeBounds(),
new OpenSeadragon.Rect( new OpenSeadragon.Rect(
(1 - Math.sqrt(2)) / 2, 0.5,
(1 - Math.sqrt(2)) / 2, -0.5,
Math.sqrt(2), Math.sqrt(2),
Math.sqrt(2)), Math.sqrt(2),
45),
0.00000001, 0.00000001,
"Test getHomeBounds with degrees = -675"); "Test getHomeBounds with degrees = -675");
start(); start();
@ -238,6 +260,57 @@
viewer.open(DZI_PATH); viewer.open(DZI_PATH);
}); });
asyncTest('getHomeBoundsWithMultiImages', function() {
function openHandler() {
viewer.removeHandler('open', openHandler);
var viewport = viewer.viewport;
Util.assertRectangleEquals(
new OpenSeadragon.Rect(0, 0, 4, 4),
viewport.getHomeBounds(),
0.00000001,
"Test getHomeBoundsWithMultiImages");
start();
}
viewer.addHandler('open', openHandler);
viewer.open([{
tileSource: DZI_PATH,
x: 0,
y: 0,
width: 2
}, {
tileSource: DZI_PATH,
x: 3,
y: 3,
width: 1
}]);
});
asyncTest('getHomeBoundsWithMultiImagesAndClipping', function() {
function openHandler() {
viewer.removeHandler('open', openHandler);
var viewport = viewer.viewport;
Util.assertRectangleEquals(
new OpenSeadragon.Rect(1, 1, 4, 4),
viewport.getHomeBounds(),
0.00000001,
"Test getHomeBoundsWithMultiImagesAndClipping");
start();
}
viewer.addHandler('open', openHandler);
viewer.open([{
tileSource: DZI_PATH,
x: 0,
y: 0,
width: 2,
clip: new OpenSeadragon.Rect(500, 500, 500, 500)
}, {
tileSource: DZI_PATH,
x: 4,
y: 4,
width: 1
}]);
});
asyncTest('getHomeZoom', function() { asyncTest('getHomeZoom', function() {
reopenViewerHelper({ reopenViewerHelper({
property: 'defaultZoomLevel', property: 'defaultZoomLevel',
@ -336,7 +409,7 @@
viewer.open(DZI_PATH); viewer.open(DZI_PATH);
}); });
asyncTest('ensureVisible', function(){ asyncTest('ensureVisible', function() {
var openHandler = function(event) { var openHandler = function(event) {
viewer.removeHandler('open', openHandler); viewer.removeHandler('open', openHandler);
var viewport = viewer.viewport; var viewport = viewer.viewport;
@ -354,17 +427,114 @@
viewer.open(DZI_PATH); viewer.open(DZI_PATH);
}); });
asyncTest('applyConstraints', function() {
var openHandler = function() {
viewer.removeHandler('open', openHandler);
var viewport = viewer.viewport;
viewport.fitBounds(new OpenSeadragon.Rect(1, 1, 1, 1), true);
viewport.visibilityRatio = 0.3;
viewport.applyConstraints(true);
var bounds = viewport.getBounds();
Util.assertRectangleEquals(
bounds,
new OpenSeadragon.Rect(0.7, 0.7, 1, 1),
EPSILON,
"Viewport.applyConstraints should move viewport.");
start();
};
viewer.addHandler('open', openHandler);
viewer.open(DZI_PATH);
});
asyncTest('applyConstraints with rotation', function() {
var openHandler = function() {
viewer.removeHandler('open', openHandler);
var viewport = viewer.viewport;
viewport.setRotation(45);
viewport.fitBounds(new OpenSeadragon.Rect(1, 1, 1, 1), true);
viewport.applyConstraints(true);
var bounds = viewport.getBounds();
Util.assertRectangleEquals(
bounds,
new OpenSeadragon.Rect(1, 0, Math.sqrt(2), Math.sqrt(2), 45),
EPSILON,
"Viewport.applyConstraints with rotation should move viewport.");
start();
};
viewer.addHandler('open', openHandler);
viewer.open(DZI_PATH);
});
// Fit bounds tests
var testRectsFitBounds = [
new OpenSeadragon.Rect(0, -0.75, 0.5, 1),
new OpenSeadragon.Rect(0.5, 0, 0.5, 0.8),
new OpenSeadragon.Rect(0.75, 0.75, 0.5, 0.5),
new OpenSeadragon.Rect(-0.3, -0.3, 0.5, 0.5),
new OpenSeadragon.Rect(0.5, 0.25, Math.sqrt(0.125), Math.sqrt(0.125), 45)
];
var expectedRectsFitBounds = [
new OpenSeadragon.Rect(-0.25, -0.75, 1, 1),
new OpenSeadragon.Rect(0.35, 0, 0.8, 0.8),
new OpenSeadragon.Rect(0.75, 0.75, 0.5, 0.5),
new OpenSeadragon.Rect(-0.3, -0.3, 0.5, 0.5),
new OpenSeadragon.Rect(0.25, 0.25, 0.5, 0.5)
];
var expectedRectsFitBoundsWithRotation = [
new OpenSeadragon.Rect(
0.25,
-1,
Math.sqrt(0.125) + Math.sqrt(0.5),
Math.sqrt(0.125) + Math.sqrt(0.5),
45),
new OpenSeadragon.Rect(
0.75,
-0.25,
Math.sqrt(0.125) + Math.sqrt(8 / 25),
Math.sqrt(0.125) + Math.sqrt(8 / 25),
45),
new OpenSeadragon.Rect(
1,
0.5,
Math.sqrt(0.125) * 2,
Math.sqrt(0.125) * 2,
45),
new OpenSeadragon.Rect(
-0.05,
-0.55,
Math.sqrt(0.125) * 2,
Math.sqrt(0.125) * 2,
45),
new OpenSeadragon.Rect(
0.5,
0.25,
Math.sqrt(0.125),
Math.sqrt(0.125),
45)
];
var expectedRectsFitBoundsWithConstraints = [
new OpenSeadragon.Rect(-0.25, -0.5, 1, 1),
new OpenSeadragon.Rect(0.35, 0, 0.8, 0.8),
new OpenSeadragon.Rect(0.75, 0.75, 0.5, 0.5),
new OpenSeadragon.Rect(-0.25, -0.25, 0.5, 0.5),
new OpenSeadragon.Rect(0.25, 0.25, 0.5, 0.5)
];
asyncTest('fitBounds', function(){ asyncTest('fitBounds', function(){
var openHandler = function(event) { var openHandler = function(event) {
viewer.removeHandler('open', openHandler); viewer.removeHandler('open', openHandler);
var viewport = viewer.viewport; var viewport = viewer.viewport;
for(var i = 0; i < testRects.length; i++){ for(var i = 0; i < testRectsFitBounds.length; i++){
var rect = testRects[i].times(viewport.getContainerSize()); var rect = testRectsFitBounds[i];
viewport.fitBounds(rect, true); viewport.fitBounds(rect, true);
propEqual( propEqual(
viewport.getBounds(), viewport.getBounds(),
rect, expectedRectsFitBounds[i],
"Fit bounds correctly." "Fit bounds correctly."
); );
} }
@ -374,19 +544,27 @@
viewer.open(DZI_PATH); viewer.open(DZI_PATH);
}); });
var testRectsFitBounds = [ asyncTest('fitBounds with viewport rotation', function(){
new OpenSeadragon.Rect(0, -0.75, 0.5, 1), var openHandler = function(event) {
new OpenSeadragon.Rect(0.5, 0, 0.5, 0.8), viewer.removeHandler('open', openHandler);
new OpenSeadragon.Rect(0.75, 0.75, 0.5, 0.5), var viewport = viewer.viewport;
new OpenSeadragon.Rect(-0.3, -0.3, 0.5, 0.5) viewport.setRotation(45);
];
var expectedRectsFitBounds = [ for(var i = 0; i < testRectsFitBounds.length; i++){
new OpenSeadragon.Rect(-0.25, -0.5, 1, 1), var rect = testRectsFitBounds[i];
new OpenSeadragon.Rect(0.35, 0, 0.8, 0.8), viewport.fitBounds(rect, true);
new OpenSeadragon.Rect(0.75, 0.75, 0.5, 0.5), Util.assertRectangleEquals(
new OpenSeadragon.Rect(-0.25, -0.25, 0.5, 0.5) viewport.getBounds(),
]; expectedRectsFitBoundsWithRotation[i],
EPSILON,
"Fit bounds correctly."
);
}
start();
};
viewer.addHandler('open', openHandler);
viewer.open(DZI_PATH);
});
asyncTest('fitBoundsWithConstraints', function(){ asyncTest('fitBoundsWithConstraints', function(){
var openHandler = function(event) { var openHandler = function(event) {
@ -399,7 +577,7 @@
viewport.fitBoundsWithConstraints(rect, true); viewport.fitBoundsWithConstraints(rect, true);
propEqual( propEqual(
viewport.getBounds(), viewport.getBounds(),
expectedRectsFitBounds[i], expectedRectsFitBoundsWithConstraints[i],
"Fit bounds correctly." "Fit bounds correctly."
); );
} }
@ -409,6 +587,44 @@
viewer.open(DZI_PATH); viewer.open(DZI_PATH);
}); });
asyncTest('fitBounds with almost same zoom', function() {
var openHandler = function() {
var viewport = viewer.viewport;
var rect1 = new OpenSeadragon.Rect(0, 0, 1, 1);
viewport.fitBounds(rect1, true);
Util.assertRectangleEquals(rect1, viewport.getBounds(), 1e-6,
'Bounds should be ' + rect1);
// Zoom and pan
var rect2 = new OpenSeadragon.Rect(1, 1, 1 + 1e-8, 1 + 1e-8);
viewport.fitBounds(rect2);
Util.assertRectangleEquals(rect2, viewport.getBounds(), 1e-6,
'Bounds should be ' + rect2);
start();
};
viewer.addOnceHandler('open', openHandler);
viewer.open(DZI_PATH);
});
asyncTest('fitBounds with big rectangle', function() {
var openHandler = function() {
var viewport = viewer.viewport;
var rect1 = new OpenSeadragon.Rect(0, 0, 1e9, 1e9);
viewport.fitBounds(rect1, true);
Util.assertRectangleEquals(rect1, viewport.getBounds(), 1e-6,
'Bounds should be ' + rect1);
// Zoom and pan
var rect2 = new OpenSeadragon.Rect(1, 1, 2e9, 2e9);
viewport.fitBounds(rect2);
Util.assertRectangleEquals(rect2, viewport.getBounds(), 1e-6,
'Bounds should be ' + rect2);
start();
};
viewer.addOnceHandler('open', openHandler);
viewer.open(DZI_PATH);
});
asyncTest('fitHorizontally', function(){ asyncTest('fitHorizontally', function(){
var openHandler = function(event) { var openHandler = function(event) {
viewer.removeHandler('open', openHandler); viewer.removeHandler('open', openHandler);
@ -440,6 +656,7 @@
viewer.addHandler('open', openHandler); viewer.addHandler('open', openHandler);
viewer.open(WIDE_PATH); viewer.open(WIDE_PATH);
}); });
// End fitBounds tests.
asyncTest('panBy', function(){ asyncTest('panBy', function(){
var openHandler = function(event) { var openHandler = function(event) {

View File

@ -17,6 +17,7 @@
<script src="/build/openseadragon/openseadragon.js"></script> <script src="/build/openseadragon/openseadragon.js"></script>
<script src="/test/helpers/legacy.mouse.shim.js"></script> <script src="/test/helpers/legacy.mouse.shim.js"></script>
<script src="/test/helpers/test.js"></script> <script src="/test/helpers/test.js"></script>
<script src="/test/helpers/touch.js"></script>
<!-- Polyfill must be inserted first because it is testing functions <!-- Polyfill must be inserted first because it is testing functions
reassignments which could be done by other test. --> reassignments which could be done by other test. -->