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