From 352bfbc3a513476b4f12696f29c473d754fb4ccb Mon Sep 17 00:00:00 2001 From: Antoine Vandecreme Date: Fri, 13 May 2016 15:18:37 -0400 Subject: [PATCH] Avoid loading clipped out tiles. Fix #889. --- src/rectangle.js | 163 +++++++++++++++++++++++++++++++++++++- src/tiledimage.js | 19 +++-- test/modules/rectangle.js | 82 +++++++++++++++++++ 3 files changed, 255 insertions(+), 9 deletions(-) diff --git a/src/rectangle.js b/src/rectangle.js index 6223c12c..3f04afc6 100644 --- a/src/rectangle.js +++ b/src/rectangle.js @@ -307,6 +307,132 @@ $.Rect.prototype = { bottom - top); }, + /** + * Returns the bounding box of the intersection of this rectangle with the + * given rectangle. + * @param {OpenSeadragon.Rect} rect + * @return {OpenSeadragon.Rect} the bounding box of the intersection + * or null if the rectangles don't intersect. + */ + intersection: function(rect) { + // Simplified version of Weiler Atherton clipping algorithm + // https://en.wikipedia.org/wiki/Weiler%E2%80%93Atherton_clipping_algorithm + // Because we just want the bounding box of the intersection, + // we can just compute the bounding box of: + // 1. all the summits of this which are inside rect + // 2. all the summits of rect which are inside this + // 3. all the intersections of rect and this + var EPSILON = 0.0000000001; + + var intersectionPoints = []; + + var thisTopLeft = this.getTopLeft(); + if (rect.containsPoint(thisTopLeft, EPSILON)) { + intersectionPoints.push(thisTopLeft); + } + var thisTopRight = this.getTopRight(); + if (rect.containsPoint(thisTopRight, EPSILON)) { + intersectionPoints.push(thisTopRight); + } + var thisBottomLeft = this.getBottomLeft(); + if (rect.containsPoint(thisBottomLeft, EPSILON)) { + intersectionPoints.push(thisBottomLeft); + } + var thisBottomRight = this.getBottomRight(); + if (rect.containsPoint(thisBottomRight, EPSILON)) { + intersectionPoints.push(thisBottomRight); + } + + var rectTopLeft = rect.getTopLeft(); + if (this.containsPoint(rectTopLeft, EPSILON)) { + intersectionPoints.push(rectTopLeft); + } + var rectTopRight = rect.getTopRight(); + if (this.containsPoint(rectTopRight, EPSILON)) { + intersectionPoints.push(rectTopRight); + } + var rectBottomLeft = rect.getBottomLeft(); + if (this.containsPoint(rectBottomLeft, EPSILON)) { + intersectionPoints.push(rectBottomLeft); + } + var rectBottomRight = rect.getBottomRight(); + if (this.containsPoint(rectBottomRight, EPSILON)) { + intersectionPoints.push(rectBottomRight); + } + + var thisSegments = this._getSegments(); + var rectSegments = rect._getSegments(); + for (var i = 0; i < thisSegments.length; i++) { + var thisSegment = thisSegments[i]; + for (var j = 0; j < rectSegments.length; j++) { + var rectSegment = rectSegments[j]; + var point = getIntersection(thisSegment[0], thisSegment[1], + rectSegment[0], rectSegment[1]); + if (point) { + intersectionPoints.push(point); + } + } + } + + // Get intersection point of segments [a,b] and [c,d] + function getIntersection(a, b, c, d) { + // http://stackoverflow.com/a/1968345/1440403 + var abVector = b.minus(a); + var cdVector = d.minus(c); + + var denom = -cdVector.x * abVector.y + abVector.x * cdVector.y; + if (denom === 0) { + return null; + } + + var s = (abVector.x * (a.y - c.y) - abVector.y * (a.x - c.x)) / denom; + var t = (cdVector.x * (a.y - c.y) - cdVector.y * (a.x - c.x)) / denom; + + if (-EPSILON <= s && s <= 1 - EPSILON && + -EPSILON <= t && t <= 1 - EPSILON) { + return new $.Point(a.x + t * abVector.x, a.y + t * abVector.y); + } + return null; + } + + if (intersectionPoints.length === 0) { + return null; + } + + var minX = intersectionPoints[0].x; + var maxX = intersectionPoints[0].x; + var minY = intersectionPoints[0].y; + var maxY = intersectionPoints[0].y; + for (var i = 1; i < intersectionPoints.length; i++) { + var point = intersectionPoints[i]; + if (point.x < minX) { + minX = point.x; + } + if (point.x > maxX) { + maxX = point.x; + } + if (point.y < minY) { + minY = point.y; + } + if (point.y > maxY) { + maxY = point.y; + } + } + return new $.Rect(minX, minY, maxX - minX, maxY - minY); + }, + + // private + _getSegments: function() { + var topLeft = this.getTopLeft(); + var topRight = this.getTopRight(); + var bottomLeft = this.getBottomLeft(); + var bottomRight = this.getBottomRight(); + return [[topLeft, topRight], + [topRight, bottomRight], + [bottomRight, bottomLeft], + [bottomLeft, topLeft]]; + }, + /** * Rotates a rectangle around a point. * @function @@ -381,6 +507,37 @@ $.Rect.prototype = { return new $.Rect(x, y, width, height); }, + /** + * Determines whether a point is inside this rectangle (edge included). + * @function + * @param {OpenSeadragon.Point} point + * @param {Number} [epsilon=0] the margin of error allowed + * @returns {Boolean} true if the point is inside this rectangle, false + * otherwise. + */ + containsPoint: function(point, epsilon) { + epsilon = epsilon || 0; + + // See http://stackoverflow.com/a/2752754/1440403 for explanation + var topLeft = this.getTopLeft(); + var topRight = this.getTopRight(); + var bottomLeft = this.getBottomLeft(); + var topDiff = topRight.minus(topLeft); + var leftDiff = bottomLeft.minus(topLeft); + + return ((point.x - topLeft.x) * topDiff.x + + (point.y - topLeft.y) * topDiff.y >= -epsilon) && + + ((point.x - topRight.x) * topDiff.x + + (point.y - topRight.y) * topDiff.y <= epsilon) && + + ((point.x - topLeft.x) * leftDiff.x + + (point.y - topLeft.y) * leftDiff.y >= -epsilon) && + + ((point.x - bottomLeft.x) * leftDiff.x + + (point.y - bottomLeft.y) * leftDiff.y <= epsilon); + }, + /** * Provides a string representation of the rectangle which is useful for * debugging. @@ -389,10 +546,10 @@ $.Rect.prototype = { */ toString: function() { return "[" + - (Math.round(this.x * 100) / 100) + "," + - (Math.round(this.y * 100) / 100) + "," + + (Math.round(this.x * 100) / 100) + ", " + + (Math.round(this.y * 100) / 100) + ", " + (Math.round(this.width * 100) / 100) + "x" + - (Math.round(this.height * 100) / 100) + "," + + (Math.round(this.height * 100) / 100) + ", " + (Math.round(this.degrees * 100) / 100) + "deg" + "]"; } diff --git a/src/tiledimage.js b/src/tiledimage.js index b2b33efa..91f76c51 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -787,7 +787,6 @@ function updateViewport( tiledImage ) { Math.log( 2 ) )) ), - degrees = tiledImage.viewport.degrees, renderPixelRatioC, renderPixelRatioT, zeroRatioT, @@ -795,16 +794,24 @@ function updateViewport( tiledImage ) { levelOpacity, levelVisibility; - viewportBounds = viewportBounds.getBoundingBox(); - viewportBounds.x -= tiledImage._xSpring.current.value; - viewportBounds.y -= tiledImage._ySpring.current.value; - // Reset tile's internal drawn state - while ( tiledImage.lastDrawn.length > 0 ) { + while (tiledImage.lastDrawn.length > 0) { tile = tiledImage.lastDrawn.pop(); tile.beingDrawn = false; } + if (!tiledImage.wrapHorizontal && !tiledImage.wrapVertical) { + var tiledImageBounds = tiledImage.getClippedBounds(true); + var intersection = viewportBounds.intersection(tiledImageBounds); + if (intersection === null) { + return; + } + viewportBounds = intersection; + } + viewportBounds = viewportBounds.getBoundingBox(); + viewportBounds.x -= tiledImage._xSpring.current.value; + viewportBounds.y -= tiledImage._ySpring.current.value; + var viewportTL = viewportBounds.getTopLeft(); var viewportBR = viewportBounds.getBottomRight(); diff --git a/test/modules/rectangle.js b/test/modules/rectangle.js index 7e905f58..402e58c4 100644 --- a/test/modules/rectangle.js +++ b/test/modules/rectangle.js @@ -161,6 +161,65 @@ "Incorrect union with non horizontal rectangles."); }); + test('intersection', function() { + var rect1 = new OpenSeadragon.Rect(2, 2, 2, 3); + var rect2 = new OpenSeadragon.Rect(0, 1, 1, 1); + var expected = null; + var actual = rect1.intersection(rect2); + equal(expected, actual, + "Rectangle " + rect2 + " should not intersect " + rect1); + actual = rect2.intersection(rect1); + equal(expected, actual, + "Rectangle " + rect1 + " should not intersect " + rect2); + + rect1 = new OpenSeadragon.Rect(0, 0, 2, 1); + rect2 = new OpenSeadragon.Rect(1, 0, 2, 2); + expected = new OpenSeadragon.Rect(1, 0, 1, 1); + actual = rect1.intersection(rect2); + Util.assertRectangleEquals(expected, actual, precision, + "Intersection of " + rect2 + " with " + rect1 + " should be " + + expected); + actual = rect2.intersection(rect1); + Util.assertRectangleEquals(expected, actual, precision, + "Intersection of " + rect1 + " with " + rect2 + " should be " + + expected); + + rect1 = new OpenSeadragon.Rect(0, 0, 3, 3); + rect2 = new OpenSeadragon.Rect(1, 1, 1, 1); + expected = new OpenSeadragon.Rect(1, 1, 1, 1); + actual = rect1.intersection(rect2); + Util.assertRectangleEquals(expected, actual, precision, + "Intersection of " + rect2 + " with " + rect1 + " should be " + + expected); + actual = rect2.intersection(rect1); + Util.assertRectangleEquals(expected, actual, precision, + "Intersection of " + rect1 + " with " + rect2 + " should be " + + expected); + + + rect1 = new OpenSeadragon.Rect(2, 2, 2, 3, 45); + rect2 = new OpenSeadragon.Rect(0, 1, 1, 1); + expected = null; + actual = rect1.intersection(rect2); + equal(expected, actual, + "Rectangle " + rect2 + " should not intersect " + rect1); + actual = rect2.intersection(rect1); + equal(expected, actual, + "Rectangle " + rect1 + " should not intersect " + rect2); + + rect1 = new OpenSeadragon.Rect(2, 0, 2, 3, 45); + rect2 = new OpenSeadragon.Rect(0, 1, 1, 1); + expected = new OpenSeadragon.Rect(0, 1, 1, 1); + actual = rect1.intersection(rect2); + Util.assertRectangleEquals(expected, actual, precision, + "Intersection of " + rect2 + " with " + rect1 + " should be " + + expected); + actual = rect2.intersection(rect1); + Util.assertRectangleEquals(expected, actual, precision, + "Intersection of " + rect1 + " with " + rect2 + " should be " + + expected); + }); + test('rotate', function() { var rect = new OpenSeadragon.Rect(0, 0, 2, 1); @@ -218,4 +277,27 @@ "Bounding box of rect rotated 270deg."); }); + test('containsPoint', function() { + var rect = new OpenSeadragon.Rect(0, 0, 1, 1, 45); + + ok(rect.containsPoint(new OpenSeadragon.Point(0, 0)), + 'Point 0,0 should be inside ' + rect); + ok(rect.containsPoint(rect.getTopRight()), + 'Top right vertex should be inside ' + rect); + ok(rect.containsPoint(rect.getBottomRight()), + 'Bottom right vertex should be inside ' + rect); + ok(rect.containsPoint(rect.getBottomLeft()), + 'Bottom left vertex should be inside ' + rect); + ok(rect.containsPoint(rect.getCenter()), + 'Center should be inside ' + rect); + notOk(rect.containsPoint(new OpenSeadragon.Point(1, 0)), + 'Point 1,0 should not be inside ' + rect); + ok(rect.containsPoint(new OpenSeadragon.Point(0.5, 0.5)), + 'Point 0.5,0.5 should be inside ' + rect); + ok(rect.containsPoint(new OpenSeadragon.Point(0.4, 0.5)), + 'Point 0.4,0.5 should be inside ' + rect); + notOk(rect.containsPoint(new OpenSeadragon.Point(0.6, 0.5)), + 'Point 0.6,0.5 should not be inside ' + rect); + }); + })();