diff --git a/.gitignore b/.gitignore index aaaabf8c..282b4dc1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ build/ sftp-config.json coverage/ temp/ -.idea \ No newline at end of file +.idea +/nbproject/private/ +.directory 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/.travis.yml b/.travis.yml index 578f402c..e6921125 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: node_js +sudo: false node_js: - - 0.10 -before_script: + - "stable" +before_install: - npm install -g grunt-cli diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..b25bfdbf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,77 @@ +### Contributing + +OpenSeadragon is truly a community project; we welcome your involvement! + +When contributing, please attempt to match the code style already in the codebase. +However, we are in the process of changing our code style (see issue [#456](https://github.com/openseadragon/openseadragon/issues/456)), so avoid spaces inside parentheses and square brackets. Note that we use four spaces per indentation stop. For easier setup you can also install [EditorConfig](http://editorconfig.org/) if your IDE is supported. For more thoughts on code style, see [idiomatic.js](https://github.com/rwldrn/idiomatic.js/). + +When fixing bugs and adding features, when appropriate please also: + +* Update related doc comments (we use [JSDoc 3](http://usejsdoc.org/)) +* Add/update related unit tests + +If you're new to the project, check out our [good first bug](https://github.com/openseadragon/openseadragon/issues?labels=good+first+bug&page=1&state=open) issues for some places to dip your toe in the water. + +If you're new to open source in general, check out [GitHub's open source intro guide](https://guides.github.com/overviews/os-contributing/). + +### First Time Setup + +All command-line operations for building and testing OpenSeadragon are scripted using [Grunt](http://gruntjs.com/) which is based on [Node.js](http://nodejs.org/). To get set up: + +1. Install Node, if you haven't already (available at the link above) +1. Install the Grunt command line runner (if you haven't already); on the command line, run `npm install -g grunt-cli` +1. Clone the openseadragon repository +1. On the command line, go in to the openseadragon folder +1. Run `npm install` + +You're set, all development dependencies should have been installed and the project built... +continue reading for build and test instructions. + +### Building from Source + +To build, just run (on the command line, in the openseadragon folder): + + grunt + +If you want Grunt to watch your source files and rebuild every time you change one, use: + + grunt watch + +To have it watch your source files and also run a server for you to test in: + + grunt dev + +The built files appear in the `build` folder. + +If you want to build tar and zip files for distribution (they will also appear in the `build` folder), use: + + grunt package + +Note that the `build` folder is masked with .gitignore; it's just for your local use, and won't be checked in to the repository. + +You can also publish the built version to the site-build repository. This assumes you have cloned it next to this repository. The command is: + + grunt publish + +... which will delete the existing openseadragon folder, along with the .zip and .tar.gz files, out of the site-build folder and replace them with newly built ones from the source in this repository; you'll then need to commit the changes to site-build. + +### Testing + +Our tests are based on [QUnit](http://qunitjs.com/) and [PhantomJS](http://phantomjs.org/); they're both installed when you run `npm install`. To run on the command line: + + grunt test + +If you wish to work interactively with the tests or test your changes: + + grunt connect watch + +and open `http://localhost:8000/test/test.html` in your browser. + +Another good page, if you want to interactively test out your changes, is `http://localhost:8000/test/demo/basic.html`. + +You can also get a report of the tests' code coverage: + + grunt coverage + +The report shows up at `coverage/html/index.html` viewable in a browser. + diff --git a/Gruntfile.js b/Gruntfile.js index 06511e31..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", @@ -39,6 +40,7 @@ module.exports = function(grunt) { "src/osmtilesource.js", "src/tmstilesource.js", "src/legacytilesource.js", + "src/imagetilesource.js", "src/tilesourcecollection.js", "src/button.js", "src/buttongroup.js", @@ -56,6 +58,12 @@ module.exports = function(grunt) { "src/world.js" ]; + var banner = "//! <%= pkg.name %> <%= pkg.version %>\n" + + "//! Built on <%= grunt.template.today('yyyy-mm-dd') %>\n" + + "//! Git commit: <%= gitInfo %>\n" + + "//! http://openseadragon.github.io\n" + + "//! License: http://openseadragon.github.io/license/\n\n"; + // ---------- grunt.event.once('git-describe', function (rev) { grunt.config.set('gitInfo', rev); @@ -84,12 +92,9 @@ module.exports = function(grunt) { }, concat: { options: { - banner: "//! <%= pkg.name %> <%= pkg.version %>\n" + - "//! Built on <%= grunt.template.today('yyyy-mm-dd') %>\n" + - "//! Git commit: <%= gitInfo %>\n" + - "//! http://openseadragon.github.io\n" + - "//! License: http://openseadragon.github.io/license/\n\n", - process: true + banner: banner, + process: true, + sourceMap: true }, dist: { src: [ "" ].concat(sources), @@ -110,12 +115,17 @@ module.exports = function(grunt) { }, uglify: { options: { - preserveComments: "some", + preserveComments: false, + banner: banner, + compress: { + sequences: false, + join_vars: false + }, sourceMap: true, sourceMapName: 'build/openseadragon/openseadragon.min.js.map' }, openseadragon: { - src: [ distribution ], + src: sources, dest: minified } }, diff --git a/README.md b/README.md index 2462fdb5..92b6fb86 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # 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) +[![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. -See it in action and get started using it at http://openseadragon.github.io/. +See it in action and get started using it at [http://openseadragon.github.io/](http://openseadragon.github.io/). ## Stable Builds @@ -11,85 +11,8 @@ See the [GitHub releases page](https://github.com/openseadragon/openseadragon/re ## Development -If you want to use OpenSeadragon in your own projects, you can find the latest stable build, API documentation, and example code at http://openseadragon.github.io/. If you want to modify OpenSeadragon and/or contribute to its development, read on. - -### First Time Setup - -All command-line operations for building and testing OpenSeadragon are scripted using [Grunt](http://gruntjs.com/) which is based on [Node.js](http://nodejs.org/). To get set up: - -1. Install Node, if you haven't already (available at the link above) -1. Install the Grunt command line runner (if you haven't already); on the command line, run `npm install -g grunt-cli` -1. Clone the openseadragon repository -1. On the command line, go in to the openseadragon folder -1. Run `npm install` - -You're set... continue reading for build and test instructions. - -### Building from Source - -To build, just run (on the command line, in the openseadragon folder): - - grunt - -If you want Grunt to watch your source files and rebuild every time you change one, use: - - grunt watch - -To have it watch your source files and also run a server for you to test in: - - grunt dev - -The built files appear in the `build` folder. - -If you want to build tar and zip files for distribution (they will also appear in the `build` folder), use: - - grunt package - -Note that the `build` folder is masked with .gitignore; it's just for your local use, and won't be checked in to the repository. - -You can also publish the built version to the site-build repository. This assumes you have cloned it next to this repository. The command is: - - grunt publish - -... which will delete the existing openseadragon folder, along with the .zip and .tar.gz files, out of the site-build folder and replace them with newly built ones from the source in this repository; you'll then need to commit the changes to site-build. - -### Testing - -Our tests are based on [QUnit](http://qunitjs.com/) and [PhantomJS](http://phantomjs.org/); they're both installed when you run `npm install`. At the moment we don't have much in the way of tests, but we're working to fix that. To run on the command line: - - grunt test - -If you wish to work interactively with the tests or test your changes: - - grunt connect watch - -and open `http://localhost:8000/test/test.html` in your browser. - -Another good page, if you want to interactively test out your changes, is `http://localhost:8000/test/demo/basic.html`. - -You can also get a report of the tests' code coverage: - - grunt coverage - -The report shows up at `coverage/html/index.html` viewable in a browser. - -### Contributing - -OpenSeadragon is truly a community project; we welcome your involvement! - -When contributing, please attempt to match the code style already in the codebase. Note that we use four spaces per indentation stop. For easier setup you can also install [EditorConfig](http://editorconfig.org/) if your IDE is supported. For more thoughts on code style, see [idiomatic.js](https://github.com/rwldrn/idiomatic.js/). - -When fixing bugs and adding features, when appropriate please also: - -* Update related doc comments (we use [JSDoc 3](http://usejsdoc.org/)) -* Add/update related unit tests - -If you're new to the project, check out our [good first bug](https://github.com/openseadragon/openseadragon/issues?labels=good+first+bug&page=1&state=open) issues for some places to dip your toe in the water. - -If you're new to open source in general, check out [GitHub's open source intro guide](https://guides.github.com/overviews/os-contributing/). +If you want to use OpenSeadragon in your own projects, you can find the latest stable build, API documentation, and example code at [http://openseadragon.github.io/](http://openseadragon.github.io/). If you want to modify OpenSeadragon and/or contribute to its development, read the [contributing guide](https://github.com/openseadragon/openseadragon/blob/master/CONTRIBUTING.md) for instructions. ## License -OpenSeadragon is released under the New BSD license. For details, see the file LICENSE.txt. - -[![Build Status](https://secure.travis-ci.org/openseadragon/openseadragon.png?branch=master)](http://travis-ci.org/openseadragon/openseadragon) +OpenSeadragon is released under the New BSD license. For details, see the [LICENSE.txt file](https://github.com/openseadragon/openseadragon/blob/master/LICENSE.txt). diff --git a/changelog.txt b/changelog.txt index 0fb26a83..8a661e31 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,36 +1,107 @@ OPENSEADRAGON CHANGELOG ======================= -2.1.0: (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) +* The Viewer's tileSources option is now smarter about detecting JSON vs XML vs URL (#999) +* The navigationControlAnchor option now works for custom toolbar as well (#1004) +* Added getFullyLoaded method and "fully-loaded-change" event to TiledImage to know when tiles are fully loaded (#837) + +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) +* Drag outside of iframe now works better, as long as both pages are on the same domain (#790) +* Coordinate conversion now takes rotation into account (#796) +* Support tile-less IIIF as per LegacyTileSource (#816) +* You can now give an empty string to the tabIndex option (#805) +* Fixed issue with rotation and clicking in the navigator (#807) +* Broadened the check for mime type in LegacyTileSource URLs to allow query strings (#819) +* 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: + * BREAKING CHANGE: the tile does not hold a reference to its image anymore. Only the tile cache keep a reference to images. * BREAKING CHANGE: TileSource.tileSize no longer exists; use TileSource.getTileWidth() and TileSource.getTileHeight() instead. * DEPRECATION: let ImageRecord.getRenderedContext create the rendered context instead of using ImageRecord.setRenderedContext * DEPRECATION: TileSource.getTileSize() is deprecated. Use TileSource.getTileWidth() and TileSource.getTileHeight() instead. -* Added "tile-loaded" event on the viewer allowing to modify a tile before it is marked ready to be drawn (#659) -* Added "tile-unloaded" event on the viewer allowing to free up memory one has allocated on a tile (#659) -* Fixed flickering tiles with useCanvas=false when no cache is used (#661) -* Added additional coordinates conversion methods to TiledImage (#662) -* 'display: none' no longer gets reset on overlays during draw (#668) -* Added `preserveImageSizeOnResize` option (#666) -* Better error reporting for tile load failures (#679) -* Added collectionColumns as a configuration parameter (#680) +* Changed resize behaviour to prevent "snapping" to world bounds when constraints allow more space (#711) * Added support for non-square tiles (#673) * TileSource.Options objects can now optionally provide tileWidth/tileHeight instead of tileSize for non-square tile support. * IIIFTileSources will now respect non-square tiles if available. +* Added new tile source for simple images: ImageTileSource (#760) +* Optimized adding large numbers of items to the world with collectionMode (#735) +* Registers as an AMD module where possible (#719) +* Added "tile-loaded" event on the viewer allowing to modify a tile before it is marked ready to be drawn (#659) +* Added "tile-unloaded" event on the viewer allowing to free up memory one has allocated on a tile (#659) +* Added 'tile-load-failed' event (#725) +* Added additional coordinates conversion methods to TiledImage (#662) +* Added `preserveImageSizeOnResize` option (#666) +* Added collectionColumns as a configuration parameter (#680) +* Added option in addTiledImage to replace tiledImage at index (#706) +* Added autoRefigureSizes flag to World for optimizing mass rearrangements (#715) +* You can now change viewport margins after the viewer is created (#721) +* Added a patch to help slow down the scroll devices that fire too fast (#754) +* Fixed flickering tiles with useCanvas=false when no cache is used (#661) +* 'display: none' no longer gets reset on overlays during draw (#668) +* Better error reporting for tile load failures (#679) * Added XDomainRequest as fallback method for ajax requests if XMLHttpRequest fails (for IE < 10) (#693) * Now avoiding using eval when JSON.parse is available (#696) * Rotation now works properly on retina display (#708) -* Added option in addTiledImage to replace tiledImage at index (#706) -* Changed resize behaviour to prevent "snapping" to world bounds when constraints allow more space (#711) -* Registers as an AMD module where possible (#719) -* Added autoRefigureSizes flag to World for optimizing mass rearrangements (#715) -* Added 'tile-load-failed' event (#725) * Fixed issue with tiledImages loading tiles at every level instead of just the best level (#728) * Fixed placeholderFillStyle flicker (#727) * Fix for Chrome (v45) issue that key is sometimes undefined outside of the for-in loop (#730) * World.removeAll now cancels any in-flight image loads; same for Viewer.open and Viewer.close (#734) -* Optimized adding large numbers of items to the world with collectionMode (#735) * Fixed overlays position (use rounding instead of flooring and ceiling) (#741) +* Fixed issue with including overlays in your tileSources array when creating/opening in the viewer (#745) +* Fixed issue in iOS devices that would cause all touch events to fail after a Multitasking Gesture was triggered (#744) +* Fixed an issue with TiledImage setPosition/setWidth/setHeight not reliably triggering a redraw (#720) +* Fixed zooming in with plus key on a Swedish keyboard (#763) 2.0.0: diff --git a/nbproject/project.properties b/nbproject/project.properties new file mode 100644 index 00000000..c57a3e0d --- /dev/null +++ b/nbproject/project.properties @@ -0,0 +1,20 @@ +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=4 +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=4 +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=8 +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=80 +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project +auxiliary.org-netbeans-modules-editor-indent.text.javascript.CodeStyle.project.continuationIndentSize=4 +auxiliary.org-netbeans-modules-editor-indent.text.javascript.CodeStyle.project.indent-shift-width=4 +auxiliary.org-netbeans-modules-editor-indent.text.javascript.CodeStyle.project.spaceBeforeAnonMethodDeclParen=false +auxiliary.org-netbeans-modules-editor-indent.text.x-json.CodeStyle.project.indent-shift-width=2 +auxiliary.org-netbeans-modules-editor-indent.text.x-json.CodeStyle.project.spaces-per-tab=2 +auxiliary.org-netbeans-modules-web-clientproject-api.js_2e_libs_2e_folder=src +browser.autorefresh.Chromium.INTEGRATED=true +browser.highlightselection.Chromium.INTEGRATED=true +file.reference.openseadragon-openseadragon=. +files.encoding=UTF-8 +site.root.folder=${file.reference.openseadragon-openseadragon} +start.file=test/demo/basic.html +web.context.root=/ diff --git a/nbproject/project.xml b/nbproject/project.xml new file mode 100644 index 00000000..53613f5c --- /dev/null +++ b/nbproject/project.xml @@ -0,0 +1,9 @@ + + + org.netbeans.modules.web.clientproject + + + openseadragon + + + diff --git a/package.json b/package.json index d2081b58..19bd4d21 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,35 @@ { - "name": "OpenSeadragon", - "version": "2.0.0", + "name": "openseadragon", + "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/", + "bugs": { + "url": "https://github.com/openseadragon/openseadragon/issues" + }, + "license": "BSD-3-Clause", + "files": ["build/openseadragon/"], + "main": "build/openseadragon/openseadragon.js", + "repository": { + "type" : "git", + "url" : "https://github.com/openseadragon/openseadragon.git" + }, "devDependencies": { "grunt": "^0.4.5", - "grunt-contrib-clean": "^0.5.0", - "grunt-contrib-compress": "^0.9.1", - "grunt-contrib-concat": "^0.4.0", - "grunt-contrib-connect": "^0.7.1", - "grunt-contrib-jshint": "^0.10.0", - "grunt-contrib-uglify": "^0.4.0", + "grunt-contrib-clean": "^0.7.0", + "grunt-contrib-compress": "^0.13.0", + "grunt-contrib-concat": "^0.5.1", + "grunt-contrib-connect": "^0.11.2", + "grunt-contrib-jshint": "^0.11.0", + "grunt-contrib-uglify": "^0.11.0", "grunt-contrib-watch": "^0.6.1", "grunt-git-describe": "^2.3.2", - "grunt-qunit-istanbul": "^0.5.0", - "grunt-text-replace": "^0.3.11", - "qunitjs": "^1.18.0" + "grunt-qunit-istanbul": "^0.6.0", + "grunt-text-replace": "^0.4.0", + "qunitjs": "^1.20.0" }, "scripts": { - "test": "grunt test" + "test": "grunt test", + "prepublish": "grunt build" } } diff --git a/src/buttongroup.js b/src/buttongroup.js index 77d61371..daee211c 100644 --- a/src/buttongroup.js +++ b/src/buttongroup.js @@ -108,7 +108,8 @@ $.ButtonGroup = function( options ) { }); }; -$.ButtonGroup.prototype = /** @lends OpenSeadragon.ButtonGroup.prototype */{ +/** @lends OpenSeadragon.ButtonGroup.prototype */ +$.ButtonGroup.prototype = { /** * TODO: Figure out why this is used on the public API and if a more useful diff --git a/src/control.js b/src/control.js index f563ac58..c1ba2186 100644 --- a/src/control.js +++ b/src/control.js @@ -152,7 +152,8 @@ $.Control = function ( element, options, container ) { } }; -$.Control.prototype = /** @lends OpenSeadragon.Control.prototype */{ +/** @lends OpenSeadragon.Control.prototype */ +$.Control.prototype = { /** * Removes the control from the container. diff --git a/src/controldock.js b/src/controldock.js index e7753743..2ee2d0f1 100644 --- a/src/controldock.js +++ b/src/controldock.js @@ -88,7 +88,8 @@ this.container.appendChild( this.controls.bottomleft ); }; - $.ControlDock.prototype = /** @lends OpenSeadragon.ControlDock.prototype */{ + /** @lends OpenSeadragon.ControlDock.prototype */ + $.ControlDock.prototype = { /** * @function diff --git a/src/drawer.js b/src/drawer.js index 9b74b981..661663d1 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -132,7 +132,8 @@ $.Drawer = function( options ) { this.container.appendChild( this.canvas ); }; -$.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ +/** @lends OpenSeadragon.Drawer.prototype */ +$.Drawer.prototype = { // deprecated addOverlay: function( element, location, placement, onDraw ) { $.console.error("drawer.addOverlay is deprecated. Use viewer.addOverlay instead."); @@ -249,31 +250,37 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ this.canvas.width = viewportSize.x; this.canvas.height = viewportSize.y; if ( this.sketchCanvas !== null ) { - this.sketchCanvas.width = this.canvas.width; - this.sketchCanvas.height = this.canvas.height; + var sketchCanvasSize = this._calculateSketchCanvasSize(); + this.sketchCanvas.width = sketchCanvasSize.x; + this.sketchCanvas.height = sketchCanvasSize.y; } } this._clear(); } }, - _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); + } }, /** - * Translates from OpenSeadragon viewer rectangle to drawer rectangle. + * Scale from OpenSeadragon viewer rectangle to drawer rectangle + * (ignoring rotation) * @param {OpenSeadragon.Rect} rectangle - The rectangle in viewport coordinate system. * @return {OpenSeadragon.Rect} Rectangle in drawer coordinate system. */ viewportToDrawerRectangle: function(rectangle) { - var topLeft = this.viewport.pixelFromPoint(rectangle.getTopLeft(), true); - var size = this.viewport.deltaPixelsFromPoints(rectangle.getSize(), true); + var topLeft = this.viewport.pixelFromPointNoRotate(rectangle.getTopLeft(), true); + var size = this.viewport.deltaPixelsFromPointsNoRotate(rectangle.getSize(), true); return new $.Rect( topLeft.x * $.pixelDensityRatio, @@ -290,22 +297,17 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ * drawingHandler({context, tile, rendered}) * @param {Boolean} useSketch - Whether to use the sketch canvas or not. * where rendered is the context with the pre-drawn image. + * @param {Float} [scale=1] - Apply a scale to tile position and size. Defaults to 1. + * @param {OpenSeadragon.Point} [translate] A translation vector to offset tile position */ - drawTile: function( tile, drawingHandler, useSketch ) { + drawTile: function(tile, drawingHandler, useSketch, scale, translate) { $.console.assert(tile, '[Drawer.drawTile] tile is required'); $.console.assert(drawingHandler, '[Drawer.drawTile] drawingHandler is required'); - if ( this.useCanvas ) { - var context = this._getContext( useSketch ); - // TODO do this in a more performant way - // specifically, don't save,rotate,restore every time we draw a tile - if( this.viewport.degrees !== 0 ) { - this._offsetForRotation( tile, this.viewport.degrees, useSketch ); - tile.drawCanvas( context, drawingHandler ); - this._restoreRotationChanges( tile, useSketch ); - } else { - tile.drawCanvas( context, drawingHandler ); - } + if (this.useCanvas) { + var context = this._getContext(useSketch); + scale = scale || 1; + tile.drawCanvas(context, drawingHandler, scale, translate); } else { tile.drawHTML( this.canvas ); } @@ -316,9 +318,23 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ if ( useSketch ) { if (this.sketchCanvas === null) { this.sketchCanvas = document.createElement( "canvas" ); - this.sketchCanvas.width = this.canvas.width; - this.sketchCanvas.height = this.canvas.height; + var sketchCanvasSize = this._calculateSketchCanvasSize(); + this.sketchCanvas.width = sketchCanvasSize.x; + this.sketchCanvas.height = sketchCanvasSize.y; this.sketchContext = this.sketchCanvas.getContext( "2d" ); + + // If the viewport is not currently rotated, the sketchCanvas + // will have the same size as the main canvas. However, if + // the viewport get rotated later on, we will need to resize it. + if (this.viewport.getRotation() === 0) { + var self = this; + this.viewer.addHandler('rotate', function resizeSketchCanvas() { + self.viewer.removeHandler('rotate', resizeSketchCanvas); + var sketchCanvasSize = self._calculateSketchCanvasSize(); + self.sketchCanvas.width = sketchCanvasSize.x; + self.sketchCanvas.height = sketchCanvasSize.y; + }); + } } context = this.sketchContext; } @@ -370,17 +386,80 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ /** * Blends the sketch canvas in the main canvas. - * @param {Float} opacity The opacity of the blending. - * @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) { + 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; } + opacity = options.opacity; + compositeOperation = options.compositeOperation; + var bounds = options.bounds; this.context.save(); this.context.globalAlpha = opacity; - this.context.drawImage(this.sketchCanvas, 0, 0); + if (compositeOperation) { + this.context.globalCompositeOperation = compositeOperation; + } + 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(); }, @@ -398,7 +477,7 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ context.fillStyle = this.debugGridColor; if ( this.viewport.degrees !== 0 ) { - this._offsetForRotation( tile, this.viewport.degrees ); + this._offsetForRotation(this.viewport.degrees); } context.strokeRect( @@ -460,7 +539,7 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ ); if ( this.viewport.degrees !== 0 ) { - this._restoreRotationChanges( tile ); + this._restoreRotationChanges(); } context.restore(); }, @@ -485,22 +564,32 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ } }, - // private - _offsetForRotation: function( tile, degrees, useSketch ){ - var cx = this.canvas.width / 2, - cy = this.canvas.height / 2; + /** + * Get the canvas size + * @param {Boolean} sketch If set to true return the size of the sketch canvas + * @returns {OpenSeadragon.Point} The size of the canvas + */ + getCanvasSize: function(sketch) { + var canvas = this._getContext(sketch).canvas; + return new $.Point(canvas.width, canvas.height); + }, - var context = this._getContext( useSketch ); + // private + _offsetForRotation: function(degrees, useSketch) { + var cx = this.canvas.width / 2; + var cy = this.canvas.height / 2; + + var context = this._getContext(useSketch); context.save(); context.translate(cx, cy); - context.rotate( Math.PI / 180 * degrees); + context.rotate(Math.PI / 180 * degrees); context.translate(-cx, -cy); }, // private - _restoreRotationChanges: function( tile, useSketch ){ - var context = this._getContext( useSketch ); + _restoreRotationChanges: function(useSketch) { + var context = this._getContext(useSketch); context.restore(); }, @@ -512,6 +601,23 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ x: viewportSize.x * pixelDensityRatio, y: viewportSize.y * pixelDensityRatio }; + }, + + // private + _calculateSketchCanvasSize: function() { + var canvasSize = this._calculateCanvasSize(); + if (this.viewport.getRotation() === 0) { + return canvasSize; + } + // If the viewport is rotated, we need a larger sketch canvas in order + // to support edge smoothing. + var sketchCanvasSize = Math.ceil(Math.sqrt( + canvasSize.x * canvasSize.x + + canvasSize.y * canvasSize.y)); + return { + x: sketchCanvasSize, + y: sketchCanvasSize + }; } }; diff --git a/src/dzitilesource.js b/src/dzitilesource.js index 99dd92d1..817f438e 100644 --- a/src/dzitilesource.js +++ b/src/dzitilesource.js @@ -139,7 +139,8 @@ $.extend( $.DziTileSource.prototype, $.TileSource.prototype, /** @lends OpenSead } if (url && !options.tilesUrl) { - options.tilesUrl = url.replace(/([^\/]+)\.(dzi|xml|js)(\?.*|$)/, '$1_files/'); + options.tilesUrl = url.replace( + /([^\/]+?)(\.(dzi|xml|js))?\/?(\?.*)?$/, '$1_files/'); if (url.search(/\.(dzi|xml|js)\?/) != -1) { options.queryParams = url.match(/\?.*/); @@ -300,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 a9578ef0..e3957d7f 100644 --- a/src/eventsource.js +++ b/src/eventsource.js @@ -53,9 +53,34 @@ $.EventSource = function() { this.events = {}; }; -$.EventSource.prototype = /** @lends OpenSeadragon.EventSource.prototype */{ +/** @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/iiiftilesource.js b/src/iiiftilesource.js index bf3da020..4584f1cd 100644 --- a/src/iiiftilesource.js +++ b/src/iiiftilesource.js @@ -36,8 +36,8 @@ /** * @class IIIFTileSource - * @classdesc A client implementation of the International Image Interoperability - * Format: Image API 1.0 - 2.0 + * @classdesc A client implementation of the International Image Interoperability Framework + * Format: Image API 1.0 - 2.1 * * @memberof OpenSeadragon * @extends OpenSeadragon.TileSource @@ -83,7 +83,7 @@ $.IIIFTileSource = function( options ){ } } } - } else { + } else if ( canBeTiled(options.profile) ) { // use the largest of tileOptions that is smaller than the short dimension var shortDim = Math.min( this.height, this.width ), tileOptions = [256,512,1024], @@ -101,13 +101,32 @@ $.IIIFTileSource = function( options ){ // If we're smaller than 256, just use the short side. options.tileSize = shortDim; } + } else if (this.sizes && this.sizes.length > 0) { + // This info.json can't be tiled, but we can still construct a legacy pyramid from the sizes array. + // In this mode, IIIFTileSource will call functions from the abstract baseTileSource or the + // LegacyTileSource instead of performing IIIF tiling. + this.emulateLegacyImagePyramid = true; + + options.levels = constructLevels( this ); + // use the largest available size to define tiles + $.extend( true, options, { + width: options.levels[ options.levels.length - 1 ].width, + height: options.levels[ options.levels.length - 1 ].height, + tileSize: Math.max( options.height, options.width ), + tileOverlap: 0, + minLevel: 0, + maxLevel: options.levels.length - 1 + }); + this.levels = options.levels; + } else { + $.console.error("Nothing in the info.json to construct image pyramids from"); } - if ( !options.maxLevel ) { - if ( !this.scale_factors ) { - options.maxLevel = Number( Math.ceil( Math.log( Math.max( this.width, this.height ), 2 ) ) ); + if (!options.maxLevel && !this.emulateLegacyImagePyramid) { + if (!this.scale_factors) { + options.maxLevel = Number(Math.ceil(Math.log(Math.max(this.width, this.height), 2))); } else { - options.maxLevel = Math.floor( Math.pow( Math.max.apply(null, this.scale_factors), 0.5) ); + options.maxLevel = Math.floor(Math.pow(Math.max.apply(null, this.scale_factors), 0.5)); } } @@ -192,6 +211,11 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea * @param {Number} level */ getTileWidth: function( level ) { + + if(this.emulateLegacyImagePyramid) { + return $.TileSource.prototype.getTileWidth.call(this, level); + } + var scaleFactor = Math.pow(2, this.maxLevel - level); if (this.tileSizePerScaleFactor && this.tileSizePerScaleFactor[scaleFactor]) { @@ -206,6 +230,11 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea * @param {Number} level */ getTileHeight: function( level ) { + + if(this.emulateLegacyImagePyramid) { + return $.TileSource.prototype.getTileHeight.call(this, level); + } + var scaleFactor = Math.pow(2, this.maxLevel - level); if (this.tileSizePerScaleFactor && this.tileSizePerScaleFactor[scaleFactor]) { @@ -214,9 +243,61 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea return this._tileHeight; }, + /** + * @function + * @param {Number} level + */ + getLevelScale: function ( level ) { + + if(this.emulateLegacyImagePyramid) { + var levelScale = NaN; + if (this.levels.length > 0 && level >= this.minLevel && level <= this.maxLevel) { + levelScale = + this.levels[level].width / + this.levels[this.maxLevel].width; + } + return levelScale; + } + + return $.TileSource.prototype.getLevelScale.call(this, level); + }, /** - * Responsible for retreiving the url which will return an image for the + * @function + * @param {Number} level + */ + getNumTiles: function( level ) { + + if(this.emulateLegacyImagePyramid) { + var scale = this.getLevelScale(level); + if (scale) { + return new $.Point(1, 1); + } else { + return new $.Point(0, 0); + } + } + + return $.TileSource.prototype.getNumTiles.call(this, level); + }, + + + /** + * @function + * @param {Number} level + * @param {OpenSeadragon.Point} point + */ + getTileAtPoint: function( level, point ) { + + if(this.emulateLegacyImagePyramid) { + return new $.Point(0, 0); + } + + return $.TileSource.prototype.getTileAtPoint.call(this, level, point); + }, + + + /** + * Responsible for retrieving the url which will return an image for the * region specified by the given x, y, and level components. * @function * @param {Number} level - z index @@ -226,6 +307,14 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea */ getTileUrl: function( level, x, y ){ + if(this.emulateLegacyImagePyramid) { + var url = null; + if ( this.levels.length > 0 && level >= this.minLevel && level <= this.maxLevel ) { + url = this.levels[ level ].url; + } + return url; + } + //# constants var IIIF_ROTATION = '0', //## get the scale (level as a decimal) @@ -280,6 +369,40 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea }); + /** + * Determine whether arbitrary tile requests can be made against a service with the given profile + * @function + * @param {object} profile - IIIF profile object + * @throws {Error} + */ + function canBeTiled (profile ) { + var level0Profiles = [ + "http://library.stanford.edu/iiif/image-api/compliance.html#level0", + "http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level0", + "http://iiif.io/api/image/2/level0.json" + ]; + var isLevel0 = (level0Profiles.indexOf(profile[0]) != -1); + return !isLevel0 || (profile.indexOf("sizeByW") != -1); + } + + /** + * Build the legacy pyramid URLs (one tile per level) + * @function + * @param {object} options - infoJson + * @throws {Error} + */ + function constructLevels(options) { + var levels = []; + for(var i=0; i= this.minLevel && level <= this.maxLevel) { + levelScale = + this.levels[level].width / + this.levels[this.maxLevel].width; + } + return levelScale; + }, + /** + * @function + * @param {Number} level + */ + getNumTiles: function (level) { + var scale = this.getLevelScale(level); + if (scale) { + return new $.Point(1, 1); + } else { + return new $.Point(0, 0); + } + }, + /** + * Retrieves a tile url + * @function + * @param {Number} level Level of the tile + * @param {Number} x x coordinate of the tile + * @param {Number} y y coordinate of the tile + */ + getTileUrl: function (level, x, y) { + var url = null; + if (level >= this.minLevel && level <= this.maxLevel) { + url = this.levels[level].url; + } + return url; + }, + /** + * Retrieves a tile context 2D + * @function + * @param {Number} level Level of the tile + * @param {Number} x x coordinate of the tile + * @param {Number} y y coordinate of the tile + */ + getContext2D: function (level, x, y) { + var context = null; + if (level >= this.minLevel && level <= this.maxLevel) { + context = this.levels[level].context2D; + } + return context; + }, + + // private + // + // Builds the differents levels of the pyramid if possible + // (i.e. if canvas API enabled and no canvas tainting issue). + _buildLevels: function () { + var levels = [{ + url: this._image.src, + width: this._image.naturalWidth, + height: this._image.naturalHeight + }]; + + if (!this.buildPyramid || !$.supportsCanvas || !this.useCanvas) { + // We don't need the image anymore. Allows it to be GC. + delete this._image; + return levels; + } + + var currentWidth = this._image.naturalWidth; + var currentHeight = this._image.naturalHeight; + + var bigCanvas = document.createElement("canvas"); + var bigContext = bigCanvas.getContext("2d"); + + bigCanvas.width = currentWidth; + bigCanvas.height = currentHeight; + bigContext.drawImage(this._image, 0, 0, currentWidth, currentHeight); + // We cache the context of the highest level because the browser + // is a lot faster at downsampling something it already has + // downsampled before. + levels[0].context2D = bigContext; + // We don't need the image anymore. Allows it to be GC. + delete this._image; + + if ($.isCanvasTainted(bigCanvas)) { + // If the canvas is tainted, we can't compute the pyramid. + return levels; + } + + // We build smaller levels until either width or height becomes + // 1 pixel wide. + while (currentWidth >= 2 && currentHeight >= 2) { + currentWidth = Math.floor(currentWidth / 2); + currentHeight = Math.floor(currentHeight / 2); + var smallCanvas = document.createElement("canvas"); + var smallContext = smallCanvas.getContext("2d"); + smallCanvas.width = currentWidth; + smallCanvas.height = currentHeight; + smallContext.drawImage(bigCanvas, 0, 0, currentWidth, currentHeight); + + levels.splice(0, 0, { + context2D: smallContext, + width: currentWidth, + height: currentHeight + }); + + bigCanvas = smallCanvas; + bigContext = smallContext; + } + return levels; + } + }); + +}(OpenSeadragon)); diff --git a/src/legacytilesource.js b/src/legacytilesource.js index 80891b43..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 @@ -215,7 +205,7 @@ function filterFiles( files ){ if( file.height && file.width && file.url && ( - file.url.toLowerCase().match(/^.*\.(png|jpg|jpeg|gif)$/) || ( + file.url.toLowerCase().match(/^.*\.(png|jpg|jpeg|gif)(?:\?.*)?$/) || ( file.mimetype && file.mimetype.toLowerCase().match(/^.*\/(png|jpg|jpeg|gif)$/) ) diff --git a/src/mousetracker.js b/src/mousetracker.js index e9b5acfc..498a72ac 100644 --- a/src/mousetracker.js +++ b/src/mousetracker.js @@ -265,7 +265,8 @@ } }; - $.MouseTracker.prototype = /** @lends OpenSeadragon.MouseTracker.prototype */{ + /** @lends OpenSeadragon.MouseTracker.prototype */ + $.MouseTracker.prototype = { /** * Clean up any events or objects created by the tracker. @@ -1117,7 +1118,9 @@ */ this.captureCount = 0; }; - $.MouseTracker.GesturePointList.prototype = /** @lends OpenSeadragon.MouseTracker.GesturePointList.prototype */{ + + /** @lends OpenSeadragon.MouseTracker.GesturePointList.prototype */ + $.MouseTracker.GesturePointList.prototype = { /** * @function * @returns {Number} Number of gesture points in the list. @@ -1354,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 ) { @@ -1367,6 +1370,14 @@ eventParams = getCaptureEventParams( tracker, $.MouseTracker.havePointerEvents ? 'pointerevent' : pointerType ); // We emulate mouse capture by hanging listeners on the document object. // (Note we listen on the capture phase so the captured handlers will get called first) + if (isInIframe && canAccessEvents(window.top)) { + $.addEvent( + window.top, + eventParams.upName, + eventParams.upHandler, + true + ); + } $.addEvent( $.MouseTracker.captureElement, eventParams.upName, @@ -1389,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 ) { @@ -1402,6 +1413,14 @@ eventParams = getCaptureEventParams( tracker, $.MouseTracker.havePointerEvents ? 'pointerevent' : pointerType ); // We emulate mouse capture by hanging listeners on the document object. // (Note we listen on the capture phase so the captured handlers will get called first) + if (isInIframe && canAccessEvents(window.top)) { + $.removeEvent( + window.top, + eventParams.upName, + eventParams.upHandler, + true + ); + } $.removeEvent( $.MouseTracker.captureElement, eventParams.moveName, @@ -2055,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 ); @@ -2109,7 +2128,7 @@ } if ( updatePointersUp( tracker, event, gPoints, 0 ) ) { - releasePointer( tracker, 'touch' ); + releasePointer( tracker, 'touch', touchCount ); } // simulate touchleave on our tracked element @@ -2190,16 +2209,10 @@ function onTouchCancel( tracker, event ) { var i, touchCount = event.changedTouches.length, - gPoints = []; - - for ( i = 0; i < touchCount; i++ ) { - gPoints.push( { - id: event.changedTouches[ i ].identifier, - type: 'touch' - } ); - } - - updatePointersCancel( tracker, event, gPoints ); + gPoints = [], + pointsList = tracker.getActivePointersListByType( 'touch' ); + + abortTouchContacts( tracker, event, pointsList ); } @@ -3254,5 +3267,29 @@ } ); } } + + // True if inside an iframe, otherwise false. + // @member {Boolean} isInIframe + // @private + // @inner + var isInIframe = (function() { + try { + return window.self !== window.top; + } catch (e) { + return true; + } + })(); + + // @function + // @private + // @inner + // @returns {Boolean} True if the target has access rights to events, otherwise false. + function canAccessEvents (target) { + try { + return target.addEventListener && target.removeEventListener; + } catch (e) { + return false; + } + } } ( OpenSeadragon ) ); diff --git a/src/navigator.js b/src/navigator.js index 7addc5ea..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); }); } @@ -223,12 +230,6 @@ $.Navigator = function( options ){ } }); - this.addHandler("reset-size", function() { - if (_this.viewport) { - _this.viewport.goHome(true); - } - }); - viewer.world.addHandler("item-index-change", function(event) { var item = _this.world.getItemAt(event.previousIndex); _this.world.setItemIndex(item, event.newIndex); @@ -305,10 +306,10 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /* this.updateSize(); } - if( viewport && this.viewport ) { - bounds = viewport.getBounds( true ); - topleft = this.viewport.pixelFromPoint( bounds.getTopLeft(), false ); - bottomright = this.viewport.pixelFromPoint( bounds.getBottomRight(), false ) + 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 ); //update style for navigator-box @@ -378,7 +379,7 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /* */ function onCanvasClick( event ) { if ( event.quick && this.viewer.viewport ) { - this.viewer.viewport.panTo( this.viewport.pointFromPixel( event.position ).rotate( -this.viewer.viewport.degrees, this.viewer.viewport.getHomeBounds().getCenter() ) ); + this.viewer.viewport.panTo(this.viewport.pointFromPixel(event.position)); this.viewer.viewport.applyConstraints(); } } diff --git a/src/openseadragon.js b/src/openseadragon.js index 6b36327b..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. @@ -206,6 +187,11 @@ * @property {Number} [opacity=1] * Default opacity of the tiled images (1=opaque, 0=transparent) * + * @property {String} [compositeOperation=null] + * Valid values are 'source-over', 'source-atop', 'source-in', 'source-out', + * 'destination-over', 'destination-atop', 'destination-in', + * 'destination-out', 'lighter', 'copy' or 'xor' + * * @property {String|CanvasGradient|CanvasPattern|Function} [placeholderFillStyle=null] * Draws a colored rectangle behind the tile if it is not loaded yet. * You can pass a CSS color value like "#FF8800". @@ -249,12 +235,28 @@ * image though it is less effective visually if the HTML5 Canvas is not * availble on the viewing device. * + * @property {Number} [smoothTileEdgesMinZoom=1.1] + * 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. * * @property {Boolean} [preserveImageSizeOnResize=false] * Set to true to have the image size preserved when the viewer is resized. This requires autoResize=true (default). * + * @property {Number} [minScrollDeltaTime=50] + * Number of milliseconds between canvas-scroll events. This value helps normalize the rate of canvas-scroll + * events between different devices, causing the faster devices to slow down enough to make the zoom control + * more manageable. + * * @property {Number} [pixelsPerWheelLine=40] * For pixel-resolution scrolling devices, the number of pixels equal to one scroll line. * @@ -358,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. @@ -390,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. * @@ -676,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( $ ){ @@ -827,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 @@ -839,6 +849,23 @@ if (typeof define === 'function' && define.amd) { canvasElement.getContext( '2d' ) ); }()); + /** + * Test whether the submitted canvas is tainted or not. + * @argument {Canvas} canvas The canvas to test. + * @returns {Boolean} True if the canvas is tainted. + */ + $.isCanvasTainted = function(canvas) { + var isTainted = false; + try { + // We test if the canvas is tainted by retrieving data from it. + // An exception will be raised if the canvas is tainted. + var data = canvas.getContext('2d').getImageData(0, 0, 1, 1); + } catch (e) { + isTainted = true; + } + return isTainted; + }; + /** * A ratio comparing the device screen's pixel density to the canvas's backing store pixel density. Defaults to 1 if canvas isn't supported by the browser. * @member {Number} pixelDensityRatio @@ -951,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 */{ /** @@ -1000,9 +1039,12 @@ if (typeof define === 'function' && define.amd) { immediateRender: false, 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 + minScrollDeltaTime: 50, //DEFAULT CONTROL SETTINGS showSequenceControl: true, //SEQUENCE @@ -1031,6 +1073,7 @@ if (typeof define === 'function' && define.amd) { navigatorHeight: null, navigatorWidth: null, navigatorAutoResize: true, + navigatorAutoFade: true, navigatorRotate: true, // INITIAL ROTATION @@ -1038,6 +1081,7 @@ if (typeof define === 'function' && define.amd) { // APPEARANCE opacity: 1, + compositeOperation: null, placeholderFillStyle: null, //REFERENCE STRIP SETTINGS @@ -1287,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). @@ -2023,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; @@ -2498,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 2fe72413..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,72 +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); }; - $.Overlay.prototype = /** @lends OpenSeadragon.Overlay.prototype */{ + /** @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; } }, @@ -180,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); } } @@ -204,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/point.js b/src/point.js index 1ceef296..7d4f6740 100644 --- a/src/point.js +++ b/src/point.js @@ -59,7 +59,8 @@ $.Point = function( x, y ) { this.y = typeof ( y ) == "number" ? y : 0; }; -$.Point.prototype = /** @lends OpenSeadragon.Point.prototype */{ +/** @lends OpenSeadragon.Point.prototype */ +$.Point.prototype = { /** * @function * @returns {OpenSeadragon.Point} a duplicate of this Point @@ -179,14 +180,46 @@ $.Point.prototype = /** @lends OpenSeadragon.Point.prototype */{ * From http://stackoverflow.com/questions/4465931/rotate-rectangle-around-a-point * @function * @param {Number} degress to rotate around the pivot. - * @param {OpenSeadragon.Point} pivot Point about which to rotate. + * @param {OpenSeadragon.Point} [pivot=(0,0)] Point around which to rotate. + * Defaults to the origin. * @returns {OpenSeadragon.Point}. A new point representing the point rotated around the specified pivot */ - rotate: function ( degrees, pivot ) { - var angle = degrees * Math.PI / 180.0, - x = Math.cos( angle ) * ( this.x - pivot.x ) - Math.sin( angle ) * ( this.y - pivot.y ) + pivot.x, - y = Math.sin( angle ) * ( this.x - pivot.x ) + Math.cos( angle ) * ( this.y - pivot.y ) + pivot.y; - return new $.Point( x, y ); + rotate: function (degrees, pivot) { + pivot = pivot || new $.Point(0, 0); + var cos; + var sin; + // Avoid float computations when possible + if (degrees % 90 === 0) { + var d = degrees % 360; + if (d < 0) { + d += 360; + } + switch (d) { + case 0: + cos = 1; + sin = 0; + break; + case 90: + cos = 0; + sin = 1; + break; + case 180: + cos = -1; + sin = 0; + break; + case 270: + cos = 0; + sin = -1; + break; + } + } else { + var angle = degrees * Math.PI / 180.0; + cos = Math.cos(angle); + sin = Math.sin(angle); + } + var x = cos * (this.x - pivot.x) - sin * (this.y - pivot.y) + pivot.x; + var y = sin * (this.x - pivot.x) + cos * (this.y - pivot.y) + pivot.y; + return new $.Point(x, y); }, /** diff --git a/src/profiler.js b/src/profiler.js index 5e6cfb7c..3f9d708f 100644 --- a/src/profiler.js +++ b/src/profiler.js @@ -68,7 +68,8 @@ $.Profiler = function() { this.maxIdleTime = 0; }; -$.Profiler.prototype = /** @lends OpenSeadragon.Profiler.prototype */{ +/** @lends OpenSeadragon.Profiler.prototype */ +$.Profiler.prototype = { /** * @function diff --git a/src/rectangle.js b/src/rectangle.js index 5d3495af..98c839de 100644 --- a/src/rectangle.js +++ b/src/rectangle.js @@ -32,55 +32,124 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -(function( $ ){ +(function($) { /** * @class Rect - * @classdesc A Rectangle really represents a 2x2 matrix where each row represents a - * 2 dimensional vector component, the first is (x,y) and the second is - * (width, height). The latter component implies the equation of a simple - * plane. + * @classdesc A Rectangle is described by it top left coordinates (x, y), width, + * height and degrees of rotation around (x, y). + * Note that the coordinate system used is the one commonly used with images: + * x increases when going to the right + * y increases when going to the bottom + * degrees increases clockwise with 0 being the horizontal + * + * The constructor normalizes the rectangle to always have 0 <= degrees < 90 * * @memberof OpenSeadragon - * @param {Number} x The vector component 'x'. - * @param {Number} y The vector component 'y'. - * @param {Number} width The vector component 'height'. - * @param {Number} height The vector component 'width'. + * @param {Number} [x=0] The vector component 'x'. + * @param {Number} [y=0] The vector component 'y'. + * @param {Number} [width=0] The vector component 'width'. + * @param {Number} [height=0] The vector component 'height'. + * @param {Number} [degrees=0] Rotation of the rectangle around (x,y) in degrees. */ -$.Rect = function( x, y, width, height ) { +$.Rect = function(x, y, width, height, degrees) { /** * The vector component 'x'. * @member {Number} x * @memberof OpenSeadragon.Rect# */ - this.x = typeof ( x ) == "number" ? x : 0; + this.x = typeof(x) === "number" ? x : 0; /** * The vector component 'y'. * @member {Number} y * @memberof OpenSeadragon.Rect# */ - this.y = typeof ( y ) == "number" ? y : 0; + this.y = typeof(y) === "number" ? y : 0; /** * The vector component 'width'. * @member {Number} width * @memberof OpenSeadragon.Rect# */ - this.width = typeof ( width ) == "number" ? width : 0; + this.width = typeof(width) === "number" ? width : 0; /** * The vector component 'height'. * @member {Number} height * @memberof OpenSeadragon.Rect# */ - this.height = typeof ( height ) == "number" ? height : 0; + this.height = typeof(height) === "number" ? height : 0; + + this.degrees = typeof(degrees) === "number" ? degrees : 0; + + // Normalizes the rectangle. + this.degrees = this.degrees % 360; + if (this.degrees < 0) { + this.degrees += 360; + } + var newTopLeft, newWidth; + if (this.degrees >= 270) { + newTopLeft = this.getTopRight(); + this.x = newTopLeft.x; + this.y = newTopLeft.y; + newWidth = this.height; + this.height = this.width; + this.width = newWidth; + this.degrees -= 270; + } else if (this.degrees >= 180) { + newTopLeft = this.getBottomRight(); + this.x = newTopLeft.x; + this.y = newTopLeft.y; + this.degrees -= 180; + } else if (this.degrees >= 90) { + newTopLeft = this.getBottomLeft(); + this.x = newTopLeft.x; + this.y = newTopLeft.y; + newWidth = this.height; + this.height = this.width; + this.width = newWidth; + this.degrees -= 90; + } }; -$.Rect.prototype = /** @lends OpenSeadragon.Rect.prototype */{ +/** + * 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 = { /** * @function * @returns {OpenSeadragon.Rect} a duplicate of this Rect */ clone: function() { - return new $.Rect(this.x, this.y, this.width, this.height); + return new $.Rect( + this.x, + this.y, + this.width, + this.height, + this.degrees); }, /** @@ -114,10 +183,8 @@ $.Rect.prototype = /** @lends OpenSeadragon.Rect.prototype */{ * the rectangle. */ getBottomRight: function() { - return new $.Point( - this.x + this.width, - this.y + this.height - ); + return new $.Point(this.x + this.width, this.y + this.height) + .rotate(this.degrees, this.getTopLeft()); }, /** @@ -128,10 +195,8 @@ $.Rect.prototype = /** @lends OpenSeadragon.Rect.prototype */{ * the rectangle. */ getTopRight: function() { - return new $.Point( - this.x + this.width, - this.y - ); + return new $.Point(this.x + this.width, this.y) + .rotate(this.degrees, this.getTopLeft()); }, /** @@ -142,10 +207,8 @@ $.Rect.prototype = /** @lends OpenSeadragon.Rect.prototype */{ * the rectangle. */ getBottomLeft: function() { - return new $.Point( - this.x, - this.y + this.height - ); + return new $.Point(this.x, this.y + this.height) + .rotate(this.degrees, this.getTopLeft()); }, /** @@ -158,7 +221,7 @@ $.Rect.prototype = /** @lends OpenSeadragon.Rect.prototype */{ return new $.Point( this.x + this.width / 2.0, this.y + this.height / 2.0 - ); + ).rotate(this.degrees, this.getTopLeft()); }, /** @@ -168,7 +231,7 @@ $.Rect.prototype = /** @lends OpenSeadragon.Rect.prototype */{ * the width and height of the rectangle. */ getSize: function() { - return new $.Point( this.width, this.height ); + return new $.Point(this.width, this.height); }, /** @@ -177,98 +240,302 @@ $.Rect.prototype = /** @lends OpenSeadragon.Rect.prototype */{ * @param {OpenSeadragon.Rect} rectangle The Rectangle to compare to. * @return {Boolean} 'true' if all components are equal, otherwise 'false'. */ - equals: function( other ) { - return ( other instanceof $.Rect ) && - ( this.x === other.x ) && - ( this.y === other.y ) && - ( this.width === other.width ) && - ( this.height === other.height ); + equals: function(other) { + return (other instanceof $.Rect) && + this.x === other.x && + this.y === other.y && + this.width === other.width && + this.height === other.height && + this.degrees === other.degrees; }, /** - * Multiply all dimensions in this Rect by a factor and return a new Rect. + * Multiply all dimensions (except degrees) in this Rect by a factor and + * return a new Rect. * @function * @param {Number} factor The factor to multiply vector components. * @returns {OpenSeadragon.Rect} A new rect representing the multiplication * of the vector components by the factor */ - times: function( factor ) { - return new OpenSeadragon.Rect( + times: function(factor) { + return new $.Rect( this.x * factor, this.y * factor, this.width * factor, - this.height * factor - ); + this.height * factor, + this.degrees); }, /** - * Returns the smallest rectangle that will contain this and the given rectangle. + * Translate/move this Rect by a vector and return new Rect. + * @function + * @param {OpenSeadragon.Point} delta The translation vector. + * @returns {OpenSeadragon.Rect} A new rect with altered position + */ + translate: function(delta) { + return new $.Rect( + this.x + delta.x, + this.y + delta.y, + this.width, + this.height, + this.degrees); + }, + + /** + * Returns the smallest rectangle that will contain this and the given + * rectangle bounding boxes. * @param {OpenSeadragon.Rect} rect * @return {OpenSeadragon.Rect} The new rectangle. */ - // ---------- union: function(rect) { - var left = Math.min(this.x, rect.x); - var top = Math.min(this.y, rect.y); - var right = Math.max(this.x + this.width, rect.x + rect.width); - var bottom = Math.max(this.y + this.height, rect.y + rect.height); + var thisBoundingBox = this.getBoundingBox(); + var otherBoundingBox = rect.getBoundingBox(); - return new OpenSeadragon.Rect(left, top, right - left, bottom - top); + var left = Math.min(thisBoundingBox.x, otherBoundingBox.x); + var top = Math.min(thisBoundingBox.y, otherBoundingBox.y); + var right = Math.max( + thisBoundingBox.x + thisBoundingBox.width, + otherBoundingBox.x + otherBoundingBox.width); + var bottom = Math.max( + thisBoundingBox.y + thisBoundingBox.height, + otherBoundingBox.y + otherBoundingBox.height); + + return new $.Rect( + left, + top, + right - left, + bottom - top); }, /** - * Rotates a rectangle around a point. Currently only 90, 180, and 270 - * degrees are supported. + * 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} */ - rotate: function( degrees, pivot ) { - // TODO support arbitrary rotation - var width = this.width, - height = this.height, - newTopLeft; - - degrees = ( degrees + 360 ) % 360; - if (degrees % 90 !== 0) { - throw new Error('Currently only 0, 90, 180, and 270 degrees are supported.'); + rotate: function(degrees, pivot) { + degrees = degrees % 360; + if (degrees === 0) { + return this.clone(); } - - if( degrees === 0 ){ - return new $.Rect( - this.x, - this.y, - this.width, - this.height - ); + if (degrees < 0) { + degrees += 360; } pivot = pivot || this.getCenter(); + var newTopLeft = this.getTopLeft().rotate(degrees, pivot); + var newTopRight = this.getTopRight().rotate(degrees, pivot); - switch ( degrees ) { - case 90: - newTopLeft = this.getBottomLeft(); - width = this.height; - height = this.width; - break; - case 180: - newTopLeft = this.getBottomRight(); - break; - case 270: - newTopLeft = this.getTopRight(); - width = this.height; - height = this.width; - break; - default: - newTopLeft = this.getTopLeft(); - break; + var diff = newTopRight.minus(newTopLeft); + 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( + newTopLeft.x, + newTopLeft.y, + this.width, + this.height, + radians / Math.PI * 180); + }, - newTopLeft = newTopLeft.rotate(degrees, pivot); + /** + * Retrieves the smallest horizontal (degrees=0) rectangle which contains + * this rectangle. + * @returns {OpenSeadragon.Rect} + */ + getBoundingBox: function() { + if (this.degrees === 0) { + return this.clone(); + } + var topLeft = this.getTopLeft(); + var topRight = this.getTopRight(); + var bottomLeft = this.getBottomLeft(); + var bottomRight = this.getBottomRight(); + var minX = Math.min(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x); + var maxX = Math.max(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x); + var minY = Math.min(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y); + var maxY = Math.max(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y); + return new $.Rect( + minX, + minY, + maxX - minX, + maxY - minY); + }, - return new $.Rect(newTopLeft.x, newTopLeft.y, width, height); + /** + * 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); }, /** @@ -279,13 +546,14 @@ $.Rect.prototype = /** @lends OpenSeadragon.Rect.prototype */{ */ toString: function() { return "[" + - (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.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.degrees * 100) / 100) + "deg" + + "]"; } }; -}( OpenSeadragon )); +}(OpenSeadragon)); diff --git a/src/referencestrip.js b/src/referencestrip.js index aef4e93e..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 } ); } }, @@ -436,7 +435,7 @@ function loadPanels( strip, viewerSize, scroll ) { animationTime: 0 } ); - miniViewer.displayRegion = $.makeNeutralElement( "textarea" ); + miniViewer.displayRegion = $.makeNeutralElement( "div" ); miniViewer.displayRegion.id = element.id + '-displayregion'; miniViewer.displayRegion.className = 'displayregion'; diff --git a/src/spring.js b/src/spring.js index 2c89ee07..71c94d06 100644 --- a/src/spring.js +++ b/src/spring.js @@ -41,7 +41,7 @@ * @param {Number} options.springStiffness - Spring stiffness. Must be greater than zero. * The closer to zero, the closer to linear animation. * @param {Number} options.animationTime - Animation duration per spring, in seconds. - * Must be greater than zero. + * Must be zero or greater. * @param {Number} [options.initial=0] - Initial value of spring. * @param {Boolean} [options.exponential=false] - Whether this spring represents * an exponential scale (such as zoom) and should be animated accordingly. Note that @@ -79,8 +79,8 @@ $.Spring = function( options ) { $.console.assert(typeof options.springStiffness === "number" && options.springStiffness !== 0, "[OpenSeadragon.Spring] options.springStiffness must be a non-zero number"); - $.console.assert(typeof options.animationTime === "number" && options.springStiffness !== 0, - "[OpenSeadragon.Spring] options.animationTime must be a non-zero number"); + $.console.assert(typeof options.animationTime === "number" && options.animationTime >= 0, + "[OpenSeadragon.Spring] options.animationTime must be a number greater than or equal to 0"); if (options.exponential) { this._exponential = true; @@ -134,7 +134,8 @@ $.Spring = function( options ) { } }; -$.Spring.prototype = /** @lends OpenSeadragon.Spring.prototype */{ +/** @lends OpenSeadragon.Spring.prototype */ +$.Spring.prototype = { /** * @function @@ -233,6 +234,15 @@ $.Spring.prototype = /** @lends OpenSeadragon.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 ad018ba7..04f3c08a 100644 --- a/src/tile.js +++ b/src/tile.js @@ -45,8 +45,10 @@ * @param {Boolean} exists Is this tile a part of a sparse image? ( Also has * this tile failed to load? ) * @param {String} url The URL of this tile's image. + * @param {CanvasRenderingContext2D} context2D The context2D of this tile if it + * is provided directly by the tile source. */ -$.Tile = function(level, x, y, bounds, exists, url) { +$.Tile = function(level, x, y, bounds, exists, url, context2D) { /** * The zoom level this tile belongs to. * @member {Number} level @@ -83,6 +85,12 @@ $.Tile = function(level, x, y, bounds, exists, url) { * @memberof OpenSeadragon.Tile# */ this.url = url; + /** + * The context2D of this tile if it is provided directly by the tile source. + * @member {CanvasRenderingContext2D} context2D + * @memberOf OpenSeadragon.Tile# + */ + this.context2D = context2D; /** * Is this tile loaded? * @member {Boolean} loaded @@ -172,7 +180,8 @@ $.Tile = function(level, x, y, bounds, exists, url) { this.lastTouchTime = 0; }; -$.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ +/** @lends OpenSeadragon.Tile.prototype */ +$.Tile.prototype = { /** * Provides a string representation of this tiles level and (x,y) @@ -184,6 +193,11 @@ $.Tile.prototype = /** @lends OpenSeadragon.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 @@ -240,21 +254,23 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ * @param {Function} drawingHandler - Method for firing the drawing event. * drawingHandler({context, tile, rendered}) * where rendered is the context with the pre-drawn image. + * @param {Number} [scale=1] - Apply a scale to position and size + * @param {OpenSeadragon.Point} [translate] - A translation vector */ - drawCanvas: function( context, drawingHandler ) { + drawCanvas: function( context, drawingHandler, scale, translate ) { - var position = this.position, - size = this.size, + var position = this.position.times($.pixelDensityRatio), + size = this.size.times($.pixelDensityRatio), rendered; - if (!this.cacheImageRecord) { + if (!this.context2D && !this.cacheImageRecord) { $.console.warn( '[Tile.drawCanvas] attempting to draw tile %s when it\'s not cached', this.toString()); return; } - rendered = this.cacheImageRecord.getRenderedContext(); + rendered = this.context2D || this.cacheImageRecord.getRenderedContext(); if ( !this.loaded || !rendered ){ $.console.warn( @@ -269,20 +285,30 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ context.globalAlpha = this.opacity; + if (typeof scale === 'number' && scale !== 1) { + // draw tile at a different scale + position = position.times(scale); + size = size.times(scale); + } + + if (translate instanceof $.Point) { + // shift tile position slightly + 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.url.match('.png') ){ + if (context.globalAlpha === 1 && this._hasTransparencyChannel()) { //clearing only the inside of the rectangle occupied //by the png prevents edge flikering context.clearRect( - (position.x * $.pixelDensityRatio)+1, - (position.y * $.pixelDensityRatio)+1, - (size.x * $.pixelDensityRatio)-2, - (size.y * $.pixelDensityRatio)-2 + position.x + 1, + position.y + 1, + size.x - 2, + size.y - 2 ); - } // This gives the application a chance to make image manipulation @@ -295,15 +321,59 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ 0, rendered.canvas.width, rendered.canvas.height, - position.x * $.pixelDensityRatio, - position.y * $.pixelDensityRatio, - size.x * $.pixelDensityRatio, - size.y * $.pixelDensityRatio + position.x, + position.y, + size.x, + size.y ); context.restore(); }, + /** + * Get the ratio between current and original size. + * @function + * @return {Float} + */ + getScaleForEdgeSmoothing: function() { + 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; + } + return context.canvas.width / (this.size.x * $.pixelDensityRatio); + }, + + /** + * Get a translation vector that when applied to the tile position produces integer coordinates. + * Needed to avoid swimming and twitching. + * @function + * @param {Number} [scale=1] - Scale to be applied to position. + * @return {OpenSeadragon.Point} + */ + getTranslationForEdgeSmoothing: function(scale, canvasSize, sketchCanvasSize) { + // The translation vector must have positive values, otherwise the image goes a bit off + // the sketch canvas to the top and left and we must use negative coordinates to repaint it + // to the main canvas. In that case, some browsers throw: + // INDEX_SIZE_ERR: DOM Exception 1: Index or size was negative, or greater than the allowed value. + var x = Math.max(1, Math.ceil((sketchCanvasSize.x - canvasSize.x) / 2)); + var y = Math.max(1, Math.ceil((sketchCanvasSize.y - canvasSize.y) / 2)); + return new $.Point(x, y).minus( + this.position + .times($.pixelDensityRatio) + .times(scale || 1) + .apply(function(x) { + return x % 1; + }) + ); + }, + /** * Removes tile from its container. * @function diff --git a/src/tilecache.js b/src/tilecache.js index 281709c8..ee3a4662 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -122,7 +122,8 @@ $.TileCache = function( options ) { this._imagesLoadedCount = 0; }; -$.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{ +/** @lends OpenSeadragon.TileCache.prototype */ +$.TileCache.prototype = { /** * @returns {Number} The total number of tiles that have been loaded by * this TileCache. diff --git a/src/tiledimage.js b/src/tiledimage.js index a6943de4..6acfc08c 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. @@ -64,7 +68,10 @@ * @param {Number} [options.blendTime] - See {@link OpenSeadragon.Options}. * @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}. * @param {String|CanvasGradient|CanvasPattern|Function} [options.placeholderFillStyle] - See {@link OpenSeadragon.Options}. * @param {String|Boolean} [options.crossOriginPolicy] - See {@link OpenSeadragon.Options}. @@ -120,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 @@ -130,25 +142,29 @@ $.TiledImage = function( options ) { lastResetTime: 0, // Last time for which the tiledImage was reset. _midDraw: false, // Is the tiledImage currently updating the viewport? _needsDraw: true, // Does the tiledImage need to update the viewport again? - _hasOpaqueTile: false, // Do we have even one fully opaque tile? - + _hasOpaqueTile: false, // Do we have even one fully opaque tile? //configurable settings - springStiffness: $.DEFAULT_SETTINGS.springStiffness, - animationTime: $.DEFAULT_SETTINGS.animationTime, - minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, - wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal, - wrapVertical: $.DEFAULT_SETTINGS.wrapVertical, - immediateRender: $.DEFAULT_SETTINGS.immediateRender, - blendTime: $.DEFAULT_SETTINGS.blendTime, - alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend, - minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio, - debugMode: $.DEFAULT_SETTINGS.debugMode, - crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy, - placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, - opacity: $.DEFAULT_SETTINGS.opacity + springStiffness: $.DEFAULT_SETTINGS.springStiffness, + animationTime: $.DEFAULT_SETTINGS.animationTime, + minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, + wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal, + wrapVertical: $.DEFAULT_SETTINGS.wrapVertical, + immediateRender: $.DEFAULT_SETTINGS.immediateRender, + blendTime: $.DEFAULT_SETTINGS.blendTime, + alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend, + minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio, + smoothTileEdgesMinZoom: $.DEFAULT_SETTINGS.smoothTileEdgesMinZoom, + iOSDevice: $.DEFAULT_SETTINGS.iOSDevice, + debugMode: $.DEFAULT_SETTINGS.debugMode, + crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy, + placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, + opacity: $.DEFAULT_SETTINGS.opacity, + compositeOperation: $.DEFAULT_SETTINGS.compositeOperation }, options ); + this._fullyLoaded = false; + this._xSpring = new $.Spring({ initial: x, springStiffness: this.springStiffness, @@ -169,12 +185,16 @@ $.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) { /** * This event is fired just before the tile is drawn giving the application a chance to alter the image. * - * NOTE: This event is only fired when the drawer is using a . + * NOTE: This event is only fired when the drawer is using a <canvas>. * * @event tile-drawing * @memberof OpenSeadragon.Viewer @@ -200,6 +220,37 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag return this._needsDraw; }, + /** + * @returns {Boolean} Whether all tiles necessary for this TiledImage to draw at the current view have been loaded. + */ + getFullyLoaded: function() { + return this._fullyLoaded; + }, + + // private + _setFullyLoaded: function(flag) { + if (flag === this._fullyLoaded) { + return; + } + + this._fullyLoaded = flag; + + /** + * Fired when the TiledImage's "fully loaded" flag (whether all tiles necessary for this TiledImage + * to draw at the current view have been loaded) changes. + * + * @event fully-loaded-change + * @memberof OpenSeadragon.TiledImage + * @type {object} + * @property {Boolean} fullyLoaded - The new "fully loaded" value. + * @property {OpenSeadragon.TiledImage} eventSource - A reference to the TiledImage which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('fully-loaded-change', { + fullyLoaded: this._fullyLoaded + }); + }, + /** * Clears all tiles and triggers an update on the next call to * {@link OpenSeadragon.TiledImage#update}. @@ -237,9 +288,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; + } }, /** @@ -269,6 +322,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. */ @@ -355,23 +428,23 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @return {OpenSeadragon.Rect} A rect representing the coordinates in the viewport. */ imageToViewportRectangle: function( imageX, imageY, pixelWidth, pixelHeight, current ) { - if (imageX instanceof $.Rect) { + var rect = imageX; + if (rect instanceof $.Rect) { //they passed a rect instead of individual components current = imageY; - pixelWidth = imageX.width; - pixelHeight = imageX.height; - imageY = imageX.y; - imageX = imageX.x; + } else { + rect = new $.Rect(imageX, imageY, pixelWidth, pixelHeight); } - var coordA = this.imageToViewportCoordinates(imageX, imageY, current); - var coordB = this._imageToViewportDelta(pixelWidth, pixelHeight, current); + var coordA = this.imageToViewportCoordinates(rect.getTopLeft(), current); + var coordB = this._imageToViewportDelta(rect.width, rect.height, current); return new $.Rect( coordA.x, coordA.y, coordB.x, - coordB.y + coordB.y, + rect.degrees ); }, @@ -387,23 +460,23 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @return {OpenSeadragon.Rect} A rect representing the coordinates in the image. */ viewportToImageRectangle: function( viewerX, viewerY, pointWidth, pointHeight, current ) { + var rect = viewerX; if (viewerX instanceof $.Rect) { //they passed a rect instead of individual components current = viewerY; - pointWidth = viewerX.width; - pointHeight = viewerX.height; - viewerY = viewerX.y; - viewerX = viewerX.x; + } else { + rect = new $.Rect(viewerX, viewerY, pointWidth, pointHeight); } - var coordA = this.viewportToImageCoordinates(viewerX, viewerY, current); - var coordB = this._viewportToImageDelta(pointWidth, pointHeight, current); + var coordA = this.viewportToImageCoordinates(rect.getTopLeft(), current); + var coordB = this._viewportToImageDelta(rect.width, rect.height, current); return new $.Rect( coordA.x, coordA.y, coordB.x, - coordB.y + coordB.y, + rect.degrees ); }, @@ -502,6 +575,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag this._xSpring.resetTo(position.x); this._ySpring.resetTo(position.y); + this._needsDraw = true; } else { if (sameTarget) { return; @@ -509,6 +583,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag this._xSpring.springTo(position.x); this._ySpring.springTo(position.y); + this._needsDraw = true; } if (!sameTarget) { @@ -536,6 +611,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. @@ -581,6 +717,21 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag this._needsDraw = true; }, + /** + * @returns {String} The TiledImage's current compositeOperation. + */ + getCompositeOperation: function() { + return this.compositeOperation; + }, + + /** + * @param {String} compositeOperation the tiled image should be drawn with this globalCompositeOperation. + */ + setCompositeOperation: function(compositeOperation) { + this.compositeOperation = compositeOperation; + this._needsDraw = true; + }, + // private _setScale: function(scale, immediately) { var sameTarget = (this._scaleSpring.target.value === scale); @@ -591,6 +742,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag this._scaleSpring.resetTo(scale); this._updateForScale(); + this._needsDraw = true; } else { if (sameTarget) { return; @@ -598,6 +750,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag this._scaleSpring.springTo(scale); this._updateForScale(); + this._needsDraw = true; } if (!sameTarget) { @@ -626,6 +779,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; } }); @@ -646,7 +804,7 @@ function updateViewport( tiledImage ) { haveDrawn = false, currentTime = $.now(), viewportBounds = tiledImage.viewport.getBoundsWithMargins( true ), - zeroRatioC = tiledImage.viewport.deltaPixelsFromPoints( + zeroRatioC = tiledImage.viewport.deltaPixelsFromPointsNoRotate( tiledImage.source.getPixelRatio( 0 ), true ).x * tiledImage._scaleSpring.current.value, @@ -664,7 +822,6 @@ function updateViewport( tiledImage ) { Math.log( 2 ) )) ), - degrees = tiledImage.viewport.degrees, renderPixelRatioC, renderPixelRatioT, zeroRatioT, @@ -672,26 +829,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(); @@ -727,7 +881,7 @@ function updateViewport( tiledImage ) { drawLevel = false; //Avoid calculations for draw if we have already drawn this - renderPixelRatioC = tiledImage.viewport.deltaPixelsFromPoints( + renderPixelRatioC = tiledImage.viewport.deltaPixelsFromPointsNoRotate( tiledImage.source.getPixelRatio( level ), true ).x * tiledImage._scaleSpring.current.value; @@ -741,12 +895,12 @@ function updateViewport( tiledImage ) { } //Perform calculations for draw if we haven't drawn this - renderPixelRatioT = tiledImage.viewport.deltaPixelsFromPoints( + renderPixelRatioT = tiledImage.viewport.deltaPixelsFromPointsNoRotate( tiledImage.source.getPixelRatio( level ), false ).x * tiledImage._scaleSpring.current.value; - zeroRatioT = tiledImage.viewport.deltaPixelsFromPoints( + zeroRatioT = tiledImage.viewport.deltaPixelsFromPointsNoRotate( tiledImage.source.getPixelRatio( Math.max( tiledImage.source.getClosestLevel( tiledImage.viewport.containerSize ) - 1, @@ -791,10 +945,12 @@ function updateViewport( tiledImage ) { drawTiles( tiledImage, tiledImage.lastDrawn ); // Load the new 'best' tile - if ( best ) { + if (best && !best.context2D) { loadTile( tiledImage, best, currentTime ); + tiledImage._setFullyLoaded(false); + } else { + tiledImage._setFullyLoaded(true); } - } @@ -846,10 +1002,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 ); } @@ -936,10 +1096,14 @@ function updateTile( tiledImage, drawLevel, haveDrawn, x, y, level, levelOpacity ); if (!tile.loaded) { - var imageRecord = tiledImage._tileCache.getImageRecord(tile.url); - if (imageRecord) { - var image = imageRecord.getImage(); - setTileLoaded(tiledImage, tile, image); + if (tile.context2D) { + setTileLoaded(tiledImage, tile); + } else { + var imageRecord = tiledImage._tileCache.getImageRecord(tile.url); + if (imageRecord) { + var image = imageRecord.getImage(); + setTileLoaded(tiledImage, tile, image); + } } } @@ -972,6 +1136,7 @@ function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWid bounds, exists, url, + context2D, tile; if ( !tilesMatrix[ level ] ) { @@ -987,6 +1152,8 @@ function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWid bounds = tileSource.getTileBounds( level, xMod, yMod ); exists = tileSource.tileExists( level, xMod, yMod ); url = tileSource.getTileUrl( level, xMod, yMod ); + context2D = tileSource.getContext2D ? + tileSource.getContext2D(level, xMod, yMod) : undefined; bounds.x += ( x - xMod ) / numTiles.x; bounds.y += (worldHeight / worldWidth) * (( y - yMod ) / numTiles.y); @@ -997,7 +1164,8 @@ function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWid y, bounds, exists, - url + url, + context2D ); } @@ -1036,12 +1204,12 @@ function onTileLoad( tiledImage, tile, time, image, errorMsg ) { * @property {string} message - The error message. */ tiledImage.viewer.raiseEvent("tile-load-failed", {tile: tile, tiledImage: tiledImage, time: time, message: errorMsg}); - if( !tiledImage.debugMode ){ - tile.loading = false; - tile.exists = false; - return; - } - } else if ( time < tiledImage.lastResetTime ) { + tile.loading = false; + tile.exists = false; + return; + } + + if ( time < tiledImage.lastResetTime ) { $.console.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url ); tile.loading = false; return; @@ -1076,12 +1244,14 @@ function setTileLoaded(tiledImage, tile, image, cutoff) { if (increment === 0) { tile.loading = false; tile.loaded = true; - tiledImage._tileCache.cacheTile({ - image: image, - tile: tile, - cutoff: cutoff, - tiledImage: tiledImage - }); + if (!tile.context2D) { + tiledImage._tileCache.cacheTile({ + image: image, + tile: tile, + cutoff: cutoff, + tiledImage: tiledImage + }); + } tiledImage._needsDraw = true; } } @@ -1124,10 +1294,10 @@ function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, boundsSize.x *= tiledImage._scaleSpring.current.value; boundsSize.y *= tiledImage._scaleSpring.current.value; - var positionC = viewport.pixelFromPoint( boundsTL, true ), - positionT = viewport.pixelFromPoint( boundsTL, false ), - sizeC = viewport.deltaPixelsFromPoints( boundsSize, true ), - sizeT = viewport.deltaPixelsFromPoints( boundsSize, false ), + var positionC = viewport.pixelFromPointNoRotate(boundsTL, true), + positionT = viewport.pixelFromPointNoRotate(boundsTL, false), + sizeC = viewport.deltaPixelsFromPointsNoRotate(boundsSize, true), + sizeT = viewport.deltaPixelsFromPointsNoRotate(boundsSize, false), tileCenter = positionT.plus( sizeT.divide( 2 ) ), tileDistance = viewportCenter.distanceTo( tileCenter ); @@ -1290,16 +1460,49 @@ function compareTiles( previousBest, tile ) { } function drawTiles( tiledImage, lastDrawn ) { - var i, - tile; - - if ( tiledImage.opacity <= 0 ) { - drawDebugInfo( tiledImage, lastDrawn ); + if (lastDrawn.length === 0) { return; } - var useSketch = tiledImage.opacity < 1; - if ( useSketch ) { - tiledImage._drawer._clear( true ); + var tile = lastDrawn[0]; + + var useSketch = tiledImage.opacity < 1 || + (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 && !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, + tiledImage._drawer.getCanvasSize(false), + tiledImage._drawer.getCanvasSize(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 + // interpolation + if (tiledImage.viewport.degrees !== 0 && !sketchScale) { + tiledImage._drawer._offsetForRotation(tiledImage.viewport.degrees, useSketch); } var usedClip = false; @@ -1308,6 +1511,12 @@ function drawTiles( tiledImage, lastDrawn ) { var box = tiledImage.imageToViewportRectangle(tiledImage._clip, true); var clipRect = tiledImage._drawer.viewportToDrawerRectangle(box); + if (sketchScale) { + clipRect = clipRect.times(sketchScale); + } + if (sketchTranslate) { + clipRect = clipRect.translate(sketchTranslate); + } tiledImage._drawer.setClip(clipRect, useSketch); usedClip = true; @@ -1315,6 +1524,12 @@ function drawTiles( tiledImage, lastDrawn ) { if ( tiledImage.placeholderFillStyle && tiledImage._hasOpaqueTile === false ) { var placeholderRect = tiledImage._drawer.viewportToDrawerRectangle(tiledImage.getBounds(true)); + if (sketchScale) { + placeholderRect = placeholderRect.times(sketchScale); + } + if (sketchTranslate) { + placeholderRect = placeholderRect.translate(sketchTranslate); + } var fillStyle = null; if ( typeof tiledImage.placeholderFillStyle === "function" ) { @@ -1327,9 +1542,9 @@ 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 ); + tiledImage._drawer.drawTile( tile, tiledImage._drawingHandler, useSketch, sketchScale, sketchTranslate ); tile.beingDrawn = true; if( tiledImage.viewer ){ @@ -1355,8 +1570,25 @@ function drawTiles( tiledImage, lastDrawn ) { tiledImage._drawer.restoreContext( useSketch ); } - if ( useSketch ) { - tiledImage._drawer.blendSketch( tiledImage.opacity ); + if (tiledImage.viewport.degrees !== 0 && !sketchScale) { + tiledImage._drawer._restoreRotationChanges(useSketch); + } + + if (useSketch) { + var offsetForRotation = tiledImage.viewport.degrees !== 0 && sketchScale; + if (offsetForRotation) { + tiledImage._drawer._offsetForRotation(tiledImage.viewport.degrees, false); + } + tiledImage._drawer.blendSketch({ + opacity: tiledImage.opacity, + scale: sketchScale, + translate: sketchTranslate, + compositeOperation: tiledImage.compositeOperation, + bounds: bounds + }); + if (offsetForRotation) { + tiledImage._drawer._restoreRotationChanges(false); + } } drawDebugInfo( tiledImage, lastDrawn ); } diff --git a/src/tilesource.js b/src/tilesource.js index b13df91d..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 ) ? @@ -230,8 +230,8 @@ $.TileSource = function( width, height, tileSize, tileOverlap, minLevel, maxLeve }; - -$.TileSource.prototype = /** @lends OpenSeadragon.TileSource.prototype */{ +/** @lends OpenSeadragon.TileSource.prototype */ +$.TileSource.prototype = { getTileSize: function( level ) { $.console.error( @@ -240,7 +240,7 @@ $.TileSource.prototype = /** @lends OpenSeadragon.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 = /** @lends OpenSeadragon.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 = /** @lends OpenSeadragon.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 = /** @lends OpenSeadragon.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 d60b5320..94dc6248 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:
+ *
    + *
  • var viewer = new OpenSeadragon.Viewer(options);
  • + *
  • var viewer = OpenSeadragon(options);
  • + *
* @class Viewer * @classdesc The main OpenSeadragon viewer class. * @@ -203,6 +208,8 @@ $.Viewer = function( options ) { this._loadQueue = []; this.currentOverlays = []; + this._lastScrollTime = $.now(); // variable used to help normalize the scroll event speed of different devices + //Inherit some behaviors and properties $.EventSource.call( this ); @@ -232,7 +239,9 @@ $.Viewer = function( options ) { style.left = "0px"; }(this.canvas.style)); $.setElementTouchActionNone( this.canvas ); - this.canvas.tabIndex = options.tabIndex || 0; + 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"; @@ -328,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()); } }); @@ -357,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({ @@ -408,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, @@ -605,6 +615,14 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, options.success = function(event) { successes++; + // TODO: now that options has other things besides tileSource, the overlays + // should probably be at the options level, not the tileSource level. + if (options.tileSource.overlays) { + for (var i = 0; i < options.tileSource.overlays.length; i++) { + _this.addOverlay(options.tileSource.overlays[i]); + } + } + if (originalSuccess) { originalSuccess(event); } @@ -628,14 +646,6 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, }; _this.addTiledImage(options); - - // TODO: now that options has other things besides tileSource, the overlays - // should probably be at the options level, not the tileSource level. - if (options.tileSource.overlays) { - for (var i = 0; i < options.tileSource.overlays.length; i++) { - _this.addOverlay(options.tileSource.overlays[i]); - } - } }; // TileSources @@ -777,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}). * @@ -908,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 @@ -971,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; @@ -1201,10 +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. @@ -1237,6 +1263,12 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, if (options.opacity === undefined) { options.opacity = this.opacity; } + 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 @@ -1286,18 +1318,20 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, } } - this._loadQueue.push(myQueueItem); - - getTileSourceImplementation( this, options.tileSource, function( tileSource ) { - - if ( tileSource instanceof Array ) { + if ($.isArray(options.tileSource)) { + setTimeout(function() { raiseAddItemFailed({ message: "[Viewer.addTiledImage] Sequences can not be added; add them one at a time instead.", - source: tileSource, + source: options.tileSource, options: options }); - return; - } + }); + return; + } + + this._loadQueue.push(myQueueItem); + + getTileSourceImplementation( this, options.tileSource, options, function( tileSource ) { myQueueItem.tileSource = tileSource; @@ -1330,9 +1364,12 @@ $.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, + compositeOperation: queueItem.options.compositeOperation, springStiffness: _this.springStiffness, animationTime: _this.animationTime, minZoomImageRatio: _this.minZoomImageRatio, @@ -1342,7 +1379,9 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, blendTime: _this.blendTime, alwaysBlend: _this.alwaysBlend, minPixelRatio: _this.minPixelRatio, - crossOriginPolicy: _this.crossOriginPolicy, + smoothTileEdgesMinZoom: _this.smoothTileEdgesMinZoom, + iOSDevice: _this.iOSDevice, + crossOriginPolicy: queueItem.options.crossOriginPolicy, debugMode: _this.debugMode }); @@ -1364,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 }); @@ -1383,6 +1423,31 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, } ); }, + /** + * Add a simple image to the viewer. + * The options are the same as the ones in {@link OpenSeadragon.Viewer#addTiledImage} + * except for options.tileSource which is replaced by options.url. + * @function + * @param {Object} options - See {@link OpenSeadragon.Viewer#addTiledImage} + * for all the options + * @param {String} options.url - The URL of the image to add. + * @fires OpenSeadragon.World.event:add-item + * @fires OpenSeadragon.Viewer.event:add-item-failed + */ + addSimpleImage: function(options) { + $.console.assert(options, "[Viewer.addSimpleImage] options is required"); + $.console.assert(options.url, "[Viewer.addSimpleImage] options.url is required"); + + var opts = $.extend({}, options, { + tileSource: { + type: 'image', + url: options.url + } + }); + delete opts.url; + this.addTiledImage(opts); + }, + // deprecated addLayer: function( options ) { var _this = this; @@ -1681,7 +1746,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, if( this.toolbar ){ this.toolbar.addControl( this.navControl, - {anchor: $.ControlAnchor.TOP_LEFT} + {anchor: this.navigationControlAnchor || $.ControlAnchor.TOP_LEFT} ); } else { this.addControl( @@ -1744,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 @@ -1789,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', { @@ -1807,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. @@ -1834,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', { @@ -1909,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 @@ -2027,25 +2115,46 @@ function _getSafeElemSize (oElement) { * @function * @private */ -function getTileSourceImplementation( viewer, tileSource, successCallback, +function getTileSourceImplementation( viewer, tileSource, imgOptions, successCallback, failCallback ) { var _this = viewer; //allow plain xml strings or json strings to be parsed here if ( $.type( tileSource ) == 'string' ) { - if ( tileSource.match( /\s*<.*/ ) ) { + //xml should start with "<" and end with ">" + if ( tileSource.match( /^\s*<.*>\s*$/ ) ) { tileSource = $.parseXml( tileSource ); - } else if ( tileSource.match( /\s*[\{\[].*/ ) ) { + //json should start with "{" or "[" and end with "}" or "]" + } else if ( tileSource.match(/^\s*[\{\[].*[\}\]]\s*$/ ) ) { tileSource = $.parseJSON(tileSource); } } + function waitUntilReady(tileSource, originalTileSource) { + if (tileSource.ready) { + successCallback(tileSource); + } else { + tileSource.addHandler('ready', function () { + successCallback(tileSource); + }); + tileSource.addHandler('open-failed', function (event) { + failCallback({ + message: event.message, + source: originalTileSource + }); + }); + } + } + setTimeout( function() { if ( $.type( tileSource ) == 'string' ) { //If its still a string it means it must be a url at this point tileSource = new $.TileSource({ url: tileSource, + crossOriginPolicy: imgOptions.crossOriginPolicy !== undefined ? + imgOptions.crossOriginPolicy : viewer.crossOriginPolicy, ajaxWithCredentials: viewer.ajaxWithCredentials, + useCanvas: viewer.useCanvas, success: function( event ) { successCallback( event.tileSource ); } @@ -2054,10 +2163,18 @@ function getTileSourceImplementation( viewer, tileSource, successCallback, failCallback( event ); } ); - } else if ( $.isPlainObject( tileSource ) || tileSource.nodeType ) { + } else if ($.isPlainObject(tileSource) || tileSource.nodeType) { + 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; } + if (tileSource.useCanvas === undefined) { + tileSource.useCanvas = viewer.useCanvas; + } if ( $.isFunction( tileSource.getTileUrl ) ) { //Custom tile source @@ -2075,14 +2192,13 @@ function getTileSourceImplementation( viewer, tileSource, successCallback, return; } var options = $TileSource.prototype.configure.apply( _this, [ tileSource ] ); - var readySource = new $TileSource( options ); - successCallback( readySource ); + waitUntilReady(new $TileSource(options), tileSource); } } else { //can assume it's already a tile source implementation - successCallback( tileSource ); + waitUntilReady(tileSource, tileSource); } - }, 1 ); + }); } function getOverlayObject( viewer, overlay ) { @@ -2111,37 +2227,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({ @@ -2149,7 +2256,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 }); } @@ -2293,6 +2403,7 @@ function onCanvasKeyDown( event ) { function onCanvasKeyPress( event ) { if ( !event.preventDefaultAction && !event.ctrl && !event.alt && !event.meta ) { switch( event.keyCode ){ + case 43://=|+ case 61://=|+ this.viewport.zoomBy(1.1); this.viewport.applyConstraints(); @@ -2461,22 +2572,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(); } @@ -2495,7 +2609,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, @@ -2737,44 +2851,60 @@ function onCanvasPinch( event ) { function onCanvasScroll( event ) { var gestureSettings, - factor; + factor, + thisScrollTime, + deltaScrollTime; - if ( !event.preventDefaultAction && this.viewport ) { - gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); - if ( gestureSettings.scrollToZoom ) { - factor = Math.pow( this.zoomPerScroll, event.scroll ); - this.viewport.zoomBy( - factor, - this.viewport.pointFromPixel( event.position, true ) - ); - this.viewport.applyConstraints(); + /* Certain scroll devices fire the scroll event way too fast so we are injecting a simple adjustment to keep things + * partially normalized. If we have already fired an event within the last 'minScrollDelta' milliseconds we skip + * this one and wait for the next event. */ + thisScrollTime = $.now(); + deltaScrollTime = thisScrollTime - this._lastScrollTime; + if (deltaScrollTime > this.minScrollDeltaTime) { + this._lastScrollTime = thisScrollTime; + + if ( !event.preventDefaultAction && this.viewport ) { + gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + if ( gestureSettings.scrollToZoom ) { + factor = Math.pow( this.zoomPerScroll, event.scroll ); + this.viewport.zoomBy( + factor, + this.viewport.pointFromPixel( event.position, true ) + ); + this.viewport.applyConstraints(); + } + } + /** + * Raised when a scroll event occurs on the {@link OpenSeadragon.Viewer#canvas} element (mouse wheel). + * + * @event canvas-scroll + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Number} scroll - The scroll delta for the event. + * @property {Boolean} shift - True if the shift key was pressed during this event. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'canvas-scroll', { + tracker: event.eventSource, + position: event.position, + scroll: event.scroll, + shift: event.shift, + originalEvent: event.originalEvent + }); + if (gestureSettings && gestureSettings.scrollToZoom) { + //cancels event + return false; } } - /** - * Raised when a scroll event occurs on the {@link OpenSeadragon.Viewer#canvas} element (mouse wheel). - * - * @event canvas-scroll - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. - * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. - * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. - * @property {Number} scroll - The scroll delta for the event. - * @property {Boolean} shift - True if the shift key was pressed during this event. - * @property {Object} originalEvent - The original DOM event. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - this.raiseEvent( 'canvas-scroll', { - tracker: event.eventSource, - position: event.position, - scroll: event.scroll, - shift: event.shift, - originalEvent: event.originalEvent - }); - - if (gestureSettings && gestureSettings.scrollToZoom) { - //cancels event - return false; + else { + gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + if (gestureSettings && gestureSettings.scrollToZoom) { + return false; // We are swallowing this event + } } } @@ -2866,33 +2996,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; } } @@ -2977,27 +3100,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 b882580e..2ac499d4 100644 --- a/src/viewport.js +++ b/src/viewport.js @@ -111,10 +111,7 @@ $.Viewport = function( options ) { }, options ); - this._containerInnerSize = new $.Point( - Math.max(1, this.containerSize.x - (this._margins.left + this._margins.right)), - Math.max(1, this.containerSize.y - (this._margins.top + this._margins.bottom)) - ); + this._updateContainerInnerSize(); this.centerSpringX = new $.Spring({ initial: 0, @@ -137,17 +134,14 @@ $.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(); }; -$.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ +/** @lends OpenSeadragon.Viewport.prototype */ +$.Viewport.prototype = { /** * Updates the viewport's home bounds and constraints for the given content size. * @function @@ -155,93 +149,112 @@ $.Viewport.prototype = /** @lends OpenSeadragon.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(); - 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"); - if( this.viewer ){ + 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(), + this.viewer.raiseEvent('reset-size', { + 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 ); @@ -252,8 +265,8 @@ $.Viewport.prototype = /** @lends OpenSeadragon.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}). * @@ -264,11 +277,11 @@ $.Viewport.prototype = /** @lends OpenSeadragon.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); }, /** @@ -289,8 +302,8 @@ $.Viewport.prototype = /** @lends OpenSeadragon.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() ); @@ -315,18 +328,61 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ }, /** + * 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() { + return $.extend({}, this._margins); // Make a copy so we are not returning our original + }, + + /** + * 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) { + $.console.assert($.type(margins) === 'object', '[Viewport.setMargins] margins must be an object'); + + this._margins = $.extend({ + left: 0, + top: 0, + right: 0, + bottom: 0 + }, margins); + + this._updateContainerInnerSize(); + 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 ); @@ -338,8 +394,19 @@ $.Viewport.prototype = /** @lends OpenSeadragon.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; @@ -407,6 +474,13 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ } }, + // private + _applyZoomConstraints: function(zoom) { + return Math.max( + Math.min(zoom, this.getMaxZoom()), + this.getMinZoom()); + }, + /** * @function * @private @@ -414,65 +488,50 @@ $.Viewport.prototype = /** @lends OpenSeadragon.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}). * @@ -492,40 +551,44 @@ $.Viewport.prototype = /** @lends OpenSeadragon.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); }, /** @@ -535,50 +598,37 @@ $.Viewport.prototype = /** @lends OpenSeadragon.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; @@ -588,58 +638,70 @@ $.Viewport.prototype = /** @lends OpenSeadragon.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 - } ); + }); }, /** @@ -647,11 +709,13 @@ $.Viewport.prototype = /** @lends OpenSeadragon.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); }, /** @@ -659,11 +723,13 @@ $.Viewport.prototype = /** @lends OpenSeadragon.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); }, @@ -679,7 +745,6 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ this.centerSpringX.target.value, this.centerSpringY.target.value ); - delta = delta.rotate( -this.degrees, new $.Point( 0, 0 ) ); return this.panTo( center.plus( delta ), immediately ); }, @@ -725,18 +790,18 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * @return {OpenSeadragon.Viewport} Chainable. * @fires OpenSeadragon.Viewer.event:zoom */ - zoomBy: function( factor, refPoint, immediately ) { - if( refPoint instanceof $.Point && !isNaN( refPoint.x ) && !isNaN( refPoint.y ) ) { - refPoint = refPoint.rotate( - -this.degrees, - new $.Point( this.centerSpringX.target.value, this.centerSpringY.target.value ) - ); - } - return this.zoomTo( this.zoomSpring.target.value * factor, refPoint, immediately ); + zoomBy: function(factor, refPoint, immediately) { + return this.zoomTo( + this.zoomSpring.target.value * factor, refPoint, immediately); }, /** + * 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 */ @@ -782,13 +847,19 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * @function * @return {OpenSeadragon.Viewport} Chainable. */ - setRotation: function( degrees ) { - if( !( this.viewer && this.viewer.drawer.canRotate() ) ) { + setRotation: function(degrees) { + if (!this.viewer || !this.viewer.drawer.canRotate()) { return this; } - degrees = ( degrees + 360 ) % 360; + degrees = degrees % 360; + if (degrees < 0) { + degrees += 360; + } this.degrees = degrees; + this._setContentBounds( + this.viewer.world.getHomeBounds(), + this.viewer.world.getContentFactor()); this.viewer.forceRedraw(); /** @@ -801,10 +872,7 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * @property {Number} degrees - The number of degrees the rotation was set to. * @property {?Object} userData - Arbitrary subscriber-defined object. */ - if (this.viewer !== null) - { - this.viewer.raiseEvent('rotate', {"degrees": degrees}); - } + this.viewer.raiseEvent('rotate', {"degrees": degrees}); return this; }, @@ -823,17 +891,14 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * @fires OpenSeadragon.Viewer.event:resize */ resize: function( newContainerSize, maintain ) { - var oldBounds = this.getBounds(), + var oldBounds = this.getBoundsNoRotate(), newBounds = oldBounds, widthDeltaFactor; this.containerSize.x = newContainerSize.x; this.containerSize.y = newContainerSize.y; - this._containerInnerSize = new $.Point( - Math.max(1, newContainerSize.x - (this._margins.left + this._margins.right)), - Math.max(1, newContainerSize.y - (this._margins.top + this._margins.bottom)) - ); + this._updateContainerInnerSize(); if ( maintain ) { // TODO: widthDeltaFactor will always be 1; probably not what's intended @@ -863,38 +928,46 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ return this.fitBounds( newBounds, true ); }, + // private + _updateContainerInnerSize: function() { + this._containerInnerSize = new $.Point( + Math.max(1, this.containerSize.x - (this._margins.left + this._margins.right)), + Math.max(1, this.containerSize.y - (this._margins.top + this._margins.bottom)) + ); + }, + /** + * 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; @@ -903,40 +976,90 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ return changed; }, - /** - * Convert a delta (translation vector) from pixels coordinates to viewport coordinates - * @function - * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + * Convert a delta (translation vector) from viewport coordinates to pixels + * coordinates. This method does not take rotation into account. + * Consider using deltaPixelsFromPoints if you need to account for rotation. + * @param {OpenSeadragon.Point} deltaPoints - The translation vector to convert. + * @param {Boolean} [current=false] - Pass true for the current location; + * defaults to false (target location). + * @returns {OpenSeadragon.Point} */ - deltaPixelsFromPoints: function( deltaPoints, current ) { + deltaPixelsFromPointsNoRotate: function(deltaPoints, current) { return deltaPoints.times( - this._containerInnerSize.x * this.getZoom( current ) + this._containerInnerSize.x * this.getZoom(current) ); }, /** - * Convert a delta (translation vector) from viewport coordinates to pixels coordinates. - * @function - * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + * Convert a delta (translation vector) from viewport coordinates to pixels + * coordinates. + * @param {OpenSeadragon.Point} deltaPoints - The translation vector to convert. + * @param {Boolean} [current=false] - Pass true for the current location; + * defaults to false (target location). + * @returns {OpenSeadragon.Point} */ - deltaPointsFromPixels: function( deltaPixels, current ) { + deltaPixelsFromPoints: function(deltaPoints, current) { + return this.deltaPixelsFromPointsNoRotate( + deltaPoints.rotate(this.getRotation()), + current); + }, + + /** + * Convert a delta (translation vector) from pixels coordinates to viewport + * coordinates. This method does not take rotation into account. + * Consider using deltaPointsFromPixels if you need to account for rotation. + * @param {OpenSeadragon.Point} deltaPixels - The translation vector to convert. + * @param {Boolean} [current=false] - Pass true for the current location; + * defaults to false (target location). + * @returns {OpenSeadragon.Point} + */ + deltaPointsFromPixelsNoRotate: function(deltaPixels, current) { return deltaPixels.divide( - this._containerInnerSize.x * this.getZoom( current ) + this._containerInnerSize.x * this.getZoom(current) ); }, /** - * Convert image pixel coordinates to viewport coordinates. - * @function - * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + * Convert a delta (translation vector) from pixels coordinates to viewport + * coordinates. + * @param {OpenSeadragon.Point} deltaPixels - The translation vector to convert. + * @param {Boolean} [current=false] - Pass true for the current location; + * defaults to false (target location). + * @returns {OpenSeadragon.Point} */ - pixelFromPoint: function( point, current ) { - return this._pixelFromPoint(point, this.getBounds( current )); + deltaPointsFromPixels: function(deltaPixels, current) { + return this.deltaPointsFromPixelsNoRotate(deltaPixels, current) + .rotate(-this.getRotation()); + }, + + /** + * Convert viewport coordinates to pixels coordinates. + * This method does not take rotation into account. + * Consider using pixelFromPoint if you need to account for rotation. + * @param {OpenSeadragon.Point} point the viewport coordinates + * @param {Boolean} [current=false] - Pass true for the current location; + * defaults to false (target location). + * @returns {OpenSeadragon.Point} + */ + pixelFromPointNoRotate: function(point, current) { + return this._pixelFromPointNoRotate( + point, this.getBoundsNoRotate(current)); + }, + + /** + * Convert viewport coordinates to pixel coordinates. + * @param {OpenSeadragon.Point} point the viewport coordinates + * @param {Boolean} [current=false] - Pass true for the current location; + * defaults to false (target location). + * @returns {OpenSeadragon.Point} + */ + pixelFromPoint: function(point, current) { + return this._pixelFromPoint(point, this.getBoundsNoRotate(current)); }, // private - _pixelFromPoint: function( point, bounds ) { + _pixelFromPointNoRotate: function(point, bounds) { return point.minus( bounds.getTopLeft() ).times( @@ -946,13 +1069,24 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ ); }, + // private + _pixelFromPoint: function(point, bounds) { + return this._pixelFromPointNoRotate( + point.rotate(this.getRotation(), this.getCenter(true)), + bounds); + }, + /** - * Convert viewport coordinates to image pixel coordinates. - * @function - * @param {Boolean} current - Pass true for the current location; defaults to false (target location). + * Convert pixel coordinates to viewport coordinates. + * This method does not take rotation into account. + * Consider using pointFromPixel if you need to account for rotation. + * @param {OpenSeadragon.Point} pixel Pixel coordinates + * @param {Boolean} [current=false] - Pass true for the current location; + * defaults to false (target location). + * @returns {OpenSeadragon.Point} */ - pointFromPixel: function( pixel, current ) { - var bounds = this.getBounds( current ); + pointFromPixelNoRotate: function(pixel, current) { + var bounds = this.getBoundsNoRotate(current); return pixel.minus( new $.Point(this._margins.left, this._margins.top) ).divide( @@ -962,11 +1096,26 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ ); }, + /** + * Convert pixel coordinates to viewport coordinates. + * @param {OpenSeadragon.Point} pixel Pixel coordinates + * @param {Boolean} [current=false] - Pass true for the current location; + * defaults to false (target location). + * @returns {OpenSeadragon.Point} + */ + pointFromPixel: function(pixel, current) { + return this.pointFromPixelNoRotate(pixel, current).rotate( + -this.getRotation(), + this.getCenter(true) + ); + }, + // 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); }, /** @@ -975,29 +1124,42 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * OpenSeadragon.Point * Note: not accurate with multi-image; use TiledImage.viewportToImageCoordinates instead. * @function - * @param {OpenSeadragon.Point} viewerX the point in viewport coordinate system. - * @param {Number} viewerX X coordinate in viewport coordinate system. - * @param {Number} viewerY Y coordinate in viewport coordinate system. + * @param {(OpenSeadragon.Point|Number)} viewerX either a point or the X + * coordinate in viewport coordinate system. + * @param {Number} [viewerY] Y coordinate in viewport coordinate system. * @return {OpenSeadragon.Point} a point representing the coordinates in the image. */ - viewportToImageCoordinates: function( viewerX, viewerY ) { - if ( arguments.length == 1 ) { + viewportToImageCoordinates: function(viewerX, viewerY) { + if (viewerX instanceof $.Point) { //they passed a point instead of individual components - return this.viewportToImageCoordinates( viewerX.x, viewerX.y ); + 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); }, /** @@ -1006,24 +1168,34 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * OpenSeadragon.Point * Note: not accurate with multi-image; use TiledImage.imageToViewportCoordinates instead. * @function - * @param {OpenSeadragon.Point} imageX the point in image coordinate system. - * @param {Number} imageX X coordinate in image coordinate system. - * @param {Number} imageY Y coordinate in image coordinate system. + * @param {(OpenSeadragon.Point | Number)} imageX the point or the + * X coordinate in image coordinate system. + * @param {Number} [imageY] Y coordinate in image coordinate system. * @return {OpenSeadragon.Point} a point representing the coordinates in the viewport. */ - imageToViewportCoordinates: function( imageX, imageY ) { - if ( arguments.length == 1 ) { + imageToViewportCoordinates: function(imageX, imageY) { + if (imageX instanceof $.Point) { //they passed a point instead of individual components - return this.imageToViewportCoordinates( imageX.x, imageX.y ); + 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; }, @@ -1034,37 +1206,43 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * OpenSeadragon.Rect * Note: not accurate with multi-image; use TiledImage.imageToViewportRectangle instead. * @function - * @param {OpenSeadragon.Rect} imageX the rectangle in image coordinate system. - * @param {Number} imageX the X coordinate of the top left corner of the rectangle + * @param {(OpenSeadragon.Rect | Number)} imageX the rectangle or the X + * coordinate of the top left corner of the rectangle in image coordinate system. + * @param {Number} [imageY] the Y coordinate of the top left corner of the rectangle * in image coordinate system. - * @param {Number} imageY the Y coordinate of the top left corner of the rectangle - * in image coordinate system. - * @param {Number} pixelWidth the width in pixel of the rectangle. - * @param {Number} pixelHeight the height in pixel of the rectangle. + * @param {Number} [pixelWidth] the width in pixel of the rectangle. + * @param {Number} [pixelHeight] the height in pixel of the rectangle. */ - imageToViewportRectangle: function( imageX, imageY, pixelWidth, pixelHeight ) { - var coordA, - coordB, - rect; - if( arguments.length == 1 ) { - //they passed a rectangle instead of individual components - rect = imageX; - return this.imageToViewportRectangle( - rect.x, rect.y, rect.width, rect.height - ); + imageToViewportRectangle: function(imageX, imageY, pixelWidth, pixelHeight) { + var rect = imageX; + if (!(rect instanceof $.Rect)) { + //they passed individual components instead of a rectangle + rect = new $.Rect(imageX, imageY, pixelWidth, pixelHeight); } - coordA = this.imageToViewportCoordinates( - imageX, imageY - ); - coordB = this._imageToViewportDelta( - 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( coordA.x, coordA.y, coordB.x, - coordB.y + coordB.y, + rect.degrees ); }, @@ -1075,33 +1253,44 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * OpenSeadragon.Rect * Note: not accurate with multi-image; use TiledImage.viewportToImageRectangle instead. * @function - * @param {OpenSeadragon.Rect} viewerX the rectangle in viewport coordinate system. - * @param {Number} viewerX the X coordinate of the top left corner of the rectangle + * @param {(OpenSeadragon.Rect | Number)} viewerX either a rectangle or + * the X coordinate of the top left corner of the rectangle in viewport + * coordinate system. + * @param {Number} [viewerY] the Y coordinate of the top left corner of the rectangle * in viewport coordinate system. - * @param {Number} imageY the Y coordinate of the top left corner of the rectangle - * in viewport coordinate system. - * @param {Number} pointWidth the width of the rectangle in viewport coordinate system. - * @param {Number} pointHeight the height of the rectangle in viewport coordinate system. + * @param {Number} [pointWidth] the width of the rectangle in viewport coordinate system. + * @param {Number} [pointHeight] the height of the rectangle in viewport coordinate system. */ - viewportToImageRectangle: function( viewerX, viewerY, pointWidth, pointHeight ) { - var coordA, - coordB, - rect; - if ( arguments.length == 1 ) { - //they passed a rectangle instead of individual components - rect = viewerX; - return this.viewportToImageRectangle( - rect.x, rect.y, rect.width, rect.height - ); + viewportToImageRectangle: function(viewerX, viewerY, pointWidth, pointHeight) { + var rect = viewerX; + if (!(rect instanceof $.Rect)) { + //they passed individual components instead of a rectangle + rect = new $.Rect(viewerX, viewerY, pointWidth, pointHeight); } - coordA = this.viewportToImageCoordinates( viewerX, viewerY ); - coordB = this._viewportToImageDelta(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( coordA.x, coordA.y, coordB.x, - coordB.y + coordB.y, + rect.degrees ); }, @@ -1135,10 +1324,12 @@ $.Viewport.prototype = /** @lends OpenSeadragon.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); }, /** @@ -1147,10 +1338,12 @@ $.Viewport.prototype = /** @lends OpenSeadragon.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)); }, /** @@ -1173,15 +1366,45 @@ $.Viewport.prototype = /** @lends OpenSeadragon.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); }, /** @@ -1189,10 +1412,12 @@ $.Viewport.prototype = /** @lends OpenSeadragon.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)); }, /** @@ -1207,14 +1432,24 @@ $.Viewport.prototype = /** @lends OpenSeadragon.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; }, @@ -1231,14 +1466,24 @@ $.Viewport.prototype = /** @lends OpenSeadragon.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 65cc2818..b04d5fda 100644 --- a/test/coverage.html +++ b/test/coverage.html @@ -1,4 +1,4 @@ - + @@ -22,6 +22,7 @@ + @@ -32,6 +33,7 @@ + @@ -51,6 +53,7 @@ + diff --git a/test/data/iiif_2_0_sizes/full/1600,/0/default.jpg b/test/data/iiif_2_0_sizes/full/1600,/0/default.jpg new file mode 100644 index 00000000..9156c9d8 Binary files /dev/null and b/test/data/iiif_2_0_sizes/full/1600,/0/default.jpg differ diff --git a/test/data/iiif_2_0_sizes/full/3200,/0/default.jpg b/test/data/iiif_2_0_sizes/full/3200,/0/default.jpg new file mode 100644 index 00000000..7cad3870 Binary files /dev/null and b/test/data/iiif_2_0_sizes/full/3200,/0/default.jpg differ diff --git a/test/data/iiif_2_0_sizes/full/400,/0/default.jpg b/test/data/iiif_2_0_sizes/full/400,/0/default.jpg new file mode 100644 index 00000000..6d2433df Binary files /dev/null and b/test/data/iiif_2_0_sizes/full/400,/0/default.jpg differ diff --git a/test/data/iiif_2_0_sizes/full/6976,/0/default.jpg b/test/data/iiif_2_0_sizes/full/6976,/0/default.jpg new file mode 100644 index 00000000..8e627bbc Binary files /dev/null and b/test/data/iiif_2_0_sizes/full/6976,/0/default.jpg differ diff --git a/test/data/iiif_2_0_sizes/full/800,/0/default.jpg b/test/data/iiif_2_0_sizes/full/800,/0/default.jpg new file mode 100644 index 00000000..b574b541 Binary files /dev/null and b/test/data/iiif_2_0_sizes/full/800,/0/default.jpg differ diff --git a/test/data/iiif_2_0_sizes/info.json b/test/data/iiif_2_0_sizes/info.json new file mode 100644 index 00000000..c78e059b --- /dev/null +++ b/test/data/iiif_2_0_sizes/info.json @@ -0,0 +1,15 @@ +{ + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "http://localhost:8000/test/data/iiif_2_0_sizes", + "protocol": "http://iiif.io/api/image", + "width": 6976, + "height": 5074, + "profile": ["http://iiif.io/api/image/2/level0.json"], + "sizes" : [ + {"width" : 400, "height" : 291}, + {"width" : 800, "height" : 582}, + {"width" : 1600, "height" : 1164}, + {"width" : 3200, "height": 2328}, + {"width" : 6976, "height": 5074} + ] +} diff --git a/test/demo/basic.html b/test/demo/basic.html index 19572b86..e238e5e1 100644 --- a/test/demo/basic.html +++ b/test/demo/basic.html @@ -6,10 +6,10 @@ diff --git a/test/demo/coordinates.html b/test/demo/coordinates.html index 188e87ee..3cf836dd 100644 --- a/test/demo/coordinates.html +++ b/test/demo/coordinates.html @@ -24,38 +24,55 @@ Window (pixel) Container (pixel) - Image 1 - top left (pixel) - Image 2 - bottom right (pixel) Viewport (point) + Big Image (pixel) + Small Image (pixel) Cursor position - - - - - + + + + + + + + Big Image top left position + + + + + + + + Small Image top left position + + + + + Zoom - - + - + diff --git a/test/demo/iiif-sizes.html b/test/demo/iiif-sizes.html new file mode 100644 index 00000000..e9785556 --- /dev/null +++ b/test/demo/iiif-sizes.html @@ -0,0 +1,51 @@ + + + + OpenSeadragon Demo - IIIF emulation of legacy image pyramid + + + + + +
+

Default OpenSeadragon viewer from IIIF Source

+

This allows IIIF even if you only have a handful of static image sizes.

+
+ + +
+ + + diff --git a/test/demo/iiif.html b/test/demo/iiif.html new file mode 100644 index 00000000..5d978df3 --- /dev/null +++ b/test/demo/iiif.html @@ -0,0 +1,34 @@ + + + + OpenSeadragon Demo - IIIF Tiled + + + + + +
+

Default OpenSeadragon viewer from IIIF Tile Source.

+

This depends on a remote server providing a IIIF Image API endpoint.

+
+
+ + + diff --git a/test/demo/legacy.html b/test/demo/legacy.html new file mode 100644 index 00000000..0c13a2b4 --- /dev/null +++ b/test/demo/legacy.html @@ -0,0 +1,56 @@ + + + + OpenSeadragon Demo - Legacy image pyramid + + + + + +
+ Use an array of full images at different sizes. +
+
+ + + diff --git a/test/demo/overlay.html b/test/demo/overlay.html index 008ad946..527ebef3 100644 --- a/test/demo/overlay.html +++ b/test/demo/overlay.html @@ -1,41 +1,99 @@ - OpenSeadragon Overlay Demo - - - + OpenSeadragon Overlay Demo + + + -
-
- - +
+
+ + + 0deg +
+ diff --git a/test/helpers/test.js b/test/helpers/test.js index 54a28412..3dbb5468 100644 --- a/test/helpers/test.js +++ b/test/helpers/test.js @@ -68,6 +68,24 @@ ok( Util.equalsWithVariance( value1, value2, variance ), message + " Expected:" + value1 + " Found: " + value2 + " Variance: " + variance ); }, + // ---------- + assertPointsEquals: function (pointA, pointB, precision, message) { + Util.assessNumericValue(pointA.x, pointB.x, precision, message + " x: "); + Util.assessNumericValue(pointA.y, pointB.y, precision, message + " y: "); + }, + + // ---------- + assertRectangleEquals: function (rectA, rectB, precision, message) { + Util.assessNumericValue(rectA.x, rectB.x, precision, message + " x: "); + Util.assessNumericValue(rectA.y, rectB.y, precision, message + " y: "); + Util.assessNumericValue(rectA.width, rectB.width, precision, + message + " width: "); + Util.assessNumericValue(rectA.height, rectB.height, precision, + message + " height: "); + Util.assessNumericValue(rectA.degrees, rectB.degrees, precision, + message + " degrees: "); + }, + // ---------- timeWatcher: function ( time ) { time = time || 2000; diff --git a/test/helpers/touch.js b/test/helpers/touch.js new file mode 100644 index 00000000..ad86ceaa --- /dev/null +++ b/test/helpers/touch.js @@ -0,0 +1,134 @@ +/* global TouchUtil, $ */ + +(function () { + + var touches, + identifier, + target; + + // ---------- + window.TouchUtil = { + reset: function () { + touches = []; + identifier = 0; + }, + + initTracker: function ( tracker ) { + // for testing in other touch-enabled browsers + if ( !('ontouchstart' in window) ) { + tracker.setTracking( false ); + OpenSeadragon.MouseTracker.subscribeEvents.push( 'touchstart', 'touchend' ); + tracker.setTracking( true ); + } + + target = tracker.element; + }, + + resetTracker: function ( tracker ) { + // for testing in other touch-enabled browsers + if ( !('ontouchstart' in window) ) { + tracker.setTracking( false ); + ['touchstart', 'touchend'].forEach(function ( type ) { + var index = OpenSeadragon.MouseTracker.subscribeEvents.indexOf( type ); + if ( index > -1 ) { + OpenSeadragon.MouseTracker.subscribeEvents.splice( index, 1 ); + } + }); + tracker.setTracking( true ); + } + + target = null; + }, + + start: function () { + var touch, + event, + newTouches = []; + + for ( var i = 0; i < arguments.length; i++ ) { + touch = createTouch( + target.offsetLeft + arguments[ i ][ 0 ], + target.offsetTop + arguments[ i ][ 1 ] + ); + + touches.push( touch ); + newTouches.push( touch ); + } + + event = createTouchEvent( 'touchstart', newTouches ); + target.dispatchEvent( event ); + return newTouches.length === 1 ? newTouches[ 0 ] : newTouches; + }, + + end: function ( changedTouches ) { + if ( !$.isArray( changedTouches ) ) { + changedTouches = [ changedTouches ]; + } + + var event; + touches = touches.filter(function ( touch ) { + return changedTouches.indexOf( touch ) === -1; + }); + + event = createTouchEvent( 'touchend', changedTouches ); + target.dispatchEvent( event ); + } + + }; + + // ---------- + function createTouch( x, y ) { + try { + // new spec + return new Touch({ + identifier: identifier++, + target: target, + pageX: target.offsetLeft + x, + pageY: target.offsetTop + y + } ); + } catch (e) { + // legacy + return document.createTouch( window, target, identifier++, x, y, x, y ); + } + } + + function createTouchList( touches ) { + // legacy + return document.createTouchList.apply( document, touches ); + } + + function createTouchEvent( type, changedTouches ) { + try { + // new spec + return new TouchEvent( type, { + view: window, + bubbles: true, + cancelable: true, + touches: touches, + targetTouches: touches, + changedTouches: changedTouches + } ); + } catch (e) { + // legacy + var touchEvent = document.createEvent( 'TouchEvent' ); + var touch1 = changedTouches[ 0 ]; + touchEvent.initTouchEvent( + createTouchList( touches ), // touches + createTouchList( touches ), // targetTouches + createTouchList( changedTouches ), // changedTouches + type, // type + window, // view + touch1.screenX, // screenX + touch1.screenY, // screenY + touch1.clientX, // clientX + touch1.clientY, // clientY + false, // ctrlKey + false, // altKey + false, // shiftKey + false // metaKey + ); + return touchEvent; + } + } + +})(); diff --git a/test/modules/basic.js b/test/modules/basic.js index 7f095777..c6164560 100644 --- a/test/modules/basic.js +++ b/test/modules/basic.js @@ -314,26 +314,15 @@ var canvas = document.createElement("canvas"); var ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); - callback(!isCanvasTainted(ctx)); + callback(!OpenSeadragon.isCanvasTainted(canvas)); }; img.src = corsImg; } - function isCanvasTainted(context) { - var isTainted = false; - try { - // We test if the canvas is tainted by retrieving data from it. - // An exception will be raised if the canvas is tainted. - var url = context.getImageData(0, 0, 1, 1); - } catch (e) { - isTainted = true; - } - return isTainted; - } - asyncTest( 'CrossOriginPolicyMissing', function () { viewer.crossOriginPolicy = false; + viewer.smoothTileEdgesMinZoom = Infinity; viewer.open( { type: 'legacy-image-pyramid', levels: [ { @@ -343,7 +332,8 @@ } ] } ); viewer.addHandler('tile-drawn', function() { - ok(isCanvasTainted(viewer.drawer.context), "Canvas should be tainted."); + ok(OpenSeadragon.isCanvasTainted(viewer.drawer.context.canvas), + "Canvas should be tainted."); start(); }); @@ -366,7 +356,8 @@ } ] } ); viewer.addHandler('tile-drawn', function() { - ok(!isCanvasTainted(viewer.drawer.context), "Canvas should not be tainted."); + ok(!OpenSeadragon.isCanvasTainted(viewer.drawer.context.canvas), + "Canvas should not be tainted."); start(); }); } @@ -374,4 +365,62 @@ } ); + asyncTest( 'CrossOriginPolicyOption', function () { + + browserSupportsImgCrossOrigin(function(supported) { + if (!supported) { + expect(0); + start(); + } else { + viewer.crossOriginPolicy = "Anonymous"; + viewer.smoothTileEdgesMinZoom = Infinity; + viewer.addTiledImage( { + tileSource: { + type: 'legacy-image-pyramid', + levels: [ { + url: corsImg, + width: 135, + height: 155 + } ] + }, + crossOriginPolicy : false + } ); + viewer.addHandler('tile-drawn', function() { + ok(OpenSeadragon.isCanvasTainted(viewer.drawer.context.canvas), + "Canvas should be tainted."); + start(); + }); + } + }); + + } ); + asyncTest( 'CrossOriginPolicyTileSource', function () { + + browserSupportsImgCrossOrigin(function(supported) { + if (!supported) { + expect(0); + start(); + } else { + viewer.crossOriginPolicy = false; + viewer.smoothTileEdgesMinZoom = Infinity; + viewer.addTiledImage( { + tileSource: { + type: 'legacy-image-pyramid', + levels: [ { + url: corsImg, + width: 135, + height: 155 + } ], + crossOriginPolicy : "Anonymous" + } + } ); + viewer.addHandler('tile-drawn', function() { + ok(!OpenSeadragon.isCanvasTainted(viewer.drawer.context.canvas), + "Canvas should not be tainted."); + start(); + }); + } + }); + + } ); })(); diff --git a/test/modules/dzitilesource.js b/test/modules/dzitilesource.js new file mode 100644 index 00000000..fe4677a0 --- /dev/null +++ b/test/modules/dzitilesource.js @@ -0,0 +1,38 @@ + +/*global module:true, test:true, equal:true, OpenSeadragon:true*/ + +(function() { + + module('DziTileSource', { + setup: function() { + testLog.reset(); + } + }); + + function testImplicitTilesUrl(dziUrl, expected, msg) { + var source = new OpenSeadragon.DziTileSource(); + var options = source.configure({ + Image: {Size: {Width:0, Height: 0}} + }, dziUrl); + equal(options.tilesUrl, expected, msg); + } + + test('test implicit tilesUrl guessed from dzi url', function() { + testImplicitTilesUrl( + '/path/my.dzi', '/path/my_files/', + 'dzi extension should be stripped'); + testImplicitTilesUrl( + '/path/my', '/path/my_files/', + 'no extension should still produce _files path'); + testImplicitTilesUrl( + '/my/', '/my_files/', + 'no extension with trailing slash should preserve slash'); + testImplicitTilesUrl( + 'my.xml', 'my_files/', + 'relative link should stay the same'); + testImplicitTilesUrl( + '/p/foo.dzi?a=1&b=2', '/p/foo_files/', + 'querystring in dzi url should be ignored'); + }); + +}()); diff --git a/test/modules/events.js b/test/modules/events.js index 62caa072..7e681d05 100644 --- a/test/modules/events.js +++ b/test/modules/events.js @@ -1,4 +1,4 @@ -/* global module, asyncTest, $, ok, equal, notEqual, start, test, Util, testLog */ +/* global module, asyncTest, $, ok, equal, notEqual, start, test, TouchUtil, Util, testLog */ (function () { var viewer; @@ -678,45 +678,96 @@ } ); // ---------- - asyncTest( 'Viewer: preventDefaultAction', function () { - var $canvas = $( viewer.element ).find( '.openseadragon-canvas' ).not( '.navigator .openseadragon-canvas' ), - tracker = viewer.innerTracker, - origClickHandler, - origDragHandler, - dragCount = 10, - originalZoom = 0, - originalBounds = null; + if ('TouchEvent' in window) { + asyncTest( 'MouseTracker: touch events', function () { + var $canvas = $( viewer.element ).find( '.openseadragon-canvas' ).not( '.navigator .openseadragon-canvas' ), + tracker = viewer.innerTracker, + touches; - var onOpen = function ( event ) { - viewer.removeHandler( 'open', onOpen ); - - // Hook viewer events to set preventDefaultAction - origClickHandler = tracker.clickHandler; - tracker.clickHandler = function ( event ) { - event.preventDefaultAction = true; - return origClickHandler( event ); - }; - origDragHandler = tracker.dragHandler; - tracker.dragHandler = function ( event ) { - event.preventDefaultAction = true; - return origDragHandler( event ); + var reset = function () { + touches = []; + TouchUtil.reset(); }; - originalZoom = viewer.viewport.getZoom(); - originalBounds = viewer.viewport.getBounds(); - - var event = { - clientX:1, - clientY:1 + var assessTouchExpectations = function ( expected ) { + var pointersList = tracker.getActivePointersListByType( 'touch' ); + if ('captureCount' in expected) { + equal( pointersList.captureCount, expected.captureCount, expected.description + 'Pointer capture count matches expected (' + expected.captureCount + ')' ); + } + if ('contacts' in expected) { + equal( pointersList.contacts, expected.contacts, expected.description + 'Pointer contact count matches expected (' + expected.contacts + ')' ); + } + if ('trackedPointers' in expected) { + equal( pointersList.getLength(), expected.trackedPointers, expected.description + 'Tracked pointer count matches expected (' + expected.trackedPointers + ')' ); + } }; - $canvas.simulate( 'focus', event ); + var onOpen = function ( event ) { + viewer.removeHandler( 'open', onOpen ); + + TouchUtil.initTracker( tracker ); + + // start-end-end (multi-touch start event) + reset(); + touches = TouchUtil.start( [0,0], [20,20] ); + assessTouchExpectations({ + description: 'start-end-end (multi-touch start event) [capture]: ', + captureCount: 2, + contacts: 2, + trackedPointers: 2 + }); + TouchUtil.end( touches[1] ); + TouchUtil.end( touches[0] ); + assessTouchExpectations({ + description: 'start-end-end (multi-touch start event) [release]: ', + captureCount: 0, + contacts: 0, + trackedPointers: 0 + }); + + // start-start-end (multi-touch end event) + reset(); + touches.push( TouchUtil.start([0, 0]) ); + touches.push( TouchUtil.start([20, 20]) ); + assessTouchExpectations({ + description: 'start-start-end (multi-touch end event) [capture]: ', + captureCount: 2, + contacts: 2, + trackedPointers: 2 + }); + TouchUtil.end( touches ); + assessTouchExpectations({ + description: 'start-start-end (multi-touch end event) [release]: ', + captureCount: 0, + contacts: 0, + trackedPointers: 0 + }); + + TouchUtil.resetTracker( tracker ); + viewer.close(); + start(); + }; + + viewer.addHandler( 'open', onOpen ); + viewer.open( '/test/data/testpattern.dzi' ); + } ); + } + + // ---------- + asyncTest('Viewer: preventDefaultAction', function() { + var $canvas = $(viewer.element).find('.openseadragon-canvas') + .not('.navigator .openseadragon-canvas'); + var tracker = viewer.innerTracker; + var epsilon = 0.0000001; + + function simulateClickAndDrag() { + $canvas.simulate('focus'); // Drag to pan Util.simulateViewerClickWithDrag( { viewer: viewer, widthFactor: 0.25, heightFactor: 0.25, - dragCount: dragCount, + dragCount: 10, dragDx: 1, dragDy: 1 } ); @@ -729,21 +780,58 @@ dragDx: 0, dragDy: 0 } ); - $canvas.simulate( 'blur', event ); + $canvas.simulate('blur'); + } - var zoom = viewer.viewport.getZoom(), - bounds = viewer.viewport.getBounds(); + var onOpen = function() { + viewer.removeHandler('open', onOpen); - equal( zoom, originalZoom, "Zoom prevented" ); - ok( bounds.x == originalBounds.x && bounds.y == originalBounds.y, 'Pan prevented' ); + // Hook viewer events to set preventDefaultAction + var origClickHandler = tracker.clickHandler; + tracker.clickHandler = function(event) { + event.preventDefaultAction = true; + return origClickHandler(event); + }; + var origDragHandler = tracker.dragHandler; + tracker.dragHandler = function(event) { + event.preventDefaultAction = true; + return origDragHandler(event); + }; + + var originalZoom = viewer.viewport.getZoom(); + var originalBounds = viewer.viewport.getBounds(); + + simulateClickAndDrag(); + + var zoom = viewer.viewport.getZoom(); + var bounds = viewer.viewport.getBounds(); + Util.assessNumericValue(zoom, originalZoom, epsilon, + "Zoom should be prevented"); + Util.assertRectangleEquals(bounds, originalBounds, epsilon, + 'Pan should be prevented'); + + tracker.clickHandler = origClickHandler; + tracker.dragHandler = origDragHandler; + + simulateClickAndDrag(); + + var zoom = viewer.viewport.getZoom(); + var bounds = viewer.viewport.getBounds(); + Util.assessNumericValue(zoom, 0.002, epsilon, + "Zoom should not be prevented"); + Util.assertRectangleEquals( + new OpenSeadragon.Rect(-249.5, -0.25, 500, 0.5), + bounds, + epsilon, + 'Pan should not be prevented'); viewer.close(); start(); }; - viewer.addHandler( 'open', onOpen ); - viewer.open( '/test/data/testpattern.dzi' ); - } ); + viewer.addHandler('open', onOpen); + viewer.open('/test/data/testpattern.dzi'); + }); // ---------- asyncTest( 'EventSource/MouseTracker/Viewer: event.originalEvent event.userData canvas-drag canvas-drag-end canvas-release canvas-click', function () { @@ -949,6 +1037,59 @@ viewer.open( '/test/data/testpattern.dzi' ); } ); + // ---------- + test('EventSource: addOnceHandler', function() { + var eventSource = new OpenSeadragon.EventSource(); + var userData = 'data'; + var eventData = { + foo: 1 + }; + var handlerCalledCount = 0; + eventSource.addOnceHandler('test-event', function(event) { + handlerCalledCount++; + strictEqual(event.foo, eventData.foo, + 'Event data should be transmitted to the event.'); + strictEqual(event.userData, userData, + 'User data should be transmitted to the event.'); + }, userData); + strictEqual(0, handlerCalledCount, + 'Handler should not have been called yet.'); + eventSource.raiseEvent('test-event', eventData); + strictEqual(1, handlerCalledCount, + 'Handler should have been called once.'); + eventSource.raiseEvent('test-event', eventData); + strictEqual(1, handlerCalledCount, + 'Handler should still have been called once.'); + }); + + // ---------- + test('EventSource: addOnceHandler 2 times', function() { + var eventSource = new OpenSeadragon.EventSource(); + var userData = 'data'; + var eventData = { + foo: 1 + }; + var handlerCalledCount = 0; + eventSource.addOnceHandler('test-event', function(event) { + handlerCalledCount++; + strictEqual(event.foo, eventData.foo, + 'Event data should be transmitted to the event.'); + strictEqual(event.userData, userData, + 'User data should be transmitted to the event.'); + }, userData, 2); + strictEqual(0, handlerCalledCount, + 'Handler should not have been called yet.'); + eventSource.raiseEvent('test-event', eventData); + strictEqual(1, handlerCalledCount, + 'Handler should have been called once.'); + eventSource.raiseEvent('test-event', eventData); + strictEqual(2, handlerCalledCount, + 'Handler should have been called twice.'); + eventSource.raiseEvent('test-event', eventData); + strictEqual(2, handlerCalledCount, + 'Handler should still have been called twice.'); + }); + // ---------- asyncTest( 'Viewer: tile-drawing event', function () { var tileDrawing = function ( event ) { diff --git a/test/modules/formats.js b/test/modules/formats.js index 2cef3576..f0edea75 100644 --- a/test/modules/formats.js +++ b/test/modules/formats.js @@ -114,6 +114,11 @@ testOpenUrl('iiif_2_0_tiled/info.json'); }); + // ---------- + asyncTest('IIIF 2.0 JSON, sizes array only', function() { + testOpenUrl('iiif_2_0_sizes/info.json'); + }); + // ---------- asyncTest('IIIF 2.0 JSON String', function() { testOpen( @@ -142,4 +147,45 @@ '}'); }); + // ---------- + asyncTest('ImageTileSource', function () { + testOpen({ + type: "image", + url: "/test/data/A.png" + }); + }); + + + // ---------- + asyncTest('Legacy Image Pyramid', function() { + // Although it is using image paths that happen to be in IIIF format, this is not a IIIFTileSource. + // The url values are opaque, just image locations. + // When emulating a legacy pyramid, IIIFTileSource calls functions from LegacyTileSource, so this + // adds a test for the legacy functionality too. + testOpen({ + type: 'legacy-image-pyramid', + levels:[{ + url: '/test/data/iiif_2_0_sizes/full/400,/0/default.jpg', + height: 291, + width: 400 + },{ + url: '/test/data/iiif_2_0_sizes/full/800,/0/default.jpg', + height: 582, + width: 800 + },{ + url: '/test/data/iiif_2_0_sizes/full/1600,/0/default.jpg', + height: 1164, + width: 1600 + },{ + url: '/test/data/iiif_2_0_sizes/full/3200,/0/default.jpg', + height: 2328, + width: 3200 + },{ + url: '/test/data/iiif_2_0_sizes/full/6976,/0/default.jpg', + height: 5074, + width: 6976 + }] + }); + }); + })(); diff --git a/test/modules/multi-image.js b/test/modules/multi-image.js index 5d4b21bd..4cbd5211 100644 --- a/test/modules/multi-image.js +++ b/test/modules/multi-image.js @@ -5,12 +5,12 @@ module( 'Multi-Image', { setup: function() { - $( '
' ).appendTo( "#qunit-fixture" ); + $( '
' ).appendTo( "#qunit-fixture" ); testLog.reset(); viewer = OpenSeadragon( { - id: 'itemsexample', + id: 'example', prefixUrl: '/build/openseadragon/images/', springStiffness: 100 // Faster animation = faster tests }); @@ -21,7 +21,7 @@ } viewer = null; - $( "#itemsexample" ).remove(); + $("#example").remove(); } } ); @@ -189,4 +189,83 @@ ]); }); + asyncTest('Viewer.addSimpleImage', function() { + viewer.addHandler("open", function openHandler() { + viewer.removeHandler("open", openHandler); + + viewer.world.addHandler('add-item', function itemAdded(event) { + viewer.world.removeHandler('add-item', itemAdded); + equal(event.item.opacity, 0.5, + 'Opacity option should be set when using addSimpleImage'); + start(); + }); + + viewer.addSimpleImage({ + url: '/test/data/A.png', + opacity: 0.5 + }); + }); + viewer.open('/test/data/testpattern.dzi'); + }); + + asyncTest('Transparent image on top of others', function() { + viewer.open('/test/data/testpattern.dzi'); + + var density = OpenSeadragon.pixelDensityRatio; + + viewer.addHandler('open', function() { + var firstImage = viewer.world.getItemAt(0); + firstImage.addHandler('fully-loaded-change', function() { + var imageData = viewer.drawer.context.getImageData(0, 0, + 500 * OpenSeadragon.pixelDensityRatio, 500 * density); + + // Pixel 250,250 will be in the hole of the A + var expectedVal = getPixelValue(imageData, 250 * density, 250 * density); + + notEqual(expectedVal.r, 0, 'Red channel should not be 0'); + notEqual(expectedVal.g, 0, 'Green channel should not be 0'); + notEqual(expectedVal.b, 0, 'Blue channel should not be 0'); + notEqual(expectedVal.a, 0, 'Alpha channel should not be 0'); + + viewer.addSimpleImage({ + url: '/test/data/A.png', + success: function() { + var secondImage = viewer.world.getItemAt(1); + secondImage.addHandler('fully-loaded-change', function() { + var imageData = viewer.drawer.context.getImageData(0, 0, 500 * density, 500 * density); + var actualVal = getPixelValue(imageData, 250 * density, 250 * density); + + equal(actualVal.r, expectedVal.r, + 'Red channel should not change in transparent part of the A'); + equal(actualVal.g, expectedVal.g, + 'Green channel should not change in transparent part of the A'); + equal(actualVal.b, expectedVal.b, + 'Blue channel should not change in transparent part of the A'); + equal(actualVal.a, expectedVal.a, + 'Alpha channel should not change in transparent part of the A'); + + var onAVal = getPixelValue(imageData, 333 * density, 250 * density); + equal(onAVal.r, 0, 'Red channel should be null on the A'); + equal(onAVal.g, 0, 'Green channel should be null on the A'); + equal(onAVal.b, 0, 'Blue channel should be null on the A'); + equal(onAVal.a, 255, 'Alpha channel should be 255 on the A'); + + start(); + }); + } + }); + }); + }); + + function getPixelValue(imageData, x, y) { + var offset = 4 * (y * imageData.width + x); + return { + r: imageData.data[offset], + g: imageData.data[offset + 1], + b: imageData.data[offset + 2], + a: imageData.data[offset + 3] + }; + } + }); + })(); diff --git a/test/modules/navigator.js b/test/modules/navigator.js index c6381a20..c58560a6 100644 --- a/test/modules/navigator.js +++ b/test/modules/navigator.js @@ -78,9 +78,11 @@ if (navigator === null) { navigator = $(".navigator"); - navigatorScaleFactor = Math.min(navigator.width() / viewer.viewport.contentSize.x, navigator.height() / viewer.viewport.contentSize.y); - displayRegionWidth = viewer.viewport.contentSize.x * navigatorScaleFactor; - displayRegionHeight = viewer.viewport.contentSize.y * navigatorScaleFactor; + navigatorScaleFactor = Math.min( + navigator.width() / viewer.viewport._contentSize.x, + navigator.height() / viewer.viewport._contentSize.y); + displayRegionWidth = viewer.viewport._contentSize.x * navigatorScaleFactor; + displayRegionHeight = viewer.viewport._contentSize.y * navigatorScaleFactor; contentStartFromLeft = (navigator.width() - displayRegionWidth) / 2; contentStartFromTop = (navigator.height() - displayRegionHeight) / 2; } @@ -91,8 +93,8 @@ regionBoundsInPoints = new OpenSeadragon.Rect(expectedDisplayRegionXLocation, expectedDisplayRegionYLocation, expectedDisplayRegionWidth, expectedDisplayRegionHeight); if (debug) { - console.log('Image width: ' + viewer.viewport.contentSize.x + '\n' + - 'Image height: ' + viewer.viewport.contentSize.y + '\n' + + console.log('Image width: ' + viewer.viewport._contentSize.x + '\n' + + 'Image height: ' + viewer.viewport._contentSize.y + '\n' + 'navigator.width(): ' + navigator.width() + '\n' + 'navigator.height(): ' + navigator.height() + '\n' + 'navigatorScaleFactor: ' + navigatorScaleFactor + '\n' + @@ -200,28 +202,28 @@ var assessViewerInCorner = function (theContentCorner) { return function () { - var expectedXCoordinate, expecteYCoordinate; + var expectedXCoordinate, expectedYCoordinate; if (theContentCorner === "TOPLEFT") { expectedXCoordinate = 0; - expecteYCoordinate = 0; + expectedYCoordinate = 0; } else if (theContentCorner === "TOPRIGHT") { expectedXCoordinate = 1 - viewer.viewport.getBounds().width; - expecteYCoordinate = 0; + expectedYCoordinate = 0; } else if (theContentCorner === "BOTTOMRIGHT") { expectedXCoordinate = 1 - viewer.viewport.getBounds().width; - expecteYCoordinate = 1 / viewer.source.aspectRatio - viewer.viewport.getBounds().height; + expectedYCoordinate = 1 / viewer.source.aspectRatio - viewer.viewport.getBounds().height; } else if (theContentCorner === "BOTTOMLEFT") { expectedXCoordinate = 0; - expecteYCoordinate = 1 / viewer.source.aspectRatio - viewer.viewport.getBounds().height; + expectedYCoordinate = 1 / viewer.source.aspectRatio - viewer.viewport.getBounds().height; } if (viewer.viewport.getBounds().width < 1) { Util.assessNumericValue(expectedXCoordinate, viewer.viewport.getBounds().x, 0.04, ' Viewer at ' + theContentCorner + ', x coord'); } if (viewer.viewport.getBounds().height < 1 / viewer.source.aspectRatio) { - Util.assessNumericValue(expecteYCoordinate, viewer.viewport.getBounds().y, 0.04, ' Viewer at ' + theContentCorner + ', y coord'); + Util.assessNumericValue(expectedYCoordinate, viewer.viewport.getBounds().y, 0.04, ' Viewer at ' + theContentCorner + ', y coord'); } }; }; @@ -801,7 +803,6 @@ }); asyncTest('Item positions including collection mode', function() { - var navAddCount = 0; viewer = OpenSeadragon({ id: 'example', @@ -815,16 +816,16 @@ var openHandler = function() { viewer.removeHandler('open', openHandler); viewer.navigator.world.addHandler('add-item', navOpenHandler); + // The navigator may already have added the items. + navOpenHandler(); }; var navOpenHandler = function(event) { - navAddCount++; - if (navAddCount === 2) { + if (viewer.navigator.world.getItemCount() === 2) { viewer.navigator.world.removeHandler('add-item', navOpenHandler); setTimeout(function() { // Test initial formation - equal(viewer.navigator.world.getItemCount(), 2, 'navigator has both items'); for (var i = 0; i < 2; i++) { propEqual(viewer.navigator.world.getItemAt(i).getBounds(), viewer.world.getItemAt(i).getBounds(), 'bounds are the same'); @@ -868,4 +869,57 @@ }); + asyncTest('Viewer rotation applied to navigator by default', function() { + + viewer = OpenSeadragon({ + id: 'example', + prefixUrl: '/build/openseadragon/images/', + tileSources: '/test/data/tall.dzi', + springStiffness: 100, // Faster animation = faster tests + showNavigator: true, + degrees: 45 + }); + viewer.addHandler('open', function openHandler() { + viewer.removeHandler('open', openHandler); + + var navigator = viewer.navigator; + + equal(navigator.viewport.getRotation(), 45, + "Rotation set in constructor should be applied to navigator by default."); + + viewer.viewport.setRotation(90); + equal(navigator.viewport.getRotation(), 90, + "Rotation set by setRotation should be applied to navigator by default."); + + start(); + }); + }); + + asyncTest('Viewer rotation not applied to navigator when navigatorRotate=false', function() { + + viewer = OpenSeadragon({ + id: 'example', + prefixUrl: '/build/openseadragon/images/', + tileSources: '/test/data/tall.dzi', + springStiffness: 100, // Faster animation = faster tests + showNavigator: true, + degrees: 45, + navigatorRotate: false + }); + viewer.addHandler('open', function openHandler() { + viewer.removeHandler('open', openHandler); + + var navigator = viewer.navigator; + + equal(navigator.viewport.getRotation(), 0, + "Rotation set in constructor should not be applied to navigator when navigatorRotate is false."); + + viewer.viewport.setRotation(90); + equal(navigator.viewport.getRotation(), 0, + "Rotation set by setRotation should not be applied to navigator when navigatorRotate is false."); + + start(); + }); + }); + })(); diff --git a/test/modules/overlays.js b/test/modules/overlays.js index 3e26b4c4..76e91272 100644 --- a/test/modules/overlays.js +++ b/test/modules/overlays.js @@ -1,109 +1,111 @@ /* global QUnit, module, Util, $, console, test, asyncTest, start, ok, equal, testLog */ -( function() { +(function() { var viewer; + // jQuery.position can give results quite different than what set in style.left + var epsilon = 1; - module( "Overlays", { + module("Overlays", { setup: function() { - var example = $( '
' ).appendTo( "#qunit-fixture" ); - var fixedOverlay = $( '
' ).appendTo( example ); - fixedOverlay.width( 70 ); - fixedOverlay.height( 60 ); + var example = $('
').appendTo("#qunit-fixture"); + var fixedOverlay = $('
').appendTo(example); + fixedOverlay.width(70); + fixedOverlay.height(60); testLog.reset(); }, teardown: function() { resetTestVariables(); } - } ); + }); var resetTestVariables = function() { - if ( viewer ) { + if (viewer) { viewer.close(); } }; - function waitForViewer( handler, count ) { - if ( typeof count !== "number" ) { + function waitForViewer(handler, count) { + if (typeof count !== "number") { count = 0; } var ready = viewer.isOpen() && viewer.drawer !== null && !viewer.world.needsDraw() && - Util.equalsWithVariance( viewer.viewport.getBounds( true ).x, - viewer.viewport.getBounds().x, 0.000 ) && - Util.equalsWithVariance( viewer.viewport.getBounds( true ).y, - viewer.viewport.getBounds().y, 0.000 ) && - Util.equalsWithVariance( viewer.viewport.getBounds( true ).width, - viewer.viewport.getBounds().width, 0.000 ); + Util.equalsWithVariance(viewer.viewport.getBounds(true).x, + viewer.viewport.getBounds().x, 0.000) && + Util.equalsWithVariance(viewer.viewport.getBounds(true).y, + viewer.viewport.getBounds().y, 0.000) && + Util.equalsWithVariance(viewer.viewport.getBounds(true).width, + viewer.viewport.getBounds().width, 0.000); - if ( ready ) { + if (ready) { handler(); - } else if ( count < 50 ) { + } else if (count < 50) { count++; - setTimeout( function() { - waitForViewer( handler, count ); - }, 100 ); + setTimeout(function() { + waitForViewer(handler, count); + }, 100); } else { - console.log( "waitForViewer:" + viewer.isOpen( ) + ":" + viewer.drawer + - ":" + viewer.world.needsDraw() ); + console.log("waitForViewer:" + viewer.isOpen( ) + ":" + viewer.drawer + + ":" + viewer.world.needsDraw()); handler(); } } - asyncTest( 'Overlays via viewer options', function() { + asyncTest('Overlays via viewer options', function() { - viewer = OpenSeadragon( { + viewer = OpenSeadragon({ id: 'example-overlays', prefixUrl: '/build/openseadragon/images/', - tileSources: [ '/test/data/testpattern.dzi', '/test/data/testpattern.dzi' ], + tileSources: ['/test/data/testpattern.dzi', '/test/data/testpattern.dzi'], springStiffness: 100, // Faster animation = faster tests - overlays: [ { + overlays: [{ x: 0.1, y: 0.4, width: 0.09, height: 0.09, id: "overlay" - } ] - } ); - viewer.addHandler( 'open', openHandler ); + }] + }); + viewer.addHandler('open', openHandler); function openHandler() { - viewer.removeHandler( 'open', openHandler ); + viewer.removeHandler('open', openHandler); - equal( viewer.overlays.length, 1, "Global overlay should be added." ); - equal( viewer.currentOverlays.length, 1, "Global overlay should be open." ); + equal(viewer.overlays.length, 1, "Global overlay should be added."); + equal(viewer.currentOverlays.length, 1, "Global overlay should be open."); - viewer.addHandler( 'open', openPageHandler ); - viewer.goToPage( 1 ); + viewer.addHandler('open', openPageHandler); + viewer.goToPage(1); } function openPageHandler() { - viewer.removeHandler( 'open', openPageHandler ); + viewer.removeHandler('open', openPageHandler); - equal( viewer.overlays.length, 1, "Global overlay should stay after page switch." ); - equal( viewer.currentOverlays.length, 1, "Global overlay should re-open after page switch." ); + equal(viewer.overlays.length, 1, "Global overlay should stay after page switch."); + equal(viewer.currentOverlays.length, 1, "Global overlay should re-open after page switch."); - viewer.addHandler( 'close', closeHandler ); + viewer.addHandler('close', closeHandler); viewer.close(); } function closeHandler() { - viewer.removeHandler( 'close', closeHandler ); + viewer.removeHandler('close', closeHandler); - equal( viewer.overlays.length, 1, "Global overlay should not be removed on close." ); - equal( viewer.currentOverlays.length, 0, "Global overlay should be closed on close." ); + equal(viewer.overlays.length, 1, "Global overlay should not be removed on close."); + equal(viewer.currentOverlays.length, 0, "Global overlay should be closed on close."); start(); } - } ); + }); - asyncTest( 'Page Overlays via viewer options', function() { + asyncTest('Page Overlays via viewer options', function() { - viewer = OpenSeadragon( { + viewer = OpenSeadragon({ id: 'example-overlays', prefixUrl: '/build/openseadragon/images/', - tileSources: [ { + tileSources: [{ Image: { xmlns: "http://schemas.microsoft.com/deepzoom/2008", Url: "/test/data/testpattern_files/", @@ -115,13 +117,13 @@ Height: 1000 } }, - overlays: [ { + overlays: [{ x: 0.1, y: 0.4, width: 0.09, height: 0.09, id: "overlay" - } ] + }] }, { Image: { xmlns: "http://schemas.microsoft.com/deepzoom/2008", @@ -134,96 +136,96 @@ Height: 1000 } } - } ], + }], springStiffness: 100 // Faster animation = faster tests - } ); - viewer.addHandler( 'open', openHandler ); + }); + viewer.addHandler('open', openHandler); function openHandler() { - viewer.removeHandler( 'open', openHandler ); + viewer.removeHandler('open', openHandler); - equal( viewer.overlays.length, 0, "No global overlay should be added." ); - equal( viewer.currentOverlays.length, 1, "Page overlay should be open." ); + equal(viewer.overlays.length, 0, "No global overlay should be added."); + equal(viewer.currentOverlays.length, 1, "Page overlay should be open."); - viewer.addHandler( 'open', openPageHandler ); - viewer.goToPage( 1 ); + viewer.addHandler('open', openPageHandler); + viewer.goToPage(1); } function openPageHandler() { - viewer.removeHandler( 'open', openPageHandler ); + viewer.removeHandler('open', openPageHandler); - equal( viewer.overlays.length, 0, "No global overlay should be added after page switch." ); - equal( viewer.currentOverlays.length, 0, "No page overlay should be opened after page switch." ); + equal(viewer.overlays.length, 0, "No global overlay should be added after page switch."); + equal(viewer.currentOverlays.length, 0, "No page overlay should be opened after page switch."); - viewer.addHandler( 'close', closeHandler ); + viewer.addHandler('close', closeHandler); viewer.close(); } function closeHandler() { - viewer.removeHandler( 'close', closeHandler ); + viewer.removeHandler('close', closeHandler); - equal( viewer.overlays.length, 0, "No global overlay should be added on close." ); - equal( viewer.currentOverlays.length, 0, "Page overlay should be closed on close." ); + equal(viewer.overlays.length, 0, "No global overlay should be added on close."); + equal(viewer.currentOverlays.length, 0, "Page overlay should be closed on close."); start(); } - } ); + }); - asyncTest( 'Overlays via addOverlay method', function() { + asyncTest('Overlays via addOverlay method', function() { - viewer = OpenSeadragon( { + viewer = OpenSeadragon({ id: 'example-overlays', prefixUrl: '/build/openseadragon/images/', - tileSources: [ '/test/data/testpattern.dzi', '/test/data/testpattern.dzi' ], + tileSources: ['/test/data/testpattern.dzi', '/test/data/testpattern.dzi'], springStiffness: 100 // Faster animation = faster tests - } ); - viewer.addHandler( 'open', openHandler ); + }); + viewer.addHandler('open', openHandler); function openHandler() { - viewer.removeHandler( 'open', openHandler ); + viewer.removeHandler('open', openHandler); - equal( viewer.overlays.length, 0, "No global overlay should be added." ); - equal( viewer.currentOverlays.length, 0, "No overlay should be open." ); + equal(viewer.overlays.length, 0, "No global overlay should be added."); + equal(viewer.currentOverlays.length, 0, "No overlay should be open."); - var rect = new OpenSeadragon.Rect( 0.1, 0.1, 0.1, 0.1 ); - var overlay = $( "
" ).prop( "id", "overlay" ).get( 0 ); - viewer.addOverlay( overlay, rect ); - equal( viewer.overlays.length, 0, "No manual overlay should be added as global overlay." ); - equal( viewer.currentOverlays.length, 1, "A manual overlay should be open." ); + var rect = new OpenSeadragon.Rect(0.1, 0.1, 0.1, 0.1); + var overlay = $("
").prop("id", "overlay").get(0); + viewer.addOverlay(overlay, rect); + equal(viewer.overlays.length, 0, "No manual overlay should be added as global overlay."); + equal(viewer.currentOverlays.length, 1, "A manual overlay should be open."); - viewer.addHandler( 'open', openPageHandler ); - viewer.goToPage( 1 ); + viewer.addHandler('open', openPageHandler); + viewer.goToPage(1); } function openPageHandler() { - viewer.removeHandler( 'open', openPageHandler ); + viewer.removeHandler('open', openPageHandler); - equal( viewer.overlays.length, 0, "No global overlay should be added after page switch." ); - equal( viewer.currentOverlays.length, 0, "Manual overlay should be removed after page switch." ); + equal(viewer.overlays.length, 0, "No global overlay should be added after page switch."); + equal(viewer.currentOverlays.length, 0, "Manual overlay should be removed after page switch."); - viewer.addHandler( 'close', closeHandler ); + viewer.addHandler('close', closeHandler); viewer.close(); } function closeHandler() { - viewer.removeHandler( 'close', closeHandler ); + viewer.removeHandler('close', closeHandler); - equal( viewer.overlays.length, 0, "No global overlay should be added on close." ); - equal( viewer.currentOverlays.length, 0, "Manual overlay should be removed on close." ); + equal(viewer.overlays.length, 0, "No global overlay should be added on close."); + equal(viewer.currentOverlays.length, 0, "Manual overlay should be removed on close."); start(); } - } ); + }); - asyncTest( 'Overlays size in pixels', function() { + asyncTest('Overlays size in pixels', function() { - viewer = OpenSeadragon( { + viewer = OpenSeadragon({ id: 'example-overlays', prefixUrl: '/build/openseadragon/images/', tileSources: '/test/data/testpattern.dzi', springStiffness: 100, // Faster animation = faster tests - overlays: [ { + overlays: [{ px: 13, py: 120, width: 124, @@ -234,60 +236,68 @@ py: 500, id: "fixed-overlay", placement: "TOP_LEFT" - } ] - } ); + }] + }); - function checkOverlayPosition( contextMessage ) { + function checkOverlayPosition(contextMessage) { var viewport = viewer.viewport; var expPosition = viewport.imageToViewerElementCoordinates( - new OpenSeadragon.Point( 13, 120 ) ).apply( Math.round ); - var actPosition = $( "#overlay" ).position(); - equal( actPosition.left, expPosition.x, "X position mismatch " + contextMessage ); - equal( actPosition.top, expPosition.y, "Y position mismatch " + contextMessage ); + new OpenSeadragon.Point(13, 120)); + var actPosition = $("#overlay").position(); + Util.assessNumericValue(actPosition.left, expPosition.x, epsilon, + "X position mismatch " + contextMessage); + Util.assessNumericValue(actPosition.top, expPosition.y, epsilon, + "Y position mismatch " + contextMessage); - var zoom = viewport.viewportToImageZoom( viewport.getZoom( true ) ); - var expectedWidth = Math.round( 124 * zoom ); - var expectedHeight = Math.round( 132 * zoom ); - equal( $( "#overlay" ).width(), expectedWidth, "Width mismatch " + contextMessage ); - equal( $( "#overlay" ).height( ), expectedHeight, "Height mismatch " + contextMessage ); + var zoom = viewport.viewportToImageZoom(viewport.getZoom(true)); + var expectedWidth = 124 * zoom; + var expectedHeight = 132 * zoom; + Util.assessNumericValue($("#overlay").width(), expectedWidth, epsilon, + "Width mismatch " + contextMessage); + Util.assessNumericValue($("#overlay").height(), expectedHeight, epsilon, + "Height mismatch " + contextMessage); expPosition = viewport.imageToViewerElementCoordinates( - new OpenSeadragon.Point( 400, 500 ) ).apply( Math.round ); - actPosition = $( "#fixed-overlay" ).position(); - equal( actPosition.left, expPosition.x, "Fixed overlay X position mismatch " + contextMessage ); - equal( actPosition.top, expPosition.y, "Fixed overlay Y position mismatch " + contextMessage ); + new OpenSeadragon.Point(400, 500)); + actPosition = $("#fixed-overlay").position(); + Util.assessNumericValue(actPosition.left, expPosition.x, epsilon, + "Fixed overlay X position mismatch " + contextMessage); + Util.assessNumericValue(actPosition.top, expPosition.y, epsilon, + "Fixed overlay Y position mismatch " + contextMessage); - equal( $( "#fixed-overlay" ).width(), 70, "Fixed overlay width mismatch " + contextMessage ); - equal( $( "#fixed-overlay" ).height( ), 60, "Fixed overlay height mismatch " + contextMessage ); + Util.assessNumericValue($("#fixed-overlay").width(), 70, epsilon, + "Fixed overlay width mismatch " + contextMessage); + Util.assessNumericValue($("#fixed-overlay").height(), 60, epsilon, + "Fixed overlay height mismatch " + contextMessage); } - waitForViewer( function() { - checkOverlayPosition( "after opening using image coordinates" ); + waitForViewer(function() { + checkOverlayPosition("after opening using image coordinates"); - viewer.viewport.zoomBy( 1.1 ).panBy( new OpenSeadragon.Point( 0.1, 0.2 ) ); - waitForViewer( function() { - checkOverlayPosition( "after zoom and pan using image coordinates" ); + viewer.viewport.zoomBy(1.1).panBy(new OpenSeadragon.Point(0.1, 0.2)); + waitForViewer(function() { + checkOverlayPosition("after zoom and pan using image coordinates"); viewer.viewport.goHome(); - waitForViewer( function() { - checkOverlayPosition( "after goHome using image coordinates" ); + waitForViewer(function() { + checkOverlayPosition("after goHome using image coordinates"); start(); - } ); - } ); + }); + }); - } ); - } ); + }); + }); - asyncTest( 'Overlays size in points', function() { + asyncTest('Overlays size in points', function() { - viewer = OpenSeadragon( { + viewer = OpenSeadragon({ id: 'example-overlays', prefixUrl: '/build/openseadragon/images/', tileSources: '/test/data/testpattern.dzi', springStiffness: 100, // Faster animation = faster tests - overlays: [ { + overlays: [{ x: 0.2, y: 0.1, width: 0.5, @@ -298,62 +308,70 @@ y: 0.6, id: "fixed-overlay", placement: "TOP_LEFT" - } ] - } ); + }] + }); - function checkOverlayPosition( contextMessage ) { + function checkOverlayPosition(contextMessage) { var viewport = viewer.viewport; var expPosition = viewport.viewportToViewerElementCoordinates( - new OpenSeadragon.Point( 0.2, 0.1 ) ).apply( Math.round ); - var actPosition = $( "#overlay" ).position(); - equal( actPosition.left, expPosition.x, "X position mismatch " + contextMessage ); - equal( actPosition.top, expPosition.y, "Y position mismatch " + contextMessage ); + new OpenSeadragon.Point(0.2, 0.1)); + var actPosition = $("#overlay").position(); + Util.assessNumericValue(actPosition.left, expPosition.x, epsilon, + "X position mismatch " + contextMessage); + Util.assessNumericValue(actPosition.top, expPosition.y, epsilon, + "Y position mismatch " + contextMessage); var expectedSize = viewport.deltaPixelsFromPoints( - new OpenSeadragon.Point( 0.5, 0.1 ) ); - equal( $( "#overlay" ).width(), expectedSize.x, "Width mismatch " + contextMessage ); - equal( $( "#overlay" ).height(), expectedSize.y, "Height mismatch " + contextMessage ); + new OpenSeadragon.Point(0.5, 0.1)); + Util.assessNumericValue($("#overlay").width(), expectedSize.x, epsilon, + "Width mismatch " + contextMessage); + Util.assessNumericValue($("#overlay").height(), expectedSize.y, epsilon, + "Height mismatch " + contextMessage); expPosition = viewport.viewportToViewerElementCoordinates( - new OpenSeadragon.Point( 0.5, 0.6 ) ).apply( Math.round ); - actPosition = $( "#fixed-overlay" ).position(); - equal( actPosition.left, expPosition.x, "Fixed overlay X position mismatch " + contextMessage ); - equal( actPosition.top, expPosition.y, "Fixed overlay Y position mismatch " + contextMessage ); + new OpenSeadragon.Point(0.5, 0.6)); + actPosition = $("#fixed-overlay").position(); + Util.assessNumericValue(actPosition.left, expPosition.x, epsilon, + "Fixed overlay X position mismatch " + contextMessage); + Util.assessNumericValue(actPosition.top, expPosition.y, epsilon, + "Fixed overlay Y position mismatch " + contextMessage); - equal( $( "#fixed-overlay" ).width(), 70, "Fixed overlay width mismatch " + contextMessage ); - equal( $( "#fixed-overlay" ).height( ), 60, "Fixed overlay height mismatch " + contextMessage ); + Util.assessNumericValue($("#fixed-overlay").width(), 70, epsilon, + "Fixed overlay width mismatch " + contextMessage); + Util.assessNumericValue($("#fixed-overlay").height(), 60, epsilon, + "Fixed overlay height mismatch " + contextMessage); } - waitForViewer( function() { - checkOverlayPosition( "after opening using viewport coordinates" ); + waitForViewer(function() { + checkOverlayPosition("after opening using viewport coordinates"); - viewer.viewport.zoomBy( 1.1 ).panBy( new OpenSeadragon.Point( 0.1, 0.2 ) ); - waitForViewer( function() { - checkOverlayPosition( "after zoom and pan using viewport coordinates" ); + viewer.viewport.zoomBy(1.1).panBy(new OpenSeadragon.Point(0.1, 0.2)); + waitForViewer(function() { + checkOverlayPosition("after zoom and pan using viewport coordinates"); viewer.viewport.goHome(); - waitForViewer( function() { - checkOverlayPosition( "after goHome using viewport coordinates" ); + waitForViewer(function() { + checkOverlayPosition("after goHome using viewport coordinates"); start(); - } ); - } ); + }); + }); - } ); - } ); + }); + }); - asyncTest( 'Overlays placement', function() { + asyncTest('Overlays placement', function() { - var scalableOverlayLocation = new OpenSeadragon.Rect( 0.2, 0.1, 0.5, 0.1 ); - var fixedOverlayLocation = new OpenSeadragon.Point( 0.5, 0.6 ); + var scalableOverlayLocation = new OpenSeadragon.Rect(0.2, 0.1, 0.5, 0.1); + var fixedOverlayLocation = new OpenSeadragon.Point(0.5, 0.6); - viewer = OpenSeadragon( { + viewer = OpenSeadragon({ id: 'example-overlays', prefixUrl: '/build/openseadragon/images/', tileSources: '/test/data/testpattern.dzi', springStiffness: 100, // Faster animation = faster tests - overlays: [ { + overlays: [{ x: scalableOverlayLocation.x, y: scalableOverlayLocation.y, width: scalableOverlayLocation.width, @@ -365,202 +383,727 @@ y: fixedOverlayLocation.y, id: "fixed-overlay", placement: "TOP_LEFT" - } ] - } ); + }] + }); // Scalable overlays are always TOP_LEFT - function checkScalableOverlayPosition( contextMessage ) { + function checkScalableOverlayPosition(contextMessage) { var viewport = viewer.viewport; var expPosition = viewport.viewportToViewerElementCoordinates( - new OpenSeadragon.Point( 0.2, 0.1 ) ).apply( Math.round ); - var actPosition = $( "#overlay" ).position(); - equal( actPosition.left, expPosition.x, "X position mismatch " + contextMessage ); - equal( actPosition.top, expPosition.y, "Y position mismatch " + contextMessage ); + new OpenSeadragon.Point(0.2, 0.1)); + var actPosition = $("#overlay").position(); + Util.assessNumericValue(actPosition.left, expPosition.x, epsilon, + "X position mismatch " + contextMessage); + Util.assessNumericValue(actPosition.top, expPosition.y, epsilon, + "Y position mismatch " + contextMessage); } - function checkFixedOverlayPosition( expectedOffset, contextMessage ) { + function checkFixedOverlayPosition(expectedOffset, contextMessage) { var viewport = viewer.viewport; var expPosition = viewport.viewportToViewerElementCoordinates( - new OpenSeadragon.Point( 0.5, 0.6 ) ) - .apply( Math.round ) - .plus( expectedOffset ); - var actPosition = $( "#fixed-overlay" ).position(); - equal( actPosition.left, expPosition.x, "Fixed overlay X position mismatch " + contextMessage ); - equal( actPosition.top, expPosition.y, "Fixed overlay Y position mismatch " + contextMessage ); + new OpenSeadragon.Point(0.5, 0.6)) + .plus(expectedOffset); + var actPosition = $("#fixed-overlay").position(); + Util.assessNumericValue(actPosition.left, expPosition.x, epsilon, + "Fixed overlay X position mismatch " + contextMessage); + Util.assessNumericValue(actPosition.top, expPosition.y, epsilon, + "Fixed overlay Y position mismatch " + contextMessage); } - waitForViewer( function() { + waitForViewer(function() { - checkScalableOverlayPosition( "with TOP_LEFT placement." ); - checkFixedOverlayPosition( new OpenSeadragon.Point( 0, 0 ), - "with TOP_LEFT placement." ); + checkScalableOverlayPosition("with TOP_LEFT placement."); + checkFixedOverlayPosition(new OpenSeadragon.Point(0, 0), + "with TOP_LEFT placement."); - viewer.updateOverlay( "overlay", scalableOverlayLocation, - OpenSeadragon.OverlayPlacement.CENTER ); - viewer.updateOverlay( "fixed-overlay", fixedOverlayLocation, - OpenSeadragon.OverlayPlacement.CENTER ); + // Check that legacy OpenSeadragon.OverlayPlacement is still working + viewer.updateOverlay("overlay", scalableOverlayLocation, + OpenSeadragon.OverlayPlacement.CENTER); + viewer.updateOverlay("fixed-overlay", fixedOverlayLocation, + OpenSeadragon.OverlayPlacement.CENTER); - setTimeout( function() { - checkScalableOverlayPosition( "with CENTER placement." ); - checkFixedOverlayPosition( new OpenSeadragon.Point( -35, -30 ), - "with CENTER placement." ); + setTimeout(function() { + checkScalableOverlayPosition("with CENTER placement."); + checkFixedOverlayPosition(new OpenSeadragon.Point(-35, -30), + "with CENTER placement."); - viewer.updateOverlay( "overlay", scalableOverlayLocation, - OpenSeadragon.OverlayPlacement.BOTTOM_RIGHT ); - viewer.updateOverlay( "fixed-overlay", fixedOverlayLocation, - OpenSeadragon.OverlayPlacement.BOTTOM_RIGHT ); - setTimeout( function() { - checkScalableOverlayPosition( "with BOTTOM_RIGHT placement." ); - checkFixedOverlayPosition( new OpenSeadragon.Point( -70, -60 ), - "with BOTTOM_RIGHT placement." ); + // Check that new OpenSeadragon.Placement is working + viewer.updateOverlay("overlay", scalableOverlayLocation, + OpenSeadragon.Placement.BOTTOM_RIGHT); + viewer.updateOverlay("fixed-overlay", fixedOverlayLocation, + OpenSeadragon.Placement.BOTTOM_RIGHT); + setTimeout(function() { + checkScalableOverlayPosition("with BOTTOM_RIGHT placement."); + checkFixedOverlayPosition(new OpenSeadragon.Point(-70, -60), + "with BOTTOM_RIGHT placement."); start(); - }, 100 ); + }, 100); - }, 100 ); + }, 100); - } ); - } ); + }); + }); - asyncTest( 'Overlays placement and resizing check', function() { + asyncTest('Overlays placement and resizing check', function() { - var fixedOverlayLocation = new OpenSeadragon.Point( 0.5, 0.6 ); + var fixedOverlayLocation = new OpenSeadragon.Point(0.5, 0.6); - viewer = OpenSeadragon( { + viewer = OpenSeadragon({ id: 'example-overlays', prefixUrl: '/build/openseadragon/images/', tileSources: '/test/data/testpattern.dzi', springStiffness: 100, // Faster animation = faster tests - overlays: [ { + overlays: [{ x: fixedOverlayLocation.x, y: fixedOverlayLocation.y, id: "fixed-overlay", placement: "CENTER", checkResize: true - } ] - } ); + }] + }); - function checkFixedOverlayPosition( expectedOffset, contextMessage ) { + function checkFixedOverlayPosition(expectedOffset, contextMessage) { var viewport = viewer.viewport; var expPosition = viewport.viewportToViewerElementCoordinates( - new OpenSeadragon.Point( 0.5, 0.6 ) ) - .apply( Math.round ) - .plus( expectedOffset ); - var actPosition = $( "#fixed-overlay" ).position(); - equal( actPosition.left, expPosition.x, "Fixed overlay X position mismatch " + contextMessage ); - equal( actPosition.top, expPosition.y, "Fixed overlay Y position mismatch " + contextMessage ); + new OpenSeadragon.Point(0.5, 0.6)) + .plus(expectedOffset); + var actPosition = $("#fixed-overlay").position(); + Util.assessNumericValue(actPosition.left, expPosition.x, epsilon, + "Fixed overlay X position mismatch " + contextMessage); + Util.assessNumericValue(actPosition.top, expPosition.y, epsilon, + "Fixed overlay Y position mismatch " + contextMessage); } - waitForViewer( function() { - checkFixedOverlayPosition( new OpenSeadragon.Point( -35, -30 ), - "with overlay of size 70,60." ); + waitForViewer(function() { + checkFixedOverlayPosition(new OpenSeadragon.Point(-35, -30), + "with overlay of size 70,60."); - $( "#fixed-overlay" ).width( 50 ); - $( "#fixed-overlay" ).height( 40 ); + $("#fixed-overlay").width(50); + $("#fixed-overlay").height(40); // The resizing of the overlays is not detected by the viewer's loop. viewer.forceRedraw(); - setTimeout( function() { - checkFixedOverlayPosition( new OpenSeadragon.Point( -25, -20 ), - "with overlay of size 50,40." ); + setTimeout(function() { + checkFixedOverlayPosition(new OpenSeadragon.Point(-25, -20), + "with overlay of size 50,40."); // Restore original size - $( "#fixed-overlay" ).width( 70 ); - $( "#fixed-overlay" ).height( 60 ); + $("#fixed-overlay").width(70); + $("#fixed-overlay").height(60); start(); - }, 100 ); - } ); + }, 100); + }); - } ); + }); - asyncTest( 'Overlays placement and no resizing check', function() { + asyncTest('Overlays placement and no resizing check', function() { - var fixedOverlayLocation = new OpenSeadragon.Point( 0.5, 0.6 ); + var fixedOverlayLocation = new OpenSeadragon.Point(0.5, 0.6); - viewer = OpenSeadragon( { + viewer = OpenSeadragon({ id: 'example-overlays', prefixUrl: '/build/openseadragon/images/', tileSources: '/test/data/testpattern.dzi', springStiffness: 100, // Faster animation = faster tests - overlays: [ { + overlays: [{ x: fixedOverlayLocation.x, y: fixedOverlayLocation.y, id: "fixed-overlay", placement: "CENTER", checkResize: false - } ] - } ); + }] + }); - function checkFixedOverlayPosition( expectedOffset, contextMessage ) { + function checkFixedOverlayPosition(expectedOffset, contextMessage) { var viewport = viewer.viewport; var expPosition = viewport.viewportToViewerElementCoordinates( - new OpenSeadragon.Point( 0.5, 0.6 ) ) - .apply( Math.round ) - .plus( expectedOffset ); - var actPosition = $( "#fixed-overlay" ).position(); - equal( actPosition.left, expPosition.x, "Fixed overlay X position mismatch " + contextMessage ); - equal( actPosition.top, expPosition.y, "Fixed overlay Y position mismatch " + contextMessage ); + new OpenSeadragon.Point(0.5, 0.6)) + .plus(expectedOffset); + var actPosition = $("#fixed-overlay").position(); + Util.assessNumericValue(actPosition.left, expPosition.x, epsilon, + "Fixed overlay X position mismatch " + contextMessage); + Util.assessNumericValue(actPosition.top, expPosition.y, epsilon, + "Fixed overlay Y position mismatch " + contextMessage); } - waitForViewer( function() { - checkFixedOverlayPosition( new OpenSeadragon.Point( -35, -30 ), - "with overlay of size 70,60." ); + waitForViewer(function() { + checkFixedOverlayPosition(new OpenSeadragon.Point(-35, -30), + "with overlay of size 70,60."); - $( "#fixed-overlay" ).width( 50 ); - $( "#fixed-overlay" ).height( 40 ); + $("#fixed-overlay").width(50); + $("#fixed-overlay").height(40); // The resizing of the overlays is not detected by the viewer's loop. viewer.forceRedraw(); - setTimeout( function() { - checkFixedOverlayPosition( new OpenSeadragon.Point( -35, -30 ), - "with overlay of size 50,40." ); + setTimeout(function() { + checkFixedOverlayPosition(new OpenSeadragon.Point(-35, -30), + "with overlay of size 50,40."); // Restore original size - $( "#fixed-overlay" ).width( 70 ); - $( "#fixed-overlay" ).height( 60 ); + $("#fixed-overlay").width(70); + $("#fixed-overlay").height(60); start(); - }, 100 ); - } ); + }, 100); + }); - } ); + }); // ---------- asyncTest('overlays appear immediately', function() { equal($('#immediate-overlay0').length, 0, 'overlay 0 does not exist'); equal($('#immediate-overlay1').length, 0, 'overlay 1 does not exist'); - viewer = OpenSeadragon( { + viewer = OpenSeadragon({ id: 'example-overlays', prefixUrl: '/build/openseadragon/images/', tileSources: '/test/data/testpattern.dzi', springStiffness: 100, // Faster animation = faster tests - overlays: [ { + overlays: [{ x: 0, y: 0, id: "immediate-overlay0" - } ] - } ); + }] + }); viewer.addHandler('open', function() { equal($('#immediate-overlay0').length, 1, 'overlay 0 exists'); - viewer.addOverlay( { + viewer.addOverlay({ x: 0, y: 0, id: "immediate-overlay1" - } ); + }); equal($('#immediate-overlay1').length, 1, 'overlay 1 exists'); start(); }); }); -} )( ); + // ---------- + asyncTest('Overlay scaled horizontally only', function() { + viewer = OpenSeadragon({ + id: 'example-overlays', + prefixUrl: '/build/openseadragon/images/', + tileSources: '/test/data/testpattern.dzi', + springStiffness: 100 // Faster animation = faster tests + }); + + viewer.addHandler('open', function() { + viewer.addOverlay({ + id: "horizontally-scaled-overlay", + x: 0, + y: 0, + width: 1 + }); + + var width = $("#horizontally-scaled-overlay").width(); + var height = 100; + var zoom = 1.1; + $("#horizontally-scaled-overlay").get(0).style.height = height + "px"; + + viewer.viewport.zoomBy(zoom); + + waitForViewer(function() { + var newWidth = $("#horizontally-scaled-overlay").width(); + var newHeight = $("#horizontally-scaled-overlay").height(); + equal(newWidth, width * zoom, "Width should be scaled."); + equal(newHeight, height, "Height should not be scaled."); + + start(); + }); + }); + }); + + // ---------- + asyncTest('Overlay scaled vertically only', function() { + viewer = OpenSeadragon({ + id: 'example-overlays', + prefixUrl: '/build/openseadragon/images/', + tileSources: '/test/data/testpattern.dzi', + springStiffness: 100 // Faster animation = faster tests + }); + + viewer.addHandler('open', function() { + viewer.addOverlay({ + id: "vertically-scaled-overlay", + x: 0, + y: 0, + height: 1 + }); + + var width = 100; + var height = $("#vertically-scaled-overlay").height(); + var zoom = 1.1; + $("#vertically-scaled-overlay").get(0).style.width = width + "px"; + + viewer.viewport.zoomBy(zoom); + + waitForViewer(function() { + var newWidth = $("#vertically-scaled-overlay").width(); + var newHeight = $("#vertically-scaled-overlay").height(); + equal(newWidth, width, "Width should not be scaled."); + equal(newHeight, height * zoom, "Height should be scaled."); + + start(); + }); + }); + }); + + // ---------- + asyncTest('Overlay.getBounds', function() { + viewer = OpenSeadragon({ + id: 'example-overlays', + prefixUrl: '/build/openseadragon/images/', + tileSources: '/test/data/testpattern.dzi', + springStiffness: 100 // Faster animation = faster tests + }); + + viewer.addHandler('open', function() { + viewer.addOverlay({ + id: "fully-scaled-overlay", + x: 1, + y: 1, + width: 1, + height: 1, + placement: OpenSeadragon.Placement.BOTTOM_RIGHT + }); + viewer.addOverlay({ + id: "horizontally-scaled-overlay", + x: 0.5, + y: 0.5, + width: 1, + placement: OpenSeadragon.Placement.CENTER + }); + viewer.addOverlay({ + id: "vertically-scaled-overlay", + x: 0, + y: 0.5, + height: 1, + placement: OpenSeadragon.Placement.LEFT + }); + viewer.addOverlay({ + id: "not-scaled-overlay", + x: 1, + y: 0, + placement: OpenSeadragon.Placement.TOP_RIGHT + }); + + var notScaledWidth = 100; + var notScaledHeight = 100; + $("#horizontally-scaled-overlay").get(0).style.height = notScaledHeight + "px"; + $("#vertically-scaled-overlay").get(0).style.width = notScaledWidth + "px"; + $("#not-scaled-overlay").get(0).style.width = notScaledWidth + "px"; + $("#not-scaled-overlay").get(0).style.height = notScaledHeight + "px"; + + var notScaledSize = viewer.viewport.deltaPointsFromPixelsNoRotate( + new OpenSeadragon.Point(notScaledWidth, notScaledHeight)); + + // Force refresh to takes new dimensions into account. + viewer._drawOverlays(); + + var actualBounds = viewer.getOverlayById("fully-scaled-overlay") + .getBounds(viewer.viewport); + var expectedBounds = new OpenSeadragon.Rect(0, 0, 1, 1); + ok(expectedBounds.equals(actualBounds), + "The fully scaled overlay should have bounds " + + expectedBounds + " but found " + actualBounds); + + + actualBounds = viewer.getOverlayById("horizontally-scaled-overlay") + .getBounds(viewer.viewport); + expectedBounds = new OpenSeadragon.Rect( + 0, 0.5 - notScaledSize.y / 2, 1, notScaledSize.y); + ok(expectedBounds.equals(actualBounds), + "The horizontally scaled overlay should have bounds " + + expectedBounds + " but found " + actualBounds); + + actualBounds = viewer.getOverlayById("vertically-scaled-overlay") + .getBounds(viewer.viewport); + expectedBounds = new OpenSeadragon.Rect( + 0, 0, notScaledSize.x, 1); + ok(expectedBounds.equals(actualBounds), + "The vertically scaled overlay should have bounds " + + expectedBounds + " but found " + actualBounds); + + actualBounds = viewer.getOverlayById("not-scaled-overlay") + .getBounds(viewer.viewport); + expectedBounds = new OpenSeadragon.Rect( + 1 - notScaledSize.x, 0, notScaledSize.x, notScaledSize.y); + ok(expectedBounds.equals(actualBounds), + "The not scaled overlay should have bounds " + + expectedBounds + " but found " + actualBounds); + + start(); + }); + }); + + // ---------- + asyncTest('Fully scaled overlay rotation mode NO_ROTATION', function() { + viewer = OpenSeadragon({ + id: 'example-overlays', + prefixUrl: '/build/openseadragon/images/', + tileSources: '/test/data/testpattern.dzi', + springStiffness: 100, // Faster animation = faster tests + degrees: 45, + overlays: [{ + id: "fully-scaled-overlay", + x: 1, + y: 1, + width: 1, + height: 1, + placement: OpenSeadragon.Placement.BOTTOM_RIGHT, + rotationMode: OpenSeadragon.OverlayRotationMode.NO_ROTATION + }] + }); + + viewer.addOnceHandler('open', function() { + var viewport = viewer.viewport; + + var $overlay = $("#fully-scaled-overlay"); + var expectedSize = viewport.deltaPixelsFromPointsNoRotate( + new OpenSeadragon.Point(1, 1)); + var expectedPosition = viewport.viewportToViewerElementCoordinates( + new OpenSeadragon.Point(1, 1)) + .minus(expectedSize); + var actualPosition = $overlay.position(); + Util.assessNumericValue(actualPosition.left, expectedPosition.x, epsilon, + "Scaled overlay position.x should adjust to rotation."); + Util.assessNumericValue(actualPosition.top, expectedPosition.y, epsilon, + "Scaled overlay position.y should adjust to rotation."); + + var actualWidth = $overlay.width(); + var actualHeight = $overlay.height(); + Util.assessNumericValue(actualWidth, expectedSize.x, epsilon, + "Scaled overlay width should not adjust to rotation."); + Util.assessNumericValue(actualHeight, expectedSize.y, epsilon, + "Scaled overlay height should not adjust to rotation."); + + var actualBounds = viewer.getOverlayById("fully-scaled-overlay") + .getBounds(viewport); + var expectedBounds = new OpenSeadragon.Rect(0, 0, 1, 1) + .rotate(-45, new OpenSeadragon.Point(1, 1)); + ok(expectedBounds.equals(actualBounds), + "The fully scaled overlay should have bounds " + + expectedBounds + " but found " + actualBounds); + + start(); + }); + }); + + // ---------- + asyncTest('Horizontally scaled overlay rotation mode NO_ROTATION', function() { + viewer = OpenSeadragon({ + id: 'example-overlays', + prefixUrl: '/build/openseadragon/images/', + tileSources: '/test/data/testpattern.dzi', + springStiffness: 100, // Faster animation = faster tests + degrees: 45, + overlays: [{ + id: "horizontally-scaled-overlay", + x: 0.5, + y: 0.5, + width: 1, + placement: OpenSeadragon.Placement.CENTER, + rotationMode: OpenSeadragon.OverlayRotationMode.NO_ROTATION + }] + }); + + viewer.addOnceHandler('open', function() { + var $overlay = $("#horizontally-scaled-overlay"); + var notScaledWidth = 100; + var notScaledHeight = 100; + $overlay.get(0).style.height = notScaledHeight + "px"; + + var viewport = viewer.viewport; + var notScaledSize = viewport.deltaPointsFromPixelsNoRotate( + new OpenSeadragon.Point(notScaledWidth, notScaledHeight)); + + // Force refresh to takes new dimensions into account. + viewer._drawOverlays(); + + var expectedWidth = viewport.deltaPixelsFromPointsNoRotate( + new OpenSeadragon.Point(1, 1)).x; + var expectedPosition = viewport.viewportToViewerElementCoordinates( + new OpenSeadragon.Point(0.5, 0.5)) + .minus(new OpenSeadragon.Point(expectedWidth / 2, notScaledHeight / 2)); + var actualPosition = $overlay.position(); + Util.assessNumericValue(actualPosition.left, expectedPosition.x, epsilon, + "Horizontally scaled overlay position.x should adjust to rotation."); + Util.assessNumericValue(actualPosition.top, expectedPosition.y, epsilon, + "Horizontally scaled overlay position.y should adjust to rotation."); + + var actualWidth = $overlay.width(); + var actualHeight = $overlay.height(); + Util.assessNumericValue(actualWidth, expectedWidth, epsilon, + "Horizontally scaled overlay width should not adjust to rotation."); + Util.assessNumericValue(actualHeight, notScaledHeight, epsilon, + "Horizontally scaled overlay height should not adjust to rotation."); + + var actualBounds = viewer.getOverlayById("horizontally-scaled-overlay") + .getBounds(viewport); + var expectedBounds = new OpenSeadragon.Rect( + 0, 0.5 - notScaledSize.y / 2, 1, notScaledSize.y) + .rotate(-45, new OpenSeadragon.Point(0.5, 0.5)); + ok(expectedBounds.equals(actualBounds), + "The horizontally scaled overlay should have bounds " + + expectedBounds + " but found " + actualBounds); + + start(); + }); + }); + + // ---------- + asyncTest('Vertically scaled overlay rotation mode NO_ROTATION', function() { + viewer = OpenSeadragon({ + id: 'example-overlays', + prefixUrl: '/build/openseadragon/images/', + tileSources: '/test/data/testpattern.dzi', + springStiffness: 100, // Faster animation = faster tests + degrees: 45, + overlays: [{ + id: "vertically-scaled-overlay", + x: 0, + y: 0.5, + height: 1, + placement: OpenSeadragon.Placement.LEFT, + rotationMode: OpenSeadragon.OverlayRotationMode.NO_ROTATION + }] + }); + + viewer.addOnceHandler('open', function() { + var $overlay = $("#vertically-scaled-overlay"); + var notScaledWidth = 100; + var notScaledHeight = 100; + $overlay.get(0).style.width = notScaledWidth + "px"; + + var viewport = viewer.viewport; + var notScaledSize = viewport.deltaPointsFromPixelsNoRotate( + new OpenSeadragon.Point(notScaledWidth, notScaledHeight)); + + // Force refresh to takes new dimensions into account. + viewer._drawOverlays(); + + var expectedHeight = viewport.deltaPixelsFromPointsNoRotate( + new OpenSeadragon.Point(1, 1)).y; + var expectedPosition = viewport.viewportToViewerElementCoordinates( + new OpenSeadragon.Point(0, 0.5)) + .minus(new OpenSeadragon.Point(0, expectedHeight / 2)); + var actualPosition = $overlay.position(); + Util.assessNumericValue(actualPosition.left, expectedPosition.x, epsilon, + "Vertically scaled overlay position.x should adjust to rotation."); + Util.assessNumericValue(actualPosition.top, expectedPosition.y, epsilon, + "Vertically scaled overlay position.y should adjust to rotation."); + + var actualWidth = $overlay.width(); + var actualHeight = $overlay.height(); + Util.assessNumericValue(actualWidth, notScaledWidth, epsilon, + "Vertically scaled overlay width should not adjust to rotation."); + Util.assessNumericValue(actualHeight, expectedHeight, epsilon, + "Vertically scaled overlay height should not adjust to rotation."); + + var actualBounds = viewer.getOverlayById("vertically-scaled-overlay") + .getBounds(viewport); + var expectedBounds = new OpenSeadragon.Rect( + 0, 0, notScaledSize.x, 1) + .rotate(-45, new OpenSeadragon.Point(0, 0.5)); + ok(expectedBounds.equals(actualBounds), + "The vertically scaled overlay should have bounds " + + expectedBounds + " but found " + actualBounds); + + start(); + }); + }); + + // ---------- + asyncTest('Not scaled overlay rotation mode NO_ROTATION', function() { + viewer = OpenSeadragon({ + id: 'example-overlays', + prefixUrl: '/build/openseadragon/images/', + tileSources: '/test/data/testpattern.dzi', + springStiffness: 100, // Faster animation = faster tests + degrees: 45, + overlays: [{ + id: "not-scaled-overlay", + x: 1, + y: 0, + placement: OpenSeadragon.Placement.TOP_RIGHT, + rotationMode: OpenSeadragon.OverlayRotationMode.NO_ROTATION + }] + }); + + viewer.addOnceHandler('open', function() { + var $overlay = $("#not-scaled-overlay"); + var notScaledWidth = 100; + var notScaledHeight = 100; + $overlay.get(0).style.width = notScaledWidth + "px"; + $overlay.get(0).style.height = notScaledHeight + "px"; + + var viewport = viewer.viewport; + var notScaledSize = viewport.deltaPointsFromPixelsNoRotate( + new OpenSeadragon.Point(notScaledWidth, notScaledHeight)); + + // Force refresh to takes new dimensions into account. + viewer._drawOverlays(); + + var expectedPosition = viewport.viewportToViewerElementCoordinates( + new OpenSeadragon.Point(1, 0)) + .minus(new OpenSeadragon.Point(notScaledWidth, 0)); + var actualPosition = $overlay.position(); + Util.assessNumericValue(actualPosition.left, expectedPosition.x, epsilon, + "Not scaled overlay position.x should adjust to rotation."); + Util.assessNumericValue(actualPosition.top, expectedPosition.y, epsilon, + "Not scaled overlay position.y should adjust to rotation."); + + var actualWidth = $overlay.width(); + var actualHeight = $overlay.height(); + Util.assessNumericValue(actualWidth, notScaledWidth, epsilon, + "Not scaled overlay width should not adjust to rotation."); + Util.assessNumericValue(actualHeight, notScaledHeight, epsilon, + "Not scaled overlay height should not adjust to rotation."); + + var actualBounds = viewer.getOverlayById("not-scaled-overlay") + .getBounds(viewport); + var expectedBounds = new OpenSeadragon.Rect( + 1 - notScaledSize.x, 0, notScaledSize.x, notScaledSize.y) + .rotate(-45, new OpenSeadragon.Point(1, 0)); + ok(expectedBounds.equals(actualBounds), + "Not scaled overlay should have bounds " + + expectedBounds + " but found " + actualBounds); + + start(); + }); + }); + + // ---------- + asyncTest('Fully scaled overlay rotation mode BOUNDING_BOX', function() { + viewer = OpenSeadragon({ + id: 'example-overlays', + prefixUrl: '/build/openseadragon/images/', + tileSources: '/test/data/testpattern.dzi', + springStiffness: 100, // Faster animation = faster tests + degrees: 45, + overlays: [{ + id: "fully-scaled-overlay", + x: 1, + y: 1, + width: 1, + height: 1, + placement: OpenSeadragon.Placement.BOTTOM_RIGHT, + rotationMode: OpenSeadragon.OverlayRotationMode.BOUNDING_BOX + }] + }); + + viewer.addOnceHandler('open', function() { + var viewport = viewer.viewport; + + var $overlay = $("#fully-scaled-overlay"); + var expectedRect = viewport.viewportToViewerElementRectangle( + new OpenSeadragon.Rect(0, 0, 1, 1)).getBoundingBox(); + var actualPosition = $overlay.position(); + Util.assessNumericValue(actualPosition.left, expectedRect.x, epsilon, + "Scaled overlay position.x should adjust to rotation."); + Util.assessNumericValue(actualPosition.top, expectedRect.y, epsilon, + "Scaled overlay position.y should adjust to rotation."); + + var actualWidth = $overlay.width(); + var actualHeight = $overlay.height(); + Util.assessNumericValue(actualWidth, expectedRect.width, epsilon, + "Scaled overlay width should not adjust to rotation."); + Util.assessNumericValue(actualHeight, expectedRect.height, epsilon, + "Scaled overlay height should not adjust to rotation."); + + var actualBounds = viewer.getOverlayById("fully-scaled-overlay") + .getBounds(viewport); + var expectedBounds = new OpenSeadragon.Rect( + 0.5, -0.5, Math.sqrt(2), Math.sqrt(2), 45); + var boundsEpsilon = 0.000001; + Util.assessNumericValue(actualBounds.x, expectedBounds.x, boundsEpsilon, + "The fully scaled overlay should have adjusted bounds.x"); + Util.assessNumericValue(actualBounds.y, expectedBounds.y, boundsEpsilon, + "The fully scaled overlay should have adjusted bounds.y"); + Util.assessNumericValue(actualBounds.width, expectedBounds.width, boundsEpsilon, + "The fully scaled overlay should have adjusted bounds.width"); + Util.assessNumericValue(actualBounds.height, expectedBounds.height, boundsEpsilon, + "The fully scaled overlay should have adjusted bounds.height"); + Util.assessNumericValue(actualBounds.degrees, expectedBounds.degrees, boundsEpsilon, + "The fully scaled overlay should have adjusted bounds.degrees"); + + start(); + }); + }); + + // ---------- + asyncTest('Fully scaled overlay rotation mode EXACT', function() { + viewer = OpenSeadragon({ + id: 'example-overlays', + prefixUrl: '/build/openseadragon/images/', + tileSources: '/test/data/testpattern.dzi', + springStiffness: 100, // Faster animation = faster tests + degrees: 45, + overlays: [{ + id: "fully-scaled-overlay", + x: 1, + y: 1, + width: 1, + height: 1, + placement: OpenSeadragon.Placement.BOTTOM_RIGHT, + rotationMode: OpenSeadragon.OverlayRotationMode.EXACT + }] + }); + + viewer.addOnceHandler('open', function() { + var viewport = viewer.viewport; + + var $overlay = $("#fully-scaled-overlay"); + var expectedSize = viewport.deltaPixelsFromPointsNoRotate( + new OpenSeadragon.Point(1, 1)); + var expectedPosition = viewport.pixelFromPoint( + new OpenSeadragon.Point(1, 1)) + .minus(expectedSize); + // We can't rely on jQuery.position with transforms. + var actualStyle = $overlay.get(0).style; + var left = Number(actualStyle.left.replace("px", "")); + var top = Number(actualStyle.top.replace("px", "")); + Util.assessNumericValue(left, expectedPosition.x, epsilon, + "Scaled overlay position.x should adjust to rotation."); + Util.assessNumericValue(top, expectedPosition.y, epsilon, + "Scaled overlay position.y should adjust to rotation."); + + var actualWidth = $overlay.width(); + var actualHeight = $overlay.height(); + Util.assessNumericValue(actualWidth, expectedSize.x, epsilon, + "Scaled overlay width should not adjust to rotation."); + Util.assessNumericValue(actualHeight, expectedSize.y, epsilon, + "Scaled overlay height should not adjust to rotation."); + + var transformOriginProp = OpenSeadragon.getCssPropertyWithVendorPrefix( + 'transformOrigin'); + var transformProp = OpenSeadragon.getCssPropertyWithVendorPrefix( + 'transform'); + var transformOrigin = actualStyle[transformOriginProp]; + // Some browsers replace "right bottom" by "100% 100%" + ok(transformOrigin.match(/(100% 100%)|(right bottom)/), + "Transform origin should be right bottom. Got: " + transformOrigin); + equal(actualStyle[transformProp], "rotate(45deg)", + "Transform should be rotate(45deg)."); + + var actualBounds = viewer.getOverlayById("fully-scaled-overlay") + .getBounds(viewport); + var expectedBounds = new OpenSeadragon.Rect(0, 0, 1, 1); + ok(expectedBounds.equals(actualBounds), + "The fully scaled overlay should have bounds " + + expectedBounds + " but found " + actualBounds); + + start(); + }); + }); +})(); diff --git a/test/modules/rectangle.js b/test/modules/rectangle.js new file mode 100644 index 00000000..402e58c4 --- /dev/null +++ b/test/modules/rectangle.js @@ -0,0 +1,303 @@ +/* global module, asyncTest, $, ok, equal, notEqual, start, test, Util, testLog */ + +(function() { + + module('Rectangle', {}); + + var precision = 0.000000001; + + test('Constructor', function() { + var rect = new OpenSeadragon.Rect(1, 2, 3, 4, 5); + strictEqual(rect.x, 1, 'rect.x should be 1'); + strictEqual(rect.y, 2, 'rect.y should be 2'); + strictEqual(rect.width, 3, 'rect.width should be 3'); + strictEqual(rect.height, 4, 'rect.height should be 4'); + strictEqual(rect.degrees, 5, 'rect.degrees should be 5'); + + rect = new OpenSeadragon.Rect(); + strictEqual(rect.x, 0, 'rect.x should be 0'); + strictEqual(rect.y, 0, 'rect.y should be 0'); + strictEqual(rect.width, 0, 'rect.width should be 0'); + strictEqual(rect.height, 0, 'rect.height should be 0'); + strictEqual(rect.degrees, 0, 'rect.degrees should be 0'); + + rect = new OpenSeadragon.Rect(0, 0, 1, 2, -405); + Util.assessNumericValue(Math.sqrt(2) / 2, rect.x, precision, + 'rect.x should be sqrt(2)/2'); + Util.assessNumericValue(-Math.sqrt(2) / 2, rect.y, precision, + 'rect.y should be -sqrt(2)/2'); + Util.assessNumericValue(2, rect.width, precision, + 'rect.width should be 2'); + Util.assessNumericValue(1, rect.height, precision, + 'rect.height should be 1'); + strictEqual(45, rect.degrees, 'rect.degrees should be 45'); + + rect = new OpenSeadragon.Rect(0, 0, 1, 2, 135); + Util.assessNumericValue(-Math.sqrt(2), rect.x, precision, + 'rect.x should be -sqrt(2)'); + Util.assessNumericValue(-Math.sqrt(2), rect.y, precision, + 'rect.y should be -sqrt(2)'); + Util.assessNumericValue(2, rect.width, precision, + 'rect.width should be 2'); + Util.assessNumericValue(1, rect.height, precision, + 'rect.height should be 1'); + strictEqual(45, rect.degrees, 'rect.degrees should be 45'); + + rect = new OpenSeadragon.Rect(0, 0, 1, 1, 585); + Util.assessNumericValue(0, rect.x, precision, + 'rect.x should be 0'); + Util.assessNumericValue(-Math.sqrt(2), rect.y, precision, + 'rect.y should be -sqrt(2)'); + Util.assessNumericValue(1, rect.width, precision, + 'rect.width should be 1'); + Util.assessNumericValue(1, rect.height, precision, + 'rect.height should be 1'); + strictEqual(45, rect.degrees, 'rect.degrees should be 45'); + }); + + test('getTopLeft', function() { + var rect = new OpenSeadragon.Rect(1, 2, 3, 4, 5); + var expected = new OpenSeadragon.Point(1, 2); + ok(expected.equals(rect.getTopLeft()), "Incorrect top left point."); + }); + + test('getTopRight', function() { + var rect = new OpenSeadragon.Rect(0, 0, 1, 3); + var expected = new OpenSeadragon.Point(1, 0); + ok(expected.equals(rect.getTopRight()), "Incorrect top right point."); + + rect.degrees = 45; + expected = new OpenSeadragon.Point(1 / Math.sqrt(2), 1 / Math.sqrt(2)); + Util.assertPointsEquals(expected, rect.getTopRight(), precision, + "Incorrect top right point with rotation."); + }); + + test('getBottomLeft', function() { + var rect = new OpenSeadragon.Rect(0, 0, 3, 1); + var expected = new OpenSeadragon.Point(0, 1); + ok(expected.equals(rect.getBottomLeft()), "Incorrect bottom left point."); + + rect.degrees = 45; + expected = new OpenSeadragon.Point(-1 / Math.sqrt(2), 1 / Math.sqrt(2)); + Util.assertPointsEquals(expected, rect.getBottomLeft(), precision, + "Incorrect bottom left point with rotation."); + }); + + test('getBottomRight', function() { + var rect = new OpenSeadragon.Rect(0, 0, 1, 1); + var expected = new OpenSeadragon.Point(1, 1); + ok(expected.equals(rect.getBottomRight()), "Incorrect bottom right point."); + + rect.degrees = 45; + expected = new OpenSeadragon.Point(0, Math.sqrt(2)); + Util.assertPointsEquals(expected, rect.getBottomRight(), precision, + "Incorrect bottom right point with 45 rotation."); + + rect.degrees = 90; + expected = new OpenSeadragon.Point(-1, 1); + Util.assertPointsEquals(expected, rect.getBottomRight(), precision, + "Incorrect bottom right point with 90 rotation."); + + rect.degrees = 135; + expected = new OpenSeadragon.Point(-Math.sqrt(2), 0); + Util.assertPointsEquals(expected, rect.getBottomRight(), precision, + "Incorrect bottom right point with 135 rotation."); + }); + + test('getCenter', function() { + var rect = new OpenSeadragon.Rect(0, 0, 1, 1); + var expected = new OpenSeadragon.Point(0.5, 0.5); + ok(expected.equals(rect.getCenter()), "Incorrect center point."); + + rect.degrees = 45; + expected = new OpenSeadragon.Point(0, 0.5 * Math.sqrt(2)); + Util.assertPointsEquals(expected, rect.getCenter(), precision, + "Incorrect bottom right point with 45 rotation."); + + rect.degrees = 90; + expected = new OpenSeadragon.Point(-0.5, 0.5); + Util.assertPointsEquals(expected, rect.getCenter(), precision, + "Incorrect bottom right point with 90 rotation."); + + rect.degrees = 135; + expected = new OpenSeadragon.Point(-0.5 * Math.sqrt(2), 0); + Util.assertPointsEquals(expected, rect.getCenter(), precision, + "Incorrect bottom right point with 135 rotation."); + }); + + test('times', function() { + var rect = new OpenSeadragon.Rect(1, 2, 3, 4, 45); + var expected = new OpenSeadragon.Rect(2, 4, 6, 8, 45); + var actual = rect.times(2); + Util.assertRectangleEquals(expected, actual, precision, + "Incorrect x2 rectangles."); + }); + + test('translate', function() { + var rect = new OpenSeadragon.Rect(1, 2, 3, 4, 45); + var expected = new OpenSeadragon.Rect(2, 4, 3, 4, 45); + var actual = rect.translate(new OpenSeadragon.Point(1, 2)); + Util.assertRectangleEquals(expected, actual, precision, + "Incorrect translation."); + }); + + test('union', function() { + var rect1 = new OpenSeadragon.Rect(2, 2, 2, 3); + var rect2 = new OpenSeadragon.Rect(0, 1, 1, 1); + var expected = new OpenSeadragon.Rect(0, 1, 4, 4); + var actual = rect1.union(rect2); + Util.assertRectangleEquals(expected, actual, precision, + "Incorrect union with horizontal rectangles."); + + rect1 = new OpenSeadragon.Rect(0, -Math.sqrt(2), 2, 2, 45); + rect2 = new OpenSeadragon.Rect(1, 0, 2, 2, 0); + expected = new OpenSeadragon.Rect( + -Math.sqrt(2), + -Math.sqrt(2), + 3 + Math.sqrt(2), + 2 + Math.sqrt(2)); + actual = rect1.union(rect2); + Util.assertRectangleEquals(expected, actual, precision, + "Incorrect union with non horizontal rectangles."); + }); + + test('intersection', function() { + var rect1 = new OpenSeadragon.Rect(2, 2, 2, 3); + var rect2 = new OpenSeadragon.Rect(0, 1, 1, 1); + var expected = null; + var actual = rect1.intersection(rect2); + equal(expected, actual, + "Rectangle " + rect2 + " should not intersect " + rect1); + actual = rect2.intersection(rect1); + equal(expected, actual, + "Rectangle " + rect1 + " should not intersect " + rect2); + + rect1 = new OpenSeadragon.Rect(0, 0, 2, 1); + rect2 = new OpenSeadragon.Rect(1, 0, 2, 2); + expected = new OpenSeadragon.Rect(1, 0, 1, 1); + actual = rect1.intersection(rect2); + Util.assertRectangleEquals(expected, actual, precision, + "Intersection of " + rect2 + " with " + rect1 + " should be " + + expected); + actual = rect2.intersection(rect1); + Util.assertRectangleEquals(expected, actual, precision, + "Intersection of " + rect1 + " with " + rect2 + " should be " + + expected); + + rect1 = new OpenSeadragon.Rect(0, 0, 3, 3); + rect2 = new OpenSeadragon.Rect(1, 1, 1, 1); + expected = new OpenSeadragon.Rect(1, 1, 1, 1); + actual = rect1.intersection(rect2); + Util.assertRectangleEquals(expected, actual, precision, + "Intersection of " + rect2 + " with " + rect1 + " should be " + + expected); + actual = rect2.intersection(rect1); + Util.assertRectangleEquals(expected, actual, precision, + "Intersection of " + rect1 + " with " + rect2 + " should be " + + expected); + + + rect1 = new OpenSeadragon.Rect(2, 2, 2, 3, 45); + rect2 = new OpenSeadragon.Rect(0, 1, 1, 1); + expected = null; + actual = rect1.intersection(rect2); + equal(expected, actual, + "Rectangle " + rect2 + " should not intersect " + rect1); + actual = rect2.intersection(rect1); + equal(expected, actual, + "Rectangle " + rect1 + " should not intersect " + rect2); + + rect1 = new OpenSeadragon.Rect(2, 0, 2, 3, 45); + rect2 = new OpenSeadragon.Rect(0, 1, 1, 1); + expected = new OpenSeadragon.Rect(0, 1, 1, 1); + actual = rect1.intersection(rect2); + Util.assertRectangleEquals(expected, actual, precision, + "Intersection of " + rect2 + " with " + rect1 + " should be " + + expected); + actual = rect2.intersection(rect1); + Util.assertRectangleEquals(expected, actual, precision, + "Intersection of " + rect1 + " with " + rect2 + " should be " + + expected); + }); + + test('rotate', function() { + var rect = new OpenSeadragon.Rect(0, 0, 2, 1); + + var expected = new OpenSeadragon.Rect( + 1 - 1 / (2 * Math.sqrt(2)), + 0.5 - 3 / (2 * Math.sqrt(2)), + 2, + 1, + 45); + var actual = rect.rotate(-675); + Util.assertRectangleEquals(expected, actual, precision, + "Incorrect rectangle after rotation of -675deg around center."); + + expected = new OpenSeadragon.Rect(0, 0, 2, 1, 33); + actual = rect.rotate(33, rect.getTopLeft()); + Util.assertRectangleEquals(expected, actual, precision, + "Incorrect rectangle after rotation of 33deg around topLeft."); + + expected = new OpenSeadragon.Rect(0, 0, 2, 1, 101); + actual = rect.rotate(101, rect.getTopLeft()); + Util.assertRectangleEquals(expected, actual, precision, + "Incorrect rectangle after rotation of 187deg around topLeft."); + + expected = new OpenSeadragon.Rect(0, 0, 2, 1, 187); + actual = rect.rotate(187, rect.getTopLeft()); + Util.assertRectangleEquals(expected, actual, precision, + "Incorrect rectangle after rotation of 187deg around topLeft."); + + expected = new OpenSeadragon.Rect(0, 0, 2, 1, 300); + actual = rect.rotate(300, rect.getTopLeft()); + Util.assertRectangleEquals(expected, actual, precision, + "Incorrect rectangle after rotation of 300deg around topLeft."); + }); + + test('getBoundingBox', function() { + var rect = new OpenSeadragon.Rect(0, 0, 2, 3); + + var bb = rect.getBoundingBox(); + ok(rect.equals(bb), "Bounding box of horizontal rectangle should be " + + "identical to rectangle."); + + rect.degrees = 90; + var expected = new OpenSeadragon.Rect(-3, 0, 3, 2); + Util.assertRectangleEquals(expected, rect.getBoundingBox(), precision, + "Bounding box of rect rotated 90deg."); + + rect.degrees = 180; + var expected = new OpenSeadragon.Rect(-2, -3, 2, 3); + Util.assertRectangleEquals(expected, rect.getBoundingBox(), precision, + "Bounding box of rect rotated 180deg."); + + rect.degrees = 270; + var expected = new OpenSeadragon.Rect(0, -2, 3, 2); + Util.assertRectangleEquals(expected, rect.getBoundingBox(), precision, + "Bounding box of rect rotated 270deg."); + }); + + test('containsPoint', function() { + var rect = new OpenSeadragon.Rect(0, 0, 1, 1, 45); + + ok(rect.containsPoint(new OpenSeadragon.Point(0, 0)), + 'Point 0,0 should be inside ' + rect); + ok(rect.containsPoint(rect.getTopRight()), + 'Top right vertex should be inside ' + rect); + ok(rect.containsPoint(rect.getBottomRight()), + 'Bottom right vertex should be inside ' + rect); + ok(rect.containsPoint(rect.getBottomLeft()), + 'Bottom left vertex should be inside ' + rect); + ok(rect.containsPoint(rect.getCenter()), + 'Center should be inside ' + rect); + notOk(rect.containsPoint(new OpenSeadragon.Point(1, 0)), + 'Point 1,0 should not be inside ' + rect); + ok(rect.containsPoint(new OpenSeadragon.Point(0.5, 0.5)), + 'Point 0.5,0.5 should be inside ' + rect); + ok(rect.containsPoint(new OpenSeadragon.Point(0.4, 0.5)), + 'Point 0.4,0.5 should be inside ' + rect); + notOk(rect.containsPoint(new OpenSeadragon.Point(0.6, 0.5)), + 'Point 0.6,0.5 should not be inside ' + rect); + }); + +})(); diff --git a/test/modules/tiledimage.js b/test/modules/tiledimage.js index 07f23f55..6dcae216 100644 --- a/test/modules/tiledimage.js +++ b/test/modules/tiledimage.js @@ -4,18 +4,18 @@ var viewer; module('TiledImage', { - setup: function () { + setup: function() { var example = $('
').appendTo("#qunit-fixture"); testLog.reset(); viewer = OpenSeadragon({ - id: 'example', - prefixUrl: '/build/openseadragon/images/', + id: 'example', + prefixUrl: '/build/openseadragon/images/', springStiffness: 100 // Faster animation = faster tests }); }, - teardown: function () { + teardown: function() { if (viewer && viewer.close) { viewer.close(); } @@ -87,7 +87,7 @@ // ---------- asyncTest('animation', function() { - viewer.addHandler("open", function () { + viewer.addHandler("open", function() { var image = viewer.world.getItemAt(0); propEqual(image.getBounds(), new OpenSeadragon.Rect(0, 0, 1, 1), 'target bounds on open'); propEqual(image.getBounds(true), new OpenSeadragon.Rect(0, 0, 1, 1), 'current bounds on open'); @@ -206,10 +206,12 @@ propEqual(image.getClip(), clip, 'clip is set correctly'); Util.spyOnce(viewer.drawer, 'setClip', function(rect) { - ok(true, 'drawer.setClip is called'); - var pixelRatio = viewer.viewport.getContainerSize().x / image.getContentSize().x; - var canvasClip = clip.times(pixelRatio * OpenSeadragon.pixelDensityRatio); - propEqual(rect, canvasClip, 'clipping to correct rect'); + var homeBounds = viewer.viewport.getHomeBounds(); + var canvasClip = viewer.drawer + .viewportToDrawerRectangle(homeBounds); + var precision = 0.00000001; + Util.assertRectangleEquals(rect, canvasClip, precision, + 'clipping should be ' + canvasClip); start(); }); }); @@ -220,6 +222,40 @@ }); }); + // ---------- + asyncTest('getClipBounds', function() { + var clip = new OpenSeadragon.Rect(100, 200, 800, 500); + + viewer.addHandler('open', function() { + var image = viewer.world.getItemAt(0); + var bounds = image.getClippedBounds(); + var expectedBounds = new OpenSeadragon.Rect(1.2, 1.4, 1.6, 1); + propEqual(bounds, expectedBounds, + 'getClipBounds should take clipping into account.'); + + image = viewer.world.getItemAt(1); + bounds = image.getClippedBounds(); + expectedBounds = new OpenSeadragon.Rect(1, 2, 2, 2); + propEqual(bounds, expectedBounds, + 'getClipBounds should work when no clipping set.'); + + start(); + }); + + viewer.open([{ + tileSource: '/test/data/testpattern.dzi', + clip: clip, + x: 1, + y: 1, + width: 2 + }, { + tileSource: '/test/data/testpattern.dzi', + x: 1, + y: 2, + width: 2 + }]); + }); + // ---------- asyncTest('opacity', function() { @@ -257,4 +293,177 @@ }); }); + // ---------- + asyncTest('fitBounds', function() { + + function assertRectEquals(actual, expected, message) { + ok(actual.equals(expected), message + ' should be ' + + expected.toString() + ', found ' + actual.toString()); + } + + viewer.addHandler('open', function openHandler() { + viewer.removeHandler('open', openHandler); + + var squareImage = viewer.world.getItemAt(0); + squareImage.fitBounds( + new OpenSeadragon.Rect(0, 0, 1, 2), + OpenSeadragon.Placement.CENTER, + true); + var actualBounds = squareImage.getBounds(true); + var expectedBounds = new OpenSeadragon.Rect(0, 0.5, 1, 1); + assertRectEquals(actualBounds, expectedBounds, 'Square image bounds'); + + var tallImage = viewer.world.getItemAt(1); + tallImage.fitBounds( + new OpenSeadragon.Rect(0, 0, 1, 2), + OpenSeadragon.Placement.TOP_LEFT, + true); + actualBounds = tallImage.getBounds(true); + expectedBounds = new OpenSeadragon.Rect(0, 0, 0.5, 2); + assertRectEquals(actualBounds, expectedBounds, 'Tall image bounds'); + + var wideImage = viewer.world.getItemAt(2); + wideImage.fitBounds( + new OpenSeadragon.Rect(0, 0, 1, 2), + OpenSeadragon.Placement.BOTTOM_RIGHT, + true); + actualBounds = wideImage.getBounds(true); + expectedBounds = new OpenSeadragon.Rect(0, 1.75, 1, 0.25); + assertRectEquals(actualBounds, expectedBounds, 'Wide image bounds'); + start(); + }); + + viewer.open([ + '/test/data/testpattern.dzi', + '/test/data/tall.dzi', + '/test/data/wide.dzi' + ]); + }); + + // ---------- + asyncTest('fitBounds in constructor', function() { + + function assertRectEquals(actual, expected, message) { + ok(actual.equals(expected), message + ' should be ' + + expected.toString() + ', found ' + actual.toString()); + } + + viewer.addHandler('open', function openHandler() { + viewer.removeHandler('open', openHandler); + + var squareImage = viewer.world.getItemAt(0); + var actualBounds = squareImage.getBounds(true); + var expectedBounds = new OpenSeadragon.Rect(0, 0.5, 1, 1); + assertRectEquals(actualBounds, expectedBounds, 'Square image bounds'); + + var tallImage = viewer.world.getItemAt(1); + actualBounds = tallImage.getBounds(true); + expectedBounds = new OpenSeadragon.Rect(0, 0, 0.5, 2); + assertRectEquals(actualBounds, expectedBounds, 'Tall image bounds'); + + var wideImage = viewer.world.getItemAt(2); + actualBounds = wideImage.getBounds(true); + expectedBounds = new OpenSeadragon.Rect(0, 1.75, 1, 0.25); + assertRectEquals(actualBounds, expectedBounds, 'Wide image bounds'); + start(); + }); + + viewer.open([{ + tileSource: '/test/data/testpattern.dzi', + x: 1, // should be ignored + y: 1, // should be ignored + width: 2, // should be ignored + fitBounds: new OpenSeadragon.Rect(0, 0, 1, 2) + // No placement specified, should default to CENTER + }, { + tileSource: '/test/data/tall.dzi', + fitBounds: new OpenSeadragon.Rect(0, 0, 1, 2), + fitBoundsPlacement: OpenSeadragon.Placement.TOP_LEFT + }, { + tileSource: '/test/data/wide.dzi', + fitBounds: new OpenSeadragon.Rect(0, 0, 1, 2), + fitBoundsPlacement: OpenSeadragon.Placement.BOTTOM_RIGHT + }]); + }); + + // ---------- + asyncTest('fitBounds with clipping', function() { + + function assertRectEquals(actual, expected, message) { + ok(actual.equals(expected), message + ' should be ' + + expected.toString() + ', found ' + actual.toString()); + } + + viewer.addHandler('open', function openHandler() { + viewer.removeHandler('open', openHandler); + + var squareImage = viewer.world.getItemAt(0); + var actualBounds = squareImage.getBounds(true); + var expectedBounds = new OpenSeadragon.Rect(-1, -1, 2, 2); + assertRectEquals(actualBounds, expectedBounds, 'Square image bounds'); + + var tallImage = viewer.world.getItemAt(1); + actualBounds = tallImage.getBounds(true); + expectedBounds = new OpenSeadragon.Rect(1, 1, 2, 8); + assertRectEquals(actualBounds, expectedBounds, 'Tall image bounds'); + + var wideImage = viewer.world.getItemAt(2); + actualBounds = wideImage.getBounds(true); + expectedBounds = new OpenSeadragon.Rect(1, 1, 16, 4); + assertRectEquals(actualBounds, expectedBounds, 'Wide image bounds'); + start(); + }); + + viewer.open([{ + tileSource: '/test/data/testpattern.dzi', + clip: new OpenSeadragon.Rect(500, 500, 500, 500), + fitBounds: new OpenSeadragon.Rect(0, 0, 1, 1) + }, { + tileSource: '/test/data/tall.dzi', + clip: new OpenSeadragon.Rect(0, 0, 250, 100), + fitBounds: new OpenSeadragon.Rect(1, 1, 1, 2), + fitBoundsPlacement: OpenSeadragon.Placement.TOP + }, { + tileSource: '/test/data/wide.dzi', + clip: new OpenSeadragon.Rect(0, 0, 100, 250), + fitBounds: new OpenSeadragon.Rect(1, 1, 1, 2), + fitBoundsPlacement: OpenSeadragon.Placement.TOP_LEFT + }]); + }); + + // ---------- + asyncTest('fullyLoaded', function() { + viewer.addHandler('open', function openHandler() { + viewer.removeHandler('open', openHandler); + + var image = viewer.world.getItemAt(0); + equal(image.getFullyLoaded(), false, 'not fully loaded at first'); + + var count = 0; + + var fullyLoadedChangeHandler = function(event) { + if (count === 0) { + equal(event.fullyLoaded, true, 'event includes true fullyLoaded property'); + equal(image.getFullyLoaded(), true, 'image is fully loaded after event'); + viewer.viewport.zoomBy(5, null, true); + } else if (count === 1) { + equal(event.fullyLoaded, false, 'event includes false fullyLoaded property'); + equal(image.getFullyLoaded(), false, 'image is not fully loaded after zoom'); + } else { + image.removeHandler('fully-loaded-change', fullyLoadedChangeHandler); + equal(image.getFullyLoaded(), true, 'image is once again fully loaded'); + start(); + } + + count++; + }; + + image.addHandler('fully-loaded-change', fullyLoadedChangeHandler); + }); + + viewer.open([{ + tileSource: '/test/data/tall.dzi', + }]); + }); + })(); diff --git a/test/modules/units.js b/test/modules/units.js index eada67b8..d4d95b08 100644 --- a/test/modules/units.js +++ b/test/modules/units.js @@ -2,6 +2,7 @@ (function () { var viewer; + var precision = 0.00000001; module('Units', { setup: function () { @@ -26,8 +27,8 @@ function pointEqual(a, b, message) { - Util.assessNumericValue(a.x, b.x, 0.00000001, message); - Util.assessNumericValue(a.y, b.y, 0.00000001, message); + Util.assessNumericValue(a.x, b.x, precision, message); + Util.assessNumericValue(a.y, b.y, precision, message); } // Check that f^-1 ( f(x) ) = x @@ -151,8 +152,6 @@ start(); }); viewer.viewport.zoomTo(0.8).panTo(new OpenSeadragon.Point(0.1, 0.2)); - - start(); }); viewer.open([{ @@ -167,6 +166,68 @@ }); + // --------- + asyncTest('Multiple images coordinates conversion with viewport rotation', function () { + + viewer.addHandler("open", function () { + var viewport = viewer.viewport; + var tiledImage1 = viewer.world.getItemAt(0); + var tiledImage2 = viewer.world.getItemAt(1); + var imageWidth = viewer.source.dimensions.x; + var imageHeight = viewer.source.dimensions.y; + + var viewerWidth = $(viewer.element).width(); + var viewerHeight = $(viewer.element).height(); + var viewerMiddleTop = new OpenSeadragon.Point(viewerWidth / 2, 0); + var viewerMiddleBottom = new OpenSeadragon.Point(viewerWidth / 2, viewerHeight); + + var point0_0 = new OpenSeadragon.Point(0, 0); + var point = viewport.viewerElementToViewportCoordinates(viewerMiddleTop); + pointEqual(point, point0_0, 'When opening, viewer middle top is also viewport 0,0'); + var image1Pixel = tiledImage1.viewerElementToImageCoordinates(viewerMiddleTop); + pointEqual(image1Pixel, point0_0, 'When opening, viewer middle top is also image 1 pixel 0,0'); + var image2Pixel = tiledImage2.viewerElementToImageCoordinates(viewerMiddleTop); + pointEqual(image2Pixel, + new OpenSeadragon.Point(-2 * imageWidth, -2 * imageHeight), + 'When opening, viewer middle top is also image 2 pixel -2*imageWidth, -2*imageHeight'); + + point = viewport.viewerElementToViewportCoordinates(viewerMiddleBottom); + pointEqual(point, new OpenSeadragon.Point(1.5, 1.5), + 'Viewer middle bottom has viewport coordinates 1.5,1.5.'); + image1Pixel = tiledImage1.viewerElementToImageCoordinates(viewerMiddleBottom); + pointEqual(image1Pixel, + new OpenSeadragon.Point(imageWidth * 1.5, imageHeight * 1.5), + 'Viewer middle bottom has image 1 pixel coordinates imageWidth * 1.5, imageHeight * 1.5'); + image2Pixel = tiledImage2.viewerElementToImageCoordinates(viewerMiddleBottom); + pointEqual(image2Pixel, + new OpenSeadragon.Point(imageWidth, imageHeight), + 'Viewer middle bottom has image 2 pixel coordinates imageWidth,imageHeight.'); + + + checkPoint(' after opening'); + viewer.addHandler('animation-finish', function animationHandler() { + viewer.removeHandler('animation-finish', animationHandler); + checkPoint(' after zoom and pan'); + + //Restore rotation + viewer.viewport.setRotation(0); + start(); + }); + viewer.viewport.zoomTo(0.8).panTo(new OpenSeadragon.Point(0.1, 0.2)); + }); + + viewer.viewport.setRotation(45); + viewer.open([{ + tileSource: "/test/data/testpattern.dzi" + }, { + tileSource: "/test/data/testpattern.dzi", + x: 1, + y: 1, + width: 0.5 + } + ]); + }); + // ---------- asyncTest('ZoomRatio 1 image', function () { viewer.addHandler("open", function () { @@ -188,10 +249,10 @@ var expectedViewportZoom = viewport.getZoom(true); var actualImageZoom = viewport.viewportToImageZoom( expectedViewportZoom); - equal(actualImageZoom, expectedImageZoom); + Util.assessNumericValue(actualImageZoom, expectedImageZoom, precision); var actualViewportZoom = viewport.imageToViewportZoom(actualImageZoom); - equal(actualViewportZoom, expectedViewportZoom); + Util.assessNumericValue(actualViewportZoom, expectedViewportZoom, precision); } checkZoom(); @@ -234,11 +295,11 @@ var actualImageZoom = image.viewportToImageZoom( expectedViewportZoom); Util.assessNumericValue(actualImageZoom, expectedImageZoom, - 0.00000001); + precision); var actualViewportImage1Zoom = image.imageToViewportZoom(actualImageZoom); Util.assessNumericValue( - actualViewportImage1Zoom, expectedViewportZoom, 0.00000001); + actualViewportImage1Zoom, expectedViewportZoom, precision); } checkZoom(image1); diff --git a/test/modules/viewport.js b/test/modules/viewport.js index 1c5fd878..fb94d11d 100644 --- a/test/modules/viewport.js +++ b/test/modules/viewport.js @@ -5,6 +5,7 @@ var VIEWER_ID = "example"; var PREFIX_URL = "/build/openseadragon/images/"; var SPRING_STIFFNESS = 100; // Faster animation = faster tests + var EPSILON = 0.0000000001; module("viewport", { setup: function () { @@ -98,7 +99,6 @@ viewer.removeHandler('open', openHandler); var viewport = viewer.viewport; viewport.zoomTo(ZOOM_FACTOR, null, true); - viewport.update(); // need to call this even with immediately=true var orig, expected, actual; for (var i = 0; i < config.testArray.length; i++){ @@ -125,7 +125,6 @@ viewer.removeHandler('open', openHandler); var viewport = viewer.viewport; viewport.zoomTo(ZOOM_FACTOR, null, true); - viewport.update(); // need to call this even with immediately=true propEqual(viewport.getContainerSize(), new OpenSeadragon.Point(500, 500), "Test container size"); start(); @@ -139,7 +138,6 @@ viewer.removeHandler('open', openHandler); var viewport = viewer.viewport; viewport.zoomTo(ZOOM_FACTOR, null, true); - viewport.update(); // need to call this even with immediately=true equal(viewport.getAspectRatio(), 1, "Test aspect ratio"); start(); @@ -221,6 +219,98 @@ }); }); + asyncTest('getHomeBoundsNoRotate with rotation', function() { + function openHandler() { + viewer.removeHandler('open', openHandler); + var viewport = viewer.viewport; + viewport.setRotation(-675); + Util.assertRectangleEquals( + viewport.getHomeBoundsNoRotate(), + new OpenSeadragon.Rect( + (1 - Math.sqrt(2)) / 2, + (1 - Math.sqrt(2)) / 2, + Math.sqrt(2), + Math.sqrt(2)), + 0.00000001, + "Test getHomeBoundsNoRotate with degrees = -675"); + start(); + } + viewer.addHandler('open', openHandler); + viewer.open(DZI_PATH); + }); + + asyncTest('getHomeBounds with rotation', function() { + function openHandler() { + viewer.removeHandler('open', openHandler); + var viewport = viewer.viewport; + viewport.setRotation(-675); + Util.assertRectangleEquals( + viewport.getHomeBounds(), + new OpenSeadragon.Rect( + 0.5, + -0.5, + Math.sqrt(2), + Math.sqrt(2), + 45), + 0.00000001, + "Test getHomeBounds with degrees = -675"); + start(); + } + viewer.addHandler('open', openHandler); + viewer.open(DZI_PATH); + }); + + asyncTest('getHomeBoundsWithMultiImages', function() { + function openHandler() { + viewer.removeHandler('open', openHandler); + var viewport = viewer.viewport; + Util.assertRectangleEquals( + new OpenSeadragon.Rect(0, 0, 4, 4), + viewport.getHomeBounds(), + 0.00000001, + "Test getHomeBoundsWithMultiImages"); + start(); + } + viewer.addHandler('open', openHandler); + viewer.open([{ + tileSource: DZI_PATH, + x: 0, + y: 0, + width: 2 + }, { + tileSource: DZI_PATH, + x: 3, + y: 3, + width: 1 + }]); + }); + + asyncTest('getHomeBoundsWithMultiImagesAndClipping', function() { + function openHandler() { + viewer.removeHandler('open', openHandler); + var viewport = viewer.viewport; + Util.assertRectangleEquals( + new OpenSeadragon.Rect(1, 1, 4, 4), + viewport.getHomeBounds(), + 0.00000001, + "Test getHomeBoundsWithMultiImagesAndClipping"); + start(); + } + viewer.addHandler('open', openHandler); + viewer.open([{ + tileSource: DZI_PATH, + x: 0, + y: 0, + width: 2, + clip: new OpenSeadragon.Rect(500, 500, 500, 500) + }, { + tileSource: DZI_PATH, + x: 4, + y: 4, + width: 1 + }]); + }); + asyncTest('getHomeZoom', function() { reopenViewerHelper({ property: 'defaultZoomLevel', @@ -243,7 +333,6 @@ viewer.removeHandler('open', openHandler); var viewport = viewer.viewport; viewport.zoomTo(ZOOM_FACTOR, null, true); - viewport.update(); // need to call this even with immediately=true // Special cases for oddball levels if (level === -1) { @@ -288,9 +377,8 @@ for(var i = 0; i < testRects.length; i++){ var rect = testRects[i].times(viewport.getContainerSize()); viewport.resetContentSize(rect.getSize()); - viewport.update(); propEqual( - viewport.contentSize, + viewport._contentSize, rect.getSize(), "Reset content size correctly." ); @@ -308,10 +396,8 @@ // zoom/pan somewhere viewport.zoomTo(ZOOM_FACTOR, true); - viewport.update(); viewport.goHome(true); - viewport.update(); propEqual( viewport.getBounds(), viewport.getHomeBounds(), @@ -323,7 +409,7 @@ viewer.open(DZI_PATH); }); - asyncTest('ensureVisible', function(){ + asyncTest('ensureVisible', function() { var openHandler = function(event) { viewer.removeHandler('open', openHandler); var viewport = viewer.viewport; @@ -331,10 +417,8 @@ // zoom/pan so that the image is out of view viewport.zoomTo(ZOOM_FACTOR * -50, true); viewport.panBy(new OpenSeadragon.Point(5000, 5000), null, true); - viewport.update(); viewport.ensureVisible(true); - viewport.update(); var bounds = viewport.getBounds(); ok(bounds.getSize().x > 1 && bounds.getSize().y > 1, "Moved viewport so that image is visible."); start(); @@ -343,52 +427,132 @@ viewer.open(DZI_PATH); }); - asyncTest('fitBounds', function(){ - var openHandler = function(event) { + asyncTest('applyConstraints', function() { + var openHandler = function() { viewer.removeHandler('open', openHandler); var viewport = viewer.viewport; - for(var i = 0; i < testRects.length; i++){ - var rect = testRects[i].times(viewport.getContainerSize()); - viewport.fitBounds(rect, true); - viewport.update(); - propEqual( - viewport.getBounds(), - rect, - "Fit bounds correctly." - ); - } + viewport.fitBounds(new OpenSeadragon.Rect(1, 1, 1, 1), true); + viewport.visibilityRatio = 0.3; + viewport.applyConstraints(true); + var bounds = viewport.getBounds(); + Util.assertRectangleEquals( + new OpenSeadragon.Rect(0.7, 0.7, 1, 1), + bounds, + EPSILON, + "Viewport.applyConstraints should move viewport."); start(); }; viewer.addHandler('open', openHandler); viewer.open(DZI_PATH); }); + asyncTest('applyConstraints with visibilityRatio = 1 shouldn\'t bounce around', function() { + var openHandler = function() { + viewer.removeHandler('open', openHandler); + var viewport = viewer.viewport; + + viewport.visibilityRatio = 1; + viewport.zoomTo(0.5, undefined, true); + viewport.panBy(new OpenSeadragon.Point(0.75, 0), true); + viewport.applyConstraints(true); + var bounds = viewport.getBounds(); + Util.assertRectangleEquals( + new OpenSeadragon.Rect(-0.5, 1, 2, 2), + bounds, + EPSILON, + "Viewport.applyConstraints should move viewport to the center, not to a side."); + start(); + }; + viewer.addHandler('open', openHandler); + viewer.open(TALL_PATH); + }); + + asyncTest('applyConstraints with rotation', function() { + var openHandler = function() { + viewer.removeHandler('open', openHandler); + var viewport = viewer.viewport; + viewport.setRotation(45); + viewport.fitBounds(new OpenSeadragon.Rect(1, 1, 1, 1), true); + viewport.applyConstraints(true); + var bounds = viewport.getBounds(); + Util.assertRectangleEquals( + bounds, + new OpenSeadragon.Rect(1, 0, Math.sqrt(2), Math.sqrt(2), 45), + EPSILON, + "Viewport.applyConstraints with rotation should move viewport."); + start(); + }; + viewer.addHandler('open', openHandler); + viewer.open(DZI_PATH); + }); + + // Fit bounds tests var testRectsFitBounds = [ new OpenSeadragon.Rect(0, -0.75, 0.5, 1), new OpenSeadragon.Rect(0.5, 0, 0.5, 0.8), new OpenSeadragon.Rect(0.75, 0.75, 0.5, 0.5), - new OpenSeadragon.Rect(-0.3, -0.3, 0.5, 0.5) + new OpenSeadragon.Rect(-0.3, -0.3, 0.5, 0.5), + new OpenSeadragon.Rect(0.5, 0.25, Math.sqrt(0.125), Math.sqrt(0.125), 45) ]; var expectedRectsFitBounds = [ + new OpenSeadragon.Rect(-0.25, -0.75, 1, 1), + new OpenSeadragon.Rect(0.35, 0, 0.8, 0.8), + new OpenSeadragon.Rect(0.75, 0.75, 0.5, 0.5), + new OpenSeadragon.Rect(-0.3, -0.3, 0.5, 0.5), + new OpenSeadragon.Rect(0.25, 0.25, 0.5, 0.5) + ]; + + var expectedRectsFitBoundsWithRotation = [ + new OpenSeadragon.Rect( + 0.25, + -1, + Math.sqrt(0.125) + Math.sqrt(0.5), + Math.sqrt(0.125) + Math.sqrt(0.5), + 45), + new OpenSeadragon.Rect( + 0.75, + -0.25, + Math.sqrt(0.125) + Math.sqrt(8 / 25), + Math.sqrt(0.125) + Math.sqrt(8 / 25), + 45), + new OpenSeadragon.Rect( + 1, + 0.5, + Math.sqrt(0.125) * 2, + Math.sqrt(0.125) * 2, + 45), + new OpenSeadragon.Rect( + -0.05, + -0.55, + Math.sqrt(0.125) * 2, + Math.sqrt(0.125) * 2, + 45), + new OpenSeadragon.Rect( + 0.5, + 0.25, + Math.sqrt(0.125), + Math.sqrt(0.125), + 45) + ]; + + var expectedRectsFitBoundsWithConstraints = [ new OpenSeadragon.Rect(-0.25, -0.5, 1, 1), new OpenSeadragon.Rect(0.35, 0, 0.8, 0.8), new OpenSeadragon.Rect(0.75, 0.75, 0.5, 0.5), - new OpenSeadragon.Rect(-0.25, -0.25, 0.5, 0.5) + new OpenSeadragon.Rect(-0.25, -0.25, 0.5, 0.5), + new OpenSeadragon.Rect(0.25, 0.25, 0.5, 0.5) ]; - asyncTest('fitBoundsWithConstraints', function(){ + asyncTest('fitBounds', function(){ var openHandler = function(event) { viewer.removeHandler('open', openHandler); var viewport = viewer.viewport; - viewport.zoomTo(ZOOM_FACTOR, null, true); - viewport.update(); + for(var i = 0; i < testRectsFitBounds.length; i++){ var rect = testRectsFitBounds[i]; - - viewport.fitBoundsWithConstraints(rect, true); - viewport.update(); + viewport.fitBounds(rect, true); propEqual( viewport.getBounds(), expectedRectsFitBounds[i], @@ -401,12 +565,92 @@ viewer.open(DZI_PATH); }); + asyncTest('fitBounds with viewport rotation', function(){ + var openHandler = function(event) { + viewer.removeHandler('open', openHandler); + var viewport = viewer.viewport; + viewport.setRotation(45); + + for(var i = 0; i < testRectsFitBounds.length; i++){ + var rect = testRectsFitBounds[i]; + viewport.fitBounds(rect, true); + Util.assertRectangleEquals( + viewport.getBounds(), + expectedRectsFitBoundsWithRotation[i], + EPSILON, + "Fit bounds correctly." + ); + } + start(); + }; + viewer.addHandler('open', openHandler); + viewer.open(DZI_PATH); + }); + + asyncTest('fitBoundsWithConstraints', function(){ + var openHandler = function(event) { + viewer.removeHandler('open', openHandler); + var viewport = viewer.viewport; + viewport.zoomTo(ZOOM_FACTOR, null, true); + for(var i = 0; i < testRectsFitBounds.length; i++){ + var rect = testRectsFitBounds[i]; + + viewport.fitBoundsWithConstraints(rect, true); + propEqual( + viewport.getBounds(), + expectedRectsFitBoundsWithConstraints[i], + "Fit bounds correctly." + ); + } + start(); + }; + viewer.addHandler('open', openHandler); + viewer.open(DZI_PATH); + }); + + asyncTest('fitBounds with almost same zoom', function() { + var openHandler = function() { + var viewport = viewer.viewport; + var rect1 = new OpenSeadragon.Rect(0, 0, 1, 1); + viewport.fitBounds(rect1, true); + Util.assertRectangleEquals(rect1, viewport.getBounds(), 1e-6, + 'Bounds should be ' + rect1); + + // Zoom and pan + var rect2 = new OpenSeadragon.Rect(1, 1, 1 + 1e-8, 1 + 1e-8); + viewport.fitBounds(rect2); + Util.assertRectangleEquals(rect2, viewport.getBounds(), 1e-6, + 'Bounds should be ' + rect2); + start(); + }; + viewer.addOnceHandler('open', openHandler); + viewer.open(DZI_PATH); + }); + + asyncTest('fitBounds with big rectangle', function() { + var openHandler = function() { + var viewport = viewer.viewport; + var rect1 = new OpenSeadragon.Rect(0, 0, 1e9, 1e9); + viewport.fitBounds(rect1, true); + Util.assertRectangleEquals(rect1, viewport.getBounds(), 1e-6, + 'Bounds should be ' + rect1); + + // Zoom and pan + var rect2 = new OpenSeadragon.Rect(1, 1, 2e9, 2e9); + viewport.fitBounds(rect2); + Util.assertRectangleEquals(rect2, viewport.getBounds(), 1e-6, + 'Bounds should be ' + rect2); + start(); + }; + viewer.addOnceHandler('open', openHandler); + viewer.open(DZI_PATH); + }); + asyncTest('fitHorizontally', function(){ var openHandler = function(event) { viewer.removeHandler('open', openHandler); var viewport = viewer.viewport; viewport.fitHorizontally(true); - viewport.update(); propEqual( viewport.getBounds(), new OpenSeadragon.Rect(0, 1.5, 1, 1), @@ -423,7 +667,6 @@ viewer.removeHandler('open', openHandler); var viewport = viewer.viewport; viewport.fitVertically(true); - viewport.update(); propEqual( viewport.getBounds(), new OpenSeadragon.Rect(0.375, 0, 0.25, 0.25), @@ -434,6 +677,7 @@ viewer.addHandler('open', openHandler); viewer.open(WIDE_PATH); }); + // End fitBounds tests. asyncTest('panBy', function(){ var openHandler = function(event) { @@ -443,7 +687,6 @@ for (var i = 0; i < testPoints.length; i++){ var expected = viewport.getCenter().plus(testPoints[i]); viewport.panBy(testPoints[i], true); - viewport.update(); // need to call this even with immediately=true propEqual( viewport.getCenter(), expected, @@ -464,7 +707,6 @@ for (var i = 0; i < testPoints.length; i++){ viewport.panTo(testPoints[i], true); - viewport.update(); // need to call this even with immediately=true propEqual( viewport.getCenter(), testPoints[i], @@ -485,7 +727,6 @@ for (var i = 0; i < testZoomLevels.length; i++){ viewport.zoomBy(testZoomLevels[i], null, true); - viewport.update(); // need to call this even with immediately=true propEqual( viewport.getZoom(), testZoomLevels[i], @@ -495,7 +736,6 @@ // now use a ref point // TODO: check the ending position due to ref point viewport.zoomBy(testZoomLevels[i], testPoints[i], true); - viewport.update(); propEqual( viewport.getZoom(), testZoomLevels[i], @@ -516,7 +756,6 @@ for (var i = 0; i < testZoomLevels.length; i++){ viewport.zoomTo(testZoomLevels[i], null, true); - viewport.update(); // need to call this even with immediately=true propEqual( viewport.getZoom(), testZoomLevels[i], @@ -526,7 +765,6 @@ // now use a ref point // TODO: check the ending position due to ref point viewport.zoomTo(testZoomLevels[i], testPoints[i], true); - viewport.update(); // need to call this even with immediately=true propEqual( viewport.getZoom(), testZoomLevels[i], @@ -565,7 +803,6 @@ for(var i = 0; i < testPoints.length; i++){ var new_size = testPoints[i].times(viewer.source.dimensions.x); viewport.resize(new_size); - viewport.update(); propEqual(viewport.getContainerSize(), new_size, "Viewport resized successfully."); } start(); diff --git a/test/test.html b/test/test.html index e52eb66a..74495ff6 100644 --- a/test/test.html +++ b/test/test.html @@ -1,4 +1,4 @@ - + @@ -17,6 +17,7 @@ + @@ -37,8 +38,10 @@ + +