//-----------------------------------------------------------------------------
//
// Copyright (c) 2010, Roaring Development
//
// This software may not be copied, modified, decompiled or redistributed. 
//
//-----------------------------------------------------------------------------

//Copyright (c) 2009, CodePlex Foundation
//All rights reserved.
//  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
// * Neither the name of CodePlex Foundation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 

if (!window.Seadragon) {
    window.Seadragon = {};
}

Seadragon.Config = {
    debugMode: false,
    animationTime: 1.5,
    blendTime: 0.5,
    alwaysBlend: false,
    immediateRender: false,
    wrapHorizontal: false,
    wrapVertical: false,
    minZoomImageRatio: .25,
    maxZoomPixelRatio: 1,
    visibilityRatio: 0.2,
    springStiffness: 5.0,
    imageLoaderLimit: 2,
    zoomPerClick: 2.0,
    zoomPerSecond: 2.0,
    maxImageCacheCount: 100,
    minPixelRatio: 0.5
};

Function.createDelegate = function (instance, method) {
    return function() {
      return method.apply(instance, arguments);
    };
};Seadragon.Point=Seadragon.Point = function(x, y) {
    this.x = typeof (x) == "number" ? x : 0;
    this.y = typeof (y) == "number" ? y : 0;
};
Seadragon.Point.fromElementOffset =  function(offset) {
	return new Seadragon.Point(offset.left, offset.top);
};
Seadragon.Point.fromEvent =  function(event) {
	return new Seadragon.Point(event.pageX, event.pageY);
};
Seadragon.Point.prototype = {
    // Methods
    plus: function(point) {
        return new Seadragon.Point(this.x + point.x, this.y + point.y);
    },

    minus: function(point) {
        return new Seadragon.Point(this.x - point.x, this.y - point.y);
    },

    times: function(factor) {
        return new Seadragon.Point(this.x * factor, this.y * factor);
    },

    divide: function(factor) {
        return new Seadragon.Point(this.x / factor, this.y / factor);
    },

    negate: function() {
        return new Seadragon.Point(-this.x, -this.y);
    },

    distanceTo: function(point) {
        return Math.sqrt(Math.pow(this.x - point.x, 2) +
                        Math.pow(this.y - point.y, 2));
    },

    apply: function(func) {
        return new Seadragon.Point(func(this.x), func(this.y));
    },

    equals: function(point) {
        return (point instanceof Seadragon.Point) &&
                (this.x === point.x) && (this.y === point.y);
    },

    toString: function() {
        return "(" + this.x + "," + this.y + ")";
    }
};Seadragon.TileSource = function(width, height, tileSize, tileOverlap, minLevel, maxLevel) {
    this.aspectRatio = width / height;
    this.dimensions = new Seadragon.Point(width, height);
    this.minLevel = minLevel ? minLevel : 0;
    this.maxLevel = maxLevel ? maxLevel :
            Math.ceil(Math.log(Math.max(width, height)) / Math.log(2));
    this.tileSize = tileSize ? tileSize : 0;
    this.tileOverlap = tileOverlap ? tileOverlap : 0;
};
Seadragon.TileSource.prototype = {
    getLevelScale: function(level) {
        // equivalent to Math.pow(0.5, numLevels - level);
        return 1 / (1 << (this.maxLevel - level));
    },

    getNumTiles: function(level) {
        var scale = this.getLevelScale(level);
        var x = Math.ceil(scale * this.dimensions.x / this.tileSize);
        var y = Math.ceil(scale * this.dimensions.y / this.tileSize);

        return new Seadragon.Point(x, y);
    },

    getPixelRatio: function(level) {
        var imageSizeScaled = this.dimensions.times(this.getLevelScale(level));
        var rx = 1.0 / imageSizeScaled.x;
        var ry = 1.0 / imageSizeScaled.y;

        return new Seadragon.Point(rx, ry);
    },

    getTileAtPoint: function(level, point) {
        var pixel = point.times(this.dimensions.x).times(this.getLevelScale(level));

        var tx = Math.floor(pixel.x / this.tileSize);
        var ty = Math.floor(pixel.y / this.tileSize);

        return new Seadragon.Point(tx, ty);
    },

    getTileBounds: function(level, x, y) {
        // work in scaled pixels for this level
        var dimensionsScaled = this.dimensions.times(this.getLevelScale(level));

        // find position, adjust for no overlap data on top and left edges
        var px = (x === 0) ? 0 : this.tileSize * x - this.tileOverlap;
        var py = (y === 0) ? 0 : this.tileSize * y - this.tileOverlap;

        // find size, adjust for no overlap data on top and left edges
        var sx = this.tileSize + (x === 0 ? 1 : 2) * this.tileOverlap;
        var sy = this.tileSize + (y === 0 ? 1 : 2) * this.tileOverlap;

        // adjust size for single-tile levels where the image size is smaller
        // than the regular tile size, and for tiles on the bottom and right
        // edges that would exceed the image bounds
        sx = Math.min(sx, dimensionsScaled.x - px);
        sy = Math.min(sy, dimensionsScaled.y - py);

        // finally, normalize...
        // note that isotropic coordinates ==> only dividing by scaled x!
        var scale = 1.0 / dimensionsScaled.x;
        return new Seadragon.Rect(px * scale, py * scale, sx * scale, sy * scale);
    },

    getTileUrl: function(level, x, y) {
        throw new Error("Method not implemented.");
    },

    tileExists: function(level, x, y) {
        var numTiles = this.getNumTiles(level);
        return level >= this.minLevel && level <= this.maxLevel &&
                x >= 0 && y >= 0 && x < numTiles.x && y < numTiles.y;
    }
};Seadragon.DziTileSource = function(width, height, tileSize, tileOverlap, tilesUrl, fileFormat, displayRects) {
	Seadragon.TileSource.apply(this, [width, height, tileSize, tileOverlap]);
    this._levelRects = {};
    this.tilesUrl = tilesUrl;

    this.fileFormat = fileFormat;
    this.displayRects = displayRects;
    
    if (!this.displayRects) {
        return;
    }
    for (var i = this.displayRects.length - 1; i >= 0; i--) {
        var rect = this.displayRects[i];
        for (var level = rect.minLevel; level <= rect.maxLevel; level++) {
            if (!this._levelRects[level]) {
                this._levelRects[level] = [];
            }
            this._levelRects[level].push(rect);
        }
    }    
};
Seadragon.DziTileSource.createFromXml = function(xmlUrl, callback) {
    // extract tile url
    var urlParts = xmlUrl.split('/');
    var filename = urlParts[urlParts.length - 1];
    var lastDot = filename.lastIndexOf('.');

    if (lastDot > -1) {
        urlParts[urlParts.length - 1] = filename.slice(0, lastDot);
    }

    var tilesUrl = urlParts.join('/') + "_files/";

    /*@todo need to handle errors on this, with .ajax*/
    $.get(xmlUrl, function(data) {
    	source = Seadragon.DziTileSource.processXml(data,tilesUrl);
    	callback(source,null);
    }, 'xml');
    
    return null;
};
Seadragon.DziTileSource.processXml = function(xml, tilesUrl) {
    var image = $($(xml).find("Image")[0]);
    if(!image) {
    	throw new Error(Seadragon.Strings.getString("Errors.Dzi"));
    }
    
    var fileFormat = image.attr("Format");
    var size = image.find("Size");
    
    
    var width = parseInt(size.attr("Width"), 10);
    var height = parseInt(size.attr("Height"), 10);
    var tileSize = parseInt(image.attr("TileSize"));
    var tileOverlap = parseInt(image.attr("Overlap"));
    var dispRects = [];  
    
    var rectNodes = image.find("DisplayRect Rect:first-child");
    rectNodes.each(function(i,rectNode) {
    	dispRects.push(
    			new Seadragon.DisplayRect(
    				parseInt(rectNode.attr("X"), 10),
    				parseInt(rectNode.attr("Y"), 10),
    				parseInt(rectNode.attr("Width"), 10),
    				parseInt(rectNode.attr("Height"), 10),
    				0,
    				parseInt(rectNode.attr("MaxLevel"), 10)
    			)
    	);
    });
    
    return new Seadragon.DziTileSource(width, height, tileSize, tileOverlap,
            tilesUrl, fileFormat, dispRects);        
};

Seadragon.DziTileSource.prototype = {
    getTileUrl: function(level, x, y) {
        // using array join because it's faster than string concatenation
        return [this.tilesUrl, level, '/', x, '_', y, '.', this.fileFormat].join('');
    },

    tileExists: function(level, x, y) {
        var rects = this._levelRects[level];

        if (!rects || !rects.length) {
            return true;
        }

        for (var i = rects.length - 1; i >= 0; i--) {
            var rect = rects[i];

            // check level
            if (level < rect.minLevel || level > rect.maxLevel) {
                continue;
            }

            // transform rectangle coordinates to this level
            var scale = this.getLevelScale(level);
            var xMin = rect.x * scale;
            var yMin = rect.y * scale;
            var xMax = xMin + rect.width * scale;
            var yMax = yMin + rect.height * scale;

            // convert to rows and columns -- note that we're ignoring tile
            // overlap, but it's a reasonable approximation. it errs on the side
            // of false positives, which is much better than false negatives.
            xMin = Math.floor(xMin / this.tileSize);
            yMin = Math.floor(yMin / this.tileSize);
            xMax = Math.ceil(xMax / this.tileSize);
            yMax = Math.ceil(yMax / this.tileSize);

            if (xMin <= x && x < xMax && yMin <= y && y < yMax) {
                return true;
            }
        }

        return false;
    }
};
Seadragon.DziTileSource.prototype = $.extend(Seadragon.TileSource.prototype,Seadragon.DziTileSource.prototype);Seadragon.Strings = {
    Errors: {
        Failure: "Sorry, but Seadragon Ajax can't run on your browser!\n" +
                    "Please try using IE 7 or Firefox 3.\n",
        Dzc: "Sorry, we don't support Deep Zoom Collections!",
        Dzi: "Hmm, this doesn't appear to be a valid Deep Zoom Image.",
        Xml: "Hmm, this doesn't appear to be a valid Deep Zoom Image.",
        Empty: "You asked us to open nothing, so we did just that.",
        ImageFormat: "Sorry, we don't support {0}-based Deep Zoom Images.",
        Security: "It looks like a security restriction stopped us from " +
                    "loading this Deep Zoom Image.",
        Status: "This space unintentionally left blank ({0} {1}).",
        Unknown: "Whoops, something inexplicably went wrong. Sorry!"
    },

    Messages: {
        Loading: "Loading..."
    },

    Tooltips: {
        FullPage: "Toggle full page",
        Home: "Go home",
        ZoomIn: "Zoom in",
        ZoomOut: "Zoom out"
    },
    getString: function(prop) {
        var props = prop.split('.');
        var string = Seadragon.Strings;

        // get property, which may contain dots, meaning subproperty
        for (var i = 0; i < props.length; i++) {
            string = string[props[i]] || {};    // in case not a subproperty
        }

        // in case the string didn't exist
        if (typeof (string) != "string") {
            string = "";
        }

        // regular expression and lambda technique from:
        // http://frogsbrain.wordpress.com/2007/04/28/javascript-stringformat-method/#comment-236
        var args = arguments;
        return string.replace(/\{\d+\}/g, function(capture) {
            var i = parseInt(capture.match(/\d+/)) + 1;
            return i < args.length ? args[i] : "";
        });
    },

    setString: function(prop, value) {
        var props = prop.split('.');
        var container = Seadragon.Strings;

        // get property's container, up to but not after last dot
        for (var i = 0; i < props.length - 1; i++) {
            if (!container[props[i]]) {
                container[props[i]] = {};
            }
            container = container[props[i]];
        }

        container[props[i]] = value;
    }

};Seadragon.Rect = function(x, y, width, height) {
    // Properties

    this.x = typeof (x) == "number" ? x : 0;
    this.y = typeof (y) == "number" ? y : 0;
    this.width = typeof (width) == "number" ? width : 0;
    this.height = typeof (height) == "number" ? height : 0;
};
Seadragon.Rect.prototype = {
    getAspectRatio: function() {
        return this.width / this.height;
    },

    getTopLeft: function() {
    return new Seadragon.Point(this.x, this.y);
    },

    getBottomRight: function() {
    return new Seadragon.Point(this.x + this.width, this.y + this.height);
    },

    getCenter: function() {
    return new Seadragon.Point(this.x + this.width / 2.0,
                        this.y + this.height / 2.0);
    },

    getSize: function() {
    return new Seadragon.Point(this.width, this.height);
    },

    equals: function(other) {
        return (other instanceof Seadragon.Rect) &&
                (this.x === other.x) && (this.y === other.y) &&
                (this.width === other.width) && (this.height === other.height);
    },

    toString: function() {
        return "[" + this.x + "," + this.y + "," + this.width + "x" +
                this.height + "]";
    }
};Seadragon.Spring = Seadragon.Spring = function(initialValue, config) {
	this._currentValue = typeof (initialValue) == "number" ? initialValue : 0;
	this._startValue = this._currentValue;
	this._targetValue = this._currentValue;
	this.config = config;

	this._currentTime = new Date().getTime(); // always work in milliseconds
	this._startTime = this._currentTime;
	this._targetTime = this._currentTime;
};
Seadragon.Spring.prototype = {
	_transform: function(x) {
		var s = this.config.springStiffness;
		return (1.0 - Math.exp(-x * s)) / (1.0 - Math.exp(-s));
	},
	getCurrent: function() {
		return this._currentValue;
	},

	getTarget: function() {
		return this._targetValue;
	},

	resetTo: function(target) {
		this._targetValue = target;
		this._targetTime = this._currentTime;
		this._startValue = this._targetValue;
		this._startTime = this._targetTime;
	},

	springTo: function(target) {
		this._startValue = this._currentValue;
		this._startTime = this._currentTime;
		this._targetValue = target;
		this._targetTime = this._startTime + 1000 * this.config.animationTime;
	},

	shiftBy: function(delta) {
		this._startValue += delta;
		this._targetValue += delta;
	},

	update: function() {
		this._currentTime = new Date().getTime();
		this._currentValue = (this._currentTime >= this._targetTime) ? this._targetValue :
                this._startValue + (this._targetValue - this._startValue) *
                this._transform((this._currentTime - this._startTime) / (this._targetTime - this._startTime));
	}
};Seadragon.Utils = function() {

    // Enumerations

    var Browser = {
        UNKNOWN: 0,
        IE: 1,
        FIREFOX: 2,
        SAFARI: 3,
        CHROME: 4,
        OPERA: 5
    };

    Seadragon.Browser = Browser;

    // Fields

    var self = this;

    var arrActiveX = ["Msxml2.XMLHTTP", "Msxml3.XMLHTTP", "Microsoft.XMLHTTP"];
    var fileFormats = {
        "bmp": false,
        "jpeg": true,
        "jpg": true,
        "png": true,
        "tif": false,
        "wdp": false
    };

    var browser = Browser.UNKNOWN;
    var browserVersion = 0;
    var badAlphaBrowser = false;    // updated in constructor

    var urlParams = {};

    // Constructor

    (function() {

        // Browser detect

        var app = navigator.appName;
        var ver = navigator.appVersion;
        var ua = navigator.userAgent;

        if (app == "Microsoft Internet Explorer" &&
                !!window.attachEvent && !!window.ActiveXObject) {

            var ieOffset = ua.indexOf("MSIE");
            browser = Browser.IE;
            browserVersion = parseFloat(
                    ua.substring(ieOffset + 5, ua.indexOf(";", ieOffset)));

        } else if (app == "Netscape" && !!window.addEventListener) {

            var ffOffset = ua.indexOf("Firefox");
            var saOffset = ua.indexOf("Safari");
            var chOffset = ua.indexOf("Chrome");

            if (ffOffset >= 0) {
                browser = Browser.FIREFOX;
                browserVersion = parseFloat(ua.substring(ffOffset + 8));
            } else if (saOffset >= 0) {
                var slash = ua.substring(0, saOffset).lastIndexOf("/");
                browser = (chOffset >= 0) ? Browser.CHROME : Browser.SAFARI;
                browserVersion = parseFloat(ua.substring(slash + 1, saOffset));
            }

        } else if (app == "Opera" && !!window.opera && !!window.attachEvent) {

            browser = Browser.OPERA;
            browserVersion = parseFloat(ver);

        }

        // Url parameters

        var query = window.location.search.substring(1);    // ignore '?'
        var parts = query.split('&');

        for (var i = 0; i < parts.length; i++) {
            var part = parts[i];
            var sep = part.indexOf('=');

            if (sep > 0) {
                urlParams[part.substring(0, sep)] =
                        decodeURIComponent(part.substring(sep + 1));
            }
        }

        // Browser behaviors

        // update: chrome 2 no longer has this problem!
        badAlphaBrowser = (browser == Browser.IE ||
                (browser == Browser.CHROME && browserVersion < 2));

    })();

    // Helpers

    function getOffsetParent(elmt, isFixed) {
        // IE and Opera "fixed" position elements don't have offset parents.
        // regardless, if it's fixed, its offset parent is the body.
        if (isFixed && elmt != document.body) {
            return document.body;
        } else {
            return elmt.offsetParent;
        }
    }

    // Methods

    this.getBrowser = function() {
        return browser;
    };

    this.getBrowserVersion = function() {
        return browserVersion;
    };

    this.getElement = function(elmt) {
        if (typeof (elmt) == "string") {
            elmt = document.getElementById(elmt);
        }

        return elmt;
    };

    this.getElementPosition = function(elmt) {
        var elmt = self.getElement(elmt);
        var result = new Seadragon.Point();

        // technique from:
        // http://www.quirksmode.org/js/findpos.html
        // with special check for "fixed" elements.

        var isFixed = self.getElementStyle(elmt).position == "fixed";
        var offsetParent = getOffsetParent(elmt, isFixed);

        while (offsetParent) {
            result.x += elmt.offsetLeft;
            result.y += elmt.offsetTop;

            if (isFixed) {
                result = result.plus(self.getPageScroll());
            }

            elmt = offsetParent;
            isFixed = self.getElementStyle(elmt).position == "fixed";
            offsetParent = getOffsetParent(elmt, isFixed);
        }

        return result;
    };

    this.getElementSize = function(elmt) {
        var elmt = self.getElement(elmt);
        return new Seadragon.Point(elmt.clientWidth, elmt.clientHeight);
    };

    this.getElementStyle = function(elmt) {
        var elmt = self.getElement(elmt);

        if (elmt.currentStyle) {
            return elmt.currentStyle;
        } else if (window.getComputedStyle) {
            return window.getComputedStyle(elmt, "");
        } else {
            //Seadragon.Debug.fail("Unknown element style, no known technique.");
        }
    };

    this.getEvent = function(event) {
        return event ? event : window.event;
    };

    this.getMousePosition = function(event) {
        var event = self.getEvent(event);
        var result = new Seadragon.Point();

        // technique from:
        // http://www.quirksmode.org/js/events_properties.html

        if (typeof (event.pageX) == "number") {
            result.x = event.pageX;
            result.y = event.pageY;
        } else if (typeof (event.clientX) == "number") {
            result.x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
            result.y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
        } else {
            //Seadragon.Debug.fail("Unknown event mouse position, no known technique.");
        }

        return result;
    };

    this.getPageScroll = function() {
        var result = new Seadragon.Point();
        var docElmt = document.documentElement || {};
        var body = document.body || {};

        // technique from:
        // http://www.howtocreate.co.uk/tutorials/javascript/browserwindow

        if (typeof (window.pageXOffset) == "number") {
            // most browsers
            result.x = window.pageXOffset;
            result.y = window.pageYOffset;
        } else if (body.scrollLeft || body.scrollTop) {
            // W3C spec, IE6+ in quirks mode
            result.x = body.scrollLeft;
            result.y = body.scrollTop;
        } else if (docElmt.scrollLeft || docElmt.scrollTop) {
            // IE6+ in standards mode
            result.x = docElmt.scrollLeft;
            result.y = docElmt.scrollTop;
        }

        // note: we specifically aren't testing for typeof here, because IE sets
        // the appropriate variables undefined instead of 0 under certain
        // conditions. this means we also shouldn't fail if none of the three
        // cases are hit; we'll just assume the page scroll is 0.

        return result;
    };

    this.getWindowSize = function() {
        var result = new Seadragon.Point();
        var docElmt = document.documentElement || {};
        var body = document.body || {};

        // technique from:
        // http://www.howtocreate.co.uk/tutorials/javascript/browserwindow

        // important: i originally cleaned up the second and third IE checks to
        // check if the typeof was number. but this fails for quirks mode,
        // because docElmt.clientWidth is indeed a number, but it's incorrectly
        // zero. so no longer checking typeof is number for those cases.

        if (typeof (window.innerWidth) == 'number') {
            // non-IE browsers
            result.x = window.innerWidth;
            result.y = window.innerHeight;
        } else if (docElmt.clientWidth || docElmt.clientHeight) {
            // IE6+ in standards mode
            result.x = docElmt.clientWidth;
            result.y = docElmt.clientHeight;
        } else if (body.clientWidth || body.clientHeight) {
            // IE6+ in quirks mode
            result.x = body.clientWidth;
            result.y = body.clientHeight;
        } else {
            //Seadragon.Debug.fail("Unknown window size, no known technique.");
        }

        return result;
    };

    this.imageFormatSupported = function(ext) {
        var ext = ext ? ext : "";
        return !!fileFormats[ext.toLowerCase()];
    };

    this.makeCenteredNode = function(elmt) {
        var elmt = Seadragon.Utils.getElement(elmt);
        var div = self.makeNeutralElement("div");
        var html = [];

        // technique for vertically centering (in IE!!!) from:
        // http://www.jakpsatweb.cz/css/css-vertical-center-solution.html
        // with explicit neutralizing of styles added by me.
        html.push('<div style="display:table; height:100%; width:100%;');
        html.push('border:none; margin:0px; padding:0px;'); // neutralizing
        html.push('#position:relative; overflow:hidden; text-align:left;">');
        // the text-align:left guards against incorrect centering in IE
        html.push('<div style="#position:absolute; #top:50%; width:100%; ');
        html.push('border:none; margin:0px; padding:0px;'); // neutralizing
        html.push('display:table-cell; vertical-align:middle;">');
        html.push('<div style="#position:relative; #top:-50%; width:100%; ');
        html.push('border:none; margin:0px; padding:0px;'); // neutralizing
        html.push('text-align:center;"></div></div></div>');

        div.innerHTML = html.join('');
        div = div.firstChild;

        // now add the element as a child to the inner-most div
        var innerDiv = div;
        var innerDivs = div.getElementsByTagName("div");
        while (innerDivs.length > 0) {
            innerDiv = innerDivs[0];
            innerDivs = innerDiv.getElementsByTagName("div");
        }

        innerDiv.appendChild(elmt);

        return div;
    };

    this.makeNeutralElementStyle = function(tagName, style) {
    	style = "background: transparent none; border: none; margin: 0px; padding: 0px; position:static;" + style;
    	return $("<"+tagName+' style="'+style+'"/>');
    };
    
    this.makeNeutralElement = function(tagName) {
        var elmt = document.createElement(tagName);
        var style = elmt.style;

        // TODO reset neutral element's style in a better way
        style.background = "transparent none";
        style.border = "none";
        style.margin = "0px";
        style.padding = "0px";
        style.position = "static";

        return elmt;
    };

    this.makeTransparentImage = function(src) {
        var img = self.makeNeutralElement("img");
        var elmt = null;

        if (browser == Browser.IE && browserVersion < 7) {
            elmt = self.makeNeutralElement("span");
            elmt.style.display = "inline-block";

            // to size span correctly, load image and get natural size,
            // but don't override any user-set CSS values
            img.onload = function() {
                elmt.style.width = elmt.style.width || img.width + "px";
                elmt.style.height = elmt.style.height || img.height + "px";

                img.onload = null;
                img = null;     // to prevent memory leaks in IE
            };

            img.src = src;
            elmt.style.filter =
                    "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" +
                    src + "', sizingMethod='scale')";
        } else {
            elmt = img;
            elmt.src = src;
        }

        return elmt;
    };

    this.setElementOpacity = function(elmt, opacity, usesAlpha) {
        var elmt = self.getElement(elmt);

        if (usesAlpha && badAlphaBrowser) {
            // images with alpha channels won't fade well, so round
            opacity = Math.round(opacity);
        }

        // for CSS opacity browsers, remove opacity value if it's unnecessary
        if (opacity < 1) {
            elmt.style.opacity = opacity;
        } else {
            elmt.style.opacity = "";
        }

        // for CSS filter browsers (IE), remove alpha filter if it's unnecessary
        if (opacity == 1) {
            var prevFilter = elmt.style.filter || "";
            elmt.style.filter = prevFilter.replace(/alpha\(.*?\)/g, "");
            // important: note the lazy star! this protects against
            // multiple filters; we don't want to delete the other ones.
            return;
        }

        var ieOpacity = Math.round(100 * opacity);
        var ieFilter = " alpha(opacity=" + ieOpacity + ") ";

        // check if this element has filters associated with it (IE only),
        // but prevent bug where IE throws error "Member not found" sometimes.
        try {
            if (elmt.filters && elmt.filters.alpha) {
                elmt.filters.alpha.opacity = ieOpacity;
            } else {
                elmt.style.filter += ieFilter;
            }
        } catch (e) {
            elmt.style.filter += ieFilter;
        }
    };

    this.addEvent = function(elmt, eventName, handler, useCapture) {
        var elmt = self.getElement(elmt);

        // technique from:
        // http://blog.paranoidferret.com/index.php/2007/08/10/javascript-working-with-events/

        if (elmt.addEventListener) {
            elmt.addEventListener(eventName, handler, useCapture);
        } else if (elmt.attachEvent) {
            elmt.attachEvent("on" + eventName, handler);
            if (useCapture && elmt.setCapture) {
                elmt.setCapture();
            }
        } else {
            //Seadragon.Debug.fail("Unable to attach event handler, no known technique.");
        }
    };

    this.removeEvent = function(elmt, eventName, handler, useCapture) {
        var elmt = self.getElement(elmt);

        // technique from:
        // http://blog.paranoidferret.com/index.php/2007/08/10/javascript-working-with-events/

        if (elmt.removeEventListener) {
            elmt.removeEventListener(eventName, handler, useCapture);
        } else if (elmt.detachEvent) {
            elmt.detachEvent("on" + eventName, handler);
            if (useCapture && elmt.releaseCapture) {
                elmt.releaseCapture();
            }
        } else {
            //Seadragon.Debug.fail("Unable to detach event handler, no known technique.");
        }
    };

    this.cancelEvent = function(event) {
        var event = self.getEvent(event);

        // technique from:
        // http://blog.paranoidferret.com/index.php/2007/08/10/javascript-working-with-events/

        if (event.preventDefault) {
            event.preventDefault();     // W3C for preventing default
        }

        event.cancel = true;            // legacy for preventing default
        event.returnValue = false;      // IE for preventing default
    };

    this.stopEvent = function(event) {
        var event = self.getEvent(event);

        // technique from:
        // http://blog.paranoidferret.com/index.php/2007/08/10/javascript-working-with-events/

        if (event.stopPropagation) {
            event.stopPropagation();    // W3C for stopping propagation
        }

        event.cancelBubble = true;      // IE for stopping propagation
    };

    this.createCallback = function(object, method) {
        // create callback args
        var initialArgs = [];
        for (var i = 2; i < arguments.length; i++) {
            initialArgs.push(arguments[i]);
        }

        // create closure to apply method
        return function() {
            // concatenate new args, but make a copy of initialArgs first
            var args = initialArgs.concat([]);
            for (var i = 0; i < arguments.length; i++) {
                args.push(arguments[i]);
            }

            return method.apply(object, args);
        };
    };

    this.getUrlParameter = function(key) {
        var value = urlParams[key];
        return value ? value : null;
    };

    this.makeAjaxRequest = function(url, callback) {
        var async = typeof (callback) == "function";
        var req = null;

        if (async) {
            var actual = callback;
            var callback = function() {
                window.setTimeout(Seadragon.Utils.createCallback(null, actual, req), 1);
            };
        }

        if (window.ActiveXObject) {
            for (var i = 0; i < arrActiveX.length; i++) {
                try {
                    req = new ActiveXObject(arrActiveX[i]);
                    break;
                } catch (e) {
                    continue;
                }
            }
        } else if (window.XMLHttpRequest) {
            req = new XMLHttpRequest();
        }

        if (!req) {
            //Seadragon.Debug.fail("Browser doesn't support XMLHttpRequest.");
        }

        // Proxy support
//        if (Seadragon.Config.proxyUrl) {
//            url = Seadragon.Config.proxyUrl + url;
//        }

        if (async) {
            req.onreadystatechange = function() {
                if (req.readyState == 4) {
                    // prevent memory leaks by breaking circular reference now
                    req.onreadystatechange = new Function();
                    callback();
                }
            };
        }

        try {
            req.open("GET", url, async);
            req.send(null);
        } catch (e) {
            //Seadragon.Debug.log(e.name + " while making AJAX request: " + e.message);

            req.onreadystatechange = null;
            req = null;

            if (async) {
                callback();
            }
        }

        return async ? null : req;
    };

    this.parseXml = function(string) {
        var xmlDoc = null;

        if (window.ActiveXObject) {
            try {
                xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
                xmlDoc.async = false;
                xmlDoc.loadXML(string);
            } catch (e) {
                //Seadragon.Debug.log(e.name + " while parsing XML (ActiveX): " + e.message);
            }
        } else if (window.DOMParser) {
            try {
                var parser = new DOMParser();
                xmlDoc = parser.parseFromString(string, "text/xml");
            } catch (e) {
                //Seadragon.Debug.log(e.name + " while parsing XML (DOMParser): " + e.message);
            }
        } else {
            //Seadragon.Debug.fail("Browser doesn't support XML DOM.");
        }

        return xmlDoc;
    };

};
// Seadragon.Utils is a static class, so make it singleton instance
Seadragon.Utils = new Seadragon.Utils();
Seadragon.Job = function(src, callback) {
    this._image = null;
    this._timeout = null;
    this._src = src;
    this._callback = callback;
    this.TIMEOUT = 5000;
};
Seadragon.Job.prototype = {
    _finish: function(success) {
        this._image.onload = null;
        this._image.onabort = null;
        this._image.onerror = null;


        if (this._timeout) {
            window.clearTimeout(this._timeout);
        }

        // call on a timeout to ensure asynchronous behavior
        var image = this._image;
        var callback = this._callback;
        window.setTimeout(function() {
            callback(this._src, success ? image : null);
        }, 1);
    },
    _onloadHandler: function() {
        this._finish(true);
    },
    _onerrorHandler: function() {
        this._finish(false);
    },
    start: function() {
        this._image = new Image();
        this._image.onload = Function.createDelegate(this, this._onloadHandler);
        this._image.onabort = Function.createDelegate(this, this._onerrorHandler);
        this._image.onerror = Function.createDelegate(this, this._onerrorHandler);

        // consider it a failure if the image times out.
        this._timeout = window.setTimeout(Function.createDelegate(this, this._onerrorHandler), this.TIMEOUT);

        this._image.src = this._src;
    }
};

Seadragon.ImageLoader = function(imageLoaderLimit) {
	this._downloading = 0;
	this.imageLoaderLimit = imageLoaderLimit;
};
Seadragon.ImageLoader.prototype = {
    _onComplete: function(callback, src, image) {
        this._downloading--;
        if (typeof (callback) == "function") {
            try {
                callback(image);
            } catch (e) {
                //Seadragon.Debug.error(e.name + " while executing " + src +
                //            " callback: " + e.message, e);
            }
        }
    },
    loadImage: function(src, callback) {
        if (this._downloading >= this.imageLoaderLimit) {
            return false;
        }

        var func = Seadragon.Utils.createCallback(null, Function.createDelegate(this, this._onComplete), callback);
        var job = new Seadragon.Job(src, func);

        this._downloading++;
        job.start();

        return true;
    }
};Seadragon.DisplayRect = function(x, y, width, height, minLevel, maxLevel) {
	Seadragon.Rect.apply(this, arguments);

    this.minLevel = minLevel;
    this.maxLevel = maxLevel;
};
Seadragon.DisplayRect.prototype = new Seadragon.Rect();Seadragon.Viewport = function(containerSize, contentSize, config) {
	this.zoomPoint = null;
	this.config = config;
	this._containerSize = containerSize;
	this._contentSize = contentSize;
	this._contentAspect = contentSize.x / contentSize.y;
	this._contentHeight = contentSize.y / contentSize.x;
	this._centerSpringX = new Seadragon.Spring(0, this.config);
	this._centerSpringY = new Seadragon.Spring(0, this.config);
	this._zoomSpring = new Seadragon.Spring(1, this.config);
	this._homeBounds = new Seadragon.Rect(0, 0, 1, this._contentHeight);
	this.goHome(true);
	this.update();
};
Seadragon.Viewport.prototype = {
	_getHomeZoom: function() {
		var aspectFactor = this._contentAspect / this.getAspectRatio();
		// if content is wider, we'll fit width, otherwise height
		return (aspectFactor >= 1) ? 1 : aspectFactor;
	},

	_getMinZoom: function() {
		var homeZoom = this._getHomeZoom();

		// for backwards compatibility, respect minZoomDimension if present
		if (this.config.minZoomDimension) {
			var zoom = (this._contentSize.x <= this._contentSize.y) ?
                this.config.minZoomDimension / this._containerSize.x :
                this.config.minZoomDimension / (this._containerSize.x * this._contentHeight);
		} else {
			var zoom = this.config.minZoomImageRatio * homeZoom;
		}

		return Math.min(zoom, homeZoom);
	},

	_getMaxZoom: function() {
		var zoom = this._contentSize.x * this.config.maxZoomPixelRatio / this._containerSize.x;
		return Math.max(zoom, this._getHomeZoom());
	},
	getAspectRatio: function() {
		return this._containerSize.x / this._containerSize.y;
	},
	getContainerSize: function() {
		return new Seadragon.Point(this._containerSize.x, this._containerSize.y);
	},

	getBounds: function(current) {
		var center = this.getCenter(current);
		var width = 1.0 / this.getZoom(current);
		var height = width / this.getAspectRatio();

		return new Seadragon.Rect(center.x - width / 2.0, center.y - height / 2.0,
            width, height);
	},

	getCenter: function(current) {
		var centerCurrent = new Seadragon.Point(this._centerSpringX.getCurrent(),
                this._centerSpringY.getCurrent());
		var centerTarget = new Seadragon.Point(this._centerSpringX.getTarget(),
                this._centerSpringY.getTarget());

		if (current) {
			return centerCurrent;
		} else if (!this.zoomPoint) {
			// no adjustment necessary since we're not zooming
			return centerTarget;
		}

		// to get the target center, we need to adjust for the zoom point.
		// we'll do this in the same way as the update() method.
		var oldZoomPixel = this.pixelFromPoint(this.zoomPoint, true);

		// manually calculate bounds based on this unadjusted target center.
		// this is mostly a duplicate of getBounds() above. note that this is
		// based on the TARGET zoom but the CURRENT center.
		var zoom = this.getZoom();
		var width = 1.0 / zoom;
		var height = width / this.getAspectRatio();
		var bounds = new Seadragon.Rect(centerCurrent.x - width / 2.0,
                centerCurrent.y - height / 2.0, width, height);

		// the conversions here are identical to the pixelFromPoint() and
		// deltaPointsFromPixels() methods.
		var newZoomPixel = this.zoomPoint.minus(bounds.getTopLeft()).times(this._containerSize.x / bounds.width);
		var deltaZoomPixels = newZoomPixel.minus(oldZoomPixel);
		var deltaZoomPoints = deltaZoomPixels.divide(this._containerSize.x * zoom);

		// finally, shift center to negate the change.
		return centerTarget.plus(deltaZoomPoints);
	},

	getZoom: function(current) {
		if (current) {
			return this._zoomSpring.getCurrent();
		} else {
			return this._zoomSpring.getTarget();
		}
	},

	// Methods -- MODIFIERS

	applyConstraints: function(immediately) {
		// first, apply zoom constraints
		var actualZoom = this.getZoom();
		var constrainedZoom = Math.max(Math.min(actualZoom, this._getMaxZoom()), this._getMinZoom());
		if (actualZoom != constrainedZoom) {
			this.zoomTo(constrainedZoom, this.zoomPoint, immediately);
		}

		// then, apply pan constraints
		var bounds = this.getBounds();
		var visibilityRatio = this.config.visibilityRatio;

		// threshold in normalized coordinates
		var horThres = visibilityRatio * bounds.width;
		var verThres = visibilityRatio * bounds.height;

		// amount visible in normalized coordinates
		var left = bounds.x + bounds.width;
		var right = 1 - bounds.x;
		var top = bounds.y + bounds.height;
		var bottom = this._contentHeight - bounds.y;

		// adjust viewport horizontally -- in normalized coordinates!
		var dx = 0;
		if (this.config.wrapHorizontal) {
			// nothing to constrain
		} else if (left < horThres) {
			dx = horThres - left;
		} else if (right < horThres) {
			dx = right - horThres;
		}

		// adjust viewport vertically -- in normalized coordinates!
		var dy = 0;
		if (this.config.wrapVertical) {
			// nothing to constrain
		} else if (top < verThres) {
			dy = verThres - top;
		} else if (bottom < verThres) {
			dy = bottom - verThres;
		}

		// pan if we aren't zooming, otherwise set the zoom point if we are.
		// we've already implemented logic in fitBounds() for this.
		if (dx || dy) {
			bounds.x += dx;
			bounds.y += dy;
			this.fitBounds(bounds, immediately);
		}
	},

	ensureVisible: function(immediately) {
		// for backwards compatibility
		this.applyConstraints(immediately);
	},

	fitBounds: function(bounds, immediately) {
		var aspect = this.getAspectRatio();
		var center = bounds.getCenter();

		// resize bounds to match viewport's aspect ratio, maintaining center.
		// note that zoom = 1/width, and width = height*aspect.
		var newBounds = new Seadragon.Rect(bounds.x, bounds.y, bounds.width, bounds.height);
		if (newBounds.getAspectRatio() >= aspect) {
			// width is bigger relative to viewport, resize height
			newBounds.height = bounds.width / aspect;
			newBounds.y = center.y - newBounds.height / 2;
		} else {
			// height is bigger relative to viewport, resize width
			newBounds.width = bounds.height * aspect;
			newBounds.x = center.x - newBounds.width / 2;
		}

		// stop movement first! this prevents the operation from missing
		this.panTo(this.getCenter(true), true);
		this.zoomTo(this.getZoom(true), null, true);

		// capture old values for bounds and width. we need both, but we'll
		// also use both for redundancy, to protect against precision errors.
		// note: use target bounds, since update() hasn't been called yet!
		var oldBounds = this.getBounds();
		var oldZoom = this.getZoom();

		// if we're already at the correct zoom, just pan and we're done.
		// we'll check both zoom and bounds for redundancy, to protect against
		// precision errors (see note below).
		var newZoom = 1.0 / newBounds.width;
		if (newZoom == oldZoom || newBounds.width == oldBounds.width) {
			this.panTo(center, immediately);
			return;
		}

		// otherwise, we need to zoom about the only point whose pixel transform
		// is constant between the old and new bounds. this is just tricky math.
		var refPoint = oldBounds.getTopLeft().times(this._containerSize.x / oldBounds.width).minus(
                newBounds.getTopLeft().times(this._containerSize.x / newBounds.width)).divide(
                this._containerSize.x / oldBounds.width - this._containerSize.x / newBounds.width);

		// note: that last line (cS.x / oldB.w - cS.x / newB.w) was causing a
		// divide by 0 in the case that oldBounds.width == newBounds.width.
		// that should have been picked up by the zoom check, but in certain
		// cases, the math is slightly off and the zooms are different. so now,
		// the zoom check has an extra check added.

		this.zoomTo(newZoom, refPoint, immediately);
	},

	goHome: function(immediately) {
		// calculate center adjusted for zooming
		var center = this.getCenter();

		// if we're wrapping horizontally, "unwind" the horizontal spring
		if (this.config.wrapHorizontal) {
			// this puts center.x into the range [0, 1) always
			center.x = (1 + (center.x % 1)) % 1;
			this._centerSpringX.resetTo(center.x);
			this._centerSpringX.update();
		}

		// if we're wrapping vertically, "unwind" the vertical spring
		if (this.config.wrapVertical) {
			// this puts center.y into the range e.g. [0, 0.75) always
			center.y = (this._contentHeight + (center.y % this._contentHeight)) % this._contentHeight;
			this._centerSpringY.resetTo(center.y);
			this._centerSpringY.update();
		}

		this.fitBounds(this._homeBounds, immediately);
	},

	panBy: function(delta, immediately) {
		// this breaks if we call self.getCenter(), since that adjusts the
		// center for zoom. we don't want that, so use the unadjusted center.
		var center = new Seadragon.Point(this._centerSpringX.getTarget(),
                this._centerSpringY.getTarget());
		this.panTo(center.plus(delta), immediately);
	},

	panTo: function(center, immediately) {
		if (immediately) {
			this._centerSpringX.resetTo(center.x);
			this._centerSpringY.resetTo(center.y);
		} else {
			this._centerSpringX.springTo(center.x);
			this._centerSpringY.springTo(center.y);
		}
	},

	zoomBy: function(factor, refPoint, immediately) {
		this.zoomTo(this._zoomSpring.getTarget() * factor, refPoint, immediately);
	},

	zoomTo: function(zoom, refPoint, immediately) {
		// we used to constrain zoom automatically here; now it needs to be
		// explicitly constrained, via applyConstraints().
		//zoom = Math.max(zoom, getMinZoom());
		//zoom = Math.min(zoom, getMaxZoom());

		if (immediately) {
			this._zoomSpring.resetTo(zoom);
		} else {		
			this._zoomSpring.springTo(zoom);
		}

		this.zoomPoint = refPoint instanceof Seadragon.Point ? refPoint : null;
	},

	resize: function(newContainerSize, maintain) {
		// default behavior: just ensure the visible content remains visible.
		// note that this keeps the center (relative to the content) constant.
		var oldBounds = this.getBounds();
		var newBounds = oldBounds;
		var widthDeltaFactor = newContainerSize.x / this._containerSize.x;

		// update container size, but make copy first
		this._containerSize = new Seadragon.Point(newContainerSize.x, newContainerSize.y);

		if (maintain) {
			// no resize relative to screen, resize relative to viewport.
			// keep origin constant, zoom out (increase bounds) by delta factor.
			newBounds.width = oldBounds.width * widthDeltaFactor;
			newBounds.height = newBounds.width / this.getAspectRatio();
		}

		this.fitBounds(newBounds, true);
	},

	update: function() {
		var oldCenterX = this._centerSpringX.getCurrent();
		var oldCenterY = this._centerSpringY.getCurrent();
		var oldZoom = this._zoomSpring.getCurrent();

		// remember position of zoom point
		if (this.zoomPoint) {
			var oldZoomPixel = this.pixelFromPoint(this.zoomPoint, true);
		}

		// now update zoom only, don't update pan yet
		this._zoomSpring.update();

		// adjust for change in position of zoom point, if we've zoomed
		if (this.zoomPoint && this._zoomSpring.getCurrent() != oldZoom) {
			var newZoomPixel = this.pixelFromPoint(this.zoomPoint, true);
			var deltaZoomPixels = newZoomPixel.minus(oldZoomPixel);
			var deltaZoomPoints = this.deltaPointsFromPixels(deltaZoomPixels, true);

			// shift pan to negate the change
			this._centerSpringX.shiftBy(deltaZoomPoints.x);
			this._centerSpringY.shiftBy(deltaZoomPoints.y);
		} else {
			// don't try to adjust next time; this improves performance
			this.zoomPoint = null;
		}

		// now after adjustment, update pan
		this._centerSpringX.update();
		this._centerSpringY.update();

		return this._centerSpringX.getCurrent() != oldCenterX ||
                this._centerSpringY.getCurrent() != oldCenterY ||
                this._zoomSpring.getCurrent() != oldZoom;
	},

	// Methods -- CONVERSION HELPERS

	deltaPixelsFromPoints: function(deltaPoints, current) {
		return deltaPoints.times(this._containerSize.x * this.getZoom(current));
	},

	deltaPointsFromPixels: function(deltaPixels, current) {
		return deltaPixels.divide(this._containerSize.x * this.getZoom(current));
	},

	pixelFromPoint: function(point, current) {
		var bounds = this.getBounds(current);
		return point.minus(bounds.getTopLeft()).times(this._containerSize.x / bounds.width);
	},

	pointFromPixel: function(pixel, current) {
		var bounds = this.getBounds(current);
		return pixel.divide(this._containerSize.x / bounds.width).plus(bounds.getTopLeft());
	}
};// Constants

var QUOTA = 100;    // the max number of images we should keep in memory
var MIN_PIXEL_RATIO = 0.5;  // the most shrunk a tile should be

// Method of drawing

var browser = Seadragon.Utils.getBrowser();
var browserVer = Seadragon.Utils.getBrowserVersion();

// only Firefox and Opera implement <canvas> with subpixel rendering.
// update: safari 4 does too now! update: and chrome 2!
var subpixelRenders = browser == Seadragon.Browser.FIREFOX ||
            browser == Seadragon.Browser.OPERA ||
            (browser == Seadragon.Browser.SAFARI && browserVer >= 4) ||
            (browser == Seadragon.Browser.CHROME && browserVer >= 2);

// make sure browser supports <canvas>, and only use it if we know browser
// does subpixel rendering with <canvas> (that's the main advantage)
var useCanvas =
            typeof (document.createElement("canvas").getContext) == "function" &&
            subpixelRenders;
Seadragon.Tile = function(level, x, y, bounds, exists, url) {
    // Core
    this.level = level;
    this.x = x;
    this.y = y;
    this.bounds = bounds;   // where this tile fits, in normalized coordinates
    this.exists = exists;   // part of sparse image? tile hasn't failed to load?
    this.loaded = false;    // is this tile loaded?
    this.loading = false;   // or is this tile loading?



    // Image
    this.elmt = null;       // the HTML element for this tile
    this.image = null;      // the Image object for this tile
    this.url = url;         // the URL of this tile's image


    // Drawing
    this.style = null;      // alias of this.elmt.style
    this.position = null;   // this tile's position on screen, in pixels
    this.size = null;       // this tile's size on screen, in pixels
    this.blendStart = null; // the start time of this tile's blending
    this.opacity = null;    // the current opacity this tile should be
    this.distance = null;   // the distance of this tile to the viewport center
    this.visibility = null; // the visibility score of this tile

    // Caching
    this.beingDrawn = false; // whether this tile is currently being drawn
    this.lastTouchTime = 0; // the time that tile was last touched
};
Seadragon.Tile.prototype = {
    dispose: function() {
    },
    toString: function() {
        return this.level + "/" + this.x + "_" + this.y;
    },
    drawHTML: function(container) {
        if (!this.loaded) {
            //Seadragon.Debug.error("Attempting to draw tile " + this.toString() +
            //        " when it's not yet loaded.");
            return;
        }

        // initialize if first time
        if (!this.elmt) {
            this.elmt = Seadragon.Utils.makeNeutralElement("img");
            this.elmt.src = this.url;
            this.style = this.elmt.style;
            this.style.position = "absolute";
            this.style.msInterpolationMode = "nearest-neighbor";
            // IE only property. bicubic is ideal, but it causes seams.
            // explicitly use nearest-neighbor so it's not overridden to
            // bicubic on page zoom.
        }

        var elmt = this.elmt;
        var style = this.style;
        var position = this.position.apply(Math.floor);
        var size = this.size.apply(Math.ceil);

        // this was an alternate idea to hopefully make the rendering more
        // accurate, by prioritizing the edges of the image and not the size,
        // but it hasn't seemed to make any difference.
        //var topLeft = this.position;
        //var bottomRight = topLeft.plus(this.size);
        //var position = topLeft.apply(Math.floor);
        //var size = bottomRight.minus(topLeft).apply(Math.floor);

        if (elmt.parentNode != container) {
            container.appendChild(elmt);
        }

        style.left = position.x + "px";
        style.top = position.y + "px";
        style.width = size.x + "px";
        style.height = size.y + "px";

        Seadragon.Utils.setElementOpacity(elmt, this.opacity);
    },
    drawCanvas: function(context) {
        if (!this.loaded) {
            //Seadragon.Debug.error("Attempting to draw tile " + this.toString() +
            //        " when it's not yet loaded.");
            return;
        }

        var position = this.position;
        var size = this.size;

        context.globalAlpha = this.opacity;
        context.drawImage(this.image, position.x, position.y, size.x, size.y);
    },
    unload: function() {
        if (this.elmt && this.elmt.parentNode) {
            this.elmt.parentNode.removeChild(this.elmt);
        }

        this.elmt = null;
        this.image = null;
        this.loaded = false;
        this.loading = false;
    }
};

Seadragon.Overlay = function(elmt, loc, placement) {
    // Core
    this.elmt = elmt;
    this.scales = (loc instanceof Seadragon.Rect);
    this.bounds = new Seadragon.Rect(loc.x, loc.y, loc.width, loc.height);
    // Drawing
    this.placement = loc instanceof Seadragon.Point ? placement : Seadragon.OverlayPlacement.TOP_LEFT;    // rects are always top-left
    this.position = new Seadragon.Point(loc.x, loc.y);
    this.size = new Seadragon.Point(loc.width, loc.height);
    this.style = elmt.style;
};
Seadragon.Overlay.prototype = {

    adjust: function(position, size) {
        switch (this.placement) {
            case Seadragon.OverlayPlacement.TOP_LEFT:
                break;
            case Seadragon.OverlayPlacement.TOP:
                position.x -= size.x / 2;
                break;
            case Seadragon.OverlayPlacement.TOP_RIGHT:
                position.x -= size.x;
                break;
            case Seadragon.OverlayPlacement.RIGHT:
                position.x -= size.x;
                position.y -= size.y / 2;
                break;
            case Seadragon.OverlayPlacement.BOTTOM_RIGHT:
                position.x -= size.x;
                position.y -= size.y;
                break;
            case Seadragon.OverlayPlacement.BOTTOM:
                position.x -= size.x / 2;
                position.y -= size.y;
                break;
            case Seadragon.OverlayPlacement.BOTTOM_LEFT:
                position.y -= size.y;
                break;
            case Seadragon.OverlayPlacement.LEFT:
                position.y -= size.y / 2;
                break;
            case Seadragon.OverlayPlacement.CENTER:
            default:
                position.x -= size.x / 2;
                position.y -= size.y / 2;
                break;
        }
    },
    destroy: function() {
        var elmt = this.elmt;
        var style = this.style;

        if (elmt.parentNode) {
            elmt.parentNode.removeChild(elmt);
        }

        style.top = "";
        style.left = "";
        style.position = "";

        if (this.scales) {
            style.width = "";
            style.height = "";
        }
    },
    drawHTML: function(container) {
        var elmt = this.elmt;
        var style = this.style;
        var scales = this.scales;

        if (elmt.parentNode != container) {
            container.appendChild(elmt);
        }

        // override calculated size if this element doesn't scale with image
        if (!scales) {
            this.size = Seadragon.Utils.getElementSize(elmt);
        }

        var position = this.position;
        var size = this.size;

        // adjust position based on placement (default is center)
        this.adjust(position, size);

        position = position.apply(Math.floor);
        size = size.apply(Math.ceil);

        style.left = position.x + "px";
        style.top = position.y + "px";
        style.position = "absolute";

        if (scales) {
            style.width = size.x + "px";
            style.height = size.y + "px";
        }
    },
    update: function(loc, placement) {
        this.scales = (loc instanceof Seadragon.Rect);
        this.bounds = new Seadragon.Rect(loc.x, loc.y, loc.width, loc.height);
        this.placement = loc instanceof Seadragon.Point ?
                placement : Seadragon.OverlayPlacement.TOP_LEFT;    // rects are always top-left
    }

};

Seadragon.Drawer = function(source, viewport, elmt) {

	this._container = Seadragon.Utils.getElement(elmt);
	this._canvas = Seadragon.Utils.makeNeutralElement(useCanvas ? "canvas" : "div");
	this._context = useCanvas ? this._canvas.getContext("2d") : null;
	this._viewport = viewport;
	this._source = source;
	this.config = this._viewport.config;

	this._imageLoader = new Seadragon.ImageLoader(this.config.imageLoaderLimit);

	this._minLevel = source.minLevel;
	this._maxLevel = source.maxLevel;
	this._tileSize = source.tileSize;
	this._tileOverlap = source.tileOverlap;
	this._normHeight = source.dimensions.y / source.dimensions.x;

	this._cacheNumTiles = {};     // 1d dictionary [level] --> Point
	this._cachePixelRatios = {};  // 1d dictionary [level] --> Point
	this._tilesMatrix = {};       // 3d dictionary [level][x][y] --> Tile
	this._tilesLoaded = [];       // unordered list of Tiles with loaded images
	this._coverage = {};          // 3d dictionary [level][x][y] --> Boolean

	this._overlays = [];          // unordered list of Overlays added
	this._lastDrawn = [];         // unordered list of Tiles drawn last frame
	this._lastResetTime = 0;
	this._midUpdate = false;
	this._updateAgain = true;

	// Properties

	this.elmt = this._container;

	// Constructor

	this._init();
};
Seadragon.Drawer.prototype = {
    dispose: function() {
        //ToDO:
    },
    _init: function() {
        this._canvas.style.width = "100%";
        this._canvas.style.height = "100%";
        this._canvas.style.position = "absolute";
        this._container.style.textAlign = "left";    // explicit left-align
        this._container.appendChild(this._canvas);
    },
    _compareTiles: function(prevBest, tile) {
        // figure out if this tile is better than the previous best tile...
        // note that if there is no prevBest, this is automatically better.
        if (!prevBest) {
            return tile;
        }

        if (tile.visibility > prevBest.visibility) {
            return tile;
        } else if (tile.visibility == prevBest.visibility) {
            if (tile.distance < prevBest.distance) {
                return tile;
            }
        }

        return prevBest;
    },
    _getNumTiles: function(level) {
        if (!this._cacheNumTiles[level]) {
            this._cacheNumTiles[level] = this._source.getNumTiles(level);
        }

        return this._cacheNumTiles[level];
    },

    _getPixelRatio: function(level) {
        if (!this._cachePixelRatios[level]) {
            this._cachePixelRatios[level] = this._source.getPixelRatio(level);
        }

        return this._cachePixelRatios[level];
    },

    // Helpers -- TILES

    _getTile: function(level, x, y, time, numTilesX, numTilesY) {
        if (!this._tilesMatrix[level]) {
            this._tilesMatrix[level] = {};
        }
        if (!this._tilesMatrix[level][x]) {
            this._tilesMatrix[level][x] = {};
        }

        // initialize tile object if first time
        if (!this._tilesMatrix[level][x][y]) {
            // where applicable, adjust x and y to support wrapping.
            var xMod = (numTilesX + (x % numTilesX)) % numTilesX;
            var yMod = (numTilesY + (y % numTilesY)) % numTilesY;
            var bounds = this._source.getTileBounds(level, xMod, yMod);
            var exists = this._source.tileExists(level, xMod, yMod);
            var url = this._source.getTileUrl(level, xMod, yMod);

            // also adjust bounds to support wrapping.
            bounds.x += 1.0 * (x - xMod) / numTilesX;
            bounds.y += this._normHeight * (y - yMod) / numTilesY;

            this._tilesMatrix[level][x][y] = new Seadragon.Tile(level, x, y, bounds, exists, url);
        }

        var tile = this._tilesMatrix[level][x][y];

        // mark tile as touched so we don't reset it too soon
        tile.lastTouchTime = time;

        return tile;
    },

    _loadTile: function(tile, time) {
        tile.loading = this._imageLoader.loadImage(tile.url,
                    Seadragon.Utils.createCallback(null, Function.createDelegate(this, this._onTileLoad), tile, time));
    },

    _onTileLoad: function(tile, time, image) {
        tile.loading = false;

        if (this._midUpdate) {
            //Seadragon.Debug.error("Tile load callback in middle of drawing routine.");
            return;
        } else if (!image) {
            //Seadragon.Debug.log("Tile " + tile + " failed to load: " + tile.url);
            tile.exists = false;
            return;
        } else if (time < this._lastResetTime) {
            //Seadragon.Debug.log("Ignoring tile " + tile + " loaded before reset: " + tile.url);
            return;
        }

        tile.loaded = true;
        tile.image = image;

        var insertionIndex = this._tilesLoaded.length;

        if (this._tilesLoaded.length >= QUOTA) {
            var cutoff = Math.ceil(Math.log(this._tileSize) / Math.log(2));
            // don't delete any single-tile levels. this takes priority.

            var worstTile = null;
            var worstTileIndex = -1;

            for (var i = this._tilesLoaded.length - 1; i >= 0; i--) {
                var prevTile = this._tilesLoaded[i];

                if (prevTile.level <= this._cutoff || prevTile.beingDrawn) {
                    continue;
                } else if (!worstTile) {
                    worstTile = prevTile;
                    worstTileIndex = i;
                    continue;
                }

                var prevTime = prevTile.lastTouchTime;
                var worstTime = worstTile.lastTouchTime;
                var prevLevel = prevTile.level;
                var worstLevel = worstTile.level;

                if (prevTime < worstTime ||
                            (prevTime == worstTime && prevLevel > worstLevel)) {
                    worstTile = prevTile;
                    worstTileIndex = i;
                }
            }

            if (worstTile && worstTileIndex >= 0) {
                worstTile.unload();
                insertionIndex = worstTileIndex;
                // note: we don't want or need to delete the actual Tile
                // object from tilesMatrix; that's negligible memory.
            }
        }

        this._tilesLoaded[insertionIndex] = tile;
        this._updateAgain = true;
    },

    _clearTiles: function() {
        this._tilesMatrix = {};
        this._tilesLoaded = [];
    },

    // Helpers -- COVERAGE

    // Coverage scheme: it's required that in the draw routine, coverage for
    // every tile within the viewport is initially explicitly set to false.
    // This way, if a given level's coverage has been initialized, and a tile
    // isn't found, it means it's offscreen and thus provides coverage (since
    // there's no content needed to be covered). And if every tile that is found
    // does provide coverage, the entire visible level provides coverage.

    /**
    * Returns true if the given tile provides coverage to lower-level tiles of
    * lower resolution representing the same content. If neither x nor y is
    * given, returns true if the entire visible level provides coverage.
    * 
    * Note that out-of-bounds tiles provide coverage in this sense, since
    * there's no content that they would need to cover. Tiles at non-existent
    * levels that are within the image bounds, however, do not.
    */
    _providesCoverage: function(level, x, y) {
        if (!this._coverage[level]) {
            return false;
        }

        if (x === undefined || y === undefined) {
            // check that every visible tile provides coverage.
            // update: protecting against properties added to the Object
            // class's prototype, which can definitely (and does) happen.
            var rows = this._coverage[level];
            for (var i in rows) {
                if (rows.hasOwnProperty(i)) {
                    var cols = rows[i];
                    for (var j in cols) {
                        if (cols.hasOwnProperty(j) && !cols[j]) {
                            return false;
                        }
                    }
                }
            }

            return true;
        }

        return (this._coverage[level][x] === undefined ||
                    this._coverage[level][x][y] === undefined ||
                    this._coverage[level][x][y] === true);
    },

    /**
    * Returns true if the given tile is completely covered by higher-level
    * tiles of higher resolution representing the same content. If neither x
    * nor y is given, returns true if the entire visible level is covered.
    */
    _isCovered: function(level, x, y) {
        if (x === undefined || y === undefined) {
            return this._providesCoverage(level + 1);
        } else {
            return (this._providesCoverage(level + 1, 2 * x, 2 * y) &&
                        this._providesCoverage(level + 1, 2 * x, 2 * y + 1) &&
                        this._providesCoverage(level + 1, 2 * x + 1, 2 * y) &&
                        this._providesCoverage(level + 1, 2 * x + 1, 2 * y + 1));
        }
    },

    /**
    * Sets whether the given tile provides coverage or not.
    */
    _setCoverage: function(level, x, y, covers) {
        if (!this._coverage[level]) {
            //Seadragon.Debug.error("Setting coverage for a tile before its " +
            //            "level's coverage has been reset: " + level);
            return;
        }

        if (!this._coverage[level][x]) {
            this._coverage[level][x] = {};
        }

        this._coverage[level][x][y] = covers;
    },

    /**
    * Resets coverage information for the given level. This should be called
    * after every draw routine. Note that at the beginning of the next draw
    * routine, coverage for every visible tile should be explicitly set. 
    */
    _resetCoverage: function(level) {
        this._coverage[level] = {};
    },

    // Helpers -- SCORING

    _compareTiles: function(prevBest, tile) {
        // figure out if this tile is better than the previous best tile...
        // note that if there is no prevBest, this is automatically better.
        if (!prevBest) {
            return tile;
        }

        if (tile.visibility > prevBest.visibility) {
            return tile;
        } else if (tile.visibility == prevBest.visibility) {
            if (tile.distance < prevBest.distance) {
                return tile;
            }
        }

        return prevBest;
    },

    // Helpers -- OVERLAYS

    _getOverlayIndex: function(elmt) {
        for (var i = this._overlays.length - 1; i >= 0; i--) {
            if (this._overlays[i].elmt == elmt) {
                return i;
            }
        }

        return -1;
    },

    // Helpers -- CORE

    _updateActual: function() {
        // assume we won't need to update again after this update.
        // we'll set this if we find a reason to update again.
        this._updateAgain = false;

        // make local references to variables & functions referenced in
        // loops in order to improve perf
        var _canvas = this._canvas;
        var _context = this._context;
        var _container = this._container;
        var _useCanvas = useCanvas;
        var _lastDrawn = this._lastDrawn;

        // the tiles that were drawn last frame, but won't be this frame,
        // can be cleared from the cache, so they should be marked as such.
        while (_lastDrawn.length > 0) {
            var tile = _lastDrawn.pop();
            tile.beingDrawn = false;
        }

        // we need the size of the viewport (in pixels) in multiple places
        var viewportSize = this._viewport.getContainerSize();
        var viewportWidth = viewportSize.x;
        var viewportHeight = viewportSize.y;

        // clear canvas, whether in <canvas> mode or HTML mode.
        // this is important as scene may be empty this frame.
        _canvas.innerHTML = "";
        if (_useCanvas) {
            _canvas.width = viewportWidth;
            _canvas.height = viewportHeight;
            _context.clearRect(0, 0, viewportWidth, viewportHeight);
            // this last line shouldn't be needed. setting the width and
            // height should clear <canvas>, but Firefox doesn't always.
        }

        // if viewport is off image entirely, don't bother drawing.
        // UPDATE: logic modified to support horizontal/vertical wrapping.
        var viewportBounds = this._viewport.getBounds(true);
        var viewportTL = viewportBounds.getTopLeft();
        var viewportBR = viewportBounds.getBottomRight();
        if (!this.config.wrapHorizontal &&
                    (viewportBR.x < 0 || viewportTL.x > 1)) {
            // we're not wrapping horizontally, and viewport is off in x
            return;
        } else if (!this.config.wrapVertical &&
                    (viewportBR.y < 0 || viewportTL.y > this._normHeight)) {
            // we're not wrapping vertically, and viewport is off in y
            return;
        }

        // the below section is commented out because it's more relevant to
        // collections, where you don't want 10 items to all load their xml
        // at the same time when 9 of them won't be in the viewport soon.

        //            // but even if the viewport is currently on the image, don't force
        //            // tiles to load if the viewport target is off the image
        //            var viewportTargetBounds = getViewportBounds(false);
        //            var viewportTargetTL = viewportTargetBounds.getTopLeft();
        //            var viewportTargetBR = viewportTargetBounds.getBottomRight();
        //            var willBeOff = viewportTargetBR.x < 0 || viewportTargetBR.y < 0 ||
        //                    viewportTargetTL.x > 1 || viewportTargetTL.y > normHeight;


        // same for Math functions
        var _abs = Math.abs;
        var _ceil = Math.ceil;
        var _floor = Math.floor;
        var _log = Math.log;
        var _max = Math.max;
        var _min = Math.min;
        // and Viewport functions
        //var _deltaPixelsFromPoints = this._viewport.deltaPixelsFromPoints;
        //var _pixelFromPoint = this._viewport.pixelFromPoint;
        // and TileSource functions
        //var _getTileAtPoint = this._source.getTileAtPoint;
        // and Config properties
        var alwaysBlend = this.config.alwaysBlend;
        var blendTimeMillis = 1000 * this.config.blendTime;
        var immediateRender = this.config.immediateRender;
        var minDimension = this.config.minZoomDimension;   // for backwards compatibility
        var minImageRatio = this.config.minImageRatio;
        var wrapHorizontal = this.config.wrapHorizontal;
        var wrapVertical = this.config.wrapVertical;

        // restrain bounds of viewport relative to image.
        // UPDATE: logic modified to support horizontal/vertical wrapping.
        if (!wrapHorizontal) {
            viewportTL.x = _max(viewportTL.x, 0);
            viewportBR.x = _min(viewportBR.x, 1);
        }
        if (!wrapVertical) {
            viewportTL.y = _max(viewportTL.y, 0);
            viewportBR.y = _min(viewportBR.y, this._normHeight);
        }

        var best = null;
        var haveDrawn = false;
        var currentTime = new Date().getTime();

        // calculate values for scoring -- this is based on TARGET values
        var viewportCenter = this._viewport.pixelFromPoint(this._viewport.getCenter());
        var zeroRatioT = this._viewport.deltaPixelsFromPoints(this._source.getPixelRatio(0), false).x;
        var optimalPixelRatio = immediateRender ? 1 : zeroRatioT;

        // adjust levels to iterate over -- this is based on CURRENT values
        // TODO change this logic to use minImageRatio, but for backwards
        // compatibility, use minDimension if it's been explicitly set.
        // TEMP for now, original minDimension logic with default 64.
        minDimension = minDimension || 64;
        var lowestLevel = _max(this._minLevel, _floor(_log(minDimension) / _log(2)));
        var zeroRatioC = this._viewport.deltaPixelsFromPoints(this._source.getPixelRatio(0), true).x;
        var highestLevel = _min(this._maxLevel,
                    _floor(_log(zeroRatioC / MIN_PIXEL_RATIO) / _log(2)));

        // with very small images, this edge case can occur...
        lowestLevel = _min(lowestLevel, highestLevel);

        for (var level = highestLevel; level >= lowestLevel; level--) {
            var drawLevel = false;
            var renderPixelRatioC = this._viewport.deltaPixelsFromPoints(
                        this._source.getPixelRatio(level), true).x;     // note the .x!

            // if we haven't drawn yet, only draw level if tiles are big enough
            if ((!haveDrawn && renderPixelRatioC >= MIN_PIXEL_RATIO) ||
                        level == lowestLevel) {
                drawLevel = true;
                haveDrawn = true;
            } else if (!haveDrawn) {
                continue;
            }

            this._resetCoverage(level);

            // calculate scores applicable to all tiles on this level --
            // note that we're basing visibility on the TARGET pixel ratio
            var levelOpacity = _min(1, (renderPixelRatioC - 0.5) / 0.5);
            var renderPixelRatioT = this._viewport.deltaPixelsFromPoints(
                        this._source.getPixelRatio(level), false).x;
            var levelVisibility = optimalPixelRatio /
                        _abs(optimalPixelRatio - renderPixelRatioT);

            // only iterate over visible tiles
            var tileTL = this._source.getTileAtPoint(level, viewportTL);
            var tileBR = this._source.getTileAtPoint(level, viewportBR);
            var numTiles = this._getNumTiles(level);
            var numTilesX = numTiles.x;
            var numTilesY = numTiles.y;
            if (!wrapHorizontal) {
                tileBR.x = _min(tileBR.x, numTilesX - 1);
            }
            if (!wrapVertical) {
                tileBR.y = _min(tileBR.y, numTilesY - 1);
            }

            for (var x = tileTL.x; x <= tileBR.x; x++) {
                for (var y = tileTL.y; y <= tileBR.y; y++) {
                    var tile = this._getTile(level, x, y, currentTime, numTilesX, numTilesY);
                    var drawTile = drawLevel;

                    // assume this tile doesn't cover initially
                    this._setCoverage(level, x, y, false);

                    if (!tile.exists) {
                        // not part of sparse image, or failed to load
                        continue;
                    }

                    // if we've drawn a higher-resolution level and we're not
                    // going to draw this level, then say this tile does cover
                    // if it's covered by higher-resolution tiles. if we're not
                    // covered, then we should draw this tile regardless.
                    if (haveDrawn && !drawTile) {
                        if (this._isCovered(level, x, y)) {
                            this._setCoverage(level, x, y, true);
                        } else {
                            drawTile = true;
                        }
                    }

                    if (!drawTile) {
                        continue;
                    }

                    // calculate tile's position and size in pixels
                    var boundsTL = tile.bounds.getTopLeft();
                    var boundsSize = tile.bounds.getSize();
                    var positionC = this._viewport.pixelFromPoint(boundsTL, true);
                    var sizeC = this._viewport.deltaPixelsFromPoints(boundsSize, true);

                    // if there is no tile overlap, we need to oversize the
                    // tiles by 1px to prevent seams at imperfect zooms.
                    // fortunately, this is not an issue with regular dzi's
                    // created from Deep Zoom Composer, which uses overlap.
                    if (!this._tileOverlap) {
                        sizeC = sizeC.plus(new Seadragon.Point(1, 1));
                    }

                    // calculate distance from center of viewport -- note
                    // that this is based on tile's TARGET position
                    var positionT = this._viewport.pixelFromPoint(boundsTL, false);
                    var sizeT = this._viewport.deltaPixelsFromPoints(boundsSize, false);
                    var tileCenter = positionT.plus(sizeT.divide(2));
                    var tileDistance = viewportCenter.distanceTo(tileCenter);

                    // update tile's scores and values
                    tile.position = positionC;
                    tile.size = sizeC;
                    tile.distance = tileDistance;
                    tile.visibility = levelVisibility;

                    if (tile.loaded) {
                        if (!tile.blendStart) {
                            // image was just added, blend it
                            tile.blendStart = currentTime;
                        }

                        var deltaTime = currentTime - tile.blendStart;
                        var opacity = _min(1, deltaTime / blendTimeMillis);
                        
                        if (alwaysBlend) {
                            opacity *= levelOpacity;
                        }

                        tile.opacity = opacity;

                        // queue tile for drawing in reverse order
                        _lastDrawn.push(tile);

                        // if fully blended in, this tile now provides coverage,
                        // otherwise we need to update again to keep blending
                        if (opacity == 1) {
                            this._setCoverage(level, x, y, true);
                        } else if (deltaTime < blendTimeMillis) {
                            this._updateAgain = true;
                        }
                    } else if (tile.loading) {
                        // nothing to see here, move on
                    } else {
                        // means tile isn't loaded yet, so score it
                        best = this._compareTiles(best, tile);
                    }
                }
            }

            // we may not need to draw any more lower-res levels
            if (this._providesCoverage(level)) {
                break;
            }
        }

        // now draw the tiles, but in reverse order since we want higher-res
        // tiles to be drawn on top of lower-res ones. also mark each tile
        // as being drawn so it won't get cleared from the cache.
        for (var i = _lastDrawn.length - 1; i >= 0; i--) {
            var tile = _lastDrawn[i];

            if (_useCanvas) {
                tile.drawCanvas(_context);
            } else {
                tile.drawHTML(_canvas);
            }

            tile.beingDrawn = true;
        }

        // draw the overlays -- TODO optimize based on viewport like tiles,
        // but this is tricky for non-scaling overlays like pins...
        var numOverlays = this._overlays.length;
        for (var i = 0; i < numOverlays; i++) {
            var overlay = this._overlays[i];
            var bounds = overlay.bounds;

            overlay.position = this._viewport.pixelFromPoint(bounds.getTopLeft(), true);
            overlay.size = this._viewport.deltaPixelsFromPoints(bounds.getSize(), true);
            overlay.drawHTML(_container);
        }

        // load next tile if there is one to load
        if (best) {
            this._loadTile(best, currentTime);
            this._updateAgain = true; // because we haven't finished drawing, so
            // we should be re-evaluating and re-scoring
        }
    },

    // Methods -- OVERLAYS

    addOverlay: function(elmt, loc, placement) {
        var elmt = Seadragon.Utils.getElement(elmt);

        if (this._getOverlayIndex(elmt) >= 0) {
            return;     // they're trying to add a duplicate overlay
        }

        this._overlays.push(new Seadragon.Overlay(elmt, loc, placement));
        this._updateAgain = true;
    },

    updateOverlay: function(elmt, loc, placement) {
        var elmt = Seadragon.Utils.getElement(elmt);
        var i = this._getOverlayIndex(elmt);

        if (i >= 0) {
            this._overlays[i].update(loc, placement);
            this._updateAgain = true;
        }
    },

    removeOverlay: function(elmt) {
        var elmt = Seadragon.Utils.getElement(elmt);
        var i = this._getOverlayIndex(elmt);

        if (i >= 0) {
            this._overlays[i].destroy();
            this._overlays.splice(i, 1);
            this._updateAgain = true;
        }
    },

    clearOverlays: function() {
        while (this._overlays.length > 0) {
            this._overlays.pop().destroy();
            this._updateAgain = true;
        }
    },

    // Methods -- CORE

    needsUpdate: function() {
        return this._updateAgain;
    },

    numTilesLoaded: function() {
        return this._tilesLoaded.length;
    },

    reset: function() {
        this._clearTiles();
        this._lastResetTime = new Date().getTime();
        this._updateAgain = true;
    },

    update: function() {
        this._midUpdate = true;
        this._updateActual();
        this._midUpdate = false;
    },

    idle: function() {
        // TODO idling function
    }
};if (!window.SIGNAL)
    window.SIGNAL = "----seadragon----";
Seadragon.Viewer = function(element) {
    //Fields
	var element = $(element);
    this.element = element;
    this.config = Seadragon.Config;
    
    var controls = this.element.find("div.controls");
    var lastInteraction = new Date().getTime();
    this.lastInteraction = lastInteraction;
    //I do it on mouse down here, because if they click on controls I don't want the later click to happen
    controls.mousedown(Function.createDelegate(this, function(event) {
    	this.lastInteraction = new Date().getTime();
    	event.stopPropagation();
    	return false;
    }));
    window.setInterval(Function.createDelegate(this, function () {
    	if(this.lastInteraction <= new Date().getTime() - 3000) {
    		controls.hide('slow');
    	}
    }), 1000);
    this.element.mousewheel(Function.createDelegate(this,function(event,delta) {
    	if(delta < 0) {
    		event.shiftKey = true;
    	}
    	this.zoom(event, .8);
    	event.stopPropagation();
    	return false;
    }));
    
    this.container = $('<div class="neutral container" />');
    this.canvas = $('<div class="neutral canvas" />');
    
    //append to DOM only at end
    this.container.append(this.canvas);
    this.element.append(this.container);
    
    this.mousedowntime = 0;
    this.moveimage = false;
    this.lastPixels = null;
    this.ismousedown = false;
    this.container.mousedown(Function.createDelegate(this,function(event) {
    	this.ismousedown = true;
    	this.mousedowntime = new Date().getTime();
    	this.lastPixels = new Seadragon.Point(event.pageX,event.pageY);
    	event.stopPropagation();
    	return false;
    }));
    $(document).mousemove(Function.createDelegate(this, function(event) {
    	if(!this.ismousedown) {
    		controls.show();
    	}
    	else {
        	if(this.moveimage) {
        		this.panToNewPixelPoint(new Seadragon.Point(event.pageX,event.pageY));
        	}
        	else {
        		if(new Date().getTime() > this.mousedowntime + 200) {
        			this.moveimage = true;
        		}
        	}    		
    	}
    	this.lastInteraction = new Date().getTime();
    	event.stopPropagation();
    }));    
    
    $(document).mouseup(Function.createDelegate(this,function(event) {
    	if(!this.ismousedown) return false;
    	this.ismousedown = false;
    	var delta = this.lastPixels.minus(new Seadragon.Point(event.pageX,event.pageY));
    	if(this.moveimage || new Date().getTime() > this.mousedowntime + 200 || (Math.abs(delta.x) + Math.abs(delta.y)) >= 5) {
    		this.panToNewPixelPoint(new Seadragon.Point(event.pageX,event.pageY));
    	}
		else {
			this.zoom(event);
		}    	
		this.moveimage = false;
    	this.mousedowntime = 0;
    	this.viewport.applyConstraints();
    }));    
    
    
};
Seadragon.Viewer.prototype = {
	openDzi: function(xmlUrl) {
		var viewer = this;
		Seadragon.DziTileSource.createFromXml(xmlUrl, function (source,errors) {
			viewer.close();
			viewer.open(source);
		});
	},	
	open: function(source) {
        // assign fields
        this.source = source;
        this.prevContainerSize = new Seadragon.Point(this.container.width(),this.container.height());
        this.viewport = new Seadragon.Viewport(this.prevContainerSize, this.source.dimensions, this.config);
        //Seadragon.Drawer was not moved to jquery, so canvas needs to not be the jquery object
        this.drawer = new Seadragon.Drawer(this.source, this.viewport, this.canvas[0]);
        // begin updating
        this.animating = false;
        this.forceRedraw = true;
        this.scheduleUpdate(Function.createDelegate(this,this.updateMulti));
	},
	scheduleUpdate: function(updateFunc, prevUpdateTime) {
        // if we're animating, update as fast as possible to stay smooth
        if (this.animating) {
            return window.setTimeout(Function.createDelegate(this,updateFunc), 1);
        }

        // if no previous update, consider this an update
        var currentTime = new Date().getTime();
        var prevUpdateTime = prevUpdateTime ? prevUpdateTime : currentTime;
        var targetTime = prevUpdateTime + 1000 / 60;    // 60 fps ideal

        // calculate delta time to be a positive number
        var deltaTime = Math.max(1, targetTime - currentTime);
        return window.setTimeout(Function.createDelegate(this, updateFunc), deltaTime);
    },
    updateMulti: function() {
        if (!this.source) {
            return;
        }

        var beginTime = new Date().getTime();

        this.updateOnce();
        this.scheduleUpdate(arguments.callee, beginTime);
    },
    updateOnce: function() {
        if (!this.source) {
            return;
        }
        var containerSize = new Seadragon.Point(this.container.width(), this.container.height());

        if (!containerSize.equals(this.prevContainerSize)) {
            this.viewport.resize(containerSize, true); // maintain image position
            this.prevContainerSize = containerSize;
        }
        var animated = this.viewport.update();
        if (animated) {
            // viewport moved
            this.drawer.update();
        } else if (this.forceRedraw || this.drawer.needsUpdate()) {
            // need to load or blend images, etc.
            this.drawer.update();
            this.forceRedraw = false;
        } else {
            // no changes, so preload images, etc.
            this.drawer.idle();
        }
        this.animating = animated;
    },	
    close: function() {
        // nullify fields and properties
        this.source = null;
        this.viewport = null;
        this.drawer = null;
        this.canvas[0].html = '';
    },
    panToNewPixelPoint: function (newPoint) {
    	var delta = this.lastPixels.minus(newPoint);
    	if(Math.abs(delta.x + delta.y) >= 5) {
			this.panByPixels(delta);
			this.lastPixels = newPoint;
		}
    },
    panByPixels: function (pixels) {
    	this.viewport.panBy(this.viewport.deltaPointsFromPixels(pixels));
    },
    zoom: function(event,factor) {
    	var pixelOffset = Seadragon.Point.fromEvent(event).minus(Seadragon.Point.fromElementOffset(this.canvas.offset()));
    	var point =  this.viewport.pointFromPixel(pixelOffset,true);
    	if(event.shiftKey) {
    		this.zoomOut(event,point,factor );
    	}
    	else {
    		this.zoomIn(event,point,factor);
    	}
    }, 
    zoomIn: function(event,point,factor) {
    	if(!factor) {
    		factor = this.config.zoomPerClick;
    	}
    	this.viewport.zoomBy(this.config.zoomPerClick,point);
        this.viewport.applyConstraints();    	
    },
    zoomOut: function(event,point,factor) {
    	if(!factor) {
    		factor = this.config.zoomPerClick;
    	}
    	this.viewport.zoomBy(1.0 / this.config.zoomPerClick,point);
        this.viewport.applyConstraints();    	
    },
    goHome : function(event) {
    	if(this.viewport) {
    		this.viewport.goHome();
    	}
    },
    expandFullPage : function(event) {
    	var fullpage = false;

    	if(this.element.hasClass("seadragonimagefullscreen")) {
    		this.element.removeClass("seadragonimagefullscreen");
    		this.element.css('height',this.oldHeight);
    	}
    	else {
    		fullpage = true;
    		this.oldHeight = this.element.css('height');
    		this.element.css('height','100%');
    		this.element.addClass("seadragonimagefullscreen"); 
    	}
    	this.prevContainerSize = new Seadragon.Point(this.container.width(),this.container.height()); 
    	
        var oldBounds = this.viewport.getBounds();
        this.viewport.resize(this.prevContainerSize);
        var newBounds = this.viewport.getBounds();
    	if(fullpage) {
    		this._fsBoundsDelta = new Seadragon.Point(newBounds.width / oldBounds.width,
                    newBounds.height / oldBounds.height);
    	}
    	else {
    		this.viewport.update();
            this.viewport.zoomBy(Math.max(this._fsBoundsDelta.x, this._fsBoundsDelta.y),
                        null, true);
    	}
    	this.forceRedraw = true;
    	this.updateOnce();
    },
    _setMessage: function(message) {
        var textNode = document.createTextNode(message);

        this._canvas.innerHTML = "";
        this._canvas.appendChild(Seadragon.Utils.makeCenteredNode(textNode));

        var textStyle = textNode.parentNode.style;

        // explicit styles for error message
        textStyle.color = "white";
        textStyle.fontFamily = "verdana";
        textStyle.fontSize = "13px";
        textStyle.fontSizeAdjust = "none";
        textStyle.fontStyle = "normal";
        textStyle.fontStretch = "normal";
        textStyle.fontVariant = "normal";
        textStyle.fontWeight = "normal";
        textStyle.lineHeight = "1em";
        textStyle.textAlign = "center";
        textStyle.textDecoration = "none";
    },
    isOpen: function() {
        return !!this.source;
    },
    openTileSource: function(tileSource) {
        var currentTime = beforeOpen();
        window.setTimeout(Function.createDelegate(this, function() {
            onOpen(currentTime, tileSource);
        }), 1);
    },
    isFullPage: function() {
    	return this.element.hasClass('seadragonimagefullscreen');
    }
};/*! Copyright (c) 2009 Brandon Aaron (http://brandonaaron.net)
 * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
 * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
 * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers.
 * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix.
 *
 * Version: 3.0.2
 * 
 * Requires: 1.2.2+
 */

(function($) {

var types = ['DOMMouseScroll', 'mousewheel'];

$.event.special.mousewheel = {
	setup: function() {
		if ( this.addEventListener )
			for ( var i=types.length; i; )
				this.addEventListener( types[--i], handler, false );
		else
			this.onmousewheel = handler;
	},
	
	teardown: function() {
		if ( this.removeEventListener )
			for ( var i=types.length; i; )
				this.removeEventListener( types[--i], handler, false );
		else
			this.onmousewheel = null;
	}
};

$.fn.extend({
	mousewheel: function(fn) {
		return fn ? this.bind("mousewheel", fn) : this.trigger("mousewheel");
	},
	
	unmousewheel: function(fn) {
		return this.unbind("mousewheel", fn);
	}
});


function handler(event) {
	var args = [].slice.call( arguments, 1 ), delta = 0, returnValue = true;
	
	event = $.event.fix(event || window.event);
	event.type = "mousewheel";
	
	if ( event.wheelDelta ) delta = event.wheelDelta/120;
	if ( event.detail     ) delta = -event.detail/3;
	
	// Add events and delta to the front of the arguments
	args.unshift(event, delta);

	return $.event.handle.apply(this, args);
}

})(jQuery);
