view src/main/webapp/imageServer/resources/js/vendor/diva-old.js @ 203:719475ad0923 iiif_diva

more work on new diva.js in imageServer
author casties
date Fri, 05 Jul 2019 16:05:57 +0200
parents src/main/webapp/imageServer/resources/js/diva.js@764f47286679
children
line wrap: on
line source

window.divaPlugins = [];

// this pattern was taken from http://www.virgentech.com/blog/2009/10/building-object-oriented-jquery-plugin.html
(function ($)
{
    var Diva = function (element, options)
    {
        // These are elements that can be overridden upon instantiation
        // See https://github.com/DDMAL/diva.js/wiki/Code-documentation for more details
        var defaults = {
            adaptivePadding: 0.05, // The ratio of padding to the page dimension
            blockMobileMove: true, // Prevent moving or scrolling the page on mobile devices
            contained: false, // Determines the location of the fullscreen icon
            objectData: '', // URL to the JSON file that provides the object dimension data - *MANDATORY*
            enableAutoHeight: false, // Automatically adjust height based on the window size
            enableAutoTitle: true, // Shows the title within a div of id diva-title
            enableAutoWidth: true, // Automatically adjust width based on the window size
            enableCanvas: true, // Used for the canvas plugin
            enableDownload: true, // Used for the download plugin
            enableFilename: true, // Uses filenames and not page numbers for links (i=bm_001.tif, not p=1)
            enableFullscreen: true, // Enable or disable fullscreen icon (mode still available)
            enableGotoPage: true, // A "go to page" jump box
            enableGridIcon: true, // A grid view of all the pages
            enableGridSlider: true, // Slider to control the pages per grid row
            enableKeyScroll: true, // Scrolling using the page up/down keys
            enableLinkIcon: true, // Controls the visibility of the link icon
            enableSpaceScroll: false, // Scrolling down by pressing the space key
            enableToolbar: true, // Enables the toolbar. Note that disabling this means you have to handle all controls yourself.
            enableZoomSlider: true, // Enable or disable the zoom slider (for zooming in and out)
            fixedPadding: 10, // Fallback if adaptive padding is set to 0
            fixedHeightGrid: true, // So each page in grid view has the same height (only widths differ)
            goDirectlyTo: 0, // Default initial page to show (0-indexed)
            iipServerURL: '', // The URL to the IIPImage installation, including the `?FIF=` - *MANDATORY*
            inFullscreen: false, // Set to true to load fullscreen mode initially
            inGrid: false, // Set to true to load grid view initially
            imageDir: '', // Image directory, either absolute path or relative to IIP's FILESYSTEM_PREFIX - *MANDATORY*
            maxPagesPerRow: 8, // Maximum number of pages per row, grid view
            maxZoomLevel: -1, // Optional; defaults to the max zoom returned in the JSON response
            minPagesPerRow: 2, // 2 for the spread view. Recommended to leave it
            minZoomLevel: 0, // Defaults to 0 (the minimum zoom)
            onDocumentLoaded: null, // Callback function for when the document is fully loaded
            onModeToggle: null, // Callback for toggling fullscreen mode
            onViewToggle: null, // Callback for switching between grid and document view
            onJump: null, // Callback function for jumping to a specific page (using the gotoPage feature)
            onPageLoad: null, // Callback function for loading pages
            onPageLoaded: null, // Callback function for after the page has been loaded
            onReady: null, // Callback function for initial load
            onScroll: null, // Callback function for scrolling
            onScrollDown: null, // Callback function for scrolling down, only
            onScrollUp: null, // Callback function for scrolling up only
            onSetCurrentPage: null, // Callback function for when the current page is set
            onZoom: null, // Callback function for zooming in general
            onZoomIn: null, // Callback function for zooming in only
            onZoomOut: null, // Callback function for zooming out only
            pageLoadTimeout: 200, // Number of milliseconds to wait before loading pages
            pagesPerRow: 5, // The default number of pages per row in grid view
            rowLoadTimeout: 50, // Number of milliseconds to wait before loading a row
            throbberTimeout: 100, // Number of milliseconds to wait before showing throbber
            tileHeight: 256, // The height of each tile, in pixels; usually 256
            tileWidth: 256, // The width of each tile, in pixels; usually 256
            toolbarParentSelector: null, // The toolbar parent selector. If null, it defaults to the primary diva element. Must be a jQuery selector (leading '#')
            viewerHeightPadding: 15, // Vertical padding when resizing the viewer, if enableAutoHeight is set
            viewerWidthPadding: 30, // Horizontal padding when resizing the viewer, if enableAutoHeight is set
            viewportMargin: 200, // Pretend tiles +/- 200px away from viewport are in
            zoomLevel: 2 // The initial zoom level (used to store the current zoom level)
        };

        // Apply the defaults, or override them with passed-in options.
        var settings = $.extend({}, defaults, options);

        // Things that cannot be changed because of the way they are used by the script
        // Many of these are declared with arbitrary values that are changed later on
        var globals = {
            allTilesLoaded: [], // A boolean for each page, indicating if all tiles have been loaded
            averageHeights: [], // The average page height for each zoom level
            averageWidths: [], // The average page width for each zoom level
            currentPageIndex: 0, // The current page in the viewport (center-most page)
            dimAfterZoom: 0, // Used for storing the item dimensions after zooming
            firstPageLoaded: -1, // The ID of the first page loaded (value set later)
            firstRowLoaded: -1, // The index of the first row loaded
            gridPageWidth: 0, // Holds the max width of each row in grid view. Calculated in loadGrid()
            hashParamSuffix: '', // Used when there are multiple document viewers on a page
            heightAbovePages: [], // The height above each page at the current zoom level
            horizontalOffset: 0, // Used in documentScroll for scrolling more precisely
            horizontalPadding: 0, // Either the fixed padding or adaptive padding
            ID: null, // The prefix of the IDs of the elements (usually 1-diva-)
            innerSelector: '', // settings.selector + 'inner', for selecting the .diva-inner element
            itemTitle: '', // The title of the document
            lastPageLoaded: -1, // The ID of the last page loaded (value set later)
            lastRowLoaded: -1, // The index of the last row loaded
            leftScrollSoFar: 0, // Current scroll from the left edge of the pane
            loaded: false, // A flag for when everything is loaded and ready to go.
            maxWidths: [], // The width of the widest page for each zoom level
            maxRatio: 0, // The max height/width ratio (for grid view)
            minHeight: 0, // Minimum height of the .diva-outer element, as defined in the CSS
            minRatio: 0, // The minimum height/width ratio for a page
            minWidth: 0, // Minimum width of the .diva-outer element, as defined in the CSS
            mobileWebkit: false, // Checks if the user is on a touch device (iPad/iPod/iPhone/Android)
            numPages: 0, // Number of pages in the array
            numRows: 0, // Number of rows
            oldPagesPerRow: 0, // Holds the previous number of pages per row after it is changed
            oldZoomLevel: -1, // Holds the previous zoom level after zooming in or out
            orientationChange: false, // For handling device orientation changes for touch devices
            originalHeight: 0, // Stores the original height of the .diva-outer element
            originalWidth: 0, // Stores the original width of the .diva-outer element
            outerSelector: '', // settings.selector + 'outer', for selecting the .diva-outer element
            pages: [], // An array containing the data for all the pages
            pageLeftOffsets: [], // Offset from the left side of the pane to the edge of the page
            pageTimeouts: [], // Stack to hold the loadPage timeouts
            pageTools: '', // The string for page tools
            panelHeight: 0, // Height of the document viewer pane
            panelWidth: 0, // Width of the document viewer pane
            plugins: [], // Filled with the enabled plugins from window.divaPlugins
            previousTopScroll: 0, // Used to determine vertical scroll direction
            preZoomOffset: null, // Holds the offset prior to zooming when double-clicking
            realMaxZoom: -1, // To hold the true max zoom level of the document (needed for calculations)
            resizeTimer: -1, // Holds the ID of the timeout used when resizing the window (for clearing)
            rowHeight: 0, // Holds the max height of each row in grid view. Calculated in loadGrid()
            scaleWait: false, // For preventing double-zoom on touch devices (iPad, etc)
            selector: '', // Uses the generated ID prefix to easily select elements
            singleClick: false, // Used for catching ctrl+double-click events in Firefox in Mac OS
            scrollbarWidth: 0, // Set to the actual scrollbar width in init()
            throbberTimeoutID: -1, // Holds the ID of the throbber loading timeout
            toolbar: null, // Holds an object with some toolbar-related functions
            topScrollSoFar: 0, // Holds the number of pixels of vertical scroll
            totalHeights: [], // The total height of all pages (stacked together) for each zoom level
            totalHeight: 0, // The total height for the current zoom level (including padding)
            verticalOffset: 0, // See horizontalOffset
            verticalPadding: 0, // Either the fixed padding or adaptive padding
            viewerXOffset: 0, // Distance between left edge of viewer and document left edge
            viewerYOffset: 0 // Like viewerXOffset but for the top edges
        };

        $.extend(settings, globals);

        // Executes a callback function with the diva instance set as the context
        // Can take an unlimited number to arguments to pass to the callback function
        var self = this;

        var executeCallback = function (callback)
        {
            var args, i, length;

            if (typeof callback === "function")
            {
                args = [];
                for (i = 1, length = arguments.length; i < length; i++)
                {
                    args.push(arguments[i]);
                }

                callback.apply(self, args);

                return true;
            }

            return false;
        };

        var getPageData = function (pageIndex, attribute)
        {
            return settings.pages[pageIndex].d[settings.zoomLevel][attribute];
        };

        // Returns the page index associated with the given filename; must called after settings settings.pages
        var getPageIndex = function (filename)
        {
            var i,
                np = settings.numPages;

            for (i = 0; i < np; i++)
            {
                if (settings.pages[i].f === filename)
                {
                    return i;
                }
            }

            return -1;
        };

        // Checks if a tile is within the viewport horizontally
        var isHorizontallyInViewport = function (left, right)
        {
            var panelWidth = settings.panelWidth;
            var leftOfViewport = settings.leftScrollSoFar - settings.viewportMargin;
            var rightOfViewport = leftOfViewport + panelWidth + settings.viewportMargin * 2;

            var leftVisible = left >= leftOfViewport && left <= rightOfViewport;
            var rightVisible = right >= leftOfViewport && right <= rightOfViewport;
            var middleVisible = left <= leftOfViewport && right >= rightOfViewport;

            return (leftVisible || middleVisible || rightVisible);
        };

        // Checks if a page or tile is within the viewport vertically
        var isVerticallyInViewport = function (top, bottom)
        {
            var panelHeight = settings.panelHeight;
            var topOfViewport = settings.topScrollSoFar - settings.viewportMargin;
            var bottomOfViewport = topOfViewport + panelHeight + settings.viewportMargin * 2;

            var topVisible = top >= topOfViewport && top <= bottomOfViewport;
            var middleVisible = top <= topOfViewport && bottom >= bottomOfViewport;
            var bottomVisible = bottom >= topOfViewport && bottom <= bottomOfViewport;

            return (topVisible || middleVisible || bottomVisible);
        };

        // Check if a tile is near the viewport and thus should be loaded
        var isTileVisible = function (pageIndex, tileRow, tileCol)
        {
            var tileTop = settings.heightAbovePages[pageIndex] + (tileRow * settings.tileHeight) + settings.verticalPadding;
            var tileBottom = tileTop + settings.tileHeight;
            var tileLeft = settings.pageLeftOffsets[pageIndex] + (tileCol * settings.tileWidth);
            var tileRight = tileLeft + settings.tileWidth;

            return isVerticallyInViewport(tileTop, tileBottom) && isHorizontallyInViewport(tileLeft, tileRight);
        };

        // Check if a tile has been appended to the DOM
        var isTileLoaded = function (pageIndex, tileIndex)
        {
            return document.getElementById(settings.ID + 'tile-' + pageIndex + '-' + tileIndex) === false;
        };

        // Check if a page index is valid
        var isPageValid = function (pageIndex)
        {
            return pageIndex >= 0 && pageIndex < settings.numPages;
        };

        // Check if a page is in or near the viewport and thus should be loaded
        var isPageVisible = function (pageIndex)
        {
            var topOfPage = settings.heightAbovePages[pageIndex];
            var bottomOfPage = topOfPage + getPageData(pageIndex, 'h') + settings.verticalPadding;

            return isVerticallyInViewport(topOfPage, bottomOfPage);
        };

        // Check if a page has been appended to the DOM
        var isPageLoaded = function (pageIndex)
        {
            return $(document.getElementById(settings.ID + 'page-' + pageIndex)).length > 0;
        };

        // Appends the page directly into the document body, or loads the relevant tiles
        var loadPage = function (pageIndex)
        {
            // If the page and all of its tiles have been loaded, exit
            if (isPageLoaded(pageIndex) && settings.allTilesLoaded[pageIndex])
            {
                return;
            }

            // Load some data for this page
            var filename = settings.pages[pageIndex].f;
            var width = getPageData(pageIndex, 'w');
            var height = getPageData(pageIndex, 'h');
            var heightFromTop = settings.heightAbovePages[pageIndex] + settings.verticalPadding;
            var pageSelector = settings.selector + 'page-' + pageIndex;
            var plugin;

            // If the page has not been loaded yet, append the div to the DOM
            if (!isPageLoaded(pageIndex))
            {
                $(document.getElementById(settings.ID + "inner")).append('<div id="' + settings.ID + 'page-' + pageIndex + '" style="top: ' + heightFromTop + 'px; width: ' + width + 'px; height: ' + height + 'px;" class="diva-document-page" title="Page ' + (pageIndex + 1) + '" data-index="' + pageIndex + '" data-filename="' + filename + '">' + settings.pageTools + '</div>');

                // Call the callback function
                executeCallback(settings.onPageLoad, pageIndex, filename, pageSelector);
                Events.publish("PageHasLoaded", [pageIndex, filename, pageSelector]);

                // @TODO: Replace this with a notification.
                // Execute the callback functions for any of the enabled plugins
                for (plugin in settings.plugins) {
                    executeCallback(settings.plugins[plugin].onPageLoad, pageIndex, filename, pageSelector);
                }
            }

            // There are still tiles to load, so try to load those (after a delay)
            settings.pageTimeouts.push(setTimeout(function ()
            {
                // If the page is no longer in the viewport, don't load any tiles
                if (!isPageVisible(pageIndex))
                {
                    return;
                }

                var imdir = settings.imageDir + "/";
                // Load some more data and initialise some variables
                var rows = getPageData(pageIndex, 'r');
                var cols = getPageData(pageIndex, 'c');
                var maxZoom = settings.pages[pageIndex].m;
                var baseURL = settings.iipServerURL + "?FIF=" + imdir + filename + '&JTL=';
                var content = [];
                var allTilesLoaded = true;
                var tileIndex = 0;
                var i;

                // Calculate the width and height of outer tiles (non-standard dimensions)
                var lastHeight = height - (rows - 1) * settings.tileHeight;
                var lastWidth = width - (cols - 1) * settings.tileWidth;

                // Declare variables used within the loops
                var row, col, tileHeight, tileWidth, top, left, displayStyle, zoomLevel, imageURL;

                // Adjust the zoom level based on the max zoom level of the page
                zoomLevel = settings.zoomLevel + maxZoom - settings.realMaxZoom;
                baseImageURL = baseURL + zoomLevel + ',';

                // Loop through all the tiles in this page
                row = 0;
                while (row < rows)
                {
                    col = 0;
                    while (col < cols)
                    {
                        top = row * settings.tileHeight;
                        left = col * settings.tileWidth;

                        // If the tile is in the last row or column, its dimensions will be different
                        tileHeight = (row === rows - 1) ? lastHeight : settings.tileHeight;
                        tileWidth = (col === cols - 1) ? lastWidth : settings.tileWidth;

                        imageURL = baseImageURL + tileIndex;

                        // this check looks to see if the tile is already loaded, and then if
                        // it isn't, if it should be visible.
                        if (!isTileLoaded(pageIndex, tileIndex)) {
                            if (isTileVisible(pageIndex, row, col)) {
                                content.push('<div id="' + settings.ID + 'tile-' + pageIndex + '-' + tileIndex + '" style="display:inline; position: absolute; top: ' + top + 'px; left: ' + left + 'px; background-image: url(\'' + imageURL + '\'); height: ' + tileHeight + 'px; width: ' + tileWidth + 'px;"></div>');
                            } else {
                                // The tile does not need to be loaded - not all have been loaded
                                allTilesLoaded = false;
                            }
                        }
                        tileIndex++;
                        col++;
                    }
                    row++;
                }

                settings.allTilesLoaded[pageIndex] = allTilesLoaded;
                $(document.getElementById(settings.ID + 'page-' + pageIndex)).append(content.join(''));

                executeCallback(settings.onPageLoaded, pageIndex, filename, pageSelector);

            }, settings.pageLoadTimeout));
        };

        // Delete a page from the DOM; will occur when a page is scrolled out of the viewport
        var deletePage = function (pageIndex)
        {
            $(document.getElementById(settings.ID + 'page-' + pageIndex)).empty().remove();
        };

        // Check if the bottom of a page is above the top of a viewport (scrolling down)
        // For when you want to keep looping but don't want to load a specific page
        var pageAboveViewport = function (pageIndex)
        {
            var bottomOfPage = settings.heightAbovePages[pageIndex] + getPageData(pageIndex, 'h') + settings.verticalPadding;
            var topOfViewport = settings.topScrollSoFar;

            return bottomOfPage < topOfViewport;
        };

        // Check if the top of a page is below the bottom of a viewport (scrolling up)
        var pageBelowViewport = function (pageIndex)
        {
            var topOfPage = settings.heightAbovePages[pageIndex];
            var bottomOfViewport = settings.topScrollSoFar + settings.panelHeight;

            return topOfPage > bottomOfViewport;
        };

        // Called by adjust pages - determine what pages should be visible, and show them
        var attemptPageShow = function (pageIndex, direction)
        {
            if (direction > 0)
            {
                // Direction is positive - we're scrolling down
                if (isPageValid(pageIndex))
                {
                    // If the page should be visible, then yes, add it
                    if (isPageVisible(pageIndex))
                    {
                        loadPage(pageIndex);

                        settings.lastPageLoaded = pageIndex;

                        // Recursively call this function until there's nothing to add
                        attemptPageShow(settings.lastPageLoaded + 1, direction);
                    }
                    else if (pageAboveViewport(pageIndex))
                    {
                        // If the page is below the viewport. try to load the next one
                        attemptPageShow(pageIndex + 1, direction);
                    }
                }
            }
            else
            {
                // Direction is negative - we're scrolling up
                if (isPageValid(pageIndex))
                {
                    // If it's near the viewport, yes, add it
                    if (isPageVisible(pageIndex))
                    {
                        loadPage(pageIndex);

                        // Reset the first page loaded to this one
                        settings.firstPageLoaded = pageIndex;

                        // Recursively call this function until there's nothing to add
                        attemptPageShow(settings.firstPageLoaded - 1, direction);
                    }
                    else if (pageBelowViewport(pageIndex))
                    {
                        // Attempt to call this on the next page, do not increment anything
                        attemptPageShow(pageIndex - 1, direction);
                    }
                }
            }
        };

        // Called by adjustPages - see what pages need to be hidden, and hide them
        var attemptPageHide = function (pageIndex, direction)
        {
            if (direction > 0)
            {
                // Scrolling down - see if this page needs to be deleted from the DOM
                if (isPageValid(pageIndex) && pageAboveViewport(pageIndex))
                {
                    // Yes, delete it, reset the first page loaded
                    deletePage(pageIndex);
                    settings.firstPageLoaded = pageIndex + 1;

                    // Try to call this function recursively until there's nothing to delete
                    attemptPageHide(settings.firstPageLoaded, direction);
                }
            }
            else
            {
                // Direction must be negative (not 0 - see adjustPages), we're scrolling up
                if (isPageValid(pageIndex) && pageBelowViewport(pageIndex))
                {
                    // Yes, delete it, reset the last page loaded
                    deletePage(pageIndex);
                    settings.lastPageLoaded = pageIndex - 1;

                    // Try to call this function recursively until there's nothing to delete
                    attemptPageHide(settings.lastPageLoaded, direction);
                }
            }
        };

        // Handles showing and hiding pages when the user scrolls
        var adjustPages = function (direction)
        {
            var i;

            // Direction is negative, so we're scrolling up
            if (direction < 0)
            {
                attemptPageShow(settings.firstPageLoaded, direction);
                setCurrentPage(-1);
                attemptPageHide(settings.lastPageLoaded, direction);
            }
            else if (direction > 0)
            {
                // Direction is positive so we're scrolling down
                attemptPageShow(settings.lastPageLoaded, direction);
                setCurrentPage(1);
                attemptPageHide(settings.firstPageLoaded, direction);
            }
            else
            {
                // Horizontal scroll, check if we need to reveal any tiles
                var lpl = settings.lastPageLoaded;
                for (i = Math.max(settings.firstPageLoaded, 0); i <= lpl; i++)
                {
                    if (isPageVisible(i))
                    {
                        loadPage(i);
                    }
                }
            }

            executeCallback(settings.onScroll, settings.topScrollSoFar);

            // If we're scrolling down
            if (direction > 0)
            {
                executeCallback(settings.onScrollDown, settings.topScrollSoFar);
            }
            else if (direction < 0)
            {
                // We're scrolling up
                executeCallback(settings.onScrollUp, settings.topScrollSoFar);
            }
        };

        // Check if a row index is valid
        var isRowValid = function (rowIndex)
        {
            return rowIndex >= 0 && rowIndex < settings.numRows;
        };

        // Check if a row should be visible in the viewport
        var isRowVisible = function (rowIndex)
        {
            var topOfRow = settings.rowHeight * rowIndex;
            var bottomOfRow = topOfRow + settings.rowHeight + settings.fixedPadding;

            return isVerticallyInViewport(topOfRow, bottomOfRow);
        };

        // Check if a row (in grid view) is present in the DOM
        var isRowLoaded = function (rowIndex)
        {
            return $(settings.selector + 'row-' + rowIndex).length > 0;
        };

        var loadRow = function (rowIndex)
        {
            // If the row has already been loaded, don't attempt to load it again
            if (isRowLoaded(rowIndex))
            {
                return;
            }

            // Load some data for this and initialise some variables
            var heightFromTop = (settings.rowHeight * rowIndex) + settings.fixedPadding;
            var content = [];

            // Create the opening tag for the row div
            content.push('<div class="diva-row" id="' + settings.ID + 'row-' + rowIndex + '" style="height: ' + settings.rowHeight + '; top: ' + heightFromTop + 'px;">');

            // Declare variables used in the loop
            var i, pageIndex, filename, realWidth, realHeight, pageWidth, pageHeight, leftOffset, imageURL;
            var imdir = settings.imageDir + "/";

            // Load each page within that row
            var ppr = settings.pagesPerRow;
            for (i = 0; i < ppr; i++)
            {
                pageIndex = rowIndex * settings.pagesPerRow + i;

                // If this page is the last row, don't try to load a nonexistent page
                if (!isPageValid(pageIndex))
                {
                    break;
                }

                // Calculate the width, height and horizontal placement of this page
                filename = settings.pages[pageIndex].f;
                realWidth = getPageData(pageIndex, 'w');
                realHeight = getPageData(pageIndex, 'h');
                pageWidth = (settings.fixedHeightGrid) ? (settings.rowHeight - settings.fixedPadding) * realWidth / realHeight : settings.gridPageWidth;
                pageHeight = (settings.fixedHeightGrid) ? settings.rowHeight - settings.fixedPadding : pageWidth / realWidth * realHeight;
                leftOffset = parseInt(i * (settings.fixedPadding + settings.gridPageWidth) + settings.fixedPadding, 10);

                // Make sure they're all integers for nice, round numbers
                pageWidth = parseInt(pageWidth, 10);
                pageHeight = parseInt(pageHeight, 10);

                // Center the page if the height is fixed (otherwise, there is no horizontal padding)
                leftOffset += (settings.fixedHeightGrid) ? (settings.gridPageWidth - pageWidth) / 2 : 0;
                imageURL = settings.iipServerURL + "?FIF=" + imdir + filename + '&amp;HEI=' + (pageHeight + 2) + '&amp;CVT=JPEG';

                // Append the HTML for this page to the string builder array
                content.push('<div id="' + settings.ID + 'page-' + pageIndex + '" class="diva-page" style="width: ' + pageWidth + 'px; height: ' + pageHeight + 'px; left: ' + leftOffset + 'px;" title="Page ' + (pageIndex + 1) + '"></div>');

                // Add each image to a queue so that images aren't loaded unnecessarily
                addPageToQueue(rowIndex, pageIndex, imageURL, pageWidth, pageHeight);
            }

            // Append this row to the DOM
            content.push('</div>');
            $(document.getElementById(settings.ID + "inner")).append(content.join(''));
        };

        var deleteRow = function (rowIndex)
        {
            $(document.getElementById(settings.ID + 'row-' + rowIndex)).empty().remove();
        };

        // Check if the bottom of a row is above the top of the viewport (scrolling down)
        var rowAboveViewport = function (rowIndex)
        {
            var bottomOfRow = settings.rowHeight * (rowIndex + 1);
            var topOfViewport = settings.topScrollSoFar;

            return (bottomOfRow < topOfViewport);
        };

        // Check if the top of a row is below the bottom of the viewport (scrolling up)
        var rowBelowViewport = function (rowIndex)
        {
            var topOfRow = settings.rowHeight * rowIndex;
            var bottomOfViewport = settings.topScrollSoFar + settings.panelHeight;

            return (topOfRow > bottomOfViewport);
        };

        // Same thing as attemptPageShow only with rows
        var attemptRowShow = function (rowIndex, direction)
        {
            if (direction > 0)
            {
                if (isRowValid(rowIndex))
                {
                    if (isRowVisible(rowIndex))
                    {
                        loadRow(rowIndex);
                        settings.lastRowLoaded = rowIndex;

                        attemptRowShow(settings.lastRowLoaded + 1, direction);
                    }
                    else if (rowAboveViewport(rowIndex))
                    {
                        attemptRowShow(rowIndex + 1, direction);
                    }
                }
            }
            else
            {
                if (isRowValid(rowIndex))
                {
                    if (isRowVisible(rowIndex))
                    {
                        loadRow(rowIndex);
                        settings.firstRowLoaded = rowIndex;

                        attemptRowShow(settings.firstRowLoaded - 1, direction);
                    }
                    else if (rowBelowViewport(rowIndex))
                    {
                        attemptRowShow(rowIndex - 1, direction);
                    }
                }
            }
        };

        var attemptRowHide = function (rowIndex, direction)
        {
            if (direction > 0)
            {
                if (isRowValid(rowIndex) && rowAboveViewport(rowIndex))
                {
                    deleteRow(rowIndex);
                    settings.firstRowLoaded++;

                    attemptRowHide(settings.firstRowLoaded, direction);
                }
            }
            else
            {
                if (isRowValid(rowIndex) && rowBelowViewport(rowIndex))
                {
                    deleteRow(rowIndex);
                    settings.lastRowLoaded--;

                    attemptRowHide(settings.lastRowLoaded, direction);
                }
            }
        };

        var adjustRows = function (direction)
        {
            if (direction < 0)
            {
                attemptRowShow(settings.firstRowLoaded, -1);
                setCurrentRow(-1);
                attemptRowHide(settings.lastRowLoaded, -1);
            }
            else if (direction > 0)
            {
                attemptRowShow(settings.lastRowLoaded, 1);
                setCurrentRow(1);
                attemptRowHide(settings.firstRowLoaded, 1);
            }

            executeCallback(settings.onScroll, settings.topScrollSoFar);

            // If we're scrolling down
            if (direction > 0)
            {
                executeCallback(settings.onScrollDown, settings.topScrollSoFar);
            }
            else if (direction < 0)
            {
                // We're scrolling up
                executeCallback(settings.onScrollUp, settings.topScrollSoFar);
            }
        };

        // Used to delay loading of page images in grid view to prevent unnecessary loads
        var addPageToQueue = function (rowIndex, pageIndex, imageURL, pageWidth, pageHeight)
        {
            settings.pageTimeouts.push(setTimeout(function ()
            {
                if (isRowVisible(rowIndex))
                {
                    $(settings.selector + 'page-' + pageIndex).html('<img src="' + imageURL + '" style="width: ' + pageWidth + 'px; height: ' + pageHeight + 'px;" />');
                }
            }, settings.rowLoadTimeout));
        };

        // Determines and sets the "current page" (settings.currentPageIndex); called within adjustPages
        // The "direction" is either 1 (downward scroll) or -1 (upward scroll)
        var setCurrentPage = function (direction)
        {
            var middleOfViewport = settings.topScrollSoFar + (settings.panelHeight / 2);
            var currentPage = settings.currentPageIndex;
            var pageToConsider = settings.currentPageIndex + direction;
            var changeCurrentPage = false;
            var pageSelector = settings.selector + 'page-' + pageToConsider;

            // When scrolling up:
            if (direction < 0)
            {
                // If the previous page > middle of viewport
                if (pageToConsider >= 0 && (settings.heightAbovePages[pageToConsider] + getPageData(pageToConsider, 'h') + (settings.verticalPadding) >= middleOfViewport))
                {
                    changeCurrentPage = true;
                }
            }
            else if (direction > 0)
            {
                // When scrolling down:
                // If this page < middle of viewport
                if (settings.heightAbovePages[currentPage] + getPageData(currentPage, 'h') + settings.verticalPadding < middleOfViewport)
                {
                    changeCurrentPage = true;
                }
            }

            if (changeCurrentPage)
            {
                // Set this to the current page
                settings.currentPageIndex = pageToConsider;
                // Now try to change the next page, given that we're not going to a specific page
                // Calls itself recursively - this way we accurately obtain the current page
                if (direction !== 0)
                {
                    if (!setCurrentPage(direction))
                    {
                        var filename = settings.pages[pageToConsider].f;
                        executeCallback(settings.onSetCurrentPage, pageToConsider, filename);
                        Events.publish("VisiblePageDidChange", [pageToConsider, filename]);
                    }
                }
                return true;
            }

            return false;
        };

        // Sets the current page in grid view
        var setCurrentRow = function (direction)
        {
            var currentRow = Math.floor(settings.currentPageIndex / settings.pagesPerRow);
            var rowToConsider = currentRow + parseInt(direction, 10);
            var middleOfViewport = settings.topScrollSoFar + (settings.panelHeight / 2);
            var changeCurrentRow = false;

            if (direction < 0)
            {
                if (rowToConsider >= 0 && (settings.rowHeight * currentRow >= middleOfViewport || settings.rowHeight * rowToConsider >= settings.topScrollSoFar))
                {
                    changeCurrentRow = true;
                }
            }
            else if (direction > 0)
            {
                if ((settings.rowHeight * (currentRow + 1)) < settings.topScrollSoFar && isRowValid(rowToConsider))
                {
                    changeCurrentRow = true;
                }
            }

            if (changeCurrentRow)
            {
                settings.currentPageIndex = rowToConsider * settings.pagesPerRow;

                if (direction !== 0)
                {
                    if (!setCurrentRow(direction))
                    {
                        var pageIndex = settings.currentPageIndex;
                        var filename = settings.pages[pageIndex].f;
                        Events.publish("VisiblePageDidChange", [pageIndex, filename]);
                    }
                }

                return true;
            }

            return false;
        };

        // Helper function for going to a particular page
        // Vertical offset: from the top of the page (including the top padding)
        // Horizontal offset: from the center of the page; can be negative if to the left
        var gotoPage = function (pageIndex, verticalOffset, horizontalOffset)
        {
            verticalOffset = (typeof verticalOffset !== 'undefined') ? verticalOffset : 0;
            horizontalOffset = (typeof horizontalOffset !== 'undefined') ? horizontalOffset: 0;
            var desiredTop = settings.heightAbovePages[pageIndex] + verticalOffset;
            var desiredLeft = (settings.maxWidths[settings.zoomLevel] - settings.panelWidth) / 2 + settings.horizontalPadding + horizontalOffset;

            $(settings.outerSelector).scrollTop(desiredTop);
            $(settings.outerSelector).scrollLeft(desiredLeft);

            // Pretend that this is the current page
            settings.currentPageIndex = pageIndex;
            //settings.toolbar.updateCurrentPage();
            var filename = settings.pages[pageIndex].f;

            Events.publish("VisiblePageDidChange", [pageIndex, filename]);
            executeCallback(settings.onSetCurrentPage, pageIndex, filename);

            // Execute the onJump callback
            executeCallback(settings.onJump, pageIndex);
        };

        // Calculates the desired row, then scrolls there
        var gotoRow = function (pageIndex)
        {
            var desiredRow = Math.floor(pageIndex / settings.pagesPerRow);
            var desiredTop = desiredRow * settings.rowHeight;
            $(settings.outerSelector).scrollTop(desiredTop);

            // Pretend that this is the current page (it probably isn't)
            settings.currentPageIndex = pageIndex;
            var filename = settings.pages[pageIndex].f;
            Events.publish("VisiblePageDidChange", [pageIndex, filename]);
        };

        // Helper function called by loadDocument to scroll to the desired place
        var documentScroll = function ()
        {
            // If settings.preZoomOffset is defined, the zoom was trigged by double-clicking
            // We then zoom in on a specific region
            if (settings.preZoomOffset)
            {
                var clickedPage = settings.preZoomOffset.i;
                var heightAbovePage = settings.heightAbovePages[clickedPage] + settings.verticalPadding;
                var pageLeftOffset = settings.pageLeftOffsets[clickedPage];
                var zoomRatio = Math.pow(2, settings.zoomLevel - settings.oldZoomLevel);

                var distanceFromViewport = {
                    x: settings.preZoomOffset.originalX - settings.viewerXOffset,
                    y: settings.preZoomOffset.originalY - settings.viewerYOffset
                };

                var newDistanceToEdge = {
                    x: settings.preZoomOffset.x * zoomRatio,
                    y: settings.preZoomOffset.y * zoomRatio
                };

                var newScroll = {
                    x: newDistanceToEdge.x - distanceFromViewport.x + pageLeftOffset,
                    y: newDistanceToEdge.y - distanceFromViewport.y + heightAbovePage
                };

                $(settings.outerSelector).scrollTop(newScroll.y).scrollLeft(newScroll.x);

                settings.preZoomOffset = undefined;
            }
            else
            {
                // Otherwise, we just scroll to the page saved in settings.goDirectlyTo (must be valid)
                // Make sure the value for settings.goDirectlyTo is valid
                if (!isPageValid(settings.goDirectlyTo))
                {
                    settings.goDirectlyTo = 0;
                }

                // We use the stored y/x offsets (relative to the top of the page and the center, respectively)
                gotoPage(settings.goDirectlyTo, settings.verticalOffset, settings.horizontalOffset);
                settings.horizontalOffset = 0;
                settings.verticalOffset = 0;
            }
        };

        // Don't call this when not in grid mode please
        // Scrolls to the relevant place when in grid view
        var gridScroll = function ()
        {
            // Figure out and scroll to the row containing the current page
            gotoRow(settings.goDirectlyTo);
        };

        // If the given zoom level is valid, returns it; else, returns the min
        var getValidZoomLevel = function (zoomLevel)
        {
            return (zoomLevel >= settings.minZoomLevel && zoomLevel <= settings.maxZoomLevel) ? zoomLevel : settings.minZoomLevel;
        };

        var getValidPagesPerRow = function (pagesPerRow)
        {
            return (pagesPerRow >= settings.minPagesPerRow && pagesPerRow <= settings.maxPagesPerRow) ? pagesPerRow : settings.maxPagesPerRow;
        };

        // Reset some settings and empty the viewport
        var clearViewer = function ()
        {
            settings.allTilesLoaded = [];
            $(settings.outerSelector).scrollTop(0);
            settings.topScrollSoFar = 0;
            $(settings.innerSelector).empty();
            settings.firstPageLoaded = 0;
            settings.firstRowLoaded = -1;
            settings.previousTopScroll = 0;

            // Clear all the timeouts to prevent undesired pages from loading
            clearTimeout(settings.resizeTimer);

            while (settings.pageTimeouts.length)
            {
                clearTimeout(settings.pageTimeouts.pop());
            }
        };

        // Called when we don't necessarily know which view to go into
        var loadViewer = function ()
        {
            if (settings.inGrid)
            {
                loadGrid();
            }
            else
            {
                loadDocument();
            }
        };

        // Called every time we need to load document view (after zooming, fullscreen, etc)
        var loadDocument = function ()
        {
            clearViewer();

            // Make sure the zoom level we've been given is valid
            settings.zoomLevel = getValidZoomLevel(settings.zoomLevel);
            var z = settings.zoomLevel;

            // Calculate the horizontal and vertical inter-page padding
            if (settings.adaptivePadding > 0)
            {
                settings.horizontalPadding = settings.averageWidths[z] * settings.adaptivePadding;
                settings.verticalPadding = settings.averageHeights[z] * settings.adaptivePadding;
            }
            else
            {
                // It's less than or equal to 0; use fixedPadding instead
                settings.horizontalPadding = settings.fixedPadding;
                settings.verticalPadding = settings.fixedPadding;
            }

            // Make sure the vertical padding is at least 40, if plugin icons are enabled
            if (settings.pageTools.length)
            {
                settings.verticalPadding = Math.max(40, settings.horizontalPadding);
            }

            // Now reset some things that need to be changed after each zoom
            settings.totalHeight = settings.totalHeights[z] + settings.verticalPadding * (settings.numPages + 1);
            settings.dimAfterZoom = settings.totalHeight;

            // Determine the width of the inner element (based on the max width)
            var maxWidthToSet = settings.maxWidths[z] + settings.horizontalPadding * 2;
            var widthToSet = Math.max(maxWidthToSet, settings.panelWidth);

            // Needed to set settings.heightAbovePages - initially just the top padding
            var heightSoFar = 0;
            var i;

            for (i = 0; i < settings.numPages; i++)
            {
                // First set the height above that page by adding this height to the previous total
                // A page includes the padding above it
                settings.heightAbovePages[i] = heightSoFar;

                // Has to be done this way otherwise you get the height of the page included too
                heightSoFar = settings.heightAbovePages[i] + getPageData(i, 'h') + settings.verticalPadding;

                // Figure out the pageLeftOffset stuff
                settings.pageLeftOffsets[i] = (widthToSet - getPageData(i, 'w')) / 2;

                // Now try to load the page ONLY if the page needs to be loaded
                // Take scrolling into account later, just try this for now
                if (isPageVisible(i))
                {
                    loadPage(i);
                    settings.lastPageLoaded = i;
                }
            }

            // If this is not the initial load, execute the zoom callbacks
            if (settings.oldZoomLevel >= 0)
            {
                if (settings.oldZoomLevel < settings.zoomLevel)
                {
                    executeCallback(settings.onZoomIn, z);
                }
                else
                {
                    executeCallback(settings.onZoomOut, z);
                }

                executeCallback(settings.onZoom, z);
            }

            // Set the height and width of documentpane (necessary for dragscrollable)
            $(settings.innerSelector).height(Math.round(settings.totalHeight));
            $(settings.innerSelector).width(Math.round(widthToSet));

            // Scroll to the proper place
            documentScroll();

            // For the iPad - wait until this request finishes before accepting others
            if (settings.scaleWait)
            {
                settings.scaleWait = false;
            }

            var fileName = settings.pages[settings.currentPageIndex].f;
            executeCallback(settings.onDocumentLoaded, settings.lastPageLoaded, fileName);
            Events.publish("DocumentHasFinishedLoading", [settings.lastPageLoaded, fileName]);
        };

        var loadGrid = function ()
        {
            clearViewer();

            // Make sure the pages per row setting is valid
            settings.pagesPerRow = getValidPagesPerRow(settings.pagesPerRow);

            var horizontalPadding = settings.fixedPadding * (settings.pagesPerRow + 1);
            var pageWidth = (settings.panelWidth - horizontalPadding) / settings.pagesPerRow;
            settings.gridPageWidth = pageWidth;

            // Calculate the row height depending on whether we want to fix the width or the height
            settings.rowHeight = (settings.fixedHeightGrid) ? settings.fixedPadding + settings.minRatio * pageWidth : settings.fixedPadding + settings.maxRatio * pageWidth;
            settings.numRows = Math.ceil(settings.numPages / settings.pagesPerRow);
            settings.totalHeight = settings.numRows * settings.rowHeight + settings.fixedPadding;

            $(settings.innerSelector).height(Math.round(settings.totalHeight));
            $(settings.innerSelector).width(Math.round(settings.panelWidth));

            // First scroll directly to the row containing the current page
            gridScroll();

            var i, rowIndex;

            // Figure out the row each page is in
            var np = settings.numPages;
            for (i = 0; i < np; i += settings.pagesPerRow)
            {
                rowIndex = Math.floor(i / settings.pagesPerRow);

                if (isRowVisible(rowIndex))
                {
                    settings.firstRowLoaded = (settings.firstRowLoaded < 0) ? rowIndex : settings.firstRowLoaded;
                    loadRow(rowIndex);
                    settings.lastRowLoaded = rowIndex;
                }
            }
        };

        // Handles switching in and out of fullscreen mode
        // Should only be called after changing settings.inFullscreen
        var handleModeChange = function (changeView)
        {
            // Save some offsets (required for scrolling properly), if it's not the initial load
            if (settings.oldZoomLevel >= 0)
            {
                if (!settings.inGrid)
                {
                    var pageOffset = $(settings.selector + 'page-' + settings.currentPageIndex).offset();
                    var topOffset = -(pageOffset.top - settings.verticalPadding - settings.viewerYOffset);
                    var expectedLeft = (settings.panelWidth - getPageData(settings.currentPageIndex, 'w')) / 2;
                    var leftOffset = -(pageOffset.left - settings.viewerXOffset - expectedLeft);
                    settings.verticalOffset = topOffset;
                    settings.horizontalOffset = leftOffset;
                }
            }

            // Change the look of the toolbar
            Events.publish("ModeDidSwitch", null);

            // Toggle the classes
            $(settings.selector + 'fullscreen').toggleClass('diva-in-fullscreen');
            $(settings.outerSelector).toggleClass('diva-fullscreen');
            $('body').toggleClass('diva-hide-scrollbar');
            $(settings.parentSelector).toggleClass('diva-full-width');

            // Reset the panel dimensions
            settings.panelHeight = $(settings.outerSelector).height();
            settings.panelWidth = $(settings.outerSelector).width() - settings.scrollbarWidth;
            $(settings.innerSelector).width(settings.panelWidth);

            // Recalculate the viewer offsets
            settings.viewerXOffset = $(settings.outerSelector).offset().left;
            settings.viewerYOffset = $(settings.outerSelector).offset().top;

            // Used by setState when we need to change the view and the mode
            if (changeView)
            {
                settings.inGrid = !settings.inGrid;
                handleViewChange();
            }
            else
            {
                loadViewer();
            }

            // Execute callbacks
            executeCallback(settings.onModeToggle, settings.inFullscreen);
            Events.publish("ModeHasChanged", [settings.inFullScreen]);
        };

        // Handles switching in and out of grid view
        // Should only be called after changing settings.inGrid
        var handleViewChange = function ()
        {
            // Switch the slider
            // Events.publish("ViewDidSwitch", null);

            loadViewer();
            executeCallback(settings.onViewToggle, settings.inGrid);
            Events.publish("ViewDidSwitch", [settings.inGrid]);
        };

        // Called when the fullscreen icon is clicked
        var toggleFullscreen = function ()
        {
            settings.goDirectlyTo = settings.currentPageIndex;
            settings.inFullscreen = !settings.inFullscreen;
            handleModeChange(false);
        };

        // Called when the grid icon is clicked
        var toggleGrid = function ()
        {
            settings.goDirectlyTo = settings.currentPageIndex;
            settings.inGrid = !settings.inGrid;
            handleViewChange();
        };

        // Called after double-click or ctrl+double-click events on pages in document view
        var handleDocumentDoubleClick = function (event)
        {
            var pageOffset = $(this).offset();
            var offsetX = event.pageX - pageOffset.left;
            var offsetY = event.pageY - pageOffset.top;

            // Store the offset information so that it can be used in documentScroll()
            settings.preZoomOffset = {
                x: offsetX,
                y: offsetY,
                originalX: event.pageX,
                originalY: event.pageY,
                i: $(this).attr('data-index')
            };

            // Hold control to zoom out, otherwise, zoom in
            var newZoomLevel = (event.ctrlKey) ? settings.zoomLevel - 1 : settings.zoomLevel + 1;

            handleZoom(newZoomLevel);
        };

        // Called after double-clicking on a page in grid view
        var handleGridDoubleClick = function (event)
        {
            // Figure out the page that was clicked, scroll to that page
            var sel = document.getElementById(settings.ID + "outer");
            var centerX = (event.pageX - settings.viewerXOffset) + sel.scrollLeft;
            var centerY = (event.pageY - settings.viewerYOffset) + sel.scrollTop;
            var rowIndex = Math.floor(centerY / settings.rowHeight);
            var colIndex = Math.floor(centerX / (settings.panelWidth / settings.pagesPerRow));
            var pageIndex = rowIndex * settings.pagesPerRow + colIndex;
            settings.goDirectlyTo = pageIndex;

            // Leave grid view, jump directly to the desired page
            settings.inGrid = false;
            handleViewChange();
        };

        // Handles pinch-zooming for mobile devices
        var handlePinchZoom = function (event)
        {
            var newZoomLevel = settings.zoomLevel;

            // First figure out the new zoom level:
            if (event.scale > 1 && newZoomLevel < settings.maxZoomLevel)
            {
                newZoomLevel++;
            }
            else if (event.scale < 1 && newZoomLevel > settings.minZoomLevel)
            {
                newZoomLevel--;
            }
            else
            {
                return;
            }

            // Set it to true so we have to wait for this one to finish
            settings.scaleWait = true;

            // Has to call handleZoomSlide so that the coordinates are kept
            handleZoom(newZoomLevel);
        };

        // Called to handle any zoom level
        var handleZoom = function (newValue)
        {
            var newZoomLevel = getValidZoomLevel(newValue);

            // If the zoom level provided is invalid, return false
            if (newZoomLevel !== newValue)
            {
                return false;
            }

            settings.oldZoomLevel = settings.zoomLevel;
            settings.zoomLevel = newZoomLevel;

            // Update the slider
            Events.publish("ZoomLevelDidChange", null);

            loadDocument();

            return true;
        };

        // Called to handle changing the pages per row slider
        var handleGrid = function (newValue)
        {
            var newPagesPerRow = getValidPagesPerRow(newValue);

            // If the value provided is invalid, return false
            if (newPagesPerRow !== newValue)
            {
                return false;
            }

            settings.oldPagesPerRow = settings.zoomLevel;
            settings.pagesPerRow = newPagesPerRow;

            // Update the slider
            Events.publish("GridRowNumberDidChange", null);

            loadGrid();
        };

        var getYOffset = function ()
        {
            var yScroll = document.getElementById(settings.ID + "outer").scrollTop;
            var topOfPage = settings.heightAbovePages[settings.currentPageIndex];

            return parseInt(yScroll - topOfPage, 10);
        };

        var getXOffset = function ()
        {
            var innerWidth = settings.maxWidths[settings.zoomLevel] + settings.horizontalPadding * 2;
            var centerX = (innerWidth - settings.panelWidth) / 2;
            var xoff = document.getElementById(settings.ID + "outer").scrollLeft - centerX;
            return parseInt(xoff, 10);
        };

        var getState = function ()
        {
            var state = {
                'f': settings.inFullscreen,
                'g': settings.inGrid,
                'z': settings.zoomLevel,
                'n': settings.pagesPerRow,
                'i': (settings.enableFilename) ? settings.pages[settings.currentPageIndex].f : false,
                'p': (settings.enableFilename) ? false : settings.currentPageIndex + 1,
                'y': (settings.inGrid) ? false : getYOffset(),
                'x': (settings.inGrid) ? false : getXOffset(),
                'h': (settings.inFullscreen) ? false : settings.panelHeight,
                'w': (settings.inFullscreen) ? false : $(settings.outerSelector).width()
            };

            return state;
        };

        var getURLHash = function ()
        {
            var hashParams = getState();
            var hashStringBuilder = [];
            var param;

            for (param in hashParams)
            {
                if (hashParams[param] !== false)
                {
                    hashStringBuilder.push(param + settings.hashParamSuffix + '=' + hashParams[param]);
                }
            }

            return hashStringBuilder.join('&');
        };

        // Returns the URL to the current state of the document viewer (so it should be an exact replica)
        var getCurrentURL = function ()
        {
            return location.protocol + '//' + location.host + location.pathname + '#' + getURLHash();
        };

        // Called in init and when the orientation changes
        var adjustMobileWebkitDims = function ()
        {
            var outerOffset = $(settings.outerSelector).offset().top;
            settings.panelHeight = window.innerHeight - outerOffset - settings.viewerHeightPadding;
            settings.panelWidth = window.innerWidth - settings.viewerWidthPadding;

            // $(settings.parentSelector).width(settings.panelWidth);
            // document.getElementById(settings.parentSelector.substring(1)).style.width = settings.panelWidth + "px";
            settings.parentSelector.style.width = settings.panelWidth + "px";

            if (settings.enableAutoHeight)
            {
                document.getElementById(settings.ID + "outer").style.height = settings.panelHeight + "px";
            }

            if (settings.enableAutoWidth)
            {
                document.getElementById(settings.ID + "outer").style.width = settings.panelWidth + "px";
            }
        };

        // Will return true if something has changed, false otherwise
        var adjustBrowserDims = function ()
        {
            // Only resize if the browser viewport is too small
            var newHeight = $(settings.outerSelector).height();
            var newWidth = $(settings.parentSelector).width() - settings.scrollbarWidth;
            var outerOffset = $(settings.outerSelector).offset().top;

            var windowHeight = window.innerHeight || document.documentElement.clientHeight;
            var windowWidth = window.innerWidth || document.documentElement.clientWidth;
            // 2 or 1 pixels for the border
            var desiredWidth = windowWidth - settings.viewerWidthPadding - settings.scrollbarWidth - 2;
            var desiredHeight = windowHeight - outerOffset - settings.viewerHeightPadding - 1;

            if (settings.enableAutoHeight)
            {
                if (newHeight + outerOffset + 16 > window.innerHeight)
                {
                    newHeight = desiredHeight;
                }
                else if (newHeight <= settings.originalHeight)
                {
                    newHeight = Math.min(desiredHeight, settings.originalHeight);
                }
            }

            if (settings.enableAutoWidth)
            {
                if (newWidth + 32 > window.innerWidth)
                {
                    newWidth = desiredWidth;
                }
                else if (newWidth <= settings.originalWidth)
                {
                    newWidth = Math.min(desiredWidth, settings.originalWidth);
                }

                settings.parentSelector[0].style.width = newWidth + settings.scrollbarWidth;
            }

            if (newWidth !== settings.panelWidth || newHeight !== settings.panelHeight)
            {
                var el = document.getElementById(settings.ID + "outer");
                el.style.height = newHeight + "px";
                el.style.width = newWidth + settings.scrollbarWidth + "px";
                settings.panelWidth = newWidth;
                settings.panelHeight = newHeight;
                return true;
            }

            return false;
        };

        // Update the panelHeight and panelWidth based on the window size
        var adjustFullscreenDims = function ()
        {
            settings.panelWidth = window.innerWidth - settings.scrollbarWidth;
            settings.panelHeight = window.innerHeight;

            return true;
        };

        var resizeViewer = function (newWidth, newHeight)
        {
            if (newWidth >= settings.minWidth)
            {
                settings.originalWidth = newWidth;
                $(settings.outerSelector).width(newWidth);
                document.getElementById(settings.ID + "outer").style.width = newWidth + "px";

                settings.panelWidth = newWidth - settings.scrollbarWidth;

                // Should also change the width of the container
                settings.parentSelector[0].style.width = newWidth + "px";
            }

            if (newHeight >= settings.minHeight)
            {
                settings.originalHeight = newHeight;
                document.getElementById(settings.ID + "outer").style.height = newHeight + "px";

                settings.panelHeight = newHeight;
            }
        };

        // Binds most of the event handlers (some more in createToolbar)
        var handleEvents = function ()
        {
            // Create the fullscreen toggle icon if fullscreen is enabled
            if (settings.enableFullscreen)
            {
                // Event handler for fullscreen toggling
                $(settings.selector + 'fullscreen').click(function ()
                {
                    toggleFullscreen();
                });
            }

            // Change the cursor for dragging
            $(settings.innerSelector).mouseover(function ()
            {
                $(this).removeClass('diva-grabbing').addClass('diva-grab');
            });

            $(settings.innerSelector).mouseout(function ()
            {
                $(this).removeClass('diva-grab');
            });

            $(settings.innerSelector).mousedown(function ()
            {
                $(this).removeClass('diva-grab').addClass('diva-grabbing');
            });

            $(settings.innerSelector).mouseup(function ()
            {
                $(this).removeClass('diva-grabbing').addClass('diva-grab');
            });

            // Set drag scroll on first descendant of class dragger on both selected elements
            $(settings.outerSelector + ', ' + settings.innerSelector).dragscrollable({dragSelector: '.diva-dragger', acceptPropagatedEvent: true});

            // Handle the scroll
            $(settings.outerSelector).scroll(function ()
            {
                settings.topScrollSoFar = document.getElementById(settings.ID + "outer").scrollTop;
                var direction = settings.topScrollSoFar - settings.previousTopScroll;

                if (settings.inGrid)
                {
                    adjustRows(direction);
                }
                else
                {
                    adjustPages(direction);
                    settings.leftScrollSoFar = $(this).scrollLeft();
                }

                settings.previousTopScroll = settings.topScrollSoFar;
            });

            // Double-click to zoom
            $(settings.outerSelector).on('dblclick', '.diva-document-page', function (event)
            {
                handleDocumentDoubleClick.call(this, event);
            });

            // Handle the control key for macs (in conjunction with double-clicking)
            $(settings.outerSelector).on('contextmenu', '.diva-document-page', function (event)
            {
                if (event.ctrlKey)
                {
                    // In Firefox, this doesn't trigger a double-click, so we apply one manually
                    clearTimeout(settings.singleClickTimeout);

                    if (settings.singleClick)
                    {
                        handleDocumentDoubleClick.call(this, event);
                        settings.singleClick = false;
                    }
                    else
                    {
                        settings.singleClick = true;

                        // Set it to false again after 500 milliseconds (standard double-click timeout)
                        settings.singleClickTimeout = setTimeout(function ()
                        {
                            settings.singleClick = false;
                        }, 500);
                    }

                    return false;
                }
            });

            $(settings.outerSelector).on('dblclick', '.diva-row', function (event)
            {
                handleGridDoubleClick.call(this, event);
            });

            // Check if the user is on a iPhone or iPod touch or iPad
            if (settings.mobileWebkit)
            {
                // Prevent resizing (below from http://matt.might.net/articles/how-to-native-iphone-ipad-apps-in-javascript/)
                var toAppend = [];
                toAppend.push('<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1" />');

                // Eliminate URL and button bars if added to home screen
                toAppend.push('<meta name="apple-mobile-web-app-capable" content="yes" />');

                // Choose how to handle the phone status bar
                toAppend.push('<meta name="apple-mobile-web-app-status-bar-style" content="black" />');
                $('head').append(toAppend.join('\n'));

                // Block the user from moving the window only if it's not integrated
                if (settings.blockMobileMove)
                {
                    $('body').bind('touchmove', function (event)
                    {
                        var e = event.originalEvent;
                        e.preventDefault();

                        return false;
                    });
                }

                // Allow pinch-zooming
                $('body').bind('gestureend', function (event)
                {
                    var e = event.originalEvent;

                    if (!settings.scaleWait)
                    {
                        // Save the page we're currently on so we scroll there
                        settings.goDirectlyTo = settings.currentPageIndex;

                        if (settings.inGrid)
                        {
                            settings.inGrid = false;

                            handleViewChange();
                        }
                        else
                        {
                            handlePinchZoom(e);
                        }
                    }
                    return false;
                });

                // Listen to orientation change event
                $(window).bind('orientationchange', function (event)
                {
                    settings.orientationChange = true;
                    adjustMobileWebkitDims();

                    // Reload the viewer to account for the resized viewport
                    settings.goDirectlyTo = settings.currentPageIndex;
                    loadViewer();
                });

                // Inertial scrolling
                $(settings.outerSelector).kinetic();
            }

            // Only check if either scrollBySpace or scrollByKeys is enabled
            if (settings.enableSpaceScroll || settings.enableKeyScroll)
            {
                var spaceKey = $.ui.keyCode.SPACE;
                var pageUpKey = $.ui.keyCode.PAGE_UP;
                var pageDownKey = $.ui.keyCode.PAGE_DOWN;
                var homeKey = $.ui.keyCode.HOME;
                var endKey = $.ui.keyCode.END;

                // Catch the key presses in document
                $(document).keydown(function (event)
                {
                    // Space or page down - go to the next page
                    if ((settings.enableSpaceScroll && event.keyCode === spaceKey) || (settings.enableKeyScroll && event.keyCode === pageDownKey))
                    {
                        $(settings.outerSelector).scrollTop(settings.topScrollSoFar + settings.panelHeight);
                        return false;
                    }

                    // Page up - go to the previous page
                    if (settings.enableKeyScroll && event.keyCode === pageUpKey)
                    {
                        $(settings.outerSelector).scrollTop(settings.topScrollSoFar - settings.panelHeight);
                        return false;
                    }

                    // Home key - go to the beginning of the document
                    if (settings.enableKeyScroll && event.keyCode === homeKey)
                    {
                        $(settings.outerSelector).scrollTop(0);
                        return false;
                    }

                    // End key - go to the end of the document
                    if (settings.enableKeyScroll && event.keyCode === endKey)
                    {
                        $(settings.outerSelector).scrollTop(settings.totalHeight);
                        return false;
                    }
                });

                // Handle window resizing events
                if (!settings.mobileWebkit)
                {
                    $(window).resize(function ()
                    {
                        var adjustSuccess = (settings.inFullscreen) ? adjustFullscreenDims() : adjustBrowserDims();

                        if (adjustSuccess)
                        {
                            // Cancel any previously-set resize timeouts
                            clearTimeout(settings.resizeTimer);

                            settings.resizeTimer = setTimeout(function ()
                            {
                                settings.goDirectlyTo = settings.currentPageIndex;
                                loadViewer();
                            }, 200);
                        }
                    });
                }
            }
        };

        // Handles all status updating etc (both fullscreen and not)
        var createToolbar = function () {
            // Prepare the HTML for the various components
            var gridIconHTML = (settings.enableGridIcon) ? '<div class="diva-grid-icon' + (settings.inGrid ? ' diva-in-grid' : '') + '" id="' + settings.ID + 'grid-icon" title="Toggle grid view"></div>' : '';
            var linkIconHTML = (settings.enableLinkIcon) ? '<div class="diva-link-icon" id="' + settings.ID + 'link-icon" style="' + (settings.enableGridIcon ? 'border-left: 0px' : '') + '" title="Link to this page"></div>' : '';
            var zoomSliderHTML = (settings.enableZoomSlider) ? '<div id="' + settings.ID + 'zoom-slider"></div>' : '';
            var gridSliderHTML = (settings.enableGridSlider) ? '<div id="' + settings.ID + 'grid-slider"></div>' : '';
            var gotoPageHTML = (settings.enableGotoPage) ? '<form id="' + settings.ID + 'goto-page" class="diva-goto-form"><input type="text" id="' + settings.ID + 'goto-page-input" / class="diva-input"> <input type="submit" value="Go" style="margin-top: 0px;" /></form>' : '';
            var zoomSliderLabelHTML = (settings.enableZoomSlider) ? '<div id="' + settings.ID + 'zoom-slider-label" class="diva-slider-label">Zoom level: <span id="' + settings.ID + 'zoom-level">' + settings.zoomLevel + '</span></div>' : '';
            var gridSliderLabelHTML = (settings.enableGridSlider) ? '<div id="' + settings.ID + 'grid-slider-label" class="diva-slider-label">Pages per row: <span id="' + settings.ID + 'pages-per-row">' + settings.pagesPerRow + '</span></div>' : '';
            var pageNumberHTML = '<div class="diva-page-label">Page <span id="' + settings.ID + 'current-page">1</span> of <span id="' + settings.ID + 'num-pages">' + settings.numPages + '</span></div>';

            // If the viewer is specified to be "contained", we make room for the fullscreen icon
            var otherToolbarClass = '';

            if (settings.contained)
            {
                // Make sure the container element does not have a static position
                // (Needed for the fullscreen icon to be contained)
                if ($(settings.parentSelector).css('position') === 'static')
                {
                    $(settings.parentSelector).addClass('diva-relative-position');
                }

                otherToolbarClass = ' diva-fullscreen-space';

                // If enableAutoTitle is set to TRUE, move it down
                if (settings.enableAutoTitle)
                {
                    $(settings.selector + 'fullscreen').addClass('diva-contained');
                }
            }

            var toolbarHTML = '<div id="' + settings.ID + 'tools-left" class="diva-tools-left' + otherToolbarClass + '">' + zoomSliderHTML + gridSliderHTML + zoomSliderLabelHTML + gridSliderLabelHTML + '</div><div id="' + settings.ID + 'tools-right" class="diva-tools-right">' + linkIconHTML + gridIconHTML + '<div class="diva-page-nav">' + gotoPageHTML + pageNumberHTML + '</div></div>';

            if (settings.toolbarParentSelector)
            {
                $(settings.toolbarParentSelector).prepend('<div id="' + settings.ID + 'tools" class="diva-tools">' + toolbarHTML + '</div>');
            }
            else
            {
                $(settings.parentSelector).prepend('<div id="' + settings.ID + 'tools" class="diva-tools">' + toolbarHTML + '</div>');
            }

            // Create the zoom slider
            $(settings.selector + 'zoom-slider').slider({
                value: settings.zoomLevel,
                min: settings.minZoomLevel,
                max: settings.maxZoomLevel,
                step: 1,
                slide: function (event, ui)
                {
                    var i = settings.currentPageIndex;
                    settings.goDirectlyTo = i;

                    // Figure out the horizontal and vertical offsets
                    // (Try to zoom in on the current center)
                    var zoomRatio = Math.pow(2, ui.value - settings.zoomLevel);
                    var innerWidth = settings.maxWidths[settings.zoomLevel] + settings.horizontalPadding * 2;
                    var centerX = $(settings.outerSelector).scrollLeft() - (innerWidth - settings.panelWidth) / 2;
                    settings.horizontalOffset = (innerWidth > settings.panelWidth) ? centerX * zoomRatio : 0;
                    settings.verticalOffset = zoomRatio * ($(settings.outerSelector).scrollTop() - settings.heightAbovePages[i]);

                    handleZoom(ui.value);
                },
                change: function (event, ui)
                {
                    if (ui.value !== settings.zoomLevel)
                    {
                        handleZoom(ui.value);
                    }
                }
            });

            // Create the grid slider
            $(settings.selector + 'grid-slider').slider(
            {
                value: settings.pagesPerRow,
                min: settings.minPagesPerRow,
                max: settings.maxPagesPerRow,
                step: 1,
                slide: function (event, ui)
                {
                    handleGrid(ui.value);
                },
                change: function (event, ui)
                {
                    if (ui.value !== settings.pagesPerRow)
                    {
                        handleGrid(ui.value);
                    }
                }
            });

            // Handle clicking of the grid icon
            $(settings.selector + 'grid-icon').click(function ()
            {
                toggleGrid();
            });

            // Handle going to a specific page using the input box
            $(settings.selector + 'goto-page').submit(function ()
            {
                var desiredPage = parseInt($(settings.selector + 'goto-page-input').val(), 10);
                var pageIndex = desiredPage - 1;

                if (!isPageValid(pageIndex))
                {
                    alert("Invalid page number");
                }
                else
                {
                    if (settings.inGrid)
                    {
                        gotoRow(pageIndex);
                    }
                    else
                    {
                        gotoPage(pageIndex, 0, 0);
                    }
                }

                // Prevent the default action of reloading the page
                return false;
            });

            // Handle the creation of the link popup box
            $(settings.selector + 'link-icon').click(function ()
            {
                $('body').prepend('<div id="' + settings.ID + 'link-popup" class="diva-link-popup"><input id="' + settings.ID + 'link-popup-input" class="diva-input" type="text" value="' + getCurrentURL() + '"/></div>');

                if (settings.inFullscreen)
                {
                    $(settings.selector + 'link-popup').addClass('in-fullscreen');
                }
                else
                {
                    // Calculate the left and top offsets
                    // Compensate for border, popup width
                    var leftOffset = $(settings.outerSelector).offset().left + settings.panelWidth;
                    leftOffset += settings.scrollbarWidth - 240 - 1;
                    var topOffset = $(settings.outerSelector).offset().top + 1;

                    $(settings.selector + 'link-popup').removeClass('in-fullscreen').css(
                    {
                        'top': topOffset + 'px',
                        'left': leftOffset + 'px'
                    });
                }

                // Catch onmouseup events outside of this div
                $('body').mouseup(function (event)
                {
                    var targetID = event.target.id;

                    if (targetID !== settings.ID + 'link-popup' && targetID !== settings.ID + 'link-popup-input')
                    {
                        $(settings.selector + 'link-popup').remove();
                    }
                });

                // Also delete it upon scroll and page up/down key events
                $(settings.outerSelector).scroll(function ()
                {
                    $(settings.selector + 'link-popup').remove();
                });
                $(settings.selector + 'link-popup input').click(function ()
                {
                    $(this).focus().select();
                });
                return false;
            });

            // Show the relevant slider
            var currentSlider = (settings.inGrid) ? 'grid' : 'zoom';
            $(settings.selector + currentSlider + '-slider').show();
            $(settings.selector + currentSlider + '-slider-label').show();

            var switchMode = function ()
            {
                // Switch from fullscreen to not
                $(settings.selector + 'tools').toggleClass('diva-fullscreen-tools');

                if (!settings.inFullscreen)
                {
                    // Leaving fullscreen
                    $(settings.selector + 'tools-left').after($(settings.selector + 'tools-right'));
                    $(settings.selector + 'tools-left').removeClass('in-fullscreen');
                }
                else
                {
                    // Entering fullscreen
                    $(settings.selector + 'tools-right').after($(settings.selector + 'tools-left'));
                    $(settings.selector + 'tools-left').addClass('in-fullscreen');
                }
            };

            var switchView = function ()
            {
                // Switch from grid to document view etc
                $(settings.selector + currentSlider + '-slider').hide();
                $(settings.selector + currentSlider + '-slider-label').hide();
                currentSlider = (settings.inGrid) ? 'grid' : 'zoom';
                $(settings.selector + currentSlider + '-slider').show();
                $(settings.selector + currentSlider + '-slider-label').show();

                // Also change the image for the grid icon
                $(settings.selector + 'grid-icon').toggleClass('diva-in-grid');
            };

            var toolbar =
            {
                updateCurrentPage: function ()
                {
                    $(settings.selector + 'current-page').text(settings.currentPageIndex + 1);
                },
                setNumPages: function (newNumber)
                {
                    $(settings.selector + 'num-pages').text(newNumber);
                },
                updateZoomSlider: function ()
                {
                    // Update the position of the handle within the slider
                    if (settings.zoomLevel !== $(settings.selector + 'zoom-slider').slider('value'))
                    {
                        $(settings.selector + 'zoom-slider').slider(
                        {
                            value: settings.zoomLevel
                        });
                    }

                    // Update the slider label
                    $(settings.selector + 'zoom-level').text(settings.zoomLevel);
                },
                updateGridSlider: function ()
                {
                    // Update the position of the handle within the slider
                    if (settings.pagesPerRow !== $(settings.selector + 'grid-slider').slider('value'))
                    {
                        $(settings.selector + 'grid-slider').slider(
                        {
                            value: settings.pagesPerRow
                        });
                    }

                    // Update the slider label
                    $(settings.selector + 'pages-per-row').text(settings.pagesPerRow);
                },
                switchView: switchView,
                switchMode: switchMode
            };
            return toolbar;
        };

        var initPlugins = function ()
        {
            if (window.divaPlugins)
            {
                var pageTools = [];

                // Add all the plugins that have not been explicitly disabled to settings.plugins
                $.each(window.divaPlugins, function (index, plugin)
                {
                    var pluginProperName = plugin.pluginName[0].toUpperCase() + plugin.pluginName.substring(1);

                    if (settings['enable' + pluginProperName])
                    {
                        // Call the init function and check return value
                        var enablePlugin = plugin.init(settings, self);

                        // If int returns false, consider the plugin disabled
                        if (!enablePlugin)
                        {
                            return;
                        }

                        // If the title text is undefined, use the name of the plugin
                        var titleText = plugin.titleText || pluginProperName + " plugin";

                        // Create the pageTools bar if handleClick is set to a function
                        if (typeof plugin.handleClick === 'function')
                        {
                            pageTools.push('<div class="diva-' + plugin.pluginName + '-icon" title="' + titleText + '"></div>');

                            // Delegate the click event - pass it the settings
                            $(settings.outerSelector).delegate('.diva-' + plugin.pluginName + '-icon', 'click', function (event)
                            {
                                plugin.handleClick.call(this, event, settings);
                            });
                        }

                        // Add it to settings.plugins so it can be used later
                        settings.plugins.push(plugin);
                    }
                });

                // Save the page tools bar so it can be added for each page
                if (pageTools.length)
                {
                    settings.pageTools = '<div class="diva-page-tools">' + pageTools.join('') + '</div>';
                }
            }
        };

        var hideThrobber = function ()
        {
            // Clear the timeout, if it hasn't executed yet
            clearTimeout(settings.throbberTimeoutID);

            // Hide the throbber if it has already executed
            $(settings.selector + 'throbber').hide();
        };

        var setupViewer = function ()
        {
            // Create the throbber element
            var throbberHTML = '<div id="' + settings.ID + 'throbber" class="diva-throbber"></div>';
            $(settings.outerSelector).append(throbberHTML);

            // If the request hasn't completed after a specified time, show it
            settings.throbberTimeoutID = setTimeout(function ()
            {
                $(settings.selector + 'throbber').show();
            }, settings.throbberTimeout);

            $.ajax({
                url: settings.objectData,
                cache: true,
                dataType: 'json',
                error: function (jqxhr, status, error)
                {
                    hideThrobber();

                    // Show a basic error message within the document viewer pane
                    $(settings.outerSelector).text("Invalid URL. Error code: " + status + " " + error);
                },
                success: function (data, status, jqxhr)
                {
                    hideThrobber();

                    // Save all the data we need
                    settings.pages = data.pgs;
                    settings.maxRatio = data.dims.max_ratio;
                    settings.minRatio = data.dims.min_ratio;
                    settings.itemTitle = data.item_title;
                    settings.numPages = data.pgs.length;

                    // These are arrays, the index corresponding to the zoom level
                    settings.maxWidths = data.dims.max_w;
                    settings.averageWidths = data.dims.a_wid;
                    settings.averageHeights = data.dims.a_hei;
                    settings.totalHeights = data.dims.t_hei;

                    // Make sure the set max and min values are valid
                    settings.realMaxZoom = data.max_zoom;
                    settings.maxZoomLevel = (settings.maxZoomLevel >= 0 && settings.maxZoomLevel <= data.max_zoom) ? settings.maxZoomLevel : data.max_zoom;
                    settings.minZoomLevel = (settings.minZoomLevel >= 0 && settings.minZoomLevel <= settings.maxZoomLevel) ? settings.minZoomLevel : 0;
                    settings.minPagesPerRow = Math.max(2, settings.minPagesPerRow);
                    settings.maxPagesPerRow = Math.max(settings.minPagesPerRow, settings.maxPagesPerRow);

                    // Check that the desired page is in range
                    if (settings.enableFilename)
                    {
                        var iParam = $.getHashParam('i' + settings.hashParamSuffix);
                        var iParamPage = getPageIndex(iParam);

                        if (isPageValid(iParamPage))
                        {
                            settings.goDirectlyTo = iParamPage;
                        }
                    }
                    else
                    {
                        // Not using the i parameter, check the p parameter
                        // Subtract 1 to get the page index
                        var pParam = parseInt($.getHashParam('p' + settings.hashParamSuffix), 10) - 1;

                        if (isPageValid(pParam))
                        {
                            settings.goDirectlyTo = pParam;
                        }
                    }

                    // Execute the setup hook for each plugin (if defined)
                    $.each(settings.plugins, function (index, plugin)
                    {
                        executeCallback(plugin.setupHook, settings);
                    });

                    // Create the toolbar and display the title + total number of pages
                    if (settings.enableToolbar)
                    {
                        settings.toolbar = createToolbar();
                        Events.subscribe("VisiblePageDidChange", settings.toolbar.updateCurrentPage);
                        Events.subscribe("ModeDidSwitch", settings.toolbar.switchMode);
                        Events.subscribe("ViewDidSwitch", settings.toolbar.switchView);
                        Events.subscribe("ZoomLevelDidChange", settings.toolbar.updateZoomSlider);
                        Events.subscribe("GridRowNumberDidChange", settings.toolbar.updateGridSlider);
                    }

                    $(settings.selector + 'current label').text(settings.numPages);

                    if (settings.enableAutoTitle)
                    {
                        $(settings.parentSelector).prepend('<div id="' + settings.ID + 'title" class="diva-title">' + settings.itemTitle + '</div>');
                    }

                    // Adjust the document panel dimensions for touch devices
                    if (settings.mobileWebkit)
                    {
                        adjustMobileWebkitDims();
                    }
                    else
                    {
                        settings.originalWidth = $(settings.parentSelector).width() - settings.scrollbarWidth;
                        settings.originalHeight = $(settings.outerSelector).height();
                        adjustBrowserDims();
                    }

                    // Calculate the viewer x and y offsets
                    var viewerOffset = $(settings.outerSelector).offset();
                    settings.viewerXOffset = viewerOffset.left;
                    settings.viewerYOffset = viewerOffset.top;

                    if (settings.inFullscreen)
                    {
                        handleModeChange(false);
                    }
                    else
                    {
                        loadViewer();
                    }

                    // Execute the callback
                    executeCallback(settings.onReady, settings);
                    Events.publish("ViewerHasFinishedLoading", [settings]);

                    // signal that everything should be set up and ready to go.
                    settings.loaded = true;
                }
            });
        };

        var checkLoaded = function()
        {
            if (!settings.loaded)
            {
                console.warn("The viewer is not completely initialized. This is likely because it is still downloading data. To fix this, only call this function if the isReady() method returns true.");
                return false;
            }
            return true;
        };

        var init = function ()
        {
            // First figure out the width of the scrollbar in this browser
            settings.scrollbarWidth = $.getScrollbarWidth();

            // If window.orientation is defined, then it's probably mobileWebkit
            settings.mobileWebkit = window.orientation !== undefined;

            // Generate an ID that can be used as a prefix for all the other IDs
            settings.ID = $.generateId('diva-');
            settings.selector = '#' + settings.ID;

            // Figure out the hashParamSuffix from the ID
            var divaNumber = parseInt(settings.ID, 10);

            if (divaNumber > 1)
            {
                // If this is document viewer #1, don't use a suffix; otherwise, use the document viewer number
                settings.hashParamSuffix = divaNumber;
            }

            // Since we need to reference these two a lot
            settings.outerSelector = settings.selector + 'outer';
            settings.innerSelector = settings.selector + 'inner';

            // Create the inner and outer panels
            $(settings.parentSelector).append('<div id="' + settings.ID + 'outer" class="diva-outer"></div>');
            $(settings.outerSelector).append('<div id="' + settings.ID + 'inner" class="diva-inner diva-dragger"></div>');

            // Create the fullscreen icon
            if (settings.enableFullscreen)
            {
                $(settings.parentSelector).prepend('<div id="' + settings.ID + 'fullscreen" class="diva-fullscreen-icon" title="Toggle fullscreen mode"></div>');
            }

            // First, n - check if it's in range
            var nParam = parseInt($.getHashParam('n' + settings.hashParamSuffix), 10);

            if (nParam >= settings.minPagesPerRow && nParam <= settings.maxPagesPerRow)
            {
                settings.pagesPerRow = nParam;
            }

            // Now z - check that it's in range
            var zParam = $.getHashParam('z' + settings.hashParamSuffix);

            if (zParam !== '')
            {
                // If it's empty, we don't want to change the default zoom level
                zParam = parseInt(zParam, 10);

                // Can't check if it exceeds the max zoom level or not because that data is not available yet ...
                if (zParam >= settings.minZoomLevel)
                {
                    settings.zoomLevel = zParam;
                }
            }

            // y - vertical offset from the top of the relevant page
            var yParam = parseInt($.getHashParam('y' + settings.hashParamSuffix), 10);

            if (!isNaN(yParam))
            {
                settings.verticalOffset = yParam;
            }

            // x - horizontal offset from the center of the page
            var xParam = parseInt($.getHashParam('x' + settings.hashParamSuffix), 10);

            if (!isNaN(xParam))
            {
                settings.horizontalOffset = xParam;
            }

            // If the "fullscreen" hash param is true, go to fullscreen initially
            // If the grid hash param is true, go to grid view initially
            var gridParam = $.getHashParam('g' + settings.hashParamSuffix);
            var goIntoGrid = gridParam === 'true';
            var fullscreenParam = $.getHashParam('f' + settings.hashParamSuffix);
            var goIntoFullscreen = fullscreenParam === 'true';

            settings.inGrid = (settings.inGrid && gridParam !== 'false') || goIntoGrid;
            settings.inFullscreen = (settings.inFullscreen && fullscreenParam !== 'false') || goIntoFullscreen;

            // Store the height and width of the viewer (the outer div), if present
            var desiredHeight = parseInt($.getHashParam('h' + settings.hashParamSuffix), 10);
            var desiredWidth = parseInt($.getHashParam('w' + settings.hashParamSuffix), 10);

            // Store the minimum and maximum height too
            settings.minHeight = parseInt($(settings.outerSelector).css('min-height'), 10);
            settings.minWidth = parseInt($(settings.outerSelector).css('min-width'), 10);

            // Just call resize, it'll take care of bounds-checking etc
            if (desiredHeight > 0 || desiredWidth > 0)
            {
                resizeViewer(desiredWidth, desiredHeight);
            }

            // Do the initial AJAX request and viewer loading
            setupViewer();

            // Do all the plugin initialisation
            initPlugins();

            handleEvents();
        };

        // Call the init function when this object is created.
        init();

        /* PUBLIC FUNCTIONS
===============================================
*/

        // Returns the title of the document, based on the directory name
        this.getItemTitle = function ()
        {
            return settings.itemTitle;
        };

        // Go to a particular page by its page number (with indexing starting at 1)
        // returns True if the page number passed is valid; false if it is not.
        this.gotoPageByNumber = function (pageNumber)
        {
            var pageIndex = pageNumber - 1;
            if (isPageValid(pageIndex))
            {
                gotoPage(pageIndex, 0, 0);
                return true;
            }
            return false;
        };

        // Go to a particular page (with indexing starting at 0)
        // returns True if the page index is valid; false if it is not.
        this.gotoPageByIndex = function (pageIndex)
        {
            if (isPageValid(pageIndex))
            {
                gotoPage(pageIndex, 0, 0);
                return true;
            }
            return false;
        };

        // Returns the page index (with indexing starting at 0)
        this.getCurrentPage = function ()
        {
            console.warn("Deprecated. Use getCurrentPageIndex instead.");
            return settings.currentPageIndex;
        };

        this.getNumberOfPages = function()
        {
            if (!checkLoaded())
            {
                return false;
            }

            return settings.numPages;
        }

        // Returns the dimensions of a given page index at a given zoom level
        this.getPageDimensionsAtZoomLevel = function(pageIdx, zoomLevel)
        {
            if (!checkLoaded())
            {
                return false;
            }

            var zoomLevel = zoomLevel - 1; // zoom levels are 1-based, but our array is 0-based;
            var pg = settings.pages[pageIdx];
            var pgAtZoom = pg.d[parseInt(zoomLevel, 10)];
            return {'width': pgAtZoom.w, 'height': pgAtZoom.h}
        };

        // Returns the dimensions of the current page at the current zoom level
        this.getCurrentPageDimensionsAtCurrentZoomLevel = function()
        {
            return this.getPageDimensionsAtZoomLevel(settings.currentPageIndex, settings.zoomLevel);
        };

        this.isReady = function()
        {
            return settings.loaded;
        };

        this.getCurrentPageIndex = function ()
        {
            return settings.currentPageIndex;
        };

        this.getCurrentPageFilename = function ()
        {
            return settings.pages[settings.currentPageIndex].f;
        };

        this.getCurrentPageNumber = function ()
        {
            return settings.currentPageIndex + 1;
        };

        // Returns the current zoom level
        this.getZoomLevel = function ()
        {
            return settings.zoomLevel;
        };

        // gets the maximum zoom level for the entire document
        this.getMaxZoomLevel = function ()
        {
            return settings.maxZoomLevel;
        };

        // gets the max zoom level for a given page
        this.getMaxZoomLevelForPage = function(pageIdx)
        {
            if (!checkLoaded)
            {
                return false;
            }

            return settings.pages[pageIdx].m;
        }

        this.getMinZoomLevel = function ()
        {
            return settings.minZoomLevel;
        };

        // Use the provided zoom level (will check for validity first)
        // Returns false if the zoom level is invalid, true otherwise
        this.setZoomLevel = function (zoomLevel)
        {
            if (settings.inGrid)
            {
                toggleGrid();
            }

            return handleZoom(zoomLevel);
        };

        // Zoom in. Will return false if it's at the maximum zoom
        this.zoomIn = function ()
        {
            return this.setZoomLevel(settings.zoomLevel + 1);
        };

        // Zoom out. Will return false if it's at the minimum zoom
        this.zoomOut = function ()
        {
            return this.setZoomLevel(settings.zoomLevel - 1);
        };

        // Uses the isVerticallyInViewport() function, but relative to a page
        // Check if something (e.g. a highlight box on a particular page) is visible
        this.inViewport = function (pageNumber, topOffset, height)
        {
            var pageIndex = pageNumber - 1;
            var top = settings.heightAbovePages[pageIndex] + topOffset;
            var bottom = top + height;

            return isVerticallyInViewport(top, bottom);
        };

        // Toggle fullscreen mode
        this.toggleFullscreenMode = function ()
        {
            toggleFullscreen();
        };

        // Enter fullscreen mode if currently not in fullscreen mode
        // Returns false if in fullscreen mode initially, true otherwise
        // This function will work even if enableFullscreen is set to false
        this.enterFullscreenMode = function ()
        {
            if (!settings.inFullscreen)
            {
                toggleFullscreen();
                return true;
            }

            return false;
        };

        // Leave fullscreen mode if currently in fullscreen mode
        // Returns true if in fullscreen mode intitially, false otherwise
        this.leaveFullscreenMode = function ()
        {
            if (settings.inFullscreen)
            {
                toggleFullscreen();
                return true;
            }

            return false;
        };

        // Toggle grid view
        this.toggleGridView = function ()
        {
            toggleGrid();
        };

        // Enter grid view if currently not in grid view
        // Returns false if in grid view initially, true otherwise
        this.enterGridView = function ()
        {
            if (!settings.inGrid) {
                toggleGrid();
                return true;
            }

            return false;
        };

        // Leave grid view if currently in grid view
        // Returns true if in grid view initially, false otherwise
        this.leaveGridView = function ()
        {
            if (settings.inGrid)
            {
                toggleGrid();
                return true;
            }

            return false;
        };

        // Jump to a page based on its filename
        // Returns true if successful and false if the filename is invalid
        this.gotoPageByName = function (filename)
        {
            var pageIndex = getPageIndex(filename);
            if (isPageValid(pageIndex))
            {
                gotoPage(pageIndex, 0, 0);
                return true;
            }

            return false;
        };

        // Get the page index (0-based) corresponding to a given filename
        // If the page index doesn't exist, this will return -1
        this.getPageIndex = function (filename)
        {
            return getPageIndex(filename);
        };

        // Get the current URL (exposes the private method)
        this.getCurrentURL = function ()
        {
            return getCurrentURL();
        };

        // Get the hash part only of the current URL (without the leading #)
        this.getURLHash = function ()
        {
            return getURLHash();
        };

        // Get an object representing the state of this diva instance (for setState)
        this.getState = function ()
        {
            return getState();
        };

        // Get the instance selector for this instance, since it's auto-generated.
        this.getInstanceSelector = function ()
        {
            return settings.selector;
        };

        // Get the instance ID -- essentially the selector without the leading '#'.
        this.getInstanceId = function()
        {
            return settings.ID;
        };

        this.getSettings = function()
        {
            return settings;
        };

        // Align this diva instance with a state object (as returned by getState)
        this.setState = function (state)
        {
            var pageIndex;

            // If we need to resize the viewer, do that first
            resizeViewer(state.w, state.h);

            // Only change settings.goDirectlyTo if state.i or state.p is valid
            pageIndex = getPageIndex(state.i);

            if (isPageValid(pageIndex))
            {
                settings.goDirectlyTo = pageIndex;
            }
            else if (isPageValid(state.p))
            {
                settings.goDirectlyTo = state.p;
            }

            settings.horizontalOffset = parseInt(state.x, 10);
            settings.verticalOffset = parseInt(state.y, 10);

            // Only change the zoom if state.z is valid
            if (state.z >= settings.minZoomLevel && state.z <= settings.maxZoomLevel)
            {
                settings.zoomLevel = state.z;
            }

            // Only change the pages per row setting if state.n is valid
            if (state.n >= settings.minPagesPerRow && state.n <= settings.maxPagesPerRow)
            {
                settings.pagesPerRow = state.n;
            }

            if (settings.inFullscreen !== state.f)
            {
                // The parameter determines if we need to change the view as well
                settings.inFullscreen = state.f;
                handleModeChange(settings.inGrid !== state.g);
            }
            else
            {
                // Don't need to change the mode, may need to change view
                if (settings.inGrid !== state.g)
                {
                    settings.inGrid = state.g;
                    handleViewChange();
                }
                else
                {
                    // Reload the viewer, just in case
                    loadViewer();
                }
            }
        };

        // Resizes the outer div to the specified width and height
        this.resize = function (newWidth, newHeight)
        {
            resizeViewer(newWidth, newHeight);
            loadViewer();
        };

        // Destroys this instance, tells plugins to do the same (for testing)
        this.destroy = function ()
        {
            // Removes the hide-scrollbar class from the body
            $('body').removeClass('diva-hide-scrollbar');

            // Empty the parent container and remove any diva-related data
            $(settings.parentSelector).empty().removeData('diva');

            // Call the destroy function for all the enabled plugins (if it exists)
            $.each(settings.plugins, function (index, plugin)
            {
                executeCallback(plugin.destroy);
            });

            // Remove any additional styling on the parent element
            $(settings.parentSelector).removeAttr('style').removeAttr('class');
        };
    };

    $.fn.diva = function (options)
    {
        return this.each(function ()
        {
            var element = $(this);

            // Return early if this element already has a plugin instance
            if (element.data('diva'))
            {
                return;
            }

            // Save the reference to the container element
            options.parentSelector = element;

            // Otherwise, instantiate the document viewer
            var diva = new Diva(this, options);
            element.data('diva', diva);
        });
    };

})(jQuery);