PageSourceSearch

https://xraylibrary.ru/i/017/BR.js

js xraylibrary.ru collected 2026-10-03 18:13:41 UTC 188,719 bytes, 5,257 lines download raw bytes

1
2
3// BookReader()
4//______________________________________________________________________________
5// After you instantiate this object, you must supply the following
6// book-specific functions, before calling init().  Some of these functions
7// can just be stubs for simple books.
8//  - getPageWidth()
9//  - getPageHeight()
10//  - getPageURI()
11//  - getPageSide()
12//  - canRotatePage()
13//  - getPageNum()
14//  - getSpreadIndices()
15// You must also add a numLeafs property before calling init().
16
17function BookReader() {
18
19    // Mode constants
20    this.constMode1up = 1;
21    this.constMode2up = 2;
22    this.constModeThumb = 3;
23
24    this.reduce  = 4;
25    this.padding = 10;          // Padding in 1up
26
27    this.mode    = this.constMode1up;
28    this.ui = 'full';           // UI mode
29    this.uiAutoHide = false;    // Controls whether nav/toolbar will autohide
30
31    // thumbnail mode
32    this.thumbWidth = 100; // will be overridden during prepareThumbnailView
33    this.thumbRowBuffer = 2; // number of rows to pre-cache out a view
34    this.thumbColumns = 6; // default
35    this.thumbMaxLoading = 4; // number of thumbnails to load at once
36    this.thumbPadding = 10; // spacing between thumbnails
37    this.displayedRows=[];
38
39    this.displayedIndices = [];
40    //this.indicesToDisplay = [];
41    this.imgs = {};
42    this.prefetchedImgs = {}; //an object with numeric keys cooresponding to page index
43
44    this.timer     = null;
45    this.animating = false;
46    this.auto      = false;
47    this.autoTimer = null;
48    this.flipSpeed = 'fast';
49
50    this.twoPagePopUp = null;
51    this.leafEdgeTmp  = null;
52    this.embedPopup = null;
53    this.printPopup = null;
54
55    this.searchTerm = '';
56    this.searchResults = null;
57
58    this.firstIndex = null;
59
60    this.lastDisplayableIndex2up = null;
61
62    // Should be overriden (before init) by custom implmentations.
63    this.logoURL = 'http://www.xraylibrary.ru';
64
65    // Base URL for UI images - should be overriden (before init) by
66    // custom implementations.
67    // $$$ This is the same directory as the images referenced by relative
68    //     path in the CSS.  Would be better to automagically find that path.
69    this.imagesBaseURL = '/images/';
70
71
72    // Zoom levels
73    // $$$ provide finer grained zooming
74    /*
75    this.reductionFactors = [ {reduce: 0.5, autofit: null},
76                              {reduce: 1, autofit: null},
77                              {reduce: 2, autofit: null},
78                              {reduce: 4, autofit: null},
79                              {reduce: 8, autofit: null},
80                              {reduce: 16, autofit: null} ];
81    */
82    /* The autofit code ensures that fit to width and fit to height will be available */
83    this.reductionFactors = [ {reduce: 0.5, autofit: null},
84                          {reduce: 1, autofit: null},
85                          {reduce: 2, autofit: null},
86                          {reduce: 3, autofit: null},
87                          {reduce: 4, autofit: null},
88                          {reduce: 6, autofit: null} ];
89
90
91    // Object to hold parameters related to 1up mode
92    this.onePage = {
93        autofit: 'height'                                     // valid values are height, width, none
94    };
95
96    // Object to hold parameters related to 2up mode
97    this.twoPage = {
98        coverInternalPadding: 0, // Width of cover
99        coverExternalPadding: 0, // Padding outside of cover
100        bookSpineDivWidth: 64,    // Width of book spine  $$$ consider sizing based on book length
101        autofit: 'auto'
102    };
103
104    // This object/dictionary controls which optional features are enabled
105    // XXXmang in progress
106    this.features = {
107        // search
108        // read aloud
109        // open library entry
110        // table of contents
111        // embed/share ui
112        // info ui
113    };
114
115    // Text-to-Speech params
116    this.ttsPlaying     = false;
117    this.ttsIndex       = null;  //leaf index
118    this.ttsPosition    = -1;    //chunk (paragraph) number
119    this.ttsBuffering   = false;
120    this.ttsPoller      = null;
121    this.ttsFormat      = null;
122
123    return this;
124};
125
126(function ($) {
127// init()
128//______________________________________________________________________________
129BookReader.prototype.init = function() {
130
131    var startIndex = undefined;
132    this.pageScale = this.reduce; // preserve current reduce
133
134    // Find start index and mode if set in location hash
135    var params = {};
136    if (window.location.hash) {
137        // params explicitly set in URL
138        params = this.paramsFromFragment(window.location.hash);
139    } else {
140        // params not explicitly set, use defaults if we have them
141        if ('defaults' in this) {
142            params = this.paramsFromFragment(this.defaults);
143        }
144    }
145
146    // Sanitize/process parameters
147
148    if ( !this.canSwitchToMode( this.mode ) ) {
149        this.mode = this.constMode1up;
150    }
151
152    if ('undefined' != typeof(params.index)) {
153        startIndex = params.index;
154    } else if ('undefined' != typeof(params.page)) {
155        startIndex = this.getPageIndex(params.page);
156    }
157
158    if ('undefined' == typeof(startIndex)) {
159        if ('undefined' != typeof(this.titleLeaf)) {
160            // title leaf is known - but only use as default if book has a few pages
161            if (this.numLeafs > 2) {
162                startIndex = this.leafNumToIndex(this.titleLeaf);
163            }
164        }
165    }
166
167    if ('undefined' == typeof(startIndex)) {
168        startIndex = 0;
169    }
170
171    if ('undefined' != typeof(params.mode)) {
172        this.mode = params.mode;
173    }
174
175    // Set document title -- may have already been set in enclosing html for
176    // search engine visibility
177    document.title = this.shortTitle(50);
178
179    $("#BookReader").empty();
180
181    this.initToolbar(this.mode, this.ui); // Build inside of toolbar div
182    $("#BookReader").append("<div id='BRcontainer' dir='ltr'></div>");
183    $("#BRcontainer").append("<div id='BRpageview'></div>");
184
185    $("#BRcontainer").bind('scroll', this, function(e) {
186        e.data.loadLeafs();
187    });
188
189    this.setupKeyListeners();
190    this.startLocationPolling();
191
192    $(window).bind('resize', this, function(e) {
193        //console.log('resize!');
194
195        if (1 == e.data.mode) {
196            //console.log('centering 1page view');
197            if (e.data.autofit) {
198                e.data.resizePageView();
199            }
200            e.data.centerPageView();
201            $('#BRpageview').empty()
202            e.data.displayedIndices = [];
203            e.data.updateSearchHilites(); //deletes hilights but does not call remove()
204            e.data.loadLeafs();
205        } else if (3 == e.data.mode){
206            e.data.prepareThumbnailView();
207        } else {
208            //console.log('drawing 2 page view');
209
210            // We only need to prepare again in autofit (size of spread changes)
211            if (e.data.twoPage.autofit) {
212                e.data.prepareTwoPageView();
213            } else {
214                // Re-center if the scrollbars have disappeared
215                var center = e.data.twoPageGetViewCenter();
216                var doRecenter = false;
217                if (e.data.twoPage.totalWidth < $('#BRcontainer').attr('clientWidth')) {
218                    center.percentageX = 0.5;
219                    doRecenter = true;
220                }
221                if (e.data.twoPage.totalHeight < $('#BRcontainer').attr('clientHeight')) {
222                    center.percentageY = 0.5;
223                    doRecenter = true;
224                }
225                if (doRecenter) {
226                    e.data.twoPageCenterView(center.percentageX, center.percentageY);
227                }
228            }
229        }
230    });
231
232    if (this.protected) {
233        $('.BRpagediv1up').live('contextmenu dragstart', this, function(e) {
234            return false;
235        });
236
237        $('.BRpageimage').live('contextmenu dragstart', this, function(e) {
238            return false;
239        });
240
241        $('.BRpagedivthumb').live('contextmenu dragstart', this, function(e) {
242            return false;
243        });
244
245        $('.BRicon.share').hide();
246    }
247
248    $('.BRpagediv1up').bind('mousedown', this, function(e) {
249        // $$$ the purpose of this is to disable selection of the image (makes it turn blue)
250        //     but this also interferes with right-click.  See https://bugs.edge.launchpad.net/gnubook/+bug/362626
251        return false;
252    });
253
254    // $$$ refactor this so it's enough to set the first index and call preparePageView
255    //     (get rid of mode-specific logic at this point)
256    if (1 == this.mode) {
257        this.firstIndex = startIndex;
258        this.prepareOnePageView();
259        this.jumpToIndex(startIndex);
260    } else if (3 == this.mode) {
261        this.firstIndex = startIndex;
262        this.prepareThumbnailView();
263        this.jumpToIndex(startIndex);
264    } else {
265        this.displayedIndices=[0];
266        this.firstIndex = startIndex;
267        this.displayedIndices = [this.firstIndex];
268        //console.log('titleLeaf: %d', this.titleLeaf);
269        //console.log('displayedIndices: %s', this.displayedIndices);
270        this.prepareTwoPageView();
271    }
272
273    // Enact other parts of initial params
274    this.updateFromParams(params);
275
276    // We init the nav bar after the params processing so that the nav slider knows where
277    // it should start (doesn't jump after init)
278    if (this.ui == "embed") {
279        this.initEmbedNavbar();
280    } else {
281        this.initNavbar();
282    }
283    this.bindNavigationHandlers();
284
285    // Set strings in the UI
286    this.initUIStrings();
287
288    // Start AJAX request for OL data
289    if (this.getOpenLibraryRecord) {
290        this.getOpenLibraryRecord(this.gotOpenLibraryRecord);
291    }
292
293}
294
295BookReader.prototype.setupKeyListeners = function() {
296    var self = this;
297
298    var KEY_PGUP = 33;
299    var KEY_PGDOWN = 34;
300    var KEY_END = 35;
301    var KEY_HOME = 36;
302
303    var KEY_LEFT = 37;
304    var KEY_UP = 38;
305    var KEY_RIGHT = 39;
306    var KEY_DOWN = 40;
307
308    // We use document here instead of window to avoid a bug in jQuery on IE7
309    $(document).keydown(function(e) {
310
311        // Keyboard navigation
312        if (!self.keyboardNavigationIsDisabled(e)) {
313            switch(e.keyCode) {
314                case KEY_PGUP:
315                case KEY_UP:
316                    // In 1up mode page scrolling is handled by browser
317                    if (2 == self.mode) {
318                        e.preventDefault();
319                        self.prev();
320                    }
321                    break;
322                case KEY_DOWN:
323                case KEY_PGDOWN:
324                    if (2 == self.mode) {
325                        e.preventDefault();
326                        self.next();
327                    }
328                    break;
329                case KEY_END:
330                    e.preventDefault();
331                    self.last();
332                    break;
333                case KEY_HOME:
334                    e.preventDefault();
335                    self.first();
336                    break;
337                case KEY_LEFT:
338                    if (2 == self.mode) {
339                        e.preventDefault();
340                        self.left();
341                    }
342                    break;
343                case KEY_RIGHT:
344                    if (2 == self.mode) {
345                        e.preventDefault();
346                        self.right();
347                    }
348                    break;
349            }
350        }
351    });
352}
353
354// drawLeafs()
355//______________________________________________________________________________
356BookReader.prototype.drawLeafs = function() {
357    if (1 == this.mode) {
358        this.drawLeafsOnePage();
359    } else if (3 == this.mode) {
360        this.drawLeafsThumbnail();
361    } else {
362        this.drawLeafsTwoPage();
363    }
364
365}
366
367// bindGestures(jElement)
368//______________________________________________________________________________
369BookReader.prototype.bindGestures = function(jElement) {
370
371    jElement.unbind('gesturechange').bind('gesturechange', function(e) {
372        e.preventDefault();
373        if (e.originalEvent.scale > 1.5) {
374            br.zoom(1);
375        } else if (e.originalEvent.scale < 0.6) {
376            br.zoom(-1);
377        }
378    });
379}
380
381BookReader.prototype.setClickHandler2UP = function( element, data, handler) {
382    //console.log('setting handler');
383    //console.log(element.tagName);
384
385    $(element).unbind('click').bind('click', data, function(e) {
386        handler(e);
387    });
388}
389
390// drawLeafsOnePage()
391//______________________________________________________________________________
392BookReader.prototype.drawLeafsOnePage = function() {
393    //alert('drawing leafs!');
394    this.timer = null;
395
396
397    var scrollTop = $('#BRcontainer').attr('scrollTop');
398    var scrollBottom = scrollTop + $('#BRcontainer').height();
399    //console.log('top=' + scrollTop + ' bottom='+scrollBottom);
400
401    var indicesToDisplay = [];
402
403    var i;
404    var leafTop = 0;
405    var leafBottom = 0;
406    for (i=0; i<this.numLeafs; i++) {
407        var height  = parseInt(this._getPageHeight(i)/this.reduce);
408
409        leafBottom += height;
410        //console.log('leafTop = '+leafTop+ ' pageH = ' + this.pageH[i] + 'leafTop>=scrollTop=' + (leafTop>=scrollTop));
411        var topInView    = (leafTop >= scrollTop) && (leafTop <= scrollBottom);
412        var bottomInView = (leafBottom >= scrollTop) && (leafBottom <= scrollBottom);
413        var middleInView = (leafTop <=scrollTop) && (leafBottom>=scrollBottom);
414        if (topInView | bottomInView | middleInView) {
415            //console.log('displayed: ' + this.displayedIndices);
416            //console.log('to display: ' + i);
417            indicesToDisplay.push(i);
418        }
419        leafTop += height +10;
420        leafBottom += 10;
421    }
422
423    // Based of the pages displayed in the view we set the current index
424    // $$$ we should consider the page in the center of the view to be the current one
425    var firstIndexToDraw  = indicesToDisplay[0];
426    if (firstIndexToDraw != this.firstIndex) {
427        this.willChangeToIndex(firstIndexToDraw);
428    }
429    this.firstIndex = firstIndexToDraw;
430
431    // Update hash, but only if we're currently displaying a leaf
432    // Hack that fixes #365790
433    if (this.displayedIndices.length > 0) {
434        this.updateLocationHash();
435    }
436
437    if ((0 != firstIndexToDraw) && (1 < this.reduce)) {
438        firstIndexToDraw--;
439        indicesToDisplay.unshift(firstIndexToDraw);
440    }
441
442    var lastIndexToDraw = indicesToDisplay[indicesToDisplay.length-1];
443    if ( ((this.numLeafs-1) != lastIndexToDraw) && (1 < this.reduce) ) {
444        indicesToDisplay.push(lastIndexToDraw+1);
445    }
446
447    leafTop = 0;
448    var i;
449    for (i=0; i<firstIndexToDraw; i++) {
450        leafTop += parseInt(this._getPageHeight(i)/this.reduce) +10;
451    }
452
453    //var viewWidth = $('#BRpageview').width(); //includes scroll bar width
454    var viewWidth = $('#BRcontainer').attr('scrollWidth');
455
456
457    for (i=0; i<indicesToDisplay.length; i++) {
458        var index = indicesToDisplay[i];
459        var height  = parseInt(this._getPageHeight(index)/this.reduce);
460
461        if (BookReader.util.notInArray(indicesToDisplay[i], this.displayedIndices)) {
462            var width   = parseInt(this._getPageWidth(index)/this.reduce);
463            //console.log("displaying leaf " + indicesToDisplay[i] + ' leafTop=' +leafTop);
464            var div = document.createElement("div");
465            div.className = 'BRpagediv1up';
466            div.id = 'pagediv'+index;
467            div.style.position = "absolute";
468            $(div).css('top', leafTop + 'px');
469            var left = (viewWidth-width)>>1;
470            if (left<0) left = 0;
471            $(div).css('left', left+'px');
472            $(div).css('width', width+'px');
473            $(div).css('height', height+'px');
474            //$(div).text('loading...');
475
476            $('#BRpageview').append(div);
477
478            var img = document.createElement("img");
479            img.src = this._getPageURI(index, this.reduce, 0);
480            $(img).addClass('BRnoselect');
481            $(img).css('width', width+'px');
482            $(img).css('height', height+'px');
483            $(div).append(img);
484
485        } else {
486            //console.log("not displaying " + indicesToDisplay[i] + ' score=' + jQuery.inArray(indicesToDisplay[i], this.displayedIndices));
487        }
488
489        leafTop += height +10;
490
491    }
492
493    for (i=0; i<this.displayedIndices.length; i++) {
494        if (BookReader.util.notInArray(this.displayedIndices[i], indicesToDisplay)) {
495            var index = this.displayedIndices[i];
496            //console.log('Removing leaf ' + index);
497            //console.log('id='+'#pagediv'+index+ ' top = ' +$('#pagediv'+index).css('top'));
498            $('#pagediv'+index).remove();
499        } else {
500            //console.log('NOT Removing leaf ' + this.displayedIndices[i]);
501        }
502    }
503
504    this.displayedIndices = indicesToDisplay.slice();
505    this.updateSearchHilites();
506
507    if (null != this.getPageNum(firstIndexToDraw))  {
508        $("#BRpagenum").val(this.getPageNum(this.currentIndex()));
509    } else {
510        $("#BRpagenum").val('');
511    }
512
513    this.updateToolbarZoom(this.reduce);
514
515}
516
517// drawLeafsThumbnail()
518//______________________________________________________________________________
519// If seekIndex is defined, the view will be drawn with that page visible (without any
520// animated scrolling)
521BookReader.prototype.drawLeafsThumbnail = function( seekIndex ) {
522    //alert('drawing leafs!');
523    this.timer = null;
524
525    var viewWidth = $('#BRcontainer').attr('scrollWidth') - 20; // width minus buffer
526
527    //console.log('top=' + scrollTop + ' bottom='+scrollBottom);
528
529    var i;
530    var leafWidth;
531    var leafHeight;
532    var rightPos = 0;
533    var bottomPos = 0;
534    var maxRight = 0;
535    var currentRow = 0;
536    var leafIndex = 0;
537    var leafMap = [];
538
539    var self = this;
540
541    // Will be set to top of requested seek index, if set
542    var seekTop;
543
544    // Calculate the position of every thumbnail.  $$$ cache instead of calculating on every draw
545    for (i=0; i<this.numLeafs; i++) {
546        leafWidth = this.thumbWidth;
547        if (rightPos + (leafWidth + this.thumbPadding) > viewWidth){
548            currentRow++;
549            rightPos = 0;
550            leafIndex = 0;
551        }
552
553        if (leafMap[currentRow]===undefined) { leafMap[currentRow] = {}; }
554        if (leafMap[currentRow].leafs===undefined) {
555            leafMap[currentRow].leafs = [];
556            leafMap[currentRow].height = 0;
557            leafMap[currentRow].top = 0;
558        }
559        leafMap[currentRow].leafs[leafIndex] = {};
560        leafMap[currentRow].leafs[leafIndex].num = i;
561        leafMap[currentRow].leafs[leafIndex].left = rightPos;
562
563        leafHeight = parseInt((this.getPageHeight(leafMap[currentRow].leafs[leafIndex].num)*this.thumbWidth)/this.getPageWidth(leafMap[currentRow].leafs[leafIndex].num), 10);
564        if (leafHeight > leafMap[currentRow].height) {
565            leafMap[currentRow].height = leafHeight;
566        }
567        if (leafIndex===0) { bottomPos += this.thumbPadding + leafMap[currentRow].height; }
568        rightPos += leafWidth + this.thumbPadding;
569        if (rightPos > maxRight) { maxRight = rightPos; }
570        leafIndex++;
571
572        if (i == seekIndex) {
573            seekTop = bottomPos - this.thumbPadding - leafMap[currentRow].height;
574        }
575    }
576
577    // reset the bottom position based on thumbnails
578    $('#BRpageview').height(bottomPos);
579
580    var pageViewBuffer = Math.floor(($('#BRcontainer').attr('scrollWidth') - maxRight) / 2) - 14;
581
582    // If seekTop is defined, seeking was requested and target found
583    if (typeof(seekTop) != 'undefined') {
584        $('#BRcontainer').scrollTop( seekTop );
585    }
586
587    var scrollTop = $('#BRcontainer').attr('scrollTop');
588    var scrollBottom = scrollTop + $('#BRcontainer').height();
589
590    var leafTop = 0;
591    var leafBottom = 0;
592    var rowsToDisplay = [];
593
594    // Visible leafs with least/greatest index
595    var leastVisible = this.numLeafs - 1;
596    var mostVisible = 0;
597
598    // Determine the thumbnails in view
599    for (i=0; i<leafMap.length; i++) {
600        leafBottom += this.thumbPadding + leafMap[i].height;
601        var topInView    = (leafTop >= scrollTop) && (leafTop <= scrollBottom);
602        var bottomInView = (leafBottom >= scrollTop) && (leafBottom <= scrollBottom);
603        var middleInView = (leafTop <=scrollTop) && (leafBottom>=scrollBottom);
604        if (topInView | bottomInView | middleInView) {
605            //console.log('row to display: ' + j);
606            rowsToDisplay.push(i);
607            if (leafMap[i].leafs[0].num < leastVisible) {
608                leastVisible = leafMap[i].leafs[0].num;
609            }
610            if (leafMap[i].leafs[leafMap[i].leafs.length - 1].num > mostVisible) {
611                mostVisible = leafMap[i].leafs[leafMap[i].leafs.length - 1].num;
612            }
613        }
614        if (leafTop > leafMap[i].top) { leafMap[i].top = leafTop; }
615        leafTop = leafBottom;
616    }
617
618    // create a buffer of preloaded rows before and after the visible rows
619    var firstRow = rowsToDisplay[0];
620    var lastRow = rowsToDisplay[rowsToDisplay.length-1];
621    for (i=1; i<this.thumbRowBuffer+1; i++) {
622        if (lastRow+i < leafMap.length) { rowsToDisplay.push(lastRow+i); }
623    }
624    for (i=1; i<this.thumbRowBuffer+1; i++) {
625        if (firstRow-i >= 0) { rowsToDisplay.push(firstRow-i); }
626    }
627
628    // Create the thumbnail divs and images (lazy loaded)
629    var j;
630    var row;
631    var left;
632    var index;
633    var div;
634    var link;
635    var img;
636    var page;
637    for (i=0; i<rowsToDisplay.length; i++) {
638        if (BookReader.util.notInArray(rowsToDisplay[i], this.displayedRows)) {
639            row = rowsToDisplay[i];
640
641            for (j=0; j<leafMap[row].leafs.length; j++) {
642                index = j;
643                leaf = leafMap[row].leafs[j].num;
644
645                leafWidth = this.thumbWidth;
646                leafHeight = parseInt((this.getPageHeight(leaf)*this.thumbWidth)/this.getPageWidth(leaf), 10);
647                leafTop = leafMap[row].top;
648                left = leafMap[row].leafs[index].left + pageViewBuffer;
649                if ('rl' == this.pageProgression){
650                    left = viewWidth - leafWidth - left;
651                }
652
653                div = document.createElement("div");
654                div.id = 'pagediv'+leaf;
655                div.style.position = "absolute";
656                div.className = "BRpagedivthumb";
657
658                left += this.thumbPadding;
659                $(div).css('top', leafTop + 'px');
660                $(div).css('left', left+'px');
661                $(div).css('width', leafWidth+'px');
662                $(div).css('height', leafHeight+'px');
663                //$(div).text('loading...');
664
665                // link to page in single page mode
666                link = document.createElement("a");
667                $(link).data('leaf', leaf);
668                $(link).bind('tap', function(event) {
669                    self.firstIndex = $(this).data('leaf');
670                    self.switchMode(self.constMode1up);
671                    event.preventDefault();
672                    event.stopPropagation();
673                });
674                $(div).append(link);
675
676                $('#BRpageview').append(div);
677
678                img = document.createElement("img");
679                var thumbReduce = Math.floor(this.getPageWidth(leaf) / this.thumbWidth);
680
681                $(img).attr('src', this.imagesBaseURL + 'transparent.png')
682                    .css({'width': leafWidth+'px', 'height': leafHeight+'px' })
683                    .addClass('BRlazyload')
684                    // Store the URL of the image that will replace this one
685                    .data('srcURL',  this._getPageURI(leaf, thumbReduce));
686                $(link).append(img);
687                //console.log('displaying thumbnail: ' + leaf);
688            }
689        }
690    }
691
692    // Remove thumbnails that are not to be displayed
693    var k;
694    for (i=0; i<this.displayedRows.length; i++) {
695        if (BookReader.util.notInArray(this.displayedRows[i], rowsToDisplay)) {
696            row = this.displayedRows[i];
697
698            // $$$ Safari doesn't like the comprehension
699            //var rowLeafs =  [leaf.num for each (leaf in leafMap[row].leafs)];
700            //console.log('Removing row ' + row + ' ' + rowLeafs);
701
702            for (k=0; k<leafMap[row].leafs.length; k++) {
703                index = leafMap[row].leafs[k].num;
704                //console.log('Removing leaf ' + index);
705                $('#pagediv'+index).remove();
706            }
707        } else {
708            // var mRow = this.displayedRows[i];
709            // var mLeafs = '[' +  [leaf.num for each (leaf in leafMap[mRow].leafs)] + ']';
710            // console.log('NOT Removing row ' + mRow + ' ' + mLeafs);
711        }
712    }
713
714    // Update which page is considered current to make sure a visible page is the current one
715    var currentIndex = this.currentIndex();
716    // console.log('current ' + currentIndex);
717    // console.log('least visible ' + leastVisible + ' most visible ' + mostVisible);
718    if (currentIndex < leastVisible) {
719        this.willChangeToIndex(leastVisible);
720        this.setCurrentIndex(leastVisible);
721    } else if (currentIndex > mostVisible) {
722        this.willChangeToIndex(mostVisible);
723        this.setCurrentIndex(mostVisible);
724    }
725
726    this.displayedRows = rowsToDisplay.slice();
727
728    // Update hash, but only if we're currently displaying a leaf
729    // Hack that fixes #365790
730    if (this.displayedRows.length > 0) {
731        this.updateLocationHash();
732    }
733
734    // remove previous highlights
735    $('.BRpagedivthumb_highlight').removeClass('BRpagedivthumb_highlight');
736
737    // highlight current page
738    $('#pagediv'+this.currentIndex()).addClass('BRpagedivthumb_highlight');
739
740    this.lazyLoadThumbnails();
741
742    // Update page number box.  $$$ refactor to function
743    if (null !== this.getPageNum(this.currentIndex()))  {
744        $("#BRpagenum").val(this.getPageNum(this.currentIndex()));
745    } else {
746        $("#BRpagenum").val('');
747    }
748
749    this.updateToolbarZoom(this.reduce);
750}
751
752BookReader.prototype.lazyLoadThumbnails = function() {
753
754    // console.log('lazy load');
755
756    // We check the complete property since load may not be fired if loading from the cache
757    $('.BRlazyloading').filter('[complete=true]').removeClass('BRlazyloading');
758
759    var loading = $('.BRlazyloading').length;
760    var toLoad = this.thumbMaxLoading - loading;
761
762    // console.log('  ' + loading + ' thumbnails loading');
763    // console.log('  this.thumbMaxLoading ' + this.thumbMaxLoading);
764
765    var self = this;
766
767    if (toLoad > 0) {
768        // $$$ TODO load those near top (but not beyond) page view first
769        $('#BRpageview img.BRlazyload').filter(':lt(' + toLoad + ')').each( function() {
770            self.lazyLoadImage(this);
771        });
772    }
773}
774
775BookReader.prototype.lazyLoadImage = function (dummyImage) {
776    //console.log(' lazy load started for ' + $(dummyImage).data('srcURL').match('([0-9]{4}).jp2')[1] );
777
778    var img = new Image();
779    var self = this;
780
781    $(img)
782        .addClass('BRlazyloading')
783        .one('load', function() {
784            //if (console) { console.log(' onload ' + $(this).attr('src').match('([0-9]{4}).jp2')[1]); };
785
786            $(this).removeClass('BRlazyloading');
787
788            // $$$ Calling lazyLoadThumbnails here was causing stack overflow on IE so
789            //     we call the function after a slight delay.  Also the img.complete property
790            //     is not yet set in IE8 inside this onload handler
791            setTimeout(function() { self.lazyLoadThumbnails(); }, 100);
792        })
793        .one('error', function() {
794            // Remove class so we no longer count as loading
795            $(this).removeClass('BRlazyloading');
796        })
797
798        //the width set with .attr is ignored by Internet Explorer, causing it to show the image at its original size
799        //but with this one line of css, even IE shows the image at the proper size
800        .css({
801            'width': $(dummyImage).width()+'px',
802            'height': $(dummyImage).height()+'px'
803        })
804        .attr({
805            'width': $(dummyImage).width(),
806            'height': $(dummyImage).height(),
807            'src': $(dummyImage).data('srcURL')
808        });
809
810    // replace with the new img
811    $(dummyImage).before(img).remove();
812
813    img = null; // tidy up closure
814}
815
816
817// drawLeafsTwoPage()
818//______________________________________________________________________________
819BookReader.prototype.drawLeafsTwoPage = function() {
820    var scrollTop = $('#BRtwopageview').attr('scrollTop');
821    var scrollBottom = scrollTop + $('#BRtwopageview').height();
822
823    // $$$ we should use calculated values in this.twoPage (recalc if necessary)
824
825    var indexL = this.twoPage.currentIndexL;
826
827    var heightL  = this._getPageHeight(indexL);
828    var widthL   = this._getPageWidth(indexL);
829
830    var leafEdgeWidthL = this.leafEdgeWidth(indexL);
831    var leafEdgeWidthR = this.twoPage.edgeWidth - leafEdgeWidthL;
832    //var bookCoverDivWidth = this.twoPage.width*2 + 20 + this.twoPage.edgeWidth; // $$$ hardcoded cover width
833    var bookCoverDivWidth = this.twoPage.bookCoverDivWidth;
834    //console.log(leafEdgeWidthL);
835
836    var middle = this.twoPage.middle; // $$$ getter instead?
837    var top = this.twoPageTop();
838    var bookCoverDivLeft = this.twoPage.bookCoverDivLeft;
839
840    this.twoPage.scaledWL = this.getPageWidth2UP(indexL);
841    this.twoPage.gutter = this.twoPageGutter();
842
843    this.prefetchImg(indexL);
844    $(this.prefetchedImgs[indexL]).css({
845        position: 'absolute',
846        left: this.twoPage.gutter-this.twoPage.scaledWL+'px',
847        right: '',
848        top:    top+'px',
849        height: this.twoPage.height +'px', // $$$ height forced the same for both pages
850        width:  this.twoPage.scaledWL + 'px',
851        zIndex: 2
852    }).appendTo('#BRtwopageview');
853
854    var indexR = this.twoPage.currentIndexR;
855    var heightR  = this._getPageHeight(indexR);
856    var widthR   = this._getPageWidth(indexR);
857
858    // $$$ should use getwidth2up?
859    //var scaledWR = this.twoPage.height*widthR/heightR;
860    this.twoPage.scaledWR = this.getPageWidth2UP(indexR);
861    this.prefetchImg(indexR);
862    $(this.prefetchedImgs[indexR]).css({
863        position: 'absolute',
864        left:   this.twoPage.gutter+'px',
865        right: '',
866        top:    top+'px',
867        height: this.twoPage.height + 'px', // $$$ height forced the same for both pages
868        width:  this.twoPage.scaledWR + 'px',
869        zIndex: 2
870    }).appendTo('#BRtwopageview');
871
872
873    this.displayedIndices = [this.twoPage.currentIndexL, this.twoPage.currentIndexR];
874    this.setMouseHandlers2UP();
875    this.twoPageSetCursor();
876
877    this.updatePageNumBox2UP();
878    this.updateToolbarZoom(this.reduce);
879
880    // this.twoPagePlaceFlipAreas();  // No longer used
881
882}
883
884// updatePageNumBox2UP
885//______________________________________________________________________________
886BookReader.prototype.updatePageNumBox2UP = function() {
887    if (null != this.getPageNum(this.twoPage.currentIndexL))  {
888        $("#BRpagenum").val(this.getPageNum(this.currentIndex()));
889    } else {
890        $("#BRpagenum").val('');
891    }
892    this.updateLocationHash();
893}
894
895// loadLeafs()
896//______________________________________________________________________________
897BookReader.prototype.loadLeafs = function() {
898
899
900    var self = this;
901    if (null == this.timer) {
902        this.timer=setTimeout(function(){self.drawLeafs()},250);
903    } else {
904        clearTimeout(this.timer);
905        this.timer=setTimeout(function(){self.drawLeafs()},250);
906    }
907}
908
909// zoom(direction)
910//
911// Pass 1 to zoom in, anything else to zoom out
912//______________________________________________________________________________
913BookReader.prototype.zoom = function(direction) {
914    switch (this.mode) {
915        case this.constMode1up:
916            if (direction == 1) {
917                // XXX other cases
918                return this.zoom1up('in');
919            } else {
920                return this.zoom1up('out');
921            }
922
923        case this.constMode2up:
924            if (direction == 1) {
925                // XXX other cases
926                return this.zoom2up('in');
927            } else {
928                return this.zoom2up('out');
929            }
930
931        case this.constModeThumb:
932            // XXX update zoomThumb for named directions
933            return this.zoomThumb(direction);
934
935    }
936}
937
938// zoom1up(dir)
939//______________________________________________________________________________
940BookReader.prototype.zoom1up = function(direction) {
941
942    if (2 == this.mode) {     //can only zoom in 1-page mode
943        this.switchMode(1);
944        return;
945    }
946
947    var reduceFactor = this.nextReduce(this.reduce, direction, this.onePage.reductionFactors);
948
949    if (this.reduce == reduceFactor.reduce) {
950        // Already at this level
951        return;
952    }
953
954    this.reduce = reduceFactor.reduce; // $$$ incorporate into function
955    this.onePage.autofit = reduceFactor.autofit;
956
957    this.pageScale = this.reduce; // preserve current reduce
958    this.resizePageView();
959
960    $('#BRpageview').empty()
961    this.displayedIndices = [];
962    this.loadLeafs();
963
964    this.updateToolbarZoom(this.reduce);
965
966    // Recalculate search hilites
967    this.removeSearchHilites();
968    this.updateSearchHilites();
969
970}
971
972// resizePageView()
973//______________________________________________________________________________
974BookReader.prototype.resizePageView = function() {
975
976    // $$$ This code assumes 1up mode
977    //     e.g. does not preserve position in thumbnail mode
978    //     See http://bugs.launchpad.net/bookreader/+bug/552972
979
980    switch (this.mode) {
981        case this.constMode1up:
982        case this.constMode2up:
983            this.resizePageView1up(); // $$$ necessary in non-1up mode?
984            break;
985        case this.constModeThumb:
986            this.prepareThumbnailView( this.currentIndex() );
987            break;
988        default:
989            alert('Resize not implemented for this mode');
990    }
991}
992
993// Resize the current one page view
994BookReader.prototype.resizePageView1up = function() {
995    var i;
996    var viewHeight = 0;
997    //var viewWidth  = $('#BRcontainer').width(); //includes scrollBar
998    var viewWidth  = $('#BRcontainer').attr('clientWidth');
999
1000    var oldScrollTop  = $('#BRcontainer').attr('scrollTop');
1001    var oldScrollLeft = $('#BRcontainer').attr('scrollLeft');
1002
1003    var oldPageViewHeight = $('#BRpageview').height();
1004    var oldPageViewWidth = $('#BRpageview').width();
1005
1006    // May have come here after preparing the view, in which case the scrollTop and view height are not set
1007
1008    var scrollRatio = 0;
1009    if (oldScrollTop > 0) {
1010        // We have scrolled - implies view has been set up
1011        var oldCenterY = this.centerY1up();
1012        var oldCenterX = this.centerX1up();
1013        scrollRatio = oldCenterY / oldPageViewHeight;
1014    } else {
1015        // Have not scrolled, e.g. because in new container
1016
1017        // We set the scroll ratio so that the current index will still be considered the
1018        // current index in drawLeafsOnePage after we create the new view container
1019
1020        // Make sure this will count as current page after resize
1021        // console.log('fudging for index ' + this.currentIndex() + ' (page ' + this.getPageNum(this.currentIndex()) + ')');
1022        var fudgeFactor = (this.getPageHeight(this.currentIndex()) / this.reduce) * 0.6;
1023        var oldLeafTop = this.onePageGetPageTop(this.currentIndex()) + fudgeFactor;
1024        var oldViewDimensions = this.onePageCalculateViewDimensions(this.reduce, this.padding);
1025        scrollRatio = oldLeafTop / oldViewDimensions.height;
1026    }
1027
1028    // Recalculate 1up reduction factors
1029    this.onePageCalculateReductionFactors( $('#BRcontainer').attr('clientWidth'),
1030                                           $('#BRcontainer').attr('clientHeight') );
1031    // Update current reduce (if in autofit)
1032    if (this.onePage.autofit) {
1033        var reductionFactor = this.nextReduce(this.reduce, this.onePage.autofit, this.onePage.reductionFactors);
1034        this.reduce = reductionFactor.reduce;
1035    }
1036
1037    var viewDimensions = this.onePageCalculateViewDimensions(this.reduce, this.padding);
1038    $('#BRpageview').height(viewDimensions.height);
1039    $('#BRpageview').width(viewDimensions.width);
1040
1041    var newCenterY = scrollRatio*viewDimensions.height;
1042    var newTop = Math.max(0, Math.floor( newCenterY - $('#BRcontainer').height()/2 ));
1043    $('#BRcontainer').attr('scrollTop', newTop);
1044
1045    // We use clientWidth here to avoid miscalculating due to scroll bar
1046    var newCenterX = oldCenterX * (viewWidth / oldPageViewWidth);
1047    var newLeft = newCenterX - $('#BRcontainer').attr('clientWidth') / 2;
1048    newLeft = Math.max(newLeft, 0);
1049    $('#BRcontainer').attr('scrollLeft', newLeft);
1050    //console.log('oldCenterX ' + oldCenterX + ' newCenterX ' + newCenterX + ' newLeft ' + newLeft);
1051
1052    //this.centerPageView();
1053    this.loadLeafs();
1054
1055    this.removeSearchHilites();
1056    this.updateSearchHilites();
1057}
1058
1059// Calculate the dimensions for a one page view with images at the given reduce and padding
1060BookReader.prototype.onePageCalculateViewDimensions = function(reduce, padding) {
1061    var viewWidth = 0;
1062    var viewHeight = 0;
1063    for (i=0; i<this.numLeafs; i++) {
1064        viewHeight += parseInt(this._getPageHeight(i)/this.reduce) + this.padding;
1065        var width = parseInt(this._getPageWidth(i)/this.reduce);
1066        if (width>viewWidth) viewWidth=width;
1067    }
1068    return { width: viewWidth, height: viewHeight }
1069}
1070
1071// centerX1up()
1072//______________________________________________________________________________
1073// Returns the current offset of the viewport center in scaled document coordinates.
1074BookReader.prototype.centerX1up = function() {
1075    var centerX;
1076    if ($('#BRpageview').width() < $('#BRcontainer').attr('clientWidth')) { // fully shown
1077        centerX = $('#BRpageview').width();
1078    } else {
1079        centerX = $('#BRcontainer').attr('scrollLeft') + $('#BRcontainer').attr('clientWidth') / 2;
1080    }
1081    centerX = Math.floor(centerX);
1082    return centerX;
1083}
1084
1085// centerY1up()
1086//______________________________________________________________________________
1087// Returns the current offset of the viewport center in scaled document coordinates.
1088BookReader.prototype.centerY1up = function() {
1089    var centerY = $('#BRcontainer').attr('scrollTop') + $('#BRcontainer').height() / 2;
1090    return Math.floor(centerY);
1091}
1092
1093// centerPageView()
1094//______________________________________________________________________________
1095BookReader.prototype.centerPageView = function() {
1096
1097    var scrollWidth  = $('#BRcontainer').attr('scrollWidth');
1098    var clientWidth  =  $('#BRcontainer').attr('clientWidth');
1099    //console.log('sW='+scrollWidth+' cW='+clientWidth);
1100    if (scrollWidth > clientWidth) {
1101        $('#BRcontainer').attr('scrollLeft', (scrollWidth-clientWidth)/2);
1102    }
1103
1104}
1105
1106// zoom2up(direction)
1107//______________________________________________________________________________
1108BookReader.prototype.zoom2up = function(direction) {
1109
1110    // Hard stop autoplay
1111    this.stopFlipAnimations();
1112
1113    // Recalculate autofit factors
1114    this.twoPageCalculateReductionFactors();
1115
1116    // Get new zoom state
1117    var reductionFactor = this.nextReduce(this.reduce, direction, this.twoPage.reductionFactors);
1118    if ((this.reduce == reductionFactor.reduce) && (this.twoPage.autofit == reductionFactor.autofit)) {
1119        // Same zoom
1120        return;
1121    }
1122    this.twoPage.autofit = reductionFactor.autofit;
1123    this.reduce = reductionFactor.reduce;
1124    this.pageScale = this.reduce; // preserve current reduce
1125
1126    // Preserve view center position
1127    var oldCenter = this.twoPageGetViewCenter();
1128
1129    // If zooming in, reload imgs.  DOM elements will be removed by prepareTwoPageView
1130    // $$$ An improvement would be to use the low res image until the larger one is loaded.
1131    if (1 == direction) {
1132        for (var img in this.prefetchedImgs) {
1133            delete this.prefetchedImgs[img];
1134        }
1135    }
1136
1137    // Prepare view with new center to minimize visual glitches
1138    this.prepareTwoPageView(oldCenter.percentageX, oldCenter.percentageY);
1139}
1140
1141BookReader.prototype.zoomThumb = function(direction) {
1142    var oldColumns = this.thumbColumns;
1143    switch (direction) {
1144        case -1:
1145            this.thumbColumns += 1;
1146            break;
1147        case 1:
1148            this.thumbColumns -= 1;
1149            break;
1150    }
1151
1152    // clamp
1153    if (this.thumbColumns < 2) {
1154        this.thumbColumns = 2;
1155    } else if (this.thumbColumns > 8) {
1156        this.thumbColumns = 8;
1157    }
1158
1159    if (this.thumbColumns != oldColumns) {
1160        this.prepareThumbnailView();
1161    }
1162}
1163
1164// Returns the width per thumbnail to display the requested number of columns
1165// Note: #BRpageview must already exist since its width is used to calculate the
1166//       thumbnail width
1167BookReader.prototype.getThumbnailWidth = function(thumbnailColumns) {
1168    var padding = (thumbnailColumns + 1) * this.thumbPadding;
1169    var width = ($('#BRpageview').width() - padding) / (thumbnailColumns + 0.5); // extra 0.5 is for some space at sides
1170    return parseInt(width);
1171}
1172
1173// quantizeReduce(reduce)
1174//______________________________________________________________________________
1175// Quantizes the given reduction factor to closest power of two from set from 12.5% to 200%
1176BookReader.prototype.quantizeReduce = function(reduce, reductionFactors) {
1177    var quantized = reductionFactors[0].reduce;
1178    var distance = Math.abs(reduce - quantized);
1179    for (var i = 1; i < reductionFactors.length; i++) {
1180        newDistance = Math.abs(reduce - reductionFactors[i].reduce);
1181        if (newDistance < distance) {
1182            distance = newDistance;
1183            quantized = reductionFactors[i].reduce;
1184        }
1185    }
1186
1187    return quantized;
1188}
1189
1190// reductionFactors should be array of sorted reduction factors
1191// e.g. [ {reduce: 0.25, autofit: null}, {reduce: 0.3, autofit: 'width'}, {reduce: 1, autofit: null} ]
1192BookReader.prototype.nextReduce = function( currentReduce, direction, reductionFactors ) {
1193
1194    // XXX add 'closest', to replace quantize function
1195
1196    if (direction == 'in') {
1197        var newReduceIndex = 0;
1198
1199        for (var i = 1; i < reductionFactors.length; i++) {
1200            if (reductionFactors[i].reduce < currentReduce) {
1201                newReduceIndex = i;
1202            }
1203        }
1204        return reductionFactors[newReduceIndex];
1205
1206    } else if (direction == 'out') { // zoom out
1207        var lastIndex = reductionFactors.length - 1;
1208        var newReduceIndex = lastIndex;
1209
1210        for (var i = lastIndex; i >= 0; i--) {
1211            if (reductionFactors[i].reduce > currentReduce) {
1212                newReduceIndex = i;
1213            }
1214        }
1215        return reductionFactors[newReduceIndex];
1216    }
1217
1218    // Asked for specific autofit mode
1219    for (var i = 0; i < reductionFactors.length; i++) {
1220        if (reductionFactors[i].autofit == direction) {
1221            return reductionFactors[i];
1222        }
1223    }
1224
1225    alert('Could not find reduction factor for direction ' + direction);
1226    return reductionFactors[0];
1227
1228}
1229
1230BookReader.prototype._reduceSort = function(a, b) {
1231    return a.reduce - b.reduce;
1232}
1233
1234// jumpToPage()
1235//______________________________________________________________________________
1236// Attempts to jump to page.  Returns true if page could be found, false otherwise.
1237BookReader.prototype.jumpToPage = function(pageNum) {
1238
1239    var pageIndex;
1240
1241    // Check for special "leaf"
1242    var re = new RegExp('^leaf(\\d+)');
1243    leafMatch = re.exec(pageNum);
1244    if (leafMatch) {
1245        console.log(leafMatch[1]);
1246        pageIndex = this.leafNumToIndex(parseInt(leafMatch[1],10));
1247        if (pageIndex === null) {
1248            pageIndex = undefined; // to match return type of getPageIndex
1249        }
1250
1251    } else {
1252        pageIndex = this.getPageIndex(pageNum);
1253    }
1254
1255    if ('undefined' != typeof(pageIndex)) {
1256        var leafTop = 0;
1257        var h;
1258        this.jumpToIndex(pageIndex);
1259        $('#BRcontainer').attr('scrollTop', leafTop);
1260        return true;
1261    }
1262
1263    // Page not found
1264    return false;
1265}
1266
1267// jumpToIndex()
1268//______________________________________________________________________________
1269BookReader.prototype.jumpToIndex = function(index, pageX, pageY) {
1270
1271    this.willChangeToIndex(index);
1272
1273    this.ttsStop();
1274
1275    if (this.constMode2up == this.mode) {
1276        this.autoStop();
1277
1278        // By checking against min/max we do nothing if requested index
1279        // is current
1280        if (index < Math.min(this.twoPage.currentIndexL, this.twoPage.currentIndexR)) {
1281            this.flipBackToIndex(index);
1282        } else if (index > Math.max(this.twoPage.currentIndexL, this.twoPage.currentIndexR)) {
1283            this.flipFwdToIndex(index);
1284        }
1285
1286    } else if (this.constModeThumb == this.mode) {
1287        var viewWidth = $('#BRcontainer').attr('scrollWidth') - 20; // width minus buffer
1288        var i;
1289        var leafWidth = 0;
1290        var leafHeight = 0;
1291        var rightPos = 0;
1292        var bottomPos = 0;
1293        var rowHeight = 0;
1294        var leafTop = 0;
1295        var leafIndex = 0;
1296
1297        for (i=0; i<(index+1); i++) {
1298            leafWidth = this.thumbWidth;
1299            if (rightPos + (leafWidth + this.thumbPadding) > viewWidth){
1300                rightPos = 0;
1301                rowHeight = 0;
1302                leafIndex = 0;
1303            }
1304
1305            leafHeight = parseInt((this.getPageHeight(leafIndex)*this.thumbWidth)/this.getPageWidth(leafIndex), 10);
1306            if (leafHeight > rowHeight) { rowHeight = leafHeight; }
1307            if (leafIndex==0) { leafTop = bottomPos; }
1308            if (leafIndex==0) { bottomPos += this.thumbPadding + rowHeight; }
1309            rightPos += leafWidth + this.thumbPadding;
1310            leafIndex++;
1311        }
1312        this.firstIndex=index;
1313        if ($('#BRcontainer').attr('scrollTop') == leafTop) {
1314            this.loadLeafs();
1315        } else {
1316            $('#BRcontainer').animate({scrollTop: leafTop },'fast');
1317        }
1318    } else {
1319        // 1up
1320        var leafTop = this.onePageGetPageTop(index);
1321
1322        if (pageY) {
1323            //console.log('pageY ' + pageY);
1324            var offset = parseInt( (pageY) / this.reduce);
1325            offset -= $('#BRcontainer').attr('clientHeight') >> 1;
1326            //console.log( 'jumping to ' + leafTop + ' ' + offset);
1327            leafTop += offset;
1328        } else {
1329            // Show page just a little below the top
1330            leafTop -= this.padding / 2;
1331        }
1332
1333        if (pageX) {
1334            var offset = parseInt( (pageX) / this.reduce);
1335            offset -= $('#BRcontainer').attr('clientWidth') >> 1;
1336            leafLeft += offset;
1337        } else {
1338            // Preserve left position
1339            leafLeft = $('#BRcontainer').scrollLeft();
1340        }
1341
1342        //$('#BRcontainer').attr('scrollTop', leafTop);
1343        $('#BRcontainer').animate({scrollTop: leafTop, scrollLeft: leafLeft },'fast');
1344    }
1345}
1346
1347// switchMode()
1348//______________________________________________________________________________
1349BookReader.prototype.switchMode = function(mode) {
1350
1351    if (mode == this.mode) {
1352        return;
1353    }
1354
1355    if (!this.canSwitchToMode(mode)) {
1356        return;
1357    }
1358
1359    this.autoStop();
1360    this.ttsStop();
1361    this.removeSearchHilites();
1362
1363    this.mode = mode;
1364    //this.switchToolbarMode(mode);
1365
1366    // reinstate scale if moving from thumbnail view
1367    if (this.pageScale != this.reduce) {
1368        this.reduce = this.pageScale;
1369    }
1370
1371    // $$$ TODO preserve center of view when switching between mode
1372    //     See https://bugs.edge.launchpad.net/gnubook/+bug/416682
1373
1374    // XXX maybe better to preserve zoom in each mode
1375    if (1 == mode) {
1376        this.onePageCalculateReductionFactors( $('#BRcontainer').attr('clientWidth'), $('#BRcontainer').attr('clientHeight'));
1377        this.reduce = this.quantizeReduce(this.reduce, this.onePage.reductionFactors);
1378        this.prepareOnePageView();
1379    } else if (3 == mode) {
1380        this.reduce = this.quantizeReduce(this.reduce, this.reductionFactors);
1381        this.prepareThumbnailView();
1382    } else {
1383        // $$$ why don't we save autofit?
1384        // this.twoPage.autofit = null; // Take zoom level from other mode
1385        this.twoPageCalculateReductionFactors();
1386        this.reduce = this.quantizeReduce(this.reduce, this.twoPage.reductionFactors);
1387        this.prepareTwoPageView();
1388        this.twoPageCenterView(0.5, 0.5); // $$$ TODO preserve center
1389    }
1390
1391}
1392
1393//prepareOnePageView()
1394//______________________________________________________________________________
1395BookReader.prototype.prepareOnePageView = function() {
1396
1397    // var startLeaf = this.displayedIndices[0];
1398    var startLeaf = this.currentIndex();
1399
1400    $('#BRcontainer').empty();
1401    $('#BRcontainer').css({
1402        overflowY: 'scroll',
1403        overflowX: 'auto'
1404    });
1405
1406    $("#BRcontainer").append("<div id='BRpageview'></div>");
1407
1408    // Attaches to first child - child must be present
1409    $('#BRcontainer').dragscrollable();
1410    this.bindGestures($('#BRcontainer'));
1411
1412    // $$$ keep select enabled for now since disabling it breaks keyboard
1413    //     nav in FF 3.6 (https://bugs.edge.launchpad.net/bookreader/+bug/544666)
1414    // BookReader.util.disableSelect($('#BRpageview'));
1415
1416    this.resizePageView();
1417
1418    this.jumpToIndex(startLeaf);
1419    this.displayedIndices = [];
1420
1421    this.drawLeafsOnePage();
1422}
1423
1424//prepareThumbnailView()
1425//______________________________________________________________________________
1426BookReader.prototype.prepareThumbnailView = function() {
1427
1428    $('#BRcontainer').empty();
1429    $('#BRcontainer').css({
1430        overflowY: 'scroll',
1431        overflowX: 'auto'
1432    });
1433
1434    $("#BRcontainer").append("<div id='BRpageview'></div>");
1435
1436    $('#BRcontainer').dragscrollable();
1437    this.bindGestures($('#BRcontainer'));
1438
1439    // $$$ keep select enabled for now since disabling it breaks keyboard
1440    //     nav in FF 3.6 (https://bugs.edge.launchpad.net/bookreader/+bug/544666)
1441    // BookReader.util.disableSelect($('#BRpageview'));
1442
1443    this.thumbWidth = this.getThumbnailWidth(this.thumbColumns);
1444    this.reduce = this.getPageWidth(0)/this.thumbWidth;
1445
1446    this.displayedRows = [];
1447
1448    // Draw leafs with current index directly in view (no animating to the index)
1449    this.drawLeafsThumbnail( this.currentIndex() );
1450
1451}
1452
1453// prepareTwoPageView()
1454//______________________________________________________________________________
1455// Some decisions about two page view:
1456//
1457// Both pages will be displayed at the same height, even if they were different physical/scanned
1458// sizes.  This simplifies the animation (from a design as well as technical standpoint).  We
1459// examine the page aspect ratios (in calculateSpreadSize) and use the page with the most "normal"
1460// aspect ratio to determine the height.
1461//
1462// The two page view div is resized to keep the middle of the book in the middle of the div
1463// even as the page sizes change.  To e.g. keep the middle of the book in the middle of the BRcontent
1464// div requires adjusting the offset of BRtwpageview and/or scrolling in BRcontent.
1465BookReader.prototype.prepareTwoPageView = function(centerPercentageX, centerPercentageY) {
1466    $('#BRcontainer').empty();
1467    $('#BRcontainer').css('overflow', 'auto');
1468
1469    // We want to display two facing pages.  We may be missing
1470    // one side of the spread because it is the first/last leaf,
1471    // foldouts, missing pages, etc
1472
1473    //var targetLeaf = this.displayedIndices[0];
1474    var targetLeaf = this.firstIndex;
1475
1476    if (targetLeaf < this.firstDisplayableIndex()) {
1477        targetLeaf = this.firstDisplayableIndex();
1478    }
1479
1480    if (targetLeaf > this.lastDisplayableIndex()) {
1481        targetLeaf = this.lastDisplayableIndex();
1482    }
1483
1484    //this.twoPage.currentIndexL = null;
1485    //this.twoPage.currentIndexR = null;
1486    //this.pruneUnusedImgs();
1487
1488    var currentSpreadIndices = this.getSpreadIndices(targetLeaf);
1489    this.twoPage.currentIndexL = currentSpreadIndices[0];
1490    this.twoPage.currentIndexR = currentSpreadIndices[1];
1491    this.firstIndex = this.twoPage.currentIndexL;
1492
1493    this.calculateSpreadSize(); //sets twoPage.width, twoPage.height and others
1494
1495    this.pruneUnusedImgs();
1496    this.prefetch(); // Preload images or reload if scaling has changed
1497
1498    //console.dir(this.twoPage);
1499
1500    // Add the two page view
1501    // $$$ Can we get everything set up and then append?
1502    $('#BRcontainer').append('<div id="BRtwopageview"></div>');
1503
1504    // Attaches to first child, so must come after we add the page view
1505    //$('#BRcontainer').dragscrollable();
1506    this.bindGestures($('#BRcontainer'));
1507
1508    // $$$ calculate first then set
1509    $('#BRtwopageview').css( {
1510        height: this.twoPage.totalHeight + 'px',
1511        width: this.twoPage.totalWidth + 'px',
1512        position: 'absolute'
1513        });
1514
1515    // If there will not be scrollbars (e.g. when zooming out) we center the book
1516    // since otherwise the book will be stuck off-center
1517    if (this.twoPage.totalWidth < $('#BRcontainer').attr('clientWidth')) {
1518        centerPercentageX = 0.5;
1519    }
1520    if (this.twoPage.totalHeight < $('#BRcontainer').attr('clientHeight')) {
1521        centerPercentageY = 0.5;
1522    }
1523
1524    this.twoPageCenterView(centerPercentageX, centerPercentageY);
1525
1526    this.twoPage.coverDiv = document.createElement('div');
1527    $(this.twoPage.coverDiv).attr('id', 'BRbookcover').css({
1528        width:  this.twoPage.bookCoverDivWidth + 'px',
1529        height: this.twoPage.bookCoverDivHeight+'px',
1530        visibility: 'visible'
1531    }).appendTo('#BRtwopageview');
1532
1533    this.leafEdgeR = document.createElement('div');
1534    this.leafEdgeR.className = 'BRleafEdgeR';
1535    $(this.leafEdgeR).css({
1536        width: this.twoPage.leafEdgeWidthR + 'px',
1537        height: this.twoPage.height + 'px',
1538        left: this.twoPage.gutter+this.twoPage.scaledWR+'px',
1539        top: this.twoPage.bookCoverDivTop+this.twoPage.coverInternalPadding+'px'
1540    }).appendTo('#BRtwopageview');
1541
1542    this.leafEdgeL = document.createElement('div');
1543    this.leafEdgeL.className = 'BRleafEdgeL';
1544    $(this.leafEdgeL).css({
1545        width: this.twoPage.leafEdgeWidthL + 'px',
1546        height: this.twoPage.height + 'px',
1547        left: this.twoPage.bookCoverDivLeft+this.twoPage.coverInternalPadding+'px',
1548        top: this.twoPage.bookCoverDivTop+this.twoPage.coverInternalPadding+'px'
1549    }).appendTo('#BRtwopageview');
1550
1551    div = document.createElement('div');
1552    $(div).attr('id', 'BRgutter').css({
1553        width:           this.twoPage.bookSpineDivWidth+'px',
1554        height:          this.twoPage.bookSpineDivHeight+'px',
1555        left:            (this.twoPage.gutter - this.twoPage.bookSpineDivWidth*0.5)+'px',
1556        top:             this.twoPage.bookSpineDivTop+'px'
1557    }).appendTo('#BRtwopageview');
1558
1559    var self = this; // for closure
1560
1561    /* Flip areas no longer used
1562    this.twoPage.leftFlipArea = document.createElement('div');
1563    this.twoPage.leftFlipArea.className = 'BRfliparea';
1564    $(this.twoPage.leftFlipArea).attr('id', 'BRleftflip').css({
1565        border: '0',
1566        width:  this.twoPageFlipAreaWidth() + 'px',
1567        height: this.twoPageFlipAreaHeight() + 'px',
1568        position: 'absolute',
1569        left:   this.twoPageLeftFlipAreaLeft() + 'px',
1570        top:    this.twoPageFlipAreaTop() + 'px',
1571        cursor: 'w-resize',
1572        zIndex: 100
1573    }).click(function(e) {
1574        self.left();
1575    }).bind('mousedown', function(e) {
1576        e.preventDefault();
1577    }).appendTo('#BRtwopageview');
1578
1579    this.twoPage.rightFlipArea = document.createElement('div');
1580    this.twoPage.rightFlipArea.className = 'BRfliparea';
1581    $(this.twoPage.rightFlipArea).attr('id', 'BRrightflip').css({
1582        border: '0',
1583        width:  this.twoPageFlipAreaWidth() + 'px',
1584        height: this.twoPageFlipAreaHeight() + 'px',
1585        position: 'absolute',
1586        left:   this.twoPageRightFlipAreaLeft() + 'px',
1587        top:    this.twoPageFlipAreaTop() + 'px',
1588        cursor: 'e-resize',
1589        zIndex: 100
1590    }).click(function(e) {
1591        self.right();
1592    }).bind('mousedown', function(e) {
1593        e.preventDefault();
1594    }).appendTo('#BRtwopageview');
1595    */
1596
1597    this.prepareTwoPagePopUp();
1598
1599    this.displayedIndices = [];
1600
1601    //this.indicesToDisplay=[firstLeaf, firstLeaf+1];
1602    //console.log('indicesToDisplay: ' + this.indicesToDisplay[0] + ' ' + this.indicesToDisplay[1]);
1603
1604    this.drawLeafsTwoPage();
1605    this.updateToolbarZoom(this.reduce);
1606
1607    this.prefetch();
1608
1609    this.removeSearchHilites();
1610    this.updateSearchHilites();
1611
1612}
1613
1614// prepareTwoPagePopUp()
1615//
1616// This function prepares the "View Page n" popup that shows while the mouse is
1617// over the left/right "stack of sheets" edges.  It also binds the mouse
1618// events for these divs.
1619//______________________________________________________________________________
1620BookReader.prototype.prepareTwoPagePopUp = function() {
1621
1622    this.twoPagePopUp = document.createElement('div');
1623    this.twoPagePopUp.className = 'BRtwoPagePopUp';
1624    $(this.twoPagePopUp).css({
1625        zIndex: '1000'
1626    }).appendTo('#BRcontainer');
1627    $(this.twoPagePopUp).hide();
1628
1629    $(this.leafEdgeL).add(this.leafEdgeR).bind('mouseenter', this, function(e) {
1630        $(e.data.twoPagePopUp).show();
1631    });
1632
1633    $(this.leafEdgeL).add(this.leafEdgeR).bind('mouseleave', this, function(e) {
1634        $(e.data.twoPagePopUp).hide();
1635    });
1636
1637    $(this.leafEdgeL).bind('click', this, function(e) {
1638        e.data.autoStop();
1639        e.data.ttsStop();
1640        var jumpIndex = e.data.jumpIndexForLeftEdgePageX(e.pageX);
1641        e.data.jumpToIndex(jumpIndex);
1642    });
1643
1644    $(this.leafEdgeR).bind('click', this, function(e) {
1645        e.data.autoStop();
1646        e.data.ttsStop();
1647        var jumpIndex = e.data.jumpIndexForRightEdgePageX(e.pageX);
1648        e.data.jumpToIndex(jumpIndex);
1649    });
1650
1651    $(this.leafEdgeR).bind('mousemove', this, function(e) {
1652
1653        var jumpIndex = e.data.jumpIndexForRightEdgePageX(e.pageX);
1654        $(e.data.twoPagePopUp).text('View ' + e.data.getPageName(jumpIndex));
1655
1656        // $$$ TODO: Make sure popup is positioned so that it is in view
1657        // (https://bugs.edge.launchpad.net/gnubook/+bug/327456)
1658        $(e.data.twoPagePopUp).css({
1659            left: e.pageX- $('#BRcontainer').offset().left + $('#BRcontainer').scrollLeft() - 100 + 'px',
1660            top: e.pageY - $('#BRcontainer').offset().top + $('#BRcontainer').scrollTop() + 'px'
1661        });
1662    });
1663
1664    $(this.leafEdgeL).bind('mousemove', this, function(e) {
1665
1666        var jumpIndex = e.data.jumpIndexForLeftEdgePageX(e.pageX);
1667        $(e.data.twoPagePopUp).text('View '+ e.data.getPageName(jumpIndex));
1668
1669        // $$$ TODO: Make sure popup is positioned so that it is in view
1670        //           (https://bugs.edge.launchpad.net/gnubook/+bug/327456)
1671        $(e.data.twoPagePopUp).css({
1672            left: e.pageX - $('#BRcontainer').offset().left + $('#BRcontainer').scrollLeft() - $(e.data.twoPagePopUp).width() + 100 + 'px',
1673            top: e.pageY-$('#BRcontainer').offset().top + $('#BRcontainer').scrollTop() + 'px'
1674        });
1675    });
1676}
1677
1678// calculateSpreadSize()
1679//______________________________________________________________________________
1680// Calculates 2-page spread dimensions based on this.twoPage.currentIndexL and
1681// this.twoPage.currentIndexR
1682// This function sets this.twoPage.height, twoPage.width
1683
1684BookReader.prototype.calculateSpreadSize = function() {
1685
1686    var firstIndex  = this.twoPage.currentIndexL;
1687    var secondIndex = this.twoPage.currentIndexR;
1688    //console.log('first page is ' + firstIndex);
1689
1690    // Calculate page sizes and total leaf width
1691    var spreadSize;
1692    if ( this.twoPage.autofit) {
1693        spreadSize = this.getIdealSpreadSize(firstIndex, secondIndex);
1694    } else {
1695        // set based on reduction factor
1696        spreadSize = this.getSpreadSizeFromReduce(firstIndex, secondIndex, this.reduce);
1697    }
1698
1699    // Both pages together
1700    this.twoPage.height = spreadSize.height;
1701    this.twoPage.width = spreadSize.width;
1702
1703    // Individual pages
1704    this.twoPage.scaledWL = this.getPageWidth2UP(firstIndex);
1705    this.twoPage.scaledWR = this.getPageWidth2UP(secondIndex);
1706
1707    // Leaf edges
1708    this.twoPage.edgeWidth = spreadSize.totalLeafEdgeWidth; // The combined width of both edges
1709    this.twoPage.leafEdgeWidthL = this.leafEdgeWidth(this.twoPage.currentIndexL);
1710    this.twoPage.leafEdgeWidthR = this.twoPage.edgeWidth - this.twoPage.leafEdgeWidthL;
1711
1712
1713    // Book cover
1714    // The width of the book cover div.  The combined width of both pages, twice the width
1715    // of the book cover internal padding (2*10) and the page edges
1716    this.twoPage.bookCoverDivWidth = this.twoPageCoverWidth(this.twoPage.scaledWL + this.twoPage.scaledWR);
1717    // The height of the book cover div
1718    this.twoPage.bookCoverDivHeight = this.twoPage.height + 2 * this.twoPage.coverInternalPadding;
1719
1720
1721    // We calculate the total width and height for the div so that we can make the book
1722    // spine centered
1723    var leftGutterOffset = this.gutterOffsetForIndex(firstIndex);
1724    var leftWidthFromCenter = this.twoPage.scaledWL - leftGutterOffset + this.twoPage.leafEdgeWidthL;
1725    var rightWidthFromCenter = this.twoPage.scaledWR + leftGutterOffset + this.twoPage.leafEdgeWidthR;
1726    var largestWidthFromCenter = Math.max( leftWidthFromCenter, rightWidthFromCenter );
1727    this.twoPage.totalWidth = 2 * (largestWidthFromCenter + this.twoPage.coverInternalPadding + this.twoPage.coverExternalPadding);
1728    this.twoPage.totalHeight = this.twoPage.height + 2 * (this.twoPage.coverInternalPadding + this.twoPage.coverExternalPadding);
1729
1730    // We want to minimize the unused space in two-up mode (maximize the amount of page
1731    // shown).  We give width to the leaf edges and these widths change (though the sum
1732    // of the two remains constant) as we flip through the book.  With the book
1733    // cover centered and fixed in the BRcontainer div the page images will meet
1734    // at the "gutter" which is generally offset from the center.
1735    this.twoPage.middle = this.twoPage.totalWidth >> 1;
1736    this.twoPage.gutter = this.twoPage.middle + this.gutterOffsetForIndex(firstIndex);
1737
1738    // The left edge of the book cover moves depending on the width of the pages
1739    // $$$ change to getter
1740    this.twoPage.bookCoverDivLeft = this.twoPage.gutter - this.twoPage.scaledWL - this.twoPage.leafEdgeWidthL - this.twoPage.coverInternalPadding;
1741    // The top edge of the book cover stays a fixed distance from the top
1742    this.twoPage.bookCoverDivTop = this.twoPage.coverExternalPadding;
1743
1744    // Book spine
1745    this.twoPage.bookSpineDivHeight = this.twoPage.height + 2*this.twoPage.coverInternalPadding;
1746    this.twoPage.bookSpineDivLeft = this.twoPage.middle - (this.twoPage.bookSpineDivWidth >> 1);
1747    this.twoPage.bookSpineDivTop = this.twoPage.bookCoverDivTop;
1748
1749
1750    this.reduce = spreadSize.reduce; // $$$ really set this here?
1751}
1752
1753BookReader.prototype.getIdealSpreadSize = function(firstIndex, secondIndex) {
1754    var ideal = {};
1755
1756    // We check which page is closest to a "normal" page and use that to set the height
1757    // for both pages.  This means that foldouts and other odd size pages will be displayed
1758    // smaller than the nominal zoom amount.
1759    var canon5Dratio = 1.5;
1760
1761    var first = {
1762        height: this._getPageHeight(firstIndex),
1763        width: this._getPageWidth(firstIndex)
1764    }
1765
1766    var second = {
1767        height: this._getPageHeight(secondIndex),
1768        width: this._getPageWidth(secondIndex)
1769    }
1770
1771    var firstIndexRatio  = first.height / first.width;
1772    var secondIndexRatio = second.height / second.width;
1773    //console.log('firstIndexRatio = ' + firstIndexRatio + ' secondIndexRatio = ' + secondIndexRatio);
1774
1775    var ratio;
1776    if (Math.abs(firstIndexRatio - canon5Dratio) < Math.abs(secondIndexRatio - canon5Dratio)) {
1777        ratio = firstIndexRatio;
1778    } else {
1779        ratio = secondIndexRatio;
1780    }
1781
1782    var totalLeafEdgeWidth = parseInt(this.numLeafs * 0.1);
1783    var maxLeafEdgeWidth   = parseInt($('#BRcontainer').attr('clientWidth') * 0.1);
1784    ideal.totalLeafEdgeWidth     = Math.min(totalLeafEdgeWidth, maxLeafEdgeWidth);
1785
1786    var widthOutsidePages = 2 * (this.twoPage.coverInternalPadding + this.twoPage.coverExternalPadding) + ideal.totalLeafEdgeWidth;
1787    var heightOutsidePages = 2* (this.twoPage.coverInternalPadding + this.twoPage.coverExternalPadding);
1788
1789    ideal.width = ($('#BRcontainer').width() - widthOutsidePages) >> 1;
1790    ideal.width -= 10; // $$$ fudge factor
1791    ideal.height = $('#BRcontainer').height() - heightOutsidePages;
1792    ideal.height -= 20; // fudge factor
1793    //console.log('init idealWidth='+ideal.width+' idealHeight='+ideal.height + ' ratio='+ratio);
1794
1795    if (ideal.height/ratio <= ideal.width) {
1796        //use height
1797        ideal.width = parseInt(ideal.height/ratio);
1798    } else {
1799        //use width
1800        ideal.height = parseInt(ideal.width*ratio);
1801    }
1802
1803    // $$$ check this logic with large spreads
1804    ideal.reduce = ((first.height + second.height) / 2) / ideal.height;
1805
1806    return ideal;
1807}
1808
1809// getSpreadSizeFromReduce()
1810//______________________________________________________________________________
1811// Returns the spread size calculated from the reduction factor for the given pages
1812BookReader.prototype.getSpreadSizeFromReduce = function(firstIndex, secondIndex, reduce) {
1813    var spreadSize = {};
1814    // $$$ Scale this based on reduce?
1815    var totalLeafEdgeWidth = parseInt(this.numLeafs * 0.1);
1816    var maxLeafEdgeWidth   = parseInt($('#BRcontainer').attr('clientWidth') * 0.1); // $$$ Assumes leaf edge width constant at all zoom levels
1817    spreadSize.totalLeafEdgeWidth     = Math.min(totalLeafEdgeWidth, maxLeafEdgeWidth);
1818
1819    // $$$ Possibly incorrect -- we should make height "dominant"
1820    var nativeWidth = this._getPageWidth(firstIndex) + this._getPageWidth(secondIndex);
1821    var nativeHeight = this._getPageHeight(firstIndex) + this._getPageHeight(secondIndex);
1822    spreadSize.height = parseInt( (nativeHeight / 2) / this.reduce );
1823    spreadSize.width = parseInt( (nativeWidth / 2) / this.reduce );
1824    spreadSize.reduce = reduce;
1825
1826    return spreadSize;
1827}
1828
1829// twoPageGetAutofitReduce()
1830//______________________________________________________________________________
1831// Returns the current ideal reduction factor
1832BookReader.prototype.twoPageGetAutofitReduce = function() {
1833    var spreadSize = this.getIdealSpreadSize(this.twoPage.currentIndexL, this.twoPage.currentIndexR);
1834    return spreadSize.reduce;
1835}
1836
1837// twoPageIsZoomedIn
1838//______________________________________________________________________________
1839// Returns true if the pages extend past the edge of the view
1840BookReader.prototype.twoPageIsZoomedIn = function() {
1841    var autofitReduce = this.twoPageGetAutofitReduce();
1842    var isZoomedIn = false;
1843    if (this.twoPage.autofit != 'auto') {
1844        if (this.reduce < this.twoPageGetAutofitReduce()) {
1845            isZoomedIn = true;
1846        }
1847    }
1848    return isZoomedIn;
1849}
1850
1851BookReader.prototype.onePageGetAutofitWidth = function() {
1852    var widthPadding = 20;
1853    return (this.getMedianPageSize().width + 0.0) / ($('#BRcontainer').attr('clientWidth') - widthPadding * 2);
1854}
1855
1856BookReader.prototype.onePageGetAutofitHeight = function() {
1857    return (this.getMedianPageSize().height + 0.0) / ($('#BRcontainer').attr('clientHeight') - this.padding * 2); // make sure a little of adjacent pages show
1858}
1859
1860// Returns where the top of the page with given index should be in one page view
1861BookReader.prototype.onePageGetPageTop = function(index)
1862{
1863    var i;
1864    var leafTop = 0;
1865    var leafLeft = 0;
1866    var h;
1867    for (i=0; i<index; i++) {
1868        h = parseInt(this._getPageHeight(i)/this.reduce);
1869        leafTop += h + this.padding;
1870    }
1871    return leafTop;
1872}
1873
1874BookReader.prototype.getMedianPageSize = function() {
1875    if (this._medianPageSize) {
1876        return this._medianPageSize;
1877    }
1878
1879    // A little expensive but we just do it once
1880    var widths = [];
1881    var heights = [];
1882    for (var i = 0; i < this.numLeafs; i++) {
1883        widths.push(this.getPageWidth(i));
1884        heights.push(this.getPageHeight(i));
1885    }
1886
1887    widths.sort();
1888    heights.sort();
1889
1890    this._medianPageSize = { width: widths[parseInt(widths.length / 2)], height: heights[parseInt(heights.length / 2)] };
1891    return this._medianPageSize;
1892}
1893
1894// Update the reduction factors for 1up mode given the available width and height.  Recalculates
1895// the autofit reduction factors.
1896BookReader.prototype.onePageCalculateReductionFactors = function( width, height ) {
1897    this.onePage.reductionFactors = this.reductionFactors.concat(
1898        [
1899            { reduce: this.onePageGetAutofitWidth(), autofit: 'width' },
1900            { reduce: this.onePageGetAutofitHeight(), autofit: 'height'}
1901        ]);
1902    this.onePage.reductionFactors.sort(this._reduceSort);
1903}
1904
1905BookReader.prototype.twoPageCalculateReductionFactors = function() {
1906    this.twoPage.reductionFactors = this.reductionFactors.concat(
1907        [
1908            { reduce: this.getIdealSpreadSize( this.twoPage.currentIndexL, this.twoPage.currentIndexR ).reduce,
1909              autofit: 'auto' }
1910        ]);
1911    this.twoPage.reductionFactors.sort(this._reduceSort);
1912}
1913
1914// twoPageSetCursor()
1915//______________________________________________________________________________
1916// Set the cursor for two page view
1917BookReader.prototype.twoPageSetCursor = function() {
1918    // console.log('setting cursor');
1919    if ( ($('#BRtwopageview').width() > $('#BRcontainer').attr('clientWidth')) ||
1920         ($('#BRtwopageview').height() > $('#BRcontainer').attr('clientHeight')) ) {
1921        $(this.prefetchedImgs[this.twoPage.currentIndexL]).css('cursor','move');
1922        $(this.prefetchedImgs[this.twoPage.currentIndexR]).css('cursor','move');
1923    } else {
1924        $(this.prefetchedImgs[this.twoPage.currentIndexL]).css('cursor','');
1925        $(this.prefetchedImgs[this.twoPage.currentIndexR]).css('cursor','');
1926    }
1927}
1928
1929// currentIndex()
1930//______________________________________________________________________________
1931// Returns the currently active index.
1932BookReader.prototype.currentIndex = function() {
1933    // $$$ we should be cleaner with our idea of which index is active in 1up/2up
1934    if (this.mode == this.constMode1up || this.mode == this.constModeThumb) {
1935        return this.firstIndex;
1935 // $$$ TODO page in center of view would be better
1936    } else if (this.mode == this.constMode2up) {
1937        // Only allow indices that are actually present in book
1938        return BookReader.util.clamp(this.firstIndex, 0, this.numLeafs - 1);
1939    } else {
1940        throw 'currentIndex called for unimplemented mode ' + this.mode;
1941    }
1942}
1943
1944// setCurrentIndex(index)
1945//______________________________________________________________________________
1946// Sets the idea of current index without triggering other actions such as animation.
1947// Compare to jumpToIndex which animates to that index
1948BookReader.prototype.setCurrentIndex = function(index) {
1949    this.firstIndex = index;
1950}
1951
1952
1953// right()
1954//______________________________________________________________________________
1955// Flip the right page over onto the left
1956BookReader.prototype.right = function() {
1957    if ('rl' != this.pageProgression) {
1958        // LTR
1959        this.next();
1960    } else {
1961        // RTL
1962        this.prev();
1963    }
1964}
1965
1966// rightmost()
1967//______________________________________________________________________________
1968// Flip to the rightmost page
1969BookReader.prototype.rightmost = function() {
1970    if ('rl' != this.pageProgression) {
1971        this.last();
1972    } else {
1973        this.first();
1974    }
1975}
1976
1977// left()
1978//______________________________________________________________________________
1979// Flip the left page over onto the right.
1980BookReader.prototype.left = function() {
1981    if ('rl' != this.pageProgression) {
1982        // LTR
1983        this.prev();
1984    } else {
1985        // RTL
1986        this.next();
1987    }
1988}
1989
1990// leftmost()
1991//______________________________________________________________________________
1992// Flip to the leftmost page
1993BookReader.prototype.leftmost = function() {
1994    if ('rl' != this.pageProgression) {
1995        this.first();
1996    } else {
1997        this.last();
1998    }
1999}
2000
2001// next()
2002//______________________________________________________________________________
2003BookReader.prototype.next = function() {
2004    if (2 == this.mode) {
2005        this.autoStop();
2006        this.flipFwdToIndex(null);
2007    } else {
2008        if (this.firstIndex < this.lastDisplayableIndex()) {
2009            this.jumpToIndex(this.firstIndex+1);
2010        }
2011    }
2012}
2013
2014// prev()
2015//______________________________________________________________________________
2016BookReader.prototype.prev = function() {
2017    if (2 == this.mode) {
2018        this.autoStop();
2019        this.flipBackToIndex(null);
2020    } else {
2021        if (this.firstIndex >= 1) {
2022            this.jumpToIndex(this.firstIndex-1);
2023        }
2024    }
2025}
2026
2027BookReader.prototype.first = function() {
2028    this.jumpToIndex(this.firstDisplayableIndex());
2029}
2030
2031BookReader.prototype.last = function() {
2032    this.jumpToIndex(this.lastDisplayableIndex());
2033}
2034
2035// scrollDown()
2036//______________________________________________________________________________
2037// Scrolls down one screen view
2038BookReader.prototype.scrollDown = function() {
2039    if ($.inArray(this.mode, [this.constMode1up, this.constModeThumb]) >= 0) {
2040        if ( this.mode == this.constMode1up && (this.reduce >= this.onePageGetAutofitHeight()) ) {
2041            // Whole pages are visible, scroll whole page only
2042            return this.next();
2043        }
2044
2045        $('#BRcontainer').animate(
2046            { scrollTop: '+=' + this._scrollAmount() + 'px'},
2047            400, 'easeInOutExpo'
2048        );
2049        return true;
2050    } else {
2051        return false;
2052    }
2053}
2054
2055// scrollUp()
2056//______________________________________________________________________________
2057// Scrolls up one screen view
2058BookReader.prototype.scrollUp = function() {
2059    if ($.inArray(this.mode, [this.constMode1up, this.constModeThumb]) >= 0) {
2060        if ( this.mode == this.constMode1up && (this.reduce >= this.onePageGetAutofitHeight()) ) {
2061            // Whole pages are visible, scroll whole page only
2062            return this.prev();
2063        }
2064
2065        $('#BRcontainer').animate(
2066            { scrollTop: '-=' + this._scrollAmount() + 'px'},
2067            400, 'easeInOutExpo'
2068        );
2069        return true;
2070    } else {
2071        return false;
2072    }
2073}
2074
2075// _scrollAmount()
2076//______________________________________________________________________________
2077// The amount to scroll vertically in integer pixels
2078BookReader.prototype._scrollAmount = function() {
2079    if (this.constMode1up == this.mode) {
2080        // Overlap by % of page size
2081        return parseInt($('#BRcontainer').attr('clientHeight') - this.getPageHeight(this.currentIndex()) / this.reduce * 0.03);
2082    }
2083
2084    return parseInt(0.9 * $('#BRcontainer').attr('clientHeight'));
2085}
2086
2087
2088// flipBackToIndex()
2089//______________________________________________________________________________
2090// to flip back one spread, pass index=null
2091BookReader.prototype.flipBackToIndex = function(index) {
2092
2093    if (1 == this.mode) return;
2094
2095    var leftIndex = this.twoPage.currentIndexL;
2096
2097    if (this.animating) return;
2098
2099    if (null != this.leafEdgeTmp) {
2100        alert('error: leafEdgeTmp should be null!');
2101        return;
2102    }
2103
2104    if (null == index) {
2105        index = leftIndex-2;
2106    }
2107    //if (index<0) return;
2108
2109    this.willChangeToIndex(index);
2110
2111    var previousIndices = this.getSpreadIndices(index);
2112
2113    if (previousIndices[0] < this.firstDisplayableIndex() || previousIndices[1] < this.firstDisplayableIndex()) {
2114        return;
2115    }
2116
2117    this.animating = true;
2118
2119    if ('rl' != this.pageProgression) {
2120        // Assume LTR and we are going backward
2121        this.prepareFlipLeftToRight(previousIndices[0], previousIndices[1]);
2122        this.flipLeftToRight(previousIndices[0], previousIndices[1]);
2123    } else {
2124        // RTL and going backward
2125        var gutter = this.prepareFlipRightToLeft(previousIndices[0], previousIndices[1]);
2126        this.flipRightToLeft(previousIndices[0], previousIndices[1], gutter);
2127    }
2128}
2129
2130// flipLeftToRight()
2131//______________________________________________________________________________
2132// Flips the page on the left towards the page on the right
2133BookReader.prototype.flipLeftToRight = function(newIndexL, newIndexR) {
2134
2135    var leftLeaf = this.twoPage.currentIndexL;
2136
2137    var oldLeafEdgeWidthL = this.leafEdgeWidth(this.twoPage.currentIndexL);
2138    var newLeafEdgeWidthL = this.leafEdgeWidth(newIndexL);
2139    var leafEdgeTmpW = oldLeafEdgeWidthL - newLeafEdgeWidthL;
2140
2141    var currWidthL   = this.getPageWidth2UP(leftLeaf);
2142    var newWidthL    = this.getPageWidth2UP(newIndexL);
2143    var newWidthR    = this.getPageWidth2UP(newIndexR);
2144
2145    var top  = this.twoPageTop();
2146    var gutter = this.twoPage.middle + this.gutterOffsetForIndex(newIndexL);
2147
2148    //console.log('leftEdgeTmpW ' + leafEdgeTmpW);
2149    //console.log('  gutter ' + gutter + ', scaledWL ' + scaledWL + ', newLeafEdgeWL ' + newLeafEdgeWidthL);
2150
2151    //animation strategy:
2152    // 0. remove search highlight, if any.
2153    // 1. create a new div, called leafEdgeTmp to represent the leaf edge between the leftmost edge
2154    //    of the left leaf and where the user clicked in the leaf edge.
2155    //    Note that if this function was triggered by left() and not a
2156    //    mouse click, the width of leafEdgeTmp is very small (zero px).
2157    // 2. animate both leafEdgeTmp to the gutter (without changing its width) and animate
2158    //    leftLeaf to width=0.
2159    // 3. When step 2 is finished, animate leafEdgeTmp to right-hand side of new right leaf
2160    //    (left=gutter+newWidthR) while also animating the new right leaf from width=0 to
2161    //    its new full width.
2162    // 4. After step 3 is finished, do the following:
2163    //      - remove leafEdgeTmp from the dom.
2164    //      - resize and move the right leaf edge (leafEdgeR) to left=gutter+newWidthR
2165    //          and width=twoPage.edgeWidth-newLeafEdgeWidthL.
2166    //      - resize and move the left leaf edge (leafEdgeL) to left=gutter-newWidthL-newLeafEdgeWidthL
2167    //          and width=newLeafEdgeWidthL.
2168    //      - resize the back cover (twoPage.coverDiv) to left=gutter-newWidthL-newLeafEdgeWidthL-10
2169    //          and width=newWidthL+newWidthR+twoPage.edgeWidth+20
2170    //      - move new left leaf (newIndexL) forward to zindex=2 so it can receive clicks.
2171    //      - remove old left and right leafs from the dom [pruneUnusedImgs()].
2172    //      - prefetch new adjacent leafs.
2173    //      - set up click handlers for both new left and right leafs.
2174    //      - redraw the search highlight.
2175    //      - update the pagenum box and the url.
2176
2177
2178    var leftEdgeTmpLeft = gutter - currWidthL - leafEdgeTmpW;
2179
2180    this.leafEdgeTmp = document.createElement('div');
2181    this.leafEdgeTmp.className = 'BRleafEdgeTmp';
2182    $(this.leafEdgeTmp).css({
2183        width: leafEdgeTmpW + 'px',
2184        height: this.twoPage.height + 'px',
2185        left: leftEdgeTmpLeft + 'px',
2186        top: top+'px',
2187        zIndex:1000
2188    }).appendTo('#BRtwopageview');
2189
2190    //$(this.leafEdgeL).css('width', newLeafEdgeWidthL+'px');
2191    $(this.leafEdgeL).css({
2192        width: newLeafEdgeWidthL+'px',
2193        left: gutter-currWidthL-newLeafEdgeWidthL+'px'
2194    });
2195
2196    // Left gets the offset of the current left leaf from the document
2197    var left = $(this.prefetchedImgs[leftLeaf]).offset().left;
2198    // $$$ This seems very similar to the gutter.  May be able to consolidate the logic.
2199    var right = $('#BRtwopageview').attr('clientWidth')-left-$(this.prefetchedImgs[leftLeaf]).width()+$('#BRtwopageview').offset().left-2+'px';
2200
2201    // We change the left leaf to right positioning
2202    // $$$ This causes animation glitches during resize.  See https://bugs.edge.launchpad.net/gnubook/+bug/328327
2203    $(this.prefetchedImgs[leftLeaf]).css({
2204        right: right,
2205        left: ''
2206    });
2207
2208    $(this.leafEdgeTmp).animate({left: gutter}, this.flipSpeed, 'easeInSine');
2209    //$(this.prefetchedImgs[leftLeaf]).animate({width: '0px'}, 'slow', 'easeInSine');
2210
2211    var self = this;
2212
2213    this.removeSearchHilites();
2214
2215    //console.log('animating leafLeaf ' + leftLeaf + ' to 0px');
2216    $(this.prefetchedImgs[leftLeaf]).animate({width: '0px'}, self.flipSpeed, 'easeInSine', function() {
2217
2218        //console.log('     and now leafEdgeTmp to left: gutter+newWidthR ' + (gutter + newWidthR));
2219        $(self.leafEdgeTmp).animate({left: gutter+newWidthR+'px'}, self.flipSpeed, 'easeOutSine');
2220
2221        $('#BRgutter').css({left: (gutter - self.twoPage.bookSpineDivWidth*0.5)+'px'});
2222
2223        //console.log('  animating newIndexR ' + newIndexR + ' to ' + newWidthR + ' from ' + $(self.prefetchedImgs[newIndexR]).width());
2224        $(self.prefetchedImgs[newIndexR]).animate({width: newWidthR+'px'}, self.flipSpeed, 'easeOutSine', function() {
2225            $(self.prefetchedImgs[newIndexL]).css('zIndex', 2);
2226
2227            //jquery adds display:block to the element style, which interferes with our print css
2228            $(self.prefetchedImgs[newIndexL]).css('display', '');
2229            $(self.prefetchedImgs[newIndexR]).css('display', '');
2230
2231            $(self.leafEdgeR).css({
2232                // Moves the right leaf edge
2233                width: self.twoPage.edgeWidth-newLeafEdgeWidthL+'px',
2234                left:  gutter+newWidthR+'px'
2235            });
2236
2237            $(self.leafEdgeL).css({
2238                // Moves and resizes the left leaf edge
2239                width: newLeafEdgeWidthL+'px',
2240                left:  gutter-newWidthL-newLeafEdgeWidthL+'px'
2241            });
2242
2243            // Resizes the brown border div
2244            $(self.twoPage.coverDiv).css({
2245                width: self.twoPageCoverWidth(newWidthL+newWidthR)+'px',
2246                left: gutter-newWidthL-newLeafEdgeWidthL-self.twoPage.coverInternalPadding+'px'
2247            });
2248
2249            $(self.leafEdgeTmp).remove();
2250            self.leafEdgeTmp = null;
2251
2252            // $$$ TODO refactor with opposite direction flip
2253
2254            self.twoPage.currentIndexL = newIndexL;
2255            self.twoPage.currentIndexR = newIndexR;
2256            self.twoPage.scaledWL = newWidthL;
2257            self.twoPage.scaledWR = newWidthR;
2258            self.twoPage.gutter = gutter;
2259
2260            self.firstIndex = self.twoPage.currentIndexL;
2261            self.displayedIndices = [newIndexL, newIndexR];
2262            self.pruneUnusedImgs();
2263            self.prefetch();
2264            self.animating = false;
2265
2266            self.updateSearchHilites2UP();
2267            self.updatePageNumBox2UP();
2268
2269            // self.twoPagePlaceFlipAreas(); // No longer used
2270            self.setMouseHandlers2UP();
2271            self.twoPageSetCursor();
2272
2273            if (self.animationFinishedCallback) {
2274                self.animationFinishedCallback();
2275                self.animationFinishedCallback = null;
2276            }
2277        });
2278    });
2279
2280}
2281
2282// flipFwdToIndex()
2283//______________________________________________________________________________
2284// Whether we flip left or right is dependent on the page progression
2285// to flip forward one spread, pass index=null
2286BookReader.prototype.flipFwdToIndex = function(index) {
2287
2288    if (this.animating) return;
2289
2290    if (null != this.leafEdgeTmp) {
2291        alert('error: leafEdgeTmp should be null!');
2292        return;
2293    }
2294
2295    if (null == index) {
2296        index = this.twoPage.currentIndexR+2; // $$$ assumes indices are continuous
2297    }
2298    if (index > this.lastDisplayableIndex()) return;
2299
2300    this.willChangeToIndex(index);
2301
2302    this.animating = true;
2303
2304    var nextIndices = this.getSpreadIndices(index);
2305
2306    //console.log('flipfwd to indices ' + nextIndices[0] + ',' + nextIndices[1]);
2307
2308    if ('rl' != this.pageProgression) {
2309        // We did not specify RTL
2310        var gutter = this.prepareFlipRightToLeft(nextIndices[0], nextIndices[1]);
2311        this.flipRightToLeft(nextIndices[0], nextIndices[1], gutter);
2312    } else {
2313        // RTL
2314        var gutter = this.prepareFlipLeftToRight(nextIndices[0], nextIndices[1]);
2315        this.flipLeftToRight(nextIndices[0], nextIndices[1]);
2316    }
2317}
2318
2319/*
2320 * Put handlers here for when we will navigate to a new page
2321 */
2322BookReader.prototype.willChangeToIndex = function(index)
2323{
2324    // Update navbar position icon - leads page change animation
2325    this.updateNavIndex(index);
2326}
2327
2328// flipRightToLeft(nextL, nextR, gutter)
2329// $$$ better not to have to pass gutter in
2330//______________________________________________________________________________
2331// Flip from left to right and show the nextL and nextR indices on those sides
2332BookReader.prototype.flipRightToLeft = function(newIndexL, newIndexR) {
2333    var oldLeafEdgeWidthL = this.leafEdgeWidth(this.twoPage.currentIndexL);
2334    var oldLeafEdgeWidthR = this.twoPage.edgeWidth-oldLeafEdgeWidthL;
2335    var newLeafEdgeWidthL = this.leafEdgeWidth(newIndexL);
2336    var newLeafEdgeWidthR = this.twoPage.edgeWidth-newLeafEdgeWidthL;
2337
2338    var leafEdgeTmpW = oldLeafEdgeWidthR - newLeafEdgeWidthR;
2339
2340    var top = this.twoPageTop();
2341    var scaledW = this.getPageWidth2UP(this.twoPage.currentIndexR);
2342
2343    var middle = this.twoPage.middle;
2344    var gutter = middle + this.gutterOffsetForIndex(newIndexL);
2345
2346    this.leafEdgeTmp = document.createElement('div');
2347    this.leafEdgeTmp.className = 'BRleafEdgeTmp';
2348    $(this.leafEdgeTmp).css({
2349        width: leafEdgeTmpW + 'px',
2350        height: this.twoPage.height + 'px',
2351        left: gutter+scaledW+'px',
2352        top: top+'px',
2353        zIndex:1000
2354    }).appendTo('#BRtwopageview');
2355
2356    //var scaledWR = this.getPageWidth2UP(newIndexR); // $$$ should be current instead?
2357    //var scaledWL = this.getPageWidth2UP(newIndexL); // $$$ should be current instead?
2358
2359    var currWidthL = this.getPageWidth2UP(this.twoPage.currentIndexL);
2360    var currWidthR = this.getPageWidth2UP(this.twoPage.currentIndexR);
2361    var newWidthL = this.getPageWidth2UP(newIndexL);
2362    var newWidthR = this.getPageWidth2UP(newIndexR);
2363
2364    $(this.leafEdgeR).css({width: newLeafEdgeWidthR+'px', left: gutter+newWidthR+'px' });
2365
2366    var self = this; // closure-tastic!
2367
2368    var speed = this.flipSpeed;
2369
2370    this.removeSearchHilites();
2371
2372    $(this.leafEdgeTmp).animate({left: gutter}, speed, 'easeInSine');
2373    $(this.prefetchedImgs[this.twoPage.currentIndexR]).animate({width: '0px'}, speed, 'easeInSine', function() {
2374        $('#BRgutter').css({left: (gutter - self.twoPage.bookSpineDivWidth*0.5)+'px'});
2375        $(self.leafEdgeTmp).animate({left: gutter-newWidthL-leafEdgeTmpW+'px'}, speed, 'easeOutSine');
2376        $(self.prefetchedImgs[newIndexL]).animate({width: newWidthL+'px'}, speed, 'easeOutSine', function() {
2377            $(self.prefetchedImgs[newIndexR]).css('zIndex', 2);
2378
2379            //jquery adds display:block to the element style, which interferes with our print css
2380            $(self.prefetchedImgs[newIndexL]).css('display', '');
2381            $(self.prefetchedImgs[newIndexR]).css('display', '');
2382
2383            $(self.leafEdgeL).css({
2384                width: newLeafEdgeWidthL+'px',
2385                left: gutter-newWidthL-newLeafEdgeWidthL+'px'
2386            });
2387
2388            // Resizes the book cover
2389            $(self.twoPage.coverDiv).css({
2390                width: self.twoPageCoverWidth(newWidthL+newWidthR)+'px',
2391                left: gutter - newWidthL - newLeafEdgeWidthL - self.twoPage.coverInternalPadding + 'px'
2392            });
2393
2394            $(self.leafEdgeTmp).remove();
2395            self.leafEdgeTmp = null;
2396
2397            self.twoPage.currentIndexL = newIndexL;
2398            self.twoPage.currentIndexR = newIndexR;
2399            self.twoPage.scaledWL = newWidthL;
2400            self.twoPage.scaledWR = newWidthR;
2401            self.twoPage.gutter = gutter;
2402
2403            self.firstIndex = self.twoPage.currentIndexL;
2404            self.displayedIndices = [newIndexL, newIndexR];
2405            self.pruneUnusedImgs();
2406            self.prefetch();
2407            self.animating = false;
2408
2409
2410            self.updateSearchHilites2UP();
2411            self.updatePageNumBox2UP();
2412
2413            // self.twoPagePlaceFlipAreas(); // No longer used
2414            self.setMouseHandlers2UP();
2415            self.twoPageSetCursor();
2416
2417            if (self.animationFinishedCallback) {
2418                self.animationFinishedCallback();
2419                self.animationFinishedCallback = null;
2420            }
2421        });
2422    });
2423}
2424
2425// setMouseHandlers2UP
2426//______________________________________________________________________________
2427BookReader.prototype.setMouseHandlers2UP = function() {
2428    this.setClickHandler2UP( this.prefetchedImgs[this.twoPage.currentIndexL],
2429        { self: this },
2430        function(e) {
2431            if (e.which == 3) {
2432                // right click
2433                if (e.data.self.protected) {
2434                    return false;
2435                }
2436                return true;
2437            }
2438
2439             if (! e.data.self.twoPageIsZoomedIn()) {
2440                e.data.self.ttsStop();
2441                e.data.self.left();
2442            }
2443            e.preventDefault();
2444        }
2445    );
2446
2447    this.setClickHandler2UP( this.prefetchedImgs[this.twoPage.currentIndexR],
2448        { self: this },
2449        function(e) {
2450            if (e.which == 3) {
2451                // right click
2452                if (e.data.self.protected) {
2453                    return false;
2454                }
2455                return true;
2456            }
2457
2458            if (! e.data.self.twoPageIsZoomedIn()) {
2459                e.data.self.ttsStop();
2460                e.data.self.right();
2461            }
2462            e.preventDefault();
2463        }
2464    );
2465}
2466
2467// prefetchImg()
2468//______________________________________________________________________________
2469BookReader.prototype.prefetchImg = function(index) {
2470    var pageURI = this._getPageURI(index);
2471
2472    // Load image if not loaded or URI has changed (e.g. due to scaling)
2473    var loadImage = false;
2474    if (undefined == this.prefetchedImgs[index]) {
2475        //console.log('no image for ' + index);
2476        loadImage = true;
2477    } else if (pageURI != this.prefetchedImgs[index].uri) {
2478        //console.log('uri changed for ' + index);
2479        loadImage = true;
2480    }
2481
2482    if (loadImage) {
2483        //console.log('prefetching ' + index);
2484        var img = document.createElement("img");
2485        $(img).addClass('BRpageimage').addClass('BRnoselect');
2486        if (index < 0 || index > (this.numLeafs - 1) ) {
2487            // Facing page at beginning or end, or beyond
2488            $(img).css({
2489                'background-color': '#efefef'
2490            });
2491        }
2492        img.src = pageURI;
2493        img.uri = pageURI; // browser may rewrite src so we stash raw URI here
2494        this.prefetchedImgs[index] = img;
2495    }
2496}
2497
2498
2499// prepareFlipLeftToRight()
2500//
2501//______________________________________________________________________________
2502//
2503// Prepare to flip the left page towards the right.  This corresponds to moving
2504// backward when the page progression is left to right.
2505BookReader.prototype.prepareFlipLeftToRight = function(prevL, prevR) {
2506
2507    //console.log('  preparing left->right for ' + prevL + ',' + prevR);
2508
2509    this.prefetchImg(prevL);
2510    this.prefetchImg(prevR);
2511
2512    var height  = this._getPageHeight(prevL);
2513    var width   = this._getPageWidth(prevL);
2514    var middle = this.twoPage.middle;
2515    var top  = this.twoPageTop();
2516    var scaledW = this.twoPage.height*width/height; // $$$ assumes height of page is dominant
2517
2518    // The gutter is the dividing line between the left and right pages.
2519    // It is offset from the middle to create the illusion of thickness to the pages
2520    var gutter = middle + this.gutterOffsetForIndex(prevL);
2521
2522    //console.log('    gutter for ' + prevL + ' is ' + gutter);
2523    //console.log('    prevL.left: ' + (gutter - scaledW) + 'px');
2524    //console.log('    changing prevL ' + prevL + ' to left: ' + (gutter-scaledW) + ' width: ' + scaledW);
2525
2526    var leftCSS = {
2527        position: 'absolute',
2528        left: gutter-scaledW+'px',
2529        right: '', // clear right property
2530        top:    top+'px',
2531        height: this.twoPage.height,
2532        width:  scaledW+'px',
2533        zIndex: 1
2534    }
2535
2536    $(this.prefetchedImgs[prevL]).css(leftCSS);
2537
2538    $('#BRtwopageview').append(this.prefetchedImgs[prevL]);
2539
2540    //console.log('    changing prevR ' + prevR + ' to left: ' + gutter + ' width: 0');
2541
2542    var rightCSS = {
2543        position: 'absolute',
2544        left:   gutter+'px',
2545        right: '',
2546        top:    top+'px',
2547        height: this.twoPage.height,
2548        width:  '0',
2549        zIndex: 2
2550    }
2551
2552    $(this.prefetchedImgs[prevR]).css(rightCSS);
2553
2554    $('#BRtwopageview').append(this.prefetchedImgs[prevR]);
2555
2556}
2557
2558// $$$ mang we're adding an extra pixel in the middle.  See https://bugs.edge.launchpad.net/gnubook/+bug/411667
2559// prepareFlipRightToLeft()
2560//______________________________________________________________________________
2561BookReader.prototype.prepareFlipRightToLeft = function(nextL, nextR) {
2562
2563    //console.log('  preparing left<-right for ' + nextL + ',' + nextR);
2564
2565    // Prefetch images
2566    this.prefetchImg(nextL);
2567    this.prefetchImg(nextR);
2568
2569    var height  = this._getPageHeight(nextR);
2570    var width   = this._getPageWidth(nextR);
2571    var middle = this.twoPage.middle;
2572    var top  = this.twoPageTop();
2573    var scaledW = this.twoPage.height*width/height;
2574
2575    var gutter = middle + this.gutterOffsetForIndex(nextL);
2576
2577    //console.log(' prepareRTL changing nextR ' + nextR + ' to left: ' + gutter);
2578    $(this.prefetchedImgs[nextR]).css({
2579        position: 'absolute',
2580        left:   gutter+'px',
2581        top:    top+'px',
2582        height: this.twoPage.height,
2583        width:  scaledW+'px',
2584        zIndex: 1
2585    });
2586
2587    $('#BRtwopageview').append(this.prefetchedImgs[nextR]);
2588
2589    height  = this._getPageHeight(nextL);
2590    width   = this._getPageWidth(nextL);
2591    scaledW = this.twoPage.height*width/height;
2592
2593    //console.log(' prepareRTL changing nextL ' + nextL + ' to right: ' + $('#BRcontainer').width()-gutter);
2594    $(this.prefetchedImgs[nextL]).css({
2595        position: 'absolute',
2596        right:   $('#BRtwopageview').attr('clientWidth')-gutter+'px',
2597        top:    top+'px',
2598        height: this.twoPage.height,
2599        width:  0+'px', // Start at 0 width, then grow to the left
2600        zIndex: 2
2601    });
2602
2603    $('#BRtwopageview').append(this.prefetchedImgs[nextL]);
2604
2605}
2606
2607// getNextLeafs() -- NOT RTL AWARE
2608//______________________________________________________________________________
2609// BookReader.prototype.getNextLeafs = function(o) {
2610//     //TODO: we might have two left or two right leafs in a row (damaged book)
2611//     //For now, assume that leafs are contiguous.
2612//
2613//     //return [this.twoPage.currentIndexL+2, this.twoPage.currentIndexL+3];
2614//     o.L = this.twoPage.currentIndexL+2;
2615//     o.R = this.twoPage.currentIndexL+3;
2616// }
2617
2618// getprevLeafs() -- NOT RTL AWARE
2619//______________________________________________________________________________
2620// BookReader.prototype.getPrevLeafs = function(o) {
2621//     //TODO: we might have two left or two right leafs in a row (damaged book)
2622//     //For now, assume that leafs are contiguous.
2623//
2624//     //return [this.twoPage.currentIndexL-2, this.twoPage.currentIndexL-1];
2625//     o.L = this.twoPage.currentIndexL-2;
2626//     o.R = this.twoPage.currentIndexL-1;
2627// }
2628
2629// pruneUnusedImgs()
2630//______________________________________________________________________________
2631BookReader.prototype.pruneUnusedImgs = function() {
2632    //console.log('current: ' + this.twoPage.currentIndexL + ' ' + this.twoPage.currentIndexR);
2633    for (var key in this.prefetchedImgs) {
2634        //console.log('key is ' + key);
2635        if ((key != this.twoPage.currentIndexL) && (key != this.twoPage.currentIndexR)) {
2636            //console.log('removing key '+ key);
2637            $(this.prefetchedImgs[key]).remove();
2638        }
2639        if ((key < this.twoPage.currentIndexL-4) || (key > this.twoPage.currentIndexR+4)) {
2640            //console.log('deleting key '+ key);
2641            delete this.prefetchedImgs[key];
2642        }
2643    }
2644}
2645
2646// prefetch()
2647//______________________________________________________________________________
2648BookReader.prototype.prefetch = function() {
2649
2650    // $$$ We should check here if the current indices have finished
2651    //     loading (with some timeout) before loading more page images
2652    //     See https://bugs.edge.launchpad.net/bookreader/+bug/511391
2653
2654    // prefetch visible pages first
2655    this.prefetchImg(this.twoPage.currentIndexL);
2656    this.prefetchImg(this.twoPage.currentIndexR);
2657
2658    var adjacentPagesToLoad = 3;
2659
2660    var lowCurrent = Math.min(this.twoPage.currentIndexL, this.twoPage.currentIndexR);
2661    var highCurrent = Math.max(this.twoPage.currentIndexL, this.twoPage.currentIndexR);
2662
2663    var start = Math.max(lowCurrent - adjacentPagesToLoad, 0);
2664    var end = Math.min(highCurrent + adjacentPagesToLoad, this.numLeafs - 1);
2665
2666    // Load images spreading out from current
2667    for (var i = 1; i <= adjacentPagesToLoad; i++) {
2668        var goingDown = lowCurrent - i;
2669        if (goingDown >= start) {
2670            this.prefetchImg(goingDown);
2671        }
2672        var goingUp = highCurrent + i;
2673        if (goingUp <= end) {
2674            this.prefetchImg(goingUp);
2675        }
2676    }
2677
2678    /*
2679    var lim = this.twoPage.currentIndexL-4;
2680    var i;
2681    lim = Math.max(lim, 0);
2682    for (i = lim; i < this.twoPage.currentIndexL; i++) {
2683        this.prefetchImg(i);
2684    }
2685
2686    if (this.numLeafs > (this.twoPage.currentIndexR+1)) {
2687        lim = Math.min(this.twoPage.currentIndexR+4, this.numLeafs-1);
2688        for (i=this.twoPage.currentIndexR+1; i<=lim; i++) {
2689            this.prefetchImg(i);
2690        }
2691    }
2692    */
2693}
2694
2695// getPageWidth2UP()
2696//______________________________________________________________________________
2697BookReader.prototype.getPageWidth2UP = function(index) {
2698    // We return the width based on the dominant height
2699    var height  = this._getPageHeight(index);
2700    var width   = this._getPageWidth(index);
2701    return Math.floor(this.twoPage.height*width/height); // $$$ we assume width is relative to current spread
2702}
2703
2704// search()
2705//______________________________________________________________________________
2706BookReader.prototype.search = function(term) {
2707    //console.log('search called with term=' + term);
2708
2709    $('#textSrch').blur(); //cause mobile safari to hide the keyboard
2710
2711    var url = 'https://'+this.server.replace(/:.+/, ''); //remove the port and userdir
2712    url    += '/fulltext/inside.php?item_id='+this.bookId;
2713    url    += '&doc='+this.subPrefix;   //TODO: test with subitem
2714    url    += '&path='+this.bookPath.replace(new RegExp('/'+this.subPrefix+'$'), ''); //remove subPrefix from end of path
2715    url    += '&q='+escape(term);
2716    //console.log('search url='+url);
2717
2718    term = term.replace(/\//g, ' '); // strip slashes, since this goes in the url
2719    this.searchTerm = term;
2720
2721    this.removeSearchResults();
2722    this.showProgressPopup('<img id="searchmarker" src="'+this.imagesBaseURL + 'marker_srch-on.png'+'"> Search results will appear below...');
2723    $.ajax({url:url, dataType:'jsonp', jsonpCallback:'br.BRSearchCallback'});
2724}
2725
2726// BRSearchCallback()
2727//______________________________________________________________________________
2728BookReader.prototype.BRSearchCallback = function(results) {
2729    //console.log('got ' + results.matches.length + ' results');
2730    br.removeSearchResults();
2731    br.searchResults = results;
2732    //console.log(br.searchResults);
2733
2734    if (0 == results.matches.length) {
2735        var errStr  = 'No matches were found.';
2736        var timeout = 1000;
2737        if (false === results.indexed) {
2738            errStr  = "<p>This book hasn't been indexed for searching yet. We've just started indexing it, so search should be available soon. Please try again later. Thanks!</p>";
2739            timeout = 5000;
2740        }
2741        $(br.popup).html(errStr);
2742        setTimeout(function(){
2743            $(br.popup).fadeOut('slow', function() {
2744                br.removeProgressPopup();
2745            })
2746        },timeout);
2747        return;
2748    }
2749
2750    var i;
2751    for (i=0; i<results.matches.length; i++) {
2752        br.addSearchResult(results.matches[i].text, br.leafNumToIndex(results.matches[i].par[0].page));
2753    }
2754    br.updateSearchHilites();
2755    br.removeProgressPopup();
2756}
2757
2758
2759// updateSearchHilites()
2760//______________________________________________________________________________
2761BookReader.prototype.updateSearchHilites = function() {
2762    if (2 == this.mode) {
2763        this.updateSearchHilites2UP();
2764    } else {
2765        this.updateSearchHilites1UP();
2766    }
2767}
2768
2769// showSearchHilites1UP()
2770//______________________________________________________________________________
2771BookReader.prototype.updateSearchHilites1UP = function() {
2772    var results = this.searchResults;
2773    if (null == results) return;
2774    var i, j;
2775    for (i=0; i<results.matches.length; i++) {
2776        //console.log(results.matches[i].par[0]);
2777        for (j=0; j<results.matches[i].par[0].boxes.length; j++) {
2778            var box = results.matches[i].par[0].boxes[j];
2779            var pageIndex = this.leafNumToIndex(box.page);
2780            if (jQuery.inArray(pageIndex, this.displayedIndices) >= 0) {
2781                if (null == box.div) {
2782                    //create a div for the search highlight, and stash it in the box object
2783                    box.div = document.createElement('div');
2784                    $(box.div).attr('className', 'BookReaderSearchHilite').appendTo('#pagediv'+pageIndex);
2785                }
2786                $(box.div).css({
2787                    width:  (box.r-box.l)/this.reduce + 'px',
2788                    height: (box.b-box.t)/this.reduce + 'px',
2789                    left:   (box.l)/this.reduce + 'px',
2790                    top:    (box.t)/this.reduce +'px'
2791                });
2792            } else {
2793                if (null != box.div) {
2794                    //console.log('removing search highlight div');
2795                    $(box.div).remove();
2796                    box.div=null;
2797                }
2798            }
2799        }
2800    }
2801
2802}
2803
2804
2805// twoPageGutter()
2806//______________________________________________________________________________
2807// Returns the position of the gutter (line between the page images)
2808BookReader.prototype.twoPageGutter = function() {
2809    return this.twoPage.middle + this.gutterOffsetForIndex(this.twoPage.currentIndexL);
2810}
2811
2812// twoPageTop()
2813//______________________________________________________________________________
2814// Returns the offset for the top of the page images
2815BookReader.prototype.twoPageTop = function() {
2816    return this.twoPage.coverExternalPadding + this.twoPage.coverInternalPadding; // $$$ + border?
2817}
2818
2819// twoPageCoverWidth()
2820//______________________________________________________________________________
2821// Returns the width of the cover div given the total page width
2822BookReader.prototype.twoPageCoverWidth = function(totalPageWidth) {
2823    return totalPageWidth + this.twoPage.edgeWidth + 2*this.twoPage.coverInternalPadding;
2824}
2825
2826// twoPageGetViewCenter()
2827//______________________________________________________________________________
2828// Returns the percentage offset into twopageview div at the center of container div
2829// { percentageX: float, percentageY: float }
2830BookReader.prototype.twoPageGetViewCenter = function() {
2831    var center = {};
2832
2833    var containerOffset = $('#BRcontainer').offset();
2834    var viewOffset = $('#BRtwopageview').offset();
2835    center.percentageX = (containerOffset.left - viewOffset.left + ($('#BRcontainer').attr('clientWidth') >> 1)) / this.twoPage.totalWidth;
2836    center.percentageY = (containerOffset.top - viewOffset.top + ($('#BRcontainer').attr('clientHeight') >> 1)) / this.twoPage.totalHeight;
2837
2838    return center;
2839}
2840
2841// twoPageCenterView(percentageX, percentageY)
2842//______________________________________________________________________________
2843// Centers the point given by percentage from left,top of twopageview
2844BookReader.prototype.twoPageCenterView = function(percentageX, percentageY) {
2845    if ('undefined' == typeof(percentageX)) {
2846        percentageX = 0.5;
2847    }
2848    if ('undefined' == typeof(percentageY)) {
2849        percentageY = 0.5;
2850    }
2851
2852    var viewWidth = $('#BRtwopageview').width();
2853    var containerClientWidth = $('#BRcontainer').attr('clientWidth');
2854    var intoViewX = percentageX * viewWidth;
2855
2856    var viewHeight = $('#BRtwopageview').height();
2857    var containerClientHeight = $('#BRcontainer').attr('clientHeight');
2858    var intoViewY = percentageY * viewHeight;
2859
2860    if (viewWidth < containerClientWidth) {
2861        // Can fit width without scrollbars - center by adjusting offset
2862        $('#BRtwopageview').css('left', (containerClientWidth >> 1) - intoViewX + 'px');
2863    } else {
2864        // Need to scroll to center
2865        $('#BRtwopageview').css('left', 0);
2866        $('#BRcontainer').scrollLeft(intoViewX - (containerClientWidth >> 1));
2867    }
2868
2869    if (viewHeight < containerClientHeight) {
2870        // Fits with scrollbars - add offset
2871        $('#BRtwopageview').css('top', (containerClientHeight >> 1) - intoViewY + 'px');
2872    } else {
2873        $('#BRtwopageview').css('top', 0);
2874        $('#BRcontainer').scrollTop(intoViewY - (containerClientHeight >> 1));
2875    }
2876}
2877
2878// twoPageFlipAreaHeight
2879//______________________________________________________________________________
2880// Returns the integer height of the click-to-flip areas at the edges of the book
2881BookReader.prototype.twoPageFlipAreaHeight = function() {
2882    return parseInt(this.twoPage.height);
2883}
2884
2885// twoPageFlipAreaWidth
2886//______________________________________________________________________________
2887// Returns the integer width of the flip areas
2888BookReader.prototype.twoPageFlipAreaWidth = function() {
2889    var max = 100; // $$$ TODO base on view width?
2890    var min = 10;
2891
2892    var width = this.twoPage.width * 0.15;
2893    return parseInt(BookReader.util.clamp(width, min, max));
2894}
2895
2896// twoPageFlipAreaTop
2897//______________________________________________________________________________
2898// Returns integer top offset for flip areas
2899BookReader.prototype.twoPageFlipAreaTop = function() {
2900    return parseInt(this.twoPage.bookCoverDivTop + this.twoPage.coverInternalPadding);
2901}
2902
2903// twoPageLeftFlipAreaLeft
2904//______________________________________________________________________________
2905// Left offset for left flip area
2906BookReader.prototype.twoPageLeftFlipAreaLeft = function() {
2907    return parseInt(this.twoPage.gutter - this.twoPage.scaledWL);
2908}
2909
2910// twoPageRightFlipAreaLeft
2911//______________________________________________________________________________
2912// Left offset for right flip area
2913BookReader.prototype.twoPageRightFlipAreaLeft = function() {
2914    return parseInt(this.twoPage.gutter + this.twoPage.scaledWR - this.twoPageFlipAreaWidth());
2915}
2916
2917// twoPagePlaceFlipAreas
2918//______________________________________________________________________________
2919// Readjusts position of flip areas based on current layout
2920BookReader.prototype.twoPagePlaceFlipAreas = function() {
2921    // We don't set top since it shouldn't change relative to view
2922    $(this.twoPage.leftFlipArea).css({
2923        left: this.twoPageLeftFlipAreaLeft() + 'px',
2924        width: this.twoPageFlipAreaWidth() + 'px'
2925    });
2926    $(this.twoPage.rightFlipArea).css({
2927        left: this.twoPageRightFlipAreaLeft() + 'px',
2928        width: this.twoPageFlipAreaWidth() + 'px'
2929    });
2930}
2931
2932// showSearchHilites2UPNew()
2933//______________________________________________________________________________
2934BookReader.prototype.updateSearchHilites2UP = function() {
2935    //console.log('updateSearchHilites2UP results = ' + this.searchResults);
2936    var results = this.searchResults;
2937    if (null == results) return;
2938    var i, j;
2939    for (i=0; i<results.matches.length; i++) {
2940        //console.log(results.matches[i].par[0]);
2941        //TODO: loop over all par objects
2942        var pageIndex = this.leafNumToIndex(results.matches[i].par[0].page);
2943        for (j=0; j<results.matches[i].par[0].boxes.length; j++) {
2944            var box = results.matches[i].par[0].boxes[j];
2945            if (jQuery.inArray(pageIndex, this.displayedIndices) >= 0) {
2946                if (null == box.div) {
2947                    //create a div for the search highlight, and stash it in the box object
2948                    box.div = document.createElement('div');
2949                    $(box.div).attr('className', 'BookReaderSearchHilite').css('zIndex', 3).appendTo('#BRtwopageview');
2950                    //console.log('appending new div');
2951                }
2952                this.setHilightCss2UP(box.div, pageIndex, box.l, box.r, box.t, box.b);
2953            } else {
2954                if (null != box.div) {
2955                    //console.log('removing search highlight div');
2956                    $(box.div).remove();
2957                    box.div=null;
2958                }
2959            }
2960        }
2961    }
2962
2963}
2964
2965// setHilightCss2UP()
2966//______________________________________________________________________________
2967//position calculation shared between search and text-to-speech functions
2968BookReader.prototype.setHilightCss2UP = function(div, index, left, right, top, bottom) {
2969
2970    // We calculate the reduction factor for the specific page because it can be different
2971    // for each page in the spread
2972    var height = this._getPageHeight(index);
2973    var width  = this._getPageWidth(index)
2974    var reduce = this.twoPage.height/height;
2975    var scaledW = parseInt(width*reduce);
2976
2977    var gutter = this.twoPageGutter();
2978    var pageL;
2979    if ('L' == this.getPageSide(index)) {
2980        pageL = gutter-scaledW;
2981    } else {
2982        pageL = gutter;
2983    }
2984    var pageT  = this.twoPageTop();
2985
2986    $(div).css({
2987        width:  (right-left)*reduce + 'px',
2988        height: (bottom-top)*reduce + 'px',
2989        left:   pageL+left*reduce + 'px',
2990        top:    pageT+top*reduce +'px'
2991    });
2992}
2993
2994// removeSearchHilites()
2995//______________________________________________________________________________
2996BookReader.prototype.removeSearchHilites = function() {
2997    var results = this.searchResults;
2998    if (null == results) return;
2999    var i, j;
3000    for (i=0; i<results.matches.length; i++) {
3001        for (j=0; j<results.matches[i].par[0].boxes.length; j++) {
3002            var box = results.matches[i].par[0].boxes[j];
3003            if (null != box.div) {
3004                $(box.div).remove();
3005                box.div=null;
3006            }
3007        }
3008    }
3009}
3010
3011
3012// printPage
3013//______________________________________________________________________________
3014BookReader.prototype.printPage = function() {
3015    window.open(this.getPrintURI(), 'printpage', 'width=400, height=500, resizable=yes, scrollbars=no, toolbar=no, location=no');
3016}
3017
3018// Get print URI from current indices and mode
3019BookReader.prototype.getPrintURI = function() {
3020    var indexToPrint;
3021    if (this.constMode2up == this.mode) {
3022        indexToPrint = this.twoPage.currentIndexL;
3023    } else {
3024        indexToPrint = this.firstIndex; // $$$ the index in the middle of the viewport would make more sense
3025    }
3026
3027    var options = 'id=' + this.subPrefix + '&server=' + this.server + '&zip=' + this.zip
3028        + '&format=' + this.imageFormat + '&file=' + this._getPageFile(indexToPrint)
3029        + '&width=' + this._getPageWidth(indexToPrint) + '&height=' + this._getPageHeight(indexToPrint);
3030
3031    if (this.constMode2up == this.mode) {
3032        options += '&file2=' + this._getPageFile(this.twoPage.currentIndexR) + '&width2=' + this._getPageWidth(this.twoPage.currentIndexR);
3033        options += '&height2=' + this._getPageHeight(this.twoPage.currentIndexR);
3034        options += '&title=' + encodeURIComponent(this.shortTitle(50) + ' - Pages ' + this.getPageNum(this.twoPage.currentIndexL) + ', ' + this.getPageNum(this.twoPage.currentIndexR));
3035    } else {
3036        options += '&title=' + encodeURIComponent(this.shortTitle(50) + ' - Page ' + this.getPageNum(indexToPrint));
3037    }
3038
3039    return '/bookreader/print.php?' + options;
3040}
3041
3042// showEmbedCode()
3043//
3044// Note: Has been replaced by the share dialog
3045//______________________________________________________________________________
3046BookReader.prototype.showEmbedCode = function() {
3047    if (null != this.embedPopup) { // check if already showing
3048        return;
3049    }
3050    this.autoStop();
3051    this.ttsStop();
3052
3053    this.embedPopup = document.createElement("div");
3054    $(this.embedPopup).css({
3055        position: 'absolute',
3056        top:      ($('#BRcontainer').attr('clientHeight')-250)/2 + 'px',
3057        left:     ($('#BRcontainer').attr('clientWidth')-400)/2 + 'px',
3058        width:    '400px',
3059        height:    '250px',
3060        padding:  '0',
3061        fontSize: '12px',
3062        color:    '#333',
3063        zIndex:   300,
3064        border: '10px solid #615132',
3065        backgroundColor: "#fff",
3066        MozBorderRadius: '8px',
3067        MozBoxShadow: '0 0 6px #000',
3068        WebkitBorderRadius: '8px',
3069        WebkitBoxShadow: '0 0 6px #000'
3070    }).appendTo('#BookReader');
3071
3072    htmlStr =  '<h3 style="background:#615132;padding:10px;margin:0 0 10px;color:#fff;">Embed Bookreader</h3>';
3073    htmlStr += '<textarea rows="2" cols="40" style="margin-left:10px;width:368px;height:40px;color:#333;font-size:12px;border:2px inset #ccc;background:#efefef;padding:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;">' + this.getEmbedCode() + '</textarea>';
3074    htmlStr += '<a href="javascript:;" class="popOff" onclick="$(this.parentNode).remove();$(\'.coverUp\').hide();return false" style="color:#999;"><span>Close</span></a>';
3075
3076    this.embedPopup.innerHTML = htmlStr;
3077    $('#BookReader').append('<div class="coverUp" style="position:absolute;z-index:299;width:100%;height:100%;background:#000;opacity:.4;filter:alpha(opacity=40);" onclick="$(\'.popped\').hide();$(this).hide();"></div>');
3078    $(this.embedPopup).find('textarea').click(function() {
3079        this.select();
3080    })
3081    $(this.embedPopup).addClass("popped");
3082}
3083
3084// showBookmarkCode()
3085//______________________________________________________________________________
3086BookReader.prototype.showBookmarkCode = function() {
3087    this.bookmarkPopup = document.createElement("div");
3088    $(this.bookmarkPopup).css({
3089        position: 'absolute',
3090        top:      ($('#BRcontainer').attr('clientHeight')-250)/2 + 'px',
3091        left:     ($('#BRcontainer').attr('clientWidth')-400)/2 + 'px',
3092        width:    '400px',
3093        height:    '250px',
3094        padding:  '0',
3095        fontSize: '12px',
3096        color:    '#333',
3097        zIndex:   300,
3098        border: '10px solid #615132',
3099        backgroundColor: "#fff",
3100        MozBorderRadius: '8px',
3101        MozBoxShadow: '0 0 6px #000',
3102        WebkitBorderRadius: '8px',
3103        WebkitBoxShadow: '0 0 6px #000'
3104    }).appendTo('#BookReader');
3105
3106    htmlStr =  '<h3 style="background:#615132;padding:10px;margin:0 0 10px;color:#fff;">Add a bookmark</h3>';
3107    htmlStr += '<p style="padding:10px;line-height:18px;">You can add a bookmark to any page in any book. If you elect to make your bookmark public, other readers will be able to see it. <em>You must be logged in to your <a href="">Open Library account</a> to add bookmarks.</em></p>';
3108    htmlStr += '<form name="bookmark" id="bookmark" style="line-height:20px;margin-left:10px;"><label style="padding-bottom"10px;><input type="radio" name="privacy" id="p2" disabled="disabled" checked="checked"/> Make this bookmark public.</label><br/><label style="padding-bottom:10px;"><input type="radio" name="privacy" id="p1" disabled="disabled"/> Keep this bookmark private.</label><br/><br/><button type="submit" style="font-size:20px;" disabled="disabled">Add a bookmark</button></form>';
3109    htmlStr += '<a href="javascript:;" class="popOff" onclick="$(this.parentNode).remove();$(\'.coverUp\').hide();return false;" style="color:#999;"><span>Close</span></a>';
3110
3111    this.bookmarkPopup.innerHTML = htmlStr;
3112    $('#BookReader').append('<div class="coverUp" style="position:absolute;z-index:299;width:100%;height:100%;background:#000;opacity:.4;filter:alpha(opacity=40);" onclick="$(\'.popped\').hide();$(this).hide();"></div>');
3113    $(this.bookmarkPopup).find('textarea').click(function() {
3114        this.select();
3115    })
3116    $(this.bookmarkPopup).addClass("popped");
3117}
3118
3119
3120// autoToggle()
3121//______________________________________________________________________________
3122BookReader.prototype.autoToggle = function() {
3123
3124    this.ttsStop();
3125
3126    var bComingFrom1up = false;
3127    if (2 != this.mode) {
3128        bComingFrom1up = true;
3129        this.switchMode(2);
3130    }
3131
3132    // Change to autofit if book is too large
3133    if (this.reduce < this.twoPageGetAutofitReduce()) {
3134        this.zoom2up('auto');
3135    }
3136
3137    var self = this;
3138    if (null == this.autoTimer) {
3139        this.flipSpeed = 2000;
3140
3141        // $$$ Draw events currently cause layout problems when they occur during animation.
3142        //     There is a specific problem when changing from 1-up immediately to autoplay in RTL so
3143        //     we workaround for now by not triggering immediate animation in that case.
3144        //     See https://bugs.launchpad.net/gnubook/+bug/328327
3145        if (('rl' == this.pageProgression) && bComingFrom1up) {
3146            // don't flip immediately -- wait until timer fires
3147        } else {
3148            // flip immediately
3149            this.flipFwdToIndex();
3150        }
3151
3152        $('#BRtoolbar .play').hide();
3153        $('#BRtoolbar .pause').show();
3154        this.autoTimer=setInterval(function(){
3155            if (self.animating) {return;}
3156
3157            if (Math.max(self.twoPage.currentIndexL, self.twoPage.currentIndexR) >= self.lastDisplayableIndex()) {
3158                self.flipBackToIndex(1); // $$$ really what we want?
3159            } else {
3160                self.flipFwdToIndex();
3161            }
3162        },5000);
3163    } else {
3164        this.autoStop();
3165    }
3166}
3167
3168// autoStop()
3169//______________________________________________________________________________
3170// Stop autoplay mode, allowing animations to finish
3171BookReader.prototype.autoStop = function() {
3172    if (null != this.autoTimer) {
3173        clearInterval(this.autoTimer);
3174        this.flipSpeed = 'fast';
3175        $('#BRtoolbar .pause').hide();
3176        $('#BRtoolbar .play').show();
3177        this.autoTimer = null;
3178    }
3179}
3180
3181// stopFlipAnimations
3182//______________________________________________________________________________
3183// Immediately stop flip animations.  Callbacks are triggered.
3184BookReader.prototype.stopFlipAnimations = function() {
3185
3186    this.autoStop(); // Clear timers
3187
3188    // Stop animation, clear queue, trigger callbacks
3189    if (this.leafEdgeTmp) {
3190        $(this.leafEdgeTmp).stop(false, true);
3191    }
3192    jQuery.each(this.prefetchedImgs, function() {
3193        $(this).stop(false, true);
3194        });
3195
3196    // And again since animations also queued in callbacks
3197    if (this.leafEdgeTmp) {
3198        $(this.leafEdgeTmp).stop(false, true);
3199    }
3200    jQuery.each(this.prefetchedImgs, function() {
3201        $(this).stop(false, true);
3202        });
3203
3204}
3205
3206// keyboardNavigationIsDisabled(event)
3207//   - returns true if keyboard navigation should be disabled for the event
3208//______________________________________________________________________________
3209BookReader.prototype.keyboardNavigationIsDisabled = function(event) {
3210    if (event.target.tagName == "INPUT") {
3211        return true;
3212    }
3213    return false;
3214}
3215
3216// gutterOffsetForIndex
3217//______________________________________________________________________________
3218//
3219// Returns the gutter offset for the spread containing the given index.
3220// This function supports RTL
3221BookReader.prototype.gutterOffsetForIndex = function(pindex) {
3222
3223    // To find the offset of the gutter from the middle we calculate our percentage distance
3224    // through the book (0..1), remap to (-0.5..0.5) and multiply by the total page edge width
3225    var offset = parseInt(((pindex / this.numLeafs) - 0.5) * this.twoPage.edgeWidth);
3226
3227    // But then again for RTL it's the opposite
3228    if ('rl' == this.pageProgression) {
3229        offset = -offset;
3230    }
3231
3232    return offset;
3233}
3234
3235// leafEdgeWidth
3236//______________________________________________________________________________
3237// Returns the width of the leaf edge div for the page with index given
3238BookReader.prototype.leafEdgeWidth = function(pindex) {
3239    // $$$ could there be single pixel rounding errors for L vs R?
3240    if ((this.getPageSide(pindex) == 'L') && (this.pageProgression != 'rl')) {
3241        return parseInt( (pindex/this.numLeafs) * this.twoPage.edgeWidth + 0.5);
3242    } else {
3243        return parseInt( (1 - pindex/this.numLeafs) * this.twoPage.edgeWidth + 0.5);
3244    }
3245}
3246
3247// jumpIndexForLeftEdgePageX
3248//______________________________________________________________________________
3249// Returns the target jump leaf given a page coordinate (inside the left page edge div)
3250BookReader.prototype.jumpIndexForLeftEdgePageX = function(pageX) {
3251    if ('rl' != this.pageProgression) {
3252        // LTR - flipping backward
3253        var jumpIndex = this.twoPage.currentIndexL - ($(this.leafEdgeL).offset().left + $(this.leafEdgeL).width() - pageX) * 10;
3254
3255        // browser may have resized the div due to font size change -- see https://bugs.launchpad.net/gnubook/+bug/333570
3256        jumpIndex = BookReader.util.clamp(Math.round(jumpIndex), this.firstDisplayableIndex(), this.twoPage.currentIndexL - 2);
3257        return jumpIndex;
3258
3259    } else {
3260        var jumpIndex = this.twoPage.currentIndexL + ($(this.leafEdgeL).offset().left + $(this.leafEdgeL).width() - pageX) * 10;
3261        jumpIndex = BookReader.util.clamp(Math.round(jumpIndex), this.twoPage.currentIndexL + 2, this.lastDisplayableIndex());
3262        return jumpIndex;
3263    }
3264}
3265
3266// jumpIndexForRightEdgePageX
3267//______________________________________________________________________________
3268// Returns the target jump leaf given a page coordinate (inside the right page edge div)
3269BookReader.prototype.jumpIndexForRightEdgePageX = function(pageX) {
3270    if ('rl' != this.pageProgression) {
3271        // LTR
3272        var jumpIndex = this.twoPage.currentIndexR + (pageX - $(this.leafEdgeR).offset().left) * 10;
3273        jumpIndex = BookReader.util.clamp(Math.round(jumpIndex), this.twoPage.currentIndexR + 2, this.lastDisplayableIndex());
3274        return jumpIndex;
3275    } else {
3276        var jumpIndex = this.twoPage.currentIndexR - (pageX - $(this.leafEdgeR).offset().left) * 10;
3277        jumpIndex = BookReader.util.clamp(Math.round(jumpIndex), this.firstDisplayableIndex(), this.twoPage.currentIndexR - 2);
3278        return jumpIndex;
3279    }
3280}
3281
3282// initNavbar
3283//______________________________________________________________________________
3284// Initialize the navigation bar.
3285// $$$ this could also add the base elements to the DOM, so disabling the nav bar
3286//     could be as simple as not calling this function
3287BookReader.prototype.initNavbar = function() {
3288    // Setup nav / chapter / search results bar
3289
3290    $('#BookReader').append(
3291        '<div id="BRnav">'
3292        +     '<div id="BRpage">'   // Page turn buttons
3293        +         '<button class="BRicon onepg"></button>'
3294        +         '<button class="BRicon twopg"></button>'
3295        +         '<button class="BRicon thumb"></button>'
3296        // $$$ not yet implemented
3297        //+         '<button class="BRicon fit"></button>'
3298        +         '<button class="BRicon zoom_in"></button>'
3299        +         '<button class="BRicon zoom_out"></button>'
3300        +         '<button class="BRicon book_left"></button>'
3301        +         '<button class="BRicon book_right"></button>'
3302        +     '</div>'
3303        +     '<div id="BRnavpos">' // Page slider and nav line
3304        //+         '<div id="BRfiller"></div>'
3305        +         '<div id="BRpager"></div>'  // Page slider
3306        +         '<div id="BRnavline">'      // Nav line with e.g. chapter markers
3307        +             '<div class="BRnavend" id="BRnavleft"></div>'
3308        +             '<div class="BRnavend" id="BRnavright"></div>'
3309        +         '</div>'
3310        +     '</div>'
3311        +     '<div id="BRnavCntlBtm" class="BRnavCntl BRdn"></div>'
3312        + '</div>'
3313    );
3314
3315    var self = this;
3316    $('#BRpager').slider({
3317        animate: true,
3318        min: 0,
3319        max: this.numLeafs - 1,
3320        value: this.currentIndex()
3321    })
3322    .bind('slide', function(event, ui) {
3323        self.updateNavPageNum(ui.value);
3324        $("#pagenum").show();
3325        return true;
3326    })
3327    .bind('slidechange', function(event, ui) {
3328        self.updateNavPageNum(ui.value); // hiding now but will show later
3329        $("#pagenum").hide();
3330
3331        // recursion prevention for jumpToIndex
3332        if ( $(this).data('swallowchange') ) {
3333            $(this).data('swallowchange', false);
3334        } else {
3335            self.jumpToIndex(ui.value);
3336        }
3337        return true;
3338    })
3339    .hover(function() {
3340            $("#pagenum").show();
3341        },function(){
3342            // XXXmang not triggering on iPad - probably due to touch event translation layer
3343            $("#pagenum").hide();
3344        }
3345    );
3346
3347    //append icon to handle
3348    var handleHelper = $('#BRpager .ui-slider-handle')
3349    .append('<div id="pagenum"><span class="currentpage"></span></div>');
3350    //.wrap('<div class="ui-handle-helper-parent"></div>').parent(); // XXXmang is this used for hiding the tooltip?
3351
3352    this.updateNavPageNum(this.currentIndex());
3353
3354    $("#BRzoombtn").draggable({axis:'y',containment:'parent'});
3355
3356    /* Initial hiding
3357        $('#BRtoolbar').delay(3000).animate({top:-40});
3358        $('#BRnav').delay(3000).animate({bottom:-53});
3359        changeArrow();
3360        $('.BRnavCntl').delay(3000).animate({height:'43px'}).delay(1000).animate({opacity:.25},1000);
3361    */
3362}
3363
3364// initEmbedNavbar
3365//______________________________________________________________________________
3366// Initialize the navigation bar when embedded
3367BookReader.prototype.initEmbedNavbar = function() {
3368    var thisLink = (window.location + '').replace('?ui=embed',''); // IA-specific
3369
3370    /* $('#BookReader').append(
3371        '<div id="BRnav">'
3372        +   "<span id='BRtoolbarbuttons'>"
3373        +         '<button class="BRicon full"></button>'
3374        +         '<button class="BRicon book_left"></button>'
3375        +         '<button class="BRicon book_right"></button>'
3376        +   "</span>"
3377        +   "<span><a class='logo' href='" + this.logoURL + "' 'target='_blank' ></a></span>"
3378        +   "<span id='BRembedreturn'><a href='" + thisLink + "' target='_blank' ></a></span>"
3379        + '</div>'
3380    ); */
3381    $('#BRembedreturn a').text(this.bookTitle);
3382}
3383
3384BookReader.prototype.updateNavPageNum = function(index) {
3385    var pageNum = this.getPageNum(index);
3386    var pageStr;
3387    if (pageNum[0] == 'n') { // funny index
3388        pageStr = index + 1 + ' / ' + this.numLeafs; // Accessible index starts at 0 (alas) so we add 1 to make human
3389    } else {
3390        pageStr = 'Page ' + pageNum;
3391    }
3392
3393    $('#pagenum .currentpage').text(pageStr);
3394}
3395
3396/*
3397 * Update the nav bar display - does not cause navigation.
3398 */
3399BookReader.prototype.updateNavIndex = function(index) {
3400    // We want to update the value, but normally moving the slider
3401    // triggers jumpToIndex which triggers this method
3402    $('#BRpager').data('swallowchange', true).slider('value', index);
3403}
3404
3405BookReader.prototype.addSearchResult = function(queryString, pageIndex) {
3406    var pageNumber = this.getPageNum(pageIndex);
3407    var uiStringSearch = "Search result"; // i18n
3408    var uiStringPage = "Page"; // i18n
3409
3410    var percentThrough = BookReader.util.cssPercentage(pageIndex, this.numLeafs - 1);
3411    var pageDisplayString = '';
3412    if (pageNumber) {
3413        pageDisplayString = uiStringPage + ' ' + pageNumber;
3414    }
3415
3416    var re = new RegExp('{{{(.+?)}}}', 'g');
3417    queryString = queryString.replace(re, '<a href="#" onclick="br.jumpToIndex('+pageIndex+'); return false;">$1</a>')
3418
3419    var marker = $('<div class="search" style="top:'+(-$('#BRcontainer').height())+'px; left:' + percentThrough + ';" title="' + uiStringSearch + '"><div class="query">'
3420        + queryString + '<span>' + uiStringPage + ' ' + pageNumber + '</span></div>')
3421    .data({'self': this, 'pageIndex': pageIndex })
3422    .appendTo('#BRnavline').bt({
3423        contentSelector: '$(this).find(".query")',
3424        trigger: 'hover',
3425        closeWhenOthersOpen: true,
3426        cssStyles: {
3427            padding: '12px 14px',
3428            backgroundColor: '#fff',
3429            border: '4px solid #e2dcc5',
3430            fontFamily: '"Lucida Grande","Arial",sans-serif',
3431            fontSize: '13px',
3432            //lineHeight: '18px',
3433            color: '#615132'
3434        },
3435        shrinkToFit: false,
3436        width: '230px',
3437        padding: 0,
3438        spikeGirth: 0,
3439        spikeLength: 0,
3440        overlap: '22px',
3441        overlay: false,
3442        killTitle: false,
3443        textzIndex: 9999,
3444        boxzIndex: 9998,
3445        wrapperzIndex: 9997,
3446        offsetParent: null,
3447        positions: ['top'],
3448        fill: 'white',
3449        windowMargin: 10,
3450        strokeWidth: 0,
3451        cornerRadius: 0,
3452        centerPointX: 0,
3453        centerPointY: 0,
3454        shadow: false
3455    })
3456    .hover( function() {
3457                // remove from other markers then turn on just for this
3458                // XXX should be done when nav slider moves
3459                $('.search,.chapter').removeClass('front');
3460                $(this).addClass('front');
3461            }, function() {
3462                $(this).removeClass('front');
3463            }
3464    )
3465    .bind('click', function() {
3466        $(this).data('self').jumpToIndex($(this).data('pageIndex'));
3467    });
3468
3469    $(marker).animate({top:'-25px'}, 'slow');
3470
3471}
3472
3473BookReader.prototype.removeSearchResults = function() {
3474    this.removeSearchHilites(); //be sure to set all box.divs to null
3475    $('#BRnavpos .search').remove();
3476}
3477
3478BookReader.prototype.addChapter = function(chapterTitle, pageNumber, pageIndex) {
3479    var uiStringPage = 'Page'; // i18n
3480
3481    var percentThrough = BookReader.util.cssPercentage(pageIndex, this.numLeafs - 1);
3482
3483    $('<div class="chapter" style="left:' + percentThrough + ';"><div class="title">'
3484        + chapterTitle + '<span>|</span> ' + uiStringPage + ' ' + pageNumber + '</div></div>')
3485    .appendTo('#BRnavline')
3486    .data({'self': this, 'pageIndex': pageIndex })
3487    .bt({
3488        contentSelector: '$(this).find(".title")',
3489        trigger: 'hover',
3490        closeWhenOthersOpen: true,
3491        cssStyles: {
3492            padding: '12px 14px',
3493            backgroundColor: '#000',
3494            border: '4px solid #e2dcc5',
3495            //borderBottom: 'none',
3496            fontFamily: '"Arial", sans-serif',
3497            fontSize: '12px',
3498            fontWeight: '700',
3499            color: '#fff',
3500            whiteSpace: 'nowrap'
3501        },
3502        shrinkToFit: true,
3503        width: '200px',
3504        padding: 0,
3505        spikeGirth: 0,
3506        spikeLength: 0,
3507        overlap: '21px',
3508        overlay: false,
3509        killTitle: true,
3510        textzIndex: 9999,
3511        boxzIndex: 9998,
3512        wrapperzIndex: 9997,
3513        offsetParent: null,
3514        positions: ['top'],
3515        fill: 'black',
3516        windowMargin: 10,
3517        strokeWidth: 0,
3518        cornerRadius: 0,
3519        centerPointX: 0,
3520        centerPointY: 0,
3521        shadow: false
3522    })
3523    .hover( function() {
3524            // remove hover effect from other markers then turn on just for this
3525            $('.search,.chapter').removeClass('front');
3526                $(this).addClass('front');
3527            }, function() {
3528                $(this).removeClass('front');
3529            }
3530    )
3531    .bind('click', function() {
3532        $(this).data('self').jumpToIndex($(this).data('pageIndex'));
3533    });
3534}
3535
3536/*
3537 * Remove all chapters.
3538 */
3539BookReader.prototype.removeChapters = function() {
3540    $('#BRnavpos .chapter').remove();
3541}
3542
3543/*
3544 * Update the table of contents based on array of TOC entries.
3545 */
3546BookReader.prototype.updateTOC = function(tocEntries) {
3547    this.removeChapters();
3548    for (var i = 0; i < tocEntries.length; i++) {
3549        this.addChapterFromEntry(tocEntries[i]);
3550    }
3551}
3552
3553/*
3554 *   Example table of contents entry - this format is defined by Open Library
3555 *   {
3556 *       "pagenum": "17",
3557 *       "level": 1,
3558 *       "label": "CHAPTER I",
3559 *       "type": {"key": "/type/toc_item"},
3560 *       "title": "THE COUNTRY AND THE MISSION"
3561 *   }
3562 */
3563BookReader.prototype.addChapterFromEntry = function(tocEntryObject) {
3564    var pageIndex = this.getPageIndex(tocEntryObject['pagenum']);
3565    // Only add if we know where it is
3566    if (pageIndex) {
3567        this.addChapter(tocEntryObject['title'], tocEntryObject['pagenum'], pageIndex);
3568    }
3569    $('.chapter').each(function(){
3570        $(this).hover(function(){
3571            $(this).addClass('front');
3572        },function(){
3573            $(this).removeClass('front');
3574        });
3575    });
3576    $('.search').each(function(){
3577        $(this).hover(function(){
3578            $(this).addClass('front');
3579        },function(){
3580            $(this).removeClass('front');
3581        });
3582    });
3583    $('.searchChap').each(function(){
3584        $(this).hover(function(){
3585            $(this).addClass('front');
3586        },function(){
3587            $(this).removeClass('front');
3588        });
3589    });
3590}
3591
3592BookReader.prototype.initToolbar = function(mode, ui) {
3593    if (ui == "embed") {
3594        return; // No toolbar at top in embed mode
3595    }
3596
3597    // $$$mang should be contained within the BookReader div instead of body
3598    var readIcon = '';
3599    if (!navigator.userAgent.match(/mobile/i)) {
3600        readIcon = "<button class='BRicon read modal'></button>";
3601    }
3602
3603    $("#BookReader").append(
3604          "<div id='BRtoolbar'>"
3605        +   "<span id='BRtoolbarbuttons'>"
3606        +     "<form action='javascript:br.search($(\"#textSrch\").val());' id='booksearch'><input type='search' id='textSrch' name='textSrch' val='' placeholder='Search inside'/><button type='submit' id='btnSrch' name='btnSrch'>GO</button></form>"
3607        +     "<button class='BRicon play'></button>"
3608        +     "<button class='BRicon pause'></button>"
3609       /*  +     "<button class='BRicon info'></button>"
3610        +     "<button class='BRicon share'></button>" */
3611        +     readIcon
3612        //+     "<button class='BRicon full'></button>"
3613        +   "</span>"
3614        +   "<span><a class='logo' href='" + this.logoURL + "'></a></span>"
3615        +   "<span id='BRreturn'><a></a></span>"
3616        +   "<div id='BRnavCntlTop' class='BRnabrbuvCntl'></div>"
3617        + "</div>"
3618        /*
3619        + "<div id='BRzoomer'>"
3620        +   "<div id='BRzoompos'>"
3621        +     "<button class='BRicon zoom_out'></button>"
3622        +     "<div id='BRzoomcontrol'>"
3623        +       "<div id='BRzoomstrip'></div>"
3624        +       "<div id='BRzoombtn'></div>"
3625        +     "</div>"
3626        +     "<button class='BRicon zoom_in'></button>"
3627        +   "</div>"
3628        + "</div>"
3629        */
3630        );
3631
3632    // Browser hack - bug with colorbox on iOS 3 see https://bugs.launchpad.net/bookreader/+bug/686220
3633  /*   if ( navigator.userAgent.match(/ipad/i) && $.browser.webkit && (parseInt($.browser.version, 10) <= 531) ) {
3634       $('#BRtoolbarbuttons .info').hide();
3635       $('#BRtoolbarbuttons .share').hide();
3636    }
3637 */
3638    $('#BRreturn a').attr('href', this.bookUrl).text(this.bookTitle);
3639
3640    $('#BRtoolbar .BRnavCntl').addClass('BRup');
3641    $('#BRtoolbar .pause').hide();
3642
3643    this.updateToolbarZoom(this.reduce); // Pretty format
3644
3645    if (ui == "embed" || ui == "touch") {
3646        $("#BookReader a.logo").attr("target","_blank");
3647    }
3648
3649    // $$$ turn this into a member variable
3650    var jToolbar = $('#BRtoolbar'); // j prefix indicates jQuery object
3651
3652    // We build in mode 2
3653    jToolbar.append();
3654
3655    // Hide mode buttons and autoplay if 2up is not available
3656    // $$$ if we end up with more than two modes we should show the applicable buttons
3657    if ( !this.canSwitchToMode(this.constMode2up) ) {
3658        jToolbar.find('.two_page_mode, .play, .pause').hide();
3659    }
3660    if ( !this.canSwitchToMode(this.constModeThumb) ) {
3661        jToolbar.find('.thumbnail_mode').hide();
3662    }
3663
3664    // Hide one page button if it is the only mode available
3665    if ( ! (this.canSwitchToMode(this.constMode2up) || this.canSwitchToMode(this.constModeThumb)) ) {
3666        jToolbar.find('.one_page_mode').hide();
3667    }
3668
3669    // $$$ Don't hardcode ids
3670    var self = this;
3671    jToolbar.find('.share').colorbox({inline: true, opacity: "0.5", href: "#BRshare", onLoad: function() { self.autoStop(); self.ttsStop(); } });
3672    jToolbar.find('.info').colorbox({inline: true, opacity: "0.5", href: "#BRinfo", onLoad: function() { self.autoStop(); self.ttsStop(); } });
3673
3674    $('<div style="display: none;"></div>').append(this.blankShareDiv()).append(this.blankInfoDiv()).appendTo($('body'));
3675
3676    $('#BRinfo .BRfloatTitle a').attr( {'href': this.bookUrl} ).text(this.bookTitle).addClass('title');
3677
3678    // These functions can be overridden
3679    this.buildInfoDiv($('#BRinfo'));
3680    this.buildShareDiv($('#BRshare'));
3681
3682    // Switch to requested mode -- binds other click handlers
3683    //this.switchToolbarMode(mode);
3684
3685}
3686
3687BookReader.prototype.blankInfoDiv = function() {
3688    return $([
3689        '<div class="BRfloat" id="BRinfo">',
3690            '<div class="BRfloatHead">About this book',
3691                '<a class="floatShut" href="javascript:;" onclick="$.fn.colorbox.close();"><span class="shift">Close</span></a>',
3692            '</div>',
3693            '<div class="BRfloatBody">',
3694                '<div class="BRfloatCover">',
3695                '</div>',
3696                '<div class="BRfloatMeta">',
3697                    '<div class="BRfloatTitle">',
3698                        '<h2><a/></h2>',
3699                    '</div>',
3700                '</div>',
3701            '</div>',
3702            '<div class="BRfloatFoot">',     
3703            '</div>',
3704        '</div>'].join('\n')
3705    );
3706}
3707
3708BookReader.prototype.blankShareDiv = function() {
3709    return $([
3710        '<div class="BRfloat" id="BRshare">',
3711            '<div class="BRfloatHead">',
3712                'Share',
3713                '<a class="floatShut" href="javascript:;" onclick="$.fn.colorbox.close();"><span class="shift">Close</span></a>',
3714            '</div>',
3715        '</div>'].join('\n')
3716    );
3717}
3718
3719
3720// switchToolbarMode
3721//______________________________________________________________________________
3722// Update the toolbar for the given mode (changes navigation buttons)
3723// $$$ we should soon split the toolbar out into its own module
3724BookReader.prototype.switchToolbarMode = function(mode) {
3725    if (1 == mode) {
3726        // 1-up
3727        $('#BRtoolbar .BRtoolbarzoom').show().css('display', 'inline');
3728        $('#BRtoolbar .BRtoolbarmode2').hide();
3729        $('#BRtoolbar .BRtoolbarmode3').hide();
3730        $('#BRtoolbar .BRtoolbarmode1').show().css('display', 'inline');
3731    } else if (2 == mode) {
3732        // 2-up
3733        $('#BRtoolbar .BRtoolbarzoom').show().css('display', 'inline');
3734        $('#BRtoolbar .BRtoolbarmode1').hide();
3735        $('#BRtoolbar .BRtoolbarmode3').hide();
3736        $('#BRtoolbar .BRtoolbarmode2').show().css('display', 'inline');
3737    } else {
3738        // 3-up
3739        $('#BRtoolbar .BRtoolbarzoom').hide();
3740        $('#BRtoolbar .BRtoolbarmode2').hide();
3741        $('#BRtoolbar .BRtoolbarmode1').hide();
3742        $('#BRtoolbar .BRtoolbarmode3').show().css('display', 'inline');
3743    }
3744}
3745
3746// updateToolbarZoom(reduce)
3747//______________________________________________________________________________
3748// Update the displayed zoom factor based on reduction factor
3749BookReader.prototype.updateToolbarZoom = function(reduce) {
3750    var value;
3751    var autofit = null;
3752
3753    // $$$ TODO preserve zoom/fit for each mode
3754    if (this.mode == this.constMode2up) {
3755        autofit = this.twoPage.autofit;
3756    } else {
3757        autofit = this.onePage.autofit;
3758    }
3759
3760    if (autofit) {
3761        value = autofit.slice(0,1).toUpperCase() + autofit.slice(1);
3762    } else {
3763        value = (100 / reduce).toFixed(2);
3764        // Strip trailing zeroes and decimal if all zeroes
3765        value = value.replace(/0+$/,'');
3766        value = value.replace(/\.$/,'');
3767        value += '%';
3768    }
3769    $('#BRzoom').text(value);
3770}
3771
3772// bindNavigationHandlers
3773//______________________________________________________________________________
3774// Bind navigation handlers
3775BookReader.prototype.bindNavigationHandlers = function() {
3776
3777    var self = this; // closure
3778    jIcons = $('.BRicon');
3779
3780    jIcons.filter('.onepg').bind('click', function(e) {
3781        self.switchMode(self.constMode1up);
3782    });
3783
3784    jIcons.filter('.twopg').bind('click', function(e) {
3785        self.switchMode(self.constMode2up);
3786    });
3787
3788    jIcons.filter('.thumb').bind('click', function(e) {
3789        self.switchMode(self.constModeThumb);
3790    });
3791
3792    jIcons.filter('.fit').bind('fit', function(e) {
3793        // XXXmang implement autofit zoom
3794    });
3795
3796    jIcons.filter('.book_left').click(function(e) {
3797        self.ttsStop();
3798        self.left();
3799        return false;
3800    });
3801
3802    jIcons.filter('.book_right').click(function(e) {
3803        self.ttsStop();
3804        self.right();
3805        return false;
3806    });
3807
3808    jIcons.filter('.book_up').bind('click', function(e) {
3809        if ($.inArray(self.mode, [self.constMode1up, self.constModeThumb]) >= 0) {
3810            self.scrollUp();
3811        } else {
3812            self.prev();
3813        }
3814        return false;
3815    });
3816
3817    jIcons.filter('.book_down').bind('click', function(e) {
3818        if ($.inArray(self.mode, [self.constMode1up, self.constModeThumb]) >= 0) {
3819            self.scrollDown();
3820        } else {
3821            self.next();
3822        }
3823        return false;
3824    });
3825
3826    jIcons.filter('.print').click(function(e) {
3827        self.printPage();
3828        return false;
3829    });
3830
3831    // Note: Functionality has been replaced by .share
3832    jIcons.filter('.embed').click(function(e) {
3833        self.showEmbedCode();
3834        return false;
3835    });
3836
3837    jIcons.filter('.bookmark').click(function(e) {
3838        self.showBookmarkCode();
3839        return false;
3840    });
3841
3842    jIcons.filter('.play').click(function(e) {
3843        self.autoToggle();
3844        return false;
3845    });
3846
3847    jIcons.filter('.pause').click(function(e) {
3848        self.autoToggle();
3849        return false;
3850    });
3851
3852    jIcons.filter('.book_top').click(function(e) {
3853        self.first();
3854        return false;
3855    });
3856
3857    jIcons.filter('.book_bottom').click(function(e) {
3858        self.last();
3859        return false;
3860    });
3861
3862    jIcons.filter('.book_leftmost').click(function(e) {
3863        self.leftmost();
3864        return false;
3865    });
3866
3867    jIcons.filter('.book_rightmost').click(function(e) {
3868        self.rightmost();
3869        return false;
3870    });
3871
3872    jIcons.filter('.read').click(function(e) {
3873        self.ttsToggle();
3874        return false;
3875    });
3876
3877    jIcons.filter('.zoom_in').bind('click', function() {
3878        self.ttsStop();
3879        self.zoom(1);
3880        return false;
3881    });
3882
3883    jIcons.filter('.zoom_out').bind('click', function() {
3884        self.ttsStop();
3885        self.zoom(-1);
3886        return false;
3887    });
3888
3889    jIcons.filter('.full').bind('click', function() {
3890        if (self.ui == 'embed') {
3891            // $$$ bit of a hack, IA-specific
3892            var url = (window.location + '').replace("?ui=embed","");
3893            window.open(url);
3894        }
3895
3896        // Not implemented
3897    });
3898
3899    $('.BRnavCntl').click(
3900        function(){
3901            if ($('#BRnavCntlBtm').hasClass('BRdn')) {
3902                $('#BRtoolbar').animate({top:-40});
3903                $('#BRnav').animate({bottom:-55});
3904                $('#BRnavCntlBtm').addClass('BRup').removeClass('BRdn');
3905                $('#BRnavCntlTop').addClass('BRdn').removeClass('BRup');
3906                $('#BRnavCntlBtm.BRnavCntl').animate({height:'45px'});
3907                $('.BRnavCntl').delay(1000).animate({opacity:.25},1000);
3908            } else {
3909                $('#BRtoolbar').animate({top:0});
3910                $('#BRnav').animate({bottom:0});
3911                $('#BRnavCntlBtm').addClass('BRdn').removeClass('BRup');
3912                $('#BRnavCntlTop').addClass('BRup').removeClass('BRdn');
3913                $('#BRnavCntlBtm.BRnavCntl').animate({height:'30px'});
3914                $('.BRvavCntl').animate({opacity:1})
3915            };
3916        }
3917    );
3918    $('#BRnavCntlBtm').mouseover(function(){
3919        if ($(this).hasClass('BRup')) {
3920            $('.BRnavCntl').animate({opacity:1},250);
3921        };
3922    });
3923    $('#BRnavCntlBtm').mouseleave(function(){
3924        if ($(this).hasClass('BRup')) {
3925            $('.BRnavCntl').animate({opacity:.25},250);
3926        };
3927    });
3928    $('#BRnavCntlTop').mouseover(function(){
3929        if ($(this).hasClass('BRdn')) {
3930            $('.BRnavCntl').animate({opacity:1},250);
3931        };
3932    });
3933    $('#BRnavCntlTop').mouseleave(function(){
3934        if ($(this).hasClass('BRdn')) {
3935            $('.BRnavCntl').animate({opacity:.25},250);
3936        };
3937    });
3938
3939
3940    this.initSwipeData();
3941    $('#BookReader').die('mousemove.navigation').live('mousemove.navigation',
3942        { 'br': this },
3943        this.navigationMousemoveHandler
3944    );
3945
3946    $('.BRpageimage').die('mousedown.swipe').live('mousedown.swipe',
3947        { 'br': this },
3948        this.swipeMousedownHandler
3949    );
3950
3951    this.bindMozTouchHandlers();
3952}
3953
3954// unbindNavigationHandlers
3955//______________________________________________________________________________
3956// Unbind navigation handlers
3957BookReader.prototype.unbindNavigationHandlers = function() {
3958    $('#BookReader').die('mousemove.navigation');
3959}
3960
3961// navigationMousemoveHandler
3962//______________________________________________________________________________
3963// Handle mousemove related to navigation.  Bind at #BookReader level to allow autohide.
3964BookReader.prototype.navigationMousemoveHandler = function(event) {
3965    // $$$ possibly not great to be calling this for every mousemove
3966
3967    if (event.data['br'].uiAutoHide) {
3968        var navkey = $(document).height() - 75;
3969        if ((event.pageY < 76) || (event.pageY > navkey)) {
3970            // inside or near navigation elements
3971            event.data['br'].hideNavigation();
3972        } else {
3973            event.data['br'].showNavigation();
3974        }
3975    }
3976}
3977
3978BookReader.prototype.initSwipeData = function(clientX, clientY) {
3979    /*
3980     * Based on the really quite awesome "Today's Guardian" at http://guardian.gyford.com/
3981     */
3982    this._swipe = {
3983        mightBeSwiping: false,
3984        didSwipe: false,
3985        mightBeDraggin: false,
3986        didDrag: false,
3987        startTime: (new Date).getTime(),
3988        startX: clientX,
3989        startY: clientY,
3990        lastX: clientX,
3991        lastY: clientY,
3992        deltaX: 0,
3993        deltaY: 0,
3994        deltaT: 0
3995    }
3996}
3997
3998BookReader.prototype.swipeMousedownHandler = function(event) {
3999    //console.log('swipe mousedown');
4000    //console.log(event);
4001
4002    var self = event.data['br'];
4003
4004    // We should be the last bubble point for the page images
4005    // Disable image drag and select, but keep right-click
4006    if (event.which == 3) {
4007        if (self.protected) {
4008            return false;
4009        }
4010        return true;
4011    }
4012
4013    $(event.target).bind('mouseout.swipe',
4014        { 'br': self},
4015        self.swipeMouseupHandler
4016    ).bind('mouseup.swipe',
4017        { 'br': self},
4018        self.swipeMouseupHandler
4019    ).bind('mousemove.swipe',
4020        { 'br': self },
4021        self.swipeMousemoveHandler
4022    );
4023
4024    self.initSwipeData(event.clientX, event.clientY);
4025    self._swipe.mightBeSwiping = true;
4026    self._swipe.mightBeDragging = true;
4027
4028    event.preventDefault();
4029    event.returnValue  = false;
4030    event.cancelBubble = true;
4031    return false;
4032}
4033
4034BookReader.prototype.swipeMousemoveHandler = function(event) {
4035    //console.log('swipe move ' + event.clientX + ',' + event.clientY);
4036
4037    var _swipe = event.data['br']._swipe;
4038    if (! _swipe.mightBeSwiping) {
4039        return;
4040    }
4041
4042    // Update swipe data
4043    _swipe.deltaX = event.clientX - _swipe.startX;
4044    _swipe.deltaY = event.clientY - _swipe.startY;
4045    _swipe.deltaT = (new Date).getTime() - _swipe.startTime;
4046
4047    var absX = Math.abs(_swipe.deltaX);
4048    var absY = Math.abs(_swipe.deltaY);
4049
4050    // Minimum distance in the amount of tim to trigger the swipe
4051    var minSwipeLength = Math.min($('#BookReader').width() / 5, 80);
4052    var maxSwipeTime = 400;
4053
4054    // Check for horizontal swipe
4055    if (absX > absY && (absX > minSwipeLength) && _swipe.deltaT < maxSwipeTime) {
4056        //console.log('swipe! ' + _swipe.deltaX + ',' + _swipe.deltaY + ' ' + _swipe.deltaT + 'ms');
4057
4058        _swipe.mightBeSwiping = false; // only trigger once
4059        _swipe.didSwipe = true;
4060        if (event.data['br'].mode == event.data['br'].constMode2up) {
4061            if (_swipe.deltaX < 0) {
4062                event.data['br'].right();
4063            } else {
4064                event.data['br'].left();
4065            }
4066        }
4067    }
4068
4069    if ( _swipe.deltaT > maxSwipeTime && !_swipe.didSwipe) {
4070        if (_swipe.mightBeDragging) {
4071            // Dragging
4072            _swipe.didDrag = true;
4073            $('#BRcontainer')
4074            .scrollTop($('#BRcontainer').scrollTop() - event.clientY + _swipe.lastY)
4075            .scrollLeft($('#BRcontainer').scrollLeft() - event.clientX + _swipe.lastX);
4076        }
4077    }
4078    _swipe.lastX = event.clientX;
4079    _swipe.lastY = event.clientY;
4080
4081    event.preventDefault();
4082    event.returnValue  = false;
4083    event.cancelBubble = true;
4084    return false;
4085}
4086BookReader.prototype.swipeMouseupHandler = function(event) {
4087    var _swipe = event.data['br']._swipe;
4088    //console.log('swipe mouseup - did swipe ' + _swipe.didSwipe);
4089    _swipe.mightBeSwiping = false;
4090    _swipe.mightBeDragging = false;
4091
4092    $(event.target).unbind('mouseout.swipe').unbind('mouseup.swipe').unbind('mousemove.swipe');
4093
4094    if (_swipe.didSwipe || _swipe.didDrag) {
4095        // Swallow event if completed swipe gesture
4096        event.preventDefault();
4097        event.returnValue  = false;
4098        event.cancelBubble = true;
4099        return false;
4100    }
4101    return true;
4102}
4103BookReader.prototype.bindMozTouchHandlers = function() {
4104    var self = this;
4105
4106    // Currently only want touch handlers in 2up
4107    $('#BookReader').bind('MozTouchDown', function(event) {
4108        //console.log('MozTouchDown ' + event.originalEvent.streamId + ' ' + event.target + ' ' + event.clientX + ',' + event.clientY);
4109        if (this.mode == this.constMode2up) {
4110            event.preventDefault();
4111        }
4112    })
4113    .bind('MozTouchMove', function(event) {
4114        //console.log('MozTouchMove - ' + event.originalEvent.streamId + ' ' + event.target + ' ' + event.clientX + ',' + event.clientY)
4115        if (this.mode == this.constMode2up) {
4116            event.preventDefault();
4117        }
4118    })
4119    .bind('MozTouchUp', function(event) {
4120        //console.log('MozTouchUp - ' + event.originalEvent.streamId + ' ' + event.target + ' ' + event.clientX + ',' + event.clientY);
4121        if (this.mode = this.constMode2up) {
4122            event.preventDefault();
4123        }
4124    });
4125}
4126
4127// navigationIsVisible
4128//______________________________________________________________________________
4129// Returns true if the navigation elements are currently visible
4130BookReader.prototype.navigationIsVisible = function() {
4131    // $$$ doesn't account for transitioning states, nav must be fully visible to return true
4132    var toolpos = $('#BRtoolbar').offset();
4133    var tooltop = toolpos.top;
4134    if (tooltop == 0) {
4135        return true;
4136    }
4137    return false;
4138}
4139
4140// hideNavigation
4141//______________________________________________________________________________
4142// Hide navigation elements, if visible
4143BookReader.prototype.hideNavigation = function() {
4144    // Check if navigation is showing
4145    if (this.navigationIsVisible()) {
4146        // $$$ don't hardcode height
4147        $('#BRtoolbar').animate({top:-60});
4148        $('#BRnav').animate({bottom:-60});
4149        //$('#BRzoomer').animate({right:-26});
4150    }
4151}
4152
4153// showNavigation
4154//______________________________________________________________________________
4155// Show navigation elements
4156BookReader.prototype.showNavigation = function() {
4157    // Check if navigation is hidden
4158    if (!this.navigationIsVisible()) {
4159        $('#BRtoolbar').animate({top:0});
4160        $('#BRnav').animate({bottom:0});
4161        //$('#BRzoomer').animate({right:0});
4162    }
4163}
4164
4165// changeArrow
4166//______________________________________________________________________________
4167// Change the nav bar arrow
4168function changeArrow(){
4169    setTimeout(function(){
4170        $('#BRnavCntlBtm').removeClass('BRdn').addClass('BRup');
4171    },3000);
4172};
4173
4174
4175// firstDisplayableIndex
4176//______________________________________________________________________________
4177// Returns the index of the first visible page, dependent on the mode.
4178// $$$ Currently we cannot display the front/back cover in 2-up and will need to update
4179// this function when we can as part of https://bugs.launchpad.net/gnubook/+bug/296788
4180BookReader.prototype.firstDisplayableIndex = function() {
4181    if (this.mode != this.constMode2up) {
4182        return 0;
4183    }
4184
4185    if ('rl' != this.pageProgression) {
4186        // LTR
4187        if (this.getPageSide(0) == 'L') {
4188            return 0;
4189        } else {
4190            return -1;
4191        }
4192    } else {
4193        // RTL
4194        if (this.getPageSide(0) == 'R') {
4195            return 0;
4196        } else {
4197            return -1;
4198        }
4199    }
4200}
4201
4202// lastDisplayableIndex
4203//______________________________________________________________________________
4204// Returns the index of the last visible page, dependent on the mode.
4205// $$$ Currently we cannot display the front/back cover in 2-up and will need to update
4206// this function when we can as pa  rt of https://bugs.launchpad.net/gnubook/+bug/296788
4207BookReader.prototype.lastDisplayableIndex = function() {
4208
4209    var lastIndex = this.numLeafs - 1;
4210
4211    if (this.mode != this.constMode2up) {
4212        return lastIndex;
4213    }
4214
4215    if ('rl' != this.pageProgression) {
4216        // LTR
4217        if (this.getPageSide(lastIndex) == 'R') {
4218            return lastIndex;
4219        } else {
4220            return lastIndex + 1;
4221        }
4222    } else {
4223        // RTL
4224        if (this.getPageSide(lastIndex) == 'L') {
4225            return lastIndex;
4226        } else {
4227            return lastIndex + 1;
4228        }
4229    }
4230}
4231
4232// shortTitle(maximumCharacters)
4233//________
4234// Returns a shortened version of the title with the maximum number of characters
4235BookReader.prototype.shortTitle = function(maximumCharacters) {
4236    if (this.bookTitle.length < maximumCharacters) {
4237        return this.bookTitle;
4238    }
4239
4240    var title = this.bookTitle.substr(0, maximumCharacters - 3);
4241    title += '...';
4242    return title;
4243}
4244
4245// Parameter related functions
4246
4247// updateFromParams(params)
4248//________
4249// Update ourselves from the params object.
4250//
4251// e.g. this.updateFromParams(this.paramsFromFragment(window.location.hash))
4252BookReader.prototype.updateFromParams = function(params) {
4253    if ('undefined' != typeof(params.mode)) {
4254        this.switchMode(params.mode);
4255    }
4256
4257    // process /search
4258    if ('undefined' != typeof(params.searchTerm)) {
4259        if (this.searchTerm != params.searchTerm) {
4260            this.search(params.searchTerm);
4261        }
4262    }
4263
4264    // $$$ process /zoom
4265
4266    // We only respect page if index is not set
4267    if ('undefined' != typeof(params.index)) {
4268        if (params.index != this.currentIndex()) {
4269            this.jumpToIndex(params.index);
4270        }
4271    } else if ('undefined' != typeof(params.page)) {
4272        // $$$ this assumes page numbers are unique
4273        if (params.page != this.getPageNum(this.currentIndex())) {
4274            this.jumpToPage(params.page);
4275        }
4276    }
4277
4278    // $$$ process /region
4279    // $$$ process /highlight
4280}
4281
4282// paramsFromFragment(urlFragment)
4283//________
4284// Returns a object with configuration parametes from a URL fragment.
4285//
4286// E.g paramsFromFragment(window.location.hash)
4287BookReader.prototype.paramsFromFragment = function(urlFragment) {
4288 
4289
4290    var params = {};
4291
4292    // For convenience we allow an initial # character (as from window.location.hash)
4293    // but don't require it
4294    if (urlFragment.substr(0,1) == '#') {
4295        urlFragment = urlFragment.substr(1);
4296    }
4297
4298    // Simple #nn syntax
4299    var oldStyleLeafNum = parseInt( /^\d+$/.exec(urlFragment) );
4300    if ( !isNaN(oldStyleLeafNum) ) {
4301        params.index = oldStyleLeafNum;
4302
4303        // Done processing if using old-style syntax
4304        return params;
4305    }
4306
4307    // Split into key-value pairs
4308    var urlArray = urlFragment.split('/');
4309    var urlHash = {};
4310    for (var i = 0; i < urlArray.length; i += 2) {
4311        urlHash[urlArray[i]] = urlArray[i+1];
4312    }
4313
4314    // Mode
4315    if ('1up' == urlHash['mode']) {
4316        params.mode = this.constMode1up;
4317    } else if ('2up' == urlHash['mode']) {
4318        params.mode = this.constMode2up;
4319    } else if ('thumb' == urlHash['mode']) {
4320        params.mode = this.constModeThumb;
4321    }
4322
4323    // Index and page
4324    if ('undefined' != typeof(urlHash['page'])) {
4325        // page was set -- may not be int
4326        params.page = urlHash['page'];
4327    }
4328
4329    // $$$ process /region
4330    // $$$ process /search
4331
4332    if (urlHash['search'] != undefined) {
4333        params.searchTerm = BookReader.util.decodeURIComponentPlus(urlHash['search']);
4334    }
4335
4336    // $$$ process /highlight
4337
4338    return params;
4339}
4340
4341// paramsFromCurrent()
4342//________
4343// Create a params object from the current parameters.
4344BookReader.prototype.paramsFromCurrent = function() {
4345
4346    var params = {};
4347
4348    var index = this.currentIndex();
4349    var pageNum = this.getPageNum(index);
4350    if ((pageNum === 0) || pageNum) {
4351        params.page = pageNum;
4352    }
4353
4354    params.index = index;
4355    params.mode = this.mode;
4356
4357    // $$$ highlight
4358    // $$$ region
4359
4360    // search
4361    if (this.searchHighlightVisible()) {
4362        params.searchTerm = this.searchTerm;
4363    }
4364
4365    return params;
4366}
4367
4368// fragmentFromParams(params)
4369//________
4370// Create a fragment string from the params object.
4371
4372BookReader.prototype.fragmentFromParams = function(params) {
4373    var separator = '/';
4374
4375    var fragments = [];
4376
4377    if ('undefined' != typeof(params.page)) {
4378        fragments.push('page', params.page);
4379    } else {
4380        if ('undefined' != typeof(params.index)) {
4381            // Don't have page numbering but we do have the index
4382            fragments.push('page', 'n' + params.index);
4383        }
4384    }
4385
4386    // $$$ highlight
4387    // $$$ region
4388
4389    // mode
4390    if ('undefined' != typeof(params.mode)) {
4391        if (params.mode == this.constMode1up) {
4392            fragments.push('mode', '1up');
4393        } else if (params.mode == this.constMode2up) {
4394            fragments.push('mode', '2up');
4395        } else if (params.mode == this.constModeThumb) {
4396            fragments.push('mode', 'thumb');
4397        } else {
4398            throw 'fragmentFromParams called with unknown mode ' + params.mode;
4399        }
4400    }
4401
4402    // search
4403    if (params.searchTerm) {
4404        fragments.push('search', params.searchTerm);
4405    }
4406
4407    return BookReader.util.encodeURIComponentPlus(fragments.join(separator)).replace(/%2F/g, '/');
4408}
4409
4410// getPageIndex(pageNum)
4411//________
4412// Returns the *highest* index the given page number, or undefined
4413BookReader.prototype.getPageIndex = function(pageNum) {
4414    var pageIndices = this.getPageIndices(pageNum);
4415
4416    if (pageIndices.length > 0) {
4417        return pageIndices[pageIndices.length - 1];
4418    }
4419
4420    return undefined;
4421}
4422
4423// getPageIndices(pageNum)
4424//________
4425// Returns an array (possibly empty) of the indices with the given page number
4426BookReader.prototype.getPageIndices = function(pageNum) {
4427    var indices = [];
4428
4429    // Check for special "nXX" page number
4430    if (pageNum.slice(0,1) == 'n') {
4431        try {
4432            var pageIntStr = pageNum.slice(1, pageNum.length);
4433            var pageIndex = parseInt(pageIntStr);
4434            indices.push(pageIndex);
4435            return indices;
4436        } catch(err) {
4437            // Do nothing... will run through page names and see if one matches
4438        }
4439    }
4440
4441    var i;
4442    for (i=0; i<this.numLeafs; i++) {
4443        if (this.getPageNum(i) == pageNum) {
4444            indices.push(i);
4445        }
4446    }
4447
4448    return indices;
4449}
4450
4451// getPageName(index)
4452//________
4453// Returns the name of the page as it should be displayed in the user interface
4454BookReader.prototype.getPageName = function(index) {
4455    return 'Page ' + this.getPageNum(index);
4456}
4457
4458// updateLocationHash
4459//________
4460// Update the location hash from the current parameters.  Call this instead of manually
4461// using window.location.replace
4462BookReader.prototype.updateLocationHash = function() {
4463    var newHash = '#' + this.fragmentFromParams(this.paramsFromCurrent());
4464    window.location.replace(newHash);
4465
4466    // This is the variable checked in the timer.  Only user-generated changes
4467    // to the URL will trigger the event.
4468    this.oldLocationHash = newHash;
4469}
4470
4471// startLocationPolling
4472//________
4473// Starts polling of window.location to see hash fragment changes
4474BookReader.prototype.startLocationPolling = function() {
4475    var self = this; // remember who I am
4476    self.oldLocationHash = window.location.hash;
4477
4478    if (this.locationPollId) {
4479        clearInterval(this.locationPollID);
4480        this.locationPollId = null;
4481    }
4482
4483    this.locationPollId = setInterval(function() {
4484        var newHash = window.location.hash;
4485        if (newHash != self.oldLocationHash) {
4486            if (newHash != self.oldUserHash) { // Only process new user hash once
4487                //console.log('url change detected ' + self.oldLocationHash + " -> " + newHash);
4488
4489                self.ttsStop();
4490
4491                // Queue change if animating
4492                if (self.animating) {
4493                    self.autoStop();
4494                    self.animationFinishedCallback = function() {
4495                        self.updateFromParams(self.paramsFromFragment(newHash));
4496                    }
4497                } else { // update immediately
4498                    self.updateFromParams(self.paramsFromFragment(newHash));
4499                }
4500                self.oldUserHash = newHash;
4501            }
4502        }
4503    }, 500);
4504}
4505
4506// canSwitchToMode
4507//________
4508// Returns true if we can switch to the requested mode
4509BookReader.prototype.canSwitchToMode = function(mode) {
4510    if (mode == this.constMode2up || mode == this.constModeThumb) {
4511        // check there are enough pages to display
4512        // $$$ this is a workaround for the mis-feature that we can't display
4513        //     short books in 2up mode
4514        if (this.numLeafs < 2) {
4515            return false;
4516        }
4517    }
4518
4519    return true;
4520}
4521
4522// searchHighlightVisible
4523//________
4524// Returns true if a search highlight is currently being displayed
4525BookReader.prototype.searchHighlightVisible = function() {
4526    var results = this.searchResults;
4527    if (null == results) return false;
4528
4529    if (this.constMode2up == this.mode) {
4530        var visiblePages = Array(this.twoPage.currentIndexL, this.twoPage.currentIndexR);
4531    } else if (this.constMode1up == this.mode) {
4532        var visiblePages = Array();
4533        visiblePages[0] = this.currentIndex();
4534    } else {
4535        return false;
4536    }
4537
4538    var i, j;
4539    for (i=0; i<results.matches.length; i++) {
4540        //console.log(results.matches[i].par[0]);
4541        for (j=0; j<results.matches[i].par[0].boxes.length; j++) {
4542            var box = results.matches[i].par[0].boxes[j];
4543            var pageIndex = this.leafNumToIndex(box.page);
4544            if (jQuery.inArray(pageIndex, visiblePages) >= 0) {
4545                return true;
4546            }
4547        }
4548    }
4549
4550    return false;
4551}
4552
4553// _getPageWidth
4554//--------
4555// Returns the page width for the given index, or first or last page if out of range
4556BookReader.prototype._getPageWidth = function(index) {
4557    // Synthesize a page width for pages not actually present in book.
4558    // May or may not be the best approach.
4559    // If index is out of range we return the width of first or last page
4560    index = BookReader.util.clamp(index, 0, this.numLeafs - 1);
4561    return this.getPageWidth(index);
4562}
4563
4564// _getPageHeight
4565//--------
4566// Returns the page height for the given index, or first or last page if out of range
4567BookReader.prototype._getPageHeight= function(index) {
4568    index = BookReader.util.clamp(index, 0, this.numLeafs - 1);
4569    return this.getPageHeight(index);
4570}
4571
4572// _getPageURI
4573//--------
4574// Returns the page URI or transparent image if out of range
4575BookReader.prototype._getPageURI = function(index, reduce, rotate) {
4576    if (index < 0 || index >= this.numLeafs) { // Synthesize page
4577        return this.imagesBaseURL + "transparent.png";
4578    }
4579
4580    if ('undefined' == typeof(reduce)) {
4581        // reduce not passed in
4582        // $$$ this probably won't work for thumbnail mode
4583        var ratio = this.getPageHeight(index) / this.twoPage.height;
4584        var scale;
4585        // $$$ we make an assumption here that the scales are available pow2 (like kakadu)
4586        if (ratio < 2) {
4587            scale = 1;
4588        } else if (ratio < 4) {
4589            scale = 2;
4590        } else if (ratio < 8) {
4591            scale = 4;
4592        } else if (ratio < 16) {
4593            scale = 8;
4594        } else  if (ratio < 32) {
4595            scale = 16;
4596        } else {
4597            scale = 32;
4598        }
4599        reduce = scale;
4600    }
4601
4602    return this.getPageURI(index, reduce, rotate);
4603}
4604
4605/*
4606 * Update based on received record from Open Library.
4607 */
4608BookReader.prototype.gotOpenLibraryRecord = function(self, olObject) {
4609    // $$$ could refactor this so that 'this' is available
4610    if (olObject) {
4611        // console.log(olObject);
4612        if (olObject['table_of_contents']) {
4613            // XXX check here that TOC is valid
4614            self.updateTOC(olObject['table_of_contents']);
4615        }
4616
4617        // $$$mang cleanup
4618        self.bookUrl = self.olHost + olObject.key;
4619        self.bookTitle = olObject['title'];
4620        $('#BRreturn a').attr( {'href': self.bookUrl, 'title': "Go to this book's page on Open Library" } );
4621        $('#BRreturn a').text(self.bookTitle);
4622
4623        $('#BRinfo').remove();
4624        $('#BRshare').after(self.blankInfoDiv());
4625        self.buildInfoDiv($('#BRinfo'));
4626
4627        // Check for borrowed book
4628        if (self.olAuth) {
4629            var returnUrl = self.olHost + olObject.key + '/do_return/borrow';
4630            var borrowUrl = self.olHost + olObject.key + '/borrow';
4631
4632            /*
4633            $('<a/>')
4634                .attr('href', borrowUrl)
4635                .text('Return this book')
4636                .click(function(event) {
4637                    event.preventDefault();
4638                    $('#BRreturnform').trigger('submit');
4639                })
4640                .appendTo('#BRreturn');
4641            */
4642
4643            $('<form id="BRreturnform" action="' + returnUrl + '" method="post"><input type="submit" value="Return book" onclick="olAuth.deleteCookies();"/><input type="hidden" name="action" value="return" /></form>')
4644                .appendTo('#BRreturn');
4645
4646        } else {
4647            $('<a/>').attr( { 'href': self.bookUrl, 'title': 'Go to this book\'s page on Open Library' } )
4648                .text('On openlibrary.org')
4649                .appendTo('#BRreturn');
4650        }
4651
4652        $('#BRreturn').css({ 'line-height': '19px'} );
4653        $('#BRreturn a').css( {'height': '18px' } );
4654
4655
4656    }
4657}
4658
4659// Library functions
4660BookReader.util = {
4661    disableSelect: function(jObject) {
4662        // Bind mouse handlers
4663        // Disable mouse click to avoid selected/highlighted page images - bug 354239
4664        jObject.bind('mousedown', function(e) {
4665            // $$$ check here for right-click and don't disable.  Also use jQuery style
4666            //     for stopping propagation. See https://bugs.edge.launchpad.net/gnubook/+bug/362626
4667            return false;
4668        });
4669        // Special hack for IE7
4670        jObject[0].onselectstart = function(e) { return false; };
4671    },
4672
4673    clamp: function(value, min, max) {
4674        return Math.min(Math.max(value, min), max);
4675    },
4676
4677    // Given value and maximum, calculate a percentage suitable for CSS
4678    cssPercentage: function(value, max) {
4679        return (((value + 0.0) / max) * 100) + '%';
4680    },
4681
4682    notInArray: function(value, array) {
4683        // inArray returns -1 or undefined if value not in array
4684        return ! (jQuery.inArray(value, array) >= 0);
4685    },
4686
4687    getIFrameDocument: function(iframe) {
4688        // Adapted from http://xkr.us/articles/dom/iframe-document/
4689        var outer = (iframe.contentWindow || iframe.contentDocument);
4690        return (outer.document || outer);
4691    },
4692
4693    escapeHTML: function (str) {
4694        return(
4695            str.replace(/&/g,'&amp;').
4696                replace(/>/g,'&gt;').
4697                replace(/</g,'&lt;').
4698                replace(/"/g,'&quot;')
4699        );
4700    },
4701
4702    decodeURIComponentPlus: function(value) {
4703        // Decodes a URI component and converts '+' to ' '
4704        return decodeURIComponent(value).replace(/\+/g, ' ');
4705    },
4706
4707    encodeURIComponentPlus: function(value) {
4708        // Encodes a URI component and converts ' ' to '+'
4709        return encodeURIComponent(value).replace(/%20/g, '+');
4710    }
4711    // The final property here must NOT have a comma after it - IE7
4712}
4713
4714
4715// ttsToggle()
4716//______________________________________________________________________________
4717BookReader.prototype.ttsToggle = function () {
4718
4719    this.autoStop();
4720
4721    if (false == this.ttsPlaying) {
4722        this.ttsPlaying = true;
4723        this.showProgressPopup('Loading audio...');
4724        if(soundManager.supported()) {
4725            this.ttsStart();
4726        } else {
4727            soundManager.onready(function(oStatus) {
4728              if (oStatus.success) {
4729                this.ttsStart();
4730              } else {
4731                alert('Could not load soundManager2, possibly due to FlashBlock. Audio playback is disabled');
4732              }
4733            }, this);
4734        }
4735    } else {
4736        this.ttsStop();
4737    }
4738}
4739
4740// ttsStart()
4741//______________________________________________________________________________
4742BookReader.prototype.ttsStart = function () {
4743    if (soundManager.debugMode) console.log('starting readAloud');
4744    if (this.constModeThumb == this.mode) this.switchMode(this.constMode1up);
4745
4746    //this.ttsPlaying = true; //set this in ttsToggle()
4747    this.ttsIndex = this.currentIndex();
4748    this.ttsFormat = 'mp3';
4749    if ($.browser.mozilla) {
4750        this.ttsFormat = 'ogg';
4751    }
4752    this.ttsGetText(this.ttsIndex, 'ttsStartCB');
4753}
4754
4755// ttsStop()
4756//______________________________________________________________________________
4757BookReader.prototype.ttsStop = function () {
4758    if (false == this.ttsPlaying) return;
4759
4760    if (soundManager.debugMode) console.log('stopping readaloud');
4761    soundManager.stopAll();
4762    soundManager.destroySound('chunk'+this.ttsIndex+'-'+this.ttsPosition);
4763    this.ttsRemoveHilites();
4764    this.removeProgressPopup();
4765
4766    this.ttsPlaying     = false;
4767    this.ttsIndex       = null;  //leaf index
4768    this.ttsPosition    = -1;    //chunk (paragraph) number
4769    this.ttsBuffering   = false;
4770    this.ttsPoller      = null;
4771}
4772
4773// ttsGetText()
4774//______________________________________________________________________________
4775BookReader.prototype.ttsGetText = function(index, callback) {
4776    var url = 'https://'+this.server+'/BookReader/BookReaderGetTextWrapper.php?path='+this.bookPath+'_djvu.xml&page='+index;
4777    this.ttsAjax = $.ajax({url:url, dataType:'jsonp', jsonpCallback:callback});
4778}
4779
4780// ttsStartCB(): text-to-speech callback
4781//______________________________________________________________________________
4782BookReader.prototype.ttsStartCB = function (data) {
4783    if (soundManager.debugMode)  console.log('ttsStartCB got data: ' + data);
4784    this.ttsChunks = data;
4785    this.ttsHilites = [];
4786
4787    //deal with the page being blank
4788    if (0 == data.length) {
4789        if (soundManager.debugMode) console.log('first page is blank!');
4790        if(this.ttsAdvance(true)) {
4791            this.ttsGetText(this.ttsIndex, 'ttsStartCB');
4792        }
4793        return;
4794    }
4795
4796    this.showProgressPopup('Loading audio...');
4797
4798    ///// Many soundManger2 callbacks are broken when using HTML5 audio.
4799    ///// whileloading: broken on safari, worked in FF4, but broken on FireFox 5
4800    ///// onload: fires on safari, but *after* the sound starts playing, and does not fire in FF or IE9
4801    ///// onbufferchange: fires in FF5 using HTML5 audio, but not in safari using flash audio
4802    ///// whileplaying: fires everywhere
4803    this.ttsPosition = -1;
4804    var snd = soundManager.createSound({
4805     id: 'chunk'+this.ttsIndex+'-0',
4806     url: 'https://'+this.server+'/BookReader/BookReaderGetTTS.php?string=' + escape(data[0][0]) + '&format=.'+this.ttsFormat, //the .ogg is to trick SoundManager2 to use the HTML5 audio player
4807     onload: function(){this.br.removeProgressPopup();}, //fires in safari...
4808     onbufferchange: function(){if (false == this.isBuffering) this.br.removeProgressPopup();} //fires in FF and IE9
4809    });
4810    snd.br = this;
4811    snd.load();
4812
4813    this.ttsNextChunk();
4814}
4815
4816// showProgressPopup
4817//______________________________________________________________________________
4818BookReader.prototype.showProgressPopup = function(msg) {
4819    //if (soundManager.debugMode) console.log('showProgressPopup index='+this.ttsIndex+' pos='+this.ttsPosition);
4820    if (this.popup) return;
4821
4822    this.popup = document.createElement("div");
4823    $(this.popup).css({
4824        top:      ($('#BookReader').height()*0.5-100) + 'px',
4825        left:     ($('#BookReader').width()-300)*0.5 + 'px'
4826    }).attr('className', 'BRprogresspopup');
4827
4828    var bar = document.createElement("div");
4829    $(bar).css({
4830        height:   '20px'
4831    }).attr('className', 'BRprogressbar');
4832    $(this.popup).append(bar);
4833
4834    if (msg) {
4835        var msgdiv = document.createElement("div");
4836        msgdiv.innerHTML = msg;
4837        $(this.popup).append(msgdiv);
4838    }
4839
4840    $(this.popup).appendTo('#BookReader');
4841}
4842
4843// removeProgressPopup
4844//______________________________________________________________________________
4845BookReader.prototype.removeProgressPopup = function() {
4846    $(this.popup).remove();
4847    this.popup=null;
4848}
4849
4850// ttsNextPageCB
4851//______________________________________________________________________________
4852BookReader.prototype.ttsNextPageCB = function (data) {
4853    this.ttsNextChunks = data;
4854    if (soundManager.debugMode) console.log('preloaded next chunks.. data is ' + data);
4855
4856    if (true == this.ttsBuffering) {
4857        if (soundManager.debugMode) console.log('ttsNextPageCB: ttsBuffering is true');
4858        this.ttsBuffering = false;
4859    }
4860}
4861
4862// ttsLoadChunk
4863//______________________________________________________________________________
4864BookReader.prototype.ttsLoadChunk = function (page, pos, string) {
4865    var snd = soundManager.createSound({
4866     id: 'chunk'+page+'-'+pos,
4867     url: 'https://'+this.server+'/BookReader/BookReaderGetTTS.php?string=' + escape(string) + '&format=.'+this.ttsFormat //the .ogg is to trick SoundManager2 to use the HTML5 audio player
4868    });
4869    snd.br = this;
4870    snd.load()
4871}
4872
4873
4874// ttsNextChunk()
4875//______________________________________________________________________________
4876// This function into two parts: ttsNextChunk gets run before page flip animation
4877// and ttsNextChunkPhase2 get run after page flip animation.
4878// If a page flip is necessary, ttsAdvance() will return false so Phase2 isn't
4879// called. Instead, this.animationFinishedCallback is set, so that Phase2
4880// continues after animation is finished.
4881
4882BookReader.prototype.ttsNextChunk = function () {
4883    if (soundManager.debugMode) console.log('nextchunk pos=' + this.ttsPosition);
4884
4885    if (-1 != this.ttsPosition) {
4886        soundManager.destroySound('chunk'+this.ttsIndex+'-'+this.ttsPosition);
4887    }
4888
4889    this.ttsRemoveHilites(); //remove old hilights
4890
4891    var moreToPlay = this.ttsAdvance();
4892
4893    if (moreToPlay) {
4894        this.ttsNextChunkPhase2();
4895    }
4896
4897    //This function is called again when ttsPlay() has finished playback.
4898    //If the next chunk of text has not yet finished loading, ttsPlay()
4899    //will start polling until the next chunk is ready.
4900}
4901
4902// ttsNextChunkPhase2()
4903//______________________________________________________________________________
4904// page flip animation has now completed
4905BookReader.prototype.ttsNextChunkPhase2 = function () {
4906    if (null == this.ttsChunks) {
4907        alert('error: ttsChunks is null?'); //TODO
4908        return;
4909    }
4910
4911    if (0 == this.ttsChunks.length) {
4912        if (soundManager.debugMode) console.log('ttsNextChunk2: ttsChunks.length is zero.. hacking...');
4913        this.ttsStartCB(this.ttsChunks);
4914        return;
4915    }
4916
4917    if (soundManager.debugMode) console.log('next chunk is ' + this.tt
4917sPosition);
4918
4919    //prefetch next page of text
4920    if (0 == this.ttsPosition) {
4921        if (this.ttsIndex<(this.numLeafs-1)) {
4922            this.ttsGetText(this.ttsIndex+1, 'ttsNextPageCB');
4923        }
4924    }
4925
4926    this.ttsPrefetchAudio();
4927
4928    this.ttsPlay();
4929}
4930
4931// ttsAdvance()
4932//______________________________________________________________________________
4933// 1. advance ttsPosition
4934// 2. if necessary, advance ttsIndex, and copy ttsNextChunks to ttsChunks
4935// 3. if necessary, flip to current page, or scroll so chunk is visible
4936// 4. do something smart is ttsNextChunks has not yet finished preloading (TODO)
4937// 5. stop playing at end of book
4938
4939BookReader.prototype.ttsAdvance = function (starting) {
4940    this.ttsPosition++;
4941
4942    if (this.ttsPosition >= this.ttsChunks.length) {
4943
4944        if (this.ttsIndex == (this.numLeafs-1)) {
4945            if (soundManager.debugMode) console.log('tts stop');
4946            return false;
4947        } else {
4948            if ((null != this.ttsNextChunks) || (starting)) {
4949                if (soundManager.debugMode) console.log('moving to next page!');
4950                this.ttsIndex++;
4951                this.ttsPosition = 0;
4952                this.ttsChunks = this.ttsNextChunks;
4953                this.ttsNextChunks = null;
4954
4955                //A page flip might be necessary. This code is confusing since
4956                //ttsNextChunks might be null if we are starting on a blank page.
4957                if (2 == this.mode) {
4958                    if ((this.ttsIndex != this.twoPage.currentIndexL) && (this.ttsIndex != this.twoPage.currentIndexR)) {
4959                        if (!starting) {
4960                            this.animationFinishedCallback = this.ttsNextChunkPhase2;
4961                            this.next();
4962                            return false;
4963                        } else {
4964                            this.next();
4965                            return true;
4966                        }
4967                    } else {
4968                        return true;
4969                    }
4970                }
4971            } else {
4972                if (soundManager.debugMode) console.log('ttsAdvance: ttsNextChunks is null');
4973                return false;
4974            }
4975        }
4976    }
4977
4978    return true;
4979}
4980
4981// ttsPrefetchAudio()
4982//______________________________________________________________________________
4983BookReader.prototype.ttsPrefetchAudio = function () {
4984
4985    if(false != this.ttsBuffering) {
4986        alert('TTS Error: prefetch() called while content still buffering!');
4987        return;
4988    }
4989
4990    //preload next chunk
4991    var nextPos = this.ttsPosition+1;
4992    if (nextPos < this.ttsChunks.length) {
4993        this.ttsLoadChunk(this.ttsIndex, nextPos, this.ttsChunks[nextPos][0]);
4994    } else {
4995        //for a short page, preload might nt have yet returned..
4996        if (soundManager.debugMode) console.log('preloading chunk 0 from next page, index='+(this.ttsIndex+1));
4997        if (null != this.ttsNextChunks) {
4998            if (0 != this.ttsNextChunks.length) {
4999                this.ttsLoadChunk(this.ttsIndex+1, 0, this.ttsNextChunks[0][0]);
5000            } else {
5001                if (soundManager.debugMode) console.log('prefetchAudio(): ttsNextChunks is zero length!');
5002            }
5003        } else {
5004            if (soundManager.debugMode) console.log('ttsNextChunks is null, not preloading next page');
5005            this.ttsBuffering = true;
5006        }
5007    }
5008
5009}
5010
5011// ttsPlay()
5012//______________________________________________________________________________
5013BookReader.prototype.ttsPlay = function () {
5014
5015    var chunk = this.ttsChunks[this.ttsPosition];
5016    if (soundManager.debugMode) {
5017        console.log('ttsPlay position = ' + this.ttsPosition);
5018        console.log('chunk = ' + chunk);
5019        console.log(this.ttsChunks);
5020    }
5021
5022    //add new hilights
5023    if (2 == this.mode) {
5024        this.ttsHilite2UP(chunk);
5025    } else {
5026        this.ttsHilite1UP(chunk);
5027    }
5028
5029    this.ttsScrollToChunk(chunk);
5030
5031    //play current chunk
5032    if (false == this.ttsBuffering) {
5033        soundManager.play('chunk'+this.ttsIndex+'-'+this.ttsPosition,{onfinish:function(){br.ttsNextChunk();}});
5034    } else {
5035        soundManager.play('chunk'+this.ttsIndex+'-'+this.ttsPosition,{onfinish:function(){br.ttsStartPolling();}});
5036    }
5037}
5038
5039// scrollToChunk()
5040//______________________________________________________________________________
5041BookReader.prototype.ttsScrollToChunk = function(chunk) {
5042    if (this.constMode1up != this.mode) return;
5043
5044    var leafTop = 0;
5045    var h;
5046    var i;
5047    for (i=0; i<this.ttsIndex; i++) {
5048        h = parseInt(this._getPageHeight(i)/this.reduce);
5049        leafTop += h + this.padding;
5050    }
5051
5052    var chunkTop = chunk[1][3]; //coords are in l,b,r,t order
5053    var chunkBot = chunk[chunk.length-1][1];
5054
5055    var topOfFirstChunk = leafTop + chunkTop/this.reduce;
5056    var botOfLastChunk  = leafTop + chunkBot/this.reduce;
5057
5058    if (soundManager.debugMode) console.log('leafTop = ' + leafTop + ' topOfFirstChunk = ' + topOfFirstChunk + ' botOfLastChunk = ' + botOfLastChunk);
5059
5060    var containerTop = $('#BRcontainer').attr('scrollTop');
5061    var containerBot = containerTop + $('#BRcontainer').height();
5062    if (soundManager.debugMode) console.log('containerTop = ' + containerTop + ' containerBot = ' + containerBot);
5063
5064    if ((topOfFirstChunk < containerTop) || (botOfLastChunk > containerBot)) {
5065        //jumpToIndex scrolls so that chunkTop is centered.. we want chunkTop at the top
5066        //this.jumpToIndex(this.ttsIndex, null, chunkTop);
5067        $('#BRcontainer').animate({scrollTop: topOfFirstChunk},'fast');
5068    }
5069}
5070
5071// ttsHilite1UP()
5072//______________________________________________________________________________
5073BookReader.prototype.ttsHilite1UP = function(chunk) {
5074    var i;
5075    for (i=1; i<chunk.length; i++) {
5076        //each rect is an array of l,b,r,t coords (djvu.xml ordering...)
5077        var l = chunk[i][0];
5078        var b = chunk[i][1];
5079        var r = chunk[i][2];
5080        var t = chunk[i][3];
5081
5082        var div = document.createElement('div');
5083        this.ttsHilites.push(div);
5084        $(div).attr('className', 'BookReaderSearchHilite').appendTo('#pagediv'+this.ttsIndex);
5085
5086        $(div).css({
5087            width:  (r-l)/this.reduce + 'px',
5088            height: (b-t)/this.reduce + 'px',
5089            left:   l/this.reduce + 'px',
5090            top:    t/this.reduce +'px'
5091        });
5092    }
5093
5094}
5095
5096// ttsHilite2UP()
5097//______________________________________________________________________________
5098BookReader.prototype.ttsHilite2UP = function (chunk) {
5099    var i;
5100    for (i=1; i<chunk.length; i++) {
5101        //each rect is an array of l,b,r,t coords (djvu.xml ordering...)
5102        var l = chunk[i][0];
5103        var b = chunk[i][1];
5104        var r = chunk[i][2];
5105        var t = chunk[i][3];
5106
5107        var div = document.createElement('div');
5108        this.ttsHilites.push(div);
5109        $(div).attr('className', 'BookReaderSearchHilite').css('zIndex', 3).appendTo('#BRtwopageview');
5110        this.setHilightCss2UP(div, this.ttsIndex, l, r, t, b);
5111    }
5112}
5113
5114// ttsRemoveHilites()
5115//______________________________________________________________________________
5116BookReader.prototype.ttsRemoveHilites = function (chunk) {
5117    $(this.ttsHilites).remove();
5118    this.ttsHilites = [];
5119}
5120
5121// ttsStartPolling()
5122//______________________________________________________________________________
5123// Play of the current chunk has ended, but the next chunk has not yet been loaded.
5124// We need to wait for the text for the next page to be loaded, so we can
5125// load the next audio chunk
5126BookReader.prototype.ttsStartPolling = function () {
5127    if (soundManager.debugMode) console.log('Starting the TTS poller...');
5128    var self = this;
5129    this.ttsPoller=setInterval(function(){
5130        if (self.ttsBuffering) {return;}
5131
5132        if (soundManager.debugMode) console.log('TTS buffering finished!');
5133        clearInterval(self.ttsPoller);
5134        self.ttsPoller = null;
5135        self.ttsPrefetchAudio();
5136        self.ttsNextChunk();
5137    },500);
5138}
5139
5140BookReader.prototype.buildShareDiv = function(jShareDiv)
5141{
5142    var pageView = document.location + '';
5143    var bookView = (pageView + '').replace(/#.*/,'');
5144    var self = this;
5145
5146    var jForm = $([
5147        '<p>Copy and paste one of these options to share this book elsewhere.</p>',
5148        '<form method="post" action="">',
5149            '<fieldset>',
5150                '<label for="pageview">Link to this page view:</label>',
5151                '<input type="text" name="pageview" id="pageview" value="' + pageView + '"/>',
5152            '</fieldset>',
5153            '<fieldset>',
5154                '<label for="booklink">Link to the book:</label>',
5155                '<input type="text" name="booklink" id="booklink" value="' + bookView + '"/>',
5156            '</fieldset>',
5157            '<fieldset>',
5158                '<label for="iframe">Embed a mini Book Reader:</label>',
5159                '<fieldset class="sub">',
5160                    '<label class="sub">',
5161                        '<input type="radio" name="pages" value="' + this.constMode1up + '" checked="checked"/>',
5162                        '1 page',
5163                    '</label>',
5164                    '<label class="sub">',
5165                        '<input type="radio" name="pages" value="' + this.constMode2up + '"/>',
5166                        '2 pages',
5167                    '</label>',
5168                    '<label class="sub">',
5169                        '<input type="checkbox" name="thispage" value="thispage"/>',
5170                        'Open to this page?',
5171                    '</label>',
5172                '</fieldset>',
5173                '<textarea cols="30" rows="4" name="iframe" class="BRframeEmbed"></textarea>',
5174            '</fieldset>',
5175            '<fieldset class="center">',
5176                '<button type="button" onclick="$.fn.colorbox.close();">Finished</button>',
5177            '</fieldset>',
5178        '</form>'].join('\n'));
5179
5180    jForm.appendTo(jShareDiv);
5181
5182    jForm.find('input').bind('change', function() {
5183        var form = $(this).parents('form:first');
5184        var params = {};
5185        params.mode = $(form.find('input[name=pages]:checked')).val();
5186        if (form.find('input[name=thispage]').attr('checked')) {
5187            params.page = self.getPageNum(self.currentIndex());
5188        }
5189
5190        // $$$ changeable width/height to be added to share UI
5191        var frameWidth = "480px";
5192        var frameHeight = "430px";
5193        form.find('.BRframeEmbed').val(self.getEmbedCode(frameWidth, frameHeight, params));
5194    })
5195    jForm.find('input[name=thispage]').trigger('change');
5196    jForm.find('input, textarea').bind('focus', function() {
5197        this.select();
5198    });
5199
5200    jForm.appendTo(jShareDiv);
5201    jForm = ''; // closure
5202
5203}
5204
5205// Should be overridden
5206BookReader.prototype.buildInfoDiv = function(jInfoDiv)
5207{
5208    jInfoDiv.find('.BRfloatTitle a').attr({'href': this.bookUrl, 'alt': this.bookTitle}).text(this.bookTitle);
5209}
5210
5211// Can be overriden
5212BookReader.prototype.initUIStrings = function()
5213{
5214    // Navigation handlers will be bound after all UI is in place -- makes moving icons between
5215    // the toolbar and nav bar easier
5216
5217    // Setup tooltips -- later we could load these from a file for i18n
5218    var titles = { '.logo': 'xraylibrary.ru', // $$$ update after getting OL record     /* заголовок тайтла в просмотрщике */
5219                   '.zoom_in': 'Zoom in',
5220                   '.zoom_out': 'Zoom out',
5221                   '.onepg': 'One-page view',
5222                   '.twopg': 'Two-page view',
5223                   '.thumb': 'Thumbnail view',
5224                   '.print': 'Print this page',
5225                   '.embed': 'Embed BookReader',
5226                   '.link': 'Link to this book (and page)',
5227                   '.bookmark': 'Bookmark this page',
5228                   '.read': 'Read this book aloud',
5229                   '.share': 'Share this book',
5230                   '.info': 'About this book',
5231                   '.full': 'Show fullscreen',
5232                   '.book_left': 'Flip left',
5233                   '.book_right': 'Flip right',
5234                   '.book_up': 'Page up',
5235                   '.book_down': 'Page down',
5236                   '.play': 'Play',
5237                   '.pause': 'Pause',
5238                   '.BRdn': 'Show/hide nav bar', // Would have to keep updating on state change to have just "Hide nav bar"
5239                   '.BRup': 'Show/hide nav bar',
5240                   '.book_top': 'First page',
5241                   '.book_bottom': 'Last page'
5242                  };
5243    if ('rl' == this.pageProgression) {
5244        titles['.book_leftmost'] = 'Last page';
5245        titles['.book_rightmost'] = 'First page';
5246    } else { // LTR
5247        titles['.book_leftmost'] = 'First page';
5248        titles['.book_rightmost'] = 'Last page';
5249    }
5250
5251    for (var icon in titles) {
5252        if (titles.hasOwnProperty(icon)) {
5253            $('#BookReader').find(icon).attr('title', titles[icon]);
5254        }
5255    }
5256}
5257})(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.