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