Merge branch 'master' of github.com:openseadragon/openseadragon

This commit is contained in:
Ian Gilman 2016-10-26 09:59:35 -07:00
commit 21884afff7
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. // the viewport get rotated later on, we will need to resize it.
if (this.viewport.getRotation() === 0) { if (this.viewport.getRotation() === 0) {
var self = this; var self = this;
this.viewer.addHandler('rotate', function resizeSketchCanvas() { this.viewer.addOnceHandler('rotate', function resizeSketchCanvas() {
self.viewer.removeHandler('rotate', resizeSketchCanvas);
var sketchCanvasSize = self._calculateSketchCanvasSize(); var sketchCanvasSize = self._calculateSketchCanvasSize();
self.sketchCanvas.width = sketchCanvasSize.x; self.sketchCanvas.width = sketchCanvasSize.x;
self.sketchCanvas.height = sketchCanvasSize.y; self.sketchCanvas.height = sketchCanvasSize.y;
@ -482,7 +481,7 @@ $.Drawer.prototype = {
}, },
// private // private
drawDebugInfo: function( tile, count, i ){ drawDebugInfo: function(tile, count, i, tiledImage) {
if ( !this.useCanvas ) { if ( !this.useCanvas ) {
return; return;
} }
@ -495,7 +494,14 @@ $.Drawer.prototype = {
context.fillStyle = this.debugGridColor; context.fillStyle = this.debugGridColor;
if ( this.viewport.degrees !== 0 ) { 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( context.strokeRect(
@ -559,6 +565,9 @@ $.Drawer.prototype = {
if ( this.viewport.degrees !== 0 ) { if ( this.viewport.degrees !== 0 ) {
this._restoreRotationChanges(); this._restoreRotationChanges();
} }
if (tiledImage.getRotation() !== 0) {
this._restoreRotationChanges();
}
context.restore(); context.restore();
}, },
@ -592,17 +601,22 @@ $.Drawer.prototype = {
return new $.Point(canvas.width, canvas.height); return new $.Point(canvas.width, canvas.height);
}, },
// private getCanvasCenter: function() {
_offsetForRotation: function(degrees, useSketch) { return new $.Point(this.canvas.width / 2, this.canvas.height / 2);
var cx = this.canvas.width / 2; },
var cy = 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.save();
context.translate(cx, cy); context.translate(point.x, point.y);
context.rotate(Math.PI / 180 * degrees); context.rotate(Math.PI / 180 * options.degrees);
context.translate(-cx, -cy); context.translate(-point.x, -point.y);
}, },
// private // private

View File

@ -341,9 +341,12 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /*
myItem._originalForNavigator = original; myItem._originalForNavigator = original;
_this._matchBounds(myItem, original, true); _this._matchBounds(myItem, original, true);
original.addHandler('bounds-change', function() { function matchBounds() {
_this._matchBounds(myItem, original); _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); 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). * Determines if a point is within the bounding rectangle of the given element (hit-test).
* @function * @function

View File

@ -202,10 +202,7 @@ $.Point.prototype = {
var sin; var sin;
// Avoid float computations when possible // Avoid float computations when possible
if (degrees % 90 === 0) { if (degrees % 90 === 0) {
var d = degrees % 360; var d = $.positiveModulo(degrees, 360);
if (d < 0) {
d += 360;
}
switch (d) { switch (d) {
case 0: case 0:
cos = 1; cos = 1;

View File

@ -81,10 +81,7 @@ $.Rect = function(x, y, width, height, degrees) {
this.degrees = typeof(degrees) === "number" ? degrees : 0; this.degrees = typeof(degrees) === "number" ? degrees : 0;
// Normalizes the rectangle. // Normalizes the rectangle.
this.degrees = this.degrees % 360; this.degrees = $.positiveModulo(this.degrees, 360);
if (this.degrees < 0) {
this.degrees += 360;
}
var newTopLeft, newWidth; var newTopLeft, newWidth;
if (this.degrees >= 270) { if (this.degrees >= 270) {
newTopLeft = this.getTopRight(); newTopLeft = this.getTopRight();
@ -442,19 +439,21 @@ $.Rect.prototype = {
* @return {OpenSeadragon.Rect} * @return {OpenSeadragon.Rect}
*/ */
rotate: function(degrees, pivot) { rotate: function(degrees, pivot) {
degrees = degrees % 360; degrees = $.positiveModulo(degrees, 360);
if (degrees === 0) { if (degrees === 0) {
return this.clone(); return this.clone();
} }
if (degrees < 0) {
degrees += 360;
}
pivot = pivot || this.getCenter(); pivot = pivot || this.getCenter();
var newTopLeft = this.getTopLeft().rotate(degrees, pivot); var newTopLeft = this.getTopLeft().rotate(degrees, pivot);
var newTopRight = this.getTopRight().rotate(degrees, pivot); var newTopRight = this.getTopRight().rotate(degrees, pivot);
var diff = newTopRight.minus(newTopLeft); 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); var radians = Math.atan(diff.y / diff.x);
if (diff.x < 0) { if (diff.x < 0) {
radians += Math.PI; radians += Math.PI;

View File

@ -132,6 +132,9 @@ $.TiledImage = function( options ) {
var fitBoundsPlacement = options.fitBoundsPlacement || OpenSeadragon.Placement.CENTER; var fitBoundsPlacement = options.fitBoundsPlacement || OpenSeadragon.Placement.CENTER;
delete options.fitBoundsPlacement; delete options.fitBoundsPlacement;
this._degrees = $.positiveModulo(options.degrees || 0, 360);
delete options.degrees;
$.extend( true, this, { $.extend( true, this, {
//internal state properties //internal state properties
@ -160,7 +163,6 @@ $.TiledImage = function( options ) {
placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle,
opacity: $.DEFAULT_SETTINGS.opacity, opacity: $.DEFAULT_SETTINGS.opacity,
compositeOperation: $.DEFAULT_SETTINGS.compositeOperation compositeOperation: $.DEFAULT_SETTINGS.compositeOperation
}, options ); }, options );
this._fullyLoaded = false; this._fullyLoaded = false;
@ -290,7 +292,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
draw: function() { draw: function() {
if (this.opacity !== 0) { if (this.opacity !== 0) {
this._midDraw = true; this._midDraw = true;
updateViewport(this); this._updateViewport();
this._midDraw = false; 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. * @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) { getBounds: function(current) {
if (current) { return this.getBoundsNoRotate(current)
return new $.Rect( this._xSpring.current.value, this._ySpring.current.value, .rotate(this._degrees, this._getRotationPoint(current));
this._worldWidthCurrent, this._worldHeightCurrent ); },
}
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 // deprecated
@ -329,9 +349,11 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @returns {$.Rect} The clipped bounds in viewport coordinates. * @returns {$.Rect} The clipped bounds in viewport coordinates.
*/ */
getClippedBounds: function(current) { getClippedBounds: function(current) {
var bounds = this.getBounds(current); var bounds = this.getBoundsNoRotate(current);
if (this._clip) { 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); var clip = this._clip.times(ratio);
bounds = new $.Rect( bounds = new $.Rect(
bounds.x + clip.x, bounds.x + clip.x,
@ -339,7 +361,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
clip.width, clip.width,
clip.height); clip.height);
} }
return bounds; return bounds.rotate(this._degrees, this._getRotationPoint(current));
}, },
/** /**
@ -365,20 +387,23 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @return {OpenSeadragon.Point} A point representing the coordinates in the image. * @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) { if (viewerX instanceof $.Point) {
//they passed a point instead of individual components //they passed a point instead of individual components
current = viewerY; current = viewerY;
viewerY = viewerX.y; point = viewerX;
viewerX = viewerX.x; } else {
point = new $.Point(viewerX, viewerY);
} }
if (current) { point = point.rotate(-this._degrees, this._getRotationPoint(current));
return this._viewportToImageDelta(viewerX - this._xSpring.current.value, return current ?
viewerY - this._ySpring.current.value); this._viewportToImageDelta(
} point.x - this._xSpring.current.value,
point.y - this._ySpring.current.value) :
return this._viewportToImageDelta(viewerX - this._xSpring.target.value, this._viewportToImageDelta(
viewerY - this._ySpring.target.value); point.x - this._xSpring.target.value,
point.y - this._ySpring.target.value);
}, },
// private // private
@ -413,7 +438,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
point.y += this._ySpring.target.value; 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, coordA.y,
coordB.x, coordB.x,
coordB.y, coordB.y,
rect.degrees rect.degrees + this._degrees
); );
}, },
@ -476,7 +501,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
coordA.y, coordA.y,
coordB.x, coordB.x,
coordB.y, coordB.y,
rect.degrees rect.degrees - this._degrees
); );
}, },
@ -524,6 +549,20 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
OpenSeadragon.getElementPosition( this.viewer.element )); 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. * Convert a viewport zoom to an image zoom.
* Image zoom: ratio of the original image size to displayed image size. * 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 * @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 * (portions of the image outside of this area will not be visible). Only works on
* browsers that support the HTML5 canvas. * browsers that support the HTML5 canvas.
* @fires OpenSeadragon.TiledImage.event:clip-change
*/ */
setClip: function(newClip) { setClip: function(newClip) {
$.console.assert(!newClip || newClip instanceof $.Rect, $.console.assert(!newClip || newClip instanceof $.Rect,
@ -700,6 +740,16 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
} }
this._needsDraw = true; 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; 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. * @returns {String} The TiledImage's current compositeOperation.
*/ */
@ -775,7 +858,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @event bounds-change * @event bounds-change
* @memberOf OpenSeadragon.TiledImage * @memberOf OpenSeadragon.TiledImage
* @type {object} * @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. * @property {?Object} userData - Arbitrary subscriber-defined object.
*/ */
this.raiseEvent('bounds-change'); this.raiseEvent('bounds-change');
@ -784,110 +868,75 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
// private // private
_isBottomItem: function() { _isBottomItem: function() {
return this.viewer.world.getItemAt(0) === this; return this.viewer.world.getItemAt(0) === this;
} },
});
/** // private
* @private _getLevelsInterval: function() {
* @inner var lowestLevel = Math.max(
* Pretty much every other line in this needs to be documented so it's clear this.source.minLevel,
* how each piece of this routine contributes to the drawing process. That's Math.floor(Math.log(this.minZoomImageRatio) / Math.log(2))
* why there are so many TODO's inside this function. );
*/ var currentZeroRatio = this.viewport.deltaPixelsFromPointsNoRotate(
function updateViewport( tiledImage ) { this.source.getPixelRatio(0), true).x *
this._scaleSpring.current.value;
tiledImage._needsDraw = false; var highestLevel = Math.min(
Math.abs(this.source.maxLevel),
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.abs(Math.floor(
Math.log( zeroRatioC / tiledImage.minPixelRatio ) / Math.log(currentZeroRatio / this.minPixelRatio) / Math.log(2)
Math.log( 2 )
)) ))
), );
renderPixelRatioC,
renderPixelRatioT, // Calculations for the interval of levels to draw
zeroRatioT, // can return invalid intervals; fix that here if necessary
optimalRatio, lowestLevel = Math.min(lowestLevel, highestLevel);
levelOpacity, return {
levelVisibility; lowestLevel: lowestLevel,
highestLevel: highestLevel
};
},
// private
_updateViewport: function() {
this._needsDraw = false;
// Reset tile's internal drawn state // Reset tile's internal drawn state
while (tiledImage.lastDrawn.length > 0) { while (this.lastDrawn.length > 0) {
tile = tiledImage.lastDrawn.pop(); var tile = this.lastDrawn.pop();
tile.beingDrawn = false; tile.beingDrawn = false;
} }
if (!tiledImage.wrapHorizontal && !tiledImage.wrapVertical) { var viewport = this.viewport;
var tiledImageBounds = tiledImage.getClippedBounds(true); var drawArea = this._viewportToTiledImageRectangle(
var intersection = viewportBounds.intersection(tiledImageBounds); viewport.getBoundsWithMargins(true));
if (intersection === null) {
if (!this.wrapHorizontal && !this.wrapVertical) {
var tiledImageBounds = this._viewportToTiledImageRectangle(
this.getClippedBounds(true));
drawArea = drawArea.intersection(tiledImageBounds);
if (drawArea === null) {
return; 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 ) ) { var levelsInterval = this._getLevelsInterval();
return; var lowestLevel = levelsInterval.lowestLevel;
} var highestLevel = levelsInterval.highestLevel;
var bestTile = null;
// Calculate viewport rect / bounds var haveDrawn = false;
if ( !tiledImage.wrapHorizontal ) { var currentTime = $.now();
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 // Update any level that will be drawn
var drawLevel; // FIXME: drawLevel should have a more explanatory name for (var level = highestLevel; level >= lowestLevel; level--) {
for ( level = highestLevel; level >= lowestLevel; level-- ) { var drawLevel = false;
drawLevel = false;
//Avoid calculations for draw if we have already drawn this //Avoid calculations for draw if we have already drawn this
renderPixelRatioC = tiledImage.viewport.deltaPixelsFromPointsNoRotate( var currentRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate(
tiledImage.source.getPixelRatio( level ), this.source.getPixelRatio(level),
true true
).x * tiledImage._scaleSpring.current.value; ).x * this._scaleSpring.current.value;
if ( ( !haveDrawn && renderPixelRatioC >= tiledImage.minPixelRatio ) || if (level === lowestLevel ||
( level == lowestLevel ) ) { (!haveDrawn && currentRenderPixelRatio >= this.minPixelRatio)) {
drawLevel = true; drawLevel = true;
haveDrawn = true; haveDrawn = true;
} else if (!haveDrawn) { } else if (!haveDrawn) {
@ -895,74 +944,66 @@ function updateViewport( tiledImage ) {
} }
//Perform calculations for draw if we haven't drawn this //Perform calculations for draw if we haven't drawn this
renderPixelRatioT = tiledImage.viewport.deltaPixelsFromPointsNoRotate( var targetRenderPixelRatio = viewport.deltaPixelsFromPointsNoRotate(
tiledImage.source.getPixelRatio( level ), this.source.getPixelRatio(level),
false false
).x * tiledImage._scaleSpring.current.value; ).x * this._scaleSpring.current.value;
zeroRatioT = tiledImage.viewport.deltaPixelsFromPointsNoRotate( var targetZeroRatio = viewport.deltaPixelsFromPointsNoRotate(
tiledImage.source.getPixelRatio( this.source.getPixelRatio(
Math.max( Math.max(
tiledImage.source.getClosestLevel( tiledImage.viewport.containerSize ) - 1, this.source.getClosestLevel(viewport.containerSize) - 1,
0 0
) )
), ),
false false
).x * tiledImage._scaleSpring.current.value; ).x * this._scaleSpring.current.value;
optimalRatio = tiledImage.immediateRender ? var optimalRatio = this.immediateRender ? 1 : targetZeroRatio;
1 : var levelOpacity = Math.min(1, (currentRenderPixelRatio - 0.5) / 0.5);
zeroRatioT; var levelVisibility = optimalRatio / Math.abs(
optimalRatio - targetRenderPixelRatio
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 // Update the level and keep track of 'best' tile to load
best = updateLevel( bestTile = updateLevel(
tiledImage, this,
haveDrawn, haveDrawn,
drawLevel, drawLevel,
level, level,
levelOpacity, levelOpacity,
levelVisibility, levelVisibility,
viewportTL, drawArea,
viewportBR,
currentTime, currentTime,
best bestTile
); );
// Stop the loop if lower-res tiles would all be covered by // Stop the loop if lower-res tiles would all be covered by
// already drawn tiles // already drawn tiles
if ( providesCoverage( tiledImage.coverage, level ) ) { if (providesCoverage(this.coverage, level)) {
break; break;
} }
} }
// Perform the actual drawing // Perform the actual drawing
drawTiles( tiledImage, tiledImage.lastDrawn ); drawTiles(this, this.lastDrawn);
// Load the new 'best' tile // Load the new 'best' tile
if (best && !best.context2D) { if (bestTile && !bestTile.context2D) {
loadTile( tiledImage, best, currentTime ); loadTile(this, bestTile, currentTime);
tiledImage._needsDraw = true; this._needsDraw = true;
tiledImage._setFullyLoaded(false); this._setFullyLoaded(false);
} else { } 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) {
/** /**
@ -977,8 +1018,9 @@ function updateLevel( tiledImage, haveDrawn, drawLevel, level, levelOpacity, lev
* @property {Object} level * @property {Object} level
* @property {Object} opacity * @property {Object} opacity
* @property {Object} visibility * @property {Object} visibility
* @property {Object} topleft * @property {OpenSeadragon.Rect} drawArea
* @property {Object} bottomright * @property {Object} topleft deprecated, use drawArea instead
* @property {Object} bottomright deprecated, use drawArea instead
* @property {Object} currenttime * @property {Object} currenttime
* @property {Object} best * @property {Object} best
* @property {?Object} userData - Arbitrary subscriber-defined object. * @property {?Object} userData - Arbitrary subscriber-defined object.
@ -989,33 +1031,46 @@ function updateLevel( tiledImage, haveDrawn, drawLevel, level, levelOpacity, lev
level: level, level: level,
opacity: levelOpacity, opacity: levelOpacity,
visibility: levelVisibility, visibility: levelVisibility,
topleft: viewportTL, drawArea: drawArea,
bottomright: viewportBR, topleft: topLeftBound,
bottomright: bottomRightBound,
currenttime: currentTime, currenttime: currentTime,
best: best best: best
}); });
} }
//OK, a new drawing so do your calculations //OK, a new drawing so do your calculations
tileTL = tiledImage.source.getTileAtPoint( level, viewportTL.divide( tiledImage._scaleSpring.current.value )); var topLeftTile = tiledImage.source.getTileAtPoint(level, topLeftBound);
tileBR = tiledImage.source.getTileAtPoint( level, viewportBR.divide( tiledImage._scaleSpring.current.value )); var bottomRightTile = tiledImage.source.getTileAtPoint(level, bottomRightBound);
numberOfTiles = tiledImage.source.getNumTiles( level ); var numberOfTiles = tiledImage.source.getNumTiles(level);
resetCoverage(tiledImage.coverage, level); resetCoverage(tiledImage.coverage, level);
if ( tiledImage.wrapHorizontal ) { if (!tiledImage.wrapHorizontal) {
tileTL.x -= 1; // left invisible column (othervise we will have empty space after scroll at left) // Adjust for floating point error
} else { topLeftTile.x = Math.max(topLeftTile.x, 0);
tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 ); bottomRightTile.x = Math.min(bottomRightTile.x, numberOfTiles.x - 1);
} }
if ( tiledImage.wrapVertical ) { if (!tiledImage.wrapVertical) {
tileTL.y -= 1; // top invisible row (othervise we will have empty space after scroll at top) // Adjust for floating point error
} else { topLeftTile.y = Math.max(topLeftTile.y, 0);
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++ ) { var viewportCenter = tiledImage.viewport.pixelFromPoint(
for ( y = tileTL.y; y <= tileBR.y; y++ ) { 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( best = updateTile(
tiledImage, tiledImage,
@ -1476,7 +1531,9 @@ function drawTiles( tiledImage, lastDrawn ) {
var zoom = tiledImage.viewport.getZoom(true); var zoom = tiledImage.viewport.getZoom(true);
var imageZoom = tiledImage.viewportToImageZoom(zoom); 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. // When zoomed in a lot (>100%) the tile edges are visible.
// So we have to composite them at ~100% and scale them up together. // 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 // 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); tiledImage._drawer._clear(true, bounds);
} }
// When scaling, we must rotate only when blending the sketch canvas to avoid // When scaling, we must rotate only when blending the sketch canvas to
// interpolation // avoid interpolation
if (tiledImage.viewport.degrees !== 0 && !sketchScale) { if (!sketchScale) {
tiledImage._drawer._offsetForRotation(tiledImage.viewport.degrees, useSketch); 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; var usedClip = false;
@ -1511,6 +1581,7 @@ function drawTiles( tiledImage, lastDrawn ) {
tiledImage._drawer.saveContext(useSketch); tiledImage._drawer.saveContext(useSketch);
var box = tiledImage.imageToViewportRectangle(tiledImage._clip, true); var box = tiledImage.imageToViewportRectangle(tiledImage._clip, true);
box = box.rotate(-tiledImage._degrees, tiledImage._getRotationPoint());
var clipRect = tiledImage._drawer.viewportToDrawerRectangle(box); var clipRect = tiledImage._drawer.viewportToDrawerRectangle(box);
if (sketchScale) { if (sketchScale) {
clipRect = clipRect.times(sketchScale); clipRect = clipRect.times(sketchScale);
@ -1571,14 +1642,31 @@ function drawTiles( tiledImage, lastDrawn ) {
tiledImage._drawer.restoreContext( useSketch ); tiledImage._drawer.restoreContext( useSketch );
} }
if (tiledImage.viewport.degrees !== 0 && !sketchScale) { if (!sketchScale) {
if (tiledImage._degrees !== 0) {
tiledImage._drawer._restoreRotationChanges(useSketch); tiledImage._drawer._restoreRotationChanges(useSketch);
} }
if (tiledImage.viewport.degrees !== 0) {
tiledImage._drawer._restoreRotationChanges(useSketch);
}
}
if (useSketch) { if (useSketch) {
var offsetForRotation = tiledImage.viewport.degrees !== 0 && sketchScale; if (sketchScale) {
if (offsetForRotation) { 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({
degrees: tiledImage._degrees,
point: tiledImage.viewport.pixelFromPointNoRotate(
tiledImage._getRotationPoint(true), true),
useSketch: false
});
}
} }
tiledImage._drawer.blendSketch({ tiledImage._drawer.blendSketch({
opacity: tiledImage.opacity, opacity: tiledImage.opacity,
@ -1587,9 +1675,14 @@ function drawTiles( tiledImage, lastDrawn ) {
compositeOperation: tiledImage.compositeOperation, compositeOperation: tiledImage.compositeOperation,
bounds: bounds bounds: bounds
}); });
if (offsetForRotation) { if (sketchScale) {
if (tiledImage._degrees !== 0) {
tiledImage._drawer._restoreRotationChanges(false); tiledImage._drawer._restoreRotationChanges(false);
} }
if (tiledImage.viewport.degrees !== 0) {
tiledImage._drawer._restoreRotationChanges(false);
}
}
} }
drawDebugInfo( tiledImage, lastDrawn ); drawDebugInfo( tiledImage, lastDrawn );
} }
@ -1599,7 +1692,8 @@ function drawDebugInfo( tiledImage, lastDrawn ) {
for ( var i = lastDrawn.length - 1; i >= 0; i-- ) { for ( var i = lastDrawn.length - 1; i >= 0; i-- ) {
var tile = lastDrawn[ i ]; var tile = lastDrawn[ i ];
try { try {
tiledImage._drawer.drawDebugInfo( tile, lastDrawn.length, i ); tiledImage._drawer.drawDebugInfo(
tile, lastDrawn.length, i, tiledImage);
} catch(e) { } catch(e) {
$.console.error(e); $.console.error(e);
} }

View File

@ -345,11 +345,19 @@ $.TileSource.prototype = {
* @param {OpenSeadragon.Point} point * @param {OpenSeadragon.Point} point
*/ */
getTileAtPoint: function(level, point) { 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); var numTiles = this.getNumTiles(level);
return new $.Point( x += numTiles.x * Math.floor(point.x);
Math.floor( (point.x * numTiles.x) / 1 ), y += numTiles.y * Math.floor(point.y * this.aspectRatio);
Math.floor( (point.y * numTiles.y * this.dimensions.x) / this.dimensions.y )
); 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 * (portions of the image outside of this area will not be visible). Only works on
* browsers that support the HTML5 canvas. * browsers that support the HTML5 canvas.
* @param {Number} [options.opacity] Opacity the tiled image should be drawn at by default. * @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.compositeOperation] How the image is composited onto other images.
* @param {String} [options.crossOriginPolicy] The crossOriginPolicy for this specific image, * @param {String} [options.crossOriginPolicy] The crossOriginPolicy for this specific image,
* overriding viewer.crossOriginPolicy. * overriding viewer.crossOriginPolicy.
@ -1369,6 +1371,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
clip: queueItem.options.clip, clip: queueItem.options.clip,
placeholderFillStyle: queueItem.options.placeholderFillStyle, placeholderFillStyle: queueItem.options.placeholderFillStyle,
opacity: queueItem.options.opacity, opacity: queueItem.options.opacity,
degrees: queueItem.options.degrees,
compositeOperation: queueItem.options.compositeOperation, compositeOperation: queueItem.options.compositeOperation,
springStiffness: _this.springStiffness, springStiffness: _this.springStiffness,
animationTime: _this.animationTime, animationTime: _this.animationTime,

View File

@ -852,11 +852,7 @@ $.Viewport.prototype = {
return this; return this;
} }
degrees = degrees % 360; this.degrees = $.positiveModulo(degrees, 360);
if (degrees < 0) {
degrees += 360;
}
this.degrees = degrees;
this._setContentBounds( this._setContentBounds(
this.viewer.world.getHomeBounds(), this.viewer.world.getHomeBounds(),
this.viewer.world.getContentFactor()); this.viewer.world.getContentFactor());

View File

@ -94,6 +94,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
this._needsDraw = true; this._needsDraw = true;
item.addHandler('bounds-change', this._delegatedFigureSizes); item.addHandler('bounds-change', this._delegatedFigureSizes);
item.addHandler('clip-change', this._delegatedFigureSizes);
/** /**
* Raised when an item is added to the World. * 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('bounds-change', this._delegatedFigureSizes);
item.removeHandler('clip-change', this._delegatedFigureSizes);
item.destroy(); item.destroy();
this._items.splice( index, 1 ); this._items.splice( index, 1 );
this._figureSizes(); this._figureSizes();
@ -213,6 +215,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
for (var i = 0; i < this._items.length; i++) { for (var i = 0; i < this._items.length; i++) {
item = this._items[i]; item = this._items[i];
item.removeHandler('bounds-change', this._delegatedFigureSizes); item.removeHandler('bounds-change', this._delegatedFigureSizes);
item.removeHandler('clip-change', this._delegatedFigureSizes);
item.destroy(); item.destroy();
} }
@ -383,7 +386,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
var item = this._items[0]; var item = this._items[0];
var bounds = item.getBounds(); var bounds = item.getBounds();
this._contentFactor = item.getContentSize().x / bounds.width; this._contentFactor = item.getContentSize().x / bounds.width;
var clippedBounds = item.getClippedBounds(); var clippedBounds = item.getClippedBounds().getBoundingBox();
var left = clippedBounds.x; var left = clippedBounds.x;
var top = clippedBounds.y; var top = clippedBounds.y;
var right = clippedBounds.x + clippedBounds.width; var right = clippedBounds.x + clippedBounds.width;
@ -393,7 +396,7 @@ $.extend( $.World.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.W
bounds = item.getBounds(); bounds = item.getBounds();
this._contentFactor = Math.max(this._contentFactor, this._contentFactor = Math.max(this._contentFactor,
item.getContentSize().x / bounds.width); item.getContentSize().x / bounds.width);
clippedBounds = item.getClippedBounds(); clippedBounds = item.getClippedBounds().getBoundingBox();
left = Math.min(left, clippedBounds.x); left = Math.min(left, clippedBounds.x);
top = Math.min(top, clippedBounds.y); top = Math.min(top, clippedBounds.y);
right = Math.max(right, clippedBounds.x + clippedBounds.width); 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() { asyncTest('getClipBounds', function() {
var clip = new OpenSeadragon.Rect(100, 200, 800, 500); 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() { asyncTest('fitBounds', function() {
function assertRectEquals(actual, expected, message) { function assertRectEquals(actual, expected, message) {

View File

@ -105,7 +105,8 @@
orig = config.getOrig(config.testArray[i], viewport); orig = config.getOrig(config.testArray[i], viewport);
expected = config.getExpected(orig, viewport); expected = config.getExpected(orig, viewport);
actual = viewport[config.method](orig); actual = viewport[config.method](orig);
propEqual( var assert = config.assert || propEqual;
assert(
actual, actual,
expected, expected,
"Correctly converted coordinates " + orig "Correctly converted coordinates " + orig
@ -118,6 +119,10 @@
viewer.open(DZI_PATH); viewer.open(DZI_PATH);
}; };
function assertPointsEquals(actual, expected, message) {
Util.assertPointsEquals(actual, expected, 1e-15, message);
}
// Tests start here. // Tests start here.
asyncTest('getContainerSize', function() { asyncTest('getContainerSize', function() {
@ -872,7 +877,8 @@
getExpected: function(orig, viewport) { getExpected: function(orig, viewport) {
return orig.divide(viewer.source.dimensions.x); return orig.divide(viewer.source.dimensions.x);
}, },
method: 'imageToViewportCoordinates' method: 'imageToViewportCoordinates',
assert: assertPointsEquals
}); });
}); });
@ -885,7 +891,8 @@
getExpected: function(orig, viewport) { getExpected: function(orig, viewport) {
return orig.divide(ZOOM_FACTOR * viewport.getContainerSize().x); return orig.divide(ZOOM_FACTOR * viewport.getContainerSize().x);
}, },
method: 'imageToViewportCoordinates' method: 'imageToViewportCoordinates',
assert: assertPointsEquals
}); });
}); });
asyncTest('imageToViewportRectangle', function() { asyncTest('imageToViewportRectangle', function() {
@ -902,7 +909,8 @@
orig.height / viewer.source.dimensions.x orig.height / viewer.source.dimensions.x
); );
}, },
method: 'imageToViewportRectangle' method: 'imageToViewportRectangle',
assert: assertPointsEquals
}); });
}); });