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