/* * OpenSeadragon - Navigator * * Copyright (C) 2009 CodePlex Foundation * Copyright (C) 2010-2013 OpenSeadragon contributors * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * - Neither the name of CodePlex Foundation nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ (function( $ ){ /** * @class Navigator * @classdesc The Navigator provides a small view of the current image as fixed * while representing the viewport as a moving box serving as a frame * of reference in the larger viewport as to which portion of the image * is currently being examined. The navigator's viewport can be interacted * with using the keyboard or the mouse. * * @memberof OpenSeadragon * @extends OpenSeadragon.Viewer * @extends OpenSeadragon.EventSource * @param {Object} options */ $.Navigator = function( options ){ var viewer = options.viewer, _this = this, viewerSize, navigatorSize; //We may need to create a new element and id if they did not //provide the id for the existing element if( !options.id ){ options.id = 'navigator-' + $.now(); this.element = $.makeNeutralElement( "div" ); options.controlOptions = { anchor: $.ControlAnchor.TOP_RIGHT, attachToViewer: true, autoFade: options.autoFade }; if( options.position ){ if( 'BOTTOM_RIGHT' === options.position ){ options.controlOptions.anchor = $.ControlAnchor.BOTTOM_RIGHT; } else if( 'BOTTOM_LEFT' === options.position ){ options.controlOptions.anchor = $.ControlAnchor.BOTTOM_LEFT; } else if( 'TOP_RIGHT' === options.position ){ options.controlOptions.anchor = $.ControlAnchor.TOP_RIGHT; } else if( 'TOP_LEFT' === options.position ){ options.controlOptions.anchor = $.ControlAnchor.TOP_LEFT; } else if( 'ABSOLUTE' === options.position ){ options.controlOptions.anchor = $.ControlAnchor.ABSOLUTE; options.controlOptions.top = options.top; options.controlOptions.left = options.left; options.controlOptions.height = options.height; options.controlOptions.width = options.width; } } } else { this.element = document.getElementById( options.id ); options.controlOptions = { anchor: $.ControlAnchor.NONE, attachToViewer: false, autoFade: false }; } this.element.id = options.id; this.element.className += ' navigator'; options = $.extend( true, { sizeRatio: $.DEFAULT_SETTINGS.navigatorSizeRatio }, options, { element: this.element, tabIndex: -1, // No keyboard navigation, omit from tab order //These need to be overridden to prevent recursion since //the navigator is a viewer and a viewer has a navigator showNavigator: false, mouseNavEnabled: false, showNavigationControl: false, showSequenceControl: false, immediateRender: true, blendTime: 0, animationTime: 0, autoResize: options.autoResize, // prevent resizing the navigator from adding unwanted space around the image minZoomImageRatio: 1.0, background: options.background, opacity: options.opacity, borderColor: options.borderColor, displayRegionColor: options.displayRegionColor }); options.minPixelRatio = this.minPixelRatio = viewer.minPixelRatio; $.setElementTouchActionNone( this.element ); this.borderWidth = 2; //At some browser magnification levels the display regions lines up correctly, but at some there appears to //be a one pixel gap. this.fudge = new $.Point(1, 1); this.totalBorderWidths = new $.Point(this.borderWidth * 2, this.borderWidth * 2).minus(this.fudge); if ( options.controlOptions.anchor !== $.ControlAnchor.NONE ) { (function( style, borderWidth ){ style.margin = '0px'; style.border = borderWidth + 'px solid ' + options.borderColor; style.padding = '0px'; style.background = options.background; style.opacity = options.opacity; style.overflow = 'hidden'; }( this.element.style, this.borderWidth)); } this.displayRegion = $.makeNeutralElement( "div" ); this.displayRegion.id = this.element.id + '-displayregion'; this.displayRegion.className = 'displayregion'; (function( style, borderWidth ){ style.position = 'relative'; style.top = '0px'; style.left = '0px'; style.fontSize = '0px'; style.overflow = 'hidden'; style.border = borderWidth + 'px solid ' + options.displayRegionColor; style.margin = '0px'; style.padding = '0px'; //TODO: IE doesn't like this property being set //try{ style.outline = '2px auto #909'; }catch(e){/*ignore*/} style.background = 'transparent'; // We use square bracket notation on the statement below, because float is a keyword. // This is important for the Google Closure compiler, if nothing else. /*jshint sub:true */ style['float'] = 'left'; //Webkit style.cssFloat = 'left'; //Firefox style.styleFloat = 'left'; //IE style.zIndex = 999999999; style.cursor = 'default'; }( this.displayRegion.style, this.borderWidth )); $.setElementPointerEventsNone( this.displayRegion ); $.setElementTouchActionNone( this.displayRegion ); this.displayRegionContainer = $.makeNeutralElement("div"); this.displayRegionContainer.id = this.element.id + '-displayregioncontainer'; this.displayRegionContainer.className = "displayregioncontainer"; this.displayRegionContainer.style.width = "100%"; this.displayRegionContainer.style.height = "100%"; $.setElementPointerEventsNone( this.displayRegionContainer ); $.setElementTouchActionNone( this.displayRegionContainer ); viewer.addControl( this.element, options.controlOptions ); this._resizeWithViewer = options.controlOptions.anchor !== $.ControlAnchor.ABSOLUTE && options.controlOptions.anchor !== $.ControlAnchor.NONE; if (options.width && options.height) { this.setWidth(options.width); this.setHeight(options.height); } else if ( this._resizeWithViewer ) { viewerSize = $.getElementSize( viewer.element ); this.element.style.height = Math.round( viewerSize.y * options.sizeRatio ) + 'px'; this.element.style.width = Math.round( viewerSize.x * options.sizeRatio ) + 'px'; this.oldViewerSize = viewerSize; navigatorSize = $.getElementSize( this.element ); this.elementArea = navigatorSize.x * navigatorSize.y; } this.oldContainerSize = new $.Point( 0, 0 ); $.Viewer.apply( this, [ options ] ); this.displayRegionContainer.appendChild(this.displayRegion); this.element.getElementsByTagName('div')[0].appendChild(this.displayRegionContainer); function rotate(degrees) { _setTransformRotate(_this.displayRegionContainer, degrees); _setTransformRotate(_this.displayRegion, -degrees); _this.viewport.setRotation(degrees); } if (options.navigatorRotate) { var degrees = options.viewer.viewport ? options.viewer.viewport.getRotation() : options.viewer.degrees || 0; rotate(degrees); options.viewer.addHandler("rotate", function (args) { rotate(args.degrees); }); } // Remove the base class' (Viewer's) innerTracker and replace it with our own this.innerTracker.destroy(); this.innerTracker = new $.MouseTracker({ userData: 'Navigator.innerTracker', element: this.element, //this.canvas, dragHandler: $.delegate( this, onCanvasDrag ), clickHandler: $.delegate( this, onCanvasClick ), releaseHandler: $.delegate( this, onCanvasRelease ), scrollHandler: $.delegate( this, onCanvasScroll ) }); this.outerTracker.userData = 'Navigator.outerTracker'; // this.innerTracker is attached to this.element...we need to allow pointer // events to pass through this Viewer's canvas/container elements so implicit // pointer capture works on touch devices //TODO an alternative is to attach the new MouseTracker to this.canvas...not // sure why it isn't already (see MouseTracker constructor call above) $.setElementPointerEventsNone( this.canvas ); $.setElementPointerEventsNone( this.container ); this.addHandler("reset-size", function() { if (_this.viewport) { _this.viewport.goHome(true); } }); viewer.world.addHandler("item-index-change", function(event) { window.setTimeout(function(){ var item = _this.world.getItemAt(event.previousIndex); _this.world.setItemIndex(item, event.newIndex); }, 1); }); viewer.world.addHandler("remove-item", function(event) { var theirItem = event.item; var myItem = _this._getMatchingItem(theirItem); if (myItem) { _this.world.removeItem(myItem); } }); this.update(viewer.viewport); }; $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /** @lends OpenSeadragon.Navigator.prototype */{ /** * Used to notify the navigator when its size has changed. * Especially useful when {@link OpenSeadragon.Options}.navigatorAutoResize is set to false and the navigator is resizable. * @function */ updateSize: function () { if ( this.viewport ) { var containerSize = new $.Point( (this.container.clientWidth === 0 ? 1 : this.container.clientWidth), (this.container.clientHeight === 0 ? 1 : this.container.clientHeight) ); if ( !containerSize.equals( this.oldContainerSize ) ) { this.viewport.resize( containerSize, true ); this.viewport.goHome(true); this.oldContainerSize = containerSize; this.drawer.clear(); this.world.draw(); } } }, /** * Explicitly sets the width of the navigator, in web coordinates. Disables automatic resizing. * @param {Number|String} width - the new width, either a number of pixels or a CSS string, such as "100%" */ setWidth: function(width) { this.width = width; this.element.style.width = typeof (width) === "number" ? (width + 'px') : width; this._resizeWithViewer = false; }, /** * Explicitly sets the height of the navigator, in web coordinates. Disables automatic resizing. * @param {Number|String} height - the new height, either a number of pixels or a CSS string, such as "100%" */ setHeight: function(height) { this.height = height; this.element.style.height = typeof (height) === "number" ? (height + 'px') : height; this._resizeWithViewer = false; }, /** * Flip navigator element * @param {Boolean} state - Flip state to set. */ setFlip: function(state) { this.viewport.setFlip(state); this.setDisplayTransform(this.viewer.viewport.getFlip() ? "scale(-1,1)" : "scale(1,1)"); return this; }, setDisplayTransform: function(rule) { setElementTransform(this.displayRegion, rule); setElementTransform(this.canvas, rule); setElementTransform(this.element, rule); }, /** * Used to update the navigator minimap's viewport rectangle when a change in the viewer's viewport occurs. * @function * @param {OpenSeadragon.Viewport} The viewport this navigator is tracking. */ update: function( viewport ) { var viewerSize, newWidth, newHeight, bounds, topleft, bottomright; viewerSize = $.getElementSize( this.viewer.element ); if ( this._resizeWithViewer && viewerSize.x && viewerSize.y && !viewerSize.equals( this.oldViewerSize ) ) { this.oldViewerSize = viewerSize; if ( this.maintainSizeRatio || !this.elementArea) { newWidth = viewerSize.x * this.sizeRatio; newHeight = viewerSize.y * this.sizeRatio; } else { newWidth = Math.sqrt(this.elementArea * (viewerSize.x / viewerSize.y)); newHeight = this.elementArea / newWidth; } this.element.style.width = Math.round( newWidth ) + 'px'; this.element.style.height = Math.round( newHeight ) + 'px'; if (!this.elementArea) { this.elementArea = newWidth * newHeight; } this.updateSize(); } if (viewport && this.viewport) { bounds = viewport.getBoundsNoRotate(true); topleft = this.viewport.pixelFromPointNoRotate(bounds.getTopLeft(), false); bottomright = this.viewport.pixelFromPointNoRotate(bounds.getBottomRight(), false) .minus( this.totalBorderWidths ); //update style for navigator-box var style = this.displayRegion.style; style.display = this.world.getItemCount() ? 'block' : 'none'; style.top = Math.round( topleft.y ) + 'px'; style.left = Math.round( topleft.x ) + 'px'; var width = Math.abs( topleft.x - bottomright.x ); var height = Math.abs( topleft.y - bottomright.y ); // make sure width and height are non-negative so IE doesn't throw style.width = Math.round( Math.max( width, 0 ) ) + 'px'; style.height = Math.round( Math.max( height, 0 ) ) + 'px'; } }, // overrides Viewer.addTiledImage addTiledImage: function(options) { var _this = this; var original = options.originalTiledImage; delete options.original; var optionsClone = $.extend({}, options, { success: function(event) { var myItem = event.item; myItem._originalForNavigator = original; _this._matchBounds(myItem, original, true); function matchBounds() { _this._matchBounds(myItem, original); } function matchOpacity() { _this._matchOpacity(myItem, original); } function matchCompositeOperation() { _this._matchCompositeOperation(myItem, original); } original.addHandler('bounds-change', matchBounds); original.addHandler('clip-change', matchBounds); original.addHandler('opacity-change', matchOpacity); original.addHandler('composite-operation-change', matchCompositeOperation); } }); return $.Viewer.prototype.addTiledImage.apply(this, [optionsClone]); }, destroy: function() { return $.Viewer.prototype.destroy.apply(this); }, // private _getMatchingItem: function(theirItem) { var count = this.world.getItemCount(); var item; for (var i = 0; i < count; i++) { item = this.world.getItemAt(i); if (item._originalForNavigator === theirItem) { return item; } } return null; }, // private _matchBounds: function(myItem, theirItem, immediately) { var bounds = theirItem.getBoundsNoRotate(); myItem.setPosition(bounds.getTopLeft(), immediately); myItem.setWidth(bounds.width, immediately); myItem.setRotation(theirItem.getRotation(), immediately); myItem.setClip(theirItem.getClip()); }, // private _matchOpacity: function(myItem, theirItem) { myItem.setOpacity(theirItem.opacity); }, // private _matchCompositeOperation: function(myItem, theirItem) { myItem.setCompositeOperation(theirItem.compositeOperation); } }); /** * @private * @inner * @function */ function onCanvasClick( event ) { var canvasClickEventArgs = { tracker: event.eventSource, position: event.position, quick: event.quick, shift: event.shift, originalEvent: event.originalEvent, preventDefaultAction: event.preventDefaultAction }; /** * Raised when a click event occurs on the {@link OpenSeadragon.Viewer#navigator} element. * * @event navigator-click * @memberof OpenSeadragon.Viewer * @type {object} * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. * @property {Boolean} quick - True only if the clickDistThreshold and clickTimeThreshold are both passed. Useful for differentiating between clicks and drags. * @property {Boolean} shift - True if the shift key was pressed during this event. * @property {Object} originalEvent - The original DOM event. * @property {?Object} userData - Arbitrary subscriber-defined object. * @property {Boolean} preventDefaultAction - Set to true to prevent default click to zoom behaviour. Default: false. */ this.viewer.raiseEvent('navigator-click', canvasClickEventArgs); if ( !canvasClickEventArgs.preventDefaultAction && event.quick && this.viewer.viewport && (this.panVertical || this.panHorizontal)) { if(this.viewer.viewport.flipped) { event.position.x = this.viewport.getContainerSize().x - event.position.x; } var target = this.viewport.pointFromPixel(event.position); if (!this.panVertical) { // perform only horizonal pan target.y = this.viewer.viewport.getCenter(true).y; } else if (!this.panHorizontal) { // perform only vertical pan target.x = this.viewer.viewport.getCenter(true).x; } this.viewer.viewport.panTo(target); this.viewer.viewport.applyConstraints(); } } /** * @private * @inner * @function */ function onCanvasDrag( event ) { var canvasDragEventArgs = { tracker: event.eventSource, position: event.position, delta: event.delta, speed: event.speed, direction: event.direction, shift: event.shift, originalEvent: event.originalEvent, preventDefaultAction: event.preventDefaultAction }; /** * Raised when a drag event occurs on the {@link OpenSeadragon.Viewer#navigator} element. * * @event navigator-drag * @memberof OpenSeadragon.Viewer * @type {object} * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. * @property {OpenSeadragon.Point} delta - The x,y components of the difference between start drag and end drag. * @property {Number} speed - Current computed speed, in pixels per second. * @property {Number} direction - Current computed direction, expressed as an angle counterclockwise relative to the positive X axis (-pi to pi, in radians). Only valid if speed > 0. * @property {Boolean} shift - True if the shift key was pressed during this event. * @property {Object} originalEvent - The original DOM event. * @property {?Object} userData - Arbitrary subscriber-defined object. * @property {Boolean} preventDefaultAction - Set to true to prevent default drag to pan behaviour. Default: false. */ this.viewer.raiseEvent('navigator-drag', canvasDragEventArgs); if ( !canvasDragEventArgs.preventDefaultAction && this.viewer.viewport ) { if( !this.panHorizontal ){ event.delta.x = 0; } if( !this.panVertical ){ event.delta.y = 0; } if(this.viewer.viewport.flipped){ event.delta.x = -event.delta.x; } this.viewer.viewport.panBy( this.viewport.deltaPointsFromPixels( event.delta ) ); if( this.viewer.constrainDuringPan ){ this.viewer.viewport.applyConstraints(); } } } /** * @private * @inner * @function */ function onCanvasRelease( event ) { if ( event.insideElementPressed && this.viewer.viewport ) { this.viewer.viewport.applyConstraints(); } } /** * @private * @inner * @function */ function onCanvasScroll( event ) { /** * Raised when a scroll event occurs on the {@link OpenSeadragon.Viewer#navigator} element (mouse wheel, touch pinch, etc.). * * @event navigator-scroll * @memberof OpenSeadragon.Viewer * @type {object} * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. * @property {Number} scroll - The scroll delta for the event. * @property {Boolean} shift - True if the shift key was pressed during this event. * @property {Object} originalEvent - The original DOM event. * @property {?Object} userData - Arbitrary subscriber-defined object. */ this.viewer.raiseEvent( 'navigator-scroll', { tracker: event.eventSource, position: event.position, scroll: event.scroll, shift: event.shift, originalEvent: event.originalEvent }); //don't scroll the page up and down if the user is scrolling //in the navigator return false; } /** * @function * @private * @param {Object} element * @param {Number} degrees */ function _setTransformRotate( element, degrees ) { setElementTransform(element, "rotate(" + degrees + "deg)"); } function setElementTransform( element, rule ) { element.style.webkitTransform = rule; element.style.mozTransform = rule; element.style.msTransform = rule; element.style.oTransform = rule; element.style.transform = rule; } }( OpenSeadragon ));