Merge pull request #1006 from avandecreme/master

Tiled image rotation
This commit is contained in:
Ian Gilman 2016-10-26 09:58:12 -07:00 committed by GitHub
commit eb8b9ccd50
12 changed files with 467 additions and 278 deletions

View File

@ -328,8 +328,7 @@ $.Drawer.prototype = {
// 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;
@ -482,7 +481,7 @@ $.Drawer.prototype = {
},
// private
drawDebugInfo: function( tile, count, i ){
drawDebugInfo: function(tile, count, i, tiledImage) {
if ( !this.useCanvas ) {
return;
}
@ -495,7 +494,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({
degrees: tiledImage.getRotation(),
point: tiledImage.viewport.pixelFromPointNoRotate(
tiledImage._getRotationPoint(true), true)
});
}
context.strokeRect(
@ -559,6 +565,9 @@ $.Drawer.prototype = {
if ( this.viewport.degrees !== 0 ) {
this._restoreRotationChanges();
}
if (tiledImage.getRotation() !== 0) {
this._restoreRotationChanges();
}
context.restore();
},
@ -592,17 +601,22 @@ $.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);
},
var context = this._getContext(useSketch);
// private
_offsetForRotation: function(options) {
var point = options.point ?
options.point.times($.pixelDensityRatio) :
this.getCanvasCenter();
var context = this._getContext(options.useSketch);
context.save();
context.translate(cx, cy);
context.rotate(Math.PI / 180 * degrees);
context.translate(-cx, -cy);
context.translate(point.x, point.y);
context.rotate(Math.PI / 180 * options.degrees);
context.translate(-point.x, -point.y);
},
// private

View File

@ -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);
}
});

View File

@ -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

View File

@ -202,10 +202,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;

View File

@ -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,19 +439,21 @@ $.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);
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;

View File

@ -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
@ -160,7 +163,6 @@ $.TiledImage = function( options ) {
placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle,
opacity: $.DEFAULT_SETTINGS.opacity,
compositeOperation: $.DEFAULT_SETTINGS.compositeOperation
}, options );
this._fullyLoaded = false;
@ -290,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;
}
},
@ -303,17 +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) {
if (current) {
return new $.Rect( this._xSpring.current.value, this._ySpring.current.value,
this._worldWidthCurrent, this._worldHeightCurrent );
}
return this.getBoundsNoRotate(current)
.rotate(this._degrees, this._getRotationPoint(current));
},
return new $.Rect( this._xSpring.target.value, this._ySpring.target.value,
this._worldWidthTarget, this._worldHeightTarget );
/**
* 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
@ -329,9 +349,11 @@ $.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,
@ -339,7 +361,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
clip.width,
clip.height);
}
return bounds;
return bounds.rotate(this._degrees, this._getRotationPoint(current));
},
/**
@ -364,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
@ -396,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;
@ -413,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));
},
/**
@ -444,7 +469,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
coordA.y,
coordB.x,
coordB.y,
rect.degrees
rect.degrees + this._degrees
);
},
@ -476,7 +501,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
coordA.y,
coordB.x,
coordB.y,
rect.degrees
rect.degrees - this._degrees
);
},
@ -524,6 +549,20 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
OpenSeadragon.getElementPosition( this.viewer.element ));
},
// private
// 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));
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.
@ -688,6 +727,7 @@ $.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,
@ -700,6 +740,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');
},
/**
@ -717,6 +767,39 @@ $.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} degrees the rotation in degrees.
* @fires OpenSeadragon.TiledImage.event:bounds-change
*/
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).getCenter();
},
/**
* @returns {String} The TiledImage's current compositeOperation.
*/
@ -775,7 +858,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');
@ -784,187 +868,144 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
// private
_isBottomItem: function() {
return this.viewer.world.getItemAt(0) === this;
}
});
},
/**
* @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),
// 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( zeroRatioC / tiledImage.minPixelRatio ) /
Math.log( 2 )
Math.log(currentZeroRatio / this.minPixelRatio) / Math.log(2)
))
),
renderPixelRatioC,
renderPixelRatioT,
zeroRatioT,
optimalRatio,
levelOpacity,
levelVisibility;
);
// 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 (tiledImage.lastDrawn.length > 0) {
tile = tiledImage.lastDrawn.pop();
while (this.lastDrawn.length > 0) {
var tile = this.lastDrawn.pop();
tile.beingDrawn = false;
}
if (!tiledImage.wrapHorizontal && !tiledImage.wrapVertical) {
var tiledImageBounds = tiledImage.getClippedBounds(true);
var intersection = viewportBounds.intersection(tiledImageBounds);
if (intersection === null) {
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;
}
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 );
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
var drawLevel; // FIXME: drawLevel should have a more explanatory name
for ( level = highestLevel; level >= lowestLevel; level-- ) {
drawLevel = false;
for (var level = highestLevel; level >= lowestLevel; level--) {
var drawLevel = false;
//Avoid calculations for draw if we have already drawn this
renderPixelRatioC = tiledImage.viewport.deltaPixelsFromPointsNoRotate(
tiledImage.source.getPixelRatio( level ),
var currentRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate(
this.source.getPixelRatio(level),
true
).x * tiledImage._scaleSpring.current.value;
).x * this._scaleSpring.current.value;
if ( ( !haveDrawn && renderPixelRatioC >= tiledImage.minPixelRatio ) ||
( level == lowestLevel ) ) {
if (level === lowestLevel ||
(!haveDrawn && currentRenderPixelRatio >= this.minPixelRatio)) {
drawLevel = true;
haveDrawn = true;
} else if ( !haveDrawn ) {
} else if (!haveDrawn) {
continue;
}
//Perform calculations for draw if we haven't drawn this
renderPixelRatioT = tiledImage.viewport.deltaPixelsFromPointsNoRotate(
tiledImage.source.getPixelRatio( level ),
var targetRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate(
this.source.getPixelRatio(level),
false
).x * tiledImage._scaleSpring.current.value;
).x * this._scaleSpring.current.value;
zeroRatioT = tiledImage.viewport.deltaPixelsFromPointsNoRotate(
tiledImage.source.getPixelRatio(
var targetZeroRatio = viewport.deltaPixelsFromPointsNoRotate(
this.source.getPixelRatio(
Math.max(
tiledImage.source.getClosestLevel( tiledImage.viewport.containerSize ) - 1,
this.source.getClosestLevel(viewport.containerSize) - 1,
0
)
),
false
).x * tiledImage._scaleSpring.current.value;
).x * this._scaleSpring.current.value;
optimalRatio = tiledImage.immediateRender ?
1 :
zeroRatioT;
levelOpacity = Math.min( 1, ( renderPixelRatioC - 0.5 ) / 0.5 );
levelVisibility = optimalRatio / Math.abs(
optimalRatio - renderPixelRatioT
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
best = updateLevel(
tiledImage,
bestTile = updateLevel(
this,
haveDrawn,
drawLevel,
level,
levelOpacity,
levelVisibility,
viewportTL,
viewportBR,
drawArea,
currentTime,
best
bestTile
);
// Stop the loop if lower-res tiles would all be covered by
// already drawn tiles
if ( providesCoverage( tiledImage.coverage, level ) ) {
if (providesCoverage(this.coverage, level)) {
break;
}
}
// Perform the actual drawing
drawTiles( tiledImage, tiledImage.lastDrawn );
drawTiles(this, this.lastDrawn);
// Load the new 'best' tile
if (best && !best.context2D) {
loadTile( tiledImage, best, currentTime );
tiledImage._needsDraw = true;
tiledImage._setFullyLoaded(false);
if (bestTile && !bestTile.context2D) {
loadTile(this, bestTile, currentTime);
this._needsDraw = true;
this._setFullyLoaded(false);
} else {
tiledImage._setFullyLoaded(true);
this._setFullyLoaded(true);
}
}
}
});
function updateLevel(tiledImage, haveDrawn, drawLevel, level, levelOpacity,
levelVisibility, drawArea, currentTime, best) {
function updateLevel( tiledImage, haveDrawn, drawLevel, level, levelOpacity, levelVisibility, viewportTL, viewportBR, currentTime, best ){
var topLeftBound = drawArea.getBoundingBox().getTopLeft();
var bottomRightBound = drawArea.getBoundingBox().getBottomRight();
var x, y,
tileTL,
tileBR,
numberOfTiles,
viewportCenter = tiledImage.viewport.pixelFromPoint( tiledImage.viewport.getCenter() );
if( tiledImage.viewer ){
if (tiledImage.viewer) {
/**
* <em>- Needs documentation -</em>
*
@ -977,45 +1018,59 @@ 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.
*/
tiledImage.viewer.raiseEvent( 'update-level', {
tiledImage.viewer.raiseEvent('update-level', {
tiledImage: tiledImage,
havedrawn: haveDrawn,
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
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, topLeftBound);
var bottomRightTile = tiledImage.source.getTileAtPoint(level, bottomRightBound);
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)
} else {
tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 );
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 ) {
tileTL.y -= 1; // top invisible row (othervise we will have empty space after scroll at top)
} else {
tileBR.y = Math.min( tileBR.y, numberOfTiles.y - 1 );
if (!tiledImage.wrapVertical) {
// Adjust for floating point error
topLeftTile.y = Math.max(topLeftTile.y, 0);
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++) {
// 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(
tiledImage,
@ -1476,7 +1531,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
@ -1500,10 +1557,23 @@ 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({
degrees: tiledImage.viewport.degrees,
useSketch: useSketch
});
}
if (tiledImage._degrees !== 0) {
tiledImage._drawer._offsetForRotation({
degrees: tiledImage._degrees,
point: tiledImage.viewport.pixelFromPointNoRotate(
tiledImage._getRotationPoint(true), true),
useSketch: useSketch
});
}
}
var usedClip = false;
@ -1511,6 +1581,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);
@ -1571,14 +1642,31 @@ function drawTiles( tiledImage, lastDrawn ) {
tiledImage._drawer.restoreContext( useSketch );
}
if (tiledImage.viewport.degrees !== 0 && !sketchScale) {
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({
degrees: tiledImage.viewport.degrees,
useSketch: false
});
}
if (tiledImage._degrees !== 0) {
tiledImage._drawer._offsetForRotation({
degrees: tiledImage._degrees,
point: tiledImage.viewport.pixelFromPointNoRotate(
tiledImage._getRotationPoint(true), true),
useSketch: false
});
}
}
tiledImage._drawer.blendSketch({
opacity: tiledImage.opacity,
@ -1587,9 +1675,14 @@ function drawTiles( tiledImage, lastDrawn ) {
compositeOperation: tiledImage.compositeOperation,
bounds: bounds
});
if (offsetForRotation) {
if (sketchScale) {
if (tiledImage._degrees !== 0) {
tiledImage._drawer._restoreRotationChanges(false);
}
if (tiledImage.viewport.degrees !== 0) {
tiledImage._drawer._restoreRotationChanges(false);
}
}
}
drawDebugInfo( tiledImage, lastDrawn );
}
@ -1599,7 +1692,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);
}

View File

@ -344,12 +344,20 @@ $.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 = $.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);
},
/**

View File

@ -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
* 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.
@ -1369,6 +1371,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,

View File

@ -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());

View File

@ -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();
}
@ -383,7 +386,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 +396,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);

View File

@ -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);
@ -294,6 +315,34 @@
});
// ----------
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) {

View File

@ -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
});
});