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/Gruntfile.js b/Gruntfile.js index 06511e31..8c7ff60a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -39,6 +39,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", diff --git a/changelog.txt b/changelog.txt index 0d06a9b5..44b640c8 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,42 +1,54 @@ OPENSEADRAGON CHANGELOG ======================= -2.1.0: (in progress) +2.1.1: (in progress) + +* 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) + +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) -* You can now change viewport margins after the viewer is created (#721) * Fixed zooming in with plus key on a Swedish keyboard (#763) -* Added a patch to help slow down the scroll devices that fire too fast (#754) 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..3e0b81f2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,19 @@ { - "name": "OpenSeadragon", - "version": "2.0.0", + "name": "openseadragon", + "version": "2.1.0", "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", diff --git a/src/drawer.js b/src/drawer.js index 919057d7..633d6086 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -267,13 +267,14 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ }, /** - * 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 +291,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 ); } @@ -371,17 +367,35 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ /** * Blends the sketch canvas in the main canvas. * @param {Float} opacity The opacity of the blending. + * @param {Float} [scale=1] The scale at which tiles were drawn on the sketch. Default is 1. + * Use scale to draw at a lower scale and then enlarge onto the main canvas. + * @param OpenSeadragon.Point} [translate] A translation vector that was used to draw the tiles * @returns {undefined} */ - blendSketch: function(opacity, compositeOperation) { + blendSketch: function(opacity, scale, translate, compositeOperation) { if (!this.useCanvas || !this.sketchCanvas) { return; } + scale = scale || 1; + var position = translate instanceof $.Point ? + translate : + new $.Point(0, 0); this.context.save(); this.context.globalAlpha = opacity; this.context.globalCompositeOperation = compositeOperation; - this.context.drawImage(this.sketchCanvas, 0, 0); + this.context.drawImage( + this.sketchCanvas, + position.x, + position.y, + this.sketchCanvas.width * scale, + this.sketchCanvas.height * scale, + 0, + 0, + this.canvas.width, + this.canvas.height + ); +>>>>>>> a0a44dbeb5e3030e0acecf108efc19dbd53aaec2 this.context.restore(); }, @@ -399,7 +413,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( @@ -461,7 +475,7 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ ); if ( this.viewport.degrees !== 0 ) { - this._restoreRotationChanges( tile ); + this._restoreRotationChanges(); } context.restore(); }, @@ -487,21 +501,21 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ }, // private - _offsetForRotation: function( tile, degrees, useSketch ){ - var cx = this.canvas.width / 2, - cy = this.canvas.height / 2; + _offsetForRotation: function(degrees, useSketch) { + var cx = this.canvas.width / 2; + var cy = this.canvas.height / 2; - var context = this._getContext( useSketch ); + 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(); }, 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); + } + }, + /** + * @function + * @param {Number} level + * @param {OpenSeadragon.Point} point + */ + getTileAtPoint: function (level, point) { + 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/mousetracker.js b/src/mousetracker.js index 0a66cfee..a57993de 100644 --- a/src/mousetracker.js +++ b/src/mousetracker.js @@ -1367,6 +1367,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, @@ -1402,6 +1410,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, @@ -3248,5 +3264,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..9fac3637 100644 --- a/src/navigator.js +++ b/src/navigator.js @@ -223,12 +223,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); @@ -307,8 +301,8 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /* if( viewport && this.viewport ) { bounds = viewport.getBounds( true ); - topleft = this.viewport.pixelFromPoint( bounds.getTopLeft(), false ); - bottomright = this.viewport.pixelFromPoint( bounds.getBottomRight(), false ) + topleft = this.viewport.pixelFromPointNoRotate(bounds.getTopLeft(), false); + bottomright = this.viewport.pixelFromPointNoRotate(bounds.getBottomRight(), false) .minus( this.totalBorderWidths ); //update style for navigator-box @@ -378,7 +372,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 f3f14c79..16587057 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -254,6 +254,11 @@ * 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. + * * @property {Boolean} [autoResize=true] * Set to false to prevent polling for viewer size changes. Useful for providing custom resize behavior. * @@ -849,6 +854,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 @@ -1010,6 +1032,7 @@ 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 pixelsPerWheelLine: 40, autoResize: true, preserveImageSizeOnResize: false, // requires autoResize=true diff --git a/src/point.js b/src/point.js index 1ceef296..ebddfffe 100644 --- a/src/point.js +++ b/src/point.js @@ -179,14 +179,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/rectangle.js b/src/rectangle.js index 5d3495af..2cfd4060 100644 --- a/src/rectangle.js +++ b/src/rectangle.js @@ -32,46 +32,82 @@ * 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 */{ @@ -80,7 +116,12 @@ $.Rect.prototype = /** @lends OpenSeadragon.Rect.prototype */{ * @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 +155,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 +167,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 +179,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 +193,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 +203,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 +212,131 @@ $.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. + * 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. * 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); - - return new $.Rect(newTopLeft.x, newTopLeft.y, width, height); + /** + * Retrieves the smallest horizontal (degrees=0) rectangle which contains + * this rectangle. + * @returns {OpenSeadrayon.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); }, /** @@ -279,13 +347,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..0743d365 100644 --- a/src/referencestrip.js +++ b/src/referencestrip.js @@ -436,7 +436,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..4e44516e 100644 --- a/src/spring.js +++ b/src/spring.js @@ -79,7 +79,7 @@ $.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, + $.console.assert(typeof options.animationTime === "number" && options.animationTime !== 0, "[OpenSeadragon.Spring] options.animationTime must be a non-zero number"); if (options.exponential) { diff --git a/src/tile.js b/src/tile.js index ad018ba7..a07bec74 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 @@ -240,21 +248,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( @@ -273,14 +283,15 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ //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.context2D || this.url.match('.png'))) { //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 ); } @@ -289,21 +300,70 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ // changes as we are rendering the image drawingHandler({context: context, tile: this, rendered: rendered}); + if (typeof scale === 'number' && scale !== 1) { + // draw tile at a different scale + position = position.times(scale); + size = size.times(scale); + } + + if (translate instanceof $.Point) { + // shift tile position slightly + position = position.plus(translate); + } + context.drawImage( rendered.canvas, 0, 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() { + if (!this.cacheImageRecord) { + $.console.warn( + '[Tile.drawCanvas] attempting to get tile scale %s when tile\'s not cached', + this.toString()); + return 1; + } + + var rendered = this.cacheImageRecord.getRenderedContext(); + return rendered.canvas.width / this.size.times($.pixelDensityRatio).x; + }, + + /** + * 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) { + // 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. And FF does not like it. It crashes the viewer. + return new $.Point(1, 1).minus( + this.position + .times($.pixelDensityRatio) + .times(scale || 1) + .apply(function(x) { + return x % 1; + }) + ); + }, + /** * Removes tile from its container. * @function diff --git a/src/tiledimage.js b/src/tiledimage.js index 4bdeb634..3e084ad1 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -64,6 +64,7 @@ * @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 {Number} [options.opacity=1] - Opacity the tiled image should be drawn at. * @param {String} [options.compositeOperation='source-over'] - 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}. @@ -133,20 +134,21 @@ $.TiledImage = function( options ) { _needsDraw: true, // Does the tiledImage need to update the viewport again? _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, - compositeOperation: $.DEFAULT_SETTINGS.compositeOperation + 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, + debugMode: $.DEFAULT_SETTINGS.debugMode, + crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy, + placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, + opacity: $.DEFAULT_SETTINGS.opacity + compositeOperation: $.DEFAULT_SETTINGS.compositeOperation }, options ); @@ -356,23 +358,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 ); }, @@ -388,23 +390,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 ); }, @@ -666,7 +668,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, @@ -747,7 +749,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; @@ -761,12 +763,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, @@ -811,7 +813,7 @@ function updateViewport( tiledImage ) { drawTiles( tiledImage, tiledImage.lastDrawn ); // Load the new 'best' tile - if ( best ) { + if (best && !best.context2D) { loadTile( tiledImage, best, currentTime ); } @@ -956,10 +958,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); + } } } @@ -992,6 +998,7 @@ function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWid bounds, exists, url, + context2D, tile; if ( !tilesMatrix[ level ] ) { @@ -1007,6 +1014,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); @@ -1017,7 +1026,8 @@ function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWid y, bounds, exists, - url + url, + context2D ); } @@ -1056,12 +1066,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; @@ -1096,12 +1106,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; } } @@ -1144,10 +1156,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 ); @@ -1311,24 +1323,47 @@ function compareTiles( previousBest, tile ) { function drawTiles( tiledImage, lastDrawn ) { var i, - tile; + tile = lastDrawn[0]; if ( tiledImage.opacity <= 0 ) { drawDebugInfo( tiledImage, lastDrawn ); return; } var useSketch = tiledImage.opacity < 1 || tiledImage.compositeOperation !== 'source-over'; + var sketchScale; + var sketchTranslate; + + var zoom = tiledImage.viewport.getZoom(true); + var imageZoom = tiledImage.viewportToImageZoom(zoom); + if ( imageZoom > tiledImage.smoothTileEdgesMinZoom && tile) { + // When zoomed in a lot (>100%) the tile edges are visible. + // So we have to composite them at ~100% and scale them up together. + useSketch = true; + sketchScale = tile.getScaleForEdgeSmoothing(); + sketchTranslate = tile.getTranslationForEdgeSmoothing(sketchScale); + } +>>>>>>> a0a44dbeb5e3030e0acecf108efc19dbd53aaec2 if ( useSketch ) { tiledImage._drawer._clear( true ); } + if (tiledImage.viewport.degrees !== 0) { + tiledImage._drawer._offsetForRotation(tiledImage.viewport.degrees, useSketch); + } + var usedClip = false; if ( tiledImage._clip ) { tiledImage._drawer.saveContext(useSketch); 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; @@ -1336,6 +1371,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" ) { @@ -1350,7 +1391,7 @@ function drawTiles( tiledImage, lastDrawn ) { for ( 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 ){ @@ -1376,8 +1417,12 @@ function drawTiles( tiledImage, lastDrawn ) { tiledImage._drawer.restoreContext( useSketch ); } + if (tiledImage.viewport.degrees !== 0) { + tiledImage._drawer._restoreRotationChanges(useSketch); + } + if ( useSketch ) { - tiledImage._drawer.blendSketch( tiledImage.opacity, tiledImage.compositeOperation ); + tiledImage._drawer.blendSketch( tiledImage.opacity, sketchScale, sketchTranslate, tiledImage.compositeOperation ); } drawDebugInfo( tiledImage, lastDrawn ); } diff --git a/src/viewer.js b/src/viewer.js index 20748429..16d5cfa1 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -234,7 +234,7 @@ $.Viewer = function( options ) { style.left = "0px"; }(this.canvas.style)); $.setElementTouchActionNone( this.canvas ); - this.canvas.tabIndex = options.tabIndex || 0; + this.canvas.tabIndex = (options.tabIndex === undefined ? 0 : options.tabIndex); //the container is created through applying the ControlDock constructor above this.container.className = "openseadragon-container"; @@ -1292,19 +1292,21 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, } } + if ($.isArray(options.tileSource)) { + setTimeout(function() { + raiseAddItemFailed({ + message: "[Viewer.addTiledImage] Sequences can not be added; add them one at a time instead.", + source: options.tileSource, + options: options + }); + }); + return; + } + this._loadQueue.push(myQueueItem); getTileSourceImplementation( this, options.tileSource, function( tileSource ) { - if ( tileSource instanceof Array ) { - raiseAddItemFailed({ - message: "[Viewer.addTiledImage] Sequences can not be added; add them one at a time instead.", - source: tileSource, - options: options - }); - return; - } - myQueueItem.tileSource = tileSource; // add everybody at the front of the queue that's ready to go @@ -1349,6 +1351,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, blendTime: _this.blendTime, alwaysBlend: _this.alwaysBlend, minPixelRatio: _this.minPixelRatio, + smoothTileEdgesMinZoom: _this.smoothTileEdgesMinZoom, crossOriginPolicy: _this.crossOriginPolicy, debugMode: _this.debugMode }); @@ -2047,12 +2050,30 @@ function getTileSourceImplementation( viewer, tileSource, successCallback, } } + 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: viewer.crossOriginPolicy, ajaxWithCredentials: viewer.ajaxWithCredentials, + useCanvas: viewer.useCanvas, success: function( event ) { successCallback( event.tileSource ); } @@ -2061,10 +2082,16 @@ function getTileSourceImplementation( viewer, tileSource, successCallback, failCallback( event ); } ); - } else if ( $.isPlainObject( tileSource ) || tileSource.nodeType ) { + } else if ($.isPlainObject(tileSource) || tileSource.nodeType) { + if (!tileSource.crossOriginPolicy && viewer.crossOriginPolicy) { + tileSource.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 @@ -2082,14 +2109,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 ) { diff --git a/src/viewport.js b/src/viewport.js index c619198d..63493371 100644 --- a/src/viewport.js +++ b/src/viewport.js @@ -175,12 +175,12 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ $.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"); - this.homeBounds = bounds.clone(); + this.homeBounds = bounds.clone().rotate(this.degrees).getBoundingBox(); this.contentSize = this.homeBounds.getSize().times(contentFactor); this.contentAspectX = this.contentSize.x / this.contentSize.y; this.contentAspectY = this.contentSize.y / this.contentSize.x; - if( this.viewer ){ + if (this.viewer) { /** * Raised when the viewer's content size or home bounds are reset * (see {@link OpenSeadragon.Viewport#resetContentSize}, @@ -195,7 +195,7 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * @property {Number} contentFactor * @property {?Object} userData - Arbitrary subscriber-defined object. */ - this.viewer.raiseEvent( 'reset-size', { + this.viewer.raiseEvent('reset-size', { contentSize: this.contentSize.clone(), contentFactor: contentFactor, homeBounds: this.homeBounds.clone() @@ -704,7 +704,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 ); }, @@ -750,14 +749,9 @@ $.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); }, /** @@ -807,13 +801,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.setHomeBounds( + this.viewer.world.getHomeBounds(), + this.viewer.world.getContentFactor()); this.viewer.forceRedraw(); /** @@ -826,10 +826,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; }, @@ -933,40 +930,89 @@ $.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.getBounds(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.getBounds(current)); }, // private - _pixelFromPoint: function( point, bounds ) { + _pixelFromPointNoRotate: function(point, bounds) { return point.minus( bounds.getTopLeft() ).times( @@ -976,12 +1022,23 @@ $.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 ) { + pointFromPixelNoRotate: function(pixel, current) { var bounds = this.getBounds( current ); return pixel.minus( new $.Point(this._margins.left, this._margins.top) @@ -992,6 +1049,20 @@ $.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; @@ -1072,29 +1143,21 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * @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 - ); + 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 ); }, @@ -1113,25 +1176,21 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ * @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); + 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 ); }, diff --git a/test/coverage.html b/test/coverage.html index 65cc2818..15d3d490 100644 --- a/test/coverage.html +++ b/test/coverage.html @@ -32,6 +32,7 @@ + @@ -74,6 +75,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..a3be7724 100644 --- a/test/demo/overlay.html +++ b/test/demo/overlay.html @@ -1,41 +1,42 @@ - OpenSeadragon Overlay Demo - - - + OpenSeadragon Overlay Demo + + + -
-
- - +
+
+ +
+ 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/modules/basic.js b/test/modules/basic.js index 7f095777..4ce56654 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(); }); } 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/navigator.js b/test/modules/navigator.js index c6381a20..b6b49e5d 100644 --- a/test/modules/navigator.js +++ b/test/modules/navigator.js @@ -200,28 +200,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 +801,6 @@ }); asyncTest('Item positions including collection mode', function() { - var navAddCount = 0; viewer = OpenSeadragon({ id: 'example', @@ -815,16 +814,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'); diff --git a/test/modules/rectangle.js b/test/modules/rectangle.js new file mode 100644 index 00000000..7e905f58 --- /dev/null +++ b/test/modules/rectangle.js @@ -0,0 +1,221 @@ +/* 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('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."); + }); + +})(); 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 f480a6cd..ff485395 100644 --- a/test/modules/viewport.js +++ b/test/modules/viewport.js @@ -218,6 +218,26 @@ }); }); + asyncTest('getHomeBoundsWithRotation', function() { + function openHandler() { + viewer.removeHandler('open', openHandler); + var viewport = viewer.viewport; + viewport.setRotation(-675); + Util.assertRectangleEquals( + viewport.getHomeBounds(), + new OpenSeadragon.Rect( + (1 - Math.sqrt(2)) / 2, + (1 - Math.sqrt(2)) / 2, + Math.sqrt(2), + Math.sqrt(2)), + 0.00000001, + "Test getHomeBounds with degrees = -675"); + start(); + } + viewer.addHandler('open', openHandler); + viewer.open(DZI_PATH); + }); + asyncTest('getHomeZoom', function() { reopenViewerHelper({ property: 'defaultZoomLevel', diff --git a/test/test.html b/test/test.html index e52eb66a..d50e53eb 100644 --- a/test/test.html +++ b/test/test.html @@ -39,6 +39,7 @@ +