Rename left/right edge icons using _ for better consistency.
[bookreader.git] / GnuBook / GnuBook.js
1 /*
2 Copyright(c)2008 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.68 $ $Date: 2009-03-04 22:00:31 $
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     
38     this.displayedLeafs = [];   
39     //this.leafsToDisplay = [];
40     this.imgs = {};
41     this.prefetchedImgs = {}; //an object with numeric keys cooresponding to leafNum
42     
43     this.timer     = null;
44     this.animating = false;
45     this.auto      = false;
46     this.autoTimer = null;
47     this.flipSpeed = 'fast';
48
49     this.twoPagePopUp = null;
50     this.leafEdgeTmp  = null;
51     this.embedPopup = null;
52     
53     this.searchResults = {};
54     
55     this.firstIndex = null;
56     
57     this.lastDisplayableIndex2up = null;
58     
59 };
60
61 // init()
62 //______________________________________________________________________________
63 GnuBook.prototype.init = function() {
64     var startLeaf = window.location.hash;
65     //console.log("startLeaf from location.hash: %s", startLeaf);
66     if ('' == startLeaf) {
67         if (this.titleLeaf) {
68             startLeaf = "#" + this.leafNumToIndex(this.titleLeaf);
69         }
70     }
71     
72     // Ideally this would be set in the HTML/PHP for better search engine visibility but
73     // it takes some time to locate the item and retrieve the metadata
74     document.title = this.shortTitle(50);
75     
76     $("#GnuBook").empty();
77     this.initToolbar(this.mode); // Build inside of toolbar div
78     $("#GnuBook").append("<div id='GBcontainer'></div>");
79     $("#GBcontainer").append("<div id='GBpageview'></div>");
80
81     $("#GBcontainer").bind('scroll', this, function(e) {
82         e.data.loadLeafs();
83     });
84
85     this.setupKeyListeners();
86
87     $(window).bind('resize', this, function(e) {
88         //console.log('resize!');
89         if (1 == e.data.mode) {
90             //console.log('centering 1page view');
91             e.data.centerPageView();
92             $('#GBpageview').empty()
93             e.data.displayedLeafs = [];
94             e.data.updateSearchHilites(); //deletes hilights but does not call remove()            
95             e.data.loadLeafs();
96         } else {
97             //console.log('drawing 2 page view');
98             e.data.prepareTwoPageView();
99         }
100     });
101
102     if (1 == this.mode) {
103         this.resizePageView();
104     
105         if ('' != startLeaf) { // Jump to the leaf specified in the URL
106             this.jumpToIndex(parseInt(startLeaf.substr(1)));
107             //console.log('jump to ' + parseInt(startLeaf.substr(1)));
108         }
109     } else {
110         //this.resizePageView();
111         
112         this.displayedLeafs=[0];
113         if ('' != startLeaf) {
114             this.displayedLeafs = [parseInt(startLeaf.substr(1))];
115         }
116         //console.log('titleLeaf: %d', this.titleLeaf);
117         //console.log('displayedLeafs: %s', this.displayedLeafs);
118         this.prepareTwoPageView();
119         //if (this.auto) this.nextPage();
120     }
121 }
122
123 GnuBook.prototype.setupKeyListeners = function() {
124     var self = this;
125
126     var KEY_PGUP = 33;
127     var KEY_PGDOWN = 34;
128     var KEY_END = 35;
129     var KEY_HOME = 36;
130
131     var KEY_LEFT = 37;
132     var KEY_UP = 38;
133     var KEY_RIGHT = 39;
134     var KEY_DOWN = 40;
135
136     // We use document here instead of window to avoid a bug in jQuery on IE7
137     $(document).keydown(function(e) {
138         
139         // Keyboard navigation        
140         switch(e.keyCode) {
141             case KEY_PGUP:
142             case KEY_UP:            
143                 // In 1up mode page scrolling is handled by browser
144                 if (2 == self.mode) {
145                     self.prev();
146                 }
147                 break;
148             case KEY_DOWN:
149             case KEY_PGDOWN:
150                 if (2 == self.mode) {
151                     self.next();
152                 }
153                 break;
154             case KEY_END:
155                 self.end();
156                 break;
157             case KEY_HOME:
158                 self.home();
159                 break;
160             case KEY_LEFT:
161                 if (self.keyboardNavigationIsDisabled(e)) {
162                     break;
163                 }
164                 if (2 == self.mode) {
165                     self.left();
166                 }
167                 break;
168             case KEY_RIGHT:
169                 if (self.keyboardNavigationIsDisabled(e)) {
170                     break;
171                 }
172                 if (2 == self.mode) {
173                     self.right();
174                 }
175                 break;
176         }
177     });
178 }
179
180 // drawLeafs()
181 //______________________________________________________________________________
182 GnuBook.prototype.drawLeafs = function() {
183     if (1 == this.mode) {
184         this.drawLeafsOnePage();
185     } else {
186         this.drawLeafsTwoPage();
187     }
188 }
189
190
191 // drawLeafsOnePage()
192 //______________________________________________________________________________
193 GnuBook.prototype.drawLeafsOnePage = function() {
194     //alert('drawing leafs!');
195     this.timer = null;
196
197
198     var scrollTop = $('#GBcontainer').attr('scrollTop');
199     var scrollBottom = scrollTop + $('#GBcontainer').height();
200     //console.log('top=' + scrollTop + ' bottom='+scrollBottom);
201     
202     var leafsToDisplay = [];
203     
204     var i;
205     var leafTop = 0;
206     var leafBottom = 0;
207     for (i=0; i<this.numLeafs; i++) {
208         var height  = parseInt(this.getPageHeight(i)/this.reduce); 
209     
210         leafBottom += height;
211         //console.log('leafTop = '+leafTop+ ' pageH = ' + this.pageH[i] + 'leafTop>=scrollTop=' + (leafTop>=scrollTop));
212         var topInView    = (leafTop >= scrollTop) && (leafTop <= scrollBottom);
213         var bottomInView = (leafBottom >= scrollTop) && (leafBottom <= scrollBottom);
214         var middleInView = (leafTop <=scrollTop) && (leafBottom>=scrollBottom);
215         if (topInView | bottomInView | middleInView) {
216             //console.log('to display: ' + i);
217             leafsToDisplay.push(i);
218         }
219         leafTop += height +10;      
220         leafBottom += 10;
221     }
222
223     var firstLeafToDraw  = leafsToDisplay[0];
224     window.location.replace('#' + firstLeafToDraw);
225     this.firstIndex      = firstLeafToDraw;
226
227     if ((0 != firstLeafToDraw) && (1 < this.reduce)) {
228         firstLeafToDraw--;
229         leafsToDisplay.unshift(firstLeafToDraw);
230     }
231     
232     var lastLeafToDraw = leafsToDisplay[leafsToDisplay.length-1];
233     if ( ((this.numLeafs-1) != lastLeafToDraw) && (1 < this.reduce) ) {
234         leafsToDisplay.push(lastLeafToDraw+1);
235     }
236     
237     leafTop = 0;
238     var i;
239     for (i=0; i<firstLeafToDraw; i++) {
240         leafTop += parseInt(this.getPageHeight(i)/this.reduce) +10;
241     }
242
243     //var viewWidth = $('#GBpageview').width(); //includes scroll bar width
244     var viewWidth = $('#GBcontainer').attr('scrollWidth');
245
246
247     for (i=0; i<leafsToDisplay.length; i++) {
248         var leafNum = leafsToDisplay[i];    
249         var height  = parseInt(this.getPageHeight(leafNum)/this.reduce); 
250
251         if(-1 == jQuery.inArray(leafsToDisplay[i], this.displayedLeafs)) {            
252             var width   = parseInt(this.getPageWidth(leafNum)/this.reduce); 
253             //console.log("displaying leaf " + leafsToDisplay[i] + ' leafTop=' +leafTop);
254             var div = document.createElement("div");
255             div.className = 'GBpagediv1up';
256             div.id = 'pagediv'+leafNum;
257             div.style.position = "absolute";
258             $(div).css('top', leafTop + 'px');
259             var left = (viewWidth-width)>>1;
260             if (left<0) left = 0;
261             $(div).css('left', left+'px');
262             $(div).css('width', width+'px');
263             $(div).css('height', height+'px');
264             //$(div).text('loading...');
265             
266             $('#GBpageview').append(div);
267
268             var img = document.createElement("img");
269             img.src = this.getPageURI(leafNum);
270             $(img).css('width', width+'px');
271             $(img).css('height', height+'px');
272             $(div).append(img);
273
274         } else {
275             //console.log("not displaying " + leafsToDisplay[i] + ' score=' + jQuery.inArray(leafsToDisplay[i], this.displayedLeafs));            
276         }
277
278         leafTop += height +10;
279
280     }
281     
282     for (i=0; i<this.displayedLeafs.length; i++) {
283         if (-1 == jQuery.inArray(this.displayedLeafs[i], leafsToDisplay)) {
284             var leafNum = this.displayedLeafs[i];
285             //console.log('Removing leaf ' + leafNum);
286             //console.log('id='+'#pagediv'+leafNum+ ' top = ' +$('#pagediv'+leafNum).css('top'));
287             $('#pagediv'+leafNum).remove();
288         } else {
289             //console.log('NOT Removing leaf ' + this.displayedLeafs[i]);
290         }
291     }
292     
293     this.displayedLeafs = leafsToDisplay.slice();
294     this.updateSearchHilites();
295     
296     if (null != this.getPageNum(firstLeafToDraw))  {
297         $("#GBpagenum").val(this.getPageNum(firstLeafToDraw));
298     } else {
299         $("#GBpagenum").val('');
300     }
301 }
302
303 // drawLeafsTwoPage()
304 //______________________________________________________________________________
305 GnuBook.prototype.drawLeafsTwoPage = function() {
306     //alert('drawing two leafs!');
307
308     var scrollTop = $('#GBcontainer').attr('scrollTop');
309     var scrollBottom = scrollTop + $('#GBcontainer').height();
310     
311     //console.log('drawLeafsTwoPage: this.currrentLeafL ' + this.currentLeafL);
312     
313     var leafNum = this.currentLeafL;
314     var height  = this.getPageHeight(leafNum); 
315     var width   = this.getPageWidth(leafNum);
316     var handSide= this.getPageSide(leafNum);
317
318     var leafEdgeWidthL = this.leafEdgeWidth(leafNum);
319     var leafEdgeWidthR = this.twoPageEdgeW - leafEdgeWidthL;
320     var bookCoverDivWidth = this.twoPageW*2+20 + this.twoPageEdgeW;
321     var bookCoverDivLeft = ($('#GBcontainer').width() - bookCoverDivWidth) >> 1;
322     //console.log(leafEdgeWidthL);
323
324     var middle = ($('#GBcontainer').width() >> 1);            
325     var left = middle - this.twoPageW;
326     var top  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
327
328     var scaledW = parseInt(this.twoPageH*width/height);
329     left = 10+leafEdgeWidthL;
330     //var right = left+scaledW;
331     var right = $(this.twoPageDiv).width()-11-$(this.leafEdgeL).width()-scaledW;
332
333     var gutter = middle + this.gutterOffsetForIndex(this.currentLeafL);
334     
335     this.prefetchImg(leafNum);
336     $(this.prefetchedImgs[leafNum]).css({
337         position: 'absolute',
338         /*right:   gutter+'px',*/
339         left: gutter-scaledW+'px',
340         right: '',
341         top:    top+'px',
342         backgroundColor: 'rgb(234, 226, 205)',
343         height: this.twoPageH +'px',
344         width:  scaledW + 'px',
345         borderRight: '1px solid black',
346         zIndex: 2
347     }).appendTo('#GBcontainer');
348     //$('#GBcontainer').append(this.prefetchedImgs[leafNum]);
349
350
351     var leafNum = this.currentLeafR;
352     var height  = this.getPageHeight(leafNum); 
353     var width   = this.getPageWidth(leafNum);
354     //    var left = ($('#GBcontainer').width() >> 1);
355     left += scaledW;
356
357     var scaledW = this.twoPageH*width/height;
358     this.prefetchImg(leafNum);
359     $(this.prefetchedImgs[leafNum]).css({
360         position: 'absolute',
361         left:   gutter+'px',
362         right: '',
363         top:    top+'px',
364         backgroundColor: 'rgb(234, 226, 205)',
365         height: this.twoPageH + 'px',
366         width:  scaledW + 'px',
367         borderLeft: '1px solid black',
368         zIndex: 2
369     }).appendTo('#GBcontainer');
370     //$('#GBcontainer').append(this.prefetchedImgs[leafNum]);
371         
372
373     this.displayedLeafs = [this.currentLeafL, this.currentLeafR];
374     this.setClickHandlers();
375
376     this.updatePageNumBox2UP();
377 }
378
379 // updatePageNumBox2UP
380 //______________________________________________________________________________
381 GnuBook.prototype.updatePageNumBox2UP = function() {
382     if (null != this.getPageNum(this.currentLeafL))  {
383         $("#GBpagenum").val(this.getPageNum(this.currentLeafL));
384     } else {
385         $("#GBpagenum").val('');
386     }
387     window.location.replace('#' + this.currentLeafL); 
388 }
389
390 // loadLeafs()
391 //______________________________________________________________________________
392 GnuBook.prototype.loadLeafs = function() {
393
394
395     var self = this;
396     if (null == this.timer) {
397         this.timer=setTimeout(function(){self.drawLeafs()},250);
398     } else {
399         clearTimeout(this.timer);
400         this.timer=setTimeout(function(){self.drawLeafs()},250);    
401     }
402 }
403
404
405 // zoom1up()
406 //______________________________________________________________________________
407 GnuBook.prototype.zoom1up = function(dir) {
408     if (2 == this.mode) {     //can only zoom in 1-page mode
409         this.switchMode(1);
410         return;
411     }
412     
413     if (1 == dir) {
414         if (this.reduce <= 0.5) return;
415         this.reduce*=0.5;           //zoom in
416     } else {
417         if (this.reduce >= 8) return;
418         this.reduce*=2;             //zoom out
419     }
420     
421     this.resizePageView();
422
423     $('#GBpageview').empty()
424     this.displayedLeafs = [];
425     this.loadLeafs();
426     
427     $('#GBzoom').text(100/this.reduce);
428 }
429
430
431 // resizePageView()
432 //______________________________________________________________________________
433 GnuBook.prototype.resizePageView = function() {
434     var i;
435     var viewHeight = 0;
436     //var viewWidth  = $('#GBcontainer').width(); //includes scrollBar
437     var viewWidth  = $('#GBcontainer').attr('clientWidth');   
438
439     var oldScrollTop  = $('#GBcontainer').attr('scrollTop');
440     var oldViewHeight = $('#GBpageview').height();
441     if (0 != oldViewHeight) {
442         var scrollRatio = oldScrollTop / oldViewHeight;
443     } else {
444         var scrollRatio = 0;
445     }
446     
447     for (i=0; i<this.numLeafs; i++) {
448         viewHeight += parseInt(this.getPageHeight(i)/this.reduce) + this.padding; 
449         var width = parseInt(this.getPageWidth(i)/this.reduce);
450         if (width>viewWidth) viewWidth=width;
451     }
452     $('#GBpageview').height(viewHeight);
453     $('#GBpageview').width(viewWidth);    
454
455     $('#GBcontainer').attr('scrollTop', Math.floor(scrollRatio*viewHeight));
456     
457     this.centerPageView();
458     this.loadLeafs();
459     
460 }
461
462 // centerPageView()
463 //______________________________________________________________________________
464 GnuBook.prototype.centerPageView = function() {
465
466     var scrollWidth  = $('#GBcontainer').attr('scrollWidth');
467     var clientWidth  =  $('#GBcontainer').attr('clientWidth');
468     //console.log('sW='+scrollWidth+' cW='+clientWidth);
469     if (scrollWidth > clientWidth) {
470         $('#GBcontainer').attr('scrollLeft', (scrollWidth-clientWidth)/2);
471     }
472
473 }
474
475 // jumpToPage()
476 //______________________________________________________________________________
477 GnuBook.prototype.jumpToPage = function(pageNum) {
478
479     var i;
480     var foundPage = false;
481     var foundLeaf = null;
482     for (i=0; i<this.numLeafs; i++) {
483         if (this.getPageNum(i) == pageNum) {
484             foundPage = true;
485             foundLeaf = i;
486             break;
487         }
488     }
489     
490     if (foundPage) {
491         var leafTop = 0;
492         var h;
493         this.jumpToIndex(foundLeaf);
494         $('#GBcontainer').attr('scrollTop', leafTop);
495     } else {
496         alert('Page not found. This book might not have pageNumbers in scandata.');
497     }
498 }
499
500 // jumpToIndex()
501 //______________________________________________________________________________
502 GnuBook.prototype.jumpToIndex = function(index) {
503
504     if (2 == this.mode) {
505         this.autoStop();
506         
507         // By checking against min/max we do nothing if requested index
508         // is current
509         if (index < Math.min(this.currentLeafL, this.currentLeafR)) {
510             this.flipBackToIndex(index);
511         } else if (index > Math.max(this.currentLeafL, this.currentLeafR)) {
512             this.flipFwdToIndex(index);
513         }
514
515     } else {        
516         var i;
517         var leafTop = 0;
518         var h;
519         for (i=0; i<index; i++) {
520             h = parseInt(this.getPageHeight(i)/this.reduce); 
521             leafTop += h + this.padding;
522         }
523         //$('#GBcontainer').attr('scrollTop', leafTop);
524         $('#GBcontainer').animate({scrollTop: leafTop },'fast');    
525     }
526 }
527
528
529
530 // switchMode()
531 //______________________________________________________________________________
532 GnuBook.prototype.switchMode = function(mode) {
533
534     //console.log('  asked to switch to mode ' + mode + ' from ' + this.mode);
535     
536     if (mode == this.mode) return;
537
538     this.autoStop();
539     this.removeSearchHilites();
540
541     this.mode = mode;
542     
543     this.switchToolbarMode(mode);
544     
545     if (1 == mode) {
546         this.prepareOnePageView();
547     } else {
548         this.prepareTwoPageView();
549     }
550
551 }
552
553 //prepareOnePageView()
554 //______________________________________________________________________________
555 GnuBook.prototype.prepareOnePageView = function() {
556
557     var startLeaf = this.displayedLeafs[0];
558     
559     $('#GBcontainer').empty();
560     $('#GBcontainer').css({
561         overflowY: 'scroll',
562         overflowX: 'auto'
563     });
564     
565     $("#GBcontainer").append("<div id='GBpageview'></div>");
566     this.resizePageView();
567     this.jumpToIndex(startLeaf);
568     this.displayedLeafs = [];    
569     this.drawLeafsOnePage();
570     $('#GBzoom').text(100/this.reduce);    
571 }
572
573 // prepareTwoPageView()
574 //______________________________________________________________________________
575 GnuBook.prototype.prepareTwoPageView = function() {
576     $('#GBcontainer').empty();
577
578     // We want to display two facing pages.  We may be missing
579     // one side of the spread because it is the first/last leaf,
580     // foldouts, missing pages, etc
581
582     var targetLeaf = this.displayedLeafs[0];
583     
584     if (targetLeaf < this.firstDisplayableIndex()) {
585         targetLeaf = this.firstDisplayableIndex();
586     }
587     
588     if (targetLeaf > this.lastDisplayableIndex()) {
589         targetLeaf = this.lastDisplayableIndex();
590     }
591     
592     this.currentLeafL = null;
593     this.currentLeafR = null;
594     this.pruneUnusedImgs();
595     
596     var currentSpreadIndices = this.getSpreadIndices(targetLeaf);
597     this.currentLeafL = currentSpreadIndices[0];
598     this.currentLeafR = currentSpreadIndices[1];
599     
600     this.calculateSpreadSize(); //sets this.twoPageW, twoPageH, and twoPageRatio
601
602     // We want to minimize the unused space in two-up mode (maximize the amount of page
603     // shown).  We give width to the leaf edges and these widths change (though the sum
604     // of the two remains constant) as we flip through the book.  With the book
605     // cover centered and fixed in the GBcontainer div the page images will meet
606     // at the "gutter" which is generally offset from the center.
607     var middle = ($('#GBcontainer').width() >> 1); // Middle of the GBcontainer div
608     //var gutter = middle+parseInt((2*this.currentLeafL - this.numLeafs)*this.twoPageEdgeW/this.numLeafs/2);
609     
610     var gutter = middle + this.gutterOffsetForIndex(this.currentLeafL);
611     
612     var scaledWL = this.getPageWidth2UP(this.currentLeafL);
613     var scaledWR = this.getPageWidth2UP(this.currentLeafR);
614     var leafEdgeWidthL = this.leafEdgeWidth(this.currentLeafL);
615     var leafEdgeWidthR = this.twoPageEdgeW - leafEdgeWidthL;
616
617     //console.log('idealWidth='+idealWidth+' idealHeight='+idealHeight);
618     //var bookCoverDivWidth = this.twoPageW*2+20 + this.twoPageEdgeW;
619     
620     // The width of the book cover div.  The combined width of both pages, twice the width
621     // of the book cover internal padding (2*10) and the page edges
622     var bookCoverDivWidth = scaledWL + scaledWR + 20 + this.twoPageEdgeW;
623     
624     // The height of the book cover div
625     var bookCoverDivHeight = this.twoPageH+20;
626     
627     //var bookCoverDivLeft = ($('#GBcontainer').width() - bookCoverDivWidth) >> 1;
628     var bookCoverDivLeft = gutter-scaledWL-leafEdgeWidthL-10;
629     var bookCoverDivTop = ($('#GBcontainer').height() - bookCoverDivHeight) >> 1;
630     //console.log('bookCoverDivWidth='+bookCoverDivWidth+' bookCoverDivHeight='+bookCoverDivHeight+ ' bookCoverDivLeft='+bookCoverDivLeft+' bookCoverDivTop='+bookCoverDivTop);
631
632     this.twoPageDiv = document.createElement('div');
633     $(this.twoPageDiv).attr('id', 'book_div_1').css({
634         border: '1px solid rgb(68, 25, 17)',
635         width:  bookCoverDivWidth + 'px',
636         height: bookCoverDivHeight+'px',
637         visibility: 'visible',
638         position: 'absolute',
639         backgroundColor: '#663929',
640         left: bookCoverDivLeft + 'px',
641         top: bookCoverDivTop+'px',
642         MozBorderRadiusTopleft: '7px',
643         MozBorderRadiusTopright: '7px',
644         MozBorderRadiusBottomright: '7px',
645         MozBorderRadiusBottomleft: '7px'
646     }).appendTo('#GBcontainer');
647     //$('#GBcontainer').append('<div id="book_div_1" style="border: 1px solid rgb(68, 25, 17); width: ' + bookCoverDivWidth + 'px; height: '+bookCoverDivHeight+'px; visibility: visible; position: absolute; background-color: rgb(136, 51, 34); left: ' + bookCoverDivLeft + 'px; top: '+bookCoverDivTop+'px; -moz-border-radius-topleft: 7px; -moz-border-radius-topright: 7px; -moz-border-radius-bottomright: 7px; -moz-border-radius-bottomleft: 7px;"/>');
648
649
650     var height  = this.getPageHeight(this.currentLeafR); 
651     var width   = this.getPageWidth(this.currentLeafR);    
652     var scaledW = this.twoPageH*width/height;
653     
654     this.leafEdgeR = document.createElement('div');
655     this.leafEdgeR.className = 'leafEdgeR';
656     $(this.leafEdgeR).css({
657         borderStyle: 'solid solid solid none',
658         borderColor: 'rgb(51, 51, 34)',
659         borderWidth: '1px 1px 1px 0px',
660         background: 'transparent url(images/right_edges.png) repeat scroll 0% 0%',
661         width: leafEdgeWidthR + 'px',
662         height: this.twoPageH-1 + 'px',
663         /*right: '10px',*/
664         left: gutter+scaledW+'px',
665         top: bookCoverDivTop+10+'px',
666         position: 'absolute'
667     }).appendTo('#GBcontainer');
668     
669     this.leafEdgeL = document.createElement('div');
670     this.leafEdgeL.className = 'leafEdgeL';
671     $(this.leafEdgeL).css({
672         borderStyle: 'solid none solid solid',
673         borderColor: 'rgb(51, 51, 34)',
674         borderWidth: '1px 0px 1px 1px',
675         background: 'transparent url(images/left_edges.png) repeat scroll 0% 0%',
676         width: leafEdgeWidthL + 'px',
677         height: this.twoPageH-1 + 'px',
678         left: bookCoverDivLeft+10+'px',
679         top: bookCoverDivTop+10+'px',    
680         position: 'absolute'
681     }).appendTo('#GBcontainer');
682
683
684
685     bookCoverDivWidth = 30;
686     bookCoverDivHeight = this.twoPageH+20;
687     bookCoverDivLeft = ($('#GBcontainer').width() - bookCoverDivWidth) >> 1;
688     bookCoverDivTop = ($('#GBcontainer').height() - bookCoverDivHeight) >> 1;
689
690     div = document.createElement('div');
691     $(div).attr('id', 'book_div_2').css({
692         border:          '1px solid rgb(68, 25, 17)',
693         width:           bookCoverDivWidth+'px',
694         height:          bookCoverDivHeight+'px',
695         position:        'absolute',
696         backgroundColor: 'rgb(68, 25, 17)',
697         left:            bookCoverDivLeft+'px',
698         top:             bookCoverDivTop+'px'
699     }).appendTo('#GBcontainer');
700     //$('#GBcontainer').append('<div id="book_div_2" style="border: 1px solid rgb(68, 25, 17); width: '+bookCoverDivWidth+'px; height: '+bookCoverDivHeight+'px; visibility: visible; position: absolute; background-color: rgb(68, 25, 17); left: '+bookCoverDivLeft+'px; top: '+bookCoverDivTop+'px;"/>');
701
702     bookCoverDivWidth = this.twoPageW*2;
703     bookCoverDivHeight = this.twoPageH;
704     bookCoverDivLeft = ($('#GBcontainer').width() - bookCoverDivWidth) >> 1;
705     bookCoverDivTop = ($('#GBcontainer').height() - bookCoverDivHeight) >> 1;
706
707
708     this.prepareTwoPagePopUp();
709
710     this.displayedLeafs = [];
711     
712     //this.leafsToDisplay=[firstLeaf, firstLeaf+1];
713     //console.log('leafsToDisplay: ' + this.leafsToDisplay[0] + ' ' + this.leafsToDisplay[1]);
714     
715     this.drawLeafsTwoPage();
716     this.updateSearchHilites2UP();
717     
718     this.prefetch();
719     $('#GBzoom').text((100*this.twoPageH/this.getPageHeight(this.currentLeafL)).toString().substr(0,4));
720 }
721
722 // prepareTwoPagePopUp()
723 //
724 // This function prepares the "View leaf n" popup that shows while the mouse is
725 // over the left/right "stack of sheets" edges.  It also binds the mouse
726 // events for these divs.
727 //______________________________________________________________________________
728 GnuBook.prototype.prepareTwoPagePopUp = function() {
729     this.twoPagePopUp = document.createElement('div');
730     $(this.twoPagePopUp).css({
731         border: '1px solid black',
732         padding: '2px 6px',
733         position: 'absolute',
734         fontFamily: 'sans-serif',
735         fontSize: '14px',
736         zIndex: '1000',
737         backgroundColor: 'rgb(255, 255, 238)',
738         opacity: 0.85
739     }).appendTo('#GBcontainer');
740     $(this.twoPagePopUp).hide();
741     
742     $(this.leafEdgeL).add(this.leafEdgeR).bind('mouseenter', this, function(e) {
743         $(e.data.twoPagePopUp).show();
744     });
745
746     $(this.leafEdgeL).add(this.leafEdgeR).bind('mouseleave', this, function(e) {
747         $(e.data.twoPagePopUp).hide();
748     });
749
750     $(this.leafEdgeL).bind('click', this, function(e) { 
751         e.data.autoStop();
752         var jumpIndex = e.data.jumpIndexForLeftEdgePageX(e.pageX);
753         e.data.jumpToIndex(jumpIndex);
754     });
755
756     $(this.leafEdgeR).bind('click', this, function(e) { 
757         e.data.autoStop();
758         var jumpIndex = e.data.jumpIndexForRightEdgePageX(e.pageX);
759         e.data.jumpToIndex(jumpIndex);    
760     });
761
762     $(this.leafEdgeR).bind('mousemove', this, function(e) {
763
764         var jumpLeaf = e.data.jumpIndexForRightEdgePageX(e.pageX);
765         $(e.data.twoPagePopUp).text('View Leaf '+jumpLeaf);
766         
767         $(e.data.twoPagePopUp).css({
768             left: e.pageX +5+ 'px',
769             top: e.pageY-$('#GBcontainer').offset().top+ 'px'
770         });
771     });
772
773     $(this.leafEdgeL).bind('mousemove', this, function(e) {
774     
775         var jumpLeaf = e.data.jumpIndexForLeftEdgePageX(e.pageX);
776         $(e.data.twoPagePopUp).text('View Leaf '+jumpLeaf);
777         
778         $(e.data.twoPagePopUp).css({
779             left: e.pageX - $(e.data.twoPagePopUp).width() - 30 + 'px',
780             top: e.pageY-$('#GBcontainer').offset().top+ 'px'
781         });
782     });
783 }
784
785 // calculateSpreadSize()
786 //______________________________________________________________________________
787 // Calculates 2-page spread dimensions based on this.currentLeafL and
788 // this.currentLeafR
789 // This function sets this.twoPageH, twoPageW, and twoPageRatio
790
791 GnuBook.prototype.calculateSpreadSize = function() {
792     var firstLeaf  = this.currentLeafL;
793     var secondLeaf = this.currentLeafR;
794     //console.log('first page is ' + firstLeaf);
795
796     var canon5Dratio = 1.5;
797     
798     var firstLeafRatio  = this.getPageHeight(firstLeaf) / this.getPageWidth(firstLeaf);
799     var secondLeafRatio = this.getPageHeight(secondLeaf) / this.getPageWidth(secondLeaf);
800     //console.log('firstLeafRatio = ' + firstLeafRatio + ' secondLeafRatio = ' + secondLeafRatio);
801
802     var ratio;
803     if (Math.abs(firstLeafRatio - canon5Dratio) < Math.abs(secondLeafRatio - canon5Dratio)) {
804         ratio = firstLeafRatio;
805         //console.log('using firstLeafRatio ' + ratio);
806     } else {
807         ratio = secondLeafRatio;
808         //console.log('using secondLeafRatio ' + ratio);
809     }
810
811     var totalLeafEdgeWidth = parseInt(this.numLeafs * 0.1);
812     var maxLeafEdgeWidth   = parseInt($('#GBcontainer').width() * 0.1);
813     totalLeafEdgeWidth     = Math.min(totalLeafEdgeWidth, maxLeafEdgeWidth);
814     
815     $('#GBcontainer').css('overflow', 'hidden');
816
817     var idealWidth  = ($('#GBcontainer').width() - 30 - totalLeafEdgeWidth)>>1;
818     var idealHeight = $('#GBcontainer').height() - 30;
819     //console.log('init idealWidth='+idealWidth+' idealHeight='+idealHeight + ' ratio='+ratio);
820
821     if (idealHeight/ratio <= idealWidth) {
822         //use height
823         idealWidth = parseInt(idealHeight/ratio);
824     } else {
825         //use width
826         idealHeight = parseInt(idealWidth*ratio);
827     }
828
829     this.twoPageH     = idealHeight;
830     this.twoPageW     = idealWidth;
831     this.twoPageRatio = ratio;
832     this.twoPageEdgeW = totalLeafEdgeWidth; // The combined width of both edges
833
834 }
835
836 // right()
837 //______________________________________________________________________________
838 // Flip the right page over onto the left
839 GnuBook.prototype.right = function() {
840     if ('rl' != this.pageProgression) {
841         // LTR
842         gb.next();
843     } else {
844         // RTL
845         gb.prev();
846     }
847 }
848
849 // left()
850 //______________________________________________________________________________
851 // Flip the left page over onto the right.
852 GnuBook.prototype.left = function() {
853     if ('rl' != this.pageProgression) {
854         // LTR
855         gb.prev();
856     } else {
857         // RTL
858         gb.next();
859     }
860 }
861
862 // next()
863 //______________________________________________________________________________
864 GnuBook.prototype.next = function() {
865     if (2 == this.mode) {
866         this.autoStop();
867         this.flipFwdToIndex(null);
868     } else {
869         if (this.firstIndex < this.lastDisplayableIndex()) {
870             this.jumpToIndex(this.firstIndex+1);
871         }
872     }
873 }
874
875 // prev()
876 //______________________________________________________________________________
877 GnuBook.prototype.prev = function() {
878     if (2 == this.mode) {
879         this.autoStop();
880         this.flipBackToIndex(null);
881     } else {
882         if (this.firstIndex >= 1) {
883             this.jumpToIndex(this.firstIndex-1);
884         }    
885     }
886 }
887
888 GnuBook.prototype.home = function() {
889     if (2 == this.mode) {
890         this.jumpToIndex(2);
891     }
892     else {
893         this.jumpToIndex(0);
894     }
895 }
896
897 GnuBook.prototype.end = function() {
898     if (2 == this.mode) {
899         this.jumpToIndex(this.lastDisplayableIndex());
900     }
901     else {
902         this.jumpToIndex(this.lastDisplayableIndex());
903     }
904 }
905
906 // flipBackToIndex()
907 //______________________________________________________________________________
908 // to flip back one spread, pass index=null
909 GnuBook.prototype.flipBackToIndex = function(index) {
910     if (1 == this.mode) return;
911
912     var leftIndex = this.currentLeafL;
913     
914     // $$$ Need to change this to be able to see first spread.
915     //     See https://bugs.launchpad.net/gnubook/+bug/296788
916     if (leftIndex <= 2) return;
917     if (this.animating) return;
918
919     if (null != this.leafEdgeTmp) {
920         alert('error: leafEdgeTmp should be null!');
921         return;
922     }
923     
924     if (null == index) {
925         index = leftIndex-2;
926     }
927     //if (index<0) return;
928     
929     var previousIndices = this.getSpreadIndices(index);
930     
931     if (previousIndices[0] < 0 || previousIndices[1] < 0) {
932         return;
933     }
934     
935     //console.log("flipping back to " + previousIndices[0] + ',' + previousIndices[1]);
936
937     this.animating = true;
938     
939     if ('rl' != this.pageProgression) {
940         // Assume LTR and we are going backward    
941         var gutter = this.prepareFlipLeftToRight(previousIndices[0], previousIndices[1]);        
942         this.flipLeftToRight(previousIndices[0], previousIndices[1], gutter);
943         
944     } else {
945         // RTL and going backward
946         var gutter = this.prepareFlipRightToLeft(previousIndices[0], previousIndices[1]);
947         this.flipRightToLeft(previousIndices[0], previousIndices[1], gutter);
948     }
949 }
950
951 // flipLeftToRight()
952 //______________________________________________________________________________
953 // Flips the page on the left towards the page on the right
954 GnuBook.prototype.flipLeftToRight = function(newIndexL, newIndexR, gutter) {
955
956     var leftLeaf = this.currentLeafL;
957     
958     var oldLeafEdgeWidthL = this.leafEdgeWidth(this.currentLeafL);
959     var newLeafEdgeWidthL = this.leafEdgeWidth(newIndexL);    
960     var leafEdgeTmpW = oldLeafEdgeWidthL - newLeafEdgeWidthL;
961     
962     var currWidthL   = this.getPageWidth2UP(leftLeaf);
963     var newWidthL    = this.getPageWidth2UP(newIndexL);
964     var newWidthR    = this.getPageWidth2UP(newIndexR);
965
966     var top  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
967
968     //console.log('leftEdgeTmpW ' + leafEdgeTmpW);
969     //console.log('  gutter ' + gutter + ', scaledWL ' + scaledWL + ', newLeafEdgeWL ' + newLeafEdgeWidthL);
970     
971     //animation strategy:
972     // 0. remove search highlight, if any.
973     // 1. create a new div, called leafEdgeTmp to represent the leaf edge between the leftmost edge 
974     //    of the left leaf and where the user clicked in the leaf edge.
975     //    Note that if this function was triggered by left() and not a
976     //    mouse click, the width of leafEdgeTmp is very small (zero px).
977     // 2. animate both leafEdgeTmp to the gutter (without changing its width) and animate
978     //    leftLeaf to width=0.
979     // 3. When step 2 is finished, animate leafEdgeTmp to right-hand side of new right leaf
980     //    (left=gutter+newWidthR) while also animating the new right leaf from width=0 to
981     //    its new full width.
982     // 4. After step 3 is finished, do the following:
983     //      - remove leafEdgeTmp from the dom.
984     //      - resize and move the right leaf edge (leafEdgeR) to left=gutter+newWidthR
985     //          and width=twoPageEdgeW-newLeafEdgeWidthL.
986     //      - resize and move the left leaf edge (leafEdgeL) to left=gutter-newWidthL-newLeafEdgeWidthL
987     //          and width=newLeafEdgeWidthL.
988     //      - resize the back cover (twoPageDiv) to left=gutter-newWidthL-newLeafEdgeWidthL-10
989     //          and width=newWidthL+newWidthR+twoPageEdgeW+20
990     //      - move new left leaf (newIndexL) forward to zindex=2 so it can receive clicks.
991     //      - remove old left and right leafs from the dom [pruneUnusedImgs()].
992     //      - prefetch new adjacent leafs.
993     //      - set up click handlers for both new left and right leafs.
994     //      - redraw the search highlight.
995     //      - update the pagenum box and the url.
996     
997     
998     var leftEdgeTmpLeft = gutter - currWidthL - leafEdgeTmpW;
999
1000     this.leafEdgeTmp = document.createElement('div');
1001     $(this.leafEdgeTmp).css({
1002         borderStyle: 'solid none solid solid',
1003         borderColor: 'rgb(51, 51, 34)',
1004         borderWidth: '1px 0px 1px 1px',
1005         background: 'transparent url(images/left_edges.png) repeat scroll 0% 0%',
1006         width: leafEdgeTmpW + 'px',
1007         height: this.twoPageH-1 + 'px',
1008         left: leftEdgeTmpLeft + 'px',
1009         top: top+'px',    
1010         position: 'absolute',
1011         zIndex:1000
1012     }).appendTo('#GBcontainer');
1013     
1014     //$(this.leafEdgeL).css('width', newLeafEdgeWidthL+'px');
1015     $(this.leafEdgeL).css({
1016         width: newLeafEdgeWidthL+'px', 
1017         left: gutter-currWidthL-newLeafEdgeWidthL+'px'
1018     });   
1019
1020     // Left gets the offset of the current left leaf from the document
1021     var left = $(this.prefetchedImgs[leftLeaf]).offset().left;
1022     // $$$ This seems very similar to the gutter.  May be able to consolidate the logic.
1023     var right = $('#GBcontainer').width()-left-$(this.prefetchedImgs[leftLeaf]).width()+$('#GBcontainer').offset().left-2+'px';
1024     // We change the left leaf to right positioning
1025     $(this.prefetchedImgs[leftLeaf]).css({
1026         right: right,
1027         left: ''
1028     });
1029
1030      left = $(this.prefetchedImgs[leftLeaf]).offset().left - $('#book_div_1').offset().left;
1031      
1032      right = left+$(this.prefetchedImgs[leftLeaf]).width()+'px';
1033
1034     $(this.leafEdgeTmp).animate({left: gutter}, this.flipSpeed, 'easeInSine');    
1035     //$(this.prefetchedImgs[leftLeaf]).animate({width: '0px'}, 'slow', 'easeInSine');
1036     
1037     var self = this;
1038
1039     this.removeSearchHilites();
1040
1041     //console.log('animating leafLeaf ' + leftLeaf + ' to 0px');
1042     $(this.prefetchedImgs[leftLeaf]).animate({width: '0px'}, self.flipSpeed, 'easeInSine', function() {
1043     
1044         //console.log('     and now leafEdgeTmp to left: gutter+newWidthR ' + (gutter + newWidthR));
1045         $(self.leafEdgeTmp).animate({left: gutter+newWidthR+'px'}, self.flipSpeed, 'easeOutSine');
1046
1047         //console.log('  animating newIndexR ' + newIndexR + ' to ' + newWidthR + ' from ' + $(self.prefetchedImgs[newIndexR]).width());
1048         $(self.prefetchedImgs[newIndexR]).animate({width: newWidthR+'px'}, self.flipSpeed, 'easeOutSine', function() {
1049             $(self.prefetchedImgs[newIndexL]).css('zIndex', 2);
1050
1051             $(self.leafEdgeR).css({
1052                 // Moves the right leaf edge
1053                 width: self.twoPageEdgeW-newLeafEdgeWidthL+'px',
1054                 left:  gutter+newWidthR+'px'
1055             });
1056
1057             $(self.leafEdgeL).css({
1058                 // Moves and resizes the left leaf edge
1059                 width: newLeafEdgeWidthL+'px',
1060                 left:  gutter-newWidthL-newLeafEdgeWidthL+'px'
1061             });
1062
1063             
1064             $(self.twoPageDiv).css({
1065                 // Resizes the brown border div
1066                 width: newWidthL+newWidthR+self.twoPageEdgeW+20+'px',
1067                 left: gutter-newWidthL-newLeafEdgeWidthL-10+'px'
1068             });
1069             
1070             $(self.leafEdgeTmp).remove();
1071             self.leafEdgeTmp = null;
1072             
1073             self.currentLeafL = newIndexL;
1074             self.currentLeafR = newIndexR;
1075             self.displayedLeafs = [newIndexL, newIndexR];
1076             self.setClickHandlers();
1077             self.pruneUnusedImgs();
1078             self.prefetch();
1079             self.animating = false;
1080             
1081             self.updateSearchHilites2UP();
1082             self.updatePageNumBox2UP();
1083             //$('#GBzoom').text((self.twoPageH/self.getPageHeight(newIndexL)).toString().substr(0,4));            
1084         });
1085     });        
1086     
1087 }
1088
1089 // flipFwdToIndex()
1090 //______________________________________________________________________________
1091 // Whether we flip left or right is dependent on the page progression
1092 // to flip forward one spread, pass index=null
1093 GnuBook.prototype.flipFwdToIndex = function(index) {
1094
1095     if (this.animating) return;
1096
1097     if (null != this.leafEdgeTmp) {
1098         alert('error: leafEdgeTmp should be null!');
1099         return;
1100     }
1101
1102     if (null == index) {
1103         index = this.currentLeafR+2; // $$$ assumes indices are continuous
1104     }
1105     if (index > this.lastDisplayableIndex()) return;
1106
1107     this.animating = true;
1108     
1109     var nextIndices = this.getSpreadIndices(index);
1110     
1111     //console.log('flipfwd to indices ' + nextIndices[0] + ',' + nextIndices[1]);
1112
1113     if ('rl' != this.pageProgression) {
1114         // We did not specify RTL
1115         var gutter = this.prepareFlipRightToLeft(nextIndices[0], nextIndices[1]);
1116         this.flipRightToLeft(nextIndices[0], nextIndices[1], gutter);
1117     } else {
1118         // RTL
1119         var gutter = this.prepareFlipLeftToRight(nextIndices[0], nextIndices[1]);
1120         this.flipLeftToRight(nextIndices[0], nextIndices[1], gutter);
1121     }
1122 }
1123
1124 // flipRightToLeft(nextL, nextR, gutter)
1125 // $$$ better not to have to pass gutter in
1126 //______________________________________________________________________________
1127 // Flip from left to right and show the nextL and nextR indices on those sides
1128 GnuBook.prototype.flipRightToLeft = function(newIndexL, newIndexR, gutter) {
1129     var oldLeafEdgeWidthL = this.leafEdgeWidth(this.currentLeafL);
1130     var oldLeafEdgeWidthR = this.twoPageEdgeW-oldLeafEdgeWidthL;
1131     var newLeafEdgeWidthL = this.leafEdgeWidth(newIndexL);  
1132     var newLeafEdgeWidthR = this.twoPageEdgeW-newLeafEdgeWidthL;
1133
1134     var leafEdgeTmpW = oldLeafEdgeWidthR - newLeafEdgeWidthR;
1135
1136     var top  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
1137
1138     var scaledW = this.getPageWidth2UP(this.currentLeafR);
1139
1140     var middle     = ($('#GBcontainer').width() >> 1);
1141     var currGutter = middle + this.gutterOffsetForIndex(this.currentLeafL);
1142
1143     this.leafEdgeTmp = document.createElement('div');
1144     $(this.leafEdgeTmp).css({
1145         borderStyle: 'solid none solid solid',
1146         borderColor: 'rgb(51, 51, 34)',
1147         borderWidth: '1px 0px 1px 1px',
1148         background: 'transparent url(images/left_edges.png) repeat scroll 0% 0%',
1149         width: leafEdgeTmpW + 'px',
1150         height: this.twoPageH-1 + 'px',
1151         left: currGutter+scaledW+'px',
1152         top: top+'px',    
1153         position: 'absolute',
1154         zIndex:1000
1155     }).appendTo('#GBcontainer');
1156
1157     //var scaledWR = this.getPageWidth2UP(newIndexR); // $$$ should be current instead?
1158     //var scaledWL = this.getPageWidth2UP(newIndexL); // $$$ should be current instead?
1159     
1160     var currWidthL = this.getPageWidth2UP(this.currentLeafL);
1161     var currWidthR = this.getPageWidth2UP(this.currentLeafR);
1162     var newWidthL = this.getPageWidth2UP(newIndexL);
1163     var newWidthR = this.getPageWidth2UP(newIndexR);
1164
1165     $(this.leafEdgeR).css({width: newLeafEdgeWidthR+'px', left: gutter+newWidthR+'px' });
1166
1167     var self = this; // closure-tastic!
1168
1169     var speed = this.flipSpeed;
1170
1171     this.removeSearchHilites();
1172     
1173     $(this.leafEdgeTmp).animate({left: gutter}, speed, 'easeInSine');    
1174     $(this.prefetchedImgs[this.currentLeafR]).animate({width: '0px'}, speed, 'easeInSine', function() {
1175         $(self.leafEdgeTmp).animate({left: gutter-newWidthL-leafEdgeTmpW+'px'}, speed, 'easeOutSine');    
1176         $(self.prefetchedImgs[newIndexL]).animate({width: newWidthL+'px'}, speed, 'easeOutSine', function() {
1177             $(self.prefetchedImgs[newIndexR]).css('zIndex', 2);
1178
1179             $(self.leafEdgeL).css({
1180                 width: newLeafEdgeWidthL+'px', 
1181                 left: gutter-newWidthL-newLeafEdgeWidthL+'px'
1182             });
1183             
1184             $(self.twoPageDiv).css({
1185                 width: newWidthL+newWidthR+self.twoPageEdgeW+20+'px',
1186                 left: gutter-newWidthL-newLeafEdgeWidthL-10+'px'
1187             });
1188             
1189             $(self.leafEdgeTmp).remove();
1190             self.leafEdgeTmp = null;
1191             
1192             self.currentLeafL = newIndexL;
1193             self.currentLeafR = newIndexR;
1194             self.displayedLeafs = [newIndexL, newIndexR];
1195             self.setClickHandlers();            
1196             self.pruneUnusedImgs();
1197             self.prefetch();
1198             self.animating = false;
1199
1200             self.updateSearchHilites2UP();
1201             self.updatePageNumBox2UP();
1202             //$('#GBzoom').text((self.twoPageH/self.getPageHeight(newIndexL)).toString().substr(0,4));
1203         });
1204     });    
1205 }
1206
1207 // setClickHandlers
1208 //______________________________________________________________________________
1209 GnuBook.prototype.setClickHandlers = function() {
1210     var self = this;
1211     $(this.prefetchedImgs[this.currentLeafL]).click(function() {
1212         //self.prevPage();
1213         self.autoStop();
1214         self.left();
1215     });
1216     $(this.prefetchedImgs[this.currentLeafR]).click(function() {
1217         //self.nextPage();'
1218         self.autoStop();
1219         self.right();        
1220     });
1221 }
1222
1223 // prefetchImg()
1224 //______________________________________________________________________________
1225 GnuBook.prototype.prefetchImg = function(leafNum) {
1226     if (undefined == this.prefetchedImgs[leafNum]) {    
1227         //console.log('prefetching ' + leafNum);
1228         var img = document.createElement("img");
1229         img.src = this.getPageURI(leafNum);
1230         this.prefetchedImgs[leafNum] = img;
1231     }
1232 }
1233
1234
1235 // prepareFlipLeftToRight()
1236 //
1237 //______________________________________________________________________________
1238 //
1239 // Prepare to flip the left page towards the right.  This corresponds to moving
1240 // backward when the page progression is left to right.
1241 GnuBook.prototype.prepareFlipLeftToRight = function(prevL, prevR) {
1242
1243     //console.log('  preparing left->right for ' + prevL + ',' + prevR);
1244
1245     this.prefetchImg(prevL);
1246     this.prefetchImg(prevR);
1247     
1248     var height  = this.getPageHeight(prevL); 
1249     var width   = this.getPageWidth(prevL);    
1250     var middle = ($('#GBcontainer').width() >> 1);
1251     var top  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
1252     var scaledW = this.twoPageH*width/height;
1253
1254     // The gutter is the dividing line between the left and right pages.
1255     // It is offset from the middle to create the illusion of thickness to the pages
1256     var gutter = middle + this.gutterOffsetForIndex(prevL);
1257     
1258     //console.log('    gutter for ' + prevL + ' is ' + gutter);
1259     //console.log('    prevL.left: ' + (gutter - scaledW) + 'px');
1260     //console.log('    changing prevL ' + prevL + ' to left: ' + (gutter-scaledW) + ' width: ' + scaledW);
1261     
1262     $(this.prefetchedImgs[prevL]).css({
1263         position: 'absolute',
1264         /*right:   middle+'px',*/
1265         left: gutter-scaledW+'px',
1266         right: '',
1267         top:    top+'px',
1268         backgroundColor: 'rgb(234, 226, 205)',
1269         height: this.twoPageH,
1270         width:  scaledW+'px',
1271         borderRight: '1px solid black',
1272         zIndex: 1
1273     });
1274
1275     $('#GBcontainer').append(this.prefetchedImgs[prevL]);
1276
1277     //console.log('    changing prevR ' + prevR + ' to left: ' + gutter + ' width: 0');
1278
1279     $(this.prefetchedImgs[prevR]).css({
1280         position: 'absolute',
1281         left:   gutter+'px',
1282         right: '',
1283         top:    top+'px',
1284         backgroundColor: 'rgb(234, 226, 205)',
1285         height: this.twoPageH,
1286         width:  '0px',
1287         borderLeft: '1px solid black',
1288         zIndex: 2
1289     });
1290
1291     $('#GBcontainer').append(this.prefetchedImgs[prevR]);
1292
1293
1294     return gutter;
1295             
1296 }
1297
1298 // prepareFlipRightToLeft()
1299 //______________________________________________________________________________
1300 GnuBook.prototype.prepareFlipRightToLeft = function(nextL, nextR) {
1301
1302     //console.log('  preparing left<-right for ' + nextL + ',' + nextR);
1303
1304     this.prefetchImg(nextL);
1305     this.prefetchImg(nextR);
1306
1307     var height  = this.getPageHeight(nextR); 
1308     var width   = this.getPageWidth(nextR);    
1309     var middle = ($('#GBcontainer').width() >> 1);
1310     var top  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
1311     var scaledW = this.twoPageH*width/height;
1312
1313     var gutter = middle + this.gutterOffsetForIndex(nextL);
1314     
1315     //console.log('right to left to %d gutter is %d', nextL, gutter);
1316     
1317     //console.log(' prepareRTL changing nextR ' + nextR + ' to left: ' + gutter);
1318     $(this.prefetchedImgs[nextR]).css({
1319         position: 'absolute',
1320         left:   gutter+'px',
1321         top:    top+'px',
1322         backgroundColor: 'rgb(234, 226, 205)',
1323         height: this.twoPageH,
1324         width:  scaledW+'px',
1325         borderLeft: '1px solid black',
1326         zIndex: 1
1327     });
1328
1329     $('#GBcontainer').append(this.prefetchedImgs[nextR]);
1330
1331     height  = this.getPageHeight(nextL); 
1332     width   = this.getPageWidth(nextL);      
1333     scaledW = this.twoPageH*width/height;
1334
1335     //console.log(' prepareRTL changing nextL ' + nextL + ' to right: ' + $('#GBcontainer').width()-gutter);
1336     $(this.prefetchedImgs[nextL]).css({
1337         position: 'absolute',
1338         right:   $('#GBcontainer').width()-gutter+'px',
1339         top:    top+'px',
1340         backgroundColor: 'rgb(234, 226, 205)',
1341         height: this.twoPageH,
1342         width:  0+'px',
1343         borderRight: '1px solid black',
1344         zIndex: 2
1345     });
1346
1347     $('#GBcontainer').append(this.prefetchedImgs[nextL]);    
1348
1349     return gutter;
1350             
1351 }
1352
1353 // getNextLeafs() -- NOT RTL AWARE
1354 //______________________________________________________________________________
1355 // GnuBook.prototype.getNextLeafs = function(o) {
1356 //     //TODO: we might have two left or two right leafs in a row (damaged book)
1357 //     //For now, assume that leafs are contiguous.
1358 //     
1359 //     //return [this.currentLeafL+2, this.currentLeafL+3];
1360 //     o.L = this.currentLeafL+2;
1361 //     o.R = this.currentLeafL+3;
1362 // }
1363
1364 // getprevLeafs() -- NOT RTL AWARE
1365 //______________________________________________________________________________
1366 // GnuBook.prototype.getPrevLeafs = function(o) {
1367 //     //TODO: we might have two left or two right leafs in a row (damaged book)
1368 //     //For now, assume that leafs are contiguous.
1369 //     
1370 //     //return [this.currentLeafL-2, this.currentLeafL-1];
1371 //     o.L = this.currentLeafL-2;
1372 //     o.R = this.currentLeafL-1;
1373 // }
1374
1375 // pruneUnusedImgs()
1376 //______________________________________________________________________________
1377 GnuBook.prototype.pruneUnusedImgs = function() {
1378     //console.log('current: ' + this.currentLeafL + ' ' + this.currentLeafR);
1379     for (var key in this.prefetchedImgs) {
1380         //console.log('key is ' + key);
1381         if ((key != this.currentLeafL) && (key != this.currentLeafR)) {
1382             //console.log('removing key '+ key);
1383             $(this.prefetchedImgs[key]).remove();
1384         }
1385         if ((key < this.currentLeafL-4) || (key > this.currentLeafR+4)) {
1386             //console.log('deleting key '+ key);
1387             delete this.prefetchedImgs[key];
1388         }
1389     }
1390 }
1391
1392 // prefetch()
1393 //______________________________________________________________________________
1394 GnuBook.prototype.prefetch = function() {
1395
1396     var lim = this.currentLeafL-4;
1397     var i;
1398     lim = Math.max(lim, 0);
1399     for (i = lim; i < this.currentLeafL; i++) {
1400         this.prefetchImg(i);
1401     }
1402     
1403     if (this.numLeafs > (this.currentLeafR+1)) {
1404         lim = Math.min(this.currentLeafR+4, this.numLeafs-1);
1405         for (i=this.currentLeafR+1; i<=lim; i++) {
1406             this.prefetchImg(i);
1407         }
1408     }
1409 }
1410
1411 // getPageWidth2UP()
1412 //______________________________________________________________________________
1413 GnuBook.prototype.getPageWidth2UP = function(index) {
1414     var height  = this.getPageHeight(index); 
1415     var width   = this.getPageWidth(index);    
1416     return Math.floor(this.twoPageH*width/height);
1417 }    
1418
1419 // search()
1420 //______________________________________________________________________________
1421 GnuBook.prototype.search = function(term) {
1422     $('#GnuBookSearchScript').remove();
1423         var script  = document.createElement("script");
1424         script.setAttribute('id', 'GnuBookSearchScript');
1425         script.setAttribute("type", "text/javascript");
1426         script.setAttribute("src", 'http://'+this.server+'/GnuBook/flipbook_search_gb.php?url='+escape(this.bookPath+'/'+this.bookId+'_djvu.xml')+'&term='+term+'&format=XML&callback=gb.GBSearchCallback');
1427         document.getElementsByTagName('head')[0].appendChild(script);
1428 }
1429
1430 // GBSearchCallback()
1431 //______________________________________________________________________________
1432 GnuBook.prototype.GBSearchCallback = function(txt) {
1433     //alert(txt);
1434     if (jQuery.browser.msie) {
1435         var dom=new ActiveXObject("Microsoft.XMLDOM");
1436         dom.async="false";
1437         dom.loadXML(txt);    
1438     } else {
1439         var parser = new DOMParser();
1440         var dom = parser.parseFromString(txt, "text/xml");    
1441     }
1442     
1443     $('#GnuBookSearchResults').empty();    
1444     $('#GnuBookSearchResults').append('<ul>');
1445     
1446     for (var key in this.searchResults) {
1447         if (null != this.searchResults[key].div) {
1448             $(this.searchResults[key].div).remove();
1449         }
1450         delete this.searchResults[key];
1451     }
1452     
1453     var pages = dom.getElementsByTagName('PAGE');
1454     
1455     if (0 == pages.length) {
1456         // $$$ it would be nice to echo the (sanitized) search result here
1457         $('#GnuBookSearchResults').append('<li>No search results found</li>');
1458     } else {    
1459         for (var i = 0; i < pages.length; i++){
1460             //console.log(pages[i].getAttribute('file').substr(1) +'-'+ parseInt(pages[i].getAttribute('file').substr(1), 10));
1461     
1462             
1463             var re = new RegExp (/_(\d{4})/);
1464             var reMatch = re.exec(pages[i].getAttribute('file'));
1465             var leafNum = parseInt(reMatch[1], 10);
1466             //var leafNum = parseInt(pages[i].getAttribute('file').substr(1), 10);
1467             
1468             var children = pages[i].childNodes;
1469             var context = '';
1470             for (var j=0; j<children.length; j++) {
1471                 //console.log(j + ' - ' + children[j].nodeName);
1472                 //console.log(children[j].firstChild.nodeValue);
1473                 if ('CONTEXT' == children[j].nodeName) {
1474                     context += children[j].firstChild.nodeValue;
1475                 } else if ('WORD' == children[j].nodeName) {
1476                     context += '<b>'+children[j].firstChild.nodeValue+'</b>';
1477                     
1478                     var index = this.leafNumToIndex(leafNum);
1479                     if (null != index) {
1480                         //coordinates are [left, bottom, right, top, [baseline]]
1481                         //we'll skip baseline for now...
1482                         var coords = children[j].getAttribute('coords').split(',',4);
1483                         if (4 == coords.length) {
1484                             this.searchResults[index] = {'l':coords[0], 'b':coords[1], 'r':coords[2], 't':coords[3], 'div':null};
1485                         }
1486                     }
1487                 }
1488             }
1489             //TODO: remove hardcoded instance name
1490             $('#GnuBookSearchResults').append('<li><b><a href="javascript:gb.jumpToIndex('+index+');">Leaf ' + leafNum + '</a></b> - ' + context+'</li>');
1491         }
1492     }
1493     $('#GnuBookSearchResults').append('</ul>');
1494
1495     this.updateSearchHilites();
1496 }
1497
1498 // updateSearchHilites()
1499 //______________________________________________________________________________
1500 GnuBook.prototype.updateSearchHilites = function() {
1501     if (2 == this.mode) {
1502         this.updateSearchHilites2UP();
1503     } else {
1504         this.updateSearchHilites1UP();
1505     }
1506 }
1507
1508 // showSearchHilites1UP()
1509 //______________________________________________________________________________
1510 GnuBook.prototype.updateSearchHilites1UP = function() {
1511
1512     for (var key in this.searchResults) {
1513         
1514         if (-1 != jQuery.inArray(parseInt(key), this.displayedLeafs)) {
1515             var result = this.searchResults[key];
1516             if(null == result.div) {
1517                 result.div = document.createElement('div');
1518                 $(result.div).attr('className', 'GnuBookSearchHilite').appendTo('#pagediv'+key);
1519                 //console.log('appending ' + key);
1520             }    
1521             $(result.div).css({
1522                 width:  (result.r-result.l)/this.reduce + 'px',
1523                 height: (result.b-result.t)/this.reduce + 'px',
1524                 left:   (result.l)/this.reduce + 'px',
1525                 top:    (result.t)/this.reduce +'px'
1526             });
1527
1528         } else {
1529             //console.log(key + ' not displayed');
1530             this.searchResults[key].div=null;
1531         }
1532     }
1533 }
1534
1535 // showSearchHilites2UP()
1536 //______________________________________________________________________________
1537 GnuBook.prototype.updateSearchHilites2UP = function() {
1538
1539     var middle = ($('#GBcontainer').width() >> 1);
1540
1541     for (var key in this.searchResults) {
1542         key = parseInt(key, 10);
1543         if (-1 != jQuery.inArray(key, this.displayedLeafs)) {
1544             var result = this.searchResults[key];
1545             if(null == result.div) {
1546                 result.div = document.createElement('div');
1547                 $(result.div).attr('className', 'GnuBookSearchHilite').css('zIndex', 3).appendTo('#GBcontainer');
1548                 //console.log('appending ' + key);
1549             }
1550
1551             var height = this.getPageHeight(key);
1552             var width  = this.getPageWidth(key)
1553             var reduce = this.twoPageH/height;
1554             var scaledW = parseInt(width*reduce);
1555             
1556             var gutter = middle + this.gutterOffsetForIndex(this.currentLeafL);
1557             
1558             if ('L' == this.getPageSide(key)) {
1559                 var pageL = gutter-scaledW;
1560             } else {
1561                 var pageL = gutter;
1562             }
1563             var pageT  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
1564                         
1565             $(result.div).css({
1566                 width:  (result.r-result.l)*reduce + 'px',
1567                 height: (result.b-result.t)*reduce + 'px',
1568                 left:   pageL+(result.l)*reduce + 'px',
1569                 top:    pageT+(result.t)*reduce +'px'
1570             });
1571
1572         } else {
1573             //console.log(key + ' not displayed');
1574             if (null != this.searchResults[key].div) {
1575                 //console.log('removing ' + key);
1576                 $(this.searchResults[key].div).remove();
1577             }
1578             this.searchResults[key].div=null;
1579         }
1580     }
1581 }
1582
1583 // removeSearchHilites()
1584 //______________________________________________________________________________
1585 GnuBook.prototype.removeSearchHilites = function() {
1586     for (var key in this.searchResults) {
1587         if (null != this.searchResults[key].div) {
1588             $(this.searchResults[key].div).remove();
1589             this.searchResults[key].div=null;
1590         }        
1591     }
1592 }
1593
1594 // showEmbedCode()
1595 //______________________________________________________________________________
1596 GnuBook.prototype.showEmbedCode = function() {
1597     if (null != this.embedPopup) { // check if already showing
1598         return;
1599     }
1600     this.autoStop();
1601     this.embedPopup = document.createElement("div");
1602     $(this.embedPopup).css({
1603         position: 'absolute',
1604         top:      '20px',
1605         left:     ($('#GBcontainer').width()-400)/2 + 'px',
1606         width:    '400px',
1607         padding:  "20px",
1608         border:   "3px double #999999",
1609         zIndex:   3,
1610         backgroundColor: "#fff"
1611     }).appendTo('#GnuBook');
1612
1613     htmlStr =  '<p style="text-align:center;"><b>Embed Bookreader in your blog!</b></p>';
1614     htmlStr += '<p><b>Note:</b> The bookreader is still in beta testing. URLs may change in the future, breaking embedded books. This feature is just for testing!</b></p>';
1615     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>';
1616     htmlStr += '<p>Embed Code: <input type="text" size="40" value="<iframe src=\'http://www.us.archive.org/GnuBook/GnuBookEmbed.php?id='+this.bookId+'\' width=\'430px\' height=\'430px\'></iframe>"></p>';
1617     htmlStr += '<p style="text-align:center;"><a href="" onclick="gb.embedPopup = null; $(this.parentNode.parentNode).remove(); return false">Close popup</a></p>';    
1618
1619     this.embedPopup.innerHTML = htmlStr;    
1620 }
1621
1622 // autoToggle()
1623 //______________________________________________________________________________
1624 GnuBook.prototype.autoToggle = function() {
1625
1626     var bComingFrom1up = false;
1627     if (2 != this.mode) {
1628         bComingFrom1up = true;
1629         this.switchMode(2);
1630     }
1631
1632     var self = this;
1633     if (null == this.autoTimer) {
1634         this.flipSpeed = 2000;
1635         
1636         // $$$ Draw events currently cause layout problems when they occur during animation.
1637         //     There is a specific problem when changing from 1-up immediately to autoplay in RTL so
1638         //     we workaround for now by not triggering immediate animation in that case.
1639         //     See https://bugs.launchpad.net/gnubook/+bug/328327
1640         if (('rl' == this.pageProgression) && bComingFrom1up) {
1641             // don't flip immediately -- wait until timer fires
1642         } else {
1643             // flip immediately
1644             this.flipFwdToIndex();        
1645         }
1646
1647         $('#GBtoolbar .play').hide();
1648         $('#GBtoolbar .pause').show();
1649         this.autoTimer=setInterval(function(){
1650             if (self.animating) {return;}
1651             
1652             if (Math.max(self.currentLeafL, self.currentLeafR) >= self.lastDisplayableIndex()) {
1653                 self.flipBackToIndex(1); // $$$ really what we want?
1654             } else {            
1655                 self.flipFwdToIndex();
1656             }
1657         },5000);
1658     } else {
1659         this.autoStop();
1660     }
1661 }
1662
1663 // autoStop()
1664 //______________________________________________________________________________
1665 GnuBook.prototype.autoStop = function() {
1666     if (null != this.autoTimer) {
1667         clearInterval(this.autoTimer);
1668         this.flipSpeed = 'fast';
1669         $('#GBtoolbar .pause').hide();
1670         $('#GBtoolbar .play').show();
1671         this.autoTimer = null;
1672     }
1673 }
1674
1675 // keyboardNavigationIsDisabled(event)
1676 //   - returns true if keyboard navigation should be disabled for the event
1677 //______________________________________________________________________________
1678 GnuBook.prototype.keyboardNavigationIsDisabled = function(event) {
1679     if (event.target.tagName == "INPUT") {
1680         return true;
1681     }   
1682     return false;
1683 }
1684
1685 // gutterOffsetForIndex
1686 //______________________________________________________________________________
1687 //
1688 // Returns the gutter offset for the spread containing the given index.
1689 // This function supports RTL
1690 GnuBook.prototype.gutterOffsetForIndex = function(pindex) {
1691
1692     // To find the offset of the gutter from the middle we calculate our percentage distance
1693     // through the book (0..1), remap to (-0.5..0.5) and multiply by the total page edge width
1694     var offset = parseInt(((pindex / this.numLeafs) - 0.5) * this.twoPageEdgeW);
1695     
1696     // But then again for RTL it's the opposite
1697     if ('rl' == this.pageProgression) {
1698         offset = -offset;
1699     }
1700     
1701     return offset;
1702 }
1703
1704 // leafEdgeWidth
1705 //______________________________________________________________________________
1706 // Returns the width of the leaf edge div for the page with index given
1707 GnuBook.prototype.leafEdgeWidth = function(pindex) {
1708     // $$$ could there be single pixel rounding errors for L vs R?
1709     if ((this.getPageSide(pindex) == 'L') && (this.pageProgression != 'rl')) {
1710         return parseInt( (pindex/this.numLeafs) * this.twoPageEdgeW + 0.5);
1711     } else {
1712         return parseInt( (1 - pindex/this.numLeafs) * this.twoPageEdgeW + 0.5);
1713     }
1714 }
1715
1716 // jumpIndexForLeftEdgePageX
1717 //______________________________________________________________________________
1718 // Returns the target jump leaf given a page coordinate (inside the left page edge div)
1719 GnuBook.prototype.jumpIndexForLeftEdgePageX = function(pageX) {
1720     if ('rl' != this.pageProgression) {
1721         // LTR - flipping backward
1722         var jumpLeaf = this.currentLeafL - ($(this.leafEdgeL).offset().left + $(this.leafEdgeL).width() - pageX) * 10;
1723         // browser may have resized the div due to font size change -- see https://bugs.launchpad.net/gnubook/+bug/333570
1724         jumpLeaf = Math.min(jumpLeaf, this.currentLeafL - 2);
1725         jumpLeaf = Math.max(jumpLeaf, this.firstDisplayableIndex());
1726         return jumpLeaf;
1727     } else {
1728         var jumpLeaf = this.currentLeafL + ($(this.leafEdgeL).offset().left + $(this.leafEdgeL).width() - pageX) * 10;
1729         jumpLeaf = Math.max(jumpLeaf, this.currentLeafL + 2);
1730         jumpLeaf = Math.min(jumpLeaf, this.lastDisplayableIndex());
1731         return jumpLeaf;
1732     }
1733 }
1734
1735 // jumpIndexForRightEdgePageX
1736 //______________________________________________________________________________
1737 // Returns the target jump leaf given a page coordinate (inside the right page edge div)
1738 GnuBook.prototype.jumpIndexForRightEdgePageX = function(pageX) {
1739     if ('rl' != this.pageProgression) {
1740         // LTR
1741         var jumpLeaf = this.currentLeafR + (pageX - $(this.leafEdgeR).offset().left) * 10;
1742         jumpLeaf = Math.max(jumpLeaf, this.currentLeafR + 2);
1743         jumpLeaf = Math.min(jumpLeaf, this.lastDisplayableIndex());
1744         return jumpLeaf;
1745     } else {
1746         var jumpLeaf = this.currentLeafR - (pageX - $(this.leafEdgeR).offset().left) * 10;
1747         jumpLeaf = Math.min(jumpLeaf, this.currentLeafR - 2);
1748         jumpLeaf = Math.max(jumpLeaf, this.firstDisplayableIndex());
1749         return jumpLeaf;
1750     }
1751 }
1752
1753 GnuBook.prototype.initToolbar = function(mode) {
1754     // XXXmang hook up logo to url action -- change buttons to image links? -- don't hardcode URL
1755     $("#GnuBook").append("<div id='GBtoolbar'><span style='float:left;'>"
1756         + "<a class='GBicon logo rollover' href='http://www.archive.org/index.php'>&nbsp;</a>"
1757         + " <button class='GBicon rollover zoom_out' onclick='gb.zoom1up(-1); return false;'/>" 
1758         + "<button class='GBicon rollover zoom_in' onclick='gb.zoom1up(1); return false;'/>"
1759         + " <span class='label'>Zoom: <span id='GBzoom'>25</span>%</span>"
1760         + " <button class='GBicon rollover one_page_mode' onclick='gb.switchMode(1); return false;'/>"
1761         + " <button class='GBicon rollover two_page_mode' onclick='gb.switchMode(2); return false;'/>"
1762         + "&nbsp;&nbsp; <a class='GBblack title' href='"+this.bookUrl+"' target='_blank'>"+this.shortTitle(50)+"</a>"
1763         + "</span></div>");
1764
1765     // $$$ turn this into a member variable
1766     var jToolbar = $('#GBtoolbar'); // j prefix indicates jQuery object
1767     
1768     // We build in mode 2
1769     jToolbar.append("<span id='GBtoolbarbuttons' style='float: right'>"
1770         + "<button class='GBicon rollover embed' />"
1771         + "<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>"
1772         + "<div class='GBtoolbarmode2' style='display: inline'><button class='GBicon rollover book_left' /><button class='GBicon rollover book_right' /></div>"
1773         + "<div class='GBtoolbarmode1' style='display: hidden'><button class='GBicon rollover book_up' /> <button class='GBicon rollover book_down' /></div>"
1774         + "<button class='GBicon rollover play' /><button class='GBicon rollover pause' style='display: none' /></span>");
1775
1776     // Bind the non-changing click handlers
1777     jToolbar.find('.embed').bind('click', function(e) {
1778         gb.showEmbedCode();
1779         return false;
1780     });
1781     jToolbar.find('.play').bind('click', function(e) {
1782         gb.autoToggle();
1783         return false;
1784     });
1785     jToolbar.find('.pause').bind('click', function(e) {
1786         gb.autoToggle();
1787         return false;
1788     });
1789     
1790     // Setup tooltips -- later we could load these from a file for i18n
1791     var titles = { '.logo': 'Go to Archive.org',
1792                    '.zoom_in': 'Zoom in',
1793                    '.zoom_out': 'Zoom out',
1794                    '.one_page_mode': 'One-page view',
1795                    '.two_page_mode': 'Two-page view',
1796                    '.embed': 'Embed bookreader',
1797                    '.book_left': 'Flip left',
1798                    '.book_right': 'Flip right',
1799                    '.book_up': 'Page up',
1800                    '.book_down': 'Page down',
1801                    '.play': 'Play',
1802                    '.pause': 'Pause',
1803                   };                  
1804     for (var icon in titles) {
1805         jToolbar.find(icon).attr('title', titles[icon]);
1806     }
1807
1808     // Switch to requested mode -- binds other click handlers
1809     this.switchToolbarMode(mode);
1810
1811 }
1812
1813
1814 // switchToolbarMode
1815 //______________________________________________________________________________
1816 // Update the toolbar for the given mode (changes navigation buttons)
1817 // $$$ we should soon split the toolbar out into its own module
1818 GnuBook.prototype.switchToolbarMode = function(mode) {
1819     if (1 == mode) {
1820         // 1-up     
1821         $('#GBtoolbar .GBtoolbarmode2').hide();
1822         $('#GBtoolbar .GBtoolbarmode1').css('display', 'inline').show();
1823     } else {
1824         // 2-up
1825         $('#GBtoolbar .GBtoolbarmode1').hide();
1826         $('#GBtoolbar .GBtoolbarmode2').css('display', 'inline').show();
1827     }
1828     
1829     this.bindToolbarNavHandlers($('#GBtoolbar'));
1830 }
1831
1832 // bindToolbarNavHandlers
1833 //______________________________________________________________________________
1834 // Binds the toolbar handlers
1835 GnuBook.prototype.bindToolbarNavHandlers = function(jToolbar) {
1836
1837     jToolbar.find('.book_left').unbind('click')
1838         .bind('click', function(e) {
1839             gb.left();
1840             return false;
1841          });
1842          
1843     jToolbar.find('.book_right').unbind('click')
1844         .bind('click', function(e) {
1845             gb.right();
1846             return false;
1847         });
1848         
1849     jToolbar.find('.book_up').unbind('click')
1850         .bind('click', function(e) {
1851             gb.prev();
1852             return false;
1853         });        
1854         
1855     jToolbar.find('.book_down').unbind('click')
1856         .bind('click', function(e) {
1857             gb.next();
1858             return false;
1859         });      
1860 }
1861
1862 // firstDisplayableIndex
1863 //______________________________________________________________________________
1864 // Returns the index of the first visible page, dependent on the mode.
1865 // $$$ Currently we cannot display the front/back cover in 2-up and will need to update
1866 // this function when we can as part of https://bugs.launchpad.net/gnubook/+bug/296788
1867 GnuBook.prototype.firstDisplayableIndex = function() {
1868     if (this.mode == 0) {
1869         return 0;
1870     } else {
1871         return 1; // $$$ we assume there are enough pages... we need logic for very short books
1872     }
1873 }
1874
1875 // lastDisplayableIndex
1876 //______________________________________________________________________________
1877 // Returns the index of the last visible page, dependent on the mode.
1878 // $$$ Currently we cannot display the front/back cover in 2-up and will need to update
1879 // this function when we can as pa  rt of https://bugs.launchpad.net/gnubook/+bug/296788
1880 GnuBook.prototype.lastDisplayableIndex = function() {
1881     if (this.mode == 2) {
1882         if (this.lastDisplayableIndex2up === null) {
1883             // Calculate and cache
1884             var candidate = this.numLeafs - 1;
1885             for ( ; candidate >= 0; candidate--) {
1886                 var spreadIndices = this.getSpreadIndices(candidate);
1887                 if (Math.max(spreadIndices[0], spreadIndices[1]) < (this.numLeafs - 1)) {
1888                     break;
1889                 }
1890             }
1891             this.lastDisplayableIndex2up = candidate;
1892         }
1893         return this.lastDisplayableIndex2up;
1894     } else {
1895         return this.numLeafs - 1;
1896     }
1897 }
1898
1899 // shortTitle(maximumCharacters)
1900 //________
1901 // Returns a shortened version of the title with the maximum number of characters
1902 GnuBook.prototype.shortTitle = function(maximumCharacters) {
1903     if (this.bookTitle.length < maximumCharacters) {
1904         return this.bookTitle;
1905     }
1906     
1907     var title = this.bookTitle.substr(0, maximumCharacters - 3);
1908     title += '...';
1909     return title;
1910 }