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