/*
	$Id: js-gallery.js,v 1.75 2007/07/15 15:46:22 dse Exp $

	js-gallery.js - JavaScript for an image gallery.

	(C) 2005-2006 Darren Stuart Embry.

	This library is free software; you can redistribute it and/or
	modify it under the terms of the GNU Lesser General Public
	License as published by the Free Software Foundation; either
	version 2.1 of the License, or (at your option) any later version.

	The LGPL 2.1 is available at
	http://dse.webonastick.com/js-gallery/lgpl-2.1.txt

	This library is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
	Lesser General Public License for more details.

	You should have received a copy of the GNU Lesser General Public
	License along with this library; if not, write to the Free Software
	Foundation, Inc., 51 Franklin St, Fifth Floor,
	Boston, MA  02110-1301  USA

	ADDITIONAL REQUIREMENTS

	HTML documents that link to this file must also link to
	md5.js, available here: http://pajhome.org.uk/crypt/md5

	BUGS:

	- Hover over an image, drag on the image in move or pan mode.
	  Tooltip pops up.  Roll slowly over the tooltip.  Dragging is
	  finished.  It should not be finished until I release the
	  mouse button.

	- We can only have so many images before cookie size
	  limitations start kicking in.  There's really no workaround
	  for this that doesn't involve server-side software.

	TODO:

	- Provide the means to disable certain features:

		- disable panning beyond a certain coordinate range.
		- allow for: one image zoomed out, X images zoomed in.
		- allow for user-defined set of controls.

	- Text labels!

	- Type "30" then enter to recenter on image #30.

	- Type "#" to list images, then number, then enter to recenter
          on image.

	- Toggle to display image numbers, maybe other things, along
	  with images.  We have this code, it's just off for now.

	- Allow multiple flags perhaps?

	- Default x, y, gridSnap, and flag status for each image in
	  <gallery> elements.

	- *Any* default placement for images if x and y aren't
	  specified.  No need for fancy bin-packing algorithms or
	  anything.  Semantics might be "fun" if some images have x
	  and y and others don't.

	- Separate cookies for flags and zIndex from the coordinates.

	- ADVANCED: only set the big cookies during "idle time"?

*/

var JS_GALLERY_URL_POPUP_HELP =
	"http://dse.webonastick.com/js-gallery/popup-help.html";

var JS_GALLERY_TOOL_SELECT = 0;
var JS_GALLERY_TOOL_PAN = 1;
var JS_GALLERY_TOOL_MOVE = 2;
var JS_GALLERY_TOOL_FLAG = 3;

Array.prototype.shallowCopy = function () {
	var newArray = new Array();
	for (var i = 0; i < this.length; ++i) {
		newArray.push(this[i]);
	}
	return newArray;
};

if (!Array.prototype.map) {
	Array.prototype.map = function (callback) {
		var newArray = new Array();
		for (var i = 0; i < this.length; ++i) {
			newArray.push(callback(this[i]));
		}
		return newArray;
	};
}

Math.snap = function (a, b) {
	return Math.round(a / b) * b;
};

var portablyStopPropagation = function (e) {
	if (e.stopPropagation) {
		e.stopPropagation();
	} else {
		e.cancelBubble = true;
	}
};
var portablyPreventDefault = function (e) {
	if (e.preventDefault) {
		e.preventDefault();
	} else {
		e.returnValue = false;
	}
};
var addBinding = function (doc, type, handler) {
	if (doc.addEventListener) {
		doc.addEventListener(type, handler, false);
	}
	else if (doc.attachEvent) {
		doc.attachEvent("on" + type, handler);
	}
};
var removeBinding = function (doc, type, handler) {
	if (doc.removeEventListener) {
		doc.removeEventListener(type, handler, false);
	}
	else if (doc.detachEvent) {
		doc.detachEvent("on" + type, handler);
	}
};

/******************************************************************************
	WallGallery
******************************************************************************/

function WallGallery (el) /* constructor */ {
	if (!el.style.position) {
		el.style.position = "relative";
	}
	el.style.overflow = "hidden";

	/* create the canvas *inside* the specified element so events
	   on widgets created within the specified element don't touch
	   the canvas. */
	var canvas = document.createElement("div");
	canvas.className = "canvas";
	canvas.style.position = "absolute";
	canvas.style.top = "0px";
	canvas.style.left = "0px";
	canvas.style.width = "100%";
	canvas.style.height = "100%";
	el.appendChild(canvas);

	var imagesContainer = document.createElement("div");
	imagesContainer.style.position = "absolute";
	imagesContainer.style.top = "0px";
	imagesContainer.style.left = "0px";
	imagesContainer.style.width = "0px";
	imagesContainer.style.height = "0px";
	canvas.appendChild(imagesContainer);

	this.galleryElement = el;
	this.imagesContainer = imagesContainer;
	this.canvas = canvas;

	this.panAcceleration = 1;
	this.x = 0;
	this.y = 0;
	this.defaultRecenterX = true;
	this.defaultRecenterY = true;
	this.gridSnap = 16;
	this.isDragging = false;
	this.selectTool(JS_GALLERY_TOOL_PAN);
	
	this.setZoomLevels(null);
	this._setZoomAndIndex(1);

	var ondblclick    = this.createDoubleClickHandler();
	var onkeypress    = this.createKeyPressHandler();
	var onmousescroll = this.createMouseScrollHandler();
	var onmousedown   = this.createMouseDownHandler();

	addBinding(document, "mousedown", onmousedown);
	addBinding(document, "DOMMouseScroll", onmousescroll);
	addBinding(document, "dblclick", ondblclick);

	/* could this be done with addBinding as well? */
	document.onkeydown = onkeypress;

	/* could this be done with addBinding as well? */
	var thatGallery = this;
	if (window.onresize != null && !window.onresizeHasWrapper) {
		window.onresizeHasWrapper = 1;
		var oldHandler = window.onresize;
		window.onresize = function () {
			oldHandler();
			thatGallery.redraw();
		};
	} else {
		window.onresize = function () {
			thatGallery.redraw();
		};
	}

	this.fullFontSize = 24;
	this.minimumFontSize = 6;
	this.maximumFontSize = 24;

	this.cookieExpires =
		"; expires=" + new Date(2000000000000).toGMTString();

	this.unLockDown();
}

WallGallery.prototype.lockDown = function () {
	this.setFunctionalityFlags(false);
};

WallGallery.prototype.unLockDown = function () {
	this.setFunctionalityFlags(true);
};

WallGallery.prototype.setFunctionalityFlags = function (flag) {
	this.enableImageDragging = flag;
	this.enableImageRaising = flag;
	this.enableImageFlagging = flag;
	this.enableSelectionBoxes = flag;
	this.enableHelp = flag;
	this.enableShowXML = flag;
};

/******************************************************************************
	Handlers
******************************************************************************/

WallGallery.prototype.createDoubleClickHandler = function () {
	var thatGallery = this;
	return function (e) {
		if (!e) e = window.event;
		portablyPreventDefault(e);

		if (thatGallery.isDragging) {
			portablyStopPropagation(e);
			return false;
		}

		var xy = thatGallery.clientToGalleryCoords(e.clientX, e.clientY);
		thatGallery.x = xy[0];
		thatGallery.y = xy[1];
		thatGallery.redraw();

		portablyStopPropagation(e);
		return false;
	};
};

WallGallery.prototype.createKeyPressHandler = function () {
	/* NOTE: I profess ignorance on how to portably check which
	   key was pressed in a manner that allows us to check for
	   all keys. */
	var thatGallery = this;
	return function (e) {
		if (!e) e = window.event;
		thatGallery.debugMessage(
			e.keyCode + ", " + e.shiftKey + ", " + e.ctrlKey
		);

		var incr = null;
		if (e.shiftKey || e.ctrlKey) { incr = 16; }

		switch (e.keyCode) {
		case 38: /* up */
			thatGallery.moveUp(incr);
			return false;
		case 40: /* down */
			thatGallery.moveDown(incr);
			return false;
		case 37: /* left */
			thatGallery.moveLeft(incr);
			return false;
		case 39: /* right */
			thatGallery.moveRight(incr);
			return false;
		case 36: /* home */
			thatGallery.recenter();
			return false;
		case 45: /* - */
		case 189:
		case 109:
			thatGallery.zoomOut();
			return false;
		case 43: /* + */
		case 187:
		case 61:
			thatGallery.zoomIn();
			return false;
		case 80: /* P */
		case 112: /* p */
			thatGallery.selectTool(JS_GALLERY_TOOL_PAN);
			return false;
		case 77: /* M */
		case 109: /* m */
			thatGallery.selectTool(JS_GALLERY_TOOL_MOVE);
			return false;
		case 83: /* S */
		case 115: /* s */
			if (thatGallery.enableSelectionBoxes) {
				thatGallery.selectTool(JS_GALLERY_TOOL_SELECT);
			}
			return false;
		case 70: /* F */
		case 102: /* f */
			if (thatGallery.enableImageFlagging) {
				thatGallery.selectTool(JS_GALLERY_TOOL_FLAG);
			}
			return false;
		case 49: /* 1 */
			thatGallery.panAcceleration = 1;
			return false;
		case 50: /* 2 */
			thatGallery.panAcceleration = 2;
			return false;
		case 51: /* 3 */
			thatGallery.panAcceleration = 4;
			return false;
		case 52: /* 4 */
			thatGallery.panAcceleration = 8;
			return false;
		}
		return true;
	};
};

WallGallery.prototype.createMouseScrollHandler = function () {
	var thatGallery = this;
	return function (e) {
		if (!e) e = window.event;
		portablyPreventDefault(e);

		if (thatGallery.isDragging) {
			portablyStopPropagation(e);
			return false;
		}

		if (e.detail > 0) {
			thatGallery.zoomOut();
		} else if (e.detail < 0) {
			thatGallery.zoomIn();
		}

		portablyStopPropagation(e);
		return false;
	};
};

WallGallery.prototype.startSelecting = function (e) {
	var thatGallery = this;
	var thatDocument = document;
	var thatWindow = window;

	if (!e) e = window.event;
	portablyPreventDefault(e);

	if (thatGallery.isDragging) {
		portablyStopPropagation(e);
		return false;
	}

	thatGallery.isDragging = true;

	var xy = thatGallery.clientToGalleryCoords(e.clientX, e.clientY);
	thatGallery.startX = xy[0];
	thatGallery.startY = xy[1];
	thatGallery.endX = null;
	thatGallery.endY = null;

	thatGallery.redrawSelectBox({ inProgress: true });

	var x = 0;

	var keepDragging = function (e) {
		if (!e) e = window.event;
		portablyPreventDefault(e);

		var xy = thatGallery.clientToGalleryCoords(e.clientX, e.clientY);
		thatGallery.endX = xy[0];
		thatGallery.endY = xy[1];

		thatGallery.redrawSelectBox({ inProgress: true });

		portablyStopPropagation(e);
		return false;
	};

	var stopDragging = function (e) {
		if (!e) e = window.event;
		portablyPreventDefault(e);

		if (e.type == "mouseout") {
			if (e.relatedTarget) {
				portablyStopPropagation(e);
				return false;
			}
		}

		thatGallery.isDragging = false;
		removeBinding(thatDocument, "mousemove", keepDragging);
		removeBinding(thatDocument, "mouseup", stopDragging);
		removeBinding(thatWindow, "mouseout", stopDragging);

		if (thatGallery.hasSelectBox()) {
			if (thatGallery.startX > thatGallery.endX) {
				var temp = thatGallery.startX;
				thatGallery.startX = thatGallery.endX;
				thatGallery.endX = temp;
			}
			if (thatGallery.startY > thatGallery.endY) {
				var temp = thatGallery.startY;
				thatGallery.startY = thatGallery.endY;
				thatGallery.endY = temp;
			}
		} else {
			thatGallery.startX = null;
			thatGallery.endX = null;
			thatGallery.startY = null;
			thatGallery.endY = null;
		}

		thatGallery.debugMessage(0);
		thatGallery.selectedImages = thatGallery.getSelectedImages();
		thatGallery.debugMessage(1);
		thatGallery.redrawSelectBox({ inProgress: false });
		thatGallery.debugMessage(2);
		portablyStopPropagation(e);
		thatGallery.debugMessage(3);

		return false;
	};

	/* dragging select box */
	addBinding(thatDocument, "mousemove", keepDragging);
	addBinding(thatDocument, "mouseup", stopDragging);
	addBinding(thatWindow, "mouseout", stopDragging);

	portablyStopPropagation(e);
	return false;
};

WallGallery.prototype.startPanning = function (e) {
	var thatGallery = this;
	var thatDocument = document;
	var thatWindow = window;

	var canvas = this.canvas;

	if (!e) e = window.event;
	portablyPreventDefault(e);

	if (thatGallery.activeTool != JS_GALLERY_TOOL_PAN
		&& thatGallery.activeTool != JS_GALLERY_TOOL_MOVE
		&& thatGallery.activeTool != JS_GALLERY_TOOL_FLAG
	) {
		portablyStopPropagation(e);
		return false;
	}

	if (thatGallery.isDragging) {
		portablyStopPropagation(e);
		return false;
	}
	thatGallery.isDragging = true;
	//	thatGallery.gridSnapActive = !e.ctrlKey;
	thatGallery.gridSnapActive = true;
	canvas._clientX = e.clientX;
	canvas._clientY = e.clientY;
	canvas._x = thatGallery.x;
	canvas._y = thatGallery.y;
	var keepDragging = function (e) {
		if (!e) e = window.event;
		portablyPreventDefault(e);

		var dx = (e.clientX - canvas._clientX) *
			thatGallery.panAcceleration;
		var dy = (e.clientY - canvas._clientY) *
			thatGallery.panAcceleration;

		var x = canvas._x - dx / thatGallery.zoom;
		var y = canvas._y - dy / thatGallery.zoom;

		/* Do you want grid snap with your panning? */
//		if (thatGallery.gridSnapActive) {
//			x = Math.snap(x, thatGallery.gridSnap);
//			y = Math.snap(y, thatGallery.gridSnap);
//		}

		thatGallery.x = x;
		thatGallery.y = y;
		thatGallery.redraw();

		portablyStopPropagation(e);
		return false;
	};
	var stopDragging = function (e) {
		if (!e) e = window.event;
		portablyPreventDefault(e);

		if (e.type == "mouseout") {
			if (e.relatedTarget) {
				portablyStopPropagation(e);
				return false;
			}
		}

		thatGallery.isDragging = false;
		removeBinding(thatDocument, "mousemove", keepDragging);
		removeBinding(thatDocument, "mouseup", stopDragging);
		removeBinding(thatWindow, "mouseout", stopDragging);
		thatGallery.setXyCookie();

		portablyStopPropagation(e);
		return false;
	};

	/* panning */
	addBinding(thatDocument, "mousemove", keepDragging);
	addBinding(thatDocument, "mouseup", stopDragging);
	addBinding(thatWindow, "mouseout", stopDragging);

	portablyStopPropagation(e);
	return false;
};

WallGallery.prototype.createMouseDownHandler = function () {
	var thatGallery = this;
	return function (e) {

		if (thatGallery.enableSelectionBoxes &&
		    (thatGallery.activeTool == JS_GALLERY_TOOL_SELECT)) {
			return thatGallery.startSelecting(e);
		}

		if (thatGallery.activeTool == JS_GALLERY_TOOL_PAN
			|| thatGallery.activeTool == JS_GALLERY_TOOL_MOVE
			|| thatGallery.activeTool == JS_GALLERY_TOOL_FLAG
		) {
			return thatGallery.startPanning(e);
		}

		else {
			if (!e) e = window.event;
			portablyPreventDefault(e);
			portablyStopPropagation(e);
			return false;
		}
	};
};

/******************************************************************************
	End Handlers
******************************************************************************/

WallGallery.prototype.resetAndReloadWindow = function () {
	this.clearCookie();
	window.location.reload();
};

WallGallery.prototype.setZoomAndRedraw = function (zoom) {
	this._setZoomAndIndex(zoom);
	this.setZoomCookie();
	this.redraw({
		hide: true,
		reposition: true
	});
};

WallGallery.prototype._setZoomAndIndex = function (zoom) {
	if (this.zoomLevels && this.zoomLevels.length) {
		var i = this.zoomLevels.indexOfGeometricallyClosestNumberTo(zoom);
		this.zoom = this.zoomLevels[i];
		this.zoomLevelIndex = i;
	} else {
		this.zoom = zoom;
	}
};

WallGallery.prototype.zoomIn = function () {
	if (this.zoomLevels && this.zoomLevels.length) {
		if (this.zoomLevelIndex < (this.zoomLevels.length - 1)) {
			++this.zoomLevelIndex;
			this.setZoomAndRedraw(this.zoomLevels[this.zoomLevelIndex]);
		}
	}
	else {
		if (this.zoom <= 0.5001) {
			this.setZoomAndRedraw(this.zoom * 2);
		}
	}
};

WallGallery.prototype.zoomOut = function () {
	if (this.zoomLevels && this.zoomLevels.length) {
		if (this.zoomLevelIndex > 0) {
			--this.zoomLevelIndex;
			this.setZoomAndRedraw(this.zoomLevels[this.zoomLevelIndex]);
		}
	}
	else {
		if (this.zoom >= 0.124999) {
			this.setZoomAndRedraw(this.zoom / 2);
		}
	}
};

WallGallery.prototype.moveUp = function (incr) {
	if (incr == null) incr = 128;
	this.y -= incr / this.zoom;
	this.setXyCookie();
	this.redraw();
};

WallGallery.prototype.moveDown = function (incr) {
	if (incr == null) incr = 128;
	this.y += incr / this.zoom;
	this.setXyCookie();
	this.redraw();
};

WallGallery.prototype.moveLeft = function (incr) {
	if (incr == null) incr = 128;
	this.x -= incr / this.zoom;
	this.setXyCookie();
	this.redraw();
};

WallGallery.prototype.moveRight = function (incr) {
	if (incr == null) incr = 128;
	this.x += incr / this.zoom;
	this.setXyCookie();
	this.redraw();
}

WallGallery.prototype.setGridSnap = function (gs) {
	this.gridSnap = gs;
	this.setGridSnapCookie();
	this.highlightGridSnapControl();
}

WallGallery.prototype.redraw = function (flags) {
	if (flags && flags.hide) {
		this.imagesContainer.style.display = "none";
	}
	for (var i = 0; i < this.images.length; ++i) {
		this.images[i].redraw(flags);
	}
	var x = Math.round(0 - this.x * this.zoom);
	var y = Math.round(0 - this.y * this.zoom);
	this.canvas.style.backgroundPosition = x + "px " + y + "px";

	x = Math.round(0 - this.x * this.zoom
			+ this.canvas.offsetWidth / 2);
	y = Math.round(0 - this.y * this.zoom
			+ this.canvas.offsetHeight / 2);
	this.imagesContainer.style.left = x + "px";
	this.imagesContainer.style.top = y + "px";

	if (flags && flags.reposition) {
		this.redrawSelectBox();
	}
	if (flags && flags.hide) {
		this.imagesContainer.style.display = "";
	}

	this.setLinkToHere();
};

WallGallery.prototype.setLinkToHere = function () {
	var link = document.getElementById("linkToHere");
	if (link) {
		link.href = (
			"?x=" + this.x
			+ "&y=" + this.y
			+ "&zoom=" + this.zoom
		);
	}
};

WallGallery.prototype.clearCanvas = function () {
	var victims = new Array();
	for (var i = this.canvas.firstChild; i; i = i.nextSibling) {
		if (i.tagName == "img") {
			push(victims, i);
		}
	}
	for (var i = 0; i < victims.length; ++i) {
		this.canvas.removeChild(i);
	}
};

WallGallery.prototype.clearImages = function () {
	this.images = new Array();
};
WallGallery.prototype.appendImages = function (xmlImages) {
	for (var i = 0; i < xmlImages.length; ++i) {
		this.appendImage(xmlImages[i]);
	}
	this.computeMd5hash();
};
WallGallery.prototype.appendImage = function (xmlImage) {
	var src      = xmlImage.getAttribute("src");
	var thumbsrc = xmlImage.getAttribute("thumbsrc");
	var x        = xmlImage.getAttribute("x");
	var y        = xmlImage.getAttribute("y");
	var width    = xmlImage.getAttribute("width");
	var height   = xmlImage.getAttribute("height");
	var title    = xmlImage.getAttribute("title");
	var zIndex   = xmlImage.getAttribute("z-index");
	var canvasImage = new CanvasImage();
	width = parseInt(width);
	height = parseInt(height);
	x = parseInt(x);
	y = parseInt(y);
	if (width != null  && !isNaN(width)) canvasImage.width = width;
	if (height != null && !isNaN(height)) canvasImage.height = height;
	if (zIndex == null) 
		zIndex = 0;
	else
		zIndex = parseInt(zIndex);
	if (isNaN(zIndex))
		zIndex = 0;

	/* FIXME: we still need a default placement if nothing is specified
	   anywhere. */
	if (x != null && !isNaN(x)) canvasImage.x = x;
	if (y != null && !isNaN(y)) canvasImage.y = y;

	canvasImage.src = src;
	canvasImage.thumbsrc = thumbsrc;
	canvasImage.title = title;
	canvasImage.gallery = this;
	canvasImage.filename = src.replace(/\?.*$/, "").replace(/^.*\//, "");
	canvasImage.zIndex = zIndex;
	canvasImage.flag = false;
	canvasImage.imageNumber = this.images.length + 1;
	canvasImage.load();
	this.images.push(canvasImage);
	var xmlThumbnails = xmlImage.getElementsByTagName("thumbnail");
	canvasImage.thumbnails = new Array();
	for (var i = 0; i < xmlThumbnails.length; ++i) {
		var zoom = xmlThumbnails[i].getAttribute("zoom");
		var src = xmlThumbnails[i].getAttribute("src");
		canvasImage.thumbnails.push({
			"zoom": zoom,
			"src": src
		});
	}
};

WallGallery.prototype.raiseImage = function (canvasImage) {
	var len = this.images.length;
	var idx = null; /* will point to index of canvasImage in this.images. */
	for (var i = 0; i < len; ++i)
		if (this.images[i] == canvasImage) idx = i;
	if (idx == null) return;

	/* make sure this image's zIndex is above all the others. */
	var zIndexMax = 0;
	for (var i = 0; i < len; ++i) {
		if (i == idx) continue;
		if (zIndexMax < this.images[i].zIndex)
			zIndexMax = this.images[i].zIndex;
	}
	this.images[idx].zIndex = zIndexMax + 1;

	/* reset all images' zIndex values to start at zero, maintaining order. */
	var images2 = this.images.shallowCopy();
	images2.sort(function (a, b) { return a.zIndex - b.zIndex; });
	for (var i = 0; i < len; ++i) { images2[i].zIndex = i; }

	this.setCookies(); /* separate zIndex cookie? */
	this.redraw({ reposition: 1 });
};

WallGallery.prototype.recenterX = function () {
	var xMin = null;
	var xMax = null;
	for (var i = 0; i < this.images.length; ++i) {
		var image = this.images[i];
		if (!image.isReady) continue;
		var x = image.x;
		var x2 = x + image.width;
		if (xMin == null || xMin > x)  xMin = x;
		if (xMax == null || xMax < x2) xMax = x2;
	}
	var x = Math.round((xMin + xMax) / 2);
	this.x = x;
	this.setXyCookie();
	this.redraw();
};

WallGallery.prototype.recenterY = function () {
	var yMin = null;
	var yMax = null;
	for (var i = 0; i < this.images.length; ++i) {
		var image = this.images[i];
		if (!image.isReady) continue;
		var y = image.y;
		var y2 = y + image.height;
		if (yMin == null || yMin > y)  yMin = y;
		if (yMax == null || yMax < y2) yMax = y2;
	}
	var y = Math.round((yMin + yMax) / 2);
	this.y = y;
	this.setXyCookie();
	this.redraw();
};

WallGallery.prototype.recenter = function () {
	this.recenterX();
	this.recenterY();
};

/* cookies */

WallGallery.prototype.computeMd5hash = function () {
	var srcs = new Array();
	for (var i = 0; i < this.images.length; ++i) {
		srcs.push(this.images[i].src);
	}
	this.md5hash = hex_md5(srcs.join("|"));
	this.cookieName = "wg." + this.md5hash;
};

WallGallery.prototype.setXyCookie = function () {
	var xyCookieName = this.cookieName + ".xy";
	document.cookie =
		xyCookieName + "=" + this.x + "," + this.y;
};
WallGallery.prototype.setZoomCookie = function () {
	var zoomCookieName = this.cookieName + ".zoom";
	document.cookie =
		zoomCookieName + "=" + this.zoom + this.cookieExpires;
};
WallGallery.prototype.setGridSnapCookie = function () {
	var gridSnapCookieName = this.cookieName + ".gridSnap";
	document.cookie =
		gridSnapCookieName + "=" + this.gridSnap + this.cookieExpires;
};

WallGallery.prototype.setCookies = function () {
	var cookieValue = new Array();
	cookieValue.push(this.x + "," + this.y);
	for (var i = 0; i < this.images.length; ++i) {
		cookieValue.push(
			this.images[i].x
			+ "," + this.images[i].y
			+ "," + this.images[i].zIndex
			+ "," + (this.images[i].flag ? "1" : "0")
		);
	}
	cookieValue = cookieValue.join(":");
	document.cookie
		= this.cookieName + "=" + cookieValue + this.cookieExpires;

	this.setXyCookie();
	this.setZoomCookie();
	this.setGridSnapCookie();
};

WallGallery.prototype.clearCookie = function () {
	var cookieName = this.cookieName;
	var xyCookieName = cookieName + ".xy";
	var zoomCookieName = cookieName + ".zoom";
	var gridSnapCookieName = cookieName + ".gridSnap";

	var cookieExpiresNow =  "; expires=" +
		new Date(1970,0,1,0,0,0,0).toGMTString(); /* FIXME */

	document.cookie = cookieName + "=" + cookieExpiresNow;
	document.cookie = xyCookieName + "=" + cookieExpiresNow;
	document.cookie = zoomCookieName + "=" + cookieExpiresNow;
	document.cookie = gridSnapCookieName + "=" + cookieExpiresNow;
};

WallGallery.prototype.loadCookie = function () {
	var cookieName = this.cookieName;
	var xyCookieName = cookieName + ".xy";
	var zoomCookieName = cookieName + ".zoom";
	var gridSnapCookieName = cookieName + ".gridSnap";
	var cookies = document.cookie;
	cookies = cookies.split("; ");

	var cookieValue;
	var xyCookieValue;

	for (var i = 0; i < cookies.length; ++i) {
		var nv = cookies[i].split("=");

		if (nv[0] == cookieName) {
			cookieValue = nv[1];
		}
		if (nv[0] == zoomCookieName) {
			var moo = parseFloat(nv[1]);
			if (!isNaN(moo) && moo > 0) {
				this._setZoomAndIndex(moo);
			}
		}
		if (nv[0] == gridSnapCookieName) {
			var moo = parseInt(nv[1]);
			if (!isNaN(moo) && moo >= 0) {
				this.gridSnap = moo;
			}
		}
		if (nv[0] == xyCookieName) {
			xyCookieValue = nv[1];
		}
	}

	if (xyCookieValue != null) {
		/* viewport */
		var xy = xyCookieValue.split(",");
		var x = parseInt(xy[0]);
		var y = parseInt(xy[1]);
		if (x != null && !isNaN(x)) {
			this.x = x;
			this.defaultRecenterX = false;
		}
		if (y != null && !isNaN(y)) {
			this.y = y;
			this.defaultRecenterY = false;
		}
	}

	if (cookieValue != null) {
		cookieValue = cookieValue.split(":");
		var junk = cookieValue.shift(); /* viewport x,y used to be here */
		/* coords, z-index, etc. for each image */
		for (var i = 0; i < cookieValue.length; ++i) {
			var xyz = cookieValue[i].split(",");
			var x = parseInt(xyz[0]);
			var y = parseInt(xyz[1]);
			var z = parseInt(xyz[2]);
			var f = parseInt(xyz[3]);
			if (x != null && !isNaN(x)) this.images[i].x = x;
			if (y != null && !isNaN(y)) this.images[i].y = y;
			if (z != null && !isNaN(z)) this.images[i].zIndex = z;
			if (f != null && !isNaN(f)) this.images[i].flag = !(f == 0);
		}
	}
};

WallGallery.prototype.setZoomLevels = function (xmlZooms) {
	if (xmlZooms && xmlZooms.length) {
		var zooms = [];

		for (var i = 0; i < xmlZooms.length; ++i) {
			var xmlZoom = xmlZooms[i];

			var level = xmlZoom.getAttribute("level");
			if (level == null) continue;
			level = parseFloat(level);
			if (isNaN(level)) continue;

			var borderWidth = xmlZoom.getAttribute("border-width");
			if (borderWidth == null) continue;
			borderWidth = parseInt(borderWidth);
			if (isNaN(borderWidth)) continue;

			zooms.push({ "level": level, "borderWidth": borderWidth });
		}

		zooms.sort(function (a, b) { return a.level - b.level; });

		this.zoomLevels = [];
		this.zoomBorderWidths = [];

		for (var i = 0; i < zooms.length; ++i) {
			this.zoomLevels.push(zooms[i].level);
			this.zoomBorderWidths.push(zooms[i].borderWidth);
		}
	} else {
		this.zoomLevels = [
			0.0625, 0.125, 0.25, 0.5, 1
		];
		this.zoomBorderWidths = [
			1, 1, 1, 2, 4
		];
	}
};


/* load from xml file */

WallGallery.prototype.processXMLResponse = function (url, req) {

	if (req.status == 200 || req.status == 0 /* local */) {
		var xml = req.responseXML;

		var xmlImages = xml.getElementsByTagName("img");
		var xmlZooms = xml.getElementsByTagName("zoom");

		window.alert(xmlImages.length);

		this.clearCanvas();
		this.clearImages();

		if (xmlImages && xmlImages.length) {
			this.appendImages(xmlImages);
		}

		if (xmlZooms && xmlZooms.length) {
			this.setZoomLevels(xmlZooms);
		}
		
		if (xml.documentElement) { // no IE?  :/
			var zoom = xml.documentElement.getAttribute("zoom");
			var x = xml.documentElement.getAttribute("x");
			var y = xml.documentElement.getAttribute("y");

			zoom = parseInt(zoom);
			x = parseInt(x);
			y = parseInt(y);

			if (zoom != null && !isNaN(zoom)) {
				this._setZoomAndIndex(zoom);
			}
			if (x != null && !isNaN(x)) {
				this.x = x;
				this.defaultRecenterX = false;
			}
			if (y != null && !isNaN(y)) {
				this.y = y;
				this.defaultRecenterY = false;
			}
		}
		this.loadCookie();

		var qs = document.location.search;
		if (qs.length > 0 && qs.charAt(0) == '?') {
			qs = qs.substring(1);
		}
		var qsa = qs.split("&");
		for (var i = 0; i < qsa.length; ++i) {
			var kv = qsa[i].split("=");
			var k = kv[0];
			var v = kv[1];
			if (v == null) continue;
			if (k == "zoom") {
				v = parseFloat(v);
				if (!isNaN(v))
					this._setZoomAndIndex(zoom);
			}
			if (k == "x") {
				v = parseInt(v);
				if (!isNaN(v)) {
					this.x = v;
					this.defaultRecenterX = false;
				}
			}
			if (k == "y") {
				v = parseInt(v);
				if (!isNaN(v)) {
					this.y = v;
					this.defaultRecenterY = false;
				}
			}
		}

		if (this.defaultRecenterX) {
			this.recenterX();
		}
		if (this.defaultRecenterY) {
			this.recenterY();
		}

		this.redraw({ reposition: true });
	} else {
		window.alert(url + " ERROR: " + req.status);
	}
};

WallGallery.prototype.loadGallery = function (url) {
	var req;
	if (typeof(XMLHttpRequest) != "undefined") {
		req = new XMLHttpRequest();
	} else {
		/* MSIE */
		try {
			req = new ActiveXObject("Microsoft.XMLHTTP");
		} catch (ex) {
			try {
				req = new ActiveXObject("Msxml2.XMLHTTP");
			} catch (ex2) {
				req = null;
			}
		}
	}
	if (!req) {
		window.alert("No XML HTTP Request methods available.  Sorry, man.");
		document.location = "http://dse.webonastick.com/js-gallery/";
	}
	req.open("GET", url);
	var that = this;
	req.onreadystatechange = function () {
		if (req.readyState == 4 /* finished */) {
			that.processXMLResponse(url, req);
		}
	};
	req.send(null);
	this.createDebugWidget();
	this.createControlsWidget();
};

WallGallery.prototype.createDebugWidget = function (msg) {
	if (this.debug) {
		if (!this.debugElement) {
			var div = document.createElement("div");
			div.className = "widget debug";
			div.style.position = "absolute";
			div.style.left = "5px";
			div.style.bottom = "5px";
			div.style.zIndex = "1000000"; /* FIXME */
			div.style.borderWidth = "1px";
			div.style.borderStyle = "solid";
			div.style.borderColor = "black";
			div.style.backgroundColor = "white";
			div.style.padding = "5px";
			var form = document.createElement("form");
			form.action = "javascript:return false;";
			form.style.margin = "0px";
			var input = document.createElement("input");
			input.type = "text";
			input.name = "moo";
			input.value = "";
			form.appendChild(input);
			div.appendChild(form);
			this.galleryElement.appendChild(div);
			this.debugElement = input;
		}
	}
};

/* convenience */

function TAG (name) {
	var el = document.createElement(name);
	for (var i = 1; i < arguments.length; ++i) {
		if (arguments[i].constructor == String) {
			el.appendChild(document.createTextNode(arguments[i]));
		} else if (arguments[i].constructor == Object) {
			for (var attr in arguments[i]) {
				if (attr == "id") {
					el.id = arguments[i][attr];
				} else if (attr.match(/^style_/)) {
					el.style[attr.substring(6)] = arguments[i][attr];
				} else if (attr == "onclick") {
					el.onclick = arguments[i][attr];
				} else {
					el.setAttribute(attr, arguments[i][attr]);
				}
			}
		} else {
			el.appendChild(arguments[i]);
		}
	}
	return el;
}

function APPEND (element) {
	for (var i = 1; i < arguments.length; ++i) {
		if (typeof(arguments[i]) == "string") {
			element.appendChild(document.createTextNode(arguments[i]));
		} else {
			element.appendChild(arguments[i]);
		}
	}
	return element;
}

var helpPopup;
function openHelpPopup () {
	if (helpPopup && !helpPopup.closed)
		helpPopup.close();
	helpPopup =
		window.open(JS_GALLERY_URL_POPUP_HELP,
			"js_gallery_popup",
			"width=600,height=400,resizable=yes,scrollbars=yes"
		);
	return false;
}

WallGallery.prototype.highlightToolControl = function () {
	if (this.toolControls) {
		for (var i = 0; i < this.toolControls.length; ++i) {
			if (this.toolControls[i]._state == this.activeTool) {
				this.toolControls[i].style.borderStyle = "inset";
			} else {
				this.toolControls[i].style.borderStyle = "";
			}
		}
	}
};

WallGallery.prototype.highlightGridSnapControl = function () {
	if (this.gridSnapControls) {
		for (var i = 0; i < this.gridSnapControls.length; ++i) {
			if (this.gridSnapControls[i]._state == this.gridSnap) {
				this.gridSnapControls[i].style.borderStyle = "inset";
			} else {
				this.gridSnapControls[i].style.borderStyle = "";
			}
		}
	}
};

WallGallery.prototype.createControlsWidget = function (msg) {
	var thatGallery = this;
	if (!this.controlsElement) {
		var div = document.createElement("div");
		div.className = "widget controls";
		div.style.position = "absolute";
		div.style.left = "5px";
		div.style.top = "5px";
		div.style.zIndex = "1000000"; /* FIXME */
		div.style.borderWidth = "1px";
		div.style.borderStyle = "solid";
		div.style.borderColor = "black";
		div.style.backgroundColor = "white";
		div.style.padding = "5px";
		var form = document.createElement("form");
		form.style.textAlign = "center";
		form.action = "javascript:return false;";
		form.style.margin = "0px";

		var CONTROL = function (value, handler) {
			var control = document.createElement("input");
			control.className = "control";
			control.type = "button";
			control.value = value;
			control.style.padding = "0px";
			control.style.margin = "0px";
			var handlerWrapper = function (e) {
				if (!e) e = window.event;
				portablyPreventDefault(e);
				handler();
				portablyStopPropagation(e);
				return false;
			};
			var sponge = function (e) {
				if (!e) e = window.event;
				portablyPreventDefault(e);
				portablyStopPropagation(e);
				return false;
			};
			addBinding(control, "click", handlerWrapper);

			/* These are needed to prevent stupid things from happening. */
			addBinding(control, "mousedown", sponge);
			addBinding(control, "dblclick", sponge);

			return control;
		};

		var controlToolSelect =
			CONTROL("select", function () { 
				thatGallery.selectTool(JS_GALLERY_TOOL_SELECT);
			});
		var controlToolPan =
			CONTROL("pan", function () {
				thatGallery.selectTool(JS_GALLERY_TOOL_PAN);
			});
		var controlToolMove =
			CONTROL("move", function () {
				thatGallery.selectTool(JS_GALLERY_TOOL_MOVE);
			});
		var controlToolFlag =
			CONTROL("flag", function () {
				thatGallery.selectTool(JS_GALLERY_TOOL_FLAG);
			});
		controlToolSelect._state = JS_GALLERY_TOOL_SELECT;
		controlToolPan._state = JS_GALLERY_TOOL_PAN;
		controlToolMove._state = JS_GALLERY_TOOL_MOVE;
		controlToolFlag._state = JS_GALLERY_TOOL_FLAG;

		if (!this.toolControls) {
			this.toolControls = new Array();
			this.toolControls.push(controlToolSelect);
			this.toolControls.push(controlToolPan);
			this.toolControls.push(controlToolMove);
			this.toolControls.push(controlToolFlag);
			this.highlightToolControl();
		}

		var controlGridSnap1 =
			CONTROL("1", function () { thatGallery.setGridSnap(1); });
		var controlGridSnap4 =
			CONTROL("4", function () { thatGallery.setGridSnap(4); });
		var controlGridSnap16 =
			CONTROL("16", function () { thatGallery.setGridSnap(16); });
		var controlGridSnap64 =
			CONTROL("64", function () { thatGallery.setGridSnap(64); });
		controlGridSnap1._state = 1;
		controlGridSnap4._state = 4;
		controlGridSnap16._state = 16;
		controlGridSnap64._state = 64;

		if (!this.gridSnapControls) {
			this.gridSnapControls = new Array();
			this.gridSnapControls.push(controlGridSnap1);
			this.gridSnapControls.push(controlGridSnap4);
			this.gridSnapControls.push(controlGridSnap16);
			this.gridSnapControls.push(controlGridSnap64);
			this.highlightGridSnapControl();
		}

		APPEND(form,
			TAG("h2",
				{ style_fontSize: "100%", style_marginTop: "0px" },
				TAG("a",
					{	href: JS_GALLERY_URL_POPUP_HELP,
						target: "_blank",
						onclick: function (e) {
							if (thatGallery.enableHelp) {
								return openHelpPopup();
							}
							return false;
						}
					},
					"js-gallery", TAG("br"), "help")),
			TAG("h3", { style_fontSize: "100%", style_margin: "0px" }, "Zoom"),
			CONTROL("\u2212", function () { thatGallery.zoomOut(); }),
			CONTROL("+", function () { thatGallery.zoomIn(); }),
			TAG("h3", { style_fontSize: "100%", style_margin: "0px" }, "Pan"),
			CONTROL("\xa0\u2191\xa0", function () { thatGallery.moveUp(); }),
			TAG("br"),
			CONTROL("\u2190", function () { thatGallery.moveLeft(); }),
			CONTROL("\u2192", function () { thatGallery.moveRight(); }),
			TAG("br"),
			CONTROL("\xa0\u2193\xa0", function () { thatGallery.moveDown(); }),
			TAG("br"),
			CONTROL("center", function () { thatGallery.recenter(); }),
			TAG("h3", { style_fontSize: "100%", style_margin: "0px" },
				"Grid", TAG("br"), "Snap"),
			controlGridSnap1,
			controlGridSnap4, TAG("br"),
			controlGridSnap16,
			controlGridSnap64,
			TAG("h3", { style_fontSize: "100%", style_margin: "0px" },
				"Tools"),
			controlToolPan, TAG("br"),
			controlToolSelect, TAG("br"),
			controlToolMove, TAG("br"),
			controlToolFlag, TAG("br"),
			TAG("br"),
			TAG("a", { id: "linkToHere", href: "#" },
				"link to this", TAG("br"), "location"),
			TAG("br"),
			CONTROL("XML", function () {
				if (thatGallery.enableShowXML) {
					thatGallery.showXML();
				}
			}),
			TAG("br"),
			CONTROL("reset",
				function () { thatGallery.resetAndReloadWindow(); })
		);
		div.appendChild(form);
		this.galleryElement.appendChild(div);
		this.controlElement = div;
	}
};

WallGallery.prototype.debugMessage = function (msg) {
	if (this.debugElement)
		this.debugElement.value = msg;
};

WallGallery.prototype.debugMessageAppend = function (msg) {
	if (this.debugElement)
		this.debugElement.value += msg;
};

/* ------------------------------------------------------------------------- */

function CanvasImage () /* constructor */ {
	this.isLoading = false;
	this.isReady = false;
}

CanvasImage.prototype.load = function () {
	if (this.isLoading || this.isReady) return;
	var theImage = this;
	if (theImage.width == null || theImage.height == null) {
		var io = this.imageObject = new Image();
		io.src = this.src;
		io.onload = function () {
			theImage.width = io.width;
			theImage.height = io.height;
			theImage.isReady = true;
			theImage.redraw();
		};
	} else {
		theImage.isReady = true;
		theImage.redraw();
	}
};

Array.prototype.indexOfGeometricallyClosestNumberTo = function (num) {
	var closeness = new Array();
	for (var i = 0; i < this.length; ++i) {
		closeness.push(Math.abs(Math.log(this[i] / num)));
	}
	var closestIndex = 0;
	for (var j = 1; j < closeness.length; ++j) {
		if (closeness[j] < closeness[closestIndex]) {
			closestIndex = j;
		}
	}
	return closestIndex;
};

/* picks a thumbnail. */
CanvasImage.prototype.closestImageSrc = function (zoom) {
	var zooms = new Array();
	var srcs = new Array();
	srcs.push(this.src);
	zooms.push(1);
	if (this.thumbsrc) {
		srcs.push(this.thumbsrc);
		zooms.push(0.0625);
	}
	if (this.thumbnails) {
		for (var i = 0; i < this.thumbnails.length; ++i) {
			srcs.push(this.thumbnails[i].src);
			zooms.push(this.thumbnails[i].zoom);
		}
	}
	var closestIndex = zooms.indexOfGeometricallyClosestNumberTo(zoom);
	return srcs[closestIndex];
};

/******************************************************************************
	Handlers
******************************************************************************/

CanvasImage.prototype.createDoubleClickHandler = function () {
	var thatCanvasImage = this;
	var thatGallery = this.gallery;
	
	return function (e) {
		if (!e) e = window.event;
		portablyPreventDefault(e);

		if (thatGallery.enableImageFlagging &&
		    (thatGallery.activeTool == JS_GALLERY_TOOL_FLAG)) {
			portablyStopPropagation(e);
			return false;
		}
		if (!e.shiftKey && !e.ctrlKey) {
			return true; /* bubble up */
		}

		thatGallery.x =
			Math.round(thatCanvasImage.x + thatCanvasImage.width / 2);
		thatGallery.y =
			Math.round(thatCanvasImage.y + thatCanvasImage.height / 2);
		thatGallery.setXyCookie();
		thatGallery.redraw();

		portablyStopPropagation(e);
		return false;
	};
};
CanvasImage.prototype.createClickHandler = function () {
	var thatCanvasImage = this;
	var thatGallery = this.gallery;

	return function (e) {
		if (!e) e = window.event;
		portablyPreventDefault(e);

		if (thatGallery.isDragging) {
			portablyStopPropagation(e);
			return false;
		}

		if (thatGallery.activeTool != JS_GALLERY_TOOL_FLAG) {
			return true; /* bubble up */
		}
		if (e.shiftKey || e.ctrlKey) {
			return true; /* bubble up */
		}

		if (!thatGallery.enableImageFlagging) {
			return true; /* bubble up */
		}
		thatCanvasImage.flag = !thatCanvasImage.flag;
		thatCanvasImage.redraw({ reposition: true });
		thatGallery.setCookies(); /* separate flags cookie? */

		portablyStopPropagation(e);
		return false;
	};
};

CanvasImage.prototype.createMouseDownHandler = function () {
	var thatCanvasImage = this;
	var thatGallery = this.gallery;
	var thatDocument = document;
	var thatWindow = window;

	return function (e) {
		if (!e) e = window.event;
		portablyPreventDefault(e);

		if (thatGallery.isDragging) {
			portablyStopPropagation(e);
			return false;
		}
		if (!((thatGallery.activeTool == JS_GALLERY_TOOL_MOVE)
			|| e.shiftKey || e.ctrlKey)) {
			return true; /* bubble up */
		}

		if (!thatGallery.enableImageDragging) {
			if (thatGallery.enableImageRaising) {
				thatGallery.raiseImage(thatCanvasImage);
				return false; /* don't bubble up */
			}
			return true; /* bubble up */
		}

		if (thatGallery.enableImageRaising) {
			thatGallery.raiseImage(thatCanvasImage);
		}

		thatGallery.isDragging = true;
		//	thatGallery.gridSnapActive = !e.ctrlKey;
		thatGallery.gridSnapActive = true;
		thatCanvasImage._clientX = e.clientX;
		thatCanvasImage._clientY = e.clientY;
		thatCanvasImage._x = thatCanvasImage.x;
		thatCanvasImage._y = thatCanvasImage.y;
		var keepDragging = function (e) {
			if (!e) e = window.event;
			portablyPreventDefault(e);

			var dx = e.clientX - thatCanvasImage._clientX;
			var dy = e.clientY - thatCanvasImage._clientY;
			var x = thatCanvasImage._x + dx / thatGallery.zoom;
			var y = thatCanvasImage._y + dy / thatGallery.zoom;
			if (thatGallery.gridSnapActive) {
				x = Math.snap(x, thatGallery.gridSnap);
				y = Math.snap(y, thatGallery.gridSnap);
			}
			thatCanvasImage.x = x;
			thatCanvasImage.y = y;
			thatCanvasImage.redraw({ reposition: true });

			portablyStopPropagation(e);
			return false;
		};
		var stopDragging = function (e) {
			if (!e) e = window.event;
			portablyPreventDefault(e);

			if (e.type == "mouseout") {
				if (e.relatedTarget) {
					portablyStopPropagation(e);
					return false;
				}
			}

			thatGallery.isDragging = false;
			removeBinding(thatDocument, "mousemove", keepDragging);
			removeBinding(thatDocument, "mouseup", stopDragging);
			removeBinding(thatWindow, "mouseout", stopDragging);
			thatGallery.setCookies();

			portablyStopPropagation(e);
			return false;
		};

		addBinding(thatDocument, "mousemove", keepDragging);
		addBinding(thatDocument, "mouseup", stopDragging);
		addBinding(thatWindow, "mouseout", stopDragging);

		portablyStopPropagation(e);
		return false;
	};
};

/******************************************************************************
	End Handlers
******************************************************************************/

CanvasImage.prototype.redraw = function (flags) {
	if (!this.isReady) return;
	var src = this.closestImageSrc(this.gallery.zoom);

	var topIC = Math.round(this.y * this.gallery.zoom);
	var leftIC = Math.round(this.x * this.gallery.zoom);

	var top = Math.round(this.gallery.canvas.offsetHeight / 2
		+ (this.y - this.gallery.y)
			* this.gallery.zoom);
	var left = Math.round(this.gallery.canvas.offsetWidth / 2
		+ (this.x - this.gallery.x)
			* this.gallery.zoom);
	var width = Math.round(this.width * this.gallery.zoom);
	var height = Math.round(this.height * this.gallery.zoom);
	var canvasWidth = this.gallery.canvas.offsetWidth;
	var canvasHeight = this.gallery.canvas.offsetHeight;
	var showImage = (
		(top < canvasHeight) && (left < canvasWidth)
		&& ((top + height) >= 0) && ((left + width) >= 0)
	);
	if (!showImage) {
		if (this.imageContainer) {
			if (this.imageNumberDiv) {
				this.imageContainer.removeChild(this.imageNumberDiv);
				this.imageNumberDiv = null;
			}
			this.imageContainer.removeChild(this.imageElement);
			this.imageElement = null;
			this.gallery.imagesContainer.removeChild(this.imageContainer);
			this.imageContainer = null;
		}
	} else {
		var newChild = false;
		if (!this.imageContainer) {
			newChild = true;

			this.imageContainer = document.createElement("div");
			this.imageContainer.className = "imageContainer";
			this.imageContainer.style.position = "absolute";

			this.imageElement = document.createElement("img");
			this.imageElement.className = "imageElement";
			if (this.title != null && this.title != "") {
				this.imageElement.alt   = this.title;
				this.imageElement.title = this.title;
			} else {
				this.imageElement.alt   = this.filename;
				this.imageElement.title = this.filename;
			}
			this.imageElement.style.position = "absolute";
			this.imageElement.style.top = "0px";
			this.imageElement.style.left = "0px";
			this.imageContainer.appendChild(this.imageElement);

			if (0) { /* turn this code off for now */
				this.imageNumberDiv = document.createElement("div");
				this.imageNumberDiv.className = "imageNumberDiv";
				this.imageNumberDiv.style.position = "absolute";
				this.imageNumberDiv.style.top = "-1.25em";
				this.imageNumberDiv.style.left = "0px";
				this.imageNumberDiv.style.height = "1.25em";
				this.imageNumberDiv.style.width = "3em";
				this.imageNumberDiv.style.fontFamily = "sans-serif";
				this.imageNumberDiv.style.color = "black";
				this.imageNumberDiv.style.backgroundColor = "white";
				this.imageNumberDiv.style.paddingBottom = "2px";
				this.imageNumberDiv.appendChild(document.createTextNode(this.imageNumber));
				this.imageContainer.appendChild(this.imageNumberDiv);
			}
		}
		if (newChild || (flags && flags.reposition)) {
			var borderWidth =
				this.gallery.zoomBorderWidths[this.gallery.zoomLevelIndex];
			if (borderWidth == null || borderWidth < 1)
				borderWidth = 1;

			var imageTop = 0;
			var imageLeft = 0;
			
			if (this.flag) {
				imageTop = -borderWidth;
				imageLeft = -borderWidth;
				this.imageElement.style.borderWidth = borderWidth + "px";
				this.imageElement.style.borderStyle = "solid";
				this.imageElement.style.borderColor = "blue"
			} else {
				this.imageElement.style.borderWidth = "0px";
				this.imageElement.style.borderStyle = "solid";
				this.imageElement.style.borderColor = "blue"
			}

			this.imageElement.src = src;
			this.imageContainer.width = width;
			this.imageContainer.height = height;
			this.imageContainer.style.top = topIC + "px";
			this.imageContainer.style.left = leftIC + "px";
			this.imageElement.width = width;
			this.imageElement.height = height;
			this.imageElement.style.position = "relative";
			this.imageElement.style.top = imageTop + "px";
			this.imageElement.style.left = imageLeft + "px";

			var fontsize = this.gallery.fullFontSize * this.gallery.zoomLevels[this.gallery.zoomLevelIndex];
			if (this.gallery.minimumFontSize) {
				if (fontsize < this.gallery.minimumFontSize) fontsize = this.gallery.minimumFontSize;
			}
			if (this.gallery.maximumFontSize) {
				if (fontsize > this.gallery.maximumFontSize) fontsize = this.gallery.maximumFontSize;
			}
			if (this.imageNumberDiv) {
				this.imageNumberDiv.style.fontSize = fontsize + "pt";
				this.imageNumberDiv.style.zIndex = this.zIndex * 2;
			}
			this.imageElement.style.zIndex = this.zIndex * 2 + 1;
		}
		if (newChild) {
			this.gallery.imagesContainer.appendChild(this.imageContainer);

			var ondblclick = this.createDoubleClickHandler();
			var onclick = this.createClickHandler();
			var onmousedown = this.createMouseDownHandler();

			addBinding(this.imageElement, "dblclick", ondblclick);
			addBinding(this.imageElement, "click", onclick);
			addBinding(this.imageElement, "mousedown", onmousedown);
		}
	}
};

function encodeEntities (str) {
	if (str == null) return "";
	if (typeof(str) != "string") str = str.toString();
	str = str.replace(/&/g, "&amp;");
	str = str.replace(/</g, "&lt;");
	str = str.replace(/>/g, "&gt;");
	str = str.replace(/\"/g, "&#34;");
	str = str.replace(/\'/g, "&#39;");
	str = str.replace(/[^\s\x21-\x7e]/g, function (x) {
		return "&#" + x.charCodeAt(0) + ";";
	});
	return str;
}

WallGallery.prototype.getSelectedImages = function () {
	var hasSelectBox = (
		this.startX != null && this.endX != null &&
		this.startY != null && this.endY != null
	);
	if (!hasSelectBox) return null;
	var images = new Array();
	for (var i = 0; i < this.images.length; ++i) {
		var image = this.images[i];
		if (hasSelectBox) {
			if (image.x < this.startX) continue;
			if (image.y < this.startY) continue;
			if ((image.x + image.width) >= this.endX) continue;
			if ((image.y + image.height) >= this.endY) continue;
		}
		images.push(image);
	}
	return images;
};

WallGallery.saveProperties = ["x", "y", "zoom"];

WallGallery.prototype.asXMLText = function () {
	var text = "<gallery";
	for (var i = 0; i < WallGallery.saveProperties.length; ++i) {
		var prop = WallGallery.saveProperties[i];
		text += " " + prop + "=\"" + encodeEntities(this[prop]) + "\"";
	}
	text += ">\n";

	var hasSelectBox = (
		this.startX != null && this.endX != null &&
		this.startY != null && this.endY != null
	);

	var images = this.images;
	if (hasSelectBox) {
		images = this.selectedImages;
	}

	for (var i = 0; i < this.zoomLevels.length; ++i) {
		text += "\t<zoom";
		text += " level=\"" + encodeEntities(this.zoomLevels[i]) + "\"";
		text += " border-width=\"" +
			encodeEntities(this.zoomBorderWidths[i]) + "\"";
		text += " />\n";
	}

	for (var i = 0; i < images.length; ++i) {
		var image = images[i];
		text += "\t<img";
		text += " z-index=\"" + encodeEntities(image.zIndex) + "\"";
		text += " width=\"" + encodeEntities(image.width) + "\"";
		text += " height=\"" + encodeEntities(image.height) + "\"";
		text += " x=\"" + encodeEntities(image.x) + "\"";
		text += " y=\"" + encodeEntities(image.y) + "\"";
		text += " src=\"" + encodeEntities(image.src) + "\"";
		text += " title=\"" + encodeEntities(image.title) + "\"";
		if (image.thumbnails && image.thumbnails.length) {
			text += ">\n";
			for (var j = 0; j < image.thumbnails.length; ++j) {
				var thumbnail = image.thumbnails[j];
				text += "\t\t<thumbnail";
				text += " zoom=\"" + encodeEntities(thumbnail.zoom) + "\"";
				text += " src=\"" + encodeEntities(thumbnail.src) + "\"";
				text += " />\n";
			}
			text += "\t</img>\n";
		} else {
			text += " />\n";
		}
	}
	text += "</gallery>\n";
	return text;
};

var xmlPopup;

WallGallery.prototype.showXML = function () {
	if (xmlPopup && !xmlPopup.closed) {
		xmlPopup.close();
	}
	xmlPopup = window.open("", "js_gallery_xml_popup",
		"width=600,height=400,resizable=yes,scrollbars=yes");
	xmlPopup.document.open();
	xmlPopup.document.write(
		"<html><head><title>XML Source</title></head><body>"
		+ "<h1>XML Source</h1>\n"
		+ "<pre>" + encodeEntities(this.asXMLText()) + "</pre>"
		+ "</body></html>"
	);
	xmlPopup.document.close();
};

WallGallery.prototype.clientToGalleryCoords = function (x, y) {
	var canvas = this.canvas;
	x = x - canvas.offsetLeft - Math.round(canvas.offsetWidth / 2);
	y = y - canvas.offsetTop - Math.round(canvas.offsetHeight / 2);
	x = this.x + x / this.zoom;
	y = this.y + y / this.zoom;
	return [x, y];
};

WallGallery.prototype.hasSelectBox = function () {
	return (
		this.startX != null && this.endX != null &&
		this.startY != null && this.endY != null
	);
};

/******************************************************************************
	Handlers
******************************************************************************/

WallGallery.prototype.createSelectBoxMouseDownHandler = function () {
	var thatGallery = this;
	var thatDocument = document;
	var thatWindow = window;

	return function (e) {
		if (!e) e = window.event;
		portablyPreventDefault(e);

		if (thatGallery.isDragging) {
			portablyStopPropagation(e);
			return false;
		}
		if (!((thatGallery.activeTool == JS_GALLERY_TOOL_MOVE)
			|| e.shiftKey || e.ctrlKey)) {
			return true; /* bubble up */
		}

		var images = thatGallery.selectedImages;
		if (!images || !images.length) {
			portablyStopPropagation(e);
			return false;
		}

		//	thatGallery.gridSnapActive = !e.ctrlKey;
		thatGallery.gridSnapActive = true;
		thatGallery.isDragging = true;
		for (var i = 0; i < images.length; ++i) {
			images[i]._origX = images[i].x;
			images[i]._origY = images[i].y;
		}
		thatGallery._origClientX = e.clientX;
		thatGallery._origClientY = e.clientY;
		thatGallery._origStartX = thatGallery.startX;
		thatGallery._origStartY = thatGallery.startY;
		thatGallery._origEndX = thatGallery.endX;
		thatGallery._origEndY = thatGallery.endY;

		var keepDragging = function (e) {
			if (!e) e = window.event;
			portablyPreventDefault(e);

			var dx = (e.clientX - thatGallery._origClientX) /
				thatGallery.zoom;
			var dy = (e.clientY - thatGallery._origClientY) /
				thatGallery.zoom;

			if (thatGallery.gridSnapActive) {
				dx = Math.snap(dx, thatGallery.gridSnap);
				dy = Math.snap(dy, thatGallery.gridSnap);
			}

			thatGallery.startX = thatGallery._origStartX + dx;
			thatGallery.startY = thatGallery._origStartY + dy;
			thatGallery.endX   = thatGallery._origEndX + dx;
			thatGallery.endY   = thatGallery._origEndY + dy;
			var images = thatGallery.selectedImages;
			for (var i = 0; i < images.length; ++i) {
				var image = images[i];
				image.x = image._origX + dx;
				image.y = image._origY + dy;
				image.redraw({ reposition: true });
				thatGallery.redrawSelectBox();
			}

			portablyStopPropagation(e);
			return false;
		};
		var stopDragging = function (e) {
			if (!e) e = window.event;
			portablyPreventDefault(e);

			if (e.type == "mouseout") {
				if (e.relatedTarget) {
					portablyStopPropagation(e);
					return false;
				}
			}

			thatGallery.isDragging = false;
			removeBinding(thatDocument, "mousemove", keepDragging);
			removeBinding(thatDocument, "mouseup", stopDragging);
			removeBinding(thatWindow, "mouseout", stopDragging);
			thatGallery.setCookies();

			portablyStopPropagation(e);
			return false;
		};

		/* events on select box */
		addBinding(thatDocument, "mousemove", keepDragging);
		addBinding(thatDocument, "mouseup", stopDragging);
		addBinding(thatWindow, "mouseout", stopDragging);

		portablyStopPropagation(e);
		return false;
	};
};

WallGallery.prototype.createSelectBoxClickHandler = function () {
	var thatGallery = this;
	var thatDocument = document;
	var thatWindow = window;

	return function (e) {
		if (!e) e = window.event;
		portablyPreventDefault(e);

		

		if (thatGallery.isDragging) {
			portablyStopPropagation(e);
			return false;
		}
		if (thatGallery.activeTool != JS_GALLERY_TOOL_FLAG) {
			return true; /* bubble up */
		}

		var images = thatGallery.selectedImages;
		if (!images || !images.length) {
			portablyStopPropagation(e);
			return false;
		}

		var flipFlags = false;
		if (e.shiftKey || e.ctrlKey) {
			flipFlags = true;
		}

		var clearFlags = true;
		if (!flipFlags) {
			for (var i = 0; i < images.length; ++i) {
				if (!images[i].flag) {
					clearFlags = false;
					break;
				}
			}
		}

		if (flipFlags) {
			for (var i = 0; i < images.length; ++i) {
				//thatGallery.debugMessage(images.length + " flip");
				images[i].flag = !images[i].flag;
			}
		}
		else if (clearFlags) {
			for (var i = 0; i < images.length; ++i) {
				//thatGallery.debugMessage(images.length + " clear");
				images[i].flag = false;
			}
		}
		else {
			for (var i = 0; i < images.length; ++i) {
				//thatGallery.debugMessage(images.length + " set");
				images[i].flag = true;
			}
		}
		for (var i = 0; i < images.length; ++i) {
			images[i].redraw({ reposition: true });
		}
		thatGallery.setCookies(); /* separate flags cookie? */

		portablyStopPropagation(e);
		return false;
	};
};

WallGallery.prototype.redrawSelectBox = function (options) {
	if (!this.imagesContainer) return;

	var inProgress = options && options.inProgress;

	if (this.hasSelectBox()) {
		if (!this.selectBoxElement) {
			this.selectBoxHasEvents = false;
			var div = this.selectBoxElement = document.createElement("div");
			div.style.position = "absolute";
			div.style.backgroundColor = "black";
			div.style.lineHeight = "0";
			div.style.zIndex = "999999"; /* FIXME */
			this.imagesContainer.appendChild(div);
		}
		var startX = this.startX;
		var startY = this.startY;
		var endX = this.endX;
		var endY = this.endY;
		if (startX > endX) {
			var temp = startX;
			startX = endX;
			endX = temp;
		}
		if (startY > endY) {
			var temp = startY;
			startY = endY;
			endY = temp;
		}
		startX = Math.round(startX * this.zoom);
		endX   = Math.round(endX   * this.zoom);
		startY = Math.round(startY * this.zoom);
		endY   = Math.round(endY   * this.zoom);
		this.selectBoxElement.style.top = startY + "px";
		this.selectBoxElement.style.left = startX + "px";
		this.selectBoxElement.style.height = (endY - startY) + "px";
		this.selectBoxElement.style.width = (endX - startX) + "px";

		if (inProgress) {
			this.selectBoxElement.style.opacity = "0.1";
			this.selectBoxElement.style.filter = "Alpha(opacity=10)";
		} else {
			this.selectBoxElement.style.opacity = "0.2";
			this.selectBoxElement.style.filter = "Alpha(opacity=20)";
		}

		if (!inProgress && !this.selectBoxHasEvents) {
			this.selectBoxHasEvents = true;
			var thatGallery = this;
			var thatDocument = document;
			var thatWindow = window;
			var thatCanvas = this.canvas;

			var onmousedown = this.createSelectBoxMouseDownHandler();
			var onclick = this.createSelectBoxClickHandler();

			if (!this.imagesContainer.hasBindings) {
				this.imagesContainer.hasBindings = 1;
				addBinding(this.imagesContainer, "click", onclick);
				addBinding(this.imagesContainer, "mousedown", onmousedown);
			}

		}

	} else {
		if (this.selectBoxElement) {
			this.imagesContainer.removeChild(this.selectBoxElement);
			this.selectBoxElement = null;
		}
	}
};

WallGallery.prototype.selectTool = function (toolNumber) {
	if (toolNumber == JS_GALLERY_TOOL_SELECT &&
	    !this.enableSelectionBoxes) {
		return;
	}
	if (toolNumber == JS_GALLERY_TOOL_FLAG &&
	    !this.enableImageFlagging) {
		return;
	}
	if (toolNumber == JS_GALLERY_TOOL_MOVE &&
	    !this.enableImageDragging) {
		return;
	}
	this.activeTool = toolNumber;
	this.highlightToolControl();
};


