diff --git a/Gruntfile.js b/Gruntfile.js index 1353c6ce..d1e86ae2 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -28,6 +28,7 @@ module.exports = function(grunt) { "src/mousetracker.js", "src/control.js", "src/controldock.js", + "src/placement.js", "src/viewer.js", "src/navigator.js", "src/strings.js", diff --git a/changelog.txt b/changelog.txt index b9da351a..725bfdc0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,12 +1,18 @@ OPENSEADRAGON CHANGELOG ======================= -2.1.1: (in progress) +2.2.0: (in progress) * BREAKING CHANGE: Viewport.homeBounds, Viewport.contentSize, Viewport.contentAspectX and -Viewport.contentAspectY have been removed. -* DEPRECATION: Viewport.setHomeBounds has been deprecated -* DEPRECATION: the Viewport constructor is now ignoring the contentSize option + Viewport.contentAspectY have been removed. (#846) +* BREAKING CHANGE: The Overlay.getBounds method now takes the viewport as parameter. (#896) +* 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) * Fixed issue with reference strip popping up virtual keyboard on mobile devices (#779) * 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) * Added Viewer.getOverlayById and Overlay.getBounds functions (#853) * 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: diff --git a/src/drawer.js b/src/drawer.js index 1677dc59..661663d1 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -259,13 +259,17 @@ $.Drawer.prototype = { } }, - _clear: function ( useSketch ) { - if ( !this.useCanvas ) { + _clear: function (useSketch, bounds) { + if (!this.useCanvas) { return; } - var context = this._getContext( useSketch ); - var canvas = context.canvas; - context.clearRect( 0, 0, canvas.width, canvas.height ); + var context = this._getContext(useSketch); + if (bounds) { + 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. - * @param {Float} opacity The opacity of the blending. - * @param {Float} [scale=1] The scale at which tiles were drawn on the sketch. Default is 1. - * Use scale to draw at a lower scale and then enlarge onto the main canvas. - * @param {OpenSeadragon.Point} [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. - * @returns {undefined} + * @param {Object} options The options + * @param {Float} options.opacity The opacity of the blending. + * @param {Float} [options.scale=1] The scale at which tiles were drawn on + * the sketch. Default is 1. + * Use scale to draw at a lower scale and then enlarge onto the main canvas. + * @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) { + var options = opacity; + if (!$.isPlainObject(options)) { + options = { + opacity: opacity, + scale: scale, + translate: translate, + compositeOperation: compositeOperation + }; + } if (!this.useCanvas || !this.sketchCanvas) { return; } - scale = scale || 1; - 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); - } + opacity = options.opacity; + compositeOperation = options.compositeOperation; + var bounds = options.bounds; this.context.save(); this.context.globalAlpha = opacity; if (compositeOperation) { this.context.globalCompositeOperation = compositeOperation; } - 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 - ); + if (bounds) { + this.context.drawImage( + this.sketchCanvas, + bounds.x, + bounds.y, + bounds.width, + bounds.height, + bounds.x, + bounds.y, + 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(); }, diff --git a/src/dzitilesource.js b/src/dzitilesource.js index 5d00980f..817f438e 100644 --- a/src/dzitilesource.js +++ b/src/dzitilesource.js @@ -301,7 +301,9 @@ function configureFromXML( tileSource, xmlDoc ){ } else if ( rootName == "Collection" ) { throw new Error( $.getString( "Errors.Dzc" ) ); } 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" ) ); diff --git a/src/eventsource.js b/src/eventsource.js index e305da25..e3957d7f 100644 --- a/src/eventsource.js +++ b/src/eventsource.js @@ -56,7 +56,31 @@ $.EventSource = function() { /** @lends OpenSeadragon.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. diff --git a/src/mousetracker.js b/src/mousetracker.js index 4528df03..498a72ac 100644 --- a/src/mousetracker.js +++ b/src/mousetracker.js @@ -1357,11 +1357,11 @@ * @private * @inner */ - function capturePointer( tracker, pointerType ) { + function capturePointer( tracker, pointerType, pointerCount ) { var pointsList = tracker.getActivePointersListByType( pointerType ), eventParams; - pointsList.captureCount++; + pointsList.captureCount += (pointerCount || 1); if ( pointsList.captureCount === 1 ) { if ( $.Browser.vendor === $.BROWSERS.IE && $.Browser.version < 9 ) { @@ -1400,11 +1400,11 @@ * @private * @inner */ - function releasePointer( tracker, pointerType ) { + function releasePointer( tracker, pointerType, pointerCount ) { var pointsList = tracker.getActivePointersListByType( pointerType ), eventParams; - pointsList.captureCount--; + pointsList.captureCount -= (pointerCount || 1); if ( pointsList.captureCount === 0 ) { 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 $.stopEvent( event ); - capturePointer( tracker, 'touch' ); + capturePointer( tracker, 'touch', touchCount ); } $.cancelEvent( event ); @@ -2128,7 +2128,7 @@ } if ( updatePointersUp( tracker, event, gPoints, 0 ) ) { - releasePointer( tracker, 'touch' ); + releasePointer( tracker, 'touch', touchCount ); } // simulate touchleave on our tracked element diff --git a/src/navigator.js b/src/navigator.js index 4b3a98d2..7b74d6ea 100644 --- a/src/navigator.js +++ b/src/navigator.js @@ -62,7 +62,7 @@ $.Navigator = function( options ){ options.controlOptions = { anchor: $.ControlAnchor.TOP_RIGHT, attachToViewer: true, - autoFade: true + autoFade: options.autoFade }; if( options.position ){ @@ -306,8 +306,8 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /* this.updateSize(); } - if( viewport && this.viewport ) { - bounds = viewport.getBounds( true ); + if (viewport && this.viewport) { + bounds = viewport.getBoundsNoRotate(true); topleft = this.viewport.pixelFromPointNoRotate(bounds.getTopLeft(), false); bottomright = this.viewport.pixelFromPointNoRotate(bounds.getBottomRight(), false) .minus( this.totalBorderWidths ); diff --git a/src/openseadragon.js b/src/openseadragon.js index a129f083..e2a03b19 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -82,28 +82,9 @@ */ -/** - * @version <%= pkg.name %> <%= pkg.version %> - * - * @file - *

OpenSeadragon - Javascript Deep Zooming

- *

- * 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. - *

- * - */ - -/** - * @module OpenSeadragon - * - */ - /** * @namespace OpenSeadragon - * + * @version <%= pkg.name %> <%= pkg.version %> * @classdesc The root namespace for OpenSeadragon. All utility methods * and classes are defined on or below this namespace. * @@ -154,7 +135,7 @@ * created. * * placement a string to define the relative position to the viewport. * 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] * DEPRECATED. A relative path to load a DZI file from the server. @@ -373,16 +354,16 @@ * * @property {String} [navigatorId=navigator-GENERATED DATE] * 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. * * @property {String} [navigatorPosition='TOP_RIGHT'] * Valid values are 'TOP_LEFT', 'TOP_RIGHT', 'BOTTOM_LEFT', 'BOTTOM_RIGHT', or 'ABSOLUTE'.
- * 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.
- * For 'TOP_LEFT', 'TOP_RIGHT', 'BOTTOM_LEFT', and 'BOTTOM_RIGHT', the navigatorSizeRatio or navigatorHeight|Width values determine the size of the navigator minimap. + * 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.
+ * 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] - * 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] * 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. * 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] * 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 * combinations of out-of-the-box configurable features. * - * @function OpenSeadragon - * @memberof module:OpenSeadragon * @param {OpenSeadragon.Options} options - Viewer options. * @returns {OpenSeadragon.Viewer} */ @@ -842,6 +825,21 @@ if (typeof define === 'function' && define.amd) { 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 @@ -1065,6 +1063,7 @@ if (typeof define === 'function' && define.amd) { navigatorHeight: null, navigatorWidth: null, navigatorAutoResize: true, + navigatorAutoFade: true, navigatorRotate: true, // INITIAL ROTATION @@ -1322,6 +1321,49 @@ if (typeof define === 'function' && define.amd) { 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). @@ -2533,185 +2575,4 @@ if (typeof define === 'function' && define.amd) { } } - /** - * @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 )); +}(OpenSeadragon)); diff --git a/src/overlay.js b/src/overlay.js index ffdd21bd..25cc50d2 100644 --- a/src/overlay.js +++ b/src/overlay.js @@ -32,14 +32,17 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -(function( $ ){ +(function($) { /** * An enumeration of positions that an overlay may be assigned relative to * the viewport. + * It is identical to OpenSeadragon.Placement but is kept for backward + * compatibility. * @member OverlayPlacement * @memberof OpenSeadragon * @static + * @readonly * @type {Object} * @property {Number} CENTER * @property {Number} TOP_LEFT @@ -51,17 +54,26 @@ * @property {Number} BOTTOM_LEFT * @property {Number} LEFT */ - $.OverlayPlacement = { - CENTER: 0, - TOP_LEFT: 1, - TOP: 2, - TOP_RIGHT: 3, - RIGHT: 4, - BOTTOM_RIGHT: 5, - BOTTOM: 6, - BOTTOM_LEFT: 7, - LEFT: 8 - }; + $.OverlayPlacement = $.Placement; + + /** + * An enumeration of possible ways to handle overlays rotation + * @member OverlayRotationMode + * @memberOf OpenSeadragon + * @static + * @readonly + * @property {Number} NO_ROTATION The overlay ignore the viewport rotation. + * @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 @@ -72,19 +84,27 @@ * @param {Element} options.element * @param {OpenSeadragon.Point|OpenSeadragon.Rect} options.location - The * location of the overlay on the image. If a {@link OpenSeadragon.Point} - * is specified, the overlay will keep a constant size independently of the - * zoom. If a {@link OpenSeadragon.Rect} is specified, the overlay size will - * be adjusted when the zoom changes. - * @param {OpenSeadragon.OverlayPlacement} [options.placement=OpenSeadragon.OverlayPlacement.TOP_LEFT] - * Relative position to the viewport. - * Only used if location is a {@link OpenSeadragon.Point}. + * is specified, the overlay will be located at this location with respect + * to the placement option. If a {@link OpenSeadragon.Rect} is specified, + * the overlay will be placed at this location with the corresponding width + * and height and placement TOP_LEFT. + * @param {OpenSeadragon.Placement} [options.placement=OpenSeadragon.Placement.TOP_LEFT] + * Defines what part of the overlay should be at the specified options.location * @param {OpenSeadragon.Overlay.OnDrawCallback} [options.onDraw] * @param {Boolean} [options.checkResize=true] Set to false to avoid to - * check the size of the overlay everytime it is drawn when using a - * {@link OpenSeadragon.Point} as options.location. It will improve - * performances but will cause a misalignment if the overlay size changes. + * check the size of the overlay everytime it is drawn in the directions + * which are not scaled. It will improve performances but will cause a + * 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}. @@ -97,7 +117,7 @@ */ var options; - if ( $.isPlainObject( element ) ) { + if ($.isPlainObject(element)) { options = element; } else { options = { @@ -107,73 +127,67 @@ }; } - this.element = options.element; - this.scales = options.location instanceof $.Rect; - this.bounds = new $.Rect( - 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; + this.element = options.element; + this.style = options.element.style; + this._init(options); }; /** @lends OpenSeadragon.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 - * @param {OpenSeadragon.OverlayPlacement} position + * @param {OpenSeadragon.Point} position * @param {OpenSeadragon.Point} size */ - adjust: function( position, size ) { - switch ( this.placement ) { - case $.OverlayPlacement.TOP_LEFT: - break; - case $.OverlayPlacement.TOP: - position.x -= size.x / 2; - break; - case $.OverlayPlacement.TOP_RIGHT: - position.x -= size.x; - break; - case $.OverlayPlacement.RIGHT: - position.x -= size.x; - position.y -= size.y / 2; - break; - case $.OverlayPlacement.BOTTOM_RIGHT: - position.x -= size.x; - position.y -= size.y; - break; - case $.OverlayPlacement.BOTTOM: - position.x -= size.x / 2; - position.y -= size.y; - break; - case $.OverlayPlacement.BOTTOM_LEFT: - position.y -= size.y; - break; - case $.OverlayPlacement.LEFT: - position.y -= size.y / 2; - break; - default: - case $.OverlayPlacement.CENTER: - position.x -= size.x / 2; - position.y -= size.y / 2; - break; + adjust: function(position, size) { + var properties = $.Placement.properties[this.placement]; + if (!properties) { + return; + } + if (properties.isHorizontallyCentered) { + position.x -= size.x / 2; + } else if (properties.isRight) { + position.x -= size.x; + } + if (properties.isVerticallyCentered) { + position.y -= size.y / 2; + } else if (properties.isBottom) { + position.y -= size.y; } }, @@ -181,20 +195,20 @@ * @function */ destroy: function() { - var element = this.element, - style = this.style; + var element = this.element; + var style = this.style; - if ( element.parentNode ) { - element.parentNode.removeChild( element ); + if (element.parentNode) { + element.parentNode.removeChild(element); //this should allow us to preserve overlays when required between //pages - if ( element.prevElementParent ) { + if (element.prevElementParent) { style.display = 'none'; //element.prevElementParent.insertBefore( // element, // element.prevNextSibling //); - document.body.appendChild( element ); + document.body.appendChild(element); } } @@ -205,122 +219,258 @@ style.left = ""; style.position = ""; - if ( this.scales ) { + if (this.width !== null) { style.width = ""; + } + if (this.height !== null) { style.height = ""; } + var transformOriginProp = $.getCssPropertyWithVendorPrefix( + 'transformOrigin'); + var transformProp = $.getCssPropertyWithVendorPrefix( + 'transform'); + if (transformOriginProp && transformProp) { + style[transformOriginProp] = ""; + style[transformProp] = ""; + } }, /** * @function * @param {Element} container */ - drawHTML: function( container, viewport ) { - var element = this.element, - style = this.style, - scales = this.scales, - degrees = viewport.degrees, - position = viewport.pixelFromPoint( - this.bounds.getTopLeft(), - true - ), - size, - overlayCenter; - - if ( element.parentNode != container ) { + drawHTML: function(container, viewport) { + var element = this.element; + if (element.parentNode !== container) { //save the source parent for later if we need it - element.prevElementParent = element.parentNode; - element.prevNextSibling = element.nextSibling; - container.appendChild( element ); - this.size = $.getElementSize( element ); + element.prevElementParent = element.parentNode; + element.prevNextSibling = element.nextSibling; + container.appendChild(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 ) { - size = viewport.deltaPixelsFromPoints( - this.bounds.getSize(), - true - ); - } else if ( this.checkResize ) { - size = $.getElementSize( element ); - } else { - size = this.size; - } + var positionAndSize = this._getOverlayPositionAndSize(viewport); - this.position = position; - this.size = size; - - 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 ) ); - } + var position = positionAndSize.position; + var size = this.size = positionAndSize.size; + var rotate = positionAndSize.rotate; // call the onDraw callback if it exists to allow one to overwrite // the drawing/positioning/sizing of the overlay - if ( this.onDraw ) { - this.onDraw( position, size, element ); + if (this.onDraw) { + this.onDraw(position, size, this.element); } else { - style.left = position.x + "px"; - style.top = position.y + "px"; + var style = this.style; + 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"; - if (style.display != 'none') { - style.display = 'block'; - } - - if ( scales ) { - style.width = size.x + "px"; - style.height = size.y + "px"; + if (style.display !== 'none') { + style.display = 'block'; } } }, - /** - * @function - * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - * @param {OpenSeadragon.OverlayPlacement} position - */ - update: function( location, placement ) { - this.scales = location instanceof $.Rect; - this.bounds = new $.Rect( - location.x, - location.y, - location.width, - location.height - ); - // rects are always top-left - this.placement = location instanceof $.Point ? - placement : - $.OverlayPlacement.TOP_LEFT; + // private + _getOverlayPositionAndSize: function(viewport) { + var position = viewport.pixelFromPoint(this.location, true); + var size = this._getSizeInPixels(viewport); + this.adjust(position, size); + + var rotate = 0; + if (viewport.degrees && + this.rotationMode !== $.OverlayRotationMode.NO_ROTATION) { + // BOUNDING_BOX is only valid if both directions get scaled. + // Get replaced by EXACT otherwise. + if (this.rotationMode === $.OverlayRotationMode.BOUNDING_BOX && + this.width !== null && this.height !== null) { + var rect = new $.Rect(position.x, position.y, size.x, size.y); + var boundingBox = this._getBoundingBox(rect, viewport.degrees); + position = boundingBox.getTopLeft(); + 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 + * @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 */ - getBounds: function() { - return this.bounds.clone(); + getBounds: function(viewport) { + $.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)); diff --git a/src/placement.js b/src/placement.js new file mode 100644 index 00000000..561d5daf --- /dev/null +++ b/src/placement.js @@ -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)); diff --git a/src/rectangle.js b/src/rectangle.js index cae13e88..98c839de 100644 --- a/src/rectangle.js +++ b/src/rectangle.js @@ -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 */ $.Rect.prototype = { /** @@ -280,11 +307,137 @@ $.Rect.prototype = { 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. * @function * @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. * @return {OpenSeadragon.Rect} */ @@ -340,6 +493,51 @@ $.Rect.prototype = { 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 * debugging. @@ -348,10 +546,10 @@ $.Rect.prototype = { */ toString: function() { return "[" + - (Math.round(this.x * 100) / 100) + "," + - (Math.round(this.y * 100) / 100) + "," + + (Math.round(this.x * 100) / 100) + ", " + + (Math.round(this.y * 100) / 100) + ", " + (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" + "]"; } diff --git a/src/referencestrip.js b/src/referencestrip.js index 0743d365..9d88a6a3 100644 --- a/src/referencestrip.js +++ b/src/referencestrip.js @@ -276,7 +276,6 @@ $.extend( $.ReferenceStrip.prototype, $.EventSource.prototype, $.Viewer.prototyp } this.currentPage = page; - $.getElement( element.id + '-displayregion' ).focus(); onStripEnter.call( this, { eventSource: this.innerTracker } ); } }, diff --git a/src/spring.js b/src/spring.js index e580b2e6..71c94d06 100644 --- a/src/spring.js +++ b/src/spring.js @@ -234,6 +234,15 @@ $.Spring.prototype = { } else { 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; } }; diff --git a/src/tile.js b/src/tile.js index 9abb14b6..04f3c08a 100644 --- a/src/tile.js +++ b/src/tile.js @@ -193,6 +193,11 @@ $.Tile.prototype = { return this.level + "/" + this.x + "_" + this.y; }, + // private + _hasTransparencyChannel: function() { + return !!this.context2D || this.url.match('.png'); + }, + /** * Renders the tile in an html container. * @function @@ -280,27 +285,6 @@ $.Tile.prototype = { 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) { // draw tile at a different scale position = position.times(scale); @@ -312,6 +296,25 @@ $.Tile.prototype = { 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( rendered.canvas, 0, @@ -333,15 +336,18 @@ $.Tile.prototype = { * @return {Float} */ getScaleForEdgeSmoothing: function() { - if (!this.cacheImageRecord) { + var context; + if (this.cacheImageRecord) { + context = this.cacheImageRecord.getRenderedContext(); + } else if (this.context2D) { + context = this.context2D; + } else { $.console.warn( '[Tile.drawCanvas] attempting to get tile scale %s when tile\'s not cached', this.toString()); return 1; } - - var rendered = this.cacheImageRecord.getRenderedContext(); - return rendered.canvas.width / this.size.times($.pixelDensityRatio).x; + return context.canvas.width / (this.size.x * $.pixelDensityRatio); }, /** diff --git a/src/tiledimage.js b/src/tiledimage.js index 7ad9d7f2..91f76c51 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -52,6 +52,10 @@ * @param {Number} [options.y=0] - Top position, in viewport coordinates. * @param {Number} [options.width=1] - Width, 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 * (portions of the image outside of this area will not be visible). Only works on * browsers that support the HTML5 canvas. @@ -122,6 +126,11 @@ $.TiledImage = function( options ) { delete options.height; } + var fitBounds = options.fitBounds; + delete options.fitBounds; + var fitBoundsPlacement = options.fitBoundsPlacement || OpenSeadragon.Placement.CENTER; + delete options.fitBoundsPlacement; + $.extend( true, this, { //internal state properties @@ -172,6 +181,10 @@ $.TiledImage = function( options ) { this._updateForScale(); + if (fitBounds) { + this.fitBounds(fitBounds, fitBoundsPlacement, true); + } + // We need a callback to give image manipulation a chance to happen this._drawingHandler = function(args) { /** @@ -274,6 +287,26 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag 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. */ @@ -543,6 +576,67 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag 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, * 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. */ this.raiseEvent('bounds-change'); + }, + + // private + _isBottomItem: function() { + return this.viewer.world.getItemAt(0) === this; } }); @@ -688,7 +787,6 @@ function updateViewport( tiledImage ) { Math.log( 2 ) )) ), - degrees = tiledImage.viewport.degrees, renderPixelRatioC, renderPixelRatioT, zeroRatioT, @@ -696,26 +794,23 @@ function updateViewport( tiledImage ) { levelOpacity, levelVisibility; - viewportBounds.x -= tiledImage._xSpring.current.value; - viewportBounds.y -= tiledImage._ySpring.current.value; - // Reset tile's internal drawn state - while ( tiledImage.lastDrawn.length > 0 ) { + while (tiledImage.lastDrawn.length > 0) { tile = tiledImage.lastDrawn.pop(); tile.beingDrawn = false; } - //Change bounds for rotation - if (degrees === 90 || degrees === 270) { - viewportBounds = viewportBounds.rotate( degrees ); - } else if (degrees !== 0 && degrees !== 180) { - // This is just an approximation. - var orthBounds = viewportBounds.rotate(90); - viewportBounds.x -= orthBounds.width / 2; - viewportBounds.y -= orthBounds.height / 2; - viewportBounds.width += orthBounds.width; - viewportBounds.height += orthBounds.height; + if (!tiledImage.wrapHorizontal && !tiledImage.wrapVertical) { + var tiledImageBounds = tiledImage.getClippedBounds(true); + var intersection = viewportBounds.intersection(tiledImageBounds); + if (intersection === null) { + return; + } + viewportBounds = intersection; } + viewportBounds = viewportBounds.getBoundingBox(); + viewportBounds.x -= tiledImage._xSpring.current.value; + viewportBounds.y -= tiledImage._ySpring.current.value; var viewportTL = viewportBounds.getTopLeft(); var viewportBR = viewportBounds.getBottomRight(); @@ -1324,22 +1419,22 @@ function compareTiles( previousBest, tile ) { } function drawTiles( tiledImage, lastDrawn ) { - var i, - tile = lastDrawn[0]; - - if ( tiledImage.opacity <= 0 ) { - drawDebugInfo( tiledImage, lastDrawn ); + if (lastDrawn.length === 0) { return; } + var tile = lastDrawn[0]; + var useSketch = tiledImage.opacity < 1 || - (tiledImage.compositeOperation && tiledImage.compositeOperation !== 'source-over'); + (tiledImage.compositeOperation && + tiledImage.compositeOperation !== 'source-over') || + (!tiledImage._isBottomItem() && tile._hasTransparencyChannel()); var sketchScale; var sketchTranslate; var zoom = tiledImage.viewport.getZoom(true); 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. // So we have to composite them at ~100% and scale them up together. useSketch = true; @@ -1349,8 +1444,17 @@ function drawTiles( tiledImage, lastDrawn ) { tiledImage._drawer.getCanvasSize(true)); } - if ( useSketch ) { - tiledImage._drawer._clear( true ); + var bounds; + 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 @@ -1396,7 +1500,7 @@ function drawTiles( tiledImage, lastDrawn ) { 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 ]; tiledImage._drawer.drawTile( tile, tiledImage._drawingHandler, useSketch, sketchScale, sketchTranslate ); tile.beingDrawn = true; @@ -1433,7 +1537,13 @@ function drawTiles( tiledImage, lastDrawn ) { if (offsetForRotation) { 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) { tiledImage._drawer._restoreRotationChanges(false); } diff --git a/src/tilesource.js b/src/tilesource.js index 290a5758..48941848 100644 --- a/src/tilesource.js +++ b/src/tilesource.js @@ -190,7 +190,7 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve this.aspectRatio = ( options.width && options.height ) ? ( options.width / options.height ) : 1; this.dimensions = new $.Point( options.width, options.height ); - + if ( this.tileSize ){ this._tileWidth = this._tileHeight = this.tileSize; delete this.tileSize; @@ -212,7 +212,7 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve this._tileHeight = 0; } } - + this.tileOverlap = options.tileOverlap ? options.tileOverlap : 0; this.minLevel = options.minLevel ? options.minLevel : 0; this.maxLevel = ( undefined !== options.maxLevel && null !== options.maxLevel ) ? @@ -240,7 +240,7 @@ $.TileSource.prototype = { ); return this._tileWidth; }, - + /** * Return the tileWidth for a given level. * 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.y / this.getTileHeight(i) ) ); - + if( tiles.x + 1 >= tilesPerSide.x && tiles.y + 1 >= tilesPerSide.y ){ break; } @@ -544,7 +544,7 @@ $.TileSource.prototype = { /** * 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 * announcing you have to implement it. Because of the variety of tile * server technologies, and various specifications for building image diff --git a/src/viewer.js b/src/viewer.js index d96f81c0..6ed18bc0 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -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.
+ *
* 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.
+ *
* 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.
+ *
+ * To create a viewer, you can use either of this methods:
+ * * @class Viewer * @classdesc The main OpenSeadragon viewer class. * @@ -234,7 +239,9 @@ $.Viewer = function( options ) { style.left = "0px"; }(this.canvas.style)); $.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 this.container.className = "openseadragon-container"; @@ -410,6 +417,7 @@ $.Viewer = function( options ) { width: this.navigatorWidth, height: this.navigatorHeight, autoResize: this.navigatorAutoResize, + autoFade: this.navigatorAutoFade, prefixUrl: this.prefixUrl, viewer: this, navigatorRotate: this.navigatorRotate, @@ -911,9 +919,14 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, docStyle.padding = "0"; this.bodyWidth = bodyStyle.width; - this.bodyHeight = bodyStyle.height; + this.docWidth = docStyle.width; bodyStyle.width = "100%"; + docStyle.width = "100%"; + + this.bodyHeight = bodyStyle.height; + this.docHeight = docStyle.height; bodyStyle.height = "100%"; + docStyle.height = "100%"; //when entering full screen on the ipad it wasnt sufficient to leave //the body intact as only only the top half of the screen would @@ -974,7 +987,10 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, docStyle.padding = this.docPadding; bodyStyle.width = this.bodyWidth; + docStyle.width = this.docWidth; + bodyStyle.height = this.bodyHeight; + docStyle.height = this.docHeight; body.removeChild( this.element ); 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.width=1] The width 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 * (portions of the image outside of this area will not be visible). Only works on * browsers that support the HTML5 canvas. @@ -1339,6 +1359,8 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, y: queueItem.options.y, width: queueItem.options.width, height: queueItem.options.height, + fitBounds: queueItem.options.fitBounds, + fitBoundsPlacement: queueItem.options.fitBoundsPlacement, clip: queueItem.options.clip, placeholderFillStyle: queueItem.options.placeholderFillStyle, opacity: queueItem.options.opacity, @@ -1780,10 +1802,12 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * is closed which include when changing page. * @method * @param {Element|String|Object} element - A reference to an element or an id for - * the element which will 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 - * rectangle which will be overlayed. - * @param {OpenSeadragon.OverlayPlacement} placement - The position of the + * rectangle which will be overlayed. This is a viewport relative location. + * @param {OpenSeadragon.Placement} placement - The position of the * viewport which the location coordinates will be treated as relative * to. * @param {function} onDraw - If supplied the callback is called when the overlay @@ -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 {Element} element - The overlay element. * @property {OpenSeadragon.Point|OpenSeadragon.Rect} location - * @property {OpenSeadragon.OverlayPlacement} placement + * @property {OpenSeadragon.Placement} placement * @property {?Object} userData - Arbitrary subscriber-defined object. */ 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 * the element which is overlayed. * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or - * rectangle which will be overlayed. - * @param {OpenSeadragon.OverlayPlacement} placement - The position of the + * rectangle which will be overlayed. This is a viewport relative location. + * @param {OpenSeadragon.Placement} placement - The position of the * viewport which the location coordinates will be treated as relative * to. * @return {OpenSeadragon.Viewer} Chainable. @@ -1870,7 +1894,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * Viewer which raised the event. * @property {Element} element * @property {OpenSeadragon.Point|OpenSeadragon.Rect} location - * @property {OpenSeadragon.OverlayPlacement} placement + * @property {OpenSeadragon.Placement} placement * @property {?Object} userData - Arbitrary subscriber-defined object. */ this.raiseEvent( 'update-overlay', { @@ -2191,37 +2215,28 @@ function getOverlayObject( viewer, overlay ) { } var location = overlay.location; - if ( !location ) { - if ( overlay.width && overlay.height ) { - location = overlay.px !== undefined ? - viewer.viewport.imageToViewportRectangle( new $.Rect( - overlay.px, - overlay.py, - overlay.width, - overlay.height - ) ) : - new $.Rect( - overlay.x, - overlay.y, - overlay.width, - overlay.height - ); - } else { - location = overlay.px !== undefined ? - viewer.viewport.imageToViewportCoordinates( new $.Point( - overlay.px, - overlay.py - ) ) : - new $.Point( - overlay.x, - overlay.y - ); + var width = overlay.width; + var height = overlay.height; + if (!location) { + var x = overlay.x; + var y = overlay.y; + if (overlay.px !== undefined) { + var rect = viewer.viewport.imageToViewportRectangle(new $.Rect( + overlay.px, + overlay.py, + width || 0, + height || 0)); + x = rect.x; + y = rect.y; + width = width !== undefined ? rect.width : undefined; + height = height !== undefined ? rect.height : undefined; } + location = new $.Point(x, y); } var placement = overlay.placement; - if ( placement && ( $.type( placement ) === "string" ) ) { - placement = $.OverlayPlacement[ overlay.placement.toUpperCase() ]; + if (placement && $.type(placement) === "string") { + placement = $.Placement[overlay.placement.toUpperCase()]; } return new $.Overlay({ @@ -2229,7 +2244,10 @@ function getOverlayObject( viewer, overlay ) { location: location, placement: placement, 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 ) { - var gestureSettings; - - if ( !event.preventDefaultAction && this.viewport ) { - gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); - if ( gestureSettings.flickEnabled && event.speed >= gestureSettings.flickMinSpeed ) { - var amplitudeX = gestureSettings.flickMomentum * ( event.speed * Math.cos( event.direction - (Math.PI / 180 * this.viewport.degrees) ) ), - amplitudeY = gestureSettings.flickMomentum * ( event.speed * Math.sin( event.direction - (Math.PI / 180 * this.viewport.degrees) ) ), - center = this.viewport.pixelFromPoint( this.viewport.getCenter( true ) ), - target = this.viewport.pointFromPixel( new $.Point( center.x - amplitudeX, center.y - amplitudeY ) ); - if( !this.panHorizontal ) { - target.x = center.x; + if (!event.preventDefaultAction && this.viewport) { + var gestureSettings = this.gestureSettingsByDeviceType(event.pointerType); + if (gestureSettings.flickEnabled && + event.speed >= gestureSettings.flickMinSpeed) { + var amplitudeX = 0; + if (this.panHorizontal) { + amplitudeX = gestureSettings.flickMomentum * event.speed * + Math.cos(event.direction); } - if( !this.panVertical ) { - target.y = center.y; + var amplitudeY = 0; + 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(); } @@ -2576,7 +2597,7 @@ function onCanvasDragEnd( event ) { * @property {Object} originalEvent - The original DOM event. * @property {?Object} userData - Arbitrary subscriber-defined object. */ - this.raiseEvent( 'canvas-drag-end', { + this.raiseEvent('canvas-drag-end', { tracker: event.eventSource, position: event.position, speed: event.speed, @@ -2963,33 +2984,26 @@ function updateOnce( viewer ) { return; } - var containerSize; - if ( viewer.autoResize ) { - containerSize = _getSafeElemSize( viewer.container ); - if ( !containerSize.equals( THIS[ viewer.hash ].prevContainerSize ) ) { - if ( viewer.preserveImageSizeOnResize ) { - var prevContainerSize = THIS[ viewer.hash ].prevContainerSize; - var bounds = viewer.viewport.getBounds(true); - var deltaX = (containerSize.x - prevContainerSize.x); - var deltaY = (containerSize.y - prevContainerSize.y); - var viewportDiff = viewer.viewport.deltaPointsFromPixels(new OpenSeadragon.Point(deltaX, deltaY), true); - viewer.viewport.resize(new OpenSeadragon.Point(containerSize.x, containerSize.y), false); - - // Keep the center of the image in the center and just adjust the amount of image shown - bounds.width += viewportDiff.x; - bounds.height += viewportDiff.y; - bounds.x -= (viewportDiff.x / 2); - bounds.y -= (viewportDiff.y / 2); - viewer.viewport.fitBoundsWithConstraints(bounds, true); - } - else { + if (viewer.autoResize) { + var containerSize = _getSafeElemSize(viewer.container); + var prevContainerSize = THIS[viewer.hash].prevContainerSize; + if (!containerSize.equals(prevContainerSize)) { + var viewport = viewer.viewport; + if (viewer.preserveImageSizeOnResize) { + var resizeRatio = prevContainerSize.x / containerSize.x; + var zoom = viewport.getZoom() * resizeRatio; + var center = viewport.getCenter(); + viewport.resize(containerSize, false); + viewport.zoomTo(zoom, null, true); + viewport.panTo(center, true); + } else { // maintain image position - var oldBounds = viewer.viewport.getBounds(); - var oldCenter = viewer.viewport.getCenter(); - resizeViewportAndRecenter(viewer, containerSize, oldBounds, oldCenter); + var oldBounds = viewport.getBounds(); + viewport.resize(containerSize, true); + viewport.fitBoundsWithConstraints(oldBounds, true); } - THIS[ viewer.hash ].prevContainerSize = containerSize; - THIS[ viewer.hash ].forceRedraw = true; + THIS[viewer.hash].prevContainerSize = containerSize; + THIS[viewer.hash].forceRedraw = true; } } @@ -3074,27 +3088,6 @@ function updateOnce( viewer ) { //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 ) { viewer.imageLoader.clear(); viewer.drawer.clear(); diff --git a/src/viewport.js b/src/viewport.js index 63b4dcaf..da2ce761 100644 --- a/src/viewport.js +++ b/src/viewport.js @@ -237,6 +237,17 @@ $.Viewport.prototype = { * @returns {OpenSeadragon.Rect} The home bounds in vewport coordinates. */ 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 width = 1.0 / this.getHomeZoom(); var height = width / this.getAspectRatio(); @@ -254,8 +265,8 @@ $.Viewport.prototype = { * @param {Boolean} immediately * @fires OpenSeadragon.Viewer.event:home */ - goHome: function( immediately ) { - if( this.viewer ){ + goHome: function(immediately) { + if (this.viewer) { /** * Raised when the "home" operation occurs (see {@link OpenSeadragon.Viewport#goHome}). * @@ -266,11 +277,11 @@ $.Viewport.prototype = { * @property {Boolean} immediately * @property {?Object} userData - Arbitrary subscriber-defined object. */ - this.viewer.raiseEvent( 'home', { + this.viewer.raiseEvent('home', { 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. + * @function * @returns {Object} Properties (Numbers, in screen coordinates): left, top, right, bottom. */ getMargins: function() { @@ -326,8 +337,8 @@ $.Viewport.prototype = { }, /** - * @function * 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. */ setMargins: function(margins) { @@ -341,7 +352,9 @@ $.Viewport.prototype = { }, margins); 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). * @returns {OpenSeadragon.Rect} The location you are zoomed/panned to, in viewport coordinates. */ - getBounds: function( current ) { - var center = this.getCenter( current ), - width = 1.0 / this.getZoom( current ), - height = width / this.getAspectRatio(); + getBounds: function(current) { + return this.getBoundsNoRotate(current).rotate(-this.getRotation()); + }, + + /** + * 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( - center.x - ( width / 2.0 ), - center.y - ( height / 2.0 ), + center.x - (width / 2.0), + center.y - (height / 2.0), width, height ); @@ -369,8 +394,19 @@ $.Viewport.prototype = { * @returns {OpenSeadragon.Rect} The location you are zoomed/panned to, * including the space taken by margins, in viewport coordinates. */ - getBoundsWithMargins: function( current ) { - var bounds = this.getBounds(current); + getBoundsWithMargins: function(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); bounds.x -= this._margins.left / 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 * @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 + * @param {Boolean} [immediately=false] * @return {OpenSeadragon.Viewport} Chainable. * @fires OpenSeadragon.Viewer.event:constrain */ - applyConstraints: function( immediately ) { - var actualZoom = this.getZoom(), - constrainedZoom = Math.max( - Math.min( actualZoom, this.getMaxZoom() ), - this.getMinZoom() - ), - bounds, - constrainedBounds; + applyConstraints: function(immediately) { + var actualZoom = this.getZoom(); + var constrainedZoom = this._applyZoomConstraints(actualZoom); - if ( actualZoom != constrainedZoom ) { - this.zoomTo( constrainedZoom, this.zoomPoint, immediately ); + if (actualZoom !== constrainedZoom) { + 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 || immediately ){ - this.fitBounds( constrainedBounds, immediately ); + if (bounds.x !== constrainedBounds.x || + bounds.y !== constrainedBounds.y || + immediately) { + this.fitBounds( + constrainedBounds.rotate(-this.getRotation()), + immediately); } - return this; }, /** + * Equivalent to {@link OpenSeadragon.Viewport#applyConstraints} * @function - * @param {Boolean} immediately + * @param {Boolean} [immediately=false] + * @return {OpenSeadragon.Viewport} Chainable. + * @fires OpenSeadragon.Viewer.event:constrain */ - ensureVisible: function( immediately ) { - return this.applyConstraints( immediately ); + ensureVisible: function(immediately) { + return this.applyConstraints(immediately); }, /** @@ -562,41 +609,37 @@ $.Viewport.prototype = { * @param {Object} options (immediately=false, constraints=false) * @return {OpenSeadragon.Viewport} Chainable. */ - _fitBounds: function( bounds, options ) { + _fitBounds: function(bounds, options) { options = options || {}; var immediately = options.immediately || false; var constraints = options.constraints || false; var aspect = this.getAspectRatio(); var center = bounds.getCenter(); + + // Compute width and height of bounding box. var newBounds = new $.Rect( bounds.x, bounds.y, bounds.width, - bounds.height - ); + bounds.height, + bounds.degrees + this.getRotation()) + .getBoundingBox(); - if ( newBounds.getAspectRatio() >= aspect ) { - newBounds.height = bounds.width / aspect; - newBounds.y = center.y - newBounds.height / 2; + if (newBounds.getAspectRatio() >= aspect) { + newBounds.height = newBounds.width / aspect; } else { - newBounds.width = bounds.height * aspect; - newBounds.x = center.x - newBounds.width / 2; + newBounds.width = newBounds.height * aspect; } - this.panTo( this.getCenter( true ), true ); - this.zoomTo( this.getZoom( true ), null, true ); - - var oldBounds = this.getBounds(); - var oldZoom = this.getZoom(); - var newZoom = 1.0 / newBounds.width; + // Compute x and y from width, height and center position + newBounds.x = center.x - newBounds.width / 2; + newBounds.y = center.y - newBounds.height / 2; + var newZoom = 1.0 / newBounds.width; if (constraints) { var newBoundsAspectRatio = newBounds.getAspectRatio(); - var newConstrainedZoom = Math.max( - Math.min(newZoom, this.getMaxZoom() ), - this.getMinZoom() - ); + var newConstrainedZoom = this._applyZoomConstraints(newZoom); if (newZoom !== newConstrainedZoom) { newZoom = newConstrainedZoom; @@ -606,58 +649,70 @@ $.Viewport.prototype = { newBounds.y = center.y - newBounds.height / 2; } - newBounds = this._applyBoundaryConstraints( newBounds, immediately ); + newBounds = this._applyBoundaryConstraints(newBounds, immediately); center = newBounds.getCenter(); } if (immediately) { - this.panTo( center, true ); + this.panTo(center, true); return this.zoomTo(newZoom, null, true); } - if (Math.abs(newZoom - oldZoom) < 0.00000001 || - Math.abs(newBounds.width - oldBounds.width) < 0.00000001) { - return this.panTo( center, immediately ); + this.panTo(this.getCenter(true), true); + this.zoomTo(this.getZoom(true), null, true); + + 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( - this._containerInnerSize.x / oldBounds.width - ).minus( - newBounds.getTopLeft().times( - this._containerInnerSize.x / newBounds.width - ) - ).divide( - this._containerInnerSize.x / oldBounds.width - - this._containerInnerSize.x / newBounds.width - ); + newBounds = newBounds.rotate(-this.getRotation()); + var referencePoint = newBounds.getTopLeft().times(newZoom) + .minus(oldBounds.getTopLeft().times(oldZoom)) + .divide(newZoom - oldZoom); - 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 * @param {OpenSeadragon.Rect} bounds - * @param {Boolean} immediately + * @param {Boolean} [immediately=false] * @return {OpenSeadragon.Viewport} Chainable. */ - fitBounds: function( bounds, immediately ) { - return this._fitBounds( bounds, { + fitBounds: function(bounds, immediately) { + return this._fitBounds(bounds, { immediately: immediately, 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 * @param {OpenSeadragon.Rect} bounds - * @param {Boolean} immediately + * @param {Boolean} [immediately=false] * @return {OpenSeadragon.Viewport} Chainable. */ - fitBoundsWithConstraints: function( bounds, immediately ) { - return this._fitBounds( bounds, { + fitBoundsWithConstraints: function(bounds, immediately) { + return this._fitBounds(bounds, { immediately: immediately, constraints: true - } ); + }); }, /** @@ -752,7 +807,12 @@ $.Viewport.prototype = { }, /** + * Zooms to the specified zoom level * @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. * @fires OpenSeadragon.Viewer.event:zoom */ @@ -842,7 +902,7 @@ $.Viewport.prototype = { * @fires OpenSeadragon.Viewer.event:resize */ resize: function( newContainerSize, maintain ) { - var oldBounds = this.getBounds(), + var oldBounds = this.getBoundsNoRotate(), newBounds = oldBounds, widthDeltaFactor; @@ -888,37 +948,37 @@ $.Viewport.prototype = { }, /** + * Update the zoom and center (X and Y) springs. * @function + * @returns {Boolean} True if any change has been made, false otherwise. */ update: function() { - var oldZoomPixel, - newZoomPixel, - deltaZoomPixels, - deltaZoomPoints; 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) { - newZoomPixel = this.pixelFromPoint( this.zoomPoint, true ); - deltaZoomPixels = newZoomPixel.minus( oldZoomPixel ); - deltaZoomPoints = this.deltaPointsFromPixels( deltaZoomPixels, true ); + this.centerSpringX.shiftBy(deltaZoomPoints.x); + this.centerSpringY.shiftBy(deltaZoomPoints.y); - this.centerSpringX.shiftBy( deltaZoomPoints.x ); - this.centerSpringY.shiftBy( deltaZoomPoints.y ); + if (this.zoomSpring.isAtTargetValue()) { + this.zoomPoint = null; + } } else { - this.zoomPoint = null; + this.zoomSpring.update(); } this.centerSpringX.update(); this.centerSpringY.update(); - var changed = this.centerSpringX.current.value != this._oldCenterX || - this.centerSpringY.current.value != this._oldCenterY || - this.zoomSpring.current.value != this._oldZoom; + var changed = this.centerSpringX.current.value !== this._oldCenterX || + this.centerSpringY.current.value !== this._oldCenterY || + this.zoomSpring.current.value !== this._oldZoom; this._oldCenterX = this.centerSpringX.current.value; this._oldCenterY = this.centerSpringY.current.value; @@ -994,7 +1054,8 @@ $.Viewport.prototype = { * @returns {OpenSeadragon.Point} */ 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} */ pixelFromPoint: function(point, current) { - return this._pixelFromPoint(point, this.getBounds(current)); + return this._pixelFromPoint(point, this.getBoundsNoRotate(current)); }, // private @@ -1036,7 +1097,7 @@ $.Viewport.prototype = { * @returns {OpenSeadragon.Point} */ pointFromPixelNoRotate: function(pixel, current) { - var bounds = this.getBounds( current ); + var bounds = this.getBoundsNoRotate(current); return pixel.minus( new $.Point(this._margins.left, this._margins.top) ).divide( @@ -1085,8 +1146,18 @@ $.Viewport.prototype = { return this.viewportToImageCoordinates(viewerX.x, viewerX.y); } - if (this.viewer && this.viewer.world.getItemCount() > 1) { - $.console.error('[Viewport.viewportToImageCoordinates] is not accurate with multi-image; use TiledImage.viewportToImageCoordinates instead.'); + if (this.viewer) { + 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( @@ -1119,8 +1190,18 @@ $.Viewport.prototype = { return this.imageToViewportCoordinates(imageX.x, imageX.y); } - if (this.viewer && this.viewer.world.getItemCount() > 1) { - $.console.error('[Viewport.imageToViewportCoordinates] is not accurate with multi-image; use TiledImage.imageToViewportCoordinates instead.'); + if (this.viewer) { + 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); @@ -1150,6 +1231,21 @@ $.Viewport.prototype = { 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 coordB = this._imageToViewportDelta(rect.width, rect.height); return new $.Rect( @@ -1183,6 +1279,21 @@ $.Viewport.prototype = { 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 coordB = this._viewportToImageDelta(rect.width, rect.height); return new $.Rect( @@ -1224,10 +1335,12 @@ $.Viewport.prototype = { * @param {OpenSeadragon.Point} pixel * @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( - OpenSeadragon.getElementPosition( this.viewer.element )); - return this.viewerElementToImageCoordinates( viewerCoordinates ); + $.getElementPosition(this.viewer.element)); + return this.viewerElementToImageCoordinates(viewerCoordinates); }, /** @@ -1236,10 +1349,12 @@ $.Viewport.prototype = { * @param {OpenSeadragon.Point} pixel * @returns {OpenSeadragon.Point} */ - imageToWindowCoordinates: function( pixel ) { - var viewerCoordinates = this.imageToViewerElementCoordinates( pixel ); + imageToWindowCoordinates: function(pixel) { + $.console.assert(this.viewer, + "[Viewport.imageToWindowCoordinates] the viewport must have a viewer."); + var viewerCoordinates = this.imageToViewerElementCoordinates(pixel); return viewerCoordinates.plus( - OpenSeadragon.getElementPosition( this.viewer.element )); + $.getElementPosition(this.viewer.element)); }, /** @@ -1262,15 +1377,45 @@ $.Viewport.prototype = { 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. * @param {OpenSeadragon.Point} pixel * @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( - OpenSeadragon.getElementPosition( this.viewer.element )); - return this.viewerElementToViewportCoordinates( viewerCoordinates ); + $.getElementPosition(this.viewer.element)); + return this.viewerElementToViewportCoordinates(viewerCoordinates); }, /** @@ -1278,10 +1423,12 @@ $.Viewport.prototype = { * @param {OpenSeadragon.Point} point * @returns {OpenSeadragon.Point} */ - viewportToWindowCoordinates: function( point ) { - var viewerCoordinates = this.viewportToViewerElementCoordinates( point ); + viewportToWindowCoordinates: function(point) { + $.console.assert(this.viewer, + "[Viewport.viewportToWindowCoordinates] the viewport must have a viewer."); + var viewerCoordinates = this.viewportToViewerElementCoordinates(point); return viewerCoordinates.plus( - OpenSeadragon.getElementPosition( this.viewer.element )); + $.getElementPosition(this.viewer.element)); }, /** @@ -1296,9 +1443,19 @@ $.Viewport.prototype = { * target zoom. * @returns {Number} imageZoom The image zoom */ - viewportToImageZoom: function( viewportZoom ) { - if (this.viewer && this.viewer.world.getItemCount() > 1) { - $.console.error('[Viewport.viewportToImageZoom] is not accurate with multi-image.'); + viewportToImageZoom: function(viewportZoom) { + if (this.viewer) { + 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; @@ -1320,9 +1477,19 @@ $.Viewport.prototype = { * target zoom. * @returns {Number} viewportZoom The viewport zoom */ - imageToViewportZoom: function( imageZoom ) { - if (this.viewer && this.viewer.world.getItemCount() > 1) { - $.console.error('[Viewport.imageToViewportZoom] is not accurate with multi-image.'); + imageToViewportZoom: function(imageZoom) { + if (this.viewer) { + 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; diff --git a/src/world.js b/src/world.js index 5597f0f8..07e99b81 100644 --- a/src/world.js +++ b/src/world.js @@ -375,34 +375,40 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W var oldContentSize = this._contentSize ? this._contentSize.clone() : null; var oldContentFactor = this._contentFactor || 0; - if ( !this._items.length ) { + if (!this._items.length) { this._homeBounds = new $.Rect(0, 0, 1, 1); this._contentSize = new $.Point(1, 1); this._contentFactor = 1; } else { - var bounds = this._items[0].getBounds(); - this._contentFactor = this._items[0].getContentSize().x / bounds.width; - var left = bounds.x; - var top = bounds.y; - var right = bounds.x + bounds.width; - var bottom = bounds.y + bounds.height; - var box; - for ( var i = 1; i < this._items.length; i++ ) { - box = this._items[i].getBounds(); - this._contentFactor = Math.max(this._contentFactor, this._items[i].getContentSize().x / box.width); - left = Math.min( left, box.x ); - top = Math.min( top, box.y ); - right = Math.max( right, box.x + box.width ); - bottom = Math.max( bottom, box.y + box.height ); + var item = this._items[0]; + var bounds = item.getBounds(); + this._contentFactor = item.getContentSize().x / bounds.width; + var clippedBounds = item.getClippedBounds(); + var left = clippedBounds.x; + var top = clippedBounds.y; + var right = clippedBounds.x + clippedBounds.width; + var bottom = clippedBounds.y + clippedBounds.height; + for (var i = 1; i < this._items.length; i++) { + item = this._items[i]; + bounds = item.getBounds(); + this._contentFactor = Math.max(this._contentFactor, + item.getContentSize().x / bounds.width); + 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._contentSize = new $.Point(this._homeBounds.width * this._contentFactor, + this._homeBounds = new $.Rect(left, top, right - left, bottom - top); + this._contentSize = new $.Point( + this._homeBounds.width * this._contentFactor, this._homeBounds.height * this._contentFactor); } - if (this._contentFactor !== oldContentFactor || !this._homeBounds.equals(oldHomeBounds) || - !this._contentSize.equals(oldContentSize)) { + if (this._contentFactor !== oldContentFactor || + !this._homeBounds.equals(oldHomeBounds) || + !this._contentSize.equals(oldContentSize)) { /** * Raised when the home bounds or content factor change. * @event metrics-change diff --git a/test/coverage.html b/test/coverage.html index bcf893ee..b04d5fda 100644 --- a/test/coverage.html +++ b/test/coverage.html @@ -22,6 +22,7 @@ + @@ -52,6 +53,7 @@ +