diff --git a/changelog.txt b/changelog.txt index 551c5bf2..831ec9c8 100644 --- a/changelog.txt +++ b/changelog.txt @@ -3,9 +3,16 @@ OPENSEADRAGON CHANGELOG 1.0.1: (in progress) +* BREAKING CHANGE: the openseadragon-canvas element now has two child divs. This means: (#298) + * The drawer element is no longer accessible via viewer.canvas.firstChild but via viewer.drawersContainer.firstChild or viewer.drawer.canvas. + * The overlays elements are no longer accessible via viewer.canvas.childNodes but via viewer.overlaysContainer.childNodes or viewer.currentOverlays[i].element. +* DEPRECATION: overlay functions have been moved from Drawer to Viewer (#331) +* Added layers support. Multiple images can now been displayed on top of each other with transparency via the Viewer.addLayer method (#298) +* Improved overlay functions (#331) * Fixed: Nav button highlight states aren't quite aligned on Firefox (#303) * Added ControlAnchor options for default controls (#304) * Enabled basic cross-domain tile loading without tainting canvas (works in Chrome and Firefox) (#308) +* Added crossOriginPolicy drawer configuration to enable or disable CORS image requests (#364) * Added a ControlAnchor.ABSOLUTE enumeration. Enables absolute positioning of control elements in the viewer (#310) * Added a 'navigator-scroll' event to Navigator. Fired when mousewheel/pinch events occur in the navigator (#310) * Added a navigatorMaintainSizeRatio option. If set to true, the navigator minimap resizes when the viewer element is resized (#310) diff --git a/src/drawer.js b/src/drawer.js index 2e100384..5856aa93 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -87,10 +87,10 @@ $.Drawer = function( options ) { //internal state / configurable settings - overlays: [], // An unordered list of Overlays added. - collectionOverlays: {}, + collectionOverlays: {}, // For collection mode. Here an overlay is actually a viewer. //configurable settings + opacity: $.DEFAULT_SETTINGS.opacity, maxImageCacheCount: $.DEFAULT_SETTINGS.maxImageCacheCount, imageLoaderLimit: $.DEFAULT_SETTINGS.imageLoaderLimit, minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, @@ -101,7 +101,8 @@ $.Drawer = function( options ) { alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend, minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio, debugMode: $.DEFAULT_SETTINGS.debugMode, - timeout: $.DEFAULT_SETTINGS.timeout + timeout: $.DEFAULT_SETTINGS.timeout, + crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy }, options ); @@ -143,23 +144,12 @@ $.Drawer = function( options ) { this.canvas.style.width = "100%"; this.canvas.style.height = "100%"; this.canvas.style.position = "absolute"; + $.setElementOpacity( this.canvas, this.opacity, true ); // explicit left-align this.container.style.textAlign = "left"; this.container.appendChild( this.canvas ); - //create the correct type of overlay by convention if the overlays - //are not already OpenSeadragon.Overlays - for( i = 0; i < this.overlays.length; i++ ){ - if( $.isPlainObject( this.overlays[ i ] ) ){ - - this.overlays[ i ] = addOverlayFromConfiguration( this, this.overlays[ i ]); - - } else if ( $.isFunction( this.overlays[ i ] ) ){ - //TODO - } - } - //this.profiler = new $.Profiler(); }; @@ -177,57 +167,15 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ * @param {OpenSeadragon.OverlayPlacement} placement - The position of the * viewport which the location coordinates will be treated as relative * to. - * @param {function} onDraw - If supplied the callback is called when the overlay + * @param {function} onDraw - If supplied the callback is called when the overlay * needs to be drawn. It it the responsibility of the callback to do any drawing/positioning. * It is passed position, size and element. * @fires OpenSeadragon.Viewer.event:add-overlay + * @deprecated - use {@link OpenSeadragon.Viewer#addOverlay} instead. */ addOverlay: function( element, location, placement, onDraw ) { - var options; - if( $.isPlainObject( element ) ){ - options = element; - } else { - options = { - element: element, - location: location, - placement: placement, - onDraw: onDraw - }; - } - - element = $.getElement(options.element); - - if ( getOverlayIndex( this.overlays, element ) >= 0 ) { - // they're trying to add a duplicate overlay - return; - } - - this.overlays.push( new $.Overlay({ - element: element, - location: options.location, - placement: options.placement, - onDraw: options.onDraw - }) ); - this.updateAgain = true; - if( this.viewer ){ - /** - * Raised when an overlay is added to the viewer (see {@link OpenSeadragon.Drawer#addOverlay}). - * - * @event add-overlay - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {Element} element - The overlay element. - * @property {OpenSeadragon.Point|OpenSeadragon.Rect} location - * @property {OpenSeadragon.OverlayPlacement} placement - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - this.viewer.raiseEvent( 'add-overlay', { - element: element, - location: options.location, - placement: options.placement - }); - } + $.console.error("drawer.addOverlay is deprecated. Use viewer.addOverlay instead."); + this.viewer.addOverlay( element, location, placement, onDraw ); return this; }, @@ -242,36 +190,11 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ * to. * @return {OpenSeadragon.Drawer} Chainable. * @fires OpenSeadragon.Viewer.event:update-overlay + * @deprecated - use {@link OpenSeadragon.Viewer#updateOverlay} instead. */ updateOverlay: function( element, location, placement ) { - var i; - - element = $.getElement( element ); - i = getOverlayIndex( this.overlays, element ); - - if ( i >= 0 ) { - this.overlays[ i ].update( location, placement ); - this.updateAgain = true; - } - if( this.viewer ){ - /** - * Raised when an overlay's location or placement changes (see {@link OpenSeadragon.Drawer#updateOverlay}). - * - * @event update-overlay - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {Element} element - * @property {OpenSeadragon.Point|OpenSeadragon.Rect} location - * @property {OpenSeadragon.OverlayPlacement} placement - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - this.viewer.raiseEvent( 'update-overlay', { - element: element, - location: location, - placement: placement - }); - } + $.console.error("drawer.updateOverlay is deprecated. Use viewer.updateOverlay instead."); + this.viewer.updateOverlay( element, location, placement ); return this; }, @@ -283,33 +206,11 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ * element id which represent the ovelay content to be removed. * @return {OpenSeadragon.Drawer} Chainable. * @fires OpenSeadragon.Viewer.event:remove-overlay + * @deprecated - use {@link OpenSeadragon.Viewer#removeOverlay} instead. */ removeOverlay: function( element ) { - var i; - - element = $.getElement( element ); - i = getOverlayIndex( this.overlays, element ); - - if ( i >= 0 ) { - this.overlays[ i ].destroy(); - this.overlays.splice( i, 1 ); - this.updateAgain = true; - } - if( this.viewer ){ - /** - * Raised when an overlay is removed from the viewer (see {@link OpenSeadragon.Drawer#removeOverlay}). - * - * @event remove-overlay - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {Element} element - The overlay element. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - this.viewer.raiseEvent( 'remove-overlay', { - element: element - }); - } + $.console.error("drawer.removeOverlay is deprecated. Use viewer.removeOverlay instead."); + this.viewer.updateOverlay( element ); return this; }, @@ -319,28 +220,34 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ * @method * @return {OpenSeadragon.Drawer} Chainable. * @fires OpenSeadragon.Viewer.event:clear-overlay + * @deprecated - use {@link OpenSeadragon.Viewer#clearOverlays} instead. */ clearOverlays: function() { - while ( this.overlays.length > 0 ) { - this.overlays.pop().destroy(); - this.updateAgain = true; - } - if( this.viewer ){ - /** - * Raised when all overlays are removed from the viewer (see {@link OpenSeadragon.Drawer#clearOverlays}). - * - * @event clear-overlay - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - this.viewer.raiseEvent( 'clear-overlay', {} ); - } + $.console.error("drawer.clearOverlays is deprecated. Use viewer.clearOverlays instead."); + this.viewer.clearOverlays(); return this; }, + /** + * Set the opacity of the drawer. + * @method + * @param {Number} opacity + * @return {OpenSeadragon.Drawer} Chainable. + */ + setOpacity: function( opacity ) { + this.opacity = opacity; + $.setElementOpacity( this.canvas, this.opacity, true ); + return this; + }, + /** + * Get the opacity of the drawer. + * @method + * @returns {Number} + */ + getOpacity: function() { + return this.opacity; + }, /** * Returns whether the Drawer is scheduled for an update at the * soonest possible opportunity. @@ -419,7 +326,10 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ this.downloading++; image = new Image(); - image.crossOrigin = 'Anonymous'; + + if (_this.crossOriginPolicy !== false) { + image.crossOrigin = _this.crossOriginPolicy; + } complete = function( imagesrc, resultingImage ){ _this.downloading--; @@ -467,61 +377,6 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ } }; -/** - * @private - * @inner - */ - function addOverlayFromConfiguration( drawer, overlay ){ - - var element = null, - rect = ( overlay.height && overlay.width ) ? new $.Rect( - overlay.x || overlay.px, - overlay.y || overlay.py, - overlay.width, - overlay.height - ) : new $.Point( - overlay.x || overlay.px, - overlay.y || overlay.py - ), - id = overlay.id ? - overlay.id : - "openseadragon-overlay-"+Math.floor(Math.random()*10000000); - - element = $.getElement(overlay.id); - if( !element ){ - element = document.createElement("a"); - element.href = "#/overlay/"+id; - } - element.id = id; - $.addClass( element, overlay.className ? - overlay.className : - "openseadragon-overlay" - ); - - - if(overlay.px !== undefined){ - //if they specified 'px' so it's in pixel coordinates so - //we need to translate to viewport coordinates - rect = drawer.viewport.imageToViewportRectangle( rect ); - } - - if( overlay.placement ){ - return new $.Overlay({ - element: element, - location: drawer.viewport.pointFromPixel(rect), - placement: $.OverlayPlacement[overlay.placement.toUpperCase()], - onDraw: overlay.onDraw - }); - }else{ - return new $.Overlay({ - element: element, - location: rect, - onDraw: overlay.onDraw - }); - } - -} - /** * @private * @inner @@ -695,7 +550,6 @@ function updateViewport( drawer ) { //TODO drawTiles( drawer, drawer.lastDrawn ); - drawOverlays( drawer.viewport, drawer.overlays, drawer.container ); //TODO if ( best ) { @@ -1142,23 +996,6 @@ function resetCoverage( coverage, level ) { coverage[ level ] = {}; } -/** - * @private - * @inner - * Determines the 'z-index' of the given overlay. Overlays are ordered in - * a z-index based on the order they are added to the Drawer. - */ -function getOverlayIndex( overlays, element ) { - var i; - for ( i = overlays.length - 1; i >= 0; i-- ) { - if ( overlays[ i ].element == element ) { - return i; - } - } - - return -1; -} - /** * @private * @inner @@ -1196,28 +1033,6 @@ function finishLoadingImage( image, callback, successful, jobid ){ } - -function drawOverlays( viewport, overlays, container ){ - var i, - length = overlays.length; - for ( i = 0; i < length; i++ ) { - drawOverlay( viewport, overlays[ i ], container ); - } -} - -function drawOverlay( viewport, overlay, container ){ - - overlay.position = viewport.pixelFromPoint( - overlay.bounds.getTopLeft(), - true - ); - overlay.size = viewport.deltaPixelsFromPoints( - overlay.bounds.getSize(), - true - ); - overlay.drawHTML( container, viewport ); -} - function drawTiles( drawer, lastDrawn ){ var i, tile, @@ -1297,7 +1112,7 @@ function drawTiles( drawer, lastDrawn ){ ')'; } - drawer.addOverlay( + drawer.viewer.addOverlay( viewer.element, tile.bounds ); diff --git a/src/openseadragon.js b/src/openseadragon.js index b56af941..41065ea8 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -136,6 +136,28 @@ * is an Array of objects, it is used to create a * {@link OpenSeadragon.LegacyTileSource}. * + * @property {Array} overlays Array of objects defining permanent overlays of + * the viewer. The overlays added via this option and later removed with + * {@link OpenSeadragon.Viewer#removeOverlay} will be added back when a new + * image is opened. + * To add overlays which can be definitively removed, one must use + * {@link OpenSeadragon.Viewer#addOverlay} + * If displaying a sequence of images, the overlays can be associated + * with a specific page by passing the overlays array to the page's + * tile source configuration. + * Expected properties: + * * x, y, (or px, py for pixel coordinates) to define the location. + * * width, height in point if using x,y or in pixels if using px,py. If width + * and height are specified, the overlay size is adjusted when zooming, + * otherwise the size stays the size of the content (or the size defined by CSS). + * * className to associate a class to the overlay + * * id to set the overlay element. If an element with this id already exists, + * it is reused, otherwise it is created. If not specified, a new element is + * created. + * * placement a string to define the relative position to the viewport. + * Only used if no width and height are specified. Default: 'TOP_LEFT'. + * See {@link OpenSeadragon.OverlayPlacement} for possible values. + * * @property {String} [xmlPath=null] * DEPRECATED. A relative path to load a DZI file from the server. * Prefer the newer Options.tileSources. @@ -190,6 +212,12 @@ * Zoom level to use when image is first opened or the home button is clicked. * If 0, adjusts to fit viewer. * + * @property {Number} [opacity=1] + * Opacity of the drawer (1=opaque, 0=transparent) + * + * @property {Number} [layersAspectRatioEpsilon=0.0001] + * Maximum aspectRatio mismatch between 2 layers. + * * @property {Number} [degrees=0] * Initial rotation. * @@ -381,6 +409,10 @@ * * @property {Number} [collectionTileSize=800] * + * @property {String} [crossOriginPolicy='Anonymous'] + * Valid values are 'Anonymous', 'use-credentials', and false. If false, canvas requests will + * not use CORS, and the canvas will be tainted. + * */ /** @@ -695,6 +727,7 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ tileSources: null, tileHost: null, initialPage: 0, + crossOriginPolicy: 'Anonymous', //PAN AND ZOOM SETTINGS AND CONSTRAINTS panHorizontal: true, @@ -750,6 +783,12 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ // INITIAL ROTATION degrees: 0, + // APPEARANCE + opacity: 1, + + // LAYERS SETTINGS + layersAspectRatioEpsilon: 0.0001, + //REFERENCE STRIP SETTINGS showReferenceStrip: false, referenceStripScroll: 'horizontal', @@ -1389,6 +1428,52 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ } }, + /** + * Find the first index at which an element is found in an array or -1 + * if not present. + * + * Code taken and adapted from + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf#Compatibility + * + * @function + * @param {Array} array The array from which to find the element + * @param {Object} searchElement The element to find + * @param {Number} [fromIndex=0] Index to start research. + * @returns {Number} The index of the element in the array. + */ + indexOf: function( array, searchElement, fromIndex ) { + if ( Array.prototype.indexOf ) { + this.indexOf = function( array, searchElement, fromIndex ) { + return array.indexOf( searchElement, fromIndex ); + }; + } else { + this.indexOf = function( array, searchElement, fromIndex ) { + var i, + pivot = ( fromIndex ) ? fromIndex : 0, + length; + if ( !array ) { + throw new TypeError( ); + } + + length = array.length; + if ( length === 0 || pivot >= length ) { + return -1; + } + + if ( pivot < 0 ) { + pivot = length - Math.abs( pivot ); + } + + for ( i = pivot; i < length; i++ ) { + if ( array[i] === searchElement ) { + return i; + } + } + return -1; + }; + } + return this.indexOf( array, searchElement, fromIndex ); + }, /** * Remove the specified CSS class from the element. diff --git a/src/overlay.js b/src/overlay.js index 29445389..accb6ea4 100644 --- a/src/overlay.js +++ b/src/overlay.js @@ -35,7 +35,8 @@ (function( $ ){ /** - * An enumeration of positions that an overlay may be assigned relative to the viewport. + * An enumeration of positions that an overlay may be assigned relative to + * the viewport. * @member OverlayPlacement * @memberof OpenSeadragon * @static @@ -69,8 +70,14 @@ * @memberof OpenSeadragon * @param {Object} options * @param {Element} options.element - * @param {OpenSeadragon.Point|OpenSeadragon.Rect} options.location - * @param {OpenSeadragon.OverlayPlacement} options.placement - Only used if location is an {@link OpenSeadragon.Point}. + * @param {OpenSeadragon.Point|OpenSeadragon.Rect} options.location - The + * location of the overlay on the image. If a {@link OpenSeadragon.Point} + * is specified, the overlay will keep a constant size independently of the + * zoom. If a {@link OpenSeadragon.Rect} is specified, the overlay size will + * be adjusted when the zoom changes. + * @param {OpenSeadragon.OverlayPlacement} [options.placement=OpenSeadragon.OverlayPlacement.TOP_LEFT] + * Relative position to the viewport. + * Only used if location is a {@link OpenSeadragon.Point}. * @param {OpenSeadragon.Overlay.OnDrawCallback} options.onDraw */ $.Overlay = function( element, location, placement ) { @@ -86,9 +93,9 @@ */ var options; - if( $.isPlainObject( element ) ){ + if ( $.isPlainObject( element ) ) { options = element; - } else{ + } else { options = { element: element, location: location, @@ -174,7 +181,7 @@ element.parentNode.removeChild( element ); //this should allow us to preserve overlays when required between //pages - if( element.prevElementParent ){ + if ( element.prevElementParent ) { style.display = 'none'; //element.prevElementParent.insertBefore( // element, @@ -209,9 +216,15 @@ viewport.viewer.drawer.canvas.width / 2, viewport.viewer.drawer.canvas.height / 2 ), - degrees = viewport.degrees, - position, - size, + degrees = viewport.degrees, + position = viewport.pixelFromPoint( + this.bounds.getTopLeft(), + true + ), + size = viewport.deltaPixelsFromPoints( + this.bounds.getSize(), + true + ), overlayCenter; if ( element.parentNode != container ) { @@ -225,8 +238,8 @@ this.size = $.getElementSize( element ); } - position = this.position; - size = this.size; + this.position = position; + this.size = size; this.adjust( position, size ); diff --git a/src/point.js b/src/point.js index b3cd928a..38aad1f1 100644 --- a/src/point.js +++ b/src/point.js @@ -76,10 +76,10 @@ $.Point.prototype = /** @lends OpenSeadragon.Point.prototype */{ }, /** - * Add another Point to this point and return a new Point. + * Substract another Point to this point and return a new Point. * @function - * @param {OpenSeadragon.Point} point The point to add vector components. - * @returns {OpenSeadragon.Point} A new point representing the sum of the + * @param {OpenSeadragon.Point} point The point to substract vector components. + * @returns {OpenSeadragon.Point} A new point representing the substraction of the * vector components */ minus: function( point ) { @@ -90,11 +90,11 @@ $.Point.prototype = /** @lends OpenSeadragon.Point.prototype */{ }, /** - * Add another Point to this point and return a new Point. + * Multiply this point by a factor and return a new Point. * @function - * @param {OpenSeadragon.Point} point The point to add vector components. - * @returns {OpenSeadragon.Point} A new point representing the sum of the - * vector components + * @param {Number} factor The factor to multiply vector components. + * @returns {OpenSeadragon.Point} A new point representing the multiplication + * of the vector components by the factor */ times: function( factor ) { return new $.Point( @@ -104,11 +104,11 @@ $.Point.prototype = /** @lends OpenSeadragon.Point.prototype */{ }, /** - * Add another Point to this point and return a new Point. + * Divide this point by a factor and return a new Point. * @function - * @param {OpenSeadragon.Point} point The point to add vector components. - * @returns {OpenSeadragon.Point} A new point representing the sum of the - * vector components + * @param {Number} factor The factor to divide vector components. + * @returns {OpenSeadragon.Point} A new point representing the division of the + * vector components by the factor */ divide: function( factor ) { return new $.Point( @@ -118,10 +118,9 @@ $.Point.prototype = /** @lends OpenSeadragon.Point.prototype */{ }, /** - * Add another Point to this point and return a new Point. + * Compute the opposite of this point and return a new Point. * @function - * @param {OpenSeadragon.Point} point The point to add vector components. - * @returns {OpenSeadragon.Point} A new point representing the sum of the + * @returns {OpenSeadragon.Point} A new point representing the opposite of the * vector components */ negate: function() { @@ -129,11 +128,10 @@ $.Point.prototype = /** @lends OpenSeadragon.Point.prototype */{ }, /** - * Add another Point to this point and return a new Point. + * Compute the distance between this point and another point. * @function - * @param {OpenSeadragon.Point} point The point to add vector components. - * @returns {OpenSeadragon.Point} A new point representing the sum of the - * vector components + * @param {OpenSeadragon.Point} point The point to compute the distance with. + * @returns {Number} The distance between the 2 points */ distanceTo: function( point ) { return Math.sqrt( @@ -143,22 +141,21 @@ $.Point.prototype = /** @lends OpenSeadragon.Point.prototype */{ }, /** - * Add another Point to this point and return a new Point. + * Apply a function to each coordinate of this point and return a new point. * @function - * @param {OpenSeadragon.Point} point The point to add vector components. - * @returns {OpenSeadragon.Point} A new point representing the sum of the - * vector components + * @param {function} func The function to apply to each coordinate. + * @returns {OpenSeadragon.Point} A new point with the coordinates computed + * by the specified function */ apply: function( func ) { return new $.Point( func( this.x ), func( this.y ) ); }, /** - * Add another Point to this point and return a new Point. + * Check if this point is equal to another one. * @function - * @param {OpenSeadragon.Point} point The point to add vector components. - * @returns {OpenSeadragon.Point} A new point representing the sum of the - * vector components + * @param {OpenSeadragon.Point} point The point to compare this point with. + * @returns {Boolean} true if they are equal, false otherwise. */ equals: function( point ) { return ( @@ -186,11 +183,10 @@ $.Point.prototype = /** @lends OpenSeadragon.Point.prototype */{ }, /** - * Add another Point to this point and return a new Point. + * Convert this point to a string in the format (x,y) where x and y are + * rounded to the nearest integer. * @function - * @param {OpenSeadragon.Point} point The point to add vector components. - * @returns {OpenSeadragon.Point} A new point representing the sum of the - * vector components + * @returns {String} A string representation of this point. */ toString: function() { return "(" + Math.round(this.x) + "," + Math.round(this.y) + ")"; diff --git a/src/viewer.js b/src/viewer.js index 790942d0..ddcdbdde 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -74,8 +74,7 @@ $.Viewer = function( options ) { xmlPath: args.length > 1 ? args[ 1 ] : undefined, prefixUrl: args.length > 2 ? args[ 2 ] : undefined, controls: args.length > 3 ? args[ 3 ] : undefined, - overlays: args.length > 4 ? args[ 4 ] : undefined, - overlayControls: args.length > 5 ? args[ 5 ] : undefined + overlays: args.length > 4 ? args[ 4 ] : undefined }; } @@ -127,9 +126,10 @@ $.Viewer = function( options ) { */ canvas: null, - //TODO: not sure how to best describe these - overlays: [], - overlayControls:[], + // Overlays list. An overlay allows to add html on top of the viewer. + overlays: [], + // Container inside the canvas where overlays are drawn. + overlaysContainer: null, //private state properties previousBody: [], @@ -150,8 +150,10 @@ $.Viewer = function( options ) { * @member {OpenSeadragon.Drawer} drawer * @memberof OpenSeadragon.Viewer# */ - drawer: null, - drawers: [], + drawer: null, + drawers: [], + // Container inside the canvas where drawers (layers) are drawn. + drawersContainer: null, /** * Handles coordinate-related functionality - zoom, pan, rotation, etc. Created for each TileSource opened. * @member {OpenSeadragon.Viewport} viewport @@ -211,6 +213,7 @@ $.Viewer = function( options ) { }; this._updateRequestId = null; + this.currentOverlays = []; //Inherit some behaviors and properties $.EventSource.call( this ); @@ -261,6 +264,8 @@ $.Viewer = function( options ) { this.element = this.element || document.getElementById( this.id ); this.canvas = $.makeNeutralElement( "div" ); this.keyboardCommandArea = $.makeNeutralElement( "textarea" ); + this.drawersContainer = $.makeNeutralElement( "div" ); + this.overlaysContainer = $.makeNeutralElement( "div" ); this.canvas.className = "openseadragon-canvas"; (function( style ){ @@ -305,6 +310,8 @@ $.Viewer = function( options ) { this.container.insertBefore( this.canvas, this.container.firstChild ); this.container.insertBefore( this.keyboardCommandArea, this.container.firstChild ); this.element.appendChild( this.container ); + this.canvas.appendChild( this.drawersContainer ); + this.canvas.appendChild( this.overlaysContainer ); //Used for toggling between fullscreen and default container size //TODO: these can be closure private and shared across Viewer @@ -485,81 +492,26 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * @fires OpenSeadragon.Viewer.event:open-failed */ open: function ( tileSource ) { - var _this = this, - customTileSource, - readySource, - $TileSource, - options; + var _this = this; _this._hideMessage(); - //allow plain xml strings or json strings to be parsed here - if( $.type( tileSource ) == 'string' ){ - if( tileSource.match(/\s*<.*/) ){ - tileSource = $.parseXml( tileSource ); - }else if( tileSource.match(/\s*[\{\[].*/) ){ - /*jshint evil:true*/ - tileSource = eval( '('+tileSource+')' ); - } - } - - setTimeout(function(){ - if ( $.type( tileSource ) == 'string') { - //If its still a string it means it must be a url at this point - tileSource = new $.TileSource( tileSource, function( event ){ - openTileSource( _this, event.tileSource ); - }); - tileSource.addHandler( 'open-failed', function ( event ) { - /** - * Raised when an error occurs loading a TileSource. - * - * @event open-failed - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {String} message - * @property {String} source - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - _this.raiseEvent( 'open-failed', event ); - }); - - } else if ( $.isPlainObject( tileSource ) || tileSource.nodeType ){ - if( $.isFunction( tileSource.getTileUrl ) ){ - //Custom tile source - customTileSource = new $.TileSource(tileSource); - customTileSource.getTileUrl = tileSource.getTileUrl; - openTileSource( _this, customTileSource ); - } else { - //inline configuration - $TileSource = $.TileSource.determineType( _this, tileSource ); - if ( !$TileSource ) { - /*** - * Raised when an error occurs loading a TileSource. - * - * @event open-failed - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {String} message - * @property {String} source - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - _this.raiseEvent( 'open-failed', { - message: "Unable to load TileSource", - source: tileSource - }); - return; - } - options = $TileSource.prototype.configure.apply( _this, [ tileSource ]); - readySource = new $TileSource( options ); - openTileSource( _this, readySource ); - } - } else { - //can assume it's already a tile source implementation - openTileSource( _this, tileSource ); - } - }, 1); + getTileSourceImplementation( _this, tileSource, function( tileSource ) { + openTileSource( _this, tileSource ); + }, function( event ) { + /** + * Raised when an error occurs loading a TileSource. + * + * @event open-failed + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {String} message + * @property {String} source + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent( 'open-failed', event ); + }); return this; }, @@ -580,18 +532,16 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, this.navigator.close(); } - if ( this.drawer ) { - this.drawer.clearOverlays(); - } + this.clearOverlays(); + this.drawersContainer.innerHTML = ""; + this.overlaysContainer.innerHTML = ""; this.source = null; this.drawer = null; + this.drawers = []; this.viewport = this.preserveViewport ? this.viewport : null; - //this.profiler = null; - if (this.canvas){ - this.canvas.innerHTML = ""; - } + VIEWERS[ this.hash ] = null; delete VIEWERS[ this.hash ]; @@ -1060,6 +1010,257 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, return this; }, + /** + * Add a layer. + * options.tileSource can be anything that {@link OpenSeadragon.Viewer#open} + * supports except arrays of images as layers cannot be sequences. + * @function + * @param {Object} options + * @param {String|Object|Function} options.tileSource The TileSource of the layer. + * @param {Number} [options.opacity=1] The opacity of the layer. + * @param {Number} [options.level] The level of the layer. Added on top of + * all other layers if not specified. + * @returns {OpenSeadragon.Viewer} Chainable. + * @fires OpenSeadragon.Viewer.event:add-layer + * @fires OpenSeadragon.Viewer.event:add-layer-failed + */ + addLayer: function( options ) { + var _this = this, + tileSource = options.tileSource; + + if ( !this.isOpen() ) { + throw new Error( "An image must be loaded before adding layers." ); + } + if ( !tileSource ) { + throw new Error( "No tile source provided as new layer." ); + } + if ( this.collectionMode ) { + throw new Error( "Layers not supported in collection mode." ); + } + + function raiseAddLayerFailed( event ) { + /** + * Raised when an error occurs while adding a layer. + * @event add-layer-failed + * @memberOf OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {String} message + * @property {String} source + * @property {Object} options The options passed to the addLayer method. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent( 'add-layer-failed', event ); + } + + getTileSourceImplementation( this, tileSource, function( tileSource ) { + + if ( tileSource instanceof Array ) { + raiseAddLayerFailed({ + message: "Sequences can not be added as layers.", + source: tileSource, + options: options + }); + return; + } + + for ( var i = 0; i < _this.drawers.length; i++ ) { + var otherAspectRatio = _this.drawers[ i ].source.aspectRatio; + var diff = otherAspectRatio - tileSource.aspectRatio; + if ( Math.abs( diff ) > _this.layersAspectRatioEpsilon ) { + raiseAddLayerFailed({ + message: "Aspect ratio mismatch with layer " + i + ".", + source: tileSource, + options: options + }); + return; + } + } + + var drawer = new $.Drawer({ + viewer: _this, + source: tileSource, + viewport: _this.viewport, + element: _this.drawersContainer, + opacity: options.opacity !== undefined ? + options.opacity : _this.opacity, + maxImageCacheCount: _this.maxImageCacheCount, + imageLoaderLimit: _this.imageLoaderLimit, + minZoomImageRatio: _this.minZoomImageRatio, + wrapHorizontal: _this.wrapHorizontal, + wrapVertical: _this.wrapVertical, + immediateRender: _this.immediateRender, + blendTime: _this.blendTime, + alwaysBlend: _this.alwaysBlend, + minPixelRatio: _this.minPixelRatio, + timeout: _this.timeout, + debugMode: _this.debugMode, + debugGridColor: _this.debugGridColor + }); + _this.drawers.push( drawer ); + if ( options.level !== undefined ) { + _this.setLayerLevel( drawer, options.level ); + } + THIS[ _this.hash ].forceRedraw = true; + /** + * Raised when a layer is successfully added. + * @event add-layer + * @memberOf OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {Object} options The options passed to the addLayer method. + * @property {OpenSeadragon.Drawer} drawer The layer's underlying drawer. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent( 'add-layer', { + options: options, + drawer: drawer + }); + }, function( event ) { + event.options = options; + raiseAddLayerFailed(event); + } ); + + return this; + }, + + /** + * Get the layer at the specified level. + * @param {Number} level The layer to retrieve level. + * @returns {OpenSeadragon.Drawer} The layer at the specified level. + */ + getLayerAtLevel: function( level ) { + if ( level >= this.drawers.length ) { + throw new Error( "Level bigger than number of layers." ); + } + return this.drawers[ level ]; + }, + + /** + * Get the level of the layer associated with the given drawer or -1 if not + * present. + * @param {OpenSeadragon.Drawer} drawer The underlying drawer of the layer. + * @returns {Number} The level of the layer or -1 if not present. + */ + getLevelOfLayer: function( drawer ) { + return $.indexOf( this.drawers, drawer ); + }, + + /** + * Get the number of layers used. + * @returns {Number} The number of layers used. + */ + getLayersCount: function() { + return this.drawers.length; + }, + + /** + * Change the level of a layer so that it appears over or under others. + * @param {OpenSeadragon.Drawer} drawer The underlying drawer of the changing + * level layer. + * @param {Number} level The new level + * @returns {OpenSeadragon.Viewer} Chainable. + * @fires OpenSeadragon.Viewer.event:layer-level-changed + */ + setLayerLevel: function( drawer, level ) { + var oldLevel = this.getLevelOfLayer( drawer ); + + if ( level >= this.drawers.length ) { + throw new Error( "Level bigger than number of layers." ); + } + if ( level === oldLevel || oldLevel === -1 ) { + return this; + } + if ( level === 0 || oldLevel === 0 ) { + if ( THIS[ this.hash ].sequenced ) { + throw new Error( "Cannot reassign base level when in sequence mode." ); + } + // We need to re-assign the base drawer and the source + this.drawer = level === 0 ? drawer : this.getLayerAtLevel( level ); + this.source = this.drawer.source; + } + this.drawers.splice( oldLevel, 1 ); + this.drawers.splice( level, 0, drawer ); + this.drawersContainer.removeChild( drawer.canvas ); + if ( level === 0 ) { + var nextLevelCanvas = this.drawers[ 1 ].canvas; + nextLevelCanvas.parentNode.insertBefore( drawer.canvas, + nextLevelCanvas ); + } else { + // Insert right after layer at level - 1 + var prevLevelCanvas = this.drawers[level - 1].canvas; + prevLevelCanvas.parentNode.insertBefore( drawer.canvas, + prevLevelCanvas.nextSibling ); + } + + /** + * Raised when the order of the layers has been changed. + * @event layer-level-changed + * @memberOf OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.Drawer} drawer - The drawer which level has + * been changed + * @property {Number} previousLevel - The previous level of the drawer + * @property {Number} newLevel - The new level of the drawer + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'layer-level-changed', { + drawer: drawer, + previousLevel: oldLevel, + newLevel: level + } ); + + return this; + }, + + /** + * Remove a layer. If there is only one layer, close the viewer. + * @function + * @param {OpenSeadragon.Drawer} drawer The underlying drawer of the layer + * to remove + * @returns {OpenSeadragon.Viewer} Chainable. + * @fires OpenSeadragon.Viewer.event:remove-layer + */ + removeLayer: function( drawer ) { + var index = this.drawers.indexOf( drawer ); + if ( index === -1 ) { + return this; + } + if ( index === 0 ) { + if ( THIS[ this.hash ].sequenced ) { + throw new Error( "Cannot remove base layer when in sequence mode." ); + } + if ( this.drawers.length === 1 ) { + this.close(); + return this; + } + this.drawer = this.drawers[ 1 ]; + } + + this.drawers.splice( index, 1 ); + this.drawersContainer.removeChild( drawer.canvas ); + /** + * Raised when a layer is removed. + * @event remove-layer + * @memberOf OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {OpenSeadragon.Drawer} drawer The layer's underlying drawer. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'remove-layer', { drawer: drawer } ); + return this; + }, + + /** + * Force the viewer to redraw its drawers. + * @returns {OpenSeadragon.Viewer} Chainable. + */ + forceRedraw: function() { + THIS[ this.hash ].forceRedraw = true; + return this; + }, /** * @function @@ -1342,6 +1543,173 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, return this; }, + /** + * Adds an html element as an overlay to the current viewport. Useful for + * highlighting words or areas of interest on an image or other zoomable + * interface. The overlays added via this method are removed when the viewport + * is closed which include when changing page. + * @method + * @param {Element|String|Object} element - A reference to an element or an id for + * the element which will overlayed. Or an Object specifying the configuration for the overlay + * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or + * rectangle which will be overlayed. + * @param {OpenSeadragon.OverlayPlacement} placement - The position of the + * viewport which the location coordinates will be treated as relative + * to. + * @param {function} onDraw - If supplied the callback is called when the overlay + * needs to be drawn. It it the responsibility of the callback to do any drawing/positioning. + * It is passed position, size and element. + * @return {OpenSeadragon.Viewer} Chainable. + * @fires OpenSeadragon.Viewer.event:add-overlay + */ + addOverlay: function( element, location, placement, onDraw ) { + var options; + if( $.isPlainObject( element ) ){ + options = element; + } else { + options = { + element: element, + location: location, + placement: placement, + onDraw: onDraw + }; + } + + element = $.getElement( options.element ); + + if ( getOverlayIndex( this.currentOverlays, element ) >= 0 ) { + // they're trying to add a duplicate overlay + return this; + } + this.currentOverlays.push( getOverlayObject( this, options ) ); + THIS[ this.hash ].forceRedraw = true; + /** + * Raised when an overlay is added to the viewer (see {@link OpenSeadragon.Viewer#addOverlay}). + * + * @event add-overlay + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {Element} element - The overlay element. + * @property {OpenSeadragon.Point|OpenSeadragon.Rect} location + * @property {OpenSeadragon.OverlayPlacement} placement + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'add-overlay', { + element: element, + location: options.location, + placement: options.placement + }); + return this; + }, + + /** + * Updates the overlay represented by the reference to the element or + * element id moving it to the new location, relative to the new placement. + * @method + * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or + * rectangle which will be overlayed. + * @param {OpenSeadragon.OverlayPlacement} placement - The position of the + * viewport which the location coordinates will be treated as relative + * to. + * @return {OpenSeadragon.Viewer} Chainable. + * @fires OpenSeadragon.Viewer.event:update-overlay + */ + updateOverlay: function( element, location, placement ) { + var i; + + element = $.getElement( element ); + i = getOverlayIndex( this.currentOverlays, element ); + + if ( i >= 0 ) { + this.currentOverlays[ i ].update( location, placement ); + THIS[ this.hash ].forceRedraw = true; + /** + * Raised when an overlay's location or placement changes + * (see {@link OpenSeadragon.Viewer#updateOverlay}). + * + * @event update-overlay + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the + * Viewer which raised the event. + * @property {Element} element + * @property {OpenSeadragon.Point|OpenSeadragon.Rect} location + * @property {OpenSeadragon.OverlayPlacement} placement + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'update-overlay', { + element: element, + location: location, + placement: placement + }); + } + return this; + }, + + /** + * Removes an overlay identified by the reference element or element id + * and schedules an update. + * @method + * @param {Element|String} element - A reference to the element or an + * element id which represent the ovelay content to be removed. + * @return {OpenSeadragon.Viewer} Chainable. + * @fires OpenSeadragon.Viewer.event:remove-overlay + */ + removeOverlay: function( element ) { + var i; + + element = $.getElement( element ); + i = getOverlayIndex( this.currentOverlays, element ); + + if ( i >= 0 ) { + this.currentOverlays[ i ].destroy(); + this.currentOverlays.splice( i, 1 ); + THIS[ this.hash ].forceRedraw = true; + /** + * Raised when an overlay is removed from the viewer + * (see {@link OpenSeadragon.Viewer#removeOverlay}). + * + * @event remove-overlay + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the + * Viewer which raised the event. + * @property {Element} element - The overlay element. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'remove-overlay', { + element: element + }); + } + return this; + }, + + /** + * Removes all currently configured Overlays from this Viewer and schedules + * an update. + * @method + * @return {OpenSeadragon.Viewer} Chainable. + * @fires OpenSeadragon.Viewer.event:clear-overlay + */ + clearOverlays: function() { + while ( this.currentOverlays.length > 0 ) { + this.currentOverlays.pop().destroy(); + } + THIS[ this.hash ].forceRedraw = true; + /** + * Raised when all overlays are removed from the viewer (see {@link OpenSeadragon.Drawer#clearOverlays}). + * + * @event clear-overlay + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'clear-overlay', {} ); + return this; + }, + /** * Updates the sequence buttons. * @function OpenSeadragon.Viewer.prototype._updateSequenceButtons @@ -1422,20 +1790,73 @@ function _getSafeElemSize (oElement) { ); } +/** + * @function + * @private + */ +function getTileSourceImplementation( viewer, tileSource, successCallback, + failCallback ) { + var _this = viewer; + + //allow plain xml strings or json strings to be parsed here + if ( $.type( tileSource ) == 'string' ) { + if ( tileSource.match( /\s*<.*/ ) ) { + tileSource = $.parseXml( tileSource ); + } else if ( tileSource.match( /\s*[\{\[].*/ ) ) { + /*jshint evil:true*/ + tileSource = eval( '(' + tileSource + ')' ); + } + } + + setTimeout( function() { + if ( $.type( tileSource ) == 'string' ) { + //If its still a string it means it must be a url at this point + tileSource = new $.TileSource( tileSource, function( event ) { + successCallback( event.tileSource ); + }); + tileSource.addHandler( 'open-failed', function( event ) { + failCallback( event ); + } ); + + } else if ( $.isPlainObject( tileSource ) || tileSource.nodeType ) { + if ( $.isFunction( tileSource.getTileUrl ) ) { + //Custom tile source + var customTileSource = new $.TileSource( tileSource ); + customTileSource.getTileUrl = tileSource.getTileUrl; + successCallback( customTileSource ); + } else { + //inline configuration + var $TileSource = $.TileSource.determineType( _this, tileSource ); + if ( !$TileSource ) { + failCallback( { + message: "Unable to load TileSource", + source: tileSource + }); + return; + } + var options = $TileSource.prototype.configure.apply( _this, [ tileSource ] ); + var readySource = new $TileSource( options ); + successCallback( readySource ); + } + } else { + //can assume it's already a tile source implementation + successCallback( tileSource ); + } + }, 1 ); +} + /** * @function * @private */ function openTileSource( viewer, source ) { - var _this = viewer, - overlay, - i; + var i, + _this = viewer; if ( _this.source ) { _this.close( ); } - _this.canvas.innerHTML = ""; THIS[ _this.hash ].prevContainerSize = _getSafeElemSize( _this.container ); @@ -1463,7 +1884,7 @@ function openTileSource( viewer, source ) { //minZoomLevel: this.minZoomLevel, //maxZoomLevel: this.maxZoomLevel }); - }else{ + } else { if( source ){ _this.source = source; } @@ -1495,8 +1916,8 @@ function openTileSource( viewer, source ) { viewer: _this, source: _this.source, viewport: _this.viewport, - element: _this.canvas, - overlays: [].concat( _this.overlays ).concat( _this.source.overlays ), + element: _this.drawersContainer, + opacity: _this.opacity, maxImageCacheCount: _this.maxImageCacheCount, imageLoaderLimit: _this.imageLoaderLimit, minZoomImageRatio: _this.minZoomImageRatio, @@ -1508,8 +1929,10 @@ function openTileSource( viewer, source ) { minPixelRatio: _this.collectionMode ? 0 : _this.minPixelRatio, timeout: _this.timeout, debugMode: _this.debugMode, - debugGridColor: _this.debugGridColor + debugGridColor: _this.debugGridColor, + crossOriginPolicy: _this.crossOriginPolicy }); + _this.drawers = [_this.drawer]; // Now that we have a drawer, see if it supports rotate. If not we need to remove the rotate buttons if (!_this.drawer.canRotate()) { @@ -1546,7 +1969,6 @@ function openTileSource( viewer, source ) { tileSources: source, tileHost: _this.tileHost, prefixUrl: _this.prefixUrl, - overlays: _this.overlays, viewer: _this }); } @@ -1564,7 +1986,6 @@ function openTileSource( viewer, source ) { tileSources: _this.tileSources, tileHost: _this.tileHost, prefixUrl: _this.prefixUrl, - overlays: _this.overlays, viewer: _this }); } @@ -1575,40 +1996,10 @@ function openTileSource( viewer, source ) { THIS[ _this.hash ].forceRedraw = true; _this._updateRequestId = scheduleUpdate( _this, updateMulti ); - //Assuming you had programatically created a bunch of overlays - //and added them via configuration - for ( i = 0; i < _this.overlayControls.length; i++ ) { - - overlay = _this.overlayControls[ i ]; - - if ( overlay.point ) { - - _this.drawer.addOverlay( - overlay.id, - new $.Point( - overlay.point.X, - overlay.point.Y - ), - $.OverlayPlacement.TOP_LEFT - ); - - } else { - - _this.drawer.addOverlay( - overlay.id, - new $.Rect( - overlay.rect.Point.X, - overlay.rect.Point.Y, - overlay.rect.Width, - overlay.rect.Height - ), - overlay.placement - ); - - } - } VIEWERS[ _this.hash ] = _this; + loadOverlays( _this ); + /** * Raised when the viewer has opened and loaded one or more TileSources. * @@ -1624,8 +2015,98 @@ function openTileSource( viewer, source ) { return _this; } +function loadOverlays( _this ) { + _this.currentOverlays = []; + for ( var i = 0; i < _this.overlays.length; i++ ) { + _this.currentOverlays[ i ] = getOverlayObject( _this, _this.overlays[ i ] ); + } + for ( var j = 0; j < _this.source.overlays.length; j++ ) { + _this.currentOverlays[ i + j ] = + getOverlayObject( _this, _this.source.overlays[ j ] ); + } +} +function getOverlayObject( viewer, overlay ) { + if ( overlay instanceof $.Overlay ) { + return overlay; + } + var element = null; + if ( overlay.element ) { + element = $.getElement( overlay.element ); + } else { + var id = overlay.id ? + overlay.id : + "openseadragon-overlay-" + Math.floor( Math.random() * 10000000 ); + + element = $.getElement( overlay.id ); + if ( !element ) { + element = document.createElement( "a" ); + element.href = "#/overlay/" + id; + } + element.id = id; + $.addClass( element, overlay.className ? + overlay.className : + "openseadragon-overlay" + ); + } + + var location = overlay.location; + if ( !location ) { + var rect = ( overlay.height && overlay.width ) ? new $.Rect( + overlay.x || overlay.px, + overlay.y || overlay.py, + overlay.width, + overlay.height + ) : new $.Point( + overlay.x || overlay.px, + overlay.y || overlay.py + ); + if( overlay.px !== undefined ) { + //if they specified 'px' so it's in pixel coordinates so + //we need to translate to viewport coordinates + rect = viewer.viewport.imageToViewportRectangle( rect ); + } + location = overlay.placement ? viewer.viewport.pointFromPixel( rect ) : + rect; + } + + var placement = overlay.placement; + if ( placement && ( $.type( placement ) === "string" ) ) { + placement = $.OverlayPlacement[ overlay.placement.toUpperCase() ]; + } + + return new $.Overlay({ + element: element, + location: location, + placement: placement, + onDraw: overlay.onDraw + }); +} + +/** + * @private + * @inner + * Determines the index of the given overlay in the given overlays array. + */ +function getOverlayIndex( overlays, element ) { + var i; + for ( i = overlays.length - 1; i >= 0; i-- ) { + if ( overlays[ i ].element === element ) { + return i; + } + } + + return -1; +} + +function drawOverlays( viewport, overlays, container ) { + var i, + length = overlays.length; + for ( i = 0; i < length; i++ ) { + overlays[ i ].drawHTML( container, viewport ); + } +} /////////////////////////////////////////////////////////////////////////////// // Schedulers provide the general engine for animation @@ -2037,7 +2518,8 @@ function updateOnce( viewer ) { } if ( animated ) { - viewer.drawer.update(); + updateDrawers( viewer ); + drawOverlays( viewer.viewport, viewer.currentOverlays, viewer.overlaysContainer ); if( viewer.navigator ){ viewer.navigator.update( viewer.viewport ); } @@ -2051,8 +2533,9 @@ function updateOnce( viewer ) { * @property {?Object} userData - Arbitrary subscriber-defined object. */ viewer.raiseEvent( "animation" ); - } else if ( THIS[ viewer.hash ].forceRedraw || viewer.drawer.needsUpdate() ) { - viewer.drawer.update(); + } else if ( THIS[ viewer.hash ].forceRedraw || drawersNeedUpdate( viewer ) ) { + updateDrawers( viewer ); + drawOverlays( viewer.viewport, viewer.currentOverlays, viewer.overlaysContainer ); if( viewer.navigator ){ viewer.navigator.update( viewer.viewport ); } @@ -2106,6 +2589,21 @@ function resizeViewportAndRecenter( viewer, containerSize, oldBounds, oldCenter viewport.fitBounds( newBounds, true ); } +function updateDrawers( viewer ) { + for (var i = 0; i < viewer.drawers.length; i++ ) { + viewer.drawers[i].update(); + } +} + +function drawersNeedUpdate( viewer ) { + for (var i = 0; i < viewer.drawers.length; i++ ) { + if (viewer.drawers[i].needsUpdate()) { + return true; + } + } + return false; +} + /////////////////////////////////////////////////////////////////////////////// // Navigation Controls /////////////////////////////////////////////////////////////////////////////// diff --git a/src/viewport.js b/src/viewport.js index 65230f4a..aadb81e9 100644 --- a/src/viewport.js +++ b/src/viewport.js @@ -652,7 +652,7 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ throw new Error('Currently only 0, 90, 180, and 270 degrees are supported.'); } this.degrees = degrees; - this.viewer.drawer.update(); + this.viewer.forceRedraw(); return this; }, @@ -920,12 +920,12 @@ $.Viewport.prototype = /** @lends OpenSeadragon.Viewport.prototype */{ /** * Convert pixel coordinates relative to the image to * viewer element coordinates. - * @param {OpenSeadragon.Point} point + * @param {OpenSeadragon.Point} pixel * @returns {OpenSeadragon.Point} */ - imageToViewerElementCoordinates: function( point ) { - var pixel = this.pixelFromPoint( point, true ); - return this.imageToViewportCoordinates( pixel ); + imageToViewerElementCoordinates: function( pixel ) { + var point = this.imageToViewportCoordinates( pixel ); + return this.pixelFromPoint( point, true ); }, /** diff --git a/test/data/A.png b/test/data/A.png new file mode 100644 index 00000000..dde07443 Binary files /dev/null and b/test/data/A.png differ diff --git a/test/data/BBlue.png b/test/data/BBlue.png new file mode 100644 index 00000000..055d7ff4 Binary files /dev/null and b/test/data/BBlue.png differ diff --git a/test/data/CCyan.png b/test/data/CCyan.png new file mode 100644 index 00000000..40e56de2 Binary files /dev/null and b/test/data/CCyan.png differ diff --git a/test/data/DDandelion.png b/test/data/DDandelion.png new file mode 100644 index 00000000..fdc849f2 Binary files /dev/null and b/test/data/DDandelion.png differ diff --git a/test/demo/layers.html b/test/demo/layers.html new file mode 100644 index 00000000..c4777f55 --- /dev/null +++ b/test/demo/layers.html @@ -0,0 +1,194 @@ + + + + OpenSeadragon Layers Demo + + + + + +
+ Simple demo page to show an OpenSeadragon viewer with layers. +
+
+ +
+
+ Available layers
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ Used layers
+ +
+ +



+ +
+ Opacity:
+ +
+ +
+ +
+ +
+
+ +



+ +
+ +
+
+ + + + diff --git a/test/layers.js b/test/layers.js new file mode 100644 index 00000000..a20c92d0 --- /dev/null +++ b/test/layers.js @@ -0,0 +1,278 @@ +/* global module, asyncTest, $, ok, equal, notEqual, start, test, Util, testLog */ + +( function() { + var viewer; + + module( 'Layers', { + setup: function() { + $( '
' ).appendTo( "#qunit-fixture" ); + + testLog.reset(); + + viewer = OpenSeadragon( { + id: 'layersexample', + prefixUrl: '/build/openseadragon/images/', + springStiffness: 100 // Faster animation = faster tests + }); + }, + teardown: function() { + if ( viewer && viewer.close ) { + viewer.close(); + } + + viewer = null; + $( "#layersexample" ).remove(); + } + } ); + + // ---------- + asyncTest( 'Layers operations', function() { + expect( 23 ); + viewer.addHandler( "open", function( ) { + equal( 1, viewer.getLayersCount( ), + "One layer should be present after opening." ); + var options = { + tileSource: { + type: 'legacy-image-pyramid', + levels: [ { + url: "data/A.png", + width: 1000, + height: 1000 + } ] + } + }; + viewer.addLayer( options ); + viewer.addHandler( "add-layer", function addFirstLayerHandler( event ) { + viewer.removeHandler( "add-layer", addFirstLayerHandler ); + var layer1 = event.drawer; + equal( viewer.getLayersCount( ), 2, + "2 layers should be present after adding a layer." ); + equal( options, event.options, + "The options should be transmitted via the event." ); + equal( viewer.getLevelOfLayer( layer1 ), 1, + "The first added layer should have a level of 1" ); + equal( viewer.getLayerAtLevel( 1 ), layer1, + "The layer at level 1 should be the first added layer." ); + + viewer.addLayer( options ); + viewer.addHandler( "add-layer", function addSecondLayerHandler( event ) { + viewer.removeHandler( "add-layer", addSecondLayerHandler ); + var layer2 = event.drawer; + equal( viewer.getLayersCount( ), 3, + "3 layers should be present after adding a second layer." ); + equal( viewer.getLevelOfLayer( layer2 ), 2, + "If not specified, a layer should be added with the highest level." ); + equal( viewer.getLayerAtLevel( 2 ), layer2, + "The layer at level 2 should be the second added layer." ); + + viewer.addHandler( "layer-level-changed", + function layerLevelChangedHandler( event ) { + viewer.removeHandler( "layer-level-changed", + layerLevelChangedHandler ); + equal( event.drawer, layer2, + "The layer which changed level should be layer2" ); + equal( event.previousLevel, 2, "Previous level should be 2." ); + equal( event.newLevel, 1, "New level should be 1." ); + }); + viewer.setLayerLevel( layer2, 1 ); + equal( viewer.getLevelOfLayer( layer2 ), 1, + "Layer2 level should be 1 after setLayerLevel." ); + equal( viewer.getLevelOfLayer( layer1 ), 2, + "Layer1 level should be 2 after setLayerLevel." ); + equal( viewer.getLayerAtLevel( 1 ), layer2, + "The layer at level 1 should be layer2." ); + equal( viewer.getLayerAtLevel( 2 ), layer1, + "The layer at level 2 should be layer1." ); + + options.level = 2; + options.tileSource.levels[0].url = "data/CCyan.png"; + options.opacity = 0.5; + viewer.addLayer( options ); + viewer.addHandler( "add-layer", function addThirdLayerHandler( event ) { + viewer.removeHandler( "add-layer", addThirdLayerHandler ); + var layer3 = event.drawer; + equal( viewer.getLayersCount( ), 4, + "4 layers should be present after adding a third layer." ); + equal( viewer.getLevelOfLayer( layer3 ), 2, + "Layer 3 should be added with level 2." ); + equal( viewer.getLevelOfLayer( layer2 ), 1, + "Layer 2 should stay at level 1." ); + + viewer.addHandler( "remove-layer", function removeLayerHandler( event ) { + viewer.removeHandler( "remove-layer", removeLayerHandler ); + + equal( layer2, event.drawer, "Removed layer should be layer2." ); + + equal( viewer.getLevelOfLayer( layer1 ), 2, + "Layer 1 should be at level 2." ); + equal( viewer.getLevelOfLayer( layer2 ), -1, + "Layer 2 should be at level -1." ); + equal( viewer.getLevelOfLayer( layer3 ), 1, + "Layer 3 should be at level 1." ); + + }); + viewer.removeLayer( layer2 ); + + options.tileSource.levels[0].width = 500; + viewer.addHandler( "add-layer-failed", function addLayerFailed( event ) { + viewer.removeHandler( "add-layer-failed", addLayerFailed ); + + equal( viewer.getLayersCount(), 3 ); + + start(); + }); + viewer.addLayer( options ); + }); + }); + }); + }); + viewer.open( '/test/data/testpattern.dzi' ); + }); + + asyncTest( 'Sequences as layers', function() { + + var options = { + tileSource: [{ + type: 'legacy-image-pyramid', + levels: [{ + url: "data/A.png", + width: 1000, + height: 1000 + }] + }, { + type: 'legacy-image-pyramid', + levels: [{ + url: "data/BBlue.png", + width: 1000, + height: 1000 + }] + }] + }; + + viewer.addHandler( "open", function openHandler() { + viewer.removeHandler( "open", openHandler ); + + viewer.addHandler( "add-layer-failed", + function addLayerFailedHandler( event ) { + viewer.removeHandler( "add-layer-failed", addLayerFailedHandler ); + equal( event.message, "Sequences can not be added as layers." ); + equal( event.options, options, "Layer failed event should give the options." ); + start(); + } ); + viewer.addLayer( options ); + + }); + viewer.open( '/test/data/testpattern.dzi' ); + }); + + + asyncTest( 'Reassign base layer', function() { + + var options = { + tileSource: { + type: 'legacy-image-pyramid', + levels: [{ + url: "data/A.png", + width: 1000, + height: 1000 + }] + }, + level: 0 + }; + viewer.addHandler( "open", function openHandler( ) { + viewer.removeHandler( "open", openHandler ); + var testPatternDrawer = viewer.drawer; + equal( viewer.drawer, testPatternDrawer, "Viewer.drawer should be set to testPatternDrawer." ); + viewer.addHandler( "add-layer", function addLayerHandler( event ) { + viewer.removeHandler( "add-layer", addLayerHandler ); + var aDrawer = event.drawer; + equal( viewer.drawer, aDrawer, "Viewer.drawer should be set to aDrawer." ); + viewer.setLayerLevel( aDrawer, 1 ); + equal( viewer.drawer, testPatternDrawer, "Viewer.drawer should be set back to testPatternDrawer." ); + + viewer.removeLayer( viewer.drawer ); + equal( viewer.drawer, aDrawer, "Viewer.drawer must be reassigned when removing base layer." ); + + viewer.removeLayer( viewer.drawer ); + ok( !viewer.isOpen(), "Viewer should be closed when removing last layer." ); + + start(); + }); + viewer.addLayer( options ); + }); + viewer.open( '/test/data/testpattern.dzi' ); + }); + + asyncTest( 'Layers and sequences', function() { + expect( 1 ); + +// TODO: Remove workaround when issue #321 is fixed. +// https://github.com/openseadragon/openseadragon/issues/321 +// viewer.open( [{ +// type: 'legacy-image-pyramid', +// levels: [ { +// url: "data/A.png", +// width: 1000, +// height: 1000 +// }] +// }, +// { +// type: 'legacy-image-pyramid', +// levels: [ { +// url: "data/BBlue.png", +// width: 1000, +// height: 1000 +// }]}] ); + + viewer.close(); + viewer = OpenSeadragon({ + id: 'layersexample', + prefixUrl: '/build/openseadragon/images/', + springStiffness: 100, // Faster animation = faster tests + tileSources: [{ + type: 'legacy-image-pyramid', + levels: [{ + url: "data/A.png", + width: 1000, + height: 1000 + }] + }, + { + type: 'legacy-image-pyramid', + levels: [{ + url: "data/BBlue.png", + width: 1000, + height: 1000 + }] + }] + }); +// End workaround + + var options = { + tileSource: { + type: 'legacy-image-pyramid', + levels: [{ + url: "data/CCyan.png", + width: 1000, + height: 1000 + }] + } + }; + + viewer.addHandler( "open", function openHandler() { + viewer.addHandler( "add-layer", function addLayerHandler( event ) { + viewer.removeHandler( "add-layer", addLayerHandler ); + + var layer = event.drawer; + try { + viewer.setLayerLevel( layer, 0 ); + } catch (e) { + ok( true ); + } + start(); + } ); + viewer.addLayer( options ); + }); + + }); +})(); diff --git a/test/overlays.js b/test/overlays.js new file mode 100644 index 00000000..47e0af89 --- /dev/null +++ b/test/overlays.js @@ -0,0 +1,344 @@ +/* global QUnit, module, Util, $, console, test, asyncTest, start, ok, equal */ + +( function() { + var viewer; + + module( "Overlays", { + setup: function() { + var example = $( '
' ).appendTo( "#qunit-fixture" ); + var fixedOverlay = $( '
' ).appendTo(example); + fixedOverlay.width(70); + fixedOverlay.height(60); + + testLog.reset(); + }, + teardown: function() { + resetTestVariables(); + } + } ); + + var resetTestVariables = function() { + if ( viewer ) { + viewer.close(); + } + }; + + function waitForViewer( handler, count ) { + if ( typeof count !== "number" ) { + count = 0; + } + var ready = viewer.isOpen() && + viewer.drawer !== null && + !viewer.drawer.needsUpdate() && + Util.equalsWithVariance( viewer.viewport.getBounds( true ).x, + viewer.viewport.getBounds().x, 0.000 ) && + Util.equalsWithVariance( viewer.viewport.getBounds( true ).y, + viewer.viewport.getBounds().y, 0.000 ) && + Util.equalsWithVariance( viewer.viewport.getBounds( true ).width, + viewer.viewport.getBounds().width, 0.000 ); + + if ( ready ) { + handler(); + } else if ( count < 50 ) { + count++; + setTimeout( function() { + waitForViewer( handler, count ); + }, 100 ); + } else { + console.log( "waitForViewer:" + viewer.isOpen( ) + ":" + viewer.drawer + + ":" + viewer.drawer.needsUpdate() ); + handler(); + } + } + + asyncTest( 'Overlays via viewer options', function() { + + viewer = OpenSeadragon( { + id: 'example-overlays', + prefixUrl: '/build/openseadragon/images/', + tileSources: [ '/test/data/testpattern.dzi', '/test/data/testpattern.dzi' ], + springStiffness: 100, // Faster animation = faster tests + overlays: [ { + x: 0.1, + y: 0.4, + width: 0.09, + height: 0.09, + id: "overlay" + } ] + } ); + viewer.addHandler( 'open', openHandler ); + + function openHandler() { + viewer.removeHandler( 'open', openHandler ); + + equal( viewer.overlays.length, 1, "Global overlay should be added." ); + equal( viewer.currentOverlays.length, 1, "Global overlay should be open." ); + + viewer.addHandler( 'open', openPageHandler ); + viewer.goToPage( 1 ); + } + + function openPageHandler() { + viewer.removeHandler( 'open', openPageHandler ); + + equal( viewer.overlays.length, 1, "Global overlay should stay after page switch." ); + equal( viewer.currentOverlays.length, 1, "Global overlay should re-open after page switch." ); + + viewer.addHandler( 'close', closeHandler ); + viewer.close(); + } + + function closeHandler() { + viewer.removeHandler( 'close', closeHandler ); + + equal( viewer.overlays.length, 1, "Global overlay should not be removed on close." ); + equal( viewer.currentOverlays.length, 0, "Global overlay should be closed on close." ); + + start(); + } + } ); + + asyncTest( 'Page Overlays via viewer options', function() { + + viewer = OpenSeadragon( { + id: 'example-overlays', + prefixUrl: '/build/openseadragon/images/', + tileSources: [ { + Image: { + xmlns: "http://schemas.microsoft.com/deepzoom/2008", + Url: "/test/data/testpattern_files/", + Format: "jpg", + Overlap: "1", + TileSize: "254", + Size: { + Width: 1000, + Height: 1000 + } + }, + overlays: [ { + x: 0.1, + y: 0.4, + width: 0.09, + height: 0.09, + id: "overlay" + } ] + }, { + Image: { + xmlns: "http://schemas.microsoft.com/deepzoom/2008", + Url: "/test/data/testpattern_files/", + Format: "jpg", + Overlap: "1", + TileSize: "254", + Size: { + Width: 1000, + Height: 1000 + } + } + } ], + springStiffness: 100 // Faster animation = faster tests + } ); + viewer.addHandler( 'open', openHandler ); + + function openHandler() { + viewer.removeHandler( 'open', openHandler ); + + equal( viewer.overlays.length, 0, "No global overlay should be added." ); + equal( viewer.currentOverlays.length, 1, "Page overlay should be open." ); + + viewer.addHandler( 'open', openPageHandler ); + viewer.goToPage( 1 ); + } + + function openPageHandler() { + viewer.removeHandler( 'open', openPageHandler ); + + equal( viewer.overlays.length, 0, "No global overlay should be added after page switch." ); + equal( viewer.currentOverlays.length, 0, "No page overlay should be opened after page switch." ); + + viewer.addHandler( 'close', closeHandler ); + viewer.close(); + } + + function closeHandler() { + viewer.removeHandler( 'close', closeHandler ); + + equal( viewer.overlays.length, 0, "No global overlay should be added on close." ); + equal( viewer.currentOverlays.length, 0, "Page overlay should be closed on close." ); + + start(); + } + } ); + + asyncTest( 'Overlays via addOverlay method', function() { + + viewer = OpenSeadragon( { + id: 'example-overlays', + prefixUrl: '/build/openseadragon/images/', + tileSources: [ '/test/data/testpattern.dzi', '/test/data/testpattern.dzi' ], + springStiffness: 100 // Faster animation = faster tests + } ); + viewer.addHandler( 'open', openHandler ); + + function openHandler() { + viewer.removeHandler( 'open', openHandler ); + + equal( viewer.overlays.length, 0, "No global overlay should be added." ); + equal( viewer.currentOverlays.length, 0, "No overlay should be open." ); + + var rect = new OpenSeadragon.Rect( 0.1, 0.1, 0.1, 0.1 ); + var overlay = $( "
" ).prop("id", "overlay").get( 0 ); + viewer.addOverlay( overlay, rect ); + equal( viewer.overlays.length, 0, "No manual overlay should be added as global overlay." ); + equal( viewer.currentOverlays.length, 1, "A manual overlay should be open." ); + + viewer.addHandler( 'open', openPageHandler ); + viewer.goToPage( 1 ); + } + + function openPageHandler() { + viewer.removeHandler( 'open', openPageHandler ); + + equal( viewer.overlays.length, 0, "No global overlay should be added after page switch." ); + equal( viewer.currentOverlays.length, 0, "Manual overlay should be removed after page switch." ); + + viewer.addHandler( 'close', closeHandler ); + viewer.close(); + } + + function closeHandler() { + viewer.removeHandler( 'close', closeHandler ); + + equal( viewer.overlays.length, 0, "No global overlay should be added on close." ); + equal( viewer.currentOverlays.length, 0, "Manual overlay should be removed on close." ); + + start(); + } + + } ); + + asyncTest( 'Overlays size in pixels', function() { + + viewer = OpenSeadragon( { + id: 'example-overlays', + prefixUrl: '/build/openseadragon/images/', + tileSources: [ '/test/data/testpattern.dzi', '/test/data/testpattern.dzi' ], + springStiffness: 100, // Faster animation = faster tests + overlays: [ { + px: 13, + py: 120, + width: 124, + height: 132, + id: "overlay" + }, { + px: 400, + py: 500, + id: "fixed-overlay" + }] + } ); + + function checkOverlayPosition( contextMessage ) { + var viewport = viewer.viewport; + + var expPosition = viewport.imageToViewerElementCoordinates( + new OpenSeadragon.Point( 13, 120 ) ).apply( Math.floor ); + var actPosition = $( "#overlay" ).position(); + equal( actPosition.left, expPosition.x, "X position mismatch " + contextMessage ); + equal( actPosition.top, expPosition.y, "Y position mismatch " + contextMessage ); + + var zoom = viewport.viewportToImageZoom( viewport.getZoom( true ) ); + var expectedWidth = Math.ceil( 124 * zoom ); + var expectedHeight = Math.ceil( 132 * zoom ); + equal( $( "#overlay" ).width(), expectedWidth, "Width mismatch " + contextMessage ); + equal( $( "#overlay" ).height( ), expectedHeight, "Height mismatch " + contextMessage ); + + + expPosition = viewport.imageToViewerElementCoordinates( + new OpenSeadragon.Point( 400, 500 ) ).apply( Math.floor ); + actPosition = $( "#fixed-overlay" ).position(); + equal( actPosition.left, expPosition.x, "Fixed overlay X position mismatch " + contextMessage ); + equal( actPosition.top, expPosition.y, "Fixed overlay Y position mismatch " + contextMessage ); + + equal( $( "#fixed-overlay" ).width(), 70, "Fixed overlay width mismatch " + contextMessage ); + equal( $( "#fixed-overlay" ).height( ), 60, "Fixed overlay height mismatch " + contextMessage ); + } + + waitForViewer( function() { + checkOverlayPosition( "after opening using image coordinates" ); + + viewer.viewport.zoomBy( 1.1 ).panBy( new OpenSeadragon.Point( 0.1, 0.2 ) ); + waitForViewer( function() { + checkOverlayPosition( "after zoom and pan using image coordinates" ); + + viewer.viewport.goHome(); + waitForViewer( function() { + checkOverlayPosition( "after goHome using image coordinates" ); + start(); + } ); + } ); + + } ); + } ); + + asyncTest( 'Overlays size in points', function() { + + viewer = OpenSeadragon( { + id: 'example-overlays', + prefixUrl: '/build/openseadragon/images/', + tileSources: [ '/test/data/testpattern.dzi', '/test/data/testpattern.dzi' ], + springStiffness: 100, // Faster animation = faster tests + overlays: [ { + x: 0.2, + y: 0.1, + width: 0.5, + height: 0.1, + id: "overlay" + },{ + x: 0.5, + y: 0.6, + id: "fixed-overlay" + } ] + } ); + + function checkOverlayPosition( contextMessage ) { + var viewport = viewer.viewport; + + var expPosition = viewport.viewportToViewerElementCoordinates( + new OpenSeadragon.Point( 0.2, 0.1 ) ).apply( Math.floor ); + var actPosition = $( "#overlay" ).position(); + equal( actPosition.left, expPosition.x, "X position mismatch " + contextMessage ); + equal( actPosition.top, expPosition.y, "Y position mismatch " + contextMessage ); + + var expectedSize = viewport.deltaPixelsFromPoints( + new OpenSeadragon.Point(0.5, 0.1)); + equal( $( "#overlay" ).width(), expectedSize.x, "Width mismatch " + contextMessage ); + equal( $( "#overlay" ).height( ), expectedSize.y, "Height mismatch " + contextMessage ); + + + expPosition = viewport.viewportToViewerElementCoordinates( + new OpenSeadragon.Point( 0.5, 0.6 ) ).apply( Math.floor ); + actPosition = $( "#fixed-overlay" ).position(); + equal( actPosition.left, expPosition.x, "Fixed overlay X position mismatch " + contextMessage ); + equal( actPosition.top, expPosition.y, "Fixed overlay Y position mismatch " + contextMessage ); + + equal( $( "#fixed-overlay" ).width(), 70, "Fixed overlay width mismatch " + contextMessage ); + equal( $( "#fixed-overlay" ).height( ), 60, "Fixed overlay height mismatch " + contextMessage ); + } + + waitForViewer( function() { + checkOverlayPosition( "after opening using viewport coordinates" ); + + viewer.viewport.zoomBy( 1.1 ).panBy( new OpenSeadragon.Point( 0.1, 0.2 ) ); + waitForViewer( function() { + checkOverlayPosition( "after zoom and pan using viewport coordinates" ); + + viewer.viewport.goHome(); + waitForViewer( function() { + checkOverlayPosition( "after goHome using viewport coordinates" ); + start(); + } ); + } ); + + } ); + } ); + +} )(); diff --git a/test/test.css b/test/test.css index 88dca730..c8e85fab 100644 --- a/test/test.css +++ b/test/test.css @@ -18,7 +18,12 @@ width: 300px; } -#unitsexample { +#unitsexample, #example-overlays { + height: 500px; + width: 500px; +} + +#layersexample { height: 500px; width: 500px; } diff --git a/test/test.html b/test/test.html index 1d5c1ce6..94602d88 100644 --- a/test/test.html +++ b/test/test.html @@ -27,6 +27,8 @@ + + diff --git a/test/units.js b/test/units.js index 348835b3..1ce10a09 100644 --- a/test/units.js +++ b/test/units.js @@ -26,12 +26,34 @@ function pointEqual(a, b, message) { - ok(a.x === b.x && a.y === b.y, message); + Util.assessNumericValue(a.x, b.x, 0.00000001, message); + Util.assessNumericValue(a.y, b.y, 0.00000001, message); } // ---------- asyncTest('Coordinates conversions', function() { + function checkPoint(context) { + var viewport = viewer.viewport; + + var point = new OpenSeadragon.Point(15, 12); + var result = viewport.viewerElementToImageCoordinates( + viewport.imageToViewerElementCoordinates(point)); + pointEqual(result, point, 'viewerElement and image ' + context); + + var result = viewport.windowToImageCoordinates( + viewport.imageToWindowCoordinates(point)); + pointEqual(result, point, 'window and image ' + context); + + var result = viewport.viewerElementToViewportCoordinates( + viewport.viewportToViewerElementCoordinates(point)); + pointEqual(result, point, 'viewerElement and viewport ' + context); + + var result = viewport.windowToViewportCoordinates( + viewport.viewportToWindowCoordinates(point)); + pointEqual(result, point, 'window and viewport ' + context); + } + viewer.addHandler("open", function () { var viewport = viewer.viewport; @@ -52,24 +74,13 @@ var pixel = viewport.viewerElementToImageCoordinates(viewerTopRight); pointEqual(pixel, imageTopRight, 'Viewer top right has viewport coordinates imageWidth,0.'); - var point = new OpenSeadragon.Point(15, 12); - var result = viewport.viewerElementToImageCoordinates( - viewport.imageToViewerElementCoordinates(point)); - pointEqual(result, point, 'viewerElement and image'); - - var result = viewport.windowToImageCoordinates( - viewport.imageToWindowCoordinates(point)); - pointEqual(result, point, 'window and image'); - - var result = viewport.viewerElementToViewportCoordinates( - viewport.viewportToViewerElementCoordinates(point)); - pointEqual(result, point, 'viewerElement and viewport'); - - var result = viewport.windowToViewportCoordinates( - viewport.viewportToWindowCoordinates(point)); - pointEqual(result, point, 'window and viewport'); - - start(); + checkPoint('after opening'); + viewer.addHandler('animation-finish', function animationHandler() { + viewer.removeHandler('animation-finish', animationHandler); + checkPoint('after zoom and pan'); + start(); + }); + viewer.viewport.zoomTo(0.8).panTo(new OpenSeadragon.Point(0.1, 0.2)); }); viewer.open('/test/data/testpattern.dzi'); }); @@ -104,14 +115,13 @@ checkZoom(); var zoomHandler = function() { - viewer.removeHandler('animationfinish', zoomHandler); + viewer.removeHandler('animation-finish', zoomHandler); checkZoom(); start(); }; - viewer.addHandler('animationfinish', zoomHandler); + viewer.addHandler('animation-finish', zoomHandler); viewport.zoomTo(2); - start(); }); viewer.open('/test/data/testpattern.dzi');