Fixes for these bugs:
[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.64 $ $Date: 2009-01-27 22:31:56 $
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 };
58
59 // init()
60 //______________________________________________________________________________
61 GnuBook.prototype.init = function() {
62     var startLeaf = window.location.hash;
63     //console.log("startLeaf from location.hash: %s", startLeaf);
64     if ('' == startLeaf) {
65         if (this.titleLeaf) {
66             startLeaf = "#" + this.leafNumToIndex(this.titleLeaf);
67         }
68     }
69     var title = this.bookTitle.substr(0,50);
70     if (this.bookTitle.length>50) title += '...';
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 = title;
75     
76     $("#GnuBook").empty();
77     $("#GnuBook").append("<div id='GBtoolbar'><span style='float:left;'><button class='GBicon' id='zoom_out' onclick='gb.zoom1up(-1); return false;'/> <button class='GBicon' id='zoom_in' onclick='gb.zoom1up(1); return false;'/> zoom: <span id='GBzoom'>25</span>% <button class='GBicon' id='script' onclick='gb.switchMode(1); return false;'/> <button class='GBicon' id='book_open' onclick='gb.switchMode(2); return false;'/>  &nbsp;&nbsp; <a href='"+this.bookUrl+"' target='_blank'>"+title+"</a></span></div>");
78     $("#GBtoolbar").append("<span id='GBtoolbarbuttons' style='float: right'><button class='GBicon' id='page_code' onclick='gb.showEmbedCode(); return false;'/><form class='GBpageform' action='javascript:' onsubmit='gb.jumpToPage(this.elements[0].value)'> page:<input id='GBpagenum' type='text' size='3' onfocus='gb.autoStop();'></input></form> <button class='GBicon' id='book_previous' onclick='gb.prev(); return false;'/> <button class='GBicon' id='book_next' onclick='gb.next(); return false;'/> <button class='GBicon play' id='autoImg' onclick='gb.autoToggle(); return false;'/></span>");
79     $("#GnuBook").append("<div id='GBcontainer'></div>");
80     $("#GBcontainer").append("<div id='GBpageview'></div>");
81
82     $("#GBcontainer").bind('scroll', this, function(e) {
83         e.data.loadLeafs();
84     });
85
86     this.setupKeyListeners();
87
88     $(window).bind('resize', this, function(e) {
89         //console.log('resize!');
90         if (1 == e.data.mode) {
91             //console.log('centering 1page view');
92             e.data.centerPageView();
93             $('#GBpageview').empty()
94             e.data.displayedLeafs = [];
95             e.data.updateSearchHilites(); //deletes hilights but does not call remove()            
96             e.data.loadLeafs();
97         } else {
98             //console.log('drawing 2 page view');
99             e.data.prepareTwoPageView();
100         }
101     });
102
103     if (1 == this.mode) {
104         this.resizePageView();
105     
106         if ('' != startLeaf) { // Jump to the leaf specified in the URL
107             this.jumpToIndex(parseInt(startLeaf.substr(1)));
108             //console.log('jump to ' + parseInt(startLeaf.substr(1)));
109         }
110     } else {
111         //this.resizePageView();
112         
113         this.displayedLeafs=[0];
114         if ('' != startLeaf) {
115             this.displayedLeafs = [parseInt(startLeaf.substr(1))];
116         }
117         //console.log('titleLeaf: %d', this.titleLeaf);
118         //console.log('displayedLeafs: %s', this.displayedLeafs);
119         this.prepareTwoPageView();
120         //if (this.auto) this.nextPage();
121     }
122 }
123
124 GnuBook.prototype.setupKeyListeners = function() {
125     var self = this;
126
127     var KEY_PGUP = 33;
128     var KEY_PGDOWN = 34;
129     var KEY_END = 35;
130     var KEY_HOME = 36;
131
132     var KEY_LEFT = 37;
133     var KEY_UP = 38;
134     var KEY_RIGHT = 39;
135     var KEY_DOWN = 40;
136
137     // We use document here instead of window to avoid a bug in jQuery on IE7
138     $(document).keydown(function(e) {
139         
140         // Keyboard navigation        
141         switch(e.keyCode) {
142             case KEY_PGUP:
143                 // In 1up mode page scrolling is handled by browser
144                 if (2 == self.mode) {
145                     self.prev();
146                 }
147                 break;
148             case KEY_PGDOWN:
149                 if (2 == self.mode) {
150                     self.next();
151                 }
152                 break;
153             case KEY_END:
154                 self.end();
155                 break;
156             case KEY_HOME:
157                 self.home();
158                 break;
159             case KEY_UP:
160             case KEY_LEFT:
161                 if (self.keyboardNavigationIsDisabled(e)) {
162                     break;
163                 }
164                 if (2 == self.mode) {
165                     self.prev();
166                 }
167                 break;
168             case KEY_DOWN:
169             case KEY_RIGHT:
170                 if (self.keyboardNavigationIsDisabled(e)) {
171                     break;
172                 }
173                 if (2 == self.mode) {
174                     self.next();
175                 }
176                 break;
177         }
178     });
179 }
180
181 // drawLeafs()
182 //______________________________________________________________________________
183 GnuBook.prototype.drawLeafs = function() {
184     if (1 == this.mode) {
185         this.drawLeafsOnePage();
186     } else {
187         this.drawLeafsTwoPage();
188     }
189 }
190
191
192 // drawLeafsOnePage()
193 //______________________________________________________________________________
194 GnuBook.prototype.drawLeafsOnePage = function() {
195     //alert('drawing leafs!');
196     this.timer = null;
197
198
199     var scrollTop = $('#GBcontainer').attr('scrollTop');
200     var scrollBottom = scrollTop + $('#GBcontainer').height();
201     //console.log('top=' + scrollTop + ' bottom='+scrollBottom);
202     
203     var leafsToDisplay = [];
204     
205     var i;
206     var leafTop = 0;
207     var leafBottom = 0;
208     for (i=0; i<this.numLeafs; i++) {
209         var height  = parseInt(this.getPageHeight(i)/this.reduce); 
210     
211         leafBottom += height;
212         //console.log('leafTop = '+leafTop+ ' pageH = ' + this.pageH[i] + 'leafTop>=scrollTop=' + (leafTop>=scrollTop));
213         var topInView    = (leafTop >= scrollTop) && (leafTop <= scrollBottom);
214         var bottomInView = (leafBottom >= scrollTop) && (leafBottom <= scrollBottom);
215         var middleInView = (leafTop <=scrollTop) && (leafBottom>=scrollBottom);
216         if (topInView | bottomInView | middleInView) {
217             //console.log('to display: ' + i);
218             leafsToDisplay.push(i);
219         }
220         leafTop += height +10;      
221         leafBottom += 10;
222     }
223
224     var firstLeafToDraw  = leafsToDisplay[0];
225     window.location.hash = firstLeafToDraw;    
226     this.firstIndex      = firstLeafToDraw;
227
228     if ((0 != firstLeafToDraw) && (1 < this.reduce)) {
229         firstLeafToDraw--;
230         leafsToDisplay.unshift(firstLeafToDraw);
231     }
232     
233     var lastLeafToDraw = leafsToDisplay[leafsToDisplay.length-1];
234     if ( ((this.numLeafs-1) != lastLeafToDraw) && (1 < this.reduce) ) {
235         leafsToDisplay.push(lastLeafToDraw+1);
236     }
237     
238     leafTop = 0;
239     var i;
240     for (i=0; i<firstLeafToDraw; i++) {
241         leafTop += parseInt(this.getPageHeight(i)/this.reduce) +10;
242     }
243
244     //var viewWidth = $('#GBpageview').width(); //includes scroll bar width
245     var viewWidth = $('#GBcontainer').attr('scrollWidth');
246
247
248     for (i=0; i<leafsToDisplay.length; i++) {
249         var leafNum = leafsToDisplay[i];    
250         var height  = parseInt(this.getPageHeight(leafNum)/this.reduce); 
251
252         if(-1 == jQuery.inArray(leafsToDisplay[i], this.displayedLeafs)) {            
253             var width   = parseInt(this.getPageWidth(leafNum)/this.reduce); 
254             //console.log("displaying leaf " + leafsToDisplay[i] + ' leafTop=' +leafTop);
255             var div = document.createElement("div");
256             div.className = 'GBpagediv1up';
257             div.id = 'pagediv'+leafNum;
258             div.style.position = "absolute";
259             $(div).css('top', leafTop + 'px');
260             var left = (viewWidth-width)>>1;
261             if (left<0) left = 0;
262             $(div).css('left', left+'px');
263             $(div).css('width', width+'px');
264             $(div).css('height', height+'px');
265             //$(div).text('loading...');
266             
267             $('#GBpageview').append(div);
268
269             var img = document.createElement("img");
270             img.src = this.getPageURI(leafNum);
271             $(img).css('width', width+'px');
272             $(img).css('height', height+'px');
273             $(div).append(img);
274
275         } else {
276             //console.log("not displaying " + leafsToDisplay[i] + ' score=' + jQuery.inArray(leafsToDisplay[i], this.displayedLeafs));            
277         }
278
279         leafTop += height +10;
280
281     }
282     
283     for (i=0; i<this.displayedLeafs.length; i++) {
284         if (-1 == jQuery.inArray(this.displayedLeafs[i], leafsToDisplay)) {
285             var leafNum = this.displayedLeafs[i];
286             //console.log('Removing leaf ' + leafNum);
287             //console.log('id='+'#pagediv'+leafNum+ ' top = ' +$('#pagediv'+leafNum).css('top'));
288             $('#pagediv'+leafNum).remove();
289         } else {
290             //console.log('NOT Removing leaf ' + this.displayedLeafs[i]);
291         }
292     }
293     
294     this.displayedLeafs = leafsToDisplay.slice();
295     this.updateSearchHilites();
296     
297     if (null != this.getPageNum(firstLeafToDraw))  {
298         $("#GBpagenum").val(this.getPageNum(firstLeafToDraw));
299     } else {
300         $("#GBpagenum").val('');
301     }
302 }
303
304 // drawLeafsTwoPage()
305 //______________________________________________________________________________
306 GnuBook.prototype.drawLeafsTwoPage = function() {
307     //alert('drawing two leafs!');
308
309     var scrollTop = $('#GBcontainer').attr('scrollTop');
310     var scrollBottom = scrollTop + $('#GBcontainer').height();
311     var leafNum = this.currentLeafL;
312     var height  = this.getPageHeight(leafNum); 
313     var width   = this.getPageWidth(leafNum);
314     var handSide= this.getPageSide(leafNum);
315
316     var leafEdgeWidthL = parseInt( (leafNum/this.numLeafs)*this.twoPageEdgeW );
317     var leafEdgeWidthR = this.twoPageEdgeW - leafEdgeWidthL;
318     var divWidth = this.twoPageW*2+20 + this.twoPageEdgeW;
319     var divLeft = ($('#GBcontainer').width() - divWidth) >> 1;
320     //console.log(leafEdgeWidthL);
321
322     var middle = ($('#GBcontainer').width() >> 1);            
323     var left = middle - this.twoPageW;
324     var top  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
325
326     var scaledW = parseInt(this.twoPageH*width/height);
327     left = 10+leafEdgeWidthL;
328     //var right = left+scaledW;
329     var right = $(this.twoPageDiv).width()-11-$(this.leafEdgeL).width()-scaledW;
330
331     var gutter = middle+parseInt((2*this.currentLeafL - this.numLeafs)*this.twoPageEdgeW/this.numLeafs/2);
332     
333     
334     this.prefetchImg(leafNum);
335     $(this.prefetchedImgs[leafNum]).css({
336         position: 'absolute',
337         /*right:   gutter+'px',*/
338         left: gutter-scaledW+'px',
339         top:    top+'px',
340         backgroundColor: 'rgb(234, 226, 205)',
341         height: this.twoPageH +'px',
342         width:  scaledW + 'px',
343         borderRight: '1px solid black',
344         zIndex: 2
345     }).appendTo('#GBcontainer');
346     //$('#GBcontainer').append(this.prefetchedImgs[leafNum]);
347
348
349     var leafNum = this.currentLeafR;
350     var height  = this.getPageHeight(leafNum); 
351     var width   = this.getPageWidth(leafNum);
352     //    var left = ($('#GBcontainer').width() >> 1);
353     left += scaledW;
354
355     var scaledW = this.twoPageH*width/height;
356     this.prefetchImg(leafNum);
357     $(this.prefetchedImgs[leafNum]).css({
358         position: 'absolute',
359         left:   gutter+'px',
360         top:    top+'px',
361         backgroundColor: 'rgb(234, 226, 205)',
362         height: this.twoPageH + 'px',
363         width:  scaledW + 'px',
364         borderLeft: '1px solid black',
365         zIndex: 2
366     }).appendTo('#GBcontainer');
367     //$('#GBcontainer').append(this.prefetchedImgs[leafNum]);
368         
369
370     this.displayedLeafs = [this.currentLeafL, this.currentLeafR];
371     this.setClickHandlers();
372
373     this.updatePageNumBox2UP();
374 }
375
376 // updatePageNumBox2UP
377 //______________________________________________________________________________
378 GnuBook.prototype.updatePageNumBox2UP = function() {
379     if (null != this.getPageNum(this.currentLeafL))  {
380         $("#GBpagenum").val(this.getPageNum(this.currentLeafL));
381     } else {
382         $("#GBpagenum").val('');
383     }
384     window.location.hash = this.currentLeafL; 
385 }
386
387 // loadLeafs()
388 //______________________________________________________________________________
389 GnuBook.prototype.loadLeafs = function() {
390
391
392     var self = this;
393     if (null == this.timer) {
394         this.timer=setTimeout(function(){self.drawLeafs()},250);
395     } else {
396         clearTimeout(this.timer);
397         this.timer=setTimeout(function(){self.drawLeafs()},250);    
398     }
399 }
400
401
402 // zoom1up()
403 //______________________________________________________________________________
404 GnuBook.prototype.zoom1up = function(dir) {
405     if (2 == this.mode) {     //can only zoom in 1-page mode
406         this.switchMode(1);
407         return;
408     }
409     
410     if (1 == dir) {
411         if (this.reduce <= 0.5) return;
412         this.reduce*=0.5;           //zoom in
413     } else {
414         if (this.reduce >= 8) return;
415         this.reduce*=2;             //zoom out
416     }
417     
418     this.resizePageView();
419
420     $('#GBpageview').empty()
421     this.displayedLeafs = [];
422     this.loadLeafs();
423     
424     $('#GBzoom').text(100/this.reduce);
425 }
426
427
428 // resizePageView()
429 //______________________________________________________________________________
430 GnuBook.prototype.resizePageView = function() {
431     var i;
432     var viewHeight = 0;
433     //var viewWidth  = $('#GBcontainer').width(); //includes scrollBar
434     var viewWidth  = $('#GBcontainer').attr('clientWidth');   
435
436     var oldScrollTop  = $('#GBcontainer').attr('scrollTop');
437     var oldViewHeight = $('#GBpageview').height();
438     if (0 != oldViewHeight) {
439         var scrollRatio = oldScrollTop / oldViewHeight;
440     } else {
441         var scrollRatio = 0;
442     }
443     
444     for (i=0; i<this.numLeafs; i++) {
445         viewHeight += parseInt(this.getPageHeight(i)/this.reduce) + this.padding; 
446         var width = parseInt(this.getPageWidth(i)/this.reduce);
447         if (width>viewWidth) viewWidth=width;
448     }
449     $('#GBpageview').height(viewHeight);
450     $('#GBpageview').width(viewWidth);    
451
452     $('#GBcontainer').attr('scrollTop', Math.floor(scrollRatio*viewHeight));
453     
454     this.centerPageView();
455     this.loadLeafs();
456     
457 }
458
459 // centerPageView()
460 //______________________________________________________________________________
461 GnuBook.prototype.centerPageView = function() {
462
463     var scrollWidth  = $('#GBcontainer').attr('scrollWidth');
464     var clientWidth  =  $('#GBcontainer').attr('clientWidth');
465     //console.log('sW='+scrollWidth+' cW='+clientWidth);
466     if (scrollWidth > clientWidth) {
467         $('#GBcontainer').attr('scrollLeft', (scrollWidth-clientWidth)/2);
468     }
469
470 }
471
472 // jumpToPage()
473 //______________________________________________________________________________
474 GnuBook.prototype.jumpToPage = function(pageNum) {
475     //if (2 == this.mode) return;
476     
477     var i;
478     var foundPage = false;
479     var foundLeaf = null;
480     for (i=0; i<this.numLeafs; i++) {
481         if (this.getPageNum(i) == pageNum) {
482             foundPage = true;
483             foundLeaf = i;
484             break;
485         }
486     }
487     
488     if (foundPage) {
489         var leafTop = 0;
490         var h;
491         this.jumpToIndex(foundLeaf);
492         $('#GBcontainer').attr('scrollTop', leafTop);
493     } else {
494         alert('Page not found. This book might not have pageNumbers in scandata.');
495     }
496 }
497
498 // jumpToIndex()
499 //______________________________________________________________________________
500 GnuBook.prototype.jumpToIndex = function(index) {
501     if (2 == this.mode) {
502         this.autoStop();
503         if (index<this.currentLeafL) {
504             if ('L' == this.getPageSide(index)) {
505                 this.flipBackToIndex(index);
506             } else {
507                 this.flipBackToIndex(index-1);
508             }
509         } else if (index>this.currentLeafR) {
510             if ('R' == this.getPageSide(index)) {
511                 this.flipFwdToIndex(index);
512             } else {
513                 this.flipFwdToIndex(index+1);
514             }        
515         }
516
517     } else {        
518         var i;
519         var leafTop = 0;
520         var h;
521         for (i=0; i<index; i++) {
522             h = parseInt(this.getPageHeight(i)/this.reduce); 
523             leafTop += h + this.padding;
524         }
525         //$('#GBcontainer').attr('scrollTop', leafTop);
526         $('#GBcontainer').animate({scrollTop: leafTop },'fast');    
527     }
528 }
529
530
531
532 // switchMode()
533 //______________________________________________________________________________
534 GnuBook.prototype.switchMode = function(mode) {
535     if (mode == this.mode) return;
536
537     this.autoStop();
538     this.removeSearchHilites();
539
540     this.mode = mode;
541     if (1 == mode) {
542         this.prepareOnePageView();
543     } else {
544         this.prepareTwoPageView();
545     }
546
547 }
548
549 //prepareOnePageView()
550 //______________________________________________________________________________
551 GnuBook.prototype.prepareOnePageView = function() {
552
553     var startLeaf = this.displayedLeafs[0];
554     
555     $('#GBcontainer').empty();
556     $('#GBcontainer').css({
557         overflowY: 'scroll',
558         overflowX: 'auto'
559     });
560     
561     $("#GBcontainer").append("<div id='GBpageview'></div>");
562     this.resizePageView();
563     this.jumpToIndex(startLeaf);
564     this.displayedLeafs = [];    
565     this.drawLeafsOnePage();
566     $('#GBzoom').text(100/this.reduce);    
567 }
568
569 // prepareTwoPageView()
570 //______________________________________________________________________________
571 GnuBook.prototype.prepareTwoPageView = function() {
572     $('#GBcontainer').empty();
573
574     var firstLeaf = this.displayedLeafs[0];
575     if ('R' == this.getPageSide(firstLeaf)) {
576         if (0 == firstLeaf) {
577             firstLeaf++;
578         } else {
579             firstLeaf--;
580         }
581     }
582
583     this.currentLeafL = null;
584     this.currentLeafR = null;
585     this.pruneUnusedImgs();
586     
587     this.currentLeafL = firstLeaf;
588     this.currentLeafR = firstLeaf + 1;
589     
590     this.calculateSpreadSize(); //sets this.twoPageW, twoPageH, and twoPageRatio
591
592     var middle = ($('#GBcontainer').width() >> 1);
593     var gutter = middle+parseInt((2*this.currentLeafL - this.numLeafs)*this.twoPageEdgeW/this.numLeafs/2);
594     var scaledWL = this.getPageWidth2UP(this.currentLeafL);
595     var scaledWR = this.getPageWidth2UP(this.currentLeafR);
596     var leafEdgeWidthL = parseInt( (firstLeaf/this.numLeafs)*this.twoPageEdgeW );
597     var leafEdgeWidthR = this.twoPageEdgeW - leafEdgeWidthL;
598
599     //console.log('idealWidth='+idealWidth+' idealHeight='+idealHeight);
600     //var divWidth = this.twoPageW*2+20 + this.twoPageEdgeW;
601     var divWidth = scaledWL + scaledWR + 20 + this.twoPageEdgeW;
602     var divHeight = this.twoPageH+20;
603     //var divLeft = ($('#GBcontainer').width() - divWidth) >> 1;
604     var divLeft = gutter-scaledWL-leafEdgeWidthL-10;
605     var divTop = ($('#GBcontainer').height() - divHeight) >> 1;
606     //console.log('divWidth='+divWidth+' divHeight='+divHeight+ ' divLeft='+divLeft+' divTop='+divTop);
607
608     this.twoPageDiv = document.createElement('div');
609     $(this.twoPageDiv).attr('id', 'book_div_1').css({
610         border: '1px solid rgb(68, 25, 17)',
611         width:  divWidth + 'px',
612         height: divHeight+'px',
613         visibility: 'visible',
614         position: 'absolute',
615         backgroundColor: 'rgb(136, 51, 34)',
616         left: divLeft + 'px',
617         top: divTop+'px',
618         MozBorderRadiusTopleft: '7px',
619         MozBorderRadiusTopright: '7px',
620         MozBorderRadiusBottomright: '7px',
621         MozBorderRadiusBottomleft: '7px'
622     }).appendTo('#GBcontainer');
623     //$('#GBcontainer').append('<div id="book_div_1" style="border: 1px solid rgb(68, 25, 17); width: ' + divWidth + 'px; height: '+divHeight+'px; visibility: visible; position: absolute; background-color: rgb(136, 51, 34); left: ' + divLeft + 'px; top: '+divTop+'px; -moz-border-radius-topleft: 7px; -moz-border-radius-topright: 7px; -moz-border-radius-bottomright: 7px; -moz-border-radius-bottomleft: 7px;"/>');
624
625
626     var height  = this.getPageHeight(this.currentLeafR); 
627     var width   = this.getPageWidth(this.currentLeafR);    
628     var scaledW = this.twoPageH*width/height;
629     
630     this.leafEdgeR = document.createElement('div');
631     $(this.leafEdgeR).css({
632         borderStyle: 'solid solid solid none',
633         borderColor: 'rgb(51, 51, 34)',
634         borderWidth: '1px 1px 1px 0px',
635         background: 'transparent url(images/right-edges.png) repeat scroll 0% 0%',
636         width: leafEdgeWidthR + 'px',
637         height: this.twoPageH-1 + 'px',
638         /*right: '10px',*/
639         left: gutter+scaledW+'px',
640         top: divTop+10+'px',
641         position: 'absolute'
642     }).appendTo('#GBcontainer');
643     
644     this.leafEdgeL = document.createElement('div');
645     $(this.leafEdgeL).css({
646         borderStyle: 'solid none solid solid',
647         borderColor: 'rgb(51, 51, 34)',
648         borderWidth: '1px 0px 1px 1px',
649         background: 'transparent url(images/left-edges.png) repeat scroll 0% 0%',
650         width: leafEdgeWidthL + 'px',
651         height: this.twoPageH-1 + 'px',
652         left: divLeft+10+'px',
653         top: divTop+10+'px',    
654         position: 'absolute'
655     }).appendTo('#GBcontainer');
656
657
658
659     divWidth = 30;
660     divHeight = this.twoPageH+20;
661     divLeft = ($('#GBcontainer').width() - divWidth) >> 1;
662     divTop = ($('#GBcontainer').height() - divHeight) >> 1;
663
664     div = document.createElement('div');
665     $(div).attr('id', 'book_div_2').css({
666         border:          '1px solid rgb(68, 25, 17)',
667         width:           divWidth+'px',
668         height:          divHeight+'px',
669         position:        'absolute',
670         backgroundColor: 'rgb(68, 25, 17)',
671         left:            divLeft+'px',
672         top:             divTop+'px'
673     }).appendTo('#GBcontainer');
674     //$('#GBcontainer').append('<div id="book_div_2" style="border: 1px solid rgb(68, 25, 17); width: '+divWidth+'px; height: '+divHeight+'px; visibility: visible; position: absolute; background-color: rgb(68, 25, 17); left: '+divLeft+'px; top: '+divTop+'px;"/>');
675
676     divWidth = this.twoPageW*2;
677     divHeight = this.twoPageH;
678     divLeft = ($('#GBcontainer').width() - divWidth) >> 1;
679     divTop = ($('#GBcontainer').height() - divHeight) >> 1;
680
681
682     this.prepareTwoPagePopUp();
683
684     this.displayedLeafs = [];
685     //this.leafsToDisplay=[firstLeaf, firstLeaf+1];
686     //console.log('leafsToDisplay: ' + this.leafsToDisplay[0] + ' ' + this.leafsToDisplay[1]);
687     this.drawLeafsTwoPage();
688     this.updateSearchHilites2UP();
689     
690     this.prefetch();
691     $('#GBzoom').text((100*this.twoPageH/this.getPageHeight(firstLeaf)).toString().substr(0,4));
692 }
693
694 // prepareTwoPagePopUp()
695 //
696 // This function prepares the "View leaf n" popup that shows while the mouse is
697 // over the left/right "stack of sheets" edges.  It also binds the mouse
698 // events for these divs.
699 //______________________________________________________________________________
700 GnuBook.prototype.prepareTwoPagePopUp = function() {
701     this.twoPagePopUp = document.createElement('div');
702     $(this.twoPagePopUp).css({
703         border: '1px solid black',
704         padding: '2px 6px',
705         position: 'absolute',
706         fontFamily: 'sans-serif',
707         fontSize: '14px',
708         zIndex: '1000',
709         backgroundColor: 'rgb(255, 255, 238)',
710         opacity: 0.85
711     }).appendTo('#GBcontainer');
712     $(this.twoPagePopUp).hide();
713     
714     $(this.leafEdgeL).add(this.leafEdgeR).bind('mouseenter', this, function(e) {
715         $(e.data.twoPagePopUp).show();
716     });
717
718     $(this.leafEdgeL).add(this.leafEdgeR).bind('mouseleave', this, function(e) {
719         $(e.data.twoPagePopUp).hide();
720     });
721
722     $(this.leafEdgeL).bind('click', this, function(e) { 
723         e.data.autoStop();
724         var jumpIndex = e.data.currentLeafL - ($(e.data.leafEdgeL).offset().left + $(e.data.leafEdgeL).width() - e.pageX) * 10;
725         jumpIndex = Math.max(jumpIndex, 0);
726         e.data.flipBackToIndex(jumpIndex);
727     
728     });
729
730     $(this.leafEdgeR).bind('click', this, function(e) { 
731         e.data.autoStop();
732         var jumpIndex = e.data.currentLeafR + (e.pageX - $(e.data.leafEdgeR).offset().left) * 10;
733         jumpIndex = Math.max(jumpIndex, 0);
734         e.data.flipFwdToIndex(jumpIndex);
735     
736     });
737
738     $(this.leafEdgeR).bind('mousemove', this, function(e) {
739
740         var jumpLeaf = e.data.currentLeafR + (e.pageX - $(e.data.leafEdgeR).offset().left) * 10;
741         jumpLeaf = Math.min(jumpLeaf, e.data.numLeafs-1);        
742         $(e.data.twoPagePopUp).text('View Leaf '+jumpLeaf);
743         
744         $(e.data.twoPagePopUp).css({
745             left: e.pageX +5+ 'px',
746             top: e.pageY-$('#GBcontainer').offset().top+ 'px'
747         });
748     });
749
750     $(this.leafEdgeL).bind('mousemove', this, function(e) {
751         var jumpLeaf = e.data.currentLeafL - ($(e.data.leafEdgeL).offset().left + $(e.data.leafEdgeL).width() - e.pageX) * 10;
752         jumpLeaf = Math.max(jumpLeaf, 0);
753         $(e.data.twoPagePopUp).text('View Leaf '+jumpLeaf);
754         
755         $(e.data.twoPagePopUp).css({
756             left: e.pageX - $(e.data.twoPagePopUp).width() - 30 + 'px',
757             top: e.pageY-$('#GBcontainer').offset().top+ 'px'
758         });
759     });
760 }
761
762 // calculateSpreadSize()
763 //______________________________________________________________________________
764 // Calculates 2-page spread dimensions based on this.currentLeafL and
765 // this.currentLeafR
766 // This function sets this.twoPageH, twoPageW, and twoPageRatio
767
768 GnuBook.prototype.calculateSpreadSize = function() {
769     var firstLeaf  = this.currentLeafL;
770     var secondLeaf = this.currentLeafR;
771     //console.log('first page is ' + firstLeaf);
772
773     var canon5Dratio = 1.5;
774     
775     var firstLeafRatio  = this.getPageHeight(firstLeaf) / this.getPageWidth(firstLeaf);
776     var secondLeafRatio = this.getPageHeight(secondLeaf) / this.getPageWidth(secondLeaf);
777     //console.log('firstLeafRatio = ' + firstLeafRatio + ' secondLeafRatio = ' + secondLeafRatio);
778
779     var ratio;
780     if (Math.abs(firstLeafRatio - canon5Dratio) < Math.abs(secondLeafRatio - canon5Dratio)) {
781         ratio = firstLeafRatio;
782         //console.log('using firstLeafRatio ' + ratio);
783     } else {
784         ratio = secondLeafRatio;
785         //console.log('using secondLeafRatio ' + ratio);
786     }
787
788     var totalLeafEdgeWidth = parseInt(this.numLeafs * 0.1);
789     var maxLeafEdgeWidth   = parseInt($('#GBcontainer').width() * 0.1);
790     totalLeafEdgeWidth     = Math.min(totalLeafEdgeWidth, maxLeafEdgeWidth);
791     
792     $('#GBcontainer').css('overflow', 'hidden');
793
794     var idealWidth  = ($('#GBcontainer').width() - 30 - totalLeafEdgeWidth)>>1;
795     var idealHeight = $('#GBcontainer').height() - 30;
796     //console.log('init idealWidth='+idealWidth+' idealHeight='+idealHeight + ' ratio='+ratio);
797
798     if (idealHeight/ratio <= idealWidth) {
799         //use height
800         idealWidth = parseInt(idealHeight/ratio);
801     } else {
802         //use width
803         idealHeight = parseInt(idealWidth*ratio);
804     }
805
806     this.twoPageH     = idealHeight;
807     this.twoPageW     = idealWidth;
808     this.twoPageRatio = ratio;
809     this.twoPageEdgeW = totalLeafEdgeWidth;    
810
811 }
812
813 // next()
814 //______________________________________________________________________________
815 GnuBook.prototype.next = function() {
816     if (2 == this.mode) {
817         this.autoStop();
818         this.flipFwdToIndex(null);
819     } else {
820         if (this.firstIndex <= (this.numLeafs - 2)) {
821             this.jumpToIndex(this.firstIndex+1);
822         }
823     }
824 }
825
826 // prev()
827 //______________________________________________________________________________
828 GnuBook.prototype.prev = function() {
829     if (2 == this.mode) {
830         this.autoStop();
831         this.flipBackToIndex(null);
832     } else {
833         if (this.firstIndex >= 1) {
834             this.jumpToIndex(this.firstIndex-1);
835         }    
836     }
837 }
838
839 GnuBook.prototype.home = function() {
840     if (2 == this.mode) {
841         this.jumpToIndex(2);
842     }
843     else {
844         this.jumpToIndex(0);
845     }
846 }
847
848 GnuBook.prototype.end = function() {
849     if (2 == this.mode) {
850         this.jumpToIndex(this.numLeafs-5);
851     }
852     else {
853         this.jumpToIndex(this.numLeafs-1);
854     }
855 }
856
857 // flipBackToIndex()
858 //______________________________________________________________________________
859 // to flip back one spread, pass index=null
860 GnuBook.prototype.flipBackToIndex = function(index) {
861     if (1 == this.mode) return;
862
863     var leftIndex = this.currentLeafL;
864     if (leftIndex <= 2) return;
865     if (this.animating) return;
866
867     if (null != this.leafEdgeTmp) {
868         alert('error: leafEdgeTmp should be null!');
869         return;
870     }
871     
872     if (null == index) {
873         index = leftIndex-2;
874     }
875     if (index<0) return;
876
877     if ('L' !=  this.getPageSide(index)) {
878         alert('img with index ' + index + ' is not a left-hand page');
879         return;
880     }
881
882     this.animating = true;
883     
884     var prevL = index;
885     var prevR = index+1;
886
887     var gutter= this.prepareFlipBack(prevL, prevR);
888
889     var leftLeaf = this.currentLeafL;
890
891     var oldLeafEdgeWidthL = parseInt( (this.currentLeafL/this.numLeafs)*this.twoPageEdgeW );
892     var newLeafEdgeWidthL = parseInt( (index            /this.numLeafs)*this.twoPageEdgeW );    
893     var leafEdgeTmpW = oldLeafEdgeWidthL - newLeafEdgeWidthL;
894
895     var scaledWL = this.getPageWidth2UP(prevL);
896
897     var top  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
898
899     this.leafEdgeTmp = document.createElement('div');
900     $(this.leafEdgeTmp).css({
901         borderStyle: 'solid none solid solid',
902         borderColor: 'rgb(51, 51, 34)',
903         borderWidth: '1px 0px 1px 1px',
904         background: 'transparent url(images/left-edges.png) repeat scroll 0% 0%',
905         width: leafEdgeTmpW + 'px',
906         height: this.twoPageH-1 + 'px',
907         left: gutter-scaledWL+10+newLeafEdgeWidthL+'px',
908         top: top+'px',    
909         position: 'absolute',
910         zIndex:1000
911     }).appendTo('#GBcontainer');
912     
913     //$(this.leafEdgeL).css('width', newLeafEdgeWidthL+'px');
914     $(this.leafEdgeL).css({
915         width: newLeafEdgeWidthL+'px', 
916         left: gutter-scaledWL-newLeafEdgeWidthL+'px'
917     });   
918
919     var left = $(this.prefetchedImgs[leftLeaf]).offset().left;
920     var right = $('#GBcontainer').width()-left-$(this.prefetchedImgs[leftLeaf]).width()+$('#GBcontainer').offset().left-2+'px';
921     $(this.prefetchedImgs[leftLeaf]).css({
922         right: right,
923         left: null
924     });
925
926      left = $(this.prefetchedImgs[leftLeaf]).offset().left - $('#book_div_1').offset().left;
927      right = left+$(this.prefetchedImgs[leftLeaf]).width()+'px';
928
929     $(this.leafEdgeTmp).animate({left: gutter}, this.flipSpeed, 'easeInSine');    
930     //$(this.prefetchedImgs[leftLeaf]).animate({width: '0px'}, 'slow', 'easeInSine');
931
932     var scaledWR = this.getPageWidth2UP(prevR);
933     
934     var self = this;
935
936     this.removeSearchHilites();
937
938     $(this.prefetchedImgs[leftLeaf]).animate({width: '0px'}, self.flipSpeed, 'easeInSine', function() {
939         $(self.leafEdgeTmp).animate({left: gutter+scaledWR+'px'}, self.flipSpeed, 'easeOutSine');    
940         $(self.prefetchedImgs[prevR]).animate({width: scaledWR+'px'}, self.flipSpeed, 'easeOutSine', function() {
941             $(self.prefetchedImgs[prevL]).css('zIndex', 2);
942
943             $(self.leafEdgeR).css({
944                 width: self.twoPageEdgeW-newLeafEdgeWidthL+'px',
945                 left:  gutter+scaledWR+'px'
946             });
947             
948             $(self.twoPageDiv).css({
949                 width: scaledWL+scaledWR+self.twoPageEdgeW+20+'px',
950                 left: gutter-scaledWL-newLeafEdgeWidthL-10+'px'
951             });
952             
953             $(self.leafEdgeTmp).remove();
954             self.leafEdgeTmp = null;
955             
956             self.currentLeafL = prevL;
957             self.currentLeafR = prevR;
958             self.displayedLeafs = [prevL, prevR];
959             self.setClickHandlers();
960             self.pruneUnusedImgs();
961             self.prefetch();
962             self.animating = false;
963             
964             self.updateSearchHilites2UP();
965             self.updatePageNumBox2UP();
966             //$('#GBzoom').text((self.twoPageH/self.getPageHeight(prevL)).toString().substr(0,4));            
967         });
968     });        
969     
970 }
971
972 // flipFwdToIndex()
973 //______________________________________________________________________________
974 // to flip forward one spread, pass index=null
975 GnuBook.prototype.flipFwdToIndex = function(index) {
976     var rightLeaf = this.currentLeafR;
977     if (rightLeaf >= this.numLeafs-3) return;
978
979     if (this.animating) return;
980
981     if (null != this.leafEdgeTmp) {
982         alert('error: leafEdgeTmp should be null!');
983         return;
984     }
985
986     
987     if (null == index) {
988         index = rightLeaf+2;
989     }
990     if (index>=this.numLeafs-3) return;
991
992     if ('R' !=  this.getPageSide(index)) {
993         alert('img with index ' + index + ' is not a right-hand page');
994         return;
995     }
996
997     this.animating = true;
998
999     var nextL = index-1;
1000     var nextR = index;
1001
1002     var gutter= this.prepareFlipFwd(nextL, nextR);
1003
1004     var oldLeafEdgeWidthL = parseInt( (this.currentLeafL/this.numLeafs)*this.twoPageEdgeW );
1005     var oldLeafEdgeWidthR = this.twoPageEdgeW-oldLeafEdgeWidthL;
1006     var newLeafEdgeWidthL = parseInt( (nextL            /this.numLeafs)*this.twoPageEdgeW );    
1007     var newLeafEdgeWidthR = this.twoPageEdgeW-newLeafEdgeWidthL;
1008
1009     var leafEdgeTmpW = oldLeafEdgeWidthR - newLeafEdgeWidthR;
1010
1011     var top  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
1012
1013     var height  = this.getPageHeight(rightLeaf); 
1014     var width   = this.getPageWidth(rightLeaf);    
1015     var scaledW = this.twoPageH*width/height;
1016
1017     var middle     = ($('#GBcontainer').width() >> 1);
1018     var currGutter = middle+parseInt((2*this.currentLeafL - this.numLeafs)*this.twoPageEdgeW/this.numLeafs/2);    
1019
1020     this.leafEdgeTmp = document.createElement('div');
1021     $(this.leafEdgeTmp).css({
1022         borderStyle: 'solid none solid solid',
1023         borderColor: 'rgb(51, 51, 34)',
1024         borderWidth: '1px 0px 1px 1px',
1025         background: 'transparent url(images/left-edges.png) repeat scroll 0% 0%',
1026         width: leafEdgeTmpW + 'px',
1027         height: this.twoPageH-1 + 'px',
1028         left: currGutter+scaledW+'px',
1029         top: top+'px',    
1030         position: 'absolute',
1031         zIndex:1000
1032     }).appendTo('#GBcontainer');
1033
1034     var scaledWR = this.getPageWidth2UP(nextR);
1035     $(this.leafEdgeR).css({width: newLeafEdgeWidthR+'px', left: gutter+scaledWR+'px' });
1036
1037     var scaledWL = this.getPageWidth2UP(nextL);
1038     
1039     var self = this;
1040
1041     var speed = this.flipSpeed;
1042
1043     this.removeSearchHilites();
1044     
1045     $(this.leafEdgeTmp).animate({left: gutter}, speed, 'easeInSine');    
1046     $(this.prefetchedImgs[rightLeaf]).animate({width: '0px'}, speed, 'easeInSine', function() {
1047         $(self.leafEdgeTmp).animate({left: gutter-scaledWL-leafEdgeTmpW+'px'}, speed, 'easeOutSine');    
1048         $(self.prefetchedImgs[nextL]).animate({width: scaledWL+'px'}, speed, 'easeOutSine', function() {
1049             $(self.prefetchedImgs[nextR]).css('zIndex', 2);
1050
1051             $(self.leafEdgeL).css({
1052                 width: newLeafEdgeWidthL+'px', 
1053                 left: gutter-scaledWL-newLeafEdgeWidthL+'px'
1054             });
1055             
1056             $(self.twoPageDiv).css({
1057                 width: scaledWL+scaledWR+self.twoPageEdgeW+20+'px',
1058                 left: gutter-scaledWL-newLeafEdgeWidthL-10+'px'
1059             });
1060             
1061             $(self.leafEdgeTmp).remove();
1062             self.leafEdgeTmp = null;
1063             
1064             self.currentLeafL = nextL;
1065             self.currentLeafR = nextR;
1066             self.displayedLeafs = [nextL, nextR];
1067             self.setClickHandlers();            
1068             self.pruneUnusedImgs();
1069             self.prefetch();
1070             self.animating = false;
1071
1072             self.updateSearchHilites2UP();
1073             self.updatePageNumBox2UP();
1074             //$('#GBzoom').text((self.twoPageH/self.getPageHeight(nextL)).toString().substr(0,4));
1075         });
1076     });
1077     
1078 }
1079
1080 // setClickHandlers
1081 //______________________________________________________________________________
1082 GnuBook.prototype.setClickHandlers = function() {
1083     var self = this;
1084     $(this.prefetchedImgs[this.currentLeafL]).click(function() {
1085         //self.prevPage();
1086         self.autoStop();
1087         self.flipBackToIndex(null);
1088     });
1089     $(this.prefetchedImgs[this.currentLeafR]).click(function() {
1090         //self.nextPage();'
1091         self.autoStop();
1092         self.flipFwdToIndex(null);        
1093     });
1094 }
1095
1096 // prefetchImg()
1097 //______________________________________________________________________________
1098 GnuBook.prototype.prefetchImg = function(leafNum) {
1099     if (undefined == this.prefetchedImgs[leafNum]) {        
1100         var img = document.createElement("img");
1101         img.src = this.getPageURI(leafNum);
1102         this.prefetchedImgs[leafNum] = img;
1103     }
1104 }
1105
1106
1107 // prepareFlipBack()
1108 //______________________________________________________________________________
1109 GnuBook.prototype.prepareFlipBack = function(prevL, prevR) {
1110
1111     this.prefetchImg(prevL);
1112     this.prefetchImg(prevR);
1113     
1114     var height  = this.getPageHeight(prevL); 
1115     var width   = this.getPageWidth(prevL);    
1116     var middle = ($('#GBcontainer').width() >> 1);
1117     var top  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
1118     var scaledW = this.twoPageH*width/height;
1119
1120     var gutter = middle+parseInt((2*prevL - this.numLeafs)*this.twoPageEdgeW/this.numLeafs/2);    
1121
1122     $(this.prefetchedImgs[prevL]).css({
1123         position: 'absolute',
1124         /*right:   middle+'px',*/
1125         left: gutter-scaledW+'px',
1126         top:    top+'px',
1127         backgroundColor: 'rgb(234, 226, 205)',
1128         height: this.twoPageH,
1129         width:  scaledW+'px',
1130         borderRight: '1px solid black',
1131         zIndex: 1
1132     });
1133
1134     $('#GBcontainer').append(this.prefetchedImgs[prevL]);
1135
1136     $(this.prefetchedImgs[prevR]).css({
1137         position: 'absolute',
1138         left:   gutter+'px',
1139         top:    top+'px',
1140         backgroundColor: 'rgb(234, 226, 205)',
1141         height: this.twoPageH,
1142         width:  '0px',
1143         borderLeft: '1px solid black',
1144         zIndex: 2
1145     });
1146
1147     $('#GBcontainer').append(this.prefetchedImgs[prevR]);
1148
1149
1150     return gutter;
1151             
1152 }
1153
1154 // prepareFlipFwd()
1155 //______________________________________________________________________________
1156 GnuBook.prototype.prepareFlipFwd = function(nextL, nextR) {
1157
1158     this.prefetchImg(nextL);
1159     this.prefetchImg(nextR);
1160
1161     var height  = this.getPageHeight(nextR); 
1162     var width   = this.getPageWidth(nextR);    
1163     var middle = ($('#GBcontainer').width() >> 1);
1164     var top  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
1165     var scaledW = this.twoPageH*width/height;
1166
1167     var gutter = middle+parseInt((2*nextL - this.numLeafs)*this.twoPageEdgeW/this.numLeafs/2);    
1168     
1169     $(this.prefetchedImgs[nextR]).css({
1170         position: 'absolute',
1171         left:   gutter+'px',
1172         top:    top+'px',
1173         backgroundColor: 'rgb(234, 226, 205)',
1174         height: this.twoPageH,
1175         width:  scaledW+'px',
1176         borderLeft: '1px solid black',
1177         zIndex: 1
1178     });
1179
1180     $('#GBcontainer').append(this.prefetchedImgs[nextR]);
1181
1182     height  = this.getPageHeight(nextL); 
1183     width   = this.getPageWidth(nextL);      
1184     scaledW = this.twoPageH*width/height;
1185
1186     $(this.prefetchedImgs[nextL]).css({
1187         position: 'absolute',
1188         right:   $('#GBcontainer').width()-gutter+'px',
1189         top:    top+'px',
1190         backgroundColor: 'rgb(234, 226, 205)',
1191         height: this.twoPageH,
1192         width:  0+'px',
1193         borderRight: '1px solid black',
1194         zIndex: 2
1195     });
1196
1197     $('#GBcontainer').append(this.prefetchedImgs[nextL]);    
1198
1199     return gutter;
1200             
1201 }
1202
1203 // getNextLeafs()
1204 //______________________________________________________________________________
1205 GnuBook.prototype.getNextLeafs = function(o) {
1206     //TODO: we might have two left or two right leafs in a row (damaged book)
1207     //For now, assume that leafs are contiguous.
1208     
1209     //return [this.currentLeafL+2, this.currentLeafL+3];
1210     o.L = this.currentLeafL+2;
1211     o.R = this.currentLeafL+3;
1212 }
1213
1214 // getprevLeafs()
1215 //______________________________________________________________________________
1216 GnuBook.prototype.getPrevLeafs = function(o) {
1217     //TODO: we might have two left or two right leafs in a row (damaged book)
1218     //For now, assume that leafs are contiguous.
1219     
1220     //return [this.currentLeafL-2, this.currentLeafL-1];
1221     o.L = this.currentLeafL-2;
1222     o.R = this.currentLeafL-1;
1223 }
1224
1225 // pruneUnusedImgs()
1226 //______________________________________________________________________________
1227 GnuBook.prototype.pruneUnusedImgs = function() {
1228     //console.log('current: ' + this.currentLeafL + ' ' + this.currentLeafR);
1229     for (var key in this.prefetchedImgs) {
1230         //console.log('key is ' + key);
1231         if ((key != this.currentLeafL) && (key != this.currentLeafR)) {
1232             //console.log('removing key '+ key);
1233             $(this.prefetchedImgs[key]).remove();
1234         }
1235         if ((key < this.currentLeafL-4) || (key > this.currentLeafR+4)) {
1236             //console.log('deleting key '+ key);
1237             delete this.prefetchedImgs[key];
1238         }
1239     }
1240 }
1241
1242 // prefetch()
1243 //______________________________________________________________________________
1244 GnuBook.prototype.prefetch = function() {
1245
1246     var lim = this.currentLeafL-4;
1247     var i;
1248     lim = Math.max(lim, 0);
1249     for (i = lim; i < this.currentLeafL; i++) {
1250         this.prefetchImg(i);
1251     }
1252     
1253     if (this.numLeafs > (this.currentLeafR+1)) {
1254         lim = Math.min(this.currentLeafR+4, this.numLeafs-1);
1255         for (i=this.currentLeafR+1; i<=lim; i++) {
1256             this.prefetchImg(i);
1257         }
1258     }
1259 }
1260
1261 // getPageWidth2UP()
1262 //______________________________________________________________________________
1263 GnuBook.prototype.getPageWidth2UP = function(index) {
1264     var height  = this.getPageHeight(index); 
1265     var width   = this.getPageWidth(index);    
1266     return Math.floor(this.twoPageH*width/height);
1267 }    
1268
1269 // search()
1270 //______________________________________________________________________________
1271 GnuBook.prototype.search = function(term) {
1272     $('#GnuBookSearchScript').remove();
1273         var script  = document.createElement("script");
1274         script.setAttribute('id', 'GnuBookSearchScript');
1275         script.setAttribute("type", "text/javascript");
1276         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');
1277         document.getElementsByTagName('head')[0].appendChild(script);
1278 }
1279
1280 // GBSearchCallback()
1281 //______________________________________________________________________________
1282 GnuBook.prototype.GBSearchCallback = function(txt) {
1283     //alert(txt);
1284     if (jQuery.browser.msie) {
1285         var dom=new ActiveXObject("Microsoft.XMLDOM");
1286         dom.async="false";
1287         dom.loadXML(txt);    
1288     } else {
1289         var parser = new DOMParser();
1290         var dom = parser.parseFromString(txt, "text/xml");    
1291     }
1292     
1293     $('#GnuBookSearchResults').empty();    
1294     $('#GnuBookSearchResults').append('<ul>');
1295     
1296     for (var key in this.searchResults) {
1297         if (null != this.searchResults[key].div) {
1298             $(this.searchResults[key].div).remove();
1299         }
1300         delete this.searchResults[key];
1301     }
1302     
1303     var pages = dom.getElementsByTagName('PAGE');
1304     
1305     if (0 == pages.length) {
1306         // $$$ it would be nice to echo the (sanitized) search result here
1307         $('#GnuBookSearchResults').append('<li>No search results found</li>');
1308     } else {    
1309         for (var i = 0; i < pages.length; i++){
1310             //console.log(pages[i].getAttribute('file').substr(1) +'-'+ parseInt(pages[i].getAttribute('file').substr(1), 10));
1311     
1312             
1313             var re = new RegExp (/_(\d{4})/);
1314             var reMatch = re.exec(pages[i].getAttribute('file'));
1315             var leafNum = parseInt(reMatch[1], 10);
1316             //var leafNum = parseInt(pages[i].getAttribute('file').substr(1), 10);
1317             
1318             var children = pages[i].childNodes;
1319             var context = '';
1320             for (var j=0; j<children.length; j++) {
1321                 //console.log(j + ' - ' + children[j].nodeName);
1322                 //console.log(children[j].firstChild.nodeValue);
1323                 if ('CONTEXT' == children[j].nodeName) {
1324                     context += children[j].firstChild.nodeValue;
1325                 } else if ('WORD' == children[j].nodeName) {
1326                     context += '<b>'+children[j].firstChild.nodeValue+'</b>';
1327                     
1328                     var index = this.leafNumToIndex(leafNum);
1329                     if (null != index) {
1330                         //coordinates are [left, bottom, right, top, [baseline]]
1331                         //we'll skip baseline for now...
1332                         var coords = children[j].getAttribute('coords').split(',',4);
1333                         if (4 == coords.length) {
1334                             this.searchResults[index] = {'l':coords[0], 'b':coords[1], 'r':coords[2], 't':coords[3], 'div':null};
1335                         }
1336                     }
1337                 }
1338             }
1339             //TODO: remove hardcoded instance name
1340             $('#GnuBookSearchResults').append('<li><b><a href="javascript:gb.jumpToIndex('+index+');">Leaf ' + leafNum + '</a></b> - ' + context+'</li>');
1341         }
1342     }
1343     $('#GnuBookSearchResults').append('</ul>');
1344
1345     this.updateSearchHilites();
1346 }
1347
1348 // updateSearchHilites()
1349 //______________________________________________________________________________
1350 GnuBook.prototype.updateSearchHilites = function() {
1351     if (2 == this.mode) {
1352         this.updateSearchHilites2UP();
1353     } else {
1354         this.updateSearchHilites1UP();
1355     }
1356 }
1357
1358 // showSearchHilites1UP()
1359 //______________________________________________________________________________
1360 GnuBook.prototype.updateSearchHilites1UP = function() {
1361
1362     for (var key in this.searchResults) {
1363         
1364         if (-1 != jQuery.inArray(parseInt(key), this.displayedLeafs)) {
1365             var result = this.searchResults[key];
1366             if(null == result.div) {
1367                 result.div = document.createElement('div');
1368                 $(result.div).attr('className', 'GnuBookSearchHilite').appendTo('#pagediv'+key);
1369                 //console.log('appending ' + key);
1370             }    
1371             $(result.div).css({
1372                 width:  (result.r-result.l)/this.reduce + 'px',
1373                 height: (result.b-result.t)/this.reduce + 'px',
1374                 left:   (result.l)/this.reduce + 'px',
1375                 top:    (result.t)/this.reduce +'px'
1376             });
1377
1378         } else {
1379             //console.log(key + ' not displayed');
1380             this.searchResults[key].div=null;
1381         }
1382     }
1383 }
1384
1385 // showSearchHilites2UP()
1386 //______________________________________________________________________________
1387 GnuBook.prototype.updateSearchHilites2UP = function() {
1388
1389     var middle = ($('#GBcontainer').width() >> 1);
1390
1391     for (var key in this.searchResults) {
1392         key = parseInt(key, 10);
1393         if (-1 != jQuery.inArray(key, this.displayedLeafs)) {
1394             var result = this.searchResults[key];
1395             if(null == result.div) {
1396                 result.div = document.createElement('div');
1397                 $(result.div).attr('className', 'GnuBookSearchHilite').css('zIndex', 3).appendTo('#GBcontainer');
1398                 //console.log('appending ' + key);
1399             }
1400
1401             var height = this.getPageHeight(key);
1402             var width  = this.getPageWidth(key)
1403             var reduce = this.twoPageH/height;
1404             var scaledW = parseInt(width*reduce);
1405             
1406             var gutter = middle+parseInt((2*this.currentLeafL - this.numLeafs)*this.twoPageEdgeW/this.numLeafs/2);
1407             
1408             if ('L' == this.getPageSide(key)) {
1409                 var pageL = gutter-scaledW;
1410             } else {
1411                 var pageL = gutter;
1412             }
1413             var pageT  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
1414                         
1415             $(result.div).css({
1416                 width:  (result.r-result.l)*reduce + 'px',
1417                 height: (result.b-result.t)*reduce + 'px',
1418                 left:   pageL+(result.l)*reduce + 'px',
1419                 top:    pageT+(result.t)*reduce +'px'
1420             });
1421
1422         } else {
1423             //console.log(key + ' not displayed');
1424             if (null != this.searchResults[key].div) {
1425                 //console.log('removing ' + key);
1426                 $(this.searchResults[key].div).remove();
1427             }
1428             this.searchResults[key].div=null;
1429         }
1430     }
1431 }
1432
1433 // removeSearchHilites()
1434 //______________________________________________________________________________
1435 GnuBook.prototype.removeSearchHilites = function() {
1436     for (var key in this.searchResults) {
1437         if (null != this.searchResults[key].div) {
1438             $(this.searchResults[key].div).remove();
1439             this.searchResults[key].div=null;
1440         }        
1441     }
1442 }
1443
1444 // showEmbedCode()
1445 //______________________________________________________________________________
1446 GnuBook.prototype.showEmbedCode = function() {
1447     if (null != this.embedPopup) { // check if already showing
1448         return;
1449     }
1450     this.autoStop();
1451     this.embedPopup = document.createElement("div");
1452     $(this.embedPopup).css({
1453         position: 'absolute',
1454         top:      '20px',
1455         left:     ($('#GBcontainer').width()-400)/2 + 'px',
1456         width:    '400px',
1457         padding:  "20px",
1458         border:   "3px double #999999",
1459         zIndex:   3,
1460         backgroundColor: "#fff"
1461     }).appendTo('#GnuBook');
1462
1463     htmlStr =  '<p style="text-align:center;"><b>Embed Bookreader in your blog!</b></p>';
1464     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>';
1465     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>';
1466     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>';
1467     htmlStr += '<p style="text-align:center;"><a href="" onclick="gb.embedPopup = null; $(this.parentNode.parentNode).remove(); return false">Close popup</a></p>';    
1468
1469     this.embedPopup.innerHTML = htmlStr;    
1470 }
1471
1472 // autoToggle()
1473 //______________________________________________________________________________
1474 GnuBook.prototype.autoToggle = function() {
1475     if (2 != this.mode) {
1476         this.switchMode(2);
1477     }
1478
1479     var self = this;
1480     if (null == this.autoTimer) {
1481         this.flipSpeed = 2000;
1482         this.flipFwdToIndex();
1483
1484         $('#autoImg').removeClass('play').addClass('pause');
1485         this.autoTimer=setInterval(function(){
1486             if (self.animating) {return;}
1487
1488             if (self.currentLeafR >= self.numLeafs-5) {
1489                 self.flipBackToIndex(1);
1490             } else {
1491                 self.flipFwdToIndex();
1492             }
1493         },5000);
1494     } else {
1495         this.autoStop();
1496     }
1497 }
1498
1499 // autoStop()
1500 //______________________________________________________________________________
1501 GnuBook.prototype.autoStop = function() {
1502     if (null != this.autoTimer) {
1503         clearInterval(this.autoTimer);
1504         this.flipSpeed = 'fast';
1505         $('#autoImg').removeClass('pause').addClass('play');
1506         this.autoTimer = null;
1507     }
1508 }
1509
1510 // keyboardNavigationIsDisabled(event)
1511 //   - returns true if keyboard navigation should be disabled for the event
1512 //______________________________________________________________________________
1513
1514 GnuBook.prototype.keyboardNavigationIsDisabled = function(event) {
1515     if (event.target.tagName == "INPUT") {
1516         return true;
1517     }   
1518     return false;
1519 }