Center two page view at resize. WIP
[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 //XXX
34 if (typeof(console) == 'undefined') {
35     console = { log: function(msg) { } };
36 }
37
38 function GnuBook() {
39     this.reduce  = 4;
40     this.padding = 10;
41     this.mode    = 1; //1 or 2
42     this.ui = 'full'; // UI mode
43     
44     this.displayedIndices = []; 
45     //this.indicesToDisplay = [];
46     this.imgs = {};
47     this.prefetchedImgs = {}; //an object with numeric keys cooresponding to page index
48     
49     this.timer     = null;
50     this.animating = false;
51     this.auto      = false;
52     this.autoTimer = null;
53     this.flipSpeed = 'fast';
54
55     this.twoPagePopUp = null;
56     this.leafEdgeTmp  = null;
57     this.embedPopup = null;
58     
59     this.searchResults = {};
60     
61     this.firstIndex = null;
62     
63     this.lastDisplayableIndex2up = null;
64     
65     // We link to index.php to avoid redirect which breaks back button
66     this.logoURL = 'http://www.archive.org/index.php';
67     
68     // Base URL for images
69     this.imagesBaseURL = '/bookreader/images/';
70     
71     // Mode constants
72     this.constMode1up = 1;
73     this.constMode2up = 2;
74     
75     // Object to hold parameters related to 2up mode
76     this.twoPage = {
77         coverInternalPadding: 10, // Width of cover
78         coverExternalPadding: 10, // Padding outside of cover
79         bookSpineDivWidth: 30,    // Width of book spine  $$$ consider sizing based on book length
80         autofit: true
81     };
82 };
83
84 // init()
85 //______________________________________________________________________________
86 GnuBook.prototype.init = function() {
87
88     var startIndex = undefined;
89     
90     // Find start index and mode if set in location hash
91     var params = this.paramsFromFragment(window.location.hash);
92         
93     if ('undefined' != typeof(params.index)) {
94         startIndex = params.index;
95     } else if ('undefined' != typeof(params.page)) {
96         startIndex = this.getPageIndex(params.page);
97     }
98     
99     if ('undefined' == typeof(startIndex)) {
100         if ('undefined' != typeof(this.titleLeaf)) {
101             startIndex = this.leafNumToIndex(this.titleLeaf);
102         }
103     }
104     
105     if ('undefined' == typeof(startIndex)) {
106         startIndex = 0;
107     }
108     
109     if ('undefined' != typeof(params.mode)) {
110         this.mode = params.mode;
111     }
112     
113     // Set document title -- may have already been set in enclosing html for
114     // search engine visibility
115     document.title = this.shortTitle(50);
116     
117     // Sanitize parameters
118     if ( !this.canSwitchToMode( this.mode ) ) {
119         this.mode = this.constMode1up;
120     }
121     
122     $("#GnuBook").empty();
123     this.initToolbar(this.mode, this.ui); // Build inside of toolbar div
124     $("#GnuBook").append("<div id='GBcontainer'></div>");
125     $("#GBcontainer").append("<div id='GBpageview'></div>");
126
127     $("#GBcontainer").bind('scroll', this, function(e) {
128         e.data.loadLeafs();
129     });
130
131     this.setupKeyListeners();
132     this.startLocationPolling();
133
134     $(window).bind('resize', this, function(e) {
135         //console.log('resize!');
136         if (1 == e.data.mode) {
137             //console.log('centering 1page view');
138             e.data.centerPageView();
139             $('#GBpageview').empty()
140             e.data.displayedIndices = [];
141             e.data.updateSearchHilites(); //deletes hilights but does not call remove()            
142             e.data.loadLeafs();
143         } else {
144             //console.log('drawing 2 page view');
145             e.data.prepareTwoPageView();
146         }
147     });
148     
149     $('.GBpagediv1up').bind('mousedown', this, function(e) {
150         //console.log('mousedown!');
151     });
152
153     if (1 == this.mode) {
154         this.resizePageView();
155         this.firstIndex = startIndex;
156         this.jumpToIndex(startIndex);
157     } else {
158         //this.resizePageView();
159         
160         this.displayedIndices=[0];
161         this.firstIndex = startIndex;
162         this.displayedIndices = [this.firstIndex];
163         //console.log('titleLeaf: %d', this.titleLeaf);
164         //console.log('displayedIndices: %s', this.displayedIndices);
165         this.prepareTwoPageView();
166         //if (this.auto) this.nextPage();
167     }
168         
169     // Enact other parts of initial params
170     this.updateFromParams(params);
171 }
172
173 GnuBook.prototype.setupKeyListeners = function() {
174     var self = this;
175
176     var KEY_PGUP = 33;
177     var KEY_PGDOWN = 34;
178     var KEY_END = 35;
179     var KEY_HOME = 36;
180
181     var KEY_LEFT = 37;
182     var KEY_UP = 38;
183     var KEY_RIGHT = 39;
184     var KEY_DOWN = 40;
185
186     // We use document here instead of window to avoid a bug in jQuery on IE7
187     $(document).keydown(function(e) {
188         
189         // Keyboard navigation        
190         switch(e.keyCode) {
191             case KEY_PGUP:
192             case KEY_UP:            
193                 // In 1up mode page scrolling is handled by browser
194                 if (2 == self.mode) {
195                     self.prev();
196                 }
197                 break;
198             case KEY_DOWN:
199             case KEY_PGDOWN:
200                 if (2 == self.mode) {
201                     self.next();
202                 }
203                 break;
204             case KEY_END:
205                 self.last();
206                 break;
207             case KEY_HOME:
208                 self.first();
209                 break;
210             case KEY_LEFT:
211                 if (self.keyboardNavigationIsDisabled(e)) {
212                     break;
213                 }
214                 if (2 == self.mode) {
215                     self.left();
216                 }
217                 break;
218             case KEY_RIGHT:
219                 if (self.keyboardNavigationIsDisabled(e)) {
220                     break;
221                 }
222                 if (2 == self.mode) {
223                     self.right();
224                 }
225                 break;
226         }
227     });
228 }
229
230 // drawLeafs()
231 //______________________________________________________________________________
232 GnuBook.prototype.drawLeafs = function() {
233     if (1 == this.mode) {
234         this.drawLeafsOnePage();
235     } else {
236         this.drawLeafsTwoPage();
237     }
238 }
239
240 // setDragHandler1up()
241 //______________________________________________________________________________
242 GnuBook.prototype.setDragHandler1up = function(div) {
243     div.dragging = false;
244
245     $(div).bind('mousedown', function(e) {
246         //console.log('mousedown at ' + e.pageY);
247
248         this.dragging = true;
249         this.prevMouseX = e.pageX;
250         this.prevMouseY = e.pageY;
251     
252         var startX    = e.pageX;
253         var startY    = e.pageY;
254         var startTop  = $('#GBcontainer').attr('scrollTop');
255         var startLeft =  $('#GBcontainer').attr('scrollLeft');
256
257         return false;
258     });
259         
260     $(div).bind('mousemove', function(ee) {
261         //console.log('mousemove ' + startY);
262         
263         var offsetX = ee.pageX - this.prevMouseX;
264         var offsetY = ee.pageY - this.prevMouseY;
265         
266         if (this.dragging) {
267             $('#GBcontainer').attr('scrollTop', $('#GBcontainer').attr('scrollTop') - offsetY);
268             $('#GBcontainer').attr('scrollLeft', $('#GBcontainer').attr('scrollLeft') - offsetX);
269         }
270         
271         this.prevMouseX = ee.pageX;
272         this.prevMouseY = ee.pageY;
273         
274         return false;
275     });
276     
277     $(div).bind('mouseup', function(ee) {
278         //console.log('mouseup');
279
280         this.dragging = false;
281         return false;
282     });
283     
284     $(div).bind('mouseleave', function(e) {
285         //console.log('mouseleave');
286
287         //$(this).unbind('mousemove mouseup');
288         this.dragging = false;
289         
290     });
291 }
292
293 // drawLeafsOnePage()
294 //______________________________________________________________________________
295 GnuBook.prototype.drawLeafsOnePage = function() {
296     //alert('drawing leafs!');
297     this.timer = null;
298
299
300     var scrollTop = $('#GBcontainer').attr('scrollTop');
301     var scrollBottom = scrollTop + $('#GBcontainer').height();
302     //console.log('top=' + scrollTop + ' bottom='+scrollBottom);
303     
304     var indicesToDisplay = [];
305     
306     var i;
307     var leafTop = 0;
308     var leafBottom = 0;
309     for (i=0; i<this.numLeafs; i++) {
310         var height  = parseInt(this.getPageHeight(i)/this.reduce); 
311     
312         leafBottom += height;
313         //console.log('leafTop = '+leafTop+ ' pageH = ' + this.pageH[i] + 'leafTop>=scrollTop=' + (leafTop>=scrollTop));
314         var topInView    = (leafTop >= scrollTop) && (leafTop <= scrollBottom);
315         var bottomInView = (leafBottom >= scrollTop) && (leafBottom <= scrollBottom);
316         var middleInView = (leafTop <=scrollTop) && (leafBottom>=scrollBottom);
317         if (topInView | bottomInView | middleInView) {
318             //console.log('displayed: ' + this.displayedIndices);
319             //console.log('to display: ' + i);
320             indicesToDisplay.push(i);
321         }
322         leafTop += height +10;      
323         leafBottom += 10;
324     }
325
326     var firstIndexToDraw  = indicesToDisplay[0];
327     this.firstIndex      = firstIndexToDraw;
328     
329     // Update hash, but only if we're currently displaying a leaf
330     // Hack that fixes #365790
331     if (this.displayedIndices.length > 0) {
332         this.updateLocationHash();
333     }
334
335     if ((0 != firstIndexToDraw) && (1 < this.reduce)) {
336         firstIndexToDraw--;
337         indicesToDisplay.unshift(firstIndexToDraw);
338     }
339     
340     var lastIndexToDraw = indicesToDisplay[indicesToDisplay.length-1];
341     if ( ((this.numLeafs-1) != lastIndexToDraw) && (1 < this.reduce) ) {
342         indicesToDisplay.push(lastIndexToDraw+1);
343     }
344     
345     leafTop = 0;
346     var i;
347     for (i=0; i<firstIndexToDraw; i++) {
348         leafTop += parseInt(this.getPageHeight(i)/this.reduce) +10;
349     }
350
351     //var viewWidth = $('#GBpageview').width(); //includes scroll bar width
352     var viewWidth = $('#GBcontainer').attr('scrollWidth');
353
354
355     for (i=0; i<indicesToDisplay.length; i++) {
356         var index = indicesToDisplay[i];    
357         var height  = parseInt(this.getPageHeight(index)/this.reduce); 
358
359         if(-1 == jQuery.inArray(indicesToDisplay[i], this.displayedIndices)) {            
360             var width   = parseInt(this.getPageWidth(index)/this.reduce); 
361             //console.log("displaying leaf " + indicesToDisplay[i] + ' leafTop=' +leafTop);
362             var div = document.createElement("div");
363             div.className = 'GBpagediv1up';
364             div.id = 'pagediv'+index;
365             div.style.position = "absolute";
366             $(div).css('top', leafTop + 'px');
367             var left = (viewWidth-width)>>1;
368             if (left<0) left = 0;
369             $(div).css('left', left+'px');
370             $(div).css('width', width+'px');
371             $(div).css('height', height+'px');
372             //$(div).text('loading...');
373             
374             this.setDragHandler1up(div);
375             
376             $('#GBpageview').append(div);
377
378             var img = document.createElement("img");
379             img.src = this.getPageURI(index);
380             $(img).css('width', width+'px');
381             $(img).css('height', height+'px');
382             $(div).append(img);
383
384         } else {
385             //console.log("not displaying " + indicesToDisplay[i] + ' score=' + jQuery.inArray(indicesToDisplay[i], this.displayedIndices));            
386         }
387
388         leafTop += height +10;
389
390     }
391     
392     for (i=0; i<this.displayedIndices.length; i++) {
393         if (-1 == jQuery.inArray(this.displayedIndices[i], indicesToDisplay)) {
394             var index = this.displayedIndices[i];
395             //console.log('Removing leaf ' + index);
396             //console.log('id='+'#pagediv'+index+ ' top = ' +$('#pagediv'+index).css('top'));
397             $('#pagediv'+index).remove();
398         } else {
399             //console.log('NOT Removing leaf ' + this.displayedIndices[i]);
400         }
401     }
402     
403     this.displayedIndices = indicesToDisplay.slice();
404     this.updateSearchHilites();
405     
406     if (null != this.getPageNum(firstIndexToDraw))  {
407         $("#GBpagenum").val(this.getPageNum(this.currentIndex()));
408     } else {
409         $("#GBpagenum").val('');
410     }
411             
412     this.updateToolbarZoom(this.reduce);
413     
414 }
415
416 // drawLeafsTwoPage()
417 //______________________________________________________________________________
418 GnuBook.prototype.drawLeafsTwoPage = function() {
419     console.log('drawing two leafs!'); // XXX
420     
421     var scrollTop = $('#GBtwopageview').attr('scrollTop');
422     var scrollBottom = scrollTop + $('#GBtwopageview').height();
423     
424     console.log('drawLeafsTwoPage: this.currrentLeafL ' + this.twoPage.currentIndexL); // XXX
425     
426     // XXX we should use calculated values in this.twoPage (recalc if necessary)
427     
428     var indexL = this.twoPage.currentIndexL;
429     var heightL  = this.getPageHeight(indexL); 
430     var widthL   = this.getPageWidth(indexL);
431
432     var leafEdgeWidthL = this.leafEdgeWidth(indexL);
433     var leafEdgeWidthR = this.twoPage.edgeWidth - leafEdgeWidthL;
434     //var bookCoverDivWidth = this.twoPage.width*2 + 20 + this.twoPage.edgeWidth; // $$$ hardcoded cover width
435     var bookCoverDivWidth = this.twoPage.bookCoverDivWidth;
436     //console.log(leafEdgeWidthL);
437
438     var middle = this.twoPage.middle; // $$$ getter instead?
439     var top = this.twoPageTop();
440     var bookCoverDivLeft = this.twoPage.bookCoverDivLeft;
441
442     // $$$ should get getwidth2up?
443     //var scaledWL = parseInt(this.twoPage.height*widthL/heightL);
444     var scaledWL = this.getPageWidth2UP(indexL);
445     var gutter = this.twoPageGutter();
446     
447     this.prefetchImg(indexL);
448     $(this.prefetchedImgs[indexL]).css({
449         position: 'absolute',
450         left: gutter-scaledWL+'px',
451         right: '',
452         top:    top+'px',
453         backgroundColor: 'rgb(234, 226, 205)',
454         height: this.twoPage.height +'px', // $$$ height forced the same for both pages
455         width:  scaledWL + 'px',
456         borderRight: '1px solid black',
457         zIndex: 2
458     }).appendTo('#GBtwopageview');
459
460
461     var indexR = this.twoPage.currentIndexR;
462     var heightR  = this.getPageHeight(indexR); 
463     var widthR   = this.getPageWidth(indexR);
464
465     // $$$ should use getwidth2up?
466     //var scaledWR = this.twoPage.height*widthR/heightR;
467     var scaledWR = this.getPageWidth2UP(indexR);
468     this.prefetchImg(indexR);
469     $(this.prefetchedImgs[indexR]).css({
470         position: 'absolute',
471         left:   gutter+'px',
472         right: '',
473         top:    top+'px',
474         backgroundColor: 'rgb(234, 226, 205)',
475         height: this.twoPage.height + 'px', // $$$ height forced the same for both pages
476         width:  scaledWR + 'px',
477         borderLeft: '1px solid black',
478         zIndex: 2
479     }).appendTo('#GBtwopageview');
480         
481
482     this.displayedIndices = [this.twoPage.currentIndexL, this.twoPage.currentIndexR];
483     this.setClickHandlers();
484
485     this.updatePageNumBox2UP();
486     this.updateToolbarZoom(this.reduce);
487 }
488
489 // updatePageNumBox2UP
490 //______________________________________________________________________________
491 GnuBook.prototype.updatePageNumBox2UP = function() {
492     if (null != this.getPageNum(this.twoPage.currentIndexL))  {
493         $("#GBpagenum").val(this.getPageNum(this.twoPage.currentIndexL));
494     } else {
495         $("#GBpagenum").val('');
496     }
497     this.updateLocationHash();
498 }
499
500 // loadLeafs()
501 //______________________________________________________________________________
502 GnuBook.prototype.loadLeafs = function() {
503
504
505     var self = this;
506     if (null == this.timer) {
507         this.timer=setTimeout(function(){self.drawLeafs()},250);
508     } else {
509         clearTimeout(this.timer);
510         this.timer=setTimeout(function(){self.drawLeafs()},250);    
511     }
512 }
513
514 // zoom(direction)
515 //
516 // Pass 1 to zoom in, anything else to zoom out
517 //______________________________________________________________________________
518 GnuBook.prototype.zoom = function(direction) {
519     switch (this.mode) {
520         case this.constMode1up:
521             return this.zoom1up(direction);
522         case this.constMode2up:
523             return this.zoom2up(direction);
524     }
525 }
526
527 // zoom1up(dir)
528 //______________________________________________________________________________
529 GnuBook.prototype.zoom1up = function(dir) {
530     if (2 == this.mode) {     //can only zoom in 1-page mode
531         this.switchMode(1);
532         return;
533     }
534     
535     // $$$ with flexible zoom we could "snap" to /2 page reductions
536     //     for better scaling
537     if (1 == dir) {
538         if (this.reduce <= 0.5) return;
539         this.reduce*=0.5;           //zoom in
540     } else {
541         if (this.reduce >= 8) return;
542         this.reduce*=2;             //zoom out
543     }
544     
545     this.resizePageView();
546
547     $('#GBpageview').empty()
548     this.displayedIndices = [];
549     this.loadLeafs();
550     
551     this.updateToolbarZoom(this.reduce);
552 }
553
554 // resizePageView()
555 //______________________________________________________________________________
556 GnuBook.prototype.resizePageView = function() {
557     var i;
558     var viewHeight = 0;
559     //var viewWidth  = $('#GBcontainer').width(); //includes scrollBar
560     var viewWidth  = $('#GBcontainer').attr('clientWidth');   
561
562     var oldScrollTop  = $('#GBcontainer').attr('scrollTop');
563     var oldScrollLeft = $('#GBcontainer').attr('scrollLeft');
564     var oldPageViewHeight = $('#GBpageview').height();
565     var oldPageViewWidth = $('#GBpageview').width();
566     
567     var oldCenterY = this.centerY1up();
568     var oldCenterX = this.centerX1up();
569     
570     if (0 != oldPageViewHeight) {
571         var scrollRatio = oldCenterY / oldPageViewHeight;
572     } else {
573         var scrollRatio = 0;
574     }
575     
576     for (i=0; i<this.numLeafs; i++) {
577         viewHeight += parseInt(this.getPageHeight(i)/this.reduce) + this.padding; 
578         var width = parseInt(this.getPageWidth(i)/this.reduce);
579         if (width>viewWidth) viewWidth=width;
580     }
581     $('#GBpageview').height(viewHeight);
582     $('#GBpageview').width(viewWidth);    
583
584     var newCenterY = scrollRatio*viewHeight;
585     var newTop = Math.max(0, Math.floor( newCenterY - $('#GBcontainer').height()/2 ));
586     $('#GBcontainer').attr('scrollTop', newTop);
587     
588     // We use clientWidth here to avoid miscalculating due to scroll bar
589     var newCenterX = oldCenterX * (viewWidth / oldPageViewWidth);
590     var newLeft = newCenterX - $('#GBcontainer').attr('clientWidth') / 2;
591     newLeft = Math.max(newLeft, 0);
592     $('#GBcontainer').attr('scrollLeft', newLeft);
593     //console.log('oldCenterX ' + oldCenterX + ' newCenterX ' + newCenterX + ' newLeft ' + newLeft);
594     
595     //this.centerPageView();
596     this.loadLeafs();
597     
598 }
599
600 // centerX1up()
601 //______________________________________________________________________________
602 // Returns the current offset of the viewport center in scaled document coordinates.
603 GnuBook.prototype.centerX1up = function() {
604     var centerX;
605     if ($('#GBpageview').width() < $('#GBcontainer').attr('clientWidth')) { // fully shown
606         centerX = $('#GBpageview').width();
607     } else {
608         centerX = $('#GBcontainer').attr('scrollLeft') + $('#GBcontainer').attr('clientWidth') / 2;
609     }
610     centerX = Math.floor(centerX);
611     return centerX;
612 }
613
614 // centerY1up()
615 //______________________________________________________________________________
616 // Returns the current offset of the viewport center in scaled document coordinates.
617 GnuBook.prototype.centerY1up = function() {
618     var centerY = $('#GBcontainer').attr('scrollTop') + $('#GBcontainer').height() / 2;
619     return Math.floor(centerY);
620 }
621
622 // centerPageView()
623 //______________________________________________________________________________
624 GnuBook.prototype.centerPageView = function() {
625
626     var scrollWidth  = $('#GBcontainer').attr('scrollWidth');
627     var clientWidth  =  $('#GBcontainer').attr('clientWidth');
628     //console.log('sW='+scrollWidth+' cW='+clientWidth);
629     if (scrollWidth > clientWidth) {
630         $('#GBcontainer').attr('scrollLeft', (scrollWidth-clientWidth)/2);
631     }
632
633 }
634
635 // zoom2up(direction)
636 //______________________________________________________________________________
637 GnuBook.prototype.zoom2up = function(direction) {
638     // $$$ this is where we can e.g. snap to %2 sizes
639     if (0 == direction) { // autofit mode
640         this.twoPage.autofit = true;;
641     } else if (1 == direction) {
642         if (this.reduce <= 0.5) return;
643         this.reduce*=0.5;           //zoom in
644         this.twoPage.autofit = false;
645     } else {
646         if (this.reduce >= 8) return;
647         this.reduce *= 2; // zoom out
648         this.twoPage.autofit = false;
649     }
650     
651     this.prepareTwoPageView();
652 }
653
654 // jumpToPage()
655 //______________________________________________________________________________
656 // Attempts to jump to page.  Returns true if page could be found, false otherwise.
657 GnuBook.prototype.jumpToPage = function(pageNum) {
658
659     var pageIndex = this.getPageIndex(pageNum);
660
661     if ('undefined' != typeof(pageIndex)) {
662         var leafTop = 0;
663         var h;
664         this.jumpToIndex(pageIndex);
665         $('#GBcontainer').attr('scrollTop', leafTop);
666         return true;
667     }
668     
669     // Page not found
670     return false;
671 }
672
673 // jumpToIndex()
674 //______________________________________________________________________________
675 GnuBook.prototype.jumpToIndex = function(index) {
676
677     if (2 == this.mode) {
678         this.autoStop();
679         
680         // By checking against min/max we do nothing if requested index
681         // is current
682         if (index < Math.min(this.twoPage.currentIndexL, this.twoPage.currentIndexR)) {
683             this.flipBackToIndex(index);
684         } else if (index > Math.max(this.twoPage.currentIndexL, this.twoPage.currentIndexR)) {
685             this.flipFwdToIndex(index);
686         }
687
688     } else {        
689         var i;
690         var leafTop = 0;
691         var h;
692         for (i=0; i<index; i++) {
693             h = parseInt(this.getPageHeight(i)/this.reduce); 
694             leafTop += h + this.padding;
695         }
696         //$('#GBcontainer').attr('scrollTop', leafTop);
697         $('#GBcontainer').animate({scrollTop: leafTop },'fast');
698     }
699 }
700
701
702
703 // switchMode()
704 //______________________________________________________________________________
705 GnuBook.prototype.switchMode = function(mode) {
706
707     //console.log('  asked to switch to mode ' + mode + ' from ' + this.mode);
708     
709     if (mode == this.mode) return;
710     
711     if (!this.canSwitchToMode(mode)) {
712         return;
713     }
714
715     this.autoStop();
716     this.removeSearchHilites();
717
718     this.mode = mode;
719     
720     this.switchToolbarMode(mode);
721     
722     if (1 == mode) {
723         this.prepareOnePageView();
724     } else {
725         this.prepareTwoPageView();
726     }
727
728 }
729
730 //prepareOnePageView()
731 //______________________________________________________________________________
732 GnuBook.prototype.prepareOnePageView = function() {
733
734     // var startLeaf = this.displayedIndices[0];
735     var startLeaf = this.currentIndex();
736     
737     $('#GBcontainer').empty();
738     $('#GBcontainer').css({
739         overflowY: 'scroll',
740         overflowX: 'auto'
741     });
742     
743     var gbPageView = $("#GBcontainer").append("<div id='GBpageview'></div>");
744     
745     this.resizePageView();
746     
747     this.jumpToIndex(startLeaf);
748     this.displayedIndices = [];
749     
750     this.drawLeafsOnePage();
751         
752     // Bind mouse handlers
753     // Disable mouse click to avoid selected/highlighted page images - bug 354239
754     gbPageView.bind('mousedown', function(e) {
755         return false;
756     })
757     // Special hack for IE7
758     gbPageView[0].onselectstart = function(e) { return false; };
759 }
760
761 // prepareTwoPageView()
762 //______________________________________________________________________________
763 // Some decisions about two page view:
764 //
765 // Both pages will be displayed at the same height, even if they were different physical/scanned
766 // sizes.  This simplifies the animation (from a design as well as technical standpoint).  We
767 // examine the page aspect ratios (in calculateSpreadSize) and use the page with the most "normal"
768 // aspect ratio to determine the height.
769 //
770 // The two page view div is resized to keep the middle of the book in the middle of the div
771 // even as the page sizes change.  To e.g. keep the middle of the book in the middle of the GBcontent
772 // div requires adjusting the offset of GBtwpageview and/or scrolling in GBcontent.
773 GnuBook.prototype.prepareTwoPageView = function() {
774     $('#GBcontainer').empty();
775     $('#GBcontainer').css('overflow', 'auto');
776
777     // Add the two page view
778     $('#GBcontainer').append('<div id="GBtwopageview"></div>');
779     // Explicitly set sizes the same
780     // $$$ calculate first then set
781     $('#GBtwopageview').css( {
782         height: $('#GBcontainer').height(),
783         width: $('#GBcontainer').width(),
784         position: 'absolute'
785         });
786         
787     // We want to display two facing pages.  We may be missing
788     // one side of the spread because it is the first/last leaf,
789     // foldouts, missing pages, etc
790
791     //var targetLeaf = this.displayedIndices[0];
792     var targetLeaf = this.firstIndex;
793
794     if (targetLeaf < this.firstDisplayableIndex()) {
795         targetLeaf = this.firstDisplayableIndex();
796     }
797     
798     if (targetLeaf > this.lastDisplayableIndex()) {
799         targetLeaf = this.lastDisplayableIndex();
800     }
801     
802     this.twoPage.currentIndexL = null;
803     this.twoPage.currentIndexR = null;
804     this.pruneUnusedImgs();
805     
806     var currentSpreadIndices = this.getSpreadIndices(targetLeaf);
807     this.twoPage.currentIndexL = currentSpreadIndices[0];
808     this.twoPage.currentIndexR = currentSpreadIndices[1];
809     this.firstIndex = this.twoPage.currentIndexL;
810     
811     this.calculateSpreadSize(); //sets twoPage.width, twoPage.height
812         
813     //console.dir(this.twoPage);
814     
815     // $$$ May need to account for scroll bars here
816     $('#GBtwopageview').width(this.twoPage.totalWidth).height(this.twoPage.totalHeight);
817     
818     // XXX shouldn't always center
819     this.twoPageCenterView();
820     
821     this.twoPage.coverDiv = document.createElement('div');
822     $(this.twoPage.coverDiv).attr('id', 'GBbookcover').css({
823         border: '1px solid rgb(68, 25, 17)',
824         width:  this.twoPage.bookCoverDivWidth + 'px',
825         height: this.twoPage.bookCoverDivHeight+'px',
826         visibility: 'visible',
827         position: 'absolute',
828         backgroundColor: '#663929',
829         left: this.twoPage.bookCoverDivLeft + 'px',
830         top: this.twoPage.bookCoverDivTop+'px',
831         MozBorderRadiusTopleft: '7px',
832         MozBorderRadiusTopright: '7px',
833         MozBorderRadiusBottomright: '7px',
834         MozBorderRadiusBottomleft: '7px'
835     }).appendTo('#GBtwopageview');
836     
837     this.leafEdgeR = document.createElement('div');
838     this.leafEdgeR.className = 'leafEdgeR'; // $$$ the static CSS should be moved into the .css file
839     $(this.leafEdgeR).css({
840         borderStyle: 'solid solid solid none',
841         borderColor: 'rgb(51, 51, 34)',
842         borderWidth: '1px 1px 1px 0px',
843         background: 'transparent url(' + this.imagesBaseURL + 'right_edges.png) repeat scroll 0% 0%',
844         width: this.twoPage.leafEdgeWidthR + 'px',
845         height: this.twoPage.height-1 + 'px',
846         /*right: '10px',*/
847         left: this.twoPage.gutter+this.twoPage.scaledWR+'px',
848         top: this.twoPage.bookCoverDivTop+this.twoPage.coverInternalPadding+'px',
849         position: 'absolute'
850     }).appendTo('#GBtwopageview');
851     
852     this.leafEdgeL = document.createElement('div');
853     this.leafEdgeL.className = 'leafEdgeL';
854     $(this.leafEdgeL).css({ // $$$ static CSS should be moved to file
855         borderStyle: 'solid none solid solid',
856         borderColor: 'rgb(51, 51, 34)',
857         borderWidth: '1px 0px 1px 1px',
858         background: 'transparent url(' + this.imagesBaseURL + 'left_edges.png) repeat scroll 0% 0%',
859         width: this.twoPage.leafEdgeWidthL + 'px',
860         height: this.twoPage.height-1 + 'px',
861         left: this.twoPage.bookCoverDivLeft+this.twoPage.coverInternalPadding+'px',
862         top: this.twoPage.bookCoverDivTop+this.twoPage.coverInternalPadding+'px',    
863         position: 'absolute'
864     }).appendTo('#GBtwopageview');
865
866     div = document.createElement('div');
867     $(div).attr('id', 'GBbookspine').css({
868         border:          '1px solid rgb(68, 25, 17)',
869         width:           this.twoPage.bookSpineDivWidth+'px',
870         height:          this.twoPage.bookSpineDivHeight+'px',
871         position:        'absolute',
872         backgroundColor: 'rgb(68, 25, 17)',
873         left:            this.twoPage.bookSpineDivLeft+'px',
874         top:             this.twoPage.bookSpineDivTop+'px'
875     }).appendTo('#GBtwopageview');
876
877     this.prepareTwoPagePopUp();
878
879     this.displayedIndices = [];
880     
881     //this.indicesToDisplay=[firstLeaf, firstLeaf+1];
882     //console.log('indicesToDisplay: ' + this.indicesToDisplay[0] + ' ' + this.indicesToDisplay[1]);
883     
884     this.drawLeafsTwoPage();
885     this.updateSearchHilites2UP();
886     this.updateToolbarZoom(this.reduce);
887     
888     this.prefetch();
889 }
890
891 // prepareTwoPagePopUp()
892 //
893 // This function prepares the "View Page n" popup that shows while the mouse is
894 // over the left/right "stack of sheets" edges.  It also binds the mouse
895 // events for these divs.
896 //______________________________________________________________________________
897 GnuBook.prototype.prepareTwoPagePopUp = function() {
898
899     this.twoPagePopUp = document.createElement('div');
900     $(this.twoPagePopUp).css({
901         border: '1px solid black',
902         padding: '2px 6px',
903         position: 'absolute',
904         fontFamily: 'sans-serif',
905         fontSize: '14px',
906         zIndex: '1000',
907         backgroundColor: 'rgb(255, 255, 238)',
908         opacity: 0.85
909     }).appendTo('#GBcontainer');
910     $(this.twoPagePopUp).hide();
911     
912     $(this.leafEdgeL).add(this.leafEdgeR).bind('mouseenter', this, function(e) {
913         $(e.data.twoPagePopUp).show();
914     });
915
916     $(this.leafEdgeL).add(this.leafEdgeR).bind('mouseleave', this, function(e) {
917         $(e.data.twoPagePopUp).hide();
918     });
919
920     $(this.leafEdgeL).bind('click', this, function(e) { 
921         e.data.autoStop();
922         var jumpIndex = e.data.jumpIndexForLeftEdgePageX(e.pageX);
923         e.data.jumpToIndex(jumpIndex);
924     });
925
926     $(this.leafEdgeR).bind('click', this, function(e) { 
927         e.data.autoStop();
928         var jumpIndex = e.data.jumpIndexForRightEdgePageX(e.pageX);
929         e.data.jumpToIndex(jumpIndex);    
930     });
931
932     $(this.leafEdgeR).bind('mousemove', this, function(e) {
933
934         var jumpIndex = e.data.jumpIndexForRightEdgePageX(e.pageX);
935         $(e.data.twoPagePopUp).text('View ' + e.data.getPageName(jumpIndex));
936         
937         // $$$ TODO: Make sure popup is positioned so that it is in view
938         // (https://bugs.edge.launchpad.net/gnubook/+bug/327456)        
939         $(e.data.twoPagePopUp).css({
940             left: e.pageX- $('#GBcontainer').offset().left + $('#GBcontainer').scrollLeft() + 20 + 'px',
941             top: e.pageY - $('#GBcontainer').offset().top + $('#GBcontainer').scrollTop() + 'px'
942         });
943     });
944
945     $(this.leafEdgeL).bind('mousemove', this, function(e) {
946     
947         var jumpIndex = e.data.jumpIndexForLeftEdgePageX(e.pageX);
948         $(e.data.twoPagePopUp).text('View '+ e.data.getPageName(jumpIndex));
949
950         // $$$ TODO: Make sure popup is positioned so that it is in view
951         //           (https://bugs.edge.launchpad.net/gnubook/+bug/327456)        
952         $(e.data.twoPagePopUp).css({
953             left: e.pageX - $('#GBcontainer').offset().left + $('#GBcontainer').scrollLeft() - $(e.data.twoPagePopUp).width() - 25 + 'px',
954             top: e.pageY-$('#GBcontainer').offset().top + $('#GBcontainer').scrollTop() + 'px'
955         });
956     });
957 }
958
959 // calculateSpreadSize()
960 //______________________________________________________________________________
961 // Calculates 2-page spread dimensions based on this.twoPage.currentIndexL and
962 // this.twoPage.currentIndexR
963 // This function sets this.twoPage.height, twoPage.width
964
965 GnuBook.prototype.calculateSpreadSize = function() {
966     console.log('calculateSpreadSize ' + this.twoPage.currentIndexL); // XXX
967
968     // $$$ TODO Calculate the spread size based on the reduction factor.  If we are using
969     // fit mode we recalculate the reduction factor based on the current page sizes
970     // and display size first.
971     
972     var firstIndex  = this.twoPage.currentIndexL;
973     var secondIndex = this.twoPage.currentIndexR;
974     //console.log('first page is ' + firstIndex);
975
976     // Calculate page sizes and total leaf width
977     var spreadSize;
978     if ( this.twoPage.autofit) {    
979         spreadSize = this.getIdealSpreadSize(firstIndex, secondIndex);
980     } else {
981         // set based on reduction factor
982         spreadSize = this.getSpreadSizeFromReduce(firstIndex, secondIndex, this.reduce);
983     }
984     
985     // Both pages together
986     this.twoPage.height = spreadSize.height;
987     this.twoPage.width = spreadSize.width;
988     
989     // Individual pages
990     this.twoPage.scaledWL = this.getPageWidth2UP(firstIndex);
991     this.twoPage.scaledWR = this.getPageWidth2UP(secondIndex);
992     
993     // Leaf edges
994     this.twoPage.leafEdgeWidthL = this.leafEdgeWidth(this.twoPage.currentIndexL);
995     this.twoPage.leafEdgeWidthR = this.twoPage.edgeWidth - this.twoPage.leafEdgeWidthL;
996     this.twoPage.edgeWidth = spreadSize.totalLeafEdgeWidth; // The combined width of both edges
997     
998     
999     // Book cover
1000     // The width of the book cover div.  The combined width of both pages, twice the width
1001     // of the book cover internal padding (2*10) and the page edges
1002     this.twoPage.bookCoverDivWidth = this.twoPage.scaledWL + this.twoPage.scaledWR + 2 * this.twoPage.coverInternalPadding + this.twoPage.edgeWidth;
1003     // The height of the book cover div
1004     this.twoPage.bookCoverDivHeight = this.twoPage.height + 2 * this.twoPage.coverInternalPadding;
1005     
1006     
1007     // We calculate the total width and height for the div so that we can make the book
1008     // spine centered
1009     var leftGutterOffset = this.gutterOffsetForIndex(firstIndex);
1010     var leftWidthFromCenter = this.twoPage.scaledWL + leftGutterOffset + this.twoPage.leafEdgeWidthL;
1011     var rightWidthFromCenter = this.twoPage.scaledWR - leftGutterOffset + this.twoPage.leafEdgeWidthR;
1012     var largestWidthFromCenter = Math.max( leftWidthFromCenter, rightWidthFromCenter );
1013     this.twoPage.totalWidth = 2 * (largestWidthFromCenter + this.twoPage.coverInternalPadding + this.twoPage.coverExternalPadding);
1014     this.twoPage.totalHeight = this.twoPage.height + 2 * (this.twoPage.coverInternalPadding + this.twoPage.coverExternalPadding);
1015         
1016     // We want to minimize the unused space in two-up mode (maximize the amount of page
1017     // shown).  We give width to the leaf edges and these widths change (though the sum
1018     // of the two remains constant) as we flip through the book.  With the book
1019     // cover centered and fixed in the GBcontainer div the page images will meet
1020     // at the "gutter" which is generally offset from the center.
1021     this.twoPage.middle = this.twoPage.totalWidth >> 1;
1022     this.twoPage.gutter = this.twoPage.middle + this.gutterOffsetForIndex(firstIndex);
1023     
1024     // The left edge of the book cover moves depending on the width of the pages
1025     // $$$ change to getter
1026     this.twoPage.bookCoverDivLeft = this.twoPage.gutter - this.twoPage.scaledWL - this.twoPage.leafEdgeWidthL - this.twoPage.coverInternalPadding;
1027     // The top edge of the book cover stays a fixed distance from the top
1028     this.twoPage.bookCoverDivTop = this.twoPage.coverExternalPadding;
1029
1030     // Book spine
1031     this.twoPage.bookSpineDivHeight = this.twoPage.height + 2*this.twoPage.coverInternalPadding;
1032     this.twoPage.bookSpineDivLeft = this.twoPage.middle - (this.twoPage.bookSpineDivWidth >> 1);
1033     this.twoPage.bookSpineDivTop = this.twoPage.bookCoverDivTop;
1034
1035
1036     this.reduce = spreadSize.reduce; // $$$ really set this here?
1037 }
1038
1039 GnuBook.prototype.getIdealSpreadSize = function(firstIndex, secondIndex) {
1040     var ideal = {};
1041
1042     // We check which page is closest to a "normal" page and use that to set the height
1043     // for both pages.  This means that foldouts and other odd size pages will be displayed
1044     // smaller than the nominal zoom amount.
1045     var canon5Dratio = 1.5;
1046     
1047     var first = {
1048         height: this.getPageHeight(firstIndex),
1049         width: this.getPageWidth(firstIndex)
1050     }
1051     
1052     var second = {
1053         height: this.getPageHeight(secondIndex),
1054         width: this.getPageWidth(secondIndex)
1055     }
1056     
1057     var firstIndexRatio  = first.height / first.width;
1058     var secondIndexRatio = second.height / second.width;
1059     //console.log('firstIndexRatio = ' + firstIndexRatio + ' secondIndexRatio = ' + secondIndexRatio);
1060
1061     var ratio;
1062     if (Math.abs(firstIndexRatio - canon5Dratio) < Math.abs(secondIndexRatio - canon5Dratio)) {
1063         ratio = firstIndexRatio;
1064         //console.log('using firstIndexRatio ' + ratio);
1065     } else {
1066         ratio = secondIndexRatio;
1067         //console.log('using secondIndexRatio ' + ratio);
1068     }
1069
1070     var totalLeafEdgeWidth = parseInt(this.numLeafs * 0.1);
1071     var maxLeafEdgeWidth   = parseInt($('#GBcontainer').attr('clientWidth') * 0.1);
1072     ideal.totalLeafEdgeWidth     = Math.min(totalLeafEdgeWidth, maxLeafEdgeWidth);
1073     
1074     ideal.width  = ($('#GBcontainer').attr('clientWidth') - 30 - ideal.totalLeafEdgeWidth)>>1;
1075     ideal.height = $('#GBcontainer').height() - 30;  // $$$ why - 30?  book edge width?
1076     //console.log('init idealWidth='+idealWidth+' idealHeight='+idealHeight + ' ratio='+ratio);
1077
1078     if (ideal.height/ratio <= ideal.width) {
1079         //use height
1080         ideal.width = parseInt(ideal.height/ratio);
1081     } else {
1082         //use width
1083         ideal.height = parseInt(ideal.width*ratio);
1084     }
1085     
1086     // XXX check this logic with large spreads
1087     ideal.reduce = ((first.height + second.height) / 2) / ideal.height;
1088     
1089     return ideal;
1090 }
1091
1092 GnuBook.prototype.getSpreadSizeFromReduce = function(firstIndex, secondIndex, reduce) {
1093     var spreadSize = {};
1094     // $$$ Scale this based on reduce?
1095     var totalLeafEdgeWidth = parseInt(this.numLeafs * 0.1);
1096     var maxLeafEdgeWidth   = parseInt($('#GBcontainer').attr('clientWidth') * 0.1); // XXX update
1097     spreadSize.totalLeafEdgeWidth     = Math.min(totalLeafEdgeWidth, maxLeafEdgeWidth);
1098
1099     // XXX incorrect -- we should make height "dominant"
1100     var nativeWidth = this.getPageWidth(firstIndex) + this.getPageWidth(secondIndex);
1101     var nativeHeight = this.getPageHeight(firstIndex) + this.getPageHeight(secondIndex);
1102     spreadSize.height = parseInt( (nativeHeight / 2) / this.reduce );
1103     spreadSize.width = parseInt( (nativeWidth / 2) / this.reduce );
1104     spreadSize.reduce = reduce;
1105     
1106     // XXX
1107     console.log('spread size: ' + firstIndex + ',' + secondIndex + ',' + reduce);
1108
1109     return spreadSize;
1110 }
1111
1112 // currentIndex()
1113 //______________________________________________________________________________
1114 // Returns the currently active index.
1115 GnuBook.prototype.currentIndex = function() {
1116     // $$$ we should be cleaner with our idea of which index is active in 1up/2up
1117     if (this.mode == this.constMode1up || this.mode == this.constMode2up) {
1118         return this.firstIndex;
1119     } else {
1120         throw 'currentIndex called for unimplemented mode ' + this.mode;
1121     }
1122 }
1123
1124 // right()
1125 //______________________________________________________________________________
1126 // Flip the right page over onto the left
1127 GnuBook.prototype.right = function() {
1128     if ('rl' != this.pageProgression) {
1129         // LTR
1130         gb.next();
1131     } else {
1132         // RTL
1133         gb.prev();
1134     }
1135 }
1136
1137 // rightmost()
1138 //______________________________________________________________________________
1139 // Flip to the rightmost page
1140 GnuBook.prototype.rightmost = function() {
1141     if ('rl' != this.pageProgression) {
1142         gb.last();
1143     } else {
1144         gb.first();
1145     }
1146 }
1147
1148 // left()
1149 //______________________________________________________________________________
1150 // Flip the left page over onto the right.
1151 GnuBook.prototype.left = function() {
1152     if ('rl' != this.pageProgression) {
1153         // LTR
1154         gb.prev();
1155     } else {
1156         // RTL
1157         gb.next();
1158     }
1159 }
1160
1161 // leftmost()
1162 //______________________________________________________________________________
1163 // Flip to the leftmost page
1164 GnuBook.prototype.leftmost = function() {
1165     if ('rl' != this.pageProgression) {
1166         gb.first();
1167     } else {
1168         gb.last();
1169     }
1170 }
1171
1172 // next()
1173 //______________________________________________________________________________
1174 GnuBook.prototype.next = function() {
1175     if (2 == this.mode) {
1176         this.autoStop();
1177         this.flipFwdToIndex(null);
1178     } else {
1179         if (this.firstIndex < this.lastDisplayableIndex()) {
1180             this.jumpToIndex(this.firstIndex+1);
1181         }
1182     }
1183 }
1184
1185 // prev()
1186 //______________________________________________________________________________
1187 GnuBook.prototype.prev = function() {
1188     if (2 == this.mode) {
1189         this.autoStop();
1190         this.flipBackToIndex(null);
1191     } else {
1192         if (this.firstIndex >= 1) {
1193             this.jumpToIndex(this.firstIndex-1);
1194         }    
1195     }
1196 }
1197
1198 GnuBook.prototype.first = function() {
1199     if (2 == this.mode) {
1200         this.jumpToIndex(2);
1201     }
1202     else {
1203         this.jumpToIndex(0);
1204     }
1205 }
1206
1207 GnuBook.prototype.last = function() {
1208     if (2 == this.mode) {
1209         this.jumpToIndex(this.lastDisplayableIndex());
1210     }
1211     else {
1212         this.jumpToIndex(this.lastDisplayableIndex());
1213     }
1214 }
1215
1216 // flipBackToIndex()
1217 //______________________________________________________________________________
1218 // to flip back one spread, pass index=null
1219 GnuBook.prototype.flipBackToIndex = function(index) {
1220     if (1 == this.mode) return;
1221
1222     var leftIndex = this.twoPage.currentIndexL;
1223     
1224     // $$$ Need to change this to be able to see first spread.
1225     //     See https://bugs.launchpad.net/gnubook/+bug/296788
1226     if (leftIndex <= 2) return;
1227     if (this.animating) return;
1228
1229     if (null != this.leafEdgeTmp) {
1230         alert('error: leafEdgeTmp should be null!');
1231         return;
1232     }
1233     
1234     if (null == index) {
1235         index = leftIndex-2;
1236     }
1237     //if (index<0) return;
1238     
1239     var previousIndices = this.getSpreadIndices(index);
1240     
1241     if (previousIndices[0] < 0 || previousIndices[1] < 0) {
1242         return;
1243     }
1244     
1245     //console.log("flipping back to " + previousIndices[0] + ',' + previousIndices[1]);
1246
1247     this.animating = true;
1248     
1249     if ('rl' != this.pageProgression) {
1250         // Assume LTR and we are going backward    
1251         this.prepareFlipLeftToRight(previousIndices[0], previousIndices[1]);        
1252         this.flipLeftToRight(previousIndices[0], previousIndices[1]);
1253     } else {
1254         // RTL and going backward
1255         var gutter = this.prepareFlipRightToLeft(previousIndices[0], previousIndices[1]);
1256         this.flipRightToLeft(previousIndices[0], previousIndices[1], gutter);
1257     }
1258 }
1259
1260 // flipLeftToRight()
1261 //______________________________________________________________________________
1262 // Flips the page on the left towards the page on the right
1263 GnuBook.prototype.flipLeftToRight = function(newIndexL, newIndexR) {
1264
1265     var leftLeaf = this.twoPage.currentIndexL;
1266     
1267     var oldLeafEdgeWidthL = this.leafEdgeWidth(this.twoPage.currentIndexL);
1268     var newLeafEdgeWidthL = this.leafEdgeWidth(newIndexL);    
1269     var leafEdgeTmpW = oldLeafEdgeWidthL - newLeafEdgeWidthL;
1270     
1271     var currWidthL   = this.getPageWidth2UP(leftLeaf);
1272     var newWidthL    = this.getPageWidth2UP(newIndexL);
1273     var newWidthR    = this.getPageWidth2UP(newIndexR);
1274
1275     var top  = this.twoPageTop();
1276     var gutter = this.twoPage.middle + this.gutterOffsetForIndex(newIndexL);
1277     
1278     //console.log('leftEdgeTmpW ' + leafEdgeTmpW);
1279     //console.log('  gutter ' + gutter + ', scaledWL ' + scaledWL + ', newLeafEdgeWL ' + newLeafEdgeWidthL);
1280     
1281     //animation strategy:
1282     // 0. remove search highlight, if any.
1283     // 1. create a new div, called leafEdgeTmp to represent the leaf edge between the leftmost edge 
1284     //    of the left leaf and where the user clicked in the leaf edge.
1285     //    Note that if this function was triggered by left() and not a
1286     //    mouse click, the width of leafEdgeTmp is very small (zero px).
1287     // 2. animate both leafEdgeTmp to the gutter (without changing its width) and animate
1288     //    leftLeaf to width=0.
1289     // 3. When step 2 is finished, animate leafEdgeTmp to right-hand side of new right leaf
1290     //    (left=gutter+newWidthR) while also animating the new right leaf from width=0 to
1291     //    its new full width.
1292     // 4. After step 3 is finished, do the following:
1293     //      - remove leafEdgeTmp from the dom.
1294     //      - resize and move the right leaf edge (leafEdgeR) to left=gutter+newWidthR
1295     //          and width=twoPage.edgeWidth-newLeafEdgeWidthL.
1296     //      - resize and move the left leaf edge (leafEdgeL) to left=gutter-newWidthL-newLeafEdgeWidthL
1297     //          and width=newLeafEdgeWidthL.
1298     //      - resize the back cover (twoPage.coverDiv) to left=gutter-newWidthL-newLeafEdgeWidthL-10
1299     //          and width=newWidthL+newWidthR+twoPage.edgeWidth+20
1300     //      - move new left leaf (newIndexL) forward to zindex=2 so it can receive clicks.
1301     //      - remove old left and right leafs from the dom [pruneUnusedImgs()].
1302     //      - prefetch new adjacent leafs.
1303     //      - set up click handlers for both new left and right leafs.
1304     //      - redraw the search highlight.
1305     //      - update the pagenum box and the url.
1306     
1307     
1308     var leftEdgeTmpLeft = gutter - currWidthL - leafEdgeTmpW;
1309
1310     this.leafEdgeTmp = document.createElement('div');
1311     $(this.leafEdgeTmp).css({
1312         borderStyle: 'solid none solid solid',
1313         borderColor: 'rgb(51, 51, 34)',
1314         borderWidth: '1px 0px 1px 1px',
1315         background: 'transparent url(' + this.imagesBaseURL + 'left_edges.png) repeat scroll 0% 0%',
1316         width: leafEdgeTmpW + 'px',
1317         height: this.twoPage.height-1 + 'px',
1318         left: leftEdgeTmpLeft + 'px',
1319         top: top+'px',    
1320         position: 'absolute',
1321         zIndex:1000
1322     }).appendTo('#GBtwopageview');
1323     
1324     //$(this.leafEdgeL).css('width', newLeafEdgeWidthL+'px');
1325     $(this.leafEdgeL).css({
1326         width: newLeafEdgeWidthL+'px', 
1327         left: gutter-currWidthL-newLeafEdgeWidthL+'px'
1328     });   
1329
1330     // Left gets the offset of the current left leaf from the document
1331     var left = $(this.prefetchedImgs[leftLeaf]).offset().left;
1332     // $$$ This seems very similar to the gutter.  May be able to consolidate the logic.
1333     // XXX need to recalc
1334     var right = $('#GBtwopageview').attr('clientWidth')-left-$(this.prefetchedImgs[leftLeaf]).width()+$('#GBtwopageview').offset().left-2+'px';
1335     // We change the left leaf to right positioning
1336     // $$$ This causes animation glitches during resize.  See https://bugs.edge.launchpad.net/gnubook/+bug/328327
1337     $(this.prefetchedImgs[leftLeaf]).css({
1338         right: right,
1339         left: ''
1340     });
1341
1342      left = $(this.prefetchedImgs[leftLeaf]).offset().left - $('#book_div_1').offset().left;
1343      
1344      right = left+$(this.prefetchedImgs[leftLeaf]).width()+'px';
1345
1346     $(this.leafEdgeTmp).animate({left: gutter}, this.flipSpeed, 'easeInSine');    
1347     //$(this.prefetchedImgs[leftLeaf]).animate({width: '0px'}, 'slow', 'easeInSine');
1348     
1349     var self = this;
1350
1351     this.removeSearchHilites();
1352
1353     //console.log('animating leafLeaf ' + leftLeaf + ' to 0px');
1354     $(this.prefetchedImgs[leftLeaf]).animate({width: '0px'}, self.flipSpeed, 'easeInSine', function() {
1355     
1356         //console.log('     and now leafEdgeTmp to left: gutter+newWidthR ' + (gutter + newWidthR));
1357         $(self.leafEdgeTmp).animate({left: gutter+newWidthR+'px'}, self.flipSpeed, 'easeOutSine');
1358
1359         //console.log('  animating newIndexR ' + newIndexR + ' to ' + newWidthR + ' from ' + $(self.prefetchedImgs[newIndexR]).width());
1360         $(self.prefetchedImgs[newIndexR]).animate({width: newWidthR+'px'}, self.flipSpeed, 'easeOutSine', function() {
1361             $(self.prefetchedImgs[newIndexL]).css('zIndex', 2);
1362
1363             $(self.leafEdgeR).css({
1364                 // Moves the right leaf edge
1365                 width: self.twoPage.edgeWidth-newLeafEdgeWidthL+'px',
1366                 left:  gutter+newWidthR+'px'
1367             });
1368
1369             $(self.leafEdgeL).css({
1370                 // Moves and resizes the left leaf edge
1371                 width: newLeafEdgeWidthL+'px',
1372                 left:  gutter-newWidthL-newLeafEdgeWidthL+'px'
1373             });
1374
1375             // XXX set values in this.twoPage here(?)
1376             $(self.twoPage.coverDiv).css({
1377                 // Resizes the brown border div
1378                 width: newWidthL+newWidthR+self.twoPage.edgeWidth+20+'px',
1379                 left: gutter-newWidthL-newLeafEdgeWidthL-self.twoPage.coverInternalPadding+'px'
1380             });
1381             
1382             $(self.leafEdgeTmp).remove();
1383             self.leafEdgeTmp = null;
1384             
1385             self.twoPage.currentIndexL = newIndexL;
1386             self.twoPage.currentIndexR = newIndexR;
1387             self.firstIndex = self.twoPage.currentIndexL;
1388             self.displayedIndices = [newIndexL, newIndexR];
1389             self.setClickHandlers();
1390             self.pruneUnusedImgs();
1391             self.prefetch();            
1392             self.animating = false;
1393             
1394             self.updateSearchHilites2UP();
1395             self.updatePageNumBox2UP();
1396             
1397             if (self.animationFinishedCallback) {
1398                 self.animationFinishedCallback();
1399                 self.animationFinishedCallback = null;
1400             }
1401         });
1402     });        
1403     
1404 }
1405
1406 // flipFwdToIndex()
1407 //______________________________________________________________________________
1408 // Whether we flip left or right is dependent on the page progression
1409 // to flip forward one spread, pass index=null
1410 GnuBook.prototype.flipFwdToIndex = function(index) {
1411
1412     if (this.animating) return;
1413
1414     if (null != this.leafEdgeTmp) {
1415         alert('error: leafEdgeTmp should be null!');
1416         return;
1417     }
1418
1419     if (null == index) {
1420         index = this.twoPage.currentIndexR+2; // $$$ assumes indices are continuous
1421     }
1422     if (index > this.lastDisplayableIndex()) return;
1423
1424     this.animating = true;
1425     
1426     var nextIndices = this.getSpreadIndices(index);
1427     
1428     //console.log('flipfwd to indices ' + nextIndices[0] + ',' + nextIndices[1]);
1429
1430     if ('rl' != this.pageProgression) {
1431         // We did not specify RTL
1432         var gutter = this.prepareFlipRightToLeft(nextIndices[0], nextIndices[1]);
1433         this.flipRightToLeft(nextIndices[0], nextIndices[1], gutter);
1434     } else {
1435         // RTL
1436         var gutter = this.prepareFlipLeftToRight(nextIndices[0], nextIndices[1]);
1437         this.flipLeftToRight(nextIndices[0], nextIndices[1]);
1438     }
1439 }
1440
1441 // flipRightToLeft(nextL, nextR, gutter)
1442 // $$$ better not to have to pass gutter in
1443 //______________________________________________________________________________
1444 // Flip from left to right and show the nextL and nextR indices on those sides
1445 GnuBook.prototype.flipRightToLeft = function(newIndexL, newIndexR) {
1446     var oldLeafEdgeWidthL = this.leafEdgeWidth(this.twoPage.currentIndexL);
1447     var oldLeafEdgeWidthR = this.twoPage.edgeWidth-oldLeafEdgeWidthL;
1448     var newLeafEdgeWidthL = this.leafEdgeWidth(newIndexL);  
1449     var newLeafEdgeWidthR = this.twoPage.edgeWidth-newLeafEdgeWidthL;
1450
1451     var leafEdgeTmpW = oldLeafEdgeWidthR - newLeafEdgeWidthR;
1452
1453     var top = this.twoPageTop();
1454     var scaledW = this.getPageWidth2UP(this.twoPage.currentIndexR);
1455
1456     var middle = this.twoPage.middle;
1457     var gutter = middle + this.gutterOffsetForIndex(newIndexL);
1458     
1459     this.leafEdgeTmp = document.createElement('div');
1460     $(this.leafEdgeTmp).css({
1461         borderStyle: 'solid none solid solid',
1462         borderColor: 'rgb(51, 51, 34)',
1463         borderWidth: '1px 0px 1px 1px',
1464         background: 'transparent url(' + this.imagesBaseURL + 'left_edges.png) repeat scroll 0% 0%',
1465         width: leafEdgeTmpW + 'px',
1466         height: this.twoPage.height-1 + 'px',
1467         left: gutter+scaledW+'px',
1468         top: top+'px',    
1469         position: 'absolute',
1470         zIndex:1000
1471     }).appendTo('#GBtwopageview');
1472
1473     //var scaledWR = this.getPageWidth2UP(newIndexR); // $$$ should be current instead?
1474     //var scaledWL = this.getPageWidth2UP(newIndexL); // $$$ should be current instead?
1475     
1476     var currWidthL = this.getPageWidth2UP(this.twoPage.currentIndexL);
1477     var currWidthR = this.getPageWidth2UP(this.twoPage.currentIndexR);
1478     var newWidthL = this.getPageWidth2UP(newIndexL);
1479     var newWidthR = this.getPageWidth2UP(newIndexR);
1480
1481     $(this.leafEdgeR).css({width: newLeafEdgeWidthR+'px', left: gutter+newWidthR+'px' });
1482
1483     var self = this; // closure-tastic!
1484
1485     var speed = this.flipSpeed;
1486
1487     this.removeSearchHilites();
1488     
1489     $(this.leafEdgeTmp).animate({left: gutter}, speed, 'easeInSine');    
1490     $(this.prefetchedImgs[this.twoPage.currentIndexR]).animate({width: '0px'}, speed, 'easeInSine', function() {
1491         $(self.leafEdgeTmp).animate({left: gutter-newWidthL-leafEdgeTmpW+'px'}, speed, 'easeOutSine');    
1492         $(self.prefetchedImgs[newIndexL]).animate({width: newWidthL+'px'}, speed, 'easeOutSine', function() {
1493             $(self.prefetchedImgs[newIndexR]).css('zIndex', 2);
1494
1495             $(self.leafEdgeL).css({
1496                 width: newLeafEdgeWidthL+'px', 
1497                 left: gutter-newWidthL-newLeafEdgeWidthL+'px'
1498             });
1499             
1500             // Resizes the book cover
1501             $(self.twoPage.coverDiv).css({
1502                 width: self.twoPageCoverWidth(newWidthL+newWidthR)+'px',
1503                 left: self.coverExternalPadding+'px'
1504             });
1505             
1506             $(self.leafEdgeTmp).remove();
1507             self.leafEdgeTmp = null;
1508             
1509             self.twoPage.currentIndexL = newIndexL;
1510             self.twoPage.currentIndexR = newIndexR;
1511             self.firstIndex = self.twoPage.currentIndexL;
1512             self.displayedIndices = [newIndexL, newIndexR];
1513             self.setClickHandlers();            
1514             self.pruneUnusedImgs();
1515             self.prefetch();
1516             self.animating = false;
1517
1518
1519             self.updateSearchHilites2UP();
1520             self.updatePageNumBox2UP();
1521             
1522             if (self.animationFinishedCallback) {
1523                 self.animationFinishedCallback();
1524                 self.animationFinishedCallback = null;
1525             }
1526         });
1527     });    
1528 }
1529
1530 // setClickHandlers
1531 //______________________________________________________________________________
1532 GnuBook.prototype.setClickHandlers = function() {
1533     var self = this;
1534     // $$$ TODO don't set again if already set
1535     $(this.prefetchedImgs[this.twoPage.currentIndexL]).click(function() {
1536         //self.prevPage();
1537         self.autoStop();
1538         self.left();
1539     });
1540     $(this.prefetchedImgs[this.twoPage.currentIndexR]).click(function() {
1541         //self.nextPage();'
1542         self.autoStop();
1543         self.right();        
1544     });
1545 }
1546
1547 // prefetchImg()
1548 //______________________________________________________________________________
1549 GnuBook.prototype.prefetchImg = function(index) {
1550     if (undefined == this.prefetchedImgs[index]) {    
1551         //console.log('prefetching ' + index);
1552         var img = document.createElement("img");
1553         img.src = this.getPageURI(index);
1554         this.prefetchedImgs[index] = img;
1555     }
1556 }
1557
1558
1559 // prepareFlipLeftToRight()
1560 //
1561 //______________________________________________________________________________
1562 //
1563 // Prepare to flip the left page towards the right.  This corresponds to moving
1564 // backward when the page progression is left to right.
1565 GnuBook.prototype.prepareFlipLeftToRight = function(prevL, prevR) {
1566
1567     //console.log('  preparing left->right for ' + prevL + ',' + prevR);
1568
1569     this.prefetchImg(prevL);
1570     this.prefetchImg(prevR);
1571     
1572     var height  = this.getPageHeight(prevL); 
1573     var width   = this.getPageWidth(prevL);    
1574     var middle = this.twoPage.middle;
1575     var top  = this.twoPageTop();                
1576     var scaledW = this.twoPage.height*width/height; // $$$ assumes height of page is dominant
1577
1578     // The gutter is the dividing line between the left and right pages.
1579     // It is offset from the middle to create the illusion of thickness to the pages
1580     var gutter = middle + this.gutterOffsetForIndex(prevL);
1581     
1582     // XXX
1583     console.log('    gutter for ' + prevL + ' is ' + gutter);
1584     //console.log('    prevL.left: ' + (gutter - scaledW) + 'px');
1585     //console.log('    changing prevL ' + prevL + ' to left: ' + (gutter-scaledW) + ' width: ' + scaledW);
1586     
1587     $(this.prefetchedImgs[prevL]).css({
1588         position: 'absolute',
1589         left: gutter-scaledW+'px',
1590         right: '', // clear right property
1591         top:    top+'px',
1592         backgroundColor: 'rgb(234, 226, 205)',
1593         height: this.twoPage.height,
1594         width:  scaledW+'px',
1595         borderRight: '1px solid black',
1596         zIndex: 1
1597     });
1598
1599     $('#GBtwopageview').append(this.prefetchedImgs[prevL]);
1600
1601     //console.log('    changing prevR ' + prevR + ' to left: ' + gutter + ' width: 0');
1602
1603     $(this.prefetchedImgs[prevR]).css({
1604         position: 'absolute',
1605         left:   gutter+'px',
1606         right: '',
1607         top:    top+'px',
1608         backgroundColor: 'rgb(234, 226, 205)',
1609         height: this.twoPage.height,
1610         width:  '0px',
1611         borderLeft: '1px solid black',
1612         zIndex: 2
1613     });
1614
1615     $('#GBtwopageview').append(this.prefetchedImgs[prevR]);
1616             
1617 }
1618
1619 // XXXmang we're adding an extra pixel in the middle
1620 // prepareFlipRightToLeft()
1621 //______________________________________________________________________________
1622 GnuBook.prototype.prepareFlipRightToLeft = function(nextL, nextR) {
1623
1624     //console.log('  preparing left<-right for ' + nextL + ',' + nextR);
1625
1626     // Prefetch images
1627     this.prefetchImg(nextL);
1628     this.prefetchImg(nextR);
1629
1630     var height  = this.getPageHeight(nextR); 
1631     var width   = this.getPageWidth(nextR);    
1632     var middle = this.twoPage.middle;
1633     var top  = this.twoPageTop();               
1634     var scaledW = this.twoPage.height*width/height;
1635
1636     var gutter = middle + this.gutterOffsetForIndex(nextL);
1637     
1638     console.log('right to left to %d gutter is %d', nextL, gutter); // XXX
1639     
1640     //console.log(' prepareRTL changing nextR ' + nextR + ' to left: ' + gutter);
1641     $(this.prefetchedImgs[nextR]).css({
1642         position: 'absolute',
1643         left:   gutter+'px',
1644         top:    top+'px',
1645         backgroundColor: 'rgb(234, 226, 205)',
1646         height: this.twoPage.height,
1647         width:  scaledW+'px',
1648         borderLeft: '1px solid black',
1649         zIndex: 1
1650     });
1651
1652     $('#GBtwopageview').append(this.prefetchedImgs[nextR]);
1653
1654     height  = this.getPageHeight(nextL); 
1655     width   = this.getPageWidth(nextL);      
1656     scaledW = this.twoPage.height*width/height;
1657
1658     //console.log(' prepareRTL changing nextL ' + nextL + ' to right: ' + $('#GBcontainer').width()-gutter);
1659     $(this.prefetchedImgs[nextL]).css({
1660         position: 'absolute',
1661         right:   $('#GBtwopageview').attr('clientWidth')-gutter+'px',
1662         top:    top+'px',
1663         backgroundColor: 'rgb(234, 226, 205)',
1664         height: this.twoPage.height,
1665         width:  0+'px', // Start at 0 width, then grow to the left
1666         borderRight: '1px solid black',
1667         zIndex: 2
1668     });
1669
1670     $('#GBtwopageview').append(this.prefetchedImgs[nextL]);    
1671             
1672 }
1673
1674 // getNextLeafs() -- NOT RTL AWARE
1675 //______________________________________________________________________________
1676 // GnuBook.prototype.getNextLeafs = function(o) {
1677 //     //TODO: we might have two left or two right leafs in a row (damaged book)
1678 //     //For now, assume that leafs are contiguous.
1679 //     
1680 //     //return [this.twoPage.currentIndexL+2, this.twoPage.currentIndexL+3];
1681 //     o.L = this.twoPage.currentIndexL+2;
1682 //     o.R = this.twoPage.currentIndexL+3;
1683 // }
1684
1685 // getprevLeafs() -- NOT RTL AWARE
1686 //______________________________________________________________________________
1687 // GnuBook.prototype.getPrevLeafs = function(o) {
1688 //     //TODO: we might have two left or two right leafs in a row (damaged book)
1689 //     //For now, assume that leafs are contiguous.
1690 //     
1691 //     //return [this.twoPage.currentIndexL-2, this.twoPage.currentIndexL-1];
1692 //     o.L = this.twoPage.currentIndexL-2;
1693 //     o.R = this.twoPage.currentIndexL-1;
1694 // }
1695
1696 // pruneUnusedImgs()
1697 //______________________________________________________________________________
1698 GnuBook.prototype.pruneUnusedImgs = function() {
1699     //console.log('current: ' + this.twoPage.currentIndexL + ' ' + this.twoPage.currentIndexR);
1700     for (var key in this.prefetchedImgs) {
1701         //console.log('key is ' + key);
1702         if ((key != this.twoPage.currentIndexL) && (key != this.twoPage.currentIndexR)) {
1703             //console.log('removing key '+ key);
1704             $(this.prefetchedImgs[key]).remove();
1705         }
1706         if ((key < this.twoPage.currentIndexL-4) || (key > this.twoPage.currentIndexR+4)) {
1707             //console.log('deleting key '+ key);
1708             delete this.prefetchedImgs[key];
1709         }
1710     }
1711 }
1712
1713 // prefetch()
1714 //______________________________________________________________________________
1715 GnuBook.prototype.prefetch = function() {
1716
1717     var lim = this.twoPage.currentIndexL-4;
1718     var i;
1719     lim = Math.max(lim, 0);
1720     for (i = lim; i < this.twoPage.currentIndexL; i++) {
1721         this.prefetchImg(i);
1722     }
1723     
1724     if (this.numLeafs > (this.twoPage.currentIndexR+1)) {
1725         lim = Math.min(this.twoPage.currentIndexR+4, this.numLeafs-1);
1726         for (i=this.twoPage.currentIndexR+1; i<=lim; i++) {
1727             this.prefetchImg(i);
1728         }
1729     }
1730 }
1731
1732 // getPageWidth2UP()
1733 //______________________________________________________________________________
1734 GnuBook.prototype.getPageWidth2UP = function(index) {
1735     // We return the width based on the dominant height
1736     var height  = this.getPageHeight(index); 
1737     var width   = this.getPageWidth(index);    
1738     return Math.floor(this.twoPage.height*width/height); // $$$ we assume width is relative to current spread
1739 }    
1740
1741 // search()
1742 //______________________________________________________________________________
1743 GnuBook.prototype.search = function(term) {
1744     $('#GnuBookSearchScript').remove();
1745         var script  = document.createElement("script");
1746         script.setAttribute('id', 'GnuBookSearchScript');
1747         script.setAttribute("type", "text/javascript");
1748         script.setAttribute("src", 'http://'+this.server+'/GnuBook/flipbook_search_gb.php?url='+escape(this.bookPath + '_djvu.xml')+'&term='+term+'&format=XML&callback=gb.GBSearchCallback');
1749         document.getElementsByTagName('head')[0].appendChild(script);
1750         $('#GnuBookSearchResults').html('Searching...');
1751 }
1752
1753 // GBSearchCallback()
1754 //______________________________________________________________________________
1755 GnuBook.prototype.GBSearchCallback = function(txt) {
1756     //alert(txt);
1757     if (jQuery.browser.msie) {
1758         var dom=new ActiveXObject("Microsoft.XMLDOM");
1759         dom.async="false";
1760         dom.loadXML(txt);    
1761     } else {
1762         var parser = new DOMParser();
1763         var dom = parser.parseFromString(txt, "text/xml");    
1764     }
1765     
1766     $('#GnuBookSearchResults').empty();    
1767     $('#GnuBookSearchResults').append('<ul>');
1768     
1769     for (var key in this.searchResults) {
1770         if (null != this.searchResults[key].div) {
1771             $(this.searchResults[key].div).remove();
1772         }
1773         delete this.searchResults[key];
1774     }
1775     
1776     var pages = dom.getElementsByTagName('PAGE');
1777     
1778     if (0 == pages.length) {
1779         // $$$ it would be nice to echo the (sanitized) search result here
1780         $('#GnuBookSearchResults').append('<li>No search results found</li>');
1781     } else {    
1782         for (var i = 0; i < pages.length; i++){
1783             //console.log(pages[i].getAttribute('file').substr(1) +'-'+ parseInt(pages[i].getAttribute('file').substr(1), 10));
1784     
1785             
1786             var re = new RegExp (/_(\d{4})/);
1787             var reMatch = re.exec(pages[i].getAttribute('file'));
1788             var index = parseInt(reMatch[1], 10);
1789             //var index = parseInt(pages[i].getAttribute('file').substr(1), 10);
1790             
1791             var children = pages[i].childNodes;
1792             var context = '';
1793             for (var j=0; j<children.length; j++) {
1794                 //console.log(j + ' - ' + children[j].nodeName);
1795                 //console.log(children[j].firstChild.nodeValue);
1796                 if ('CONTEXT' == children[j].nodeName) {
1797                     context += children[j].firstChild.nodeValue;
1798                 } else if ('WORD' == children[j].nodeName) {
1799                     context += '<b>'+children[j].firstChild.nodeValue+'</b>';
1800                     
1801                     var index = this.leafNumToIndex(index);
1802                     if (null != index) {
1803                         //coordinates are [left, bottom, right, top, [baseline]]
1804                         //we'll skip baseline for now...
1805                         var coords = children[j].getAttribute('coords').split(',',4);
1806                         if (4 == coords.length) {
1807                             this.searchResults[index] = {'l':coords[0], 'b':coords[1], 'r':coords[2], 't':coords[3], 'div':null};
1808                         }
1809                     }
1810                 }
1811             }
1812             var pageName = this.getPageName(index);
1813             //TODO: remove hardcoded instance name
1814             $('#GnuBookSearchResults').append('<li><b><a href="javascript:gb.jumpToIndex('+index+');">' + pageName + '</a></b> - ' + context + '</li>');
1815         }
1816     }
1817     $('#GnuBookSearchResults').append('</ul>');
1818
1819     this.updateSearchHilites();
1820 }
1821
1822 // updateSearchHilites()
1823 //______________________________________________________________________________
1824 GnuBook.prototype.updateSearchHilites = function() {
1825     if (2 == this.mode) {
1826         this.updateSearchHilites2UP();
1827     } else {
1828         this.updateSearchHilites1UP();
1829     }
1830 }
1831
1832 // showSearchHilites1UP()
1833 //______________________________________________________________________________
1834 GnuBook.prototype.updateSearchHilites1UP = function() {
1835
1836     for (var key in this.searchResults) {
1837         
1838         if (-1 != jQuery.inArray(parseInt(key), this.displayedIndices)) {
1839             var result = this.searchResults[key];
1840             if(null == result.div) {
1841                 result.div = document.createElement('div');
1842                 $(result.div).attr('className', 'GnuBookSearchHilite').appendTo('#pagediv'+key);
1843                 //console.log('appending ' + key);
1844             }    
1845             $(result.div).css({
1846                 width:  (result.r-result.l)/this.reduce + 'px',
1847                 height: (result.b-result.t)/this.reduce + 'px',
1848                 left:   (result.l)/this.reduce + 'px',
1849                 top:    (result.t)/this.reduce +'px'
1850             });
1851
1852         } else {
1853             //console.log(key + ' not displayed');
1854             this.searchResults[key].div=null;
1855         }
1856     }
1857 }
1858
1859 // XXX move, clean up, use everywhere
1860 GnuBook.prototype.twoPageGutter = function() {
1861     return this.twoPage.middle + this.gutterOffsetForIndex(this.twoPage.currentIndexL);
1862 }
1863
1864 // XXX move, clean up, use everywhere
1865 GnuBook.prototype.twoPageTop = function() {
1866     return this.twoPage.coverExternalPadding + this.twoPage.coverInternalPadding; // $$$ + border?
1867 }
1868     
1869 GnuBook.prototype.twoPageCoverWidth = function(totalPageWidth) {
1870     return totalPageWidth + this.twoPage.edgeWidth + 2*this.twoPage.coverInternalPadding;
1871 }
1872
1873 GnuBook.prototype.twoPageCenterView = function() {
1874     var viewWidth = $('#GBtwopageview').width();
1875     var containerClientWidth = $('#GBcontainer').attr('clientWidth');
1876     
1877     var viewHeight = $('#GBtwopageview').height();
1878     var containerClientHeight = $('#GBcontainer').attr('clientHeight');
1879     
1880     if (viewWidth < containerClientWidth) {
1881         // Can fit width without scrollbars - center by adjusting offset
1882         $('#GBtwopageview').css('left', ((containerClientWidth - viewWidth) >> 1) + 'px');    
1883     } else {
1884         // Need to scroll to center
1885         $('#GBtwopageview').css('left', 0);
1886         $('#GBcontainer').scrollLeft((viewWidth - containerClientWidth) >> 1);
1887     }
1888     
1889     if (viewHeight < containerClientHeight) {
1890         // Fits with scrollbars - add offset
1891         $('#GBtwopageview').css('top', ((containerClientHeight - viewHeight) >> 1) + 'px');
1892     } else {
1893         $('#GBtwopageview').css('top', 0);
1894         $('#GBcontainer').scrollTop((viewHeight - containerClientHeight) >> 1);
1895     }
1896 }
1897     
1898 // showSearchHilites2UP()
1899 //______________________________________________________________________________
1900 GnuBook.prototype.updateSearchHilites2UP = function() {
1901
1902     for (var key in this.searchResults) {
1903         key = parseInt(key, 10);
1904         if (-1 != jQuery.inArray(key, this.displayedIndices)) {
1905             var result = this.searchResults[key];
1906             if(null == result.div) {
1907                 result.div = document.createElement('div');
1908                 $(result.div).attr('className', 'GnuBookSearchHilite').css('zIndex', 3).appendTo('#GBtwopageview');
1909                 //console.log('appending ' + key);
1910             }
1911
1912             // We calculate the reduction factor for the specific page because it can be different
1913             // for each page in the spread
1914             var height = this.getPageHeight(key);
1915             var width  = this.getPageWidth(key)
1916             var reduce = this.twoPage.height/height;
1917             var scaledW = parseInt(width*reduce);
1918             
1919             var gutter = this.twoPageGutter();
1920             var pageL;
1921             if ('L' == this.getPageSide(key)) {
1922                 pageL = gutter-scaledW;
1923             } else {
1924                 pageL = gutter;
1925             }
1926             var pageT  = this.twoPageTop();
1927             
1928             $(result.div).css({
1929                 width:  (result.r-result.l)*reduce + 'px',
1930                 height: (result.b-result.t)*reduce + 'px',
1931                 left:   pageL+(result.l)*reduce + 'px',
1932                 top:    pageT+(result.t)*reduce +'px'
1933             });
1934
1935         } else {
1936             //console.log(key + ' not displayed');
1937             if (null != this.searchResults[key].div) {
1938                 //console.log('removing ' + key);
1939                 $(this.searchResults[key].div).remove();
1940             }
1941             this.searchResults[key].div=null;
1942         }
1943     }
1944 }
1945
1946 // removeSearchHilites()
1947 //______________________________________________________________________________
1948 GnuBook.prototype.removeSearchHilites = function() {
1949     for (var key in this.searchResults) {
1950         if (null != this.searchResults[key].div) {
1951             $(this.searchResults[key].div).remove();
1952             this.searchResults[key].div=null;
1953         }        
1954     }
1955 }
1956
1957 // showEmbedCode()
1958 //______________________________________________________________________________
1959 GnuBook.prototype.showEmbedCode = function() {
1960     if (null != this.embedPopup) { // check if already showing
1961         return;
1962     }
1963     this.autoStop();
1964     this.embedPopup = document.createElement("div");
1965     $(this.embedPopup).css({
1966         position: 'absolute',
1967         top:      '20px',
1968         left:     ($('#GBcontainer').attr('clientWidth')-400)/2 + 'px',
1969         width:    '400px',
1970         padding:  "20px",
1971         border:   "3px double #999999",
1972         zIndex:   3,
1973         backgroundColor: "#fff"
1974     }).appendTo('#GnuBook');
1975
1976     htmlStr =  '<p style="text-align:center;"><b>Embed Bookreader in your blog!</b></p>';
1977     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>';
1978     htmlStr += '<p>Embed Code: <input type="text" size="40" value="' + this.getEmbedCode() + '"></p>';
1979     htmlStr += '<p style="text-align:center;"><a href="" onclick="gb.embedPopup = null; $(this.parentNode.parentNode).remove(); return false">Close popup</a></p>';    
1980
1981     this.embedPopup.innerHTML = htmlStr;
1982     $(this.embedPopup).find('input').bind('click', function() {
1983         this.select();
1984     })
1985 }
1986
1987 // autoToggle()
1988 //______________________________________________________________________________
1989 GnuBook.prototype.autoToggle = function() {
1990
1991     var bComingFrom1up = false;
1992     if (2 != this.mode) {
1993         bComingFrom1up = true;
1994         this.switchMode(2);
1995     }
1996
1997     var self = this;
1998     if (null == this.autoTimer) {
1999         this.flipSpeed = 2000;
2000         
2001         // $$$ Draw events currently cause layout problems when they occur during animation.
2002         //     There is a specific problem when changing from 1-up immediately to autoplay in RTL so
2003         //     we workaround for now by not triggering immediate animation in that case.
2004         //     See https://bugs.launchpad.net/gnubook/+bug/328327
2005         if (('rl' == this.pageProgression) && bComingFrom1up) {
2006             // don't flip immediately -- wait until timer fires
2007         } else {
2008             // flip immediately
2009             this.flipFwdToIndex();        
2010         }
2011
2012         $('#GBtoolbar .play').hide();
2013         $('#GBtoolbar .pause').show();
2014         this.autoTimer=setInterval(function(){
2015             if (self.animating) {return;}
2016             
2017             if (Math.max(self.twoPage.currentIndexL, self.twoPage.currentIndexR) >= self.lastDisplayableIndex()) {
2018                 self.flipBackToIndex(1); // $$$ really what we want?
2019             } else {            
2020                 self.flipFwdToIndex();
2021             }
2022         },5000);
2023     } else {
2024         this.autoStop();
2025     }
2026 }
2027
2028 // autoStop()
2029 //______________________________________________________________________________
2030 GnuBook.prototype.autoStop = function() {
2031     if (null != this.autoTimer) {
2032         clearInterval(this.autoTimer);
2033         this.flipSpeed = 'fast';
2034         $('#GBtoolbar .pause').hide();
2035         $('#GBtoolbar .play').show();
2036         this.autoTimer = null;
2037     }
2038 }
2039
2040 // keyboardNavigationIsDisabled(event)
2041 //   - returns true if keyboard navigation should be disabled for the event
2042 //______________________________________________________________________________
2043 GnuBook.prototype.keyboardNavigationIsDisabled = function(event) {
2044     if (event.target.tagName == "INPUT") {
2045         return true;
2046     }   
2047     return false;
2048 }
2049
2050 // gutterOffsetForIndex
2051 //______________________________________________________________________________
2052 //
2053 // Returns the gutter offset for the spread containing the given index.
2054 // This function supports RTL
2055 GnuBook.prototype.gutterOffsetForIndex = function(pindex) {
2056
2057     // To find the offset of the gutter from the middle we calculate our percentage distance
2058     // through the book (0..1), remap to (-0.5..0.5) and multiply by the total page edge width
2059     var offset = parseInt(((pindex / this.numLeafs) - 0.5) * this.twoPage.edgeWidth);
2060     
2061     // But then again for RTL it's the opposite
2062     if ('rl' == this.pageProgression) {
2063         offset = -offset;
2064     }
2065     
2066     return offset;
2067 }
2068
2069 // leafEdgeWidth
2070 //______________________________________________________________________________
2071 // Returns the width of the leaf edge div for the page with index given
2072 GnuBook.prototype.leafEdgeWidth = function(pindex) {
2073     // $$$ could there be single pixel rounding errors for L vs R?
2074     if ((this.getPageSide(pindex) == 'L') && (this.pageProgression != 'rl')) {
2075         return parseInt( (pindex/this.numLeafs) * this.twoPage.edgeWidth + 0.5);
2076     } else {
2077         return parseInt( (1 - pindex/this.numLeafs) * this.twoPage.edgeWidth + 0.5);
2078     }
2079 }
2080
2081 // jumpIndexForLeftEdgePageX
2082 //______________________________________________________________________________
2083 // Returns the target jump leaf given a page coordinate (inside the left page edge div)
2084 GnuBook.prototype.jumpIndexForLeftEdgePageX = function(pageX) {
2085     if ('rl' != this.pageProgression) {
2086         // LTR - flipping backward
2087         var jumpIndex = this.twoPage.currentIndexL - ($(this.leafEdgeL).offset().left + $(this.leafEdgeL).width() - pageX) * 10;
2088
2089         // browser may have resized the div due to font size change -- see https://bugs.launchpad.net/gnubook/+bug/333570        
2090         jumpIndex = GnuBook.util.clamp(Math.round(jumpIndex), this.firstDisplayableIndex(), this.twoPage.currentIndexL - 2);
2091         return jumpIndex;
2092
2093     } else {
2094         var jumpIndex = this.twoPage.currentIndexL + ($(this.leafEdgeL).offset().left + $(this.leafEdgeL).width() - pageX) * 10;
2095         jumpIndex = GnuBook.util.clamp(Math.round(jumpIndex), this.twoPage.currentIndexL + 2, this.lastDisplayableIndex());
2096         return jumpIndex;
2097     }
2098 }
2099
2100 // jumpIndexForRightEdgePageX
2101 //______________________________________________________________________________
2102 // Returns the target jump leaf given a page coordinate (inside the right page edge div)
2103 GnuBook.prototype.jumpIndexForRightEdgePageX = function(pageX) {
2104     if ('rl' != this.pageProgression) {
2105         // LTR
2106         var jumpIndex = this.twoPage.currentIndexR + (pageX - $(this.leafEdgeR).offset().left) * 10;
2107         jumpIndex = GnuBook.util.clamp(Math.round(jumpIndex), this.twoPage.currentIndexR + 2, this.lastDisplayableIndex());
2108         return jumpIndex;
2109     } else {
2110         var jumpIndex = this.twoPage.currentIndexR - (pageX - $(this.leafEdgeR).offset().left) * 10;
2111         jumpIndex = GnuBook.util.clamp(Math.round(jumpIndex), this.firstDisplayableIndex(), this.twoPage.currentIndexR - 2);
2112         return jumpIndex;
2113     }
2114 }
2115
2116 GnuBook.prototype.initToolbar = function(mode, ui) {
2117
2118     $("#GnuBook").append("<div id='GBtoolbar'><span style='float:left;'>"
2119         + "<a class='GBicon logo rollover' href='" + this.logoURL + "'>&nbsp;</a>"
2120         + " <button class='GBicon rollover zoom_out' onclick='gb.zoom(-1); return false;'/>" 
2121         + "<button class='GBicon rollover zoom_in' onclick='gb.zoom(1); return false;'/>"
2122         + " <span class='label'>Zoom: <span id='GBzoom'>"+parseInt(100/this.reduce)+"</span>%</span>"
2123         + " <button class='GBicon rollover one_page_mode' onclick='gb.switchMode(1); return false;'/>"
2124         + " <button class='GBicon rollover two_page_mode' onclick='gb.switchMode(2); return false;'/>"
2125         + "&nbsp;&nbsp;<a class='GBblack title' href='"+this.bookUrl+"' target='_blank'>"+this.shortTitle(50)+"</a>"
2126         + "</span></div>");
2127         
2128     if (ui == "embed") {
2129         $("#GnuBook a.logo").attr("target","_blank");
2130     }
2131
2132     // $$$ turn this into a member variable
2133     var jToolbar = $('#GBtoolbar'); // j prefix indicates jQuery object
2134     
2135     // We build in mode 2
2136     jToolbar.append("<span id='GBtoolbarbuttons' style='float: right'>"
2137         + "<button class='GBicon rollover embed' />"
2138         + "<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>"
2139         + "<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>"
2140         + "<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>"
2141         + "<button class='GBicon rollover play' /><button class='GBicon rollover pause' style='display: none' /></span>");
2142
2143     this.bindToolbarNavHandlers(jToolbar);
2144     
2145     // Setup tooltips -- later we could load these from a file for i18n
2146     var titles = { '.logo': 'Go to Archive.org',
2147                    '.zoom_in': 'Zoom in',
2148                    '.zoom_out': 'Zoom out',
2149                    '.one_page_mode': 'One-page view',
2150                    '.two_page_mode': 'Two-page view',
2151                    '.embed': 'Embed bookreader',
2152                    '.book_left': 'Flip left',
2153                    '.book_right': 'Flip right',
2154                    '.book_up': 'Page up',
2155                    '.book_down': 'Page down',
2156                    '.play': 'Play',
2157                    '.pause': 'Pause',
2158                    '.book_top': 'First page',
2159                    '.book_bottom': 'Last page'
2160                   };
2161     if ('rl' == this.pageProgression) {
2162         titles['.book_leftmost'] = 'Last page';
2163         titles['.book_rightmost'] = 'First page';
2164     } else { // LTR
2165         titles['.book_leftmost'] = 'First page';
2166         titles['.book_rightmost'] = 'Last page';
2167     }
2168                   
2169     for (var icon in titles) {
2170         jToolbar.find(icon).attr('title', titles[icon]);
2171     }
2172     
2173     // Hide mode buttons and autoplay if 2up is not available
2174     // $$$ if we end up with more than two modes we should show the applicable buttons
2175     if ( !this.canSwitchToMode(this.constMode2up) ) {
2176         jToolbar.find('.one_page_mode, .two_page_mode, .play, .pause').hide();
2177     }
2178
2179     // Switch to requested mode -- binds other click handlers
2180     this.switchToolbarMode(mode);
2181
2182 }
2183
2184
2185 // switchToolbarMode
2186 //______________________________________________________________________________
2187 // Update the toolbar for the given mode (changes navigation buttons)
2188 // $$$ we should soon split the toolbar out into its own module
2189 GnuBook.prototype.switchToolbarMode = function(mode) {
2190     if (1 == mode) {
2191         // 1-up     
2192         $('#GBtoolbar .GBtoolbarmode2').hide();
2193         $('#GBtoolbar .GBtoolbarmode1').show().css('display', 'inline');
2194     } else {
2195         // 2-up
2196         $('#GBtoolbar .GBtoolbarmode1').hide();
2197         $('#GBtoolbar .GBtoolbarmode2').show().css('display', 'inline');
2198     }
2199 }
2200
2201 // bindToolbarNavHandlers
2202 //______________________________________________________________________________
2203 // Binds the toolbar handlers
2204 GnuBook.prototype.bindToolbarNavHandlers = function(jToolbar) {
2205
2206     jToolbar.find('.book_left').bind('click', function(e) {
2207         gb.left();
2208         return false;
2209     });
2210          
2211     jToolbar.find('.book_right').bind('click', function(e) {
2212         gb.right();
2213         return false;
2214     });
2215         
2216     jToolbar.find('.book_up').bind('click', function(e) {
2217         gb.prev();
2218         return false;
2219     });        
2220         
2221     jToolbar.find('.book_down').bind('click', function(e) {
2222         gb.next();
2223         return false;
2224     });
2225         
2226     jToolbar.find('.embed').bind('click', function(e) {
2227         gb.showEmbedCode();
2228         return false;
2229     });
2230
2231     jToolbar.find('.play').bind('click', function(e) {
2232         gb.autoToggle();
2233         return false;
2234     });
2235
2236     jToolbar.find('.pause').bind('click', function(e) {
2237         gb.autoToggle();
2238         return false;
2239     });
2240     
2241     jToolbar.find('.book_top').bind('click', function(e) {
2242         gb.first();
2243         return false;
2244     });
2245
2246     jToolbar.find('.book_bottom').bind('click', function(e) {
2247         gb.last();
2248         return false;
2249     });
2250     
2251     jToolbar.find('.book_leftmost').bind('click', function(e) {
2252         gb.leftmost();
2253         return false;
2254     });
2255   
2256     jToolbar.find('.book_rightmost').bind('click', function(e) {
2257         gb.rightmost();
2258         return false;
2259     });
2260 }
2261
2262 // updateToolbarZoom(reduce)
2263 //______________________________________________________________________________
2264 // Update the displayed zoom factor based on reduction factor
2265 GnuBook.prototype.updateToolbarZoom = function(reduce) {
2266     // $$$ TODO: Move toolbar to it's own object/plugin
2267     $('#GBzoom').text(parseInt(100/reduce));
2268 }
2269
2270 // firstDisplayableIndex
2271 //______________________________________________________________________________
2272 // Returns the index of the first visible page, dependent on the mode.
2273 // $$$ Currently we cannot display the front/back cover in 2-up and will need to update
2274 // this function when we can as part of https://bugs.launchpad.net/gnubook/+bug/296788
2275 GnuBook.prototype.firstDisplayableIndex = function() {
2276     if (this.mode == 0) {
2277         return 0;
2278     } else {
2279         return 1; // $$$ we assume there are enough pages... we need logic for very short books
2280     }
2281 }
2282
2283 // lastDisplayableIndex
2284 //______________________________________________________________________________
2285 // Returns the index of the last visible page, dependent on the mode.
2286 // $$$ Currently we cannot display the front/back cover in 2-up and will need to update
2287 // this function when we can as pa  rt of https://bugs.launchpad.net/gnubook/+bug/296788
2288 GnuBook.prototype.lastDisplayableIndex = function() {
2289     if (this.mode == 2) {
2290         if (this.lastDisplayableIndex2up === null) {
2291             // Calculate and cache
2292             var candidate = this.numLeafs - 1;
2293             for ( ; candidate >= 0; candidate--) {
2294                 var spreadIndices = this.getSpreadIndices(candidate);
2295                 if (Math.max(spreadIndices[0], spreadIndices[1]) < (this.numLeafs - 1)) {
2296                     break;
2297                 }
2298             }
2299             this.lastDisplayableIndex2up = candidate;
2300         }
2301         return this.lastDisplayableIndex2up;
2302     } else {
2303         return this.numLeafs - 1;
2304     }
2305 }
2306
2307 // shortTitle(maximumCharacters)
2308 //________
2309 // Returns a shortened version of the title with the maximum number of characters
2310 GnuBook.prototype.shortTitle = function(maximumCharacters) {
2311     if (this.bookTitle.length < maximumCharacters) {
2312         return this.bookTitle;
2313     }
2314     
2315     var title = this.bookTitle.substr(0, maximumCharacters - 3);
2316     title += '...';
2317     return title;
2318 }
2319
2320
2321
2322 // Parameter related functions
2323
2324 // updateFromParams(params)
2325 //________
2326 // Update ourselves from the params object.
2327 //
2328 // e.g. this.updateFromParams(this.paramsFromFragment(window.location.hash))
2329 GnuBook.prototype.updateFromParams = function(params) {
2330     if ('undefined' != typeof(params.mode)) {
2331         this.switchMode(params.mode);
2332     }
2333
2334     // $$$ process /search
2335     // $$$ process /zoom
2336     
2337     // We only respect page if index is not set
2338     if ('undefined' != typeof(params.index)) {
2339         if (params.index != this.currentIndex()) {
2340             this.jumpToIndex(params.index);
2341         }
2342     } else if ('undefined' != typeof(params.page)) {
2343         // $$$ this assumes page numbers are unique
2344         if (params.page != this.getPageNum(this.currentIndex())) {
2345             this.jumpToPage(params.page);
2346         }
2347     }
2348     
2349     // $$$ process /region
2350     // $$$ process /highlight
2351 }
2352
2353 // paramsFromFragment(urlFragment)
2354 //________
2355 // Returns a object with configuration parametes from a URL fragment.
2356 //
2357 // E.g paramsFromFragment(window.location.hash)
2358 GnuBook.prototype.paramsFromFragment = function(urlFragment) {
2359     // URL fragment syntax specification: http://openlibrary.org/dev/docs/bookurls
2360     
2361     var params = {};
2362     
2363     // For convenience we allow an initial # character (as from window.location.hash)
2364     // but don't require it
2365     if (urlFragment.substr(0,1) == '#') {
2366         urlFragment = urlFragment.substr(1);
2367     }
2368     
2369     // Simple #nn syntax
2370     var oldStyleLeafNum = parseInt( /^\d+$/.exec(urlFragment) );
2371     if ( !isNaN(oldStyleLeafNum) ) {
2372         params.index = oldStyleLeafNum;
2373         
2374         // Done processing if using old-style syntax
2375         return params;
2376     }
2377     
2378     // Split into key-value pairs
2379     var urlArray = urlFragment.split('/');
2380     var urlHash = {};
2381     for (var i = 0; i < urlArray.length; i += 2) {
2382         urlHash[urlArray[i]] = urlArray[i+1];
2383     }
2384     
2385     // Mode
2386     if ('1up' == urlHash['mode']) {
2387         params.mode = this.constMode1up;
2388     } else if ('2up' == urlHash['mode']) {
2389         params.mode = this.constMode2up;
2390     }
2391     
2392     // Index and page
2393     if ('undefined' != typeof(urlHash['page'])) {
2394         // page was set -- may not be int
2395         params.page = urlHash['page'];
2396     }
2397     
2398     // $$$ process /region
2399     // $$$ process /search
2400     // $$$ process /highlight
2401         
2402     return params;
2403 }
2404
2405 // paramsFromCurrent()
2406 //________
2407 // Create a params object from the current parameters.
2408 GnuBook.prototype.paramsFromCurrent = function() {
2409
2410     var params = {};
2411
2412     var pageNum = this.getPageNum(this.currentIndex());
2413     if ((pageNum === 0) || pageNum) {
2414         params.page = pageNum;
2415     }
2416     
2417     params.index = this.currentIndex();
2418     params.mode = this.mode;
2419     
2420     // $$$ highlight
2421     // $$$ region
2422     // $$$ search
2423     
2424     return params;
2425 }
2426
2427 // fragmentFromParams(params)
2428 //________
2429 // Create a fragment string from the params object.
2430 // See http://openlibrary.org/dev/docs/bookurls for an explanation of the fragment syntax.
2431 GnuBook.prototype.fragmentFromParams = function(params) {
2432     var separator = '/';
2433     
2434     var fragments = [];
2435     
2436     if ('undefined' != typeof(params.page)) {
2437         fragments.push('page', params.page);
2438     } else {
2439         // Don't have page numbering -- use index instead
2440         fragments.push('page', 'n' + params.index);
2441     }
2442     
2443     // $$$ highlight
2444     // $$$ region
2445     // $$$ search
2446     
2447     // mode
2448     if ('undefined' != typeof(params.mode)) {    
2449         if (params.mode == this.constMode1up) {
2450             fragments.push('mode', '1up');
2451         } else if (params.mode == this.constMode2up) {
2452             fragments.push('mode', '2up');
2453         } else {
2454             throw 'fragmentFromParams called with unknown mode ' + params.mode;
2455         }
2456     }
2457     
2458     return fragments.join(separator);
2459 }
2460
2461 // getPageIndex(pageNum)
2462 //________
2463 // Returns the *highest* index the given page number, or undefined
2464 GnuBook.prototype.getPageIndex = function(pageNum) {
2465     var pageIndices = this.getPageIndices(pageNum);
2466     
2467     if (pageIndices.length > 0) {
2468         return pageIndices[pageIndices.length - 1];
2469     }
2470
2471     return undefined;
2472 }
2473
2474 // getPageIndices(pageNum)
2475 //________
2476 // Returns an array (possibly empty) of the indices with the given page number
2477 GnuBook.prototype.getPageIndices = function(pageNum) {
2478     var indices = [];
2479
2480     // Check for special "nXX" page number
2481     if (pageNum.slice(0,1) == 'n') {
2482         try {
2483             var pageIntStr = pageNum.slice(1, pageNum.length);
2484             var pageIndex = parseInt(pageIntStr);
2485             indices.append(pageIndex);
2486             return indices;
2487         } catch(err) {
2488             // Do nothing... will run through page names and see if one matches
2489         }
2490     }
2491
2492     var i;
2493     for (i=0; i<this.numLeafs; i++) {
2494         if (this.getPageNum(i) == pageNum) {
2495             indices.push(i);
2496         }
2497     }
2498     
2499     return indices;
2500 }
2501
2502 // getPageName(index)
2503 //________
2504 // Returns the name of the page as it should be displayed in the user interface
2505 GnuBook.prototype.getPageName = function(index) {
2506     return 'Page ' + this.getPageNum(index);
2507 }
2508
2509 // updateLocationHash
2510 //________
2511 // Update the location hash from the current parameters.  Call this instead of manually
2512 // using window.location.replace
2513 GnuBook.prototype.updateLocationHash = function() {
2514     var newHash = '#' + this.fragmentFromParams(this.paramsFromCurrent());
2515     window.location.replace(newHash);
2516     
2517     // This is the variable checked in the timer.  Only user-generated changes
2518     // to the URL will trigger the event.
2519     this.oldLocationHash = newHash;
2520 }
2521
2522 // startLocationPolling
2523 //________
2524 // Starts polling of window.location to see hash fragment changes
2525 GnuBook.prototype.startLocationPolling = function() {
2526     var self = this; // remember who I am
2527     self.oldLocationHash = window.location.hash;
2528     
2529     if (this.locationPollId) {
2530         clearInterval(this.locationPollID);
2531         this.locationPollId = null;
2532     }
2533     
2534     this.locationPollId = setInterval(function() {
2535         var newHash = window.location.hash;
2536         if (newHash != self.oldLocationHash) {
2537             if (newHash != self.oldUserHash) { // Only process new user hash once
2538                 //console.log('url change detected ' + self.oldLocationHash + " -> " + newHash);
2539                 
2540                 // Queue change if animating
2541                 if (self.animating) {
2542                     self.autoStop();
2543                     self.animationFinishedCallback = function() {
2544                         self.updateFromParams(self.paramsFromFragment(newHash));
2545                     }                        
2546                 } else { // update immediately
2547                     self.updateFromParams(self.paramsFromFragment(newHash));
2548                 }
2549                 self.oldUserHash = newHash;
2550             }
2551         }
2552     }, 500);
2553 }
2554
2555 // getEmbedURL
2556 //________
2557 // Returns a URL for an embedded version of the current book
2558 GnuBook.prototype.getEmbedURL = function() {
2559     // We could generate a URL hash fragment here but for now we just leave at defaults
2560     return 'http://' + window.location.host + '/stream/'+this.bookId + '?ui=embed';
2561 }
2562
2563 // getEmbedCode
2564 //________
2565 // Returns the embed code HTML fragment suitable for copy and paste
2566 GnuBook.prototype.getEmbedCode = function() {
2567     return "<iframe src='" + this.getEmbedURL() + "' width='480px' height='430px'></iframe>";
2568 }
2569
2570 // canSwitchToMode
2571 //________
2572 // Returns true if we can switch to the requested mode
2573 GnuBook.prototype.canSwitchToMode = function(mode) {
2574     if (mode == this.constMode2up) {
2575         // check there are enough pages to display
2576         // $$$ this is a workaround for the mis-feature that we can't display
2577         //     short books in 2up mode
2578         if (this.numLeafs < 6) {
2579             return false;
2580         }
2581     }
2582     
2583     return true;
2584 }
2585
2586 // Library functions
2587 GnuBook.util = {
2588     clamp: function(value, min, max) {
2589         return Math.min(Math.max(value, min), max);
2590     }
2591 }