From 65b59c08d68b5c6e666c656421850f18cf390ba1 Mon Sep 17 00:00:00 2001 From: Antoine Vandecreme Date: Wed, 17 Aug 2016 15:43:08 +0200 Subject: [PATCH 01/15] First draft of tiled image rotation. --- src/drawer.js | 25 ++++++++--- src/openseadragon.js | 15 +++++++ src/point.js | 5 +-- src/rectangle.js | 10 +---- src/tiledimage.js | 103 +++++++++++++++++++++++++++++++++---------- src/viewer.js | 1 + src/viewport.js | 6 +-- 7 files changed, 118 insertions(+), 47 deletions(-) diff --git a/src/drawer.js b/src/drawer.js index 661663d1..3c4a58b1 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -464,7 +464,7 @@ $.Drawer.prototype = { }, // private - drawDebugInfo: function( tile, count, i ){ + drawDebugInfo: function(tile, count, i, tiledImage) { if ( !this.useCanvas ) { return; } @@ -479,6 +479,12 @@ $.Drawer.prototype = { if ( this.viewport.degrees !== 0 ) { this._offsetForRotation(this.viewport.degrees); } + if (tiledImage.degrees) { + this._offsetForRotation( + tiledImage.degrees, + tiledImage.viewport.pixelFromPointNoRotate( + tiledImage.getBounds(true).getTopLeft(), true)); + } context.strokeRect( tile.position.x * $.pixelDensityRatio, @@ -541,6 +547,9 @@ $.Drawer.prototype = { if ( this.viewport.degrees !== 0 ) { this._restoreRotationChanges(); } + if (tiledImage.degrees) { + this._restoreRotationChanges(); + } context.restore(); }, @@ -574,17 +583,19 @@ $.Drawer.prototype = { return new $.Point(canvas.width, canvas.height); }, - // private - _offsetForRotation: function(degrees, useSketch) { - var cx = this.canvas.width / 2; - var cy = this.canvas.height / 2; + getCanvasCenter: function() { + return new $.Point(this.canvas.width / 2, this.canvas.height / 2); + }, + // private + _offsetForRotation: function(degrees, point, useSketch) { + point = point || this.getCanvasCenter(); var context = this._getContext(useSketch); context.save(); - context.translate(cx, cy); + context.translate(point.x, point.y); context.rotate(Math.PI / 180 * degrees); - context.translate(-cx, -cy); + context.translate(-point.x, -point.y); }, // private diff --git a/src/openseadragon.js b/src/openseadragon.js index de7399e8..bd252e1c 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -1375,6 +1375,21 @@ function OpenSeadragon( options ){ return string.charAt(0).toUpperCase() + string.slice(1); }, + /** + * Compute the modulo of a number but makes sure to always return + * a positive value. + * @param {Number} number the number to computes the modulo of + * @param {Number} modulo the modulo + * @returns {Number} the result of the modulo of number + */ + positiveModulo: function(number, modulo) { + var result = number % modulo; + if (result < 0) { + result += modulo; + } + return result; + }, + /** * Determines if a point is within the bounding rectangle of the given element (hit-test). * @function diff --git a/src/point.js b/src/point.js index 7d4f6740..113f7036 100644 --- a/src/point.js +++ b/src/point.js @@ -190,10 +190,7 @@ $.Point.prototype = { var sin; // Avoid float computations when possible if (degrees % 90 === 0) { - var d = degrees % 360; - if (d < 0) { - d += 360; - } + var d = $.positiveModulo(degrees, 360); switch (d) { case 0: cos = 1; diff --git a/src/rectangle.js b/src/rectangle.js index 98c839de..9d284df0 100644 --- a/src/rectangle.js +++ b/src/rectangle.js @@ -81,10 +81,7 @@ $.Rect = function(x, y, width, height, degrees) { this.degrees = typeof(degrees) === "number" ? degrees : 0; // Normalizes the rectangle. - this.degrees = this.degrees % 360; - if (this.degrees < 0) { - this.degrees += 360; - } + this.degrees = $.positiveModulo(this.degrees, 360); var newTopLeft, newWidth; if (this.degrees >= 270) { newTopLeft = this.getTopRight(); @@ -442,13 +439,10 @@ $.Rect.prototype = { * @return {OpenSeadragon.Rect} */ rotate: function(degrees, pivot) { - degrees = degrees % 360; + degrees = $.positiveModulo(degrees, 360); if (degrees === 0) { return this.clone(); } - if (degrees < 0) { - degrees += 360; - } pivot = pivot || this.getCenter(); var newTopLeft = this.getTopLeft().rotate(degrees, pivot); diff --git a/src/tiledimage.js b/src/tiledimage.js index e4749fbc..ad65d3e7 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -159,8 +159,8 @@ $.TiledImage = function( options ) { crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy, placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, opacity: $.DEFAULT_SETTINGS.opacity, - compositeOperation: $.DEFAULT_SETTINGS.compositeOperation - + compositeOperation: $.DEFAULT_SETTINGS.compositeOperation, + degrees: 0 }, options ); this._xSpring = new $.Spring({ @@ -274,13 +274,19 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @param {Boolean} [current=false] - Pass true for the current location; false for target location. */ getBounds: function(current) { - if (current) { - return new $.Rect( this._xSpring.current.value, this._ySpring.current.value, - this._worldWidthCurrent, this._worldHeightCurrent ); - } - - return new $.Rect( this._xSpring.target.value, this._ySpring.target.value, - this._worldWidthTarget, this._worldHeightTarget ); + return current ? + new $.Rect( + this._xSpring.current.value, + this._ySpring.current.value, + this._worldWidthCurrent, + this._worldHeightCurrent, + this.degrees) : + new $.Rect( + this._xSpring.target.value, + this._ySpring.target.value, + this._worldWidthTarget, + this._worldHeightTarget, + this.degrees); }, // deprecated @@ -304,7 +310,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag bounds.x + clip.x, bounds.y + clip.y, clip.width, - clip.height); + clip.height, + this.degrees); } return bounds; }, @@ -660,6 +667,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag $.console.assert(!newClip || newClip instanceof $.Rect, "[TiledImage.setClip] newClip must be an OpenSeadragon.Rect or null"); +//TODO: should this._raiseBoundsChange(); be called? + if (newClip instanceof $.Rect) { this._clip = newClip.clone(); } else { @@ -684,6 +693,23 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag this._needsDraw = true; }, + /** + * Get the current rotation of this tiled image in degrees. + * @returns {Number} the current rotation of this tiled image in degrees. + */ + getRotation: function() { + return this.degrees; + }, + + /** + * Set the current rotation of this tiled image in degrees. + * @param {Number} the rotation in degrees. + */ + setRotation: function(degrees) { + this.degrees = $.positiveModulo(degrees, 360); + this._needsDraw = true; + }, + /** * @returns {String} The TiledImage's current compositeOperation. */ @@ -803,7 +829,8 @@ function updateViewport( tiledImage ) { } if (!tiledImage.wrapHorizontal && !tiledImage.wrapVertical) { - var tiledImageBounds = tiledImage.getClippedBounds(true); + var tiledImageBounds = tiledImage.getClippedBounds(true) + .getBoundingBox(); var intersection = viewportBounds.intersection(tiledImageBounds); if (intersection === null) { return; @@ -1464,10 +1491,20 @@ function drawTiles( tiledImage, lastDrawn ) { tiledImage._drawer._clear(true, bounds); } - // When scaling, we must rotate only when blending the sketch canvas to avoid - // interpolation - if (tiledImage.viewport.degrees !== 0 && !sketchScale) { - tiledImage._drawer._offsetForRotation(tiledImage.viewport.degrees, useSketch); + // When scaling, we must rotate only when blending the sketch canvas to + // avoid interpolation + if (!sketchScale) { + if (tiledImage.viewport.degrees !== 0) { + tiledImage._drawer._offsetForRotation( + tiledImage.viewport.degrees, useSketch); + } + if (tiledImage.degrees !== 0) { + tiledImage._drawer._offsetForRotation( + tiledImage.degrees, + tiledImage.viewport.pixelFromPointNoRotate( + tiledImage.getBounds(true).getTopLeft(), true), + useSketch); + } } var usedClip = false; @@ -1535,14 +1572,28 @@ function drawTiles( tiledImage, lastDrawn ) { tiledImage._drawer.restoreContext( useSketch ); } - if (tiledImage.viewport.degrees !== 0 && !sketchScale) { - tiledImage._drawer._restoreRotationChanges(useSketch); + if (!sketchScale) { + if (tiledImage.degrees !== 0) { + tiledImage._drawer._restoreRotationChanges(useSketch); + } + if (tiledImage.viewport.degrees !== 0) { + tiledImage._drawer._restoreRotationChanges(useSketch); + } } if (useSketch) { - var offsetForRotation = tiledImage.viewport.degrees !== 0 && sketchScale; - if (offsetForRotation) { - tiledImage._drawer._offsetForRotation(tiledImage.viewport.degrees, false); + if (sketchScale) { + if (tiledImage.viewport.degrees !== 0) { + tiledImage._drawer._offsetForRotation( + tiledImage.viewport.degrees, false); + } + if (tiledImage.degrees !== 0) { + tiledImage._drawer._offsetForRotation( + tiledImage.degrees, + tiledImage.viewport.pixelFromPointNoRotate( + tiledImage.getBounds(true).getTopLeft(), true), + useSketch); + } } tiledImage._drawer.blendSketch({ opacity: tiledImage.opacity, @@ -1551,8 +1602,13 @@ function drawTiles( tiledImage, lastDrawn ) { compositeOperation: tiledImage.compositeOperation, bounds: bounds }); - if (offsetForRotation) { - tiledImage._drawer._restoreRotationChanges(false); + if (sketchScale) { + if (tiledImage.degrees !== 0) { + tiledImage._drawer._restoreRotationChanges(false); + } + if (tiledImage.viewport.degrees !== 0) { + tiledImage._drawer._restoreRotationChanges(false); + } } } drawDebugInfo( tiledImage, lastDrawn ); @@ -1563,7 +1619,8 @@ function drawDebugInfo( tiledImage, lastDrawn ) { for ( var i = lastDrawn.length - 1; i >= 0; i-- ) { var tile = lastDrawn[ i ]; try { - tiledImage._drawer.drawDebugInfo( tile, lastDrawn.length, i ); + tiledImage._drawer.drawDebugInfo( + tile, lastDrawn.length, i, tiledImage); } catch(e) { $.console.error(e); } diff --git a/src/viewer.js b/src/viewer.js index 02556bcf..6ef8b4de 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -1369,6 +1369,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, clip: queueItem.options.clip, placeholderFillStyle: queueItem.options.placeholderFillStyle, opacity: queueItem.options.opacity, + degrees: queueItem.options.degrees, compositeOperation: queueItem.options.compositeOperation, springStiffness: _this.springStiffness, animationTime: _this.animationTime, diff --git a/src/viewport.js b/src/viewport.js index 2ac499d4..4cac87c3 100644 --- a/src/viewport.js +++ b/src/viewport.js @@ -852,11 +852,7 @@ $.Viewport.prototype = { return this; } - degrees = degrees % 360; - if (degrees < 0) { - degrees += 360; - } - this.degrees = degrees; + this.degrees = $.positiveModulo(degrees, 360); this._setContentBounds( this.viewer.world.getHomeBounds(), this.viewer.world.getContentFactor()); From a9f5e7ec73d9dff2be5a33fba4e96799a62bf29a Mon Sep 17 00:00:00 2001 From: Antoine Vandecreme Date: Sun, 21 Aug 2016 12:54:33 +0200 Subject: [PATCH 02/15] Add unit test and fix code review comments. --- src/drawer.js | 6 +++--- src/tiledimage.js | 33 ++++++++++++++++++++------------- src/viewer.js | 2 ++ test/modules/tiledimage.js | 29 +++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/src/drawer.js b/src/drawer.js index 3c4a58b1..3175a575 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -479,9 +479,9 @@ $.Drawer.prototype = { if ( this.viewport.degrees !== 0 ) { this._offsetForRotation(this.viewport.degrees); } - if (tiledImage.degrees) { + if (tiledImage.getRotation() !== 0) { this._offsetForRotation( - tiledImage.degrees, + tiledImage.getRotation(), tiledImage.viewport.pixelFromPointNoRotate( tiledImage.getBounds(true).getTopLeft(), true)); } @@ -547,7 +547,7 @@ $.Drawer.prototype = { if ( this.viewport.degrees !== 0 ) { this._restoreRotationChanges(); } - if (tiledImage.degrees) { + if (tiledImage.getRotation() !== 0) { this._restoreRotationChanges(); } context.restore(); diff --git a/src/tiledimage.js b/src/tiledimage.js index ad65d3e7..69747482 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -132,6 +132,9 @@ $.TiledImage = function( options ) { var fitBoundsPlacement = options.fitBoundsPlacement || OpenSeadragon.Placement.CENTER; delete options.fitBoundsPlacement; + this._degrees = $.positiveModulo(options.degrees || 0, 360); + delete options.degrees; + $.extend( true, this, { //internal state properties @@ -159,8 +162,7 @@ $.TiledImage = function( options ) { crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy, placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, opacity: $.DEFAULT_SETTINGS.opacity, - compositeOperation: $.DEFAULT_SETTINGS.compositeOperation, - degrees: 0 + compositeOperation: $.DEFAULT_SETTINGS.compositeOperation }, options ); this._xSpring = new $.Spring({ @@ -280,13 +282,13 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag this._ySpring.current.value, this._worldWidthCurrent, this._worldHeightCurrent, - this.degrees) : + this._degrees) : new $.Rect( this._xSpring.target.value, this._ySpring.target.value, this._worldWidthTarget, this._worldHeightTarget, - this.degrees); + this._degrees); }, // deprecated @@ -311,7 +313,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag bounds.y + clip.y, clip.width, clip.height, - this.degrees); + this._degrees); } return bounds; }, @@ -698,7 +700,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @returns {Number} the current rotation of this tiled image in degrees. */ getRotation: function() { - return this.degrees; + return this._degrees; }, /** @@ -706,8 +708,13 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @param {Number} the rotation in degrees. */ setRotation: function(degrees) { - this.degrees = $.positiveModulo(degrees, 360); + degrees = $.positiveModulo(degrees, 360); + if (this._degrees === degrees) { + return; + } + this._degrees = degrees; this._needsDraw = true; + this._raiseBoundsChange(); }, /** @@ -1498,9 +1505,9 @@ function drawTiles( tiledImage, lastDrawn ) { tiledImage._drawer._offsetForRotation( tiledImage.viewport.degrees, useSketch); } - if (tiledImage.degrees !== 0) { + if (tiledImage._degrees !== 0) { tiledImage._drawer._offsetForRotation( - tiledImage.degrees, + tiledImage._degrees, tiledImage.viewport.pixelFromPointNoRotate( tiledImage.getBounds(true).getTopLeft(), true), useSketch); @@ -1573,7 +1580,7 @@ function drawTiles( tiledImage, lastDrawn ) { } if (!sketchScale) { - if (tiledImage.degrees !== 0) { + if (tiledImage._degrees !== 0) { tiledImage._drawer._restoreRotationChanges(useSketch); } if (tiledImage.viewport.degrees !== 0) { @@ -1587,9 +1594,9 @@ function drawTiles( tiledImage, lastDrawn ) { tiledImage._drawer._offsetForRotation( tiledImage.viewport.degrees, false); } - if (tiledImage.degrees !== 0) { + if (tiledImage._degrees !== 0) { tiledImage._drawer._offsetForRotation( - tiledImage.degrees, + tiledImage._degrees, tiledImage.viewport.pixelFromPointNoRotate( tiledImage.getBounds(true).getTopLeft(), true), useSketch); @@ -1603,7 +1610,7 @@ function drawTiles( tiledImage, lastDrawn ) { bounds: bounds }); if (sketchScale) { - if (tiledImage.degrees !== 0) { + if (tiledImage._degrees !== 0) { tiledImage._drawer._restoreRotationChanges(false); } if (tiledImage.viewport.degrees !== 0) { diff --git a/src/viewer.js b/src/viewer.js index 6ef8b4de..e2d68862 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -1228,6 +1228,8 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * (portions of the image outside of this area will not be visible). Only works on * browsers that support the HTML5 canvas. * @param {Number} [options.opacity] Opacity the tiled image should be drawn at by default. + * @param {Number} [options.degrees=0] Initial rotation of the tiled image around + * it top left corner in degrees. * @param {String} [options.compositeOperation] How the image is composited onto other images. * @param {String} [options.crossOriginPolicy] The crossOriginPolicy for this specific image, * overriding viewer.crossOriginPolicy. diff --git a/test/modules/tiledimage.js b/test/modules/tiledimage.js index d7a46aef..9c2356a6 100644 --- a/test/modules/tiledimage.js +++ b/test/modules/tiledimage.js @@ -292,6 +292,35 @@ }); }); + // ---------- + asyncTest('rotation', function() { + + function testDefaultRotation() { + var image = viewer.world.getItemAt(0); + strictEqual(image.getRotation(), 0, 'image has default rotation'); + + image.setRotation(400); + strictEqual(image.getRotation(), 40, 'rotation is set correctly'); + + viewer.addOnceHandler('open', testTileSourceRotation); + viewer.open({ + tileSource: '/test/data/testpattern.dzi', + degrees: -60 + }); + } + + function testTileSourceRotation() { + var image = viewer.world.getItemAt(0); + strictEqual(image.getRotation(), 300, 'image has correct rotation'); + start(); + } + + viewer.addOnceHandler('open', testDefaultRotation); + viewer.open({ + tileSource: '/test/data/testpattern.dzi', + }); + }); + asyncTest('fitBounds', function() { function assertRectEquals(actual, expected, message) { From f0cb707ff238c0c9fcc7963524beff8797dc3e83 Mon Sep 17 00:00:00 2001 From: Antoine Vandecreme Date: Sun, 21 Aug 2016 19:24:40 +0200 Subject: [PATCH 03/15] Improve code readability --- src/tiledimage.js | 365 ++++++++++++++++++++++------------------------ 1 file changed, 175 insertions(+), 190 deletions(-) diff --git a/src/tiledimage.js b/src/tiledimage.js index 34cd2e26..59c449c9 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -292,7 +292,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag draw: function() { if (this.opacity !== 0) { this._midDraw = true; - updateViewport(this); + this._updateViewport(); this._midDraw = false; } }, @@ -817,187 +817,168 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag // private _isBottomItem: function() { return this.viewer.world.getItemAt(0) === this; + }, + + // private + _getLevelsInterval: function() { + var lowestLevel = Math.max( + this.source.minLevel, + Math.floor(Math.log(this.minZoomImageRatio) / Math.log(2)) + ); + var currentZeroRatio = this.viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio(0), true).x * + this._scaleSpring.current.value; + var highestLevel = Math.min( + Math.abs(this.source.maxLevel), + Math.abs(Math.floor( + Math.log(currentZeroRatio / this.minPixelRatio) / Math.log(2) + )) + ); + + // Calculations for the interval of levels to draw + // can return invalid intervals; fix that here if necessary + lowestLevel = Math.min(lowestLevel, highestLevel); + return { + lowestLevel: lowestLevel, + highestLevel: highestLevel + }; + }, + + // private + _updateViewport: function() { + this._needsDraw = false; + + var viewport = this.viewport; + var viewportBounds = viewport.getBoundsWithMargins(true); + + // Reset tile's internal drawn state + while (this.lastDrawn.length > 0) { + var tile = this.lastDrawn.pop(); + tile.beingDrawn = false; + } + + if (!this.wrapHorizontal && !this.wrapVertical) { + var tiledImageBounds = this.getClippedBounds(true) + .getBoundingBox(); + var intersection = viewportBounds.intersection(tiledImageBounds); + if (intersection === null) { + return; + } + viewportBounds = intersection; + } + viewportBounds = viewportBounds.getBoundingBox(); + viewportBounds.x -= this._xSpring.current.value; + viewportBounds.y -= this._ySpring.current.value; + + var viewportTL = viewportBounds.getTopLeft(); + var viewportBR = viewportBounds.getBottomRight(); + + //Don't draw if completely outside of the viewport + if (!this.wrapHorizontal && + (viewportBR.x < 0 || viewportTL.x > this._worldWidthCurrent)) { + return; + } + + if (!this.wrapVertical && + (viewportBR.y < 0 || viewportTL.y > this._worldHeightCurrent)) { + return; + } + + // Calculate viewport rect / bounds + if (!this.wrapHorizontal) { + viewportTL.x = Math.max(viewportTL.x, 0); + viewportBR.x = Math.min(viewportBR.x, this._worldWidthCurrent ); + } + + if (!this.wrapVertical) { + viewportTL.y = Math.max(viewportTL.y, 0); + viewportBR.y = Math.min(viewportBR.y, this._worldHeightCurrent); + } + + var levelsInterval = this._getLevelsInterval(); + var lowestLevel = levelsInterval.lowestLevel; + var highestLevel = levelsInterval.highestLevel; + var bestTile = null; + var haveDrawn = false; + var currentTime = $.now(); + + // Update any level that will be drawn + for (var level = highestLevel; level >= lowestLevel; level--) { + var drawLevel = false; + + //Avoid calculations for draw if we have already drawn this + var currentRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio(level), + true + ).x * this._scaleSpring.current.value; + + if (level === lowestLevel || + (!haveDrawn && currentRenderPixelRatio >= this.minPixelRatio)) { + drawLevel = true; + haveDrawn = true; + } else if (!haveDrawn) { + continue; + } + + //Perform calculations for draw if we haven't drawn this + var targetRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio(level), + false + ).x * this._scaleSpring.current.value; //TODO: shouldn't that be target.value? + + var targetZeroRatio = viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio( + Math.max( + this.source.getClosestLevel(viewport.containerSize) - 1, + 0 + ) + ), + false + ).x * this._scaleSpring.current.value; //TODO: shouldn't that be target.value? + + var optimalRatio = this.immediateRender ? 1 : targetZeroRatio; + var levelOpacity = Math.min(1, (currentRenderPixelRatio - 0.5) / 0.5); + var levelVisibility = optimalRatio / Math.abs( + optimalRatio - targetRenderPixelRatio + ); + + // Update the level and keep track of 'best' tile to load + bestTile = updateLevel( + this, + haveDrawn, + drawLevel, + level, + levelOpacity, + levelVisibility, + viewportTL, + viewportBR, + currentTime, + bestTile + ); + + // Stop the loop if lower-res tiles would all be covered by + // already drawn tiles + if (providesCoverage(this.coverage, level)) { + break; + } + } + + // Perform the actual drawing + drawTiles(this, this.lastDrawn); + + // Load the new 'best' tile + if (bestTile && !bestTile.context2D) { + loadTile(this, bestTile, currentTime); + this._setFullyLoaded(false); + } else { + this._setFullyLoaded(true); + } } }); -/** - * @private - * @inner - * Pretty much every other line in this needs to be documented so it's clear - * how each piece of this routine contributes to the drawing process. That's - * why there are so many TODO's inside this function. - */ -function updateViewport( tiledImage ) { - - tiledImage._needsDraw = false; - - var tile, - level, - best = null, - haveDrawn = false, - currentTime = $.now(), - viewportBounds = tiledImage.viewport.getBoundsWithMargins( true ), - zeroRatioC = tiledImage.viewport.deltaPixelsFromPointsNoRotate( - tiledImage.source.getPixelRatio( 0 ), - true - ).x * tiledImage._scaleSpring.current.value, - lowestLevel = Math.max( - tiledImage.source.minLevel, - Math.floor( - Math.log( tiledImage.minZoomImageRatio ) / - Math.log( 2 ) - ) - ), - highestLevel = Math.min( - Math.abs(tiledImage.source.maxLevel), - Math.abs(Math.floor( - Math.log( zeroRatioC / tiledImage.minPixelRatio ) / - Math.log( 2 ) - )) - ), - renderPixelRatioC, - renderPixelRatioT, - zeroRatioT, - optimalRatio, - levelOpacity, - levelVisibility; - - // Reset tile's internal drawn state - while (tiledImage.lastDrawn.length > 0) { - tile = tiledImage.lastDrawn.pop(); - tile.beingDrawn = false; - } - - if (!tiledImage.wrapHorizontal && !tiledImage.wrapVertical) { - var tiledImageBounds = tiledImage.getClippedBounds(true) - .getBoundingBox(); - var intersection = viewportBounds.intersection(tiledImageBounds); - if (intersection === null) { - return; - } - viewportBounds = intersection; - } - viewportBounds = viewportBounds.getBoundingBox(); - viewportBounds.x -= tiledImage._xSpring.current.value; - viewportBounds.y -= tiledImage._ySpring.current.value; - - var viewportTL = viewportBounds.getTopLeft(); - var viewportBR = viewportBounds.getBottomRight(); - - //Don't draw if completely outside of the viewport - if ( !tiledImage.wrapHorizontal && (viewportBR.x < 0 || viewportTL.x > tiledImage._worldWidthCurrent ) ) { - return; - } - - if ( !tiledImage.wrapVertical && ( viewportBR.y < 0 || viewportTL.y > tiledImage._worldHeightCurrent ) ) { - return; - } - - // Calculate viewport rect / bounds - if ( !tiledImage.wrapHorizontal ) { - viewportTL.x = Math.max( viewportTL.x, 0 ); - viewportBR.x = Math.min( viewportBR.x, tiledImage._worldWidthCurrent ); - } - - if ( !tiledImage.wrapVertical ) { - viewportTL.y = Math.max( viewportTL.y, 0 ); - viewportBR.y = Math.min( viewportBR.y, tiledImage._worldHeightCurrent ); - } - - // Calculations for the interval of levels to draw - // (above in initial var statement) - // can return invalid intervals; fix that here if necessary - lowestLevel = Math.min( lowestLevel, highestLevel ); - - // Update any level that will be drawn - var drawLevel; // FIXME: drawLevel should have a more explanatory name - for ( level = highestLevel; level >= lowestLevel; level-- ) { - drawLevel = false; - - //Avoid calculations for draw if we have already drawn this - renderPixelRatioC = tiledImage.viewport.deltaPixelsFromPointsNoRotate( - tiledImage.source.getPixelRatio( level ), - true - ).x * tiledImage._scaleSpring.current.value; - - if ( ( !haveDrawn && renderPixelRatioC >= tiledImage.minPixelRatio ) || - ( level == lowestLevel ) ) { - drawLevel = true; - haveDrawn = true; - } else if ( !haveDrawn ) { - continue; - } - - //Perform calculations for draw if we haven't drawn this - renderPixelRatioT = tiledImage.viewport.deltaPixelsFromPointsNoRotate( - tiledImage.source.getPixelRatio( level ), - false - ).x * tiledImage._scaleSpring.current.value; - - zeroRatioT = tiledImage.viewport.deltaPixelsFromPointsNoRotate( - tiledImage.source.getPixelRatio( - Math.max( - tiledImage.source.getClosestLevel( tiledImage.viewport.containerSize ) - 1, - 0 - ) - ), - false - ).x * tiledImage._scaleSpring.current.value; - - optimalRatio = tiledImage.immediateRender ? - 1 : - zeroRatioT; - - levelOpacity = Math.min( 1, ( renderPixelRatioC - 0.5 ) / 0.5 ); - - levelVisibility = optimalRatio / Math.abs( - optimalRatio - renderPixelRatioT - ); - - // Update the level and keep track of 'best' tile to load - best = updateLevel( - tiledImage, - haveDrawn, - drawLevel, - level, - levelOpacity, - levelVisibility, - viewportTL, - viewportBR, - currentTime, - best - ); - - // Stop the loop if lower-res tiles would all be covered by - // already drawn tiles - if ( providesCoverage( tiledImage.coverage, level ) ) { - break; - } - } - - // Perform the actual drawing - drawTiles( tiledImage, tiledImage.lastDrawn ); - - // Load the new 'best' tile - if (best && !best.context2D) { - loadTile( tiledImage, best, currentTime ); - tiledImage._setFullyLoaded(false); - } else { - tiledImage._setFullyLoaded(true); - } -} - - function updateLevel( tiledImage, haveDrawn, drawLevel, level, levelOpacity, levelVisibility, viewportTL, viewportBR, currentTime, best ){ - var x, y, - tileTL, - tileBR, - numberOfTiles, - viewportCenter = tiledImage.viewport.pixelFromPoint( tiledImage.viewport.getCenter() ); - - - if( tiledImage.viewer ){ + if (tiledImage.viewer) { /** * - Needs documentation - * @@ -1016,7 +997,7 @@ function updateLevel( tiledImage, haveDrawn, drawLevel, level, levelOpacity, lev * @property {Object} best * @property {?Object} userData - Arbitrary subscriber-defined object. */ - tiledImage.viewer.raiseEvent( 'update-level', { + tiledImage.viewer.raiseEvent('update-level', { tiledImage: tiledImage, havedrawn: haveDrawn, level: level, @@ -1030,25 +1011,29 @@ function updateLevel( tiledImage, haveDrawn, drawLevel, level, levelOpacity, lev } //OK, a new drawing so do your calculations - tileTL = tiledImage.source.getTileAtPoint( level, viewportTL.divide( tiledImage._scaleSpring.current.value )); - tileBR = tiledImage.source.getTileAtPoint( level, viewportBR.divide( tiledImage._scaleSpring.current.value )); - numberOfTiles = tiledImage.source.getNumTiles( level ); + var topLeftTile = tiledImage.source.getTileAtPoint( + level, viewportTL.divide(tiledImage._scaleSpring.current.value)); + var bottomRightTile = tiledImage.source.getTileAtPoint( + level, viewportBR.divide(tiledImage._scaleSpring.current.value)); + var numberOfTiles = tiledImage.source.getNumTiles(level); - resetCoverage( tiledImage.coverage, level ); + resetCoverage(tiledImage.coverage, level); - if ( tiledImage.wrapHorizontal ) { - tileTL.x -= 1; // left invisible column (othervise we will have empty space after scroll at left) + if (tiledImage.wrapHorizontal) { + topLeftTile.x -= 1; // left invisible column (othervise we will have empty space after scroll at left) } else { - tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 ); + bottomRightTile.x = Math.min(bottomRightTile.x, numberOfTiles.x - 1); } - if ( tiledImage.wrapVertical ) { - tileTL.y -= 1; // top invisible row (othervise we will have empty space after scroll at top) + if (tiledImage.wrapVertical) { + topLeftTile.y -= 1; // top invisible row (othervise we will have empty space after scroll at top) } else { - tileBR.y = Math.min( tileBR.y, numberOfTiles.y - 1 ); + bottomRightTile.y = Math.min(bottomRightTile.y, numberOfTiles.y - 1); } - for ( x = tileTL.x; x <= tileBR.x; x++ ) { - for ( y = tileTL.y; y <= tileBR.y; y++ ) { + var viewportCenter = tiledImage.viewport.pixelFromPoint( + tiledImage.viewport.getCenter()); + for (var x = topLeftTile.x; x <= bottomRightTile.x; x++) { + for (var y = topLeftTile.y; y <= bottomRightTile.y; y++) { best = updateTile( tiledImage, From 2e3f57401fa9eea094112fa0c317e26fdaaf2a42 Mon Sep 17 00:00:00 2001 From: Antoine Vandecreme Date: Sun, 28 Aug 2016 12:10:35 +0200 Subject: [PATCH 04/15] Fix tiles missing with rotation + rotate around center --- src/drawer.js | 2 +- src/rectangle.js | 5 ++ src/tiledimage.js | 187 ++++++++++++++++++++++++++++------------------ src/world.js | 4 +- 4 files changed, 121 insertions(+), 77 deletions(-) diff --git a/src/drawer.js b/src/drawer.js index 3175a575..a77edd56 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -483,7 +483,7 @@ $.Drawer.prototype = { this._offsetForRotation( tiledImage.getRotation(), tiledImage.viewport.pixelFromPointNoRotate( - tiledImage.getBounds(true).getTopLeft(), true)); + tiledImage._getRotationPoint(true), true)); } context.strokeRect( diff --git a/src/rectangle.js b/src/rectangle.js index 9d284df0..a73ad3f3 100644 --- a/src/rectangle.js +++ b/src/rectangle.js @@ -449,6 +449,11 @@ $.Rect.prototype = { var newTopRight = this.getTopRight().rotate(degrees, pivot); var diff = newTopRight.minus(newTopLeft); + // Handle floating point error + diff = diff.apply(function(x) { + var EPSILON = 1e-15; + return Math.abs(x) < EPSILON ? 0 : x; + }); var radians = Math.atan(diff.y / diff.x); if (diff.x < 0) { radians += Math.PI; diff --git a/src/tiledimage.js b/src/tiledimage.js index 59c449c9..75989142 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -305,23 +305,35 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag }, /** + * Get this TiledImage's bounds in viewport coordinates. + * @param {Boolean} [current=false] - Pass true for the current location; + * false for target location. * @returns {OpenSeadragon.Rect} This TiledImage's bounds in viewport coordinates. - * @param {Boolean} [current=false] - Pass true for the current location; false for target location. */ getBounds: function(current) { + return this.getBoundsNoRotate(current) + .rotate(this._degrees, this._getRotationPoint(current)); + }, + + /** + * Get this TiledImage's bounds in viewport coordinates without taking + * rotation into account. + * @param {Boolean} [current=false] - Pass true for the current location; + * false for target location. + * @returns {OpenSeadragon.Rect} This TiledImage's bounds in viewport coordinates. + */ + getBoundsNoRotate: function(current) { return current ? new $.Rect( this._xSpring.current.value, this._ySpring.current.value, this._worldWidthCurrent, - this._worldHeightCurrent, - this._degrees) : + this._worldHeightCurrent) : new $.Rect( this._xSpring.target.value, this._ySpring.target.value, this._worldWidthTarget, - this._worldHeightTarget, - this._degrees); + this._worldHeightTarget); }, // deprecated @@ -337,18 +349,19 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @returns {$.Rect} The clipped bounds in viewport coordinates. */ getClippedBounds: function(current) { - var bounds = this.getBounds(current); + var bounds = this.getBoundsNoRotate(current); if (this._clip) { - var ratio = this._worldWidthCurrent / this.source.dimensions.x; + var worldWidth = current ? + this._worldWidthCurrent : this._worldWidthTarget; + var ratio = worldWidth / this.source.dimensions.x; var clip = this._clip.times(ratio); bounds = new $.Rect( bounds.x + clip.x, bounds.y + clip.y, clip.width, - clip.height, - this._degrees); + clip.height); } - return bounds; + return bounds.rotate(this._degrees, this._getRotationPoint(current)); }, /** @@ -373,21 +386,24 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. * @return {OpenSeadragon.Point} A point representing the coordinates in the image. */ - viewportToImageCoordinates: function( viewerX, viewerY, current ) { + viewportToImageCoordinates: function(viewerX, viewerY, current) { + var point; if (viewerX instanceof $.Point) { //they passed a point instead of individual components current = viewerY; - viewerY = viewerX.y; - viewerX = viewerX.x; + point = viewerX; + } else { + point = new $.Point(viewerX, viewerY); } - if (current) { - return this._viewportToImageDelta(viewerX - this._xSpring.current.value, - viewerY - this._ySpring.current.value); - } - - return this._viewportToImageDelta(viewerX - this._xSpring.target.value, - viewerY - this._ySpring.target.value); + point = point.rotate(-this._degrees, this._getRotationPoint(current)); + return current ? + this._viewportToImageDelta( + point.x - this._xSpring.current.value, + point.y - this._ySpring.current.value) : + this._viewportToImageDelta( + point.x - this._xSpring.target.value, + point.y - this._ySpring.target.value); }, // private @@ -405,7 +421,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. * @return {OpenSeadragon.Point} A point representing the coordinates in the viewport. */ - imageToViewportCoordinates: function( imageX, imageY, current ) { + imageToViewportCoordinates: function(imageX, imageY, current) { if (imageX instanceof $.Point) { //they passed a point instead of individual components current = imageY; @@ -422,7 +438,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag point.y += this._ySpring.target.value; } - return point; + return point.rotate(this._degrees, this._getRotationPoint(current)); }, /** @@ -453,7 +469,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag coordA.y, coordB.x, coordB.y, - rect.degrees + rect.degrees + this._degrees ); }, @@ -485,7 +501,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag coordA.y, coordB.x, coordB.y, - rect.degrees + rect.degrees - this._degrees ); }, @@ -533,6 +549,32 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag OpenSeadragon.getElementPosition( this.viewer.element )); }, + // private + // Convert rectangle in tiled image coordinates to viewport coordinates. + _tiledImageToViewportRectangle: function(rect) { + var scale = this._scaleSpring.current.value; + return new $.Rect( + rect.x * scale + this._xSpring.current.value, + rect.y * scale + this._ySpring.current.value, + rect.width * scale, + rect.height * scale, + rect.degrees) + .rotate(this.getRotation(), this._getRotationPoint(true)); + }, + + // private + // Convert rectangle in viewport coordinates to tiled image coordinates. + _viewportToTiledImageRectangle: function(rect) { + var scale = this._scaleSpring.current.value; + rect = rect.rotate(-this.getRotation(), this._getRotationPoint(true)); + return new $.Rect( + (rect.x - this._xSpring.current.value) / scale, + (rect.y - this._ySpring.current.value) / scale, + rect.width / scale, + rect.height / scale, + rect.degrees); + }, + /** * Convert a viewport zoom to an image zoom. * Image zoom: ratio of the original image size to displayed image size. @@ -738,7 +780,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag /** * Set the current rotation of this tiled image in degrees. - * @param {Number} the rotation in degrees. + * @param {Number} degrees the rotation in degrees. */ setRotation: function(degrees) { degrees = $.positiveModulo(degrees, 360); @@ -750,6 +792,16 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag this._raiseBoundsChange(); }, + /** + * @private + * Get the point around which this tiled image is rotated + * @param {Boolean} current True for current rotation point, false for target. + * @returns {OpenSeadragon.Point} + */ + _getRotationPoint: function(current) { + return this.getBoundsNoRotate(current).getTopLeft(); + }, + /** * @returns {String} The TiledImage's current compositeOperation. */ @@ -848,51 +900,23 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag _updateViewport: function() { this._needsDraw = false; - var viewport = this.viewport; - var viewportBounds = viewport.getBoundsWithMargins(true); - // Reset tile's internal drawn state while (this.lastDrawn.length > 0) { var tile = this.lastDrawn.pop(); tile.beingDrawn = false; } + var viewport = this.viewport; + var drawArea = this._viewportToTiledImageRectangle( + viewport.getBoundsWithMargins(true)); + if (!this.wrapHorizontal && !this.wrapVertical) { - var tiledImageBounds = this.getClippedBounds(true) - .getBoundingBox(); - var intersection = viewportBounds.intersection(tiledImageBounds); - if (intersection === null) { + var tiledImageBounds = this._viewportToTiledImageRectangle( + this.getClippedBounds(true)); + drawArea = drawArea.intersection(tiledImageBounds); + if (drawArea === null) { return; } - viewportBounds = intersection; - } - viewportBounds = viewportBounds.getBoundingBox(); - viewportBounds.x -= this._xSpring.current.value; - viewportBounds.y -= this._ySpring.current.value; - - var viewportTL = viewportBounds.getTopLeft(); - var viewportBR = viewportBounds.getBottomRight(); - - //Don't draw if completely outside of the viewport - if (!this.wrapHorizontal && - (viewportBR.x < 0 || viewportTL.x > this._worldWidthCurrent)) { - return; - } - - if (!this.wrapVertical && - (viewportBR.y < 0 || viewportTL.y > this._worldHeightCurrent)) { - return; - } - - // Calculate viewport rect / bounds - if (!this.wrapHorizontal) { - viewportTL.x = Math.max(viewportTL.x, 0); - viewportBR.x = Math.min(viewportBR.x, this._worldWidthCurrent ); - } - - if (!this.wrapVertical) { - viewportTL.y = Math.max(viewportTL.y, 0); - viewportBR.y = Math.min(viewportBR.y, this._worldHeightCurrent); } var levelsInterval = this._getLevelsInterval(); @@ -950,8 +974,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag level, levelOpacity, levelVisibility, - viewportTL, - viewportBR, + drawArea, currentTime, bestTile ); @@ -976,7 +999,11 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag } }); -function updateLevel( tiledImage, haveDrawn, drawLevel, level, levelOpacity, levelVisibility, viewportTL, viewportBR, currentTime, best ){ +function updateLevel(tiledImage, haveDrawn, drawLevel, level, levelOpacity, + levelVisibility, drawArea, currentTime, best) { + + var topLeftBound = drawArea.getBoundingBox().getTopLeft(); + var bottomRightBound = drawArea.getBoundingBox().getBottomRight(); if (tiledImage.viewer) { /** @@ -991,8 +1018,9 @@ function updateLevel( tiledImage, haveDrawn, drawLevel, level, levelOpacity, lev * @property {Object} level * @property {Object} opacity * @property {Object} visibility - * @property {Object} topleft - * @property {Object} bottomright + * @property {OpenSeadragon.Rect} drawArea + * @property {Object} topleft deprecated, use drawArea instead + * @property {Object} bottomright deprecated, use drawArea instead * @property {Object} currenttime * @property {Object} best * @property {?Object} userData - Arbitrary subscriber-defined object. @@ -1003,18 +1031,17 @@ function updateLevel( tiledImage, haveDrawn, drawLevel, level, levelOpacity, lev level: level, opacity: levelOpacity, visibility: levelVisibility, - topleft: viewportTL, - bottomright: viewportBR, + drawArea: drawArea, + topleft: topLeftBound, + bottomright: bottomRightBound, currenttime: currentTime, best: best }); } //OK, a new drawing so do your calculations - var topLeftTile = tiledImage.source.getTileAtPoint( - level, viewportTL.divide(tiledImage._scaleSpring.current.value)); - var bottomRightTile = tiledImage.source.getTileAtPoint( - level, viewportBR.divide(tiledImage._scaleSpring.current.value)); + var topLeftTile = tiledImage.source.getTileAtPoint(level, topLeftBound); + var bottomRightTile = tiledImage.source.getTileAtPoint(level, bottomRightBound); var numberOfTiles = tiledImage.source.getNumTiles(level); resetCoverage(tiledImage.coverage, level); @@ -1022,11 +1049,15 @@ function updateLevel( tiledImage, haveDrawn, drawLevel, level, levelOpacity, lev if (tiledImage.wrapHorizontal) { topLeftTile.x -= 1; // left invisible column (othervise we will have empty space after scroll at left) } else { + // Adjust for floating point error + topLeftTile.x = Math.max(topLeftTile.x, 0); bottomRightTile.x = Math.min(bottomRightTile.x, numberOfTiles.x - 1); } if (tiledImage.wrapVertical) { topLeftTile.y -= 1; // top invisible row (othervise we will have empty space after scroll at top) } else { + // Adjust for floating point error + topLeftTile.y = Math.max(topLeftTile.y, 0); bottomRightTile.y = Math.min(bottomRightTile.y, numberOfTiles.y - 1); } @@ -1035,6 +1066,13 @@ function updateLevel( tiledImage, haveDrawn, drawLevel, level, levelOpacity, lev for (var x = topLeftTile.x; x <= bottomRightTile.x; x++) { for (var y = topLeftTile.y; y <= bottomRightTile.y; y++) { + var tileBounds = tiledImage.source.getTileBounds(level, x, y); + + if (drawArea.intersection(tileBounds) === null) { + // This tile is outside of the viewport, no need to draw it + continue; + } + best = updateTile( tiledImage, drawLevel, @@ -1529,7 +1567,7 @@ function drawTiles( tiledImage, lastDrawn ) { tiledImage._drawer._offsetForRotation( tiledImage._degrees, tiledImage.viewport.pixelFromPointNoRotate( - tiledImage.getBounds(true).getTopLeft(), true), + tiledImage._getRotationPoint(true), true), useSketch); } } @@ -1539,6 +1577,7 @@ function drawTiles( tiledImage, lastDrawn ) { tiledImage._drawer.saveContext(useSketch); var box = tiledImage.imageToViewportRectangle(tiledImage._clip, true); + box = box.rotate(-tiledImage._degrees, tiledImage._getRotationPoint()); var clipRect = tiledImage._drawer.viewportToDrawerRectangle(box); if (sketchScale) { clipRect = clipRect.times(sketchScale); @@ -1618,7 +1657,7 @@ function drawTiles( tiledImage, lastDrawn ) { tiledImage._drawer._offsetForRotation( tiledImage._degrees, tiledImage.viewport.pixelFromPointNoRotate( - tiledImage.getBounds(true).getTopLeft(), true), + tiledImage._getRotationPoint(true), true), useSketch); } } diff --git a/src/world.js b/src/world.js index 07e99b81..b06ae484 100644 --- a/src/world.js +++ b/src/world.js @@ -383,7 +383,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W var item = this._items[0]; var bounds = item.getBounds(); this._contentFactor = item.getContentSize().x / bounds.width; - var clippedBounds = item.getClippedBounds(); + var clippedBounds = item.getClippedBounds().getBoundingBox(); var left = clippedBounds.x; var top = clippedBounds.y; var right = clippedBounds.x + clippedBounds.width; @@ -393,7 +393,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W bounds = item.getBounds(); this._contentFactor = Math.max(this._contentFactor, item.getContentSize().x / bounds.width); - clippedBounds = item.getClippedBounds(); + clippedBounds = item.getClippedBounds().getBoundingBox(); left = Math.min(left, clippedBounds.x); top = Math.min(top, clippedBounds.y); right = Math.max(right, clippedBounds.x + clippedBounds.width); From 33332bf7748ca36bcc8df7c4478d4a2d281d81e3 Mon Sep 17 00:00:00 2001 From: Antoine Vandecreme Date: Sun, 28 Aug 2016 13:39:26 +0200 Subject: [PATCH 05/15] Set rotation around center and fix typo. --- src/tiledimage.js | 2 +- src/tiledimage.js.orig | 1707 ++++++++++++++++++++++++++++++++++++++++ src/viewer.js | 2 +- 3 files changed, 1709 insertions(+), 2 deletions(-) create mode 100644 src/tiledimage.js.orig diff --git a/src/tiledimage.js b/src/tiledimage.js index 446ed1f5..bd54d232 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -799,7 +799,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @returns {OpenSeadragon.Point} */ _getRotationPoint: function(current) { - return this.getBoundsNoRotate(current).getTopLeft(); + return this.getBoundsNoRotate(current).getCenter(); }, /** diff --git a/src/tiledimage.js.orig b/src/tiledimage.js.orig new file mode 100644 index 00000000..2eaa471e --- /dev/null +++ b/src/tiledimage.js.orig @@ -0,0 +1,1707 @@ +/* + * OpenSeadragon - TiledImage + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 OpenSeadragon contributors + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of CodePlex Foundation nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +(function( $ ){ + +/** + * You shouldn't have to create a TiledImage directly; use {@link OpenSeadragon.Viewer#open} + * or {@link OpenSeadragon.Viewer#addTiledImage} instead. + * @class TiledImage + * @memberof OpenSeadragon + * @extends OpenSeadragon.EventSource + * @classdesc Handles rendering of tiles for an {@link OpenSeadragon.Viewer}. + * A new instance is created for each TileSource opened. + * @param {Object} options - Configuration for this TiledImage. + * @param {OpenSeadragon.TileSource} options.source - The TileSource that defines this TiledImage. + * @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this TiledImage. + * @param {OpenSeadragon.TileCache} options.tileCache - The TileCache for this TiledImage to use. + * @param {OpenSeadragon.Drawer} options.drawer - The Drawer for this TiledImage to draw onto. + * @param {OpenSeadragon.ImageLoader} options.imageLoader - The ImageLoader for this TiledImage to use. + * @param {Number} [options.x=0] - Left position, in viewport coordinates. + * @param {Number} [options.y=0] - Top position, in viewport coordinates. + * @param {Number} [options.width=1] - Width, in viewport coordinates. + * @param {Number} [options.height] - Height, in viewport coordinates. + * @param {OpenSeadragon.Rect} [options.fitBounds] The bounds in viewport coordinates + * to fit the image into. If specified, x, y, width and height get ignored. + * @param {OpenSeadragon.Placement} [options.fitBoundsPlacement=OpenSeadragon.Placement.CENTER] + * How to anchor the image in the bounds if options.fitBounds is set. + * @param {OpenSeadragon.Rect} [options.clip] - An area, in image pixels, to clip to + * (portions of the image outside of this area will not be visible). Only works on + * browsers that support the HTML5 canvas. + * @param {Number} [options.springStiffness] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.animationTime] - See {@link OpenSeadragon.Options}. + * @param {Number} [options.minZoomImageRatio] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.wrapHorizontal] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.wrapVertical] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.immediateRender] - See {@link OpenSeadragon.Options}. + * @param {Number} [options.blendTime] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.alwaysBlend] - See {@link OpenSeadragon.Options}. + * @param {Number} [options.minPixelRatio] - See {@link OpenSeadragon.Options}. + * @param {Number} [options.smoothTileEdgesMinZoom] - See {@link OpenSeadragon.Options}. + * @param {Boolean} [options.iOSDevice] - See {@link OpenSeadragon.Options}. + * @param {Number} [options.opacity=1] - Opacity the tiled image should be drawn at. + * @param {String} [options.compositeOperation] - How the image is composited onto other images; see compositeOperation in {@link OpenSeadragon.Options} for possible values. + * @param {Boolean} [options.debugMode] - See {@link OpenSeadragon.Options}. + * @param {String|CanvasGradient|CanvasPattern|Function} [options.placeholderFillStyle] - See {@link OpenSeadragon.Options}. + * @param {String|Boolean} [options.crossOriginPolicy] - See {@link OpenSeadragon.Options}. + */ +$.TiledImage = function( options ) { + var _this = this; + + $.console.assert( options.tileCache, "[TiledImage] options.tileCache is required" ); + $.console.assert( options.drawer, "[TiledImage] options.drawer is required" ); + $.console.assert( options.viewer, "[TiledImage] options.viewer is required" ); + $.console.assert( options.imageLoader, "[TiledImage] options.imageLoader is required" ); + $.console.assert( options.source, "[TiledImage] options.source is required" ); + $.console.assert(!options.clip || options.clip instanceof $.Rect, + "[TiledImage] options.clip must be an OpenSeadragon.Rect if present"); + + $.EventSource.call( this ); + + this._tileCache = options.tileCache; + delete options.tileCache; + + this._drawer = options.drawer; + delete options.drawer; + + this._imageLoader = options.imageLoader; + delete options.imageLoader; + + if (options.clip instanceof $.Rect) { + this._clip = options.clip.clone(); + } + + delete options.clip; + + var x = options.x || 0; + delete options.x; + var y = options.y || 0; + delete options.y; + + // Ratio of zoomable image height to width. + this.normHeight = options.source.dimensions.y / options.source.dimensions.x; + this.contentAspectX = options.source.dimensions.x / options.source.dimensions.y; + + var scale = 1; + if ( options.width ) { + scale = options.width; + delete options.width; + + if ( options.height ) { + $.console.error( "specifying both width and height to a tiledImage is not supported" ); + delete options.height; + } + } else if ( options.height ) { + scale = options.height / this.normHeight; + delete options.height; + } + + var fitBounds = options.fitBounds; + delete options.fitBounds; + var fitBoundsPlacement = options.fitBoundsPlacement || OpenSeadragon.Placement.CENTER; + delete options.fitBoundsPlacement; + + this._degrees = $.positiveModulo(options.degrees || 0, 360); + delete options.degrees; + + $.extend( true, this, { + + //internal state properties + viewer: null, + tilesMatrix: {}, // A '3d' dictionary [level][x][y] --> Tile. + coverage: {}, // A '3d' dictionary [level][x][y] --> Boolean. + lastDrawn: [], // An unordered list of Tiles drawn last frame. + lastResetTime: 0, // Last time for which the tiledImage was reset. + _midDraw: false, // Is the tiledImage currently updating the viewport? + _needsDraw: true, // Does the tiledImage need to update the viewport again? + _hasOpaqueTile: false, // Do we have even one fully opaque tile? + //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, + smoothTileEdgesMinZoom: $.DEFAULT_SETTINGS.smoothTileEdgesMinZoom, + iOSDevice: $.DEFAULT_SETTINGS.iOSDevice, + debugMode: $.DEFAULT_SETTINGS.debugMode, + crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy, + placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, + opacity: $.DEFAULT_SETTINGS.opacity, + compositeOperation: $.DEFAULT_SETTINGS.compositeOperation + }, options ); + + this._fullyLoaded = false; + + this._xSpring = new $.Spring({ + initial: x, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + + this._ySpring = new $.Spring({ + initial: y, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + + this._scaleSpring = new $.Spring({ + initial: scale, + springStiffness: this.springStiffness, + animationTime: this.animationTime + }); + + this._updateForScale(); + + if (fitBounds) { + this.fitBounds(fitBounds, fitBoundsPlacement, true); + } + + // We need a callback to give image manipulation a chance to happen + this._drawingHandler = function(args) { + /** + * This event is fired just before the tile is drawn giving the application a chance to alter the image. + * + * NOTE: This event is only fired when the drawer is using a <canvas>. + * + * @event tile-drawing + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.Tile} tile - The Tile being drawn. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {OpenSeadragon.Tile} context - The HTML canvas context being drawn into. + * @property {OpenSeadragon.Tile} rendered - The HTML canvas context containing the tile imagery. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.viewer.raiseEvent('tile-drawing', $.extend({ + tiledImage: _this + }, args)); + }; +}; + +$.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.TiledImage.prototype */{ + /** + * @returns {Boolean} Whether the TiledImage needs to be drawn. + */ + needsDraw: function() { + return this._needsDraw; + }, + + /** + * @returns {Boolean} Whether all tiles necessary for this TiledImage to draw at the current view have been loaded. + */ + getFullyLoaded: function() { + return this._fullyLoaded; + }, + + // private + _setFullyLoaded: function(flag) { + if (flag === this._fullyLoaded) { + return; + } + + this._fullyLoaded = flag; + + /** + * Fired when the TiledImage's "fully loaded" flag (whether all tiles necessary for this TiledImage + * to draw at the current view have been loaded) changes. + * + * @event fully-loaded-change + * @memberof OpenSeadragon.TiledImage + * @type {object} + * @property {Boolean} fullyLoaded - The new "fully loaded" value. + * @property {OpenSeadragon.TiledImage} eventSource - A reference to the TiledImage which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('fully-loaded-change', { + fullyLoaded: this._fullyLoaded + }); + }, + + /** + * Clears all tiles and triggers an update on the next call to + * {@link OpenSeadragon.TiledImage#update}. + */ + reset: function() { + this._tileCache.clearTilesFor(this); + this.lastResetTime = $.now(); + this._needsDraw = true; + }, + + /** + * Updates the TiledImage's bounds, animating if needed. + * @returns {Boolean} Whether the TiledImage animated. + */ + update: function() { + var oldX = this._xSpring.current.value; + var oldY = this._ySpring.current.value; + var oldScale = this._scaleSpring.current.value; + + this._xSpring.update(); + this._ySpring.update(); + this._scaleSpring.update(); + + if (this._xSpring.current.value !== oldX || this._ySpring.current.value !== oldY || + this._scaleSpring.current.value !== oldScale) { + this._updateForScale(); + this._needsDraw = true; + return true; + } + + return false; + }, + + /** + * Draws the TiledImage to its Drawer. + */ + draw: function() { + if (this.opacity !== 0) { + this._midDraw = true; + this._updateViewport(); + this._midDraw = false; + } + }, + + /** + * Destroy the TiledImage (unload current loaded tiles). + */ + destroy: function() { + this.reset(); + }, + + /** + * Get this TiledImage's bounds in viewport coordinates. + * @param {Boolean} [current=false] - Pass true for the current location; + * false for target location. + * @returns {OpenSeadragon.Rect} This TiledImage's bounds in viewport coordinates. + */ + getBounds: function(current) { + return this.getBoundsNoRotate(current) + .rotate(this._degrees, this._getRotationPoint(current)); + }, + + /** + * Get this TiledImage's bounds in viewport coordinates without taking + * rotation into account. + * @param {Boolean} [current=false] - Pass true for the current location; + * false for target location. + * @returns {OpenSeadragon.Rect} This TiledImage's bounds in viewport coordinates. + */ + getBoundsNoRotate: function(current) { + return current ? + new $.Rect( + this._xSpring.current.value, + this._ySpring.current.value, + this._worldWidthCurrent, + this._worldHeightCurrent) : + new $.Rect( + this._xSpring.target.value, + this._ySpring.target.value, + this._worldWidthTarget, + this._worldHeightTarget); + }, + + // deprecated + getWorldBounds: function() { + $.console.error('[TiledImage.getWorldBounds] is deprecated; use TiledImage.getBounds instead'); + return this.getBounds(); + }, + + /** + * Get the bounds of the displayed part of the tiled image. + * @param {Boolean} [current=false] Pass true for the current location, + * false for the target location. + * @returns {$.Rect} The clipped bounds in viewport coordinates. + */ + getClippedBounds: function(current) { + var bounds = this.getBoundsNoRotate(current); + if (this._clip) { + var worldWidth = current ? + this._worldWidthCurrent : this._worldWidthTarget; + var ratio = worldWidth / this.source.dimensions.x; + var clip = this._clip.times(ratio); + bounds = new $.Rect( + bounds.x + clip.x, + bounds.y + clip.y, + clip.width, + clip.height); + } + return bounds.rotate(this._degrees, this._getRotationPoint(current)); + }, + + /** + * @returns {OpenSeadragon.Point} This TiledImage's content size, in original pixels. + */ + getContentSize: function() { + return new $.Point(this.source.dimensions.x, this.source.dimensions.y); + }, + + // private + _viewportToImageDelta: function( viewerX, viewerY, current ) { + var scale = (current ? this._scaleSpring.current.value : this._scaleSpring.target.value); + return new $.Point(viewerX * (this.source.dimensions.x / scale), + viewerY * ((this.source.dimensions.y * this.contentAspectX) / scale)); + }, + + /** + * Translates from OpenSeadragon viewer coordinate system to image coordinate system. + * This method can be called either by passing X,Y coordinates or an {@link OpenSeadragon.Point}. + * @param {Number|OpenSeadragon.Point} viewerX - The X coordinate or point in viewport coordinate system. + * @param {Number} [viewerY] - The Y coordinate in viewport coordinate system. + * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. + * @return {OpenSeadragon.Point} A point representing the coordinates in the image. + */ + viewportToImageCoordinates: function(viewerX, viewerY, current) { + var point; + if (viewerX instanceof $.Point) { + //they passed a point instead of individual components + current = viewerY; + point = viewerX; + } else { + point = new $.Point(viewerX, viewerY); + } + + point = point.rotate(-this._degrees, this._getRotationPoint(current)); + return current ? + this._viewportToImageDelta( + point.x - this._xSpring.current.value, + point.y - this._ySpring.current.value) : + this._viewportToImageDelta( + point.x - this._xSpring.target.value, + point.y - this._ySpring.target.value); + }, + + // private + _imageToViewportDelta: function( imageX, imageY, current ) { + var scale = (current ? this._scaleSpring.current.value : this._scaleSpring.target.value); + return new $.Point((imageX / this.source.dimensions.x) * scale, + (imageY / this.source.dimensions.y / this.contentAspectX) * scale); + }, + + /** + * Translates from image coordinate system to OpenSeadragon viewer coordinate system + * This method can be called either by passing X,Y coordinates or an {@link OpenSeadragon.Point}. + * @param {Number|OpenSeadragon.Point} imageX - The X coordinate or point in image coordinate system. + * @param {Number} [imageY] - The Y coordinate in image coordinate system. + * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. + * @return {OpenSeadragon.Point} A point representing the coordinates in the viewport. + */ + imageToViewportCoordinates: function(imageX, imageY, current) { + if (imageX instanceof $.Point) { + //they passed a point instead of individual components + current = imageY; + imageY = imageX.y; + imageX = imageX.x; + } + + var point = this._imageToViewportDelta(imageX, imageY); + if (current) { + point.x += this._xSpring.current.value; + point.y += this._ySpring.current.value; + } else { + point.x += this._xSpring.target.value; + point.y += this._ySpring.target.value; + } + + return point.rotate(this._degrees, this._getRotationPoint(current)); + }, + + /** + * Translates from a rectangle which describes a portion of the image in + * pixel coordinates to OpenSeadragon viewport rectangle coordinates. + * This method can be called either by passing X,Y,width,height or an {@link OpenSeadragon.Rect}. + * @param {Number|OpenSeadragon.Rect} imageX - The left coordinate or rectangle in image coordinate system. + * @param {Number} [imageY] - The top coordinate in image coordinate system. + * @param {Number} [pixelWidth] - The width in pixel of the rectangle. + * @param {Number} [pixelHeight] - The height in pixel of the rectangle. + * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. + * @return {OpenSeadragon.Rect} A rect representing the coordinates in the viewport. + */ + imageToViewportRectangle: function( imageX, imageY, pixelWidth, pixelHeight, current ) { + var rect = imageX; + if (rect instanceof $.Rect) { + //they passed a rect instead of individual components + current = imageY; + } else { + rect = new $.Rect(imageX, imageY, pixelWidth, pixelHeight); + } + + 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, + rect.degrees + this._degrees + ); + }, + + /** + * Translates from a rectangle which describes a portion of + * the viewport in point coordinates to image rectangle coordinates. + * This method can be called either by passing X,Y,width,height or an {@link OpenSeadragon.Rect}. + * @param {Number|OpenSeadragon.Rect} viewerX - The left coordinate or rectangle in viewport coordinate system. + * @param {Number} [viewerY] - The top coordinate in viewport coordinate system. + * @param {Number} [pointWidth] - The width in viewport coordinate system. + * @param {Number} [pointHeight] - The height in viewport coordinate system. + * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. + * @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; + } else { + rect = new $.Rect(viewerX, viewerY, pointWidth, pointHeight); + } + + 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, + rect.degrees - this._degrees + ); + }, + + /** + * Convert pixel coordinates relative to the viewer element to image + * coordinates. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + viewerElementToImageCoordinates: function( pixel ) { + var point = this.viewport.pointFromPixel( pixel, true ); + return this.viewportToImageCoordinates( point ); + }, + + /** + * Convert pixel coordinates relative to the image to + * viewer element coordinates. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + imageToViewerElementCoordinates: function( pixel ) { + var point = this.imageToViewportCoordinates( pixel ); + return this.viewport.pixelFromPoint( point, true ); + }, + + /** + * Convert pixel coordinates relative to the window to image coordinates. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + windowToImageCoordinates: function( pixel ) { + var viewerCoordinates = pixel.minus( + OpenSeadragon.getElementPosition( this.viewer.element )); + return this.viewerElementToImageCoordinates( viewerCoordinates ); + }, + + /** + * Convert image coordinates to pixel coordinates relative to the window. + * @param {OpenSeadragon.Point} pixel + * @returns {OpenSeadragon.Point} + */ + imageToWindowCoordinates: function( pixel ) { + var viewerCoordinates = this.imageToViewerElementCoordinates( pixel ); + return viewerCoordinates.plus( + OpenSeadragon.getElementPosition( this.viewer.element )); + }, + + // private + // Convert rectangle in tiled image coordinates to viewport coordinates. + _tiledImageToViewportRectangle: function(rect) { + var scale = this._scaleSpring.current.value; + return new $.Rect( + rect.x * scale + this._xSpring.current.value, + rect.y * scale + this._ySpring.current.value, + rect.width * scale, + rect.height * scale, + rect.degrees) + .rotate(this.getRotation(), this._getRotationPoint(true)); + }, + + // private + // Convert rectangle in viewport coordinates to tiled image coordinates. + _viewportToTiledImageRectangle: function(rect) { + var scale = this._scaleSpring.current.value; + rect = rect.rotate(-this.getRotation(), this._getRotationPoint(true)); + return new $.Rect( + (rect.x - this._xSpring.current.value) / scale, + (rect.y - this._ySpring.current.value) / scale, + rect.width / scale, + rect.height / scale, + rect.degrees); + }, + + /** + * Convert a viewport zoom to an image zoom. + * Image zoom: ratio of the original image size to displayed image size. + * 1 means original image size, 0.5 half size... + * Viewport zoom: ratio of the displayed image's width to viewport's width. + * 1 means identical width, 2 means image's width is twice the viewport's width... + * @function + * @param {Number} viewportZoom The viewport zoom + * @returns {Number} imageZoom The image zoom + */ + viewportToImageZoom: function( viewportZoom ) { + var ratio = this._scaleSpring.current.value * + this.viewport._containerInnerSize.x / this.source.dimensions.x; + return ratio * viewportZoom ; + }, + + /** + * Convert an image zoom to a viewport zoom. + * Image zoom: ratio of the original image size to displayed image size. + * 1 means original image size, 0.5 half size... + * Viewport zoom: ratio of the displayed image's width to viewport's width. + * 1 means identical width, 2 means image's width is twice the viewport's width... + * Note: not accurate with multi-image. + * @function + * @param {Number} imageZoom The image zoom + * @returns {Number} viewportZoom The viewport zoom + */ + imageToViewportZoom: function( imageZoom ) { + var ratio = this._scaleSpring.current.value * + this.viewport._containerInnerSize.x / this.source.dimensions.x; + return imageZoom / ratio; + }, + + /** + * Sets the TiledImage's position in the world. + * @param {OpenSeadragon.Point} position - The new position, in viewport coordinates. + * @param {Boolean} [immediately=false] - Whether to animate to the new position or snap immediately. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + setPosition: function(position, immediately) { + var sameTarget = (this._xSpring.target.value === position.x && + this._ySpring.target.value === position.y); + + if (immediately) { + if (sameTarget && this._xSpring.current.value === position.x && + this._ySpring.current.value === position.y) { + return; + } + + this._xSpring.resetTo(position.x); + this._ySpring.resetTo(position.y); + this._needsDraw = true; + } else { + if (sameTarget) { + return; + } + + this._xSpring.springTo(position.x); + this._ySpring.springTo(position.y); + this._needsDraw = true; + } + + if (!sameTarget) { + this._raiseBoundsChange(); + } + }, + + /** + * Sets the TiledImage's width in the world, adjusting the height to match based on aspect ratio. + * @param {Number} width - The new width, in viewport coordinates. + * @param {Boolean} [immediately=false] - Whether to animate to the new size or snap immediately. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + setWidth: function(width, immediately) { + this._setScale(width, immediately); + }, + + /** + * Sets the TiledImage's height in the world, adjusting the width to match based on aspect ratio. + * @param {Number} height - The new height, in viewport coordinates. + * @param {Boolean} [immediately=false] - Whether to animate to the new size or snap immediately. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + setHeight: function(height, immediately) { + this._setScale(height / this.normHeight, immediately); + }, + + /** + * Positions and scales the TiledImage to fit in the specified bounds. + * Note: this method fires OpenSeadragon.TiledImage.event:bounds-change + * twice + * @param {OpenSeadragon.Rect} bounds The bounds to fit the image into. + * @param {OpenSeadragon.Placement} [anchor=OpenSeadragon.Placement.CENTER] + * How to anchor the image in the bounds. + * @param {Boolean} [immediately=false] Whether to animate to the new size + * or snap immediately. + * @fires OpenSeadragon.TiledImage.event:bounds-change + */ + fitBounds: function(bounds, anchor, immediately) { + anchor = anchor || $.Placement.CENTER; + var anchorProperties = $.Placement.properties[anchor]; + var aspectRatio = this.contentAspectX; + var xOffset = 0; + var yOffset = 0; + var displayedWidthRatio = 1; + var displayedHeightRatio = 1; + if (this._clip) { + aspectRatio = this._clip.getAspectRatio(); + displayedWidthRatio = this._clip.width / this.source.dimensions.x; + displayedHeightRatio = this._clip.height / this.source.dimensions.y; + if (bounds.getAspectRatio() > aspectRatio) { + xOffset = this._clip.x / this._clip.height * bounds.height; + yOffset = this._clip.y / this._clip.height * bounds.height; + } else { + xOffset = this._clip.x / this._clip.width * bounds.width; + yOffset = this._clip.y / this._clip.width * bounds.width; + } + } + + if (bounds.getAspectRatio() > aspectRatio) { + // We will have margins on the X axis + var height = bounds.height / displayedHeightRatio; + var marginLeft = 0; + if (anchorProperties.isHorizontallyCentered) { + marginLeft = (bounds.width - bounds.height * aspectRatio) / 2; + } else if (anchorProperties.isRight) { + marginLeft = bounds.width - bounds.height * aspectRatio; + } + this.setPosition( + new $.Point(bounds.x - xOffset + marginLeft, bounds.y - yOffset), + immediately); + this.setHeight(height, immediately); + } else { + // We will have margins on the Y axis + var width = bounds.width / displayedWidthRatio; + var marginTop = 0; + if (anchorProperties.isVerticallyCentered) { + marginTop = (bounds.height - bounds.width / aspectRatio) / 2; + } else if (anchorProperties.isBottom) { + marginTop = bounds.height - bounds.width / aspectRatio; + } + this.setPosition( + new $.Point(bounds.x - xOffset, bounds.y - yOffset + marginTop), + immediately); + this.setWidth(width, immediately); + } + }, + + /** + * @returns {OpenSeadragon.Rect|null} The TiledImage's current clip rectangle, + * in image pixels, or null if none. + */ + getClip: function() { + if (this._clip) { + return this._clip.clone(); + } + + return null; + }, + + /** + * @param {OpenSeadragon.Rect|null} newClip - An area, in image pixels, to clip to + * (portions of the image outside of this area will not be visible). Only works on + * browsers that support the HTML5 canvas. + */ + setClip: function(newClip) { + $.console.assert(!newClip || newClip instanceof $.Rect, + "[TiledImage.setClip] newClip must be an OpenSeadragon.Rect or null"); + +//TODO: should this._raiseBoundsChange(); be called? + + if (newClip instanceof $.Rect) { + this._clip = newClip.clone(); + } else { + this._clip = null; + } + + this._needsDraw = true; + }, + + /** + * @returns {Number} The TiledImage's current opacity. + */ + getOpacity: function() { + return this.opacity; + }, + + /** + * @param {Number} opacity Opacity the tiled image should be drawn at. + */ + setOpacity: function(opacity) { + this.opacity = opacity; + this._needsDraw = true; + }, + + /** + * Get the current rotation of this tiled image in degrees. + * @returns {Number} the current rotation of this tiled image in degrees. + */ + getRotation: function() { + return this._degrees; + }, + + /** + * Set the current rotation of this tiled image in degrees. + * @param {Number} degrees the rotation in degrees. + */ + setRotation: function(degrees) { + degrees = $.positiveModulo(degrees, 360); + if (this._degrees === degrees) { + return; + } + this._degrees = degrees; + this._needsDraw = true; + this._raiseBoundsChange(); + }, + + /** + * @private + * Get the point around which this tiled image is rotated + * @param {Boolean} current True for current rotation point, false for target. + * @returns {OpenSeadragon.Point} + */ + _getRotationPoint: function(current) { + return this.getBoundsNoRotate(current).getTopLeft(); + }, + + /** + * @returns {String} The TiledImage's current compositeOperation. + */ + getCompositeOperation: function() { + return this.compositeOperation; + }, + + /** + * @param {String} compositeOperation the tiled image should be drawn with this globalCompositeOperation. + */ + setCompositeOperation: function(compositeOperation) { + this.compositeOperation = compositeOperation; + this._needsDraw = true; + }, + + // private + _setScale: function(scale, immediately) { + var sameTarget = (this._scaleSpring.target.value === scale); + if (immediately) { + if (sameTarget && this._scaleSpring.current.value === scale) { + return; + } + + this._scaleSpring.resetTo(scale); + this._updateForScale(); + this._needsDraw = true; + } else { + if (sameTarget) { + return; + } + + this._scaleSpring.springTo(scale); + this._updateForScale(); + this._needsDraw = true; + } + + if (!sameTarget) { + this._raiseBoundsChange(); + } + }, + + // private + _updateForScale: function() { + this._worldWidthTarget = this._scaleSpring.target.value; + this._worldHeightTarget = this.normHeight * this._scaleSpring.target.value; + this._worldWidthCurrent = this._scaleSpring.current.value; + this._worldHeightCurrent = this.normHeight * this._scaleSpring.current.value; + }, + + // private + _raiseBoundsChange: function() { + /** + * Raised when the TiledImage's bounds are changed. + * Note that this event is triggered only when the animation target is changed; + * not for every frame of animation. + * @event bounds-change + * @memberOf OpenSeadragon.TiledImage + * @type {object} + * @property {OpenSeadragon.World} eventSource - A reference to the TiledImage which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('bounds-change'); + }, + + // private + _isBottomItem: function() { + return this.viewer.world.getItemAt(0) === this; + }, + + // private + _getLevelsInterval: function() { + var lowestLevel = Math.max( + this.source.minLevel, + Math.floor(Math.log(this.minZoomImageRatio) / Math.log(2)) + ); + var currentZeroRatio = this.viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio(0), true).x * + this._scaleSpring.current.value; + var highestLevel = Math.min( + Math.abs(this.source.maxLevel), + Math.abs(Math.floor( + Math.log(currentZeroRatio / this.minPixelRatio) / Math.log(2) + )) + ); + + // Calculations for the interval of levels to draw + // can return invalid intervals; fix that here if necessary + lowestLevel = Math.min(lowestLevel, highestLevel); + return { + lowestLevel: lowestLevel, + highestLevel: highestLevel + }; + }, + + // private + _updateViewport: function() { + this._needsDraw = false; + + // Reset tile's internal drawn state + while (this.lastDrawn.length > 0) { + var tile = this.lastDrawn.pop(); + tile.beingDrawn = false; + } + + var viewport = this.viewport; + var drawArea = this._viewportToTiledImageRectangle( + viewport.getBoundsWithMargins(true)); + + if (!this.wrapHorizontal && !this.wrapVertical) { + var tiledImageBounds = this._viewportToTiledImageRectangle( + this.getClippedBounds(true)); + drawArea = drawArea.intersection(tiledImageBounds); + if (drawArea === null) { + return; + } + } + + var levelsInterval = this._getLevelsInterval(); + var lowestLevel = levelsInterval.lowestLevel; + var highestLevel = levelsInterval.highestLevel; + var bestTile = null; + var haveDrawn = false; + var currentTime = $.now(); + + // Update any level that will be drawn + for (var level = highestLevel; level >= lowestLevel; level--) { + var drawLevel = false; + + //Avoid calculations for draw if we have already drawn this + var currentRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio(level), + true + ).x * this._scaleSpring.current.value; + + if (level === lowestLevel || + (!haveDrawn && currentRenderPixelRatio >= this.minPixelRatio)) { + drawLevel = true; + haveDrawn = true; + } else if (!haveDrawn) { + continue; + } + + //Perform calculations for draw if we haven't drawn this + var targetRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio(level), + false + ).x * this._scaleSpring.current.value; //TODO: shouldn't that be target.value? + + var targetZeroRatio = viewport.deltaPixelsFromPointsNoRotate( + this.source.getPixelRatio( + Math.max( + this.source.getClosestLevel(viewport.containerSize) - 1, + 0 + ) + ), + false + ).x * this._scaleSpring.current.value; //TODO: shouldn't that be target.value? + + var optimalRatio = this.immediateRender ? 1 : targetZeroRatio; + var levelOpacity = Math.min(1, (currentRenderPixelRatio - 0.5) / 0.5); + var levelVisibility = optimalRatio / Math.abs( + optimalRatio - targetRenderPixelRatio + ); + + // Update the level and keep track of 'best' tile to load + bestTile = updateLevel( + this, + haveDrawn, + drawLevel, + level, + levelOpacity, + levelVisibility, + drawArea, + currentTime, + bestTile + ); + + // Stop the loop if lower-res tiles would all be covered by + // already drawn tiles + if (providesCoverage(this.coverage, level)) { + break; + } + } + + // Perform the actual drawing + drawTiles(this, this.lastDrawn); + +<<<<<<< HEAD + // Load the new 'best' tile + if (bestTile && !bestTile.context2D) { + loadTile(this, bestTile, currentTime); + this._setFullyLoaded(false); + } else { + this._setFullyLoaded(true); + } +======= + // Load the new 'best' tile + if (best && !best.context2D) { + loadTile( tiledImage, best, currentTime ); + tiledImage._needsDraw = true; + tiledImage._setFullyLoaded(false); + } else { + tiledImage._setFullyLoaded(true); +>>>>>>> e8324627e144f6e01ab1ce70cd88194fbab46487 + } +}); + +function updateLevel(tiledImage, haveDrawn, drawLevel, level, levelOpacity, + levelVisibility, drawArea, currentTime, best) { + + var topLeftBound = drawArea.getBoundingBox().getTopLeft(); + var bottomRightBound = drawArea.getBoundingBox().getBottomRight(); + + if (tiledImage.viewer) { + /** + * - Needs documentation - + * + * @event update-level + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {Object} havedrawn + * @property {Object} level + * @property {Object} opacity + * @property {Object} visibility + * @property {OpenSeadragon.Rect} drawArea + * @property {Object} topleft deprecated, use drawArea instead + * @property {Object} bottomright deprecated, use drawArea instead + * @property {Object} currenttime + * @property {Object} best + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + tiledImage.viewer.raiseEvent('update-level', { + tiledImage: tiledImage, + havedrawn: haveDrawn, + level: level, + opacity: levelOpacity, + visibility: levelVisibility, + drawArea: drawArea, + topleft: topLeftBound, + bottomright: bottomRightBound, + currenttime: currentTime, + best: best + }); + } + + //OK, a new drawing so do your calculations + var topLeftTile = tiledImage.source.getTileAtPoint(level, topLeftBound); + var bottomRightTile = tiledImage.source.getTileAtPoint(level, bottomRightBound); + var numberOfTiles = tiledImage.source.getNumTiles(level); + + resetCoverage(tiledImage.coverage, level); + + if (tiledImage.wrapHorizontal) { + topLeftTile.x -= 1; // left invisible column (othervise we will have empty space after scroll at left) + } else { + // Adjust for floating point error + topLeftTile.x = Math.max(topLeftTile.x, 0); + bottomRightTile.x = Math.min(bottomRightTile.x, numberOfTiles.x - 1); + } + if (tiledImage.wrapVertical) { + topLeftTile.y -= 1; // top invisible row (othervise we will have empty space after scroll at top) + } else { + // Adjust for floating point error + topLeftTile.y = Math.max(topLeftTile.y, 0); + bottomRightTile.y = Math.min(bottomRightTile.y, numberOfTiles.y - 1); + } + + var viewportCenter = tiledImage.viewport.pixelFromPoint( + tiledImage.viewport.getCenter()); + for (var x = topLeftTile.x; x <= bottomRightTile.x; x++) { + for (var y = topLeftTile.y; y <= bottomRightTile.y; y++) { + + var tileBounds = tiledImage.source.getTileBounds(level, x, y); + + if (drawArea.intersection(tileBounds) === null) { + // This tile is outside of the viewport, no need to draw it + continue; + } + + best = updateTile( + tiledImage, + drawLevel, + haveDrawn, + x, y, + level, + levelOpacity, + levelVisibility, + viewportCenter, + numberOfTiles, + currentTime, + best + ); + + } + } + + return best; +} + +function updateTile( tiledImage, drawLevel, haveDrawn, x, y, level, levelOpacity, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){ + + var tile = getTile( + x, y, + level, + tiledImage.source, + tiledImage.tilesMatrix, + currentTime, + numberOfTiles, + tiledImage._worldWidthCurrent, + tiledImage._worldHeightCurrent + ), + drawTile = drawLevel; + + if( tiledImage.viewer ){ + /** + * - Needs documentation - + * + * @event update-tile + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {OpenSeadragon.Tile} tile + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + tiledImage.viewer.raiseEvent( 'update-tile', { + tiledImage: tiledImage, + tile: tile + }); + } + + setCoverage( tiledImage.coverage, level, x, y, false ); + + if ( !tile.exists ) { + return best; + } + + if ( haveDrawn && !drawTile ) { + if ( isCovered( tiledImage.coverage, level, x, y ) ) { + setCoverage( tiledImage.coverage, level, x, y, true ); + } else { + drawTile = true; + } + } + + if ( !drawTile ) { + return best; + } + + positionTile( + tile, + tiledImage.source.tileOverlap, + tiledImage.viewport, + viewportCenter, + levelVisibility, + tiledImage + ); + + if (!tile.loaded) { + if (tile.context2D) { + setTileLoaded(tiledImage, tile); + } else { + var imageRecord = tiledImage._tileCache.getImageRecord(tile.url); + if (imageRecord) { + var image = imageRecord.getImage(); + setTileLoaded(tiledImage, tile, image); + } + } + } + + if ( tile.loaded ) { + var needsDraw = blendTile( + tiledImage, + tile, + x, y, + level, + levelOpacity, + currentTime + ); + + if ( needsDraw ) { + tiledImage._needsDraw = true; + } + } else if ( tile.loading ) { + // the tile is already in the download queue + // thanks josh1093 for finally translating this typo + } else { + best = compareTiles( best, tile ); + } + + return best; +} + +function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWidth, worldHeight ) { + var xMod, + yMod, + bounds, + exists, + url, + context2D, + tile; + + if ( !tilesMatrix[ level ] ) { + tilesMatrix[ level ] = {}; + } + if ( !tilesMatrix[ level ][ x ] ) { + tilesMatrix[ level ][ x ] = {}; + } + + if ( !tilesMatrix[ level ][ x ][ y ] ) { + xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; + yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; + 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); + + tilesMatrix[ level ][ x ][ y ] = new $.Tile( + level, + x, + y, + bounds, + exists, + url, + context2D + ); + } + + tile = tilesMatrix[ level ][ x ][ y ]; + tile.lastTouchTime = time; + + return tile; +} + +function loadTile( tiledImage, tile, time ) { + tile.loading = true; + tiledImage._imageLoader.addJob({ + src: tile.url, + crossOriginPolicy: tiledImage.crossOriginPolicy, + callback: function( image, errorMsg ){ + onTileLoad( tiledImage, tile, time, image, errorMsg ); + }, + abort: function() { + tile.loading = false; + } + }); +} + +function onTileLoad( tiledImage, tile, time, image, errorMsg ) { + if ( !image ) { + $.console.log( "Tile %s failed to load: %s - error: %s", tile, tile.url, errorMsg ); + /** + * Triggered when a tile fails to load. + * + * @event tile-load-failed + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Tile} tile - The tile that failed to load. + * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image the tile belongs to. + * @property {number} time - The time in milliseconds when the tile load began. + * @property {string} message - The error message. + */ + tiledImage.viewer.raiseEvent("tile-load-failed", {tile: tile, tiledImage: tiledImage, time: time, message: errorMsg}); + 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; + } + + var finish = function() { + var cutoff = Math.ceil( Math.log( + tiledImage.source.getTileWidth(tile.level) ) / Math.log( 2 ) ); + setTileLoaded(tiledImage, tile, image, cutoff); + }; + + // Check if we're mid-update; this can happen on IE8 because image load events for + // cached images happen immediately there + if ( !tiledImage._midDraw ) { + finish(); + } else { + // Wait until after the update, in case caching unloads any tiles + window.setTimeout( finish, 1); + } +} + +function setTileLoaded(tiledImage, tile, image, cutoff) { + var increment = 0; + + function getCompletionCallback() { + increment++; + return completionCallback; + } + + function completionCallback() { + increment--; + if (increment === 0) { + tile.loading = false; + tile.loaded = true; + if (!tile.context2D) { + tiledImage._tileCache.cacheTile({ + image: image, + tile: tile, + cutoff: cutoff, + tiledImage: tiledImage + }); + } + tiledImage._needsDraw = true; + } + } + + /** + * Triggered when a tile has just been loaded in memory. That means that the + * image has been downloaded and can be modified before being drawn to the canvas. + * + * @event tile-loaded + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {Image} image - The image of the tile. + * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile. + * @property {OpenSeadragon.Tile} tile - The tile which has been loaded. + * @property {function} getCompletionCallback - A function giving a callback to call + * when the asynchronous processing of the image is done. The image will be + * marked as entirely loaded when the callback has been called once for each + * call to getCompletionCallback. + */ + tiledImage.viewer.raiseEvent("tile-loaded", { + tile: tile, + tiledImage: tiledImage, + image: image, + getCompletionCallback: getCompletionCallback + }); + // In case the completion callback is never called, we at least force it once. + getCompletionCallback()(); +} + +function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, tiledImage ){ + var boundsTL = tile.bounds.getTopLeft(); + + boundsTL.x *= tiledImage._scaleSpring.current.value; + boundsTL.y *= tiledImage._scaleSpring.current.value; + boundsTL.x += tiledImage._xSpring.current.value; + boundsTL.y += tiledImage._ySpring.current.value; + + var boundsSize = tile.bounds.getSize(); + + boundsSize.x *= tiledImage._scaleSpring.current.value; + boundsSize.y *= tiledImage._scaleSpring.current.value; + + 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 ); + + if ( !overlap ) { + sizeC = sizeC.plus( new $.Point( 1, 1 ) ); + } + + tile.position = positionC; + tile.size = sizeC; + tile.distance = tileDistance; + tile.visibility = levelVisibility; +} + + +function blendTile( tiledImage, tile, x, y, level, levelOpacity, currentTime ){ + var blendTimeMillis = 1000 * tiledImage.blendTime, + deltaTime, + opacity; + + if ( !tile.blendStart ) { + tile.blendStart = currentTime; + } + + deltaTime = currentTime - tile.blendStart; + opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1; + + if ( tiledImage.alwaysBlend ) { + opacity *= levelOpacity; + } + + tile.opacity = opacity; + + tiledImage.lastDrawn.push( tile ); + + if ( opacity == 1 ) { + setCoverage( tiledImage.coverage, level, x, y, true ); + tiledImage._hasOpaqueTile = true; + } else if ( deltaTime < blendTimeMillis ) { + return true; + } + + return false; +} + +/** + * @private + * @inner + * Returns true if the given tile provides coverage to lower-level tiles of + * lower resolution representing the same content. If neither x nor y is + * given, returns true if the entire visible level provides coverage. + * + * Note that out-of-bounds tiles provide coverage in this sense, since + * there's no content that they would need to cover. Tiles at non-existent + * levels that are within the image bounds, however, do not. + */ +function providesCoverage( coverage, level, x, y ) { + var rows, + cols, + i, j; + + if ( !coverage[ level ] ) { + return false; + } + + if ( x === undefined || y === undefined ) { + rows = coverage[ level ]; + for ( i in rows ) { + if ( rows.hasOwnProperty( i ) ) { + cols = rows[ i ]; + for ( j in cols ) { + if ( cols.hasOwnProperty( j ) && !cols[ j ] ) { + return false; + } + } + } + } + + return true; + } + + return ( + coverage[ level ][ x] === undefined || + coverage[ level ][ x ][ y ] === undefined || + coverage[ level ][ x ][ y ] === true + ); +} + +/** + * @private + * @inner + * Returns true if the given tile is completely covered by higher-level + * tiles of higher resolution representing the same content. If neither x + * nor y is given, returns true if the entire visible level is covered. + */ +function isCovered( coverage, level, x, y ) { + if ( x === undefined || y === undefined ) { + return providesCoverage( coverage, level + 1 ); + } else { + return ( + providesCoverage( coverage, level + 1, 2 * x, 2 * y ) && + providesCoverage( coverage, level + 1, 2 * x, 2 * y + 1 ) && + providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y ) && + providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y + 1 ) + ); + } +} + +/** + * @private + * @inner + * Sets whether the given tile provides coverage or not. + */ +function setCoverage( coverage, level, x, y, covers ) { + if ( !coverage[ level ] ) { + $.console.warn( + "Setting coverage for a tile before its level's coverage has been reset: %s", + level + ); + return; + } + + if ( !coverage[ level ][ x ] ) { + coverage[ level ][ x ] = {}; + } + + coverage[ level ][ x ][ y ] = covers; +} + +/** + * @private + * @inner + * Resets coverage information for the given level. This should be called + * after every draw routine. Note that at the beginning of the next draw + * routine, coverage for every visible tile should be explicitly set. + */ +function resetCoverage( coverage, level ) { + coverage[ level ] = {}; +} + +/** + * @private + * @inner + * Determines whether the 'last best' tile for the area is better than the + * tile in question. + */ +function compareTiles( previousBest, tile ) { + if ( !previousBest ) { + return tile; + } + + if ( tile.visibility > previousBest.visibility ) { + return tile; + } else if ( tile.visibility == previousBest.visibility ) { + if ( tile.distance < previousBest.distance ) { + return tile; + } + } + + return previousBest; +} + +function drawTiles( tiledImage, lastDrawn ) { + if (lastDrawn.length === 0) { + return; + } + var tile = lastDrawn[0]; + + var useSketch = tiledImage.opacity < 1 || + (tiledImage.compositeOperation && + tiledImage.compositeOperation !== 'source-over') || + (!tiledImage._isBottomItem() && tile._hasTransparencyChannel()); + + var sketchScale; + var sketchTranslate; + + var zoom = tiledImage.viewport.getZoom(true); + var imageZoom = tiledImage.viewportToImageZoom(zoom); + if (imageZoom > tiledImage.smoothTileEdgesMinZoom && !tiledImage.iOSDevice) { + // When zoomed in a lot (>100%) the tile edges are visible. + // So we have to composite them at ~100% and scale them up together. + // Note: Disabled on iOS devices per default as it causes a native crash + useSketch = true; + sketchScale = tile.getScaleForEdgeSmoothing(); + sketchTranslate = tile.getTranslationForEdgeSmoothing(sketchScale, + tiledImage._drawer.getCanvasSize(false), + tiledImage._drawer.getCanvasSize(true)); + } + + var bounds; + if (useSketch) { + if (!sketchScale) { + // Except when edge smoothing, we only clean the part of the + // sketch canvas we are going to use for performance reasons. + bounds = tiledImage.viewport.viewportToViewerElementRectangle( + tiledImage.getClippedBounds(true)) + .getIntegerBoundingBox() + .times($.pixelDensityRatio); + } + tiledImage._drawer._clear(true, bounds); + } + + // When scaling, we must rotate only when blending the sketch canvas to + // avoid interpolation + if (!sketchScale) { + if (tiledImage.viewport.degrees !== 0) { + tiledImage._drawer._offsetForRotation( + tiledImage.viewport.degrees, useSketch); + } + if (tiledImage._degrees !== 0) { + tiledImage._drawer._offsetForRotation( + tiledImage._degrees, + tiledImage.viewport.pixelFromPointNoRotate( + tiledImage._getRotationPoint(true), true), + useSketch); + } + } + + var usedClip = false; + if ( tiledImage._clip ) { + tiledImage._drawer.saveContext(useSketch); + + var box = tiledImage.imageToViewportRectangle(tiledImage._clip, true); + box = box.rotate(-tiledImage._degrees, tiledImage._getRotationPoint()); + 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; + } + + 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" ) { + fillStyle = tiledImage.placeholderFillStyle(tiledImage, tiledImage._drawer.context); + } + else { + fillStyle = tiledImage.placeholderFillStyle; + } + + tiledImage._drawer.drawRectangle(placeholderRect, fillStyle, useSketch); + } + + for (var i = lastDrawn.length - 1; i >= 0; i--) { + tile = lastDrawn[ i ]; + tiledImage._drawer.drawTile( tile, tiledImage._drawingHandler, useSketch, sketchScale, sketchTranslate ); + tile.beingDrawn = true; + + if( tiledImage.viewer ){ + /** + * - Needs documentation - + * + * @event tile-drawn + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. + * @property {OpenSeadragon.Tile} tile + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + tiledImage.viewer.raiseEvent( 'tile-drawn', { + tiledImage: tiledImage, + tile: tile + }); + } + } + + if ( usedClip ) { + tiledImage._drawer.restoreContext( useSketch ); + } + + if (!sketchScale) { + if (tiledImage._degrees !== 0) { + tiledImage._drawer._restoreRotationChanges(useSketch); + } + if (tiledImage.viewport.degrees !== 0) { + tiledImage._drawer._restoreRotationChanges(useSketch); + } + } + + if (useSketch) { + if (sketchScale) { + if (tiledImage.viewport.degrees !== 0) { + tiledImage._drawer._offsetForRotation( + tiledImage.viewport.degrees, false); + } + if (tiledImage._degrees !== 0) { + tiledImage._drawer._offsetForRotation( + tiledImage._degrees, + tiledImage.viewport.pixelFromPointNoRotate( + tiledImage._getRotationPoint(true), true), + useSketch); + } + } + tiledImage._drawer.blendSketch({ + opacity: tiledImage.opacity, + scale: sketchScale, + translate: sketchTranslate, + compositeOperation: tiledImage.compositeOperation, + bounds: bounds + }); + if (sketchScale) { + if (tiledImage._degrees !== 0) { + tiledImage._drawer._restoreRotationChanges(false); + } + if (tiledImage.viewport.degrees !== 0) { + tiledImage._drawer._restoreRotationChanges(false); + } + } + } + drawDebugInfo( tiledImage, lastDrawn ); +} + +function drawDebugInfo( tiledImage, lastDrawn ) { + if( tiledImage.debugMode ) { + for ( var i = lastDrawn.length - 1; i >= 0; i-- ) { + var tile = lastDrawn[ i ]; + try { + tiledImage._drawer.drawDebugInfo( + tile, lastDrawn.length, i, tiledImage); + } catch(e) { + $.console.error(e); + } + } + } +} + +}( OpenSeadragon )); diff --git a/src/viewer.js b/src/viewer.js index 842f7f2d..06b224e0 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -1229,7 +1229,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * browsers that support the HTML5 canvas. * @param {Number} [options.opacity] Opacity the tiled image should be drawn at by default. * @param {Number} [options.degrees=0] Initial rotation of the tiled image around - * it top left corner in degrees. + * its top left corner in degrees. * @param {String} [options.compositeOperation] How the image is composited onto other images. * @param {String} [options.crossOriginPolicy] The crossOriginPolicy for this specific image, * overriding viewer.crossOriginPolicy. From 62c96ebad7390317c8e329c2a903f99062cfcbef Mon Sep 17 00:00:00 2001 From: Antoine Vandecreme Date: Sun, 28 Aug 2016 14:39:14 +0200 Subject: [PATCH 06/15] Add clip-change event. --- src/navigator.js | 7 +- src/tiledimage.js | 17 +- src/tiledimage.js.orig | 1707 ------------------------------------ src/world.js | 3 + test/modules/tiledimage.js | 21 + 5 files changed, 43 insertions(+), 1712 deletions(-) delete mode 100644 src/tiledimage.js.orig diff --git a/src/navigator.js b/src/navigator.js index 7b74d6ea..fe977d0c 100644 --- a/src/navigator.js +++ b/src/navigator.js @@ -341,9 +341,12 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /* myItem._originalForNavigator = original; _this._matchBounds(myItem, original, true); - original.addHandler('bounds-change', function() { + function matchBounds() { _this._matchBounds(myItem, original); - }); + } + + original.addHandler('bounds-change', matchBounds); + original.addHandler('clip-change', matchBounds); } }); diff --git a/src/tiledimage.js b/src/tiledimage.js index bd54d232..15ec6668 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -739,13 +739,12 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @param {OpenSeadragon.Rect|null} newClip - An area, in image pixels, to clip to * (portions of the image outside of this area will not be visible). Only works on * browsers that support the HTML5 canvas. + * @fires OpenSeadragon.TiledImage.event:clip-change */ setClip: function(newClip) { $.console.assert(!newClip || newClip instanceof $.Rect, "[TiledImage.setClip] newClip must be an OpenSeadragon.Rect or null"); -//TODO: should this._raiseBoundsChange(); be called? - if (newClip instanceof $.Rect) { this._clip = newClip.clone(); } else { @@ -753,6 +752,16 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag } this._needsDraw = true; + /** + * Raised when the TiledImage's clip is changed. + * @event clip-change + * @memberOf OpenSeadragon.TiledImage + * @type {object} + * @property {OpenSeadragon.TiledImage} eventSource - A reference to the + * TiledImage which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('clip-change'); }, /** @@ -781,6 +790,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag /** * Set the current rotation of this tiled image in degrees. * @param {Number} degrees the rotation in degrees. + * @fires OpenSeadragon.TiledImage.event:bounds-change */ setRotation: function(degrees) { degrees = $.positiveModulo(degrees, 360); @@ -860,7 +870,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @event bounds-change * @memberOf OpenSeadragon.TiledImage * @type {object} - * @property {OpenSeadragon.World} eventSource - A reference to the TiledImage which raised the event. + * @property {OpenSeadragon.TiledImage} eventSource - A reference to the + * TiledImage which raised the event. * @property {?Object} userData - Arbitrary subscriber-defined object. */ this.raiseEvent('bounds-change'); diff --git a/src/tiledimage.js.orig b/src/tiledimage.js.orig deleted file mode 100644 index 2eaa471e..00000000 --- a/src/tiledimage.js.orig +++ /dev/null @@ -1,1707 +0,0 @@ -/* - * OpenSeadragon - TiledImage - * - * Copyright (C) 2009 CodePlex Foundation - * Copyright (C) 2010-2013 OpenSeadragon contributors - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * - Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * - Neither the name of CodePlex Foundation nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -(function( $ ){ - -/** - * You shouldn't have to create a TiledImage directly; use {@link OpenSeadragon.Viewer#open} - * or {@link OpenSeadragon.Viewer#addTiledImage} instead. - * @class TiledImage - * @memberof OpenSeadragon - * @extends OpenSeadragon.EventSource - * @classdesc Handles rendering of tiles for an {@link OpenSeadragon.Viewer}. - * A new instance is created for each TileSource opened. - * @param {Object} options - Configuration for this TiledImage. - * @param {OpenSeadragon.TileSource} options.source - The TileSource that defines this TiledImage. - * @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this TiledImage. - * @param {OpenSeadragon.TileCache} options.tileCache - The TileCache for this TiledImage to use. - * @param {OpenSeadragon.Drawer} options.drawer - The Drawer for this TiledImage to draw onto. - * @param {OpenSeadragon.ImageLoader} options.imageLoader - The ImageLoader for this TiledImage to use. - * @param {Number} [options.x=0] - Left position, in viewport coordinates. - * @param {Number} [options.y=0] - Top position, in viewport coordinates. - * @param {Number} [options.width=1] - Width, in viewport coordinates. - * @param {Number} [options.height] - Height, in viewport coordinates. - * @param {OpenSeadragon.Rect} [options.fitBounds] The bounds in viewport coordinates - * to fit the image into. If specified, x, y, width and height get ignored. - * @param {OpenSeadragon.Placement} [options.fitBoundsPlacement=OpenSeadragon.Placement.CENTER] - * How to anchor the image in the bounds if options.fitBounds is set. - * @param {OpenSeadragon.Rect} [options.clip] - An area, in image pixels, to clip to - * (portions of the image outside of this area will not be visible). Only works on - * browsers that support the HTML5 canvas. - * @param {Number} [options.springStiffness] - See {@link OpenSeadragon.Options}. - * @param {Boolean} [options.animationTime] - See {@link OpenSeadragon.Options}. - * @param {Number} [options.minZoomImageRatio] - See {@link OpenSeadragon.Options}. - * @param {Boolean} [options.wrapHorizontal] - See {@link OpenSeadragon.Options}. - * @param {Boolean} [options.wrapVertical] - See {@link OpenSeadragon.Options}. - * @param {Boolean} [options.immediateRender] - See {@link OpenSeadragon.Options}. - * @param {Number} [options.blendTime] - See {@link OpenSeadragon.Options}. - * @param {Boolean} [options.alwaysBlend] - See {@link OpenSeadragon.Options}. - * @param {Number} [options.minPixelRatio] - See {@link OpenSeadragon.Options}. - * @param {Number} [options.smoothTileEdgesMinZoom] - See {@link OpenSeadragon.Options}. - * @param {Boolean} [options.iOSDevice] - See {@link OpenSeadragon.Options}. - * @param {Number} [options.opacity=1] - Opacity the tiled image should be drawn at. - * @param {String} [options.compositeOperation] - How the image is composited onto other images; see compositeOperation in {@link OpenSeadragon.Options} for possible values. - * @param {Boolean} [options.debugMode] - See {@link OpenSeadragon.Options}. - * @param {String|CanvasGradient|CanvasPattern|Function} [options.placeholderFillStyle] - See {@link OpenSeadragon.Options}. - * @param {String|Boolean} [options.crossOriginPolicy] - See {@link OpenSeadragon.Options}. - */ -$.TiledImage = function( options ) { - var _this = this; - - $.console.assert( options.tileCache, "[TiledImage] options.tileCache is required" ); - $.console.assert( options.drawer, "[TiledImage] options.drawer is required" ); - $.console.assert( options.viewer, "[TiledImage] options.viewer is required" ); - $.console.assert( options.imageLoader, "[TiledImage] options.imageLoader is required" ); - $.console.assert( options.source, "[TiledImage] options.source is required" ); - $.console.assert(!options.clip || options.clip instanceof $.Rect, - "[TiledImage] options.clip must be an OpenSeadragon.Rect if present"); - - $.EventSource.call( this ); - - this._tileCache = options.tileCache; - delete options.tileCache; - - this._drawer = options.drawer; - delete options.drawer; - - this._imageLoader = options.imageLoader; - delete options.imageLoader; - - if (options.clip instanceof $.Rect) { - this._clip = options.clip.clone(); - } - - delete options.clip; - - var x = options.x || 0; - delete options.x; - var y = options.y || 0; - delete options.y; - - // Ratio of zoomable image height to width. - this.normHeight = options.source.dimensions.y / options.source.dimensions.x; - this.contentAspectX = options.source.dimensions.x / options.source.dimensions.y; - - var scale = 1; - if ( options.width ) { - scale = options.width; - delete options.width; - - if ( options.height ) { - $.console.error( "specifying both width and height to a tiledImage is not supported" ); - delete options.height; - } - } else if ( options.height ) { - scale = options.height / this.normHeight; - delete options.height; - } - - var fitBounds = options.fitBounds; - delete options.fitBounds; - var fitBoundsPlacement = options.fitBoundsPlacement || OpenSeadragon.Placement.CENTER; - delete options.fitBoundsPlacement; - - this._degrees = $.positiveModulo(options.degrees || 0, 360); - delete options.degrees; - - $.extend( true, this, { - - //internal state properties - viewer: null, - tilesMatrix: {}, // A '3d' dictionary [level][x][y] --> Tile. - coverage: {}, // A '3d' dictionary [level][x][y] --> Boolean. - lastDrawn: [], // An unordered list of Tiles drawn last frame. - lastResetTime: 0, // Last time for which the tiledImage was reset. - _midDraw: false, // Is the tiledImage currently updating the viewport? - _needsDraw: true, // Does the tiledImage need to update the viewport again? - _hasOpaqueTile: false, // Do we have even one fully opaque tile? - //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, - smoothTileEdgesMinZoom: $.DEFAULT_SETTINGS.smoothTileEdgesMinZoom, - iOSDevice: $.DEFAULT_SETTINGS.iOSDevice, - debugMode: $.DEFAULT_SETTINGS.debugMode, - crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy, - placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, - opacity: $.DEFAULT_SETTINGS.opacity, - compositeOperation: $.DEFAULT_SETTINGS.compositeOperation - }, options ); - - this._fullyLoaded = false; - - this._xSpring = new $.Spring({ - initial: x, - springStiffness: this.springStiffness, - animationTime: this.animationTime - }); - - this._ySpring = new $.Spring({ - initial: y, - springStiffness: this.springStiffness, - animationTime: this.animationTime - }); - - this._scaleSpring = new $.Spring({ - initial: scale, - springStiffness: this.springStiffness, - animationTime: this.animationTime - }); - - this._updateForScale(); - - if (fitBounds) { - this.fitBounds(fitBounds, fitBoundsPlacement, true); - } - - // We need a callback to give image manipulation a chance to happen - this._drawingHandler = function(args) { - /** - * This event is fired just before the tile is drawn giving the application a chance to alter the image. - * - * NOTE: This event is only fired when the drawer is using a <canvas>. - * - * @event tile-drawing - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.Tile} tile - The Tile being drawn. - * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. - * @property {OpenSeadragon.Tile} context - The HTML canvas context being drawn into. - * @property {OpenSeadragon.Tile} rendered - The HTML canvas context containing the tile imagery. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - _this.viewer.raiseEvent('tile-drawing', $.extend({ - tiledImage: _this - }, args)); - }; -}; - -$.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.TiledImage.prototype */{ - /** - * @returns {Boolean} Whether the TiledImage needs to be drawn. - */ - needsDraw: function() { - return this._needsDraw; - }, - - /** - * @returns {Boolean} Whether all tiles necessary for this TiledImage to draw at the current view have been loaded. - */ - getFullyLoaded: function() { - return this._fullyLoaded; - }, - - // private - _setFullyLoaded: function(flag) { - if (flag === this._fullyLoaded) { - return; - } - - this._fullyLoaded = flag; - - /** - * Fired when the TiledImage's "fully loaded" flag (whether all tiles necessary for this TiledImage - * to draw at the current view have been loaded) changes. - * - * @event fully-loaded-change - * @memberof OpenSeadragon.TiledImage - * @type {object} - * @property {Boolean} fullyLoaded - The new "fully loaded" value. - * @property {OpenSeadragon.TiledImage} eventSource - A reference to the TiledImage which raised the event. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - this.raiseEvent('fully-loaded-change', { - fullyLoaded: this._fullyLoaded - }); - }, - - /** - * Clears all tiles and triggers an update on the next call to - * {@link OpenSeadragon.TiledImage#update}. - */ - reset: function() { - this._tileCache.clearTilesFor(this); - this.lastResetTime = $.now(); - this._needsDraw = true; - }, - - /** - * Updates the TiledImage's bounds, animating if needed. - * @returns {Boolean} Whether the TiledImage animated. - */ - update: function() { - var oldX = this._xSpring.current.value; - var oldY = this._ySpring.current.value; - var oldScale = this._scaleSpring.current.value; - - this._xSpring.update(); - this._ySpring.update(); - this._scaleSpring.update(); - - if (this._xSpring.current.value !== oldX || this._ySpring.current.value !== oldY || - this._scaleSpring.current.value !== oldScale) { - this._updateForScale(); - this._needsDraw = true; - return true; - } - - return false; - }, - - /** - * Draws the TiledImage to its Drawer. - */ - draw: function() { - if (this.opacity !== 0) { - this._midDraw = true; - this._updateViewport(); - this._midDraw = false; - } - }, - - /** - * Destroy the TiledImage (unload current loaded tiles). - */ - destroy: function() { - this.reset(); - }, - - /** - * Get this TiledImage's bounds in viewport coordinates. - * @param {Boolean} [current=false] - Pass true for the current location; - * false for target location. - * @returns {OpenSeadragon.Rect} This TiledImage's bounds in viewport coordinates. - */ - getBounds: function(current) { - return this.getBoundsNoRotate(current) - .rotate(this._degrees, this._getRotationPoint(current)); - }, - - /** - * Get this TiledImage's bounds in viewport coordinates without taking - * rotation into account. - * @param {Boolean} [current=false] - Pass true for the current location; - * false for target location. - * @returns {OpenSeadragon.Rect} This TiledImage's bounds in viewport coordinates. - */ - getBoundsNoRotate: function(current) { - return current ? - new $.Rect( - this._xSpring.current.value, - this._ySpring.current.value, - this._worldWidthCurrent, - this._worldHeightCurrent) : - new $.Rect( - this._xSpring.target.value, - this._ySpring.target.value, - this._worldWidthTarget, - this._worldHeightTarget); - }, - - // deprecated - getWorldBounds: function() { - $.console.error('[TiledImage.getWorldBounds] is deprecated; use TiledImage.getBounds instead'); - return this.getBounds(); - }, - - /** - * Get the bounds of the displayed part of the tiled image. - * @param {Boolean} [current=false] Pass true for the current location, - * false for the target location. - * @returns {$.Rect} The clipped bounds in viewport coordinates. - */ - getClippedBounds: function(current) { - var bounds = this.getBoundsNoRotate(current); - if (this._clip) { - var worldWidth = current ? - this._worldWidthCurrent : this._worldWidthTarget; - var ratio = worldWidth / this.source.dimensions.x; - var clip = this._clip.times(ratio); - bounds = new $.Rect( - bounds.x + clip.x, - bounds.y + clip.y, - clip.width, - clip.height); - } - return bounds.rotate(this._degrees, this._getRotationPoint(current)); - }, - - /** - * @returns {OpenSeadragon.Point} This TiledImage's content size, in original pixels. - */ - getContentSize: function() { - return new $.Point(this.source.dimensions.x, this.source.dimensions.y); - }, - - // private - _viewportToImageDelta: function( viewerX, viewerY, current ) { - var scale = (current ? this._scaleSpring.current.value : this._scaleSpring.target.value); - return new $.Point(viewerX * (this.source.dimensions.x / scale), - viewerY * ((this.source.dimensions.y * this.contentAspectX) / scale)); - }, - - /** - * Translates from OpenSeadragon viewer coordinate system to image coordinate system. - * This method can be called either by passing X,Y coordinates or an {@link OpenSeadragon.Point}. - * @param {Number|OpenSeadragon.Point} viewerX - The X coordinate or point in viewport coordinate system. - * @param {Number} [viewerY] - The Y coordinate in viewport coordinate system. - * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. - * @return {OpenSeadragon.Point} A point representing the coordinates in the image. - */ - viewportToImageCoordinates: function(viewerX, viewerY, current) { - var point; - if (viewerX instanceof $.Point) { - //they passed a point instead of individual components - current = viewerY; - point = viewerX; - } else { - point = new $.Point(viewerX, viewerY); - } - - point = point.rotate(-this._degrees, this._getRotationPoint(current)); - return current ? - this._viewportToImageDelta( - point.x - this._xSpring.current.value, - point.y - this._ySpring.current.value) : - this._viewportToImageDelta( - point.x - this._xSpring.target.value, - point.y - this._ySpring.target.value); - }, - - // private - _imageToViewportDelta: function( imageX, imageY, current ) { - var scale = (current ? this._scaleSpring.current.value : this._scaleSpring.target.value); - return new $.Point((imageX / this.source.dimensions.x) * scale, - (imageY / this.source.dimensions.y / this.contentAspectX) * scale); - }, - - /** - * Translates from image coordinate system to OpenSeadragon viewer coordinate system - * This method can be called either by passing X,Y coordinates or an {@link OpenSeadragon.Point}. - * @param {Number|OpenSeadragon.Point} imageX - The X coordinate or point in image coordinate system. - * @param {Number} [imageY] - The Y coordinate in image coordinate system. - * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. - * @return {OpenSeadragon.Point} A point representing the coordinates in the viewport. - */ - imageToViewportCoordinates: function(imageX, imageY, current) { - if (imageX instanceof $.Point) { - //they passed a point instead of individual components - current = imageY; - imageY = imageX.y; - imageX = imageX.x; - } - - var point = this._imageToViewportDelta(imageX, imageY); - if (current) { - point.x += this._xSpring.current.value; - point.y += this._ySpring.current.value; - } else { - point.x += this._xSpring.target.value; - point.y += this._ySpring.target.value; - } - - return point.rotate(this._degrees, this._getRotationPoint(current)); - }, - - /** - * Translates from a rectangle which describes a portion of the image in - * pixel coordinates to OpenSeadragon viewport rectangle coordinates. - * This method can be called either by passing X,Y,width,height or an {@link OpenSeadragon.Rect}. - * @param {Number|OpenSeadragon.Rect} imageX - The left coordinate or rectangle in image coordinate system. - * @param {Number} [imageY] - The top coordinate in image coordinate system. - * @param {Number} [pixelWidth] - The width in pixel of the rectangle. - * @param {Number} [pixelHeight] - The height in pixel of the rectangle. - * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. - * @return {OpenSeadragon.Rect} A rect representing the coordinates in the viewport. - */ - imageToViewportRectangle: function( imageX, imageY, pixelWidth, pixelHeight, current ) { - var rect = imageX; - if (rect instanceof $.Rect) { - //they passed a rect instead of individual components - current = imageY; - } else { - rect = new $.Rect(imageX, imageY, pixelWidth, pixelHeight); - } - - 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, - rect.degrees + this._degrees - ); - }, - - /** - * Translates from a rectangle which describes a portion of - * the viewport in point coordinates to image rectangle coordinates. - * This method can be called either by passing X,Y,width,height or an {@link OpenSeadragon.Rect}. - * @param {Number|OpenSeadragon.Rect} viewerX - The left coordinate or rectangle in viewport coordinate system. - * @param {Number} [viewerY] - The top coordinate in viewport coordinate system. - * @param {Number} [pointWidth] - The width in viewport coordinate system. - * @param {Number} [pointHeight] - The height in viewport coordinate system. - * @param {Boolean} [current=false] - Pass true to use the current location; false for target location. - * @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; - } else { - rect = new $.Rect(viewerX, viewerY, pointWidth, pointHeight); - } - - 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, - rect.degrees - this._degrees - ); - }, - - /** - * Convert pixel coordinates relative to the viewer element to image - * coordinates. - * @param {OpenSeadragon.Point} pixel - * @returns {OpenSeadragon.Point} - */ - viewerElementToImageCoordinates: function( pixel ) { - var point = this.viewport.pointFromPixel( pixel, true ); - return this.viewportToImageCoordinates( point ); - }, - - /** - * Convert pixel coordinates relative to the image to - * viewer element coordinates. - * @param {OpenSeadragon.Point} pixel - * @returns {OpenSeadragon.Point} - */ - imageToViewerElementCoordinates: function( pixel ) { - var point = this.imageToViewportCoordinates( pixel ); - return this.viewport.pixelFromPoint( point, true ); - }, - - /** - * Convert pixel coordinates relative to the window to image coordinates. - * @param {OpenSeadragon.Point} pixel - * @returns {OpenSeadragon.Point} - */ - windowToImageCoordinates: function( pixel ) { - var viewerCoordinates = pixel.minus( - OpenSeadragon.getElementPosition( this.viewer.element )); - return this.viewerElementToImageCoordinates( viewerCoordinates ); - }, - - /** - * Convert image coordinates to pixel coordinates relative to the window. - * @param {OpenSeadragon.Point} pixel - * @returns {OpenSeadragon.Point} - */ - imageToWindowCoordinates: function( pixel ) { - var viewerCoordinates = this.imageToViewerElementCoordinates( pixel ); - return viewerCoordinates.plus( - OpenSeadragon.getElementPosition( this.viewer.element )); - }, - - // private - // Convert rectangle in tiled image coordinates to viewport coordinates. - _tiledImageToViewportRectangle: function(rect) { - var scale = this._scaleSpring.current.value; - return new $.Rect( - rect.x * scale + this._xSpring.current.value, - rect.y * scale + this._ySpring.current.value, - rect.width * scale, - rect.height * scale, - rect.degrees) - .rotate(this.getRotation(), this._getRotationPoint(true)); - }, - - // private - // Convert rectangle in viewport coordinates to tiled image coordinates. - _viewportToTiledImageRectangle: function(rect) { - var scale = this._scaleSpring.current.value; - rect = rect.rotate(-this.getRotation(), this._getRotationPoint(true)); - return new $.Rect( - (rect.x - this._xSpring.current.value) / scale, - (rect.y - this._ySpring.current.value) / scale, - rect.width / scale, - rect.height / scale, - rect.degrees); - }, - - /** - * Convert a viewport zoom to an image zoom. - * Image zoom: ratio of the original image size to displayed image size. - * 1 means original image size, 0.5 half size... - * Viewport zoom: ratio of the displayed image's width to viewport's width. - * 1 means identical width, 2 means image's width is twice the viewport's width... - * @function - * @param {Number} viewportZoom The viewport zoom - * @returns {Number} imageZoom The image zoom - */ - viewportToImageZoom: function( viewportZoom ) { - var ratio = this._scaleSpring.current.value * - this.viewport._containerInnerSize.x / this.source.dimensions.x; - return ratio * viewportZoom ; - }, - - /** - * Convert an image zoom to a viewport zoom. - * Image zoom: ratio of the original image size to displayed image size. - * 1 means original image size, 0.5 half size... - * Viewport zoom: ratio of the displayed image's width to viewport's width. - * 1 means identical width, 2 means image's width is twice the viewport's width... - * Note: not accurate with multi-image. - * @function - * @param {Number} imageZoom The image zoom - * @returns {Number} viewportZoom The viewport zoom - */ - imageToViewportZoom: function( imageZoom ) { - var ratio = this._scaleSpring.current.value * - this.viewport._containerInnerSize.x / this.source.dimensions.x; - return imageZoom / ratio; - }, - - /** - * Sets the TiledImage's position in the world. - * @param {OpenSeadragon.Point} position - The new position, in viewport coordinates. - * @param {Boolean} [immediately=false] - Whether to animate to the new position or snap immediately. - * @fires OpenSeadragon.TiledImage.event:bounds-change - */ - setPosition: function(position, immediately) { - var sameTarget = (this._xSpring.target.value === position.x && - this._ySpring.target.value === position.y); - - if (immediately) { - if (sameTarget && this._xSpring.current.value === position.x && - this._ySpring.current.value === position.y) { - return; - } - - this._xSpring.resetTo(position.x); - this._ySpring.resetTo(position.y); - this._needsDraw = true; - } else { - if (sameTarget) { - return; - } - - this._xSpring.springTo(position.x); - this._ySpring.springTo(position.y); - this._needsDraw = true; - } - - if (!sameTarget) { - this._raiseBoundsChange(); - } - }, - - /** - * Sets the TiledImage's width in the world, adjusting the height to match based on aspect ratio. - * @param {Number} width - The new width, in viewport coordinates. - * @param {Boolean} [immediately=false] - Whether to animate to the new size or snap immediately. - * @fires OpenSeadragon.TiledImage.event:bounds-change - */ - setWidth: function(width, immediately) { - this._setScale(width, immediately); - }, - - /** - * Sets the TiledImage's height in the world, adjusting the width to match based on aspect ratio. - * @param {Number} height - The new height, in viewport coordinates. - * @param {Boolean} [immediately=false] - Whether to animate to the new size or snap immediately. - * @fires OpenSeadragon.TiledImage.event:bounds-change - */ - setHeight: function(height, immediately) { - this._setScale(height / this.normHeight, immediately); - }, - - /** - * Positions and scales the TiledImage to fit in the specified bounds. - * Note: this method fires OpenSeadragon.TiledImage.event:bounds-change - * twice - * @param {OpenSeadragon.Rect} bounds The bounds to fit the image into. - * @param {OpenSeadragon.Placement} [anchor=OpenSeadragon.Placement.CENTER] - * How to anchor the image in the bounds. - * @param {Boolean} [immediately=false] Whether to animate to the new size - * or snap immediately. - * @fires OpenSeadragon.TiledImage.event:bounds-change - */ - fitBounds: function(bounds, anchor, immediately) { - anchor = anchor || $.Placement.CENTER; - var anchorProperties = $.Placement.properties[anchor]; - var aspectRatio = this.contentAspectX; - var xOffset = 0; - var yOffset = 0; - var displayedWidthRatio = 1; - var displayedHeightRatio = 1; - if (this._clip) { - aspectRatio = this._clip.getAspectRatio(); - displayedWidthRatio = this._clip.width / this.source.dimensions.x; - displayedHeightRatio = this._clip.height / this.source.dimensions.y; - if (bounds.getAspectRatio() > aspectRatio) { - xOffset = this._clip.x / this._clip.height * bounds.height; - yOffset = this._clip.y / this._clip.height * bounds.height; - } else { - xOffset = this._clip.x / this._clip.width * bounds.width; - yOffset = this._clip.y / this._clip.width * bounds.width; - } - } - - if (bounds.getAspectRatio() > aspectRatio) { - // We will have margins on the X axis - var height = bounds.height / displayedHeightRatio; - var marginLeft = 0; - if (anchorProperties.isHorizontallyCentered) { - marginLeft = (bounds.width - bounds.height * aspectRatio) / 2; - } else if (anchorProperties.isRight) { - marginLeft = bounds.width - bounds.height * aspectRatio; - } - this.setPosition( - new $.Point(bounds.x - xOffset + marginLeft, bounds.y - yOffset), - immediately); - this.setHeight(height, immediately); - } else { - // We will have margins on the Y axis - var width = bounds.width / displayedWidthRatio; - var marginTop = 0; - if (anchorProperties.isVerticallyCentered) { - marginTop = (bounds.height - bounds.width / aspectRatio) / 2; - } else if (anchorProperties.isBottom) { - marginTop = bounds.height - bounds.width / aspectRatio; - } - this.setPosition( - new $.Point(bounds.x - xOffset, bounds.y - yOffset + marginTop), - immediately); - this.setWidth(width, immediately); - } - }, - - /** - * @returns {OpenSeadragon.Rect|null} The TiledImage's current clip rectangle, - * in image pixels, or null if none. - */ - getClip: function() { - if (this._clip) { - return this._clip.clone(); - } - - return null; - }, - - /** - * @param {OpenSeadragon.Rect|null} newClip - An area, in image pixels, to clip to - * (portions of the image outside of this area will not be visible). Only works on - * browsers that support the HTML5 canvas. - */ - setClip: function(newClip) { - $.console.assert(!newClip || newClip instanceof $.Rect, - "[TiledImage.setClip] newClip must be an OpenSeadragon.Rect or null"); - -//TODO: should this._raiseBoundsChange(); be called? - - if (newClip instanceof $.Rect) { - this._clip = newClip.clone(); - } else { - this._clip = null; - } - - this._needsDraw = true; - }, - - /** - * @returns {Number} The TiledImage's current opacity. - */ - getOpacity: function() { - return this.opacity; - }, - - /** - * @param {Number} opacity Opacity the tiled image should be drawn at. - */ - setOpacity: function(opacity) { - this.opacity = opacity; - this._needsDraw = true; - }, - - /** - * Get the current rotation of this tiled image in degrees. - * @returns {Number} the current rotation of this tiled image in degrees. - */ - getRotation: function() { - return this._degrees; - }, - - /** - * Set the current rotation of this tiled image in degrees. - * @param {Number} degrees the rotation in degrees. - */ - setRotation: function(degrees) { - degrees = $.positiveModulo(degrees, 360); - if (this._degrees === degrees) { - return; - } - this._degrees = degrees; - this._needsDraw = true; - this._raiseBoundsChange(); - }, - - /** - * @private - * Get the point around which this tiled image is rotated - * @param {Boolean} current True for current rotation point, false for target. - * @returns {OpenSeadragon.Point} - */ - _getRotationPoint: function(current) { - return this.getBoundsNoRotate(current).getTopLeft(); - }, - - /** - * @returns {String} The TiledImage's current compositeOperation. - */ - getCompositeOperation: function() { - return this.compositeOperation; - }, - - /** - * @param {String} compositeOperation the tiled image should be drawn with this globalCompositeOperation. - */ - setCompositeOperation: function(compositeOperation) { - this.compositeOperation = compositeOperation; - this._needsDraw = true; - }, - - // private - _setScale: function(scale, immediately) { - var sameTarget = (this._scaleSpring.target.value === scale); - if (immediately) { - if (sameTarget && this._scaleSpring.current.value === scale) { - return; - } - - this._scaleSpring.resetTo(scale); - this._updateForScale(); - this._needsDraw = true; - } else { - if (sameTarget) { - return; - } - - this._scaleSpring.springTo(scale); - this._updateForScale(); - this._needsDraw = true; - } - - if (!sameTarget) { - this._raiseBoundsChange(); - } - }, - - // private - _updateForScale: function() { - this._worldWidthTarget = this._scaleSpring.target.value; - this._worldHeightTarget = this.normHeight * this._scaleSpring.target.value; - this._worldWidthCurrent = this._scaleSpring.current.value; - this._worldHeightCurrent = this.normHeight * this._scaleSpring.current.value; - }, - - // private - _raiseBoundsChange: function() { - /** - * Raised when the TiledImage's bounds are changed. - * Note that this event is triggered only when the animation target is changed; - * not for every frame of animation. - * @event bounds-change - * @memberOf OpenSeadragon.TiledImage - * @type {object} - * @property {OpenSeadragon.World} eventSource - A reference to the TiledImage which raised the event. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - this.raiseEvent('bounds-change'); - }, - - // private - _isBottomItem: function() { - return this.viewer.world.getItemAt(0) === this; - }, - - // private - _getLevelsInterval: function() { - var lowestLevel = Math.max( - this.source.minLevel, - Math.floor(Math.log(this.minZoomImageRatio) / Math.log(2)) - ); - var currentZeroRatio = this.viewport.deltaPixelsFromPointsNoRotate( - this.source.getPixelRatio(0), true).x * - this._scaleSpring.current.value; - var highestLevel = Math.min( - Math.abs(this.source.maxLevel), - Math.abs(Math.floor( - Math.log(currentZeroRatio / this.minPixelRatio) / Math.log(2) - )) - ); - - // Calculations for the interval of levels to draw - // can return invalid intervals; fix that here if necessary - lowestLevel = Math.min(lowestLevel, highestLevel); - return { - lowestLevel: lowestLevel, - highestLevel: highestLevel - }; - }, - - // private - _updateViewport: function() { - this._needsDraw = false; - - // Reset tile's internal drawn state - while (this.lastDrawn.length > 0) { - var tile = this.lastDrawn.pop(); - tile.beingDrawn = false; - } - - var viewport = this.viewport; - var drawArea = this._viewportToTiledImageRectangle( - viewport.getBoundsWithMargins(true)); - - if (!this.wrapHorizontal && !this.wrapVertical) { - var tiledImageBounds = this._viewportToTiledImageRectangle( - this.getClippedBounds(true)); - drawArea = drawArea.intersection(tiledImageBounds); - if (drawArea === null) { - return; - } - } - - var levelsInterval = this._getLevelsInterval(); - var lowestLevel = levelsInterval.lowestLevel; - var highestLevel = levelsInterval.highestLevel; - var bestTile = null; - var haveDrawn = false; - var currentTime = $.now(); - - // Update any level that will be drawn - for (var level = highestLevel; level >= lowestLevel; level--) { - var drawLevel = false; - - //Avoid calculations for draw if we have already drawn this - var currentRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate( - this.source.getPixelRatio(level), - true - ).x * this._scaleSpring.current.value; - - if (level === lowestLevel || - (!haveDrawn && currentRenderPixelRatio >= this.minPixelRatio)) { - drawLevel = true; - haveDrawn = true; - } else if (!haveDrawn) { - continue; - } - - //Perform calculations for draw if we haven't drawn this - var targetRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate( - this.source.getPixelRatio(level), - false - ).x * this._scaleSpring.current.value; //TODO: shouldn't that be target.value? - - var targetZeroRatio = viewport.deltaPixelsFromPointsNoRotate( - this.source.getPixelRatio( - Math.max( - this.source.getClosestLevel(viewport.containerSize) - 1, - 0 - ) - ), - false - ).x * this._scaleSpring.current.value; //TODO: shouldn't that be target.value? - - var optimalRatio = this.immediateRender ? 1 : targetZeroRatio; - var levelOpacity = Math.min(1, (currentRenderPixelRatio - 0.5) / 0.5); - var levelVisibility = optimalRatio / Math.abs( - optimalRatio - targetRenderPixelRatio - ); - - // Update the level and keep track of 'best' tile to load - bestTile = updateLevel( - this, - haveDrawn, - drawLevel, - level, - levelOpacity, - levelVisibility, - drawArea, - currentTime, - bestTile - ); - - // Stop the loop if lower-res tiles would all be covered by - // already drawn tiles - if (providesCoverage(this.coverage, level)) { - break; - } - } - - // Perform the actual drawing - drawTiles(this, this.lastDrawn); - -<<<<<<< HEAD - // Load the new 'best' tile - if (bestTile && !bestTile.context2D) { - loadTile(this, bestTile, currentTime); - this._setFullyLoaded(false); - } else { - this._setFullyLoaded(true); - } -======= - // Load the new 'best' tile - if (best && !best.context2D) { - loadTile( tiledImage, best, currentTime ); - tiledImage._needsDraw = true; - tiledImage._setFullyLoaded(false); - } else { - tiledImage._setFullyLoaded(true); ->>>>>>> e8324627e144f6e01ab1ce70cd88194fbab46487 - } -}); - -function updateLevel(tiledImage, haveDrawn, drawLevel, level, levelOpacity, - levelVisibility, drawArea, currentTime, best) { - - var topLeftBound = drawArea.getBoundingBox().getTopLeft(); - var bottomRightBound = drawArea.getBoundingBox().getBottomRight(); - - if (tiledImage.viewer) { - /** - * - Needs documentation - - * - * @event update-level - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. - * @property {Object} havedrawn - * @property {Object} level - * @property {Object} opacity - * @property {Object} visibility - * @property {OpenSeadragon.Rect} drawArea - * @property {Object} topleft deprecated, use drawArea instead - * @property {Object} bottomright deprecated, use drawArea instead - * @property {Object} currenttime - * @property {Object} best - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - tiledImage.viewer.raiseEvent('update-level', { - tiledImage: tiledImage, - havedrawn: haveDrawn, - level: level, - opacity: levelOpacity, - visibility: levelVisibility, - drawArea: drawArea, - topleft: topLeftBound, - bottomright: bottomRightBound, - currenttime: currentTime, - best: best - }); - } - - //OK, a new drawing so do your calculations - var topLeftTile = tiledImage.source.getTileAtPoint(level, topLeftBound); - var bottomRightTile = tiledImage.source.getTileAtPoint(level, bottomRightBound); - var numberOfTiles = tiledImage.source.getNumTiles(level); - - resetCoverage(tiledImage.coverage, level); - - if (tiledImage.wrapHorizontal) { - topLeftTile.x -= 1; // left invisible column (othervise we will have empty space after scroll at left) - } else { - // Adjust for floating point error - topLeftTile.x = Math.max(topLeftTile.x, 0); - bottomRightTile.x = Math.min(bottomRightTile.x, numberOfTiles.x - 1); - } - if (tiledImage.wrapVertical) { - topLeftTile.y -= 1; // top invisible row (othervise we will have empty space after scroll at top) - } else { - // Adjust for floating point error - topLeftTile.y = Math.max(topLeftTile.y, 0); - bottomRightTile.y = Math.min(bottomRightTile.y, numberOfTiles.y - 1); - } - - var viewportCenter = tiledImage.viewport.pixelFromPoint( - tiledImage.viewport.getCenter()); - for (var x = topLeftTile.x; x <= bottomRightTile.x; x++) { - for (var y = topLeftTile.y; y <= bottomRightTile.y; y++) { - - var tileBounds = tiledImage.source.getTileBounds(level, x, y); - - if (drawArea.intersection(tileBounds) === null) { - // This tile is outside of the viewport, no need to draw it - continue; - } - - best = updateTile( - tiledImage, - drawLevel, - haveDrawn, - x, y, - level, - levelOpacity, - levelVisibility, - viewportCenter, - numberOfTiles, - currentTime, - best - ); - - } - } - - return best; -} - -function updateTile( tiledImage, drawLevel, haveDrawn, x, y, level, levelOpacity, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){ - - var tile = getTile( - x, y, - level, - tiledImage.source, - tiledImage.tilesMatrix, - currentTime, - numberOfTiles, - tiledImage._worldWidthCurrent, - tiledImage._worldHeightCurrent - ), - drawTile = drawLevel; - - if( tiledImage.viewer ){ - /** - * - Needs documentation - - * - * @event update-tile - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. - * @property {OpenSeadragon.Tile} tile - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - tiledImage.viewer.raiseEvent( 'update-tile', { - tiledImage: tiledImage, - tile: tile - }); - } - - setCoverage( tiledImage.coverage, level, x, y, false ); - - if ( !tile.exists ) { - return best; - } - - if ( haveDrawn && !drawTile ) { - if ( isCovered( tiledImage.coverage, level, x, y ) ) { - setCoverage( tiledImage.coverage, level, x, y, true ); - } else { - drawTile = true; - } - } - - if ( !drawTile ) { - return best; - } - - positionTile( - tile, - tiledImage.source.tileOverlap, - tiledImage.viewport, - viewportCenter, - levelVisibility, - tiledImage - ); - - if (!tile.loaded) { - if (tile.context2D) { - setTileLoaded(tiledImage, tile); - } else { - var imageRecord = tiledImage._tileCache.getImageRecord(tile.url); - if (imageRecord) { - var image = imageRecord.getImage(); - setTileLoaded(tiledImage, tile, image); - } - } - } - - if ( tile.loaded ) { - var needsDraw = blendTile( - tiledImage, - tile, - x, y, - level, - levelOpacity, - currentTime - ); - - if ( needsDraw ) { - tiledImage._needsDraw = true; - } - } else if ( tile.loading ) { - // the tile is already in the download queue - // thanks josh1093 for finally translating this typo - } else { - best = compareTiles( best, tile ); - } - - return best; -} - -function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWidth, worldHeight ) { - var xMod, - yMod, - bounds, - exists, - url, - context2D, - tile; - - if ( !tilesMatrix[ level ] ) { - tilesMatrix[ level ] = {}; - } - if ( !tilesMatrix[ level ][ x ] ) { - tilesMatrix[ level ][ x ] = {}; - } - - if ( !tilesMatrix[ level ][ x ][ y ] ) { - xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x; - yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y; - 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); - - tilesMatrix[ level ][ x ][ y ] = new $.Tile( - level, - x, - y, - bounds, - exists, - url, - context2D - ); - } - - tile = tilesMatrix[ level ][ x ][ y ]; - tile.lastTouchTime = time; - - return tile; -} - -function loadTile( tiledImage, tile, time ) { - tile.loading = true; - tiledImage._imageLoader.addJob({ - src: tile.url, - crossOriginPolicy: tiledImage.crossOriginPolicy, - callback: function( image, errorMsg ){ - onTileLoad( tiledImage, tile, time, image, errorMsg ); - }, - abort: function() { - tile.loading = false; - } - }); -} - -function onTileLoad( tiledImage, tile, time, image, errorMsg ) { - if ( !image ) { - $.console.log( "Tile %s failed to load: %s - error: %s", tile, tile.url, errorMsg ); - /** - * Triggered when a tile fails to load. - * - * @event tile-load-failed - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Tile} tile - The tile that failed to load. - * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image the tile belongs to. - * @property {number} time - The time in milliseconds when the tile load began. - * @property {string} message - The error message. - */ - tiledImage.viewer.raiseEvent("tile-load-failed", {tile: tile, tiledImage: tiledImage, time: time, message: errorMsg}); - 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; - } - - var finish = function() { - var cutoff = Math.ceil( Math.log( - tiledImage.source.getTileWidth(tile.level) ) / Math.log( 2 ) ); - setTileLoaded(tiledImage, tile, image, cutoff); - }; - - // Check if we're mid-update; this can happen on IE8 because image load events for - // cached images happen immediately there - if ( !tiledImage._midDraw ) { - finish(); - } else { - // Wait until after the update, in case caching unloads any tiles - window.setTimeout( finish, 1); - } -} - -function setTileLoaded(tiledImage, tile, image, cutoff) { - var increment = 0; - - function getCompletionCallback() { - increment++; - return completionCallback; - } - - function completionCallback() { - increment--; - if (increment === 0) { - tile.loading = false; - tile.loaded = true; - if (!tile.context2D) { - tiledImage._tileCache.cacheTile({ - image: image, - tile: tile, - cutoff: cutoff, - tiledImage: tiledImage - }); - } - tiledImage._needsDraw = true; - } - } - - /** - * Triggered when a tile has just been loaded in memory. That means that the - * image has been downloaded and can be modified before being drawn to the canvas. - * - * @event tile-loaded - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {Image} image - The image of the tile. - * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile. - * @property {OpenSeadragon.Tile} tile - The tile which has been loaded. - * @property {function} getCompletionCallback - A function giving a callback to call - * when the asynchronous processing of the image is done. The image will be - * marked as entirely loaded when the callback has been called once for each - * call to getCompletionCallback. - */ - tiledImage.viewer.raiseEvent("tile-loaded", { - tile: tile, - tiledImage: tiledImage, - image: image, - getCompletionCallback: getCompletionCallback - }); - // In case the completion callback is never called, we at least force it once. - getCompletionCallback()(); -} - -function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, tiledImage ){ - var boundsTL = tile.bounds.getTopLeft(); - - boundsTL.x *= tiledImage._scaleSpring.current.value; - boundsTL.y *= tiledImage._scaleSpring.current.value; - boundsTL.x += tiledImage._xSpring.current.value; - boundsTL.y += tiledImage._ySpring.current.value; - - var boundsSize = tile.bounds.getSize(); - - boundsSize.x *= tiledImage._scaleSpring.current.value; - boundsSize.y *= tiledImage._scaleSpring.current.value; - - 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 ); - - if ( !overlap ) { - sizeC = sizeC.plus( new $.Point( 1, 1 ) ); - } - - tile.position = positionC; - tile.size = sizeC; - tile.distance = tileDistance; - tile.visibility = levelVisibility; -} - - -function blendTile( tiledImage, tile, x, y, level, levelOpacity, currentTime ){ - var blendTimeMillis = 1000 * tiledImage.blendTime, - deltaTime, - opacity; - - if ( !tile.blendStart ) { - tile.blendStart = currentTime; - } - - deltaTime = currentTime - tile.blendStart; - opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1; - - if ( tiledImage.alwaysBlend ) { - opacity *= levelOpacity; - } - - tile.opacity = opacity; - - tiledImage.lastDrawn.push( tile ); - - if ( opacity == 1 ) { - setCoverage( tiledImage.coverage, level, x, y, true ); - tiledImage._hasOpaqueTile = true; - } else if ( deltaTime < blendTimeMillis ) { - return true; - } - - return false; -} - -/** - * @private - * @inner - * Returns true if the given tile provides coverage to lower-level tiles of - * lower resolution representing the same content. If neither x nor y is - * given, returns true if the entire visible level provides coverage. - * - * Note that out-of-bounds tiles provide coverage in this sense, since - * there's no content that they would need to cover. Tiles at non-existent - * levels that are within the image bounds, however, do not. - */ -function providesCoverage( coverage, level, x, y ) { - var rows, - cols, - i, j; - - if ( !coverage[ level ] ) { - return false; - } - - if ( x === undefined || y === undefined ) { - rows = coverage[ level ]; - for ( i in rows ) { - if ( rows.hasOwnProperty( i ) ) { - cols = rows[ i ]; - for ( j in cols ) { - if ( cols.hasOwnProperty( j ) && !cols[ j ] ) { - return false; - } - } - } - } - - return true; - } - - return ( - coverage[ level ][ x] === undefined || - coverage[ level ][ x ][ y ] === undefined || - coverage[ level ][ x ][ y ] === true - ); -} - -/** - * @private - * @inner - * Returns true if the given tile is completely covered by higher-level - * tiles of higher resolution representing the same content. If neither x - * nor y is given, returns true if the entire visible level is covered. - */ -function isCovered( coverage, level, x, y ) { - if ( x === undefined || y === undefined ) { - return providesCoverage( coverage, level + 1 ); - } else { - return ( - providesCoverage( coverage, level + 1, 2 * x, 2 * y ) && - providesCoverage( coverage, level + 1, 2 * x, 2 * y + 1 ) && - providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y ) && - providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y + 1 ) - ); - } -} - -/** - * @private - * @inner - * Sets whether the given tile provides coverage or not. - */ -function setCoverage( coverage, level, x, y, covers ) { - if ( !coverage[ level ] ) { - $.console.warn( - "Setting coverage for a tile before its level's coverage has been reset: %s", - level - ); - return; - } - - if ( !coverage[ level ][ x ] ) { - coverage[ level ][ x ] = {}; - } - - coverage[ level ][ x ][ y ] = covers; -} - -/** - * @private - * @inner - * Resets coverage information for the given level. This should be called - * after every draw routine. Note that at the beginning of the next draw - * routine, coverage for every visible tile should be explicitly set. - */ -function resetCoverage( coverage, level ) { - coverage[ level ] = {}; -} - -/** - * @private - * @inner - * Determines whether the 'last best' tile for the area is better than the - * tile in question. - */ -function compareTiles( previousBest, tile ) { - if ( !previousBest ) { - return tile; - } - - if ( tile.visibility > previousBest.visibility ) { - return tile; - } else if ( tile.visibility == previousBest.visibility ) { - if ( tile.distance < previousBest.distance ) { - return tile; - } - } - - return previousBest; -} - -function drawTiles( tiledImage, lastDrawn ) { - if (lastDrawn.length === 0) { - return; - } - var tile = lastDrawn[0]; - - var useSketch = tiledImage.opacity < 1 || - (tiledImage.compositeOperation && - tiledImage.compositeOperation !== 'source-over') || - (!tiledImage._isBottomItem() && tile._hasTransparencyChannel()); - - var sketchScale; - var sketchTranslate; - - var zoom = tiledImage.viewport.getZoom(true); - var imageZoom = tiledImage.viewportToImageZoom(zoom); - if (imageZoom > tiledImage.smoothTileEdgesMinZoom && !tiledImage.iOSDevice) { - // When zoomed in a lot (>100%) the tile edges are visible. - // So we have to composite them at ~100% and scale them up together. - // Note: Disabled on iOS devices per default as it causes a native crash - useSketch = true; - sketchScale = tile.getScaleForEdgeSmoothing(); - sketchTranslate = tile.getTranslationForEdgeSmoothing(sketchScale, - tiledImage._drawer.getCanvasSize(false), - tiledImage._drawer.getCanvasSize(true)); - } - - var bounds; - if (useSketch) { - if (!sketchScale) { - // Except when edge smoothing, we only clean the part of the - // sketch canvas we are going to use for performance reasons. - bounds = tiledImage.viewport.viewportToViewerElementRectangle( - tiledImage.getClippedBounds(true)) - .getIntegerBoundingBox() - .times($.pixelDensityRatio); - } - tiledImage._drawer._clear(true, bounds); - } - - // When scaling, we must rotate only when blending the sketch canvas to - // avoid interpolation - if (!sketchScale) { - if (tiledImage.viewport.degrees !== 0) { - tiledImage._drawer._offsetForRotation( - tiledImage.viewport.degrees, useSketch); - } - if (tiledImage._degrees !== 0) { - tiledImage._drawer._offsetForRotation( - tiledImage._degrees, - tiledImage.viewport.pixelFromPointNoRotate( - tiledImage._getRotationPoint(true), true), - useSketch); - } - } - - var usedClip = false; - if ( tiledImage._clip ) { - tiledImage._drawer.saveContext(useSketch); - - var box = tiledImage.imageToViewportRectangle(tiledImage._clip, true); - box = box.rotate(-tiledImage._degrees, tiledImage._getRotationPoint()); - 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; - } - - 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" ) { - fillStyle = tiledImage.placeholderFillStyle(tiledImage, tiledImage._drawer.context); - } - else { - fillStyle = tiledImage.placeholderFillStyle; - } - - tiledImage._drawer.drawRectangle(placeholderRect, fillStyle, useSketch); - } - - for (var i = lastDrawn.length - 1; i >= 0; i--) { - tile = lastDrawn[ i ]; - tiledImage._drawer.drawTile( tile, tiledImage._drawingHandler, useSketch, sketchScale, sketchTranslate ); - tile.beingDrawn = true; - - if( tiledImage.viewer ){ - /** - * - Needs documentation - - * - * @event tile-drawn - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. - * @property {OpenSeadragon.Tile} tile - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - tiledImage.viewer.raiseEvent( 'tile-drawn', { - tiledImage: tiledImage, - tile: tile - }); - } - } - - if ( usedClip ) { - tiledImage._drawer.restoreContext( useSketch ); - } - - if (!sketchScale) { - if (tiledImage._degrees !== 0) { - tiledImage._drawer._restoreRotationChanges(useSketch); - } - if (tiledImage.viewport.degrees !== 0) { - tiledImage._drawer._restoreRotationChanges(useSketch); - } - } - - if (useSketch) { - if (sketchScale) { - if (tiledImage.viewport.degrees !== 0) { - tiledImage._drawer._offsetForRotation( - tiledImage.viewport.degrees, false); - } - if (tiledImage._degrees !== 0) { - tiledImage._drawer._offsetForRotation( - tiledImage._degrees, - tiledImage.viewport.pixelFromPointNoRotate( - tiledImage._getRotationPoint(true), true), - useSketch); - } - } - tiledImage._drawer.blendSketch({ - opacity: tiledImage.opacity, - scale: sketchScale, - translate: sketchTranslate, - compositeOperation: tiledImage.compositeOperation, - bounds: bounds - }); - if (sketchScale) { - if (tiledImage._degrees !== 0) { - tiledImage._drawer._restoreRotationChanges(false); - } - if (tiledImage.viewport.degrees !== 0) { - tiledImage._drawer._restoreRotationChanges(false); - } - } - } - drawDebugInfo( tiledImage, lastDrawn ); -} - -function drawDebugInfo( tiledImage, lastDrawn ) { - if( tiledImage.debugMode ) { - for ( var i = lastDrawn.length - 1; i >= 0; i-- ) { - var tile = lastDrawn[ i ]; - try { - tiledImage._drawer.drawDebugInfo( - tile, lastDrawn.length, i, tiledImage); - } catch(e) { - $.console.error(e); - } - } - } -} - -}( OpenSeadragon )); diff --git a/src/world.js b/src/world.js index b06ae484..e68590b3 100644 --- a/src/world.js +++ b/src/world.js @@ -94,6 +94,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W this._needsDraw = true; item.addHandler('bounds-change', this._delegatedFigureSizes); + item.addHandler('clip-change', this._delegatedFigureSizes); /** * Raised when an item is added to the World. @@ -194,6 +195,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W } item.removeHandler('bounds-change', this._delegatedFigureSizes); + item.removeHandler('clip-change', this._delegatedFigureSizes); item.destroy(); this._items.splice( index, 1 ); this._figureSizes(); @@ -213,6 +215,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W for (var i = 0; i < this._items.length; i++) { item = this._items[i]; item.removeHandler('bounds-change', this._delegatedFigureSizes); + item.removeHandler('clip-change', this._delegatedFigureSizes); item.destroy(); } diff --git a/test/modules/tiledimage.js b/test/modules/tiledimage.js index 8ff45ccd..20171432 100644 --- a/test/modules/tiledimage.js +++ b/test/modules/tiledimage.js @@ -222,6 +222,27 @@ }); }); + // ---------- + asyncTest('clip-change event', function() { + expect(0); + var clip = new OpenSeadragon.Rect(100, 100, 800, 800); + + viewer.addHandler('open', function() { + var image = viewer.world.getItemAt(0); + image.addOnceHandler('clip-change', function() { + image.addOnceHandler('clip-change', function() { + start(); + }); + image.setClip(clip); + }); + image.setClip(null); + }); + + viewer.open({ + tileSource: '/test/data/testpattern.dzi' + }); + }); + // ---------- asyncTest('getClipBounds', function() { var clip = new OpenSeadragon.Rect(100, 200, 800, 500); From fbcf78c894d837c0c165508b8a1bdd1e2f843794 Mon Sep 17 00:00:00 2001 From: Antoine Vandecreme Date: Sun, 28 Aug 2016 19:59:36 +0200 Subject: [PATCH 07/15] Fix tests. --- test/modules/viewport.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/test/modules/viewport.js b/test/modules/viewport.js index fb94d11d..e49c1e68 100644 --- a/test/modules/viewport.js +++ b/test/modules/viewport.js @@ -105,7 +105,8 @@ orig = config.getOrig(config.testArray[i], viewport); expected = config.getExpected(orig, viewport); actual = viewport[config.method](orig); - propEqual( + var assert = config.assert || propEqual; + assert( actual, expected, "Correctly converted coordinates " + orig @@ -118,6 +119,10 @@ viewer.open(DZI_PATH); }; + function assertPointsEquals(actual, expected, message) { + Util.assertPointsEquals(actual, expected, 1e-15, message); + } + // Tests start here. asyncTest('getContainerSize', function() { @@ -872,7 +877,8 @@ getExpected: function(orig, viewport) { return orig.divide(viewer.source.dimensions.x); }, - method: 'imageToViewportCoordinates' + method: 'imageToViewportCoordinates', + assert: assertPointsEquals }); }); @@ -885,7 +891,8 @@ getExpected: function(orig, viewport) { return orig.divide(ZOOM_FACTOR * viewport.getContainerSize().x); }, - method: 'imageToViewportCoordinates' + method: 'imageToViewportCoordinates', + assert: assertPointsEquals }); }); asyncTest('imageToViewportRectangle', function() { @@ -902,7 +909,8 @@ orig.height / viewer.source.dimensions.x ); }, - method: 'imageToViewportRectangle' + method: 'imageToViewportRectangle', + assert: assertPointsEquals }); }); From 2821c8f67bd3b5a4ec5f9335f9972b53917b7c59 Mon Sep 17 00:00:00 2001 From: Antoine Vandecreme Date: Thu, 6 Oct 2016 22:18:32 +0200 Subject: [PATCH 08/15] Partialy fix edge smoothing. --- src/drawer.js | 7 ++++--- src/tiledimage.js | 2 +- src/world.js | 10 ++++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/drawer.js b/src/drawer.js index a77edd56..7029320d 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -323,13 +323,13 @@ $.Drawer.prototype = { this.sketchCanvas.height = sketchCanvasSize.y; this.sketchContext = this.sketchCanvas.getContext( "2d" ); + // FIXME: should check if any tiled image get rotated as well. // If the viewport is not currently rotated, the sketchCanvas // will have the same size as the main canvas. However, if // the viewport get rotated later on, we will need to resize it. if (this.viewport.getRotation() === 0) { var self = this; - this.viewer.addHandler('rotate', function resizeSketchCanvas() { - self.viewer.removeHandler('rotate', resizeSketchCanvas); + this.viewer.addOnceHandler('rotate', function resizeSketchCanvas() { var sketchCanvasSize = self._calculateSketchCanvasSize(); self.sketchCanvas.width = sketchCanvasSize.x; self.sketchCanvas.height = sketchCanvasSize.y; @@ -617,7 +617,8 @@ $.Drawer.prototype = { // private _calculateSketchCanvasSize: function() { var canvasSize = this._calculateCanvasSize(); - if (this.viewport.getRotation() === 0) { + if (this.viewport.getRotation() === 0 && + !this.viewer.world._hasRotatedItem()) { return canvasSize; } // If the viewport is rotated, we need a larger sketch canvas in order diff --git a/src/tiledimage.js b/src/tiledimage.js index 15ec6668..90c9222a 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -1670,7 +1670,7 @@ function drawTiles( tiledImage, lastDrawn ) { tiledImage._degrees, tiledImage.viewport.pixelFromPointNoRotate( tiledImage._getRotationPoint(true), true), - useSketch); + false); } } tiledImage._drawer.blendSketch({ diff --git a/src/world.js b/src/world.js index e68590b3..ad6285d9 100644 --- a/src/world.js +++ b/src/world.js @@ -436,6 +436,16 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W * @property {?Object} userData - Arbitrary subscriber-defined object. */ this.raiseEvent( 'remove-item', { item: item } ); + }, + + // private + _hasRotatedItem: function() { + for (var i = 0; i < this._items.length; i++) { + if (this._items[i].getRotation() !== 0) { + return true; + } + } + return false; } }); From cca6b47fc01cedd533c5dcdb72a4e22a5dde138e Mon Sep 17 00:00:00 2001 From: Antoine Vandecreme Date: Sun, 9 Oct 2016 14:05:22 +0200 Subject: [PATCH 09/15] Fix TileSource.getTileAtPoint --- src/tilesource.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/tilesource.js b/src/tilesource.js index eb5aabc0..fd0084c4 100644 --- a/src/tilesource.js +++ b/src/tilesource.js @@ -344,12 +344,13 @@ $.TileSource.prototype = { * @param {Number} level * @param {OpenSeadragon.Point} point */ - getTileAtPoint: function( level, point ) { - var numTiles = this.getNumTiles( level ); - return new $.Point( - Math.floor( (point.x * numTiles.x) / 1 ), - Math.floor( (point.y * numTiles.y * this.dimensions.x) / this.dimensions.y ) - ); + getTileAtPoint: function(level, point) { + var widthScaled = this.dimensions.x * this.getLevelScale(level); + var pixelX = point.x * widthScaled; + var pixelY = point.y * widthScaled; + var x = Math.floor(pixelX / this.getTileWidth()); + var y = Math.floor(pixelY / this.getTileHeight()); + return new $.Point(x, y); }, /** From 5bfccec7a35a4e4e8f3322cf7e37b4978e9a6b01 Mon Sep 17 00:00:00 2001 From: Antoine Vandecreme Date: Sun, 23 Oct 2016 17:45:39 +0200 Subject: [PATCH 10/15] Disable tile edge smoothing with tiled image rotation --- src/tiledimage.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tiledimage.js b/src/tiledimage.js index 90c9222a..3eb9fafa 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -1544,7 +1544,9 @@ function drawTiles( tiledImage, lastDrawn ) { var zoom = tiledImage.viewport.getZoom(true); var imageZoom = tiledImage.viewportToImageZoom(zoom); - if (imageZoom > tiledImage.smoothTileEdgesMinZoom && !tiledImage.iOSDevice) { + // TODO: support tile edge smoothing with tiled image rotation. + if (imageZoom > tiledImage.smoothTileEdgesMinZoom && !tiledImage.iOSDevice && + tiledImage.getRotation() == 0) { // When zoomed in a lot (>100%) the tile edges are visible. // So we have to composite them at ~100% and scale them up together. // Note: Disabled on iOS devices per default as it causes a native crash From 77310b0229b2853b835c8c872d14f841735b1d78 Mon Sep 17 00:00:00 2001 From: Antoine Vandecreme Date: Sun, 23 Oct 2016 18:25:14 +0200 Subject: [PATCH 11/15] Remove TODO --- src/tiledimage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tiledimage.js b/src/tiledimage.js index 3eb9fafa..0d155325 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -959,7 +959,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag var targetRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate( this.source.getPixelRatio(level), false - ).x * this._scaleSpring.current.value; //TODO: shouldn't that be target.value? + ).x * this._scaleSpring.current.value; var targetZeroRatio = viewport.deltaPixelsFromPointsNoRotate( this.source.getPixelRatio( @@ -969,7 +969,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag ) ), false - ).x * this._scaleSpring.current.value; //TODO: shouldn't that be target.value? + ).x * this._scaleSpring.current.value; var optimalRatio = this.immediateRender ? 1 : targetZeroRatio; var levelOpacity = Math.min(1, (currentRenderPixelRatio - 0.5) / 0.5); From a9b60057ea990c6c9ec0c63f28bc7c77327fa8a8 Mon Sep 17 00:00:00 2001 From: Antoine Vandecreme Date: Sun, 23 Oct 2016 22:25:16 +0200 Subject: [PATCH 12/15] Fix wrapping. --- src/tiledimage.js | 21 ++++++++++----------- src/tilesource.js | 11 +++++++++-- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/tiledimage.js b/src/tiledimage.js index 0d155325..992cc895 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -1058,16 +1058,12 @@ function updateLevel(tiledImage, haveDrawn, drawLevel, level, levelOpacity, resetCoverage(tiledImage.coverage, level); - if (tiledImage.wrapHorizontal) { - topLeftTile.x -= 1; // left invisible column (othervise we will have empty space after scroll at left) - } else { + if (!tiledImage.wrapHorizontal) { // Adjust for floating point error topLeftTile.x = Math.max(topLeftTile.x, 0); bottomRightTile.x = Math.min(bottomRightTile.x, numberOfTiles.x - 1); } - if (tiledImage.wrapVertical) { - topLeftTile.y -= 1; // top invisible row (othervise we will have empty space after scroll at top) - } else { + if (!tiledImage.wrapVertical) { // Adjust for floating point error topLeftTile.y = Math.max(topLeftTile.y, 0); bottomRightTile.y = Math.min(bottomRightTile.y, numberOfTiles.y - 1); @@ -1078,11 +1074,14 @@ function updateLevel(tiledImage, haveDrawn, drawLevel, level, levelOpacity, for (var x = topLeftTile.x; x <= bottomRightTile.x; x++) { for (var y = topLeftTile.y; y <= bottomRightTile.y; y++) { - var tileBounds = tiledImage.source.getTileBounds(level, x, y); - - if (drawArea.intersection(tileBounds) === null) { - // This tile is outside of the viewport, no need to draw it - continue; + // Optimisation disabled with wrapping because getTileBounds does not + // work correctly with x and y outside of the number of tiles + if (!tiledImage.wrapHorizontal && !tiledImage.wrapVertical) { + var tileBounds = tiledImage.source.getTileBounds(level, x, y); + if (drawArea.intersection(tileBounds) === null) { + // This tile is outside of the viewport, no need to draw it + continue; + } } best = updateTile( diff --git a/src/tilesource.js b/src/tilesource.js index fd0084c4..fa52f61e 100644 --- a/src/tilesource.js +++ b/src/tilesource.js @@ -346,10 +346,17 @@ $.TileSource.prototype = { */ getTileAtPoint: function(level, point) { var widthScaled = this.dimensions.x * this.getLevelScale(level); - var pixelX = point.x * widthScaled; - var pixelY = point.y * widthScaled; + var pixelX = $.positiveModulo(point.x, 1) * widthScaled; + var pixelY = $.positiveModulo(point.y, 1 / this.aspectRatio) * widthScaled; + var x = Math.floor(pixelX / this.getTileWidth()); var y = Math.floor(pixelY / this.getTileHeight()); + + // Fix for wrapping + var numTiles = this.getNumTiles(level); + x += numTiles.x * Math.floor(point.x); + y += numTiles.y * Math.floor(point.y * this.aspectRatio); + return new $.Point(x, y); }, From b992d545721c9343c7db70285c93bab6ba2cac09 Mon Sep 17 00:00:00 2001 From: Antoine Vandecreme Date: Mon, 24 Oct 2016 21:34:29 +0200 Subject: [PATCH 13/15] Fix jshint error --- src/tiledimage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tiledimage.js b/src/tiledimage.js index 992cc895..12d62950 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -1545,7 +1545,7 @@ function drawTiles( tiledImage, lastDrawn ) { var imageZoom = tiledImage.viewportToImageZoom(zoom); // TODO: support tile edge smoothing with tiled image rotation. if (imageZoom > tiledImage.smoothTileEdgesMinZoom && !tiledImage.iOSDevice && - tiledImage.getRotation() == 0) { + tiledImage.getRotation() === 0) { // When zoomed in a lot (>100%) the tile edges are visible. // So we have to composite them at ~100% and scale them up together. // Note: Disabled on iOS devices per default as it causes a native crash From 5ac1502ccd03cc857f5398225cc8f52c1344853d Mon Sep 17 00:00:00 2001 From: Antoine Vandecreme Date: Mon, 24 Oct 2016 22:03:31 +0200 Subject: [PATCH 14/15] Take pixelDensityRatio into account when rotating --- src/drawer.js | 22 +++++++++++++--------- src/tiledimage.js | 30 ++++++++++++++++++------------ 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/drawer.js b/src/drawer.js index 7029320d..9440c280 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -477,13 +477,14 @@ $.Drawer.prototype = { context.fillStyle = this.debugGridColor; if ( this.viewport.degrees !== 0 ) { - this._offsetForRotation(this.viewport.degrees); + this._offsetForRotation({degrees: this.viewport.degrees}); } if (tiledImage.getRotation() !== 0) { - this._offsetForRotation( - tiledImage.getRotation(), - tiledImage.viewport.pixelFromPointNoRotate( - tiledImage._getRotationPoint(true), true)); + this._offsetForRotation({ + degrees: tiledImage.getRotation(), + point: tiledImage.viewport.pixelFromPointNoRotate( + tiledImage._getRotationPoint(true), true) + }); } context.strokeRect( @@ -588,13 +589,16 @@ $.Drawer.prototype = { }, // private - _offsetForRotation: function(degrees, point, useSketch) { - point = point || this.getCanvasCenter(); - var context = this._getContext(useSketch); + _offsetForRotation: function(options) { + var point = options.point ? + options.point.times($.pixelDensityRatio) : + this.getCanvasCenter(); + + var context = this._getContext(options.useSketch); context.save(); context.translate(point.x, point.y); - context.rotate(Math.PI / 180 * degrees); + context.rotate(Math.PI / 180 * options.degrees); context.translate(-point.x, -point.y); }, diff --git a/src/tiledimage.js b/src/tiledimage.js index 12d62950..c6659c77 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -1573,15 +1573,18 @@ function drawTiles( tiledImage, lastDrawn ) { // avoid interpolation if (!sketchScale) { if (tiledImage.viewport.degrees !== 0) { - tiledImage._drawer._offsetForRotation( - tiledImage.viewport.degrees, useSketch); + tiledImage._drawer._offsetForRotation({ + degrees: tiledImage.viewport.degrees, + useSketch: useSketch + }); } if (tiledImage._degrees !== 0) { - tiledImage._drawer._offsetForRotation( - tiledImage._degrees, - tiledImage.viewport.pixelFromPointNoRotate( + tiledImage._drawer._offsetForRotation({ + degrees: tiledImage._degrees, + point: tiledImage.viewport.pixelFromPointNoRotate( tiledImage._getRotationPoint(true), true), - useSketch); + useSketch: useSketch + }); } } @@ -1663,15 +1666,18 @@ function drawTiles( tiledImage, lastDrawn ) { if (useSketch) { if (sketchScale) { if (tiledImage.viewport.degrees !== 0) { - tiledImage._drawer._offsetForRotation( - tiledImage.viewport.degrees, false); + tiledImage._drawer._offsetForRotation({ + degrees: tiledImage.viewport.degrees, + useSketch: false + }); } if (tiledImage._degrees !== 0) { - tiledImage._drawer._offsetForRotation( - tiledImage._degrees, - tiledImage.viewport.pixelFromPointNoRotate( + tiledImage._drawer._offsetForRotation({ + degrees: tiledImage._degrees, + point: tiledImage.viewport.pixelFromPointNoRotate( tiledImage._getRotationPoint(true), true), - false); + useSketch: false + }); } } tiledImage._drawer.blendSketch({ From 4b487170104a577ebafbab9f53e4f7b2f758f72c Mon Sep 17 00:00:00 2001 From: Antoine Vandecreme Date: Tue, 25 Oct 2016 21:41:42 +0200 Subject: [PATCH 15/15] Rollback sketchCanvas scaling when tiled image rotated --- src/drawer.js | 4 +--- src/tiledimage.js | 16 ++-------------- src/world.js | 10 ---------- 3 files changed, 3 insertions(+), 27 deletions(-) diff --git a/src/drawer.js b/src/drawer.js index 9440c280..73772a88 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -323,7 +323,6 @@ $.Drawer.prototype = { this.sketchCanvas.height = sketchCanvasSize.y; this.sketchContext = this.sketchCanvas.getContext( "2d" ); - // FIXME: should check if any tiled image get rotated as well. // If the viewport is not currently rotated, the sketchCanvas // will have the same size as the main canvas. However, if // the viewport get rotated later on, we will need to resize it. @@ -621,8 +620,7 @@ $.Drawer.prototype = { // private _calculateSketchCanvasSize: function() { var canvasSize = this._calculateCanvasSize(); - if (this.viewport.getRotation() === 0 && - !this.viewer.world._hasRotatedItem()) { + if (this.viewport.getRotation() === 0) { return canvasSize; } // If the viewport is rotated, we need a larger sketch canvas in order diff --git a/src/tiledimage.js b/src/tiledimage.js index c6659c77..ca6b8eeb 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -550,20 +550,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag }, // private - // Convert rectangle in tiled image coordinates to viewport coordinates. - _tiledImageToViewportRectangle: function(rect) { - var scale = this._scaleSpring.current.value; - return new $.Rect( - rect.x * scale + this._xSpring.current.value, - rect.y * scale + this._ySpring.current.value, - rect.width * scale, - rect.height * scale, - rect.degrees) - .rotate(this.getRotation(), this._getRotationPoint(true)); - }, - - // private - // Convert rectangle in viewport coordinates to tiled image coordinates. + // Convert rectangle in viewport coordinates to this tiled image point + // coordinates (x in [0, 1] and y in [0, aspectRatio]) _viewportToTiledImageRectangle: function(rect) { var scale = this._scaleSpring.current.value; rect = rect.rotate(-this.getRotation(), this._getRotationPoint(true)); diff --git a/src/world.js b/src/world.js index ad6285d9..e68590b3 100644 --- a/src/world.js +++ b/src/world.js @@ -436,16 +436,6 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W * @property {?Object} userData - Arbitrary subscriber-defined object. */ this.raiseEvent( 'remove-item', { item: item } ); - }, - - // private - _hasRotatedItem: function() { - for (var i = 0; i < this._items.length; i++) { - if (this._items[i].getRotation() !== 0) { - return true; - } - } - return false; } });