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