Improved print UI
[bookreader.git] / GnuBook / GnuBook.js
1 /*
2 Copyright(c)2008-2009 Internet Archive. Software license AGPL version 3.
3
4 This file is part of GnuBook.
5
6     GnuBook is free software: you can redistribute it and/or modify
7     it under the terms of the GNU Affero General Public License as published by
8     the Free Software Foundation, either version 3 of the License, or
9     (at your option) any later version.
10
11     GnuBook is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU Affero General Public License for more details.
15
16     You should have received a copy of the GNU Affero General Public License
17     along with GnuBook.  If not, see <http://www.gnu.org/licenses/>.
18     
19     The GnuBook source is hosted at http://github.com/openlibrary/bookreader/
20
21     archive.org cvs $Revision: 1.2 $ $Date: 2009-06-22 18:42:51 $
22 */
23
24 // GnuBook()
25 //______________________________________________________________________________
26 // After you instantiate this object, you must supply the following
27 // book-specific functions, before calling init():
28 //  - getPageWidth()
29 //  - getPageHeight()
30 //  - getPageURI()
31 // You must also add a numLeafs property before calling init().
32
33 function GnuBook() {
34     this.reduce  = 4;
35     this.padding = 10;
36     this.mode    = 1; //1 or 2
37     this.ui = 'full'; // UI mode
38     
39     this.displayedIndices = []; 
40     //this.indicesToDisplay = [];
41     this.imgs = {};
42     this.prefetchedImgs = {}; //an object with numeric keys cooresponding to page index
43     
44     this.timer     = null;
45     this.animating = false;
46     this.auto      = false;
47     this.autoTimer = null;
48     this.flipSpeed = 'fast';
49
50     this.twoPagePopUp = null;
51     this.leafEdgeTmp  = null;
52     this.embedPopup = null;
53     this.printPopup = null;
54     
55     this.searchResults = {};
56     
57     this.firstIndex = null;
58     
59     this.lastDisplayableIndex2up = null;
60     
61     // We link to index.php to avoid redirect which breaks back button
62     this.logoURL = 'http://www.archive.org/index.php';
63     
64     // Base URL for images
65     this.imagesBaseURL = '/bookreader/images/';
66     
67     // Mode constants
68     this.constMode1up = 1;
69     this.constMode2up = 2;
70     
71     // Zoom levels
72     this.reductionFactors = [0.5, 1, 2, 4, 8, 16];
73
74     // Object to hold parameters related to 2up mode
75     this.twoPage = {
76         coverInternalPadding: 10, // Width of cover
77         coverExternalPadding: 10, // Padding outside of cover
78         bookSpineDivWidth: 30,    // Width of book spine  $$$ consider sizing based on book length
79         autofit: true
80     };
81 };
82
83 // init()
84 //______________________________________________________________________________
85 GnuBook.prototype.init = function() {
86
87     var startIndex = undefined;
88     
89     // Find start index and mode if set in location hash
90     var params = this.paramsFromFragment(window.location.hash);
91         
92     if ('undefined' != typeof(params.index)) {
93         startIndex = params.index;
94     } else if ('undefined' != typeof(params.page)) {
95         startIndex = this.getPageIndex(params.page);
96     }
97     
98     if ('undefined' == typeof(startIndex)) {
99         if ('undefined' != typeof(this.titleLeaf)) {
100             startIndex = this.leafNumToIndex(this.titleLeaf);
101         }
102     }
103     
104     if ('undefined' == typeof(startIndex)) {
105         startIndex = 0;
106     }
107     
108     if ('undefined' != typeof(params.mode)) {
109         this.mode = params.mode;
110     }
111     
112     // Set document title -- may have already been set in enclosing html for
113     // search engine visibility
114     document.title = this.shortTitle(50);
115     
116     // Sanitize parameters
117     if ( !this.canSwitchToMode( this.mode ) ) {
118         this.mode = this.constMode1up;
119     }
120     
121     $("#GnuBook").empty();
122     this.initToolbar(this.mode, this.ui); // Build inside of toolbar div
123     $("#GnuBook").append("<div id='GBcontainer'></div>");
124     $("#GBcontainer").append("<div id='GBpageview'></div>");
125
126     $("#GBcontainer").bind('scroll', this, function(e) {
127         e.data.loadLeafs();
128     });
129
130     this.setupKeyListeners();
131     this.startLocationPolling();
132
133     $(window).bind('resize', this, function(e) {
134         //console.log('resize!');
135         if (1 == e.data.mode) {
136             //console.log('centering 1page view');
137             e.data.centerPageView();
138             $('#GBpageview').empty()
139             e.data.displayedIndices = [];
140             e.data.updateSearchHilites(); //deletes hilights but does not call remove()            
141             e.data.loadLeafs();
142         } else {
143             //console.log('drawing 2 page view');
144             
145             // We only need to prepare again in autofit (size of spread changes)
146             if (e.data.twoPage.autofit) {
147                 e.data.prepareTwoPageView();
148             } else {
149                 // Re-center if the scrollbars have disappeared
150                 var center = e.data.twoPageGetViewCenter();
151                 var doRecenter = false;
152                 if (e.data.twoPage.totalWidth < $('#GBcontainer').attr('clientWidth')) {
153                     center.percentageX = 0.5;
154                     doRecenter = true;
155                 }
156                 if (e.data.twoPage.totalHeight < $('#GBcontainer').attr('clientHeight')) {
157                     center.percentageY = 0.5;
158                     doRecenter = true;
159                 }
160                 if (doRecenter) {
161                     e.data.twoPageCenterView(center.percentageX, center.percentageY);
162                 }
163             }
164         }
165     });
166     
167     $('.GBpagediv1up').bind('mousedown', this, function(e) {
168         // $$$ the purpose of this is to disable selection of the image (makes it turn blue)
169         //     but this also interferes with right-click.  See https://bugs.edge.launchpad.net/gnubook/+bug/362626
170     });
171
172     if (1 == this.mode) {
173         this.resizePageView();
174         this.firstIndex = startIndex;
175         this.jumpToIndex(startIndex);
176     } else {
177         //this.resizePageView();
178         
179         this.displayedIndices=[0];
180         this.firstIndex = startIndex;
181         this.displayedIndices = [this.firstIndex];
182         //console.log('titleLeaf: %d', this.titleLeaf);
183         //console.log('displayedIndices: %s', this.displayedIndices);
184         this.prepareTwoPageView();
185     }
186         
187     // Enact other parts of initial params
188     this.updateFromParams(params);
189 }
190
191 GnuBook.prototype.setupKeyListeners = function() {
192     var self = this;
193     
194     var KEY_PGUP = 33;
195     var KEY_PGDOWN = 34;
196     var KEY_END = 35;
197     var KEY_HOME = 36;
198
199     var KEY_LEFT = 37;
200     var KEY_UP = 38;
201     var KEY_RIGHT = 39;
202     var KEY_DOWN = 40;
203
204     // We use document here instead of window to avoid a bug in jQuery on IE7
205     $(document).keydown(function(e) {
206         // Keyboard navigation        
207         switch(e.keyCode) {
208             case KEY_PGUP:
209             case KEY_UP:            
210                 // In 1up mode page scrolling is handled by browser
211                 if (2 == self.mode) {
212                     e.preventDefault();
213                     self.prev();
214                 }
215                 break;
216             case KEY_DOWN:
217             case KEY_PGDOWN:
218                 if (2 == self.mode) {
219                     e.preventDefault();
220                     self.next();
221                 }
222                 break;
223             case KEY_END:
224                 e.preventDefault();
225                 self.last();
226                 break;
227             case KEY_HOME:
228                 e.preventDefault();
229                 self.first();
230                 break;
231             case KEY_LEFT:
232                 if (self.keyboardNavigationIsDisabled(e)) {
233                     e.preventDefault();
234                     break;
235                 }
236                 if (2 == self.mode) {
237                     e.preventDefault();
238                     self.left();
239                 }
240                 break;
241             case KEY_RIGHT:
242                 if (self.keyboardNavigationIsDisabled(e)) {
243                     e.preventDefault();
244                     break;
245                 }
246                 if (2 == self.mode) {
247                     e.preventDefault();
248                     self.right();
249                 }
250                 break;
251         }
252     });
253 }
254
255 // drawLeafs()
256 //______________________________________________________________________________
257 GnuBook.prototype.drawLeafs = function() {
258     if (1 == this.mode) {
259         this.drawLeafsOnePage();
260     } else {
261         this.drawLeafsTwoPage();
262     }
263 }
264
265 // setDragHandler()
266 //______________________________________________________________________________
267 GnuBook.prototype.setDragHandler = function(div) {
268     div.dragging = false;
269
270     $(div).unbind('mousedown').bind('mousedown', function(e) {
271         e.preventDefault();
272         
273         //console.log('mousedown at ' + e.pageY);
274
275         this.dragging = true;
276         this.prevMouseX = e.pageX;
277         this.prevMouseY = e.pageY;
278     
279         var startX    = e.pageX;
280         var startY    = e.pageY;
281         var startTop  = $('#GBcontainer').attr('scrollTop');
282         var startLeft =  $('#GBcontainer').attr('scrollLeft');
283
284     });
285         
286     $(div).unbind('mousemove').bind('mousemove', function(ee) {
287         ee.preventDefault();
288
289         // console.log('mousemove ' + ee.pageX + ',' + ee.pageY);
290         
291         var offsetX = ee.pageX - this.prevMouseX;
292         var offsetY = ee.pageY - this.prevMouseY;
293         
294         if (this.dragging) {
295             $('#GBcontainer').attr('scrollTop', $('#GBcontainer').attr('scrollTop') - offsetY);
296             $('#GBcontainer').attr('scrollLeft', $('#GBcontainer').attr('scrollLeft') - offsetX);
297         }
298         
299         this.prevMouseX = ee.pageX;
300         this.prevMouseY = ee.pageY;
301         
302     });
303     
304     $(div).unbind('mouseup').bind('mouseup', function(ee) {
305         ee.preventDefault();
306         //console.log('mouseup');
307
308         this.dragging = false;
309     });
310     
311     $(div).unbind('mouseleave').bind('mouseleave', function(e) {
312         e.preventDefault();
313         //console.log('mouseleave');
314
315         this.dragging = false;        
316     });
317     
318     $(div).unbind('mouseenter').bind('mouseenter', function(e) {
319         e.preventDefault();
320         //console.log('mouseenter');
321         
322         this.dragging = false;
323     });
324 }
325
326 // setDragHandler2UP()
327 //______________________________________________________________________________
328 GnuBook.prototype.setDragHandler2UP = function(div) {
329     div.dragging = false;
330     
331     $(div).unbind('mousedown').bind('mousedown', function(e) {
332         e.preventDefault();
333         
334         //console.log('mousedown at ' + e.pageY);
335
336         this.dragStart = {x: e.pageX, y: e.pageY };
337         this.mouseDown = true;
338         this.dragging = false; // wait until drag distance
339         this.prevMouseX = e.pageX;
340         this.prevMouseY = e.pageY;
341     
342         var startX    = e.pageX;
343         var startY    = e.pageY;
344         var startTop  = $('#GBcontainer').attr('scrollTop');
345         var startLeft =  $('#GBcontainer').attr('scrollLeft');
346
347     });
348         
349     $(div).unbind('mousemove').bind('mousemove', function(ee) {
350         ee.preventDefault();
351
352         // console.log('mousemove ' + ee.pageX + ',' + ee.pageY);
353         
354         var offsetX = ee.pageX - this.prevMouseX;
355         var offsetY = ee.pageY - this.prevMouseY;
356         
357         var minDragDistance = 5; // $$$ constant
358
359         var distance = Math.max(Math.abs(offsetX), Math.abs(offsetY));
360                 
361         if (this.mouseDown && (distance > minDragDistance)) {
362             //console.log('drag start!');
363             
364             this.dragging = true;
365         }
366         
367         if (this.dragging) {        
368             $('#GBcontainer').attr('scrollTop', $('#GBcontainer').attr('scrollTop') - offsetY);
369             $('#GBcontainer').attr('scrollLeft', $('#GBcontainer').attr('scrollLeft') - offsetX);
370             this.prevMouseX = ee.pageX;
371             this.prevMouseY = ee.pageY;
372         }
373         
374         
375     });
376     
377     /*
378     $(div).unbind('mouseup').bind('mouseup', function(ee) {
379         ee.preventDefault();
380         //console.log('mouseup');
381
382         this.dragging = false;
383         this.mouseDown = false;
384     });
385     */
386     
387     
388     $(div).unbind('mouseleave').bind('mouseleave', function(e) {
389         e.preventDefault();
390         //console.log('mouseleave');
391
392         this.dragging = false;  
393         this.mouseDown = false;
394     });
395     
396     $(div).unbind('mouseenter').bind('mouseenter', function(e) {
397         e.preventDefault();
398         //console.log('mouseenter');
399         
400         this.dragging = false;
401         this.mouseDown = false;
402     });
403 }
404
405 GnuBook.prototype.setClickHandler2UP = function( element, data, handler) {
406     //console.log('setting handler');
407     //console.log(element.tagName);
408     
409     $(element).unbind('click').bind('click', data, function(e) {
410         e.preventDefault();
411         
412         //console.log('click!');
413         
414         if (this.mouseDown && (!this.dragging)) {
415             //console.log('click not dragging!');
416             handler(e);
417         }
418         
419         this.dragging = false;
420         this.mouseDown = false;
421     });
422 }
423
424 // drawLeafsOnePage()
425 //______________________________________________________________________________
426 GnuBook.prototype.drawLeafsOnePage = function() {
427     //alert('drawing leafs!');
428     this.timer = null;
429
430
431     var scrollTop = $('#GBcontainer').attr('scrollTop');
432     var scrollBottom = scrollTop + $('#GBcontainer').height();
433     //console.log('top=' + scrollTop + ' bottom='+scrollBottom);
434     
435     var indicesToDisplay = [];
436     
437     var i;
438     var leafTop = 0;
439     var leafBottom = 0;
440     for (i=0; i<this.numLeafs; i++) {
441         var height  = parseInt(this.getPageHeight(i)/this.reduce); 
442     
443         leafBottom += height;
444         //console.log('leafTop = '+leafTop+ ' pageH = ' + this.pageH[i] + 'leafTop>=scrollTop=' + (leafTop>=scrollTop));
445         var topInView    = (leafTop >= scrollTop) && (leafTop <= scrollBottom);
446         var bottomInView = (leafBottom >= scrollTop) && (leafBottom <= scrollBottom);
447         var middleInView = (leafTop <=scrollTop) && (leafBottom>=scrollBottom);
448         if (topInView | bottomInView | middleInView) {
449             //console.log('displayed: ' + this.displayedIndices);
450             //console.log('to display: ' + i);
451             indicesToDisplay.push(i);
452         }
453         leafTop += height +10;      
454         leafBottom += 10;
455     }
456
457     var firstIndexToDraw  = indicesToDisplay[0];
458     this.firstIndex      = firstIndexToDraw;
459     
460     // Update hash, but only if we're currently displaying a leaf
461     // Hack that fixes #365790
462     if (this.displayedIndices.length > 0) {
463         this.updateLocationHash();
464     }
465
466     if ((0 != firstIndexToDraw) && (1 < this.reduce)) {
467         firstIndexToDraw--;
468         indicesToDisplay.unshift(firstIndexToDraw);
469     }
470     
471     var lastIndexToDraw = indicesToDisplay[indicesToDisplay.length-1];
472     if ( ((this.numLeafs-1) != lastIndexToDraw) && (1 < this.reduce) ) {
473         indicesToDisplay.push(lastIndexToDraw+1);
474     }
475     
476     leafTop = 0;
477     var i;
478     for (i=0; i<firstIndexToDraw; i++) {
479         leafTop += parseInt(this.getPageHeight(i)/this.reduce) +10;
480     }
481
482     //var viewWidth = $('#GBpageview').width(); //includes scroll bar width
483     var viewWidth = $('#GBcontainer').attr('scrollWidth');
484
485
486     for (i=0; i<indicesToDisplay.length; i++) {
487         var index = indicesToDisplay[i];    
488         var height  = parseInt(this.getPageHeight(index)/this.reduce); 
489
490         if(-1 == jQuery.inArray(indicesToDisplay[i], this.displayedIndices)) {            
491             var width   = parseInt(this.getPageWidth(index)/this.reduce); 
492             //console.log("displaying leaf " + indicesToDisplay[i] + ' leafTop=' +leafTop);
493             var div = document.createElement("div");
494             div.className = 'GBpagediv1up';
495             div.id = 'pagediv'+index;
496             div.style.position = "absolute";
497             $(div).css('top', leafTop + 'px');
498             var left = (viewWidth-width)>>1;
499             if (left<0) left = 0;
500             $(div).css('left', left+'px');
501             $(div).css('width', width+'px');
502             $(div).css('height', height+'px');
503             //$(div).text('loading...');
504             
505             this.setDragHandler(div);
506             
507             $('#GBpageview').append(div);
508
509             var img = document.createElement("img");
510             img.src = this.getPageURI(index, this.reduce, 0);
511             $(img).css('width', width+'px');
512             $(img).css('height', height+'px');
513             $(div).append(img);
514
515         } else {
516             //console.log("not displaying " + indicesToDisplay[i] + ' score=' + jQuery.inArray(indicesToDisplay[i], this.displayedIndices));            
517         }
518
519         leafTop += height +10;
520
521     }
522     
523     for (i=0; i<this.displayedIndices.length; i++) {
524         if (-1 == jQuery.inArray(this.displayedIndices[i], indicesToDisplay)) {
525             var index = this.displayedIndices[i];
526             //console.log('Removing leaf ' + index);
527             //console.log('id='+'#pagediv'+index+ ' top = ' +$('#pagediv'+index).css('top'));
528             $('#pagediv'+index).remove();
529         } else {
530             //console.log('NOT Removing leaf ' + this.displayedIndices[i]);
531         }
532     }
533     
534     this.displayedIndices = indicesToDisplay.slice();
535     this.updateSearchHilites();
536     
537     if (null != this.getPageNum(firstIndexToDraw))  {
538         $("#GBpagenum").val(this.getPageNum(this.currentIndex()));
539     } else {
540         $("#GBpagenum").val('');
541     }
542             
543     this.updateToolbarZoom(this.reduce);
544     
545 }
546
547 // drawLeafsTwoPage()
548 //______________________________________________________________________________
549 GnuBook.prototype.drawLeafsTwoPage = function() {
550     var scrollTop = $('#GBtwopageview').attr('scrollTop');
551     var scrollBottom = scrollTop + $('#GBtwopageview').height();
552     
553     // $$$ we should use calculated values in this.twoPage (recalc if necessary)
554     
555     var indexL = this.twoPage.currentIndexL;
556     var heightL  = this.getPageHeight(indexL); 
557     var widthL   = this.getPageWidth(indexL);
558
559     var leafEdgeWidthL = this.leafEdgeWidth(indexL);
560     var leafEdgeWidthR = this.twoPage.edgeWidth - leafEdgeWidthL;
561     //var bookCoverDivWidth = this.twoPage.width*2 + 20 + this.twoPage.edgeWidth; // $$$ hardcoded cover width
562     var bookCoverDivWidth = this.twoPage.bookCoverDivWidth;
563     //console.log(leafEdgeWidthL);
564
565     var middle = this.twoPage.middle; // $$$ getter instead?
566     var top = this.twoPageTop();
567     var bookCoverDivLeft = this.twoPage.bookCoverDivLeft;
568
569     this.twoPage.scaledWL = this.getPageWidth2UP(indexL);
570     this.twoPage.gutter = this.twoPageGutter();
571     
572     this.prefetchImg(indexL);
573     $(this.prefetchedImgs[indexL]).css({
574         position: 'absolute',
575         left: this.twoPage.gutter-this.twoPage.scaledWL+'px',
576         right: '',
577         top:    top+'px',
578         backgroundColor: 'rgb(234, 226, 205)',
579         height: this.twoPage.height +'px', // $$$ height forced the same for both pages
580         width:  this.twoPage.scaledWL + 'px',
581         borderRight: '1px solid black',
582         zIndex: 2
583     }).appendTo('#GBtwopageview');
584     
585     var indexR = this.twoPage.currentIndexR;
586     var heightR  = this.getPageHeight(indexR); 
587     var widthR   = this.getPageWidth(indexR);
588
589     // $$$ should use getwidth2up?
590     //var scaledWR = this.twoPage.height*widthR/heightR;
591     this.twoPage.scaledWR = this.getPageWidth2UP(indexR);
592     this.prefetchImg(indexR);
593     $(this.prefetchedImgs[indexR]).css({
594         position: 'absolute',
595         left:   this.twoPage.gutter+'px',
596         right: '',
597         top:    top+'px',
598         backgroundColor: 'rgb(234, 226, 205)',
599         height: this.twoPage.height + 'px', // $$$ height forced the same for both pages
600         width:  this.twoPage.scaledWR + 'px',
601         borderLeft: '1px solid black',
602         zIndex: 2
603     }).appendTo('#GBtwopageview');
604         
605
606     this.displayedIndices = [this.twoPage.currentIndexL, this.twoPage.currentIndexR];
607     this.setMouseHandlers2UP();
608     this.twoPageSetCursor();
609
610     this.updatePageNumBox2UP();
611     this.updateToolbarZoom(this.reduce);
612     
613     // this.twoPagePlaceFlipAreas();  // No longer used
614
615 }
616
617 // updatePageNumBox2UP
618 //______________________________________________________________________________
619 GnuBook.prototype.updatePageNumBox2UP = function() {
620     if (null != this.getPageNum(this.twoPage.currentIndexL))  {
621         $("#GBpagenum").val(this.getPageNum(this.twoPage.currentIndexL));
622     } else {
623         $("#GBpagenum").val('');
624     }
625     this.updateLocationHash();
626 }
627
628 // loadLeafs()
629 //______________________________________________________________________________
630 GnuBook.prototype.loadLeafs = function() {
631
632
633     var self = this;
634     if (null == this.timer) {
635         this.timer=setTimeout(function(){self.drawLeafs()},250);
636     } else {
637         clearTimeout(this.timer);
638         this.timer=setTimeout(function(){self.drawLeafs()},250);    
639     }
640 }
641
642 // zoom(direction)
643 //
644 // Pass 1 to zoom in, anything else to zoom out
645 //______________________________________________________________________________
646 GnuBook.prototype.zoom = function(direction) {
647     switch (this.mode) {
648         case this.constMode1up:
649             return this.zoom1up(direction);
650         case this.constMode2up:
651             return this.zoom2up(direction);
652     }
653 }
654
655 // zoom1up(dir)
656 //______________________________________________________________________________
657 GnuBook.prototype.zoom1up = function(dir) {
658     if (2 == this.mode) {     //can only zoom in 1-page mode
659         this.switchMode(1);
660         return;
661     }
662     
663     // $$$ with flexible zoom we could "snap" to /2 page reductions
664     //     for better scaling
665     if (1 == dir) {
666         if (this.reduce <= 0.5) return;
667         this.reduce*=0.5;           //zoom in
668     } else {
669         if (this.reduce >= 8) return;
670         this.reduce*=2;             //zoom out
671     }
672     
673     this.resizePageView();
674
675     $('#GBpageview').empty()
676     this.displayedIndices = [];
677     this.loadLeafs();
678     
679     this.updateToolbarZoom(this.reduce);
680 }
681
682 // resizePageView()
683 //______________________________________________________________________________
684 GnuBook.prototype.resizePageView = function() {
685     var i;
686     var viewHeight = 0;
687     //var viewWidth  = $('#GBcontainer').width(); //includes scrollBar
688     var viewWidth  = $('#GBcontainer').attr('clientWidth');   
689
690     var oldScrollTop  = $('#GBcontainer').attr('scrollTop');
691     var oldScrollLeft = $('#GBcontainer').attr('scrollLeft');
692     var oldPageViewHeight = $('#GBpageview').height();
693     var oldPageViewWidth = $('#GBpageview').width();
694     
695     var oldCenterY = this.centerY1up();
696     var oldCenterX = this.centerX1up();
697     
698     if (0 != oldPageViewHeight) {
699         var scrollRatio = oldCenterY / oldPageViewHeight;
700     } else {
701         var scrollRatio = 0;
702     }
703     
704     for (i=0; i<this.numLeafs; i++) {
705         viewHeight += parseInt(this.getPageHeight(i)/this.reduce) + this.padding; 
706         var width = parseInt(this.getPageWidth(i)/this.reduce);
707         if (width>viewWidth) viewWidth=width;
708     }
709     $('#GBpageview').height(viewHeight);
710     $('#GBpageview').width(viewWidth);    
711
712     var newCenterY = scrollRatio*viewHeight;
713     var newTop = Math.max(0, Math.floor( newCenterY - $('#GBcontainer').height()/2 ));
714     $('#GBcontainer').attr('scrollTop', newTop);
715     
716     // We use clientWidth here to avoid miscalculating due to scroll bar
717     var newCenterX = oldCenterX * (viewWidth / oldPageViewWidth);
718     var newLeft = newCenterX - $('#GBcontainer').attr('clientWidth') / 2;
719     newLeft = Math.max(newLeft, 0);
720     $('#GBcontainer').attr('scrollLeft', newLeft);
721     //console.log('oldCenterX ' + oldCenterX + ' newCenterX ' + newCenterX + ' newLeft ' + newLeft);
722     
723     //this.centerPageView();
724     this.loadLeafs();
725     
726 }
727
728 // centerX1up()
729 //______________________________________________________________________________
730 // Returns the current offset of the viewport center in scaled document coordinates.
731 GnuBook.prototype.centerX1up = function() {
732     var centerX;
733     if ($('#GBpageview').width() < $('#GBcontainer').attr('clientWidth')) { // fully shown
734         centerX = $('#GBpageview').width();
735     } else {
736         centerX = $('#GBcontainer').attr('scrollLeft') + $('#GBcontainer').attr('clientWidth') / 2;
737     }
738     centerX = Math.floor(centerX);
739     return centerX;
740 }
741
742 // centerY1up()
743 //______________________________________________________________________________
744 // Returns the current offset of the viewport center in scaled document coordinates.
745 GnuBook.prototype.centerY1up = function() {
746     var centerY = $('#GBcontainer').attr('scrollTop') + $('#GBcontainer').height() / 2;
747     return Math.floor(centerY);
748 }
749
750 // centerPageView()
751 //______________________________________________________________________________
752 GnuBook.prototype.centerPageView = function() {
753
754     var scrollWidth  = $('#GBcontainer').attr('scrollWidth');
755     var clientWidth  =  $('#GBcontainer').attr('clientWidth');
756     //console.log('sW='+scrollWidth+' cW='+clientWidth);
757     if (scrollWidth > clientWidth) {
758         $('#GBcontainer').attr('scrollLeft', (scrollWidth-clientWidth)/2);
759     }
760
761 }
762
763 // zoom2up(direction)
764 //______________________________________________________________________________
765 GnuBook.prototype.zoom2up = function(direction) {
766
767     // Hard stop autoplay
768     this.stopFlipAnimations();
769     
770     // Get new zoom state    
771     var newZoom = this.twoPageNextReduce(this.reduce, direction);
772     if ((this.reduce == newZoom.reduce) && (this.twoPage.autofit == newZoom.autofit)) {
773         return;
774     }
775     this.twoPage.autofit = newZoom.autofit;
776     this.reduce = newZoom.reduce;
777
778     // Preserve view center position
779     var oldCenter = this.twoPageGetViewCenter();
780     
781     // If zooming in, reload imgs.  DOM elements will be removed by prepareTwoPageView
782     // $$$ An improvement would be to use the low res image until the larger one is loaded.
783     if (1 == direction) {
784         for (var img in this.prefetchedImgs) {
785             delete this.prefetchedImgs[img];
786         }
787     }
788     
789     // Prepare view with new center to minimize visual glitches
790     this.prepareTwoPageView(oldCenter.percentageX, oldCenter.percentageY);
791 }
792
793
794 // quantizeReduce(reduce)
795 //______________________________________________________________________________
796 // Quantizes the given reduction factor to closest power of two from set from 12.5% to 200%
797 GnuBook.prototype.quantizeReduce = function(reduce) {
798     var quantized = this.reductionFactors[0];
799     var distance = Math.abs(reduce - quantized);
800     for (var i = 1; i < this.reductionFactors.length; i++) {
801         newDistance = Math.abs(reduce - this.reductionFactors[i]);
802         if (newDistance < distance) {
803             distance = newDistance;
804             quantized = this.reductionFactors[i];
805         }
806     }
807     
808     return quantized;
809 }
810
811 // twoPageNextReduce()
812 //______________________________________________________________________________
813 // Returns the next reduction level
814 GnuBook.prototype.twoPageNextReduce = function(reduce, direction) {
815     var result = {};
816     var autofitReduce = this.twoPageGetAutofitReduce();
817
818     if (0 == direction) { // autofit
819         result.autofit = true;
820         result.reduce = autofitReduce;
821         
822     } else if (1 == direction) { // zoom in
823         var newReduce = this.reductionFactors[0];
824     
825         for (var i = 1; i < this.reductionFactors.length; i++) {
826             if (this.reductionFactors[i] < reduce) {
827                 newReduce = this.reductionFactors[i];
828             }
829         }
830         
831         if (!this.twoPage.autofit && (autofitReduce < reduce && autofitReduce > newReduce)) {
832             // use autofit
833             result.autofit = true;
834             result.reduce = autofitReduce;
835         } else {        
836             result.autofit = false;
837             result.reduce = newReduce;
838         }
839         
840     } else { // zoom out
841         var lastIndex = this.reductionFactors.length - 1;
842         var newReduce = this.reductionFactors[lastIndex];
843         
844         for (var i = lastIndex; i >= 0; i--) {
845             if (this.reductionFactors[i] > reduce) {
846                 newReduce = this.reductionFactors[i];
847             }
848         }
849          
850         if (!this.twoPage.autofit && (autofitReduce > reduce && autofitReduce < newReduce)) {
851             // use autofit
852             result.autofit = true;
853             result.reduce = autofitReduce;
854         } else {
855             result.autofit = false;
856             result.reduce = newReduce;
857         }
858     }
859     
860     return result;
861 }
862
863 // jumpToPage()
864 //______________________________________________________________________________
865 // Attempts to jump to page.  Returns true if page could be found, false otherwise.
866 GnuBook.prototype.jumpToPage = function(pageNum) {
867
868     var pageIndex = this.getPageIndex(pageNum);
869
870     if ('undefined' != typeof(pageIndex)) {
871         var leafTop = 0;
872         var h;
873         this.jumpToIndex(pageIndex);
874         $('#GBcontainer').attr('scrollTop', leafTop);
875         return true;
876     }
877     
878     // Page not found
879     return false;
880 }
881
882 // jumpToIndex()
883 //______________________________________________________________________________
884 GnuBook.prototype.jumpToIndex = function(index) {
885
886     if (2 == this.mode) {
887         this.autoStop();
888         
889         // By checking against min/max we do nothing if requested index
890         // is current
891         if (index < Math.min(this.twoPage.currentIndexL, this.twoPage.currentIndexR)) {
892             this.flipBackToIndex(index);
893         } else if (index > Math.max(this.twoPage.currentIndexL, this.twoPage.currentIndexR)) {
894             this.flipFwdToIndex(index);
895         }
896
897     } else {        
898         var i;
899         var leafTop = 0;
900         var h;
901         for (i=0; i<index; i++) {
902             h = parseInt(this.getPageHeight(i)/this.reduce); 
903             leafTop += h + this.padding;
904         }
905         //$('#GBcontainer').attr('scrollTop', leafTop);
906         $('#GBcontainer').animate({scrollTop: leafTop },'fast');
907     }
908 }
909
910
911
912 // switchMode()
913 //______________________________________________________________________________
914 GnuBook.prototype.switchMode = function(mode) {
915
916     //console.log('  asked to switch to mode ' + mode + ' from ' + this.mode);
917     
918     if (mode == this.mode) return;
919     
920     if (!this.canSwitchToMode(mode)) {
921         return;
922     }
923
924     this.autoStop();
925     this.removeSearchHilites();
926
927     this.mode = mode;
928     
929     this.switchToolbarMode(mode);
930     
931     // $$$ TODO preserve center of view when switching between mode
932     //     See https://bugs.edge.launchpad.net/gnubook/+bug/416682
933     
934     if (1 == mode) {
935         this.reduce = this.quantizeReduce(this.reduce);
936         this.prepareOnePageView();
937     } else {
938         this.twoPage.autofit = false; // Take zoom level from other mode
939         this.reduce = this.quantizeReduce(this.reduce);
940         this.prepareTwoPageView();
941         this.twoPageCenterView(0.5, 0.5); // $$$ TODO preserve center
942     }
943
944 }
945
946 //prepareOnePageView()
947 //______________________________________________________________________________
948 GnuBook.prototype.prepareOnePageView = function() {
949
950     // var startLeaf = this.displayedIndices[0];
951     var startLeaf = this.currentIndex();
952     
953     $('#GBcontainer').empty();
954     $('#GBcontainer').css({
955         overflowY: 'scroll',
956         overflowX: 'auto'
957     });
958     
959     var gbPageView = $("#GBcontainer").append("<div id='GBpageview'></div>");
960     
961     this.resizePageView();
962     
963     this.jumpToIndex(startLeaf);
964     this.displayedIndices = [];
965     
966     this.drawLeafsOnePage();
967         
968     // Bind mouse handlers
969     // Disable mouse click to avoid selected/highlighted page images - bug 354239
970     gbPageView.bind('mousedown', function(e) {
971         // $$$ check here for right-click and don't disable.  Also use jQuery style
972         //     for stopping propagation. See https://bugs.edge.launchpad.net/gnubook/+bug/362626
973         return false;
974     })
975     // Special hack for IE7
976     gbPageView[0].onselectstart = function(e) { return false; };
977 }
978
979 // prepareTwoPageView()
980 //______________________________________________________________________________
981 // Some decisions about two page view:
982 //
983 // Both pages will be displayed at the same height, even if they were different physical/scanned
984 // sizes.  This simplifies the animation (from a design as well as technical standpoint).  We
985 // examine the page aspect ratios (in calculateSpreadSize) and use the page with the most "normal"
986 // aspect ratio to determine the height.
987 //
988 // The two page view div is resized to keep the middle of the book in the middle of the div
989 // even as the page sizes change.  To e.g. keep the middle of the book in the middle of the GBcontent
990 // div requires adjusting the offset of GBtwpageview and/or scrolling in GBcontent.
991 GnuBook.prototype.prepareTwoPageView = function(centerPercentageX, centerPercentageY) {
992     $('#GBcontainer').empty();
993     $('#GBcontainer').css('overflow', 'auto');
994         
995     // We want to display two facing pages.  We may be missing
996     // one side of the spread because it is the first/last leaf,
997     // foldouts, missing pages, etc
998
999     //var targetLeaf = this.displayedIndices[0];
1000     var targetLeaf = this.firstIndex;
1001
1002     if (targetLeaf < this.firstDisplayableIndex()) {
1003         targetLeaf = this.firstDisplayableIndex();
1004     }
1005     
1006     if (targetLeaf > this.lastDisplayableIndex()) {
1007         targetLeaf = this.lastDisplayableIndex();
1008     }
1009     
1010     //this.twoPage.currentIndexL = null;
1011     //this.twoPage.currentIndexR = null;
1012     //this.pruneUnusedImgs();
1013     
1014     var currentSpreadIndices = this.getSpreadIndices(targetLeaf);
1015     this.twoPage.currentIndexL = currentSpreadIndices[0];
1016     this.twoPage.currentIndexR = currentSpreadIndices[1];
1017     this.firstIndex = this.twoPage.currentIndexL;
1018     
1019     this.calculateSpreadSize(); //sets twoPage.width, twoPage.height and others
1020
1021     this.pruneUnusedImgs();
1022     this.prefetch(); // Preload images or reload if scaling has changed
1023
1024     //console.dir(this.twoPage);
1025     
1026     // Add the two page view
1027     // $$$ Can we get everything set up and then append?
1028     $('#GBcontainer').append('<div id="GBtwopageview"></div>');
1029
1030     // $$$ calculate first then set
1031     $('#GBtwopageview').css( {
1032         height: this.twoPage.totalHeight + 'px',
1033         width: this.twoPage.totalWidth + 'px',
1034         position: 'absolute'
1035         });
1036         
1037     // If there will not be scrollbars (e.g. when zooming out) we center the book
1038     // since otherwise the book will be stuck off-center
1039     if (this.twoPage.totalWidth < $('#GBcontainer').attr('clientWidth')) {
1040         centerPercentageX = 0.5;
1041     }
1042     if (this.twoPage.totalHeight < $('#GBcontainer').attr('clientHeight')) {
1043         centerPercentageY = 0.5;
1044     }
1045         
1046     this.twoPageCenterView(centerPercentageX, centerPercentageY);
1047     
1048     this.twoPage.coverDiv = document.createElement('div');
1049     $(this.twoPage.coverDiv).attr('id', 'GBbookcover').css({
1050         border: '1px solid rgb(68, 25, 17)',
1051         width:  this.twoPage.bookCoverDivWidth + 'px',
1052         height: this.twoPage.bookCoverDivHeight+'px',
1053         visibility: 'visible',
1054         position: 'absolute',
1055         backgroundColor: '#663929',
1056         left: this.twoPage.bookCoverDivLeft + 'px',
1057         top: this.twoPage.bookCoverDivTop+'px',
1058         MozBorderRadiusTopleft: '7px',
1059         MozBorderRadiusTopright: '7px',
1060         MozBorderRadiusBottomright: '7px',
1061         MozBorderRadiusBottomleft: '7px'
1062     }).appendTo('#GBtwopageview');
1063     
1064     this.leafEdgeR = document.createElement('div');
1065     this.leafEdgeR.className = 'leafEdgeR'; // $$$ the static CSS should be moved into the .css file
1066     $(this.leafEdgeR).css({
1067         borderStyle: 'solid solid solid none',
1068         borderColor: 'rgb(51, 51, 34)',
1069         borderWidth: '1px 1px 1px 0px',
1070         background: 'transparent url(' + this.imagesBaseURL + 'right_edges.png) repeat scroll 0% 0%',
1071         width: this.twoPage.leafEdgeWidthR + 'px',
1072         height: this.twoPage.height-1 + 'px',
1073         /*right: '10px',*/
1074         left: this.twoPage.gutter+this.twoPage.scaledWR+'px',
1075         top: this.twoPage.bookCoverDivTop+this.twoPage.coverInternalPadding+'px',
1076         position: 'absolute'
1077     }).appendTo('#GBtwopageview');
1078     
1079     this.leafEdgeL = document.createElement('div');
1080     this.leafEdgeL.className = 'leafEdgeL';
1081     $(this.leafEdgeL).css({ // $$$ static CSS should be moved to file
1082         borderStyle: 'solid none solid solid',
1083         borderColor: 'rgb(51, 51, 34)',
1084         borderWidth: '1px 0px 1px 1px',
1085         background: 'transparent url(' + this.imagesBaseURL + 'left_edges.png) repeat scroll 0% 0%',
1086         width: this.twoPage.leafEdgeWidthL + 'px',
1087         height: this.twoPage.height-1 + 'px',
1088         left: this.twoPage.bookCoverDivLeft+this.twoPage.coverInternalPadding+'px',
1089         top: this.twoPage.bookCoverDivTop+this.twoPage.coverInternalPadding+'px',    
1090         position: 'absolute'
1091     }).appendTo('#GBtwopageview');
1092
1093     div = document.createElement('div');
1094     $(div).attr('id', 'GBbookspine').css({
1095         border:          '1px solid rgb(68, 25, 17)',
1096         width:           this.twoPage.bookSpineDivWidth+'px',
1097         height:          this.twoPage.bookSpineDivHeight+'px',
1098         position:        'absolute',
1099         backgroundColor: 'rgb(68, 25, 17)',
1100         left:            this.twoPage.bookSpineDivLeft+'px',
1101         top:             this.twoPage.bookSpineDivTop+'px'
1102     }).appendTo('#GBtwopageview');
1103     
1104     var self = this; // for closure
1105     
1106     /* Flip areas no longer used
1107     this.twoPage.leftFlipArea = document.createElement('div');
1108     this.twoPage.leftFlipArea.className = 'GBfliparea';
1109     $(this.twoPage.leftFlipArea).attr('id', 'GBleftflip').css({
1110         border: '0',
1111         width:  this.twoPageFlipAreaWidth() + 'px',
1112         height: this.twoPageFlipAreaHeight() + 'px',
1113         position: 'absolute',
1114         left:   this.twoPageLeftFlipAreaLeft() + 'px',
1115         top:    this.twoPageFlipAreaTop() + 'px',
1116         cursor: 'w-resize',
1117         zIndex: 100
1118     }).bind('click', function(e) {
1119         self.left();
1120     }).bind('mousedown', function(e) {
1121         e.preventDefault();
1122     }).appendTo('#GBtwopageview');
1123     
1124     this.twoPage.rightFlipArea = document.createElement('div');
1125     this.twoPage.rightFlipArea.className = 'GBfliparea';
1126     $(this.twoPage.rightFlipArea).attr('id', 'GBrightflip').css({
1127         border: '0',
1128         width:  this.twoPageFlipAreaWidth() + 'px',
1129         height: this.twoPageFlipAreaHeight() + 'px',
1130         position: 'absolute',
1131         left:   this.twoPageRightFlipAreaLeft() + 'px',
1132         top:    this.twoPageFlipAreaTop() + 'px',
1133         cursor: 'e-resize',
1134         zIndex: 100
1135     }).bind('click', function(e) {
1136         self.right();
1137     }).bind('mousedown', function(e) {
1138         e.preventDefault();
1139     }).appendTo('#GBtwopageview');
1140     */
1141     
1142     this.prepareTwoPagePopUp();
1143     
1144     this.displayedIndices = [];
1145     
1146     //this.indicesToDisplay=[firstLeaf, firstLeaf+1];
1147     //console.log('indicesToDisplay: ' + this.indicesToDisplay[0] + ' ' + this.indicesToDisplay[1]);
1148     
1149     this.drawLeafsTwoPage();
1150     this.updateSearchHilites2UP();
1151     this.updateToolbarZoom(this.reduce);
1152     
1153     this.prefetch();
1154 }
1155
1156 // prepareTwoPagePopUp()
1157 //
1158 // This function prepares the "View Page n" popup that shows while the mouse is
1159 // over the left/right "stack of sheets" edges.  It also binds the mouse
1160 // events for these divs.
1161 //______________________________________________________________________________
1162 GnuBook.prototype.prepareTwoPagePopUp = function() {
1163
1164     this.twoPagePopUp = document.createElement('div');
1165     $(this.twoPagePopUp).css({
1166         border: '1px solid black',
1167         padding: '2px 6px',
1168         position: 'absolute',
1169         fontFamily: 'sans-serif',
1170         fontSize: '14px',
1171         zIndex: '1000',
1172         backgroundColor: 'rgb(255, 255, 238)',
1173         opacity: 0.85
1174     }).appendTo('#GBcontainer');
1175     $(this.twoPagePopUp).hide();
1176     
1177     $(this.leafEdgeL).add(this.leafEdgeR).bind('mouseenter', this, function(e) {
1178         $(e.data.twoPagePopUp).show();
1179     });
1180
1181     $(this.leafEdgeL).add(this.leafEdgeR).bind('mouseleave', this, function(e) {
1182         $(e.data.twoPagePopUp).hide();
1183     });
1184
1185     $(this.leafEdgeL).bind('click', this, function(e) { 
1186         e.data.autoStop();
1187         var jumpIndex = e.data.jumpIndexForLeftEdgePageX(e.pageX);
1188         e.data.jumpToIndex(jumpIndex);
1189     });
1190
1191     $(this.leafEdgeR).bind('click', this, function(e) { 
1192         e.data.autoStop();
1193         var jumpIndex = e.data.jumpIndexForRightEdgePageX(e.pageX);
1194         e.data.jumpToIndex(jumpIndex);    
1195     });
1196
1197     $(this.leafEdgeR).bind('mousemove', this, function(e) {
1198
1199         var jumpIndex = e.data.jumpIndexForRightEdgePageX(e.pageX);
1200         $(e.data.twoPagePopUp).text('View ' + e.data.getPageName(jumpIndex));
1201         
1202         // $$$ TODO: Make sure popup is positioned so that it is in view
1203         // (https://bugs.edge.launchpad.net/gnubook/+bug/327456)        
1204         $(e.data.twoPagePopUp).css({
1205             left: e.pageX- $('#GBcontainer').offset().left + $('#GBcontainer').scrollLeft() + 20 + 'px',
1206             top: e.pageY - $('#GBcontainer').offset().top + $('#GBcontainer').scrollTop() + 'px'
1207         });
1208     });
1209
1210     $(this.leafEdgeL).bind('mousemove', this, function(e) {
1211     
1212         var jumpIndex = e.data.jumpIndexForLeftEdgePageX(e.pageX);
1213         $(e.data.twoPagePopUp).text('View '+ e.data.getPageName(jumpIndex));
1214
1215         // $$$ TODO: Make sure popup is positioned so that it is in view
1216         //           (https://bugs.edge.launchpad.net/gnubook/+bug/327456)        
1217         $(e.data.twoPagePopUp).css({
1218             left: e.pageX - $('#GBcontainer').offset().left + $('#GBcontainer').scrollLeft() - $(e.data.twoPagePopUp).width() - 25 + 'px',
1219             top: e.pageY-$('#GBcontainer').offset().top + $('#GBcontainer').scrollTop() + 'px'
1220         });
1221     });
1222 }
1223
1224 // calculateSpreadSize()
1225 //______________________________________________________________________________
1226 // Calculates 2-page spread dimensions based on this.twoPage.currentIndexL and
1227 // this.twoPage.currentIndexR
1228 // This function sets this.twoPage.height, twoPage.width
1229
1230 GnuBook.prototype.calculateSpreadSize = function() {
1231
1232     var firstIndex  = this.twoPage.currentIndexL;
1233     var secondIndex = this.twoPage.currentIndexR;
1234     //console.log('first page is ' + firstIndex);
1235
1236     // Calculate page sizes and total leaf width
1237     var spreadSize;
1238     if ( this.twoPage.autofit) {    
1239         spreadSize = this.getIdealSpreadSize(firstIndex, secondIndex);
1240     } else {
1241         // set based on reduction factor
1242         spreadSize = this.getSpreadSizeFromReduce(firstIndex, secondIndex, this.reduce);
1243     }
1244     
1245     // Both pages together
1246     this.twoPage.height = spreadSize.height;
1247     this.twoPage.width = spreadSize.width;
1248     
1249     // Individual pages
1250     this.twoPage.scaledWL = this.getPageWidth2UP(firstIndex);
1251     this.twoPage.scaledWR = this.getPageWidth2UP(secondIndex);
1252     
1253     // Leaf edges
1254     this.twoPage.edgeWidth = spreadSize.totalLeafEdgeWidth; // The combined width of both edges
1255     this.twoPage.leafEdgeWidthL = this.leafEdgeWidth(this.twoPage.currentIndexL);
1256     this.twoPage.leafEdgeWidthR = this.twoPage.edgeWidth - this.twoPage.leafEdgeWidthL;
1257     
1258     
1259     // Book cover
1260     // The width of the book cover div.  The combined width of both pages, twice the width
1261     // of the book cover internal padding (2*10) and the page edges
1262     this.twoPage.bookCoverDivWidth = this.twoPage.scaledWL + this.twoPage.scaledWR + 2 * this.twoPage.coverInternalPadding + this.twoPage.edgeWidth;
1263     // The height of the book cover div
1264     this.twoPage.bookCoverDivHeight = this.twoPage.height + 2 * this.twoPage.coverInternalPadding;
1265     
1266     
1267     // We calculate the total width and height for the div so that we can make the book
1268     // spine centered
1269     var leftGutterOffset = this.gutterOffsetForIndex(firstIndex);
1270     var leftWidthFromCenter = this.twoPage.scaledWL - leftGutterOffset + this.twoPage.leafEdgeWidthL;
1271     var rightWidthFromCenter = this.twoPage.scaledWR + leftGutterOffset + this.twoPage.leafEdgeWidthR;
1272     var largestWidthFromCenter = Math.max( leftWidthFromCenter, rightWidthFromCenter );
1273     this.twoPage.totalWidth = 2 * (largestWidthFromCenter + this.twoPage.coverInternalPadding + this.twoPage.coverExternalPadding);
1274     this.twoPage.totalHeight = this.twoPage.height + 2 * (this.twoPage.coverInternalPadding + this.twoPage.coverExternalPadding);
1275         
1276     // We want to minimize the unused space in two-up mode (maximize the amount of page
1277     // shown).  We give width to the leaf edges and these widths change (though the sum
1278     // of the two remains constant) as we flip through the book.  With the book
1279     // cover centered and fixed in the GBcontainer div the page images will meet
1280     // at the "gutter" which is generally offset from the center.
1281     this.twoPage.middle = this.twoPage.totalWidth >> 1;
1282     this.twoPage.gutter = this.twoPage.middle + this.gutterOffsetForIndex(firstIndex);
1283     
1284     // The left edge of the book cover moves depending on the width of the pages
1285     // $$$ change to getter
1286     this.twoPage.bookCoverDivLeft = this.twoPage.gutter - this.twoPage.scaledWL - this.twoPage.leafEdgeWidthL - this.twoPage.coverInternalPadding;
1287     // The top edge of the book cover stays a fixed distance from the top
1288     this.twoPage.bookCoverDivTop = this.twoPage.coverExternalPadding;
1289
1290     // Book spine
1291     this.twoPage.bookSpineDivHeight = this.twoPage.height + 2*this.twoPage.coverInternalPadding;
1292     this.twoPage.bookSpineDivLeft = this.twoPage.middle - (this.twoPage.bookSpineDivWidth >> 1);
1293     this.twoPage.bookSpineDivTop = this.twoPage.bookCoverDivTop;
1294
1295
1296     this.reduce = spreadSize.reduce; // $$$ really set this here?
1297 }
1298
1299 GnuBook.prototype.getIdealSpreadSize = function(firstIndex, secondIndex) {
1300     var ideal = {};
1301
1302     // We check which page is closest to a "normal" page and use that to set the height
1303     // for both pages.  This means that foldouts and other odd size pages will be displayed
1304     // smaller than the nominal zoom amount.
1305     var canon5Dratio = 1.5;
1306     
1307     var first = {
1308         height: this.getPageHeight(firstIndex),
1309         width: this.getPageWidth(firstIndex)
1310     }
1311     
1312     var second = {
1313         height: this.getPageHeight(secondIndex),
1314         width: this.getPageWidth(secondIndex)
1315     }
1316     
1317     var firstIndexRatio  = first.height / first.width;
1318     var secondIndexRatio = second.height / second.width;
1319     //console.log('firstIndexRatio = ' + firstIndexRatio + ' secondIndexRatio = ' + secondIndexRatio);
1320
1321     var ratio;
1322     if (Math.abs(firstIndexRatio - canon5Dratio) < Math.abs(secondIndexRatio - canon5Dratio)) {
1323         ratio = firstIndexRatio;
1324         //console.log('using firstIndexRatio ' + ratio);
1325     } else {
1326         ratio = secondIndexRatio;
1327         //console.log('using secondIndexRatio ' + ratio);
1328     }
1329
1330     var totalLeafEdgeWidth = parseInt(this.numLeafs * 0.1);
1331     var maxLeafEdgeWidth   = parseInt($('#GBcontainer').attr('clientWidth') * 0.1);
1332     ideal.totalLeafEdgeWidth     = Math.min(totalLeafEdgeWidth, maxLeafEdgeWidth);
1333     
1334     var widthOutsidePages = 2 * (this.twoPage.coverInternalPadding + this.twoPage.coverExternalPadding) + ideal.totalLeafEdgeWidth;
1335     var heightOutsidePages = 2* (this.twoPage.coverInternalPadding + this.twoPage.coverExternalPadding);
1336     
1337     ideal.width = ($('#GBcontainer').width() - widthOutsidePages) >> 1;
1338     ideal.width -= 10; // $$$ fudge factor
1339     ideal.height = $('#GBcontainer').height() - heightOutsidePages;
1340     ideal.height -= 20; // fudge factor
1341     //console.log('init idealWidth='+ideal.width+' idealHeight='+ideal.height + ' ratio='+ratio);
1342
1343     if (ideal.height/ratio <= ideal.width) {
1344         //use height
1345         ideal.width = parseInt(ideal.height/ratio);
1346     } else {
1347         //use width
1348         ideal.height = parseInt(ideal.width*ratio);
1349     }
1350     
1351     // $$$ check this logic with large spreads
1352     ideal.reduce = ((first.height + second.height) / 2) / ideal.height;
1353     
1354     return ideal;
1355 }
1356
1357 // getSpreadSizeFromReduce()
1358 //______________________________________________________________________________
1359 // Returns the spread size calculated from the reduction factor for the given pages
1360 GnuBook.prototype.getSpreadSizeFromReduce = function(firstIndex, secondIndex, reduce) {
1361     var spreadSize = {};
1362     // $$$ Scale this based on reduce?
1363     var totalLeafEdgeWidth = parseInt(this.numLeafs * 0.1);
1364     var maxLeafEdgeWidth   = parseInt($('#GBcontainer').attr('clientWidth') * 0.1); // $$$ Assumes leaf edge width constant at all zoom levels
1365     spreadSize.totalLeafEdgeWidth     = Math.min(totalLeafEdgeWidth, maxLeafEdgeWidth);
1366
1367     // $$$ Possibly incorrect -- we should make height "dominant"
1368     var nativeWidth = this.getPageWidth(firstIndex) + this.getPageWidth(secondIndex);
1369     var nativeHeight = this.getPageHeight(firstIndex) + this.getPageHeight(secondIndex);
1370     spreadSize.height = parseInt( (nativeHeight / 2) / this.reduce );
1371     spreadSize.width = parseInt( (nativeWidth / 2) / this.reduce );
1372     spreadSize.reduce = reduce;
1373     
1374     return spreadSize;
1375 }
1376
1377 // twoPageGetAutofitReduce()
1378 //______________________________________________________________________________
1379 // Returns the current ideal reduction factor
1380 GnuBook.prototype.twoPageGetAutofitReduce = function() {
1381     var spreadSize = this.getIdealSpreadSize(this.twoPage.currentIndexL, this.twoPage.currentIndexR);
1382     return spreadSize.reduce;
1383 }
1384
1385 // twoPageSetCursor()
1386 //______________________________________________________________________________
1387 // Set the cursor for two page view
1388 GnuBook.prototype.twoPageSetCursor = function() {
1389     // console.log('setting cursor');
1390     if ( ($('#GBtwopageview').width() > $('#GBcontainer').attr('clientWidth')) ||
1391          ($('#GBtwopageview').height() > $('#GBcontainer').attr('clientHeight')) ) {
1392         $(this.prefetchedImgs[this.twoPage.currentIndexL]).css('cursor','move');
1393         $(this.prefetchedImgs[this.twoPage.currentIndexR]).css('cursor','move');
1394     } else {
1395         $(this.prefetchedImgs[this.twoPage.currentIndexL]).css('cursor','');
1396         $(this.prefetchedImgs[this.twoPage.currentIndexR]).css('cursor','');
1397     }
1398 }
1399
1400 // currentIndex()
1401 //______________________________________________________________________________
1402 // Returns the currently active index.
1403 GnuBook.prototype.currentIndex = function() {
1404     // $$$ we should be cleaner with our idea of which index is active in 1up/2up
1405     if (this.mode == this.constMode1up || this.mode == this.constMode2up) {
1406         return this.firstIndex;
1407     } else {
1408         throw 'currentIndex called for unimplemented mode ' + this.mode;
1409     }
1410 }
1411
1412 // right()
1413 //______________________________________________________________________________
1414 // Flip the right page over onto the left
1415 GnuBook.prototype.right = function() {
1416     if ('rl' != this.pageProgression) {
1417         // LTR
1418         gb.next();
1419     } else {
1420         // RTL
1421         gb.prev();
1422     }
1423 }
1424
1425 // rightmost()
1426 //______________________________________________________________________________
1427 // Flip to the rightmost page
1428 GnuBook.prototype.rightmost = function() {
1429     if ('rl' != this.pageProgression) {
1430         gb.last();
1431     } else {
1432         gb.first();
1433     }
1434 }
1435
1436 // left()
1437 //______________________________________________________________________________
1438 // Flip the left page over onto the right.
1439 GnuBook.prototype.left = function() {
1440     if ('rl' != this.pageProgression) {
1441         // LTR
1442         gb.prev();
1443     } else {
1444         // RTL
1445         gb.next();
1446     }
1447 }
1448
1449 // leftmost()
1450 //______________________________________________________________________________
1451 // Flip to the leftmost page
1452 GnuBook.prototype.leftmost = function() {
1453     if ('rl' != this.pageProgression) {
1454         gb.first();
1455     } else {
1456         gb.last();
1457     }
1458 }
1459
1460 // next()
1461 //______________________________________________________________________________
1462 GnuBook.prototype.next = function() {
1463     if (2 == this.mode) {
1464         this.autoStop();
1465         this.flipFwdToIndex(null);
1466     } else {
1467         if (this.firstIndex < this.lastDisplayableIndex()) {
1468             this.jumpToIndex(this.firstIndex+1);
1469         }
1470     }
1471 }
1472
1473 // prev()
1474 //______________________________________________________________________________
1475 GnuBook.prototype.prev = function() {
1476     if (2 == this.mode) {
1477         this.autoStop();
1478         this.flipBackToIndex(null);
1479     } else {
1480         if (this.firstIndex >= 1) {
1481             this.jumpToIndex(this.firstIndex-1);
1482         }    
1483     }
1484 }
1485
1486 GnuBook.prototype.first = function() {
1487     if (2 == this.mode) {
1488         this.jumpToIndex(2);
1489     }
1490     else {
1491         this.jumpToIndex(0);
1492     }
1493 }
1494
1495 GnuBook.prototype.last = function() {
1496     if (2 == this.mode) {
1497         this.jumpToIndex(this.lastDisplayableIndex());
1498     }
1499     else {
1500         this.jumpToIndex(this.lastDisplayableIndex());
1501     }
1502 }
1503
1504 // flipBackToIndex()
1505 //______________________________________________________________________________
1506 // to flip back one spread, pass index=null
1507 GnuBook.prototype.flipBackToIndex = function(index) {
1508     if (1 == this.mode) return;
1509
1510     var leftIndex = this.twoPage.currentIndexL;
1511     
1512     // $$$ Need to change this to be able to see first spread.
1513     //     See https://bugs.launchpad.net/gnubook/+bug/296788
1514     if (leftIndex <= 2) return;
1515     if (this.animating) return;
1516
1517     if (null != this.leafEdgeTmp) {
1518         alert('error: leafEdgeTmp should be null!');
1519         return;
1520     }
1521     
1522     if (null == index) {
1523         index = leftIndex-2;
1524     }
1525     //if (index<0) return;
1526     
1527     var previousIndices = this.getSpreadIndices(index);
1528     
1529     if (previousIndices[0] < 0 || previousIndices[1] < 0) {
1530         return;
1531     }
1532     
1533     //console.log("flipping back to " + previousIndices[0] + ',' + previousIndices[1]);
1534
1535     this.animating = true;
1536     
1537     if ('rl' != this.pageProgression) {
1538         // Assume LTR and we are going backward    
1539         this.prepareFlipLeftToRight(previousIndices[0], previousIndices[1]);        
1540         this.flipLeftToRight(previousIndices[0], previousIndices[1]);
1541     } else {
1542         // RTL and going backward
1543         var gutter = this.prepareFlipRightToLeft(previousIndices[0], previousIndices[1]);
1544         this.flipRightToLeft(previousIndices[0], previousIndices[1], gutter);
1545     }
1546 }
1547
1548 // flipLeftToRight()
1549 //______________________________________________________________________________
1550 // Flips the page on the left towards the page on the right
1551 GnuBook.prototype.flipLeftToRight = function(newIndexL, newIndexR) {
1552
1553     var leftLeaf = this.twoPage.currentIndexL;
1554     
1555     var oldLeafEdgeWidthL = this.leafEdgeWidth(this.twoPage.currentIndexL);
1556     var newLeafEdgeWidthL = this.leafEdgeWidth(newIndexL);    
1557     var leafEdgeTmpW = oldLeafEdgeWidthL - newLeafEdgeWidthL;
1558     
1559     var currWidthL   = this.getPageWidth2UP(leftLeaf);
1560     var newWidthL    = this.getPageWidth2UP(newIndexL);
1561     var newWidthR    = this.getPageWidth2UP(newIndexR);
1562
1563     var top  = this.twoPageTop();
1564     var gutter = this.twoPage.middle + this.gutterOffsetForIndex(newIndexL);
1565     
1566     //console.log('leftEdgeTmpW ' + leafEdgeTmpW);
1567     //console.log('  gutter ' + gutter + ', scaledWL ' + scaledWL + ', newLeafEdgeWL ' + newLeafEdgeWidthL);
1568     
1569     //animation strategy:
1570     // 0. remove search highlight, if any.
1571     // 1. create a new div, called leafEdgeTmp to represent the leaf edge between the leftmost edge 
1572     //    of the left leaf and where the user clicked in the leaf edge.
1573     //    Note that if this function was triggered by left() and not a
1574     //    mouse click, the width of leafEdgeTmp is very small (zero px).
1575     // 2. animate both leafEdgeTmp to the gutter (without changing its width) and animate
1576     //    leftLeaf to width=0.
1577     // 3. When step 2 is finished, animate leafEdgeTmp to right-hand side of new right leaf
1578     //    (left=gutter+newWidthR) while also animating the new right leaf from width=0 to
1579     //    its new full width.
1580     // 4. After step 3 is finished, do the following:
1581     //      - remove leafEdgeTmp from the dom.
1582     //      - resize and move the right leaf edge (leafEdgeR) to left=gutter+newWidthR
1583     //          and width=twoPage.edgeWidth-newLeafEdgeWidthL.
1584     //      - resize and move the left leaf edge (leafEdgeL) to left=gutter-newWidthL-newLeafEdgeWidthL
1585     //          and width=newLeafEdgeWidthL.
1586     //      - resize the back cover (twoPage.coverDiv) to left=gutter-newWidthL-newLeafEdgeWidthL-10
1587     //          and width=newWidthL+newWidthR+twoPage.edgeWidth+20
1588     //      - move new left leaf (newIndexL) forward to zindex=2 so it can receive clicks.
1589     //      - remove old left and right leafs from the dom [pruneUnusedImgs()].
1590     //      - prefetch new adjacent leafs.
1591     //      - set up click handlers for both new left and right leafs.
1592     //      - redraw the search highlight.
1593     //      - update the pagenum box and the url.
1594     
1595     
1596     var leftEdgeTmpLeft = gutter - currWidthL - leafEdgeTmpW;
1597
1598     this.leafEdgeTmp = document.createElement('div');
1599     $(this.leafEdgeTmp).css({
1600         borderStyle: 'solid none solid solid',
1601         borderColor: 'rgb(51, 51, 34)',
1602         borderWidth: '1px 0px 1px 1px',
1603         background: 'transparent url(' + this.imagesBaseURL + 'left_edges.png) repeat scroll 0% 0%',
1604         width: leafEdgeTmpW + 'px',
1605         height: this.twoPage.height-1 + 'px',
1606         left: leftEdgeTmpLeft + 'px',
1607         top: top+'px',    
1608         position: 'absolute',
1609         zIndex:1000
1610     }).appendTo('#GBtwopageview');
1611     
1612     //$(this.leafEdgeL).css('width', newLeafEdgeWidthL+'px');
1613     $(this.leafEdgeL).css({
1614         width: newLeafEdgeWidthL+'px', 
1615         left: gutter-currWidthL-newLeafEdgeWidthL+'px'
1616     });   
1617
1618     // Left gets the offset of the current left leaf from the document
1619     var left = $(this.prefetchedImgs[leftLeaf]).offset().left;
1620     // $$$ This seems very similar to the gutter.  May be able to consolidate the logic.
1621     var right = $('#GBtwopageview').attr('clientWidth')-left-$(this.prefetchedImgs[leftLeaf]).width()+$('#GBtwopageview').offset().left-2+'px';
1622     
1623     // We change the left leaf to right positioning
1624     // $$$ This causes animation glitches during resize.  See https://bugs.edge.launchpad.net/gnubook/+bug/328327
1625     $(this.prefetchedImgs[leftLeaf]).css({
1626         right: right,
1627         left: ''
1628     });
1629
1630     $(this.leafEdgeTmp).animate({left: gutter}, this.flipSpeed, 'easeInSine');    
1631     //$(this.prefetchedImgs[leftLeaf]).animate({width: '0px'}, 'slow', 'easeInSine');
1632     
1633     var self = this;
1634
1635     this.removeSearchHilites();
1636
1637     //console.log('animating leafLeaf ' + leftLeaf + ' to 0px');
1638     $(this.prefetchedImgs[leftLeaf]).animate({width: '0px'}, self.flipSpeed, 'easeInSine', function() {
1639     
1640         //console.log('     and now leafEdgeTmp to left: gutter+newWidthR ' + (gutter + newWidthR));
1641         $(self.leafEdgeTmp).animate({left: gutter+newWidthR+'px'}, self.flipSpeed, 'easeOutSine');
1642
1643         //console.log('  animating newIndexR ' + newIndexR + ' to ' + newWidthR + ' from ' + $(self.prefetchedImgs[newIndexR]).width());
1644         $(self.prefetchedImgs[newIndexR]).animate({width: newWidthR+'px'}, self.flipSpeed, 'easeOutSine', function() {
1645             $(self.prefetchedImgs[newIndexL]).css('zIndex', 2);
1646             
1647             $(self.leafEdgeR).css({
1648                 // Moves the right leaf edge
1649                 width: self.twoPage.edgeWidth-newLeafEdgeWidthL+'px',
1650                 left:  gutter+newWidthR+'px'
1651             });
1652
1653             $(self.leafEdgeL).css({
1654                 // Moves and resizes the left leaf edge
1655                 width: newLeafEdgeWidthL+'px',
1656                 left:  gutter-newWidthL-newLeafEdgeWidthL+'px'
1657             });
1658
1659             // Resizes the brown border div
1660             $(self.twoPage.coverDiv).css({
1661                 width: self.twoPageCoverWidth(newWidthL+newWidthR)+'px',
1662                 left: gutter-newWidthL-newLeafEdgeWidthL-self.twoPage.coverInternalPadding+'px'
1663             });
1664             
1665             $(self.leafEdgeTmp).remove();
1666             self.leafEdgeTmp = null;
1667
1668             // $$$ TODO refactor with opposite direction flip
1669             
1670             self.twoPage.currentIndexL = newIndexL;
1671             self.twoPage.currentIndexR = newIndexR;
1672             self.twoPage.scaledWL = newWidthL;
1673             self.twoPage.scaledWR = newWidthR;
1674             self.twoPage.gutter = gutter;
1675             
1676             self.firstIndex = self.twoPage.currentIndexL;
1677             self.displayedIndices = [newIndexL, newIndexR];
1678             self.pruneUnusedImgs();
1679             self.prefetch();            
1680             self.animating = false;
1681             
1682             self.updateSearchHilites2UP();
1683             self.updatePageNumBox2UP();
1684             
1685             // self.twoPagePlaceFlipAreas(); // No longer used
1686             self.setMouseHandlers2UP();
1687             self.twoPageSetCursor();
1688             
1689             if (self.animationFinishedCallback) {
1690                 self.animationFinishedCallback();
1691                 self.animationFinishedCallback = null;
1692             }
1693         });
1694     });        
1695     
1696 }
1697
1698 // flipFwdToIndex()
1699 //______________________________________________________________________________
1700 // Whether we flip left or right is dependent on the page progression
1701 // to flip forward one spread, pass index=null
1702 GnuBook.prototype.flipFwdToIndex = function(index) {
1703
1704     if (this.animating) return;
1705
1706     if (null != this.leafEdgeTmp) {
1707         alert('error: leafEdgeTmp should be null!');
1708         return;
1709     }
1710
1711     if (null == index) {
1712         index = this.twoPage.currentIndexR+2; // $$$ assumes indices are continuous
1713     }
1714     if (index > this.lastDisplayableIndex()) return;
1715
1716     this.animating = true;
1717     
1718     var nextIndices = this.getSpreadIndices(index);
1719     
1720     //console.log('flipfwd to indices ' + nextIndices[0] + ',' + nextIndices[1]);
1721
1722     if ('rl' != this.pageProgression) {
1723         // We did not specify RTL
1724         var gutter = this.prepareFlipRightToLeft(nextIndices[0], nextIndices[1]);
1725         this.flipRightToLeft(nextIndices[0], nextIndices[1], gutter);
1726     } else {
1727         // RTL
1728         var gutter = this.prepareFlipLeftToRight(nextIndices[0], nextIndices[1]);
1729         this.flipLeftToRight(nextIndices[0], nextIndices[1]);
1730     }
1731 }
1732
1733 // flipRightToLeft(nextL, nextR, gutter)
1734 // $$$ better not to have to pass gutter in
1735 //______________________________________________________________________________
1736 // Flip from left to right and show the nextL and nextR indices on those sides
1737 GnuBook.prototype.flipRightToLeft = function(newIndexL, newIndexR) {
1738     var oldLeafEdgeWidthL = this.leafEdgeWidth(this.twoPage.currentIndexL);
1739     var oldLeafEdgeWidthR = this.twoPage.edgeWidth-oldLeafEdgeWidthL;
1740     var newLeafEdgeWidthL = this.leafEdgeWidth(newIndexL);  
1741     var newLeafEdgeWidthR = this.twoPage.edgeWidth-newLeafEdgeWidthL;
1742
1743     var leafEdgeTmpW = oldLeafEdgeWidthR - newLeafEdgeWidthR;
1744
1745     var top = this.twoPageTop();
1746     var scaledW = this.getPageWidth2UP(this.twoPage.currentIndexR);
1747
1748     var middle = this.twoPage.middle;
1749     var gutter = middle + this.gutterOffsetForIndex(newIndexL);
1750     
1751     this.leafEdgeTmp = document.createElement('div');
1752     $(this.leafEdgeTmp).css({
1753         borderStyle: 'solid none solid solid',
1754         borderColor: 'rgb(51, 51, 34)',
1755         borderWidth: '1px 0px 1px 1px',
1756         background: 'transparent url(' + this.imagesBaseURL + 'left_edges.png) repeat scroll 0% 0%',
1757         width: leafEdgeTmpW + 'px',
1758         height: this.twoPage.height-1 + 'px',
1759         left: gutter+scaledW+'px',
1760         top: top+'px',    
1761         position: 'absolute',
1762         zIndex:1000
1763     }).appendTo('#GBtwopageview');
1764
1765     //var scaledWR = this.getPageWidth2UP(newIndexR); // $$$ should be current instead?
1766     //var scaledWL = this.getPageWidth2UP(newIndexL); // $$$ should be current instead?
1767     
1768     var currWidthL = this.getPageWidth2UP(this.twoPage.currentIndexL);
1769     var currWidthR = this.getPageWidth2UP(this.twoPage.currentIndexR);
1770     var newWidthL = this.getPageWidth2UP(newIndexL);
1771     var newWidthR = this.getPageWidth2UP(newIndexR);
1772     
1773     $(this.leafEdgeR).css({width: newLeafEdgeWidthR+'px', left: gutter+newWidthR+'px' });
1774
1775     var self = this; // closure-tastic!
1776
1777     var speed = this.flipSpeed;
1778
1779     this.removeSearchHilites();
1780     
1781     $(this.leafEdgeTmp).animate({left: gutter}, speed, 'easeInSine');    
1782     $(this.prefetchedImgs[this.twoPage.currentIndexR]).animate({width: '0px'}, speed, 'easeInSine', function() {
1783         $(self.leafEdgeTmp).animate({left: gutter-newWidthL-leafEdgeTmpW+'px'}, speed, 'easeOutSine');    
1784         $(self.prefetchedImgs[newIndexL]).animate({width: newWidthL+'px'}, speed, 'easeOutSine', function() {
1785             $(self.prefetchedImgs[newIndexR]).css('zIndex', 2);
1786             
1787             $(self.leafEdgeL).css({
1788                 width: newLeafEdgeWidthL+'px', 
1789                 left: gutter-newWidthL-newLeafEdgeWidthL+'px'
1790             });
1791             
1792             // Resizes the book cover
1793             $(self.twoPage.coverDiv).css({
1794                 width: self.twoPageCoverWidth(newWidthL+newWidthR)+'px',
1795                 left: gutter - newWidthL - newLeafEdgeWidthL - self.twoPage.coverInternalPadding + 'px'
1796             });
1797             
1798             $(self.leafEdgeTmp).remove();
1799             self.leafEdgeTmp = null;
1800             
1801             self.twoPage.currentIndexL = newIndexL;
1802             self.twoPage.currentIndexR = newIndexR;
1803             self.twoPage.scaledWL = newWidthL;
1804             self.twoPage.scaledWR = newWidthR;
1805             self.twoPage.gutter = gutter;
1806
1807             self.firstIndex = self.twoPage.currentIndexL;
1808             self.displayedIndices = [newIndexL, newIndexR];
1809             self.pruneUnusedImgs();
1810             self.prefetch();
1811             self.animating = false;
1812
1813
1814             self.updateSearchHilites2UP();
1815             self.updatePageNumBox2UP();
1816             
1817             // self.twoPagePlaceFlipAreas(); // No longer used
1818             self.setMouseHandlers2UP();     
1819             self.twoPageSetCursor();
1820             
1821             if (self.animationFinishedCallback) {
1822                 self.animationFinishedCallback();
1823                 self.animationFinishedCallback = null;
1824             }
1825         });
1826     });    
1827 }
1828
1829 // setMouseHandlers2UP
1830 //______________________________________________________________________________
1831 GnuBook.prototype.setMouseHandlers2UP = function() {
1832     /*
1833     $(this.prefetchedImgs[this.twoPage.currentIndexL]).bind('dblclick', function() {
1834         //self.prevPage();
1835         self.autoStop();
1836         self.left();
1837     });
1838     $(this.prefetchedImgs[this.twoPage.currentIndexR]).bind('dblclick', function() {
1839         //self.nextPage();'
1840         self.autoStop();
1841         self.right();        
1842     });
1843     */
1844     
1845     this.setDragHandler2UP( this.prefetchedImgs[this.twoPage.currentIndexL] );
1846     this.setClickHandler2UP( this.prefetchedImgs[this.twoPage.currentIndexL],
1847         { self: this },
1848         function(e) {
1849             e.data.self.left();
1850         }
1851     );
1852         
1853     this.setDragHandler2UP( this.prefetchedImgs[this.twoPage.currentIndexR] );
1854     this.setClickHandler2UP( this.prefetchedImgs[this.twoPage.currentIndexR],
1855         { self: this },
1856         function(e) {
1857             e.data.self.right();
1858         }
1859     );
1860 }
1861
1862 // prefetchImg()
1863 //______________________________________________________________________________
1864 GnuBook.prototype.prefetchImg = function(index) {
1865     var pageURI = this.getPageURI(index);
1866
1867     // Load image if not loaded or URI has changed (e.g. due to scaling)
1868     var loadImage = false;
1869     if (undefined == this.prefetchedImgs[index]) {
1870         //console.log('no image for ' + index);
1871         loadImage = true;
1872     } else if (pageURI != this.prefetchedImgs[index].uri) {
1873         //console.log('uri changed for ' + index);
1874         loadImage = true;
1875     }
1876     
1877     if (loadImage) {
1878         //console.log('prefetching ' + index);
1879         var img = document.createElement("img");
1880         img.src = pageURI;
1881         img.uri = pageURI; // browser may rewrite src so we stash raw URI here
1882         this.prefetchedImgs[index] = img;
1883     }
1884 }
1885
1886
1887 // prepareFlipLeftToRight()
1888 //
1889 //______________________________________________________________________________
1890 //
1891 // Prepare to flip the left page towards the right.  This corresponds to moving
1892 // backward when the page progression is left to right.
1893 GnuBook.prototype.prepareFlipLeftToRight = function(prevL, prevR) {
1894
1895     //console.log('  preparing left->right for ' + prevL + ',' + prevR);
1896
1897     this.prefetchImg(prevL);
1898     this.prefetchImg(prevR);
1899     
1900     var height  = this.getPageHeight(prevL); 
1901     var width   = this.getPageWidth(prevL);    
1902     var middle = this.twoPage.middle;
1903     var top  = this.twoPageTop();                
1904     var scaledW = this.twoPage.height*width/height; // $$$ assumes height of page is dominant
1905
1906     // The gutter is the dividing line between the left and right pages.
1907     // It is offset from the middle to create the illusion of thickness to the pages
1908     var gutter = middle + this.gutterOffsetForIndex(prevL);
1909     
1910     //console.log('    gutter for ' + prevL + ' is ' + gutter);
1911     //console.log('    prevL.left: ' + (gutter - scaledW) + 'px');
1912     //console.log('    changing prevL ' + prevL + ' to left: ' + (gutter-scaledW) + ' width: ' + scaledW);
1913     
1914     $(this.prefetchedImgs[prevL]).css({
1915         position: 'absolute',
1916         left: gutter-scaledW+'px',
1917         right: '', // clear right property
1918         top:    top+'px',
1919         backgroundColor: 'rgb(234, 226, 205)',
1920         height: this.twoPage.height,
1921         width:  scaledW+'px',
1922         borderRight: '1px solid black',
1923         zIndex: 1
1924     });
1925
1926     $('#GBtwopageview').append(this.prefetchedImgs[prevL]);
1927
1928     //console.log('    changing prevR ' + prevR + ' to left: ' + gutter + ' width: 0');
1929
1930     $(this.prefetchedImgs[prevR]).css({
1931         position: 'absolute',
1932         left:   gutter+'px',
1933         right: '',
1934         top:    top+'px',
1935         backgroundColor: 'rgb(234, 226, 205)',
1936         height: this.twoPage.height,
1937         width:  '0px',
1938         borderLeft: '1px solid black',
1939         zIndex: 2
1940     });
1941
1942     $('#GBtwopageview').append(this.prefetchedImgs[prevR]);
1943             
1944 }
1945
1946 // $$$ mang we're adding an extra pixel in the middle.  See https://bugs.edge.launchpad.net/gnubook/+bug/411667
1947 // prepareFlipRightToLeft()
1948 //______________________________________________________________________________
1949 GnuBook.prototype.prepareFlipRightToLeft = function(nextL, nextR) {
1950
1951     //console.log('  preparing left<-right for ' + nextL + ',' + nextR);
1952
1953     // Prefetch images
1954     this.prefetchImg(nextL);
1955     this.prefetchImg(nextR);
1956
1957     var height  = this.getPageHeight(nextR); 
1958     var width   = this.getPageWidth(nextR);    
1959     var middle = this.twoPage.middle;
1960     var top  = this.twoPageTop();               
1961     var scaledW = this.twoPage.height*width/height;
1962
1963     var gutter = middle + this.gutterOffsetForIndex(nextL);
1964         
1965     //console.log(' prepareRTL changing nextR ' + nextR + ' to left: ' + gutter);
1966     $(this.prefetchedImgs[nextR]).css({
1967         position: 'absolute',
1968         left:   gutter+'px',
1969         top:    top+'px',
1970         backgroundColor: 'rgb(234, 226, 205)',
1971         height: this.twoPage.height,
1972         width:  scaledW+'px',
1973         borderLeft: '1px solid black',
1974         zIndex: 1
1975     });
1976
1977     $('#GBtwopageview').append(this.prefetchedImgs[nextR]);
1978
1979     height  = this.getPageHeight(nextL); 
1980     width   = this.getPageWidth(nextL);      
1981     scaledW = this.twoPage.height*width/height;
1982
1983     //console.log(' prepareRTL changing nextL ' + nextL + ' to right: ' + $('#GBcontainer').width()-gutter);
1984     $(this.prefetchedImgs[nextL]).css({
1985         position: 'absolute',
1986         right:   $('#GBtwopageview').attr('clientWidth')-gutter+'px',
1987         top:    top+'px',
1988         backgroundColor: 'rgb(234, 226, 205)',
1989         height: this.twoPage.height,
1990         width:  0+'px', // Start at 0 width, then grow to the left
1991         borderRight: '1px solid black',
1992         zIndex: 2
1993     });
1994
1995     $('#GBtwopageview').append(this.prefetchedImgs[nextL]);    
1996             
1997 }
1998
1999 // getNextLeafs() -- NOT RTL AWARE
2000 //______________________________________________________________________________
2001 // GnuBook.prototype.getNextLeafs = function(o) {
2002 //     //TODO: we might have two left or two right leafs in a row (damaged book)
2003 //     //For now, assume that leafs are contiguous.
2004 //     
2005 //     //return [this.twoPage.currentIndexL+2, this.twoPage.currentIndexL+3];
2006 //     o.L = this.twoPage.currentIndexL+2;
2007 //     o.R = this.twoPage.currentIndexL+3;
2008 // }
2009
2010 // getprevLeafs() -- NOT RTL AWARE
2011 //______________________________________________________________________________
2012 // GnuBook.prototype.getPrevLeafs = function(o) {
2013 //     //TODO: we might have two left or two right leafs in a row (damaged book)
2014 //     //For now, assume that leafs are contiguous.
2015 //     
2016 //     //return [this.twoPage.currentIndexL-2, this.twoPage.currentIndexL-1];
2017 //     o.L = this.twoPage.currentIndexL-2;
2018 //     o.R = this.twoPage.currentIndexL-1;
2019 // }
2020
2021 // pruneUnusedImgs()
2022 //______________________________________________________________________________
2023 GnuBook.prototype.pruneUnusedImgs = function() {
2024     //console.log('current: ' + this.twoPage.currentIndexL + ' ' + this.twoPage.currentIndexR);
2025     for (var key in this.prefetchedImgs) {
2026         //console.log('key is ' + key);
2027         if ((key != this.twoPage.currentIndexL) && (key != this.twoPage.currentIndexR)) {
2028             //console.log('removing key '+ key);
2029             $(this.prefetchedImgs[key]).remove();
2030         }
2031         if ((key < this.twoPage.currentIndexL-4) || (key > this.twoPage.currentIndexR+4)) {
2032             //console.log('deleting key '+ key);
2033             delete this.prefetchedImgs[key];
2034         }
2035     }
2036 }
2037
2038 // prefetch()
2039 //______________________________________________________________________________
2040 GnuBook.prototype.prefetch = function() {
2041
2042     // prefetch visible pages first
2043     this.prefetchImg(this.twoPage.currentIndexL);
2044     this.prefetchImg(this.twoPage.currentIndexR);
2045     
2046     var adjacentPagesToLoad = 3;
2047     
2048     var lowCurrent = Math.min(this.twoPage.currentIndexL, this.twoPage.currentIndexR);
2049     var highCurrent = Math.max(this.twoPage.currentIndexL, this.twoPage.currentIndexR);
2050         
2051     var start = Math.max(lowCurrent - adjacentPagesToLoad, 0);
2052     var end = Math.min(highCurrent + adjacentPagesToLoad, this.numLeafs - 1);
2053     
2054     // Load images spreading out from current
2055     for (var i = 1; i <= adjacentPagesToLoad; i++) {
2056         var goingDown = lowCurrent - i;
2057         if (goingDown >= start) {
2058             this.prefetchImg(goingDown);
2059         }
2060         var goingUp = highCurrent + i;
2061         if (goingUp <= end) {
2062             this.prefetchImg(goingUp);
2063         }
2064     }
2065
2066     /*
2067     var lim = this.twoPage.currentIndexL-4;
2068     var i;
2069     lim = Math.max(lim, 0);
2070     for (i = lim; i < this.twoPage.currentIndexL; i++) {
2071         this.prefetchImg(i);
2072     }
2073     
2074     if (this.numLeafs > (this.twoPage.currentIndexR+1)) {
2075         lim = Math.min(this.twoPage.currentIndexR+4, this.numLeafs-1);
2076         for (i=this.twoPage.currentIndexR+1; i<=lim; i++) {
2077             this.prefetchImg(i);
2078         }
2079     }
2080     */
2081 }
2082
2083 // getPageWidth2UP()
2084 //______________________________________________________________________________
2085 GnuBook.prototype.getPageWidth2UP = function(index) {
2086     // We return the width based on the dominant height
2087     var height  = this.getPageHeight(index); 
2088     var width   = this.getPageWidth(index);    
2089     return Math.floor(this.twoPage.height*width/height); // $$$ we assume width is relative to current spread
2090 }    
2091
2092 // search()
2093 //______________________________________________________________________________
2094 GnuBook.prototype.search = function(term) {
2095     $('#GnuBookSearchScript').remove();
2096         var script  = document.createElement("script");
2097         script.setAttribute('id', 'GnuBookSearchScript');
2098         script.setAttribute("type", "text/javascript");
2099         script.setAttribute("src", 'http://'+this.server+'/GnuBook/flipbook_search_gb.php?url='+escape(this.bookPath + '_djvu.xml')+'&term='+term+'&format=XML&callback=gb.GBSearchCallback');
2100         document.getElementsByTagName('head')[0].appendChild(script);
2101         $('#GnuBookSearchResults').html('Searching...');
2102 }
2103
2104 // GBSearchCallback()
2105 //______________________________________________________________________________
2106 GnuBook.prototype.GBSearchCallback = function(txt) {
2107     //alert(txt);
2108     if (jQuery.browser.msie) {
2109         var dom=new ActiveXObject("Microsoft.XMLDOM");
2110         dom.async="false";
2111         dom.loadXML(txt);    
2112     } else {
2113         var parser = new DOMParser();
2114         var dom = parser.parseFromString(txt, "text/xml");    
2115     }
2116     
2117     $('#GnuBookSearchResults').empty();    
2118     $('#GnuBookSearchResults').append('<ul>');
2119     
2120     for (var key in this.searchResults) {
2121         if (null != this.searchResults[key].div) {
2122             $(this.searchResults[key].div).remove();
2123         }
2124         delete this.searchResults[key];
2125     }
2126     
2127     var pages = dom.getElementsByTagName('PAGE');
2128     
2129     if (0 == pages.length) {
2130         // $$$ it would be nice to echo the (sanitized) search result here
2131         $('#GnuBookSearchResults').append('<li>No search results found</li>');
2132     } else {    
2133         for (var i = 0; i < pages.length; i++){
2134             //console.log(pages[i].getAttribute('file').substr(1) +'-'+ parseInt(pages[i].getAttribute('file').substr(1), 10));
2135     
2136             
2137             var re = new RegExp (/_(\d{4})/);
2138             var reMatch = re.exec(pages[i].getAttribute('file'));
2139             var index = parseInt(reMatch[1], 10);
2140             //var index = parseInt(pages[i].getAttribute('file').substr(1), 10);
2141             
2142             var children = pages[i].childNodes;
2143             var context = '';
2144             for (var j=0; j<children.length; j++) {
2145                 //console.log(j + ' - ' + children[j].nodeName);
2146                 //console.log(children[j].firstChild.nodeValue);
2147                 if ('CONTEXT' == children[j].nodeName) {
2148                     context += children[j].firstChild.nodeValue;
2149                 } else if ('WORD' == children[j].nodeName) {
2150                     context += '<b>'+children[j].firstChild.nodeValue+'</b>';
2151                     
2152                     var index = this.leafNumToIndex(index);
2153                     if (null != index) {
2154                         //coordinates are [left, bottom, right, top, [baseline]]
2155                         //we'll skip baseline for now...
2156                         var coords = children[j].getAttribute('coords').split(',',4);
2157                         if (4 == coords.length) {
2158                             this.searchResults[index] = {'l':coords[0], 'b':coords[1], 'r':coords[2], 't':coords[3], 'div':null};
2159                         }
2160                     }
2161                 }
2162             }
2163             var pageName = this.getPageName(index);
2164             //TODO: remove hardcoded instance name
2165             $('#GnuBookSearchResults').append('<li><b><a href="javascript:gb.jumpToIndex('+index+');">' + pageName + '</a></b> - ' + context + '</li>');
2166         }
2167     }
2168     $('#GnuBookSearchResults').append('</ul>');
2169
2170     this.updateSearchHilites();
2171 }
2172
2173 // updateSearchHilites()
2174 //______________________________________________________________________________
2175 GnuBook.prototype.updateSearchHilites = function() {
2176     if (2 == this.mode) {
2177         this.updateSearchHilites2UP();
2178     } else {
2179         this.updateSearchHilites1UP();
2180     }
2181 }
2182
2183 // showSearchHilites1UP()
2184 //______________________________________________________________________________
2185 GnuBook.prototype.updateSearchHilites1UP = function() {
2186
2187     for (var key in this.searchResults) {
2188         
2189         if (-1 != jQuery.inArray(parseInt(key), this.displayedIndices)) {
2190             var result = this.searchResults[key];
2191             if(null == result.div) {
2192                 result.div = document.createElement('div');
2193                 $(result.div).attr('className', 'GnuBookSearchHilite').appendTo('#pagediv'+key);
2194                 //console.log('appending ' + key);
2195             }    
2196             $(result.div).css({
2197                 width:  (result.r-result.l)/this.reduce + 'px',
2198                 height: (result.b-result.t)/this.reduce + 'px',
2199                 left:   (result.l)/this.reduce + 'px',
2200                 top:    (result.t)/this.reduce +'px'
2201             });
2202
2203         } else {
2204             //console.log(key + ' not displayed');
2205             this.searchResults[key].div=null;
2206         }
2207     }
2208 }
2209
2210 // twoPageGutter()
2211 //______________________________________________________________________________
2212 // Returns the position of the gutter (line between the page images)
2213 GnuBook.prototype.twoPageGutter = function() {
2214     return this.twoPage.middle + this.gutterOffsetForIndex(this.twoPage.currentIndexL);
2215 }
2216
2217 // twoPageTop()
2218 //______________________________________________________________________________
2219 // Returns the offset for the top of the page images
2220 GnuBook.prototype.twoPageTop = function() {
2221     return this.twoPage.coverExternalPadding + this.twoPage.coverInternalPadding; // $$$ + border?
2222 }
2223
2224 // twoPageCoverWidth()
2225 //______________________________________________________________________________
2226 // Returns the width of the cover div given the total page width
2227 GnuBook.prototype.twoPageCoverWidth = function(totalPageWidth) {
2228     return totalPageWidth + this.twoPage.edgeWidth + 2*this.twoPage.coverInternalPadding;
2229 }
2230
2231 // twoPageGetViewCenter()
2232 //______________________________________________________________________________
2233 // Returns the percentage offset into twopageview div at the center of container div
2234 // { percentageX: float, percentageY: float }
2235 GnuBook.prototype.twoPageGetViewCenter = function() {
2236     var center = {};
2237
2238     var containerOffset = $('#GBcontainer').offset();
2239     var viewOffset = $('#GBtwopageview').offset();
2240     center.percentageX = (containerOffset.left - viewOffset.left + ($('#GBcontainer').attr('clientWidth') >> 1)) / this.twoPage.totalWidth;
2241     center.percentageY = (containerOffset.top - viewOffset.top + ($('#GBcontainer').attr('clientHeight') >> 1)) / this.twoPage.totalHeight;
2242     
2243     return center;
2244 }
2245
2246 // twoPageCenterView(percentageX, percentageY)
2247 //______________________________________________________________________________
2248 // Centers the point given by percentage from left,top of twopageview
2249 GnuBook.prototype.twoPageCenterView = function(percentageX, percentageY) {
2250     if ('undefined' == typeof(percentageX)) {
2251         percentageX = 0.5;
2252     }
2253     if ('undefined' == typeof(percentageY)) {
2254         percentageY = 0.5;
2255     }
2256
2257     var viewWidth = $('#GBtwopageview').width();
2258     var containerClientWidth = $('#GBcontainer').attr('clientWidth');
2259     var intoViewX = percentageX * viewWidth;
2260     
2261     var viewHeight = $('#GBtwopageview').height();
2262     var containerClientHeight = $('#GBcontainer').attr('clientHeight');
2263     var intoViewY = percentageY * viewHeight;
2264     
2265     if (viewWidth < containerClientWidth) {
2266         // Can fit width without scrollbars - center by adjusting offset
2267         $('#GBtwopageview').css('left', (containerClientWidth >> 1) - intoViewX + 'px');    
2268     } else {
2269         // Need to scroll to center
2270         $('#GBtwopageview').css('left', 0);
2271         $('#GBcontainer').scrollLeft(intoViewX - (containerClientWidth >> 1));
2272     }
2273     
2274     if (viewHeight < containerClientHeight) {
2275         // Fits with scrollbars - add offset
2276         $('#GBtwopageview').css('top', (containerClientHeight >> 1) - intoViewY + 'px');
2277     } else {
2278         $('#GBtwopageview').css('top', 0);
2279         $('#GBcontainer').scrollTop(intoViewY - (containerClientHeight >> 1));
2280     }
2281 }
2282
2283 // twoPageFlipAreaHeight
2284 //______________________________________________________________________________
2285 // Returns the integer height of the click-to-flip areas at the edges of the book
2286 GnuBook.prototype.twoPageFlipAreaHeight = function() {
2287     return parseInt(this.twoPage.height);
2288 }
2289
2290 // twoPageFlipAreaWidth
2291 //______________________________________________________________________________
2292 // Returns the integer width of the flip areas 
2293 GnuBook.prototype.twoPageFlipAreaWidth = function() {
2294     var max = 100; // $$$ TODO base on view width?
2295     var min = 10;
2296     
2297     var width = this.twoPage.width * 0.15;
2298     return parseInt(GnuBook.util.clamp(width, min, max));
2299 }
2300
2301 // twoPageFlipAreaTop
2302 //______________________________________________________________________________
2303 // Returns integer top offset for flip areas
2304 GnuBook.prototype.twoPageFlipAreaTop = function() {
2305     return parseInt(this.twoPage.bookCoverDivTop + this.twoPage.coverInternalPadding);
2306 }
2307
2308 // twoPageLeftFlipAreaLeft
2309 //______________________________________________________________________________
2310 // Left offset for left flip area
2311 GnuBook.prototype.twoPageLeftFlipAreaLeft = function() {
2312     return parseInt(this.twoPage.gutter - this.twoPage.scaledWL);
2313 }
2314
2315 // twoPageRightFlipAreaLeft
2316 //______________________________________________________________________________
2317 // Left offset for right flip area
2318 GnuBook.prototype.twoPageRightFlipAreaLeft = function() {
2319     return parseInt(this.twoPage.gutter + this.twoPage.scaledWR - this.twoPageFlipAreaWidth());
2320 }
2321
2322 // twoPagePlaceFlipAreas
2323 //______________________________________________________________________________
2324 // Readjusts position of flip areas based on current layout
2325 GnuBook.prototype.twoPagePlaceFlipAreas = function() {
2326     // We don't set top since it shouldn't change relative to view
2327     $(this.twoPage.leftFlipArea).css({
2328         left: this.twoPageLeftFlipAreaLeft() + 'px',
2329         width: this.twoPageFlipAreaWidth() + 'px'
2330     });
2331     $(this.twoPage.rightFlipArea).css({
2332         left: this.twoPageRightFlipAreaLeft() + 'px',
2333         width: this.twoPageFlipAreaWidth() + 'px'
2334     });
2335 }
2336     
2337 // showSearchHilites2UP()
2338 //______________________________________________________________________________
2339 GnuBook.prototype.updateSearchHilites2UP = function() {
2340
2341     for (var key in this.searchResults) {
2342         key = parseInt(key, 10);
2343         if (-1 != jQuery.inArray(key, this.displayedIndices)) {
2344             var result = this.searchResults[key];
2345             if(null == result.div) {
2346                 result.div = document.createElement('div');
2347                 $(result.div).attr('className', 'GnuBookSearchHilite').css('zIndex', 3).appendTo('#GBtwopageview');
2348                 //console.log('appending ' + key);
2349             }
2350
2351             // We calculate the reduction factor for the specific page because it can be different
2352             // for each page in the spread
2353             var height = this.getPageHeight(key);
2354             var width  = this.getPageWidth(key)
2355             var reduce = this.twoPage.height/height;
2356             var scaledW = parseInt(width*reduce);
2357             
2358             var gutter = this.twoPageGutter();
2359             var pageL;
2360             if ('L' == this.getPageSide(key)) {
2361                 pageL = gutter-scaledW;
2362             } else {
2363                 pageL = gutter;
2364             }
2365             var pageT  = this.twoPageTop();
2366             
2367             $(result.div).css({
2368                 width:  (result.r-result.l)*reduce + 'px',
2369                 height: (result.b-result.t)*reduce + 'px',
2370                 left:   pageL+(result.l)*reduce + 'px',
2371                 top:    pageT+(result.t)*reduce +'px'
2372             });
2373
2374         } else {
2375             //console.log(key + ' not displayed');
2376             if (null != this.searchResults[key].div) {
2377                 //console.log('removing ' + key);
2378                 $(this.searchResults[key].div).remove();
2379             }
2380             this.searchResults[key].div=null;
2381         }
2382     }
2383 }
2384
2385 // removeSearchHilites()
2386 //______________________________________________________________________________
2387 GnuBook.prototype.removeSearchHilites = function() {
2388     for (var key in this.searchResults) {
2389         if (null != this.searchResults[key].div) {
2390             $(this.searchResults[key].div).remove();
2391             this.searchResults[key].div=null;
2392         }        
2393     }
2394 }
2395
2396 // printPage
2397 //______________________________________________________________________________
2398 GnuBook.prototype.printPage = function() {
2399     if (null != this.printPopup) { // check if already showing
2400         return;
2401     }
2402     this.printPopup = document.createElement("div");
2403     $(this.printPopup).css({
2404         position: 'absolute',
2405         top:      '20px',
2406         left:     ($('#GBcontainer').width()-400)/2 + 'px',
2407         width:    '400px',
2408         padding:  "20px",
2409         border:   "3px double #999999",
2410         zIndex:   3,
2411         backgroundColor: "#fff"
2412     }).appendTo('#GnuBook');
2413
2414     var indexToPrint;
2415     if (this.constMode1up == this.mode) {
2416         indexToPrint = this.firstIndex;
2417     } else {
2418         indexToPrint = this.twoPage.currentIndexL;
2419     }
2420     
2421     this.indexToPrint = indexToPrint;
2422     
2423     var htmlStr = '<div style="text-align: center;">';
2424     htmlStr =  '<p style="text-align:center;"><b><a href="javascript:void(0);" onclick="window.frames[0].focus(); window.frames[0].print(); return false;">Click here to print this page</a></b></p>';
2425     htmlStr += '<div id="printDiv" name="printDiv" style="text-align: center; width: 233px; margin: auto">'
2426     htmlStr +=   '<p style="text-align:right; margin: 0; font-size: 0.85em">';
2427     //htmlStr +=     '<button class="GBicon rollover book_up" onclick="gb.updatePrintFrame(-1); return false;"></button> ';
2428     //htmlStr +=     '<button class="GBicon rollover book_down" onclick="gb.updatePrintFrame(1); return false;"></button>';
2429     htmlStr += '<a href="#" onclick="gb.updatePrintFrame(-1); return false;">Prev</a> <a href="#" onclick="gb.updatePrintFrame(1); return false;">Next</a>';
2430     htmlStr +=   '</p>';
2431     htmlStr += '</div>';
2432     htmlStr += '<p style="text-align:center;"><a href="" onclick="gb.printPopup = null; $(this.parentNode.parentNode).remove(); return false">Close popup</a></p>';
2433     htmlStr += '</div>';
2434     
2435     this.printPopup.innerHTML = htmlStr;
2436     
2437     var iframe = document.createElement('iframe');
2438     iframe.id = 'printFrame';
2439     iframe.name = 'printFrame';
2440     iframe.width = '233px'; // 8.5 x 11 aspect
2441     iframe.height = '300px';
2442     
2443     var self = this; // closure
2444         
2445     $(iframe).load(function() {
2446         var doc = GnuBook.util.getIFrameDocument(this);
2447         $('body', doc).html(self.getPrintFrameContent(self.indexToPrint));
2448     });
2449     
2450     $('#printDiv').prepend(iframe);
2451 }
2452
2453 GnuBook.prototype.getPrintFrameContent = function(index) {    
2454     // We fit the image based on an assumed US Letter aspect ratio.
2455     var paperAspect = 8.5 / 11;
2456     var imageAspect = this.getPageWidth(index) / this.getPageHeight(index);
2457     
2458     var rotate = 0;
2459     
2460     // Rotate if possible and appropriate, to get larger image size on printed page
2461     if (this.canRotatePage(index)) {
2462         if (imageAspect > 1 && imageAspect > paperAspect) {
2463             // more wide than square, and more wide than paper
2464             rotate = 90;
2465             imageAspect = 1/imageAspect;
2466         }
2467     }
2468     
2469     var fitAttrs;
2470     if (imageAspect > paperAspect) {
2471         // wider than paper, fit width
2472         fitAttrs = 'width="100%"';
2473     } else {
2474         // taller than paper, fit height
2475         fitAttrs = 'height="100%"';
2476     }
2477
2478     var imageURL = this.getPageURI(index, 1, rotate);
2479     var iframeStr = '<html style="padding: 0; border: 0; margin: 0"><head><title>' + this.bookTitle + '</title></head><body style="padding: 0; border:0; margin: 0">';
2480     iframeStr += '<div style="width: 100%; height: 100%; overflow: hidden; text-align: center;">';
2481     iframeStr +=   '<img src="' + imageURL + '" ' + fitAttrs + ' />';
2482     iframeStr += '</div>';
2483     iframeStr += '</body></html>';
2484     
2485     return iframeStr;
2486 }
2487
2488 GnuBook.prototype.updatePrintFrame = function(delta) {
2489     var newIndex = this.indexToPrint + delta;
2490     newIndex = GnuBook.util.clamp(newIndex, 0, this.numLeafs - 1);
2491     if (newIndex == this.indexToPrint) {
2492         return;
2493     }
2494     this.indexToPrint = newIndex;
2495     var doc = GnuBook.util.getIFrameDocument($('#printFrame')[0]);
2496     $('body', doc).html(this.getPrintFrameContent(this.indexToPrint));
2497 }
2498
2499 // showEmbedCode()
2500 //______________________________________________________________________________
2501 GnuBook.prototype.showEmbedCode = function() {
2502     if (null != this.embedPopup) { // check if already showing
2503         return;
2504     }
2505     this.autoStop();
2506     this.embedPopup = document.createElement("div");
2507     $(this.embedPopup).css({
2508         position: 'absolute',
2509         top:      '20px',
2510         left:     ($('#GBcontainer').attr('clientWidth')-400)/2 + 'px',
2511         width:    '400px',
2512         padding:  "20px",
2513         border:   "3px double #999999",
2514         zIndex:   3,
2515         backgroundColor: "#fff"
2516     }).appendTo('#GnuBook');
2517
2518     htmlStr =  '<p style="text-align:center;"><b>Embed Bookreader in your blog!</b></p>';
2519     htmlStr += '<p>The bookreader uses iframes for embedding. It will not work on web hosts that block iframes. The embed feature has been tested on blogspot.com blogs as well as self-hosted Wordpress blogs. This feature will NOT work on wordpress.com blogs.</p>';
2520     htmlStr += '<p>Embed Code: <input type="text" size="40" value="' + this.getEmbedCode() + '"></p>';
2521     htmlStr += '<p style="text-align:center;"><a href="" onclick="gb.embedPopup = null; $(this.parentNode.parentNode).remove(); return false">Close popup</a></p>';    
2522
2523     this.embedPopup.innerHTML = htmlStr;
2524     $(this.embedPopup).find('input').bind('click', function() {
2525         this.select();
2526     })
2527 }
2528
2529 // autoToggle()
2530 //______________________________________________________________________________
2531 GnuBook.prototype.autoToggle = function() {
2532
2533     var bComingFrom1up = false;
2534     if (2 != this.mode) {
2535         bComingFrom1up = true;
2536         this.switchMode(2);
2537     }
2538     
2539     // Change to autofit if book is too large
2540     if (this.reduce < this.twoPageGetAutofitReduce()) {
2541         this.zoom2up(0);
2542     }
2543
2544     var self = this;
2545     if (null == this.autoTimer) {
2546         this.flipSpeed = 2000;
2547         
2548         // $$$ Draw events currently cause layout problems when they occur during animation.
2549         //     There is a specific problem when changing from 1-up immediately to autoplay in RTL so
2550         //     we workaround for now by not triggering immediate animation in that case.
2551         //     See https://bugs.launchpad.net/gnubook/+bug/328327
2552         if (('rl' == this.pageProgression) && bComingFrom1up) {
2553             // don't flip immediately -- wait until timer fires
2554         } else {
2555             // flip immediately
2556             this.flipFwdToIndex();        
2557         }
2558
2559         $('#GBtoolbar .play').hide();
2560         $('#GBtoolbar .pause').show();
2561         this.autoTimer=setInterval(function(){
2562             if (self.animating) {return;}
2563             
2564             if (Math.max(self.twoPage.currentIndexL, self.twoPage.currentIndexR) >= self.lastDisplayableIndex()) {
2565                 self.flipBackToIndex(1); // $$$ really what we want?
2566             } else {            
2567                 self.flipFwdToIndex();
2568             }
2569         },5000);
2570     } else {
2571         this.autoStop();
2572     }
2573 }
2574
2575 // autoStop()
2576 //______________________________________________________________________________
2577 // Stop autoplay mode, allowing animations to finish
2578 GnuBook.prototype.autoStop = function() {
2579     if (null != this.autoTimer) {
2580         clearInterval(this.autoTimer);
2581         this.flipSpeed = 'fast';
2582         $('#GBtoolbar .pause').hide();
2583         $('#GBtoolbar .play').show();
2584         this.autoTimer = null;
2585     }
2586 }
2587
2588 // stopFlipAnimations
2589 //______________________________________________________________________________
2590 // Immediately stop flip animations.  Callbacks are triggered.
2591 GnuBook.prototype.stopFlipAnimations = function() {
2592
2593     this.autoStop(); // Clear timers
2594
2595     // Stop animation, clear queue, trigger callbacks
2596     if (this.leafEdgeTmp) {
2597         $(this.leafEdgeTmp).stop(false, true);
2598     }
2599     jQuery.each(this.prefetchedImgs, function() {
2600         $(this).stop(false, true);
2601         });
2602
2603     // And again since animations also queued in callbacks
2604     if (this.leafEdgeTmp) {
2605         $(this.leafEdgeTmp).stop(false, true);
2606     }
2607     jQuery.each(this.prefetchedImgs, function() {
2608         $(this).stop(false, true);
2609         });
2610    
2611 }
2612
2613 // keyboardNavigationIsDisabled(event)
2614 //   - returns true if keyboard navigation should be disabled for the event
2615 //______________________________________________________________________________
2616 GnuBook.prototype.keyboardNavigationIsDisabled = function(event) {
2617     if (event.target.tagName == "INPUT") {
2618         return true;
2619     }   
2620     return false;
2621 }
2622
2623 // gutterOffsetForIndex
2624 //______________________________________________________________________________
2625 //
2626 // Returns the gutter offset for the spread containing the given index.
2627 // This function supports RTL
2628 GnuBook.prototype.gutterOffsetForIndex = function(pindex) {
2629
2630     // To find the offset of the gutter from the middle we calculate our percentage distance
2631     // through the book (0..1), remap to (-0.5..0.5) and multiply by the total page edge width
2632     var offset = parseInt(((pindex / this.numLeafs) - 0.5) * this.twoPage.edgeWidth);
2633     
2634     // But then again for RTL it's the opposite
2635     if ('rl' == this.pageProgression) {
2636         offset = -offset;
2637     }
2638     
2639     return offset;
2640 }
2641
2642 // leafEdgeWidth
2643 //______________________________________________________________________________
2644 // Returns the width of the leaf edge div for the page with index given
2645 GnuBook.prototype.leafEdgeWidth = function(pindex) {
2646     // $$$ could there be single pixel rounding errors for L vs R?
2647     if ((this.getPageSide(pindex) == 'L') && (this.pageProgression != 'rl')) {
2648         return parseInt( (pindex/this.numLeafs) * this.twoPage.edgeWidth + 0.5);
2649     } else {
2650         return parseInt( (1 - pindex/this.numLeafs) * this.twoPage.edgeWidth + 0.5);
2651     }
2652 }
2653
2654 // jumpIndexForLeftEdgePageX
2655 //______________________________________________________________________________
2656 // Returns the target jump leaf given a page coordinate (inside the left page edge div)
2657 GnuBook.prototype.jumpIndexForLeftEdgePageX = function(pageX) {
2658     if ('rl' != this.pageProgression) {
2659         // LTR - flipping backward
2660         var jumpIndex = this.twoPage.currentIndexL - ($(this.leafEdgeL).offset().left + $(this.leafEdgeL).width() - pageX) * 10;
2661
2662         // browser may have resized the div due to font size change -- see https://bugs.launchpad.net/gnubook/+bug/333570        
2663         jumpIndex = GnuBook.util.clamp(Math.round(jumpIndex), this.firstDisplayableIndex(), this.twoPage.currentIndexL - 2);
2664         return jumpIndex;
2665
2666     } else {
2667         var jumpIndex = this.twoPage.currentIndexL + ($(this.leafEdgeL).offset().left + $(this.leafEdgeL).width() - pageX) * 10;
2668         jumpIndex = GnuBook.util.clamp(Math.round(jumpIndex), this.twoPage.currentIndexL + 2, this.lastDisplayableIndex());
2669         return jumpIndex;
2670     }
2671 }
2672
2673 // jumpIndexForRightEdgePageX
2674 //______________________________________________________________________________
2675 // Returns the target jump leaf given a page coordinate (inside the right page edge div)
2676 GnuBook.prototype.jumpIndexForRightEdgePageX = function(pageX) {
2677     if ('rl' != this.pageProgression) {
2678         // LTR
2679         var jumpIndex = this.twoPage.currentIndexR + (pageX - $(this.leafEdgeR).offset().left) * 10;
2680         jumpIndex = GnuBook.util.clamp(Math.round(jumpIndex), this.twoPage.currentIndexR + 2, this.lastDisplayableIndex());
2681         return jumpIndex;
2682     } else {
2683         var jumpIndex = this.twoPage.currentIndexR - (pageX - $(this.leafEdgeR).offset().left) * 10;
2684         jumpIndex = GnuBook.util.clamp(Math.round(jumpIndex), this.firstDisplayableIndex(), this.twoPage.currentIndexR - 2);
2685         return jumpIndex;
2686     }
2687 }
2688
2689 GnuBook.prototype.initToolbar = function(mode, ui) {
2690
2691     $("#GnuBook").append("<div id='GBtoolbar'><span style='float:left;'>"
2692         + "<a class='GBicon logo rollover' href='" + this.logoURL + "'>&nbsp;</a>"
2693         + " <button class='GBicon rollover zoom_out' onclick='gb.zoom(-1); return false;'/>" 
2694         + "<button class='GBicon rollover zoom_in' onclick='gb.zoom(1); return false;'/>"
2695         + " <span class='label'>Zoom: <span id='GBzoom'>"+parseInt(100/this.reduce)+"</span></span>"
2696         + " <button class='GBicon rollover one_page_mode' onclick='gb.switchMode(1); return false;'/>"
2697         + " <button class='GBicon rollover two_page_mode' onclick='gb.switchMode(2); return false;'/>"
2698         + "&nbsp;&nbsp;<a class='GBblack title' href='"+this.bookUrl+"' target='_blank'>"+this.shortTitle(50)+"</a>"
2699         + "</span></div>");
2700     
2701     this.updateToolbarZoom(this.reduce); // Pretty format
2702         
2703     if (ui == "embed") {
2704         $("#GnuBook a.logo").attr("target","_blank");
2705     }
2706
2707     // $$$ turn this into a member variable
2708     var jToolbar = $('#GBtoolbar'); // j prefix indicates jQuery object
2709     
2710     // We build in mode 2
2711     jToolbar.append("<span id='GBtoolbarbuttons' style='float: right'>"
2712         + "<button class='GBicon print rollover' /> <button class='GBicon rollover embed' />"
2713         + "<form class='GBpageform' action='javascript:' onsubmit='gb.jumpToPage(this.elements[0].value)'> <span class='label'>Page:<input id='GBpagenum' type='text' size='3' onfocus='gb.autoStop();'></input></span></form>"
2714         + "<div class='GBtoolbarmode2' style='display: none'><button class='GBicon rollover book_leftmost' /><button class='GBicon rollover book_left' /><button class='GBicon rollover book_right' /><button class='GBicon rollover book_rightmost' /></div>"
2715         + "<div class='GBtoolbarmode1' style='display: none'><button class='GBicon rollover book_top' /><button class='GBicon rollover book_up' /> <button class='GBicon rollover book_down' /><button class='GBicon rollover book_bottom' /></div>"
2716         + "<button class='GBicon rollover play' /><button class='GBicon rollover pause' style='display: none' /></span>");
2717
2718     this.bindToolbarNavHandlers(jToolbar);
2719     
2720     // Setup tooltips -- later we could load these from a file for i18n
2721     var titles = { '.logo': 'Go to Archive.org',
2722                    '.zoom_in': 'Zoom in',
2723                    '.zoom_out': 'Zoom out',
2724                    '.one_page_mode': 'One-page view',
2725                    '.two_page_mode': 'Two-page view',
2726                    '.print': 'Print this page',
2727                    '.embed': 'Embed bookreader',
2728                    '.book_left': 'Flip left',
2729                    '.book_right': 'Flip right',
2730                    '.book_up': 'Page up',
2731                    '.book_down': 'Page down',
2732                    '.play': 'Play',
2733                    '.pause': 'Pause',
2734                    '.book_top': 'First page',
2735                    '.book_bottom': 'Last page'
2736                   };
2737     if ('rl' == this.pageProgression) {
2738         titles['.book_leftmost'] = 'Last page';
2739         titles['.book_rightmost'] = 'First page';
2740     } else { // LTR
2741         titles['.book_leftmost'] = 'First page';
2742         titles['.book_rightmost'] = 'Last page';
2743     }
2744                   
2745     for (var icon in titles) {
2746         jToolbar.find(icon).attr('title', titles[icon]);
2747     }
2748     
2749     // Hide mode buttons and autoplay if 2up is not available
2750     // $$$ if we end up with more than two modes we should show the applicable buttons
2751     if ( !this.canSwitchToMode(this.constMode2up) ) {
2752         jToolbar.find('.one_page_mode, .two_page_mode, .play, .pause').hide();
2753     }
2754
2755     // Switch to requested mode -- binds other click handlers
2756     this.switchToolbarMode(mode);
2757
2758 }
2759
2760
2761 // switchToolbarMode
2762 //______________________________________________________________________________
2763 // Update the toolbar for the given mode (changes navigation buttons)
2764 // $$$ we should soon split the toolbar out into its own module
2765 GnuBook.prototype.switchToolbarMode = function(mode) {
2766     if (1 == mode) {
2767         // 1-up     
2768         $('#GBtoolbar .GBtoolbarmode2').hide();
2769         $('#GBtoolbar .GBtoolbarmode1').show().css('display', 'inline');
2770     } else {
2771         // 2-up
2772         $('#GBtoolbar .GBtoolbarmode1').hide();
2773         $('#GBtoolbar .GBtoolbarmode2').show().css('display', 'inline');
2774     }
2775 }
2776
2777 // bindToolbarNavHandlers
2778 //______________________________________________________________________________
2779 // Binds the toolbar handlers
2780 GnuBook.prototype.bindToolbarNavHandlers = function(jToolbar) {
2781
2782     jToolbar.find('.book_left').bind('click', function(e) {
2783         gb.left();
2784         return false;
2785     });
2786          
2787     jToolbar.find('.book_right').bind('click', function(e) {
2788         gb.right();
2789         return false;
2790     });
2791         
2792     jToolbar.find('.book_up').bind('click', function(e) {
2793         gb.prev();
2794         return false;
2795     });        
2796         
2797     jToolbar.find('.book_down').bind('click', function(e) {
2798         gb.next();
2799         return false;
2800     });
2801
2802     jToolbar.find('.print').bind('click', function(e) {
2803         gb.printPage();
2804         return false;
2805     });
2806         
2807     jToolbar.find('.embed').bind('click', function(e) {
2808         gb.showEmbedCode();
2809         return false;
2810     });
2811
2812     jToolbar.find('.play').bind('click', function(e) {
2813         gb.autoToggle();
2814         return false;
2815     });
2816
2817     jToolbar.find('.pause').bind('click', function(e) {
2818         gb.autoToggle();
2819         return false;
2820     });
2821     
2822     jToolbar.find('.book_top').bind('click', function(e) {
2823         gb.first();
2824         return false;
2825     });
2826
2827     jToolbar.find('.book_bottom').bind('click', function(e) {
2828         gb.last();
2829         return false;
2830     });
2831     
2832     jToolbar.find('.book_leftmost').bind('click', function(e) {
2833         gb.leftmost();
2834         return false;
2835     });
2836   
2837     jToolbar.find('.book_rightmost').bind('click', function(e) {
2838         gb.rightmost();
2839         return false;
2840     });
2841 }
2842
2843 // updateToolbarZoom(reduce)
2844 //______________________________________________________________________________
2845 // Update the displayed zoom factor based on reduction factor
2846 GnuBook.prototype.updateToolbarZoom = function(reduce) {
2847     var value;
2848     if (this.constMode2up == this.mode && this.twoPage.autofit) {
2849         value = 'Auto';
2850     } else {
2851         value = (100 / reduce).toFixed(2);
2852         // Strip trailing zeroes and decimal if all zeroes
2853         value = value.replace(/0+$/,'');
2854         value = value.replace(/\.$/,'');
2855         value += '%';
2856     }
2857     $('#GBzoom').text(value);
2858 }
2859
2860 // firstDisplayableIndex
2861 //______________________________________________________________________________
2862 // Returns the index of the first visible page, dependent on the mode.
2863 // $$$ Currently we cannot display the front/back cover in 2-up and will need to update
2864 // this function when we can as part of https://bugs.launchpad.net/gnubook/+bug/296788
2865 GnuBook.prototype.firstDisplayableIndex = function() {
2866     if (this.mode == 0) {
2867         return 0;
2868     } else {
2869         return 1; // $$$ we assume there are enough pages... we need logic for very short books
2870     }
2871 }
2872
2873 // lastDisplayableIndex
2874 //______________________________________________________________________________
2875 // Returns the index of the last visible page, dependent on the mode.
2876 // $$$ Currently we cannot display the front/back cover in 2-up and will need to update
2877 // this function when we can as pa  rt of https://bugs.launchpad.net/gnubook/+bug/296788
2878 GnuBook.prototype.lastDisplayableIndex = function() {
2879     if (this.mode == 2) {
2880         if (this.lastDisplayableIndex2up === null) {
2881             // Calculate and cache
2882             var candidate = this.numLeafs - 1;
2883             for ( ; candidate >= 0; candidate--) {
2884                 var spreadIndices = this.getSpreadIndices(candidate);
2885                 if (Math.max(spreadIndices[0], spreadIndices[1]) < (this.numLeafs - 1)) {
2886                     break;
2887                 }
2888             }
2889             this.lastDisplayableIndex2up = candidate;
2890         }
2891         return this.lastDisplayableIndex2up;
2892     } else {
2893         return this.numLeafs - 1;
2894     }
2895 }
2896
2897 // shortTitle(maximumCharacters)
2898 //________
2899 // Returns a shortened version of the title with the maximum number of characters
2900 GnuBook.prototype.shortTitle = function(maximumCharacters) {
2901     if (this.bookTitle.length < maximumCharacters) {
2902         return this.bookTitle;
2903     }
2904     
2905     var title = this.bookTitle.substr(0, maximumCharacters - 3);
2906     title += '...';
2907     return title;
2908 }
2909
2910 // Parameter related functions
2911
2912 // updateFromParams(params)
2913 //________
2914 // Update ourselves from the params object.
2915 //
2916 // e.g. this.updateFromParams(this.paramsFromFragment(window.location.hash))
2917 GnuBook.prototype.updateFromParams = function(params) {
2918     if ('undefined' != typeof(params.mode)) {
2919         this.switchMode(params.mode);
2920     }
2921
2922     // $$$ process /search
2923     // $$$ process /zoom
2924     
2925     // We only respect page if index is not set
2926     if ('undefined' != typeof(params.index)) {
2927         if (params.index != this.currentIndex()) {
2928             this.jumpToIndex(params.index);
2929         }
2930     } else if ('undefined' != typeof(params.page)) {
2931         // $$$ this assumes page numbers are unique
2932         if (params.page != this.getPageNum(this.currentIndex())) {
2933             this.jumpToPage(params.page);
2934         }
2935     }
2936     
2937     // $$$ process /region
2938     // $$$ process /highlight
2939 }
2940
2941 // paramsFromFragment(urlFragment)
2942 //________
2943 // Returns a object with configuration parametes from a URL fragment.
2944 //
2945 // E.g paramsFromFragment(window.location.hash)
2946 GnuBook.prototype.paramsFromFragment = function(urlFragment) {
2947     // URL fragment syntax specification: http://openlibrary.org/dev/docs/bookurls
2948     
2949     var params = {};
2950     
2951     // For convenience we allow an initial # character (as from window.location.hash)
2952     // but don't require it
2953     if (urlFragment.substr(0,1) == '#') {
2954         urlFragment = urlFragment.substr(1);
2955     }
2956     
2957     // Simple #nn syntax
2958     var oldStyleLeafNum = parseInt( /^\d+$/.exec(urlFragment) );
2959     if ( !isNaN(oldStyleLeafNum) ) {
2960         params.index = oldStyleLeafNum;
2961         
2962         // Done processing if using old-style syntax
2963         return params;
2964     }
2965     
2966     // Split into key-value pairs
2967     var urlArray = urlFragment.split('/');
2968     var urlHash = {};
2969     for (var i = 0; i < urlArray.length; i += 2) {
2970         urlHash[urlArray[i]] = urlArray[i+1];
2971     }
2972     
2973     // Mode
2974     if ('1up' == urlHash['mode']) {
2975         params.mode = this.constMode1up;
2976     } else if ('2up' == urlHash['mode']) {
2977         params.mode = this.constMode2up;
2978     }
2979     
2980     // Index and page
2981     if ('undefined' != typeof(urlHash['page'])) {
2982         // page was set -- may not be int
2983         params.page = urlHash['page'];
2984     }
2985     
2986     // $$$ process /region
2987     // $$$ process /search
2988     // $$$ process /highlight
2989         
2990     return params;
2991 }
2992
2993 // paramsFromCurrent()
2994 //________
2995 // Create a params object from the current parameters.
2996 GnuBook.prototype.paramsFromCurrent = function() {
2997
2998     var params = {};
2999
3000     var pageNum = this.getPageNum(this.currentIndex());
3001     if ((pageNum === 0) || pageNum) {
3002         params.page = pageNum;
3003     }
3004     
3005     params.index = this.currentIndex();
3006     params.mode = this.mode;
3007     
3008     // $$$ highlight
3009     // $$$ region
3010     // $$$ search
3011     
3012     return params;
3013 }
3014
3015 // fragmentFromParams(params)
3016 //________
3017 // Create a fragment string from the params object.
3018 // See http://openlibrary.org/dev/docs/bookurls for an explanation of the fragment syntax.
3019 GnuBook.prototype.fragmentFromParams = function(params) {
3020     var separator = '/';
3021     
3022     var fragments = [];
3023     
3024     if ('undefined' != typeof(params.page)) {
3025         fragments.push('page', params.page);
3026     } else {
3027         // Don't have page numbering -- use index instead
3028         fragments.push('page', 'n' + params.index);
3029     }
3030     
3031     // $$$ highlight
3032     // $$$ region
3033     // $$$ search
3034     
3035     // mode
3036     if ('undefined' != typeof(params.mode)) {    
3037         if (params.mode == this.constMode1up) {
3038             fragments.push('mode', '1up');
3039         } else if (params.mode == this.constMode2up) {
3040             fragments.push('mode', '2up');
3041         } else {
3042             throw 'fragmentFromParams called with unknown mode ' + params.mode;
3043         }
3044     }
3045     
3046     return fragments.join(separator);
3047 }
3048
3049 // getPageIndex(pageNum)
3050 //________
3051 // Returns the *highest* index the given page number, or undefined
3052 GnuBook.prototype.getPageIndex = function(pageNum) {
3053     var pageIndices = this.getPageIndices(pageNum);
3054     
3055     if (pageIndices.length > 0) {
3056         return pageIndices[pageIndices.length - 1];
3057     }
3058
3059     return undefined;
3060 }
3061
3062 // getPageIndices(pageNum)
3063 //________
3064 // Returns an array (possibly empty) of the indices with the given page number
3065 GnuBook.prototype.getPageIndices = function(pageNum) {
3066     var indices = [];
3067
3068     // Check for special "nXX" page number
3069     if (pageNum.slice(0,1) == 'n') {
3070         try {
3071             var pageIntStr = pageNum.slice(1, pageNum.length);
3072             var pageIndex = parseInt(pageIntStr);
3073             indices.push(pageIndex);
3074             return indices;
3075         } catch(err) {
3076             // Do nothing... will run through page names and see if one matches
3077         }
3078     }
3079
3080     var i;
3081     for (i=0; i<this.numLeafs; i++) {
3082         if (this.getPageNum(i) == pageNum) {
3083             indices.push(i);
3084         }
3085     }
3086     
3087     return indices;
3088 }
3089
3090 // getPageName(index)
3091 //________
3092 // Returns the name of the page as it should be displayed in the user interface
3093 GnuBook.prototype.getPageName = function(index) {
3094     return 'Page ' + this.getPageNum(index);
3095 }
3096
3097 // updateLocationHash
3098 //________
3099 // Update the location hash from the current parameters.  Call this instead of manually
3100 // using window.location.replace
3101 GnuBook.prototype.updateLocationHash = function() {
3102     var newHash = '#' + this.fragmentFromParams(this.paramsFromCurrent());
3103     window.location.replace(newHash);
3104     
3105     // This is the variable checked in the timer.  Only user-generated changes
3106     // to the URL will trigger the event.
3107     this.oldLocationHash = newHash;
3108 }
3109
3110 // startLocationPolling
3111 //________
3112 // Starts polling of window.location to see hash fragment changes
3113 GnuBook.prototype.startLocationPolling = function() {
3114     var self = this; // remember who I am
3115     self.oldLocationHash = window.location.hash;
3116     
3117     if (this.locationPollId) {
3118         clearInterval(this.locationPollID);
3119         this.locationPollId = null;
3120     }
3121     
3122     this.locationPollId = setInterval(function() {
3123         var newHash = window.location.hash;
3124         if (newHash != self.oldLocationHash) {
3125             if (newHash != self.oldUserHash) { // Only process new user hash once
3126                 //console.log('url change detected ' + self.oldLocationHash + " -> " + newHash);
3127                 
3128                 // Queue change if animating
3129                 if (self.animating) {
3130                     self.autoStop();
3131                     self.animationFinishedCallback = function() {
3132                         self.updateFromParams(self.paramsFromFragment(newHash));
3133                     }                        
3134                 } else { // update immediately
3135                     self.updateFromParams(self.paramsFromFragment(newHash));
3136                 }
3137                 self.oldUserHash = newHash;
3138             }
3139         }
3140     }, 500);
3141 }
3142
3143 // canSwitchToMode
3144 //________
3145 // Returns true if we can switch to the requested mode
3146 GnuBook.prototype.canSwitchToMode = function(mode) {
3147     if (mode == this.constMode2up) {
3148         // check there are enough pages to display
3149         // $$$ this is a workaround for the mis-feature that we can't display
3150         //     short books in 2up mode
3151         if (this.numLeafs < 6) {
3152             return false;
3153         }
3154     }
3155     
3156     return true;
3157 }
3158
3159 // Library functions
3160 GnuBook.util = {
3161     clamp: function(value, min, max) {
3162         return Math.min(Math.max(value, min), max);
3163     },
3164
3165     getIFrameDocument: function(iframe) {
3166         // Adapted from http://xkr.us/articles/dom/iframe-document/
3167         var outer = (iframe.contentWindow || iframe.contentDocument);
3168         return (outer.document || outer);
3169     }
3170 }