/***************************************************************************************\
    Name:    PhotoZoomer
    Files:   PhotoZoomer.js, PhotoZoomer.css
    Author:  Abe Heckenbach, Indra Heckenbach
    Date:    7/18/07
    Vers:    0.9.9
	    
    Description:
            PhotoZoomer is a javascript based html image viewer that allows the user 
            to zoom and pan into a high res image using mouse wheel and click-drag 
            actions. PhotoZoomer can also display a mini-map of photo to show zoom 
            context, and to allow for faster panning by dragging the magnifier.
            
    Usage (required steps):
			1) include js and css files, and a variable to access PhotoZoomer from:
				<script type="text/javascript" src="PhotoZoomer.js">var z;/script>
				<link rel="stylesheet" type="text/css" href="PhotoZoomer.css">/style>
			2) create HTML placeholder:
				<div id="zoomer"></div>
			3) create the PhotoZoomer object on page load given placeholder div id:
				<body onload="z = new PhotoZoomer('zoomer');">
			4) load an image:
				z.setImage('image.jpg', 'thumb.jpg');
            
    Usage (option API):
			1) set size of minimap:
				z.setThumbnailWidth(200);
			2) use inline mode:  (note that if used, these calls should be made prior to the setImage() call)
				z.setDimensions(700, 400); 
				z.setDisplayMode(WINDOWED);
			3) toggle visibility:
				z.setDisplayMode(INVISBLE);
			or
				z.setVisible(false);


	Legal:
                         Copyright 2006-2007 VCarious Inc.
                               All rights reserved.
		
	      *** For licensing information, please email stonemonk@gmail.com ***
		
\****************************************************************************************/



// "constants" to use with PhotoZoomer() constructor
var FULLSCREEN_CONTROL	= 0x000001;
var PAN_CONTROLS   		= 0x000010;
var ZOOM_CONTROLS   	= 0x000100;
var MAGNIFIER_CONTROL  	= 0x001000;
var SLIDESHOW_CONTROLS 	= 0x010000;
var ZOOM_TOGGLE_CONTROL	= 0x100000;

var STANDARD_CONTROLS	= PAN_CONTROLS | ZOOM_CONTROLS | MAGNIFIER_CONTROL;
var FULL_CONTROLS	= STANDARD_CONTROLS | FULLSCREEN_CONTROL | SLIDESHOW_CONTROLS | ZOOM_TOGGLE_CONTROL;

// "constants" to use with setDisplayMode()
var MAXIMIZED   		= 0;
var WINDOWED    		= 1;
var INVISIBLE   		= 2;

// "constants" to use with setZoom()
var NATURAL_SIZE		= 0;
var HALF_SIZE			= 1;
var DOUBLE_SIZE			= 2;
var WINDOW_SIZE  		= 3;
var DOUBLE_WINDOW_SIZE  = 4;

// "constants" to use with setImage()
var SHOW_NORMAL			= 0;
var SHOW_WITH_PREVIEW	= 1;
var SHOW_WITH_FADEIN	= 2;

// callback event constants
var PZE_SLIDESHOW		= 0;
var PZE_KEYDOWN			= 1;
var PZE_ENTERFULLSCREEN	= 2;
var PZE_EXITFULLSCREEN	= 3;
var PZE_PREVIEWCLICKED	= 4;
var PZE_ENTERZOOMMODE	= 5;
var PZE_EXITZOOMMODE	= 6;

// internal layout config
var padding = 2;
var mpadding = 4;
var xOffset = 2;
var yOffset = 2;

var None = "none";
var Hidden = "hidden";
var Visible = "visible";
var Style = "style";
var Prototype = "prototype";

// our global internal access point
var _gpz;

PhotoZoomer[Prototype].mediaPath = '';
PhotoZoomer[Prototype].img = 0;
PhotoZoomer[Prototype].dragging = false;
PhotoZoomer[Prototype].imgAspect = 0;
PhotoZoomer[Prototype].imgWidth = 0;
PhotoZoomer[Prototype].imgHeight = 0;
PhotoZoomer[Prototype].visible = 1;
PhotoZoomer[Prototype].isZoomed = 1;
PhotoZoomer[Prototype].reactive = 0;
PhotoZoomer[Prototype].mainControlsReactive = 0;		// 0 means reactive due to reference counting
PhotoZoomer[Prototype].containerName = "";
PhotoZoomer[Prototype].lastX = 0;
PhotoZoomer[Prototype].lastY = 0;
PhotoZoomer[Prototype].xImgOffset=0;
PhotoZoomer[Prototype].yImgOffset=0;
PhotoZoomer[Prototype].thumbnailWidth = 200;
PhotoZoomer[Prototype].thumbnailWidthPercent = 15;
PhotoZoomer[Prototype].displayMode=-1;
PhotoZoomer[Prototype].zoomMode=0;
PhotoZoomer[Prototype].fW = 0;
PhotoZoomer[Prototype].fH = 0;
PhotoZoomer[Prototype].keyStateMap = new Array (0,0,0,0);		// arrow keys specifically, should be general keyCode down map
PhotoZoomer[Prototype].showControls = STANDARD_CONTROLS;
PhotoZoomer[Prototype].imgDataModel = null;
PhotoZoomer[Prototype].imgIndex = 0;
PhotoZoomer[Prototype].fsck = 0;
PhotoZoomer[Prototype].callbackPZ = 0;
PhotoZoomer[Prototype].pzBaseUrl = 0;
PhotoZoomer[Prototype].previewShowing = 1;
PhotoZoomer[Prototype].childZoomer = 0;			// for fullscreen mode, this allows comunication...

PhotoZoomer[Prototype].frame = 0;
PhotoZoomer[Prototype].preview = 0;
PhotoZoomer[Prototype].thumbnail = 0;
PhotoZoomer[Prototype].magnifier = 0;
PhotoZoomer[Prototype].mainControlArea = 0;
PhotoZoomer[Prototype].zoomerControlArea = 0;


// begin public API
function PhotoZoomer(container, pzBaseUrl, media, controls, callbackPZ) {
    _gpz = this;

    this.displayMode = MAXIMIZED;
    this.containerName = container;
	this.pzBaseUrl = pzBaseUrl;
	this.callbackPZ = callbackPZ;
	
	if (media)
		this.mediaPath = media;
	if (controls)
		this.showControls = controls;
    var rootObj = document.getElementById(container);
    rootObj.innerHTML = '<div id="PZ_RefDiv"><div id="frame" style="overflow:Hidden;position:relative;-moz-user-select:none;"></div><div id="pD"></div><table cellspacing=0 cellpadding=0 id="loadingMessage" style="position:absolute;display:none;background-color:#000;padding:10px;"><tr><td><img src="' + this.mediaPath + 'pz-exclamation.gif" style="display:none;" id="exclamationIcon"><img src="' + this.mediaPath + 'pz-loading.gif" id="loadingIcon"></td><td style="color:#fff;font-size:larger;" id="messageText"></td></tr></table><img id="thumbnail" onMouseDown="return _gpz.onThumbClick();" onMouseUp="return _gpz.onThumbRelease();"><div id="magnifier" onmousedown="return _gpz.onMagnifierClick();" onmouseup="return _gpz.onMagnifierRelease();"></div></div>';
    rootObj.onmousemove = onMouseMove;

    this.frame = document.getElementById('frame');
    this.thumbnail = document.getElementById('thumbnail');
    this.magnifier = document.getElementById('magnifier');
	
    this.magnifier.onmouseout = onMouseOut;

    this.renderControls();
    
    if (!window.addEventListener)  // hack to fix minimap alignment in IE
        mpadding = -1;

    if (window.addEventListener)
        rootObj.addEventListener('DOMMouseScroll', onWheel, false);
    else
        rootObj.onmousewheel = onWheel;
		
	document.onkeydown = onKeyPress;
	document.onkeyup = onKeyRelease;
	document.onmouseup = onMouseUp;
	
	if (window.name == "FSPZ" && callbackPZ)
		callbackPZ(PZE_ENTERFULLSCREEN, this);
}

// applies the properties of another PhotoZoomer object to this one
PhotoZoomer[Prototype].applyProperties = function(par) {
	this.setThumbnailWidth(par.thumbnailWidth);
	this.setDisplayMode(MAXIMIZED);
	this.setZoom(WINDOW_SIZE);
	this.setZoomed(par.isZoomed);
	this.setImageDataModel(par.imgDataModel);
	this.setImageIndex(par.imgIndex);
}

// photo zoomer internal fullscreen callback to facilitate syncronization between fullscrren pz and its pz parent, as well as the invoker
function pzIFSCB(pzEvent, param) {
	if (pzEvent == PZE_SLIDESHOW)
		_gpz.imgIndex = param;
	else if (pzEvent == PZE_ENTERFULLSCREEN)
		_gpz.childZoomer = param;
	else if (pzEvent == PZE_EXITFULLSCREEN) {
		_gpz.loadImage();
		_gpz.childZoomer = 0;
	}
		
	if (_gpz.callbackPZ)
		_gpz.callbackPZ(pzEvent, param);
}

// creates a control button DOM and inserts into a control panel
PhotoZoomer[Prototype].createControl = function(controlArea, src, tooltip, onClick) {
	createControl(document.getElementById(controlArea), src, tooltip, onClick);
}

PhotoZoomer[Prototype].setThumbnailWidth = function(w) {
    this.thumbnailWidthPercent = w;
	if (this.visible)
	    this.adjustSize();
}

PhotoZoomer[Prototype].setZoom = function(z) {
	this.zoomMode = z;
}

PhotoZoomer[Prototype].setDimensions = function(w, h) {
    this.fW = w;
    this.fH = h;
    
    if (this.displayMode == WINDOWED)
        this.adjustSize();
}

PhotoZoomer[Prototype].setDisplayMode = function(mode) {
    if (this.displayMode == INVISIBLE) {
        this.setVisible(0);
        return;
    }

	this.displayMode = mode;
	
	// in maximized mode we make all the other children of body inVisible
	for (i=0; i < document.body.childNodes.length; i++) {
		var el = document.body.childNodes.item(i);
		if (el.style && el.id != "zoomer") {
			if (mode == MAXIMIZED) {
				el.oldDisplay = el[Style].display;
				el[Style].display = None;
			} else if (el.oldDisplay)
				el[Style].display = el.oldDisplay;
		}
	}

	var style = document.body.style;
	var b = document.body;
	if (mode == MAXIMIZED) {
		b.oldBGColor = style.backgroundColor;
		b.oldPadding = style.padding;
		b.oldMargin = style.margin;
		b.oldOverflow = style.overflow;
		style.backgroundColor = "#000";
		style.padding = 0;
		style.margin = 0;
		style.overflow = Hidden;
	} else if (b.oldPadding) {
		style.backgroundColor = b.oldBGColor;
		style.padding = b.oldPadding;
		style.margin = b.oldMargin;
		style.overflow = b.oldOverflow;
	}
	
    this.adjustSize();
}

PhotoZoomer[Prototype].getDisplayMode = function() {
    return this.displayMode;
}
	
PhotoZoomer[Prototype].setVisible = function(vis) {
    this.visible = vis;
    document.getElementById(this.containerName)[Style].display = (vis)?"":None;

    this.adjustSize();
}

PhotoZoomer[Prototype].setImageDataModel = function(imgDataModel) {
	this.imgDataModel = imgDataModel;
}

PhotoZoomer[Prototype].setImageIndex = function(imgI) {
	if (this.childZoomer)
		this.childZoomer.setImageIndex(imgI);
	else if (this.imgDataModel && imgI >=0 && imgI < this.imgDataModel.getTotal()) {
		this.imgIndex = imgI;
		if (this.callbackPZ)
			this.callbackPZ(PZE_SLIDESHOW, this.imgIndex);
		var desc = this.imgDataModel.getImageDescriptor(imgI);
		if (desc) {
			desc.preload();
			this.loadImage();
		}
	}
}
	
PhotoZoomer[Prototype].setImage = function(img, thumb, medium) {
    if (!thumb && !medium) {
        thumb = img;
        medium = img;
    }
    if (!medium)
        medium = thumb;
    if (!thumb)
        thumb = medium; 

	this.imgDataModel = new PZInteralDataModel(new PZImageDescriptor(img, thumb, medium));
	this.setImageIndex(0);
}
// end public API


// begin internal "private" functions
PhotoZoomer[Prototype].loadImage = function() {
	var desc;
	if (this.imgDataModel && this.imgDataModel.getTotal() > 0) {
		desc = this.imgDataModel.getImageDescriptor(this.imgIndex);
		if (!desc)
			return;
	}
	
	this.reactive = 0;
	// seemingly unnecassary step, but is needed for a bug workaround in IE...
	this.frame.innerHTML = '<img id="s" onmousedown="return _gpz.onMouseDown()" onmouseup="return _gpz.onMouseUp()" style="position:relative; visibility: Hidden;" GALLERYIMG="no"/>';
    this.img = document.getElementById('s');
    this.img.onmouseOut = onMouseOut;
	
	document.getElementById("pD").innerHTML = '<img id="p" onmousedown="return _gpz.onPreviewClicked()" style="position: absolute; visibility: Hidden;" GALLERYIMG="no"/>'
    this.preview = document.getElementById('p');
	// end silly IE hack

	this.magnifier[Style].visibility = Hidden;
	this.thumbnail[Style].display = None;
	this.zoomerControlArea[Style].display = None;
	

	this.showLoadingMessage();

	this.previewShowing = 1;	// always start with preview image showing and only remove if full image is loaded and superior
	this.preview[Style].left = findPosX(document.getElementById("PZ_RefDiv"));
	this.preview[Style].top = findPosY(document.getElementById("PZ_RefDiv"));
	this.preview.onload = this.previewImageLoaded;

	// start loading real image
	if (this.isZoomed) {
	    this.img.onload = this.imageLoaded;
		this.img.src = desc.original;
	} else {
		this.mainControlArea[Style].visibility = Hidden;
    }

	// give user a temp image to look at...
	this.preview.src = desc.medium;

	this.thumbnail.src = desc.thumb;
}

PhotoZoomer[Prototype].preloadNeighbors = function() {
	if (_gpz.imgDataModel) {
		if (_gpz.imgIndex+1 < _gpz.imgDataModel.getTotal()) {
			var desc = _gpz.imgDataModel.getImageDescriptor(_gpz.imgIndex+1);
			if (desc)
				desc.preload();
		}
		if (_gpz.imgIndex > 0) {
			var desc = _gpz.imgDataModel.getImageDescriptor(_gpz.imgIndex-1);
			if (desc)
				desc.preload();
		}
	}
}

PhotoZoomer[Prototype].previewImageLoaded = function() {
	normalizeImage(this);
	if (_gpz.previewShowing)
		_gpz.adjustSize();
	if (!_gpz.isZoomed) {
		_gpz.hideLoadingMessage();

		_gpz.loadingComplete();
		
		// preload original for in case user wants to zoom
		if (0 && _gpz.imgDataModel) {
			var desc = _gpz.imgDataModel.getImageDescriptor(_gpz.imgIndex);
			if (desc) {
				desc.preloadOriginal();
			}
		}
	}
}

PhotoZoomer[Prototype].imageLoaded = function() {
	normalizeImage(this);		// this is to fix IE, but ONLY works because we flush the old source img DOM and recreate a fresh IMG holder in setImage
    _gpz.imgAspect = this.height / this.width;
    _gpz.imgWidth = this.width;
    _gpz.imgHeight = this.height;
    
	_gpz.hideLoadingMessage();
	
	// if image is smaller than viewport revert to standard inline image style without zoomer controls
	if (_gpz.imgWidth*1.3 < _gpz.getViewerWidth() || _gpz.imgHeight*1.3 < _gpz.getViewerHeight()) {
		_gpz.reactive = 0;
		_gpz.previewShowing = 1;
		// hrmm tell user here that original image suxor...
		_gpz.showMessage("Original photo not large enough for zooming","exclamationIcon", 800);
	} else {
		// setup image for requested default zoom level
		_gpz.reactive = 1;
		if (_gpz.zoomMode != NATURAL_SIZE) {
			if (_gpz.zoomMode == HALF_SIZE)
				_gpz.img.width = _gpz.imgWidth / 2;
			else if (_gpz.zoomMode == DOUBLE_SIZE)
				_gpz.img.width = _gpz.imgWidth * 2;
			else
				_gpz.img.width = 0;
				
			if (_gpz.zoomMode == DOUBLE_WINDOW_SIZE) {
				_gpz.img.height = 0;
			    _gpz.adjustSize();
				_gpz.img.width *= 2;
			}
			_gpz.img.height = _gpz.imgAspect * _gpz.img.width;
		}
		
		_gpz.previewShowing = 0;
	    _gpz.adjustSize();
		// center image
		_gpz.handlePan(-(_gpz.img.width - _gpz.getViewerWidth())/2, -(_gpz.img.height - _gpz.getViewerHeight())/2);
		_gpz.setZoomed(_gpz.isZoomed);
		_gpz.img[Style].visibility = Visible;
	}
	
	_gpz.loadingComplete();
}

PhotoZoomer[Prototype].loadingComplete = function() {
	_gpz.preloadNeighbors();
	if (window.name == "FSPZ" && !this.fsck) {
		this.showMessage("Press F11 to go completely Fullscreen","exclamationIcon", 1000);
		this.fsck = 1;
		this.adjustSize();
	}
}

// adjustSize() recalculates the dimensions of all PhotoZoomer components to perform layout
PhotoZoomer[Prototype].adjustSize = function() {
    if (!this.visible)
        return;
		
	this.thumbnailWidth = this.thumbnailWidthPercent * this.getViewerWidth() / 100;
	this.thumbnailWidth = range(this.thumbnailWidth, 120, 300);
	
	var canvas = this.frame;
	
	if (window.name == "FSPZ" && this.fsck==1) {
		//alert(parseInt(canvas[Style].width) + " " + this.getViewerWidth() + " " + screen.width);
		if (parseInt(canvas[Style].width) < this.getViewerWidth() || this.getViewerWidth() == screen.width) {
			this.hideMessage();
			this.fsck++;
		}
	}
	
	// always display and position main control area
	if (this.displayMode == MAXIMIZED)
		canvas[Style].position = "absolute";
	else
		canvas[Style].position = "relative";
	canvas[Style].left = 0;
	canvas[Style].top = 0;
	canvas[Style].width = this.getViewerWidth();
	canvas[Style].height = this.getViewerHeight();

    
	// size and position preview image maximally and centered
	var previewImage = this.preview;
	if (previewImage.naturalWidth) {	// this code breaks IE until previewImageLoaded is called
		if (previewImage.naturalWidth > _gpz.getViewerWidth() || previewImage.naturalHeight > _gpz.getViewerHeight()) {
			if (this.getViewerWidth()/this.getViewerHeight() < previewImage.naturalWidth/previewImage.naturalHeight) {
				previewImage[Style].width = this.getViewerWidth();
				previewImage[Style].height = this.getViewerWidth() * previewImage.naturalHeight / previewImage.naturalWidth;
			} else {
				previewImage[Style].height = this.getViewerHeight();
				previewImage[Style].width = this.getViewerHeight() / previewImage.naturalHeight * previewImage.naturalWidth;
			}
		} else {
			if (this.displayMode == MAXIMIZED) { // hrmmm... maybe not ?
				previewImage[Style].width = 1.2 * previewImage.naturalWidth;
				previewImage[Style].height = 1.2 * previewImage.naturalHeight;
				if (parseInt(previewImage[Style].width) > this.getViewerWidth() || parseInt(previewImage[Style].height) > this.getViewerHeight()) {
					if (this.getViewerWidth()/this.getViewerHeight() < parseInt(previewImage[Style].width)/parseInt(previewImage[Style].height)) {
						previewImage[Style].width = this.getViewerWidth();
						previewImage[Style].height = this.getViewerWidth() * previewImage.naturalHeight / previewImage.naturalWidth;
					} else {
						previewImage[Style].height = this.getViewerHeight();
						previewImage[Style].width = this.getViewerHeight() / previewImage.naturalHeight * previewImage.naturalWidth;
					}
				}
			} else {
				previewImage[Style].width = previewImage.naturalWidth;
				previewImage[Style].height = previewImage.naturalHeight;
			}
		}
		if (this.displayMode != MAXIMIZED) {
			var centerOffsetX = _gpz.getViewerWidth() - parseInt(previewImage[Style].width);
			 if (centerOffsetX < 0)	// this should never happen, but we check anyways to be safe ;-)
				 centerOffsetX = 0;
			previewImage[Style].left = findPosX(canvas) + centerOffsetX/2;
			previewImage[Style].top = findPosY(canvas) + (_gpz.getViewerHeight() - parseInt(previewImage[Style].height))/2;
		} else {
			previewImage[Style].left = (_gpz.getViewerWidth() - parseInt(previewImage[Style].width))/2;
			previewImage[Style].top = (_gpz.getViewerHeight() - parseInt(previewImage[Style].height))/2;
		}
		
		if (this.previewShowing)
			previewImage[Style].visibility = Visible;
	}
	
	if (this.mainControlArea) {
		if ((this.showControls & SLIDESHOW_CONTROLS) || this.displayMode == MAXIMIZED) {
			if (this.imgDataModel && this.imgDataModel.getTotal() > 1) {
				document.getElementById("slideshowPrevious")[Style].display = "inline";
				document.getElementById("slideshowNext")[Style].display = "inline";
			} else {
				document.getElementById("slideshowPrevious")[Style].display = None;
				document.getElementById("slideshowNext")[Style].display = None;
			}
		}
	
		this.mainControlArea[Style].display = "";
        this.mainControlArea[Style].top = findPosY(canvas) + 2 + "px";
		if (this.displayMode == MAXIMIZED)
    	    this.mainControlArea[Style].left = findPosX(canvas) + parseInt(canvas[Style].width) - parseInt(this.mainControlArea.clientWidth) - 16 + "px";
   	    else {
			if (!this.previewShowing)
    	    	this.mainControlArea[Style].left = findPosX(canvas) + parseInt(canvas[Style].width) - parseInt(this.mainControlArea.clientWidth) - 3 + "px";
			else if (previewImage.naturalWidth) {
    	    	this.mainControlArea[Style].left = parseInt(previewImage[Style].left) + parseInt(previewImage[Style].width) - parseInt(this.mainControlArea.clientWidth) - 3 + "px";
		        this.mainControlArea[Style].top = parseInt(previewImage[Style].top) + 4 + "px";
			}
		}
		if (this.displayMode == MAXIMIZED || !this.previewShowing || (previewImage.naturalWidth && parseInt(previewImage[Style].width) > 0))
			this.mainControlArea[Style].visibility = Visible;
		else
			this.mainControlArea[Style].visibility = Hidden;
    }        

	if (!this.reactive)
        return;


	var canvasRight = findPosX(canvas) + parseInt(canvas[Style].width);
    var canvasBottom = findPosY(canvas) + parseInt(canvas[Style].height);
	
    this.thumbnail[Style].width = this.thumbnailWidth;
    this.thumbnail[Style].height = this.thumbnailWidth * this.imgAspect;
    this.thumbnail[Style].top = canvasBottom - this.thumbnailWidth*this.imgAspect - yOffset - padding;
    this.thumbnail[Style].left = canvasRight - this.thumbnailWidth - xOffset - padding;
	if (this.isZoomed && !this.previewShowing) {
	    this.thumbnail[Style].display = "";
    	this.thumbnail[Style].visibility = Visible;
    	this.magnifier[Style].visibility = Visible;
	}
	
    if (this.zoomerControlArea) {
        this.zoomerControlArea[Style].top = findPosY(canvas) + 5;
        this.zoomerControlArea[Style].left = findPosX(canvas) + 5;
//        this.zoomerControlArea[Style].top = canvasBottom - this.thumbnailWidth*this.imgAspect - yOffset + 20 + "px";
//        this.zoomerControlArea[Style].left = canvasRight - this.thumbnailWidth - xOffset - 50 + "px";
		if (this.isZoomed && !this.previewShowing)
	        this.zoomerControlArea[Style].display = "";
    }

    this.handleZoom(0); // this will re-enforce image size/bounds to prevent whitespace from appearing
}

PhotoZoomer[Prototype].showMessage = function(text, icon, timeout) {
	this.hideMessage();
	var loadingMessage = document.getElementById("loadingMessage");
    loadingMessage[Style].visibility = Hidden;
	
	document.getElementById(icon)[Style].display = "";
	document.getElementById("messageText").innerHTML = "&nbsp;" + text + "&nbsp;";
	
    loadingMessage[Style].display = "";
    loadingMessage[Style].left = findPosX(this.frame) + this.getViewerWidth()/2 - loadingMessage.clientWidth/2;
    loadingMessage[Style].top = findPosY(this.frame) + this.getViewerHeight()/2 - loadingMessage.clientHeight/2;
    loadingMessage[Style].visibility = Visible;
	
	if (timeout)
		setTimeout(function () {_gpz.hideMessage();}, timeout);
}

PhotoZoomer[Prototype].hideMessage = function() {
    document.getElementById("loadingMessage")[Style].display = None;
	document.getElementById("exclamationIcon")[Style].display = None;
	document.getElementById("loadingIcon")[Style].display = None;
}

PhotoZoomer[Prototype].showLoadingMessage = function() {
	this.showMessage("Loading...", "loadingIcon");
	this.mainControlsReactive += 1;
}

PhotoZoomer[Prototype].hideLoadingMessage = function() {
    this.hideMessage();
	this.mainControlsReactive -= 1;
}

PhotoZoomer[Prototype].enterZoomMode = function() {
	// enter zoom mode at window size.... todo: enter zoom mode at specified zoom constant
	if (!this.isZoomed) {
		this.setZoomed(true);
		this.adjustSize();
	}
}

PhotoZoomer[Prototype].setZoomed = function(zoomed) {
	if (this.showControls & ZOOM_TOGGLE_CONTROL) {
		if (!this.isZoomed && zoomed) {
			this.previewShowing = 0;
			if (this.callbackPZ)
				this.callbackPZ(PZE_ENTERZOOMMODE);
			if (_gpz.imgDataModel) {
				var desc = _gpz.imgDataModel.getImageDescriptor(_gpz.imgIndex);
				if (desc && justifyUrl(_gpz.img.src) != justifyUrl(desc.original)) {
					_gpz.showLoadingMessage();
					desc.preloadOriginal();
					this.isZoomed = zoomed;
					_gpz.img.onload = _gpz.imageLoaded;
					_gpz.img.src = desc.original;
					return;
				}
			}
		} else if (this.isZoomed && !zoomed) {
			this.previewShowing = 1;
			if (this.callbackPZ)
				this.callbackPZ(PZE_EXITZOOMMODE);
		}

		this.isZoomed = zoomed;
	}
	
	// adjust component visibility as per zoom mode
	if (this.isZoomed && !this.previewShowing) {
		if (this.img.naturalWidth > _gpz.getViewerWidth() && this.img.naturalHeight > _gpz.getViewerHeight()) {
			this.magnifier[Style].visibility = Visible;
			this.thumbnail[Style].visibility = Visible;
			this.thumbnail[Style].display = "";
			this.zoomerControlArea[Style].display = "";
		}
		this.img[Style].display = "";
		this.preview[Style].visibility = Hidden;
		if (this.showControls & ZOOM_TOGGLE_CONTROL) {
			document.getElementById("zoomOn")[Style].display = None;
			document.getElementById("zoomOff")[Style].display = "inline";
		}
	} else {
		this.magnifier[Style].visibility = Hidden;
		this.thumbnail[Style].display = None;
		this.zoomerControlArea[Style].display = None;
		if (this.img) {
			this.img[Style].display = None;
			this.preview[Style].visibility = Visible;
		}
		document.getElementById("zoomOn")[Style].display = "inline";
		document.getElementById("zoomOff")[Style].display = None;
	}
}

PhotoZoomer[Prototype].handleZoom = function(delta) {
	if (!this.reactive)
		return;
		
    var oldW = this.img.width;
    var oldH = this.img.height;

    var w = this.getViewerWidth();
    var h = this.getViewerHeight();

	if (this.img[Style].visibility == Visible && (this.showControls & ZOOM_TOGGLE_CONTROL)) {
		if (this.img.width > 0 && this.img.width+100*delta <= w && delta < 0) {
			this.setZoomed(false);
			this.adjustSize();
			return;
		} else if (delta != 0) {		// dont turn on zoom when in degenerate handleZoom case (when used for bounds checking)
			//this.setZoomed(true);		// enterZoomMode is explicitly called now so this is no longer needed
		}
	}
	
    // since we cannot simutaneously resize the image in both diretcions, we do it incrementally alternating the direction being resized... only do this in mozilla, as IE is painfully slow
    if (0 && window.addEventListener) {
		for (i=0; i < 10; i++) {
			// do not allow zoom out so far as to expose a white bar at sides
			if (this.img.width <= w && delta <= 0) {
				this.img.width = w;
				this.img.height = this.imgAspect * w;
				break;
			}
			this.img.width += 10*delta;
			this.img.height = this.imgAspect * this.img.width;
		}
	} else {
		var imgDelta = this.img.width*.05*delta;
		if (this.img.width+imgDelta <= w && delta <= 0) {
			this.img.width = w;
			this.img.height = this.imgAspect * w;
		} else if (this.img.width+imgDelta < 32767) {
			this.img.width += imgDelta;
			this.img.height = this.imgAspect * this.img.width;
		}
	}

	// do not allow zoom out so far as to expose a white bar at top/bottom, or so far in as to overflow a signed short int ;-)
    if (this.img.height < h || this.img.height > 32767) {
        this.img.height = h;
    }
	
	this.img.width = this.img.height / this.imgAspect;	// ensure proper aspect

    // compute the translation amount to result in the zoom being centered .. this is somewhat complicated to compute, since we are really acheiving the zoom by resizing the image behind the viewport. effectively this works by preserving the ratio of the center point to the old image size with that of the new center point to the new image size
	var centerX = this.xImgOffset+w/2;
	var centerY = this.yImgOffset+h/2;
    var deltaX = centerX - centerX*this.img.width/oldW;
    var deltaY = centerY - centerY*this.img.height/oldH;

    this.handlePan(deltaX, deltaY);
}

PhotoZoomer[Prototype].handlePan = function(ix, iy) {
	if (!this.reactive || !this.isZoomed)
		return;

	var canvas = this.frame;
    var thumb = this.thumbnail;
    var mag = this.magnifier;

    // compute new image offset
    if (this.draggingMag) {
        this.xImgOffset = range(this.xImgOffset - this.img.width*ix/-this.thumbnailWidth, 0, this.img.width - this.getViewerWidth());
        this.yImgOffset = range(this.yImgOffset - this.img.height*iy/(-this.thumbnailWidth*this.imgAspect), 0, this.img.height - this.getViewerHeight());
    } else { // this code can run even when no user dragging is occuring (e.g. browser resize, or zoom)
        this.xImgOffset = range(this.xImgOffset - ix, 0, this.img.width - this.getViewerWidth());
        this.yImgOffset = range(this.yImgOffset - iy, 0, this.img.height - this.getViewerHeight());
    }
    
    // pan image
    this.img[Style].left = -1 * this.xImgOffset + "px";
    this.img[Style].top = -1 * this.yImgOffset + "px";

    // move magnifier on minimap
    mag[Style].left = parseInt(thumb[Style].left) + padding + this.xImgOffset * thumb.width / this.img.width;
    mag[Style].top = parseInt(thumb[Style].top) + padding + this.yImgOffset * thumb.height / this.img.height;
    mag[Style].width = parseInt(canvas[Style].width) * thumb.width / this.img.width - mpadding;
    mag[Style].height = parseInt(canvas[Style].height) * thumb.height / this.img.height - mpadding;
}

// Viewer size represents the desired size of the view port
PhotoZoomer[Prototype].getViewerWidth = function() {
	return (this.displayMode == MAXIMIZED) ? getWinWidth() : this.fW;
}

PhotoZoomer[Prototype].getViewerHeight = function() {
	return (this.displayMode == MAXIMIZED) ? getWinHeight() : this.fH;
}


////////// event handlers /////////////

// global to local context switch wrappers
function onMouseOut(e) {
    _gpz.onMouseOut(e);
}

function onMouseMove(e) {
    _gpz.onMouseMove(e);
}

function onMouseUp(e) {
    _gpz.onMouseUp(e);
}

function onWheel(e) {
    _gpz.onWheel(e);
}

function onKeyPress(e) {
	if (!e) var e = window.event;
	if (!_gpz.visible) {
		if (_gpz.callbackPZ)
			return _gpz.callbackPZ(PZE_KEYDOWN, e);
		else
			return true;
	}
		
	if (e.keyCode) code = e.keyCode;
	else if (e.which) code = e.which;
	//alert(e.keyCode-37);
	
	if (code == 27 && window.name == "FSPZ") {
		window.close();
		return false;
	}

	code -= 37;

	if ((e.ctrlKey || _gpz.previewShowing || !_gpz.isZoomed) && !_gpz.mainControlsReactive) {
		if (code == 2) {
			_gpz.setImageIndex(_gpz.imgIndex+1);
			cancelEvents(e);
			return false;
		} else if (code == 0) {
			_gpz.setImageIndex(_gpz.imgIndex-1);
			cancelEvents(e);
			return false;
		}
	}
	
	if ((e.ctrlKey && code == 1) || code == 24) {
		_gpz.enterZoomMode();
		_gpz.handleZoom(5);
		cancelEvents(e);
		return false;
	} else if ((e.ctrlKey && code == 3) || code == 72) {
		_gpz.handleZoom(-5);
		cancelEvents(e);
		return false;
	}
	
	if (code >= 0 && code <= 4) {
		if (!e.ctrlKey) {
			_gpz.keyStateMap[code] = 1;
			var panX = _gpz.keyStateMap[0] ? 100 : (_gpz.keyStateMap[2] ? -100 : 0);
			var panY = _gpz.keyStateMap[1] ? 100 : (_gpz.keyStateMap[3] ? -100 : 0);
			_gpz.handlePan(panX, panY);
		}
		
		cancelEvents(e);
		return false;
	}
	
	if (_gpz.callbackPZ)
		return _gpz.callbackPZ(PZE_KEYDOWN, e);
}

function onKeyRelease(e) {
	if (!e) var e = window.event;
	if (e.keyCode) code = e.keyCode;
	else if (e.which) code = e.which;
	
	code -= 37;

	if (code >= 0 && code <= 4) {
		_gpz.keyStateMap[code] = 0;
		return false;
	}
}

PhotoZoomer[Prototype].onMagnifierClick = function() {
    this.draggingMag = true;
    return false;
}

PhotoZoomer[Prototype].onMagnifierRelease = function() {
    this.draggingMag = false;
}

PhotoZoomer[Prototype].onThumbClick = function() {
    this.draggingMag = true;
	this.handlePan(this.lastX - (findPosX(this.magnifier) + 0.5*parseInt(this.magnifier[Style].width)), this.lastY - (findPosY(this.magnifier) + 0.5*parseInt(this.magnifier[Style].height)));
	return false;
}

PhotoZoomer[Prototype].onThumbRelease = function() {
    this.draggingMag = false;
}

PhotoZoomer[Prototype].onPreviewClicked = function() {
	if (this.callbackPZ)
		this.callbackPZ(PZE_PREVIEWCLICKED);
}

PhotoZoomer[Prototype].onMouseDown = function() {
	if (!this.reactive)
		return;

	this.frame[Style].cursor = "move";
	this.dragging = true;
    setOpacity(this.zoomerControlArea, 60);
    this.zoomerControlArea[Style].display = None;
    return false;
}

PhotoZoomer[Prototype].onMouseUp = function() {
	this.frame[Style].cursor = "";
    if (this.dragging) {
        this.dragging = false;
        this.draggingMag = false;
        this.zoomerControlArea[Style].display = "";
        fadeIn(this.zoomerControlArea, 20);
    }
}

PhotoZoomer[Prototype].onMouseOut = function(e) {
    if (!e) var e = window.event;
    var relTarg = e.relatedTarget || e.toElement;
    if (relTarg != this.thumbnail) {
        this.onMouseUp();
        this.draggingMag = false;
    }
}

PhotoZoomer[Prototype].onMouseMove = function(e) {
    if (!e) var e = window.event;

    var thisX = (e.pageX) ? e.pageX : e.clientX;
    var thisY = (e.pageY) ? e.pageY : e.clientY;

    if (this.dragging || this.draggingMag)
        this.handlePan(thisX-this.lastX , thisY-this.lastY);
	else
		this.frame[Style].cursor = "";

    this.lastX = (e.pageX) ? e.pageX : e.clientX;
    this.lastY = (e.pageY) ? e.pageY : e.clientY;

    if (e.preventDefault)
        e.preventDefault();
    e.returnValue = false;
}

PhotoZoomer[Prototype].onWheel = function(event){
        var delta = 0;
        if (!event) /* For IE. */
                event = window.event;
        if (event.wheelDelta) { /* IE/Opera. */
                delta = event.wheelDelta/120;
                /** In Opera 9, delta differs in sign as compared to IE.
                 */
                if (window.opera)
                        delta = -delta;
        } else if (event.detail) { /** Mozilla case. */
                /** In Mozilla, sign of delta is different than in IE.
                 * Also, delta is multiple of 3.
                 */
                delta = -event.detail/3;
        }
        /** If delta is nonzero, handle it.
         * Basically, delta is now positive if wheel was scrolled up,
         * and negative, if wheel was scrolled down.
         */
        if (delta) {
				if (!this.isZoomed && delta > 0)
					this.enterZoomMode();
					
   	            this.handleZoom(delta);
		}
        /** Prevent default actions caused by mouse wheel.
         * That might be ugly, but we handle scrolls somehow
         * anyway, so don't bother here..
         */
        if (event.preventDefault)
                event.preventDefault();
        event.returnValue = false;
}

//  custom control overlay
PhotoZoomer[Prototype].renderControls = function(){
    var div, img;
    var cont = document.getElementById(this.containerName);
	
	// setup the zoomer control area
    var controlArea = createControlArea("zoomerControlArea");

	if (this.showControls & PAN_CONTROLS) {
		div = createDiv(controlArea);
		div[Style].paddingLeft = "10px";
		div[Style].paddingRight = "10px";
		img = createImageButton('map-nav-up-off.png', div, "Pan Up");
		img.onclick = function() { _gpz.handlePan(0, 100); };
	
		var paddingArea = createDiv(controlArea);
		paddingArea[Style].paddingTop = "3px";
	
		div = createDiv(controlArea);
		div[Style].paddingRight = "6px";
		div[Style].display = "inline";
		img = createImageButton('map-nav-left-off.png', div, "Pan Left");
		img.onclick = function() { _gpz.handlePan(100, 0); };
	
		div = createDiv(controlArea);
		div[Style].display = "inline";
		img = createImageButton('map-nav-right-off.png', div, "Pan Right");
		img.onclick = function() { _gpz.handlePan(-100, 0); };
	
		div = createDiv(controlArea);
		div[Style].paddingLeft = "10px";
		div[Style].paddingRight = "10px";
		img = createImageButton('map-nav-down-off.png', div, "Pan Down");
		img.onclick = function() { _gpz.handlePan(0, -100); };
	}
	
	if (this.showControls & ZOOM_CONTROLS) {
		div = createDiv(controlArea);
		div[Style].paddingLeft = "10px";
		div[Style].paddingRight = "10px";
		div[Style].paddingTop = "10px";
		img = createImageButton('map-zoom-in-off.png', div, "Zoom In");
		img.onclick = function() { _gpz.handleZoom(5); };
		
		div = createDiv(controlArea);
		div[Style].paddingLeft = "10px";
		div[Style].paddingRight = "10px";
		div[Style].paddingTop = "5px";
		img = createImageButton('map-zoom-out-off.png', div, "Zoom Out");
		img.onclick = function() { _gpz.handleZoom(-5); };
	}
	
	if (0 && this.showControls & FULLSCREEN_CONTROL) {
		div = createDiv(controlArea);
		div[Style].paddingLeft = "10px";
		div[Style].paddingTop = "10px";
		img = createImageButton('pz-full-screen.png', div, "Maximize");
		img.onclick = function() { if (_gpz.getDisplayMode() == WINDOWED) _gpz.setDisplayMode(MAXIMIZED); else _gpz.setDisplayMode(WINDOWED);};
	}

	cont.appendChild(controlArea);
	this.zoomerControlArea = controlArea;

// top control area
    var cont = document.getElementById(this.containerName);
    var controlArea = createControlArea("mainControlArea");

		div = createDiv(controlArea);
		div[Style].display = "inline";
	    div.setAttribute("id", "slideshowPrevious");
		img = createImageButton('pz-left.png', div, "View Previous Photo");
		img.onclick = function() { if (!_gpz.mainControlsReactive) _gpz.setImageIndex(_gpz.imgIndex-1); };
	
		div = createDiv(controlArea);
		div[Style].display = "inline";
		div[Style].paddingLeft = "5px";
	    div.setAttribute("id", "slideshowNext");
		img = createImageButton('pz-right.png', div, "View Next Photo");
		img.onclick = function() { if (!_gpz.mainControlsReactive) _gpz.setImageIndex(_gpz.imgIndex+1); };

	if (this.showControls & ZOOM_TOGGLE_CONTROL) {
		div = createDiv(controlArea);
		div[Style].paddingLeft = "5px";
		div[Style].display = None;
	    div.setAttribute("id", "zoomOff");
		img = createImageButton('pz-zoom-out.png', div, "Exit Zoom Mode");
		img.onclick = function() { if (!_gpz.mainControlsReactive) {_gpz.setZoomed(false); _gpz.adjustSize();} };

		div = createDiv(controlArea);
		div[Style].paddingLeft = "5px";
		div[Style].display = "inline";
	    div.setAttribute("id", "zoomOn");
		img = createImageButton('pz-zoom-in.png', div, "Explore Photo in Zoom Mode");
		img.onclick = function() { if (!_gpz.mainControlsReactive) _gpz.enterZoomMode(); };
	}
	if (this.showControls & FULLSCREEN_CONTROL) {
		div = createDiv(controlArea);
		div[Style].paddingLeft = "5px";
		div[Style].display = "inline";
		if (window.name == "FSPZ") {
			img = createImageButton('pz-close.png', div, "Exit Fullscreen");
			img.onclick = function() {
				window.close();
			};
		} else {
			img = createImageButton('pz-full-screen.png', div, "Go Fullscreen");
			img.onclick = function() {
				var newWin=window.open('', 'FSPZ', 'top=0,left=0,height=' + screen.height + ',width=' + screen.width + ',fullscreen=yes,toolbar=no,scrollbars=no,resizable=yes,status=no,copyhistory=no,location=no,menubar=no,directories=no');
				newWin.moveTo(0,0);
				newWin.focus();
				newWin.document.write('<html><head><script type="text/javascript" src="' + _gpz.pzBaseUrl + 'PhotoZoomer.js">' + chrr(60) + '/script><script type="text/javascript">var z; function init() {z = new PhotoZoomer("zoomer","' + _gpz.pzBaseUrl + '","' + _gpz.mediaPath + '", FULL_CONTROLS, opener.pzIFSCB); z.applyProperties(opener._gpz);}' + chrr(60) + '/script><link rel="stylesheet" type="text/css" href="' + _gpz.pzBaseUrl +'PhotoZoomer.css"></head><body onload="init();" onunload="opener.pzIFSCB(PZE_EXITFULLSCREEN);" onresize="if (z) z.adjustSize();"><div id="zoomer"></div></body></html>');
				newWin.document.close();
			};
		}
	}

	cont.appendChild(controlArea);
	
	this.mainControlArea = controlArea;
}

////////// generic functions /////////////

function chrr(x) {
    return String.fromCharCode(x);
}

function cancelEvents(event) {
	if (window.event) {
		window.event.returnValue = false;
		window.event.cancelBubble = true;
	} else if (event) {
		event.preventDefault();
		event.stopPropagation();
	}
	return false;
}

function justifyUrl(url) {
	if (!url || url == "")
		return "";
	else if (url.charAt(0) == '/')
		return window.location.protocol + "//" + window.location.host + url;
	else if (url.indexOf(window.location.protocol) != 0) {
		if (url.indexOf(window.location.host) != 0)
			return window.location.protocol + "//" + window.location.host + window.location.pathname + "/" + url;
		else
			return window.location.protocol + "//" + url;
	} else
		return url;
}

function range(x, y, z) {
    if (x < y)
        return y;
    else if (x > z)
        return z;
    else
        return x;
}

function getWinWidth() {
    var size;
    if (window.innerWidth) {
        size = window.innerWidth;
    } else if (document.documentElement && document.documentElement.clientWidth) {
        size = document.documentElement.clientWidth;
    } else {
        size = document.body.clientWidth;
    }

    return size;
}

function getWinHeight() {
    var size;
    if (window.innerHeight) {
        size = window.innerHeight;
    } else if (document.documentElement && document.documentElement.clientHeight) {
        size = document.documentElement.clientHeight;
    } else {
        size = document.body.clientHeight;
    }

    return size;
}

function setOpacity(obj, opacity) {
  opacity = (opacity == 100)?99.999:opacity;
  
  // IE/Win
  obj[Style].filter = "alpha(opacity:"+opacity+")";
  
  // Safari<1.2, Konqueror
  obj[Style].KHTMLOpacity = opacity/100;
  
  // Older Mozilla and Firefox
  obj[Style].MozOpacity = opacity/100;
  
  // Safari 1.2, newer Firefox and Mozilla, CSS3
  obj[Style].opacity = opacity/100;
}

function fadeIn(obj, op) {
    setOpacity(obj, op);
    
    if (op < 100)
        setTimeout(function() { fadeIn(obj, op+10 + "");}, 100);
}

function fadeOut(obj, op) {
    setOpacity(obj, op);
    if (op > 0)
        setTimeout(function() { fadeOut(obj, op-10 + "");}, 100);
}

function findPosX(obj) {
    var curleft = 0;
    if (obj.offsetParent) {
        do {
            curleft += obj.offsetLeft
            obj = obj.offsetParent;
        } while (obj);
    } else if (obj.x)
        curleft += obj.x;
    return curleft;
}

function findPosY(obj) {
    var curtop = 0;
    if (obj.offsetParent) {
        do {
            curtop += obj.offsetTop
            obj = obj.offsetParent;
        } while (obj);
    } else if (obj.y)
        curtop += obj.y;
    return curtop;
}

function createControl(controlArea, src, tooltip, onClick) {
	var div = createDiv(controlArea);
	div[Style].paddingLeft = "5px";
	div[Style].display = "inline";
	var img = createImageButton(src, div, tooltip);
	img.onclick = onClick;
}

function createControlArea(id) {
    var controlArea = document.createElement("DIV");
    controlArea.setAttribute("id", id);
    controlArea[Style].position = "absolute";
    controlArea[Style].display = None;
	controlArea[Style].MozUserSelect = None;
	controlArea[Style].whiteSpace = "nowrap";
	
	return controlArea;
}

function createDiv(area) {
    var div = document.createElement("DIV");
    area.appendChild(div);
    return div;
}

// create image with automatic button states built-in
function createImageButton(src, area, tooltip) {
    var img = document.createElement("IMG");
    area.appendChild(img);
	
	if (tooltip)
	    img.title = tooltip;
    img.src = _gpz.mediaPath + src;
	img.on = function() {
        img.src = img.src.replace(/off/, "on");
	}
	img.off = function() {
        img.src = img.src.replace(/on/, "off");
	}
    img.onmouseover = function() {
        img.on();
    }
    img.onmouseout = function() {
        img.off();
    }
    return img;
}

// creates the standard naturalWidth property for FRESH IMG DOM only.. will NOT work if img.src is changed...
function normalizeImage(img) {
	if (!img.naturalWidth) {
		var remove = 0;
		if (!img.parentNode) {	// if not part of document, inject so IE will give us the damn size properties
			document.body.appendChild(img);
			img[Style].display = None;
			remove = 1;
		}
			
		var oldDis;
		if (img.style) {
			var oldVis = img[Style].visibility;
			oldDis = img[Style].display;
			if (oldDis == None) {
				img[Style].visibility = Hidden;
				img[Style].display = "inline";
			}
		}
		img.naturalHeight = img.height;
		img.naturalWidth = img.width;
		if (oldDis == None) {
			img[Style].display = oldDis;
			img[Style].visibility = oldVis;
		}
		if (remove)
			document.body.removeChild(img);
	}
}


// PZImageDescriptor is the internal representation of an image, as managed by the ImageDataModel
PZImageDescriptor[Prototype].original = "";
PZImageDescriptor[Prototype].thumb = "";
PZImageDescriptor[Prototype].medium = "";
PZImageDescriptor[Prototype].imgCache = 0;
PZImageDescriptor[Prototype].imgCache2 = 0;
PZImageDescriptor[Prototype].imgCache3 = 0;

function PZImageDescriptor(original, thumb, medium) {
	this.original = original;
	this.thumb = thumb;
	this.medium = medium;
}

PZImageDescriptor[Prototype].preloadOriginal = function() {
	if (!this.imgCache) {
		this.imgCache = new Image();
		this.imgCache.src = this.original;
	}
}

PZImageDescriptor[Prototype].preload = function() {
	if (!this.imgCache2) {
		this.imgCache2 = new Image();
		this.imgCache3 = new Image();
		this.imgCache3.src = this.medium;
		this.imgCache2.src = this.thumb;
	}
	
	if (_gpz.isZoomed)
		this.preloadOriginal();
}

// used for degenerate case of one image when setImage() is called
PZInteralDataModel[Prototype].data = 0;
function PZInteralDataModel(data) {
	this.data = data;
}

PZInteralDataModel[Prototype].getTotal = function() {
	return 1;
}

PZInteralDataModel[Prototype].getImageDescriptor = function(i) {
	return this.data;
}
