From 05f3c1d811a4bc2d26849222972f819cfe20a0c0 Mon Sep 17 00:00:00 2001 From: thatcher Date: Tue, 3 Apr 2012 03:08:27 -0400 Subject: [PATCH] several bug fixes and enhancements. legacy tile source issue discovered and corrected for images with width greater than height. adding basic support for sequenced tile sources including previous and next buttons. added mouse drag and scroll interactions for viewport navigator. --- build.properties | 2 +- images/next_grouphover.png | Bin 0 -> 3004 bytes images/next_hover.png | Bin 0 -> 3433 bytes images/next_pressed.png | Bin 0 -> 3503 bytes images/next_rest.png | Bin 0 -> 3061 bytes images/previous_grouphover.png | Bin 0 -> 2987 bytes images/previous_hover.png | Bin 0 -> 3461 bytes images/previous_pressed.png | Bin 0 -> 3499 bytes images/previous_rest.png | Bin 0 -> 3064 bytes openseadragon.js | 815 +++++++++++++++++++++++---------- src/button.js | 22 + src/drawer.js | 3 +- src/legacytilesource.js | 10 +- src/mousetracker.js | 78 +++- src/navigator.js | 93 +++- src/openseadragon.js | 105 +++-- src/strings.js | 39 +- src/tile.js | 13 +- src/viewer.js | 384 +++++++++++----- src/viewport.js | 69 ++- 20 files changed, 1162 insertions(+), 471 deletions(-) create mode 100644 images/next_grouphover.png create mode 100644 images/next_hover.png create mode 100644 images/next_pressed.png create mode 100644 images/next_rest.png create mode 100644 images/previous_grouphover.png create mode 100644 images/previous_hover.png create mode 100644 images/previous_pressed.png create mode 100644 images/previous_rest.png diff --git a/build.properties b/build.properties index 2fa88b2e..f245e5be 100644 --- a/build.properties +++ b/build.properties @@ -6,7 +6,7 @@ PROJECT: openseadragon BUILD_MAJOR: 0 BUILD_MINOR: 9 -BUILD_ID: 37 +BUILD_ID: 40 BUILD: ${PROJECT}.${BUILD_MAJOR}.${BUILD_MINOR}.${BUILD_ID} VERSION: ${BUILD_MAJOR}.${BUILD_MINOR}.${BUILD_ID} diff --git a/images/next_grouphover.png b/images/next_grouphover.png new file mode 100644 index 0000000000000000000000000000000000000000..8d83d8a142854fedbee4bccee591fafb5be7f179 GIT binary patch literal 3004 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 LNkvXXu0mjf%T$KU literal 0 HcmV?d00001 diff --git a/images/next_pressed.png b/images/next_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..95f169d65150b1e5162585bd0695e4617363afbe GIT binary patch literal 3503 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 literal 0 HcmV?d00001 diff --git a/images/next_rest.png b/images/next_rest.png new file mode 100644 index 0000000000000000000000000000000000000000..5ead544b5d7ef6de16e83f9d28aee515666fcb89 GIT binary patch literal 3061 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 DA#uj9 literal 0 HcmV?d00001 diff --git a/images/previous_grouphover.png b/images/previous_grouphover.png new file mode 100644 index 0000000000000000000000000000000000000000..016e63957a87778ba2562ec7a91dee22248590d8 GIT binary patch literal 2987 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 literal 0 HcmV?d00001 diff --git a/images/previous_hover.png b/images/previous_hover.png new file mode 100644 index 0000000000000000000000000000000000000000..d4a5c1552c7a8a5e7ddae0d2a94f235e00e44c62 GIT binary patch literal 3461 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 Z{{Y0&G1asaZ{h#|002ovPDHLkV1h%en=b$W literal 0 HcmV?d00001 diff --git a/images/previous_rest.png b/images/previous_rest.png new file mode 100644 index 0000000000000000000000000000000000000000..9716dac67b7ccd4c68b427eeca7de3548eed0726 GIT binary patch literal 3064 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 @@ -188,6 +188,12 @@ * interactions include draging the image in a plane, and zooming in toward * and away from the image. * + * @param {Boolean} [options.preserveViewport=false] + * If the viewer has been configured with a sequence of tile sources, then + * normally navigating to through each image resets the viewport to 'home' + * position. If preserveViewport is set to true, then the viewport position + * is preserved when navigating between images in the sequence. + * * @param {String} [options.prefixUrl=''] * Appends the prefixUrl to navImages paths, which is very useful * since the default paths are rarely useful for production @@ -428,43 +434,50 @@ OpenSeadragon = window.OpenSeadragon || function( options ){ * @static */ DEFAULT_SETTINGS: { - xmlPath: null, - tileSources: null, - debugMode: true, - animationTime: 1.5, - blendTime: 0.5, - alwaysBlend: false, - autoHideControls: true, - immediateRender: false, - wrapHorizontal: false, - wrapVertical: false, - minZoomImageRatio: 0.8, - maxZoomPixelRatio: 2, - visibilityRatio: 0.5, - springStiffness: 5.0, - imageLoaderLimit: 0, - clickTimeThreshold: 200, - clickDistThreshold: 5, - zoomPerClick: 2.0, - zoomPerScroll: 1.2, - zoomPerSecond: 2.0, - showNavigationControl: true, - - showNavigator: false, - navigatorElement: null, - navigatorHeight: null, - navigatorWidth: null, - navigatorPosition: null, - navigatorSizeRatio: 0.25, + //DATA SOURCE DETAILS + xmlPath: null, + tileSources: null, + tileHost: null, + + //INTERFACE FEATURES + debugMode: true, + animationTime: 1.5, + blendTime: 0.5, + alwaysBlend: false, + autoHideControls: true, + immediateRender: false, + wrapHorizontal: false, + wrapVertical: false, + panHorizontal: true, + panVertical: true, + visibilityRatio: 0.5, + springStiffness: 5.0, + clickTimeThreshold: 200, + clickDistThreshold: 5, + zoomPerClick: 2.0, + zoomPerScroll: 1.2, + zoomPerSecond: 2.0, + showNavigationControl: true, + controlsFadeDelay: 2000, + controlsFadeLength: 1500, + mouseNavEnabled: true, + showNavigator: false, + navigatorElement: null, + navigatorHeight: null, + navigatorWidth: null, + navigatorPosition: null, + navigatorSizeRatio: 0.25, + preserveViewport: false, - //These two were referenced but never defined - controlsFadeDelay: 2000, - controlsFadeLength: 1500, + //PERFORMANCE SETTINGS + minPixelRatio: 0.5, + imageLoaderLimit: 0, + maxImageCacheCount: 200, + minZoomImageRatio: 0.9, + maxZoomPixelRatio: 2, - maxImageCacheCount: 200, - minPixelRatio: 0.5, - mouseNavEnabled: true, - prefixUrl: null, + //INTERFACE RESOURCE SETTINGS + prefixUrl: null, navImages: { zoomIn: { REST: '/images/zoomin_rest.png', @@ -489,6 +502,18 @@ OpenSeadragon = window.OpenSeadragon || function( options ){ GROUP: '/images/fullpage_grouphover.png', HOVER: '/images/fullpage_hover.png', DOWN: '/images/fullpage_pressed.png' + }, + previous: { + REST: '/images/previous_rest.png', + GROUP: '/images/previous_grouphover.png', + HOVER: '/images/previous_hover.png', + DOWN: '/images/previous_pressed.png' + }, + next: { + REST: '/images/next_rest.png', + GROUP: '/images/next_grouphover.png', + HOVER: '/images/next_hover.png', + DOWN: '/images/next_pressed.png' } } }, @@ -1192,7 +1217,7 @@ OpenSeadragon = window.OpenSeadragon || function( options ){ * @param {String} xmlString * @param {Function} callback */ - createFromDZI: function( dzi, callback ) { + createFromDZI: function( dzi, callback, tileHost ) { var async = typeof ( callback ) == "function", xmlUrl = dzi.substring(0,1) != '<' ? dzi : null, xmlString = xmlUrl ? null : dzi, @@ -1203,7 +1228,12 @@ OpenSeadragon = window.OpenSeadragon || function( options ){ tilesUrl; - if( xmlUrl ){ + if( tileHost ){ + + tilesUrl = tileHost + "/_files/"; + + } else if( xmlUrl ) { + urlParts = xmlUrl.split( '/' ); filename = urlParts[ urlParts.length - 1 ]; lastDot = filename.lastIndexOf( '.' ); @@ -1213,6 +1243,7 @@ OpenSeadragon = window.OpenSeadragon || function( options ){ } tilesUrl = urlParts.join( '/' ) + "_files/"; + } function finish( func, obj ) { @@ -2143,7 +2174,7 @@ $.EventHandler.prototype = { function triggerOthers( tracker, handler, event ) { var otherHash; for ( otherHash in ACTIVE ) { - if ( trackers.hasOwnProperty( otherHash ) && tracker.hash != otherHash ) { + if ( ACTIVE.hasOwnProperty( otherHash ) && tracker.hash != otherHash ) { handler( ACTIVE[ otherHash ], event ); } } @@ -2156,13 +2187,16 @@ $.EventHandler.prototype = { */ function onFocus( tracker, event ){ //console.log( "focus %s", event ); + var propagate; if ( tracker.focusHandler ) { try { - tracker.focusHandler( + propagate = tracker.focusHandler( tracker, event ); - $.cancelEvent( event ); + if( propagate === false ){ + $.cancelEvent( event ); + } } catch ( e ) { $.console.error( "%s while executing key handler: %s", @@ -2181,13 +2215,16 @@ $.EventHandler.prototype = { */ function onBlur( tracker, event ){ //console.log( "blur %s", event ); + var propagate; if ( tracker.blurHandler ) { try { - tracker.blurHandler( + propagate = tracker.blurHandler( tracker, event ); - $.cancelEvent( event ); + if( propagate === false ){ + $.cancelEvent( event ); + } } catch ( e ) { $.console.error( "%s while executing key handler: %s", @@ -2214,7 +2251,7 @@ $.EventHandler.prototype = { event.keyCode ? event.keyCode : event.charCode, event.shiftKey ); - if( !propagate ){ + if( propagate === false ){ $.cancelEvent( event ); } } catch ( e ) { @@ -2236,7 +2273,8 @@ $.EventHandler.prototype = { function onMouseOver( tracker, event ) { var event = $.getEvent( event ), - delegate = THIS[ tracker.hash ]; + delegate = THIS[ tracker.hash ], + propagate; if ( $.Browser.vendor == $.BROWSERS.IE && delegate.capturing && @@ -2262,12 +2300,15 @@ $.EventHandler.prototype = { if ( tracker.enterHandler ) { try { - tracker.enterHandler( + propagate = tracker.enterHandler( tracker, getMouseRelative( event, tracker.element ), delegate.buttonDown, IS_BUTTON_DOWN ); + if( propagate === false ){ + $.cancelEvent( event ); + } } catch ( e ) { $.console.error( "%s while executing enter handler: %s", @@ -2286,7 +2327,8 @@ $.EventHandler.prototype = { */ function onMouseOut( tracker, event ) { var event = $.getEvent( event ), - delegate = THIS[ tracker.hash ]; + delegate = THIS[ tracker.hash ], + propagate; if ( $.Browser.vendor == $.BROWSERS.IE && delegate.capturing && @@ -2312,12 +2354,16 @@ $.EventHandler.prototype = { if ( tracker.exitHandler ) { try { - tracker.exitHandler( + propagate = tracker.exitHandler( tracker, getMouseRelative( event, tracker.element ), delegate.buttonDown, IS_BUTTON_DOWN ); + + if( propagate === false ){ + $.cancelEvent( event ); + } } catch ( e ) { $.console.error( "%s while executing exit handler: %s", @@ -2336,7 +2382,8 @@ $.EventHandler.prototype = { */ function onMouseDown( tracker, event ) { var event = $.getEvent( event ), - delegate = THIS[ tracker.hash ]; + delegate = THIS[ tracker.hash ], + propagate; if ( event.button == 2 ) { return; @@ -2350,10 +2397,13 @@ $.EventHandler.prototype = { if ( tracker.pressHandler ) { try { - tracker.pressHandler( + propagate = tracker.pressHandler( tracker, getMouseRelative( event, tracker.element ) ); + if( propagate === false ){ + $.cancelEvent( event ); + } } catch (e) { $.console.error( "%s while executing press handler: %s", @@ -2420,7 +2470,8 @@ $.EventHandler.prototype = { //were we inside the tracked element when we were pressed insideElementPress = delegate.buttonDown, //are we still inside the tracked element when we released - insideElementRelease = delegate.insideElement; + insideElementRelease = delegate.insideElement, + propagate; if ( event.button == 2 ) { return; @@ -2430,12 +2481,15 @@ $.EventHandler.prototype = { if ( tracker.releaseHandler ) { try { - tracker.releaseHandler( + propagate = tracker.releaseHandler( tracker, getMouseRelative( event, tracker.element ), insideElementPress, insideElementRelease ); + if( propagate === false ){ + $.cancelEvent( event ); + } } catch (e) { $.console.error( "%s while executing release handler: %s", @@ -2544,7 +2598,8 @@ $.EventHandler.prototype = { * @inner */ function onMouseWheelSpin( tracker, event ) { - var nDelta = 0; + var nDelta = 0, + propagate; if ( !event ) { // For IE, access the global (window) event object event = window.event; @@ -2565,12 +2620,15 @@ $.EventHandler.prototype = { if ( tracker.scrollHandler ) { try { - tracker.scrollHandler( + propagate = tracker.scrollHandler( tracker, getMouseRelative( event, tracker.element ), nDelta, event.shiftKey ); + if( propagate === false ){ + $.cancelEvent( event ); + } } catch (e) { $.console.error( "%s while executing scroll handler: %s", @@ -2579,8 +2637,6 @@ $.EventHandler.prototype = { e ); } - - $.cancelEvent( event ); } }; @@ -2591,7 +2647,8 @@ $.EventHandler.prototype = { */ function handleMouseClick( tracker, event ) { var event = $.getEvent( event ), - delegate = THIS[ tracker.hash ]; + delegate = THIS[ tracker.hash ], + propagate; if ( event.button == 2 ) { return; @@ -2605,12 +2662,15 @@ $.EventHandler.prototype = { if ( tracker.clickHandler ) { try { - tracker.clickHandler( + propagate = tracker.clickHandler( tracker, getMouseRelative( event, tracker.element ), quick, event.shiftKey ); + if( propagate === false ){ + $.cancelEvent( event ); + } } catch ( e ) { $.console.error( "%s while executing click handler: %s", @@ -2631,18 +2691,22 @@ $.EventHandler.prototype = { var event = $.getEvent( event ), delegate = THIS[ tracker.hash ], point = getMouseAbsolute( event ), - delta = point.minus( delegate.lastPoint ); + delta = point.minus( delegate.lastPoint ), + propagate; delegate.lastPoint = point; if ( tracker.dragHandler ) { try { - tracker.dragHandler( + propagate = tracker.dragHandler( tracker, getMouseRelative( event, tracker.element ), delta, event.shiftKey ); + if( propagate === false ){ + $.cancelEvent( event ); + } } catch (e) { $.console.error( "%s while executing drag handler: %s", @@ -2652,7 +2716,6 @@ $.EventHandler.prototype = { ); } - $.cancelEvent( event ); } }; @@ -3131,16 +3194,25 @@ $.Viewer = function( options ) { delete options.config; } + //Public properties //Allow the options object to override global defaults $.extend( true, this, { - id: options.id, - hash: options.id, - overlays: [], - overlayControls: [], + //internal state and dom identifiers + id: options.id, + hash: options.id, + + //dom nodes + element: null, + canvas: null, + container: null, + + //TODO: not sure how to best describe these + overlays: [], + overlayControls:[], //private state properties - previousBody: [], + previousBody: [], //This was originally initialized in the constructor and so could never //have anything in it. now it can because we allow it to be specified @@ -3156,13 +3228,76 @@ $.Viewer = function( options ) { drawer: null, viewport: null, navigator: null, + + //UI image resources + //TODO: rename navImages to uiImages + navImages: null, + + //interface button controls + buttons: null, + + //TODO: this is defunct so safely remove it profiler: null }, $.DEFAULT_SETTINGS, options ); + //Private state properties + THIS[ this.hash ] = { + "fsBoundsDelta": new $.Point( 1, 1 ), + "prevContainerSize": null, + "lastOpenStartTime": 0, + "lastOpenEndTime": 0, + "animating": false, + "forceRedraw": false, + "mouseInside": false, + "group": null, + // whether we should be continuously zooming + "zooming": false, + // how much we should be continuously zooming by + "zoomFactor": null, + "lastZoomTime": null, + // did we decide this viewer has a sequence of tile sources + "sequenced": false, + "sequence": 0 + }; + + //Inherit some behaviors and properties $.EventHandler.call( this ); $.ControlDock.call( this, options ); + //Deal with tile sources + var initialTileSource, + customTileSource; + + if ( this.xmlPath ){ + //Deprecated option. Now it is preferred to use the tileSources option + this.tileSources = [ this.xmlPath ]; + } + + if ( this.tileSources ){ + //tileSources is a complex option... + //It can be a string, object, function, or an array of any of these. + // - A String implies a DZI + // - An Srray of Objects implies a simple image + // - A Function implies a custom tile source callback + // - An Array that is not an Array of simple Objects implies a sequence + // of tile sources which can be any of the above + if( $.isArray( this.tileSources ) ){ + if( $.isPlainObject( this.tileSources[ 0 ] ) ){ + //This is a non-sequenced legacy tile source + initialTileSource = this.tileSources; + } else { + //Sequenced tile source + initialTileSource = this.tileSources[ 0 ]; + THIS[ this.hash ].sequenced = true; + } + } else { + initialTileSource = this.tileSources; + } + + this.openTileSource( initialTileSource ); + } + this.element = this.element || document.getElementById( this.id ); this.canvas = $.makeNeutralElement( "div" ); @@ -3184,7 +3319,7 @@ $.Viewer = function( options ) { container.textAlign = "left"; // needed to protect against }( this.container.style )); - this.container.insertBefore( this.canvas, this.container.firstChild); + this.container.insertBefore( this.canvas, this.container.firstChild ); this.element.appendChild( this.container ); //Used for toggling between fullscreen and default container size @@ -3195,16 +3330,6 @@ $.Viewer = function( options ) { this.bodyOverflow = document.body.style.overflow; this.docOverflow = document.documentElement.style.overflow; - THIS[ this.hash ] = { - "fsBoundsDelta": new $.Point( 1, 1 ), - "prevContainerSize": null, - "lastOpenStartTime": 0, - "lastOpenEndTime": 0, - "animating": false, - "forceRedraw": false, - "mouseInside": false - }; - this.innerTracker = new $.MouseTracker({ element: this.canvas, clickTimeThreshold: this.clickTimeThreshold, @@ -3225,16 +3350,6 @@ $.Viewer = function( options ) { }).setTracking( this.mouseNavEnabled ? true : false ); // always tracking - //private state properties - $.extend( THIS[ this.hash ], { - "group": null, - // whether we should be continuously zooming - "zooming": false, - // how much we should be continuously zooming by - "zoomFactor": null, - "lastZoomTime": null - }); - ////////////////////////////////////////////////////////////////////////// // Navigation Controls ////////////////////////////////////////////////////////////////////////// @@ -3247,16 +3362,15 @@ $.Viewer = function( options ) { onFullPageHandler = $.delegate( this, onFullPage ), onFocusHandler = $.delegate( this, onFocus ), onBlurHandler = $.delegate( this, onBlur ), - navImages = this.navImages; + onNextHandler = $.delegate( this, onNext ), + onPreviousHandler = $.delegate( this, onPrevious ), + navImages = this.navImages, + buttons = []; - this.zoomInButton = null; - this.zoomOutButton = null; - this.goHomeButton = null; - this.fullPageButton = null; if( this.showNavigationControl ){ - this.zoomInButton = new $.Button({ + buttons.push( this.zoomInButton = new $.Button({ clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, tooltip: $.getString( "Tooltips.ZoomIn" ), @@ -3271,9 +3385,9 @@ $.Viewer = function( options ) { onExit: endZoomingHandler, onFocus: onFocusHandler, onBlur: onBlurHandler - }); + })); - this.zoomOutButton = new $.Button({ + buttons.push( this.zoomOutButton = new $.Button({ clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, tooltip: $.getString( "Tooltips.ZoomOut" ), @@ -3288,9 +3402,9 @@ $.Viewer = function( options ) { onExit: endZoomingHandler, onFocus: onFocusHandler, onBlur: onBlurHandler - }); + })); - this.goHomeButton = new $.Button({ + buttons.push( this.homeButton = new $.Button({ clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, tooltip: $.getString( "Tooltips.Home" ), @@ -3301,9 +3415,9 @@ $.Viewer = function( options ) { onRelease: onHomeHandler, onFocus: onFocusHandler, onBlur: onBlurHandler - }); + })); - this.fullPageButton = new $.Button({ + buttons.push( this.fullPageButton = new $.Button({ clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, tooltip: $.getString( "Tooltips.FullPage" ), @@ -3314,17 +3428,44 @@ $.Viewer = function( options ) { onRelease: onFullPageHandler, onFocus: onFocusHandler, onBlur: onBlurHandler - }); + })); - this.buttons = new $.ButtonGroup({ + if( THIS[ this.hash ].sequenced ){ + + buttons.push( this.previousButton = new $.Button({ + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.PreviousPage" ), + srcRest: resolveUrl( this.prefixUrl, navImages.previous.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.previous.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.previous.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.previous.DOWN ), + onRelease: onPreviousHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + })); + + buttons.push( this.nextButton = new $.Button({ + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.NextPage" ), + srcRest: resolveUrl( this.prefixUrl, navImages.next.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.next.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.next.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.next.DOWN ), + onRelease: onNextHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + })); + + this.previousButton.disable(); + + } + + this.buttons = new $.ButtonGroup({ + buttons: buttons, clickTimeThreshold: this.clickTimeThreshold, - clickDistThreshold: this.clickDistThreshold, - buttons: [ - this.zoomInButton, - this.zoomOutButton, - this.goHomeButton, - this.fullPageButton - ] + clickDistThreshold: this.clickDistThreshold }); this.navControl = this.buttons.element; @@ -3346,73 +3487,10 @@ $.Viewer = function( options ) { ); } - //Instantiate a navigator if configured - if ( this.showNavigator ){ - this.navigator = new $.Navigator({ - id: this.navigatorElement, - position: this.navigatorPosition, - sizeRatio: this.navigatorSizeRatio, - height: this.navigatorHeight, - width: this.navigatorWidth, - tileSources: this.tileSources, - prefixUrl: this.prefixUrl, - overlays: this.overlays, - viewer: this - }); - } - window.setTimeout( function(){ beginControlsAutoHide( _this ); }, 1 ); // initial fade out - var initialTileSource, - customTileSource; - - if ( this.xmlPath ){ - //Deprecated option. Now it is preferred to use the tileSources option - this.tileSources = [ this.xmlPath ]; - } - - if ( this.tileSources ){ - //tileSource is a complex option... - //It can be a string, object, function, or an array of any of these. - //A string implies a DZI - //An object implies a simple image - //A function implies a custom tile source callback - //An array implies a sequence of tile sources which can be any of the - //above - if( $.isArray( this.tileSources ) ){ - if( $.isPlainObject( this.tileSources[ 0 ] ) ){ - //This is a non-sequenced legacy tile source - initialTileSource = this.tileSources; - } else { - //Sequenced tile source - initialTileSource = this.tileSources[ 0 ]; - } - } else { - initialTileSource = this.tileSources - } - - if ( $.type( initialTileSource ) == 'string') { - //Standard DZI format - this.openDzi( initialTileSource ); - } else if ( $.isArray( initialTileSource ) ){ - //Legacy image pyramid - this.open( new $.LegacyTileSource( initialTileSource ) ); - } else if ( $.isPlainObject( initialTileSource ) && $.isFunction( initialTileSource.getTileUrl ) ){ - //Custom tile source - customTileSource = new $.TileSource( - initialTileSource.width, - initialTileSource.height, - initialTileSource.tileSize, - initialTileSource.tileOverlap, - initialTileSource.minLevel, - initialTileSource.maxLevel - ); - customTileSource.getTileUrl = initialTileSource.getTileUrl; - this.open( customTileSource ); - } - } }; $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, { @@ -3442,7 +3520,8 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, dzi, function( source ){ _this.open( source ); - } + }, + this.tileHost ); return this; }, @@ -3453,10 +3532,34 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, * @return {OpenSeadragon.Viewer} Chainable. */ openTileSource: function ( tileSource ) { - var _this = this; - window.setTimeout( function () { - _this.open( tileSource ); - }, 1 ); + var _this = this, + customTileSource; + + setTimeout(function(){ + if ( $.type( tileSource ) == 'string') { + //Standard DZI format + _this.openDzi( tileSource ); + } else if ( $.isArray( tileSource ) ){ + //Legacy image pyramid + _this.open( new $.LegacyTileSource( tileSource ) ); + } else if ( $.isPlainObject( tileSource ) && $.isFunction( tileSource.getTileUrl ) ){ + //Custom tile source + customTileSource = new $.TileSource( + tileSource.width, + tileSource.height, + tileSource.tileSize, + tileSource.tileOverlap, + tileSource.minLevel, + tileSource.maxLevel + ); + customTileSource.getTileUrl = tileSource.getTileUrl; + _this.open( customTileSource ); + } else { + //can assume it's already a tile source implementation + _this.open( tileSource ); + } + }, 1); + return this; }, @@ -3471,7 +3574,7 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, i; if ( this.source ) { - this.close(); + this.close( ); } // to ignore earlier opens @@ -3491,7 +3594,7 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, this.source = source; } - this.viewport = new $.Viewport({ + this.viewport = this.viewport ? this.viewport : new $.Viewport({ containerSize: THIS[ this.hash ].prevContainerSize, contentSize: this.source.dimensions, springStiffness: this.springStiffness, @@ -3502,6 +3605,9 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, wrapHorizontal: this.wrapHorizontal, wrapVertical: this.wrapVertical }); + if( this.preserveVewport ){ + this.viewport.resetContentSize( this.source.dimensions ); + } this.drawer = new $.Drawer({ source: this.source, @@ -3519,6 +3625,22 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, minPixelRatio: this.minPixelRatio }); + //Instantiate a navigator if configured + if ( this.showNavigator && ! this.navigator ){ + this.navigator = new $.Navigator({ + id: this.navigatorElement, + position: this.navigatorPosition, + sizeRatio: this.navigatorSizeRatio, + height: this.navigatorHeight, + width: this.navigatorWidth, + tileSources: this.tileSources, + tileHost: this.tileHost, + prefixUrl: this.prefixUrl, + overlays: this.overlays, + viewer: this + }); + } + //this.profiler = new $.Profiler(); THIS[ this.hash ].animating = false; @@ -3559,6 +3681,11 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, } VIEWERS[ this.hash ] = this; this.raiseEvent( "open" ); + + if( this.navigator ){ + this.navigator.open( source ); + } + return this; }, @@ -3567,10 +3694,10 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, * @name OpenSeadragon.Viewer.prototype.close * @return {OpenSeadragon.Viewer} Chainable. */ - close: function () { + close: function ( ) { this.source = null; - this.viewport = null; this.drawer = null; + this.viewport = this.preserveViewport ? this.viewport : null; //this.profiler = null; this.canvas.innerHTML = ""; @@ -3794,6 +3921,9 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, viewer = VIEWERS[ hash ]; if( viewer !== this && viewer != this.navigator ){ viewer.open( viewer.source ); + if( viewer.navigator ){ + viewer.navigator.open( viewer.source ); + } } } } @@ -3829,10 +3959,11 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, }); + + /////////////////////////////////////////////////////////////////////////////// // Schedulers provide the general engine for animation /////////////////////////////////////////////////////////////////////////////// - function scheduleUpdate( viewer, updateFunc, prevUpdateTime ){ var currentTime, targetTime, @@ -3855,6 +3986,7 @@ function scheduleUpdate( viewer, updateFunc, prevUpdateTime ){ }, deltaTime ); }; + //provides a sequence in the fade animation function scheduleControlsFade( viewer ) { window.setTimeout( function(){ @@ -3862,6 +3994,7 @@ function scheduleControlsFade( viewer ) { }, 20); }; + //initiates an animation to hide the controls function beginControlsAutoHide( viewer ) { if ( !viewer.autoHideControls ) { @@ -3903,6 +4036,7 @@ function updateControlsFade( viewer ) { } }; + //stop the fade animation on the controls and show them function abortControlsAutoHide( viewer ) { var i; @@ -3913,6 +4047,7 @@ function abortControlsAutoHide( viewer ) { }; + /////////////////////////////////////////////////////////////////////////////// // Default view event handlers. /////////////////////////////////////////////////////////////////////////////// @@ -3941,6 +4076,12 @@ function onCanvasClick( tracker, position, quick, shift ) { function onCanvasDrag( tracker, position, delta, shift ) { if ( this.viewport ) { + if( !this.panHorizontal ){ + delta.x = 0; + } + if( !this.panVertical ){ + delta.y = 0; + } this.viewport.panBy( this.viewport.deltaPointsFromPixels( delta.negate() @@ -3965,6 +4106,8 @@ function onCanvasScroll( tracker, position, scroll, shift ) { ); this.viewport.applyConstraints(); } + //cancels event + return false; }; function onContainerExit( tracker, position, buttonDownElement, buttonDownAny ) { @@ -4060,36 +4203,42 @@ function updateOnce( viewer ) { //viewer.profiler.endUpdate(); }; + + /////////////////////////////////////////////////////////////////////////////// // Navigation Controls /////////////////////////////////////////////////////////////////////////////// - function resolveUrl( prefix, url ) { return prefix ? prefix + url : url; }; + function beginZoomingIn() { THIS[ this.hash ].lastZoomTime = +new Date(); THIS[ this.hash ].zoomFactor = this.zoomPerSecond; THIS[ this.hash ].zooming = true; scheduleZoom( this ); -} +}; + function beginZoomingOut() { THIS[ this.hash ].lastZoomTime = +new Date(); THIS[ this.hash ].zoomFactor = 1.0 / this.zoomPerSecond; THIS[ this.hash ].zooming = true; scheduleZoom( this ); -} +}; + function endZooming() { THIS[ this.hash ].zooming = false; -} +}; + function scheduleZoom( viewer ) { window.setTimeout( $.delegate( viewer, doZoom ), 10 ); -} +}; + function doZoom() { var currentTime, @@ -4108,6 +4257,7 @@ function doZoom() { } }; + function doSingleZoomIn() { if ( this.viewport ) { THIS[ this.hash ].zooming = false; @@ -4118,6 +4268,7 @@ function doSingleZoomIn() { } }; + function doSingleZoomOut() { if ( this.viewport ) { THIS[ this.hash ].zooming = false; @@ -4128,17 +4279,20 @@ function doSingleZoomOut() { } }; + function lightUp() { this.buttons.emulateEnter(); this.buttons.emulateExit(); }; + function onHome() { if ( this.viewport ) { this.viewport.goHome(); } }; + function onFullPage() { this.setFullPage( !this.isFullPage() ); // correct for no mouseout event on change @@ -4149,6 +4303,49 @@ function onFullPage() { } }; + +function onPrevious(){ + var previous = THIS[ this.hash ].sequence - 1, + preserveVewport = true; + if( previous >= 0 ){ + + THIS[ this.hash ].sequence = previous; + + if( 0 === previous ){ + //Disable previous button + this.previousButton.disable(); + } + if( this.tileSources.length > 0 ){ + //Enable next button + this.nextButton.enable(); + } + + this.openTileSource( this.tileSources[ previous ] ); + } +}; + + +function onNext(){ + var next = THIS[ this.hash ].sequence + 1, + preserveVewport = true; + if( this.tileSources.length > next ){ + + THIS[ this.hash ].sequence = next; + + if( ( this.tileSources.length - 1 ) === next ){ + //Disable next button + this.nextButton.disable(); + } + if( next > 0 ){ + //Enable previous button + this.previousButton.enable(); + } + + this.openTileSource( this.tileSources[ next ] ); + } +}; + + }( OpenSeadragon )); (function( $ ){ @@ -4215,13 +4412,31 @@ $.Navigator = function( options ){ style.cssFloat = 'left'; //Firefox style.styleFloat = 'left'; //IE style.zIndex = 999999999; + style.cursor = 'default'; }( this.displayRegion.style )); + this.element.innerTracker = new $.MouseTracker({ + element: this.element, + scrollHandler: function(){ + //dont scroll the page up and down if the user is scrolling + //in the navigator + return false; + } + }).setTracking( true ); + this.displayRegion.innerTracker = new $.MouseTracker({ element: this.displayRegion, clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, + clickHandler: $.delegate( this, onCanvasClick ), + dragHandler: $.delegate( this, onCanvasDrag ), + releaseHandler: $.delegate( this, onCanvasRelease ), + scrollHandler: $.delegate( this, onCanvasScroll ), focusHandler: function(){ + var point = $.getElementPosition( _this.viewer.element ); + + window.scrollTo( 0, point.y ); + _this.viewer.setControlsEnabled( true ); (function( style ){ style.border = '2px solid #437AB2'; @@ -4241,12 +4456,15 @@ $.Navigator = function( options ){ switch( keyCode ){ case 61://=|+ _this.viewer.viewport.zoomBy(1.1); + _this.viewer.viewport.applyConstraints(); return false; case 45://-|_ _this.viewer.viewport.zoomBy(0.9); + _this.viewer.viewport.applyConstraints(); return false; case 48://0|) _this.viewer.viewport.goHome(); + _this.viewer.viewport.applyConstraints(); return false; case 119://w case 87://W @@ -4254,6 +4472,7 @@ $.Navigator = function( options ){ shiftKey ? _this.viewer.viewport.zoomBy(1.1): _this.viewer.viewport.panBy(new $.Point(0, -0.05)); + _this.viewer.viewport.applyConstraints(); return false; case 115://s case 83://S @@ -4261,14 +4480,17 @@ $.Navigator = function( options ){ shiftKey ? _this.viewer.viewport.zoomBy(0.9): _this.viewer.viewport.panBy(new $.Point(0, 0.05)); + _this.viewer.viewport.applyConstraints(); return false; case 97://a case 37://left arrow _this.viewer.viewport.panBy(new $.Point(-0.05, 0)); + _this.viewer.viewport.applyConstraints(); return false; case 100://d case 39://right arrow _this.viewer.viewport.panBy(new $.Point(0.05, 0)); + _this.viewer.viewport.applyConstraints(); return false; default: //console.log( 'navigator keycode %s', keyCode ); @@ -4309,25 +4531,75 @@ $.extend( $.Navigator.prototype, $.EventHandler.prototype, $.Viewer.prototype, { update: function( viewport ){ - var bounds = viewport.getBounds( true ), - topleft = this.viewport.pixelFromPoint( bounds.getTopLeft() ) + var bounds, + topleft, + bottomright; + + if( viewport && this.viewport ){ + bounds = viewport.getBounds( true ); + topleft = this.viewport.pixelFromPoint( bounds.getTopLeft() ); bottomright = this.viewport.pixelFromPoint( bounds.getBottomRight() ); - //update style for navigator-box - (function(style){ + //update style for navigator-box + (function(style){ - style.top = topleft.y + 'px'; - style.left = topleft.x + 'px'; - style.width = ( Math.abs( topleft.x - bottomright.x ) - 3 ) + 'px'; - style.height = ( Math.abs( topleft.y - bottomright.y ) - 3 ) + 'px'; + style.top = topleft.y + 'px'; + style.left = topleft.x + 'px'; + style.width = ( Math.abs( topleft.x - bottomright.x ) - 3 ) + 'px'; + style.height = ( Math.abs( topleft.y - bottomright.y ) - 3 ) + 'px'; - }( this.displayRegion.style )); + }( this.displayRegion.style )); + } } }); +function onCanvasClick( tracker, position, quick, shift ) { + this.displayRegion.focus(); +}; + + +function onCanvasDrag( tracker, position, delta, shift ) { + if ( this.viewer.viewport ) { + if( !this.panHorizontal ){ + delta.x = 0; + } + if( !this.panVertical ){ + delta.y = 0; + } + this.viewer.viewport.panBy( + this.viewport.deltaPointsFromPixels( + delta + ) + ); + } +}; + + +function onCanvasRelease( tracker, position, insideElementPress, insideElementRelease ) { + if ( insideElementPress && this.viewer.viewport ) { + this.viewer.viewport.applyConstraints(); + } +}; + +function onCanvasScroll( tracker, position, scroll, shift ) { + var factor; + if ( this.viewer.viewport ) { + factor = Math.pow( this.zoomPerScroll, scroll ); + this.viewer.viewport.zoomBy( + factor, + //this.viewport.pointFromPixel( position, true ) + this.viewport.getCenter() + ); + this.viewer.viewport.applyConstraints(); + } + //cancels event + return false; +}; + + }( OpenSeadragon )); (function( $ ){ @@ -4336,28 +4608,30 @@ $.extend( $.Navigator.prototype, $.EventHandler.prototype, $.Viewer.prototype, { // pythons gettext might be a reasonable approach. var I18N = { Errors: { - Failure: "Sorry, but Seadragon Ajax can't run on your browser!\n" + - "Please try using IE 7 or Firefox 3.\n", - Dzc: "Sorry, we don't support Deep Zoom Collections!", - Dzi: "Hmm, this doesn't appear to be a valid Deep Zoom Image.", - Xml: "Hmm, this doesn't appear to be a valid Deep Zoom Image.", - Empty: "You asked us to open nothing, so we did just that.", - ImageFormat: "Sorry, we don't support {0}-based Deep Zoom Images.", - Security: "It looks like a security restriction stopped us from " + - "loading this Deep Zoom Image.", - Status: "This space unintentionally left blank ({0} {1}).", - Unknown: "Whoops, something inexplicably went wrong. Sorry!" + Failure: "Sorry, but Seadragon Ajax can't run on your browser!\n" + + "Please try using IE 7 or Firefox 3.\n", + Dzc: "Sorry, we don't support Deep Zoom Collections!", + Dzi: "Hmm, this doesn't appear to be a valid Deep Zoom Image.", + Xml: "Hmm, this doesn't appear to be a valid Deep Zoom Image.", + Empty: "You asked us to open nothing, so we did just that.", + ImageFormat: "Sorry, we don't support {0}-based Deep Zoom Images.", + Security: "It looks like a security restriction stopped us from " + + "loading this Deep Zoom Image.", + Status: "This space unintentionally left blank ({0} {1}).", + Unknown: "Whoops, something inexplicably went wrong. Sorry!" }, Messages: { - Loading: "Loading..." + Loading: "Loading..." }, Tooltips: { - FullPage: "Toggle full page", - Home: "Go home", - ZoomIn: "Zoom in", - ZoomOut: "Zoom out" + FullPage: "Toggle full page", + Home: "Go home", + ZoomIn: "Zoom in", + ZoomOut: "Zoom out", + NextPage: "Next page", + PreviousPage: "Previous page" } }; @@ -4371,12 +4645,13 @@ $.extend( $, { getString: function( prop ) { var props = prop.split('.'), - string = I18N, + string = null, args = arguments, i; for ( i = 0; i < props.length; i++ ) { - string = string[ props[ i ] ] || {}; // in case not a subproperty + // in case not a subproperty + string = I18N[ props[ i ] ] || {}; } if ( typeof( string ) != "string" ) { @@ -4848,8 +5123,8 @@ $.LegacyTileSource.prototype = { var levelScale = NaN; if ( level >= this.minLevel && level <= this.maxLevel ){ levelScale = - this.files[ level ].height / - this.files[ this.maxLevel ].height; + this.files[ level ].width / + this.files[ this.maxLevel ].width; } return levelScale; }, @@ -4900,9 +5175,9 @@ $.LegacyTileSource.prototype = { py = ( y === 0 ) ? 0 : this.files[ level ].height, sx = this.files[ level ].width, sy = this.files[ level ].height, - scale = Math.max( - 1.0 / dimensionsScaled.x, - 1.0 / dimensionsScaled.y + scale = 1.0 / ( this.width >= this.height ? + dimensionsScaled.y : + dimensionsScaled.x ); sx = Math.min( sx, dimensionsScaled.x - px ); @@ -5173,6 +5448,18 @@ $.extend( $.Button.prototype, $.EventHandler.prototype, { */ notifyGroupExit: function() { outTo( this, $.ButtonState.REST ); + }, + + disable: function(){ + this.notifyGroupExit(); + this.element.disabled = true; + $.setElementOpacity( this.element, 0.2, true ); + }, + + enable: function(){ + this.element.disabled = false; + $.setElementOpacity( this.element, 1.0, true ); + this.notifyGroupEnter(); } }); @@ -5218,6 +5505,11 @@ function stopFading( button ) { }; function inTo( button, newState ) { + + if( button.element.disabled ){ + return; + } + if ( newState >= $.ButtonState.GROUP && button.currentState == $.ButtonState.REST ) { stopFading( button ); @@ -5239,6 +5531,11 @@ function inTo( button, newState ) { function outTo( button, newState ) { + + if( button.element.disabled ){ + return; + } + if ( newState <= $.ButtonState.HOVER && button.currentState == $.ButtonState.DOWN ) { button.imgDown.style.visibility = "hidden"; @@ -5678,7 +5975,7 @@ $.Tile = function(level, x, y, bounds, exists, url) { this.loading = false; this.element = null; - this.image = null; + this.image = null; this.style = null; this.position = null; @@ -5714,7 +6011,7 @@ $.Tile.prototype = { var position = this.position.apply( Math.floor ), size = this.size.apply( Math.ceil ); - if ( !this.loaded ) { + if ( !this.loaded || !this.image ) { $.console.warn( "Attempting to draw tile %s when it's not yet loaded.", this.toString() @@ -5723,9 +6020,9 @@ $.Tile.prototype = { } if ( !this.element ) { - this.element = $.makeNeutralElement("img"); - this.element.src = this.url; - this.style = this.element.style; + this.element = $.makeNeutralElement("img"); + this.element.src = this.url; + this.style = this.element.style; this.style.position = "absolute"; this.style.msInterpolationMode = "nearest-neighbor"; @@ -5755,14 +6052,13 @@ $.Tile.prototype = { var position = this.position, size = this.size; - if ( !this.loaded ) { + if ( !this.loaded || !this.image ) { $.console.warn( "Attempting to draw tile %s when it's not yet loaded.", this.toString() ); return; } - context.globalAlpha = this.opacity; context.drawImage( this.image, position.x, position.y, size.x, size.y ); }, @@ -6958,9 +7254,6 @@ $.Viewport = function( options ) { }, options ); - - this.contentAspect = this.contentSize.x / this.contentSize.y; - this.contentHeight = this.contentSize.y / this.contentSize.x; this.centerSpringX = new $.Spring({ initial: 0, springStiffness: this.springStiffness, @@ -6976,19 +7269,39 @@ $.Viewport = function( options ) { springStiffness: this.springStiffness, animationTime: this.animationTime }); - this.homeBounds = new $.Rect( 0, 0, 1, this.contentHeight ); + this.resetContentSize( this.contentSize ); this.goHome( true ); + //this.fitHorizontally( true ); this.update(); }; $.Viewport.prototype = { + resetContentSize: function( contentSize ){ + this.contentSize = contentSize; + this.contentAspectX = this.contentSize.x / this.contentSize.y; + this.contentAspectY = this.contentSize.y / this.contentSize.x; + this.homeBounds = new $.Rect( + 0, + 0, + 1, + this.contentAspectY + ); + this.fitWidthBounds = new $.Rect( 0, 0, 1, this.contentAspectX ); + this.fitHeightBounds = new $.Rect( 0, 0, 1, this.contentAspectY ); + }, + /** * @function */ getHomeZoom: function() { - var aspectFactor = this.contentAspect / this.getAspectRatio(); + + var aspectFactor = Math.min( + this.contentAspectX, + this.contentAspectY + ) / this.getAspectRatio(); + return ( aspectFactor >= 1 ) ? 1 : aspectFactor; @@ -7141,7 +7454,7 @@ $.Viewport.prototype = { left = bounds.x + bounds.width; right = 1 - bounds.x; top = bounds.y + bounds.height; - bottom = this.contentHeight - bounds.y; + bottom = this.contentAspectY - bounds.y; if ( this.wrapHorizontal ) { //do nothing @@ -7223,15 +7536,22 @@ $.Viewport.prototype = { this.containerSize.x / newBounds.width ); - this.zoomTo( newZoom, referencePoint, immediately ); }, + + /** + * @function + * @param {Boolean} immediately + */ + goHome: function( immediately ) { + return this.fitVertically( immediately ); + }, /** * @function * @param {Boolean} immediately */ - goHome: function( immediately ) { + fitVertically: function( immediately ) { var center = this.getCenter(); if ( this.wrapHorizontal ) { @@ -7242,8 +7562,8 @@ $.Viewport.prototype = { if ( this.wrapVertical ) { center.y = ( - this.contentHeight + ( center.y % this.contentHeight ) - ) % this.contentHeight; + this.contentAspectY + ( center.y % this.contentAspectY ) + ) % this.contentAspectY; this.centerSpringY.resetTo( center.y ); this.centerSpringY.update(); } @@ -7251,6 +7571,31 @@ $.Viewport.prototype = { this.fitBounds( this.homeBounds, immediately ); }, + /** + * @function + * @param {Boolean} immediately + */ + fitHorizontally: function( immediately ) { + var center = this.getCenter(); + + if ( this.wrapHorizontal ) { + center.x = ( + this.contentAspectX + ( center.x % this.contentAspectX ) + ) % this.contentAspectX; + this.centerSpringX.resetTo( center.x ); + this.centerSpringX.update(); + } + + if ( this.wrapVertical ) { + center.y = ( 1 + ( center.y % 1 ) ) % 1; + this.centerSpringY.resetTo( center.y ); + this.centerSpringY.update(); + } + + this.fitBounds( this.fitWidthBounds, immediately ); + }, + + /** * @function * @param {OpenSeadragon.Point} delta diff --git a/src/button.js b/src/button.js index d1220332..ec3217e6 100644 --- a/src/button.js +++ b/src/button.js @@ -223,6 +223,18 @@ $.extend( $.Button.prototype, $.EventHandler.prototype, { */ notifyGroupExit: function() { outTo( this, $.ButtonState.REST ); + }, + + disable: function(){ + this.notifyGroupExit(); + this.element.disabled = true; + $.setElementOpacity( this.element, 0.2, true ); + }, + + enable: function(){ + this.element.disabled = false; + $.setElementOpacity( this.element, 1.0, true ); + this.notifyGroupEnter(); } }); @@ -268,6 +280,11 @@ function stopFading( button ) { }; function inTo( button, newState ) { + + if( button.element.disabled ){ + return; + } + if ( newState >= $.ButtonState.GROUP && button.currentState == $.ButtonState.REST ) { stopFading( button ); @@ -289,6 +306,11 @@ function inTo( button, newState ) { function outTo( button, newState ) { + + if( button.element.disabled ){ + return; + } + if ( newState <= $.ButtonState.HOVER && button.currentState == $.ButtonState.DOWN ) { button.imgDown.style.visibility = "hidden"; diff --git a/src/drawer.js b/src/drawer.js index 1d13bad2..4c629e22 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -10,7 +10,8 @@ var TIMEOUT = 5000, ( BROWSER == $.BROWSERS.FIREFOX ) || ( BROWSER == $.BROWSERS.OPERA ) || ( BROWSER == $.BROWSERS.SAFARI && BROWSER_VERSION >= 4 ) || - ( BROWSER == $.BROWSERS.CHROME && BROWSER_VERSION >= 2 ) + ( BROWSER == $.BROWSERS.CHROME && BROWSER_VERSION >= 2 ) || + ( BROWSER == $.BROWSERS.IE && BROWSER_VERSION >= 9 ) ) && ( !navigator.appVersion.match( 'Mobile' ) ), USE_CANVAS = $.isFunction( document.createElement( "canvas" ).getContext ) && diff --git a/src/legacytilesource.js b/src/legacytilesource.js index ea668d75..b551169e 100644 --- a/src/legacytilesource.js +++ b/src/legacytilesource.js @@ -49,8 +49,8 @@ $.LegacyTileSource.prototype = { var levelScale = NaN; if ( level >= this.minLevel && level <= this.maxLevel ){ levelScale = - this.files[ level ].height / - this.files[ this.maxLevel ].height; + this.files[ level ].width / + this.files[ this.maxLevel ].width; } return levelScale; }, @@ -101,9 +101,9 @@ $.LegacyTileSource.prototype = { py = ( y === 0 ) ? 0 : this.files[ level ].height, sx = this.files[ level ].width, sy = this.files[ level ].height, - scale = Math.max( - 1.0 / dimensionsScaled.x, - 1.0 / dimensionsScaled.y + scale = 1.0 / ( this.width >= this.height ? + dimensionsScaled.y : + dimensionsScaled.x ); sx = Math.min( sx, dimensionsScaled.x - px ); diff --git a/src/mousetracker.js b/src/mousetracker.js index 60e23747..bd0c359f 100644 --- a/src/mousetracker.js +++ b/src/mousetracker.js @@ -466,7 +466,7 @@ function triggerOthers( tracker, handler, event ) { var otherHash; for ( otherHash in ACTIVE ) { - if ( trackers.hasOwnProperty( otherHash ) && tracker.hash != otherHash ) { + if ( ACTIVE.hasOwnProperty( otherHash ) && tracker.hash != otherHash ) { handler( ACTIVE[ otherHash ], event ); } } @@ -479,13 +479,16 @@ */ function onFocus( tracker, event ){ //console.log( "focus %s", event ); + var propagate; if ( tracker.focusHandler ) { try { - tracker.focusHandler( + propagate = tracker.focusHandler( tracker, event ); - $.cancelEvent( event ); + if( propagate === false ){ + $.cancelEvent( event ); + } } catch ( e ) { $.console.error( "%s while executing key handler: %s", @@ -504,13 +507,16 @@ */ function onBlur( tracker, event ){ //console.log( "blur %s", event ); + var propagate; if ( tracker.blurHandler ) { try { - tracker.blurHandler( + propagate = tracker.blurHandler( tracker, event ); - $.cancelEvent( event ); + if( propagate === false ){ + $.cancelEvent( event ); + } } catch ( e ) { $.console.error( "%s while executing key handler: %s", @@ -537,7 +543,7 @@ event.keyCode ? event.keyCode : event.charCode, event.shiftKey ); - if( !propagate ){ + if( propagate === false ){ $.cancelEvent( event ); } } catch ( e ) { @@ -559,7 +565,8 @@ function onMouseOver( tracker, event ) { var event = $.getEvent( event ), - delegate = THIS[ tracker.hash ]; + delegate = THIS[ tracker.hash ], + propagate; if ( $.Browser.vendor == $.BROWSERS.IE && delegate.capturing && @@ -585,12 +592,15 @@ if ( tracker.enterHandler ) { try { - tracker.enterHandler( + propagate = tracker.enterHandler( tracker, getMouseRelative( event, tracker.element ), delegate.buttonDown, IS_BUTTON_DOWN ); + if( propagate === false ){ + $.cancelEvent( event ); + } } catch ( e ) { $.console.error( "%s while executing enter handler: %s", @@ -609,7 +619,8 @@ */ function onMouseOut( tracker, event ) { var event = $.getEvent( event ), - delegate = THIS[ tracker.hash ]; + delegate = THIS[ tracker.hash ], + propagate; if ( $.Browser.vendor == $.BROWSERS.IE && delegate.capturing && @@ -635,12 +646,16 @@ if ( tracker.exitHandler ) { try { - tracker.exitHandler( + propagate = tracker.exitHandler( tracker, getMouseRelative( event, tracker.element ), delegate.buttonDown, IS_BUTTON_DOWN ); + + if( propagate === false ){ + $.cancelEvent( event ); + } } catch ( e ) { $.console.error( "%s while executing exit handler: %s", @@ -659,7 +674,8 @@ */ function onMouseDown( tracker, event ) { var event = $.getEvent( event ), - delegate = THIS[ tracker.hash ]; + delegate = THIS[ tracker.hash ], + propagate; if ( event.button == 2 ) { return; @@ -673,10 +689,13 @@ if ( tracker.pressHandler ) { try { - tracker.pressHandler( + propagate = tracker.pressHandler( tracker, getMouseRelative( event, tracker.element ) ); + if( propagate === false ){ + $.cancelEvent( event ); + } } catch (e) { $.console.error( "%s while executing press handler: %s", @@ -743,7 +762,8 @@ //were we inside the tracked element when we were pressed insideElementPress = delegate.buttonDown, //are we still inside the tracked element when we released - insideElementRelease = delegate.insideElement; + insideElementRelease = delegate.insideElement, + propagate; if ( event.button == 2 ) { return; @@ -753,12 +773,15 @@ if ( tracker.releaseHandler ) { try { - tracker.releaseHandler( + propagate = tracker.releaseHandler( tracker, getMouseRelative( event, tracker.element ), insideElementPress, insideElementRelease ); + if( propagate === false ){ + $.cancelEvent( event ); + } } catch (e) { $.console.error( "%s while executing release handler: %s", @@ -867,7 +890,8 @@ * @inner */ function onMouseWheelSpin( tracker, event ) { - var nDelta = 0; + var nDelta = 0, + propagate; if ( !event ) { // For IE, access the global (window) event object event = window.event; @@ -888,12 +912,15 @@ if ( tracker.scrollHandler ) { try { - tracker.scrollHandler( + propagate = tracker.scrollHandler( tracker, getMouseRelative( event, tracker.element ), nDelta, event.shiftKey ); + if( propagate === false ){ + $.cancelEvent( event ); + } } catch (e) { $.console.error( "%s while executing scroll handler: %s", @@ -902,8 +929,6 @@ e ); } - - $.cancelEvent( event ); } }; @@ -914,7 +939,8 @@ */ function handleMouseClick( tracker, event ) { var event = $.getEvent( event ), - delegate = THIS[ tracker.hash ]; + delegate = THIS[ tracker.hash ], + propagate; if ( event.button == 2 ) { return; @@ -928,12 +954,15 @@ if ( tracker.clickHandler ) { try { - tracker.clickHandler( + propagate = tracker.clickHandler( tracker, getMouseRelative( event, tracker.element ), quick, event.shiftKey ); + if( propagate === false ){ + $.cancelEvent( event ); + } } catch ( e ) { $.console.error( "%s while executing click handler: %s", @@ -954,18 +983,22 @@ var event = $.getEvent( event ), delegate = THIS[ tracker.hash ], point = getMouseAbsolute( event ), - delta = point.minus( delegate.lastPoint ); + delta = point.minus( delegate.lastPoint ), + propagate; delegate.lastPoint = point; if ( tracker.dragHandler ) { try { - tracker.dragHandler( + propagate = tracker.dragHandler( tracker, getMouseRelative( event, tracker.element ), delta, event.shiftKey ); + if( propagate === false ){ + $.cancelEvent( event ); + } } catch (e) { $.console.error( "%s while executing drag handler: %s", @@ -975,7 +1008,6 @@ ); } - $.cancelEvent( event ); } }; diff --git a/src/navigator.js b/src/navigator.js index 96ffdf1d..497f6894 100644 --- a/src/navigator.js +++ b/src/navigator.js @@ -63,13 +63,31 @@ $.Navigator = function( options ){ style.cssFloat = 'left'; //Firefox style.styleFloat = 'left'; //IE style.zIndex = 999999999; + style.cursor = 'default'; }( this.displayRegion.style )); + this.element.innerTracker = new $.MouseTracker({ + element: this.element, + scrollHandler: function(){ + //dont scroll the page up and down if the user is scrolling + //in the navigator + return false; + } + }).setTracking( true ); + this.displayRegion.innerTracker = new $.MouseTracker({ element: this.displayRegion, clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, + clickHandler: $.delegate( this, onCanvasClick ), + dragHandler: $.delegate( this, onCanvasDrag ), + releaseHandler: $.delegate( this, onCanvasRelease ), + scrollHandler: $.delegate( this, onCanvasScroll ), focusHandler: function(){ + var point = $.getElementPosition( _this.viewer.element ); + + window.scrollTo( 0, point.y ); + _this.viewer.setControlsEnabled( true ); (function( style ){ style.border = '2px solid #437AB2'; @@ -89,12 +107,15 @@ $.Navigator = function( options ){ switch( keyCode ){ case 61://=|+ _this.viewer.viewport.zoomBy(1.1); + _this.viewer.viewport.applyConstraints(); return false; case 45://-|_ _this.viewer.viewport.zoomBy(0.9); + _this.viewer.viewport.applyConstraints(); return false; case 48://0|) _this.viewer.viewport.goHome(); + _this.viewer.viewport.applyConstraints(); return false; case 119://w case 87://W @@ -102,6 +123,7 @@ $.Navigator = function( options ){ shiftKey ? _this.viewer.viewport.zoomBy(1.1): _this.viewer.viewport.panBy(new $.Point(0, -0.05)); + _this.viewer.viewport.applyConstraints(); return false; case 115://s case 83://S @@ -109,14 +131,17 @@ $.Navigator = function( options ){ shiftKey ? _this.viewer.viewport.zoomBy(0.9): _this.viewer.viewport.panBy(new $.Point(0, 0.05)); + _this.viewer.viewport.applyConstraints(); return false; case 97://a case 37://left arrow _this.viewer.viewport.panBy(new $.Point(-0.05, 0)); + _this.viewer.viewport.applyConstraints(); return false; case 100://d case 39://right arrow _this.viewer.viewport.panBy(new $.Point(0.05, 0)); + _this.viewer.viewport.applyConstraints(); return false; default: //console.log( 'navigator keycode %s', keyCode ); @@ -157,23 +182,73 @@ $.extend( $.Navigator.prototype, $.EventHandler.prototype, $.Viewer.prototype, { update: function( viewport ){ - var bounds = viewport.getBounds( true ), - topleft = this.viewport.pixelFromPoint( bounds.getTopLeft() ) + var bounds, + topleft, + bottomright; + + if( viewport && this.viewport ){ + bounds = viewport.getBounds( true ); + topleft = this.viewport.pixelFromPoint( bounds.getTopLeft() ); bottomright = this.viewport.pixelFromPoint( bounds.getBottomRight() ); - //update style for navigator-box - (function(style){ + //update style for navigator-box + (function(style){ - style.top = topleft.y + 'px'; - style.left = topleft.x + 'px'; - style.width = ( Math.abs( topleft.x - bottomright.x ) - 3 ) + 'px'; - style.height = ( Math.abs( topleft.y - bottomright.y ) - 3 ) + 'px'; + style.top = topleft.y + 'px'; + style.left = topleft.x + 'px'; + style.width = ( Math.abs( topleft.x - bottomright.x ) - 3 ) + 'px'; + style.height = ( Math.abs( topleft.y - bottomright.y ) - 3 ) + 'px'; - }( this.displayRegion.style )); + }( this.displayRegion.style )); + } } }); +function onCanvasClick( tracker, position, quick, shift ) { + this.displayRegion.focus(); +}; + + +function onCanvasDrag( tracker, position, delta, shift ) { + if ( this.viewer.viewport ) { + if( !this.panHorizontal ){ + delta.x = 0; + } + if( !this.panVertical ){ + delta.y = 0; + } + this.viewer.viewport.panBy( + this.viewport.deltaPointsFromPixels( + delta + ) + ); + } +}; + + +function onCanvasRelease( tracker, position, insideElementPress, insideElementRelease ) { + if ( insideElementPress && this.viewer.viewport ) { + this.viewer.viewport.applyConstraints(); + } +}; + +function onCanvasScroll( tracker, position, scroll, shift ) { + var factor; + if ( this.viewer.viewport ) { + factor = Math.pow( this.zoomPerScroll, scroll ); + this.viewer.viewport.zoomBy( + factor, + //this.viewport.pointFromPixel( position, true ) + this.viewport.getCenter() + ); + this.viewer.viewport.applyConstraints(); + } + //cancels event + return false; +}; + + }( OpenSeadragon )); \ No newline at end of file diff --git a/src/openseadragon.js b/src/openseadragon.js index b0ebb4d9..3887dea6 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -188,6 +188,12 @@ * interactions include draging the image in a plane, and zooming in toward * and away from the image. * + * @param {Boolean} [options.preserveViewport=false] + * If the viewer has been configured with a sequence of tile sources, then + * normally navigating to through each image resets the viewport to 'home' + * position. If preserveViewport is set to true, then the viewport position + * is preserved when navigating between images in the sequence. + * * @param {String} [options.prefixUrl=''] * Appends the prefixUrl to navImages paths, which is very useful * since the default paths are rarely useful for production @@ -428,43 +434,50 @@ OpenSeadragon = window.OpenSeadragon || function( options ){ * @static */ DEFAULT_SETTINGS: { - xmlPath: null, - tileSources: null, - debugMode: true, - animationTime: 1.5, - blendTime: 0.5, - alwaysBlend: false, - autoHideControls: true, - immediateRender: false, - wrapHorizontal: false, - wrapVertical: false, - minZoomImageRatio: 0.8, - maxZoomPixelRatio: 2, - visibilityRatio: 0.5, - springStiffness: 5.0, - imageLoaderLimit: 0, - clickTimeThreshold: 200, - clickDistThreshold: 5, - zoomPerClick: 2.0, - zoomPerScroll: 1.2, - zoomPerSecond: 2.0, - showNavigationControl: true, - - showNavigator: false, - navigatorElement: null, - navigatorHeight: null, - navigatorWidth: null, - navigatorPosition: null, - navigatorSizeRatio: 0.25, + //DATA SOURCE DETAILS + xmlPath: null, + tileSources: null, + tileHost: null, + + //INTERFACE FEATURES + debugMode: true, + animationTime: 1.5, + blendTime: 0.5, + alwaysBlend: false, + autoHideControls: true, + immediateRender: false, + wrapHorizontal: false, + wrapVertical: false, + panHorizontal: true, + panVertical: true, + visibilityRatio: 0.5, + springStiffness: 5.0, + clickTimeThreshold: 200, + clickDistThreshold: 5, + zoomPerClick: 2.0, + zoomPerScroll: 1.2, + zoomPerSecond: 2.0, + showNavigationControl: true, + controlsFadeDelay: 2000, + controlsFadeLength: 1500, + mouseNavEnabled: true, + showNavigator: false, + navigatorElement: null, + navigatorHeight: null, + navigatorWidth: null, + navigatorPosition: null, + navigatorSizeRatio: 0.25, + preserveViewport: false, - //These two were referenced but never defined - controlsFadeDelay: 2000, - controlsFadeLength: 1500, + //PERFORMANCE SETTINGS + minPixelRatio: 0.5, + imageLoaderLimit: 0, + maxImageCacheCount: 200, + minZoomImageRatio: 0.9, + maxZoomPixelRatio: 2, - maxImageCacheCount: 200, - minPixelRatio: 0.5, - mouseNavEnabled: true, - prefixUrl: null, + //INTERFACE RESOURCE SETTINGS + prefixUrl: null, navImages: { zoomIn: { REST: '/images/zoomin_rest.png', @@ -489,6 +502,18 @@ OpenSeadragon = window.OpenSeadragon || function( options ){ GROUP: '/images/fullpage_grouphover.png', HOVER: '/images/fullpage_hover.png', DOWN: '/images/fullpage_pressed.png' + }, + previous: { + REST: '/images/previous_rest.png', + GROUP: '/images/previous_grouphover.png', + HOVER: '/images/previous_hover.png', + DOWN: '/images/previous_pressed.png' + }, + next: { + REST: '/images/next_rest.png', + GROUP: '/images/next_grouphover.png', + HOVER: '/images/next_hover.png', + DOWN: '/images/next_pressed.png' } } }, @@ -1192,7 +1217,7 @@ OpenSeadragon = window.OpenSeadragon || function( options ){ * @param {String} xmlString * @param {Function} callback */ - createFromDZI: function( dzi, callback ) { + createFromDZI: function( dzi, callback, tileHost ) { var async = typeof ( callback ) == "function", xmlUrl = dzi.substring(0,1) != '<' ? dzi : null, xmlString = xmlUrl ? null : dzi, @@ -1203,7 +1228,12 @@ OpenSeadragon = window.OpenSeadragon || function( options ){ tilesUrl; - if( xmlUrl ){ + if( tileHost ){ + + tilesUrl = tileHost + "/_files/"; + + } else if( xmlUrl ) { + urlParts = xmlUrl.split( '/' ); filename = urlParts[ urlParts.length - 1 ]; lastDot = filename.lastIndexOf( '.' ); @@ -1213,6 +1243,7 @@ OpenSeadragon = window.OpenSeadragon || function( options ){ } tilesUrl = urlParts.join( '/' ) + "_files/"; + } function finish( func, obj ) { diff --git a/src/strings.js b/src/strings.js index c0301641..22e676e6 100644 --- a/src/strings.js +++ b/src/strings.js @@ -6,28 +6,30 @@ // pythons gettext might be a reasonable approach. var I18N = { Errors: { - Failure: "Sorry, but Seadragon Ajax can't run on your browser!\n" + - "Please try using IE 7 or Firefox 3.\n", - Dzc: "Sorry, we don't support Deep Zoom Collections!", - Dzi: "Hmm, this doesn't appear to be a valid Deep Zoom Image.", - Xml: "Hmm, this doesn't appear to be a valid Deep Zoom Image.", - Empty: "You asked us to open nothing, so we did just that.", - ImageFormat: "Sorry, we don't support {0}-based Deep Zoom Images.", - Security: "It looks like a security restriction stopped us from " + - "loading this Deep Zoom Image.", - Status: "This space unintentionally left blank ({0} {1}).", - Unknown: "Whoops, something inexplicably went wrong. Sorry!" + Failure: "Sorry, but Seadragon Ajax can't run on your browser!\n" + + "Please try using IE 7 or Firefox 3.\n", + Dzc: "Sorry, we don't support Deep Zoom Collections!", + Dzi: "Hmm, this doesn't appear to be a valid Deep Zoom Image.", + Xml: "Hmm, this doesn't appear to be a valid Deep Zoom Image.", + Empty: "You asked us to open nothing, so we did just that.", + ImageFormat: "Sorry, we don't support {0}-based Deep Zoom Images.", + Security: "It looks like a security restriction stopped us from " + + "loading this Deep Zoom Image.", + Status: "This space unintentionally left blank ({0} {1}).", + Unknown: "Whoops, something inexplicably went wrong. Sorry!" }, Messages: { - Loading: "Loading..." + Loading: "Loading..." }, Tooltips: { - FullPage: "Toggle full page", - Home: "Go home", - ZoomIn: "Zoom in", - ZoomOut: "Zoom out" + FullPage: "Toggle full page", + Home: "Go home", + ZoomIn: "Zoom in", + ZoomOut: "Zoom out", + NextPage: "Next page", + PreviousPage: "Previous page" } }; @@ -41,12 +43,13 @@ $.extend( $, { getString: function( prop ) { var props = prop.split('.'), - string = I18N, + string = null, args = arguments, i; for ( i = 0; i < props.length; i++ ) { - string = string[ props[ i ] ] || {}; // in case not a subproperty + // in case not a subproperty + string = I18N[ props[ i ] ] || {}; } if ( typeof( string ) != "string" ) { diff --git a/src/tile.js b/src/tile.js index d677153e..09632e8c 100644 --- a/src/tile.js +++ b/src/tile.js @@ -45,7 +45,7 @@ $.Tile = function(level, x, y, bounds, exists, url) { this.loading = false; this.element = null; - this.image = null; + this.image = null; this.style = null; this.position = null; @@ -81,7 +81,7 @@ $.Tile.prototype = { var position = this.position.apply( Math.floor ), size = this.size.apply( Math.ceil ); - if ( !this.loaded ) { + if ( !this.loaded || !this.image ) { $.console.warn( "Attempting to draw tile %s when it's not yet loaded.", this.toString() @@ -90,9 +90,9 @@ $.Tile.prototype = { } if ( !this.element ) { - this.element = $.makeNeutralElement("img"); - this.element.src = this.url; - this.style = this.element.style; + this.element = $.makeNeutralElement("img"); + this.element.src = this.url; + this.style = this.element.style; this.style.position = "absolute"; this.style.msInterpolationMode = "nearest-neighbor"; @@ -122,14 +122,13 @@ $.Tile.prototype = { var position = this.position, size = this.size; - if ( !this.loaded ) { + if ( !this.loaded || !this.image ) { $.console.warn( "Attempting to draw tile %s when it's not yet loaded.", this.toString() ); return; } - context.globalAlpha = this.opacity; context.drawImage( this.image, position.x, position.y, size.x, size.y ); }, diff --git a/src/viewer.js b/src/viewer.js index 0e2da784..12e977a4 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -59,16 +59,25 @@ $.Viewer = function( options ) { delete options.config; } + //Public properties //Allow the options object to override global defaults $.extend( true, this, { - id: options.id, - hash: options.id, - overlays: [], - overlayControls: [], + //internal state and dom identifiers + id: options.id, + hash: options.id, + + //dom nodes + element: null, + canvas: null, + container: null, + + //TODO: not sure how to best describe these + overlays: [], + overlayControls:[], //private state properties - previousBody: [], + previousBody: [], //This was originally initialized in the constructor and so could never //have anything in it. now it can because we allow it to be specified @@ -84,13 +93,76 @@ $.Viewer = function( options ) { drawer: null, viewport: null, navigator: null, + + //UI image resources + //TODO: rename navImages to uiImages + navImages: null, + + //interface button controls + buttons: null, + + //TODO: this is defunct so safely remove it profiler: null }, $.DEFAULT_SETTINGS, options ); + //Private state properties + THIS[ this.hash ] = { + "fsBoundsDelta": new $.Point( 1, 1 ), + "prevContainerSize": null, + "lastOpenStartTime": 0, + "lastOpenEndTime": 0, + "animating": false, + "forceRedraw": false, + "mouseInside": false, + "group": null, + // whether we should be continuously zooming + "zooming": false, + // how much we should be continuously zooming by + "zoomFactor": null, + "lastZoomTime": null, + // did we decide this viewer has a sequence of tile sources + "sequenced": false, + "sequence": 0 + }; + + //Inherit some behaviors and properties $.EventHandler.call( this ); $.ControlDock.call( this, options ); + //Deal with tile sources + var initialTileSource, + customTileSource; + + if ( this.xmlPath ){ + //Deprecated option. Now it is preferred to use the tileSources option + this.tileSources = [ this.xmlPath ]; + } + + if ( this.tileSources ){ + //tileSources is a complex option... + //It can be a string, object, function, or an array of any of these. + // - A String implies a DZI + // - An Srray of Objects implies a simple image + // - A Function implies a custom tile source callback + // - An Array that is not an Array of simple Objects implies a sequence + // of tile sources which can be any of the above + if( $.isArray( this.tileSources ) ){ + if( $.isPlainObject( this.tileSources[ 0 ] ) ){ + //This is a non-sequenced legacy tile source + initialTileSource = this.tileSources; + } else { + //Sequenced tile source + initialTileSource = this.tileSources[ 0 ]; + THIS[ this.hash ].sequenced = true; + } + } else { + initialTileSource = this.tileSources; + } + + this.openTileSource( initialTileSource ); + } + this.element = this.element || document.getElementById( this.id ); this.canvas = $.makeNeutralElement( "div" ); @@ -112,7 +184,7 @@ $.Viewer = function( options ) { container.textAlign = "left"; // needed to protect against }( this.container.style )); - this.container.insertBefore( this.canvas, this.container.firstChild); + this.container.insertBefore( this.canvas, this.container.firstChild ); this.element.appendChild( this.container ); //Used for toggling between fullscreen and default container size @@ -123,16 +195,6 @@ $.Viewer = function( options ) { this.bodyOverflow = document.body.style.overflow; this.docOverflow = document.documentElement.style.overflow; - THIS[ this.hash ] = { - "fsBoundsDelta": new $.Point( 1, 1 ), - "prevContainerSize": null, - "lastOpenStartTime": 0, - "lastOpenEndTime": 0, - "animating": false, - "forceRedraw": false, - "mouseInside": false - }; - this.innerTracker = new $.MouseTracker({ element: this.canvas, clickTimeThreshold: this.clickTimeThreshold, @@ -153,16 +215,6 @@ $.Viewer = function( options ) { }).setTracking( this.mouseNavEnabled ? true : false ); // always tracking - //private state properties - $.extend( THIS[ this.hash ], { - "group": null, - // whether we should be continuously zooming - "zooming": false, - // how much we should be continuously zooming by - "zoomFactor": null, - "lastZoomTime": null - }); - ////////////////////////////////////////////////////////////////////////// // Navigation Controls ////////////////////////////////////////////////////////////////////////// @@ -175,16 +227,15 @@ $.Viewer = function( options ) { onFullPageHandler = $.delegate( this, onFullPage ), onFocusHandler = $.delegate( this, onFocus ), onBlurHandler = $.delegate( this, onBlur ), - navImages = this.navImages; + onNextHandler = $.delegate( this, onNext ), + onPreviousHandler = $.delegate( this, onPrevious ), + navImages = this.navImages, + buttons = []; - this.zoomInButton = null; - this.zoomOutButton = null; - this.goHomeButton = null; - this.fullPageButton = null; if( this.showNavigationControl ){ - this.zoomInButton = new $.Button({ + buttons.push( this.zoomInButton = new $.Button({ clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, tooltip: $.getString( "Tooltips.ZoomIn" ), @@ -199,9 +250,9 @@ $.Viewer = function( options ) { onExit: endZoomingHandler, onFocus: onFocusHandler, onBlur: onBlurHandler - }); + })); - this.zoomOutButton = new $.Button({ + buttons.push( this.zoomOutButton = new $.Button({ clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, tooltip: $.getString( "Tooltips.ZoomOut" ), @@ -216,9 +267,9 @@ $.Viewer = function( options ) { onExit: endZoomingHandler, onFocus: onFocusHandler, onBlur: onBlurHandler - }); + })); - this.goHomeButton = new $.Button({ + buttons.push( this.homeButton = new $.Button({ clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, tooltip: $.getString( "Tooltips.Home" ), @@ -229,9 +280,9 @@ $.Viewer = function( options ) { onRelease: onHomeHandler, onFocus: onFocusHandler, onBlur: onBlurHandler - }); + })); - this.fullPageButton = new $.Button({ + buttons.push( this.fullPageButton = new $.Button({ clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, tooltip: $.getString( "Tooltips.FullPage" ), @@ -242,17 +293,44 @@ $.Viewer = function( options ) { onRelease: onFullPageHandler, onFocus: onFocusHandler, onBlur: onBlurHandler - }); + })); - this.buttons = new $.ButtonGroup({ + if( THIS[ this.hash ].sequenced ){ + + buttons.push( this.previousButton = new $.Button({ + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.PreviousPage" ), + srcRest: resolveUrl( this.prefixUrl, navImages.previous.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.previous.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.previous.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.previous.DOWN ), + onRelease: onPreviousHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + })); + + buttons.push( this.nextButton = new $.Button({ + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.NextPage" ), + srcRest: resolveUrl( this.prefixUrl, navImages.next.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.next.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.next.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.next.DOWN ), + onRelease: onNextHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + })); + + this.previousButton.disable(); + + } + + this.buttons = new $.ButtonGroup({ + buttons: buttons, clickTimeThreshold: this.clickTimeThreshold, - clickDistThreshold: this.clickDistThreshold, - buttons: [ - this.zoomInButton, - this.zoomOutButton, - this.goHomeButton, - this.fullPageButton - ] + clickDistThreshold: this.clickDistThreshold }); this.navControl = this.buttons.element; @@ -274,73 +352,10 @@ $.Viewer = function( options ) { ); } - //Instantiate a navigator if configured - if ( this.showNavigator ){ - this.navigator = new $.Navigator({ - id: this.navigatorElement, - position: this.navigatorPosition, - sizeRatio: this.navigatorSizeRatio, - height: this.navigatorHeight, - width: this.navigatorWidth, - tileSources: this.tileSources, - prefixUrl: this.prefixUrl, - overlays: this.overlays, - viewer: this - }); - } - window.setTimeout( function(){ beginControlsAutoHide( _this ); }, 1 ); // initial fade out - var initialTileSource, - customTileSource; - - if ( this.xmlPath ){ - //Deprecated option. Now it is preferred to use the tileSources option - this.tileSources = [ this.xmlPath ]; - } - - if ( this.tileSources ){ - //tileSource is a complex option... - //It can be a string, object, function, or an array of any of these. - //A string implies a DZI - //An object implies a simple image - //A function implies a custom tile source callback - //An array implies a sequence of tile sources which can be any of the - //above - if( $.isArray( this.tileSources ) ){ - if( $.isPlainObject( this.tileSources[ 0 ] ) ){ - //This is a non-sequenced legacy tile source - initialTileSource = this.tileSources; - } else { - //Sequenced tile source - initialTileSource = this.tileSources[ 0 ]; - } - } else { - initialTileSource = this.tileSources - } - - if ( $.type( initialTileSource ) == 'string') { - //Standard DZI format - this.openDzi( initialTileSource ); - } else if ( $.isArray( initialTileSource ) ){ - //Legacy image pyramid - this.open( new $.LegacyTileSource( initialTileSource ) ); - } else if ( $.isPlainObject( initialTileSource ) && $.isFunction( initialTileSource.getTileUrl ) ){ - //Custom tile source - customTileSource = new $.TileSource( - initialTileSource.width, - initialTileSource.height, - initialTileSource.tileSize, - initialTileSource.tileOverlap, - initialTileSource.minLevel, - initialTileSource.maxLevel - ); - customTileSource.getTileUrl = initialTileSource.getTileUrl; - this.open( customTileSource ); - } - } }; $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, { @@ -370,7 +385,8 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, dzi, function( source ){ _this.open( source ); - } + }, + this.tileHost ); return this; }, @@ -381,10 +397,34 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, * @return {OpenSeadragon.Viewer} Chainable. */ openTileSource: function ( tileSource ) { - var _this = this; - window.setTimeout( function () { - _this.open( tileSource ); - }, 1 ); + var _this = this, + customTileSource; + + setTimeout(function(){ + if ( $.type( tileSource ) == 'string') { + //Standard DZI format + _this.openDzi( tileSource ); + } else if ( $.isArray( tileSource ) ){ + //Legacy image pyramid + _this.open( new $.LegacyTileSource( tileSource ) ); + } else if ( $.isPlainObject( tileSource ) && $.isFunction( tileSource.getTileUrl ) ){ + //Custom tile source + customTileSource = new $.TileSource( + tileSource.width, + tileSource.height, + tileSource.tileSize, + tileSource.tileOverlap, + tileSource.minLevel, + tileSource.maxLevel + ); + customTileSource.getTileUrl = tileSource.getTileUrl; + _this.open( customTileSource ); + } else { + //can assume it's already a tile source implementation + _this.open( tileSource ); + } + }, 1); + return this; }, @@ -399,7 +439,7 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, i; if ( this.source ) { - this.close(); + this.close( ); } // to ignore earlier opens @@ -419,7 +459,7 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, this.source = source; } - this.viewport = new $.Viewport({ + this.viewport = this.viewport ? this.viewport : new $.Viewport({ containerSize: THIS[ this.hash ].prevContainerSize, contentSize: this.source.dimensions, springStiffness: this.springStiffness, @@ -430,6 +470,9 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, wrapHorizontal: this.wrapHorizontal, wrapVertical: this.wrapVertical }); + if( this.preserveVewport ){ + this.viewport.resetContentSize( this.source.dimensions ); + } this.drawer = new $.Drawer({ source: this.source, @@ -447,6 +490,22 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, minPixelRatio: this.minPixelRatio }); + //Instantiate a navigator if configured + if ( this.showNavigator && ! this.navigator ){ + this.navigator = new $.Navigator({ + id: this.navigatorElement, + position: this.navigatorPosition, + sizeRatio: this.navigatorSizeRatio, + height: this.navigatorHeight, + width: this.navigatorWidth, + tileSources: this.tileSources, + tileHost: this.tileHost, + prefixUrl: this.prefixUrl, + overlays: this.overlays, + viewer: this + }); + } + //this.profiler = new $.Profiler(); THIS[ this.hash ].animating = false; @@ -487,6 +546,11 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, } VIEWERS[ this.hash ] = this; this.raiseEvent( "open" ); + + if( this.navigator ){ + this.navigator.open( source ); + } + return this; }, @@ -495,10 +559,10 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, * @name OpenSeadragon.Viewer.prototype.close * @return {OpenSeadragon.Viewer} Chainable. */ - close: function () { + close: function ( ) { this.source = null; - this.viewport = null; this.drawer = null; + this.viewport = this.preserveViewport ? this.viewport : null; //this.profiler = null; this.canvas.innerHTML = ""; @@ -722,6 +786,9 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, viewer = VIEWERS[ hash ]; if( viewer !== this && viewer != this.navigator ){ viewer.open( viewer.source ); + if( viewer.navigator ){ + viewer.navigator.open( viewer.source ); + } } } } @@ -757,10 +824,11 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype, }); + + /////////////////////////////////////////////////////////////////////////////// // Schedulers provide the general engine for animation /////////////////////////////////////////////////////////////////////////////// - function scheduleUpdate( viewer, updateFunc, prevUpdateTime ){ var currentTime, targetTime, @@ -783,6 +851,7 @@ function scheduleUpdate( viewer, updateFunc, prevUpdateTime ){ }, deltaTime ); }; + //provides a sequence in the fade animation function scheduleControlsFade( viewer ) { window.setTimeout( function(){ @@ -790,6 +859,7 @@ function scheduleControlsFade( viewer ) { }, 20); }; + //initiates an animation to hide the controls function beginControlsAutoHide( viewer ) { if ( !viewer.autoHideControls ) { @@ -831,6 +901,7 @@ function updateControlsFade( viewer ) { } }; + //stop the fade animation on the controls and show them function abortControlsAutoHide( viewer ) { var i; @@ -841,6 +912,7 @@ function abortControlsAutoHide( viewer ) { }; + /////////////////////////////////////////////////////////////////////////////// // Default view event handlers. /////////////////////////////////////////////////////////////////////////////// @@ -869,6 +941,12 @@ function onCanvasClick( tracker, position, quick, shift ) { function onCanvasDrag( tracker, position, delta, shift ) { if ( this.viewport ) { + if( !this.panHorizontal ){ + delta.x = 0; + } + if( !this.panVertical ){ + delta.y = 0; + } this.viewport.panBy( this.viewport.deltaPointsFromPixels( delta.negate() @@ -893,6 +971,8 @@ function onCanvasScroll( tracker, position, scroll, shift ) { ); this.viewport.applyConstraints(); } + //cancels event + return false; }; function onContainerExit( tracker, position, buttonDownElement, buttonDownAny ) { @@ -988,36 +1068,42 @@ function updateOnce( viewer ) { //viewer.profiler.endUpdate(); }; + + /////////////////////////////////////////////////////////////////////////////// // Navigation Controls /////////////////////////////////////////////////////////////////////////////// - function resolveUrl( prefix, url ) { return prefix ? prefix + url : url; }; + function beginZoomingIn() { THIS[ this.hash ].lastZoomTime = +new Date(); THIS[ this.hash ].zoomFactor = this.zoomPerSecond; THIS[ this.hash ].zooming = true; scheduleZoom( this ); -} +}; + function beginZoomingOut() { THIS[ this.hash ].lastZoomTime = +new Date(); THIS[ this.hash ].zoomFactor = 1.0 / this.zoomPerSecond; THIS[ this.hash ].zooming = true; scheduleZoom( this ); -} +}; + function endZooming() { THIS[ this.hash ].zooming = false; -} +}; + function scheduleZoom( viewer ) { window.setTimeout( $.delegate( viewer, doZoom ), 10 ); -} +}; + function doZoom() { var currentTime, @@ -1036,6 +1122,7 @@ function doZoom() { } }; + function doSingleZoomIn() { if ( this.viewport ) { THIS[ this.hash ].zooming = false; @@ -1046,6 +1133,7 @@ function doSingleZoomIn() { } }; + function doSingleZoomOut() { if ( this.viewport ) { THIS[ this.hash ].zooming = false; @@ -1056,17 +1144,20 @@ function doSingleZoomOut() { } }; + function lightUp() { this.buttons.emulateEnter(); this.buttons.emulateExit(); }; + function onHome() { if ( this.viewport ) { this.viewport.goHome(); } }; + function onFullPage() { this.setFullPage( !this.isFullPage() ); // correct for no mouseout event on change @@ -1077,4 +1168,47 @@ function onFullPage() { } }; + +function onPrevious(){ + var previous = THIS[ this.hash ].sequence - 1, + preserveVewport = true; + if( previous >= 0 ){ + + THIS[ this.hash ].sequence = previous; + + if( 0 === previous ){ + //Disable previous button + this.previousButton.disable(); + } + if( this.tileSources.length > 0 ){ + //Enable next button + this.nextButton.enable(); + } + + this.openTileSource( this.tileSources[ previous ] ); + } +}; + + +function onNext(){ + var next = THIS[ this.hash ].sequence + 1, + preserveVewport = true; + if( this.tileSources.length > next ){ + + THIS[ this.hash ].sequence = next; + + if( ( this.tileSources.length - 1 ) === next ){ + //Disable next button + this.nextButton.disable(); + } + if( next > 0 ){ + //Enable previous button + this.previousButton.enable(); + } + + this.openTileSource( this.tileSources[ next ] ); + } +}; + + }( OpenSeadragon )); diff --git a/src/viewport.js b/src/viewport.js index b6807edd..4079a75c 100644 --- a/src/viewport.js +++ b/src/viewport.js @@ -46,9 +46,6 @@ $.Viewport = function( options ) { }, options ); - - this.contentAspect = this.contentSize.x / this.contentSize.y; - this.contentHeight = this.contentSize.y / this.contentSize.x; this.centerSpringX = new $.Spring({ initial: 0, springStiffness: this.springStiffness, @@ -64,19 +61,39 @@ $.Viewport = function( options ) { springStiffness: this.springStiffness, animationTime: this.animationTime }); - this.homeBounds = new $.Rect( 0, 0, 1, this.contentHeight ); + this.resetContentSize( this.contentSize ); this.goHome( true ); + //this.fitHorizontally( true ); this.update(); }; $.Viewport.prototype = { + resetContentSize: function( contentSize ){ + this.contentSize = contentSize; + this.contentAspectX = this.contentSize.x / this.contentSize.y; + this.contentAspectY = this.contentSize.y / this.contentSize.x; + this.homeBounds = new $.Rect( + 0, + 0, + 1, + this.contentAspectY + ); + this.fitWidthBounds = new $.Rect( 0, 0, 1, this.contentAspectX ); + this.fitHeightBounds = new $.Rect( 0, 0, 1, this.contentAspectY ); + }, + /** * @function */ getHomeZoom: function() { - var aspectFactor = this.contentAspect / this.getAspectRatio(); + + var aspectFactor = Math.min( + this.contentAspectX, + this.contentAspectY + ) / this.getAspectRatio(); + return ( aspectFactor >= 1 ) ? 1 : aspectFactor; @@ -229,7 +246,7 @@ $.Viewport.prototype = { left = bounds.x + bounds.width; right = 1 - bounds.x; top = bounds.y + bounds.height; - bottom = this.contentHeight - bounds.y; + bottom = this.contentAspectY - bounds.y; if ( this.wrapHorizontal ) { //do nothing @@ -311,15 +328,22 @@ $.Viewport.prototype = { this.containerSize.x / newBounds.width ); - this.zoomTo( newZoom, referencePoint, immediately ); }, + + /** + * @function + * @param {Boolean} immediately + */ + goHome: function( immediately ) { + return this.fitVertically( immediately ); + }, /** * @function * @param {Boolean} immediately */ - goHome: function( immediately ) { + fitVertically: function( immediately ) { var center = this.getCenter(); if ( this.wrapHorizontal ) { @@ -330,8 +354,8 @@ $.Viewport.prototype = { if ( this.wrapVertical ) { center.y = ( - this.contentHeight + ( center.y % this.contentHeight ) - ) % this.contentHeight; + this.contentAspectY + ( center.y % this.contentAspectY ) + ) % this.contentAspectY; this.centerSpringY.resetTo( center.y ); this.centerSpringY.update(); } @@ -339,6 +363,31 @@ $.Viewport.prototype = { this.fitBounds( this.homeBounds, immediately ); }, + /** + * @function + * @param {Boolean} immediately + */ + fitHorizontally: function( immediately ) { + var center = this.getCenter(); + + if ( this.wrapHorizontal ) { + center.x = ( + this.contentAspectX + ( center.x % this.contentAspectX ) + ) % this.contentAspectX; + this.centerSpringX.resetTo( center.x ); + this.centerSpringX.update(); + } + + if ( this.wrapVertical ) { + center.y = ( 1 + ( center.y % 1 ) ) % 1; + this.centerSpringY.resetTo( center.y ); + this.centerSpringY.update(); + } + + this.fitBounds( this.fitWidthBounds, immediately ); + }, + + /** * @function * @param {OpenSeadragon.Point} delta