diff --git a/.jshintrc b/.jshintrc index 626284b2..013e31e8 100644 --- a/.jshintrc +++ b/.jshintrc @@ -10,6 +10,7 @@ "globals": { "OpenSeadragon": true, - "define": false + "define": false, + "module": false } } diff --git a/Gruntfile.js b/Gruntfile.js index d5891153..9871806b 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/README.md b/README.md index f8564916..92b6fb86 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # OpenSeadragon -[![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/openseadragon/openseadragon?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://secure.travis-ci.org/openseadragon/openseadragon.png?branch=master)](http://travis-ci.org/openseadragon/openseadragon) +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/openseadragon/openseadragon?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://secure.travis-ci.org/openseadragon/openseadragon.png?branch=master)](http://travis-ci.org/openseadragon/openseadragon) An open-source, web-based viewer for zoomable images, implemented in pure JavaScript. diff --git a/changelog.txt b/changelog.txt index 265a75a8..eb18e085 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,8 +1,30 @@ OPENSEADRAGON CHANGELOG ======================= -2.1.1: (in progress) +2.2.2: (in progress) +* Fixed CORS bug in IE 10 (#967) +* Added support for commonjs (#984) +* Added an option to addTiledImage to change the crossOriginPolicy (#981) +* Fixed issue with tiles not appearing with wrapHorizontal/wrapVertical if you pan too far away from the origin (#987) + +2.2.1: + +* Fixed problems with zoom/pan constraints with certain extreme settings (#965) +* Fixed an issue causing the browser to crash on iOS (#966) + +2.2.0: + +* BREAKING CHANGE: Viewport.homeBounds, Viewport.contentSize, Viewport.contentAspectX and + 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) @@ -15,6 +37,29 @@ OPENSEADRAGON CHANGELOG * Added globalCompositeOperation option for tiledImage, to allow for different transfer modes (#814) * Added Viewer.addSimpleImage method for easily adding non-tiled images (#827) * DziTileSource now works properly with DZI files that have no extension (#835) +* Fixed content clipping with rotation (#463, #567 and #833) +* Fixed navigator not being rotated when viewport rotation is set in constructor (#840) +* Fixed: Viewer.setMouseNavEnabled wasn't affecting all of the viewer's trackers (#845) +* 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) +* Fixed an issue in viewer.addTiledImage with replace:true when viewer has navigator (#948) 2.1.0: diff --git a/package.json b/package.json index 7da958cc..19bd4d21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openseadragon", - "version": "2.1.0", + "version": "2.2.1", "description": "Provides a smooth, zoomable user interface for HTML/Javascript.", "keywords": ["image", "zoom", "pan", "openseadragon", "seadragon", "deepzoom", "dzi", "iiif", "osm", "tms"], "homepage": "http://openseadragon.github.io/", 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/imagetilesource.js b/src/imagetilesource.js index f4ef5fa9..3562bf50 100644 --- a/src/imagetilesource.js +++ b/src/imagetilesource.js @@ -166,14 +166,6 @@ return new $.Point(0, 0); } }, - /** - * @function - * @param {Number} level - * @param {OpenSeadragon.Point} point - */ - getTileAtPoint: function (level, point) { - return new $.Point(0, 0); - }, /** * Retrieves a tile url * @function diff --git a/src/legacytilesource.js b/src/legacytilesource.js index b661b967..04579034 100644 --- a/src/legacytilesource.js +++ b/src/legacytilesource.js @@ -169,16 +169,6 @@ $.extend( $.LegacyTileSource.prototype, $.TileSource.prototype, /** @lends OpenS } }, - /** - * @function - * @param {Number} level - * @param {OpenSeadragon.Point} point - */ - getTileAtPoint: function( level, point ) { - return new $.Point( 0, 0 ); - }, - - /** * This method is not implemented by this class other than to throw an Error * announcing you have to implement it. Because of the variety of tile 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 9fac3637..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 ){ @@ -199,11 +199,18 @@ $.Navigator = function( options ){ this.displayRegionContainer.appendChild(this.displayRegion); this.element.getElementsByTagName('div')[0].appendChild(this.displayRegionContainer); + function rotate(degrees) { + _setTransformRotate(_this.displayRegionContainer, degrees); + _setTransformRotate(_this.displayRegion, -degrees); + _this.viewport.setRotation(degrees); + } if (options.navigatorRotate) { + var degrees = options.viewer.viewport ? + options.viewer.viewport.getRotation() : + options.viewer.degrees || 0; + rotate(degrees); options.viewer.addHandler("rotate", function (args) { - _setTransformRotate(_this.displayRegionContainer, args.degrees); - _setTransformRotate(_this.displayRegion, -args.degrees); - _this.viewport.setRotation(args.degrees); + rotate(args.degrees); }); } @@ -299,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..de7399e8 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. @@ -258,6 +239,12 @@ * A zoom percentage ( where 1 is 100% ) of the highest resolution level. * When zoomed in beyond this value alternative compositing will be used to * smooth out the edges between tiles. This will have a performance impact. + * Can be set to Infinity to turn it off. + * Note: This setting is ignored on iOS devices due to a known bug (See {@link https://github.com/openseadragon/openseadragon/issues/952}) + * + * @property {Boolean} [iOSDevice=?] + * True if running on an iOS device, false otherwise. + * Used to disable certain features that behave differently on iOS devices. * * @property {Boolean} [autoResize=true] * Set to false to prevent polling for viewer size changes. Useful for providing custom resize behavior. @@ -373,16 +360,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 +392,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,24 +682,13 @@ * 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} */ -window.OpenSeadragon = window.OpenSeadragon || function( options ){ - +function OpenSeadragon( options ){ return new OpenSeadragon.Viewer( options ); - -}; - -if (typeof define === 'function' && define.amd) { - define(function () { - return (window.OpenSeadragon); - }); } - (function( $ ){ @@ -842,6 +822,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 @@ -983,6 +978,18 @@ if (typeof define === 'function' && define.amd) { return target; }; + var isIOSDevice = function () { + if (typeof navigator !== 'object') { + return false; + } + var userAgent = navigator.userAgent; + if (typeof userAgent !== 'string') { + return false; + } + return userAgent.indexOf('iPhone') !== -1 || + userAgent.indexOf('iPad') !== -1 || + userAgent.indexOf('iPod') !== -1; + }; $.extend( $, /** @lends OpenSeadragon */{ /** @@ -1033,6 +1040,7 @@ if (typeof define === 'function' && define.amd) { minZoomImageRatio: 0.9, //-> closer to 0 allows zoom out to infinity maxZoomPixelRatio: 1.1, //-> higher allows 'over zoom' into pixels smoothTileEdgesMinZoom: 1.1, //-> higher than maxZoomPixelRatio disables it + iOSDevice: isIOSDevice(), pixelsPerWheelLine: 40, autoResize: true, preserveImageSizeOnResize: false, // requires autoResize=true @@ -1065,6 +1073,7 @@ if (typeof define === 'function' && define.amd) { navigatorHeight: null, navigatorWidth: null, navigatorAutoResize: true, + navigatorAutoFade: true, navigatorRotate: true, // INITIAL ROTATION @@ -1322,6 +1331,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). @@ -2058,12 +2110,13 @@ if (typeof define === 'function' && define.amd) { } }; - if (withCredentials) { - request.withCredentials = true; - } - try { request.open( "GET", url, true ); + + if (withCredentials) { + request.withCredentials = true; + } + request.send( null ); } catch (e) { var msg = e.message; @@ -2533,185 +2586,21 @@ 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; +}(OpenSeadragon)); - 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 ); +// Universal Module Definition, supports CommonJS, AMD and simple script tag +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // expose as amd module + define([], factory); + } else if (typeof module === 'object' && module.exports) { + // expose as commonjs module + module.exports = factory(); + } else { + // expose as window.OpenSeadragon + root.OpenSeadragon = factory(); } - - /** - * @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 )); +}(this, function () { + return OpenSeadragon; +})); diff --git a/src/overlay.js b/src/overlay.js index 41f8b9be..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,115 +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(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 b9746c08..c273223b 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. @@ -65,6 +69,7 @@ * @param {Boolean} [options.alwaysBlend] - See {@link OpenSeadragon.Options}. * @param {Number} [options.minPixelRatio] - See {@link OpenSeadragon.Options}. * @param {Number} [options.smoothTileEdgesMinZoom] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.iOSDevice] - See {@link OpenSeadragon.Options}. * @param {Number} [options.opacity=1] - Opacity the tiled image should be drawn at. * @param {String} [options.compositeOperation] - How the image is composited onto other images; see compositeOperation in {@link OpenSeadragon.Options} for possible values. * @param {Boolean} [options.debugMode] - See {@link OpenSeadragon.Options}. @@ -122,6 +127,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 @@ -144,6 +154,7 @@ $.TiledImage = function( options ) { alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend, minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio, smoothTileEdgesMinZoom: $.DEFAULT_SETTINGS.smoothTileEdgesMinZoom, + iOSDevice: $.DEFAULT_SETTINGS.iOSDevice, debugMode: $.DEFAULT_SETTINGS.debugMode, crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy, placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, @@ -172,6 +183,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) { /** @@ -240,9 +255,11 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * Draws the TiledImage to its Drawer. */ draw: function() { - this._midDraw = true; - updateViewport( this ); - this._midDraw = false; + if (this.opacity !== 0) { + this._midDraw = true; + updateViewport(this); + this._midDraw = false; + } }, /** @@ -272,6 +289,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. */ @@ -541,6 +578,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. @@ -648,6 +746,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; } }); @@ -686,7 +789,6 @@ function updateViewport( tiledImage ) { Math.log( 2 ) )) ), - degrees = tiledImage.viewport.degrees, renderPixelRatioC, renderPixelRatioT, zeroRatioT, @@ -694,26 +796,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(); @@ -879,10 +978,14 @@ function updateLevel( tiledImage, haveDrawn, drawLevel, level, levelOpacity, lev resetCoverage( tiledImage.coverage, level ); - if ( !tiledImage.wrapHorizontal ) { + if ( tiledImage.wrapHorizontal ) { + tileTL.x -= 1; // left invisible column (othervise we will have empty space after scroll at left) + } else { tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 ); } - if ( !tiledImage.wrapVertical ) { + if ( tiledImage.wrapVertical ) { + tileTL.y -= 1; // top invisible row (othervise we will have empty space after scroll at top) + } else { tileBR.y = Math.min( tileBR.y, numberOfTiles.y - 1 ); } @@ -1333,24 +1436,25 @@ 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 && !tiledImage.iOSDevice) { // When zoomed in a lot (>100%) the tile edges are visible. // So we have to composite them at ~100% and scale them up together. + // Note: Disabled on iOS devices per default as it causes a native crash useSketch = true; sketchScale = tile.getScaleForEdgeSmoothing(); sketchTranslate = tile.getTranslationForEdgeSmoothing(sketchScale, @@ -1358,8 +1462,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 @@ -1405,7 +1518,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; @@ -1442,7 +1555,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..eb5aabc0 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; } @@ -345,11 +345,11 @@ $.TileSource.prototype = { * @param {OpenSeadragon.Point} point */ getTileAtPoint: function( level, point ) { - var pixel = point.times( this.dimensions.x ).times( this.getLevelScale(level) ), - tx = Math.floor( pixel.x / this.getTileWidth(level) ), - ty = Math.floor( pixel.y / this.getTileHeight(level) ); - - return new $.Point( tx, ty ); + var numTiles = this.getNumTiles( level ); + return new $.Point( + Math.floor( (point.x * numTiles.x) / 1 ), + Math.floor( (point.y * numTiles.y * this.dimensions.x) / this.dimensions.y ) + ); }, /** @@ -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 ca77de89..02556bcf 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"; @@ -330,7 +337,7 @@ $.Viewer = function( options ) { this.world.addHandler('metrics-change', function(event) { if (_this.viewport) { - _this.viewport.setHomeBounds(_this.world.getHomeBounds(), _this.world.getContentFactor()); + _this.viewport._setContentBounds(_this.world.getHomeBounds(), _this.world.getContentFactor()); } }); @@ -359,7 +366,7 @@ $.Viewer = function( options ) { margins: this.viewportMargins }); - this.viewport.setHomeBounds(this.world.getHomeBounds(), this.world.getContentFactor()); + this.viewport._setContentBounds(this.world.getHomeBounds(), this.world.getContentFactor()); // Create the image loader this.imageLoader = new $.ImageLoader({ @@ -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, @@ -779,6 +787,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, */ setMouseNavEnabled: function( enabled ){ this.innerTracker.setTracking( enabled ); + this.outerTracker.setTracking( enabled ); /** * Raised when mouse/touch navigation is enabled or disabled (see {@link OpenSeadragon.Viewer#setMouseNavEnabled}). * @@ -910,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 @@ -973,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; @@ -1203,11 +1220,17 @@ $.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. * @param {Number} [options.opacity] Opacity the tiled image should be drawn at by default. * @param {String} [options.compositeOperation] How the image is composited onto other images. + * @param {String} [options.crossOriginPolicy] The crossOriginPolicy for this specific image, + * overriding viewer.crossOriginPolicy. * @param {Function} [options.success] A function that gets called when the image is * successfully added. It's passed the event object which contains a single property: * "item", the resulting TiledImage. @@ -1243,6 +1266,9 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, if (options.compositeOperation === undefined) { options.compositeOperation = this.compositeOperation; } + if (options.crossOriginPolicy === undefined) { + options.crossOriginPolicy = options.tileSource.crossOriginPolicy !== undefined ? options.tileSource.crossOriginPolicy : this.crossOriginPolicy; + } var myQueueItem = { options: options @@ -1305,7 +1331,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, this._loadQueue.push(myQueueItem); - getTileSourceImplementation( this, options.tileSource, function( tileSource ) { + getTileSourceImplementation( this, options.tileSource, options, function( tileSource ) { myQueueItem.tileSource = tileSource; @@ -1338,6 +1364,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, @@ -1352,7 +1380,8 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, alwaysBlend: _this.alwaysBlend, minPixelRatio: _this.minPixelRatio, smoothTileEdgesMinZoom: _this.smoothTileEdgesMinZoom, - crossOriginPolicy: _this.crossOriginPolicy, + iOSDevice: _this.iOSDevice, + crossOriginPolicy: queueItem.options.crossOriginPolicy, debugMode: _this.debugMode }); @@ -1374,6 +1403,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, if (_this.navigator) { optionsClone = $.extend({}, queueItem.options, { + replace: false, // navigator already removed the layer, nothing to replace originalTiledImage: tiledImage, tileSource: queueItem.tileSource }); @@ -1779,10 +1809,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 @@ -1824,7 +1856,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', { @@ -1842,8 +1874,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. @@ -1869,7 +1901,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', { @@ -1944,6 +1976,27 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, return this; }, + /** + * Finds an overlay identified by the reference element or element id + * and returns it as an object, return null if not found. + * @method + * @param {Element|String} element - A reference to the element or an + * element id which represents the overlay content. + * @return {OpenSeadragon.Overlay} the matching overlay or null if none found. + */ + getOverlayById: function( element ) { + var i; + + element = $.getElement( element ); + i = getOverlayIndex( this.currentOverlays, element ); + + if (i>=0) { + return this.currentOverlays[i]; + } else { + return null; + } + }, + /** * Updates the sequence buttons. * @function OpenSeadragon.Viewer.prototype._updateSequenceButtons @@ -2062,7 +2115,7 @@ function _getSafeElemSize (oElement) { * @function * @private */ -function getTileSourceImplementation( viewer, tileSource, successCallback, +function getTileSourceImplementation( viewer, tileSource, imgOptions, successCallback, failCallback ) { var _this = viewer; @@ -2096,7 +2149,8 @@ function getTileSourceImplementation( viewer, tileSource, successCallback, //If its still a string it means it must be a url at this point tileSource = new $.TileSource({ url: tileSource, - crossOriginPolicy: viewer.crossOriginPolicy, + crossOriginPolicy: imgOptions.crossOriginPolicy !== undefined ? + imgOptions.crossOriginPolicy : viewer.crossOriginPolicy, ajaxWithCredentials: viewer.ajaxWithCredentials, useCanvas: viewer.useCanvas, success: function( event ) { @@ -2108,8 +2162,10 @@ function getTileSourceImplementation( viewer, tileSource, successCallback, } ); } else if ($.isPlainObject(tileSource) || tileSource.nodeType) { - if (!tileSource.crossOriginPolicy && viewer.crossOriginPolicy) { - tileSource.crossOriginPolicy = viewer.crossOriginPolicy; + if (tileSource.crossOriginPolicy === undefined && + (imgOptions.crossOriginPolicy !== undefined || viewer.crossOriginPolicy !== undefined)) { + tileSource.crossOriginPolicy = imgOptions.crossOriginPolicy !== undefined ? + imgOptions.crossOriginPolicy : viewer.crossOriginPolicy; } if (tileSource.ajaxWithCredentials === undefined) { tileSource.ajaxWithCredentials = viewer.ajaxWithCredentials; @@ -2169,37 +2225,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({ @@ -2207,7 +2254,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 }); } @@ -2520,22 +2570,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(); } @@ -2554,7 +2607,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, @@ -2846,7 +2899,10 @@ function onCanvasScroll( event ) { } } else { - return false; // We are swallowing this event + gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + if (gestureSettings && gestureSettings.scrollToZoom) { + return false; // We are swallowing this event + } } } @@ -2938,33 +2994,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; } } @@ -3049,27 +3098,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 a9b12eb7..2ac499d4 100644 --- a/src/viewport.js +++ b/src/viewport.js @@ -134,13 +134,9 @@ $.Viewport = function( options ) { this._oldCenterY = this.centerSpringY.current.value; this._oldZoom = this.zoomSpring.current.value; - if (this.contentSize) { - this.resetContentSize( this.contentSize ); - } else { - this.setHomeBounds(new $.Rect(0, 0, 1, 1), 1); - } + this._setContentBounds(new $.Rect(0, 0, 1, 1), 1); - this.goHome( true ); + this.goHome(true); this.update(); }; @@ -153,93 +149,112 @@ $.Viewport.prototype = { * @return {OpenSeadragon.Viewport} Chainable. * @fires OpenSeadragon.Viewer.event:reset-size */ - resetContentSize: function( contentSize ){ + resetContentSize: function(contentSize) { $.console.assert(contentSize, "[Viewport.resetContentSize] contentSize is required"); $.console.assert(contentSize instanceof $.Point, "[Viewport.resetContentSize] contentSize must be an OpenSeadragon.Point"); $.console.assert(contentSize.x > 0, "[Viewport.resetContentSize] contentSize.x must be greater than 0"); $.console.assert(contentSize.y > 0, "[Viewport.resetContentSize] contentSize.y must be greater than 0"); - this.setHomeBounds(new $.Rect(0, 0, 1, contentSize.y / contentSize.x), contentSize.x); + this._setContentBounds(new $.Rect(0, 0, 1, contentSize.y / contentSize.x), contentSize.x); return this; }, - /** - * Updates the viewport's home bounds and constraints. - * @function - * @param {OpenSeadragon.Rect} bounds - the new bounds in viewport coordinates - * @param {Number} contentFactor - how many content units per viewport unit - * @fires OpenSeadragon.Viewer.event:reset-size - */ + // deprecated setHomeBounds: function(bounds, contentFactor) { - $.console.assert(bounds, "[Viewport.setHomeBounds] bounds is required"); - $.console.assert(bounds instanceof $.Rect, "[Viewport.setHomeBounds] bounds must be an OpenSeadragon.Rect"); - $.console.assert(bounds.width > 0, "[Viewport.setHomeBounds] bounds.width must be greater than 0"); - $.console.assert(bounds.height > 0, "[Viewport.setHomeBounds] bounds.height must be greater than 0"); + $.console.error("[Viewport.setHomeBounds] this function is deprecated; The content bounds should not be set manually."); + this._setContentBounds(bounds, contentFactor); + }, - this.homeBounds = bounds.clone().rotate(this.degrees).getBoundingBox(); - this.contentSize = this.homeBounds.getSize().times(contentFactor); - this.contentAspectX = this.contentSize.x / this.contentSize.y; - this.contentAspectY = this.contentSize.y / this.contentSize.x; + // Set the viewport's content bounds + // @param {OpenSeadragon.Rect} bounds - the new bounds in viewport coordinates + // without rotation + // @param {Number} contentFactor - how many content units per viewport unit + // @fires OpenSeadragon.Viewer.event:reset-size + // @private + _setContentBounds: function(bounds, contentFactor) { + $.console.assert(bounds, "[Viewport._setContentBounds] bounds is required"); + $.console.assert(bounds instanceof $.Rect, "[Viewport._setContentBounds] bounds must be an OpenSeadragon.Rect"); + $.console.assert(bounds.width > 0, "[Viewport._setContentBounds] bounds.width must be greater than 0"); + $.console.assert(bounds.height > 0, "[Viewport._setContentBounds] bounds.height must be greater than 0"); + + this._contentBoundsNoRotate = bounds.clone(); + this._contentSizeNoRotate = this._contentBoundsNoRotate.getSize().times( + contentFactor); + + this._contentBounds = bounds.rotate(this.degrees).getBoundingBox(); + this._contentSize = this._contentBounds.getSize().times(contentFactor); + this._contentAspectRatio = this._contentSize.x / this._contentSize.y; if (this.viewer) { /** * Raised when the viewer's content size or home bounds are reset - * (see {@link OpenSeadragon.Viewport#resetContentSize}, - * {@link OpenSeadragon.Viewport#setHomeBounds}). + * (see {@link OpenSeadragon.Viewport#resetContentSize}). * * @event reset-size * @memberof OpenSeadragon.Viewer * @type {object} * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. * @property {OpenSeadragon.Point} contentSize - * @property {OpenSeadragon.Rect} homeBounds + * @property {OpenSeadragon.Rect} contentBounds - Content bounds. + * @property {OpenSeadragon.Rect} homeBounds - Content bounds. + * Deprecated use contentBounds instead. * @property {Number} contentFactor * @property {?Object} userData - Arbitrary subscriber-defined object. */ this.viewer.raiseEvent('reset-size', { - contentSize: this.contentSize.clone(), + contentSize: this._contentSizeNoRotate.clone(), contentFactor: contentFactor, - homeBounds: this.homeBounds.clone() + homeBounds: this._contentBoundsNoRotate.clone(), + contentBounds: this._contentBounds.clone() }); } }, /** + * Returns the home zoom in "viewport zoom" value. * @function + * @returns {Number} The home zoom in "viewport zoom". */ getHomeZoom: function() { - if( this.defaultZoomLevel ){ + if (this.defaultZoomLevel) { return this.defaultZoomLevel; - } else { - var aspectFactor = - this.contentAspectX / this.getAspectRatio(); - - var output; - if( this.homeFillsViewer ){ // fill the viewer and clip the image - output = ( aspectFactor >= 1) ? - aspectFactor : - 1; - } else { - output = ( aspectFactor >= 1 ) ? - 1 : - aspectFactor; - } - - return output / this.homeBounds.width; } + + var aspectFactor = this._contentAspectRatio / this.getAspectRatio(); + var output; + if (this.homeFillsViewer) { // fill the viewer and clip the image + output = aspectFactor >= 1 ? aspectFactor : 1; + } else { + output = aspectFactor >= 1 ? 1 : aspectFactor; + } + + return output / this._contentBounds.width; }, /** + * Returns the home bounds in viewport coordinates. * @function + * @returns {OpenSeadragon.Rect} The home bounds in vewport coordinates. */ getHomeBounds: function() { - var center = this.homeBounds.getCenter( ), - width = 1.0 / this.getHomeZoom( ), - height = width / this.getAspectRatio(); + 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(); 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 ); @@ -250,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}). * @@ -262,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); }, /** @@ -287,8 +302,8 @@ $.Viewport.prototype = { getMaxZoom: function() { var zoom = this.maxZoomLevel; if (!zoom) { - zoom = this.contentSize.x * this.maxZoomPixelRatio / this._containerInnerSize.x; - zoom /= this.homeBounds.width; + zoom = this._contentSize.x * this.maxZoomPixelRatio / this._containerInnerSize.x; + zoom /= this._contentBounds.width; } return Math.max( zoom, this.getHomeZoom() ); @@ -313,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() { @@ -322,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) { @@ -337,22 +352,37 @@ $.Viewport.prototype = { }, margins); this._updateContainerInnerSize(); - this.viewer.forceRedraw(); + if (this.viewer) { + this.viewer.forceRedraw(); + } }, /** + * Returns the bounds of the visible area in viewport coordinates. * @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. */ - 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 ); @@ -364,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; @@ -433,6 +474,13 @@ $.Viewport.prototype = { } }, + // private + _applyZoomConstraints: function(zoom) { + return Math.max( + Math.min(zoom, this.getMaxZoom()), + this.getMinZoom()); + }, + /** * @function * @private @@ -440,65 +488,50 @@ $.Viewport.prototype = { * @param {Boolean} immediately * @return {OpenSeadragon.Rect} constrained bounds. */ - _applyBoundaryConstraints: function( bounds, immediately ) { - var dx = 0, - dy = 0, - newBounds = new $.Rect( + _applyBoundaryConstraints: function(bounds, immediately) { + var newBounds = new $.Rect( bounds.x, bounds.y, bounds.width, - bounds.height - ); + bounds.height); - var horizontalThreshold = this.visibilityRatio * newBounds.width; - var verticalThreshold = this.visibilityRatio * newBounds.height; - - if ( this.wrapHorizontal ) { + if (this.wrapHorizontal) { //do nothing } else { - var thresholdLeft = newBounds.x + (newBounds.width - horizontalThreshold); - if (this.homeBounds.x > thresholdLeft) { - dx = this.homeBounds.x - thresholdLeft; - } + var horizontalThreshold = this.visibilityRatio * newBounds.width; + var boundsRight = newBounds.x + newBounds.width; + var contentRight = this._contentBoundsNoRotate.x + this._contentBoundsNoRotate.width; + var leftDx = this._contentBoundsNoRotate.x - boundsRight + horizontalThreshold; + var rightDx = contentRight - newBounds.x - horizontalThreshold; - var homeRight = this.homeBounds.x + this.homeBounds.width; - var thresholdRight = newBounds.x + horizontalThreshold; - if (homeRight < thresholdRight) { - var newDx = homeRight - thresholdRight; - if (dx) { - dx = (dx + newDx) / 2; - } else { - dx = newDx; - } + if (horizontalThreshold > this._contentBoundsNoRotate.width) { + newBounds.x += (leftDx + rightDx) / 2; + } else if (rightDx < 0) { + newBounds.x += rightDx; + } else if (leftDx > 0) { + newBounds.x += leftDx; } } - if ( this.wrapVertical ) { + if (this.wrapVertical) { //do nothing } else { - var thresholdTop = newBounds.y + (newBounds.height - verticalThreshold); - if (this.homeBounds.y > thresholdTop) { - dy = this.homeBounds.y - thresholdTop; - } + var verticalThreshold = this.visibilityRatio * newBounds.height; + var boundsBottom = newBounds.y + newBounds.height; + var contentBottom = this._contentBoundsNoRotate.y + this._contentBoundsNoRotate.height; + var topDy = this._contentBoundsNoRotate.y - boundsBottom + verticalThreshold; + var bottomDy = contentBottom - newBounds.y - verticalThreshold; - var homeBottom = this.homeBounds.y + this.homeBounds.height; - var thresholdBottom = newBounds.y + verticalThreshold; - if (homeBottom < thresholdBottom) { - var newDy = homeBottom - thresholdBottom; - if (dy) { - dy = (dy + newDy) / 2; - } else { - dy = newDy; - } + if (verticalThreshold > this._contentBoundsNoRotate.height) { + newBounds.y += (topDy + bottomDy) / 2; + } else if (bottomDy < 0) { + newBounds.y += bottomDy; + } else if (topDy > 0) { + newBounds.y += topDy; } } - if ( dx || dy ) { - newBounds.x += dx; - newBounds.y += dy; - } - - if( this.viewer ){ + if (this.viewer) { /** * Raised when the viewport constraints are applied (see {@link OpenSeadragon.Viewport#applyConstraints}). * @@ -518,40 +551,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); }, /** @@ -561,50 +598,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(), - center = bounds.getCenter(), - newBounds = new $.Rect( - bounds.x, - bounds.y, - bounds.width, - bounds.height - ), - oldBounds, - oldZoom, - newZoom, - referencePoint, - newBoundsAspectRatio, - newConstrainedZoom; + var aspect = this.getAspectRatio(); + var center = bounds.getCenter(); - if ( newBounds.getAspectRatio() >= aspect ) { - newBounds.height = bounds.width / aspect; - newBounds.y = center.y - newBounds.height / 2; + // Compute width and height of bounding box. + var newBounds = new $.Rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + bounds.degrees + this.getRotation()) + .getBoundingBox(); + + 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; } - if ( constraints ) { - newBoundsAspectRatio = newBounds.getAspectRatio(); - } + // 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; - this.panTo( this.getCenter( true ), true ); - this.zoomTo( this.getZoom( true ), null, true ); - - oldBounds = this.getBounds(); - oldZoom = this.getZoom(); - newZoom = 1.0 / newBounds.width; - - if ( constraints ) { - newConstrainedZoom = Math.max( - Math.min(newZoom, this.getMaxZoom() ), - this.getMinZoom() - ); + if (constraints) { + var newBoundsAspectRatio = newBounds.getAspectRatio(); + var newConstrainedZoom = this._applyZoomConstraints(newZoom); if (newZoom !== newConstrainedZoom) { newZoom = newConstrainedZoom; @@ -614,58 +638,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); } - 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 - } ); + }); }, /** @@ -673,11 +709,13 @@ $.Viewport.prototype = { * @param {Boolean} immediately * @return {OpenSeadragon.Viewport} Chainable. */ - fitVertically: function( immediately ) { - var box = new $.Rect(this.homeBounds.x + (this.homeBounds.width / 2), this.homeBounds.y, - 0, this.homeBounds.height); - - return this.fitBounds( box, immediately ); + fitVertically: function(immediately) { + var box = new $.Rect( + this._contentBounds.x + (this._contentBounds.width / 2), + this._contentBounds.y, + 0, + this._contentBounds.height); + return this.fitBounds(box, immediately); }, /** @@ -685,11 +723,13 @@ $.Viewport.prototype = { * @param {Boolean} immediately * @return {OpenSeadragon.Viewport} Chainable. */ - fitHorizontally: function( immediately ) { - var box = new $.Rect(this.homeBounds.x, this.homeBounds.y + (this.homeBounds.height / 2), - this.homeBounds.width, 0); - - return this.fitBounds( box, immediately ); + fitHorizontally: function(immediately) { + var box = new $.Rect( + this._contentBounds.x, + this._contentBounds.y + (this._contentBounds.height / 2), + this._contentBounds.width, + 0); + return this.fitBounds(box, immediately); }, @@ -756,7 +796,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 */ @@ -812,7 +857,7 @@ $.Viewport.prototype = { degrees += 360; } this.degrees = degrees; - this.setHomeBounds( + this._setContentBounds( this.viewer.world.getHomeBounds(), this.viewer.world.getContentFactor()); this.viewer.forceRedraw(); @@ -846,7 +891,7 @@ $.Viewport.prototype = { * @fires OpenSeadragon.Viewer.event:resize */ resize: function( newContainerSize, maintain ) { - var oldBounds = this.getBounds(), + var oldBounds = this.getBoundsNoRotate(), newBounds = oldBounds, widthDeltaFactor; @@ -892,37 +937,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; @@ -998,7 +1043,8 @@ $.Viewport.prototype = { * @returns {OpenSeadragon.Point} */ pixelFromPointNoRotate: function(point, current) { - return this._pixelFromPointNoRotate(point, this.getBounds(current)); + return this._pixelFromPointNoRotate( + point, this.getBoundsNoRotate(current)); }, /** @@ -1009,7 +1055,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 @@ -1040,7 +1086,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( @@ -1066,9 +1112,10 @@ $.Viewport.prototype = { // private _viewportToImageDelta: function( viewerX, viewerY ) { - var scale = this.homeBounds.width; - return new $.Point(viewerX * (this.contentSize.x / scale), - viewerY * ((this.contentSize.y * this.contentAspectX) / scale)); + var scale = this._contentBoundsNoRotate.width; + return new $.Point( + viewerX * this._contentSizeNoRotate.x / scale, + viewerY * this._contentSizeNoRotate.x / scale); }, /** @@ -1088,18 +1135,31 @@ $.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(viewerX - this.homeBounds.x, viewerY - this.homeBounds.y); + return this._viewportToImageDelta( + viewerX - this._contentBoundsNoRotate.x, + viewerY - this._contentBoundsNoRotate.y); }, // private _imageToViewportDelta: function( imageX, imageY ) { - var scale = this.homeBounds.width; - return new $.Point((imageX / this.contentSize.x) * scale, - (imageY / this.contentSize.y / this.contentAspectX) * scale); + var scale = this._contentBoundsNoRotate.width; + return new $.Point( + imageX / this._contentSizeNoRotate.x * scale, + imageY / this._contentSizeNoRotate.x * scale); }, /** @@ -1119,13 +1179,23 @@ $.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); - point.x += this.homeBounds.x; - point.y += this.homeBounds.y; + point.x += this._contentBoundsNoRotate.x; + point.y += this._contentBoundsNoRotate.y; return point; }, @@ -1150,6 +1220,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 +1268,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 +1324,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 +1338,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 +1366,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 +1412,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,14 +1432,24 @@ $.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.contentSize.x; + var imageWidth = this._contentSizeNoRotate.x; var containerWidth = this._containerInnerSize.x; - var scale = this.homeBounds.width; + var scale = this._contentBoundsNoRotate.width; var viewportToImageZoomRatio = (containerWidth / imageWidth) * scale; return viewportZoom * viewportToImageZoomRatio; }, @@ -1320,14 +1466,24 @@ $.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.contentSize.x; + var imageWidth = this._contentSizeNoRotate.x; var containerWidth = this._containerInnerSize.x; - var scale = this.homeBounds.width; + var scale = this._contentBoundsNoRotate.width; var viewportToImageZoomRatio = (imageWidth / containerWidth) / scale; return imageZoom * viewportToImageZoomRatio; } 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 @@ +