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