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