Cruft removal.
[bookreader.git] / GnuBook / GnuBook.js
index 2dbb993..ee9401b 100644 (file)
@@ -72,8 +72,14 @@ function GnuBook() {
     this.constMode1up = 1;
     this.constMode2up = 2;
     
+    // Zoom levels
+    this.reductionFactors = [0.5, 1, 2, 4, 8, 16];
+
     // Object to hold parameters related to 2up mode
     this.twoPage = {
+        coverInternalPadding: 10, // Width of cover
+        coverExternalPadding: 10, // Padding outside of cover
+        bookSpineDivWidth: 30,    // Width of book spine  $$$ consider sizing based on book length
         autofit: true
     };
 };
@@ -139,7 +145,26 @@ GnuBook.prototype.init = function() {
             e.data.loadLeafs();
         } else {
             //console.log('drawing 2 page view');
-            e.data.prepareTwoPageView();
+            
+            // We only need to prepare again in autofit (size of spread changes)
+            if (e.data.twoPage.autofit) {
+                e.data.prepareTwoPageView();
+            } else {
+                // Re-center if the scrollbars have disappeared
+                var center = e.data.twoPageGetViewCenter();
+                var doRecenter = false;
+                if (e.data.twoPage.totalWidth < $('#GBcontainer').attr('clientWidth')) {
+                    center.percentageX = 0.5;
+                    doRecenter = true;
+                }
+                if (e.data.twoPage.totalHeight < $('#GBcontainer').attr('clientHeight')) {
+                    center.percentageY = 0.5;
+                    doRecenter = true;
+                }
+                if (doRecenter) {
+                    e.data.twoPageCenterView(center.percentageX, center.percentageY);
+                }
+            }
         }
     });
     
@@ -160,7 +185,6 @@ GnuBook.prototype.init = function() {
         //console.log('titleLeaf: %d', this.titleLeaf);
         //console.log('displayedIndices: %s', this.displayedIndices);
         this.prepareTwoPageView();
-        //if (this.auto) this.nextPage();
     }
         
     // Enact other parts of initial params
@@ -405,11 +429,8 @@ GnuBook.prototype.drawLeafsOnePage = function() {
     } else {
         $("#GBpagenum").val('');
     }
-    
-    //var centerY = this.centerY1up();
-    //var centerX = this.centerX1up();
-    //console.log('draw center ' + centerY + ',' + centerX);
-    //console.log('scroll left ' + $('#GBcontainer').attr('scrollLeft'));
+            
+    this.updateToolbarZoom(this.reduce);
     
 }
 
@@ -417,39 +438,30 @@ GnuBook.prototype.drawLeafsOnePage = function() {
 //______________________________________________________________________________
 GnuBook.prototype.drawLeafsTwoPage = function() {
     console.log('drawing two leafs!'); // XXX
-
+    
     var scrollTop = $('#GBtwopageview').attr('scrollTop');
     var scrollBottom = scrollTop + $('#GBtwopageview').height();
     
     console.log('drawLeafsTwoPage: this.currrentLeafL ' + this.twoPage.currentIndexL); // XXX
     
+    // XXX we should use calculated values in this.twoPage (recalc if necessary)
+    
     var indexL = this.twoPage.currentIndexL;
     var heightL  = this.getPageHeight(indexL); 
     var widthL   = this.getPageWidth(indexL);
 
     var leafEdgeWidthL = this.leafEdgeWidth(indexL);
     var leafEdgeWidthR = this.twoPage.edgeWidth - leafEdgeWidthL;
-    var bookCoverDivWidth = this.twoPage.width*2+20 + this.twoPage.edgeWidth; // $$$ hardcoded cover width
+    //var bookCoverDivWidth = this.twoPage.width*2 + 20 + this.twoPage.edgeWidth; // $$$ hardcoded cover width
+    var bookCoverDivWidth = this.twoPage.bookCoverDivWidth;
     //console.log(leafEdgeWidthL);
 
-    var middle, top, bookCoverDivLeft;
-    if (this.twoPage.autofit) {    
-        middle = ($('#GBtwopageview').attr('clientWidth') >> 1);            
-        top  = ($('#GBtwopageview').attr('clientHeight') - this.twoPage.height) >> 1;
-        bookCoverDivLeft = ($('#GBcontainer').attr('clientWidth') - bookCoverDivWidth) >> 1;
-    } else {
-        // $$$ add external padding
-        middle = this.twoPage.width / 2;
-        top = 0;
-        bookCoverDivLeft = 0;
-        
-        // XXX add in padding
-        $('#GBtwopageview').width(this.twoPage.width + this.twoPage.totalLeafEdgeWidth);
-        $('#GBtwopageview').height(this.twoPage.height + 20);
-    }
+    var middle = this.twoPage.middle; // $$$ getter instead?
+    var top = this.twoPageTop();
+    var bookCoverDivLeft = this.twoPage.bookCoverDivLeft;
 
-    var scaledWL = parseInt(this.twoPage.height*widthL/heightL);
-    var gutter = middle + this.gutterOffsetForIndex(this.twoPage.currentIndexL);
+    var scaledWL = this.getPageWidth2UP(indexL);
+    var gutter = this.twoPageGutter();
     
     this.prefetchImg(indexL);
     $(this.prefetchedImgs[indexL]).css({
@@ -469,7 +481,9 @@ GnuBook.prototype.drawLeafsTwoPage = function() {
     var heightR  = this.getPageHeight(indexR); 
     var widthR   = this.getPageWidth(indexR);
 
-    var scaledWR = this.twoPage.height*widthR/heightR;
+    // $$$ should use getwidth2up?
+    //var scaledWR = this.twoPage.height*widthR/heightR;
+    var scaledWR = this.getPageWidth2UP(indexR);
     this.prefetchImg(indexR);
     $(this.prefetchedImgs[indexR]).css({
         position: 'absolute',
@@ -488,6 +502,7 @@ GnuBook.prototype.drawLeafsTwoPage = function() {
     this.setClickHandlers();
 
     this.updatePageNumBox2UP();
+    this.updateToolbarZoom(this.reduce);
 }
 
 // updatePageNumBox2UP
@@ -552,7 +567,7 @@ GnuBook.prototype.zoom1up = function(dir) {
     this.displayedIndices = [];
     this.loadLeafs();
     
-    $('#GBzoom').text(100/this.reduce);
+    this.updateToolbarZoom(this.reduce);
 }
 
 // resizePageView()
@@ -639,20 +654,94 @@ GnuBook.prototype.centerPageView = function() {
 // zoom2up(direction)
 //______________________________________________________________________________
 GnuBook.prototype.zoom2up = function(direction) {
-    // $$$ this is where we can e.g. snap to %2 sizes
-    if (0 == direction) { // autofit mode
-        this.twoPage.autofit = true;;
-    } else if (1 == direction) {
-        if (this.reduce <= 0.5) return;
-        this.reduce*=0.5;           //zoom in
-        this.twoPage.autofit = false;
-    } else {
-        if (this.reduce >= 8) return;
-        this.reduce *= 2; // zoom out
-        this.twoPage.autofit = false;
+
+    // Stop autoplay
+    this.autoStop();
+    this.stopFlipAnimations(); // hard stop animations
+    
+    // Get new zoom state    
+    var newZoom = this.twoPageNextReduce(this.reduce, direction);
+    if ((this.reduce == newZoom.reduce) && (this.twoPage.autofit == newZoom.autofit)) {
+        return;
     }
+    this.twoPage.autofit = newZoom.autofit;
+    this.reduce = newZoom.reduce;
+
+    // Preserve view center position
+    var oldCenter = this.twoPageGetViewCenter();
     
-    this.prepareTwoPageView();
+    // Prepare view with new center to minimize visual glitches
+    this.prepareTwoPageView(oldCenter.percentageX, oldCenter.percentageY);
+}
+
+
+// quantizeReduce(reduce)
+//______________________________________________________________________________
+// Quantizes the given reduction factor to closest power of two from set from 12.5% to 200%
+GnuBook.prototype.quantizeReduce = function(reduce) {
+    var quantized = this.reductionFactors[0];
+    var distance = Math.abs(reduce - quantized);
+    for (var i = 1; i < this.reductionFactors.length; i++) {
+        newDistance = Math.abs(reduce - reductionFactors[i]);
+        if (newDistance < distance) {
+            distance = newDistance;
+            quantized = reductionFactors[i];
+        }
+    }
+    
+    return quantized;
+}
+
+// twoPageNextReduce()
+//______________________________________________________________________________
+// Returns the next reduction level
+GnuBook.prototype.twoPageNextReduce = function(reduce, direction) {
+    var result = {};
+    var autofitReduce = this.twoPageGetAutofitReduce();
+
+    if (0 == direction) { // autofit
+        result.autofit = true;
+        result.reduce = autofitReduce;
+        
+    } else if (1 == direction) { // zoom in
+        var newReduce = this.reductionFactors[0];
+    
+        for (var i = 1; i < this.reductionFactors.length; i++) {
+            if (this.reductionFactors[i] < reduce) {
+                newReduce = this.reductionFactors[i];
+            }
+        }
+        
+        if (!this.twoPage.autofit && (autofitReduce < reduce && autofitReduce > newReduce)) {
+            // use autofit
+            result.autofit = true;
+            result.reduce = autofitReduce;
+        } else {        
+            result.autofit = false;
+            result.reduce = newReduce;
+        }
+        
+    } else { // zoom out
+        var lastIndex = this.reductionFactors.length - 1;
+        var newReduce = this.reductionFactors[lastIndex];
+        
+        for (var i = lastIndex; i >= 0; i--) {
+            if (this.reductionFactors[i] > reduce) {
+                newReduce = this.reductionFactors[i];
+            }
+        }
+         
+        if (!this.twoPage.autofit && (autofitReduce > reduce && autofitReduce < newReduce)) {
+            // use autofit
+            result.autofit = true;
+            result.reduce = autofitReduce;
+        } else {
+            result.autofit = false;
+            result.reduce = newReduce;
+        }
+    }
+    
+    return result;
 }
 
 // jumpToPage()
@@ -724,9 +813,11 @@ GnuBook.prototype.switchMode = function(mode) {
     this.switchToolbarMode(mode);
     
     if (1 == mode) {
+        this.reduce = this.twoPageQuantizeReduce(this.reduce);
         this.prepareOnePageView();
     } else {
         this.prepareTwoPageView();
+        this.twoPageCenterView(0.5, 0.5);
     }
 
 }
@@ -752,7 +843,6 @@ GnuBook.prototype.prepareOnePageView = function() {
     this.displayedIndices = [];
     
     this.drawLeafsOnePage();
-    $('#GBzoom').text(100/this.reduce);
         
     // Bind mouse handlers
     // Disable mouse click to avoid selected/highlighted page images - bug 354239
@@ -765,20 +855,20 @@ GnuBook.prototype.prepareOnePageView = function() {
 
 // prepareTwoPageView()
 //______________________________________________________________________________
-GnuBook.prototype.prepareTwoPageView = function() {
+// Some decisions about two page view:
+//
+// Both pages will be displayed at the same height, even if they were different physical/scanned
+// sizes.  This simplifies the animation (from a design as well as technical standpoint).  We
+// examine the page aspect ratios (in calculateSpreadSize) and use the page with the most "normal"
+// aspect ratio to determine the height.
+//
+// The two page view div is resized to keep the middle of the book in the middle of the div
+// even as the page sizes change.  To e.g. keep the middle of the book in the middle of the GBcontent
+// div requires adjusting the offset of GBtwpageview and/or scrolling in GBcontent.
+GnuBook.prototype.prepareTwoPageView = function(centerPercentageX, centerPercentageY) {
     $('#GBcontainer').empty();
     $('#GBcontainer').css('overflow', 'auto');
-
-    
-    // Add the two page view
-    $('#GBcontainer').append('<div id="GBtwopageview"></div>');
-    // Explicitly set sizes the same
-    $('#GBtwopageview').css( {
-        height: $('#GBcontainer').height(),
-        width: $('#GBcontainer').width(),
-        position: 'absolute'
-        });
-
+        
     // We want to display two facing pages.  We may be missing
     // one side of the spread because it is the first/last leaf,
     // foldouts, missing pages, etc
@@ -803,59 +893,47 @@ GnuBook.prototype.prepareTwoPageView = function() {
     this.twoPage.currentIndexR = currentSpreadIndices[1];
     this.firstIndex = this.twoPage.currentIndexL;
     
-    this.calculateSpreadSize(); //sets twoPage.width, twoPage.height, and twoPage.ratio
-
-    // We want to minimize the unused space in two-up mode (maximize the amount of page
-    // shown).  We give width to the leaf edges and these widths change (though the sum
-    // of the two remains constant) as we flip through the book.  With the book
-    // cover centered and fixed in the GBcontainer div the page images will meet
-    // at the "gutter" which is generally offset from the center.
-    var middle = ($('#GBcontainer').attr('clientWidth') >> 1); // Middle of the GBcontainer div
-    //var gutter = middle+parseInt((2*this.twoPage.currentIndexL - this.numLeafs)*this.twoPage.edgeWidth/this.numLeafs/2);
-    
-    var gutter = middle + this.gutterOffsetForIndex(this.twoPage.currentIndexL);
+    this.calculateSpreadSize(); //sets twoPage.width, twoPage.height
+        
+    //console.dir(this.twoPage);
     
-    var scaledWL = this.getPageWidth2UP(this.twoPage.currentIndexL);
-    var scaledWR = this.getPageWidth2UP(this.twoPage.currentIndexR);
-    var leafEdgeWidthL = this.leafEdgeWidth(this.twoPage.currentIndexL);
-    var leafEdgeWidthR = this.twoPage.edgeWidth - leafEdgeWidthL;
+    // Add the two page view
+    // $$$ Can we get everything set up and then append?
+    $('#GBcontainer').append('<div id="GBtwopageview"></div>');
 
-    //console.log('idealWidth='+idealWidth+' idealHeight='+idealHeight);
-    //var bookCoverDivWidth = this.twoPage.width*2+20 + this.twoPage.edgeWidth;
-    
-    // The width of the book cover div.  The combined width of both pages, twice the width
-    // of the book cover internal padding (2*10) and the page edges
-    var bookCoverDivWidth = scaledWL + scaledWR + 20 + this.twoPage.edgeWidth;
-    
-    // The height of the book cover div
-    var bookCoverDivHeight = this.twoPage.height+20;
+    // $$$ calculate first then set
+    $('#GBtwopageview').css( {
+        height: this.twoPage.totalHeight + 'px',
+        width: this.twoPage.totalWidth + 'px',
+        position: 'absolute'
+        });
+        
+    // If there will not be scrollbars (e.g. when zooming out) we center the book
+    // since otherwise the book will be stuck off-center
+    if (this.twoPage.totalWidth < $('#GBcontainer').attr('clientWidth')) {
+        centerPercentageX = 0.5;
+    }
+    if (this.twoPage.totalHeight < $('#GBcontainer').attr('clientHeight')) {
+        centerPercentageY = 0.5;
+    }
+        
+    this.twoPageCenterView(centerPercentageX, centerPercentageY);
     
-    //var bookCoverDivLeft = ($('#GBcontainer').width() - bookCoverDivWidth) >> 1;
-    var bookCoverDivLeft = gutter-scaledWL-leafEdgeWidthL-10;
-    var bookCoverDivTop = ($('#GBcontainer').height() - bookCoverDivHeight) >> 1;
-    //console.log('bookCoverDivWidth='+bookCoverDivWidth+' bookCoverDivHeight='+bookCoverDivHeight+ ' bookCoverDivLeft='+bookCoverDivLeft+' bookCoverDivTop='+bookCoverDivTop);
-
-    this.twoPageDiv = document.createElement('div');
-    $(this.twoPageDiv).attr('id', 'GBbookcover').css({
+    this.twoPage.coverDiv = document.createElement('div');
+    $(this.twoPage.coverDiv).attr('id', 'GBbookcover').css({
         border: '1px solid rgb(68, 25, 17)',
-        width:  bookCoverDivWidth + 'px',
-        height: bookCoverDivHeight+'px',
+        width:  this.twoPage.bookCoverDivWidth + 'px',
+        height: this.twoPage.bookCoverDivHeight+'px',
         visibility: 'visible',
         position: 'absolute',
         backgroundColor: '#663929',
-        left: bookCoverDivLeft + 'px',
-        top: bookCoverDivTop+'px',
+        left: this.twoPage.bookCoverDivLeft + 'px',
+        top: this.twoPage.bookCoverDivTop+'px',
         MozBorderRadiusTopleft: '7px',
         MozBorderRadiusTopright: '7px',
         MozBorderRadiusBottomright: '7px',
         MozBorderRadiusBottomleft: '7px'
     }).appendTo('#GBtwopageview');
-    //$('#GBcontainer').append('<div id="book_div_1" style="border: 1px solid rgb(68, 25, 17); width: ' + bookCoverDivWidth + 'px; height: '+bookCoverDivHeight+'px; visibility: visible; position: absolute; background-color: rgb(136, 51, 34); left: ' + bookCoverDivLeft + 'px; top: '+bookCoverDivTop+'px; -moz-border-radius-topleft: 7px; -moz-border-radius-topright: 7px; -moz-border-radius-bottomright: 7px; -moz-border-radius-bottomleft: 7px;"/>');
-
-
-    var height  = this.getPageHeight(this.twoPage.currentIndexR); 
-    var width   = this.getPageWidth(this.twoPage.currentIndexR);    
-    var scaledW = this.twoPage.height*width/height;
     
     this.leafEdgeR = document.createElement('div');
     this.leafEdgeR.className = 'leafEdgeR'; // $$$ the static CSS should be moved into the .css file
@@ -864,11 +942,11 @@ GnuBook.prototype.prepareTwoPageView = function() {
         borderColor: 'rgb(51, 51, 34)',
         borderWidth: '1px 1px 1px 0px',
         background: 'transparent url(' + this.imagesBaseURL + 'right_edges.png) repeat scroll 0% 0%',
-        width: leafEdgeWidthR + 'px',
+        width: this.twoPage.leafEdgeWidthR + 'px',
         height: this.twoPage.height-1 + 'px',
         /*right: '10px',*/
-        left: gutter+scaledW+'px',
-        top: bookCoverDivTop+10+'px',
+        left: this.twoPage.gutter+this.twoPage.scaledWR+'px',
+        top: this.twoPage.bookCoverDivTop+this.twoPage.coverInternalPadding+'px',
         position: 'absolute'
     }).appendTo('#GBtwopageview');
     
@@ -879,37 +957,23 @@ GnuBook.prototype.prepareTwoPageView = function() {
         borderColor: 'rgb(51, 51, 34)',
         borderWidth: '1px 0px 1px 1px',
         background: 'transparent url(' + this.imagesBaseURL + 'left_edges.png) repeat scroll 0% 0%',
-        width: leafEdgeWidthL + 'px',
+        width: this.twoPage.leafEdgeWidthL + 'px',
         height: this.twoPage.height-1 + 'px',
-        left: bookCoverDivLeft+10+'px',
-        top: bookCoverDivTop+10+'px',    
+        left: this.twoPage.bookCoverDivLeft+this.twoPage.coverInternalPadding+'px',
+        top: this.twoPage.bookCoverDivTop+this.twoPage.coverInternalPadding+'px',    
         position: 'absolute'
     }).appendTo('#GBtwopageview');
 
-
-
-    bookCoverDivWidth = 30;
-    bookCoverDivHeight = this.twoPage.height+20;
-    bookCoverDivLeft = ($('#GBcontainer').attr('clientWidth') - bookCoverDivWidth) >> 1;
-    bookCoverDivTop = ($('#GBcontainer').height() - bookCoverDivHeight) >> 1;
-
     div = document.createElement('div');
     $(div).attr('id', 'GBbookspine').css({
         border:          '1px solid rgb(68, 25, 17)',
-        width:           bookCoverDivWidth+'px',
-        height:          bookCoverDivHeight+'px',
+        width:           this.twoPage.bookSpineDivWidth+'px',
+        height:          this.twoPage.bookSpineDivHeight+'px',
         position:        'absolute',
         backgroundColor: 'rgb(68, 25, 17)',
-        left:            bookCoverDivLeft+'px',
-        top:             bookCoverDivTop+'px'
+        left:            this.twoPage.bookSpineDivLeft+'px',
+        top:             this.twoPage.bookSpineDivTop+'px'
     }).appendTo('#GBtwopageview');
-    //$('#GBcontainer').append('<div id="book_div_2" style="border: 1px solid rgb(68, 25, 17); width: '+bookCoverDivWidth+'px; height: '+bookCoverDivHeight+'px; visibility: visible; position: absolute; background-color: rgb(68, 25, 17); left: '+bookCoverDivLeft+'px; top: '+bookCoverDivTop+'px;"/>');
-
-    bookCoverDivWidth = this.twoPage.width*2;
-    bookCoverDivHeight = this.twoPage.height;
-    bookCoverDivLeft = ($('#GBcontainer').attr('clientWidth') - bookCoverDivWidth) >> 1;
-    bookCoverDivTop = ($('#GBcontainer').height() - bookCoverDivHeight) >> 1;
-
 
     this.prepareTwoPagePopUp();
 
@@ -920,9 +984,9 @@ GnuBook.prototype.prepareTwoPageView = function() {
     
     this.drawLeafsTwoPage();
     this.updateSearchHilites2UP();
+    this.updateToolbarZoom(this.reduce);
     
     this.prefetch();
-    $('#GBzoom').text((100*this.twoPage.height/this.getPageHeight(this.twoPage.currentIndexL)).toString().substr(0,4));
 }
 
 // prepareTwoPagePopUp()
@@ -997,7 +1061,7 @@ GnuBook.prototype.prepareTwoPagePopUp = function() {
 //______________________________________________________________________________
 // Calculates 2-page spread dimensions based on this.twoPage.currentIndexL and
 // this.twoPage.currentIndexR
-// This function sets this.twoPage.height, twoPage.width, and twoPage.ratio
+// This function sets this.twoPage.height, twoPage.width
 
 GnuBook.prototype.calculateSpreadSize = function() {
     console.log('calculateSpreadSize ' + this.twoPage.currentIndexL); // XXX
@@ -1010,27 +1074,75 @@ GnuBook.prototype.calculateSpreadSize = function() {
     var secondIndex = this.twoPage.currentIndexR;
     //console.log('first page is ' + firstIndex);
 
-    // $$$ Right now we just use the ideal size
+    // Calculate page sizes and total leaf width
     var spreadSize;
     if ( this.twoPage.autofit) {    
         spreadSize = this.getIdealSpreadSize(firstIndex, secondIndex);
     } else {
         // set based on reduction factor
         spreadSize = this.getSpreadSizeFromReduce(firstIndex, secondIndex, this.reduce);
-        // XXX
-        console.dir(spreadSize);
     }
     
+    // Both pages together
     this.twoPage.height = spreadSize.height;
     this.twoPage.width = spreadSize.width;
-    this.twoPage.ratio = spreadSize.ratio;
+    
+    // Individual pages
+    this.twoPage.scaledWL = this.getPageWidth2UP(firstIndex);
+    this.twoPage.scaledWR = this.getPageWidth2UP(secondIndex);
+    
+    // Leaf edges
+    this.twoPage.leafEdgeWidthL = this.leafEdgeWidth(this.twoPage.currentIndexL);
+    this.twoPage.leafEdgeWidthR = this.twoPage.edgeWidth - this.twoPage.leafEdgeWidthL;
     this.twoPage.edgeWidth = spreadSize.totalLeafEdgeWidth; // The combined width of both edges
-    this.reduce = spreadSize.reduce;
+    
+    
+    // Book cover
+    // The width of the book cover div.  The combined width of both pages, twice the width
+    // of the book cover internal padding (2*10) and the page edges
+    this.twoPage.bookCoverDivWidth = this.twoPage.scaledWL + this.twoPage.scaledWR + 2 * this.twoPage.coverInternalPadding + this.twoPage.edgeWidth;
+    // The height of the book cover div
+    this.twoPage.bookCoverDivHeight = this.twoPage.height + 2 * this.twoPage.coverInternalPadding;
+    
+    
+    // We calculate the total width and height for the div so that we can make the book
+    // spine centered
+    var leftGutterOffset = this.gutterOffsetForIndex(firstIndex);
+    var leftWidthFromCenter = this.twoPage.scaledWL + leftGutterOffset + this.twoPage.leafEdgeWidthL;
+    var rightWidthFromCenter = this.twoPage.scaledWR - leftGutterOffset + this.twoPage.leafEdgeWidthR;
+    var largestWidthFromCenter = Math.max( leftWidthFromCenter, rightWidthFromCenter );
+    this.twoPage.totalWidth = 2 * (largestWidthFromCenter + this.twoPage.coverInternalPadding + this.twoPage.coverExternalPadding);
+    this.twoPage.totalHeight = this.twoPage.height + 2 * (this.twoPage.coverInternalPadding + this.twoPage.coverExternalPadding);
+        
+    // We want to minimize the unused space in two-up mode (maximize the amount of page
+    // shown).  We give width to the leaf edges and these widths change (though the sum
+    // of the two remains constant) as we flip through the book.  With the book
+    // cover centered and fixed in the GBcontainer div the page images will meet
+    // at the "gutter" which is generally offset from the center.
+    this.twoPage.middle = this.twoPage.totalWidth >> 1;
+    this.twoPage.gutter = this.twoPage.middle + this.gutterOffsetForIndex(firstIndex);
+    
+    // The left edge of the book cover moves depending on the width of the pages
+    // $$$ change to getter
+    this.twoPage.bookCoverDivLeft = this.twoPage.gutter - this.twoPage.scaledWL - this.twoPage.leafEdgeWidthL - this.twoPage.coverInternalPadding;
+    // The top edge of the book cover stays a fixed distance from the top
+    this.twoPage.bookCoverDivTop = this.twoPage.coverExternalPadding;
+
+    // Book spine
+    this.twoPage.bookSpineDivHeight = this.twoPage.height + 2*this.twoPage.coverInternalPadding;
+    this.twoPage.bookSpineDivLeft = this.twoPage.middle - (this.twoPage.bookSpineDivWidth >> 1);
+    this.twoPage.bookSpineDivTop = this.twoPage.bookCoverDivTop;
+
+
+    this.reduce = spreadSize.reduce; // $$$ really set this here?
 }
 
 GnuBook.prototype.getIdealSpreadSize = function(firstIndex, secondIndex) {
     var ideal = {};
 
+    // We check which page is closest to a "normal" page and use that to set the height
+    // for both pages.  This means that foldouts and other odd size pages will be displayed
+    // smaller than the nominal zoom amount.
     var canon5Dratio = 1.5;
     
     var first = {
@@ -1047,11 +1159,12 @@ GnuBook.prototype.getIdealSpreadSize = function(firstIndex, secondIndex) {
     var secondIndexRatio = second.height / second.width;
     //console.log('firstIndexRatio = ' + firstIndexRatio + ' secondIndexRatio = ' + secondIndexRatio);
 
+    var ratio;
     if (Math.abs(firstIndexRatio - canon5Dratio) < Math.abs(secondIndexRatio - canon5Dratio)) {
-        ideal.ratio = firstIndexRatio;
+        ratio = firstIndexRatio;
         //console.log('using firstIndexRatio ' + ratio);
     } else {
-        ideal.ratio = secondIndexRatio;
+        ratio = secondIndexRatio;
         //console.log('using secondIndexRatio ' + ratio);
     }
 
@@ -1061,42 +1174,50 @@ GnuBook.prototype.getIdealSpreadSize = function(firstIndex, secondIndex) {
     
     ideal.width  = ($('#GBcontainer').attr('clientWidth') - 30 - ideal.totalLeafEdgeWidth)>>1;
     ideal.height = $('#GBcontainer').height() - 30;  // $$$ why - 30?  book edge width?
-    //console.log('init idealWidth='+idealWidth+' idealHeight='+idealHeight + ' ratio='+ratio);
+    //console.log('init idealWidth='+ideal.width+' idealHeight='+ideal.height + ' ratio='+ratio);
 
-    if (ideal.height/ideal.ratio <= ideal.width) {
+    if (ideal.height/ratio <= ideal.width) {
         //use height
-        ideal.width = parseInt(ideal.height/ideal.ratio);
+        ideal.width = parseInt(ideal.height/ratio);
     } else {
         //use width
-        ideal.height = parseInt(ideal.width*ideal.ratio);
+        ideal.height = parseInt(ideal.width*ratio);
     }
     
     // XXX check this logic with large spreads
-    ideal.reduce = ((first.width + second.width) / 2) / ideal.width;
+    ideal.reduce = ((first.height + second.height) / 2) / ideal.height;
     
     return ideal;
 }
 
+// getSpreadSizeFromReduce()
+//______________________________________________________________________________
+// Returns the spread size calculated from the reduction factor for the given pages
 GnuBook.prototype.getSpreadSizeFromReduce = function(firstIndex, secondIndex, reduce) {
     var spreadSize = {};
     // $$$ Scale this based on reduce?
     var totalLeafEdgeWidth = parseInt(this.numLeafs * 0.1);
-    var maxLeafEdgeWidth   = parseInt($('#GBcontainer').attr('clientWidth') * 0.1);
+    var maxLeafEdgeWidth   = parseInt($('#GBcontainer').attr('clientWidth') * 0.1); // XXX update
     spreadSize.totalLeafEdgeWidth     = Math.min(totalLeafEdgeWidth, maxLeafEdgeWidth);
 
-    // $$$ this isn't quite right when the pages are different sizes
+    // $$$ Possibly incorrect -- we should make height "dominant"
     var nativeWidth = this.getPageWidth(firstIndex) + this.getPageWidth(secondIndex);
     var nativeHeight = this.getPageHeight(firstIndex) + this.getPageHeight(secondIndex);
     spreadSize.height = parseInt( (nativeHeight / 2) / this.reduce );
     spreadSize.width = parseInt( (nativeWidth / 2) / this.reduce );
     spreadSize.reduce = reduce;
     
-    // XXX
-    console.log('spread size: ' + firstIndex + ',' + secondIndex + ',' + reduce);
-
     return spreadSize;
 }
 
+// twoPageGetAutofitReduce()
+//______________________________________________________________________________
+// Returns the current ideal reduction factor
+GnuBook.prototype.twoPageGetAutofitReduce = function() {
+    var spreadSize = this.getIdealSpreadSize(this.twoPage.currentIndexL, this.twoPage.currentIndexR);
+    return spreadSize.reduce;
+}
+
 // currentIndex()
 //______________________________________________________________________________
 // Returns the currently active index.
@@ -1236,9 +1357,8 @@ GnuBook.prototype.flipBackToIndex = function(index) {
     
     if ('rl' != this.pageProgression) {
         // Assume LTR and we are going backward    
-        var gutter = this.prepareFlipLeftToRight(previousIndices[0], previousIndices[1]);        
-        this.flipLeftToRight(previousIndices[0], previousIndices[1], gutter);
-        
+        this.prepareFlipLeftToRight(previousIndices[0], previousIndices[1]);        
+        this.flipLeftToRight(previousIndices[0], previousIndices[1]);
     } else {
         // RTL and going backward
         var gutter = this.prepareFlipRightToLeft(previousIndices[0], previousIndices[1]);
@@ -1249,7 +1369,7 @@ GnuBook.prototype.flipBackToIndex = function(index) {
 // flipLeftToRight()
 //______________________________________________________________________________
 // Flips the page on the left towards the page on the right
-GnuBook.prototype.flipLeftToRight = function(newIndexL, newIndexR, gutter) {
+GnuBook.prototype.flipLeftToRight = function(newIndexL, newIndexR) {
 
     var leftLeaf = this.twoPage.currentIndexL;
     
@@ -1261,8 +1381,9 @@ GnuBook.prototype.flipLeftToRight = function(newIndexL, newIndexR, gutter) {
     var newWidthL    = this.getPageWidth2UP(newIndexL);
     var newWidthR    = this.getPageWidth2UP(newIndexR);
 
-    var top  = ($('#GBtwopageview').height() - this.twoPage.height) >> 1;                
-
+    var top  = this.twoPageTop();
+    var gutter = this.twoPage.middle + this.gutterOffsetForIndex(newIndexL);
+    
     //console.log('leftEdgeTmpW ' + leafEdgeTmpW);
     //console.log('  gutter ' + gutter + ', scaledWL ' + scaledWL + ', newLeafEdgeWL ' + newLeafEdgeWidthL);
     
@@ -1283,7 +1404,7 @@ GnuBook.prototype.flipLeftToRight = function(newIndexL, newIndexR, gutter) {
     //          and width=twoPage.edgeWidth-newLeafEdgeWidthL.
     //      - resize and move the left leaf edge (leafEdgeL) to left=gutter-newWidthL-newLeafEdgeWidthL
     //          and width=newLeafEdgeWidthL.
-    //      - resize the back cover (twoPageDiv) to left=gutter-newWidthL-newLeafEdgeWidthL-10
+    //      - resize the back cover (twoPage.coverDiv) to left=gutter-newWidthL-newLeafEdgeWidthL-10
     //          and width=newWidthL+newWidthR+twoPage.edgeWidth+20
     //      - move new left leaf (newIndexL) forward to zindex=2 so it can receive clicks.
     //      - remove old left and right leafs from the dom [pruneUnusedImgs()].
@@ -1319,16 +1440,14 @@ GnuBook.prototype.flipLeftToRight = function(newIndexL, newIndexR, gutter) {
     var left = $(this.prefetchedImgs[leftLeaf]).offset().left;
     // $$$ This seems very similar to the gutter.  May be able to consolidate the logic.
     var right = $('#GBtwopageview').attr('clientWidth')-left-$(this.prefetchedImgs[leftLeaf]).width()+$('#GBtwopageview').offset().left-2+'px';
+    
     // We change the left leaf to right positioning
+    // $$$ This causes animation glitches during resize.  See https://bugs.edge.launchpad.net/gnubook/+bug/328327
     $(this.prefetchedImgs[leftLeaf]).css({
         right: right,
         left: ''
     });
 
-     left = $(this.prefetchedImgs[leftLeaf]).offset().left - $('#book_div_1').offset().left;
-     
-     right = left+$(this.prefetchedImgs[leftLeaf]).width()+'px';
-
     $(this.leafEdgeTmp).animate({left: gutter}, this.flipSpeed, 'easeInSine');    
     //$(this.prefetchedImgs[leftLeaf]).animate({width: '0px'}, 'slow', 'easeInSine');
     
@@ -1345,7 +1464,7 @@ GnuBook.prototype.flipLeftToRight = function(newIndexL, newIndexR, gutter) {
         //console.log('  animating newIndexR ' + newIndexR + ' to ' + newWidthR + ' from ' + $(self.prefetchedImgs[newIndexR]).width());
         $(self.prefetchedImgs[newIndexR]).animate({width: newWidthR+'px'}, self.flipSpeed, 'easeOutSine', function() {
             $(self.prefetchedImgs[newIndexL]).css('zIndex', 2);
-
+            
             $(self.leafEdgeR).css({
                 // Moves the right leaf edge
                 width: self.twoPage.edgeWidth-newLeafEdgeWidthL+'px',
@@ -1358,11 +1477,10 @@ GnuBook.prototype.flipLeftToRight = function(newIndexL, newIndexR, gutter) {
                 left:  gutter-newWidthL-newLeafEdgeWidthL+'px'
             });
 
-            
-            $(self.twoPageDiv).css({
-                // Resizes the brown border div
-                width: newWidthL+newWidthR+self.twoPage.edgeWidth+20+'px',
-                left: gutter-newWidthL-newLeafEdgeWidthL-10+'px'
+            // Resizes the brown border div
+            $(self.twoPage.coverDiv).css({
+                width: self.twoPageCoverWidth(newWidthL+newWidthR)+'px',
+                left: gutter-newWidthL-newLeafEdgeWidthL-self.twoPage.coverInternalPadding+'px'
             });
             
             $(self.leafEdgeTmp).remove();
@@ -1379,7 +1497,6 @@ GnuBook.prototype.flipLeftToRight = function(newIndexL, newIndexR, gutter) {
             
             self.updateSearchHilites2UP();
             self.updatePageNumBox2UP();
-            //$('#GBzoom').text((self.twoPage.height/self.getPageHeight(newIndexL)).toString().substr(0,4));            
             
             if (self.animationFinishedCallback) {
                 self.animationFinishedCallback();
@@ -1421,7 +1538,7 @@ GnuBook.prototype.flipFwdToIndex = function(index) {
     } else {
         // RTL
         var gutter = this.prepareFlipLeftToRight(nextIndices[0], nextIndices[1]);
-        this.flipLeftToRight(nextIndices[0], nextIndices[1], gutter);
+        this.flipLeftToRight(nextIndices[0], nextIndices[1]);
     }
 }
 
@@ -1429,7 +1546,7 @@ GnuBook.prototype.flipFwdToIndex = function(index) {
 // $$$ better not to have to pass gutter in
 //______________________________________________________________________________
 // Flip from left to right and show the nextL and nextR indices on those sides
-GnuBook.prototype.flipRightToLeft = function(newIndexL, newIndexR, gutter) {
+GnuBook.prototype.flipRightToLeft = function(newIndexL, newIndexR) {
     var oldLeafEdgeWidthL = this.leafEdgeWidth(this.twoPage.currentIndexL);
     var oldLeafEdgeWidthR = this.twoPage.edgeWidth-oldLeafEdgeWidthL;
     var newLeafEdgeWidthL = this.leafEdgeWidth(newIndexL);  
@@ -1437,16 +1554,12 @@ GnuBook.prototype.flipRightToLeft = function(newIndexL, newIndexR, gutter) {
 
     var leafEdgeTmpW = oldLeafEdgeWidthR - newLeafEdgeWidthR;
 
-    var top  = ($('#GBtwopageview').height() - this.twoPage.height) >> 1;                
-
+    var top = this.twoPageTop();
     var scaledW = this.getPageWidth2UP(this.twoPage.currentIndexR);
 
-    var middle     = ($('#GBtwopageview').attr('clientWidth') >> 1);
-    var currGutter = middle + this.gutterOffsetForIndex(this.twoPage.currentIndexL);
+    var middle = this.twoPage.middle;
+    var gutter = middle + this.gutterOffsetForIndex(newIndexL);
     
-    // XXX
-    console.log('R->L middle(' + middle +') currGutter(' + currGutter + ') gutter(' + gutter + ')');
-
     this.leafEdgeTmp = document.createElement('div');
     $(this.leafEdgeTmp).css({
         borderStyle: 'solid none solid solid',
@@ -1455,7 +1568,7 @@ GnuBook.prototype.flipRightToLeft = function(newIndexL, newIndexR, gutter) {
         background: 'transparent url(' + this.imagesBaseURL + 'left_edges.png) repeat scroll 0% 0%',
         width: leafEdgeTmpW + 'px',
         height: this.twoPage.height-1 + 'px',
-        left: currGutter+scaledW+'px',
+        left: gutter+scaledW+'px',
         top: top+'px',    
         position: 'absolute',
         zIndex:1000
@@ -1482,15 +1595,16 @@ GnuBook.prototype.flipRightToLeft = function(newIndexL, newIndexR, gutter) {
         $(self.leafEdgeTmp).animate({left: gutter-newWidthL-leafEdgeTmpW+'px'}, speed, 'easeOutSine');    
         $(self.prefetchedImgs[newIndexL]).animate({width: newWidthL+'px'}, speed, 'easeOutSine', function() {
             $(self.prefetchedImgs[newIndexR]).css('zIndex', 2);
-
+            
             $(self.leafEdgeL).css({
                 width: newLeafEdgeWidthL+'px', 
                 left: gutter-newWidthL-newLeafEdgeWidthL+'px'
             });
             
-            $(self.twoPageDiv).css({
-                width: newWidthL+newWidthR+self.twoPage.edgeWidth+20+'px',
-                left: gutter-newWidthL-newLeafEdgeWidthL-10+'px'
+            // Resizes the book cover
+            $(self.twoPage.coverDiv).css({
+                width: self.twoPageCoverWidth(newWidthL+newWidthR)+'px',
+                left: gutter - newWidthL - newLeafEdgeWidthL - self.coverInternalPadding + 'px'
             });
             
             $(self.leafEdgeTmp).remove();
@@ -1508,7 +1622,6 @@ GnuBook.prototype.flipRightToLeft = function(newIndexL, newIndexR, gutter) {
 
             self.updateSearchHilites2UP();
             self.updatePageNumBox2UP();
-            //$('#GBzoom').text((self.twoPage.height/self.getPageHeight(newIndexL)).toString().substr(0,4));
             
             if (self.animationFinishedCallback) {
                 self.animationFinishedCallback();
@@ -1562,9 +1675,9 @@ GnuBook.prototype.prepareFlipLeftToRight = function(prevL, prevR) {
     
     var height  = this.getPageHeight(prevL); 
     var width   = this.getPageWidth(prevL);    
-    var middle = ($('#GBtwopageview').attr('clientWidth') >> 1);
-    var top  = ($('#GBtwopageview').height() - this.twoPage.height) >> 1;                
-    var scaledW = this.twoPage.height*width/height;
+    var middle = this.twoPage.middle;
+    var top  = this.twoPageTop();                
+    var scaledW = this.twoPage.height*width/height; // $$$ assumes height of page is dominant
 
     // The gutter is the dividing line between the left and right pages.
     // It is offset from the middle to create the illusion of thickness to the pages
@@ -1577,9 +1690,8 @@ GnuBook.prototype.prepareFlipLeftToRight = function(prevL, prevR) {
     
     $(this.prefetchedImgs[prevL]).css({
         position: 'absolute',
-        /*right:   middle+'px',*/
         left: gutter-scaledW+'px',
-        right: '',
+        right: '', // clear right property
         top:    top+'px',
         backgroundColor: 'rgb(234, 226, 205)',
         height: this.twoPage.height,
@@ -1605,9 +1717,6 @@ GnuBook.prototype.prepareFlipLeftToRight = function(prevL, prevR) {
     });
 
     $('#GBtwopageview').append(this.prefetchedImgs[prevR]);
-
-
-    return gutter;
             
 }
 
@@ -1618,13 +1727,14 @@ GnuBook.prototype.prepareFlipRightToLeft = function(nextL, nextR) {
 
     //console.log('  preparing left<-right for ' + nextL + ',' + nextR);
 
+    // Prefetch images
     this.prefetchImg(nextL);
     this.prefetchImg(nextR);
 
     var height  = this.getPageHeight(nextR); 
     var width   = this.getPageWidth(nextR);    
-    var middle = ($('#GBtwopageview').attr('clientWidth') >> 1);
-    var top  = ($('#GBtwopageview').height() - this.twoPage.height) >> 1;                
+    var middle = this.twoPage.middle;
+    var top  = this.twoPageTop();               
     var scaledW = this.twoPage.height*width/height;
 
     var gutter = middle + this.gutterOffsetForIndex(nextL);
@@ -1656,14 +1766,12 @@ GnuBook.prototype.prepareFlipRightToLeft = function(nextL, nextR) {
         top:    top+'px',
         backgroundColor: 'rgb(234, 226, 205)',
         height: this.twoPage.height,
-        width:  0+'px',
+        width:  0+'px', // Start at 0 width, then grow to the left
         borderRight: '1px solid black',
         zIndex: 2
     });
 
     $('#GBtwopageview').append(this.prefetchedImgs[nextL]);    
-
-    return gutter;
             
 }
 
@@ -1728,9 +1836,10 @@ GnuBook.prototype.prefetch = function() {
 // getPageWidth2UP()
 //______________________________________________________________________________
 GnuBook.prototype.getPageWidth2UP = function(index) {
+    // We return the width based on the dominant height
     var height  = this.getPageHeight(index); 
     var width   = this.getPageWidth(index);    
-    return Math.floor(this.twoPage.height*width/height);
+    return Math.floor(this.twoPage.height*width/height); // $$$ we assume width is relative to current spread
 }    
 
 // search()
@@ -1851,12 +1960,72 @@ GnuBook.prototype.updateSearchHilites1UP = function() {
     }
 }
 
+// XXX move, clean up, use everywhere
+GnuBook.prototype.twoPageGutter = function() {
+    return this.twoPage.middle + this.gutterOffsetForIndex(this.twoPage.currentIndexL);
+}
+
+// XXX move, clean up, use everywhere
+GnuBook.prototype.twoPageTop = function() {
+    return this.twoPage.coverExternalPadding + this.twoPage.coverInternalPadding; // $$$ + border?
+}
+    
+GnuBook.prototype.twoPageCoverWidth = function(totalPageWidth) {
+    return totalPageWidth + this.twoPage.edgeWidth + 2*this.twoPage.coverInternalPadding;
+}
+
+GnuBook.prototype.twoPageGetViewCenter = function() {
+    var center = {};
+
+    var containerOffset = $('#GBcontainer').offset();
+    var viewOffset = $('#GBtwopageview').offset();
+    center.percentageX = (containerOffset.left - viewOffset.left + ($('#GBcontainer').attr('clientWidth') >> 1)) / this.twoPage.totalWidth;
+    center.percentageY = (containerOffset.top - viewOffset.top + ($('#GBcontainer').attr('clientHeight') >> 1)) / this.twoPage.totalHeight;
+    
+    return center;
+}
+
+// twoPageCenterView(percentageX, percentageY)
+//______________________________________________________________________________
+// Centers the point given by percentage from left,top of twopageview
+GnuBook.prototype.twoPageCenterView = function(percentageX, percentageY) {
+    if ('undefined' == typeof(percentageX)) {
+        percentageX = 0.5;
+    }
+    if ('undefined' == typeof(percentageY)) {
+        percentageY = 0.5;
+    }
+
+    var viewWidth = $('#GBtwopageview').width();
+    var containerClientWidth = $('#GBcontainer').attr('clientWidth');
+    var intoViewX = percentageX * viewWidth;
+    
+    var viewHeight = $('#GBtwopageview').height();
+    var containerClientHeight = $('#GBcontainer').attr('clientHeight');
+    var intoViewY = percentageY * viewHeight;
+    
+    if (viewWidth < containerClientWidth) {
+        // Can fit width without scrollbars - center by adjusting offset
+        $('#GBtwopageview').css('left', (containerClientWidth >> 1) - intoViewX + 'px');    
+    } else {
+        // Need to scroll to center
+        $('#GBtwopageview').css('left', 0);
+        $('#GBcontainer').scrollLeft(intoViewX - (containerClientWidth >> 1));
+    }
+    
+    if (viewHeight < containerClientHeight) {
+        // Fits with scrollbars - add offset
+        $('#GBtwopageview').css('top', (containerClientHeight >> 1) - intoViewY + 'px');
+    } else {
+        $('#GBtwopageview').css('top', 0);
+        $('#GBcontainer').scrollTop(intoViewY - (containerClientHeight >> 1));
+    }
+}
+    
 // showSearchHilites2UP()
 //______________________________________________________________________________
 GnuBook.prototype.updateSearchHilites2UP = function() {
 
-    var middle = ($('#GBtwopageview').attr('clientWidth') >> 1);
-
     for (var key in this.searchResults) {
         key = parseInt(key, 10);
         if (-1 != jQuery.inArray(key, this.displayedIndices)) {
@@ -1867,20 +2036,22 @@ GnuBook.prototype.updateSearchHilites2UP = function() {
                 //console.log('appending ' + key);
             }
 
+            // We calculate the reduction factor for the specific page because it can be different
+            // for each page in the spread
             var height = this.getPageHeight(key);
             var width  = this.getPageWidth(key)
             var reduce = this.twoPage.height/height;
             var scaledW = parseInt(width*reduce);
             
-            var gutter = middle + this.gutterOffsetForIndex(this.twoPage.currentIndexL);
-            
+            var gutter = this.twoPageGutter();
+            var pageL;
             if ('L' == this.getPageSide(key)) {
-                var pageL = gutter-scaledW;
+                pageL = gutter-scaledW;
             } else {
-                var pageL = gutter;
+                pageL = gutter;
             }
-            var pageT  = ($('#GBtwopagview').height() - this.twoPage.height) >> 1;                
-                        
+            var pageT  = this.twoPageTop();
+            
             $(result.div).css({
                 width:  (result.r-result.l)*reduce + 'px',
                 height: (result.b-result.t)*reduce + 'px',
@@ -1949,6 +2120,11 @@ GnuBook.prototype.autoToggle = function() {
         bComingFrom1up = true;
         this.switchMode(2);
     }
+    
+    // Change to autofit if book is too large
+    if (this.reduce < this.twoPageGetAutofitReduce()) {
+        this.zoom2up(0);
+    }
 
     var self = this;
     if (null == this.autoTimer) {
@@ -1993,6 +2169,29 @@ GnuBook.prototype.autoStop = function() {
     }
 }
 
+// $$$ document
+GnuBook.prototype.stopFlipAnimations = function() {
+
+    // Stop animation, clear queue, trigger callbacks
+    if (this.leafEdgeTmp) {
+        $(this.leafEdgeTmp).stop(false, true);
+    }
+    console.log(this.leafEdgeTmp);
+    jQuery.each(this.prefetchedImgs, function() {
+        $(this).stop(false, true);
+        });
+
+    // And again since animations also queued in callbacks
+    if (this.leafEdgeTmp) {
+        $(this.leafEdgeTmp).stop(false, true);
+    }
+    console.log(this.leafEdgeTmp);
+    jQuery.each(this.prefetchedImgs, function() {
+        $(this).stop(false, true);
+        });
+   
+}
+
 // keyboardNavigationIsDisabled(event)
 //   - returns true if keyboard navigation should be disabled for the event
 //______________________________________________________________________________
@@ -2075,7 +2274,7 @@ GnuBook.prototype.initToolbar = function(mode, ui) {
         + "<a class='GBicon logo rollover' href='" + this.logoURL + "'>&nbsp;</a>"
         + " <button class='GBicon rollover zoom_out' onclick='gb.zoom(-1); return false;'/>" 
         + "<button class='GBicon rollover zoom_in' onclick='gb.zoom(1); return false;'/>"
-        + " <span class='label'>Zoom: <span id='GBzoom'>"+100/this.reduce+"</span>%</span>"
+        + " <span class='label'>Zoom: <span id='GBzoom'>"+parseInt(100/this.reduce)+"</span>%</span>"
         + " <button class='GBicon rollover one_page_mode' onclick='gb.switchMode(1); return false;'/>"
         + " <button class='GBicon rollover two_page_mode' onclick='gb.switchMode(2); return false;'/>"
         + "&nbsp;&nbsp;<a class='GBblack title' href='"+this.bookUrl+"' target='_blank'>"+this.shortTitle(50)+"</a>"
@@ -2215,6 +2414,14 @@ GnuBook.prototype.bindToolbarNavHandlers = function(jToolbar) {
     });
 }
 
+// updateToolbarZoom(reduce)
+//______________________________________________________________________________
+// Update the displayed zoom factor based on reduction factor
+GnuBook.prototype.updateToolbarZoom = function(reduce) {
+    // $$$ TODO: Move toolbar to it's own object/plugin
+    $('#GBzoom').text(parseInt(100/reduce));
+}
+
 // firstDisplayableIndex
 //______________________________________________________________________________
 // Returns the index of the first visible page, dependent on the mode.